pytermgui.window_manager.manager

The WindowManager class, whos job it is to move, control and update windows, while letting Compositor draw them.

  1"""The WindowManager class, whos job it is to move, control and update windows,
  2while letting `Compositor` draw them."""
  3
  4from __future__ import annotations
  5
  6from enum import Enum, auto as _auto
  7from typing import Type, Any, Iterator
  8
  9from ..input import getch
 10from ..enums import Overflow
 11from ..regex import real_length
 12from ..terminal import terminal
 13from ..colors import str_to_color
 14from ..widgets import Widget, Container
 15from ..widgets.base import BoundCallback
 16from ..ansi_interface import MouseAction, MouseEvent
 17from ..context_managers import alt_buffer, mouse_handler, MouseTranslator
 18from ..animations import animator, AttrAnimation, FloatAnimation, Animation
 19
 20from .window import Window
 21from .layouts import Layout
 22from .compositor import Compositor
 23
 24
 25def _center_during_animation(animation: AttrAnimation) -> None:
 26    """Centers a window, when applicable, while animating."""
 27
 28    window = animation.target
 29    assert isinstance(window, Window), window
 30
 31    if window.centered_axis is not None:
 32        window.center()
 33
 34
 35class Edge(Enum):
 36    """Enum for window edges."""
 37
 38    LEFT = _auto()
 39    TOP = _auto()
 40    RIGHT = _auto()
 41    BOTTOM = _auto()
 42
 43
 44class WindowManager(Widget):  # pylint: disable=too-many-instance-attributes
 45    """The manager of windows.
 46
 47    This class can be used, or even subclassed in order to create full-screen applications,
 48    using the `pytermgui.window_manager.window.Window` class and the general Widget API.
 49    """
 50
 51    is_bindable = True
 52
 53    focusing_actions = (MouseAction.LEFT_CLICK, MouseAction.RIGHT_CLICK)
 54    """These mouse actions will focus the window they are acted upon."""
 55
 56    autorun = True
 57
 58    def __init__(
 59        self,
 60        *,
 61        layout_type: Type[Layout] = Layout,
 62        framerate: int = 60,
 63        autorun: bool | None = None,
 64    ) -> None:
 65        """Initialize the manager."""
 66
 67        super().__init__()
 68
 69        self._is_running = False
 70        self._windows: list[Window] = []
 71        self._drag_offsets: tuple[int, int] = (0, 0)
 72        self._drag_target: tuple[Window, Edge] | None = None
 73        self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {}
 74
 75        self.focused: Window | None = None
 76
 77        if autorun is not None:
 78            self.autorun = autorun
 79
 80        self.layout = layout_type()
 81        self.compositor = Compositor(self._windows, framerate=framerate)
 82        self.mouse_translator: MouseTranslator | None = None
 83
 84        # This isn't quite implemented at the moment.
 85        self.restrict_within_bounds = True
 86
 87        terminal.subscribe(terminal.RESIZE, self.on_resize)
 88
 89    def __iadd__(self, other: object) -> WindowManager:
 90        """Adds a window to the manager."""
 91
 92        if not isinstance(other, Window):
 93            raise ValueError("You may only add windows to a WindowManager.")
 94
 95        return self.add(other)
 96
 97    def __isub__(self, other: object) -> WindowManager:
 98        """Removes a window from the manager."""
 99
