From 11767487ef9435d08f31460e0f01202eea81aafe Mon Sep 17 00:00:00 2001 From: Thomas Peterson Date: Thu, 23 Oct 2025 10:30:51 +0200 Subject: [PATCH] async --- examples/SimpleButtonExample.php | 55 +++++++++++-- examples/async_button_example.php | 75 ++++++++++++++++++ examples/async_http_example.php | 126 ++++++++++++++++++++++++++++++ src/Async/AsyncTask.php | 123 +++++++++++++++++++++++++++++ src/Async/TaskManager.php | 74 ++++++++++++++++++ src/Framework/Application.php | 4 + src/Ui/Widget/Button.php | 38 ++++++++- 7 files changed, 488 insertions(+), 7 deletions(-) create mode 100644 examples/async_button_example.php create mode 100644 examples/async_http_example.php create mode 100644 src/Async/AsyncTask.php create mode 100644 src/Async/TaskManager.php diff --git a/examples/SimpleButtonExample.php b/examples/SimpleButtonExample.php index 161b277..5a7af29 100644 --- a/examples/SimpleButtonExample.php +++ b/examples/SimpleButtonExample.php @@ -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(); diff --git a/examples/async_button_example.php b/examples/async_button_example.php new file mode 100644 index 0000000..20e1a7b --- /dev/null +++ b/examples/async_button_example.php @@ -0,0 +1,75 @@ +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(); diff --git a/examples/async_http_example.php b/examples/async_http_example.php new file mode 100644 index 0000000..c6cbc5f --- /dev/null +++ b/examples/async_http_example.php @@ -0,0 +1,126 @@ +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(); diff --git a/src/Async/AsyncTask.php b/src/Async/AsyncTask.php new file mode 100644 index 0000000..837efed --- /dev/null +++ b/src/Async/AsyncTask.php @@ -0,0 +1,123 @@ +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; + } +} diff --git a/src/Async/TaskManager.php b/src/Async/TaskManager.php new file mode 100644 index 0000000..d10cc6b --- /dev/null +++ b/src/Async/TaskManager.php @@ -0,0 +1,74 @@ +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; + } +} diff --git a/src/Framework/Application.php b/src/Framework/Application.php index c20cc30..7b0b215 100644 --- a/src/Framework/Application.php +++ b/src/Framework/Application.php @@ -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(); } diff --git a/src/Ui/Widget/Button.php b/src/Ui/Widget/Button.php index 0d02620..3148b3a 100644 --- a/src/Ui/Widget/Button.php +++ b/src/Ui/Widget/Button.php @@ -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