*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Serializer\Tests\Normalizer\Features;

class CallbacksObject
{
    public $bar;

    /**
     * @var string|null
     */
    public $foo;

    public function __construct($bar = null, ?string $foo = null)
    {
        $this->bar = $bar;
        $this->f

        return $this->bar;
    }

    public function setBar($bar)
    {
        $this->bar = $bar;
    }

    public function getFoo(): ?string
    {
        return $this->foo;
    }

    public function setFoo(?string $foo)
    {
        $this->foo = $f

oo = $foo;
    }

    public function getBar()
    {


 This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Serializer\Tests\Normalizer\Features;

use PHPUnit\Framework\Attributes\DataProvider;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

/**
 * Test AbstractNormalizer::FILTER_BOOL.
 */
trait FilterBoolTestTrait
{
    abstract protected func

izer = $this->getNormalizerForFilterBool();

        $dummy = $normalizer->denormalize(['value' => $parameterValue], FilterBoolObject::class, context: ['filter_bool' => true]);

        $this->assertSame($expectedValue, $dummy->value);
    }

    public st

tion getNormalizerForFilterBool(): DenormalizerInterface;

    #[DataProvider('provideObjectWithBoolArguments')]
    public function testObjectWithBoolArguments(?bool $expectedValue, ?string $parameterValue)
    {
        $normal


he Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Serializer\Tests\Normalizer\Features;

use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
 * Test AbstractObjectNormalizer::SKIP_NULL_VALUES.
 */
trait SkipNullValuesTestTrait
{
    abstract protected function getNormalizerForSkipNullValues(): NormalizerInterfac

       $dummy = new ObjectDummy();
        $dummy->bar = 'present';

        $normalizer = $this->getNormalizerForSkipNullValues();
        $result = $normalizer->normalize($dummy, null, ['skip_null_values' => true]);
        $this->assertSame(['fooBar' =>

e;

    public function testSkipNullValues()
    {


myPrivate;
use Symfony\Component\Serializer\Tests\Fixtures\Php80Dummy;
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
use Symfony\Component\Serializer\Tests\Fixtures\StdClassNormalizer;
use Symfony\Component\Serializer\Tests\Fixtures\VoidNeverReturnTypeDummy;
use Symfony\Component\Serializer\Tests\Normalizer\Features\AttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CacheableObjectAttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\Ca

llbacksTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\ConstructorArgumentsTestTrait;
use Symfony\Component\Serializer\T


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Serializer\Tests\Normalizer\Features;

use Symfony\Component\Serializer\Exception\ExtraAttributesException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
 * Test AbstractNormalizer::ATTRIBUTES and AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES.
 */
trait Attrib

ze()
    {
        $normalizer = $this->getNormalizerForAttributes();

        $objectInner = new ObjectInner();
        $objectInner->foo = 'innerFoo';
        $objectInner->bar = 'innerBar';

        $objectDummy = new ObjectDummy();
        $objectDummy

utesTestTrait
{
    abstract protected function getNormalizerForAttributes(): NormalizerInterface;

    abstract protected function getDenormalizerForAttributes(): DenormalizerInterface;

    public function testAttributesNormali


n Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Serializer\Tests\Dummy;

use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Attribute\Ignore;
use Symfony\Component\Serializer\Attribute\MaxDepth;
use Symfony\Component\Serializer\Attribute\SerializedName;
use Symfony\Component\Ser

ne
{
    #[MaxDepth(1)]
    #[Groups(['book:read', 'book:write'])]
    #[SerializedName('identifier')]
    #[Ignore]
    #[Context(
        normalizationContext: ['groups' => ['book:read']],
        denormalizationContext: ['groups' => ['book:write']],

ializer\Attribute\SerializedPath;

class DummyClassO


 This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Serializer\Tests\Normalizer\Features;

#[\AllowDynamicProperties]
class ObjectDummy
{
    protected $foo;
    /**
     * @var array
     */
    public $bar;
    private $baz;
    protected $camelCase;
    protected $object;
    private $go;

    public funct

  {
        $this->baz = $baz;
    }

    public function getFooBar()
    {
        return $this->foo.$this->bar;
    }

    public function getCamelCase()
    {
        return $this->camelCase;
    }

    public function setCamelCase($camelCase)
    {

ion getFoo()
    {
        return $this->foo;
    }

    public function setFoo($foo)
    {
        $this->foo = $foo;
    }

    public function isBaz()
    {
        return $this->baz;
    }

    public function setBaz($baz)


n Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Serializer\Tests\Normalizer;

use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
 * Provides a test Normalizer which only implements the NormalizerInterface.
 *
 * @author Lin Clark <lin@lin-clark.com>
 */
class TestNormalizer implements NormalizerInterface
{
    public function normalize(mixed $

array|string|int|float|bool|\ArrayObject|null
    {
        return null;
    }

    public function getSupportedTypes(?string $format): array
    {
        return ['*' => false];
    }

    public function supportsNormalization($data, ?string $format = nul

data, ?string $format = null, array $context = []):


buted with this source code.
 */

namespace Symfony\Component\Tui\Ansi;

/**
 * Tracks active ANSI SGR codes to preserve styling across line breaks.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class AnsiCodeTracker
{
    private bool $bold = false;
    private bool $dim = false;
    private bool $italic = false;
    private bool $underline = false;
    private bool $doubleUnderline = false;
    private bool $blink = false;
    private bool $inverse = fal

escape code and update tracking state.
     */
    public function process(string $ansiCode): void
    {
        // Fast direct parsing: skip regex, extract params between \x1b[ and m
        if (\strlen($ansiCode) < 3 || "\x1b" !== $ansiCode[0] || '[' !==

se;
    private bool $hidden = false;
    private bool $strikethrough = false;
    private ?string $fgColor = null;
    private ?string $bgColor = null;
    private ?string $underlineColor = null;

    /**
     * Process an ANSI


 This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Render;

/**
 * Represents the absolute position and size of a rendered widget on screen.
 *
 * Coordinates are in terminal character cells, with (0, 0) at the top-left.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.c

     * Check if the given screen coordinates are within this rect.
     *
     * @param int $row 0-indexed row
     * @param int $col 0-indexed column
     */
    public function contains(int $row, int $col): bool
    {
        return $row >= $this->row

om>
 */
final class WidgetRect
{
    public function __construct(
        public readonly int $row,
        public readonly int $col,
        public readonly int $columns,
        public readonly int $rows,
    ) {
    }

    /**


 This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Render;

use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Widget\AbstractWidget;

/**
 * Interface for rendering individual widgets and resolving their styles.
 *
 * Used by LayoutEngine and ChromeApplier to call back into the Renderer
 *

 pipeline.
     *
     * @return string[]
     */
    public function renderWidget(AbstractWidget $widget, RenderContext $context): array;

    /**
     * Resolve the style for a widget by merging cascade layers.
     */
    public function resolveStyle(Ab

 without creating a circular class dependency.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface WidgetRendererInterface
{
    /**
     * Render a single widget through the full


er\RenderContext;
use Symfony\Component\Tui\Style\DefaultStyleSheet;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Style\StyleSheet;
use Symfony\Component\Tui\Tui;

/**
 * Base widget class with lifecycle hooks and dirty tracking.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
abstract class AbstractWidget
{
    private int $renderRevision = 0;
    private ?string $id = null;
    private ?string $label = null;
    private ?AbstractWidget $parent = null;
    pr

tring<AbstractEvent>, list<callable>> */
    private array $listeners = [];

    // Render cache: stores the last output of Renderer::renderWidget()
    // keyed on (renderRevision, columns, rows) so unchanged widgets
    // skip style resolution, layout,

ivate ?WidgetContext $context = null;
    private ?Style $internalStyle = null;
    private static ?StyleSheet $defaultStyleSheet = null;

    /** @var string[] */
    private array $styleClasses = [];

    /** @var array<class-s


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget;

/**
 * Handles bracketed paste mode detection and buffering.
 *
 * Terminals that support bracketed paste wrap pasted text between
 * ESC[200~ (start) and ESC[201~ (end) sequences. This trait
 * accumulates chunks until the end marker is received, then
 * returns the complete paste content.
 *
 * @experimental
 *
 * @author Fabien Po

= false;
    private string $pasteBuffer = '';

    private function isBufferingPaste(): bool
    {
        return $this->inPaste;
    }

    /**
     * Process bracketed paste sequences in input data.
     *
     * Detects paste start/end markers and buff

tencier <fabien@symfony.com>
 */
trait BracketedPasteTrait
{
    private const int MAX_PASTE_BYTES = 16 * 1024 * 1024;
    private const string PASTE_OVERFLOW_MESSAGE = '[paste exceeded 16 MiB limit]';

    private bool $inPaste


 part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget;

use Symfony\Component\Tui\Event\CancelEvent;
use Symfony\Component\Tui\Input\Key;

/**
 * Loader that can be cancelled with Escape key.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class CancellableLoaderWidget extends LoaderWi

truct($message);
    }

    /**
     * Check if the loader was cancelled.
     */
    public function isCancelled(): bool
    {
        return $this->cancelled;
    }

    /**
     * Reset the cancelled state.
     */
    public function reset(): void

dget implements FocusableInterface
{
    use FocusableTrait;
    use KeybindingsTrait;

    private bool $cancelled = false;

    public function __construct(
        string $message = 'Loading...',
    ) {
        parent::__cons


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget;

use Symfony\Component\Tui\Exception\LogicException;
use Symfony\Component\Tui\Render\RenderContext;

/**
 * Container widget that groups child widgets with optional styling.
 *
 * Supports:
 * - Vertical or horizontal layout (via Style::direction)
 * - Gap between children (via Style::gap)
 * - Padding, border, background via Style

Or via stylesheet rules:
 *
 *     $stylesheet->addRule('.panes', new Style(direction: Direction::Horizontal, gap: 2));
 *
 * Layout and chrome rendering is handled by the Renderer.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *

* - Vertically expandable children
 *
 * Layout direction and gap are style properties, configurable via
 * stylesheets or inline styles:
 *
 *     $container->setStyle(new Style(direction: Direction::Horizontal, gap: 1));
 *
 *


ent;
use Symfony\Component\Tui\Input\Key;
use Symfony\Component\Tui\Input\Keybindings;
use Symfony\Component\Tui\Render\RenderContext;
use Symfony\Component\Tui\Style\CursorShape;
use Symfony\Component\Tui\Widget\Editor\EditorDocument;
use Symfony\Component\Tui\Widget\Editor\EditorRenderer;
use Symfony\Component\Tui\Widget\Editor\EditorViewport;
use Symfony\Component\Tui\Widget\Util\KillRing;
use Symfony\Component\Tui\Widget\Util\StringUtils;

/**
 * Multi-line text editor.
 *
 * Orchestrates input routing

 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class EditorWidget extends AbstractWidget implements FocusableInterface, VerticallyExpandableInterface
{
    use BracketedPasteTrait;
    use FocusableTrait;
    use KeybindingsTrait

between collaborators:
 * - {@see EditorDocument}: text buffer, cursor, undo/redo, kill ring
 * - {@see EditorViewport}: scroll offset, viewport calculations
 * - {@see EditorRenderer}: line rendering with cursor and word-wrap
 *


ge.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget;

use Symfony\Component\Tui\Ansi\AnsiUtils;
use Symfony\Component\Tui\Input\Keybindings;

/**
 * Interface for widgets that can receive focus.
 *
 * Widgets that accept user input (text editors, inputs, lists) implement
 * this interface so the focus manager can route keyboard events t

ar at the right spot.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface FocusableInterface
{
    /**
     * Check if the widget currently has focus.
     */
    public function isFocused(): bool;

    /**
     * Set the

o them.
 *
 * Widgets that display a text cursor should emit
 * {@see AnsiUtils::cursorMarker()} at the cursor position when focused
 * so the terminal's hardware cursor handles blinking natively and IME
 * candidate windows appe


ony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget;

/**
 * Default implementation of focus state for focusable widgets.
 *
 * Invalidates the widget when focus changes. Override setFocused()
 * for custom behavior (e.g. cursor blinker management).
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
trait FocusableTrait
{
    private bool $focused = fals

    return $this->focused;
    }

    /**
     * @return $this
     */
    public function setFocused(bool $focused): static
    {
        if ($this->focused !== $focused) {
            $this->focused = $focused;
            $this->invalidate();
        }

e;

    public function isFocused(): bool
    {


;
use Symfony\Component\Tui\Event\SubmitEvent;
use Symfony\Component\Tui\Input\Key;
use Symfony\Component\Tui\Input\Keybindings;
use Symfony\Component\Tui\Render\RenderContext;
use Symfony\Component\Tui\Style\CursorShape;
use Symfony\Component\Tui\Widget\Util\Line;
use Symfony\Component\Tui\Widget\Util\StringUtils;

/**
 * Single-line text input with horizontal scrolling.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class InputWidget extends AbstractWidget implements Focusable

 ?Keybindings $keybindings = null,
    ) {
        if (null !== $keybindings) {
            $this->setKeybindings($keybindings);
        }
        $this->line = new Line();
    }

    /**
     * @param callable(SubmitEvent): void $callback
     *
     * @r

Interface
{
    use BracketedPasteTrait;
    use FocusableTrait;
    use KeybindingsTrait;

    private Line $line;
    private string $prompt = '> ';
    private bool $submitted = false;

    public function __construct(


ge.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget;

use Symfony\Component\Tui\Input\Keybindings;

/**
 * Default implementation of keybindings for focusable widgets.
 *
 * Resolution order (later overrides earlier):
 * 1. Widget defaults (from getDefaultKeybindings())
 * 2. Global keybindings from the TUI (via WidgetContext)
 * 3. Exp

e(string): bool)|null */
    private ?\Closure $onInput = null;

    /**
     * Return the effective keybindings for this widget.
     *
     * Resolution order (later overrides earlier):
     * 1. Widget defaults (from getDefaultKeybindings())
     * 2. G

licit keybindings set on this widget (via setKeybindings())
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
trait KeybindingsTrait
{
    private ?Keybindings $keybindings = null;

    /** @var (\Closur


buted with this source code.
 */

namespace Symfony\Component\Tui\Widget;

use Symfony\Component\Tui\Ansi\AnsiUtils;
use Symfony\Component\Tui\Exception\InvalidArgumentException;
use Symfony\Component\Tui\Loop\PeriodicStepper;
use Symfony\Component\Tui\Render\RenderContext;
use Symfony\Component\Tui\Widget\Util\StringUtils;

/**
 * Animated loading spinner.
 *
 * Newly constructed loaders are stopped; call {@see start()} to begin
 * the animation. Attaching a stopped loader to the widget tree does not
 * sc

 = 'dots';
    private const DEFAULT_INTERVAL_MS = 80;

    /** @var array<string, string[]> */
    private static array $styles = [
        'line' => ['-', '\\', '|', '/'],
        'dots' => ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
        'bou

hedule any ticks until start() is called.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class LoaderWidget extends AbstractWidget
{
    use ScheduledTickTrait;

    private const string DEFAULT_STYLE


ommonMark\Node\Block\ThematicBreak;
use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
use League\CommonMark\Extension\CommonMark\Node\Inline\Emphasis;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\CommonMark\Extension\CommonMark\Node\Inline\Strong;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
use League\CommonMark\Extension\Strikethrough\Strikethrough;
use League\CommonMark\Extension\Table\Table;
use League\CommonMark\Extension\Table\TableCell;
use Le

eague\CommonMark\Node\Inline\Text;
use League\CommonMark\Node\Node;
use League\CommonMark\Parser\MarkdownParser;
use Symfony\Component\Tui\Ansi\AnsiUtils;
use Symfony\Component\Tui\Ansi\TextWrapper;
use Symfony\Component\Tui\Exception\LogicException;
use S

ague\CommonMark\Extension\Table\TableRow;
use League\CommonMark\Extension\Table\TableSection;
use League\CommonMark\Node\Block\Document;
use League\CommonMark\Node\Block\Paragraph;
use League\CommonMark\Node\Inline\Newline;
use L


ringUtils;

/**
 * Animated progress bar widget.
 *
 * Supports both determinate (with max steps) and indeterminate (no max) modes.
 * Uses a format string with placeholders to render the bar.
 *
 * Built-in placeholders: %current%, %max%, %bar%, %percent%, %elapsed%,
 * %remaining%, %estimated%, %memory%, %message%.
 *
 * The bar animates via the event loop, like the LoaderWidget spinner.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ProgressBarWidget extends AbstractWid

private int $startTime;
    private bool $running = false;

    private int $barWidth = 28;
    private string $barChar = '━';
    private string $emptyBarChar = '━';
    private string $progressChar = '';
    private string $format;

    /** @var array<st

get
{
    use ScheduledTickTrait;

    private const TICK_INTERVAL_MS = 100;

    private int $step = 0;
    private int $startingStep = 0;
    private ?int $max;
    private int $stepWidth;
    private float $percent = 0.0;


 part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget;

use Symfony\Component\Tui\Event\QuitEvent;

/**
 * Trait for widgets that support a quit action.
 *
 * Dispatches a {@see QuitEvent} when the quit key is pressed.
 * If no listener is registered for QuitEvent (neither globally on the
 * Tui nor locally on th

   * @param callable(QuitEvent): void $callback
     *
     * @return $this
     */
    public function onQuit(callable $callback): static
    {
        return $this->on(QuitEvent::class, $callback);
    }

    /**
     * Dispatch the quit event.
     *

e widget), the default behavior is to stop the TUI.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
trait QuitableTrait
{
    /**
     * Register a listener for the quit event on this widget.
     *


ge.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget;

use Symfony\Component\Tui\Exception\InvalidArgumentException;

/**
 * Shared scheduling lifecycle for runtime objects driven by WidgetContext ticks.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
trait ScheduledTickTrait
{
    private ?string $scheduledTi

cheduledTick(float $intervalSeconds): void
    {
        if ($intervalSeconds <= 0.0) {
            throw new InvalidArgumentException(\sprintf('Interval must be greater than 0, got %d.', $intervalSeconds));
        }

        if (null !== $this->scheduled

ckId = null;
    private ?float $scheduledTickInterval = null;

    abstract protected function resolveScheduledTickContext(): ?WidgetContext;

    abstract protected function onScheduledTick(): void;

    private function startS


;
use Symfony\Component\Tui\Event\SelectionChangeEvent;
use Symfony\Component\Tui\Input\Key;
use Symfony\Component\Tui\Input\Keybindings;
use Symfony\Component\Tui\Render\RenderContext;

/**
 * Interactive selection list with keyboard navigation.
 *
 * Item `label`, `description`, and `value` are rendered to the terminal
 * as-is and are not sanitized. See {@see TextWidget} for the raw-passthrough
 * contract: never pass untrusted bytes here; sanitize upstream via
 * {@see Util\StringUtils::stripControlByte

<array{value: string, label: string, description?: string}> */
    private array $filteredItems;

    private int $selectedIndex = 0;
    private bool $selected = false;

    /**
     * @param array<array{value: string, label: string, description?: string}

s()}.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class SelectListWidget extends AbstractWidget implements FocusableInterface
{
    use FocusableTrait;
    use KeybindingsTrait;

    /** @var array


ge.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget;

use Symfony\Component\Tui\Exception\LogicException;
use Symfony\Component\Tui\Widget\Util\StringUtils;

/**
 * Represents a single item in a SettingsListWidget.
 *
 * The `$label` and `$description` constructor arguments are stored verbatim
 * and rendered as-is. Only `$currentValue`

mfony.com>
 *
 * @phpstan-type SubmenuFactory callable(string, callable(?string): void): (FocusableInterface&AbstractWidget)
 */
final class SettingItem
{
    private string $currentValue;

    /**
     * @param list<string>        $values  Predefined valu

 is sanitized. See {@see TextWidget}
 * for the raw-passthrough contract: sanitize untrusted label/description
 * upstream via {@see StringUtils::stripControlBytes()}.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@sy


;
use Symfony\Component\Tui\Event\SettingChangeEvent;
use Symfony\Component\Tui\Input\Key;
use Symfony\Component\Tui\Input\Keybindings;
use Symfony\Component\Tui\Render\RenderContext;

/**
 * Settings panel with value cycling and submenus.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class SettingsListWidget extends AbstractWidget implements FocusableInterface, ParentInterface
{
    use FocusableTrait;
    use KeybindingsTrait;

    private int $selectedIndex = 0;

    // Subm

[];

    /**
     * @param list<SettingItem> $items
     */
    public function __construct(
        private array $items,
        private int $maxVisible = 10,
        ?Keybindings $keybindings = null,
    ) {
        if (null !== $keybindings) {

enu state
    private (FocusableInterface&AbstractWidget)|null $activeSubmenu = null;

    /** @var list<array{string, callable}> Listeners to remove from the global dispatcher on cleanup */
    private array $submenuListeners =


buted with this source code.
 */

namespace Symfony\Component\Tui\Widget;

use Symfony\Component\Tui\Ansi\AnsiUtils;
use Symfony\Component\Tui\Ansi\TextWrapper;
use Symfony\Component\Tui\Render\RenderContext;
use Symfony\Component\Tui\Widget\Figlet\FigletRenderer;

/**
 * Text component - displays text with word wrapping or truncation.
 *
 * When truncate is false (default), text wraps to multiple lines.
 * When truncate is true, each line is truncated to fit the width with an ellipsis.
 *
 * When a FIGlet

rules, CSS classes, or Tailwind utility classes:
 *
 *     // Stylesheet rule
 *     $stylesheet->addRule('.title', new Style(font: 'big'));
 *
 *     // Tailwind utility class
 *     $widget->addStyleClass('font-big');
 *
 *     // Template
 *     <text c

font is set via the Style system, the text is rendered as large
 * ASCII art instead. Bundled fonts: big, small, slant, standard, mini.
 * Custom fonts can be registered via the FontRegistry.
 *
 * Font can be set via stylesheet


hp

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget;

/**
 * Interface for widgets that can expand to fill available vertical space.
 *
 * When a widget implements this interface and vertical expansion is enabled,
 * it will expand to use available vertical space in its parent container.

rface VerticallyExpandableInterface
{
    /**
     * Set whether the widget should expand to fill available height.
     *
     * @return $this
     */
    public function expandVertically(bool $expand): static;

    /**
     * Check if the widget should e

* In vertical layouts, multiple expanded siblings share the space equally.
 * In horizontal layouts, all children receive the full available height.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
inte


ony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget;

/**
 * Interface for widgets that can contain and mutate child widgets.
 *
 * Extends ParentInterface with mutation methods (add, remove, clear).
 * Use ParentInterface when you only need read-only tree traversal.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface WidgetContainerInterface ex

     */
    public function add(AbstractWidget $widget): static;

    /**
     * @return $this
     */
    public function remove(AbstractWidget $widget): static;

    /**
     * Remove all child widgets.
     *
     * @return $this
     */
    public fun

tends ParentInterface
{
    /**
     * @return $this


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget;

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Tui\Event\AbstractEvent;
use Symfony\Component\Tui\Focus\FocusManager;
use Symfony\Component\Tui\Input\Keybindings;
use Symfony\Component\Tui\Render\RenderContext;
use Symfony\Component\Tui\Render\Renderer;
use Symfony\Component\Tui\Style\Style;
use

final class WidgetContext
{
    /** @var array<string, string> */
    private array $tickIds = [];

    /**
     * @internal constructed by the framework; user code receives the context via {@see AbstractWidget::onAttach()}
     */
    public function __co

 Symfony\Component\Tui\Terminal\TerminalInterface;
use Symfony\Component\Tui\Tui;

/**
 * Runtime context provided to widgets when attached to the tree.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */


ge.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget;

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Tui\Focus\FocusManager;
use Symfony\Component\Tui\Input\Keybindings;
use Symfony\Component\Tui\Render\Renderer;
use Symfony\Component\Tui\Terminal\TerminalInterface;
use Symfony\Component\Tui\Tui;

$terminal;
    private ?AbstractWidget $root = null;

    public function __construct(
        Tui $tui,
        Keybindings $keybindings,
        FocusManager $focusManager,
        Renderer $renderer,
        TerminalInterface $terminal,
        EventDis

/**
 * Internal widget tree manager.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class WidgetTree
{
    private WidgetContext $context;
    private readonly TerminalInterface


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget\Util;

/**
 * Emacs-style kill ring for cut/paste operations.
 *
 * Consecutive kill operations are appended to the same entry.
 * Yank/yank-pop cycles through the ring.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class KillRing
{
    /** @var string[] */
    private array $entries = [];
    private ?str

= 50,
    ) {
    }

    /**
     * Add text to the kill ring.
     *
     * If the last action was also a kill, the text is appended/prepended
     * to the most recent entry (consecutive kills accumulate).
     */
    public function add(string $text, bo

ing $lastAction = null;

    /**
     * @var array{start_line: int, start_col: int, end_line: int, end_col: int}|null
     */
    private ?array $lastYank = null;

    public function __construct(
        private int $maxEntries


buted with this source code.
 */

namespace Symfony\Component\Tui\Widget\Util;

/**
 * Grapheme-aware single-line text buffer with cursor position.
 *
 * Encapsulates text content and a byte-offset cursor. Every editing method
 * mutates internal state and returns whether the operation had an effect,
 * letting the caller decide whether to invalidate, push undo snapshots, etc.
 *
 * Both InputWidget (single-line) and EditorWidget (multi-line, per-line)
 * delegate grapheme-level operations here.
 *
 * @expe

     $this->text = $text;
        $this->cursor = max(0, min($cursor, \strlen($text)));
    }

    public function getText(): string
    {
        return $this->text;
    }

    public function getCursor(): int
    {
        return $this->cursor;
    }

rimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class Line
{
    private string $text;
    private int $cursor;

    public function __construct(string $text = '', int $cursor = 0)
    {


ge.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget\Util;

/**
 * General-purpose string utilities for terminal input handling.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class StringUtils
{
    /**
     * Check if the input data contains control characters (C0 controls + DEL).

   * in multi-byte characters like emojis (e.g. 😀 = \xF0\x9F\x98\x80).
     */
    public static function hasControlChars(string $data): bool
    {
        for ($i = 0; $i < \strlen($data); ++$i) {
            $code = \ord($data[$i]);
            if ($code

 *
     * Only checks for ASCII control characters (0x00-0x1F and 0x7F).
     * Does NOT check for C1 control characters (U+0080-U+009F) at the byte
     * level, because bytes 0x80-0x9F are valid UTF-8 continuation bytes used


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget\Util;

use Symfony\Component\Tui\Ansi\AnsiUtils;

/**
 * Grapheme-aware word navigation for text editing.
 *
 * Provides cursor movement logic: skip whitespace, then skip a punctuation run
 * or a word run. Both InputWidget and EditorWidget delegate to this class for
 * within-line word navigation.
 *
 * @experimental
 *
 * @internal

   * Algorithm: skip trailing whitespace, then skip a punctuation run or a
     * word-character run.
     */
    public static function skipWordBackward(string $text, int $cursor): int
    {
        if (0 === $cursor) {
            return 0;
        }

*
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class WordNavigator
{
    /**
     * Returns the new cursor position after moving one word backward within
     * the given text. The cursor is a byte offset.
     *


24;

    /** @var string[] */
    private array $lines = [''];
    private int $cursorLine = 0;
    private int $cursorCol = 0;

    private KillRing $killRing;

    // Undo/Redo
    /** @var array<int, array{lines: string[], cursor_line: int, cursor_col: int}> */
    private array $undoStack = [];
    /** @var array<int, array{lines: string[], cursor_line: int, cursor_col: int}> */
    private array $redoStack = [];
    private int $undoStackBytes = 0;

    // Character jump mode
    private ?string $jumpM

KillRing $killRing = null)
    {
        $this->killRing = $killRing ?? new KillRing();
    }

    // --- Accessors ---

    /**
     * @return string[]
     */
    public function getLines(): array
    {
        return $this->lines;
    }

    public func

ode = null; // 'forward' or 'backward'

    // Paste handling
    private int $pasteCount = 0;
    /** @var array<array{marker: string, content: string}> */
    private array $pasteMarkers = [];

    public function __construct(?


buted with this source code.
 */

namespace Symfony\Component\Tui\Widget\Editor;

use Symfony\Component\Tui\Ansi\AnsiUtils;
use Symfony\Component\Tui\Ansi\TextWrapper;
use Symfony\Component\Tui\Style\CursorShape;
use Symfony\Component\Tui\Style\Style;

/**
 * Renders editor content lines with cursor and word-wrap.
 *
 * This is a stateless helper: all state (document content, cursor position,
 * scroll offset) is passed in from the EditorWidget.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@sym

                   $lines              Document lines
     * @param array{scroll_offset: int, visible_line_count: int, lines_above: int, lines_below: int} $viewport           Viewport parameters
     * @param int

fony.com>
 *
 * @internal
 */
final class EditorRenderer
{
    /**
     * Render the full editor output: borders + content lines + padding.
     *
     * @param string[]


buted with this source code.
 */

namespace Symfony\Component\Tui\Widget\Editor;

use Symfony\Component\Tui\Ansi\TextWrapper;

/**
 * Manages scroll offset and viewport calculations for the editor.
 *
 * Owns the scroll offset and computes which logical lines are visible
 * in the terminal viewport, accounting for word-wrap. Also handles
 * mouse cursor placement (display-row → logical line+col mapping).
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @internal
 */
final class

/**
     * Scroll by a full page.
     *
     * @param string[] $lines      Document lines
     * @param int      $direction  1 for down, -1 for up
     * @param int      $pageSize   Number of lines per page
     * @param int      $cursorLine Current curso

EditorViewport
{
    private int $scrollOffset = 0;

    public function getScrollOffset(): int
    {
        return $this->scrollOffset;
    }

    public function reset(): void
    {
        $this->scrollOffset = 0;
    }


buted with this source code.
 */

namespace Symfony\Component\Tui\Widget\Figlet;

use Symfony\Component\Tui\Exception\InvalidArgumentException;

/**
 * Parses and represents a FIGlet font (.flf file).
 *
 * The FIGlet font format stores ASCII art representations of characters.
 * Each character is defined as a fixed number of lines (the font height).
 * Lines are terminated by the font's "end mark" character (@), and the
 * last line of each character uses a double end mark (@@).
 *
 * The "hardblank" chara

 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class FigletFont
{
    private const int MAX_DECOMPRESSED_BYTES = 4 * 1024 * 1024;

    /** @var array<int, string[]> codepoint → array of lines */
    private array $characters = [];

    /**

cter (typically $) is rendered as a visible space
 * that prevents smushing; it's replaced with a regular space on output.
 *
 * @see https://github.com/cmatsuoka/figlet/blob/master/figfont.txt
 *
 * @experimental
 *
 * @internal


class TextWrapper
{
    /**
     * Wrap a single line into chunks with position tracking.
     *
     * Unlike wrapTextWithAnsi(), each chunk carries its start/end position
     * in the original line, allowing callers to map cursor positions
     * accurately through word-wrap boundaries.
     *
     * The chunk text may include trailing whitespace; callers that need
     * trimmed display text can rtrim() themselves.
     *
     * @param string $line  A single line of text (no newlines)
     * @param int

'' === $line) {
            return [['text' => '', 'start_index' => 0, 'end_index' => 0]];
        }

        if ($width <= 0) {
            return [['text' => $line, 'start_index' => 0, 'end_index' => \strlen($line)]];
        }

        $lineWidth = Ansi

   $width Maximum visible width per chunk
     *
     * @return list<array{text: string, start_index: int, end_index: int}>
     */
    public static function wrapLineIntoChunks(string $line, int $width): array
    {
        if (


hp

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Exception;

/**
 * Exception thrown when rendering fails.
 *
 * Typically thrown when a component renders a line that exceeds the terminal width.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class RenderException e

 {
        parent::__construct($message);
    }

    public function getLineNumber(): int
    {
        return $this->lineNumber;
    }

    public function getLineWidth(): int
    {
        return $this->lineWidth;
    }

    public function getTerminalWi

xtends RuntimeException
{
    public function __construct(
        string $message,
        private readonly int $lineNumber = 0,
        private readonly int $lineWidth = 0,
        private readonly int $terminalWidth = 0,
    )


 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class CellBuffer
{
    // Attribute bitmask constants
    public const ATTR_BOLD = 1;
    public const ATTR_DIM = 2;
    public const ATTR_ITALIC = 4;
    public const ATTR_UNDERLINE = 8;
    public const ATTR_BLINK = 16;
    public const ATTR_REVERSE = 32;
    public const ATTR_STRIKETHROUGH = 64;

    /**
     * Flat arrays indexed by (row * width + col).
     *
     * @var string[] Character/grapheme at each cell
     */
    private array $ch

array $fg;

    /** @var string[] Background color code (e.g., "48;2;30;30;46") or "" for default */
    private array $bg;

    /** @var int[] Attribute bitmask (bold|dim|italic|underline|blink|reverse|strikethrough) */
    private array $attrs;

    /* R

ars;

    /** @var int[] Display width of each cell (1 for normal, 2 for CJK, 0 for continuation) */
    private array $widths;

    /** @var string[] Foreground color code (e.g., "38;2;255;0;0") or "" for default */
    private


 part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Render;

use Symfony\Component\Tui\Ansi\AnsiUtils;

/**
 * Composites multiple layers into a single set of ANSI-formatted lines.
 *
 * Layers are applied in order: layer 0 is the base (typically opaque),
 * subsequent layers are painted on top. Transparent layers let

 Usage:
 *
 *     $lines = Compositor::composite(
 *         new Layer($backgroundLines),
 *         new Layer($foregroundLines, transparent: true),
 *     );
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final

 the
 * content below show through where no explicit background is set.
 *
 * The canvas dimensions are derived from the first (base) layer:
 * height is the number of lines, width is the visible width of the
 * first line.
 *
 *


 part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Render;

/**
 * A compositing layer: content lines at a position, optionally transparent.
 *
 * When transparent, cells with no explicit background preserve the
 * background from the layer below. Fully unstyled spaces are completely
 * transparent (the entire cell b

$row         Vertical offset in the composite
     * @param int      $col         Horizontal offset in the composite
     * @param bool     $transparent Whether cells with default background inherit from layers below
     * @param int|null $width       Exp

elow shows through).
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class Layer
{
    /**
     * @param string[] $lines       ANSI-formatted content lines
     * @param int


ge.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Render;

use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Widget\Figlet\FontRegistry;

/**
 * Context passed to widgets during rendering.
 *
 * Contains the available dimensions for rendering in terminal character cells,
 * the resolved style for the widget, and the font regis

* - Terminals measure size in character cells (columns × rows), not pixels
 * - The TerminalInterface uses `getColumns()` and `getRows()`
 * - Standard tools like `stty`, `tput`, and env vars `COLUMNS`/`LINES` use this terminology
 *
 * @experimental
 *
 *

try for FIGlet rendering.
 *
 * This is an immutable value object - use with*() methods to create modified copies.
 *
 * ## Terminology: columns and rows
 *
 * This class uses `columns` and `rows` to match terminal conventions:


/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Render;

/**
 * Capability interface for managing the render lifecycle.
 *
 * Used by internal collaborators (FocusManager, MouseCoordinator)
 * that need to trigger or flush a render pass without depending
 * on the full Tui API.
 *
 * @experiment

d do a full re-render
     */
    public function requestRender(bool $force = false): void;

    /**
     * Flush any pending render immediately.
     *
     * Unlike requestRender() which defers to the next tick, this
     * synchronously renders the curr

al
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface RenderRequestorInterface
{
    /**
     * Request a render on the next tick.
     *
     * @param bool $force If true, clear all cached state an


 part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Serializer\Encoder;

/**
 * Encodes JSON data.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class JsonEncoder implements EncoderInterface, DecoderInterface
{
    public const FORMAT = 'json';

    protected JsonEncode $encodingImpl;
    protected JsonDecode $dec

  {
        $this->defaultContext = array_merge($this->defaultContext, $defaultContext);
        $this->encodingImpl = $encodingImpl ?? new JsonEncode($this->defaultContext);
        $this->decodingImpl = $decodingImpl ?? new JsonDecode($this->defaultConte

odingImpl;

    private array $defaultContext = [
        JsonDecode::ASSOCIATIVE => true,
    ];

    public function __construct(?JsonEncode $encodingImpl = null, ?JsonDecode $decodingImpl = null, array $defaultContext = [])


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Serializer\Encoder;

use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Yaml\Dumper;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Yaml;

/**
 * Encodes YAML data.
 *
 * @autho

'preserve_empty_objects';

    /**
     * Override the amount of spaces to use for indentation of nested nodes.
     *
     * This option only works in the constructor, not in calls to `encode`.
     */
    public const YAML_INDENTATION = 'yaml_indentation

r Kévin Dunglas <dunglas@gmail.com>
 */
class YamlEncoder implements EncoderInterface, DecoderInterface
{
    public const FORMAT = 'yaml';
    private const ALTERNATIVE_FORMAT = 'yml';

    public const PRESERVE_EMPTY_OBJECTS =


mpiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Serializer\Debug\TraceableEncoder;
use Symfony\Component\Serializer\Debug\TraceableNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Nor

zer" as
 * encoders and normalizers to the "serializer" service.
 *
 * @author Javier Lopez <f12loalf@gmail.com>
 * @author Robin Chalas <robin.chalas@gmail.com>
 */
class SerializerPass implements CompilerPassInterface
{
    use PriorityTaggedServiceTrait

malizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\SerializerInterface;

/**
 * Adds all services with the tags "serializer.encoder" and "serializer.normali


Dispatcher\EventDispatcherInterface;
use Symfony\Component\Tui\Event\AbstractEvent;
use Symfony\Component\Tui\Event\InputEvent;
use Symfony\Component\Tui\Event\TickEvent;
use Symfony\Component\Tui\Exception\InvalidArgumentException;
use Symfony\Component\Tui\Focus\FocusManager;
use Symfony\Component\Tui\Input\Keybindings;
use Symfony\Component\Tui\Loop\AdaptativeTicker;
use Symfony\Component\Tui\Loop\TickRuntimeInterface;
use Symfony\Component\Tui\Loop\TickScheduler;
use Symfony\Component\Tui\Render\Rendere

nal\TerminalInterface;
use Symfony\Component\Tui\Widget\AbstractWidget;
use Symfony\Component\Tui\Widget\ContainerWidget;
use Symfony\Component\Tui\Widget\Figlet\FontRegistry;
use Symfony\Component\Tui\Widget\FocusableInterface;
use Symfony\Component\Tui\W

r;
use Symfony\Component\Tui\Render\RenderRequestorInterface;
use Symfony\Component\Tui\Render\ScreenWriter;
use Symfony\Component\Tui\Style\StyleSheet;
use Symfony\Component\Tui\Terminal\Terminal;
use Symfony\Component\Tui\Termi


buted with this source code.
 */

namespace Symfony\Component\Tui\Style;

use Symfony\Component\Tui\Exception\InvalidArgumentException;

/**
 * Represents border values like CSS border.
 *
 * Supports 1, 2, 3, or 4 values:
 * - 1 value:  all sides
 * - 2 values: top/bottom, left/right
 * - 3 values: top, left/right, bottom
 * - 4 values: top, right, bottom, left
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class Border
{
    private const DEFAULT_PATTERN = BorderPattern:

unction __construct(
        int $top,
        int $right,
        int $bottom,
        int $left,
        BorderPattern|string|null $pattern = null,
        Color|string|int|null $color = null,
    ) {
        $this->top = max(0, $top);
        $this->rig

:NORMAL;

    public readonly int $top;
    public readonly int $right;
    public readonly int $bottom;
    public readonly int $left;

    public readonly BorderPattern $pattern;
    public readonly ?Color $color;

    public f


?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Style;

/**
 * Terminal cursor shape, mapped to DECSCUSR escape sequences.
 *
 * These values correspond to the parameter N in `ESC [ N SP q`
 * (DECSCUSR: Set Cursor Style). Odd values produce a blinking
 * cursor; even values produce a stead

the blinking
 * variants so the terminal provides native cursor animation at
 * zero CPU cost.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
enum CursorShape: int
{
    case Block = 1;
    case Underline = 3;
    case Bar = 5;


ge.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Style;

use Symfony\Component\Tui\Exception\InvalidArgumentException;

/**
 * Represents padding values like CSS padding.
 *
 * Supports 1, 2, 3, or 4 values:
 * - 1 value:  all sides
 * - 2 values: top/bottom, left/right
 * - 3 values: top, left/right, bottom
 * - 4 values: top, right, botto

t;

    public function __construct(int $top, int $right, int $bottom, int $left)
    {
        $this->top = max(0, $top);
        $this->right = max(0, $right);
        $this->bottom = max(0, $bottom);
        $this->left = max(0, $left);
    }

    /**

m, left
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class Padding
{
    public readonly int $top;
    public readonly int $right;
    public readonly int $bottom;
    public readonly int $lef


l` means "not set" - the value will be inherited from parent styles during merge
 * - An explicit value (even if zero/false) means "explicitly set" - overrides inheritance
 *
 * This applies to:
 * - `padding` - null (inherit) vs Padding instance (explicit, even if all zeros)
 * - `border` - null (inherit) vs Border instance (explicit, even if all zeros)
 * - `background` - null (inherit) vs Color instance (explicit color)
 * - `color` - null (inherit) vs Color instance (explicit color)
 * - `bold`, `dim`,

roperties will inherit from parent rules
 *     $style = new Style()->withColor('red');
 *     $style->getPadding(); // null - will inherit
 *     $style->getBold();    // null - will inherit
 *
 *     // Explicit bold=false to override a parent's bold=tru

`italic`, `strikethrough`, `underline`, `reverse` - null (inherit) vs bool (explicit true/false)
 * - `hidden` - null (inherit) vs bool (true = hidden, false = explicitly visible)
 *
 * Examples:
 *
 *     // Color only - other p


e:
 * - FQCN: 'Symfony\Component\Tui\Widget\Input' or Input::class
 * - FQCN with state: 'Symfony\Component\Tui\Widget\Input:focus'
 * - CSS class: '.sidebar'
 * - CSS class with state: '.sidebar:focus'
 * - Standalone pseudo-class: ':root' (matches the root widget)
 * - Universal: '*' (matches all widgets)
 * - Sub-element (pseudo-element): SelectList::class.'::selected'
 * - Sub-element with state: SelectList::class.'::selected:focus'
 * - Class sub-element: '.my-list::selected'
 * - Class sub-element wit

ass)
 * 3. CSS class selectors (e.g., '.header')
 * 4. State selectors (e.g., ':root', Input::class.':focus')
 * 5. Instance style (widget's own setStyle())
 *
 * All style properties use `null` to mean "inherit from earlier rules":
 *
 *     // This rule

h state: '.my-list::selected:focus'
 *
 * ## Style Inheritance
 *
 * When resolving styles, rules are applied in this order (later rules override earlier):
 * 1. Universal selector ('*')
 * 2. Widget FQCN selector (e.g., Text::cl


buted with this source code.
 */

namespace Symfony\Component\Tui\Focus;

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Tui\Event\FocusEvent;
use Symfony\Component\Tui\Input\Key;
use Symfony\Component\Tui\Input\Keybindings;
use Symfony\Component\Tui\Render\RenderRequestorInterface;
use Symfony\Component\Tui\Widget\AbstractWidget;
use Symfony\Component\Tui\Widget\FocusableInterface;

/**
 * Owns the focused-widget state and handles focus navigation.
 *
 * Default bindi

vious' => ['shift+f6'],
    ];

    private ?AbstractWidget $focused = null;

    /** @var array<int, FocusableInterface&AbstractWidget> */
    private array $focusables = [];

    private Keybindings $keybindings;

    /**
     * @internal constructed by

ngs: F6 (next) and Shift+F6 (previous).
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class FocusManager
{
    private const DEFAULT_BINDINGS = [
        'focus_next' => [Key::F6],
        'focus_pre


mfony\Component\Tui\Style\VerticalAlign;
use Symfony\Component\Tui\Widget\AbstractWidget;
use Symfony\Component\Tui\Widget\Figlet\FontRegistry;
use Symfony\Component\Tui\Widget\ParentInterface;
use Symfony\Component\Tui\Widget\VerticallyExpandableInterface;

/**
 * Lays out children vertically or horizontally with gap, fill, and alignment.
 *
 * The layout engine distributes available space among children, handles
 * fill-expanding children, and applies horizontal/vertical alignment.
 *
 * @experimental
 *

racker $positionTracker,
        private readonly FontRegistry $fontRegistry,
    ) {
    }

    /**
     * Layout children based on direction.
     *
     * @param AbstractWidget[] $children
     *
     * @return string[]
     */
    public function layou

 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class LayoutEngine
{
    public function __construct(
        private readonly WidgetRendererInterface $widgetRenderer,
        private readonly PositionT


gn;
use Symfony\Component\Tui\Style\DefaultStyleSheet;
use Symfony\Component\Tui\Style\Direction;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Style\StyleSheet;
use Symfony\Component\Tui\Widget\AbstractWidget;
use Symfony\Component\Tui\Widget\ContainerWidget;
use Symfony\Component\Tui\Widget\Figlet\FontRegistry;
use Symfony\Component\Tui\Widget\ParentInterface;

/**
 * Renders the widget tree with style resolution, layout, and chrome.
 *
 * The Renderer:
 * 1. Resolves styles through cas

dget content
 *
 * All widget types are rendered through the Renderer: containers via
 * renderContainer(), and leaf widgets by delegating to widget->render().
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
fina

cade (* → FQCN → CSS class → state → instance)
 * 2. Computes layout (vertical/horizontal with gap and fill children)
 * 3. Calls widget->render() with enriched context
 * 4. Applies chrome (padding, border, background) around wi


 EncoderInterface, DecoderInterface, NormalizationAwareInterface, SerializerAwareInterface
{
    use SerializerAwareTrait;

    public const FORMAT = 'xml';

    public const AS_COLLECTION = 'as_collection';

    /**
     * An array of ignored XML node types while decoding, each one of the DOM Predefined XML_* constants.
     */
    public const DECODER_IGNORED_NODE_TYPES = 'decoder_ignored_node_types';

    /**
     * An array of ignored XML node types while encoding, each one of the DOM Predefined XML_* c

_* constants for loading XML documents.
     */
    public const LOAD_OPTIONS = 'load_options';

    /**
     * A bit field of LIBXML_* constants for saving XML documents.
     */
    public const SAVE_OPTIONS = 'save_options';

    public const REMOVE_EMP

onstants.
     */
    public const ENCODER_IGNORED_NODE_TYPES = 'encoder_ignored_node_types';
    public const ENCODING = 'xml_encoding';
    public const FORMAT_OUTPUT = 'xml_format_output';

    /**
     * A bit field of LIBXML


ge.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Serializer\DependencyInjection;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Serializer\Exception\MappingException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class Attrib

}

        $resolve = $container->getParameterBag()->resolveValue(...);
        $taggedClasses = [];
        foreach ($container->getDefinitions() as $id => $definition) {
            if (!$definition->hasTag('serializer.attribute_metadata')) {

uteMetadataPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        if (!$container->hasDefinition('serializer.mapping.attribute_loader')) {
            return;


rmats:
 * - Basic 16 ANSI colors (named: 'black', 'red', 'green', etc.)
 * - 256-color palette (integers 0-255)
 * - True color RGB (hex strings like '#ff5500' or '#f50')
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class Color
{
    // Basic 16 ANSI color codes (foreground)
    private const BASIC_COLORS = [
        'black' => 30,
        'red' => 31,
        'green' => 32,
        'yellow' => 33,
        'blue' => 34,
        'magenta' => 35,
        'cyan' => 36,

   'bright_cyan' => 96,
        'bright_white' => 97,
        // Aliases
        'gray' => 90,
        'grey' => 90,
    ];

    /** Standard RGB values for named ANSI colors (xterm defaults). */
    private const NAMED_RGB = [
        'black' => [0, 0, 0]

    'white' => 37,
        'default' => 39,
        'bright_black' => 90,
        'bright_red' => 91,
        'bright_green' => 92,
        'bright_yellow' => 93,
        'bright_blue' => 94,
        'bright_magenta' => 95,


asses are parsed from widget style classes and dynamically
 * converted to Style objects. They coexist with regular CSS-like rules
 * and take precedence over them in the cascade (they are "immutable").
 *
 * ## Supported utility classes
 *
 * ### Padding
 *     p-{n}               All sides
 *     px-{n}              Left and right
 *     py-{n}              Top and bottom
 *     pt-{n} pr-{n} pb-{n} pl-{n}   Individual sides
 *
 * ### Border
 *     border               All sides, width 1
 *     border-{n}

 border
 *     border-{pattern}     Pattern: normal, rounded, double, tall, wide, tall-medium, wide-medium, tall-large, wide-large
 *     border-{color}       Color: {family}-{shade}, [#hex], or palette index
 *
 * ### Background color
 *     bg-{family}-{

           All sides, width n
 *     border-t border-r border-b border-l             Individual side, width 1
 *     border-t-{n} border-r-{n} border-b-{n} border-l-{n}  Individual side, width n
 *     border-none          Remove


buted with this source code.
 */

namespace Symfony\Component\Tui\Ansi;

use Symfony\Component\Tui\Style\Color;
use Symfony\Component\Tui\Terminal\ScreenBuffer;

/**
 * Renders a ScreenBuffer to HTML with inline CSS styles.
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class ScreenBufferHtmlRenderer
{
    private Color $defaultForeground;
    private Color $defaultBackground;

    public function __construct(
        ?Color $defaultForeground = null,
        ?Color $defau

ffer to HTML with inline styles.
     */
    public function convert(ScreenBuffer $screen): string
    {
        $cells = $screen->getCells();
        $height = $screen->getHeight();
        $result = '';
        $lastNonEmptyEnd = 0;

        for ($row =

ltBackground = null,
    ) {
        $this->defaultForeground = $defaultForeground ?? Color::hex('#d4d4d4');
        $this->defaultBackground = $defaultBackground ?? Color::hex('#1e1e1e');
    }

    /**
     * Convert a ScreenBu


buted with this source code.
 */

namespace Symfony\Component\Tui\Render;

use Symfony\Component\Tui\Ansi\AnsiUtils;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Style\TextAlign;
use Symfony\Component\Tui\Widget\AbstractWidget;

/**
 * Applies chrome (padding, border, background) around widget content.
 *
 * Chrome is the visual frame around a widget's rendered lines:
 * padding adds space inside, borders draw a box, and background colors
 * fill the area. The result is cached for perfor

 ) {
    }

    /**
     * Apply chrome (padding, border, background) to rendered lines.
     *
     * @param string[] $lines
     *
     * @return string[]
     */
    public function apply(array $lines, int $width, Style $style, AbstractWidget $widget):

mance.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class ChromeApplier
{
    public function __construct(
        private readonly WidgetRendererInterface $widgetRenderer,


<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Serializer\Extractor;

use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;

/**
 * @author David Maicher <mail@dmaicher.de>
 */
final class ObjectPropertyListExtractor implements ObjectPropertyListExtractorInterface
{
    private \

objectClassResolver ?? 'get_class')(...);
    }

    public function getProperties(object $object, array $context = []): ?array
    {
        $class = ($this->objectClassResolver)($object);

        return $this->propertyListExtractor->getProperties($class

Closure $objectClassResolver;

    public function __construct(
        private PropertyListExtractorInterface $propertyListExtractor,
        ?callable $objectClassResolver = null,
    ) {
        $this->objectClassResolver = ($


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Style;

use Symfony\Component\Tui\Widget\CancellableLoaderWidget;
use Symfony\Component\Tui\Widget\EditorWidget;
use Symfony\Component\Tui\Widget\InputWidget;
use Symfony\Component\Tui\Widget\LoaderWidget;
use Symfony\Component\Tui\Widget\MarkdownWidget;
use Symfony\Component\Tui\Widget\SelectListWidget;
use Symfony\Component\Tui\Widget\Setti

.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class DefaultStyleSheet
{
    public static function create(): StyleSheet
    {
        return new StyleSheet([
            // Heading classes (used by <h1>

ngsListWidget;

/**
 * Default TUI stylesheet with base styling rules.
 *
 * Provides sensible defaults for all core widget sub-elements.
 * These can be overridden by application or theme stylesheets
 * via the cascade mechanism


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Render;

use Symfony\Component\Tui\Widget\AbstractWidget;

/**
 * Tracks absolute positions of rendered widgets on screen.
 *
 * Maintains a stack of absolute [row, col] offsets for the current rendering
 * context and records each widget's final position as a WidgetRect.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fab

.
     *
     * @var list<array{int, int}>
     */
    private array $positionStack = [];

    public function __construct()
    {
        $this->widgetPositions = new \WeakMap();
    }

    /**
     * Reset the position stack for a new render pass.
     *

ien@symfony.com>
 */
final class PositionTracker
{
    /** @var \WeakMap<AbstractWidget, WidgetRect> */
    private \WeakMap $widgetPositions;

    /**
     * Stack of absolute [row, col] offsets for the current rendering context


 The 3x3 strategy matrix is used by renderers to decide how to swap colors
 * for block-style borders:
 * - 0: border color on inner background (standard border rendering)
 * - 1: border color on outer background (blend with outer background)
 * - 2: reverse video border/outer — filled part shows outer bg, empty part shows border color
 * - 3: reverse video border/inner — filled part shows inner bg, empty part shows border color
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
fin

;
    public const TALL_MEDIUM = 'tall-medium';
    public const WIDE_MEDIUM = 'wide-medium';
    public const TALL_LARGE = 'tall-large';
    public const WIDE_LARGE = 'wide-large';

    /**
     * @param array<int, array<int, string>> $chars
     * @param

al class BorderPattern
{
    public const NONE = 'none';
    public const NORMAL = 'normal';
    public const ROUNDED = 'rounded';
    public const DOUBLE = 'double';
    public const TALL = 'tall';
    public const WIDE = 'wide'


terminal
 * - Managing cursor position for differential rendering
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class ScreenWriter
{
    private const PRINTABLE_ASCII = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';

    /** @var string[] */
    private array $previousLines = [];
    private int $previousWidth = 0;
    private int $cursorRow = 0;
    private int $hardwareCursorRow = 0;
    private int $

    private ?array $previousCursorPos = null;

    public function __construct(
        private readonly TerminalInterface $terminal,
    ) {
    }

    public function setShowHardwareCursor(bool $enabled): void
    {
        if ($this->showHardwareCursor

maxLinesRendered = 0;
    private bool $showHardwareCursor = true;
    private int $scrollOffset = 0;

    /** @var string[] */
    private array $previousRawLines = [];

    /** @var array{row: int, col: int, shape: int}|null */


pe, positions the hardware cursor, and
     * sets the cursor style via `ESC [ N SP q`.
     *
     * @see cursorMarker()
     */
    public const CURSOR_MARKER_PREFIX = "\x1b_pi:c;";

    /**
     * Full SGR reset and OSC 8 reset sequence.
     */
    public const SEGMENT_RESET = "\x1b[0m\x1b]8;;\x07";

    /**
     * Combined pattern matching all ECMA-48 escape sequences in a single regex.
     * Alternation order: CSI (most common), string sequences, nF, two-byte.
     */
    private const ALL_ESC_PATTER

RAM_CHARS = '0123456789:;<=>?';

    /**
     * Character set for CSI intermediate bytes (0x20-0x2F).
     */
    private const CSI_INTERMEDIATE_CHARS = " !\"#\$%&'()*+,-./";

    /**
     * Create a cursor marker embedding the given shape.
     *
     * T


umentException;
use Symfony\Component\Tui\Style\Border;
use Symfony\Component\Tui\Style\BorderPattern;
use Symfony\Component\Tui\Style\Color;
use Symfony\Component\Tui\Style\Style;

class BorderTest extends TestCase
{
    public function testNegativeValuesClampedToZero()
    {
        $border = new Border(-5, -3, -1, -2);

        $this->assertSame(0, $border->top);
        $this->assertSame(0, $border->right);
        $this->assertSame(0, $border->bottom);
        $this->assertSame(0, $border->left);
    }

        $this->assertSame($top, $border->top);
        $this->assertSame($right, $border->right);
        $this->assertSame($bottom, $border->bottom);
        $this->assertSame($left, $border->left);
    }

    /**
     * @return iterable<string, array{l

    /**
     * @param list<int> $input
     */
    #[DataProvider('fromArrayProvider')]
    public function testFromArray(array $input, int $top, int $right, int $bottom, int $left)
    {
        $border = Border::from($input);


idget\TextWidget;

class ContainerTest extends TestCase
{
    public function testRender()
    {
        $container = new ContainerWidget();
        $container->add(new TextWidget('First'));
        $container->add(new TextWidget('Second'));

        $lines = $this->renderContainer($container);

        $content = implode("\n", $lines);
        $this->assertStringContainsString('First', $content);
        $this->assertStringContainsString('Second', $content);
    }

    public function testRenderEmpty()

tainerWidget();
        $container->setStyle(new Style(border: Border::all(1)));

        $lines = $this->renderContainer($container);

        $stripped = array_map(static fn ($l) => AnsiUtils::stripAnsiCodes($l), $lines);
        // Should have at least

 {
        $container = new ContainerWidget();
        $lines = $this->renderContainer($container);

        $this->assertSame([], $lines);
    }

    public function testRenderEmptyWithBorder()
    {
        $container = new Con


space

        $this->assertSame('Hell', $editor->getText());
    }

    #[DataProvider('deleteWordBackwardProvider')]
    public function testDeleteWordBackward(string $keySequence, string $description)
    {
        $editor = new EditorWidget();
        $editor->setText('hello world');

        $editor->handleInput("\x1b[F"); // End key
        $editor->handleInput($keySequence);

        $this->assertSame('hello ', $editor->getText());
    }

    /**
     * @return iterable<string, array{string, string}>

+Backspace (Kitty protocol)' => ["\x1b[127;3u", 'Kitty Alt+Backspace'];
    }

    public function testEnterCreatesNewLine()
    {
        $editor = new EditorWidget();
        $editor->setText('Hello');
        // Move cursor to end using Ctrl+E
        $

     */
    public static function deleteWordBackwardProvider(): iterable
    {
        yield 'Alt+Backspace (legacy: ESC + DEL)' => ["\x1b\x7f", 'Alt+Backspace'];
        yield 'Ctrl+W' => ["\x17", 'Ctrl+W'];
        yield 'Alt


buted with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Ansi\AnsiUtils;
use Symfony\Component\Tui\Exception\InvalidArgumentException;
use Symfony\Component\Tui\Render\RenderContext;
use Symfony\Component\Tui\Render\Renderer;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Style\StyleSheet;
use Symfony\Component\Tui\Style\TailwindStylesheet;
use Symfony\Component\

rWithFont(string $font, int $expectedLines)
    {
        $widget = new TextWidget('Hi');
        $lines = $this->renderWidgetWithFont($widget, $font, 80, 24);

        $this->assertCount($expectedLines, $lines);
    }

    /**
     * @return iterable<stri

Tui\Widget\ContainerWidget;
use Symfony\Component\Tui\Widget\Figlet\FontRegistry;
use Symfony\Component\Tui\Widget\TextWidget;

class FigletTest extends TestCase
{
    #[DataProvider('fontProvider')]
    public function testRende


 Symfony\Component\Tui\Event\CancelEvent;
use Symfony\Component\Tui\Event\ChangeEvent;
use Symfony\Component\Tui\Event\SubmitEvent;
use Symfony\Component\Tui\Render\RenderContext;
use Symfony\Component\Tui\Terminal\VirtualTerminal;
use Symfony\Component\Tui\Tui;
use Symfony\Component\Tui\Widget\InputWidget;

class InputTest extends TestCase
{
    public function testRenderEmpty()
    {
        $input = new InputWidget();
        $lines = $input->render(new RenderContext(40, 24));

        $this->assertCount

lo');
        $lines = $input->render(new RenderContext(40, 24));

        $this->assertCount(1, $lines);
        // Full width available for text, no prompt prefix
        $this->assertSame(40, AnsiUtils::visibleWidth($lines[0]));
        $stripped = Ansi

(1, $lines);
        $this->assertStringContainsString('>', $lines[0]);
    }

    public function testRenderWithEmptyPrompt()
    {
        $input = new InputWidget();
        $input->setPrompt('');
        $input->setValue('Hel


buted with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Exception\InvalidArgumentException;
use Symfony\Component\Tui\Render\RenderContext;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Style\StyleSheet;
use Symfony\Component\Tui\Terminal\VirtualTerminal;
use Symfony\Component\Tui\Tui;
use Symfony\Component\Tui\Widget\LoaderWidget;

class LoaderTest extends TestCase
{
    public function testRenderIncludesB

essage
        $this->assertSame('', $lines[0]);
        $this->assertCount(2, $lines);
    }

    public function testRenderIncludesSpinnerAndMessage()
    {
        $loader = new LoaderWidget(message: 'Working...');
        $loader->start();

        $li

lankLine()
    {
        $loader = new LoaderWidget(message: 'Test');
        $loader->start();

        $lines = $loader->render(new RenderContext(80, 24));

        // First line should be blank, second line has the spinner + m


buted with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Ansi\AnsiUtils;
use Symfony\Component\Tui\Render\RenderContext;
use Symfony\Component\Tui\Render\Renderer;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Terminal\VirtualTerminal;
use Symfony\Component\Tui\Tui;
use Symfony\Component\Tui\Widget\MarkdownWid

r(new RenderContext(40, 24));

        $this->assertSame([], $lines);
    }

    /**
     * @param list<string> $expectedSubstrings substrings to find in the joined output
     */
    #[DataProvider('markdownElementProvider')]
    public function testRende

get;

#[IgnoreDeprecations('League\\\\CommonMark\\\\Util\\\\ArrayCollection')]
class MarkdownTest extends TestCase
{
    public function testRenderEmpty()
    {
        $md = $this->createMarkdown('');
        $lines = $md->rende


ymfony\Component\Tui\Style\StyleSheet;
use Symfony\Component\Tui\Terminal\VirtualTerminal;
use Symfony\Component\Tui\Tui;
use Symfony\Component\Tui\Widget\ProgressBarWidget;

/**
 * @covers \Symfony\Component\Tui\Widget\ProgressBarWidget
 */
class ProgressBarTest extends TestCase
{
    public function testConstructionDeterminate()
    {
        $bar = new ProgressBarWidget(100);

        $this->assertSame(100, $bar->getMaxSteps());
        $this->assertSame(0, $bar->getProgress());
        $this->assertSame

tProgress());
    }

    public function testAdvance()
    {
        $bar = new ProgressBarWidget(100);
        $bar->advance(10);

        $this->assertSame(10, $bar->getProgress());
        $this->assertEqualsWithDelta(0.1, $bar->getProgressPercent(), 0.

(0.0, $bar->getProgressPercent());
    }

    public function testConstructionIndeterminate()
    {
        $bar = new ProgressBarWidget();

        $this->assertSame(0, $bar->getMaxSteps());
        $this->assertSame(0, $bar->ge


buted with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Ansi\AnsiUtils;
use Symfony\Component\Tui\Event\CancelEvent;
use Symfony\Component\Tui\Event\SelectEvent;
use Symfony\Component\Tui\Event\SelectionChangeEvent;
use Symfony\Component\Tui\Render\RenderContext;
use Symfony\Component\Tui\Terminal\VirtualTerminal;
use Symfony\Component\Tui\Tui;
use Symfony\Component\Tui\Widget\Se

tringContainsString('Option 1', $lines[0]);
    }

    public function testRenderShowsSelectedIndicator()
    {
        $list = $this->createTestList();
        $lines = $list->render(new RenderContext(80, 24));

        // First item should have arrow ind

lectListWidget;

class SelectListTest extends TestCase
{
    public function testRenderShowsItems()
    {
        $list = $this->createTestList();
        $lines = $list->render(new RenderContext(80, 24));

        $this->assertS


ymfony\Component\Tui\Event\SelectEvent;
use Symfony\Component\Tui\Event\SettingChangeEvent;
use Symfony\Component\Tui\Render\RenderContext;
use Symfony\Component\Tui\Render\Renderer;
use Symfony\Component\Tui\Style\Border;
use Symfony\Component\Tui\Style\Padding;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Terminal\VirtualTerminal;
use Symfony\Component\Tui\Tui;
use Symfony\Component\Tui\Widget\SelectListWidget;
use Symfony\Component\Tui\Widget\SettingItem;
use Symfony\Component\Tui\Wid

t(80, 24));

        $this->assertStringContainsString('Theme', $lines[0]);
    }

    public function testCycleValueRight()
    {
        [$widget, $tui] = $this->createWithTui();

        $this->assertSame('dark', $widget->getValue('theme'));

        //

get\SettingsListWidget;

class SettingsListTest extends TestCase
{
    public function testRenderShowsItems()
    {
        $widget = new SettingsListWidget($this->createItems());
        $lines = $widget->render(new RenderContex


buted with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Ansi\AnsiUtils;
use Symfony\Component\Tui\Render\RenderContext;
use Symfony\Component\Tui\Render\Renderer;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Widget\TextWidget;

class TextTest extends TestCase
{
    public function testRenderSimpleText()
    {
        $text = new TextWidget('Hello');
        $lines = $this->renderWidget($text, 20, 24);

Hello');
        $text->setStyle($style);
        $lines = $this->renderWidget($text, 20, 24);

        // Should have 1 top padding + 1 content + 1 bottom padding = 3 lines
        $this->assertCount(3, $lines);

        // All lines should be full width

     $this->assertCount(1, $lines);
        $this->assertStringContainsString('Hello', $lines[0]);
    }

    public function testRenderWithPadding()
    {
        $style = Style::padding([1, 2]);
        $text = new TextWidget('


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Focus\FocusManager;
use Symfony\Component\Tui\Input\Keybindings;
use Symfony\Component\Tui\Render\Renderer;
use Symfony\Component\Tui\Render\RenderRequestorInterface;
use Symfony\Component\Tui\Terminal\VirtualTerminal;
use Symfony\Component\Tui\Tui;
use Symfony\Component

e $tree;

    protected function setUp(): void
    {
        $terminal = new VirtualTerminal(80, 24);
        $tui = new Tui(terminal: $terminal);
        $this->tree = new WidgetTree($tui, new Keybindings(), new FocusManager($this->createStub(RenderReques

\Tui\Widget\ContainerWidget;
use Symfony\Component\Tui\Widget\TextWidget;
use Symfony\Component\Tui\Widget\WidgetContext;
use Symfony\Component\Tui\Widget\WidgetTree;

class WidgetTreeTest extends TestCase
{
    private WidgetTre


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget\Util;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Widget\Util\KillRing;

class KillRingTest extends TestCase
{
    public function testAddAndPeek()
    {
        $ring = new KillRing();
        $ring->add('hello', false);

        $this->assertSame('hello', $ring->peek());
    }

    public function testAddEmptyStr

$ring->add('hello', false);
        $ring->add(' world', false);

        $this->assertSame('hello world', $ring->peek());
    }

    public function testConsecutiveKillsPrepend()
    {
        $ring = new KillRing();
        $ring->add('world', true);

ingIsIgnored()
    {
        $ring = new KillRing();
        $ring->add('', false);

        $this->assertNull($ring->peek());
    }

    public function testConsecutiveKillsAppend()
    {
        $ring = new KillRing();


ne;

class LineTest extends TestCase
{
    // --- bounds clamping ---

    public function testConstructorClampsNegativeCursor()
    {
        $line = new Line('hello', -5);
        $this->assertSame(0, $line->getCursor());
    }

    public function testConstructorClampsCursorBeyondEnd()
    {
        $line = new Line('hello', 100);
        $this->assertSame(5, $line->getCursor());
    }

    public function testSetCursorClampsNegativeValue()
    {
        $line = new Line('hello', 3);
        $line->setCu

getCursor());
    }

    public function testSetTextAdjustsCursorWhenBeyondNewLength()
    {
        $line = new Line('hello world', 10);
        $line->setText('hi');
        $this->assertSame('hi', $line->getText());
        $this->assertSame(2, $line->g

rsor(-1);
        $this->assertSame(0, $line->getCursor());
    }

    public function testSetCursorClampsBeyondEnd()
    {
        $line = new Line('hello', 0);
        $line->setCursor(100);
        $this->assertSame(5, $line->


 part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget\Util;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Widget\Util\StringUtils;

class StringUtilsTest extends TestCase
{
    // --- hasControlChars ---

    /**
     * @return iterable<string, arr

yield 'mixed (control in text)' => ["hello\x00world", true];
        yield 'printable ASCII' => ['hello', false];
        yield 'latin accents' => ['café', false];
        yield 'emoji' => ['👋', false];
        yield 'emoji mixed with text' => ['hello 🎉 wo

ay{string, bool}>
     */
    public static function hasControlCharsProvider(): iterable
    {
        yield 'null byte' => ["\x00", true];
        yield 'escape' => ["\x1b", true];
        yield 'del' => ["\x7f", true];


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget\Util;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Widget\Util\WordNavigator;

final class WordNavigatorTest extends TestCase
{
    /**
     * @return iterable<string, array{string, int, int}>
     */
    public static function skipWordBackwardProvider(): iterable
    {

 then word (from mid)' => ['hello  world', 7, 0];
        yield 'skips punctuation (from end)' => ['hello...world', 13, 8];
        yield 'skips punctuation (from mid)' => ['hello...world', 8, 5];
        yield 'skips word after punctuation' => ['hello...w

        yield 'at start of string' => ['hello world', 0, 0];
        yield 'skips word' => ['hello world', 11, 6];
        yield 'skips whitespace then word (from end)' => ['hello  world', 12, 7];
        yield 'skips whitespace


r\EditorDocument;

class EditorDocumentTest extends TestCase
{
    // --- Text Operations ---

    public function testSetTextReturnsWhetherStateChanged()
    {
        $doc = new EditorDocument();

        $this->assertTrue($doc->setText('hello'), 'setText should return true when text changes');
        $this->assertFalse($doc->setText('hello'), 'setText on identical text+cursor should return false');
    }

    public function testSetTextNormalizesLineEndings()
    {
        $doc = new EditorDocument();

e 2\nline 3");
        $doc->moveCursorDown();
        $doc->moveCursorDown();

        $doc->setText('new');

        $this->assertSame(0, $doc->getCursorLine());
        $this->assertSame(0, $doc->getCursorCol());
    }

    public function testSetTextCl

       $doc->setText("a\r\nb\rc");

        $this->assertSame("a\nb\nc", $doc->getText());
    }

    public function testSetTextResetsCursorPosition()
    {
        $doc = new EditorDocument();
        $doc->setText("line 1\nlin


buted with this source code.
 */

namespace Symfony\Component\Tui\Tests\Widget\Editor;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Ansi\AnsiUtils;
use Symfony\Component\Tui\Style\CursorShape;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Widget\Editor\EditorRenderer;

class EditorRendererTest extends TestCase
{
    private EditorRenderer $renderer;

    protected function setUp(): void
    {
        $this->renderer = new EditorRenderer();
    }

    public function testRend

    {
        $lines = $this->renderSimple(['Line 1', 'Line 2', 'Line 3'], 0, 0, 40, 10);

        // Top border + 3 content lines + bottom border
        $this->assertCount(5, $lines);
    }

    public function testRenderLinesDoNotExceedWidth()
    {

erEmptyDocument()
    {
        $lines = $this->renderSimple([''], 0, 0, 40, 10);

        // Top border + 1 content line + bottom border
        $this->assertCount(3, $lines);
    }

    public function testRenderMultipleLines()


 = "\x1b[?25l";
    private const SHOW_CURSOR = "\x1b[?25h";

    // --- Full render (initial) ---

    public function testFirstRenderWritesAllLines()
    {
        $terminal = new VirtualTerminal(80, 24);
        $writer = new ScreenWriter($terminal);

        $writer->writeLines(['Line 1', 'Line 2', 'Line 3']);

        $output = $terminal->getOutput();

        // Should use synchronized output
        $this->assertStringContainsString(self::SYNC_START, $output);
        $this->assertStringContainsStrin

sString('Line 1', $output);
        $this->assertStringContainsString('Line 2', $output);
        $this->assertStringContainsString('Line 3', $output);

        // Plain text lines should be separated by \r\n without reset
        $this->assertStringContai

g(self::SYNC_END, $output);

        // Should NOT clear screen on first render
        $this->assertStringNotContainsString(self::CLEAR_SCREEN, $output);

        // All lines should be present
        $this->assertStringContain


 part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests\Render;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Render\WidgetRect;

class WidgetRectTest extends TestCase
{
    #[DataProvider('containsProvider')]
    public function testContains(int $row, int

 /**
     * @return iterable<string, array{int, int, bool}>
     */
    public static function containsProvider(): iterable
    {
        // Inside
        yield 'top-left corner' => [5, 10, true];
        yield 'center' => [15, 30, true];
        yield 'b

$col, bool $expected)
    {
        // Rect at (5, 10) with size 40x20 → covers rows 5..24, cols 10..49
        $rect = new WidgetRect(5, 10, 40, 20);

        $this->assertSame($expected, $rect->contains($row, $col));
    }


ge.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget\Figlet;

use Symfony\Component\Tui\Render\Compositor;
use Symfony\Component\Tui\Style\Color;

/**
 * Renders text using a FIGlet font.
 *
 * Concatenates the ASCII art for each character horizontally, line by line.
 * Strips trailing whitespace from each line and removes blank trailing

nt mode: spaces at the end of lines won't carry styling that
 * would prevent lower layers from showing through.
 *
 * @experimental
 *
 * @internal
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class FigletRenderer
{
    public function __

 lines.
 *
 * When a color is provided, each line is wrapped with the foreground ANSI
 * escape code. Since trailing whitespace is already stripped, the colored
 * output is safe for use with the {@see Compositor}
 * in transpare


ge.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Widget\Figlet;

use Symfony\Component\Tui\Exception\InvalidArgumentException;

/**
 * Registry for FIGlet fonts.
 *
 * Maps font names to file paths and lazily loads FigletFont instances.
 * Bundled fonts (big, small, slant, standard, mini) are registered
 * by default. Custom fonts can be re

 Style(font: 'custom'));
 *     $widget->addStyleClass('font-custom');
 *
 * @experimental
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class FontRegistry
{
    private const string BUNDLED_FONTS_DIR = __DIR__.'/fonts';
    private const a

gistered by name:
 *
 *     $registry = new FontRegistry();
 *     $registry->register('custom', '/path/to/custom.flf');
 *
 * Fonts are referenced by name throughout the Style system:
 *
 *     $stylesheet->addRule('.title', new


ge.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests;

/**
 * Handles key sequence parsing for TUI tests.
 *
 * This centralizes the key name to escape sequence mapping used by
 * both the fixture recorder and regression tests.
 */
final class KeySequenceParser
{
    /**
     * Map of human-readable key names to terminal escape sequences.

     *
     * @var array<string, string>
     */
    private const KEY_MAP = [
        '<Enter>' => "\r",
        '<Return>' => "\r",
        '<Escape>' => "\x1b",
        '<Esc>' => "\x1b",
        '<Tab>' => "\t",
        '<Sh


 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class StateDiffTest extends TestCase
{
    public function testIdenticalOutputs()
    {
        $output = "Line 1\nLine 2\nLine 3";

        $result = StateDiff::compare($output, $output);

        $this->assertTrue($result['identical']);
        $this->as

=> 0, 'unchanged' => 0], $result['stats']);
    }

    public function testAddedLines()
    {
        $expected = "Line 1\nLine 2";
        $actual = "Line 1\nLine 2\nLine 3";

        $result = StateDiff::compare($expected, $actual);

        $this->asser

sertSame('Outputs are identical', $result['summary']);
        $this->assertSame([], $result['diff_lines']);
        $this->assertSame('', $result['diff_text']);
        $this->assertSame(['added' => 0, 'removed' => 0, 'changed'


omponent\Tui\Event\TickEvent;
use Symfony\Component\Tui\Exception\InvalidArgumentException;
use Symfony\Component\Tui\Input\Key;
use Symfony\Component\Tui\Input\Keybindings;
use Symfony\Component\Tui\Render\Renderer;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Style\StyleSheet;
use Symfony\Component\Tui\Terminal\VirtualTerminal;
use Symfony\Component\Tui\Tui;
use Symfony\Component\Tui\Widget\ContainerWidget;
use Symfony\Component\Tui\Widget\InputWidget;
use Symfony\Component\Tui\Widget\

d'));

        $tui->start();
        $tui->processRender();

        $output = $terminal->getOutput();
        $this->assertStringContainsString('Hello World', $output);
    }

    public function testMultipleComponents()
    {
        $terminal = new Vir

TextWidget;

class TuiTest extends TestCase
{
    public function testBasicRender()
    {
        $terminal = new VirtualTerminal(40, 10);
        $tui = new Tui(terminal: $terminal);

        $tui->add(new TextWidget('Hello Worl


buted with this source code.
 */

namespace Symfony\Component\Tui\Tests\Style;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Exception\InvalidArgumentException;
use Symfony\Component\Tui\Style\BorderPattern;
use Symfony\Component\Tui\Style\Color;
use Symfony\Component\Tui\Style\Style;

class BorderPatternTest extends TestCase
{
    #[DataProvider('allPatternsProvider')]
    public function testFromNameReturnsExpectedCharsAndIsNone(string $name, ?st

omName->getChars());
            $this->assertSame($fromMethod->getStrategies(), $fromName->getStrategies());
        }

        $this->assertSame($isNone, $fromName->isNone());
    }

    /**
     * @return iterable<string, array{string, ?string, bool}>

ring $method, bool $isNone)
    {
        $fromName = BorderPattern::fromName($name);

        if (null !== $method) {
            $fromMethod = BorderPattern::$method();
            $this->assertSame($fromMethod->getChars(), $fr


ge.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Tui\Tests\Style;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Tui\Exception\InvalidArgumentException;
use Symfony\Component\Tui\Style\Padding;

class PaddingTest extends TestCase
{
    /**
     * @param list<int> $input
     */
    #[DataPr

this->assertSame($right, $padding->right);
        $this->assertSame($bottom, $padding->bottom);
        $this->assertSame($left, $padding->left);
    }

    /**
     * @return iterable<string, array{list<int>, int, int, int, int}>
     */
    public stati

ovider('fromArrayProvider')]
    public function testFromArray(array $input, int $top, int $right, int $bottom, int $left)
    {
        $padding = Padding::from($input);

        $this->assertSame($top, $padding->top);
        $


fony\Component\Tui\Style\Border;
use Symfony\Component\Tui\Style\BorderPattern;
use Symfony\Component\Tui\Style\Color;
use Symfony\Component\Tui\Style\CursorShape;
use Symfony\Component\Tui\Style\Direction;
use Symfony\Component\Tui\Style\Padding;
use Symfony\Component\Tui\Style\Style;
use Symfony\Component\Tui\Style\TextAlign;
use Symfony\Component\Tui\Style\VerticalAlign;

class StyleTest extends TestCase
{
    public function testBorderWithSingleElementArray()
    {
        $style = Style::border([2], Bo

me(2, $border->left);
        $this->assertSame(BorderPattern::double()->getChars(), $border->pattern->getChars());
        $this->assertSame(BorderPattern::double()->getStrategies(), $border->pattern->getStrategies());
        $this->assertSame(

rderPattern::DOUBLE, 'red');
        $border = $style->getBorder();

        $this->assertSame(2, $border->top);
        $this->assertSame(2, $border->right);
        $this->assertSame(2, $border->bottom);
        $this->assertSa


 */
    public static function paddingProvider(): iterable
    {
        yield 'all' => ['p-2', 2, 2, 2, 2];
        yield 'zero' => ['p-0', 0, 0, 0, 0];
        yield 'horizontal' => ['px-3', 0, 3, 0, 3];
        yield 'vertical' => ['py-1', 1, 0, 1, 0];
    }

    public function testPaddingIndividualSides()
    {
        $stylesheet = new TailwindStylesheet();
        $widget = new TextWidget('Hello');
        $widget->addStyleClass('pt-1');
        $widget->addStyleClass('pr-2');
        $widget->addSty

);
        $this->assertSame(3, $resolved->getPadding()->bottom);
        $this->assertSame(4, $resolved->getPadding()->left);
    }

    public function testPaddingLastWins()
    {
        $stylesheet = new TailwindStylesheet();
        $widget = new Text

leClass('pb-3');
        $widget->addStyleClass('pl-4');

        $resolved = $stylesheet->resolve($widget);

        $this->assertSame(1, $resolved->getPadding()->top);
        $this->assertSame(2, $resolved->getPadding()->right