100        if not isinstance(other, Window):
101            raise ValueError("You may only add windows to a WindowManager.")
102
103        return self.remove(other)
104
105    def __enter__(self) -> WindowManager:
106        """Starts context manager."""
107
108        return self
109
110    def __exit__(self, _: Any, exception: Exception, __: Any) -> bool:
111        """Ends context manager."""
112
113        # Run the manager if it hasnt been run before.
114        if self.autorun and exception is None and self.mouse_translator is None:
115            self.run()
116
117        if exception is not None:
118            self.stop()
119            raise exception
120
121        return True
122
123    def __iter__(self) -> Iterator[Window]:
124        """Iterates this manager's windows."""
125
126        return iter(self._windows)
127
128    def _run_input_loop(self) -> None:
129        """The main input loop of the WindowManager."""
130
131        while self._is_running:
132            key = getch(interrupts=False)
133
134            if key == chr(3):
135                self.stop()
136                break
137
138            if self.handle_key(key):
139                continue
140
141            self.process_mouse(key)
142
143    def get_lines(self) -> list[str]:
144        """Gets the empty list."""
145
146        # TODO: Allow using WindowManager as a widget.
147
148        return []
149
150    def clear_cache(self, window: Window) -> None:
151        """Clears the compositor's cache related to the given window."""
152
153        self.compositor.clear_cache(window)
154
155    def on_resize(self, size: tuple[int, int]) -> None:
156        """Correctly updates window positions & prints when terminal gets resized.
157
158        Args:
159            size: The new terminal size.
160        """
161
162        width, height = size
163
164        for window in self._windows:
165            newx = max(0, min(window.pos[0], width - window.width))
166            newy = max(0, min(window.pos[1], height - window.height + 1))
167
168            window.pos = (newx, newy)
169
170        self.layout.apply()
171        self.compositor.redraw()
172
173    def run(self, mouse_events: list[str] | None = None) -> None:
174        """Starts the WindowManager.
175
176        Args:
177            mouse_events: A list of mouse event types to listen to. See
178                `pytermgui.ansi_interface.report_mouse` for more information.
179                Defaults to `["press_hold", "hover"]`.
180
181        Returns:
182            The WindowManager's compositor instance.
183        """
184
185        self._is_running = True
186
187        if mouse_events is None:
188            mouse_events = ["press_hold", "hover"]
189
190        with alt_buffer(cursor=False, echo=False):
191            with mouse_handler(mouse_events, "decimal_xterm") as translate:
192                self.mouse_translator = translate
193                self.compositor.run()
194
195                self._run_input_loop()
196
197    def stop(self) -> None:
198        """Stops the WindowManager and its compositor."""
199
200        self.compositor.stop()
201        self._is_running = False
202
203    def add(
204        self, window: Window, assign: str | bool = True, animate: bool = True
205    ) -> WindowManager:
206        """Adds a window to the manager.
207
208        Args:
209            window: The window to add.
210            assign: The name of the slot the new window should be assigned to, or a
211                boolean. If it is given a str, it is treated as the name of a slot. When
212                given True, the next non-filled slot will be assigned, and when given
213                False no assignment will be done.
214            animate: If set, an animation will be played on the window once it's added.
215        """
216
217        self._windows.insert(0, window)
218        window.manager = self
219
220        if assign:
221            if isinstance(assign, str):
222                getattr(self.layout, assign).content = window
223
224            elif len(self._windows) <= len(self.layout.slots):
225                self.layout.assign(window, index=len(self._windows) - 1)
226
227            self.layout.apply()
228
229        # New windows take focus-precedence over already
230        # existing ones, even if they are modal.
231        self.focus(window)
232
233        if not animate:
234            return self
235
236        if window.height > 1:
237            animator.animate_attr(
238                target=window,
239                attr="height",
240                start=0,
241                end=window.height,
242                duration=300,
243                on_step=_center_during_animation,
244            )
245
246        return self
247
248    def remove(
249        self,
250        window: Window,
251        autostop: bool = True,
252        animate: bool = True,
253    ) -> WindowManager:
254        """Removes a window from the manager.
255
256        Args:
257            window: The window to remove.
258            autostop: If set, the manager will be stopped if the length of its windows
259                hits 0.
260        """
261
262        def _on_finish(_: AttrAnimation | None) -> bool:
263            self._windows.remove(window)
264
265            if autostop and len(self._windows) == 0:
266                self.stop()
267            else:
268                self.focus(self._windows[0])
269
270            return True
271
272        if not animate:
273            _on_finish(None)
274            return self
275
276        animator.animate_attr(
277            target=window,
278            attr="height",
279            end=0,
280            duration=300,
281            on_step=_center_during_animation,
282            on_finish=_on_finish,
283        )
284
285        return self
286
287    def focus(self, window: Window | None) -> None:
288        """Focuses a window by moving it to the first index in _windows."""
289
290        if self.focused is not None:
291            self.focused.blur()
292
293        self.focused = window
294
295        if window is not None:
296            self._windows.remove(window)
297            self._windows.insert(0, window)
298
299            window.focus()
300
301    def focus_next(self) -> Window | None:
302        """Focuses the next window in focus order, looping to first at the end."""
303
304        if self.focused is None:
305            self.focus(self._windows[0])
306            return self.focused
307
308        index = self._windows.index(self.focused)
309        if index == len(self._windows) - 1:
310            index = 0
311
312        window = self._windows[index]
313        traversed = 0
314        while window.is_persistent or window is self.focused:
315            if index >= len(self._windows):
316                index = 0
317
318            window = self._windows[index]
319
320            index += 1
321            traversed += 1
322            if traversed >= len(self._windows):
323                return self.focused
324
325        self.focus(self._windows[index])
326
327        return self.focused
328
329    def handle_key(self, key: str) -> bool:
330        """Processes a keypress.
331
332        Args:
333            key: The key to handle.
334
335        Returns:
336            True if the given key could be processed, False otherwise.
337        """
338
339        # Apply WindowManager bindings
340        if self.execute_binding(key):
341            return True
342
343        # Apply focused window binding, or send to InputField
344        if self.focused is not None:
345            if self.focused.execute_binding(key):
346                return True
347
348            if self.focused.handle_key(key):
349                return True
350
351        return False
352
353    # I prefer having the _click, _drag and _release helpers within this function, for
354    # easier readability.
355    def process_mouse(self, key: str) -> None:  # pylint: disable=too-many-statements
356        """Processes (potential) mouse input.
357
358        Args:
359            key: Input to handle.
360        """
361
362        window: Window
363
364        def _clamp_pos(pos: tuple[int, int], index: int) -> int:
365            """Clamp a value using index to address x/y & width/height"""
366
367            offset = self._drag_offsets[index]
368
369            # TODO: This -2 is a very magical number. Not good.
370            maximum = terminal.size[index] - ((window.width, window.height)[index] - 2)
371
372            start_margin_index = abs(index - 1)
373
374            if self.restrict_within_bounds:
375                return max(
376                    index + terminal.margins[start_margin_index],
377                    min(
378                        pos[index] - offset,
379                        maximum
380                        - terminal.margins[start_margin_index + 2]
381                        - terminal.origin[index],
382                    ),
383                )
384
385            return pos[index] - offset
386
387        def _click(pos: tuple[int, int], window: Window) -> bool:
388            """Process clicking a window."""
389
390            left, top, right, bottom = window.rect
391            borders = window.chars.get("border", [" "] * 4)
392
393            if real_length(borders[1]) > 0 and pos[1] == top and left <= pos[0] < right:
394                self._drag_target = (window, Edge.TOP)
395
396            elif (
397                real_length(borders[3]) > 0
398                and pos[1] == bottom - 1
399                and left <= pos[0] < right
400            ):
401                self._drag_target = (window, Edge.BOTTOM)
402
403            elif (
404                real_length(borders[0]) > 0
405                and pos[0] == left
406                and top <= pos[1] < bottom
407            ):
408                self._drag_target = (window, Edge.LEFT)
409
410            elif (
411                real_length(borders[2]) > 0
412                and pos[0] == right - 1
413                and top <= pos[1] < bottom
414            ):
415                self._drag_target = (window, Edge.RIGHT)
416
417            else:
418                return False
419
420            self._drag_offsets = (
421                pos[0] - window.pos[0],
422                pos[1] - window.pos[1],
423            )
424
425            return True
426
427        def _drag(pos: tuple[int, int], window: Window) -> bool:
428            """Process dragging a window"""
429
430            if self._drag_target is None:
431                return False
432
433            target_window, edge = self._drag_target
434            handled = False
435
436            if window is not target_window:
437                return False
438
439            left, top, right, bottom = window.rect
440
441            if not window.is_static and edge is Edge.TOP:
442                window.pos = (
443                    _clamp_pos(pos, 0),
444                    _clamp_pos(pos, 1),
445                )
446
447                handled = True
448
449            # TODO: Why are all these arbitrary offsets needed?
450            elif not window.is_noresize:
451                if edge is Edge.RIGHT:
452                    window.rect = (left, top, pos[0] + 1, bottom)
453                    handled = True
454
455                elif edge is Edge.LEFT:
456                    window.rect = (pos[0], top, right, bottom)
457                    handled = True
458
459                elif edge is Edge.BOTTOM:
460                    window.rect = (left, top, right, pos[1] + 1)
461                    handled = True
462
463            if handled:
464                window.is_dirty = True
465                self.compositor.set_redraw()
466
467            return handled
468
469        def _release(_: tuple[int, int], __: Window) -> bool:
470            """Process release of key"""
471
472            self._drag_target = None
473
474            # This return False so Window can handle the mouse action as well,
475            # as not much is done in this callback.
476            return False
477
478        handlers = {
479            MouseAction.LEFT_CLICK: _click,
480            MouseAction.LEFT_DRAG: _drag,
481            MouseAction.RELEASE: _release,
482        }
483
484        translate = self.mouse_translator
485        event_list = None if translate is None else translate(key)
486
487        if event_list is None:
488            return
489
490        for event in event_list:
491            # Ignore null-events
492            if event is None:
493                continue
494
495            for window in self._windows:
496                contains = window.contains(event.position)
497
498                if event.action in self.focusing_actions:
499                    self.focus(window)
500
501                if event.action in handlers and handlers[event.action](
502                    event.position, window
503                ):
504                    break
505
506                if window.handle_mouse(event) or (contains or window.is_modal):
507                    break
508
509            # Unset drag_target if no windows received the input
510            else:
511                self._drag_target = None
512
513    def screenshot(self, title: str, filename: str = "screenshot.svg") -> None:
514        """Takes a screenshot of the current state.
515
516        See `pytermgui.exporters.to_svg` for more information.
517
518        Args:
519            filename: The name of the file.
520        """
521
522        self.compositor.capture(title=title, filename=filename)
523
524    def show_positions(self) -> None:
525        """Shows the positions of each Window's widgets."""
526
527        def _show_positions(widget, color_base: int = 60) -> None:
528            """Show positions of widget."""
529
530            if isinstance(widget, Container):
531                for i, subwidget in enumerate(widget):
532                    _show_positions(subwidget, color_base + i)
533
534                return
535
536            if not widget.is_selectable:
537                return
538
539            debug = widget.debug()
540            color = str_to_color(f"@{color_base}")
541            buff = color(" ", reset=False)
542
543            for i in range(min(widget.width, real_length(debug)) - 1):
544                buff += debug[i]
545
546            self.terminal.write(buff, pos=widget.pos)
547
548        for widget in self._windows:
549            _show_positions(widget)
550        self.terminal.flush()
551
552        getch()
553
554    def alert(self, *items, center: bool = True, **attributes) -> Window:
555        """Creates a modal popup of the given elements and attributes.
556
557        Args:
558            *items: All widget-convertable objects passed as children of the new window.
559            center: If set, `pytermgui.window_manager.window.center` is called on the window.
560            **attributes: kwargs passed as the new window's attributes.
561        """
562
563        window = Window(*items, is_modal=True, **attributes)
564
565        if center:
566            window.center()
567
568        self.add(window, assign=False)
569
570        return window
571
572    def toast(
573        self,
574        *items,
575        offset: int = 0,
576        duration: int = 300,
577        delay: int = 1000,
578        **attributes,
579    ) -> Window:
580        """Creates a Material UI-inspired toast window of the given elements and attributes.
581
582        Args:
583            *items: All widget-convertable objects passed as children of the new window.
584            delay: The amount of time before the window will start animating out.
585            **attributes: kwargs passed as the new window's attributes.
586        """
587
588        # pylint: disable=no-value-for-parameter
589
590        toast = Window(*items, is_noblur=True, **attributes)
591
592        target_height = toast.height
593        toast.overflow = Overflow.HIDE
594
595        def _finish(_: Animation) -> None:
596            self.remove(toast, animate=False)
597
598        def _progressively_show(anim: Animation, invert: bool = False) -> bool:
599            height = int(anim.state * target_height)
600
601            toast.center()
602
603            if invert:
604                toast.height = target_height - 1 - height
605                toast.pos = (
606                    toast.pos[0],
607                    self.terminal.height - toast.height + 1 - offset,
608                )
609                return False
610
611            toast.height = height
612            toast.pos = (toast.pos[0], self.terminal.height - toast.height + 1 - offset)
613
614            return False
615
616        def _animate_toast_out(_: Animation) -> None:
617            animator.schedule(
618                FloatAnimation(
619                    delay,
620                    on_finish=lambda *_: animator.schedule(
621                        FloatAnimation(
622                            duration,
623                            on_step=lambda anim: _progressively_show(anim, invert=True),
624                            on_finish=_finish,
625                        )
626                    ),
627                )
628            )
629
630        leadup = FloatAnimation(
631            duration, on_step=_progressively_show, on_finish=_animate_toast_out
632        )
633
634        # pylint: enable=no-value-for-parameter
635
636        self.add(toast.center(), animate=False, assign=False)
637        self.focus(toast)
638        animator.schedule(leadup)
639
640        return toast
class Edge(enum.Enum):
36class Edge(Enum):
37    """Enum for window edges."""
38
39    LEFT = _auto()
40    TOP = _auto()
41    RIGHT = _auto()
42    BOTTOM = _auto()

