Skip to content

Commit

Permalink
Add view state for scroll prompts (#43)
Browse files Browse the repository at this point in the history
* Add view state for scroll prompts

* Fix search and suggest prompt view state not create bug

* Add reset for suggest and search prompt

* Fix search and suggest view state reset time

* Add zero count reset for view state

* Use view state instead of highlighted value to calculate scrollbar

* Formatting

* Refactor

---------

Co-authored-by: Jess Archer <jess@jessarcher.com>
  • Loading branch information
crazywhalecc and jessarcher authored Aug 18, 2023
1 parent 8c9348d commit b514c56
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 67 deletions.
27 changes: 27 additions & 0 deletions src/MultiSelectPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ class MultiSelectPrompt extends Prompt
*/
public int $highlighted = 0;

/**
* The index of the first visible option.
*/
public int $firstVisible = 0;

/**
* The options for the multi-select prompt.
*
Expand Down Expand Up @@ -85,6 +90,16 @@ public function labels(): array
return array_values(array_intersect_key($this->options, array_flip($this->values)));
}

/**
* The currently visible options.
*
* @return array<int|string, string>
*/
public function visible(): array
{
return array_slice($this->options, $this->firstVisible, $this->scroll, preserve_keys: true);
}

/**
* Check whether the value is currently highlighted.
*/
Expand All @@ -111,6 +126,12 @@ public function isSelected(string $value): bool
protected function highlightPrevious(): void
{
$this->highlighted = $this->highlighted === 0 ? count($this->options) - 1 : $this->highlighted - 1;

if ($this->highlighted < $this->firstVisible) {
$this->firstVisible--;
} elseif ($this->highlighted === count($this->options) - 1) {
$this->firstVisible = count($this->options) - min($this->scroll, count($this->options));
}
}

/**
Expand All @@ -119,6 +140,12 @@ protected function highlightPrevious(): void
protected function highlightNext(): void
{
$this->highlighted = $this->highlighted === count($this->options) - 1 ? 0 : $this->highlighted + 1;

if ($this->highlighted > $this->firstVisible + $this->scroll - 1) {
$this->firstVisible++;
} elseif ($this->highlighted === 0) {
$this->firstVisible = 0;
}
}

/**
Expand Down
27 changes: 27 additions & 0 deletions src/SearchPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ class SearchPrompt extends Prompt
*/
public ?int $highlighted = null;

/**
* The index of the first visible option.
*/
public int $firstVisible = 0;

/**
* The cached matches.
*
Expand Down Expand Up @@ -89,6 +94,16 @@ public function matches(): array
return $this->matches = ($this->options)($this->typedValue);
}

/**
* The currently visible matches.
*
* @return array<string>
*/
public function visible(): array
{
return array_slice($this->matches(), $this->firstVisible, $this->scroll, preserve_keys: true);
}

/**
* Highlight the previous entry, or wrap around to the last entry.
*/
Expand All @@ -103,6 +118,12 @@ protected function highlightPrevious(): void
} else {
$this->highlighted = $this->highlighted - 1;
}

if ($this->highlighted < $this->firstVisible) {
$this->firstVisible--;
} elseif ($this->highlighted === count($this->matches) - 1) {
$this->firstVisible = count($this->matches) - min($this->scroll, count($this->matches));
}
}

/**
Expand All @@ -117,6 +138,12 @@ protected function highlightNext(): void
} else {
$this->highlighted = $this->highlighted === count($this->matches) - 1 ? null : $this->highlighted + 1;
}

if ($this->highlighted > $this->firstVisible + $this->scroll - 1) {
$this->firstVisible++;
} elseif ($this->highlighted === 0 || $this->highlighted === null) {
$this->firstVisible = 0;
}
}

/**
Expand Down
27 changes: 27 additions & 0 deletions src/SelectPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ class SelectPrompt extends Prompt
*/
public int $highlighted = 0;

/**
* The index of the first visible option.
*/
public int $firstVisible = 0;

/**
* The options for the select prompt.
*
Expand Down Expand Up @@ -74,12 +79,28 @@ public function label(): ?string
}
}

/**
* The currently visible options.
*
* @return array<int|string, string>
*/
public function visible(): array
{
return array_slice($this->options, $this->firstVisible, $this->scroll, preserve_keys: true);
}

