zettcode-tui 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. zettcode/tui/__init__.py +422 -0
  2. zettcode/tui/_compat.py +32 -0
  3. zettcode/tui/capabilities.py +107 -0
  4. zettcode/tui/clipboard.py +91 -0
  5. zettcode/tui/console.py +229 -0
  6. zettcode/tui/core/__init__.py +63 -0
  7. zettcode/tui/core/app.py +658 -0
  8. zettcode/tui/core/events.py +134 -0
  9. zettcode/tui/core/focus.py +85 -0
  10. zettcode/tui/core/geometry.py +212 -0
  11. zettcode/tui/core/host.py +66 -0
  12. zettcode/tui/core/keymap.py +218 -0
  13. zettcode/tui/core/scheduler.py +105 -0
  14. zettcode/tui/core/screen.py +80 -0
  15. zettcode/tui/core/theme.py +248 -0
  16. zettcode/tui/core/widget.py +204 -0
  17. zettcode/tui/gallery.py +588 -0
  18. zettcode/tui/glyphs.py +73 -0
  19. zettcode/tui/input/__init__.py +18 -0
  20. zettcode/tui/input/bridge.py +46 -0
  21. zettcode/tui/input/decoder.py +234 -0
  22. zettcode/tui/input/events.py +64 -0
  23. zettcode/tui/input/keys.py +289 -0
  24. zettcode/tui/input/reader.py +147 -0
  25. zettcode/tui/layout/__init__.py +24 -0
  26. zettcode/tui/layout/box.py +153 -0
  27. zettcode/tui/layout/overlay.py +164 -0
  28. zettcode/tui/layout/scroll.py +477 -0
  29. zettcode/tui/layout/solver.py +95 -0
  30. zettcode/tui/layout/spacing.py +137 -0
  31. zettcode/tui/py.typed +0 -0
  32. zettcode/tui/render/__init__.py +71 -0
  33. zettcode/tui/render/ansi.py +371 -0
  34. zettcode/tui/render/canvas.py +153 -0
  35. zettcode/tui/render/code.py +456 -0
  36. zettcode/tui/render/color.py +177 -0
  37. zettcode/tui/render/renderer.py +151 -0
  38. zettcode/tui/render/rich_text.py +80 -0
  39. zettcode/tui/render/style.py +121 -0
  40. zettcode/tui/render/sweep.py +87 -0
  41. zettcode/tui/render/text.py +319 -0
  42. zettcode/tui/runner.py +316 -0
  43. zettcode/tui/terminal.py +222 -0
  44. zettcode/tui/testing/__init__.py +6 -0
  45. zettcode/tui/testing/harness.py +220 -0
  46. zettcode/tui/testing/snapshot.py +64 -0
  47. zettcode/tui/theme_file.py +300 -0
  48. zettcode/tui/themes/ayu-mirage.toml +44 -0
  49. zettcode/tui/themes/catppuccin-latte.toml +44 -0
  50. zettcode/tui/themes/catppuccin-mocha.toml +44 -0
  51. zettcode/tui/themes/dracula.toml +44 -0
  52. zettcode/tui/themes/github-dimmed.toml +44 -0
  53. zettcode/tui/themes/github-light.toml +44 -0
  54. zettcode/tui/themes/gruvbox-dark.toml +44 -0
  55. zettcode/tui/themes/gruvbox-light.toml +44 -0
  56. zettcode/tui/themes/kanagawa.toml +44 -0
  57. zettcode/tui/themes/nord.toml +44 -0
  58. zettcode/tui/themes/one-dark.toml +44 -0
  59. zettcode/tui/themes/rose-pine-dawn.toml +44 -0
  60. zettcode/tui/themes/rose-pine.toml +44 -0
  61. zettcode/tui/themes/solarized-dark.toml +44 -0
  62. zettcode/tui/themes/solarized-light.toml +44 -0
  63. zettcode/tui/themes/tokyo-night.toml +44 -0
  64. zettcode/tui/widgets/__init__.py +121 -0
  65. zettcode/tui/widgets/collapsible.py +116 -0
  66. zettcode/tui/widgets/completion.py +211 -0
  67. zettcode/tui/widgets/console.py +442 -0
  68. zettcode/tui/widgets/dialog.py +165 -0
  69. zettcode/tui/widgets/diff.py +236 -0
  70. zettcode/tui/widgets/list.py +293 -0
  71. zettcode/tui/widgets/list_page.py +121 -0
  72. zettcode/tui/widgets/markdown.py +657 -0
  73. zettcode/tui/widgets/progress.py +166 -0
  74. zettcode/tui/widgets/rich_text.py +84 -0
  75. zettcode/tui/widgets/status.py +106 -0
  76. zettcode/tui/widgets/table.py +101 -0
  77. zettcode/tui/widgets/tasks.py +130 -0
  78. zettcode/tui/widgets/text.py +111 -0
  79. zettcode/tui/widgets/textarea.py +671 -0
  80. zettcode/tui/widgets/toast.py +137 -0
  81. zettcode_tui-0.4.0.dist-info/METADATA +20 -0
  82. zettcode_tui-0.4.0.dist-info/RECORD +83 -0
  83. zettcode_tui-0.4.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,147 @@