Enum for window edges.

LEFT = <Edge.LEFT: 1>
TOP = <Edge.TOP: 2>
RIGHT = <Edge.RIGHT: 3>
BOTTOM = <Edge.BOTTOM: 4>
Inherited Members
enum.Enum
name
value
class WindowManager(pytermgui.widgets.base.Widget):
 45class WindowManager(Widget):  # pylint: disable=too-many-instance-attributes
 46    """The manager of windows.
 47
 48    This class can be used, or even subclassed in order to create full-screen applications,
 49    using the `pytermgui.window_manager.window.Window` class and the general Widget API.
 50    """
 51
 52    is_bindable = True
 53
 54    focusing_actions = (MouseAction.LEFT_CLICK, MouseAction.RIGHT_CLICK)
 55    """These mouse actions will focus the window they are acted upon."""
 56
 57    autorun = True
 58
 59    def __init__(
 60        self,
 61        *,
 62        layout_type: Type[Layout] = Layout,
 63        framerate: int = 60,
 64        autorun: bool | None = None,
 65    ) -> None:
 66        """Initialize the manager."""
 67
 68        super().__init__()
 69
 70        self._is_running = False
 71        self._windows: list[Window] = []
 72        self._drag_offsets: tuple[int, int] = (0, 0)
 73        self._drag_target: tuple[Window, Edge] | None = None
 74        self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {}
 75
 76        self.focused: Window | None = None
 77
 78        if autorun is not None:
 79            self.autorun = autorun
 80
 81        self.layout = layout_type()
 82        self.compositor = Compositor(self._windows, framerate=framerate)
 83        self.mouse_translator: MouseTranslator | None = None
 84
 85        # This isn't quite implemented at the moment.
 86        self.restrict_within_bounds = True
 87
 88        terminal.subscribe(terminal.RESIZE, self.on_resize)
 89
 90    def __iadd__(self, other: object) -> WindowManager:
 91        """Adds a window to the manager."""
 92
 93        if not isinstance(other, Window):
 94            raise ValueError("You may only add windows to a WindowManager.")
 95
 96        return self.add(other)
 97
 98    def __isub__(self, other: object) -> WindowManager:
 99        """Removes a window from the manager."""
