diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..be0b470 --- /dev/null +++ b/404.html @@ -0,0 +1,447 @@ + + + + + + + + + + + + + + + + + + Spiel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..35bcd32 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +www.spiel.how diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/index.html b/api/index.html new file mode 100644 index 0000000..95c77ac --- /dev/null +++ b/api/index.html @@ -0,0 +1,1982 @@ + + + + + + + + + + + + + + + + + + + + + + + + API Reference - Spiel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + +

API Reference

+

Decks and Slides

+ + +
+ + + +

+ spiel.Deck + + + + dataclass + + +

+ + +
+

+ Bases: Sequence[Slide]

+ + +

Represents a "deck" of "slides": a presentation.

+ + + + + +
+ + + + + + + +
+ + + +

+name: str + + + instance-attribute + + +

+ + +
+ +

The name of the Deck, which will be displayed in the footer.

+
+ +
+ +
+ + + +

+default_transition: Type[Transition] | None = Swipe + + + class-attribute + instance-attribute + + +

+ + +
+ +

The default slide transition animation; +used if the slide being moved to does not specify its own transition. +Defaults to the Swipe transition. +Set to None for no transition animation.

+
+ +
+ + + +
+ + + +

+slide(title='', bindings=None, transition=None) + +

+ + +
+ +

A decorator that creates a new slide in the deck, +with the decorated function as the Slide.content.

+ + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
title +

The title to display for the slide.

+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
bindings +

A mapping of +keys +to callables to be executed when those keys are pressed, +when on this slide.

+

+ + TYPE: + Mapping[str, Callable[..., None]] | None + + + DEFAULT: + None + +

+
transition +

The transition animation to use when moving to this slide. +Set to None to use the +Deck.default_transition +of the deck this slide is in.

+

+ + TYPE: + Type[Transition] | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+add_slides(*slides) + +

+ + +
+ +

Add Slides to a Deck.

+

This function is primarily useful when adding multiple slides at once, +probably generated programmatically. +If adding a single slide, prefer the Deck.slide decorator.

+ + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
*slides +

The Slides to add.

+

+ + TYPE: + Slide + + + DEFAULT: + () + +

+
+ +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ spiel.Slide + + + + dataclass + + +

+ + +
+ + +

Represents a single slide in the presentation.

+ + + + + +
+ + + + + + + +
+ + + +

+title: str = '' + + + class-attribute + instance-attribute + + +

+ + +
+ +

The title of the Slide, which will be displayed in the footer.

+
+ +
+ +
+ + + +

+content: Content = Text + + + class-attribute + instance-attribute + + +

+ + +
+ +

A callable that is invoked by Spiel to display the slide's content.

+

The function may optionally take arguments with these names:

+
    +
  • trigger: The current Trigger state, for use in animations.
  • +
+
+ +
+ +
+ + + +

+bindings: Mapping[str, Callable[..., None]] = field(default_factory=dict) + + + class-attribute + instance-attribute + + +

+ + +
+ +

A mapping of +keys +to callables to be executed when those keys are pressed, +when on this slide.

+
+ +
+ +
+ + + +

+transition: Type[Transition] | None = Swipe + + + class-attribute + instance-attribute + + +

+ + +
+ +

The transition animation to use when moving to this slide. +Set to None to use the +Deck.default_transition +of the deck this slide is in.

+
+ +
+ + + + + +
+ +
+ +

Rendering Content

+ + +
+ + + +

+ spiel.Triggers + + + + dataclass + + +

+ + +
+

+ Bases: Sequence[float]

+ + +

Provides information to Slide.content about the current slide's "trigger state".

+

Triggers is a Sequence of times +(produced by time.monotonic) +that the current slide was triggered at. +Note that the slide will be triggered once when it starts being displayed, +so the first trigger time will be the time when the slide started being displayed.

+ + + + + +
+ + + + + + + +
+ + + +

+now: float + + + instance-attribute + + +

+ + +
+ +

The time that the slide content is being rendered at. +Use this is as a single consistent value to base relative times on.

+
+ +
+ +
+ + + +

+time_since_last_trigger: float + + + property + cached + + +

+ + +
+ +

The elapsed time since the most recent trigger.

+
+ +
+ +
+ + + +

+time_since_first_trigger: float + + + property + cached + + +

+ + +
+ +

The elapsed time since the first trigger, +which is equivalent to the time since the slide started being displayed.

+
+ +
+ +
+ + + +

+triggered: bool + + + property + cached + + +

+ + +
+ +

Returns whether the slide has been manually triggered +(i.e., this ignores the initial trigger from when the slide starts being displayed).

+
+ +
+ + + +
+ + + +

+take(iter, offset=1) + +

+ + +
+ +

Takes elements from the iterable iter +equal to the number of times in the Triggers minus the offset.

+ + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
iter +

The iterable to take elements from.

+

+ + TYPE: + Iterable[T] + +

+
offset +

This offset will be subtracted from the number of triggers, +reducing the number of elements that will be returned. +It defaults to 1 to ignore the automatic trigger from when the +slide starts being shown.

+

+ + TYPE: + int + + + DEFAULT: + 1 + +

+
+ + + + + + + + + + + + + + +
RETURNSDESCRIPTION
+ + Iterator[T] + + +

An iterator over the first len(self) - offset elements of iter.

+
+ +
+ +
+ + + +
+ +
+ +

Transitions

+ + +
+ + + +

+ spiel.Direction + + +

+ + +
+

+ Bases: Enum

+ + +

An enumeration that describes which direction a slide transition +animation should move in: whether we're going to the next slide, +or to the previous slide.

+ + + + + +
+ + + + + + + +
+ + + +

+Next = 'next' + + + class-attribute + instance-attribute + + +

+ + +
+ +

Indicates that the transition should handle going to the next slide.

+
+ +
+ +
+ + + +

+Previous = 'previous' + + + class-attribute + instance-attribute + + +

+ + +
+ +

Indicates that the transition should handle going to the previous slide.

+
+ +
+ + + + + +
+ +
+ +
+ +
+ + + +

+ spiel.Transition + + +

+ + +
+

+ Bases: Protocol

+ + +

A protocol that describes how to implement a transition animation.

+

See Writing Custom Transitions +for more details on how to implement the protocol.

+ + + + + +
+ + + + + + + + + +
+ + + +

+initialize(from_widget, to_widget, direction) + +

+ + +
+ +

A hook function to set up any CSS that should be present at the start of the transition.

+ + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
from_widget +

The widget showing the slide that we are leaving.

+

+ + TYPE: + Widget + +

+
to_widget +

The widget showing the slide that we are entering.

+

+ + TYPE: + Widget + +

+
direction +

The desired direction of the transition animation.

+

+ + TYPE: + Direction + +

+
+ +
+ +
+ +
+ + + +

+progress(from_widget, to_widget, direction, progress) + +

+ + +
+ +

A hook function that is called each time the progress +of the transition animation updates.

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
from_widget +

The widget showing the slide that we are leaving.

+

+ + TYPE: + Widget + +

+
to_widget +

The widget showing the slide that we are entering.

+

+ + TYPE: + Widget + +

+
direction +

The desired direction of the transition animation.

+

+ + TYPE: + Direction + +

+
progress +

The progress of the animation, as a percentage +(e.g., initial state is 0, final state is 100). +Note that this is not necessarily bounded between 0 and 100, +nor is it necessarily monotonically increasing, +depending on the underlying Textual animation easing function, +which may overshoot or bounce. +However, it will always start at 0 and end at 100, +no matter which direction the transition should move in.

+

+ + TYPE: + float + +

+
+ +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ spiel.Swipe + + +

+ + +
+

+ Bases: Transition

+ + +

A transition where the current and incoming slide are placed side-by-side +and gradually slide across the screen, +with the current slide leaving and the incoming slide entering.

+ + + + + +
+ + + + + + + + + + + +
+ +
+ +

Presenting Decks

+ + +
+ + + +

+spiel.present(deck_path, watch_path=None) + +

+ + +
+ +

Present the deck defined in the given deck_path.

+ + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
deck_path +

The file to look for a deck in.

+

+ + TYPE: + Path | str + +

+
watch_path +

When filesystem changes are detected below this path (recursively), reload the deck from the deck_path. +If None (the default), use the parent directory of the deck_path.

+

