From 949cd47c962b32d3a83fbbcfb7c629940a487021 Mon Sep 17 00:00:00 2001 From: Thomas Peterson Date: Wed, 22 Oct 2025 18:11:46 +0200 Subject: [PATCH] First Commit --- .gitignore | 51 +++++ composer.json | 22 ++ examples/Todo.php | 15 ++ src/Core/Collection.php | 129 +++++++++++ src/Core/TypedCollection.php | 20 ++ src/Framework/Application.php | 209 ++++++++++++++++++ src/Framework/TextRenderer.php | 177 +++++++++++++++ src/Renderer/Widget.php | 11 + src/Renderer/Widget/Container.php | 14 ++ src/Tailwind/Data/Icon.php | 10 + src/Tailwind/Data/colors.json | 288 +++++++++++++++++++++++++ src/Tailwind/LayoutParser.php | 55 +++++ src/Tailwind/Model/Style.php | 17 ++ src/Tailwind/Model/StyleCollection.php | 56 +++++ src/Tailwind/Parser/Background.php | 22 ++ src/Tailwind/Parser/Basis.php | 60 ++++++ src/Tailwind/Parser/Border.php | 151 +++++++++++++ src/Tailwind/Parser/Color.php | 41 ++++ src/Tailwind/Parser/Flex.php | 48 +++++ src/Tailwind/Parser/Height.php | 60 ++++++ src/Tailwind/Parser/Margin.php | 78 +++++++ src/Tailwind/Parser/MediaQuery.php | 18 ++ src/Tailwind/Parser/Overflow.php | 39 ++++ src/Tailwind/Parser/Padding.php | 78 +++++++ src/Tailwind/Parser/Parser.php | 12 ++ src/Tailwind/Parser/State.php | 18 ++ src/Tailwind/Parser/Text.php | 75 +++++++ src/Tailwind/Parser/Width.php | 58 +++++ src/Tailwind/Style/AlignEnum.php | 10 + src/Tailwind/Style/Background.php | 12 ++ src/Tailwind/Style/Basis.php | 12 ++ src/Tailwind/Style/Border.php | 21 ++ src/Tailwind/Style/Color.php | 12 ++ src/Tailwind/Style/DirectionEnum.php | 9 + src/Tailwind/Style/Flex.php | 12 ++ src/Tailwind/Style/FlexTypeEnum.php | 12 ++ src/Tailwind/Style/Height.php | 12 ++ src/Tailwind/Style/Margin.php | 12 ++ src/Tailwind/Style/MediaQuery.php | 10 + src/Tailwind/Style/MediaQueryEnum.php | 33 +++ src/Tailwind/Style/Overflow.php | 14 ++ src/Tailwind/Style/OverflowEnum.php | 11 + src/Tailwind/Style/Padding.php | 12 ++ src/Tailwind/Style/State.php | 10 + src/Tailwind/Style/StateEnum.php | 12 ++ src/Tailwind/Style/Style.php | 9 + src/Tailwind/Style/Text.php | 12 ++ src/Tailwind/Style/Unit.php | 11 + src/Tailwind/Style/Width.php | 12 ++ src/Tailwind/StyleParser.php | 74 +++++++ src/Ui/Component.php | 134 ++++++++++++ src/Ui/Viewport.php | 22 ++ src/Ui/Widget/Container.php | 12 ++ src/Ui/Window.php | 7 + 54 files changed, 2351 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 examples/Todo.php create mode 100644 src/Core/Collection.php create mode 100644 src/Core/TypedCollection.php create mode 100644 src/Framework/Application.php create mode 100644 src/Framework/TextRenderer.php create mode 100644 src/Renderer/Widget.php create mode 100644 src/Renderer/Widget/Container.php create mode 100644 src/Tailwind/Data/Icon.php create mode 100644 src/Tailwind/Data/colors.json create mode 100644 src/Tailwind/LayoutParser.php create mode 100644 src/Tailwind/Model/Style.php create mode 100644 src/Tailwind/Model/StyleCollection.php create mode 100644 src/Tailwind/Parser/Background.php create mode 100644 src/Tailwind/Parser/Basis.php create mode 100644 src/Tailwind/Parser/Border.php create mode 100644 src/Tailwind/Parser/Color.php create mode 100644 src/Tailwind/Parser/Flex.php create mode 100644 src/Tailwind/Parser/Height.php create mode 100644 src/Tailwind/Parser/Margin.php create mode 100644 src/Tailwind/Parser/MediaQuery.php create mode 100644 src/Tailwind/Parser/Overflow.php create mode 100644 src/Tailwind/Parser/Padding.php create mode 100644 src/Tailwind/Parser/Parser.php create mode 100644 src/Tailwind/Parser/State.php create mode 100644 src/Tailwind/Parser/Text.php create mode 100644 src/Tailwind/Parser/Width.php create mode 100644 src/Tailwind/Style/AlignEnum.php create mode 100644 src/Tailwind/Style/Background.php create mode 100644 src/Tailwind/Style/Basis.php create mode 100644 src/Tailwind/Style/Border.php create mode 100644 src/Tailwind/Style/Color.php create mode 100644 src/Tailwind/Style/DirectionEnum.php create mode 100644 src/Tailwind/Style/Flex.php create mode 100644 src/Tailwind/Style/FlexTypeEnum.php create mode 100644 src/Tailwind/Style/Height.php create mode 100644 src/Tailwind/Style/Margin.php create mode 100644 src/Tailwind/Style/MediaQuery.php create mode 100644 src/Tailwind/Style/MediaQueryEnum.php create mode 100644 src/Tailwind/Style/Overflow.php create mode 100644 src/Tailwind/Style/OverflowEnum.php create mode 100644 src/Tailwind/Style/Padding.php create mode 100644 src/Tailwind/Style/State.php create mode 100644 src/Tailwind/Style/StateEnum.php create mode 100644 src/Tailwind/Style/Style.php create mode 100644 src/Tailwind/Style/Text.php create mode 100644 src/Tailwind/Style/Unit.php create mode 100644 src/Tailwind/Style/Width.php create mode 100644 src/Tailwind/StyleParser.php create mode 100644 src/Ui/Component.php create mode 100644 src/Ui/Viewport.php create mode 100644 src/Ui/Widget/Container.php create mode 100644 src/Ui/Window.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..891c80a --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# Composer +/vendor/ +composer.phar +composer.lock + +# PHP +*.log +*.cache +.phpunit.result.cache + +# IDE & Editor +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store +*.sublime-project +*.sublime-workspace + +# System Files +Thumbs.db +.DS_Store +desktop.ini + +# Build & Distribution +/build/ +/dist/ +*.phar + +# Environment +.env +.env.local +.env.*.local + +# Temporary Files +/tmp/ +/temp/ +*.tmp + +# Logs +/logs/ +*.log + +# Coverage & Testing +/coverage/ +.phpunit.cache + +# OS +.Trash-* +.nfs* diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2558e2d --- /dev/null +++ b/composer.json @@ -0,0 +1,22 @@ +{ + "name": "phpnative/framework", + "type": "library", + "require": { + "ext-parallel": "^1.2", + "monolog/monolog": "^3.9", + "php": "^8.4", + "symfony/var-dumper": "^7.3" + }, + "license": "MIT", + "autoload": { + "psr-4": { + "PHPNative\\": "src/" + } + }, + "authors": [ + { + "name": "Thomas Peterson", + "email": "info@thomas-peterson.de" + } + ] +} diff --git a/examples/Todo.php b/examples/Todo.php new file mode 100644 index 0000000..23c959c --- /dev/null +++ b/examples/Todo.php @@ -0,0 +1,15 @@ +addComponent($containerMenu); +$app->setRoot($container); +$app->run(); diff --git a/src/Core/Collection.php b/src/Core/Collection.php new file mode 100644 index 0000000..809fb17 --- /dev/null +++ b/src/Core/Collection.php @@ -0,0 +1,129 @@ +elements, $fn, $initial); + } + + public function map(callable $fn): array + { + return array_map($fn, $this->elements); + } + + public function each(callable $fn): void + { + array_walk($this->elements, $fn); + } + + public function some(callable $fn): bool + { + foreach ($this->elements as $index => $element) { + if ($fn($element, $index, $this->elements)) { + return true; + } + } + + return false; + } + + public function setElements($elements): void + { + $this->elements = $elements; + } + + public function filter(callable $fn): static + { + return new static(array_filter($this->elements, $fn, ARRAY_FILTER_USE_BOTH)); + } + + public function first(): mixed + { + return reset($this->elements); + } + + public function last(): mixed + { + return end($this->elements); + } + + public function count(): int + { + return count($this->elements); + } + + public function isEmpty(): bool + { + return empty($this->elements); + } + + public function add(mixed $element): void + { + $this->elements[] = $element; + } + + public function values(): array + { + return array_values($this->elements); + } + + public function items(): array + { + return $this->elements; + } + + #[\Override] + public function getIterator(): Traversable + { + return new ArrayIterator($this->elements); + } + + public function removeFirstItem(): void + { + if (count($this->elements) > 0) { + array_shift($this->elements); + } + } + + public function sort(callable $callback): void + { + usort($this->elements, $callback); + } + + public function removeItem($item): void + { + $temp = $this->filter(function ($value, $key) use ($item) { + return $value::class != $item::class; + }); + + $this->elements = $temp->items(); + } + + public function clear(): void + { + $this->elements = []; + } +} diff --git a/src/Core/TypedCollection.php b/src/Core/TypedCollection.php new file mode 100644 index 0000000..48cd3a9 --- /dev/null +++ b/src/Core/TypedCollection.php @@ -0,0 +1,20 @@ +title = $title; + $this->windowWidth = $width; + $this->windowHeight = $height; + + // Create window + $this->window = rgfw_createWindow($title, 100, 100, $width, $height, RGFW_CENTER); + if (!$this->window) { + throw new \Exception('Failed to create window'); + } + + // Initialize RSGL renderer + if (!rsgl_init($this->window)) { + throw new \Exception('Failed to initialize RSGL renderer'); + } + + // Initialize text renderer + $this->textRenderer = new TextRenderer($this->window); + if (!$this->textRenderer->init()) { + error_log('Warning: Failed to initialize text renderer. Text rendering will not be available.'); + } + + // Get actual window size + $size = rgfw_window_getSize($this->window); + $this->windowWidth = $size[0]; + $this->windowHeight = $size[1]; + $this->viewport = new Viewport( + windowWidth: $this->windowWidth, + windowHeight: $this->windowHeight, + width: $this->windowWidth, + height: $this->windowHeight, + ); + } + + public function createWindow(): void + { + } + + public function run(): void + { + while ($this->running && !rgfw_window_shouldClose($this->window)) { + $this->handleEvents(); + $this->update(); + $this->layout(); + $this->render(); + + // Limit frame rate to ~60 FPS + usleep(16666); + } + + $this->cleanup(); + } + + /** + * Handle window and input events + */ + protected function handleEvents(): void + { + while ($event = rgfw_window_checkEvent($this->window)) { + switch ($event['type']) { + case RGFW_quit: + $this->running = false; + break; + + case RGFW_keyPressed: + $keyCode = $event['keyCode'] ?? 0; + if ($keyCode == RGFW_Escape) { + $this->running = false; + } + break; + + case RGFW_windowResized: + // Update window dimensions (from event data) + $newWidth = $event[0] ?? $this->windowWidth; + $newHeight = $event[1] ?? $this->windowHeight; + + $this->windowWidth = $newWidth; + $this->windowHeight = $newHeight; + + // Update RSGL renderer size and viewport + // This ensures the renderer is properly configured for the new window size + rsgl_updateRendererSize($this->window); + + // Update text renderer framebuffer + if ($this->textRenderer && $this->textRenderer->isInitialized()) { + $this->textRenderer->updateFramebuffer($newWidth, $newHeight); + } + + $this->viewport->x = 0; + $this->viewport->y = 0; + $this->viewport->windowWidth = $newWidth; + $this->viewport->width = $newWidth; + $this->viewport->height = $newHeight; + $this->viewport->windowHeight = $newHeight; + $this->shouldBeReLayouted = true; + break; + + case RGFW_mousePosChanged: + $this->mouseX = $event[0] ?? 0; + $this->mouseY = $event[1] ?? 0; + + // Propagate mouse move to root component + if ($this->rootComponent) { + // $this->rootComponent->handleMouseMove($this->mouseX, $this->mouseY); + } + break; + + case RGFW_mouseButtonPressed: + $button = $event['button'] ?? 0; + + // Propagate click to root component + if ($this->rootComponent) { + $this->rootComponent->handleMouseClick($this->mouseX, $this->mouseY, $button); + } + break; + } + } + } + + /** + * Update application state + */ + protected function update(): void + { + if ($this->rootComponent) { + $this->rootComponent->update(); + } + } + + /** + * Render the application + */ + protected function render(): void + { + rsgl_clear($this->window, 255, 255, 255, 0); + + // Render root component tree + if ($this->rootComponent) { + $this->rootComponent->render($this->textRenderer); + $this->rootComponent->renderContent($this->textRenderer); + } + + // Render to screen + rsgl_render($this->window); + rgfw_window_swapBuffers($this->window); + } + + protected function layout(): void + { + // Render root component tree + if ($this->rootComponent && $this->shouldBeReLayouted) { + $this->rootComponent->setViewport($this->viewport); + $this->rootComponent->setWindow($this->window); + $this->rootComponent->layout($this->textRenderer); + $this->shouldBeReLayouted = false; + } + } + + /** + * Clean up resources + */ + protected function cleanup(): void + { + if ($this->textRenderer) { + $this->textRenderer->free(); + } + rsgl_close($this->window); + rgfw_window_close($this->window); + } + + /** + * Stop the application + */ + public function quit(): void + { + $this->running = false; + } + + public function setRoot(Component $component): self + { + $this->rootComponent = $component; + return $this; + } +} diff --git a/src/Framework/TextRenderer.php b/src/Framework/TextRenderer.php new file mode 100644 index 0000000..dff5673 --- /dev/null +++ b/src/Framework/TextRenderer.php @@ -0,0 +1,177 @@ +window = $window; + } + + /** + * Initialize RFont with a font file + * + * @param string|null $fontPath Path to TTF font file + * @param int $fontSize Default font size + * @param int $atlasWidth Font atlas texture width (default 512) + * @param int $atlasHeight Font atlas texture height (default 512) + * @return bool Success + */ + public function init( + null|string $fontPath = null, + int $fontSize = 16, + int $atlasWidth = 512, + int $atlasHeight = 512, + ): bool { + // Try to find a suitable font if none provided + if ($fontPath === null) { + $fontPath = $this->findSystemFont(); + } + + if (!file_exists($fontPath)) { + error_log("Font file not found: {$fontPath}"); + return false; + } + + $this->fontPath = $fontPath; + $this->fontSize = $fontSize; + + $this->initialized = rfont_init($this->window, $fontPath, $fontSize, $atlasWidth, $atlasHeight); + + if (!$this->initialized) { + error_log("Failed to initialize RFont with font: {$fontPath}"); + return false; + } + + // Set default color to white + $this->setColor(1.0, 1.0, 1.0, 1.0); + + return true; + } + + /** + * Find a system font + * + * @return string|null Font path or null if not found + */ + private function findSystemFont(): null|string + { + $fontPaths = [ + // Linux + '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', + '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf', + '/usr/share/fonts/TTF/DejaVuSans.ttf', + // macOS + '/System/Library/Fonts/Helvetica.ttc', + '/Library/Fonts/Arial.ttf', + // Windows + 'C:/Windows/Fonts/arial.ttf', + 'C:/Windows/Fonts/calibri.ttf', + ]; + + foreach ($fontPaths as $path) { + if (file_exists($path)) { + return $path; + } + } + + return null; + } + + /** + * Draw text at position + * + * @param string $text Text to draw + * @param int $x X position + * @param int $y Y position + * @param int|null $size Font size + */ + public function drawText(string $text, int $x, int $y, null|int $size = null): void + { + if (!$this->initialized) { + return; + } + + if ($size === null) { + $size = $this->fontSize; + } + + rfont_drawText($this->window, $text, $x, $y, $size); + } + + /** + * Set text color + * + * @param float $r Red (0.0 - 1.0) + * @param float $g Green (0.0 - 1.0) + * @param float $b Blue (0.0 - 1.0) + * @param float $a Alpha (0.0 - 1.0) + */ + public function setColor(float $r, float $g, float $b, float $a = 1.0): void + { + if (!$this->initialized) { + return; + } + + rfont_setColor($this->window, $r, $g, $b, $a); + } + + /** + * Update framebuffer size (call on window resize) + * + * @param int $width Window width + * @param int $height Window height + */ + public function updateFramebuffer(int $width, int $height): void + { + if (!$this->initialized) { + return; + } + + rfont_setFramebuffer($this->window, $width, $height); + } + + /** + * Free RFont resources + */ + public function free(): void + { + if ($this->initialized) { + rfont_free($this->window); + $this->initialized = false; + } + } + + /** + * Check if initialized + * + * @return bool + */ + public function isInitialized(): bool + { + return $this->initialized; + } + + /** + * Destructor + */ + public function __destruct() + { + $this->free(); + } +} diff --git a/src/Renderer/Widget.php b/src/Renderer/Widget.php new file mode 100644 index 0000000..c49fe9a --- /dev/null +++ b/src/Renderer/Widget.php @@ -0,0 +1,11 @@ +getViews()->map(function (View $a) use (&$flexOne, &$i, &$stylesCache, $viewport) { + $aStyles = $stylesCache->getStyle($a->getId(), $viewport->windowMediaQuery, $a->getState(), $a->getStyle()); + if(isset($aStyles[Flex::class]) && $aStyles[Flex::class]->type == FlexTypeEnum::one) { + $flexOne++; + } + $a->setRenderSort($i); + $i++; + }); + + $container->getViews()->sort(function (View $a, View $b) use (&$stylesCache, $viewport) { + $aStyles = $stylesCache->getStyle($a->getId(), $viewport->windowMediaQuery, $a->getState(), $a->getStyle()); + $bStyles = $stylesCache->getStyle($b->getId(), $viewport->windowMediaQuery, $b->getState(), $b->getStyle()); + if(isset($aStyles[Flex::class]) && $aStyles[Flex::class]->type == FlexTypeEnum::none && + isset($bStyles[Flex::class]) && $bStyles[Flex::class]->type == FlexTypeEnum::one) { + return -1; + } + + return 1; + }); + + $container->getViews()->countOne = $flexOne; + + return $container; + + } + + public static function sortByRenderSort(View $container) + { + $container->getViews()->sort(function (View $a, View $b) { + if($a->getRenderSort() == $b->getRenderSort()) { + return 0; + } + + return $a->getRenderSort() <=> $b->getRenderSort(); + }); + } +} \ No newline at end of file diff --git a/src/Tailwind/Model/Style.php b/src/Tailwind/Model/Style.php new file mode 100644 index 0000000..fa1cdec --- /dev/null +++ b/src/Tailwind/Model/Style.php @@ -0,0 +1,17 @@ +items() as $style) { + if(($style->state == StateEnum::normal || $style->state === $state) && ($style->mediaQuery->value === 0 || $style->mediaQuery->value <= $mediaQueryEnum->value)) { + $items[] = $style; + } + } + + return $this->merge($items); + } + + private function merge(array $styles): array + { + $tmp = []; + + foreach($styles as $style) { + if(isset($tmp[$style->style::class]) && $style->style::class === Padding::class) { + \PHPNative\Tailwind\Parser\Padding::merge($tmp[$style->style::class], $style->style); + }elseif(isset($tmp[$style->style::class]) && $style->style::class === Margin::class) { + \PHPNative\Tailwind\Parser\Margin::merge($tmp[$style->style::class], $style->style); + }elseif(isset($tmp[$style->style::class]) && $style->style::class === Border::class) { + \PHPNative\Tailwind\Parser\Border::merge($tmp[$style->style::class], $style->style); + }elseif(isset($tmp[$style->style::class]) && $style->style::class === Text::class) { + \PHPNative\Tailwind\Parser\Text::merge($tmp[$style->style::class], $style->style); + }elseif(isset($tmp[$style->style::class]) && $style->style::class === Flex::class) { + \PHPNative\Tailwind\Parser\Flex::merge($tmp[$style->style::class], $style->style); + }else{ + $tmp[$style->style::class] = $style->style; + } + } + + return $tmp; + } +} \ No newline at end of file diff --git a/src/Tailwind/Parser/Background.php b/src/Tailwind/Parser/Background.php new file mode 100644 index 0000000..8df86af --- /dev/null +++ b/src/Tailwind/Parser/Background.php @@ -0,0 +1,22 @@ + 0) { + $colorStyle = $output_array[1][0]; + $color = Color::parse($colorStyle); + return new \PHPNative\Tailwind\Style\Background($color); + } + + return null; + } +} diff --git a/src/Tailwind/Parser/Basis.php b/src/Tailwind/Parser/Basis.php new file mode 100644 index 0000000..ac83204 --- /dev/null +++ b/src/Tailwind/Parser/Basis.php @@ -0,0 +1,60 @@ + 0) { + $value1 = (int)$output_array[1][0]; + $value2 = (int)$output_array[2][0]; + $unit = Unit::Percent; + $value = (int)round(100/$value2*$value1,0); + $found = true; + } + + preg_match_all('/basis-(\d*)/', $style, $output_array); + if (!$found && count($output_array[0]) > 0) { + $value = (int)$output_array[1][0]; + } + + preg_match_all('/basis-full/', $style, $output_array); + if (!$found && count($output_array[0]) > 0) { + $value = 100; + $unit = Unit::Percent; + } + + + + if($value != -1) { + return new \PHPNative\Tailwind\Style\Basis($unit, $value); + } + + return null; + } + + public static function merge(\PHPNative\Tailwind\Style\Padding $class, \PHPNative\Tailwind\Style\Padding $style) + { + if($style->left != null) { + $class->left = $style->left; + } + if($style->right != null) { + $class->right = $style->right; + } + if($style->top != null) { + $class->top = $style->top; + } + if($style->bottom != null) { + $class->bottom = $style->bottom; + } + } +} diff --git a/src/Tailwind/Parser/Border.php b/src/Tailwind/Parser/Border.php new file mode 100644 index 0000000..04ab533 --- /dev/null +++ b/src/Tailwind/Parser/Border.php @@ -0,0 +1,151 @@ + 0) { + $size = match ((string) $output_array[2][0]) { + 'none' => 0, + 'sm' => 2, + 'md' => 6, + 'lg' => 8, + 'xl' => 12, + '2xl' => 16, + '3xl' => 24, + }; + return match ((string) $output_array[1][0]) { + 't' => new \PHPNative\Tailwind\Style\Border( + roundTopLeft: $size, + roundTopRight: $size, + ), + 'b' => new \PHPNative\Tailwind\Style\Border( + roundBottomLeft: $size, + roundBottomRight: $size, + ), + 'r' => new \PHPNative\Tailwind\Style\Border( + roundTopRight: $size, + roundBottomRight: $size, + ), + 'l' => new \PHPNative\Tailwind\Style\Border( + roundTopLeft: $size, + roundBottomLeft: $size, + ), + }; + } + + preg_match_all('/rounded-(none|sm|md|lg|xl|2xl|3xl)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $size = match ((string) $output_array[1][0]) { + 'none' => 0, + 'sm' => 2, + 'md' => 6, + 'lg' => 8, + 'xl' => 12, + '2xl' => 16, + '3xl' => 24, + }; + + return new \PHPNative\Tailwind\Style\Border( + roundTopLeft: $size, + roundTopRight: $size, + roundBottomLeft: $size, + roundBottomRight: $size, + ); + } + + preg_match_all('/rounded/', $style, $output_array); + if (count($output_array[0]) > 0) { + $size = 4; + + return new \PHPNative\Tailwind\Style\Border( + roundTopLeft: $size, + roundTopRight: $size, + roundBottomLeft: $size, + roundBottomRight: $size, + ); + } + + preg_match_all('/border-([tblr])-(.*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + return match ((string) $output_array[1][0]) { + 't' => new \PHPNative\Tailwind\Style\Border(true, top: (int) $output_array[2][0]), + 'b' => new \PHPNative\Tailwind\Style\Border(true, bottom: (int) $output_array[2][0]), + 'r' => new \PHPNative\Tailwind\Style\Border(true, right: (int) $output_array[2][0]), + 'l' => new \PHPNative\Tailwind\Style\Border(true, left: (int) $output_array[2][0]), + }; + } + + preg_match_all('/border-(.*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $colorStyle = $output_array[1][0]; + $color = Color::parse($colorStyle); + return new \PHPNative\Tailwind\Style\Border(false, $color); + } + + preg_match_all('/border/', $style, $output_array); + if (count($output_array[0]) > 0) { + return new \PHPNative\Tailwind\Style\Border( + enabled: true, + left: 1, + right: 1, + top: 1, + bottom: 1, + ); + } + + return null; + } + + public static function merge( + \PHPNative\Tailwind\Style\Border $style1, + \PHPNative\Tailwind\Style\Border $style2, + ): void { + if ($style2->enabled && !$style1->enabled) { + $style1->enabled = true; + } + if ($style2->color->red != null) { + $style1->color->red = $style2->color->red; + } + if ($style2->color->green != null) { + $style1->color->green = $style2->color->green; + } + if ($style2->color->blue != null) { + $style1->color->blue = $style2->color->blue; + } + if ($style2->color->alpha != null) { + $style1->color->alpha = $style2->color->alpha; + } + if ($style2->top != null) { + $style1->top = $style2->top; + } + if ($style2->bottom != null) { + $style1->bottom = $style2->bottom; + } + if ($style2->left != null) { + $style1->left = $style2->left; + } + if ($style2->right != null) { + $style1->right = $style2->right; + } + if ($style2->roundTopLeft != null) { + $style1->roundTopLeft = $style2->roundTopLeft; + } + if ($style2->roundTopRight != null) { + $style1->roundTopRight = $style2->roundTopRight; + } + if ($style2->roundBottomLeft != null) { + $style1->roundBottomLeft = $style2->roundBottomLeft; + } + if ($style2->roundBottomRight != null) { + $style1->roundBottomRight = $style2->roundBottomRight; + } + } +} diff --git a/src/Tailwind/Parser/Color.php b/src/Tailwind/Parser/Color.php new file mode 100644 index 0000000..f1d7bf3 --- /dev/null +++ b/src/Tailwind/Parser/Color.php @@ -0,0 +1,41 @@ + 0) { + $color = (string)$output_array[1][0]; + [$red, $green, $blue] = sscanf($data[$color]['500'], "#%02x%02x%02x"); + } + + preg_match_all('/(\w{1,8})-(\d{1,3})/', $style, $output_array); + if (count($output_array[0]) > 0) { + $color = (string)$output_array[1][0]; + $variant = (string)$output_array[2][0]; + [$red, $green, $blue] = sscanf($data[$color][$variant], "#%02x%02x%02x"); + } + + + + return new \PHPNative\Tailwind\Style\Color($red, $green, $blue); + } +} diff --git a/src/Tailwind/Parser/Flex.php b/src/Tailwind/Parser/Flex.php new file mode 100644 index 0000000..69bd18b --- /dev/null +++ b/src/Tailwind/Parser/Flex.php @@ -0,0 +1,48 @@ + 0) { + return new \PHPNative\Tailwind\Style\Flex(type:FlexTypeEnum::none); + } + preg_match_all('/flex-1/', $style, $output_array); + if (count($output_array[0]) > 0) { + return new \PHPNative\Tailwind\Style\Flex(type:FlexTypeEnum::one); + } + preg_match_all('/flex-auto/', $style, $output_array); + if (count($output_array[0]) > 0) { + return new \PHPNative\Tailwind\Style\Flex(type:FlexTypeEnum::auto); + } + preg_match_all('/flex-col/', $style, $output_array); + if (count($output_array[0]) > 0) { + return new \PHPNative\Tailwind\Style\Flex(DirectionEnum::column); + } + + preg_match_all('/(?!flex-col)(flex-row|flex)/', $style, $output_array); + if (count($output_array[0]) > 0) { + return new \PHPNative\Tailwind\Style\Flex(DirectionEnum::row); + } + + return null; + + } + + public static function merge(\PHPNative\Tailwind\Style\Flex $class, \PHPNative\Tailwind\Style\Flex $style) + { + if($style->type != FlexTypeEnum::none) { + $class->type = $style->type; + } + if($style->direction != DirectionEnum::row) { + $class->direction = $style->direction; + } + } +} \ No newline at end of file diff --git a/src/Tailwind/Parser/Height.php b/src/Tailwind/Parser/Height.php new file mode 100644 index 0000000..06aaf58 --- /dev/null +++ b/src/Tailwind/Parser/Height.php @@ -0,0 +1,60 @@ + 0) { + $value1 = (int)$output_array[1][0]; + $value2 = (int)$output_array[2][0]; + $unit = Unit::Percent; + $value = 100/$value2*$value1; + $found = true; + } + + preg_match_all('/h-(\d*)/', $style, $output_array); + if (!$found && count($output_array[0]) > 0) { + $value = (int)$output_array[1][0]; + } + + preg_match_all('/(h-full|h-screen)/', $style, $output_array); + if (!$found && count($output_array[0]) > 0) { + $value = 100; + $unit = Unit::Percent; + } + + + + if($value != -1) { + return new \PHPNative\Tailwind\Style\Height($unit, $value); + } + + return null; + } + + public static function merge(\PHPNative\Tailwind\Style\Padding $class, \PHPNative\Tailwind\Style\Padding $style) + { + if($style->left != null) { + $class->left = $style->left; + } + if($style->right != null) { + $class->right = $style->right; + } + if($style->top != null) { + $class->top = $style->top; + } + if($style->bottom != null) { + $class->bottom = $style->bottom; + } + } +} diff --git a/src/Tailwind/Parser/Margin.php b/src/Tailwind/Parser/Margin.php new file mode 100644 index 0000000..6ebbb68 --- /dev/null +++ b/src/Tailwind/Parser/Margin.php @@ -0,0 +1,78 @@ + 0) { + $l = (int)$output_array[1][0]; + $r = (int)$output_array[1][0]; + $t = (int)$output_array[1][0]; + $b = (int)$output_array[1][0]; + } + + preg_match_all('/mx-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $l = (int)$output_array[1][0]; + $r = (int)$output_array[1][0]; + } + + preg_match_all('/my-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $t = (int)$output_array[1][0]; + $b = (int)$output_array[1][0]; + } + + preg_match_all('/mt-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $t = (int)$output_array[1][0]; + } + + preg_match_all('/mb-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $b = (int)$output_array[1][0]; + } + + preg_match_all('/ml-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $l = (int)$output_array[1][0]; + } + + preg_match_all('/mr-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $r = (int)$output_array[1][0]; + } + + if($l != null || $r != null || $t != null || $b != null) { + return new \PHPNative\Tailwind\Style\Margin($l, $r, $t, $b); + } + + return null; + } + + public static function merge(\PHPNative\Tailwind\Style\Margin $class, \PHPNative\Tailwind\Style\Margin $style) + { + if($style->left != null) { + $class->left = $style->left; + } + if($style->right != null) { + $class->right = $style->right; + } + if($style->top != null) { + $class->top = $style->top; + } + if($style->bottom != null) { + $class->bottom = $style->bottom; + } + } +} diff --git a/src/Tailwind/Parser/MediaQuery.php b/src/Tailwind/Parser/MediaQuery.php new file mode 100644 index 0000000..31f0c77 --- /dev/null +++ b/src/Tailwind/Parser/MediaQuery.php @@ -0,0 +1,18 @@ + 0) { + $query = strtolower(strrev($output_array[1][0])); + return \PHPNative\Tailwind\Style\MediaQueryEnum::{$query}; + } + + return null; + } +} \ No newline at end of file diff --git a/src/Tailwind/Parser/Overflow.php b/src/Tailwind/Parser/Overflow.php new file mode 100644 index 0000000..69ca2cb --- /dev/null +++ b/src/Tailwind/Parser/Overflow.php @@ -0,0 +1,39 @@ + 0) { + return new \PHPNative\Tailwind\Style\Overflow(x: OverflowEnum::auto, y: OverflowEnum::auto); + } + + preg_match_all("/overflow-x-(auto|scroll|hidden|clip)/", $style, $output_array); + if (count($output_array[0]) > 0) { + $value = match($output_array[1][0]) { + 'auto' => OverflowEnum::auto, + 'scroll' => OverflowEnum::scroll, + 'hidden' => OverflowEnum::hidden, + 'clip' => OverflowEnum::clip + }; + return new \PHPNative\Tailwind\Style\Overflow(x: $value); + } + preg_match_all("/overflow-y-(auto|scroll|hidden|clip)/", $style, $output_array); + if (count($output_array[0]) > 0) { + $value = match($output_array[1][0]) { + 'auto' => OverflowEnum::auto, + 'scroll' => OverflowEnum::scroll, + 'hidden' => OverflowEnum::hidden, + 'clip' => OverflowEnum::clip + }; + return new \PHPNative\Tailwind\Style\Overflow(y: $value); + } + return null; + } +} \ No newline at end of file diff --git a/src/Tailwind/Parser/Padding.php b/src/Tailwind/Parser/Padding.php new file mode 100644 index 0000000..ccb537a --- /dev/null +++ b/src/Tailwind/Parser/Padding.php @@ -0,0 +1,78 @@ + 0) { + $l = (int)$output_array[1][0]; + $r = (int)$output_array[1][0]; + $t = (int)$output_array[1][0]; + $b = (int)$output_array[1][0]; + } + + preg_match_all('/px-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $l = (int)$output_array[1][0]; + $r = (int)$output_array[1][0]; + } + + preg_match_all('/py-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $t = (int)$output_array[1][0]; + $b = (int)$output_array[1][0]; + } + + preg_match_all('/pt-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $t = (int)$output_array[1][0]; + } + + preg_match_all('/pb-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $b = (int)$output_array[1][0]; + } + + preg_match_all('/pl-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $l = (int)$output_array[1][0]; + } + + preg_match_all('/pr-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $r = (int)$output_array[1][0]; + } + + if($l != null || $r != null || $t != null || $b != null) { + return new \PHPNative\Tailwind\Style\Padding($l, $r, $t, $b); + } + + return null; + } + + public static function merge(\PHPNative\Tailwind\Style\Padding $class, \PHPNative\Tailwind\Style\Padding $style) + { + if($style->left != null) { + $class->left = $style->left; + } + if($style->right != null) { + $class->right = $style->right; + } + if($style->top != null) { + $class->top = $style->top; + } + if($style->bottom != null) { + $class->bottom = $style->bottom; + } + } +} diff --git a/src/Tailwind/Parser/Parser.php b/src/Tailwind/Parser/Parser.php new file mode 100644 index 0000000..e47bcd7 --- /dev/null +++ b/src/Tailwind/Parser/Parser.php @@ -0,0 +1,12 @@ + 0) { + $query = strtolower($output_array[1][0]); + return \PHPNative\Tailwind\Style\StateEnum::{$query}; + } + + return null; + } +} \ No newline at end of file diff --git a/src/Tailwind/Parser/Text.php b/src/Tailwind/Parser/Text.php new file mode 100644 index 0000000..209154b --- /dev/null +++ b/src/Tailwind/Parser/Text.php @@ -0,0 +1,75 @@ + 0) { + $size = match ((string)$output_array[1][0]) { + 'xs' => 12, + 'sm' => 14, + 'base' => 16, + 'lg' => 18, + 'xl' => 20, + '2xl' => 24, + '3xl' => 30, + '4xl' => 36, + '5xl' => 48, + '6xl' => 60, + '7xl' => 72, + '8xl' => 96, + '9xl' => 128, + }; + return new \PHPNative\Tailwind\Style\Text(size: $size); + } + + preg_match_all('/text-(center|right|left)/', $style, $output_array); + if (count($output_array[0]) > 0) { + return match ((string)$output_array[1][0]) { + 'left' => new \PHPNative\Tailwind\Style\Text(align: AlignEnum::left), + 'right' => new \PHPNative\Tailwind\Style\Text(align: AlignEnum::right), + 'center' => new \PHPNative\Tailwind\Style\Text(align: AlignEnum::center), + }; + } + + preg_match_all('/text-(.*)/', $style, $output_array); + if (count($output_array[0]) > 0) { + $colorStyle = $output_array[1][0]; + $color = Color::parse($colorStyle); + return new \PHPNative\Tailwind\Style\Text($color); + } + + return null; + } + + public static function merge(\PHPNative\Tailwind\Style\Text $style1, \PHPNative\Tailwind\Style\Text $style2): void + { + if($style2->color->red != -1) { + $style1->color->red = $style2->color->red; + } + if($style2->color->green != -1) { + $style1->color->green = $style2->color->green; + } + if($style2->color->blue != -1) { + $style1->color->blue = $style2->color->blue; + } + if($style2->color->alpha != -1) { + $style1->color->alpha = $style2->color->alpha; + } + if($style2->size != 16) { + $style1->size = $style2->size; + } + if($style2->align != AlignEnum::left) { + $style1->align = $style2->align; + } + } +} diff --git a/src/Tailwind/Parser/Width.php b/src/Tailwind/Parser/Width.php new file mode 100644 index 0000000..348c48f --- /dev/null +++ b/src/Tailwind/Parser/Width.php @@ -0,0 +1,58 @@ + 0) { + $value1 = (int)$output_array[1][0]; + $value2 = (int)$output_array[2][0]; + $unit = Unit::Percent; + $value = 100/$value2*$value1; + $found = true; + } + + preg_match_all('/w-(\d*)/', $style, $output_array); + if (!$found && count($output_array[0]) > 0) { + $value = (int)$output_array[1][0]; + } + + preg_match_all('/(w-screen|w-full)/', $style, $output_array); + if (!$found && count($output_array[0]) > 0) { + $value = 100; + $unit = Unit::Percent; + } + + if($value != -1) { + return new \PHPNative\Tailwind\Style\Width($unit, $value); + } + + return null; + } + + public static function merge(\PHPNative\Tailwind\Style\Padding $class, \PHPNative\Tailwind\Style\Padding $style) + { + if($style->left != null) { + $class->left = $style->left; + } + if($style->right != null) { + $class->right = $style->right; + } + if($style->top != null) { + $class->top = $style->top; + } + if($style->bottom != null) { + $class->bottom = $style->bottom; + } + } +} diff --git a/src/Tailwind/Style/AlignEnum.php b/src/Tailwind/Style/AlignEnum.php new file mode 100644 index 0000000..c7dce94 --- /dev/null +++ b/src/Tailwind/Style/AlignEnum.php @@ -0,0 +1,10 @@ + self::lx2->value) { + return self::lx2; + } + if($windowWidth > self::lx->value) { + return self::lx; + } + if($windowWidth > self::gl->value) { + return self::gl; + } + if($windowWidth > self::dm->value) { + return self::dm; + } + if($windowWidth > self::ms->value) { + return self::ms; + } + return self::normal; + } +} \ No newline at end of file diff --git a/src/Tailwind/Style/Overflow.php b/src/Tailwind/Style/Overflow.php new file mode 100644 index 0000000..8b774c9 --- /dev/null +++ b/src/Tailwind/Style/Overflow.php @@ -0,0 +1,14 @@ +mediaQuery = $mq; + } + $state = \PHPNative\Tailwind\Parser\State::parse($styleStr); + if($state) { + $s->state = $state; + } + $computed->add($s); + } + return $computed; + } + + private static function parseSimpleStyle(string $style): ?Style + { + if($pd = \PHPNative\Tailwind\Parser\Padding::parse($style)) { + return $pd; + } + if($m = \PHPNative\Tailwind\Parser\Margin::parse($style)) { + return $m; + } + if($o = \PHPNative\Tailwind\Parser\Overflow::parse($style)) { + return $o; + } + if($w = \PHPNative\Tailwind\Parser\Width::parse($style)) { + return $w; + } + if($h = \PHPNative\Tailwind\Parser\Height::parse($style)) { + return $h; + } + if($b = \PHPNative\Tailwind\Parser\Basis::parse($style)) { + return $b; + } + if($f = \PHPNative\Tailwind\Parser\Flex::parse($style)) { + return $f; + } + if($bg = \PHPNative\Tailwind\Parser\Background::parse($style)) { + return $bg; + } + if($t = \PHPNative\Tailwind\Parser\Text::parse($style)) { + return $t; + } + if($b = \PHPNative\Tailwind\Parser\Border::parse($style)) { + return $b; + } + + return null; + } +} diff --git a/src/Ui/Component.php b/src/Ui/Component.php new file mode 100644 index 0000000..11eddf9 --- /dev/null +++ b/src/Ui/Component.php @@ -0,0 +1,134 @@ +viewport = $viewport; + } + + public function setWindow($window): void + { + $this->window = $window; + } + + public function update(): void + { + foreach ($this->children as $child) { + $child->update(); + } + } + + public function layout(null|TextRenderer $textRenderer = null): void + { + $this->computedStyles = StyleParser::parse($this->style)->getValidStyles( + MediaQueryEnum::normal, + StateEnum::normal, + ); + + if (isset($this->computedStyles[Margin::class]) && ($m = $this->computedStyles[Margin::class])) { + $this->viewport->x += $m->left; + $this->viewport->width -= $m->right + $m->left; + $this->viewport->y += $m->top; + $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 += $p->left; + $this->contentViewport->width -= $p->right + $p->left; + $this->contentViewport->y += $p->top; + $this->contentViewport->height -= $p->bottom + $p->top; + } + + foreach ($this->children as $child) { + $child->setViewport($this->contentViewport); + $child->layout($textRenderer); + } + } + + public function render(null|TextRenderer $textRenderer = null): void + { + if (!$this->visible) { + return; + } + + if ( + isset($this->computedStyles[\PHPNative\Tailwind\Style\Background::class]) && + ($bg = $this->computedStyles[\PHPNative\Tailwind\Style\Background::class]) + ) { + rsgl_setColor($this->window, $bg->color->red, $bg->color->green, $bg->color->blue, $bg->color->alpha); + + if ( + isset($this->computedStyles[\PHPNative\Tailwind\Style\Border::class]) && + ($border = $this->computedStyles[\PHPNative\Tailwind\Style\Border::class]) + ) { + rsgl_drawRoundRectExF( + $this->window, + $this->viewport->x, + $this->viewport->y, + $this->viewport->width, + $this->viewport->height, + $border->roundTopLeft ?? 0, + $border->roundTopLeft ?? 0, + $border->roundTopRight ?? 0, + $border->roundTopRight ?? 0, + $border->roundBottomLeft ?? 0, + $border->roundBottomLeft ?? 0, + $border->roundBottomRight ?? 0, + $border->roundBottomRight ?? 0, + ); + } else { + rsgl_drawRectF( + $this->window, + $this->viewport->x, + $this->viewport->y, + $this->viewport->width, + $this->viewport->height, + ); + } + } + } + + public function renderContent(null|TextRenderer $textRenderer = null): void + { + if (!$this->visible) { + return; + } + + // Render children + foreach ($this->children as $child) { + $child->setWindow($this->window); + $child->render($textRenderer); + $child->renderContent($textRenderer); + } + } + + public function addComponent(Component $component): void + { + $this->children[] = $component; + } +} diff --git a/src/Ui/Viewport.php b/src/Ui/Viewport.php new file mode 100644 index 0000000..7da977d --- /dev/null +++ b/src/Ui/Viewport.php @@ -0,0 +1,22 @@ +