100
101        if not isinstance(other, Window):
102            raise ValueError("You may only add windows to a WindowManager.")
103
104        return self.remove(other)
105
106    def __enter__(self) -> WindowManager:
107        """Starts context manager."""
108
109        return self
110
111    def __exit__(self, _: Any, exception: Exception, __: Any) -> bool:
112        """Ends context manager."""
113
114        # Run the manager if it hasnt been run before.
115        if self.autorun and exception is None and self.mouse_translator is None:
116            self.run()
117
118        if exception is not None:
119            self.stop()
120            raise exception
121
122        return True
123
124    def __iter__(self) -> Iterator[Window]:
125        """Iterates this manager's windows."""
126
127        return iter(self._windows)
128
129    def _run_input_loop(self) -> None:
130        """The main input loop of the WindowManager."""
131
132        while self._is_running:
133            key = getch(interrupts=False)
134
135            if key == chr(3):
136                self.stop()
137                break
138
139            if self.handle_key(key):
140                continue
141
142            self.process_mouse(key)
143
144    def get_lines(self) -> list[str]:
145        """Gets the empty list."""
146
147        # TODO: Allow using WindowManager as a widget.
148
149        return []
150
151    def clear_cache(self, window: Window) -> None:
152        """Clears the compositor's cache related to the given window."""
153
154        self.compositor.clear_cache(window)
155
156    def on_resize(self, size: tuple[int, int]) -> None:
157        """Correctly updates window positions & prints when terminal gets resized.
158
159        Args:
160            size: The new terminal size.
161        """
162
163        width, height = size
164
165        for window in self._windows:
166            newx = max(0, min(window.pos[0], width - window.width))
167            newy = max(0, min(window.pos[1], height - window.height + 1))
168
169            window.pos = (newx, newy)
170
171        self.layout.apply()
172        self.compositor.redraw()
173
174    def run(self, mouse_events: list[str] | None = None) -> None:
175        """Starts the WindowManager.
176
177        Args:
178            mouse_events: A list of mouse event types to listen to. See
179                `pytermgui.ansi_interface.report_mouse` for more information.
180                Defaults to `["press_hold", "hover"]`.
181
182        Returns:
183            The WindowManager's compositor instance.
184        """
185
186        self._is_running = True
187
188        if mouse_events is None:
189            mouse_events = ["press_hold", "hover"]
190
191        with alt_buffer(cursor=False, echo=False):
192            with mouse_handler(mouse_events, "decimal_xterm") as translate:
193                self.mouse_translator = translate
194                self.compositor.run()
195
196                self._run_input_loop()
197
198    def stop(self) -> None:
199        """Stops the WindowManager and its compositor."""
200
201        self.compositor.stop()
202        self._is_running = False
203
204    def add(
205        self, window: Window, assign: str | bool = True, animate: bool = True
206    ) -> WindowManager:
207        """Adds a window to the manager.
208
209        Args:
210            window: The window to add.
211            assign: The name of the slot the new window should be assigned to, or a
212                boolean. If it is given a str, it is treated as the name of a slot. When
213                given True, the next non-filled slot will be assigned, and when given
214                False no assignment will be done.
215            animate: If set, an animation will be played on the window once it's added.
216        """
217
218        self._windows.insert(0, window)
219        window.manager = self
220
221        if assign:
222            if isinstance(assign, str):
223                getattr(self.layout, assign).content = window
224
225            elif len(self._windows) <= len(self.layout.slots):
226                self.layout.assign(window, index=len(self._windows) - 1)
227
228            self.layout.apply()
229
230        # New windows take focus-precedence over already
231        # existing ones, even if they are modal.
232        self.focus(window)
233
234        if not animate:
235            return self
236
237        if window.height > 1:
238            animator.animate_attr(
239                target=window,
240                attr="height",
241                start=0,
242                end=window.height,
243                duration=300,
244                on_step=_center_during_animation,
245            )
246
247        return self
248
249    def remove(
250        self,
251        window: Window,
252        autostop: bool = True,
253        animate: bool = True,
254    ) -> WindowManager:
255        """Removes a window from the manager.
256
257        Args:
258            window: The window to remove.
259            autostop: If set, the manager will be stopped if the length of its windows
260                hits 0.
261        """
262
263        def _on_finish(_: AttrAnimation | None) -> bool:
264            self._windows.remove(window)
265
266            if autostop and len(self._windows) == 0:
267                self.stop()
268            else:
269                self.focus(self._windows[0])
270
271            return True
272
273        if not animate:
274            _on_finish(None)
275            return self
276
277        animator.animate_attr(
278            target=window,
279            attr="height",
280            end=0,
281            duration=300,
282            on_step=_center_during_animation,
283            on_finish=_on_finish,
284        )
285
286        return self
287
288    def focus(self, window: Window | None) -> None:
289        """Focuses a window by moving it to the first index in _windows."""
290
291        if self.focused is not None:
292            self.focused.blur()
293
294        self.focused = window
295
296        if window is not None:
297            self._windows.remove(window)
298            self._windows.insert(0, window)
299
300            window.focus()
301
302    def focus_next(self) -> Window | None:
303        """Focuses the next window in focus order, looping to first at the end."""
304
305        if self.focused is None:
306            self.focus(self._windows[0])
307            return self.focused
308
309        index = self._windows.index(self.focused)
310        if index == len(self._windows) - 1:
311            index = 0
312
313        window = self._windows[index]
314        traversed = 0
315        while window.is_persistent or window is self.focused:
316            if index >= len(self._windows):
317                index = 0
318
319            window = self._windows[index]
320
321            index += 1
322            traversed += 1
323            if traversed >= len(self._windows):
324                return self.focused
325
326        self.focus(self._windows[index])
327
328        return self.focused
329
330    def handle_key(self, key: str) -> bool:
331        """Processes a keypress.
332
333        Args:
334            key: The key to handle.
335
336        Returns:
337            True if the given key could be processed, False otherwise.
338        """
339
340        # Apply WindowManager bindings
341        if self.execute_binding(key):
342            return True
343
344        # Apply focused window binding, or send to InputField
345        if self.focused is not None:
346            if self.focused.execute_binding(key):
347                return True
348
349            if self.focused.handle_key(key):
350                return True
351
352        return False
353
354    # I prefer having the _click, _drag and _release helpers within this function, for
355    # easier readability.
356    def process_mouse(self, key: str) -> None:  # pylint: disable=too-many-statements
357        """Processes (potential) mouse input.
358
359        Args:
360            key: Input to handle.
361        """
362
363        window: Window
364
365        def _clamp_pos(pos: tuple[int, int], index: int) -> int:
366            """Clamp a value using index to address x/y & width/height"""
367
368            offset = self._drag_offsets[index]
369
370            # TODO: This -2 is a very magical number. Not good.
371            maximum = terminal.size[index] - ((window.width, window.height)[index] - 2)
372
373            start_margin_index = abs(index - 1)
374
375            if self.restrict_within_bounds:
376                return max(
377                    index + terminal.margins[start_margin_index],
378                    min(
379                        pos[index] - offset,
380                        maximum
381                        - terminal.margins[start_margin_index + 2]
382                        - terminal.origin[index],
383                    ),
384                )
385
386            return pos[index] - offset
387
388        def _click(pos: tuple[int, int], window: Window) -> bool:
389            """Process clicking a window."""
390
391            left, top, right, bottom = window.rect
392            borders = window.chars.get("border", [" "] * 4)
393
394            if real_length(borders[1]) > 0 and pos[1] == top and left <= pos[0] < right:
395                self._drag_target = (window, Edge.TOP)
396
397            elif (
398                real_length(borders[3]) > 0
399                and pos[1] == bottom - 1
400                and left <= pos[0] < right
401            ):
402                self._drag_target = (window, Edge.BOTTOM)
403
404            elif (
405                real_length(borders[0]) > 0
406                and pos[0] == left
407                and top <= pos[1] < bottom
408            ):
409                self._drag_target = (window, Edge.LEFT)
410
411            elif (
412                real_length(borders[2]) > 0
413                and pos[0] == right - 1
414                and top <= pos[1] < bottom
415            ):
416                self._drag_target = (window, Edge.RIGHT)
417
418            else:
419                return False
420
421            self._drag_offsets = (
422                pos[0] - window.pos[0],
423                pos[1] - window.pos[1],
424            )
425
426            return True
427
428        def _drag(pos: tuple[int, int], window: Window) -> bool:
429            """Process dragging a window"""
430
431            if self._drag_target is None:
432                return False
433
434            target_window, edge = self._drag_target
435            handled = False
436
437            if window is not target_window:
438                return False
439
440            left, top, right, bottom = window.rect
441
442            if not window.is_static and edge is Edge.TOP:
443                window.pos = (
444                    _clamp_pos(pos, 0),
445                    _clamp_pos(pos, 1),
446                )
447
448                handled = True
449
450            # TODO: Why are all these arbitrary offsets needed?
451            elif not window.is_noresize:
452                if edge is Edge.RIGHT:
453                    window.rect = (left, top, pos[0] + 1, bottom)
454                    handled = True
455
456                elif edge is Edge.LEFT:
457                    window.rect = (pos[0], top, right, bottom)
458                    handled = True
459
460                elif edge is Edge.BOTTOM:
461                    window.rect = (left, top, right, pos[1] + 1)
462                    handled = True
463
464            if handled:
465                window.is_dirty = True
466                self.compositor.set_redraw()
467
468            return handled
469
470        def _release(_: tuple[int, int], __: Window) -> bool:
471            """Process release of key"""
472
473            self._drag_target = None
474
475            # This return False so Window can handle the mouse action as well,
476            # as not much is done in this callback.
477            return False
478
479        handlers = {
480            MouseAction.LEFT_CLICK: _click,
481            MouseAction.LEFT_DRAG: _drag,
482            MouseAction.RELEASE: _release,
483        }
484
485        translate = self.mouse_translator
486        event_list = None if translate is None else translate(key)
487
488        if event_list is None:
489            return
490
491        for event in event_list:
492            # Ignore null-events
493            if event is None:
494                continue
495
496            for window in self._windows:
497                contains = window.contains(event.position)
498
499                if event.action in self.focusing_actions:
500                    self.focus(window)
501
502                if event.action in handlers and handlers[event.action](
503                    event.position, window
504                ):
505                    break
506
507                if window.handle_mouse(event) or (contains or window.is_modal):
508                    break
509
510            # Unset drag_target if no windows received the input
511            else:
512                self._drag_target = None
513
514    def screenshot(self, title: str, filename: str = "screenshot.svg") -> None:
515        """Takes a screenshot of the current state.
516
517        See `pytermgui.exporters.to_svg` for more information.
518
519        Args:
520            filename: The name of the file.
521        """
522
523        self.compositor.capture(title=title, filename=filename)
524
525    def show_positions(self) -> None:
526        """Shows the positions of each Window's widgets."""
527
528        def _show_positions(widget, color_base: int = 60) -> None:
529            """Show positions of widget."""
530
531            if isinstance(widget, Container):
532                for i, subwidget in enumerate(widget):
533                    _show_positions(subwidget, color_base + i)
534
535                return
536
537            if not widget.is_selectable:
538                return
539
540            debug = widget.debug()
541            color = str_to_color(f"@{color_base}")
542            buff = color(" ", reset=False)
543
544            for i in range(min(widget.width, real_length(debug)) - 1):
545                buff += debug[i]
546
547            self.terminal.write(buff, pos=widget.pos)
548
549        for widget in self._windows:
550            _show_positions(widget)
551        self.terminal.flush()
552
553        getch()
554
555    def alert(self, *items, center: bool = True, **attributes) -> Window:
556        """Creates a modal popup of the given elements and attributes.
557
558        Args:
559            *items: All widget-convertable objects passed as children of the new window.
560            center: If set, `pytermgui.window_manager.window.center` is called on the window.
561            **attributes: kwargs passed as the new window's attributes.
562        """
563
564        window = Window(*items, is_modal=True, **attributes)
565
566        if center:
567            window.center()
568
569        self.add(window, assign=False)
570
571        return window
572
573    def toast(
574        self,
575        *items,
576        offset: int = 0,
577        duration: int = 300,
578        delay: int = 1000,
579        **attributes,
580    ) -> Window:
581        """Creates a Material UI-inspired toast window of the given elements and attributes.
582
583        Args:
584            *items: All widget-convertable objects passed as children of the new window.
585            delay: The amount of time before the window will start animating out.
586            **attributes: kwargs passed as the new window's attributes.
587        """
588
589        # pylint: disable=no-value-for-parameter
590
591        toast = Window(*items, is_noblur=True, **attributes)
592
593        target_height = toast.height
594        toast.overflow = Overflow.HIDE
595
596        def _finish(_: Animation) -> None:
597            self.remove(toast, animate=False)
598
599        def _progressively_show(anim: Animation, invert: bool = False) -> bool:
600            height = int(anim.state * target_height)
601
602            toast.center()
603
604            if invert:
605                toast.height = target_height - 1 - height
606                toast.pos = (
607                    toast.pos[0],
608                    self.terminal.height - toast.height + 1 - offset,
609                )
610                return False
611
612            toast.height = height
613            toast.pos = (toast.pos[0], self.terminal.height - toast.height + 1 - offset)
614
615            return False
616
617        def _animate_toast_out(_: Animation) -> None:
618            animator.schedule(
619                FloatAnimation(
620                    delay,
621                    on_finish=lambda *_: animator.schedule(
622                        FloatAnimation(
623                            duration,
624                            on_step=lambda anim: _progressively_show(anim, invert=True),
625                            on_finish=_finish,
626                        )
627                    ),
628                )
629            )
630
631        leadup = FloatAnimation(
632            duration, on_step=_progressively_show, on_finish=_animate_toast_out
633        )
634
635        # pylint: enable=no-value-for-parameter
636
637        self.add(toast.center(), animate=False, assign=False)
638        self.focus(toast)
639        animator.schedule(leadup)
640
641        return toast

