diff --git a/src/Console/Components/Details.php b/src/Console/Components/Details.php index 1e2ed2c..62cbc97 100644 --- a/src/Console/Components/Details.php +++ b/src/Console/Components/Details.php @@ -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; @@ -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 { @@ -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) diff --git a/src/Console/Components/LogList.php b/src/Console/Components/LogList.php index 2f3922a..c4d9c9d 100644 --- a/src/Console/Components/LogList.php +++ b/src/Console/Components/LogList.php @@ -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; @@ -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) diff --git a/src/Console/Support/Scroll.php b/src/Console/Support/Scroll.php index c948328..6568052 100644 --- a/src/Console/Support/Scroll.php +++ b/src/Console/Support/Scroll.php @@ -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) { diff --git a/tests/Unit/ScrollTest.php b/tests/Unit/ScrollTest.php index 92fb97b..7085d67 100644 --- a/tests/Unit/ScrollTest.php +++ b/tests/Unit/ScrollTest.php @@ -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); + }); +}); diff --git a/tests/Unit/ScrollbarRenderTest.php b/tests/Unit/ScrollbarRenderTest.php new file mode 100644 index 0000000..acff7d5 --- /dev/null +++ b/tests/Unit/ScrollbarRenderTest.php @@ -0,0 +1,68 @@ +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); +});