This commit is contained in:
Thomas Peterson 2025-10-23 10:30:51 +02:00
parent 9f2b88e0eb
commit 11767487ef
7 changed files with 488 additions and 7 deletions

View File

@ -14,13 +14,58 @@ if (PHP_VERSION_ID < 80100) {
$app = new Application('Button Example', 800, 600);
$container = new Container('p-4');
// Button with different padding
$button2 = new Button(
text: 'Another Button',
style: 'm-5 p-15 hover:bg-green-200',
onClick: function () {
echo 'test2';
},
style: 'm-5 p-15 bg-lime-500 hover:bg-green-200',
);
$app->setRoot($button2);
$button2->setOnClickAsync(
onClickAsync: function () {
// Fetch data from API (example: JSON placeholder)
$url = 'https://jsonplaceholder.typicode.com/todos/1';
$context = stream_context_create([
'http' => [
'timeout' => 10,
'method' => 'GET',
],
]);
$response = file_get_contents($url, false, $context);
if ($response === false) {
throw new \Exception('Failed to fetch data');
}
return json_decode($response, true);
},
onComplete: function ($data) use ($container) {
$statusLabel = new Label(
text: 'Klicken Sie den Button, um Daten zu laden...',
style: 'text-base text-gray-700',
);
$resultLabel = new Label(
text: '',
style: 'text-sm text-blue-600',
);
$statusLabel->setText('✓ Daten erfolgreich geladen!');
$formatted = 'ID: ' . ($data['id'] ?? 'N/A') . "\n";
$formatted .= 'Titel: ' . ($data['title'] ?? 'N/A') . "\n";
$formatted .= 'Erledigt: ' . (($data['completed'] ?? false) ? 'Ja' : 'Nein');
$resultLabel->setText($formatted);
$container->addComponent($statusLabel);
$container->addComponent($resultLabel);
},
onError: function ($error) {},
);
$container->addComponent($button2);
$app->setRoot($container);
$app->run();

View File

@ -0,0 +1,75 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Button;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Label;
// Create application
$app = new Application('Async Button Example', 800, 600);
// Create UI components
$container = new Container('flex flex-col p-4 gap-4');
// Status label to show loading state
$statusLabel = new Label(
text: 'Klicken Sie den Button, um Daten zu laden...',
style: 'text-base text-gray-700'
);
// Result label to show fetched data
$resultLabel = new Label(
text: '',
style: 'text-sm text-blue-600'
);
// Button with async onClick handler
$button = new Button(
text: 'Daten aus dem Web laden',
style: 'bg-blue-500 hover:bg-blue-700 text-white p-4 rounded-lg'
);
// Set async click handler
$button->setOnClickAsync(
// Task that runs in background thread
onClickAsync: function() {
// Simulate web request (or use real HTTP client)
sleep(2); // Simulates network delay
// In production, you would use something like:
// $response = file_get_contents('https://api.example.com/data');
// return json_decode($response, true);
return [
'status' => 'success',
'data' => 'Daten erfolgreich geladen!',
'timestamp' => date('H:i:s')
];
},
// Callback when task completes successfully
onComplete: function($result) use ($statusLabel, $resultLabel) {
$statusLabel->setText('✓ Laden abgeschlossen!');
$resultLabel->setText(
'Status: ' . $result['status'] . "\n" .
'Daten: ' . $result['data'] . "\n" .
'Zeit: ' . $result['timestamp']
);
},
// Callback when task fails
onError: function($error) use ($statusLabel, $resultLabel) {
$statusLabel->setText('✗ Fehler beim Laden!');
$resultLabel->setText('Error: ' . $error->getMessage());
}
);
// Add components to container
$container->addComponent($statusLabel);
$container->addComponent($button);
$container->addComponent($resultLabel);
// Set root component and run
$app->setRoot($container)->run();

View File

@ -0,0 +1,126 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Button;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Label;
// Create application
$app = new Application('Async HTTP Request Example', 900, 700);
// Create UI
$container = new Container('flex flex-col p-6 gap-3 bg-gray-100');
$title = new Label(
text: 'Asynchrone HTTP Requests Demo',
style: 'text-2xl text-gray-900 mb-4'
);
$statusLabel = new Label(
text: 'Bereit zum Laden...',
style: 'text-base text-gray-700 p-2 bg-white rounded'
);
// Button for fetching JSON data
$fetchButton = new Button(
text: 'JSON API abrufen',
style: 'bg-green-500 hover:bg-green-700 text-white p-3 rounded-lg'
);
$resultLabel = new Label(
text: '',
style: 'text-sm text-gray-800 p-3 bg-white rounded whitespace-pre-wrap'
);
// Set async handler for JSON fetch
$fetchButton->setOnClickAsync(
onClickAsync: function() {
// Fetch data from API (example: JSON placeholder)
$url = 'https://jsonplaceholder.typicode.com/todos/1';
$context = stream_context_create([
'http' => [
'timeout' => 10,
'method' => 'GET',
]
]);
$response = @file_get_contents($url, false, $context);
if ($response === false) {
throw new \Exception('Failed to fetch data');
}
return json_decode($response, true);
},
onComplete: function($data) use ($statusLabel, $resultLabel) {
$statusLabel->setText('✓ Daten erfolgreich geladen!');
$formatted = "ID: " . ($data['id'] ?? 'N/A') . "\n";
$formatted .= "Titel: " . ($data['title'] ?? 'N/A') . "\n";
$formatted .= "Erledigt: " . (($data['completed'] ?? false) ? 'Ja' : 'Nein');
$resultLabel->setText($formatted);
},
onError: function($error) use ($statusLabel, $resultLabel) {
$statusLabel->setText('✗ Fehler aufgetreten');
$resultLabel->setText('Error: ' . $error->getMessage());
}
);
// Button for simulating slow operation
$slowButton = new Button(
text: 'Langsame Operation (5s)',
style: 'bg-orange-500 hover:bg-orange-700 text-white p-3 rounded-lg'
);
$slowButton->setOnClickAsync(
onClickAsync: function() {
$startTime = microtime(true);
// Simulate heavy computation
sleep(5);
$endTime = microtime(true);
$duration = round($endTime - $startTime, 2);
return [
'message' => 'Operation abgeschlossen',
'duration' => $duration . ' Sekunden'
];
},
onComplete: function($result) use ($statusLabel, $resultLabel) {
$statusLabel->setText('✓ Operation abgeschlossen!');
$resultLabel->setText(
$result['message'] . "\n" .
'Dauer: ' . $result['duration']
);
},
onError: function($error) use ($statusLabel, $resultLabel) {
$statusLabel->setText('✗ Fehler bei Operation');
$resultLabel->setText('Error: ' . $error->getMessage());
}
);
// Add all components
$container->addComponent($title);
$container->addComponent($statusLabel);
$container->addComponent($fetchButton);
$container->addComponent($slowButton);
$container->addComponent($resultLabel);
// Info label
$infoLabel = new Label(
text: 'Die UI bleibt während der Requests responsive!',
style: 'text-xs text-gray-500 mt-4 italic'
);
$container->addComponent($infoLabel);
// Run application
$app->setRoot($container)->run();

123
src/Async/AsyncTask.php Normal file
View File

@ -0,0 +1,123 @@
<?php
namespace PHPNative\Async;
use parallel\Runtime;
use parallel\Future;
class AsyncTask
{
private ?Future $future = null;
private mixed $result = null;
private bool $completed = false;
private bool $failed = false;
private mixed $error = null;
private mixed $onComplete = null;
private mixed $onError = null;
private mixed $task = null;
public function __construct(
callable $task
) {
$this->task = $task;
}
/**
* Start the async task execution
*/
public function start(): void
{
if ($this->future !== null) {
return; // Already started
}
try {
$runtime = new Runtime();
$this->future = $runtime->run($this->task);
} catch (\Throwable $e) {
$this->failed = true;
$this->error = $e;
$this->completed = true;
}
}
/**
* Check if task is completed and process result
*/
public function update(): void
{
if ($this->completed || $this->future === null) {
return;
}
// Check if future is done without blocking
if ($this->future->done()) {
try {
$this->result = $this->future->value();
$this->completed = true;
if ($this->onComplete !== null) {
($this->onComplete)($this->result);
}
} catch (\Throwable $e) {
$this->failed = true;
$this->error = $e;
$this->completed = true;
if ($this->onError !== null) {
($this->onError)($this->error);
}
}
}
}
/**
* Set callback for successful completion
*/
public function onComplete(callable $callback): self
{
$this->onComplete = $callback;
return $this;
}
/**
* Set callback for errors
*/
public function onError(callable $callback): self
{
$this->onError = $callback;
return $this;
}
/**
* Check if task is completed
*/
public function isCompleted(): bool
{
return $this->completed;
}
/**
* Check if task failed
*/
public function isFailed(): bool
{
return $this->failed;
}
/**
* Get the result (only available after completion)
*/
public function getResult(): mixed
{
return $this->result;
}
/**
* Get the error (only available after failure)
*/
public function getError(): mixed
{
return $this->error;
}
}

74
src/Async/TaskManager.php Normal file
View File

@ -0,0 +1,74 @@
<?php
namespace PHPNative\Async;
class TaskManager
{
private static ?TaskManager $instance = null;
private array $tasks = [];
private function __construct()
{
}
/**
* Get singleton instance
*/
public static function getInstance(): TaskManager
{
if (self::$instance === null) {
self::$instance = new TaskManager();
}
return self::$instance;
}
/**
* Add a new task and start it
*/
public function addTask(AsyncTask $task): void
{
$this->tasks[] = $task;
$task->start();
}
/**
* Create and start a new async task
*/
public function runAsync(callable $task): AsyncTask
{
$asyncTask = new AsyncTask($task);
$this->addTask($asyncTask);
return $asyncTask;
}
/**
* Update all running tasks (call this in your main loop)
*/
public function update(): void
{
foreach ($this->tasks as $key => $task) {
$task->update();
// Remove completed tasks
if ($task->isCompleted()) {
unset($this->tasks[$key]);
}
}
}
/**
* Get count of running tasks
*/
public function getRunningTaskCount(): int
{
return count($this->tasks);
}
/**
* Check if any tasks are running
*/
public function hasRunningTasks(): bool
{
return count($this->tasks) > 0;
}
}

View File

@ -2,6 +2,7 @@
namespace PHPNative\Framework;
use PHPNative\Async\TaskManager;
use PHPNative\Ui\Component;
use PHPNative\Ui\Viewport;
@ -165,6 +166,9 @@ class Application
*/
protected function update(): void
{
// Update async tasks
TaskManager::getInstance()->update();
if ($this->rootComponent) {
$this->rootComponent->update();
}

View File

@ -2,12 +2,15 @@
namespace PHPNative\Ui\Widget;
use PHPNative\Async\AsyncTask;
use PHPNative\Async\TaskManager;
use PHPNative\Framework\TextRenderer;
class Button extends Container
{
private Label $label;
private $onClick = null;
private $onClickAsync = null;
public function __construct(
public string $text = '',
@ -42,6 +45,24 @@ class Button extends Container
$this->onClick = $onClick;
}
/**
* 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,
?callable $onComplete = null,
?callable $onError = null
): void {
$this->onClickAsync = [
'task' => $onClickAsync,
'onComplete' => $onComplete,
'onError' => $onError,
];
}
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
{
// Check if click is within button bounds
@ -51,10 +72,23 @@ class Button extends Container
$mouseY >= $this->viewport->y &&
$mouseY <= ($this->viewport->y + $this->viewport->height)
) {
// Call onClick callback if set
if ($this->onClick !== null) {
// 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;
}
// Propagate to parent if click was outside button