First Scroll
This commit is contained in:
parent
4e510b2ac2
commit
29102c567c
63
examples/ButtonExample.php
Normal file
63
examples/ButtonExample.php
Normal 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
27
examples/TestStack.php
Normal 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();
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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)
|
||||||
*
|
*
|
||||||
|
|||||||
@ -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
36
src/Ui/Widget/Button.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user