From 47f03267050e10e86d973b28095b78b525545ff9 Mon Sep 17 00:00:00 2001 From: Andrew Masiye Date: Sun, 22 Mar 2026 10:57:48 +0200 Subject: [PATCH 1/2] refactor(ui): enhance Text class for better rendering and dimension calculation --- src/Core/Scenes/TitleScene.php | 27 +- src/Game.php | 2 +- src/UI/Text/Text.php | 502 ++++++++++++++++++--------------- 3 files changed, 291 insertions(+), 240 deletions(-) diff --git a/src/Core/Scenes/TitleScene.php b/src/Core/Scenes/TitleScene.php index 1cfe01a..ea097ca 100644 --- a/src/Core/Scenes/TitleScene.php +++ b/src/Core/Scenes/TitleScene.php @@ -79,6 +79,13 @@ class TitleScene extends AbstractScene */ protected int|string $newGameSceneTarget = 1; + protected int $uiHeight { + get { + $gap = 1; + return $this->titleText->getHeight() + $gap + $this->menu->getItems()->count() + 2; // 1 for each border + } + } + /** * @inheritDoc * @throws Exception @@ -86,12 +93,14 @@ class TitleScene extends AbstractScene public function awake(): void { $gameName = getGameName() ?? $this->name; + $screenWidth = $this->resolveScreenWidth(); + $titleTextHeight = 5; $this->titleText = new Text( scene: $this, name: $gameName, position: new Vector2(0, $this->titleTopMargin), - size: new Vector2($this->resolveScreenWidth(), 5) + size: new Vector2($screenWidth, $titleTextHeight) ); $this->titleText->setFontName(FontName::BIG->value); $this->setTitleText($gameName); @@ -100,7 +109,8 @@ public function awake(): void $gameName = $_ENV['GAME_NAME'] ?? $this->name; } - $this->menu = new Menu(title: $gameName, description: 'q:quit', dimensions: new Rect(new Vector2($this->getMenuLeftMargin(), $this->getMenuTopMargin()), new Vector2($this->menuWidth, $this->menuHeight)), cancelKey: [KeyCode::Q, KeyCode::q], onCancel: fn() => quitGame()); + $menuDimensions = new Rect(new Vector2($this->getMenuLeftMargin(), $this->getMenuTopMargin()), new Vector2($this->menuWidth, $this->menuHeight)); + $this->menu = new Menu(title: $gameName, description: 'q:quit', dimensions: $menuDimensions, cancelKey: [KeyCode::Q, KeyCode::q], onCancel: fn() => quitGame()); $this->menu->addItem(new MenuItem(label: 'New Game', description: 'Start a new game', icon: '🎮')); $this->menu->addItem(new MenuItem(label: 'Quit', description: 'Quit the game', icon: '🚪', callback: function () { quitGame(); @@ -121,7 +131,6 @@ public function awake(): void private function getMenuLeftMargin(): int { $screenWidth = $this->resolveScreenWidth(); - Debug::log("Screen width: $screenWidth"); return (int)round($screenWidth / 2) - (int)round($this->menuWidth / 2); } @@ -144,7 +153,16 @@ public function setTitleText(string $text): self $screenWidth = $this->resolveScreenWidth(); $this->titleLeftMargin = round(($screenWidth / 2) - ($this->titleText->getWidth() / 2)); $this->titleTopMargin = self::TOP_MARGIN_OFFSET; - $this->titleText->setPosition(new Vector2(round($this->titleLeftMargin), round($this->titleTopMargin))); + $this->titleText->setPosition(new Vector2($this->titleLeftMargin, $this->titleTopMargin)); + Debug::log(var_export([ + 'screenWidth' => $screenWidth, + 'titleText' => $this->titleText->getText(), + 'titleTextWidth' => $this->titleText->getWidth(), + 'titleLeftMargin' => $this->titleLeftMargin, + 'titleTopMargin' => $this->titleTopMargin, + 'titlePosition' => (string)$this->titleText->getPosition(), + 'timestamp' => time(), + ], true)); return $this; } @@ -269,7 +287,6 @@ public function addMenuItems(MenuItemInterface ...$item): self private function resolveScreenWidth(): int { return $this->resolveDimension( - get_screen_width(), $this->screenWidth, $this->sceneManager->getSettings('screen_width'), $this->getSettings('screen_width'), diff --git a/src/Game.php b/src/Game.php index da0d847..d9ad00b 100644 --- a/src/Game.php +++ b/src/Game.php @@ -152,7 +152,7 @@ class Game implements ObservableInterface public int $screenHeight { get { if (is_null($this->resolvedScreenWidth)) { - return exec('tput cols') ?: 80; + return exec('tput lines') ?: 40; } return $this->resolvedScreenHeight; diff --git a/src/UI/Text/Text.php b/src/UI/Text/Text.php index 8b5b473..8cab578 100644 --- a/src/UI/Text/Text.php +++ b/src/UI/Text/Text.php @@ -4,8 +4,10 @@ use Amasiye\Figlet\Figlet; use Exception; +use Override; use Sendama\Engine\Core\Scenes\Interfaces\SceneInterface; use Sendama\Engine\Core\Vector2; +use Sendama\Engine\Debug\Debug; use Sendama\Engine\IO\Console\Console; use Sendama\Engine\IO\Console\Cursor; use Sendama\Engine\IO\Enumerations\Color; @@ -16,238 +18,270 @@ */ class Text extends UIElement { - /** - * The text of the UI element. - * - * @var string - */ - protected string $text = ''; - - /** - * The raw lines of the text. - * - * @var string[] - */ - protected array $rawLines = []; - - /** - * The width of the rendered text. - * - * @var int - */ - protected int $renderWidth = 0; - - /** - * The color of the text. - * - * @var Color - */ - protected Color $color = Color::WHITE; - - /** - * The background color of the text. - * - * @var Color - */ - protected Color $backgroundColor = Color::BLACK; - - /** - * The font size of the text. - * - * @var int - */ - protected int $fontSize = 12; - - /** - * The font name of the text. - * - * @var string - */ - protected string $fontName = 'basic'; - - /** - * A reference to the Figlet object. - * - * @var Figlet|null The reference to the Figlet object. - */ - protected ?Figlet $figlet = null; - - /** - * A reference to the cursor object. - * - * @var Cursor|null The reference to the cursor object. - */ - protected ?Cursor $cursor = null; - /** - * The height of the rendered text. - * - * @var int - */ - protected int $renderHeight = 0; - - /** - * @inheritDoc - * - * @throws Exception - */ - public function __construct( - SceneInterface $scene, - string $name, - Vector2 $position = new Vector2(0, 0), - Vector2 $size = new Vector2(1, 1) - ) - { - parent::__construct($scene, $name, $position, $size); - - $this->cursor = Console::cursor(); - $this->figlet = new Figlet(); - $this->figlet - ->setFont($this->getFontName()) - ->setBackgroundColor(str_replace(' ', '_', strtolower($this->backgroundColor->getPhoneticName()))) - ->setFontColor(str_replace(' ', '_', strtolower($this->color->getPhoneticName()))); - - $this->rawLines = $this->getRawLines(); - } - - /** - * Sets the text of the UI element. - * - * @param string $text The text of the UI element. - * @return void - * @throws Exception - */ - public function setText(string $text): void - { - $this->text = $text; - $this->rawLines = $this->getRawLines(); - } - - /** - * Returns the text of the UI element. - * - * @return string The text of the UI element. - */ - public function getText(): string - { - return $this->text; - } - - /** - * @inheritDoc - * - * @throws Exception - */ - public function render(): void - { - $this->renderAt($this->position->getX(), $this->position->getY()); - } - - /** - * @inheritDoc - * @throws Exception - */ - public function renderAt(?int $x = null, ?int $y = null): void - { - Console::writeLines($this->rawLines, $x ?? 0, $y ?? 0); - } - - /** - * @inheritDoc - */ - public function erase(): void - { - $this->eraseAt($this->position->getX(), $this->position->getY()); - } - - /** - * @inheritDoc - */ - public function eraseAt(?int $x = null, ?int $y = null): void - { - // TODO: Implement eraseAt() method. - } - - /** - * @inheritDoc - * - * @throws Exception - */ - public function start(): void - { - // Do nothing - } - - /** - * @inheritDoc - */ - public function update(): void - { - // TODO: Implement update() method. - - // Handle text animation here - } - - /** - * Returns the raw lines of the text. - * - * @return string[] The raw lines of the text. - * @throws Exception If the text is empty. - */ - protected function getRawLines(): array - { - $render = $this->figlet?->render($this->getText()); - $rawLines = explode("\n", $render ?? ''); - - foreach ($rawLines as $line) - { - $this->renderWidth = max($this->renderWidth, strlen($line)); - } - $this->renderHeight = count($rawLines); - - return $rawLines; - } - - /** - * Sets the font name of the text. - * - * @param string $fontName The font name of the text. - * @return void - * @throws Exception - */ - public function setFontName(string $fontName): void - { - $this->figlet?->setFont($fontName); - $this->fontName = $fontName; - $this->rawLines = $this->getRawLines(); - } - - /** - * Returns the font name of the text. - * - * @return string The font name of the text. - */ - public function getFontName(): string - { - return $this->fontName; - } - - /** - * Returns the width of the rendered text. - * - * @return int The width of the rendered text. - */ - public function getWidth(): int - { - return $this->renderWidth; - } - - /** - * Returns the height of the rendered text. - * - * @return int The height of the rendered text. - */ - public function getHeight(): int - { - return $this->renderHeight; - } + /** + * The text of the UI element. + * + * @var string + */ + protected string $text = ''; + + /** + * The raw lines of the text. + * + * @var string[] + */ + protected array $rawLines = []; + + /** + * The width of the rendered text. + * + * @var int + */ + protected int $renderWidth = 0; + + /** + * The color of the text. + * + * @var Color + */ + protected Color $color = Color::WHITE; + + /** + * The background color of the text. + * + * @var Color + */ + protected Color $backgroundColor = Color::BLACK; + + /** + * The font size of the text. + * + * @var int + */ + protected int $fontSize = 12; + + /** + * The font name of the text. + * + * @var string + */ + protected string $fontName = 'basic'; + + /** + * A reference to the Figlet object. + * + * @var Figlet|null The reference to the Figlet object. + */ + protected ?Figlet $figlet = null; + + /** + * A reference to the cursor object. + * + * @var Cursor|null The reference to the cursor object. + */ + protected ?Cursor $cursor = null; + /** + * The height of the rendered text. + * + * @var int + */ + protected int $renderHeight = 0; + + /** + * @inheritDoc + * + * @throws Exception + */ + public function __construct( + SceneInterface $scene, + string $name, + Vector2 $position = new Vector2(0, 0), + Vector2 $size = new Vector2(1, 1) + ) + { + parent::__construct($scene, $name, $position, $size); + + $this->cursor = Console::cursor(); + $this->figlet = new Figlet(); + $this->figlet + ->setFont($this->getFontName()) + ->setBackgroundColor(str_replace(' ', '_', strtolower($this->backgroundColor->getPhoneticName()))) + ->setFontColor(str_replace(' ', '_', strtolower($this->color->getPhoneticName()))); + + $this->updateRawLines(); + $this->calculateDimensions(); + } + + /** + * Returns the font name of the text. + * + * @return string The font name of the text. + */ + public function getFontName(): string + { + return $this->fontName; + } + + /** + * Sets the font name of the text. + * + * @param string $fontName The font name of the text. + * @return void + * @throws Exception + */ + public function setFontName(string $fontName): void + { + $this->figlet?->setFont($fontName); + $this->fontName = $fontName; + $this->updateRawLines(); + } + + /** + * @return void + * @throws Exception + */ + private function updateRawLines(): void + { + $render = $this->figlet?->render($this->getText()); + $this->rawLines = explode("\n", $render ?? ''); + } + + /** + * Returns the raw lines of the text. + * + * @return string[] The raw lines of the text. + * @throws Exception If the text is empty. + */ + protected function getRawLines(): array + { + return $this->rawLines; + } + + /** + * @inheritDoc + * + * @throws Exception + */ + public function render(): void + { + $this->renderAt($this->position->getX(), $this->position->getY()); + } + + /** + * @inheritDoc + * @throws Exception + */ + public function renderAt(?int $x = null, ?int $y = null): void + { + Console::writeLines($this->rawLines, $x ?? 0, $y ?? 0); + } + + /** + * Returns the text of the UI element. + * + * @return string The text of the UI element. + */ + public function getText(): string + { + return $this->text; + } + + /** + * Sets the text of the UI element. + * + * @param string $text The text of the UI element. + * @return void + * @throws Exception + */ + public function setText(string $text): void + { + $this->text = $text; + $this->updateRawLines(); + $this->calculateDimensions(); + } + + /** + * @inheritDoc + */ + public function erase(): void + { + $this->eraseAt($this->position->getX(), $this->position->getY()); + } + + /** + * @inheritDoc + * @throws Exception + */ + #[Override] + public function setPosition(Vector2 $position): void + { + $this->calculateDimensions(); + + parent::setPosition($position); + } + + /** + * @inheritDoc + */ + public function eraseAt(?int $x = null, ?int $y = null): void + { + // TODO: Implement eraseAt() method. + } + + /** + * @inheritDoc + * + * @throws Exception + */ + public function start(): void + { + // Do nothing + } + + /** + * @inheritDoc + */ + public function update(): void + { + // TODO: Implement update() method. + + // Handle text animation here + } + + /** + * Returns the width of the rendered text. + * + * @return int The width of the rendered text. + */ + public function getWidth(): int + { + return $this->renderWidth; + } + + /** + * Returns the height of the rendered text. + * + * @return int The height of the rendered text. + */ + public function getHeight(): int + { + return $this->renderHeight; + } + + /** + * @return void + * @throws Exception + */ + private function calculateDimensions(): void + { + $longestLineLength = strlen($this->rawLines[0]); + + foreach ($this->rawLines as $line) { + $longestLineLength = max($longestLineLength, strlen($line)); + } + + $this->renderWidth = $longestLineLength; + $this->renderHeight = count($this->rawLines); + Debug::log(time() . ' - ' . __METHOD__ . " - width: {$this->getWidth()}"); + } } \ No newline at end of file From 478825d6a02d999a9ecf8e35d4315c3a6127eaef Mon Sep 17 00:00:00 2001 From: Andrew Masiye Date: Tue, 24 Mar 2026 11:30:43 +0200 Subject: [PATCH 2/2] feat(text): add refreshDimensions method for accurate dimension recalculation --- src/Core/Scenes/TitleScene.php | 14 +++++++++++++- src/UI/Text/Text.php | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Core/Scenes/TitleScene.php b/src/Core/Scenes/TitleScene.php index ea097ca..aafddcb 100644 --- a/src/Core/Scenes/TitleScene.php +++ b/src/Core/Scenes/TitleScene.php @@ -150,8 +150,20 @@ private function getMenuTopMargin(): int public function setTitleText(string $text): self { $this->titleText->setText($text); + usleep(300000); + // Ensure the Text has fresh dimensions — Figlet/font rendering or + // console init may have completed after setText() ran. Refresh + // dimensions if the helper exists so getWidth() is reliable here. + try { + if (method_exists($this->titleText, 'refreshDimensions')) { + $this->titleText->refreshDimensions(); + } + } catch (\Throwable $_) { + // best-effort + } + $screenWidth = $this->resolveScreenWidth(); - $this->titleLeftMargin = round(($screenWidth / 2) - ($this->titleText->getWidth() / 2)); + $this->titleLeftMargin = (int)intdiv(max(0, $screenWidth - $this->titleText->getWidth()), 2); $this->titleTopMargin = self::TOP_MARGIN_OFFSET; $this->titleText->setPosition(new Vector2($this->titleLeftMargin, $this->titleTopMargin)); Debug::log(var_export([ diff --git a/src/UI/Text/Text.php b/src/UI/Text/Text.php index 8cab578..6d0ce9c 100644 --- a/src/UI/Text/Text.php +++ b/src/UI/Text/Text.php @@ -284,4 +284,24 @@ private function calculateDimensions(): void $this->renderHeight = count($this->rawLines); Debug::log(time() . ' - ' . __METHOD__ . " - width: {$this->getWidth()}"); } + + /** + * Force re-evaluation of raw lines and calculated dimensions. + * Use this when external state (fonts, console, colors) may have changed + * and an immediate, accurate width/height is required. + * + * This is intentionally a best-effort helper: it will not throw if Figlet + * rendering fails; instead it logs a warning. + * + * @return void + */ + public function refreshDimensions(): void + { + try { + $this->updateRawLines(); + $this->calculateDimensions(); + } catch (\Throwable $e) { + Debug::warn('Text::refreshDimensions failed: ' . $e->getMessage()); + } + } } \ No newline at end of file