The manager of windows.

This class can be used, or even subclassed in order to create full-screen applications, using the pytermgui.window_manager.window.Window class and the general Widget API.

WindowManager( *, layout_type: Type[pytermgui.window_manager.layouts.Layout] = <class 'pytermgui.window_manager.layouts.Layout'>, framerate: int = 60, autorun: bool | None = None)
59    def __init__(
60        self,
61        *,
62        layout_type: Type[Layout] = Layout,
63        framerate: int = 60,
64        autorun: bool | None = None,
65    ) -> None:
66        """Initialize the manager."""
67
68        super().__init__()
69
70        self._is_running = False
71        self._windows: list[Window] = []
72        self._drag_offsets: tuple[int, int] = (0, 0)
73        self._drag_target: tuple[Window, Edge] | None = None
74        self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {}
75
76        self.focused: Window | None = None
77
78        if autorun is not None:
79            self.autorun = autorun
80
81        self.layout = layout_type()
82        self.compositor = Compositor(self._windows, framerate=framerate)
83        self.mouse_translator: MouseTranslator | None = None
84
85        # This isn't quite implemented at the moment.
86        self.restrict_within_bounds = True
87
88        terminal.subscribe(terminal.RESIZE, self.on_resize)

Initialize the manager.

is_bindable = True

