Packages

A framework to build terminal UI applications in Elixir

Current section

Files

Jump to
orange lib component vertical_scrollable_rect.ex
Raw

lib/component/vertical_scrollable_rect.ex

defmodule Orange.Component.VerticalScrollableRect do
@moduledoc """
A component that renders a vertical scrollable rectangle. When the container is overflown, a scrollbar is rendered.
## Attributes
- `:height` - The height of the container rect. This attribute is required.
- `:content_height` - The height of the inner content. This attribute is required.
- `:scroll_offset` - The vertical scroll offset. This attribute is required.
- `:children` - The children of the container rect. This attribute is required.
- `:title` - The title of the scrolling box. This attribute is optional.
## Examples
defmodule Example do
@behaviour Orange.Component
import Orange.Macro
@impl true
def init(_attrs), do: %{state: nil}
@impl true
def render(_state, attrs, _update) do
total_items = 30
rect style: [width: 20, height: 20, border: attrs[:highlight]] do
{
Orange.Component.VerticalScrollableRect,
height: 20,
content_height: total_items,
scroll_offset: 5,
children: for(i <- 1..total_items, do: "Item \#{i}")
}
end
end
end
![rendered result](assets/vertical-scrollable-rect-example.png)
For more examples, see `examples/scrollable_rect.exs`.
"""
@behaviour Orange.Component
import Orange.Macro
@impl true
def init(_attrs), do: %{state: nil}
@impl true
def render(_state, attrs, _update) do
overflow = overflow?(attrs)
width = if overflow, do: "calc(100% - 1)", else: "100%"
scrolling_box_style = [
width: width,
height: "100%",
border: true,
border_right: !overflow
]
scrolling_box_style = Keyword.merge(attrs[:style] || [], scrolling_box_style)
rect direction: :row, style: [height: "100%"] do
rect style: scrolling_box_style, title: attrs[:title], scroll_y: scroll_offset(attrs) do
attrs[:children]
end
if overflow, do: scroll_bar(attrs)
end
end
defp scroll_offset(attrs) do
if overflow?(attrs) do
# Minus two arrows at two ends
max = attrs[:content_height] - (attrs[:height] - 2)
min(attrs[:scroll_offset], max)
else
0
end
end
defp overflow?(attrs), do: attrs[:content_height] > attrs[:height] - 2
defp scroll_bar(attrs) do
# Minus two arrows at two ends
# Due to scrolling, we only see a portion of the content at one time. This is the height of that portion
scroll_bar_track = attrs[:height] - 2
content_viewport_height = scroll_bar_track
scroll_thumb_height =
round(content_viewport_height / attrs[:content_height] * scroll_bar_track)
total_scrolled = scroll_offset(attrs) + content_viewport_height
# Important variant that we need to preserve here:
# a. When the scroll_offset is 0, the scroll thumb MUST be at the top of the track
# b. When the scroll_offset is maximum (we can no longer scroll down), the scroll thumb MUST
# be at the bottom of the track
scroll_thumb_end = round(total_scrolled / attrs[:content_height] * scroll_bar_track)
scroll_thumb_start = scroll_thumb_end - scroll_thumb_height
rect style: [width: 1], direction: :column do
[
"▲",
List.duplicate("│", scroll_thumb_start),
List.duplicate("█", scroll_thumb_height),
List.duplicate("│", scroll_bar_track - scroll_thumb_end),
"▼"
]
|> List.flatten()
end
end
end