/**
* Highlight the previous entry, or wrap around to the last entry.
*/
protected function highlightPrevious(): void
{
$this->highlighted = $this->highlighted === 0 ? count($this->options) - 1 : $this->highlighted - 1;

if ($this->highlighted < $this->firstVisible) {
$this->firstVisible--;
} elseif ($this->highlighted === count($this->options) - 1) {
$this->firstVisible = count($this->options) - min($this->scroll, count($this->options));
}
}

/**
Expand All @@ -88,5 +109,11 @@ protected function highlightPrevious(): void
protected function highlightNext(): void
{
$this->highlighted = $this->highlighted === count($this->options) - 1 ? 0 : $this->highlighted + 1;

if ($this->highlighted > $this->firstVisible + $this->scroll - 1) {
$this->firstVisible++;
} elseif ($this->highlighted === 0) {
$this->firstVisible = 0;
}
}
}
27 changes: 27 additions & 0 deletions src/SuggestPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class SuggestPrompt extends Prompt
*/
public ?int $highlighted = null;

/**
* The index of the first visible option.
*/
public int $firstVisible = 0;

/**
* The options for the suggest prompt.
*
Expand Down Expand Up @@ -98,6 +103,16 @@ public function matches(): array
}));
}

/**
* The current visible matches.
*
* @return array<string>
*/
public function visible(): array
{
return array_slice($this->matches(), $this->firstVisible, $this->scroll, preserve_keys: true);
}

/**
* Highlight the previous entry, or wrap around to the last entry.
*/
Expand All @@ -112,6 +127,12 @@ protected function highlightPrevious(): void
} else {
$this->highlighted = $this->highlighted - 1;
}

if ($this->highlighted < $this->firstVisible) {
$this->firstVisible--;
} elseif ($this->highlighted === count($this->matches()) - 1) {
$this->firstVisible = count($this->matches()) - min($this->scroll, count($this->matches()));
}
}

