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.Deck
+
+
+
+ dataclass
+
+
+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
.
PARAMETER | +DESCRIPTION | +
---|---|
title |
+
+ The title to display for the slide. +
+
+ TYPE:
+ |
+
bindings |
+
+ A mapping of +keys +to callables to be executed when those keys are pressed, +when on this slide. +
+
+ TYPE:
+ |
+
transition |
+
+ The transition animation to use when moving to this slide.
+Set to
+
+ TYPE:
+ |
+
add_slides(*slides)
+
+Add Slide
s 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
+
+ TYPE:
+ |
+
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.
spiel.Triggers
+
+
+
+ dataclass
+
+
+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.
PARAMETER | +DESCRIPTION | +
---|---|
iter |
+
+ The iterable to take elements from. +
+
+ TYPE:
+ |
+
offset |
+
+ This
+
+ TYPE:
+ |
+
RETURNS | +DESCRIPTION | +
---|---|
+
+ Iterator[T]
+
+ |
+
+ An iterator over the first |
+
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.
+ +PARAMETER | +DESCRIPTION | +
---|---|
from_widget |
+
+ The widget showing the slide that we are leaving. +
+
+ TYPE:
+ |
+
to_widget |
+
+ The widget showing the slide that we are entering. +
+
+ TYPE:
+ |
+
direction |
+
+ The desired direction of the transition animation. +
+
+ TYPE:
+ |
+
progress(from_widget, to_widget, direction, progress)
+
+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:
+ |
+
to_widget |
+
+ The widget showing the slide that we are entering. +
+
+ TYPE:
+ |
+
direction |
+
+ The desired direction of the transition animation. +
+
+ TYPE:
+ |
+
progress |
+
+ The progress of the animation, as a percentage
+(e.g., initial state is
+
+ TYPE:
+ |
+
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.
+ + + + + +spiel.present(deck_path, watch_path=None)
+
+Present the deck defined in the given deck_path
.
PARAMETER | +DESCRIPTION | +
---|---|
deck_path |
+
+ The file to look for a deck in. + + |
+
watch_path |
+
+ When filesystem changes are detected below this path (recursively), reload the deck from the |
+
0.5.1
Released 2023-04-21
0.11.1
temporarily to resolve issues with slide transitions.0.5.0
Released 2023-02-19
0.4.6
Released 2023-01-19
textual==0.4.0
and allowed textual>=0.10.0
, which includes textual#1558.0.4.5
Released 2023-01-16
Triggers.take
to make gradually revealing content on a slide more straightforward.Image
example in the demo deck is now centered inside its Panel
.0.4.4
Released 2023-01-13
Deck.slide
decorator now returns the decorated function, not the Slide
it was attached to.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
spiel.Deck
is now a Sequence[Slide]
, and spiel.Triggers
is now a Sequence[float]
.suspend
optional argument to slide-level keybinding functions is now available as spiel.SuspendType
.spiel
package directory inside the image under /app
.0.4.2
Released 2022-12-10
spiel.present()
function that presents the deck at the given file.0.4.1
Released 2022-11-25
0.4.0
Released 2022-11-25
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
<=3.9
.Spiel is open to contributions!
+ +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.To set up a local development environment after cloning the repository:
+poetry
.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.poetry install
to install Spiel's dependencies.pre-commit install
to configure pre-commit
's integration with git
.
+ Do not commit without pre-commit
installed!Run pytest
to run tests.
Run mypy
to check types.
To build the docs and start a local web server to view the results of your edits with live reloading, run +
+from the repository root. + + + + + + +If you've made a talk with Spiel, please feel free to submit a pull request +to add it to the gallery.
+ + + + + + +Spiel +is a framework for building and presenting +richly-styled presentations in your terminal using Python.
++ +
+ + + + + + +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.
spiel
CLIInstalling 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.
present
functionThe 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:
#!/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: +
+Or by running the script as a module (you must have atalk/__init__.py
file):
+
+Or by running the script via its shebang
+(after running chmod +x talk/slides.py
to mark talk/slides.py
as executable):
+
+
+
+
+
+
+
+ After installing Spiel (pip install spiel
),
+create a file called deck.py
and copy this code into it:
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:
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:
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:
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.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 Layout
s, 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:
spiel demo present
.spiel demo source
.spiel demo copy <destination>
.You can also check out the gallery to see talks that other users have made.
+ + + + + + +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.
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.
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
.
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)
","text":"Add Slide
s 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.
*slides
The Slide
s to add.
TYPE: Slide
DEFAULT: ()
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.
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.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.
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.
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.
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
Iterator[T]
An iterator over the first len(self) - offset
elements of iter
.
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 DESCRIPTIONfrom_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)
","text":"A hook function that is called each time the progress
of the transition animation updates.
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
","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
.
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
0.5.1
","text":"Released 2023-04-21
0.11.1
temporarily to resolve issues with slide transitions.0.5.0
","text":"Released 2023-02-19
0.4.6
","text":"Released 2023-01-19
textual==0.4.0
and allowed textual>=0.10.0
, which includes textual#1558.0.4.5
","text":"Released 2023-01-16
Triggers.take
to make gradually revealing content on a slide more straightforward.Image
example in the demo deck is now centered inside its Panel
.0.4.4
","text":"Released 2023-01-13
Deck.slide
decorator now returns the decorated function, not the Slide
it was attached to.spiel present
's --watch
option now defaults to the parent directory of the deck file instead of the current working directory.0.4.3
","text":"Released 2023-01-02
spiel.Deck
is now a Sequence[Slide]
, and spiel.Triggers
is now a Sequence[float]
.suspend
optional argument to slide-level keybinding functions is now available as spiel.SuspendType
.spiel
package directory inside the image under /app
.0.4.2
","text":"Released 2022-12-10
spiel.present()
function that presents the deck at the given file.0.4.1
","text":"Released 2022-11-25
0.4.0
","text":"Released 2022-11-25
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
","text":""},{"location":"changelog/#removed_1","title":"Removed","text":"<=3.9
.Spiel is open to contributions!
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.To set up a local development environment after cloning the repository:
poetry
.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.poetry install
to install Spiel's dependencies.pre-commit install
to configure pre-commit
's integration with git
. Do not commit without pre-commit
installed!Run pytest
to run tests.
Run mypy
to check types.
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":"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.
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.
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:
#!/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:
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:
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:
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.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 Layout
s, 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:
spiel demo present
.spiel demo source
.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 Slide
s 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.
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:
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":"triggers
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.
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):
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.
To implement your own custom transition, you must write a class which implements the Transition
protocol.
The protocol is:
Transition Protocolfrom __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:
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 @@ + +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",
+ )
+
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
+Slide
s 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"))),
+)
+
+ +
+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.
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:
+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.
+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
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.
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:
+ +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):
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:
++ + +
+ + + + + + +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.
+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
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
:
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.
+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.
To implement your own custom transition, you must write a class which implements
+the Transition
protocol.
The protocol is:
+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:
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!
+