Icon für Buttons
This commit is contained in:
parent
91ac766f4c
commit
bf087c798d
77
src/Framework/IconFontRegistry.php
Normal file
77
src/Framework/IconFontRegistry.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\Framework;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central registry for icon fonts (e.g. Font Awesome) used by UI components.
|
||||||
|
* Provides lazy loading and caching of TTF font handles per font path/size.
|
||||||
|
*/
|
||||||
|
class IconFontRegistry
|
||||||
|
{
|
||||||
|
private static null|string $defaultFontPath = null;
|
||||||
|
|
||||||
|
/** @var array<string,array<int,resource>> */
|
||||||
|
private static array $loadedFonts = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the default font that should be used for icons.
|
||||||
|
* Calling this clears any previously cached fonts for other paths.
|
||||||
|
*/
|
||||||
|
public static function setDefaultFontPath(string $fontPath): void
|
||||||
|
{
|
||||||
|
if (!is_file($fontPath)) {
|
||||||
|
throw new \InvalidArgumentException("Icon font not found at path: {$fontPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$defaultFontPath !== $fontPath) {
|
||||||
|
self::clear();
|
||||||
|
self::$defaultFontPath = $fontPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultFontPath(): null|string
|
||||||
|
{
|
||||||
|
return self::$defaultFontPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve (and lazily load) a font handle for the requested size.
|
||||||
|
*
|
||||||
|
* @return resource|null
|
||||||
|
*/
|
||||||
|
public static function getFont(int $size, null|string $fontPath = null)
|
||||||
|
{
|
||||||
|
$path = $fontPath ?? self::$defaultFontPath;
|
||||||
|
if ($path === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $path . '|' . $size;
|
||||||
|
|
||||||
|
if (isset(self::$loadedFonts[$key]) && is_resource(self::$loadedFonts[$key])) {
|
||||||
|
return self::$loadedFonts[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
$font = @ttf_open_font($path, $size);
|
||||||
|
if ($font === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$loadedFonts[$key] = $font;
|
||||||
|
|
||||||
|
return $font;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close all cached font handles.
|
||||||
|
*/
|
||||||
|
public static function clear(): void
|
||||||
|
{
|
||||||
|
foreach (self::$loadedFonts as $font) {
|
||||||
|
if (is_resource($font)) {
|
||||||
|
@ttf_close_font($font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self::$loadedFonts = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,10 +5,13 @@ namespace PHPNative\Ui\Widget;
|
|||||||
use PHPNative\Async\AsyncTask;
|
use PHPNative\Async\AsyncTask;
|
||||||
use PHPNative\Async\TaskManager;
|
use PHPNative\Async\TaskManager;
|
||||||
use PHPNative\Framework\TextRenderer;
|
use PHPNative\Framework\TextRenderer;
|
||||||
|
use PHPNative\Ui\Component;
|
||||||
|
|
||||||
class Button extends Container
|
class Button extends Container
|
||||||
{
|
{
|
||||||
private Label $label;
|
private Label $label;
|
||||||
|
private null|Icon $leadingIcon = null;
|
||||||
|
private string $iconPosition = 'left';
|
||||||
private $onClick = null;
|
private $onClick = null;
|
||||||
private $onClickAsync = null;
|
private $onClickAsync = null;
|
||||||
|
|
||||||
@ -29,6 +32,33 @@ class Button extends Container
|
|||||||
$this->onClick = $onClick;
|
$this->onClick = $onClick;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setIcon(null|Icon $icon, string $position = 'left'): void
|
||||||
|
{
|
||||||
|
$position = strtolower($position);
|
||||||
|
if (!in_array($position, ['left', 'right'], true)) {
|
||||||
|
throw new \InvalidArgumentException('Button icon position must be "left" or "right".');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->leadingIcon !== null) {
|
||||||
|
$this->removeChild($this->leadingIcon);
|
||||||
|
$this->leadingIcon = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($icon !== null) {
|
||||||
|
$this->addComponent($icon);
|
||||||
|
$this->leadingIcon = $icon;
|
||||||
|
$this->iconPosition = $position;
|
||||||
|
|
||||||
|
if ($position === 'left') {
|
||||||
|
$this->moveChildBefore($icon, $this->label);
|
||||||
|
} else {
|
||||||
|
$this->moveChildAfter($icon, $this->label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->markDirty(true);
|
||||||
|
}
|
||||||
|
|
||||||
public function setText(string $text): void
|
public function setText(string $text): void
|
||||||
{
|
{
|
||||||
$this->text = $text;
|
$this->text = $text;
|
||||||
@ -127,4 +157,47 @@ class Button extends Container
|
|||||||
// Don't call parent::handleMouseClick as that would cause double event propagation
|
// Don't call parent::handleMouseClick as that would cause double event propagation
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function removeChild(Component $component): void
|
||||||
|
{
|
||||||
|
foreach ($this->children as $index => $child) {
|
||||||
|
if ($child === $component) {
|
||||||
|
if (method_exists($child, 'detachFromWindow')) {
|
||||||
|
$child->detachFromWindow();
|
||||||
|
}
|
||||||
|
$child->setParent(null);
|
||||||
|
unset($this->children[$index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->children = array_values($this->children);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function moveChildBefore(Component $child, Component $reference): void
|
||||||
|
{
|
||||||
|
$childIndex = array_search($child, $this->children, true);
|
||||||
|
$referenceIndex = array_search($reference, $this->children, true);
|
||||||
|
|
||||||
|
if ($childIndex === false || $referenceIndex === false || $childIndex === $referenceIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
array_splice($this->children, $childIndex, 1);
|
||||||
|
$referenceIndex = array_search($reference, $this->children, true);
|
||||||
|
array_splice($this->children, $referenceIndex, 0, [$child]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function moveChildAfter(Component $child, Component $reference): void
|
||||||
|
{
|
||||||
|
$childIndex = array_search($child, $this->children, true);
|
||||||
|
$referenceIndex = array_search($reference, $this->children, true);
|
||||||
|
|
||||||
|
if ($childIndex === false || $referenceIndex === false || $childIndex === $referenceIndex + 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
array_splice($this->children, $childIndex, 1);
|
||||||
|
$referenceIndex = array_search($reference, $this->children, true);
|
||||||
|
array_splice($this->children, $referenceIndex + 1, 0, [$child]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
200
src/Ui/Widget/Icon.php
Normal file
200
src/Ui/Widget/Icon.php
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\Ui\Widget;
|
||||||
|
|
||||||
|
use PHPNative\Framework\IconFontRegistry;
|
||||||
|
use PHPNative\Tailwind\Style\Padding;
|
||||||
|
use PHPNative\Tailwind\Style\Text;
|
||||||
|
use PHPNative\Ui\Component;
|
||||||
|
|
||||||
|
class Icon extends Component
|
||||||
|
{
|
||||||
|
private string $glyph;
|
||||||
|
private int $size;
|
||||||
|
private null|string $fontPath;
|
||||||
|
private mixed $texture = null;
|
||||||
|
private int $textureWidth = 0;
|
||||||
|
private int $textureHeight = 0;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string|int|\PHPNative\Tailwind\Data\Icon $icon,
|
||||||
|
int $size = 16,
|
||||||
|
string $style = '',
|
||||||
|
null|string $fontPath = null,
|
||||||
|
) {
|
||||||
|
parent::__construct($style);
|
||||||
|
|
||||||
|
$this->glyph = $this->resolveGlyph($icon);
|
||||||
|
$this->size = max(1, $size);
|
||||||
|
$this->fontPath = $fontPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIcon(string|int|\PHPNative\Tailwind\Data\Icon $icon): void
|
||||||
|
{
|
||||||
|
$glyph = $this->resolveGlyph($icon);
|
||||||
|
if ($glyph === $this->glyph) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->glyph = $glyph;
|
||||||
|
$this->clearTexture();
|
||||||
|
$this->markDirty(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSize(int $size): void
|
||||||
|
{
|
||||||
|
$size = max(1, $size);
|
||||||
|
if ($this->size === $size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->size = $size;
|
||||||
|
$this->clearTexture();
|
||||||
|
$this->markDirty(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFontPath(null|string $fontPath): void
|
||||||
|
{
|
||||||
|
if ($this->fontPath === $fontPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fontPath = $fontPath;
|
||||||
|
$this->clearTexture();
|
||||||
|
$this->markDirty(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function layout(null|\PHPNative\Framework\TextRenderer $textRenderer = null): void
|
||||||
|
{
|
||||||
|
parent::layout($textRenderer);
|
||||||
|
|
||||||
|
$font = IconFontRegistry::getFont($this->size, $this->fontPath);
|
||||||
|
$width = $this->size;
|
||||||
|
$height = $this->size;
|
||||||
|
|
||||||
|
if ($font) {
|
||||||
|
$dimensions = ttf_size_text($font, $this->glyph);
|
||||||
|
if ($dimensions !== false) {
|
||||||
|
$width = (int) $dimensions['w'];
|
||||||
|
$height = (int) $dimensions['h'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($this->computedStyles[\PHPNative\Tailwind\Style\Width::class])) {
|
||||||
|
$this->viewport->width = $width;
|
||||||
|
$this->contentViewport->width = $width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($this->computedStyles[\PHPNative\Tailwind\Style\Height::class])) {
|
||||||
|
$this->viewport->height = $height;
|
||||||
|
$this->contentViewport->height = $height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->computedStyles[Padding::class]) && ($padding = $this->computedStyles[Padding::class])) {
|
||||||
|
$this->viewport->width = $width + $padding->left + $padding->right;
|
||||||
|
$this->viewport->height = $height + $padding->top + $padding->bottom;
|
||||||
|
$this->contentViewport->width = $width;
|
||||||
|
$this->contentViewport->height = $height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderContent(&$renderer, null|\PHPNative\Framework\TextRenderer $textRenderer = null): void
|
||||||
|
{
|
||||||
|
if (!$this->isVisible()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->texture === null || $this->isDirty()) {
|
||||||
|
$this->createTexture($renderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->texture !== null) {
|
||||||
|
sdl_render_texture($renderer, $this->texture, [
|
||||||
|
'x' => (int) $this->contentViewport->x,
|
||||||
|
'y' => (int) $this->contentViewport->y,
|
||||||
|
'w' => $this->textureWidth,
|
||||||
|
'h' => $this->textureHeight,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::renderContent($renderer, $textRenderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
$this->clearTexture();
|
||||||
|
parent::__destruct();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createTexture($renderer): void
|
||||||
|
{
|
||||||
|
$this->clearTexture();
|
||||||
|
|
||||||
|
$font = IconFontRegistry::getFont($this->size, $this->fontPath);
|
||||||
|
if (!$font) {
|
||||||
|
$this->renderDirty = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$textStyle = $this->computedStyles[Text::class] ?? new Text();
|
||||||
|
$color = $textStyle->color;
|
||||||
|
$r = max(0, min(255, $color->red >= 0 ? $color->red : 0));
|
||||||
|
$g = max(0, min(255, $color->green >= 0 ? $color->green : 0));
|
||||||
|
$b = max(0, min(255, $color->blue >= 0 ? $color->blue : 0));
|
||||||
|
$a = max(0, min(255, $color->alpha));
|
||||||
|
|
||||||
|
$surface = ttf_render_text_blended($font, $this->glyph, $r, $g, $b);
|
||||||
|
if (!$surface) {
|
||||||
|
$this->renderDirty = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$texture = sdl_create_texture_from_surface($renderer, $surface);
|
||||||
|
if (!$texture) {
|
||||||
|
$this->renderDirty = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dimensions = ttf_size_text($font, $this->glyph);
|
||||||
|
if ($dimensions !== false) {
|
||||||
|
$this->textureWidth = (int) $dimensions['w'];
|
||||||
|
$this->textureHeight = (int) $dimensions['h'];
|
||||||
|
} else {
|
||||||
|
$this->textureWidth = $this->size;
|
||||||
|
$this->textureHeight = $this->size;
|
||||||
|
}
|
||||||
|
|
||||||
|
sdl_set_texture_blend_mode($texture, SDL_BLENDMODE_BLEND);
|
||||||
|
if (function_exists('sdl_set_texture_alpha_mod')) {
|
||||||
|
sdl_set_texture_alpha_mod($texture, $a);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->texture = $texture;
|
||||||
|
$this->renderDirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function clearTexture(): void
|
||||||
|
{
|
||||||
|
if ($this->texture !== null) {
|
||||||
|
sdl_destroy_texture($this->texture);
|
||||||
|
$this->texture = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveGlyph(string|int|\PHPNative\Tailwind\Data\Icon $icon): string
|
||||||
|
{
|
||||||
|
if ($icon instanceof \PHPNative\Tailwind\Data\Icon) {
|
||||||
|
return mb_chr($icon->value, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_int($icon)) {
|
||||||
|
return mb_chr($icon, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mb_strlen($icon, 'UTF-8') === 1) {
|
||||||
|
return $icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \InvalidArgumentException('Icon must be a valid unicode codepoint or a single character string.');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user