Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DrawsTabs trait with tests #143

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/Themes/Default/Concerns/DrawsTabs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Laravel\Prompts\Themes\Default\Concerns;

use Illuminate\Support\Collection;

trait DrawsTabs
{
use InteractsWithStrings;

/**
* Render a row of tabs.
*
* @param Collection<int, string> $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);
}
}
99 changes: 99 additions & 0 deletions tests/Feature/DrawsTabsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

use Illuminate\Support\Collection;
use Laravel\Prompts\Prompt;
use Laravel\Prompts\Themes\Default\Concerns\DrawsTabs;
use Laravel\Prompts\Themes\Default\Renderer;

class TestPrompt extends Prompt
{
public function __construct(
public Collection $tabs,
public int $selected = 0,
public int $width = 60,
) {
static::$themes['default'][static::class] = TestRenderer::class;
}

public function value(): mixed
{
return null;
}

public function display(): void
{
static::output()->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);
});
Loading