Widget
Fondamentaux
Engine\Native\Widget n'est pas un widget qu'on instancie : c'est l'interface abstraite que tous les widgets du moteur natif implémentent — Container, Flex, Button, CustomPaint, absolument tous. Elle définit deux passes, exactement le contrat du RenderObject de Flutter : layout() est une négociation descendante (le parent propose une boîte de Constraints, l'enfant renvoie le Size qu'il a choisi — jamais l'inverse, ce qui garde le layout en une seule passe au lieu d'une boucle de reflow façon navigateur) ; paint() est une seconde passe descendante qui transforme ce que layout() a décidé en commandes de dessin à coordonnées absolues, empilées dans un Canvas.
Le contrat
<?php
namespace Engine\Native;
interface Widget
{
public function layout(Constraints $constraints): Size;
public function paint(Canvas $canvas, float $x, float $y): void;
}layout() est toujours appelé avant paint(), jamais l'inverse : un nœud ne peut pas se peindre lui-même sans connaître d'abord sa taille finale, et cette taille doit tenir dans les Constraints reçues — c'est un contrat, pas une suggestion.
Exemple : implémenter son propre Widget
<?php
use Engine\Native\Widget;
use Engine\Native\Constraints;
use Engine\Native\Size;
use Engine\Native\Canvas;
/**
* Un petit pastille de statut carrée, dessinée à la main —
* un exemple minimal de ce que chaque widget du moteur fait déjà
* en interne pour implémenter le contrat Widget.
*/
final class StatusDot implements Widget
{
private Size $size;
public function __construct(
private readonly float $diameter,
private readonly string $color,
) {
$this->size = Size::zero();
}
public function layout(Constraints $constraints): Size
{
$this->size = $constraints->constrain(new Size($this->diameter, $this->diameter));
return $this->size;
}
public function paint(Canvas $canvas, float $x, float $y): void
{
$radius = $this->size->width / 2;
$canvas->circle($x + $radius, $y + $radius, $radius, $this->color);
}
}StatusDot respecte le contrat à la lettre : layout() se contente de contraindre sa taille intrinsèque (le diamètre demandé) dans la boîte imposée par le parent, et paint() traduit cette taille finale en une seule commande Canvas::circle() à coordonnées absolues. Une fois cette classe écrite, elle est utilisable partout où un Widget est attendu — comme enfant d'un Flex, d'un Stack, etc.
À savoir. Un
Widgetpersonnalisé n'a besoin d'aucune classe de base ni d'enregistrement particulier —implements Widgetsuffit, exactement commeCircularProgressouCustomPaintle font déjà dans le cœur du framework.