pytermgui.window_manager.manager

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

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

#   def get_lines(self) -> list[str]:
View Source
133    def get_lines(self) -> list[str]:
134        """Gets the empty list."""
135
136        # TODO: Allow using WindowManager as a widget.
137
138        return []

Gets the empty list.

#   def clear_cache(self, window: pytermgui.window_manager.window.Window) -> None:
View Source
140    def clear_cache(self, window: Window) -> None:
141        """Clears the compositor's cache related to the given window."""
142
143        self.compositor.clear_cache(window)

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

#   def on_resize(self, size: tuple[int, int]) -> None:
View Source
145    def on_resize(self, size: tuple[int, int]) -> None:
146        """Correctly updates window positions & prints when terminal gets resized.
147
148        Args:
149            size: The new terminal size.
150        """
151
152        width, height = size
153
154        for window in self._windows:
155            newx = max(0, min(window.pos[0], width - window.width))
156            newy = max(0, min(window.pos[1], height - window.height + 1))
157
158            window.pos = (newx, newy)
159
160        self.layout.apply()
161        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:
View Source
163    def run(self, mouse_events: list[str] | None = None) -> None:
164        """Starts the WindowManager.
165
166        Args:
167            mouse_events: A list of mouse event types to listen to. See
168                `pytermgui.ansi_interface.report_mouse` for more information.
169                Defaults to `["press_hold", "hover"]`.
170
171        Returns:
172            The WindowManager's compositor instance.
173        """
174
175        self._is_running = True
176
177        if mouse_events is None:
178            mouse_events = ["press_hold", "hover"]
179
180        with alt_buffer(cursor=False, echo=False):
181            with mouse_handler(mouse_events, "decimal_xterm") as translate:
182                self.mouse_translator = translate
183                self.compositor.run()
184
185                self._run_input_loop()

Starts the WindowManager.

Args
Returns

The WindowManager's compositor instance.

#   def stop(self) -> None:
View Source
187    def stop(self) -> None:
188        """Stops the WindowManager and its compositor."""
189
190        self.compositor.stop()
191        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:
View Source
193    def add(
194        self, window: Window, assign: str | bool = True, animate: bool = True
195    ) -> WindowManager:
196        """Adds a window to the manager.
197
198        Args:
199            window: The window to add.
200            assign: The name of the slot the new window should be assigned to, or a
201                boolean. If it is given a str, it is treated as the name of a slot. When
202                given True, the next non-filled slot will be assigned, and when given
203                False no assignment will be done.
204            animate: If set, an animation will be played on the window once it's added.
205        """
206
207        self._windows.insert(0, window)
208        window.manager = self
209
210        if assign:
211            if isinstance(assign, str):
212                getattr(self.layout, assign).content = window
213
214            elif len(self._windows) <= len(self.layout.slots):
215                self.layout.assign(window, index=len(self._windows) - 1)
216
217            self.layout.apply()
218
219        # New windows take focus-precedence over already
220        # existing ones, even if they are modal.
221        self.focus(window)
222
223        if not animate:
224            return self
225
226        if window.height > 1:
227            animator.animate_attr(
228                target=window,
229                attr="height",
230                start=0,
231                end=window.height,
232                duration=300,
233                on_step=_center_during_animation,
234            )
235
236        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:
View Source
238    def remove(
239        self,
240        window: Window,
241        autostop: bool = True,
242        animate: bool = True,
243    ) -> WindowManager:
244        """Removes a window from the manager.
245
246        Args:
247            window: The window to remove.
248            autostop: If set, the manager will be stopped if the length of its windows
249                hits 0.
250        """
251
252        def _on_finish(_: AttrAnimation | None) -> bool:
253            self._windows.remove(window)
254
255            if autostop and len(self._windows) == 0:
256                self.stop()
257            else:
258                self.focus(self._windows[0])
259
260            return True
261
262        if not animate:
263            _on_finish(None)
264            return self
265
266        animator.animate_attr(
267            target=window,
268            attr="height",
269            end=0,
270            duration=300,
271            on_step=_center_during_animation,
272            on_finish=_on_finish,
273        )
274
275        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:
View Source
277    def focus(self, window: Window | None) -> None:
278        """Focuses a window by moving it to the first index in _windows."""
279
280        if self.focused is not None:
281            self.focused.blur()
282
283        self.focused = window
284
285        if window is not None:
286            self._windows.remove(window)
287            self._windows.insert(0, window)
288
289            window.focus()

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

