Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/Console/Components/Details.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
use PhpTui\Tui\Extension\Core\Widget\List\ListItem;
use PhpTui\Tui\Extension\Core\Widget\ListWidget;
use PhpTui\Tui\Extension\Core\Widget\Scrollbar\ScrollbarOrientation;
use PhpTui\Tui\Extension\Core\Widget\Scrollbar\ScrollbarState;
use PhpTui\Tui\Extension\Core\Widget\Scrollbar\ScrollbarSymbols;
use PhpTui\Tui\Extension\Core\Widget\ScrollbarWidget;
use PhpTui\Tui\Style\Style;
Expand All @@ -25,6 +24,7 @@
use Tapper\Console\CommandAttributes\Mouse;
use Tapper\Console\Component;
use Tapper\Console\MessageFormatter;
use Tapper\Console\Support\Scroll;

class Details extends Component
{
Expand Down Expand Up @@ -153,7 +153,7 @@ protected function view(Area $area): Widget
->items(...$allItems)
->offset($this->appState->detailsOffset),
ScrollbarWidget::default()
->state(new ScrollbarState(max(0, $this->count - $this->area->height), $this->appState->detailsOffset, 1))
->state(Scroll::scrollbarState($this->count, $this->area->height, $this->appState->detailsOffset))
->orientation(ScrollbarOrientation::VerticalRight)
->symbols(new ScrollbarSymbols('│', '█', '', ''))
->endSymbol(null)
Expand Down
3 changes: 1 addition & 2 deletions src/Console/Components/LogList.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use PhpTui\Tui\Extension\Core\Widget\CompositeWidget;
use PhpTui\Tui\Extension\Core\Widget\GridWidget;
use PhpTui\Tui\Extension\Core\Widget\Scrollbar\ScrollbarOrientation;
use PhpTui\Tui\Extension\Core\Widget\Scrollbar\ScrollbarState;
use PhpTui\Tui\Extension\Core\Widget\Scrollbar\ScrollbarSymbols;
use PhpTui\Tui\Extension\Core\Widget\ScrollbarWidget;
use PhpTui\Tui\Layout\Constraint;
Expand Down Expand Up @@ -162,7 +161,7 @@ protected function view(Area $area): Widget
),
),
ScrollbarWidget::default()
->state(new ScrollbarState(max(0, $this->count - $this->visible), $this->appState->offset, 1))
->state(Scroll::scrollbarState($this->count, $this->visible, $this->appState->offset))
->orientation(ScrollbarOrientation::VerticalRight)
->symbols(new ScrollbarSymbols('│', '█', '', ''))
->endSymbol(null)
Expand Down
6 changes: 6 additions & 0 deletions src/Console/Support/Scroll.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@

namespace Tapper\Console\Support;

use PhpTui\Tui\Extension\Core\Widget\Scrollbar\ScrollbarState;
use Tapper\Console\State\AppState;

class Scroll
{
public function __construct(private readonly AppState $appState) {}

public static function scrollbarState(int $count, int $visible, int $offset): ScrollbarState
{
return new ScrollbarState(max(0, $count - $visible), $offset, $visible);
}

public function cursorDown(int $count, int $visible): void
{
if ($this->appState->cursor < $count - 1) {
Expand Down
26 changes: 26 additions & 0 deletions tests/Unit/ScrollTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,29 @@
expect($this->appState->offset)->toBe(5);
});
});

describe('scrollbar state', function () {
it('maps count/visible/offset onto content length, viewport length and position', function () {
$state = Scroll::scrollbarState(count: 100, visible: 20, offset: 35);

expect($state->contentLength)->toBe(80)
->and($state->viewportContentLength)->toBe(20)
->and($state->position)->toBe(35);
});

it('never lets content length go negative when everything fits on screen', function () {
$state = Scroll::scrollbarState(count: 5, visible: 20, offset: 0);

expect($state->contentLength)->toBe(0);
});

it('uses the max offset (count - visible) as content length so the thumb can reach the bottom', function () {
$count = 100;
$visible = 20;
$maxOffset = $count - $visible;

$state = Scroll::scrollbarState($count, $visible, $maxOffset);

expect($state->position)->toBe($state->contentLength);
});
});
68 changes: 68 additions & 0 deletions tests/Unit/ScrollbarRenderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

use PhpTui\Tui\Display\Area;
use PhpTui\Tui\Display\Buffer;
use PhpTui\Tui\Extension\Core\Widget\Scrollbar\ScrollbarOrientation;
use PhpTui\Tui\Extension\Core\Widget\Scrollbar\ScrollbarSymbols;
use PhpTui\Tui\Extension\Core\Widget\ScrollbarRenderer;
use PhpTui\Tui\Extension\Core\Widget\ScrollbarWidget;
use PhpTui\Tui\Position\Position;
use PhpTui\Tui\Widget\WidgetRenderer\AggregateWidgetRenderer;
use Tapper\Console\Support\Scroll;

const THUMB_SYMBOL = '█';

/**
* @return string[] one character per row of the rendered scrollbar track
*/
function renderScrollbarColumn(int $count, int $visible, int $offset, int $trackHeight): array
{
$area = Area::fromScalars(0, 0, 3, $trackHeight);
$buffer = Buffer::empty($area);

$widget = ScrollbarWidget::default()
->state(Scroll::scrollbarState($count, $visible, $offset))
->orientation(ScrollbarOrientation::VerticalRight)
->symbols(new ScrollbarSymbols('│', THUMB_SYMBOL, '', ''))
->endSymbol(null)
->beginSymbol(null);

(new ScrollbarRenderer)->render(new AggregateWidgetRenderer([]), $widget, $buffer, $area);

$column = [];
for ($y = 0; $y < $trackHeight; $y++) {
$column[] = $buffer->get(Position::at($area->right() - 1, $y))->char;
}

return $column;
}

it('places the thumb at the very top when scrolled to the top', function () {
$column = renderScrollbarColumn(count: 100, visible: 20, offset: 0, trackHeight: 40);

expect($column[0])->toBe(THUMB_SYMBOL);
});

it('places the thumb at the very bottom when scrolled to the bottom', function () {
$column = renderScrollbarColumn(count: 100, visible: 20, offset: 80, trackHeight: 40);

expect($column[count($column) - 1])->toBe(THUMB_SYMBOL);
});

it('sizes the thumb proportionally instead of collapsing to a single row', function () {
$column = renderScrollbarColumn(count: 100, visible: 20, offset: 0, trackHeight: 40);

$thumbRows = count(array_filter($column, fn (string $char) => $char === THUMB_SYMBOL));

expect($thumbRows)->toBeGreaterThan(1);
});

it('draws nothing when everything already fits on screen', function () {
$column = renderScrollbarColumn(count: 5, visible: 20, offset: 0, trackHeight: 40);

$thumbRows = count(array_filter($column, fn (string $char) => $char === THUMB_SYMBOL));

expect($thumbRows)->toBe(0);
});
Loading