mirror of
				https://github.com/Textualize/textual.git
				synced 2025-10-17 02:38:12 +03:00 
			
		
		
		
	markdowns
This commit is contained in:
		| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Will McGugan
					Will McGugan