diff --git a/src/Themes/Default/Concerns/DrawsTabs.php b/src/Themes/Default/Concerns/DrawsTabs.php new file mode 100644 index 0000000..830e27e --- /dev/null +++ b/src/Themes/Default/Concerns/DrawsTabs.php @@ -0,0 +1,66 @@ + $tabs + */ + protected function tabs( + Collection $tabs, + int $selected, + int $width, + string $color = 'cyan', + ): string { + $strippedWidth = fn (string $value): int => mb_strwidth($this->stripEscapeSequences($value)); + + // Build the top row for the tabs by adding whitespace equal + // to the width of each tab plus padding, or by adding an + // equal number of box characters for the selected tab. + $top_row = $tabs->map(fn($value, $key) => $key === $selected + ? '╭' . str_repeat('─', $strippedWidth($value) + 2) . '╮' + : str_repeat(' ', $strippedWidth($value) + 4) + )->implode(''); + + // Build the middle row for the tabs by adding the tab name + // surrounded by some padding. But if the tab is selected + // then highlight the tab and surround it in box chars. + $middle_row = $tabs->map(fn($value, $key) => $key === $selected + ? "{$this->dim('│')} {$this->{$color}($value)} {$this->dim('│')}" + : " {$value} " + )->implode(''); + + // Build the bottom row for the tabs by adding box characters equal to the width + // of each tab, plus padding. If the tab is selected, add the appropriate box + // characters instead. Finally, pad the whole line to fill the width fully. + $bottom_row = $tabs->map(fn($value, $key) => $key === $selected + ? '┴' . str_repeat('─', $strippedWidth($value) + 2) . '┴' + : str_repeat('─', $strippedWidth($value) + 4) + )->implode(''); + $bottom_row = $this->pad($bottom_row, $width, '─'); + + // If the tabs are wider than the provided width, we need to trim the tabs to fit. + // We remove the appropriate number of characters from the beginning and end of + // each row by using the highlighted tab's index to get it's scroll position. + if ($strippedWidth($top_row) > $width) { + $scroll = $selected / ($tabs->count() - 1); + $chars_to_kill = $strippedWidth($top_row) - $width; + $offset = (int) round($scroll * $chars_to_kill); + foreach ([&$top_row, &$middle_row, &$bottom_row] as &$row) { + $row = mb_substr($row, $offset, mb_strwidth($row) - $chars_to_kill); + } + } + + // We wait until now to dim the top and bottom + // rows, otherwise the horizontal scrolling + // could easily strip those instructions. + return collect([$this->dim($top_row), $middle_row, $this->dim($bottom_row)])->implode(PHP_EOL); + } +} diff --git a/tests/Feature/DrawsTabsTest.php b/tests/Feature/DrawsTabsTest.php new file mode 100644 index 0000000..d581765 --- /dev/null +++ b/tests/Feature/DrawsTabsTest.php @@ -0,0 +1,99 @@ +write($this->renderTheme()); + } +} + +class TestRenderer extends Renderer +{ + use DrawsTabs; + + public function __invoke(TestPrompt $prompt) + { + return $this->tabs($prompt->tabs, $prompt->selected, $prompt->width); + } +} + +/** + * Note: Trailing whitespace is intentional in order to match the output. + * Removing it will cause the test to fail (correctly) while allowing + * the output to appear indistinguishable from the expected output. + */ + +it('renders tabs', function () { + Prompt::fake(); + + $tabs = collect(['One', 'Two', 'Three', 'Four', 'Five', 'Six']); + + (new TestPrompt($tabs))->display(); + + Prompt::assertStrippedOutputContains(<<<'OUTPUT' + ╭─────╮ + │ One │ Two Three Four Five Six + ┴─────┴───────────────────────────────────────────────────── + OUTPUT); +}); + +it('highlights tabs', function () { + Prompt::fake(); + + $tabs = collect(['One', 'Two', 'Three', 'Four', 'Five', 'Six']); + + (new TestPrompt($tabs, 2))->display(); + + Prompt::assertStrippedOutputContains(<<<'OUTPUT' + ╭───────╮ + One Two │ Three │ Four Five Six + ──────────────┴───────┴───────────────────────────────────── + OUTPUT); +}); + +it('truncates tabs', function () { + Prompt::fake(); + + $tabs = collect(['One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight']); + + (new TestPrompt($tabs))->display(); + + Prompt::assertStrippedOutputContains(<<<'OUTPUT' + ╭─────╮ + │ One │ Two Three Four Five Six Seven Eig + ┴─────┴───────────────────────────────────────────────────── + OUTPUT); +}); + +it('scrolls tabs', function () { + Prompt::fake(); + + $tabs = collect(['One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight']); + + (new TestPrompt($tabs, 7))->display(); + + Prompt::assertStrippedOutputContains(<<<'OUTPUT' + ╭───────╮ + e Two Three Four Five Six Seven │ Eight │ + ───────────────────────────────────────────────────┴───────┴ + OUTPUT); +});