1
+ """Asynchronous bridge from the TTY file descriptor into decoded events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import signal
7
+ import threading
8
+ from collections.abc import Callable
9
+ from typing import Any
10
+
11
+ from ..terminal import Terminal
12
+ from .decoder import InputDecoder
13
+ from .events import EventType, InputEvent
14
+
15
+ #: How often a platform without a resize signal asks the console for its size.
16
+ RESIZE_POLL_SECONDS = 0.25
17
+
18
+ #: How long the reader thread waits for input before checking whether to stop.
19
+ POLL_SECONDS = 0.1
20
+
21
+
22
+ class AsyncInput:
23
+ """Publish decoded terminal input into one application queue."""
24
+
25
+ def __init__(self, terminal: Terminal, publish: Callable[[InputEvent], None]) -> None:
26
+ """Store the terminal, the publish sink, and a fresh decoder.
27
+
28
+ Args:
29
+ terminal: Owns the input file descriptor.
30
+ publish: Called synchronously on the event loop for every decoded
31
+ event, including the synthetic resize.
32
+ """
33
+ self.terminal = terminal
34
+ self.publish = publish
35
+ self.decoder = InputDecoder()
36
+ self.loop: asyncio.AbstractEventLoop | None = None
37
+ self._previous_resize_handler: Any = None
38
+ self._escape_handle: asyncio.TimerHandle | None = None
39
+ self._thread: threading.Thread | None = None
40
+ self._stopping = threading.Event()
41
+ self._resize_handle: asyncio.TimerHandle | None = None
42
+ self._size: tuple[int, int] | None = None
43
+
44
+ def start(self) -> None:
45
+ """Attach the reader and the resize handler to the running loop."""
46
+ self.loop = asyncio.get_running_loop()
47
+ if self.terminal.console.watches_descriptor:
48
+ self.loop.add_reader(self.terminal.input_fd, self._read_ready)
49
+ if hasattr(signal, "SIGWINCH"):
50
+ self._previous_resize_handler = signal.getsignal(signal.SIGWINCH)
51
+ self.loop.add_signal_handler(signal.SIGWINCH, self._resize)
52
+ return
53
+ # A Windows console handle is not something the loop can watch, so a
54
+ # thread does the blocking read and hands chunks back to the loop; with
55
+ # no SIGWINCH either, the size is polled on the same loop.
56
+ self._stopping.clear()
57
+ self._thread = threading.Thread(target=self._pump, name="zettcode-input", daemon=True)
58
+ self._thread.start()
59
+ self._size = self.terminal.size
60
+ self._resize_handle = self.loop.call_later(RESIZE_POLL_SECONDS, self._poll_resize)
61
+
62
+ def close(self) -> None:
63
+ """Detach every loop hook and restore the previous resize handler."""
64
+ if self.loop is None:
65
+ return
66
+ if self.terminal.console.watches_descriptor:
67
+ self.loop.remove_reader(self.terminal.input_fd)
68
+ if self._thread is not None:
69
+ self._stopping.set()
70
+ self._thread.join(timeout=1.0)
71
+ self._thread = None
72
+ if self._resize_handle is not None:
73
+ self._resize_handle.cancel()
74
+ self._resize_handle = None
75
+ if hasattr(signal, "SIGWINCH") and self.terminal.console.watches_descriptor:
76
+ self.loop.remove_signal_handler(signal.SIGWINCH)
77
+ if self._escape_handle is not None:
78
+ self._escape_handle.cancel()
79
+ self._escape_handle = None
80
+ if (
81
+ self.terminal.console.watches_descriptor
82
+ and hasattr(signal, "SIGWINCH")
83
+ and callable(self._previous_resize_handler)
84
+ ):
85
+ signal.signal(signal.SIGWINCH, self._previous_resize_handler)
86
+ self.loop = None
87
+
88
+ def _read_ready(self) -> None:
89
+ """Read one chunk on the loop thread, when the loop watches the descriptor."""
90
+ try:
91
+ data = self.terminal.console.read()
92
+ except OSError:
93
+ return
94
+ self._feed(data)
95
+
96
+ def _feed(self, data: bytes) -> None:
97
+ """Publish whatever one chunk decodes, and arm the escape timer."""
98
+ if self._escape_handle is not None:
99
+ self._escape_handle.cancel()
100
+ self._escape_handle = None
101
+ for event in self.decoder.feed(data):
102
+ self.publish(event)
103
+ # A trailing ESC may still be growing into a control sequence, so give
104
+ # it a moment (30 ms, the usual terminal timeout) before reporting the
105
+ # Escape key itself.
106
+ if self.decoder.buffer.startswith(b"\x1b") and self.loop is not None:
107
+ self._escape_handle = self.loop.call_later(0.03, self._flush_escape)
108
+
109
+ def _pump(self) -> None:
110
+ """Read the console from a thread and hand each chunk to the loop."""
111
+ while not self._stopping.is_set():
112
+ try:
113
+ if not self.terminal.console.wait_readable(POLL_SECONDS):
114
+ continue
115
+ data = self.terminal.console.read()
116
+ except OSError:
117
+ return
118
+ if not data:
119
+ continue
120
+ loop = self.loop
121
+ if loop is not None:
122
+ loop.call_soon_threadsafe(self._feed, data)
123
+
124
+ def _poll_resize(self) -> None:
125
+ """Publish a resize when the console's size changed.
126
+
127
+ Windows raises no SIGWINCH, so the loop asks instead of being told; the
128
+ probe is one console call, cheap enough for the frame interval.
129
+ """
130
+ self._resize_handle = None
131
+ size = self.terminal.size
132
+ if self._size is not None and size != self._size:
133
+ self._size = size
134
+ self.publish(InputEvent(EventType.RESIZE))
135
+ if self.loop is not None:
136
+ self._resize_handle = self.loop.call_later(RESIZE_POLL_SECONDS, self._poll_resize)
137
+
138
+ def _resize(self) -> None:
139
+ """Publish the resize the terminal raised through SIGWINCH."""
140
+ self.publish(InputEvent(EventType.RESIZE))
141
+
142
+ def _flush_escape(self) -> None:
143
+ """Resolve a lone escape that never grew into a control sequence."""
144
+ self._escape_handle = None
145
+ # One event per call, so a run of escapes is reported press by press.
146
+ while (event := self.decoder.flush_escape()) is not None:
147
+ self.publish(event)
@@ -0,0 +1,24 @@
1
+ """Layout containers: boxes, spacing, anchored overlays, and scrolling."""
2
+
3
+ from .box import HBox, Slot, VBox
4
+ from .overlay import Anchor, Overlay, OverlaySlot, centered
5
+ from .scroll import LineSource, ScrollView, StaticLines
6
+ from .solver import Track, resolve_tracks
7
+ from .spacing import Border, Padding
8
+
9
+ __all__ = [
10
+ "Anchor",
11
+ "Border",
12
+ "HBox",
13
+ "LineSource",
14
+ "Overlay",
15
+ "OverlaySlot",
16
+ "Padding",
17
+ "ScrollView",
18
+ "Slot",
19
+ "StaticLines",
20
+ "Track",
21
+ "VBox",
22
+ "centered",
23
+ "resolve_tracks",
24
+ ]
@@ -0,0 +1,153 @@
1
+ """Vertical and horizontal box containers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Sequence
6
+ from dataclasses import dataclass
7
+
8
+ from ..core.geometry import Constraints, Point, Rect, Size
9
+ from ..core.widget import Widget
10
+ from ..render import Canvas
11
+ from .solver import Track, resolve_tracks
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class Slot:
16
+ """One child of a box, with its own sizing policy on the main axis.
17
+
18
+ Attributes:
19
+ widget: The child laid out in this slot.
20
+ size: Fixed cell count on the main axis, or a callable receiving the
21
+ box's extent on the cross axis (the width of a ``VBox``, the height
22
+ of an ``HBox``) and returning main-axis cells. ``None`` contributes
23
+ nothing to the fixed budget, leaving the slot to ``flex``.
24
+ flex: Share of the leftover space; zero means the slot never grows.
25
+ min_size: Floor applied to the main-axis extent.
26
+ """
27
+
28
+ widget: Widget
29
+ size: int | Callable[[int], int] | None = None
30
+ flex: int = 0
31
+ min_size: int = 0
32
+
33
+ def track(self) -> Track:
34
+ """Describe this slot to the track solver on the main axis."""
35
+ return Track(self.size, self.flex, self.min_size)
36
+
37
+
38
+ class _Box(Widget):
39
+ """Shared behavior for the two box directions."""
40
+
41
+ def __init__(self, slots: Sequence[Slot]) -> None:
42
+ """Store the slots in paint order."""
43
+ super().__init__()
44
+ self.slots = tuple(slots)
45
+
46
+ @property
47
+ def children(self) -> tuple[Widget, ...]:
48
+ """Expose one child per slot."""
49
+ return tuple(slot.widget for slot in self.slots)
50
+
51
+ @property
52
+ def _vertical(self) -> bool:
53
+ """Return whether children stack vertically; subclasses decide."""
54
+ raise NotImplementedError
55
+
56
+ def render(self, canvas: Canvas) -> None:
57
+ """Paint every child in slot order; the box itself draws nothing."""
58
+ for slot in self.slots:
59
+ slot.widget.render(canvas)
60
+
61
+ def cursor(self) -> Point | None:
62
+ """Return the first cursor any child asks for, in slot order."""
63
+ for slot in self.slots:
64
+ cursor = slot.widget.cursor()
65
+ if cursor is not None:
66
+ return cursor
67
+ return None
68
+
69
+ def _cross_constraints(self, constraints: Constraints) -> Constraints:
70
+ """Leave the main axis open and bound the cross axis by the parent."""
71
+ if self._vertical:
72
+ return Constraints(0, constraints.max_width, 0, None)
73
+ return Constraints(0, None, 0, constraints.max_height)
74
+
75
+ def measure(self, constraints: Constraints) -> Size:
76
+ """Sum the main-axis sizes and take the largest cross-axis child."""
77
+ inner = self._cross_constraints(constraints)
78
+ total = 0
79
+ widest = 0
80
+ tallest = 0
81
+ for slot in self.slots:
82
+ child = slot.widget.measure(inner)
83
+ natural = child.height if self._vertical else child.width
84
+ main = natural if slot.size is None or callable(slot.size) else slot.size
85
+ total += max(slot.min_size, main)
86
+ widest = max(widest, child.width)
87
+ tallest = max(tallest, child.height)
88
+ size = Size(widest, total) if self._vertical else Size(total, tallest)
89
+ return constraints.constrain(size)
90
+
91
+
92
+ class VBox(_Box):
93
+ """Stack children top to bottom, sharing leftover height by flex.
94
+
95
+ Shape::
96
+
97
+ +-------------------+ <- the box's own rectangle
98
+ | header (size=1) | <- fixed rows keep their height
99
+ +-------------------+
100
+ | body (flex=1) | <- flexible rows share what is left
101
+ | |
102
+ +-------------------+
103
+ | status (size=1) |
104
+ +-------------------+
105
+
106
+ A fixed size may be a callable, which receives the box's *width* — a
107
+ wrapping child's height depends on the space across, not along. When the
108
+ fixed rows alone overflow, every row is scaled down proportionally so the
109
+ box never paints outside its rectangle.
110
+ """
111
+
112
+ @property
113
+ def _vertical(self) -> bool:
114
+ """Stack children top to bottom."""
115
+ return True
116
+
117
+ def layout(self, rect: Rect) -> None:
118
+ """Split the height across the slots and stack them from the top."""
119
+ super().layout(rect)
120
+ y = rect.y
121
+ measured = resolve_tracks(rect.height, [s.track() for s in self.slots], cross=rect.width)
122
+ for slot, extent in zip(self.slots, measured, strict=True):
123
+ slot.widget.layout(Rect(rect.x, y, rect.width, extent))
124
+ y += extent
125
+
126
+
127
+ class HBox(_Box):
128
+ """Place children left to right, sharing leftover width by flex.
129
+
130
+ Shape::
131
+
132
+ +--------+---------------------------+
133
+ | left | right | <- size=8, then flex=1
134
+ +--------+---------------------------+
135
+ ^ the callable form receives the box's *height*
136
+
137
+ The horizontal mirror of `VBox`: same track solver, same proportional
138
+ shrink when the fixed columns do not fit.
139
+ """
140
+
141
+ @property
142
+ def _vertical(self) -> bool:
143
+ """Place children left to right."""
144
+ return False
145
+
146
+ def layout(self, rect: Rect) -> None:
147
+ """Split the width across the slots and place them from the left."""
148
+ super().layout(rect)
149
+ x = rect.x
150
+ measured = resolve_tracks(rect.width, [s.track() for s in self.slots], cross=rect.height)
151
+ for slot, extent in zip(self.slots, measured, strict=True):
152
+ slot.widget.layout(Rect(x, rect.y, extent, rect.height))
153
+ x += extent
@@ -0,0 +1,164 @@
1
+ """Anchored placement of children inside their parent's rectangle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+
8
+ from ..core.geometry import Constraints, Rect, Size
9
+ from ..core.widget import Widget
10
+ from ..render import Canvas
11
+
12
+ HORIZONTAL = ("start", "center", "end", "stretch")
13
+ VERTICAL = ("start", "center", "end", "stretch")
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class Anchor:
18
+ """Where a child sits inside its parent's bounds.
19
+
20
+ Attributes:
21
+ horizontal: One of ``HORIZONTAL``. ``"stretch"`` fills the parent width,
22
+ ``"center"`` and ``"end"`` offset the child inside the parent, and an
23
+ explicit ``width`` overrides both.
24
+ vertical: One of ``VERTICAL``, with the same meanings on the other axis.
25
+ offset_x: Cells added to the resolved left edge, after alignment.
26
+ offset_y: Cells added to the resolved top edge, after alignment.
27
+ width: Fixed width in cells; ``None`` uses the child's measured width.
28
+ Either way the value is capped to the parent's width.
29
+ height: Fixed height in cells; ``None`` uses the measured height.
30
+ """
31
+
32
+ horizontal: str = "start"
33
+ vertical: str = "start"
34
+ offset_x: int = 0
35
+ offset_y: int = 0
36
+ width: int | None = None
37
+ height: int | None = None
38
+
39
+ def __post_init__(self) -> None:
40
+ """Reject anchor names the resolver does not know."""
41
+ if self.horizontal not in HORIZONTAL:
42
+ raise ValueError(f"Unknown horizontal anchor: {self.horizontal!r}")
43
+ if self.vertical not in VERTICAL:
44
+ raise ValueError(f"Unknown vertical anchor: {self.vertical!r}")
45
+
46
+ def resolve(self, bounds: Rect, measured: Size) -> Rect:
47
+ """Return the child rectangle this anchor picks inside bounds.
48
+
49
+ Args:
50
+ bounds: Parent rectangle the child is placed inside.
51
+ measured: Size the child asked for; unused on an axis the anchor
52
+ stretches, and used otherwise when ``width``/``height`` is unset.
53
+ The result is capped to ``bounds`` either way.
54
+ """
55
+ width = self._extent(self.horizontal, bounds.width, measured.width, self.width)
56
+ height = self._extent(self.vertical, bounds.height, measured.height, self.height)
57
+ x = bounds.x + self._offset(self.horizontal, bounds.width, width) + self.offset_x
58
+ y = bounds.y + self._offset(self.vertical, bounds.height, height) + self.offset_y
59
+ return Rect(x, y, max(0, width), max(0, height))
60
+
61
+ @staticmethod
62
+ def _extent(axis: str, available: int, measured: int, pinned: int | None) -> int:
63
+ """Return one axis extent, letting an explicit pin beat the anchor.
64
+
65
+ Args:
66
+ axis: Anchor name for this axis.
67
+ available: Cells the parent offers on the axis.
68
+ measured: Size the child asked for.
69
+ pinned: Explicit width or height; ``None`` defers to the anchor, and
70
+ a pin is still capped to ``available``.
71
+ """
72
+ if pinned is not None:
73
+ return min(pinned, available)
74
+ if axis == "stretch":
75
+ return available
76
+ return min(measured, available)
77
+
78
+ @staticmethod
79
+ def _offset(axis: str, available: int, extent: int) -> int:
80
+ """Return the leading offset for one axis and placement mode."""
81
+ if axis == "center":
82
+ return max(0, (available - extent) // 2)
83
+ if axis == "end":
84
+ return max(0, available - extent)
85
+ return 0
86
+
87
+
88
+ @dataclass(frozen=True, slots=True)
89
+ class OverlaySlot:
90
+ """One anchored child.
91
+
92
+ Attributes:
93
+ widget: The child to place.
94
+ anchor: Where inside the overlay's rectangle the child goes.
95
+ """
96
+
97
+ widget: Widget
98
+ anchor: Anchor = Anchor()
99
+
100
+
101
+ class Overlay(Widget):
102
+ """Stack children on top of each other at anchored positions.
103
+
104
+ Shape::
105
+
106
+ parent rectangle
107
+ +---------------------------+
108
+ | +--------+ |
109
+ | | child | | <- measured, then placed by its anchor
110
+ | +--------+ |
111
+ +---------------------------+
112
+
113
+ Later slots paint over earlier ones. Each child is measured against the full
114
+ rectangle first, so an anchor can align by the child's own size; the result
115
+ is always capped to the parent, and ``"stretch"`` fills it.
116
+ """
117
+
118
+ def __init__(self, slots: Sequence[OverlaySlot]) -> None:
119
+ """Store the slots in paint order, so later slots sit on top.
120
+
121
+ Args:
122
+ slots: Children to place; each is measured against the full overlay
123
+ rectangle before its anchor resolves the final position.
124
+ """
125
+ super().__init__()
126
+ self.slots = tuple(slots)
127
+
128
+ @property
129
+ def children(self) -> tuple[Widget, ...]:
130
+ """Expose the anchored widgets in paint order."""
131
+ return tuple(slot.widget for slot in self.slots)
132
+
133
+ def measure(self, constraints: Constraints) -> Size:
134
+ """Report the smallest rectangle that fits every child."""
135
+ widest = 0
136
+ tallest = 0
137
+ for slot in self.slots:
138
+ child = slot.widget.measure(constraints)
139
+ widest = max(widest, child.width)
140
+ tallest = max(tallest, child.height)
141
+ return constraints.constrain(Size(widest, tallest))
142
+
143
+ def layout(self, rect: Rect) -> None:
144
+ """Measure each child, then place it at its anchored rectangle."""
145
+ super().layout(rect)
146
+ for slot in self.slots:
147
+ measured = slot.widget.measure(Constraints.loose(rect.size))
148
+ slot.widget.layout(slot.anchor.resolve(rect, measured))
149
+
150
+ def render(self, canvas: Canvas) -> None:
151
+ """Paint the children in slot order, so later slots cover earlier ones."""
152
+ for slot in self.slots:
153
+ slot.widget.render(canvas)
154
+
155
+
156
+ def centered(widget: Widget, *, horizontal: str = "center", vertical: str = "center") -> Overlay:
157
+ """Wrap one widget in a full-rect overlay anchored at the given edges.
158
+
159
+ Args:
160
+ widget: The single child to place.
161
+ horizontal: Horizontal anchor name, from ``"start"`` to ``"stretch"``.
162
+ vertical: Vertical anchor name, from ``"start"`` to ``"stretch"``.
163
+ """
164
+ return Overlay([OverlaySlot(widget, Anchor(horizontal=horizontal, vertical=vertical))])