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);
|
||||
|
||||
$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();
|
||||
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user