async
This commit is contained in:
parent
9f2b88e0eb
commit
11767487ef
@ -14,13 +14,58 @@ if (PHP_VERSION_ID < 80100) {
|
|||||||
|
|
||||||
$app = new Application('Button Example', 800, 600);
|
$app = new Application('Button Example', 800, 600);
|
||||||
|
|
||||||
|
$container = new Container('p-4');
|
||||||
// Button with different padding
|
// Button with different padding
|
||||||
$button2 = new Button(
|
$button2 = new Button(
|
||||||
text: 'Another Button',
|
text: 'Another Button',
|
||||||
style: 'm-5 p-15 hover:bg-green-200',
|
style: 'm-5 p-15 bg-lime-500 hover:bg-green-200',
|
||||||
onClick: function () {
|
|
||||||
echo 'test2';
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
$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();
|
$app->run();
|
||||||
|
|||||||
75
examples/async_button_example.php
Normal file
75
examples/async_button_example.php
Normal 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();
|
||||||
126
examples/async_http_example.php
Normal file
126
examples/async_http_example.php
Normal 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
123
src/Async/AsyncTask.php
Normal 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
74
src/Async/TaskManager.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace PHPNative\Framework;
|
namespace PHPNative\Framework;
|
||||||
|
|
||||||
|
use PHPNative\Async\TaskManager;
|
||||||
use PHPNative\Ui\Component;
|
use PHPNative\Ui\Component;
|
||||||
use PHPNative\Ui\Viewport;
|
use PHPNative\Ui\Viewport;
|
||||||
|
|
||||||
@ -165,6 +166,9 @@ class Application
|
|||||||
*/
|
*/
|
||||||
protected function update(): void
|
protected function update(): void
|
||||||
{
|
{
|
||||||
|
// Update async tasks
|
||||||
|
TaskManager::getInstance()->update();
|
||||||
|
|
||||||
if ($this->rootComponent) {
|
if ($this->rootComponent) {
|
||||||
$this->rootComponent->update();
|
$this->rootComponent->update();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
namespace PHPNative\Ui\Widget;
|
namespace PHPNative\Ui\Widget;
|
||||||
|
|
||||||
|
use PHPNative\Async\AsyncTask;
|
||||||
|
use PHPNative\Async\TaskManager;
|
||||||
use PHPNative\Framework\TextRenderer;
|
use PHPNative\Framework\TextRenderer;
|
||||||
|
|
||||||
class Button extends Container
|
class Button extends Container
|
||||||
{
|
{
|
||||||
private Label $label;
|
private Label $label;
|
||||||
private $onClick = null;
|
private $onClick = null;
|
||||||
|
private $onClickAsync = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $text = '',
|
public string $text = '',
|
||||||
@ -42,6 +45,24 @@ class Button extends Container
|
|||||||
$this->onClick = $onClick;
|
$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
|
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
|
||||||
{
|
{
|
||||||
// Check if click is within button bounds
|
// Check if click is within button bounds
|
||||||
@ -51,10 +72,23 @@ class Button extends Container
|
|||||||
$mouseY >= $this->viewport->y &&
|
$mouseY >= $this->viewport->y &&
|
||||||
$mouseY <= ($this->viewport->y + $this->viewport->height)
|
$mouseY <= ($this->viewport->y + $this->viewport->height)
|
||||||
) {
|
) {
|
||||||
// Call onClick callback if set
|
// Call async onClick callback if set
|
||||||
if ($this->onClick !== null) {
|
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)();
|
($this->onClick)();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Propagate to parent if click was outside button
|
// Propagate to parent if click was outside button
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user