You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/src/trezor/ui/components/tt/scroll.py

288 lines
8.3 KiB

from micropython import const
6 years ago
from trezor import loop, res, ui, utils
from .button import Button, ButtonCancel, ButtonConfirm, ButtonDefault
from .confirm import CANCELLED, CONFIRMED, Confirm
from .swipe import SWIPE_DOWN, SWIPE_UP, SWIPE_VERTICAL, Swipe
from .text import TEXT_MAX_LINES, Span, Text
if __debug__:
from apps.debug import confirm_signal, swipe_signal, notify_layout_change
if False:
from typing import List, Tuple, Union
_PAGINATED_LINE_WIDTH = const(204)
def render_scrollbar(pages: int, page: int) -> None:
BBOX = const(220)
SIZE = const(8)
padding = 14
if pages * padding > BBOX:
padding = BBOX // pages
X = const(220)
Y = (BBOX // 2) - (pages // 2) * padding
for i in range(0, pages):
if i == page:
fg = ui.FG
else:
fg = ui.GREY
ui.display.bar_radius(X, Y + i * padding, SIZE, SIZE, fg, ui.BG, 4)
def render_swipe_icon() -> None:
if utils.DISABLE_ANIMATION:
c = ui.GREY
else:
PULSE_PERIOD = const(1_200_000)
t = ui.pulse(PULSE_PERIOD)
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
icon = res.load(ui.ICON_SWIPE)
ui.display.icon(70, 205, icon, c, ui.BG)
def render_swipe_text() -> None:
6 years ago
ui.display.text_center(130, 220, "Swipe", ui.BOLD, ui.GREY, ui.BG)
class Paginated(ui.Layout):
def __init__(
self, pages: List[ui.Component], page: int = 0, one_by_one: bool = False
):
super().__init__()
self.pages = pages
self.page = page
self.one_by_one = one_by_one
def dispatch(self, event: int, x: int, y: int) -> None:
pages = self.pages
page = self.page
pages[page].dispatch(event, x, y)
if event is ui.RENDER:
length = len(pages)
if page < length - 1:
render_swipe_icon()
if self.repaint:
render_swipe_text()
if self.repaint:
render_scrollbar(length, page)
self.repaint = False
async def handle_paging(self) -> None:
if self.page == 0:
directions = SWIPE_UP
elif self.page == len(self.pages) - 1:
directions = SWIPE_DOWN
else:
directions = SWIPE_VERTICAL
if __debug__:
swipe = await loop.race(Swipe(directions), swipe_signal())
else:
swipe = await Swipe(directions)
if swipe is SWIPE_UP:
self.page = min(self.page + 1, len(self.pages) - 1)
elif swipe is SWIPE_DOWN:
self.page = max(self.page - 1, 0)
self.pages[self.page].dispatch(ui.REPAINT, 0, 0)
self.repaint = True
if __debug__:
notify_layout_change(self)
self.on_change()
def create_tasks(self) -> Tuple[loop.Task, ...]:
tasks: Tuple[loop.Task, ...] = (
self.handle_input(),
self.handle_rendering(),
self.handle_paging(),
)
if __debug__:
# XXX This isn't strictly correct, as it allows *any* Paginated layout to be
# shut down by a DebugLink confirm, even if used outside of a confirm() call
# But we don't have any such usages in the codebase, and it doesn't actually
# make much sense to use a Paginated without a way to confirm it.
return tasks + (confirm_signal(),)
else:
return tasks
def on_change(self) -> None:
if self.one_by_one:
raise ui.Result(self.page)
if __debug__:
def read_content(self) -> List[str]:
return self.pages[self.page].read_content()
class PageWithButtons(ui.Component):
def __init__(
self,
content: ui.Component,
paginated: "PaginatedWithButtons",
index: int,
count: int,
) -> None:
super().__init__()
self.content = content
self.paginated = paginated
self.index = index
self.count = count
# somewhere in the middle, we can go up or down
left = res.load(ui.ICON_BACK)
left_style = ButtonDefault
right = res.load(ui.ICON_CLICK)
right_style = ButtonDefault
if self.index == 0:
# first page, we can cancel or go down
left = res.load(ui.ICON_CANCEL)
left_style = ButtonCancel
right = res.load(ui.ICON_CLICK)
right_style = ButtonDefault
elif self.index == count - 1:
# last page, we can go up or confirm
left = res.load(ui.ICON_BACK)
left_style = ButtonDefault
right = res.load(ui.ICON_CONFIRM)
right_style = ButtonConfirm
self.left = Button(ui.grid(8, n_x=2), left, left_style)
self.left.on_click = self.on_left # type: ignore
self.right = Button(ui.grid(9, n_x=2), right, right_style)
self.right.on_click = self.on_right # type: ignore
def dispatch(self, event: int, x: int, y: int) -> None:
self.content.dispatch(event, x, y)
self.left.dispatch(event, x, y)
self.right.dispatch(event, x, y)
def on_left(self) -> None:
if self.index == 0:
self.paginated.on_cancel()
else:
self.paginated.on_up()
def on_right(self) -> None:
if self.index == self.count - 1:
self.paginated.on_confirm()
else:
self.paginated.on_down()
if __debug__:
def read_content(self) -> List[str]:
return self.content.read_content()
class PaginatedWithButtons(ui.Layout):
def __init__(
self, pages: List[ui.Component], page: int = 0, one_by_one: bool = False
) -> None:
super().__init__()
self.pages = [
PageWithButtons(p, self, i, len(pages)) for i, p in enumerate(pages)
]
self.page = page
self.one_by_one = one_by_one
def dispatch(self, event: int, x: int, y: int) -> None:
pages = self.pages
page = self.page
pages[page].dispatch(event, x, y)
if event is ui.RENDER:
render_scrollbar(len(pages), page)
def on_up(self) -> None:
self.page = max(self.page - 1, 0)
self.pages[self.page].dispatch(ui.REPAINT, 0, 0)
self.on_change()
def on_down(self) -> None:
self.page = min(self.page + 1, len(self.pages) - 1)
self.pages[self.page].dispatch(ui.REPAINT, 0, 0)
self.on_change()
def on_confirm(self) -> None:
raise ui.Result(CONFIRMED)
def on_cancel(self) -> None:
raise ui.Result(CANCELLED)
def on_change(self) -> None:
if self.one_by_one:
raise ui.Result(self.page)
if __debug__:
def read_content(self) -> List[str]:
return self.pages[self.page].read_content()
def create_tasks(self) -> Tuple[loop.Task, ...]:
return super().create_tasks() + (confirm_signal(),)
def paginate_text(
text: str,
header: str,
font: int = ui.NORMAL,
header_icon: str = ui.ICON_DEFAULT,
icon_color: int = ui.ORANGE_ICON,
break_words: bool = False,
) -> Union[Confirm, Paginated]:
span = Span(text, 0, font, break_words=break_words)
if span.count_lines() <= TEXT_MAX_LINES:
result = Text(
header,
header_icon=header_icon,
icon_color=icon_color,
new_lines=False,
break_words=break_words,
)
result.content = [font, text]
return Confirm(result)
else:
pages: List[ui.Component] = []
span.reset(
text, 0, font, break_words=break_words, line_width=_PAGINATED_LINE_WIDTH
)
while span.has_more_content():
# advance to first line of the page
span.next_line()
page = Text(
header,
header_icon=header_icon,
icon_color=icon_color,
new_lines=False,
content_offset=0,
char_offset=span.start,
line_width=_PAGINATED_LINE_WIDTH,
break_words=break_words,
render_page_overflow=False,
)
page.content = [font, text]
pages.append(page)
# roll over the remaining lines on the page
for _ in range(TEXT_MAX_LINES - 1):
span.next_line()
pages[-1] = Confirm(pages[-1])
return Paginated(pages)