markdowns

This commit is contained in:
Will McGugan
2024-11-15 12:09:39 +00:00
parent 672daf8aa8
commit df630305d2
3 changed files with 186 additions and 58 deletions

View File

@@ -128,7 +128,6 @@ Hit `return` to toggle an checkbox / radio button, when focused.
"""
RADIOSET_MD = """\
### Radio Sets
A *radio set* is a list of mutually exclusive options.
@@ -408,6 +407,108 @@ def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:
rich_log.write(traceback, animate=True)
class Markdowns(containers.VerticalGroup):
DEFAULT_CLASSES = "column"
DEFAULT_CSS = """
Markdowns {
#container {
border: tall transparent;
height: 16;
padding: 0 1;
&:focus { border: tall $border; }
&.-maximized { height: 1fr; }
}
#movies {
padding: 0 1;
MarkdownBlock { padding: 0 1 0 0; }
}
}
"""
MD_MD = """\
## Markdown
Display Markdown in your apps with the Markdown widget.
Most of the text on this page is Markdown.
Here's an AI generated Markdown document:
"""
MOVIES_MD = """\
# The Golden Age of Action Cinema: The 1980s
The 1980s marked a transformative era in action cinema, defined by **excessive machismo**, explosive practical effects, and unforgettable one-liners. This decade gave birth to many of Hollywood's most enduring action franchises, from _Die Hard_ to _Rambo_, setting templates that filmmakers still reference today.
## Technical Innovation
Technologically, the 80s represented a sweet spot between practical effects and early CGI. Filmmakers relied heavily on:
* Practical stunts
* Pyrotechnics
* Hand-built models
These elements lent the films a tangible quality that many argue remains superior to modern digital effects.
## The Action Hero Archetype
The quintessential action hero emerged during this period, with key characteristics:
1. Impressive physique
2. Military background
3. Anti-authority attitude
4. Memorable catchphrases
> "I'll be back" - The Terminator (1984)
Heroes like Arnold Schwarzenegger and Sylvester Stallone became global icons. However, the decade also saw more nuanced characters emerge, like Bruce Willis's everyman John McClane in *Die Hard*, and powerful female protagonists like Sigourney Weaver's Ellen Ripley in *Aliens*.
### Political Influence
Cold War politics heavily influenced these films' narratives, with many plots featuring American heroes facing off against Soviet adversaries. This political subtext, combined with themes of individual triumph over bureaucratic systems, perfectly captured the era's zeitgeist.
---
While often dismissed as simple entertainment, 80s action films left an indelible mark on cinema history, influencing everything from filming techniques to narrative structures, and continuing to inspire filmmakers and delight audiences decades later.
"""
def compose(self) -> ComposeResult:
yield Markdown(self.MD_MD)
with containers.VerticalScroll(
id="container", can_focus=True, can_maximize=True
):
yield Markdown(self.MOVIES_MD, id="movies")
class Selects(containers.VerticalGroup):
DEFAULT_CLASSES = "column"
SELECTS_MD = """\
## Selects
Selects (AKA *Combo boxes*), present a list of options in a menu that may be expanded by the user.
"""
HEROS = [
"Arnold Schwarzenegger",
"Brigitte Nielsen",
"Bruce Willis",
"Carl Weathers",
"Chuck Norris",
"Dolph Lundgren",
"Grace Jones",
"Harrison Ford",
"Jean-Claude Van Damme",
"Kurt Russell",
"Linda Hamilton",
"Mel Gibson",
"Michelle Yeoh",
"Sigourney Weaver",
"Sylvester Stallone",
]
def compose(self) -> ComposeResult:
yield Markdown(self.SELECTS_MD)
yield Select.from_values(self.HEROS, prompt="80s action hero")
class Sparklines(containers.VerticalGroup):
"""Demonstrates sparklines."""
@@ -514,6 +615,10 @@ Switches {
def on_switch_changed(self, event: Switch.Changed) -> None:
# Don't issue more Changed events
if not event.value:
self.query_one("#textual-dark", Switch).value = True
return
with self.prevent(Switch.Changed):
# Reset all other switches
for switch in self.query("Switch").results(Switch):
@@ -578,10 +683,28 @@ from textual import App, ComposeResult
prompt="Highlight language",
)
yield TextArea(self.DEFAULT_TEXT, show_line_numbers=True)
yield TextArea(self.DEFAULT_TEXT, show_line_numbers=True, language=None)
def on_select_changed(self, event: Select.Changed) -> None:
self.query_one(TextArea).language = (event.value or "").lower()
self.query_one(TextArea).language = (
event.value.lower() if isinstance(event.value, str) else None
)
class YourWidgets(containers.VerticalGroup):
DEFAULT_CLASSES = "column"
YOUR_MD = """\
## Your widget here
The Textual API allows you to [build custom re-usable widgets](https://textual.textualize.io/guide/widgets/#custom-widgets) and share them across projects.
Custom widgets can be themed, just like the builtin widget library.
Combine existing widgets to add new functionality, or use the powerful [Line API](https://textual.textualize.io/guide/widgets/#line-api) for unique creations.
"""
def compose(self) -> ComposeResult:
yield Markdown(self.YOUR_MD)
class WidgetsScreen(PageScreen):
@@ -607,7 +730,7 @@ class WidgetsScreen(PageScreen):
BINDINGS = [Binding("escape", "blur", "Unfocus any focused widget", show=False)]
def compose(self) -> ComposeResult:
with lazy.Reveal(containers.VerticalScroll(can_focus=False)):
with lazy.Reveal(containers.VerticalScroll(can_focus=True)):
yield Markdown(WIDGETS_MD, classes="column")
yield Buttons()
yield Checkboxes()
@@ -615,7 +738,10 @@ class WidgetsScreen(PageScreen):
yield Inputs()
yield ListViews()
yield Logs()
yield Markdowns()
yield Selects()
yield Sparklines()
yield Switches()
yield TextAreas()
yield YourWidgets()
yield Footer()