#   def focus_next(self) -> pytermgui.window_manager.window.Window | None:
View Source
291    def focus_next(self) -> Window | None:
292        """Focuses the next window in focus order, looping to first at the end."""
293
294        if self.focused is None:
295            self.focus(self._windows[0])
296            return self.focused
297
298        index = self._windows.index(self.focused)
299        if index == len(self._windows) - 1:
300            index = 0
301
302        window = self._windows[index]
303        traversed = 0
304        while window.is_persistent or window is self.focused:
305            index += 1
306
307            if index >= len(self._windows):
308                index = 0
309
310            window = self._windows[index]
311
312            traversed += 1
313            if traversed >= len(self._windows):
314                return self.focused
315
316        self.focus(self._windows[index])
317
318        return self.focused

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

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

Processes (potential) mouse input.

Args
  • key: Input to handle.
#   def screenshot(self, title: str, filename: str = 'screenshot.svg') -> None:
View Source
504    def screenshot(self, title: str, filename: str = "screenshot.svg") -> None:
505        """Takes a screenshot of the current state.
506
507        See `pytermgui.exporters.to_svg` for more information.
508
509        Args:
510            filename: The name of the file.
511        """
512
513        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:
View Source
515    def show_positions(self) -> None:
516        """Shows the positions of each Window's widgets."""
517
518        def _show_positions(widget, color_base: int = 60) -> None:
519            """Show positions of widget."""
520
521            if isinstance(widget, Container):
522                for i, subwidget in enumerate(widget):
523                    _show_positions(subwidget, color_base + i)
524
525                return
526
527            if not widget.is_selectable:
528                return
529
530            debug = widget.debug()
531            color = str_to_color(f"@{color_base}")
532            buff = color(" ", reset=False)
533
534            for i in range(min(widget.width, real_length(debug)) - 1):
535                buff += debug[i]
536
537            self.terminal.write(buff, pos=widget.pos)
538
539        for widget in self._windows:
540            _show_positions(widget)
541        self.terminal.flush()
542
543        getch()

Shows the positions of each Window's widgets.

#   def alert( self, *items, center: bool = True, **attributes ) -> pytermgui.window_manager.window.Window:
View Source
545    def alert(self, *items, center: bool = True, **attributes) -> Window:
546        """Creates a modal popup of the given elements and attributes.
547
548        Args:
549            *items: All widget-convertable objects passed as children of the new window.
550            center: If set, `pytermgui.window_manager.window.center` is called on the window.
551            **attributes: kwargs passed as the new window's attributes.
552        """
553
554        window = Window(*items, is_modal=True, **attributes)
555
556        if center:
557            window.center()
558
559        self.add(window, assign=False)
560
561        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:
View Source
563    def toast(
564        self,
565        *items,
566        offset: int = 0,
567        duration: int = 300,
568        delay: int = 1000,
569        **attributes,
570    ) -> Window:
571        """Creates a Material UI-inspired toast window of the given elements and attributes.
572
573        Args:
574            *items: All widget-convertable objects passed as children of the new window.
575            delay: The amount of time before the window will start animating out.
576            **attributes: kwargs passed as the new window's attributes.
577        """
578
579        # pylint: disable=no-value-for-parameter
580
581        toast = Window(*items, is_noblur=True, **attributes)
582
583        target_height = toast.height
584        toast.overflow = Overflow.HIDE
585
586        def _finish(_: Animation) -> None:
587            self.remove(toast, animate=False)
588
589        def _progressively_show(anim: Animation, invert: bool = False) -> bool:
590            height = int(anim.state * target_height)
591
592            toast.center()
593
594            if invert:
595                toast.height = target_height - 1 - height
596                toast.pos = (
597                    toast.pos[0],
598                    self.terminal.height - toast.height + 1 - offset,
599                )
600                return False
601
602            toast.height = height
603            toast.pos = (toast.pos[0], self.terminal.height - toast.height + 1 - offset)
604
605            return False
606
607        def _animate_toast_out(_: Animation) -> None:
608            animator.schedule(
609                FloatAnimation(
610                    delay,
611                    on_finish=lambda *_: animator.schedule(
612                        FloatAnimation(
613                            duration,
614                            on_step=lambda anim: _progressively_show(anim, invert=True),
615                            on_finish=_finish,
616                        )
617                    ),
618                )
619            )
620
621        leadup = FloatAnimation(
622            duration, on_step=_progressively_show, on_finish=_animate_toast_out
623        )
624
625        # pylint: enable=no-value-for-parameter
626
627        self.add(toast.center(), animate=False, assign=False)
628        self.focus(toast)
629        animator.schedule(leadup)
630
631        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.