Allow binding support

focusing_actions = (<MouseAction.LEFT_CLICK: 1>, <MouseAction.RIGHT_CLICK: 3>)

These mouse actions will focus the window they are acted upon.

autorun = True
def get_lines(self) -> list[str]:
144    def get_lines(self) -> list[str]:
145        """Gets the empty list."""
146
147        # TODO: Allow using WindowManager as a widget.
148
149        return []

Gets the empty list.

def clear_cache(self, window: pytermgui.window_manager.window.Window) -> None:
151    def clear_cache(self, window: Window) -> None:
152        """Clears the compositor's cache related to the given window."""
153
154        self.compositor.clear_cache(window)

Clears the compositor's cache related to the given window.

def on_resize(self, size: tuple[int, int]) -> None:
156    def on_resize(self, size: tuple[int, int]) -> None:
157        """Correctly updates window positions & prints when terminal gets resized.
158
159        Args:
160            size: The new terminal size.
161        """
162
163        width, height = size
164
165        for window in self._windows:
166            newx = max(0, min(window.pos[0], width - window.width))
167            newy = max(0, min(window.pos[1], height - window.height + 1))
168
169            window.pos = (newx, newy)
170
171        self.layout.apply()
172        self.compositor.redraw()

Correctly updates window positions & prints when terminal gets resized.

Args
  • size: The new terminal size.
def run(self, mouse_events: list[str] | None = None) -> None:
174    def run(self, mouse_events: list[str] | None = None) -> None:
175        """Starts the WindowManager.
176
177        Args:
178            mouse_events: A list of mouse event types to listen to. See
179                `pytermgui.ansi_interface.report_mouse` for more information.
180                Defaults to `["press_hold", "hover"]`.
181
182        Returns:
183            The WindowManager's compositor instance.
184        """
185
186        self._is_running = True
187
188        if mouse_events is None:
189            mouse_events = ["press_hold", "hover"]
190
191        with alt_buffer(cursor=False, echo=False):
192            with mouse_handler(mouse_events, "decimal_xterm") as translate:
193                self.mouse_translator = translate
194                self.compositor.run()
195
196                self._run_input_loop()

Starts the WindowManager.

Args
Returns

The WindowManager's compositor instance.

def stop(self) -> None:
198    def stop(self) -> None:
199        """Stops the WindowManager and its compositor."""
200
201        self.compositor.stop()
202        self._is_running = False

Stops the WindowManager and its compositor.

def add( self, window: pytermgui.window_manager.window.Window, assign: str | bool = True, animate: bool = True) -> pytermgui.window_manager.manager.WindowManager:
204    def add(
205        self, window: Window, assign: str | bool = True, animate: bool = True
206    ) -> WindowManager:
207        """Adds a window to the manager.
208
209        Args:
210            window: The window to add.
211            assign: The name of the slot the new window should be assigned to, or a
212                boolean. If it is given a str, it is treated as the name of a slot. When
213                given True, the next non-filled slot will be assigned, and when given
214                False no assignment will be done.
215            animate: If set, an animation will be played on the window once it's added.
216        """
217
218        self._windows.insert(0, window)
219        window.manager = self
220
221        if assign:
222            if isinstance(assign, str):
223                getattr(self.layout, assign).content = window
224
225            elif len(self._windows) <= len(self.layout.slots):
226                self.layout.assign(window, index=len(self._windows) - 1)
227
228            self.layout.apply()
229
230        # New windows take focus-precedence over already
231        # existing ones, even if they are modal.
232        self.focus(window)
233
234        if not animate:
235            return self
236
237        if window.height > 1:
238            animator.animate_attr(
239                target=window,
240                attr="height",
241                start=0,
242                end=window.height,
243                duration=300,
244                on_step=_center_during_animation,
245            )
246
247        return self

