207 lines
6.4 KiB
PHP
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]);
|
|
}
|
|
}
|