Skip to content

Commit

Permalink
This PR adds a scrollbar to the slide contents when needed. (#195)
Browse files Browse the repository at this point in the history
* This PR adds a scrollbar to the slide contents when needed.
  • Loading branch information
d0c-s4vage committed Dec 20, 2022
1 parent 0a6667a commit efe843e
Show file tree
Hide file tree
Showing 9 changed files with 468 additions and 16 deletions.
2 changes: 2 additions & 0 deletions lookatme/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ def create_log(log_path):
stderr_handler.setLevel(logging.ERROR)
res.addHandler(stderr_handler)

logging.getLogger("markdown_it").setLevel(logging.CRITICAL)

return res


Expand Down
5 changes: 2 additions & 3 deletions lookatme/pres.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@


import os
import sys
import threading
import time

Expand All @@ -29,9 +30,7 @@
|-----------------------|-----------------|--------------------------------|
| basic markdown slides | in the terminal | anywhere with markdown support |
> **NOTE** `l | j | right arrow` advance the slides
>
> **NOTE** `q` quits
Press `l`, `j`, or `right arrow` to go to the next slide, or `q` to quit.
""",
order=0,
)
Expand Down
5 changes: 5 additions & 0 deletions lookatme/render/markdown_inline.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ def _next_token_is_html_close(ctx: Context) -> bool:
)


@contrib_first
def render_hardbreak(_, ctx: Context):
ctx.inline_push((ctx.spec_text, "\n"))


@contrib_first
def render_softbreak(_, ctx: Context):
# do not add spaces between HTML tags!
Expand Down
36 changes: 36 additions & 0 deletions lookatme/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,39 @@ class TableSchema(Schema):
)


class ScrollbarGutterSchema(Schema):
"""Schema for the slider on the scrollbar"""

fill = fields.Str(dump_default="▕")
fg = fields.Str(dump_default="#2c2c2c")
bg = fields.Str(dump_default="")


class ScrollbarSliderSchema(Schema):
"""Schema for the slider on the scrollbar"""

top_chars = fields.List(
fields.Str(), dump_default=["⡀", "⣀", "⣠", "⣤", "⣦", "⣶", "⣾", "⣿"]
)
fill = fields.Str(dump_default="⣿")
bottom_chars = fields.List(
fields.Str(), dump_default=["⠈", "⠉", "⠋", "⠛", "⠻", "⠿", "⡿", "⣿"]
)
fg = fields.Str(dump_default="#4c4c4c")
bg = fields.Str(dump_default="")


class ScrollbarSchema(Schema):
"""Schema for the scroll bar"""

gutter = fields.Nested(
ScrollbarGutterSchema, dump_default=ScrollbarGutterSchema().dump(None)
)
slider = fields.Nested(
ScrollbarSliderSchema, dump_default=ScrollbarSliderSchema().dump(None)
)


class StyleSchema(Schema):
"""Styles schema for themes and style overrides within presentations"""

Expand Down Expand Up @@ -568,6 +601,9 @@ class Meta:
"bg": "default",
},
)
scrollbar = fields.Nested(
ScrollbarSchema, dump_default=ScrollbarSchema().dump(None)
)


class MetaSchema(Schema):
Expand Down
75 changes: 64 additions & 11 deletions lookatme/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
from lookatme.utils import spec_from_style
from lookatme.widgets.clickable_text import ClickableText
import lookatme.widgets.codeblock as codeblock
from lookatme.widgets.scrollbar import Scrollbar
from lookatme.widgets.scroll_monitor import ScrollMonitor


def text(style, data, align="left"):
Expand Down Expand Up @@ -53,7 +55,7 @@ def __init__(self, ctx: Context):
self._log = lookatme.config.get_log().getChild("RENDER")

def flush_cache(self):
"""Clea everything out of the queue and the cache."""
"""Clear everything out of the queue and the cache."""
# clear all pending items
with self.queue.mutex:
self.queue.queue.clear()
Expand Down Expand Up @@ -203,7 +205,7 @@ def render_XXX(token, body, stack, loop):
|----------------------------------:|---------------------|
| Tables | Footnotes |
| Headings | *Images |
| Paragraphs | Inline HTML |
| Paragraphs | |
| Block quotes | |
| Ordered lists | |
| Unordered lists | |
Expand All @@ -212,6 +214,7 @@ def render_XXX(token, body, stack, loop):
| Double emphasis | |
| Single Emphasis | |
| Strikethrough | |
| Inline HTML | |
| Links | |
\*Images may be supported through extensions
Expand All @@ -235,10 +238,10 @@ def _render_tokens(self, tokens):
class MarkdownTui(urwid.Frame):
def __init__(self, pres, start_idx=0, no_threads=False):
"""Create a new MarkdownTui"""
# self.slide_body = urwid.Pile(urwid.SimpleListWalker([urwid.Text("test")]))
self.slide_body = urwid.ListBox(
urwid.SimpleFocusListWalker([urwid.Text("test")])
)
self.slide_body_scrollbar = Scrollbar(self.slide_body)
self.slide_title = ClickableText([""], align="center")
self.top_spacing = urwid.Filler(self.slide_title, top=0, bottom=0)
self.top_spacing_box = urwid.BoxAdapter(self.top_spacing, 1)
Expand All @@ -257,6 +260,9 @@ def __init__(self, pres, start_idx=0, no_threads=False):

self.root_margins = urwid.Padding(self, left=2, right=2)
self.root_paddings = urwid.Padding(self.slide_body, left=10, right=10)
self.scrolled_root_paddings = ScrollMonitor(
self.root_paddings, self.slide_body_scrollbar
)
self.pres = pres

self.init_ctx()
Expand All @@ -269,6 +275,8 @@ def __init__(self, pres, start_idx=0, no_threads=False):
self.ctx.loop = self.loop
self.no_threads = no_threads

self._slide_focus_cache = {}

# used to track slides that are being rendered
self.slide_renderer = SlideRenderer(self.ctx.clone())
if not no_threads:
Expand All @@ -278,7 +286,7 @@ def __init__(self, pres, start_idx=0, no_threads=False):

urwid.Frame.__init__(
self,
self.root_paddings,
self.scrolled_root_paddings,
self.top_spacing_box,
self.bottom_spacing_box,
)
Expand Down Expand Up @@ -322,8 +330,6 @@ def update_slide_num(self):
contents
```
<!-- stop -->
## 2. Metadata
Set the title explicitly through YAML metadata at the start of the slide:
Expand All @@ -338,8 +344,6 @@ def update_slide_num(self):
Slide contents
```
<!-- stop -->
> **NOTE** Metadata and styling will be covered later in this tutorial
>
> **NOTE** `h | k | delete | backspace | left arrow` reverse the slides
Expand Down Expand Up @@ -396,18 +400,35 @@ def update_creation(self):
def update_body(self):
"""Render the provided slide body"""
rendered = self.slide_renderer.render_slide(self.curr_slide)

self.slide_body.body = rendered

self._restore_slide_scroll_state()

scroll_style = config.get_style()["scrollbar"]

self.slide_body_scrollbar.gutter_spec = spec_from_style(scroll_style["gutter"])
self.slide_body_scrollbar.gutter_fill_char = scroll_style["gutter"]["fill"]

self.slide_body_scrollbar.slider_top_chars = scroll_style["slider"]["top_chars"]
self.slide_body_scrollbar.slider_bottom_chars = scroll_style["slider"][
"bottom_chars"
]
self.slide_body_scrollbar.slider_spec = spec_from_style(scroll_style["slider"])
self.slide_body_scrollbar.slider_fill_char = scroll_style["slider"]["fill"]

def update_slide_settings(self):
"""Update the slide margins and paddings"""
style = config.get_style()

# reset the base spec from the slides settings
self.ctx.spec_pop()
self.ctx.spec_push(spec_from_style(config.get_style()["slides"]))
self.ctx.spec_push(spec_from_style(style["slides"]))
# re-wrap the root widget with the new styles
self.loop.widget = self.ctx.wrap_widget(self.root_widget)

margin = config.get_style()["margin"]
padding = config.get_style()["padding"]
margin = style["margin"]
padding = style["padding"]

self.root_margins.left = margin["left"]
self.root_margins.right = margin["right"]
Expand Down Expand Up @@ -438,6 +459,8 @@ def init_ctx(self):

def reload(self):
"""Reload the input, keeping the current slide in focus"""
self._cache_slide_scroll_state()

self.init_ctx()
self.slide_renderer.ctx = self.ctx

Expand All @@ -449,6 +472,22 @@ def reload(self):
self.prep_pres(self.pres, curr_slide_idx)
self.update()

@tutor(
"general",
"Navigation and Keybindings",
r"""
Slides are navigated using vim direction keys, arrow keys, and page up/page down:
| key(s) | action |
|----------------------:|-------------------------|
| `l` `j` `right arrow` | Next slide |
| `h` `k` `left arrow` | Previous slide |
| up / down arrows | Scroll by line |
| page up / page down | Scroll by pages |
| `r` | Reload |
""",
order=0.1,
)
def keypress(self, size, key):
"""Handle keypress events"""
self._log.debug(f"KEY: {key}")
Expand Down Expand Up @@ -479,9 +518,23 @@ def keypress(self, size, key):
if new_slide_num == self.curr_slide.number:
return

self._cache_slide_scroll_state()
self.curr_slide = self.pres.slides[new_slide_num]
self.update()

def _cache_slide_scroll_state(self):
self._slide_focus_cache[self.curr_slide.number] = (
self.slide_body.offset_rows,
self.slide_body.focus_position,
)

def _restore_slide_scroll_state(self):
offset_rows, focus_pos = self._slide_focus_cache.setdefault(
self.curr_slide.number, (0, 0)
)
self.slide_body.set_focus(focus_pos)
self.slide_body.offset_rows = offset_rows

def _get_key(self, size, key):
"""Resolve the key that was pressed."""
try:
Expand Down
4 changes: 2 additions & 2 deletions lookatme/tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def __init__(
group: str,
slides_md: str,
impl_fn: Callable,
order: int,
order: float,
lazy_formatting: Optional[Callable] = None,
):
"""Create a new Tutor
Expand Down Expand Up @@ -202,7 +202,7 @@ def tutor(
group: str,
name: str,
slides_md: str,
order: int = 99999,
order: float = 99999.0,
lazy_formatting: Optional[Callable] = None,
):
"""Define tutorial slides by using this as a decorator on a function!"""
Expand Down
54 changes: 54 additions & 0 deletions lookatme/widgets/scroll_monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import urwid


from lookatme.widgets.scrollbar import Scrollbar
from lookatme.tutorial import tutor


@tutor(
"general",
"scrolling",
r"""
The scrollbar that kkjjj
Scroll with:
| amount | up key | down key |
|-------:|----------|------------|
| line | up arrow | down arrow |
| page | page up | page down |
Try scrolling through the numbers 0-49 below:
{numbers}
""".format(
numbers="\n\n ".join(str(n) for n in range(50))
),
order=0.2,
)
class ScrollMonitor(urwid.Frame):
def __init__(self, main_widget: urwid.Widget, scrollbar: Scrollbar):
self.main_widget = main_widget
self.scrollbar = scrollbar
super().__init__(self.main_widget)

def render(self, size, focus: bool = True):
if not self.scrollbar.should_display(size):
return super().render(size, focus)

width, height = size

main_canvas = super().render((width - 1, height), focus)
if not isinstance(main_canvas, urwid.CompositeCanvas):
main_canvas = urwid.CompositeCanvas(main_canvas)

scroll_canvas = self.scrollbar.render((1, height), focus)
if not isinstance(scroll_canvas, urwid.CompositeCanvas):
scroll_canvas = urwid.CompositeCanvas(scroll_canvas)

return urwid.CanvasJoin(
[
(main_canvas, None, True, main_canvas.cols()),
(scroll_canvas, None, False, scroll_canvas.cols()),
]
)
Loading

0 comments on commit efe843e

Please sign in to comment.