Adds a window to the manager.

Args
  • window: The window to add.
  • assign: The name of the slot the new window should be assigned to, or a boolean. If it is given a str, it is treated as the name of a slot. When given True, the next non-filled slot will be assigned, and when given False no assignment will be done.
  • animate: If set, an animation will be played on the window once it's added.
def remove( self, window: pytermgui.window_manager.window.Window, autostop: bool = True, animate: bool = True) -> pytermgui.window_manager.manager.WindowManager:
249    def remove(
250        self,
251        window: Window,
252        autostop: bool = True,
253        animate: bool = True,
254    ) -> WindowManager:
255        """Removes a window from the manager.
256
257        Args:
258            window: The window to remove.
259            autostop: If set, the manager will be stopped if the length of its windows
260                hits 0.
261        """
262
263        def _on_finish(_: AttrAnimation | None) -> bool:
264            self._windows.remove(window)
265
266            if autostop and len(self._windows) == 0:
267                self.stop()
268            else:
269                self.focus(self._windows[0])
270
271            return True
272
273        if not animate:
274            _on_finish(None)
275            return self
276
277        animator.animate_attr(
278            target=window,
279            attr="height",
280            end=0,
281            duration=300,
282            on_step=_center_during_animation,
283            on_finish=_on_finish,
284        )
285
286        return self

Removes a window from the manager.

Args
  • window: The window to remove.
  • autostop: If set, the manager will be stopped if the length of its windows hits 0.
def focus(self, window: pytermgui.window_manager.window.Window | None) -> None:
288    def focus(self, window: Window | None) -> None:
289        """Focuses a window by moving it to the first index in _windows."""
290
291        if self.focused is not None:
292            self.focused.blur()
293
294        self.focused = window
295
296        if window is not None:
297            self._windows.remove(window)
298            self._windows.insert(0, window)
299
300            window.focus()

Focuses a window by moving it to the first index in _windows.

def focus_next(self) -> pytermgui.window_manager.window.Window | None:
302    def focus_next(self) -> Window | None:
303        """Focuses the next window in focus order, looping to first at the end."""
304
305        if self.focused is None:
306            self.focus(self._windows[0])
307            return self.focused
308
309        index = self._windows.index(self.focused)
310        if index == len(self._windows) - 1:
311            index = 0
312
313        window = self._windows[index]
314        traversed = 0
315        while window.is_persistent or window is self.focused:
316            if index >= len(self._windows):
317                index = 0
318
319            window = self._windows[index]
320
321            index += 1
322            traversed += 1
323            if traversed >= len(self._windows):
324                return self.focused
325
326        self.focus(self._windows[index])
327
328        return self.focused

Focuses the next window in focus order, looping to first at the end.

def handle_key(self, key: str) -> bool:
330    def handle_key(self, key: str) -> bool:
331        """Processes a keypress.
332
333        Args:
334            key: The key to handle.
335
336        Returns:
337            True if the given key could be processed, False otherwise.
338        """
339
340        # Apply WindowManager bindings
341        if self.execute_binding(key):
342            return True
343
344        # Apply focused window binding, or send to InputField
345        if self.focused is not None:
346            if self.focused.execute_binding(key):
347                return True
348
349            if self.focused.handle_key(key):
350                return True
351
352        return False

Processes a keypress.

Args
  • key: The key to handle.
Returns

True if the given key could be processed, False otherwise.

def process_mouse(self, key: str) -> None:
356    def process_mouse(self, key: str) -> None:  # pylint: disable=too-many-statements
357        """Processes (potential) mouse input.
358
359        Args:
360            key: Input to handle.
361        """
362
363        window: Window
364
365        def _clamp_pos(pos: tuple[int, int], index: int) -> int:
366            """Clamp a value using index to address x/y & width/height"""
367
368            offset = self._drag_offsets[index]
369
370            # TODO: This -2 is a very magical number. Not good.
371            maximum = terminal.size[index] - ((window.width, window.height)[index] - 2)
372
373            start_margin_index = abs(index - 1)
374
375            if self.restrict_within_bounds:
376                return max(
377                    index + terminal.margins[start_margin_index],
378                    min(
379                        pos[index] - offset,
380                        maximum
381                        - terminal.margins[start_margin_index + 2]
382                        - terminal.origin[index],
383                    ),
384                )
385
386            return pos[index] - offset
387
388        def _click(pos: tuple[int, int], window: Window) -> bool:
389            """Process clicking a window."""
390
391            left, top, right, bottom = window.rect
392            borders = window.chars.get("border", [" "] * 4)
393
394            if real_length(borders[1]) > 0 and pos[1] == top and left <= pos[0] < right:
395                self._drag_target = (window, Edge.TOP)
396
397            elif (
398                real_length(borders[3]) > 0
399                and pos[1] == bottom - 1
400                and left <= pos[0] < right
401            ):
402                self._drag_target = (window, Edge.BOTTOM)
403
404            elif (
405                real_length(borders[0]) > 0
406                and pos[0] == left
407                and top <= pos[1] < bottom
408            ):
409                self._drag_target = (window, Edge.LEFT)
410
411            elif (
412                real_length(borders[2]) > 0
413                and pos[0] == right - 1
414                and top <= pos[1] < bottom
415            ):
416                self._drag_target = (window, Edge.RIGHT)
417
418            else:
419                return False
420
421            self._drag_offsets = (
422                pos[0] - window.pos[0],
423                pos[1] - window.pos[1],
424            )
425
426            return True
427
428        def _drag(pos: tuple[int, int], window: Window) -> bool:
429            """Process dragging a window"""
430
431            if self._drag_target is None:
432                return False
433
434            target_window, edge = self._drag_target
435            handled = False
436
437            if window is not target_window:
438                return False
439
440            left, top, right, bottom = window.rect
441
442            if not window.is_static and edge is Edge.TOP:
443                window.pos = (
444                    _clamp_pos(pos, 0),
445                    _clamp_pos(pos, 1),
446                )
447
448                handled = True
449
450            # TODO: Why are all these arbitrary offsets needed?
451            elif not window.is_noresize:
452                if edge is Edge.RIGHT:
453                    window.rect = (left, top, pos[0] + 1, bottom)
454                    handled = True
455
456                elif edge is Edge.LEFT:
457                    window.rect = (pos[0], top, right, bottom)
458                    handled = True
459
460                elif edge is Edge.BOTTOM:
461                    window.rect = (left, top, right, pos[1] + 1)
462                    handled = True
463
464            if handled:
465                window.is_dirty = True
466                self.compositor.set_redraw()
467
468            return handled
469
470        def _release(_: tuple[int, int], __: Window) -> bool:
471            """Process release of key"""
472
473            self._drag_target = None
474
475            # This return False so Window can handle the mouse action as well,
476            # as not much is done in this callback.
477            return False
478
479        handlers = {
480            MouseAction.LEFT_CLICK: _click,
481            MouseAction.LEFT_DRAG: _drag,
482            MouseAction.RELEASE: _release,
483        }
484
485        translate = self.mouse_translator
486        event_list = None if translate is None else translate(key)
487
488        if event_list is None:
489            return
490
491        for event in event_list:
492            # Ignore null-events
493            if event is None:
494                continue
495
496            for window in self._windows:
497                contains = window.contains(event.position)
498
499                if event.action in self.focusing_actions:
500                    self.focus(window)
501
502                if event.action in handlers and handlers[event.action](
503                    event.position, window
504                ):
505                    break
506
507                if window.handle_mouse(event) or (contains or window.is_modal):
508                    break
509
510            # Unset drag_target if no windows received the input
511            else:
512                self._drag_target = None

