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',
);
$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
for ($i = 1; $i <= 20; $i++) {
$item = new Container(style: 'bg-blue-500 m-2 p-3 rounded-lg');
for ($i = 1; $i <= 100; $i++) {
$item = new Container(style: 'bg-blue-500 m-2 p-3');
$label = new Label(
text: "Item {$i} - Scroll vertically with mouse wheel or drag the scrollbar",
style: 'text-white',
text: "Item {$i}",
style: 'text-green-500',
);
$item->addComponent($label);
$scrollContainer->addComponent($item);

View File

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

View File

@ -131,6 +131,35 @@ class TextRenderer
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)
*

View File

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

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
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
if (isset($this->computedStyles[Flex::class])) {
$flex = $this->computedStyles[Flex::class];
$this->layoutChildren($flex, $textRenderer);
if ($isFlexContainer) {
$this->layoutChildren($flexStyle, $textRenderer);
} else {
// Non-flex layout: just layout children normally
// Non-flex layout: stack children vertically
$currentY = $this->contentViewport->y;
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);
// Get the actual height after layout
$actualHeight = $child->getViewport()->height;
$currentY += $actualHeight;
}
}
// Calculate total content size after children are laid out
$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
@ -152,8 +206,9 @@ class Container extends Component
private function calculateContentSize(): void
{
if (empty($this->children)) {
$this->contentWidth = $this->contentViewport->width;
$this->contentHeight = $this->contentViewport->height;
// No children: content size is 0
$this->contentWidth = 0;
$this->contentHeight = 0;
return;
}
@ -166,8 +221,9 @@ class Container extends Component
$maxY = max($maxY, ($childViewport->y + $childViewport->height) - $this->contentViewport->y);
}
$this->contentWidth = max($this->contentViewport->width, $maxX);
$this->contentHeight = max($this->contentViewport->height, $maxY);
// Content size is the actual space used by children within contentViewport
$this->contentWidth = $maxX;
$this->contentHeight = $maxY;
}
private function hasOverflow(): array
@ -291,7 +347,7 @@ class Container extends Component
(int) (self::SCROLLBAR_WIDTH - 4),
(int) $thumbHeight,
4,
4
4,
);
}
@ -332,7 +388,7 @@ class Container extends Component
(int) $thumbWidth,
(int) (self::SCROLLBAR_WIDTH - 4),
4,
4
4,
);
}
}

View File

@ -8,6 +8,9 @@ use PHPNative\Ui\Component;
class Label extends Component
{
private int $intrinsicWidth = 0;
private int $intrinsicHeight = 0;
public function __construct(
public string $text = '',
public string $style = '',
@ -18,6 +21,28 @@ class Label extends Component
$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
{
if (!$this->visible || $textRenderer === null || !$textRenderer->isInitialized()) {