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\TaskManager;
|
||||
use PHPNative\Framework\TextRenderer;
|
||||
use PHPNative\Ui\Component;
|
||||
|
||||
class Button extends Container
|
||||
{
|
||||
private Label $label;
|
||||
private null|Icon $leadingIcon = null;
|
||||
private string $iconPosition = 'left';
|
||||
private $onClick = null;
|
||||
private $onClickAsync = null;
|
||||
|
||||
@ -29,6 +32,33 @@ class Button extends Container
|
||||
$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
|
||||
{
|
||||
$this->text = $text;
|
||||
@ -127,4 +157,47 @@ class Button extends Container
|
||||
// Don't call parent::handleMouseClick as that would cause double event propagation
|
||||
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