ai-station/.venv/lib/python3.12/site-packages/textual/widgets/_toast.py

203 lines
5.8 KiB
Python

"""Widgets for showing notification messages in toasts."""
from __future__ import annotations
from typing import ClassVar
from textual import on
from textual.containers import Container
from textual.content import Content
from textual.css.query import NoMatches
from textual.events import Click, Mount
from textual.notifications import Notification, Notifications
from textual.widgets._static import Static
class ToastHolder(Container, inherit_css=False):
"""Container that holds a single toast.
Used to control the alignment of each of the toasts in the main toast
container.
"""
DEFAULT_CSS = """
ToastHolder {
align-horizontal: right;
width: 1fr;
height: auto;
visibility: hidden;
}
"""
class Toast(Static, inherit_css=False):
"""A widget for displaying short-lived notifications."""
DEFAULT_CSS = """
Toast {
width: 60;
max-width: 50%;
height: auto;
margin-top: 1;
visibility: visible;
padding: 1 1;
background: $panel-lighten-1;
link-background: initial;
link-color: $foreground;
link-style: underline;
link-background-hover: $primary;
link-color-hover: $foreground;
link-style-hover: bold not underline;
}
.toast--title {
text-style: bold;
color: $foreground;
}
Toast.-information {
border-left: outer $success;
}
Toast.-information .toast--title {
color: $text-success;
}
Toast.-warning {
border-left: outer $warning;
}
Toast.-warning .toast--title {
color: $text-warning;
}
Toast.-error {
border-left: outer $error;
}
Toast.-error .toast--title {
color: $text-error;
}
"""
COMPONENT_CLASSES: ClassVar[set[str]] = {"toast--title"}
"""
| Class | Description |
| :- | :- |
| `toast--title` | Targets the title of the toast. |
"""
DEFAULT_CLASSES = "-textual-system"
def __init__(self, notification: Notification) -> None:
"""Initialise the toast.
Args:
notification: The notification to show in the toast.
"""
super().__init__(classes=f"-{notification.severity}")
self._notification = notification
self._timeout = notification.time_left
def render(self) -> Content:
"""Render the toast's content.
Returns:
A Rich renderable for the title and content of the Toast.
"""
notification = self._notification
message_content = (
Content.from_markup(notification.message)
if notification.markup
else Content(notification.message)
)
if notification.title:
header_style = self.get_visual_style("toast--title")
message_content = Content.assemble(
(notification.title, header_style), "\n", message_content
)
return message_content
def _on_mount(self, _: Mount) -> None:
"""Set the time running once the toast is mounted."""
self.set_timer(self._timeout, self._expire)
@on(Click)
def _expire(self) -> None:
"""Remove the toast once the timer has expired."""
# Before we removed ourself, we also call on the app to forget about
# the notification that caused us to exist. Note that we tell the
# app to not bother refreshing the display on our account, we're
# about to handle that anyway.
self.app._unnotify(self._notification, refresh=False)
# Note that we attempt to remove our parent, because we're wrapped
# inside an alignment container. The testing that we are is as much
# to keep type checkers happy as anything else.
(self.parent if isinstance(self.parent, ToastHolder) else self).remove()
class ToastRack(Container, inherit_css=False):
"""A container for holding toasts."""
DEFAULT_CSS = """
ToastRack {
display: none;
layer: _toastrack;
width: 1fr;
height: auto;
dock: bottom;
align: right bottom;
visibility: hidden;
layout: vertical;
overflow-y: scroll;
margin-bottom: 1;
}
"""
DEFAULT_CLASSES = "-textual-system"
@staticmethod
def _toast_id(notification: Notification) -> str:
"""Create a Textual-DOM-internal ID for the given notification.
Args:
notification: The notification to create the ID for.
Returns:
An ID for the notification that can be used within the DOM.
"""
return f"--textual-toast-{notification.identity}"
def show(self, notifications: Notifications) -> None:
"""Show the notifications as toasts.
Args:
notifications: The notifications to show.
"""
self.display = bool(notifications)
# Look for any stale toasts and remove them.
for toast in self.query(Toast):
if toast._notification not in notifications:
toast.remove()
# Gather up all the notifications that we don't have toasts for yet.
new_toasts: list[Notification] = []
for notification in notifications:
try:
# See if there's already a toast for that notification.
_ = self.get_child_by_id(self._toast_id(notification))
except NoMatches:
if not notification.has_expired:
new_toasts.append(notification)
# If we got any...
if new_toasts:
# ...mount them.
self.mount_all(
ToastHolder(Toast(toast), id=self._toast_id(toast))
for toast in new_toasts
)
self.call_later(self.scroll_end, animate=False, force=True)