/**
Expand All @@ -126,6 +147,12 @@ protected function highlightNext(): void
} else {
$this->highlighted = $this->highlighted === count($this->matches()) - 1 ? null : $this->highlighted + 1;
}

if ($this->highlighted > $this->firstVisible + $this->scroll - 1) {
$this->firstVisible++;
} elseif ($this->highlighted === 0 || $this->highlighted === null) {
$this->firstVisible = 0;
}
}

/**
Expand Down
57 changes: 20 additions & 37 deletions src/Themes/Default/Concerns/DrawsScrollbars.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,66 +7,49 @@
trait DrawsScrollbars
{
/**
* Scroll the given lines.
* Render a scrollbar beside the visible items.
*
* @param \Illuminate\Support\Collection<int, string> $lines
* @param \Illuminate\Support\Collection<int, string> $visible
* @return \Illuminate\Support\Collection<int, string>
*/
protected function scroll(Collection $lines, ?int $focused, int $height, int $width, string $color = 'cyan'): Collection
protected function scrollbar(Collection $visible, int $firstVisible, int $height, int $total, int $width, string $color = 'cyan'): Collection
{
if ($lines->count() <= $height) {
return $lines;
if ($height >= $total) {
return $visible;
}

$visible = $this->visible($lines, $focused, $height);
$scrollPosition = $this->scrollPosition($firstVisible, $height, $total);

return $visible
->values()
->map(fn ($line) => $this->pad($line, $width))
->map(fn ($line, $index) => match (true) {
$index === $this->scrollPosition($visible, $focused, $height, $lines->count()) => preg_replace('/.$/', $this->{$color}(''), $line),
->map(fn ($line, $index) => match ($index) {
$scrollPosition => preg_replace('/.$/', $this->{$color}(''), $line),
default => preg_replace('/.$/', $this->gray(''), $line),
});
}

/**
* Get a scrolled version of the items.
*
* @param \Illuminate\Support\Collection<int, string> $lines
* @return \Illuminate\Support\Collection<int, string>
* Return the position where the scrollbar "handle" should be rendered.
*/
protected function visible(Collection $lines, ?int $focused, int $height): Collection
protected function scrollPosition(int $firstVisible, int $height, int $total): int
{
if ($lines->count() <= $height) {
return $lines;
}

if ($focused === null || $focused < $height) {
return $lines->slice(0, $height);
if ($firstVisible === 0) {
return 0;
}

return $lines->slice($focused - $height + 1, $height);
}
$maxPosition = $total - $height;

/**
* Scroll the given lines.
*
* @param \Illuminate\Support\Collection<int, string> $visible
*/
protected function scrollPosition(Collection $visible, ?int $focused, int $height, int $total): int
{
if ($focused < $height) {
return 0;
if ($firstVisible === $maxPosition) {
return $height - 1;
}

if ($focused === $total - 1) {
return $total - 1;
if ($height <= 2) {
return -1;
}

$percent = ($focused + 1 - $height) / ($total - $height);

$keys = $visible->slice(1, -1)->keys();
$position = (int) ceil($percent * count($keys) - 1);
$percent = $firstVisible / $maxPosition;

return $keys[$position] ?? 0;
return (int) round($percent * ($height - 3)) + 1;
}
}
18 changes: 11 additions & 7 deletions src/Themes/Default/MultiSelectPromptRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class MultiSelectPromptRenderer extends Renderer
*/
public function __invoke(MultiSelectPrompt $prompt): string
{
$prompt->scroll = min($prompt->scroll, $prompt->terminal()->lines() - 5);

return match ($prompt->state) {
'submit' => $this
->box(
Expand Down Expand Up @@ -55,11 +57,11 @@ public function __invoke(MultiSelectPrompt $prompt): string
*/
protected function renderOptions(MultiSelectPrompt $prompt): string
{
return $this->scroll(
collect($prompt->options)
->values()
return $this->scrollbar(
collect($prompt->visible())
->map(fn ($label) => $this->truncate($label, $prompt->terminal()->cols() - 12))
->map(function ($label, $index) use ($prompt) {
->map(function ($label, $key) use ($prompt) {
$index = array_search($key, array_keys($prompt->options));
$active = $index === $prompt->highlighted;
if (array_is_list($prompt->options)) {
$value = $prompt->options[$index];
Expand All @@ -83,9 +85,11 @@ protected function renderOptions(MultiSelectPrompt $prompt): string
$selected => " {$this->cyan('')} {$this->dim($label)} ",
default => " {$this->dim('')} {$this->dim($label)} ",
};
}),
$prompt->highlighted,
min($prompt->scroll, $prompt->terminal()->lines() - 5),
})
->values(),
$prompt->firstVisible,
$prompt->scroll,
count($prompt->options),
min($this->longest($prompt->options, padding: 6), $prompt->terminal()->cols() - 6),
$prompt->state === 'cancel' ? 'dim' : 'cyan'
)->implode(PHP_EOL);
Expand Down
23 changes: 14 additions & 9 deletions src/Themes/Default/SearchPromptRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class SearchPromptRenderer extends Renderer
*/
public function __invoke(SearchPrompt $prompt): string
{
$prompt->scroll = min($prompt->scroll, $prompt->terminal()->lines() - 7);
$maxWidth = $prompt->terminal()->cols() - 6;

return match ($prompt->state) {
Expand Down Expand Up @@ -105,16 +106,20 @@ protected function renderOptions(SearchPrompt $prompt): string
return $this->gray(' '.($prompt->state === 'searching' ? 'Searching...' : 'No results.'));
}

return $this->scroll(
collect($prompt->matches())
->values()
return $this->scrollbar(
collect($prompt->visible())
->map(fn ($label) => $this->truncate($label, $prompt->terminal()->cols() - 10))
->map(fn ($label, $i) => $prompt->highlighted === $i
? "{$this->cyan('')} {$label} "
: " {$this->dim($label)} "
),
$prompt->highlighted,
min($prompt->scroll, $prompt->terminal()->lines() - 7),
->map(function ($label, $key) use ($prompt) {
$index = array_search($key, array_keys($prompt->matches()));

return $prompt->highlighted === $index
? "{$this->cyan('')} {$label} "
: " {$this->dim($label)} ";
})
->values(),
$prompt->firstVisible,
$prompt->scroll,
count($prompt->matches()),
min($this->longest($prompt->matches(), padding: 4), $prompt->terminal()->cols() - 6)
)->implode(PHP_EOL);
}
Expand Down
Loading

0 comments on commit b514c56

Please sign in to comment.