Icon für Buttons

This commit is contained in:
Thomas Peterson 2025-11-05 19:15:36 +01:00
parent 91ac766f4c
commit bf087c798d
3 changed files with 350 additions and 0 deletions

View 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 = [];
}
}

View File

@ -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
View 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.');
}
}