sdl3/src/Ui/Widget/Button.php
2025-11-11 11:39:33 +01:00

207 lines
6.4 KiB
PHP

<?php
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;
public function __construct(
public string $text = '',
string $style = '',
null|callable $onClick = null,
) {
parent::__construct($style);
// Enable texture caching for buttons (huge performance boost!)
$this->setUseTextureCache(true);
// Create label inside button
$this->label = new Label(
text: $text,
style: 'text-black',
);
$this->addComponent($this->label);
$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;
$this->label->setText($text);
}
public function getText(): string
{
return $this->text;
}
public function setOnClick(callable $onClick): void
{
$this->onClick = $onClick;
}
public function setStyle(string $style): void
{
$this->style = $style;
}
/**
* Set async click handler that runs in background thread
* @param callable $onClickAsync Task to run asynchronously
* @param callable|null $onComplete Optional callback when task completes
* @param callable|null $onError Optional callback on error
*/
public function setOnClickAsync(
callable $onClickAsync,
null|callable $onComplete = null,
null|callable $onError = null,
): void {
$this->onClickAsync = [
'task' => $onClickAsync,
'onComplete' => $onComplete,
'onError' => $onError,
];
}
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
{
// Don't handle events if button is not visible
if (!$this->visible) {
return false;
}
// Debug output
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
error_log(sprintf(
"[Button '%s'] Click at (%.1f, %.1f), bounds: (%.1f, %.1f) to (%.1f, %.1f)",
$this->text,
$mouseX,
$mouseY,
$this->viewport->x,
$this->viewport->y,
$this->viewport->x + $this->viewport->width,
$this->viewport->y + $this->viewport->height,
));
}
// Check if click is within button bounds
if (
$mouseX >= $this->viewport->x &&
$mouseX <= ($this->viewport->x + $this->viewport->width) &&
$mouseY >= $this->viewport->y &&
$mouseY <= ($this->viewport->y + $this->viewport->height)
) {
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
error_log("[Button '{$this->text}'] Click INSIDE button - executing callback");
}
// Call async onClick callback if set
if ($this->onClickAsync !== null) {
$task = TaskManager::getInstance()->runAsync($this->onClickAsync['task']);
if ($this->onClickAsync['onComplete'] !== null) {
$task->onComplete($this->onClickAsync['onComplete']);
}
if ($this->onClickAsync['onError'] !== null) {
$task->onError($this->onClickAsync['onError']);
}
} // Call sync onClick callback if set
elseif ($this->onClick !== null) {
($this->onClick)();
}
return true;
}
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
error_log("[Button '{$this->text}'] Click OUTSIDE button");
}
// Click was outside button - don't handle it
// 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]);
}
}