View File

@@ -4,8 +4,6 @@ Tools for lazy loading widgets.
from __future__ import annotations
from functools import partial
from textual.widget import Widget
@@ -66,82 +64,72 @@ class Lazy(Widget):
class Reveal(Widget):
"""Similar to [Lazy][textual.lazy.Lazy], but mounts children sequentially.
This is useful when you have so many child widgets that there is a noticeable delay before
you see anything. By mounting the children over several frames, the user will feel that
something is happening.
Example:
```python
def compose(self) -> ComposeResult:
with lazy.Reveal(containers.VerticalScroll(can_focus=False)):
yield Markdown(WIDGETS_MD, classes="column")
yield Buttons()
yield Checkboxes()
yield Datatables()
yield Inputs()
yield ListViews()
yield Logs()
yield Sparklines()
yield Footer()
```
"""
DEFAULT_CSS = """
Reveal {
display: none;
}
"""
def __init__(self, widget: Widget, delay: float = 1 / 60) -> None:
"""Similar to [Lazy][textual.lazy.Lazy], but also displays *children* sequentially.
The first frame will display the first child with all other children hidden.
The remaining children will be displayed 1-by-1, over as may frames are required.
This is useful when you have so many child widgets that there is a noticeable delay before
you see anything. By mounting the children over several frames, the user will feel that
something is happening.
Example:
```python
def compose(self) -> ComposeResult:
with lazy.Reveal(containers.VerticalScroll(can_focus=False)):
yield Markdown(WIDGETS_MD, classes="column")
yield Buttons()
yield Checkboxes()
yield Datatables()
yield Inputs()
yield ListViews()
yield Logs()
yield Sparklines()
yield Footer()
```
def __init__(self, widget: Widget) -> None:
"""
Args:
widget: A widget that should be mounted after a refresh.
delay: A (short) delay between mounting widgets.
widget: A widget to mount.
"""
self._replace_widget = widget
self._delay = delay
self._widgets: list[Widget] = []
super().__init__()
@classmethod
def _reveal(cls, parent: Widget, delay: float = 1 / 60) -> None:
def _reveal(cls, parent: Widget, widgets: list[Widget]) -> None:
"""Reveal children lazily.
Args:
parent: The parent widget.
delay: A delay between reveals.
widgets: Child widgets.
"""
def check_children() -> None:
"""Check for un-displayed children."""
iter_children = iter(parent.children)
for child in iter_children:
if not child.display:
child.display = True
break
for child in iter_children:
if not child.display:
parent.set_timer(
delay, partial(parent.call_after_refresh, check_children)
)
break
async def check_children() -> None:
"""Check for pending children"""
if not widgets:
return
widget = widgets.pop(0)
await parent.mount(widget)
if widgets:
parent.call_next(check_children)
check_children()
parent.call_next(check_children)
def compose_add_child(self, widget: Widget) -> None:
widget.display = False
self._replace_widget.compose_add_child(widget)
self._widgets.append(widget)
async def mount_composed_widgets(self, widgets: list[Widget]) -> None:
parent = self.parent
if parent is None:
return
assert isinstance(parent, Widget)
if self._replace_widget.children:
self._replace_widget.children[0].display = True
await parent.mount(self._replace_widget, after=self)
await self.remove()
self._reveal(self._replace_widget, self._delay)
self._reveal(self._replace_widget, self._widgets.copy())
self._widgets.clear()

View File

@@ -9,6 +9,7 @@ from asyncio import create_task, gather, wait
from collections import Counter
from contextlib import asynccontextmanager
from fractions import Fraction
from time import monotonic
from types import TracebackType
from typing import (
TYPE_CHECKING,
@@ -491,6 +492,8 @@ class Widget(DOMNode):
"""Used to cache :last-of-type pseudoclass state."""
self._odd: tuple[int, bool] = (-1, False)
"""Used to cache :odd pseudoclass state."""
self._last_scroll_time = monotonic()
"""Time of last scroll."""
@property
def is_mounted(self) -> bool:
@@ -2211,6 +2214,7 @@ class Widget(DOMNode):
@property
def is_scrolling(self) -> bool:
"""Is this widget currently scrolling?"""
current_time = monotonic()
for node in self.ancestors:
if not isinstance(node, Widget):
break
@@ -2219,6 +2223,9 @@ class Widget(DOMNode):
or node.scroll_y != node.scroll_target_y
):
return True
if current_time - node._last_scroll_time < 0.1:
# Scroll ended very recently
return True
return False
@property
@@ -2360,6 +2367,11 @@ class Widget(DOMNode):
animator.force_stop_animation(self, "scroll_x")
animator.force_stop_animation(self, "scroll_y")
def _animate_on_complete():
self._last_scroll_time = monotonic()
if on_complete is not None:
self.call_next(on_complete)
if animate:
# TODO: configure animation speed
if duration is None and speed is None:
@@ -2378,7 +2390,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
on_complete=on_complete,
on_complete=_animate_on_complete,
level=level,
)
scrolled_x = True
@@ -2392,7 +2404,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
on_complete=on_complete,
on_complete=_animate_on_complete,
level=level,
)
scrolled_y = True
@@ -2409,6 +2421,7 @@ class Widget(DOMNode):
self.scroll_target_y = self.scroll_y = y
scrolled_y = scroll_y != self.scroll_y
self._last_scroll_time = monotonic()
if on_complete is not None:
self.call_after_refresh(on_complete)
@@ -2892,6 +2905,7 @@ class Widget(DOMNode):
force=force,
on_complete=on_complete,
level=level,
immediate=immediate,
)
def _scroll_up_for_pointer(