Processes (potential) mouse input.

Args
  • key: Input to handle.
def screenshot(self, title: str, filename: str = 'screenshot.svg') -> None:
514    def screenshot(self, title: str, filename: str = "screenshot.svg") -> None:
515        """Takes a screenshot of the current state.
516
517        See `pytermgui.exporters.to_svg` for more information.
518
519        Args:
520            filename: The name of the file.
521        """
522
523        self.compositor.capture(title=title, filename=filename)

Takes a screenshot of the current state.

See pytermgui.exporters.to_svg for more information.

Args
  • filename: The name of the file.
def show_positions(self) -> None:
525    def show_positions(self) -> None:
526        """Shows the positions of each Window's widgets."""
527
528        def _show_positions(widget, color_base: int = 60) -> None:
529            """Show positions of widget."""
530
531            if isinstance(widget, Container):
532                for i, subwidget in enumerate(widget):
533                    _show_positions(subwidget, color_base + i)
534
535                return
536
537            if not widget.is_selectable:
538                return
539
540            debug = widget.debug()
541            color = str_to_color(f"@{color_base}")
542            buff = color(" ", reset=False)
543
544            for i in range(min(widget.width, real_length(debug)) - 1):
545                buff += debug[i]
546
547            self.terminal.write(buff, pos=widget.pos)
548
549        for widget in self._windows:
550            _show_positions(widget)
551        self.terminal.flush()
552
553        getch()

Shows the positions of each Window's widgets.

def alert( self, *items, center: bool = True, **attributes) -> pytermgui.window_manager.window.Window:
555    def alert(self, *items, center: bool = True, **attributes) -> Window:
556        """Creates a modal popup of the given elements and attributes.
557
558        Args:
559            *items: All widget-convertable objects passed as children of the new window.
560            center: If set, `pytermgui.window_manager.window.center` is called on the window.
561            **attributes: kwargs passed as the new window's attributes.
562        """
563
564        window = Window(*items, is_modal=True, **attributes)
565
566        if center:
567            window.center()
568
569        self.add(window, assign=False)
570
571        return window

Creates a modal popup of the given elements and attributes.

Args
  • *items: All widget-convertable objects passed as children of the new window.
  • center: If set, pytermgui.window_manager.window.center is called on the window.
  • **attributes: kwargs passed as the new window's attributes.
def toast( self, *items, offset: int = 0, duration: int = 300, delay: int = 1000, **attributes) -> pytermgui.window_manager.window.Window:
573    def toast(
574        self,
575        *items,
576        offset: int = 0,
577        duration: int = 300,
578        delay: int = 1000,
579        **attributes,
580    ) -> Window:
581        """Creates a Material UI-inspired toast window of the given elements and attributes.
582
583        Args:
584            *items: All widget-convertable objects passed as children of the new window.
585            delay: The amount of time before the window will start animating out.
586            **attributes: kwargs passed as the new window's attributes.
587        """
588
589        # pylint: disable=no-value-for-parameter
590
591        toast = Window(*items, is_noblur=True, **attributes)
592
593        target_height = toast.height
594        toast.overflow = Overflow.HIDE
595
596        def _finish(_: Animation) -> None:
597            self.remove(toast, animate=False)
598
599        def _progressively_show(anim: Animation, invert: bool = False) -> bool:
600            height = int(anim.state * target_height)
601
602            toast.center()
603
604            if invert:
605                toast.height = target_height - 1 - height
606                toast.pos = (
607                    toast.pos[0],
608                    self.terminal.height - toast.height + 1 - offset,
609                )
610                return False
611
612            toast.height = height
613            toast.pos = (toast.pos[0], self.terminal.height - toast.height + 1 - offset)
614
615            return False
616
617        def _animate_toast_out(_: Animation) -> None:
618            animator.schedule(
619                FloatAnimation(
620                    delay,
621                    on_finish=lambda *_: animator.schedule(
622                        FloatAnimation(
623                            duration,
624                            on_step=lambda anim: _progressively_show(anim, invert=True),
625                            on_finish=_finish,
626                        )
627                    ),
628                )
629            )
630
631        leadup = FloatAnimation(
632            duration, on_step=_progressively_show, on_finish=_animate_toast_out
633        )
634
635        # pylint: enable=no-value-for-parameter
636
637        self.add(toast.center(), animate=False, assign=False)
638        self.focus(toast)
639        animator.schedule(leadup)
640
641        return toast

Creates a Material UI-inspired toast window of the given elements and attributes.

Args
  • *items: All widget-convertable objects passed as children of the new window.
  • delay: The amount of time before the window will start animating out.
  • **attributes: kwargs passed as the new window's attributes.