Source code for lookatme.widgets.scrollbar
import math
from typing import List, Optional, Tuple
import urwid
from urwid.signals import connect_signal
from lookatme.widgets.smart_attr_spec import SmartAttrSpec
[docs]class Scrollbar(urwid.Widget):
"""A scrollbar that reflects the current scroll state of a list box.
Non-selectable, non-interactive, informative only. Especially useful as
an overlay or a box column so that you can have additional padding around
your ListBox.
"""
_sizing = frozenset([urwid.BOX])
_selectable = False
ignore_focus = True
def __init__(self, listbox: urwid.ListBox):
super().__init__()
self.scroll_percent = 1.0
self.slider_top_chars = ["⡀", "⣀", "⣠", "⣤", "⣦", "⣶", "⣾", "⣿"]
self.slider_bottom_chars = ["⠈", "⠉", "⠋", "⠛", "⠻", "⠿", "⡿", "⣿"]
self.slider_fill_char = "⣿"
self.slider_spec: urwid.AttrSpec = SmartAttrSpec("#4c4c4c", "")
self.gutter_fill_char = "▕"
self.gutter_spec: urwid.AttrSpec = SmartAttrSpec("#2c2c2c", "")
self._listbox_widget_size_cache: Optional[
Tuple[List[Tuple[int, int, int]], Tuple, bool, int]
] = None
self.listbox = listbox
connect_signal(self.listbox.body, "modified", self._invalidate)
self.listbox.render_orig = self.listbox.render
self.listbox.render = self._listbox_render
def _listbox_invalidate(self):
self.listbox._invalidate_orig()
self._invalidate()
def _listbox_render(self, size, focus: bool = False):
self.listbox._last_size = size
self.listbox._last_focus = focus
res = self.listbox.render_orig(size, focus)
self._invalidate()
return res
[docs] def update_scroll_percent(
self, size: Tuple[int, int], focus: bool = False
) -> Tuple[int, int, int, int]:
"""Update the scroll percent of the monitored ListBox"""
before, visible, after, total = self._get_listbox_visible_scroll_range()
scroll_percent = before / float(before + after)
self.scroll_percent = max(0.0, scroll_percent)
return before, visible, after, total
[docs] def render(self, size: Tuple[int, int], focus: bool = False):
"""Please use `needs_scrollbar()` if manually deciding to render the
Scrollbar (e.g. if you're overlaying the rendered results onto a canvas)
"""
if not self.slider_top_chars:
self.slider_top_chars.append(self.slider_fill_char)
if not self.slider_bottom_chars:
self.slider_bottom_chars.append(self.slider_fill_char)
before, visible, after, total = self.update_scroll_percent(size, focus)
_, height = size
total_chars = height
slider_height = max(height * float(visible) / total, 2.0)
fill_count = slider_height
slider_end_idx_f = (
slider_height + (total_chars - slider_height) * self.scroll_percent
)
slider_end_val = slider_end_idx_f - math.floor(slider_end_idx_f)
if slider_end_val == 0.0:
slider_end_char = ""
else:
fill_count -= slider_end_val
slider_end_char = self.slider_bottom_chars[
int(slider_end_val // (1.0 / len(self.slider_bottom_chars)))
]
slider_start_idx_f = slider_end_idx_f - slider_height
slider_start_val = 1.0 - (slider_start_idx_f - math.floor(slider_start_idx_f))
if slider_start_val == 1.0:
slider_start_char = ""
else:
fill_count -= slider_start_val
slider_start_char = self.slider_top_chars[
int(slider_start_val // (1.0 / len(self.slider_top_chars)))
]
slider_start_idx = math.floor(slider_start_idx_f)
slider_end_idx = math.ceil(slider_end_idx_f)
slider_chars = "{}{}{}".format(
slider_start_char,
self.slider_fill_char * round(fill_count),
slider_end_char,
)
scroll_text = [
(
self.gutter_spec,
"\n".join(self.gutter_fill_char * slider_start_idx),
),
(self.slider_spec, "\n".join(slider_chars)),
(
self.gutter_spec,
"\n".join(self.gutter_fill_char * (total_chars - slider_end_idx)),
),
]
return urwid.Text(scroll_text).render((1,), False)
def _get_listbox_visible_scroll_range(self) -> Tuple[int, int, int, int]:
"""Return a tuple containing: (visible_start_idx, visible_stop_idx, total_rows)"""
# we're assuming that the 1 char for the scrollbar is already taken
# out
size = self.listbox._last_size # type: ignore
height = size[1]
widget_sizes = self._get_listbox_widget_sizes(size, self.listbox._last_focus)
# using calculate_visible has turned out to be the most reliable way
# to determine the bounds of the current view of the ListBox. Other
# ways had problems when the screen is resized (e.g., scroll to the
middle, top, bottom = self.listbox.calculate_visible(size, True)
# from urwid's source:
# *middle*
# (*row offset*(when +ve) or *inset*(when -ve),
# *focus widget*, *focus position*, *focus rows*,
# *cursor coords* or ``None``)
row_offset, focus_widget, focus_pos, focus_rows, cursor = middle
top_trim, top_widgets = top
if len(top_widgets) > 0:
# from urwid's source:
# *top*
# (*# lines to trim off top*,
# list of (*widget*, *position*, *rows*) tuples above focus
# in order from bottom to top)
top_w, top_pos, top_rows = top_widgets[-1]
before = widget_sizes[top_pos][1] + top_trim
else:
before = widget_sizes[focus_pos][1] + abs(row_offset)
total = widget_sizes[-1][-1]
after = total - before - height
return before, height, after, total
def _get_listbox_widget_sizes(self, size, focus):
curr_body_id = id(self.listbox.body)
if self._listbox_widget_size_cache is not None:
(
cached_widget_sizes,
cached_size,
cached_focus,
cached_body_id,
) = self._listbox_widget_size_cache
if (
True
and cached_size == size
and cached_focus == focus
and cached_body_id == curr_body_id
):
return cached_widget_sizes
widget_sizes = []
total = 0
for widget in self.listbox.body:
w_rows = widget.rows((size[0],))
widget_sizes.append((w_rows, total, total + w_rows))
total += w_rows
self._listbox_widget_size_cache = (widget_sizes, size, focus, curr_body_id)
return widget_sizes
[docs] def should_display(self, size, focus: bool = False):
# will return ['top', 'bottom'] if both ends of the content are visible
return len(self.listbox.ends_visible(size, focus)) != 2