+ + TYPE: + Path | str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/assets/_mkdocstrings.css b/assets/_mkdocstrings.css new file mode 100644 index 0000000..a65078d --- /dev/null +++ b/assets/_mkdocstrings.css @@ -0,0 +1,36 @@ + +/* Don't capitalize names. */ +h5.doc-heading { + text-transform: none !important; +} + +/* Avoid breaking parameters name, etc. in table cells. */ +.doc-contents td code { + word-break: normal !important; +} + +/* For pieces of Markdown rendered in table cells. */ +.doc-contents td p { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +/* Max width for docstring sections tables. */ +.doc .md-typeset__table, +.doc .md-typeset__table table { + display: table !important; + width: 100%; +} +.doc .md-typeset__table tr { + display: table-row; +} + +/* Avoid line breaks in rendered fields. */ +.field-body p { + display: inline; +} + +/* Defaults in Spacy table style. */ +.doc-param-default { + float: right; +} diff --git a/assets/deck.svg b/assets/deck.svg new file mode 100644 index 0000000..736c84b --- /dev/null +++ b/assets/deck.svg @@ -0,0 +1,381 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spiel Demo Deck + + + + + + + + + + ╭─────────── 1 | What is Spiel? â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â•®â•­â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ 2 | Decks and Slides â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â•®â•­â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ 3 | Dynamic Content â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â•® +││││││ +│What isWhy use││Decks are made of Slides││Slides can have dynamic│ +│Spiel?Spiel?││││content!│ +│││Here's the code for Deck and│││ +│Spiel is aIt's fun!││Slide!││Since slides are created using│ +│││││normal Python code,any output│ +│ContributiInspiration││The source code is pulled││you can imagine producing via│ +│ngs││directly from the definitions││Python can make it into your│ +│││via inspect.getsource.││slides.│ +│PleaseBrandon││┌──────────────────â”┌───────────────────â”│││ +╰───────────────────────────────────────────╯╰───────────────────────────────────────────╯╰───────────────────────────────────────────╯ +╭────────────── 4 | Triggers â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â•®â•­â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ 5 | Triggers: Reveals â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â•®â•­â”€â”€â”€â”€â”€â”€â”€â”€ 6 | Triggers: Animations â”€â”€â”€â”€â”€â”€â”€â”€â”€â•® +││││││ +│Triggers││Triggers: Reveals││Triggers: Animations│ +││││││ +│Triggers are a mechanism for││Triggers can be useful even││Here's an example of how│ +│making dynamic content that││without considering their││triggers can be used to build│ +│depends on relative time.││tracking of relative time.││more complex animations.│ +││││││ +│Triggers can be used to││We can track the number of││The position and facing│ +│implement effects like fades,││times the slide has been││direction of the bullet are│ +│motion, and other "animations".││triggered to graduallyreveal││calculated deterministically│ +│││content.││based on the time since the│ +╰───────────────────────────────────────────╯╰───────────────────────────────────────────╯╰───────────────────────────────────────────╯ +╭──────────────── 7 | Views â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â•®â•­â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ 8 | Displaying Images â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â•®â•­â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ 9 | Watch Mode â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â•® +│││â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”┓│││ +│Deck View││Images┃▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀┃││Developing a Deck│ +│││┃▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀┃│││ +│Try pressing d to go into││Spiel can┃▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀┃││Spiel will reload your deck as│ +│"deck" view.You can move││display┃▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀┃││you edit it to make development│ +│between slides in deck view││images...┃▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀┃││easier.│ +│using your arrow keys (right â†’,││sort of!┃▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀┃│││ +│left â†, up â†‘, and down â†“).││┃▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀┃││The reload is triggered│ +│││Spiel┃▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀┃││whenever any files under the│ +│Press enter or escape to go││includes┃▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀┃││path passed to the--watch│ +│back to "slide" view (this││an Imageâ”—â”â”â”â” tree.jpg â”â”â”â”â”┛││argument of spiel present│ +╰───────────────────────────────────────────╯╰───────────────────────────────────────────╯╰───────────────────────────────────────────╯ +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +Spiel Demo Deck | Triggers: Reveals                                                                  2022-12-17 03:31 PM   [05 / 11] + + + diff --git a/assets/demo.svg b/assets/demo.svg new file mode 100644 index 0000000..a82284e --- /dev/null +++ b/assets/demo.svg @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spiel Demo Deck + + + + + + + + + + +What is Spiel?Why use Spiel? + +Spiel is a framework for building and presentingIt's fun! +richly-styled presentations in your terminal using +Python.It's weird! + +Spiel uses Rich to render slide content.Anything youWhy not? +can display with Rich, you can display with Spiel (plus +some other things)!Maybe you shouldn't. + +Use your right â†’ and left â† arrows keys to go forwardsHonestly, it's unclear whether it's a good idea. +and backwards through the deck.Press ctrl-c to exit. +There's always Powerpoint! +Press ? at any time to see the help screen, which +describes all of the built-in actions you can take. + +ContributingInspirations + +Please report bugs via GitHub Issues.Brandon Rhodes' PyCon 2017 and North Bay Python 2017 +talks. +If you have ideas about how Spiel can be improved,or +you have a cool deck to show off,please post to GitHubDavid Beazley's Lambda Calculus from the Ground Up +Discussions.tutorial at PyCon 2019. + +LaTeX's Beamer document class. + + + + + + + +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +Spiel Demo Deck | What is Spiel?                                                                2022-12-17 03:31 PM   [01 / 11] + + + diff --git a/assets/favicon.png b/assets/favicon.png new file mode 100644 index 0000000..7170d79 Binary files /dev/null and b/assets/favicon.png differ diff --git a/assets/help.svg b/assets/help.svg new file mode 100644 index 0000000..1b502fb --- /dev/null +++ b/assets/help.svg @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spiel Demo Deck + + + + + + + + + + +               All Views                 +â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”┳â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”┓ +┃Key          â”ƒDescription         â”ƒ +┡â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”╇â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”┩ +│d            â”‚Go to the Deck view.│ +│question_mark│Go to the Help view.│ +│i            â”‚Switch to the REPL. â”‚ +│p            â”‚Take a screenshot.  â”‚ +└───────────────┴──────────────────────┘ + + +              Slide View                                       Deck View                           +â”â”â”â”â”â”â”â”┳â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”┓â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”┳â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”┓ +┃Key  â”ƒDescription               â”ƒâ”ƒKey         â”ƒDescription                              â”ƒ +┡â”â”â”â”â”â”â”╇â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”┩┡â”â”â”â”â”â”â”â”â”â”â”â”â”â”╇â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”┩ +│right│Go to next slide.         â”‚│right       â”‚Go to next slide.                        â”‚ +│left â”‚Go to previous slide.     â”‚│left        â”‚Go to previous slide.                    â”‚ +│t    â”‚Trigger the current slide.││down        â”‚Go to next row of slides.                â”‚ +│r    â”‚Reset trigger state.      â”‚│up          â”‚Go to previous row of slides.            â”‚ +└───────┴────────────────────────────┘│escape,enter│Go to Slide view with the selected slide.│ +└──────────────┴───────────────────────────────────────────┘ + + +                   Help View                    +â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”┳â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”┓ +┃Key         â”ƒDescription                 â”ƒ +┡â”â”â”â”â”â”â”â”â”â”â”â”â”â”╇â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”┩ +│escape,enter│Return to the previous view.│ +└──────────────┴──────────────────────────────┘ + + + +────────────────────────────────────────────────────────────────────────────────────────────────────────────── +Spiel Demo Deck | What is Spiel?                                            2022-12-17 03:31 PM   [01 / 11] + + + diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000..1cf13b9 Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/javascripts/bundle.fac441b0.min.js b/assets/javascripts/bundle.fac441b0.min.js new file mode 100644 index 0000000..4bb4cd6 --- /dev/null +++ b/assets/javascripts/bundle.fac441b0.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Ci=Object.create;var gr=Object.defineProperty;var Ri=Object.getOwnPropertyDescriptor;var ki=Object.getOwnPropertyNames,Ht=Object.getOwnPropertySymbols,Hi=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,nn=Object.prototype.propertyIsEnumerable;var rn=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&rn(e,r,t[r]);if(Ht)for(var r of Ht(t))nn.call(t,r)&&rn(e,r,t[r]);return e};var on=(e,t)=>{var r={};for(var n in e)yr.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Ht)for(var n of Ht(e))t.indexOf(n)<0&&nn.call(e,n)&&(r[n]=e[n]);return r};var Pt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Pi=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ki(t))!yr.call(e,o)&&o!==r&&gr(e,o,{get:()=>t[o],enumerable:!(n=Ri(t,o))||n.enumerable});return e};var yt=(e,t,r)=>(r=e!=null?Ci(Hi(e)):{},Pi(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var sn=Pt((xr,an)=>{(function(e,t){typeof xr=="object"&&typeof an!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(xr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,s={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function a(O){return!!(O&&O!==document&&O.nodeName!=="HTML"&&O.nodeName!=="BODY"&&"classList"in O&&"contains"in O.classList)}function f(O){var Qe=O.type,De=O.tagName;return!!(De==="INPUT"&&s[Qe]&&!O.readOnly||De==="TEXTAREA"&&!O.readOnly||O.isContentEditable)}function c(O){O.classList.contains("focus-visible")||(O.classList.add("focus-visible"),O.setAttribute("data-focus-visible-added",""))}function u(O){O.hasAttribute("data-focus-visible-added")&&(O.classList.remove("focus-visible"),O.removeAttribute("data-focus-visible-added"))}function p(O){O.metaKey||O.altKey||O.ctrlKey||(a(r.activeElement)&&c(r.activeElement),n=!0)}function m(O){n=!1}function d(O){a(O.target)&&(n||f(O.target))&&c(O.target)}function h(O){a(O.target)&&(O.target.classList.contains("focus-visible")||O.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(O.target))}function v(O){document.visibilityState==="hidden"&&(o&&(n=!0),Q())}function Q(){document.addEventListener("mousemove",N),document.addEventListener("mousedown",N),document.addEventListener("mouseup",N),document.addEventListener("pointermove",N),document.addEventListener("pointerdown",N),document.addEventListener("pointerup",N),document.addEventListener("touchmove",N),document.addEventListener("touchstart",N),document.addEventListener("touchend",N)}function B(){document.removeEventListener("mousemove",N),document.removeEventListener("mousedown",N),document.removeEventListener("mouseup",N),document.removeEventListener("pointermove",N),document.removeEventListener("pointerdown",N),document.removeEventListener("pointerup",N),document.removeEventListener("touchmove",N),document.removeEventListener("touchstart",N),document.removeEventListener("touchend",N)}function N(O){O.target.nodeName&&O.target.nodeName.toLowerCase()==="html"||(n=!1,B())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",v,!0),Q(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var cn=Pt(Er=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(c){return!1}},r=t(),n=function(c){var u={next:function(){var p=c.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(c){return encodeURIComponent(c).replace(/%20/g,"+")},i=function(c){return decodeURIComponent(String(c).replace(/\+/g," "))},s=function(){var c=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof c){var d=this;p.forEach(function(B,N){d.append(N,B)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),c._entries&&(c._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(f,c){typeof f!="string"&&(f=String(f)),c&&typeof c!="string"&&(c=String(c));var u=document,p;if(c&&(e.location===void 0||c!==e.location.href)){c=c.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=c,u.head.appendChild(p);try{if(p.href.indexOf(c)!==0)throw new Error(p.href)}catch(O){throw new Error("URL unable to set base "+c+" due to "+O)}}var m=u.createElement("a");m.href=f,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=f,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!c)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),v=!0,Q=!0,B=this;["append","delete","set"].forEach(function(O){var Qe=h[O];h[O]=function(){Qe.apply(h,arguments),v&&(Q=!1,B.search=h.toString(),Q=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var N=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==N&&(N=this.search,Q&&(v=!1,this.searchParams._fromString(this.search),v=!0))}})},s=i.prototype,a=function(f){Object.defineProperty(s,f,{get:function(){return this._anchorElement[f]},set:function(c){this._anchorElement[f]=c},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(f){a(f)}),Object.defineProperty(s,"search",{get:function(){return this._anchorElement.search},set:function(f){this._anchorElement.search=f,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(s,{toString:{get:function(){var f=this;return function(){return f.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(f){this._anchorElement.href=f,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(f){this._anchorElement.pathname=f},enumerable:!0},origin:{get:function(){var f={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],c=this._anchorElement.port!=f&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(c?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(f){},enumerable:!0},username:{get:function(){return""},set:function(f){},enumerable:!0}}),i.createObjectURL=function(f){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(f){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er)});var qr=Pt((Mt,Nr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Mt=="object"&&typeof Nr=="object"?Nr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Mt=="object"?Mt.ClipboardJS=r():t.ClipboardJS=r()})(Mt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return Ai}});var s=i(279),a=i.n(s),f=i(370),c=i.n(f),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(T){return!1}}var d=function(T){var E=p()(T);return m("cut"),E},h=d;function v(j){var T=document.documentElement.getAttribute("dir")==="rtl",E=document.createElement("textarea");E.style.fontSize="12pt",E.style.border="0",E.style.padding="0",E.style.margin="0",E.style.position="absolute",E.style[T?"right":"left"]="-9999px";var H=window.pageYOffset||document.documentElement.scrollTop;return E.style.top="".concat(H,"px"),E.setAttribute("readonly",""),E.value=j,E}var Q=function(T,E){var H=v(T);E.container.appendChild(H);var I=p()(H);return m("copy"),H.remove(),I},B=function(T){var E=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},H="";return typeof T=="string"?H=Q(T,E):T instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(T==null?void 0:T.type)?H=Q(T.value,E):(H=p()(T),m("copy")),H},N=B;function O(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?O=function(E){return typeof E}:O=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},O(j)}var Qe=function(){var T=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},E=T.action,H=E===void 0?"copy":E,I=T.container,q=T.target,Me=T.text;if(H!=="copy"&&H!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&O(q)==="object"&&q.nodeType===1){if(H==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(H==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Me)return N(Me,{container:I});if(q)return H==="cut"?h(q):N(q,{container:I})},De=Qe;function $e(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?$e=function(E){return typeof E}:$e=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},$e(j)}function Ei(j,T){if(!(j instanceof T))throw new TypeError("Cannot call a class as a function")}function tn(j,T){for(var E=0;E0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof I.action=="function"?I.action:this.defaultAction,this.target=typeof I.target=="function"?I.target:this.defaultTarget,this.text=typeof I.text=="function"?I.text:this.defaultText,this.container=$e(I.container)==="object"?I.container:document.body}},{key:"listenClick",value:function(I){var q=this;this.listener=c()(I,"click",function(Me){return q.onClick(Me)})}},{key:"onClick",value:function(I){var q=I.delegateTarget||I.currentTarget,Me=this.action(q)||"copy",kt=De({action:Me,container:this.container,target:this.target(q),text:this.text(q)});this.emit(kt?"success":"error",{action:Me,text:kt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(I){return vr("action",I)}},{key:"defaultTarget",value:function(I){var q=vr("target",I);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(I){return vr("text",I)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(I){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return N(I,q)}},{key:"cut",value:function(I){return h(I)}},{key:"isSupported",value:function(){var I=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof I=="string"?[I]:I,Me=!!document.queryCommandSupported;return q.forEach(function(kt){Me=Me&&!!document.queryCommandSupported(kt)}),Me}}]),E}(a()),Ai=Li},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function s(a,f){for(;a&&a.nodeType!==o;){if(typeof a.matches=="function"&&a.matches(f))return a;a=a.parentNode}}n.exports=s},438:function(n,o,i){var s=i(828);function a(u,p,m,d,h){var v=c.apply(this,arguments);return u.addEventListener(m,v,h),{destroy:function(){u.removeEventListener(m,v,h)}}}function f(u,p,m,d,h){return typeof u.addEventListener=="function"?a.apply(null,arguments):typeof m=="function"?a.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return a(v,p,m,d,h)}))}function c(u,p,m,d){return function(h){h.delegateTarget=s(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=f},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var s=Object.prototype.toString.call(i);return s==="[object Function]"}},370:function(n,o,i){var s=i(879),a=i(438);function f(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(h))throw new TypeError("Third argument must be a Function");if(s.node(m))return c(m,d,h);if(s.nodeList(m))return u(m,d,h);if(s.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(v){v.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(v){v.removeEventListener(d,h)})}}}function p(m,d,h){return a(document.body,m,d,h)}n.exports=f},817:function(n){function o(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var f=window.getSelection(),c=document.createRange();c.selectNodeContents(i),f.removeAllRanges(),f.addRange(c),s=f.toString()}return s}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,s,a){var f=this.e||(this.e={});return(f[i]||(f[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var f=this;function c(){f.off(i,c),s.apply(a,arguments)}return c._=s,this.on(i,c,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),f=0,c=a.length;for(f;f{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var rs=/["'&<>]/;Yo.exports=ns;function ns(e){var t=""+e,r=rs.exec(t);if(!r)return t;var n,o="",i=0,s=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],s;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(a){s={error:a}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(s)throw s.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||a(m,d)})})}function a(m,d){try{f(n[m](d))}catch(h){p(i[0][3],h)}}function f(m){m.value instanceof et?Promise.resolve(m.value.v).then(c,u):p(i[0][2],m)}function c(m){a("next",m)}function u(m){a("throw",m)}function p(m,d){m(d),i.shift(),i.length&&a(i[0][0],i[0][1])}}function pn(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof Ee=="function"?Ee(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(s){return new Promise(function(a,f){s=e[i](s),o(a,f,s.done,s.value)})}}function o(i,s,a,f){Promise.resolve(f).then(function(c){i({value:c,done:a})},s)}}function C(e){return typeof e=="function"}function at(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var It=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Ve(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ie=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=Ee(s),f=a.next();!f.done;f=a.next()){var c=f.value;c.remove(this)}}catch(v){t={error:v}}finally{try{f&&!f.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var u=this.initialTeardown;if(C(u))try{u()}catch(v){i=v instanceof It?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var m=Ee(p),d=m.next();!d.done;d=m.next()){var h=d.value;try{ln(h)}catch(v){i=i!=null?i:[],v instanceof It?i=D(D([],W(i)),W(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{d&&!d.done&&(o=m.return)&&o.call(m)}finally{if(n)throw n.error}}}if(i)throw new It(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)ln(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ve(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ve(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Sr=Ie.EMPTY;function jt(e){return e instanceof Ie||e&&"closed"in e&&C(e.remove)&&C(e.add)&&C(e.unsubscribe)}function ln(e){C(e)?e():e.unsubscribe()}var Le={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,s=o.isStopped,a=o.observers;return i||s?Sr:(this.currentObservers=null,a.push(r),new Ie(function(){n.currentObservers=null,Ve(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,s=n.isStopped;o?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,n){return new xn(r,n)},t}(F);var xn=function(e){ie(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Sr},t}(x);var Et={now:function(){return(Et.delegate||Date).now()},delegate:void 0};var wt=function(e){ie(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Et);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,s=n._infiniteTimeWindow,a=n._timestampProvider,f=n._windowTime;o||(i.push(r),!s&&i.push(a.now()+f)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,s=o._buffer,a=s.slice(),f=0;f0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var s=r.actions;n!=null&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Wt);var Sn=function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Dt);var Oe=new Sn(wn);var _=new F(function(e){return e.complete()});function Vt(e){return e&&C(e.schedule)}function Cr(e){return e[e.length-1]}function Ye(e){return C(Cr(e))?e.pop():void 0}function Te(e){return Vt(Cr(e))?e.pop():void 0}function zt(e,t){return typeof Cr(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Nt(e){return C(e==null?void 0:e.then)}function qt(e){return C(e[ft])}function Kt(e){return Symbol.asyncIterator&&C(e==null?void 0:e[Symbol.asyncIterator])}function Qt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Yt=zi();function Gt(e){return C(e==null?void 0:e[Yt])}function Bt(e){return un(this,arguments,function(){var r,n,o,i;return $t(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,et(r.read())];case 3:return n=s.sent(),o=n.value,i=n.done,i?[4,et(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,et(o)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Jt(e){return C(e==null?void 0:e.getReader)}function U(e){if(e instanceof F)return e;if(e!=null){if(qt(e))return Ni(e);if(pt(e))return qi(e);if(Nt(e))return Ki(e);if(Kt(e))return On(e);if(Gt(e))return Qi(e);if(Jt(e))return Yi(e)}throw Qt(e)}function Ni(e){return new F(function(t){var r=e[ft]();if(C(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function qi(e){return new F(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?A(function(o,i){return e(o,i,n)}):de,ge(1),r?He(t):Dn(function(){return new Zt}))}}function Vn(){for(var e=[],t=0;t=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new x}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,f=a===void 0?!0:a;return function(c){var u,p,m,d=0,h=!1,v=!1,Q=function(){p==null||p.unsubscribe(),p=void 0},B=function(){Q(),u=m=void 0,h=v=!1},N=function(){var O=u;B(),O==null||O.unsubscribe()};return y(function(O,Qe){d++,!v&&!h&&Q();var De=m=m!=null?m:r();Qe.add(function(){d--,d===0&&!v&&!h&&(p=$r(N,f))}),De.subscribe(Qe),!u&&d>0&&(u=new rt({next:function($e){return De.next($e)},error:function($e){v=!0,Q(),p=$r(B,o,$e),De.error($e)},complete:function(){h=!0,Q(),p=$r(B,s),De.complete()}}),U(O).subscribe(u))})(c)}}function $r(e,t){for(var r=[],n=2;ne.next(document)),e}function K(e,t=document){return Array.from(t.querySelectorAll(e))}function z(e,t=document){let r=ce(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ce(e,t=document){return t.querySelector(e)||void 0}function _e(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function tr(e){return L(b(document.body,"focusin"),b(document.body,"focusout")).pipe(ke(1),l(()=>{let t=_e();return typeof t!="undefined"?e.contains(t):!1}),V(e===_e()),J())}function Xe(e){return{x:e.offsetLeft,y:e.offsetTop}}function Kn(e){return L(b(window,"load"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>Xe(e)),V(Xe(e)))}function rr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return L(b(e,"scroll"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>rr(e)),V(rr(e)))}var Yn=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!Wr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),va?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!Wr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=ba.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Gn=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Jn=typeof WeakMap!="undefined"?new WeakMap:new Yn,Xn=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=ga.getInstance(),n=new La(t,r,this);Jn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){Xn.prototype[e]=function(){var t;return(t=Jn.get(this))[e].apply(t,arguments)}});var Aa=function(){return typeof nr.ResizeObserver!="undefined"?nr.ResizeObserver:Xn}(),Zn=Aa;var eo=new x,Ca=$(()=>k(new Zn(e=>{for(let t of e)eo.next(t)}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function he(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ye(e){return Ca.pipe(S(t=>t.observe(e)),g(t=>eo.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(()=>he(e)))),V(he(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ar(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var to=new x,Ra=$(()=>k(new IntersectionObserver(e=>{for(let t of e)to.next(t)},{threshold:0}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function sr(e){return Ra.pipe(S(t=>t.observe(e)),g(t=>to.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function ro(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=he(e),o=bt(e);return r>=o.height-n.height-t}),J())}var cr={drawer:z("[data-md-toggle=drawer]"),search:z("[data-md-toggle=search]")};function no(e){return cr[e].checked}function Ke(e,t){cr[e].checked!==t&&cr[e].click()}function Ue(e){let t=cr[e];return b(t,"change").pipe(l(()=>t.checked),V(t.checked))}function ka(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ha(){return L(b(window,"compositionstart").pipe(l(()=>!0)),b(window,"compositionend").pipe(l(()=>!1))).pipe(V(!1))}function oo(){let e=b(window,"keydown").pipe(A(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:no("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),A(({mode:t,type:r})=>{if(t==="global"){let n=_e();if(typeof n!="undefined")return!ka(n,r)}return!0}),pe());return Ha().pipe(g(t=>t?_:e))}function le(){return new URL(location.href)}function ot(e){location.href=e.href}function io(){return new x}function ao(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)ao(e,r)}function M(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)ao(n,o);return n}function fr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function so(){return location.hash.substring(1)}function Dr(e){let t=M("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Pa(e){return L(b(window,"hashchange"),e).pipe(l(so),V(so()),A(t=>t.length>0),X(1))}function co(e){return Pa(e).pipe(l(t=>ce(`[id="${t}"]`)),A(t=>typeof t!="undefined"))}function Vr(e){let t=matchMedia(e);return er(r=>t.addListener(()=>r(t.matches))).pipe(V(t.matches))}function fo(){let e=matchMedia("print");return L(b(window,"beforeprint").pipe(l(()=>!0)),b(window,"afterprint").pipe(l(()=>!1))).pipe(V(e.matches))}function zr(e,t){return e.pipe(g(r=>r?t():_))}function ur(e,t={credentials:"same-origin"}){return ue(fetch(`${e}`,t)).pipe(fe(()=>_),g(r=>r.status!==200?Ot(()=>new Error(r.statusText)):k(r)))}function We(e,t){return ur(e,t).pipe(g(r=>r.json()),X(1))}function uo(e,t){let r=new DOMParser;return ur(e,t).pipe(g(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),X(1))}function pr(e){let t=M("script",{src:e});return $(()=>(document.head.appendChild(t),L(b(t,"load"),b(t,"error").pipe(g(()=>Ot(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),R(()=>document.head.removeChild(t)),ge(1))))}function po(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function lo(){return L(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(l(po),V(po()))}function mo(){return{width:innerWidth,height:innerHeight}}function ho(){return b(window,"resize",{passive:!0}).pipe(l(mo),V(mo()))}function bo(){return Y([lo(),ho()]).pipe(l(([e,t])=>({offset:e,size:t})),X(1))}function lr(e,{viewport$:t,header$:r}){let n=t.pipe(ee("size")),o=Y([n,r]).pipe(l(()=>Xe(e)));return Y([r,t,o]).pipe(l(([{height:i},{offset:s,size:a},{x:f,y:c}])=>({offset:{x:s.x-f,y:s.y-c+i},size:a})))}(()=>{function e(n,o){parent.postMessage(n,o||"*")}function t(...n){return n.reduce((o,i)=>o.then(()=>new Promise(s=>{let a=document.createElement("script");a.src=i,a.onload=s,document.body.appendChild(a)})),Promise.resolve())}var r=class extends EventTarget{constructor(n){super(),this.url=n,this.m=i=>{i.source===this.w&&(this.dispatchEvent(new MessageEvent("message",{data:i.data})),this.onmessage&&this.onmessage(i))},this.e=(i,s,a,f,c)=>{if(s===`${this.url}`){let u=new ErrorEvent("error",{message:i,filename:s,lineno:a,colno:f,error:c});this.dispatchEvent(u),this.onerror&&this.onerror(u)}};let o=document.createElement("iframe");o.hidden=!0,document.body.appendChild(this.iframe=o),this.w.document.open(),this.w.document.write(` + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + +

Changelog

+

0.5.1

+

Released 2023-04-21

+

Changed

+
    +
  • #222 Pin to Textual 0.11.1 temporarily to resolve issues with slide transitions.
  • +
+

0.5.0

+

Released 2023-02-19

+

Added

+
    +
  • #207 Add a default "swipe" transition between slides and support for user-defined transitions.
  • +
+

0.4.6

+

Released 2023-01-19

+

Changed

+
    +
  • #208 Unpinned textual==0.4.0 and allowed textual>=0.10.0, which includes textual#1558.
  • +
+

0.4.5

+

Released 2023-01-16

+

Added

+
    +
  • #205 Add Triggers.take to make gradually revealing content on a slide more straightforward.
  • +
+

Fixed

+
    +
  • #202 Returning un-renderable content from a slide content function now displays an error instead of crashing Spiel.
  • +
+

Changed

+
    +
  • #203 The Image example in the demo deck is now centered inside its Panel.
  • +
+

0.4.4

+

Released 2023-01-13

+

Added

+
    +
  • #185 The docs page now includes copy-to-clipboard buttons on all code snippets.
  • +
  • #194 The demo slides now render their own source code directly to demo bindings functionality.
  • +
+

Changed

+
    +
  • #194 The Deck.slide decorator now returns the decorated function, not the Slide it was attached to.
  • +
  • #199 The CLI command spiel present's --watch option now defaults to the parent directory of the deck file instead of the current working directory.
  • +
+

0.4.3

+

Released 2023-01-02

+

Added

+
    +
  • #169 The Textual application title and subtitle are now set dynamically from the Spiel deck name and slide title, respectively.
  • +
  • #178 spiel.Deck is now a Sequence[Slide], and spiel.Triggers is now a Sequence[float].
  • +
+

Fixed

+
    +
  • #168 The correct type for the suspend optional argument to slide-level keybinding functions is now available as spiel.SuspendType.
  • +
  • #168 The Spiel container image no longer has a leftover copy of the spiel package directory inside the image under /app.
  • +
+

0.4.2

+

Released 2022-12-10

+

Added

+
    +
  • #163 Added a public spiel.present() function that presents the deck at the given file.
  • +
+

0.4.1

+

Released 2022-11-25

+

Fixed

+ +

0.4.0

+

Released 2022-11-25

+

Changed

+
    +
  • #154 Switch to Textual as the overall control and rendering engine.
  • +
+

Removed

+
    +
  • #154 Removed library-provided Example slides, Options, and various other small features + as part of the Textual migration. Some of these features will likely be reintroduced later.
  • +
+

0.3.0

+

Removed

+
    +
  • #129 Dropped support for Python <=3.9.
  • +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/contributing/index.html b/contributing/index.html new file mode 100644 index 0000000..0a1df6b --- /dev/null +++ b/contributing/index.html @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + Contributing Guide - Spiel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + +

Contributing Guide

+
+

Spiel is open to contributions!

+ +
+

Development Environment

+

Spiel uses:

+ +

Initial Setup

+

To set up a local development environment after cloning the repository:

+
    +
  1. Install poetry.
  2. +
  3. Run poetry shell to create a virtual environment for spiel and spawn a new shell session with that virtual environment activated. + In the future you'll run poetry shell again to activate the virtual environment.
  4. +
  5. Run poetry install to install Spiel's dependencies.
  6. +
  7. Run pre-commit install to configure pre-commit's integration with git. + Do not commit without pre-commit installed!
  8. +
+

Running Tests and Type-Checking

+

Run pytest to run tests.

+

Run mypy to check types.

+

Building the Docs Locally

+

To build the docs and start a local web server to view the results of your edits with live reloading, run +

mkdocs serve
+
+from the repository root.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/quickstart.py b/examples/quickstart.py new file mode 100644 index 0000000..3718383 --- /dev/null +++ b/examples/quickstart.py @@ -0,0 +1,14 @@ +from rich.console import RenderableType + +from spiel import Deck, present + +deck = Deck(name="Your Deck Name") + + +@deck.slide(title="Slide 1 Title") +def slide_1() -> RenderableType: + return "Your content here!" + + +if __name__ == "__main__": + present(__file__) diff --git a/examples/slide_loop.py b/examples/slide_loop.py new file mode 100644 index 0000000..c142f3d --- /dev/null +++ b/examples/slide_loop.py @@ -0,0 +1,25 @@ +from rich.align import Align +from rich.console import RenderableType +from rich.style import Style +from rich.text import Text + +from spiel import Deck, Slide + +deck = Deck(name="Deck Name") + + +def make_slide( + title_prefix: str, + text: Text, +) -> Slide: + def content() -> RenderableType: + return Align(text, align="center", vertical="middle") + + return Slide(title=f"{title_prefix} Slide", content=content) + + +deck.add_slides( + make_slide(title_prefix="First", text=Text("Foo", style=Style(color="blue"))), + make_slide(title_prefix="Second", text=Text("Bar", style=Style(color="red"))), + make_slide(title_prefix="Third", text=Text("Baz", style=Style(color="green"))), +) diff --git a/examples/slide_via_decorator.py b/examples/slide_via_decorator.py new file mode 100644 index 0000000..5905eb4 --- /dev/null +++ b/examples/slide_via_decorator.py @@ -0,0 +1,18 @@ +from rich.align import Align +from rich.console import RenderableType +from rich.text import Text + +from spiel import Deck + +deck = Deck(name="Deck Name") + + +@deck.slide(title="Slide Title") +def slide_content() -> RenderableType: + return Align( + Text.from_markup( + "[blue]Your[/blue] [red underline]content[/red underline] [green italic]here[/green italic]!" + ), + align="center", + vertical="middle", + ) diff --git a/examples/triggers_animation.py b/examples/triggers_animation.py new file mode 100644 index 0000000..95dea80 --- /dev/null +++ b/examples/triggers_animation.py @@ -0,0 +1,32 @@ +from math import floor + +from rich.align import Align +from rich.console import Group, RenderableType +from rich.panel import Panel +from rich.text import Text + +from spiel import Deck, Triggers + +deck = Deck(name="Trigger Examples") + + +@deck.slide(title="Animating Content") +def animate(triggers: Triggers) -> RenderableType: + bang = "!" + space = " " + bar_length = 5 + + spaces_before_bang = min(floor(triggers.time_since_first_trigger), bar_length) + spaces_after_bang = bar_length - spaces_before_bang + + bar = (space * spaces_before_bang) + bang + (space * spaces_after_bang) + + return Align( + Group( + Align.center(Text(f"{triggers=}")), + Align.center(Text(f"{spaces_before_bang=} | {spaces_after_bang=}")), + Align.center(Panel(Text(bar), expand=False, height=3)), + ), + align="center", + vertical="middle", + ) diff --git a/examples/triggers_reveal.py b/examples/triggers_reveal.py new file mode 100644 index 0000000..9cbf6fd --- /dev/null +++ b/examples/triggers_reveal.py @@ -0,0 +1,25 @@ +from rich.align import Align +from rich.console import Group, RenderableType +from rich.padding import Padding +from rich.style import Style +from rich.text import Text + +from spiel import Deck, Triggers + +deck = Deck(name="Trigger Examples") + + +@deck.slide(title="Revealing Content") +def reveal(triggers: Triggers) -> RenderableType: + lines = [ + Text.from_markup( + f"This slide has been triggered [yellow]{len(triggers)}[/yellow] time{'s' if len(triggers) > 1 else ''}." + ), + Text("First line.", style=Style(color="red")) if len(triggers) >= 1 else None, + Text("Second line.", style=Style(color="blue")) if len(triggers) >= 2 else None, + Text("Third line.", style=Style(color="green")) if len(triggers) >= 3 else None, + ] + + return Group( + *(Padding(Align.center(line), pad=(0, 0, 1, 0)) for line in lines if line is not None) + ) diff --git a/gallery/index.html b/gallery/index.html new file mode 100644 index 0000000..d8666b7 --- /dev/null +++ b/gallery/index.html @@ -0,0 +1,536 @@ + + + + + + + + + + + + + + + + + + + + + + + + Gallery - Spiel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/generate_screenshots.py b/generate_screenshots.py new file mode 100644 index 0000000..cf65a4a --- /dev/null +++ b/generate_screenshots.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python + +import os +from collections.abc import Iterable +from concurrent.futures import ProcessPoolExecutor, as_completed +from datetime import datetime +from functools import partial +from io import StringIO +from pathlib import Path +from time import monotonic + +from more_itertools import intersperse +from rich.console import Console +from rich.text import Text +from textual.app import App +from textual.pilot import Pilot + +from spiel.app import SpielApp +from spiel.constants import DEMO_FILE +from spiel.triggers import Triggers + +ROOT_DIR = Path(__file__).resolve().parent.parent +ASSETS_DIR = ROOT_DIR / "docs" / "assets" + +# lie to Rich to make sure the screenshots are always generated in full color +os.environ["TERMCOLOR"] = "truecolor" + +console = Console() + + +def take_reproducible_screenshot(app: App[object]) -> str: + """ + Textual's screenshot functions don't let you control the unique_id argument to console.export_svg, + so this little shim just reproduces the internals of Textual's methods with more control. + """ + width, height = app.size + renderer = Console( + width=width, + height=height, + file=StringIO(), + force_terminal=True, + color_system="truecolor", + record=True, + legacy_windows=False, + ) + screen_render = app.screen._compositor.render(full=True) + renderer.print(screen_render) + return renderer.export_svg(title=app.title, unique_id="spieldocs") + + +async def auto_pilot(pilot: Pilot[object], name: str, keys: Iterable[str]) -> None: + await pilot.press(*intersperse("wait:50", keys), "wait:100") + + (ASSETS_DIR / name).with_suffix(".svg").write_text(take_reproducible_screenshot(pilot.app)) + + await pilot.app.action_quit() + + +def take_screenshot( + name: str, + deck_file: Path, + size: tuple[int, int], + keys: Iterable[str], + triggers: Triggers, +) -> str: + console.print(Text.from_markup(f":camera: Generating [bold cyan]{name}[/bold cyan] ...")) + + SpielApp( + deck_path=deck_file, + watch_path=deck_file.parent, + _show_messages=False, + _fixed_time=datetime(year=2022, month=12, day=17, hour=15, minute=31, second=42), + _fixed_triggers=triggers, + _enable_transitions=False, + ).run( + headless=True, + auto_pilot=partial(auto_pilot, name=name, keys=keys), + size=size, + ) + + return name + + +if __name__ == "__main__": + start_time = monotonic() + + demo_deck = DEMO_FILE + quickstart_deck = ROOT_DIR / "docs" / "examples" / "quickstart.py" + slide_via_decorator = ROOT_DIR / "docs" / "examples" / "slide_via_decorator.py" + slide_loop = ROOT_DIR / "docs" / "examples" / "slide_loop.py" + triggers_reveal = ROOT_DIR / "docs" / "examples" / "triggers_reveal.py" + triggers_animation = ROOT_DIR / "docs" / "examples" / "triggers_animation.py" + + triggers = Triggers(now=0, _times=(0,)) + + with ProcessPoolExecutor() as pool: + futures = [ + pool.submit( + take_screenshot, + name="triggers_animation_1", + deck_file=triggers_animation, + size=(70, 15), + keys=(), + triggers=Triggers(now=0, _times=(0,)), + ), + pool.submit( + take_screenshot, + name="triggers_animation_2", + deck_file=triggers_animation, + size=(70, 15), + keys=(), + triggers=Triggers(now=1.5, _times=(0,)), + ), + pool.submit( + take_screenshot, + name="triggers_animation_3", + deck_file=triggers_animation, + size=(70, 15), + keys=(), + triggers=Triggers(now=2.5, _times=(0,)), + ), + pool.submit( + take_screenshot, + name="triggers_animation_4", + deck_file=triggers_animation, + size=(70, 15), + keys=(), + triggers=Triggers(now=5.5, _times=(0,)), + ), + pool.submit( + take_screenshot, + name="demo", + deck_file=demo_deck, + size=(130, 35), + keys=(), + triggers=triggers, + ), + pool.submit( + take_screenshot, + name="deck", + deck_file=demo_deck, + size=(135, 40), + keys=("d", "right", "down"), + triggers=triggers, + ), + pool.submit( + take_screenshot, + name="help", + deck_file=demo_deck, + size=(110, 35), + keys=("?",), + triggers=triggers, + ), + pool.submit( + take_screenshot, + name="quickstart_basic", + deck_file=quickstart_deck, + size=(70, 20), + keys=(), + triggers=triggers, + ), + pool.submit( + take_screenshot, + name="quickstart_code", + deck_file=demo_deck, + size=(140, 45), + keys=("right",), + triggers=triggers, + ), + pool.submit( + take_screenshot, + name="slide_via_decorator", + deck_file=slide_via_decorator, + size=(60, 15), + keys=(), + triggers=triggers, + ), + pool.submit( + take_screenshot, + name="slide_loop_1", + deck_file=slide_loop, + size=(60, 15), + keys=(), + triggers=triggers, + ), + pool.submit( + take_screenshot, + name="slide_loop_2", + deck_file=slide_loop, + size=(60, 15), + keys=("right",), + triggers=triggers, + ), + pool.submit( + take_screenshot, + name="slide_loop_3", + deck_file=slide_loop, + size=(60, 15), + keys=("right", "right"), + triggers=triggers, + ), + pool.submit( + take_screenshot, + name="triggers_reveal_1", + deck_file=triggers_reveal, + size=(70, 15), + keys=(), + triggers=Triggers(now=0, _times=(0,)), + ), + pool.submit( + take_screenshot, + name="triggers_reveal_2", + deck_file=triggers_reveal, + size=(70, 15), + keys=(), + triggers=Triggers(now=1, _times=(0, 1)), + ), + pool.submit( + take_screenshot, + name="triggers_reveal_3", + deck_file=triggers_reveal, + size=(70, 15), + keys=(), + triggers=Triggers(now=2, _times=(0, 1, 2)), + ), + ] + + for future in as_completed(futures, timeout=60): + console.print( + Text.from_markup( + f":camera_with_flash: Generated [bold cyan]{future.result()}[/bold cyan]" + ) + ) + + end_time = monotonic() + + console.print( + Text.from_markup( + f"Generated [green]{len(futures)}[/green] screenshots in [green]{end_time - start_time:0.2f}[/green] seconds" + ) + ) diff --git a/index.html b/index.html new file mode 100644 index 0000000..71eb2a2 --- /dev/null +++ b/index.html @@ -0,0 +1,488 @@ + + + + + + + + + + + + + + + + + + + + + + Spiel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + +

Spiel

+

Spiel +is a framework for building and presenting +richly-styled presentations in your terminal using Python.

+

The first slide of the demo deck +The demo deck in "deck view" +The demo deck in "help view"

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/objects.inv b/objects.inv new file mode 100644 index 0000000..3fd5611 --- /dev/null +++ b/objects.inv @@ -0,0 +1,6 @@ +# Sphinx inventory version 2 +# Project: Spiel +# Version: 0.0.0 +# The remainder of this file is compressed using zlib. +xÚ“ÁNÃ0 †ï + + + + + + + + + + + + + + + + + + + + + + Presenting Decks - Spiel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + +

Presenting Decks

+

Depending on your preferred workflow, +you can start a presentation in a variety of different ways.

+
+

Sandboxed Execution

+

Spiel presentations are live Python code: they can do anything that Python can do. +You may want to run untrusted presentations (or even your own presentations) inside a container (but remember, even containers are not perfectly safe!). +We produce a container image +that can be run by (for example) Docker.

+

Presentations without extra Python dependencies might just need to be bind-mounted into the container. +For example, if your demo file is at $PWD/presentation/deck.py, you could do +

$ docker run -it --rm --mount type=bind,source=$PWD/presentation,target=/presentation ghcr.io/joshkarpel/spiel spiel present /presentation/deck.py
+

+

If the presentation has extra dependencies (like other Python packages), +we recommend building a new image that inherits our image (e.g., FROM ghcr.io/joshkarpel/spiel:vX.Y.Z). +Spiel's image itself inherits from the Python base image.

+
+

Using the spiel CLI

+

Installing the Spiel package provides a CLI tool called spiel. +The spiel present subcommand allows you to present a deck; +run spiel present --help to see the arguments and available options.

+

Using the present function

+

The present function lets you start a presentation programmatically (i.e., from a Python script).

+

If your deck is defined in talk/slides.py like so:

+
talk/slides.py
#!/usr/bin/env python
+
+from spiel import Deck, present
+
+deck = Deck(...)
+
+...  # construct your deck
+
+if __name__ == "__main__":
+    present(__file__)
+
+

You can then present the deck by running the script: +

python talk/slides.py
+
+Or by running the script as a module (you must have a talk/__init__.py file): +
python -m talk.slides
+
+Or by running the script via its shebang +(after running chmod +x talk/slides.py to mark talk/slides.py as executable): +
talk/slides.py
+

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/quickstart/index.html b/quickstart/index.html new file mode 100644 index 0000000..dc81ea5 --- /dev/null +++ b/quickstart/index.html @@ -0,0 +1,657 @@ + + + + + + + + + + + + + + + + + + + + + + + + Quick Start - Spiel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + +

Quick Start

+

The Most Basic Deck

+

After installing Spiel (pip install spiel), +create a file called deck.py and copy this code into it:

+
deck.py
from rich.console import RenderableType
+
+from spiel import Deck, present
+
+deck = Deck(name="Your Deck Name")
+
+
+@deck.slide(title="Slide 1 Title")
+def slide_1() -> RenderableType:
+    return "Your content here!"
+
+
+if __name__ == "__main__":
+    present(__file__)
+
+

That is the most basic Spiel presentation you can make. +To present the deck, run python deck.py. +You should see:

+

Barebones slide

+

In the example above, you first create a Deck and provide the name of your presentation. +Then you create slides by decorating functions with @deck.slide, providing the title of the slide. +The slide function can return anything that +Rich can render; +that return value will be displayed as the slide's content when you present it. +The order of the @deck.slide-decorated functions in your file is the order in which they will appear in your presentation.

+

Running python deck.py started the presentation because of the call to present in the +if __name__ == "__main__" block.

+

To see available keybindings for doing things like moving between slides, +press ? to open the help view, which should look like this:

+

Help view

+

Making Richer Slides

+

You can make your slides a lot prettier, of course. +As mentioned above, Spiel renders its slides using Rich, so you can bring in Rich functionality to spruce up your slides. +Let's explore some advanced features by recreating one of the slides from the demo deck. +Update your deck.py file with these imports and utility definitions:

+
deck.py
import inspect
+from textwrap import dedent
+
+from rich.box import SQUARE
+from rich.console import RenderableType
+from rich.layout import Layout
+from rich.markdown import Markdown
+from rich.padding import Padding
+from rich.panel import Panel
+from rich.style import Style
+from rich.syntax import Syntax
+
+from spiel import Deck, Slide, present
+from spiel.deck import Deck
+
+
+SPIEL = "[Spiel](https://github.com/JoshKarpel/spiel)"
+RICH = "[Rich](https://rich.readthedocs.io/)"
+
+def pad_markdown(markup: str) -> RenderableType:
+    return Padding(Markdown(dedent(markup), justify="center"), pad=(0, 5))
+
+

And then paste this code into your deck.py file below your first slide:

+
deck.py
@deck.slide(title="Decks and Slides")
+def code() -> RenderableType:
+    markup = f"""\
+        ## Decks are made of Slides
+
+        Here's the code for `Deck` and `Slide`!
+
+        The source code is pulled directly from the definitions via [inspect.getsource](https://docs.python.org/3/library/inspect.html#inspect.getsource).
+
+        ({RICH} supports syntax highlighting, so {SPIEL} does too!)
+        """
+    root = Layout()
+    upper = Layout(pad_markdown(markup), size=len(markup.split("\n")) + 1)
+    lower = Layout()
+    root.split_column(upper, lower)
+
+    def make_code_panel(obj: type) -> RenderableType:
+        lines, line_number = inspect.getsourcelines(obj)
+        return Panel(
+            Syntax(
+                "".join(lines),
+                lexer="python",
+                line_numbers=True,
+                start_line=line_number,
+            ),
+            box=SQUARE,
+            border_style=Style(dim=True),
+            height=len(lines) + 2,
+        )
+
+    lower.split_row(
+        Layout(make_code_panel(Deck)),
+        Layout(make_code_panel(Slide)),
+    )
+
+    return root
+
+

We start out by creating our text content and setting up some Layouts, which will let us divide the slide space into chunks. +Then, we create the make_code_panel function to take some lines of code from the Deck and Slide classes +and put them in a syntax-highlighted Panel (with some additional fancy Rich styling). +Finally, we add the code panels to our layout side-by-side and return root, the top-level Layout.

+

Run python deck.py again and go to the second slide (press ? if you're not sure how to navigate!):

+

Demo Code Slide

+

Check out the source code of the demo deck +for more inspiration on ways to use Rich to make your slides beautiful! +Spiel provides a spiel CLI tool to make this easy:

+
    +
  • Present the demo deck in your terminal by running spiel demo present.
  • +
  • View the source in your terminal with spiel demo source.
  • +
  • Copy it to use as a starting point with spiel demo copy <destination>.
  • +
+

You can also check out the gallery to see talks that other users have made.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 0000000..ea9d4c6 --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Spiel","text":"

Spiel is a framework for building and presenting richly-styled presentations in your terminal using Python.

"},{"location":"api/","title":"API Reference","text":""},{"location":"api/#decks-and-slides","title":"Decks and Slides","text":""},{"location":"api/#spiel.Deck","title":"spiel.Deck dataclass","text":"

Bases: Sequence[Slide]

Represents a \"deck\" of \"slides\": a presentation.

"},{"location":"api/#spiel.deck.Deck.name","title":"name: str instance-attribute","text":"

The name of the Deck, which will be displayed in the footer.

"},{"location":"api/#spiel.deck.Deck.default_transition","title":"default_transition: Type[Transition] | None = Swipe class-attribute instance-attribute","text":"

The default slide transition animation; used if the slide being moved to does not specify its own transition. Defaults to the Swipe transition. Set to None for no transition animation.

"},{"location":"api/#spiel.deck.Deck.slide","title":"slide(title='', bindings=None, transition=None)","text":"

A decorator that creates a new slide in the deck, with the decorated function as the Slide.content.

PARAMETER DESCRIPTION title

The title to display for the slide.

TYPE: str DEFAULT: ''

bindings

A mapping of keys to callables to be executed when those keys are pressed, when on this slide.

TYPE: Mapping[str, Callable[..., None]] | None DEFAULT: None

transition

The transition animation to use when moving to this slide. Set to None to use the Deck.default_transition of the deck this slide is in.

TYPE: Type[Transition] | None DEFAULT: None

"},{"location":"api/#spiel.deck.Deck.add_slides","title":"add_slides(*slides)","text":"

Add Slides to a Deck.

This function is primarily useful when adding multiple slides at once, probably generated programmatically. If adding a single slide, prefer the Deck.slide decorator.

PARAMETER DESCRIPTION *slides

The Slides to add.

TYPE: Slide DEFAULT: ()

"},{"location":"api/#spiel.Slide","title":"spiel.Slide dataclass","text":"

Represents a single slide in the presentation.

"},{"location":"api/#spiel.slide.Slide.title","title":"title: str = '' class-attribute instance-attribute","text":"

The title of the Slide, which will be displayed in the footer.

"},{"location":"api/#spiel.slide.Slide.content","title":"content: Content = Text class-attribute instance-attribute","text":"

A callable that is invoked by Spiel to display the slide's content.

The function may optionally take arguments with these names:

  • trigger: The current Trigger state, for use in animations.
"},{"location":"api/#spiel.slide.Slide.bindings","title":"bindings: Mapping[str, Callable[..., None]] = field(default_factory=dict) class-attribute instance-attribute","text":"

A mapping of keys to callables to be executed when those keys are pressed, when on this slide.

"},{"location":"api/#spiel.slide.Slide.transition","title":"transition: Type[Transition] | None = Swipe class-attribute instance-attribute","text":"

The transition animation to use when moving to this slide. Set to None to use the Deck.default_transition of the deck this slide is in.

"},{"location":"api/#rendering-content","title":"Rendering Content","text":""},{"location":"api/#spiel.Triggers","title":"spiel.Triggers dataclass","text":"

Bases: Sequence[float]

Provides information to Slide.content about the current slide's \"trigger state\".

Triggers is a Sequence of times (produced by time.monotonic) that the current slide was triggered at. Note that the slide will be triggered once when it starts being displayed, so the first trigger time will be the time when the slide started being displayed.

"},{"location":"api/#spiel.triggers.Triggers.now","title":"now: float instance-attribute","text":"

The time that the slide content is being rendered at. Use this is as a single consistent value to base relative times on.

"},{"location":"api/#spiel.triggers.Triggers.time_since_last_trigger","title":"time_since_last_trigger: float property cached","text":"

The elapsed time since the most recent trigger.

"},{"location":"api/#spiel.triggers.Triggers.time_since_first_trigger","title":"time_since_first_trigger: float property cached","text":"

The elapsed time since the first trigger, which is equivalent to the time since the slide started being displayed.

"},{"location":"api/#spiel.triggers.Triggers.triggered","title":"triggered: bool property cached","text":"

Returns whether the slide has been manually triggered (i.e., this ignores the initial trigger from when the slide starts being displayed).

"},{"location":"api/#spiel.triggers.Triggers.take","title":"take(iter, offset=1)","text":"

Takes elements from the iterable iter equal to the number of times in the Triggers minus the offset.

PARAMETER DESCRIPTION iter

The iterable to take elements from.

TYPE: Iterable[T]

offset

This offset will be subtracted from the number of triggers, reducing the number of elements that will be returned. It defaults to 1 to ignore the automatic trigger from when the slide starts being shown.

TYPE: int DEFAULT: 1

RETURNS DESCRIPTION Iterator[T]

An iterator over the first len(self) - offset elements of iter.

"},{"location":"api/#transitions","title":"Transitions","text":""},{"location":"api/#spiel.Direction","title":"spiel.Direction","text":"

Bases: Enum

An enumeration that describes which direction a slide transition animation should move in: whether we're going to the next slide, or to the previous slide.

"},{"location":"api/#spiel.transitions.protocol.Direction.Next","title":"Next = 'next' class-attribute instance-attribute","text":"

Indicates that the transition should handle going to the next slide.

"},{"location":"api/#spiel.transitions.protocol.Direction.Previous","title":"Previous = 'previous' class-attribute instance-attribute","text":"

Indicates that the transition should handle going to the previous slide.

"},{"location":"api/#spiel.Transition","title":"spiel.Transition","text":"

Bases: Protocol

A protocol that describes how to implement a transition animation.

See Writing Custom Transitions for more details on how to implement the protocol.

"},{"location":"api/#spiel.transitions.protocol.Transition.initialize","title":"initialize(from_widget, to_widget, direction)","text":"

A hook function to set up any CSS that should be present at the start of the transition.

PARAMETER DESCRIPTION from_widget

The widget showing the slide that we are leaving.

TYPE: Widget

to_widget

The widget showing the slide that we are entering.

TYPE: Widget

direction

The desired direction of the transition animation.

TYPE: Direction

"},{"location":"api/#spiel.transitions.protocol.Transition.progress","title":"progress(from_widget, to_widget, direction, progress)","text":"

A hook function that is called each time the progress of the transition animation updates.

PARAMETER DESCRIPTION from_widget

The widget showing the slide that we are leaving.

TYPE: Widget

to_widget

The widget showing the slide that we are entering.

TYPE: Widget

direction

The desired direction of the transition animation.

TYPE: Direction

progress

The progress of the animation, as a percentage (e.g., initial state is 0, final state is 100). Note that this is not necessarily bounded between 0 and 100, nor is it necessarily monotonically increasing, depending on the underlying Textual animation easing function, which may overshoot or bounce. However, it will always start at 0 and end at 100, no matter which direction the transition should move in.

TYPE: float

"},{"location":"api/#spiel.Swipe","title":"spiel.Swipe","text":"

Bases: Transition

A transition where the current and incoming slide are placed side-by-side and gradually slide across the screen, with the current slide leaving and the incoming slide entering.

"},{"location":"api/#presenting-decks","title":"Presenting Decks","text":""},{"location":"api/#spiel.present","title":"spiel.present(deck_path, watch_path=None)","text":"

Present the deck defined in the given deck_path.

PARAMETER DESCRIPTION deck_path

The file to look for a deck in.

TYPE: Path | str

watch_path

When filesystem changes are detected below this path (recursively), reload the deck from the deck_path. If None (the default), use the parent directory of the deck_path.

TYPE: Path | str | None DEFAULT: None

"},{"location":"changelog/","title":"Changelog","text":""},{"location":"changelog/#051","title":"0.5.1","text":"

Released 2023-04-21

"},{"location":"changelog/#changed","title":"Changed","text":"
  • #222 Pin to Textual 0.11.1 temporarily to resolve issues with slide transitions.
"},{"location":"changelog/#050","title":"0.5.0","text":"

Released 2023-02-19

"},{"location":"changelog/#added","title":"Added","text":"
  • #207 Add a default \"swipe\" transition between slides and support for user-defined transitions.
"},{"location":"changelog/#046","title":"0.4.6","text":"

Released 2023-01-19

"},{"location":"changelog/#changed_1","title":"Changed","text":"
  • #208 Unpinned textual==0.4.0 and allowed textual>=0.10.0, which includes textual#1558.
"},{"location":"changelog/#045","title":"0.4.5","text":"

Released 2023-01-16

"},{"location":"changelog/#added_1","title":"Added","text":"
  • #205 Add Triggers.take to make gradually revealing content on a slide more straightforward.
"},{"location":"changelog/#fixed","title":"Fixed","text":"
  • #202 Returning un-renderable content from a slide content function now displays an error instead of crashing Spiel.
"},{"location":"changelog/#changed_2","title":"Changed","text":"
  • #203 The Image example in the demo deck is now centered inside its Panel.
"},{"location":"changelog/#044","title":"0.4.4","text":"

Released 2023-01-13

"},{"location":"changelog/#added_2","title":"Added","text":"
  • #185 The docs page now includes copy-to-clipboard buttons on all code snippets.
  • #194 The demo slides now render their own source code directly to demo bindings functionality.
"},{"location":"changelog/#changed_3","title":"Changed","text":"
  • #194 The Deck.slide decorator now returns the decorated function, not the Slide it was attached to.
  • #199 The CLI command spiel present's --watch option now defaults to the parent directory of the deck file instead of the current working directory.
"},{"location":"changelog/#043","title":"0.4.3","text":"

Released 2023-01-02

"},{"location":"changelog/#added_3","title":"Added","text":"
  • #169 The Textual application title and subtitle are now set dynamically from the Spiel deck name and slide title, respectively.
  • #178 spiel.Deck is now a Sequence[Slide], and spiel.Triggers is now a Sequence[float].
"},{"location":"changelog/#fixed_1","title":"Fixed","text":"
  • #168 The correct type for the suspend optional argument to slide-level keybinding functions is now available as spiel.SuspendType.
  • #168 The Spiel container image no longer has a leftover copy of the spiel package directory inside the image under /app.
"},{"location":"changelog/#042","title":"0.4.2","text":"

Released 2022-12-10

"},{"location":"changelog/#added_4","title":"Added","text":"
  • #163 Added a public spiel.present() function that presents the deck at the given file.
"},{"location":"changelog/#041","title":"0.4.1","text":"

Released 2022-11-25

"},{"location":"changelog/#fixed_2","title":"Fixed","text":"
  • #157 Pinned to Textual v0.4.0 to work around Textual#1274.
"},{"location":"changelog/#040","title":"0.4.0","text":"

Released 2022-11-25

"},{"location":"changelog/#changed_4","title":"Changed","text":"
  • #154 Switch to Textual as the overall control and rendering engine.
"},{"location":"changelog/#removed","title":"Removed","text":"
  • #154 Removed library-provided Example slides, Options, and various other small features as part of the Textual migration. Some of these features will likely be reintroduced later.
"},{"location":"changelog/#030","title":"0.3.0","text":""},{"location":"changelog/#removed_1","title":"Removed","text":"
  • #129 Dropped support for Python <=3.9.
"},{"location":"contributing/","title":"Contributing Guide","text":"

Spiel is open to contributions!

  • Report bugs and request features
  • General discussion
  • Pull requests
"},{"location":"contributing/#development-environment","title":"Development Environment","text":"

Spiel uses:

  • poetry to manage development dependencies.
  • pre-commit to run various linters and formatters.
  • pytest for testing and mypy for static type-checking.
  • mkdocs with the Material theme for documentation.
"},{"location":"contributing/#initial-setup","title":"Initial Setup","text":"

To set up a local development environment after cloning the repository:

  1. Install poetry.
  2. Run poetry shell to create a virtual environment for spiel and spawn a new shell session with that virtual environment activated. In the future you'll run poetry shell again to activate the virtual environment.
  3. Run poetry install to install Spiel's dependencies.
  4. Run pre-commit install to configure pre-commit's integration with git. Do not commit without pre-commit installed!
"},{"location":"contributing/#running-tests-and-type-checking","title":"Running Tests and Type-Checking","text":"

Run pytest to run tests.

Run mypy to check types.

"},{"location":"contributing/#building-the-docs-locally","title":"Building the Docs Locally","text":"

To build the docs and start a local web server to view the results of your edits with live reloading, run

mkdocs serve\n
from the repository root.

"},{"location":"gallery/","title":"Gallery","text":"
  • pytest: It's What's For Testing by JoshKarpel.
"},{"location":"gallery/#submitting-to-the-gallery","title":"Submitting to the Gallery","text":"

If you've made a talk with Spiel, please feel free to submit a pull request to add it to the gallery.

"},{"location":"presenting/","title":"Presenting Decks","text":"

Depending on your preferred workflow, you can start a presentation in a variety of different ways.

Sandboxed Execution

Spiel presentations are live Python code: they can do anything that Python can do. You may want to run untrusted presentations (or even your own presentations) inside a container (but remember, even containers are not perfectly safe!). We produce a container image that can be run by (for example) Docker.

Presentations without extra Python dependencies might just need to be bind-mounted into the container. For example, if your demo file is at $PWD/presentation/deck.py, you could do

$ docker run -it --rm --mount type=bind,source=$PWD/presentation,target=/presentation ghcr.io/joshkarpel/spiel spiel present /presentation/deck.py\n

If the presentation has extra dependencies (like other Python packages), we recommend building a new image that inherits our image (e.g., FROM ghcr.io/joshkarpel/spiel:vX.Y.Z). Spiel's image itself inherits from the Python base image.

"},{"location":"presenting/#using-the-spiel-cli","title":"Using the spiel CLI","text":"

Installing the Spiel package provides a CLI tool called spiel. The spiel present subcommand allows you to present a deck; run spiel present --help to see the arguments and available options.

"},{"location":"presenting/#using-the-present-function","title":"Using the present function","text":"

The present function lets you start a presentation programmatically (i.e., from a Python script).

If your deck is defined in talk/slides.py like so:

talk/slides.py
#!/usr/bin/env python\nfrom spiel import Deck, present\ndeck = Deck(...)\n...  # construct your deck\nif __name__ == \"__main__\":\npresent(__file__)\n

You can then present the deck by running the script:

python talk/slides.py\n
Or by running the script as a module (you must have a talk/__init__.py file):
python -m talk.slides\n
Or by running the script via its shebang (after running chmod +x talk/slides.py to mark talk/slides.py as executable):
talk/slides.py\n

"},{"location":"quickstart/","title":"Quick Start","text":""},{"location":"quickstart/#the-most-basic-deck","title":"The Most Basic Deck","text":"

After installing Spiel (pip install spiel), create a file called deck.py and copy this code into it:

deck.py
from rich.console import RenderableType\nfrom spiel import Deck, present\ndeck = Deck(name=\"Your Deck Name\")\n@deck.slide(title=\"Slide 1 Title\")\ndef slide_1() -> RenderableType:\nreturn \"Your content here!\"\nif __name__ == \"__main__\":\npresent(__file__)\n

That is the most basic Spiel presentation you can make. To present the deck, run python deck.py. You should see:

In the example above, you first create a Deck and provide the name of your presentation. Then you create slides by decorating functions with @deck.slide, providing the title of the slide. The slide function can return anything that Rich can render; that return value will be displayed as the slide's content when you present it. The order of the @deck.slide-decorated functions in your file is the order in which they will appear in your presentation.

Running python deck.py started the presentation because of the call to present in the if __name__ == \"__main__\" block.

To see available keybindings for doing things like moving between slides, press ? to open the help view, which should look like this:

"},{"location":"quickstart/#making-richer-slides","title":"Making Richer Slides","text":"

You can make your slides a lot prettier, of course. As mentioned above, Spiel renders its slides using Rich, so you can bring in Rich functionality to spruce up your slides. Let's explore some advanced features by recreating one of the slides from the demo deck. Update your deck.py file with these imports and utility definitions:

deck.py
import inspect\nfrom textwrap import dedent\nfrom rich.box import SQUARE\nfrom rich.console import RenderableType\nfrom rich.layout import Layout\nfrom rich.markdown import Markdown\nfrom rich.padding import Padding\nfrom rich.panel import Panel\nfrom rich.style import Style\nfrom rich.syntax import Syntax\nfrom spiel import Deck, Slide, present\nfrom spiel.deck import Deck\nSPIEL = \"[Spiel](https://github.com/JoshKarpel/spiel)\"\nRICH = \"[Rich](https://rich.readthedocs.io/)\"\ndef pad_markdown(markup: str) -> RenderableType:\nreturn Padding(Markdown(dedent(markup), justify=\"center\"), pad=(0, 5))\n

And then paste this code into your deck.py file below your first slide:

deck.py
@deck.slide(title=\"Decks and Slides\")\ndef code() -> RenderableType:\nmarkup = f\"\"\"\\\n        ## Decks are made of Slides\n        Here's the code for `Deck` and `Slide`!\n        The source code is pulled directly from the definitions via [inspect.getsource](https://docs.python.org/3/library/inspect.html#inspect.getsource).\n        ({RICH} supports syntax highlighting, so {SPIEL} does too!)\n        \"\"\"\nroot = Layout()\nupper = Layout(pad_markdown(markup), size=len(markup.split(\"\\n\")) + 1)\nlower = Layout()\nroot.split_column(upper, lower)\ndef make_code_panel(obj: type) -> RenderableType:\nlines, line_number = inspect.getsourcelines(obj)\nreturn Panel(\nSyntax(\n\"\".join(lines),\nlexer=\"python\",\nline_numbers=True,\nstart_line=line_number,\n),\nbox=SQUARE,\nborder_style=Style(dim=True),\nheight=len(lines) + 2,\n)\nlower.split_row(\nLayout(make_code_panel(Deck)),\nLayout(make_code_panel(Slide)),\n)\nreturn root\n

We start out by creating our text content and setting up some Layouts, which will let us divide the slide space into chunks. Then, we create the make_code_panel function to take some lines of code from the Deck and Slide classes and put them in a syntax-highlighted Panel (with some additional fancy Rich styling). Finally, we add the code panels to our layout side-by-side and return root, the top-level Layout.

Run python deck.py again and go to the second slide (press ? if you're not sure how to navigate!):

Check out the source code of the demo deck for more inspiration on ways to use Rich to make your slides beautiful! Spiel provides a spiel CLI tool to make this easy:

  • Present the demo deck in your terminal by running spiel demo present.
  • View the source in your terminal with spiel demo source.
  • Copy it to use as a starting point with spiel demo copy <destination>.

You can also check out the gallery to see talks that other users have made.

"},{"location":"slides/","title":"Making Slides","text":""},{"location":"slides/#slide-content-functions","title":"Slide Content Functions","text":"

Each slide's content is rendered by calling a \"content function\" that returns a Rich RenderableType.

There are two primary ways to define these content functions. For unique slides you can use the Deck.slide decorator:

from rich.align import Align\nfrom rich.console import RenderableType\nfrom rich.text import Text\nfrom spiel import Deck\ndeck = Deck(name=\"Deck Name\")\n@deck.slide(title=\"Slide Title\")\ndef slide_content() -> RenderableType:\nreturn Align(\nText.from_markup(\n\"[blue]Your[/blue] [red underline]content[/red underline] [green italic]here[/green italic]!\"\n),\nalign=\"center\",\nvertical=\"middle\",\n)\n

You might also find yourself wanting to create a set of slides programmatically (well, even more programmatically). You can use the Deck.add_slides function to add Slides that you've created manually to your deck.

from rich.align import Align\nfrom rich.console import RenderableType\nfrom rich.style import Style\nfrom rich.text import Text\nfrom spiel import Deck, Slide\ndeck = Deck(name=\"Deck Name\")\ndef make_slide(\ntitle_prefix: str,\ntext: Text,\n) -> Slide:\ndef content() -> RenderableType:\nreturn Align(text, align=\"center\", vertical=\"middle\")\nreturn Slide(title=f\"{title_prefix} Slide\", content=content)\ndeck.add_slides(\nmake_slide(title_prefix=\"First\", text=Text(\"Foo\", style=Style(color=\"blue\"))),\nmake_slide(title_prefix=\"Second\", text=Text(\"Bar\", style=Style(color=\"red\"))),\nmake_slide(title_prefix=\"Third\", text=Text(\"Baz\", style=Style(color=\"green\"))),\n)\n

This pattern is useful when you have a generic \"slide template\" that you want to feed multiple values into without copying a lot of code. You have the full power of Python to define your slides, so you can use as much (or as little) abstraction as you want.

Slides are added to the deck in execution order

The slide order in the presentation is determined by the order that the Deck.slide decorator and Deck.add_slides functions are used. The two methods can be freely mixed; just make sure to call them in the order you want the slides to be presented in.

"},{"location":"slides/#when-and-how-often-are-slide-content-functions-called","title":"When and how often are slide content functions called?","text":"

The slide content function is called for a wide variety of reasons and it is not generally possible to predict how many times or exactly when it will be called due a mix of time-interval-based and on-demand needs.

Here are some examples of when the content function will be called:

  • When you move to the slide in Slide view.
  • Sixty times per second while the slide is active in Slide view (see Triggers below).
  • When you switch to Deck view.
  • The active slide's content function will be called if the deck is reloaded.

Tip

Because of how many times they will be called, your content functions should be fast and stateless.

If your content function needs state, it should store and use it via the Fixtures discussed below.

"},{"location":"slides/#fixtures","title":"Fixtures","text":"

The slide content function can take extra keyword arguments that provide additional information for advanced rendering techniques.

To have Spiel pass your content function one of these fixtures, include a keyword argument with the corresponding fixture name in your content function's signature.

"},{"location":"slides/#triggers","title":"Triggers","text":"
  • Keyword: triggers
  • Type: Triggers

The triggers fixture is useful for making slides whose content depends either on relative time (e.g., time since the slide started being displayed) or where the content should change when the user \"triggers\" it (similar to how a PowerPoint animation can be configured to run On Click).

To trigger a slide, press t in Slide view while displaying it. Additionally, each slide is automatically triggered once when it starts being displayed so that properties like Triggers.time_since_last_trigger will always have usable values.

The Triggers object in any given call of the content function behaves like an immutable sequence of floats, which represent relative times (in seconds) at which the slide has been triggered. These relative times are comparable to each other, but are not comparable to values generated by e.g. time.time (they are generated by time.monotonic). Over multiple calls of the content function, the sequence of relative times is append-only: any trigger time that has been added to the sequence will stay there until the

Triggers.now is also available, representing the relative time that the slide is being rendered at.

Triggers are reset when changing slides: if you trigger a slide, go to another slide, then back to the initial slide, the triggers from the first \"instance\" of showing the slide not be remembered.

Trigger.now resolution

Your slide content function will be called every sixtieth of a second, so the best time resolution you can get is about 16 milliseconds between renders, and therefore between Trigger.now values.

"},{"location":"slides/#revealing-content-using-triggers","title":"Revealing Content using Triggers","text":"

A simple use case for triggers is to gradually reveal content. We won't even use the \"relative time\" component for this: we'll just track how many times the slide has been triggered.

from rich.align import Align\nfrom rich.console import Group, RenderableType\nfrom rich.padding import Padding\nfrom rich.style import Style\nfrom rich.text import Text\nfrom spiel import Deck, Triggers\ndeck = Deck(name=\"Trigger Examples\")\n@deck.slide(title=\"Revealing Content\")\ndef reveal(triggers: Triggers) -> RenderableType:\nlines = [\nText.from_markup(\nf\"This slide has been triggered [yellow]{len(triggers)}[/yellow] time{'s' if len(triggers) > 1 else ''}.\"\n),\nText(\"First line.\", style=Style(color=\"red\")) if len(triggers) >= 1 else None,\nText(\"Second line.\", style=Style(color=\"blue\")) if len(triggers) >= 2 else None,\nText(\"Third line.\", style=Style(color=\"green\")) if len(triggers) >= 3 else None,\n]\nreturn Group(\n*(Padding(Align.center(line), pad=(0, 0, 1, 0)) for line in lines if line is not None)\n)\n

When first displayed, the slide will look like this:

Note that the slide has already been triggered once, even though we haven't pressed t yet! As mentioned above, each slide is automatically triggered once when it starts being displayed.

After pressing t to trigger the slide (really the second trigger):

And after pressing t again (really the third trigger):

"},{"location":"slides/#animating-content-using-triggers","title":"Animating Content using Triggers","text":"

Let's build a simple animation that is driven by the time since the slide started being displayed:

from math import floor\nfrom rich.align import Align\nfrom rich.console import Group, RenderableType\nfrom rich.panel import Panel\nfrom rich.text import Text\nfrom spiel import Deck, Triggers\ndeck = Deck(name=\"Trigger Examples\")\n@deck.slide(title=\"Animating Content\")\ndef animate(triggers: Triggers) -> RenderableType:\nbang = \"!\"\nspace = \" \"\nbar_length = 5\nspaces_before_bang = min(floor(triggers.time_since_first_trigger), bar_length)\nspaces_after_bang = bar_length - spaces_before_bang\nbar = (space * spaces_before_bang) + bang + (space * spaces_after_bang)\nreturn Align(\nGroup(\nAlign.center(Text(f\"{triggers=}\")),\nAlign.center(Text(f\"{spaces_before_bang=} | {spaces_after_bang=}\")),\nAlign.center(Panel(Text(bar), expand=False, height=3)),\n),\nalign=\"center\",\nvertical=\"middle\",\n)\n

Here are some screenshots showing what the slide looks like at various times after being displayed, with no additional key presses:

"},{"location":"transitions/","title":"Slide Transitions","text":"

Under construction!

Transitions are a new and experiment feature in Spiel and the interface might change dramatically from version to version. If you plan on using transitions, we recommend pinning the exact version of Spiel your presentation was developed in to ensure stability.

"},{"location":"transitions/#setting-transitions","title":"Setting Transitions","text":"

To set the default transition for the entire deck, which will be used if a slide does not override it, set Deck.default_transition to a type that implements the Transition protocol.

For example, the default transition is Swipe, so not passing default_transition at all is equivalent to

from spiel import Deck, Swipe\ndeck = Deck(name=f\"Spiel Demo Deck\", default_transition=Swipe)\n

To override the deck-wide default for an individual slide, specify the transition type in the @slide decorator:

from spiel import Deck, Swipe\ndeck = Deck(name=f\"Spiel Demo Deck\")\n@deck.slide(title=\"My Title\", transition=Swipe)\ndef slide():\n...\n

Or, in the arguments to Slide:

from spiel import Slide, Swipe\nslide = Slide(title=\"My Title\", transition=Swipe)\n

In either case, the specified transition will be used when transitioning to that slide. It does not matter whether the slide is the \"next\" or \"previous\" slide: the slide being moved to determines which transition effect will be used.

"},{"location":"transitions/#disabling-transitions","title":"Disabling Transitions","text":"

In any of the above examples, you can also set default_transition/transition to None. In that case, there will be no transition effect when moving to the slide; it will just be displayed on the next render, already in-place.

"},{"location":"transitions/#writing-custom-transitions","title":"Writing Custom Transitions","text":"

To implement your own custom transition, you must write a class which implements the Transition protocol.

The protocol is:

Transition Protocol
from __future__ import annotations\nfrom enum import Enum\nfrom typing import Protocol, runtime_checkable\nfrom textual.widget import Widget\nclass Direction(Enum):\n\"\"\"\n    An enumeration that describes which direction a slide transition\n    animation should move in: whether we're going to the next slide,\n    or to the previous slide.\n    \"\"\"\nNext = \"next\"\n\"\"\"Indicates that the transition should handle going to the next slide.\"\"\"\nPrevious = \"previous\"\n\"\"\"Indicates that the transition should handle going to the previous slide.\"\"\"\n@runtime_checkable\nclass Transition(Protocol):\n\"\"\"\n    A protocol that describes how to implement a transition animation.\n    See [Writing Custom Transitions](./transitions.md#writing-custom-transitions)\n    for more details on how to implement the protocol.\n    \"\"\"\ndef initialize(\nself,\nfrom_widget: Widget,\nto_widget: Widget,\ndirection: Direction,\n) -> None:\n\"\"\"\n        A hook function to set up any CSS that should be present at the start of the transition.\n        Args:\n            from_widget: The widget showing the slide that we are leaving.\n            to_widget: The widget showing the slide that we are entering.\n            direction: The desired direction of the transition animation.\n        \"\"\"\n...\ndef progress(\nself,\nfrom_widget: Widget,\nto_widget: Widget,\ndirection: Direction,\nprogress: float,\n) -> None:\n\"\"\"\n        A hook function that is called each time the `progress`\n        of the transition animation updates.\n        Args:\n            from_widget: The widget showing the slide that we are leaving.\n            to_widget: The widget showing the slide that we are entering.\n            direction: The desired direction of the transition animation.\n            progress: The progress of the animation, as a percentage\n                (e.g., initial state is `0`, final state is `100`).\n                Note that this is **not necessarily** bounded between `0` and `100`,\n                nor is it necessarily [monotonically increasing](https://en.wikipedia.org/wiki/Monotonic_function),\n                depending on the underlying Textual animation easing function,\n                which may overshoot or bounce.\n                However, it will always start at `0` and end at `100`,\n                no matter which `direction` the transition should move in.\n        \"\"\"\n...\n

As an example, consider the Swipe transition included in Spiel:

Swipe Transition
from __future__ import annotations\nfrom textual.widget import Widget\nfrom spiel.transitions.protocol import Direction, Transition\nclass Swipe(Transition):\n\"\"\"\n    A transition where the current and incoming slide are placed side-by-side\n    and gradually slide across the screen,\n    with the current slide leaving and the incoming slide entering.\n    \"\"\"\ndef initialize(\nself,\nfrom_widget: Widget,\nto_widget: Widget,\ndirection: Direction,\n) -> None:\nmatch direction:\ncase Direction.Next:\nto_widget.styles.offset = (\"100%\", 0)\ncase Direction.Previous:\nto_widget.styles.offset = (\"-100%\", 0)\ndef progress(\nself,\nfrom_widget: Widget,\nto_widget: Widget,\ndirection: Direction,\nprogress: float,\n) -> None:\nmatch direction:\ncase Direction.Next:\nfrom_widget.styles.offset = (f\"-{progress:.2f}%\", 0)\nto_widget.styles.offset = (f\"{100 - progress:.2f}%\", 0)\ncase Direction.Previous:\nfrom_widget.styles.offset = (f\"{progress:.2f}%\", 0)\nto_widget.styles.offset = (f\"-{100 - progress:.2f}%\", 0)\n

The transition effect is implemented using Textual CSS styles on the widgets that represent the \"from\" and \"to\" widgets.

Because the slide widgets are on different layers, they would normally both try to render in the \"upper left corner\" of the screen, and since the from slide is on the upper layer, it would be the one that actually gets rendered.

In Swipe.initialize, the to widget is moved to either the left or the right (depending on the transition direction) by 100%, i.e., it's own width. This puts the slides side-by-side, with the to slide fully off-screen.

As the transition progresses, the horizontal offsets of the two widgets are adjusted in lockstep so that they appear to move across the screen. Again, the direction of offset adjustment depends on the transition direction. The absolute value of the horizontal offsets always sums to 100%, which keeps the slides glued together as they move across the screen.

When progress=100 in the final state, the to widget will be at zero horizontal offset, and the from widget will be at plus or minus 100%, fully moved off-screen.

Contribute your transitions!

If you have developed a cool transition, consider contributing it to Spiel!

"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..c5515df --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,48 @@ + + + + https://www.spiel.how/ + 2023-10-07 + daily + + + https://www.spiel.how/api/ + 2023-10-07 + daily + + + https://www.spiel.how/changelog/ + 2023-10-07 + daily + + + https://www.spiel.how/contributing/ + 2023-10-07 + daily + + + https://www.spiel.how/gallery/ + 2023-10-07 + daily + + + https://www.spiel.how/presenting/ + 2023-10-07 + daily + + + https://www.spiel.how/quickstart/ + 2023-10-07 + daily + + + https://www.spiel.how/slides/ + 2023-10-07 + daily + + + https://www.spiel.how/transitions/ + 2023-10-07 + daily + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 0000000..798f2be Binary files /dev/null and b/sitemap.xml.gz differ diff --git a/slides/index.html b/slides/index.html new file mode 100644 index 0000000..4d3c0cb --- /dev/null +++ b/slides/index.html @@ -0,0 +1,836 @@ + + + + + + + + + + + + + + + + + + + + + + + + Making Slides - Spiel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + +

Making Slides

+

Slide Content Functions

+

Each slide's content is rendered by calling a "content function" that returns a +Rich RenderableType.

+

There are two primary ways to define these content functions. +For unique slides you can use the Deck.slide decorator:

+

from rich.align import Align
+from rich.console import RenderableType
+from rich.text import Text
+
+from spiel import Deck
+
+deck = Deck(name="Deck Name")
+
+
+@deck.slide(title="Slide Title")
+def slide_content() -> RenderableType:
+    return Align(
+        Text.from_markup(
+            "[blue]Your[/blue] [red underline]content[/red underline] [green italic]here[/green italic]!"
+        ),
+        align="center",
+        vertical="middle",
+    )
+
+Slide content via decorator

+

You might also find yourself wanting to create a set of slides programmatically +(well, even more programmatically). +You can use the Deck.add_slides function to add +Slides that you've created manually to your deck.

+
from rich.align import Align
+from rich.console import RenderableType
+from rich.style import Style
+from rich.text import Text
+
+from spiel import Deck, Slide
+
+deck = Deck(name="Deck Name")
+
+
+def make_slide(
+    title_prefix: str,
+    text: Text,
+) -> Slide:
+    def content() -> RenderableType:
+        return Align(text, align="center", vertical="middle")
+
+    return Slide(title=f"{title_prefix} Slide", content=content)
+
+
+deck.add_slides(
+    make_slide(title_prefix="First", text=Text("Foo", style=Style(color="blue"))),
+    make_slide(title_prefix="Second", text=Text("Bar", style=Style(color="red"))),
+    make_slide(title_prefix="Third", text=Text("Baz", style=Style(color="green"))),
+)
+
+

Slide content via loop 1 +Slide content via loop 2 +Slide content via loop 3

+

This pattern is useful when you have a generic "slide template" +that you want to feed multiple values into without copying a lot of code. +You have the full power of Python to define your slides, +so you can use as much (or as little) abstraction as you want.

+
+

Slides are added to the deck in execution order

+

The slide order in the presentation is determined by the order +that the Deck.slide decorator and Deck.add_slides functions are used. +The two methods can be freely mixed; +just make sure to call them in the order you want the slides to +be presented in.

+
+

When and how often are slide content functions called?

+

The slide content function is called for a wide variety of reasons +and it is not generally possible to predict how many times or exactly when +it will be called due a mix of time-interval-based and on-demand needs.

+

Here are some examples of when the content function will be called:

+
    +
  • When you move to the slide in Slide view.
  • +
  • Sixty times per second while the slide is active in Slide view (see Triggers below).
  • +
  • When you switch to Deck view.
  • +
  • The active slide's content function will be called if the deck is reloaded.
  • +
+
+

Tip

+

Because of how many times they will be called, +your content functions should be fast and stateless.

+

If your content function needs state, +it should store and use it via the Fixtures discussed below.

+
+

Fixtures

+

The slide content function can take extra +keyword arguments +that provide additional information for advanced rendering techniques.

+

To have Spiel pass your content function one of these fixtures, +include a keyword argument with the corresponding fixture name in your content function's signature.

+

Triggers

+ +

The triggers fixture is useful for making slides whose content depends either on +relative time (e.g., time since the slide started being displayed) +or where the content should change when the user "triggers" it +(similar to how a PowerPoint animation can be configured to run +On Click).

+

To trigger a slide, press t in Slide view while displaying it. +Additionally, each slide is automatically triggered once when it starts being +displayed so that properties like +Triggers.time_since_last_trigger +will always have usable values.

+

The Triggers object in any given call of the content function behaves like an immutable sequence of floats, +which represent relative times (in seconds) at which the slide has been triggered. +These relative times are comparable to each other, but are not comparable +to values generated by e.g. time.time (they are generated by time.monotonic). +Over multiple calls of the content function, +the sequence of relative times is append-only: +any trigger time that has been added to the sequence will stay there until the

+

Triggers.now is also available, +representing the relative time that the slide is being rendered at.

+

Triggers are reset when changing slides: +if you trigger a slide, +go to another slide, +then back to the initial slide, +the triggers from the first "instance" +of showing the slide not be remembered.

+
+

Trigger.now resolution

+

Your slide content function will be called every sixtieth of a second, +so the best time resolution you can get is about 16 milliseconds between +renders, and therefore between Trigger.now values.

+
+

Revealing Content using Triggers

+

A simple use case for triggers is to gradually reveal content. +We won't even use the "relative time" component for this: +we'll just track how many times the slide has been triggered.

+
from rich.align import Align
+from rich.console import Group, RenderableType
+from rich.padding import Padding
+from rich.style import Style
+from rich.text import Text
+
+from spiel import Deck, Triggers
+
+deck = Deck(name="Trigger Examples")
+
+
+@deck.slide(title="Revealing Content")
+def reveal(triggers: Triggers) -> RenderableType:
+    lines = [
+        Text.from_markup(
+            f"This slide has been triggered [yellow]{len(triggers)}[/yellow] time{'s' if len(triggers) > 1 else ''}."
+        ),
+        Text("First line.", style=Style(color="red")) if len(triggers) >= 1 else None,
+        Text("Second line.", style=Style(color="blue")) if len(triggers) >= 2 else None,
+        Text("Third line.", style=Style(color="green")) if len(triggers) >= 3 else None,
+    ]
+
+    return Group(
+        *(Padding(Align.center(line), pad=(0, 0, 1, 0)) for line in lines if line is not None)
+    )
+
+

When first displayed, the slide will look like this:

+

Triggers reveal 1

+

Note that the slide has already been triggered once, +even though we haven't pressed t yet! +As mentioned above, each slide is automatically triggered once +when it starts being displayed.

+

After pressing t to trigger the slide (really the second trigger):

+

Triggers reveal 2

+

And after pressing t again (really the third trigger):

+

Triggers reveal 3

+

Animating Content using Triggers

+

Let's build a simple animation that is driven by the time since the slide +started being displayed:

+
from math import floor
+
+from rich.align import Align
+from rich.console import Group, RenderableType
+from rich.panel import Panel
+from rich.text import Text
+
+from spiel import Deck, Triggers
+
+deck = Deck(name="Trigger Examples")
+
+
+@deck.slide(title="Animating Content")
+def animate(triggers: Triggers) -> RenderableType:
+    bang = "!"
+    space = " "
+    bar_length = 5
+
+    spaces_before_bang = min(floor(triggers.time_since_first_trigger), bar_length)
+    spaces_after_bang = bar_length - spaces_before_bang
+
+    bar = (space * spaces_before_bang) + bang + (space * spaces_after_bang)
+
+    return Align(
+        Group(
+            Align.center(Text(f"{triggers=}")),
+            Align.center(Text(f"{spaces_before_bang=} | {spaces_after_bang=}")),
+            Align.center(Panel(Text(bar), expand=False, height=3)),
+        ),
+        align="center",
+        vertical="middle",
+    )
+
+

Here are some screenshots showing what the slide looks like at various times +after being displayed, with no additional key presses:

+

Triggers animation 1 +Triggers animation 2 +Triggers animation 3 +Triggers animation 4

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/transitions/index.html b/transitions/index.html new file mode 100644 index 0000000..3979066 --- /dev/null +++ b/transitions/index.html @@ -0,0 +1,741 @@ + + + + + + + + + + + + + + + + + + + + + + + + Slide Transitions - Spiel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + +

Slide Transitions

+
+

Under construction!

+

Transitions are a new and experiment feature in Spiel +and the interface might change dramatically from version to version. +If you plan on using transitions, we recommend pinning the +exact version of Spiel your presentation was developed in to ensure stability.

+
+

Setting Transitions

+

To set the default transition for the entire deck, +which will be used if a slide does not override it, +set Deck.default_transition to +a type that implements the Transition +protocol.

+

For example, the default transition is Swipe, +so not passing default_transition at all is equivalent to

+
from spiel import Deck, Swipe
+
+deck = Deck(name=f"Spiel Demo Deck", default_transition=Swipe)
+
+

To override the deck-wide default for an individual slide, +specify the transition type in the @slide decorator:

+
from spiel import Deck, Swipe
+
+deck = Deck(name=f"Spiel Demo Deck")
+
+@deck.slide(title="My Title", transition=Swipe)
+def slide():
+    ...
+
+

Or, in the arguments to Slide:

+
from spiel import Slide, Swipe
+
+slide = Slide(title="My Title", transition=Swipe)
+
+

In either case, the specified transition will be used when +transitioning to that slide. +It does not matter whether the slide is the "next" or "previous" +slide: the slide being moved to determines which transition +effect will be used.

+

Disabling Transitions

+

In any of the above examples, you can also set default_transition/transition to None. +In that case, there will be no transition effect when moving to the slide; +it will just be displayed on the next render, already in-place.

+

Writing Custom Transitions

+

To implement your own custom transition, you must write a class which implements +the Transition protocol.

+

The protocol is:

+
Transition Protocol
from __future__ import annotations
+
+from enum import Enum
+from typing import Protocol, runtime_checkable
+
+from textual.widget import Widget
+
+
+class Direction(Enum):
+    """
+    An enumeration that describes which direction a slide transition
+    animation should move in: whether we're going to the next slide,
+    or to the previous slide.
+    """
+
+    Next = "next"
+    """Indicates that the transition should handle going to the next slide."""
+
+    Previous = "previous"
+    """Indicates that the transition should handle going to the previous slide."""
+
+
+@runtime_checkable
+class Transition(Protocol):
+    """
+    A protocol that describes how to implement a transition animation.
+
+    See [Writing Custom Transitions](./transitions.md#writing-custom-transitions)
+    for more details on how to implement the protocol.
+    """
+
+    def initialize(
+        self,
+        from_widget: Widget,
+        to_widget: Widget,
+        direction: Direction,
+    ) -> None:
+        """
+        A hook function to set up any CSS that should be present at the start of the transition.
+
+        Args:
+            from_widget: The widget showing the slide that we are leaving.
+            to_widget: The widget showing the slide that we are entering.
+            direction: The desired direction of the transition animation.
+        """
+        ...
+
+    def progress(
+        self,
+        from_widget: Widget,
+        to_widget: Widget,
+        direction: Direction,
+        progress: float,
+    ) -> None:
+        """
+        A hook function that is called each time the `progress`
+        of the transition animation updates.
+
+        Args:
+            from_widget: The widget showing the slide that we are leaving.
+            to_widget: The widget showing the slide that we are entering.
+            direction: The desired direction of the transition animation.
+            progress: The progress of the animation, as a percentage
+                (e.g., initial state is `0`, final state is `100`).
+                Note that this is **not necessarily** bounded between `0` and `100`,
+                nor is it necessarily [monotonically increasing](https://en.wikipedia.org/wiki/Monotonic_function),
+                depending on the underlying Textual animation easing function,
+                which may overshoot or bounce.
+                However, it will always start at `0` and end at `100`,
+                no matter which `direction` the transition should move in.
+        """
+        ...
+
+

As an example, consider the Swipe transition included in Spiel:

+
Swipe Transition
from __future__ import annotations
+
+from textual.widget import Widget
+
+from spiel.transitions.protocol import Direction, Transition
+
+
+class Swipe(Transition):
+    """
+    A transition where the current and incoming slide are placed side-by-side
+    and gradually slide across the screen,
+    with the current slide leaving and the incoming slide entering.
+    """
+
+    def initialize(
+        self,
+        from_widget: Widget,
+        to_widget: Widget,
+        direction: Direction,
+    ) -> None:
+        match direction:
+            case Direction.Next:
+                to_widget.styles.offset = ("100%", 0)
+            case Direction.Previous:
+                to_widget.styles.offset = ("-100%", 0)
+
+    def progress(
+        self,
+        from_widget: Widget,
+        to_widget: Widget,
+        direction: Direction,
+        progress: float,
+    ) -> None:
+        match direction:
+            case Direction.Next:
+                from_widget.styles.offset = (f"-{progress:.2f}%", 0)
+                to_widget.styles.offset = (f"{100 - progress:.2f}%", 0)
+            case Direction.Previous:
+                from_widget.styles.offset = (f"{progress:.2f}%", 0)
+                to_widget.styles.offset = (f"-{100 - progress:.2f}%", 0)
+
+

The transition effect is implemented using +Textual CSS styles +on the widgets +that represent the "from" and "to" widgets.

+

Because the slide widgets are on different layers, +they would normally both try to render in the "upper left corner" of the screen, +and since the from slide is on the upper layer, it would be the one that actually gets rendered.

+

In Swipe.initialize, the to widget is moved to either the left or the right +(depending on the transition direction) by 100%, i.e., it's own width. +This puts the slides side-by-side, with the to slide fully off-screen.

+

As the transition progresses, the horizontal offsets of the two widgets are adjusted in lockstep +so that they appear to move across the screen. +Again, the direction of offset adjustment depends on the transition direction. +The absolute value of the horizontal offsets always sums to 100%, which keeps the slides glued together +as they move across the screen.

+

When progress=100 in the final state, the to widget will be at zero horizontal offset, +and the from widget will be at plus or minus 100%, fully moved off-screen.

+
+

Contribute your transitions!

+

If you have developed a cool transition, consider contributing it to Spiel!

+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file