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

491 lines
15 KiB
Python
Raw Permalink Normal View History

2025-12-25 14:54:33 +00:00
from __future__ import annotations
from functools import partial
from typing import TYPE_CHECKING, cast
import rich.repr
from rich.cells import cell_len
from rich.console import ConsoleRenderable, RenderableType
from typing_extensions import Literal, Self
from textual import events
if TYPE_CHECKING:
from textual.app import RenderResult
from rich.style import Style
from textual.binding import Binding
from textual.content import Content, ContentText
from textual.css._error_tools import friendly_list
from textual.geometry import Size
from textual.message import Message
from textual.reactive import reactive
from textual.widget import Widget
ButtonVariant = Literal["default", "primary", "success", "warning", "error"]
"""The names of the valid button variants.
These are the variants that can be used with a [`Button`][textual.widgets.Button].
"""
_VALID_BUTTON_VARIANTS = {"default", "primary", "success", "warning", "error"}
class InvalidButtonVariant(Exception):
"""Exception raised if an invalid button variant is used."""
class Button(Widget, can_focus=True):
"""A simple clickable button.
Clicking the button will send a [Button.Pressed][textual.widgets.Button.Pressed] message,
unless the `action` parameter is provided.
"""
ALLOW_SELECT = False
DEFAULT_CSS = """
Button {
width: auto;
min-width: 16;
height:auto;
line-pad: 1;
text-align: center;
content-align: center middle;
&.-style-flat {
text-style: bold;
color: auto 90%;
background: $surface;
border: block $surface;
&:hover {
background: $primary;
border: block $primary;
}
&:focus {
text-style: $button-focus-text-style;
}
&.-active {
background: $surface;
border: block $surface;
tint: $background 30%;
}
&:disabled {
color: auto 50%;
}
&.-primary {
background: $primary-muted;
border: block $primary-muted;
color: $text-primary;
&:hover {
color: $text;
background: $primary;
border: block $primary;
}
}
&.-success {
background: $success-muted;
border: block $success-muted;
color: $text-success;
&:hover {
color: $text;
background: $success;
border: block $success;
}
}
&.-warning {
background: $warning-muted;
border: block $warning-muted;
color: $text-warning;
&:hover {
color: $text;
background: $warning;
border: block $warning;
}
}
&.-error {
background: $error-muted;
border: block $error-muted;
color: $text-error;
&:hover {
color: $text;
background: $error;
border: block $error;
}
}
}
&.-style-default {
text-style: bold;
color: $button-foreground;
background: $surface;
border: none;
border-top: tall $surface-lighten-1;
border-bottom: tall $surface-darken-1;
&.-textual-compact {
border: none !important;
}
&:disabled {
text-opacity: 0.6;
}
&:focus {
text-style: $button-focus-text-style;
background-tint: $foreground 5%;
}
&:hover {
border-top: tall $surface;
background: $surface-darken-1;
}
&.-active {
background: $surface;
border-bottom: tall $surface-lighten-1;
border-top: tall $surface-darken-1;
tint: $background 30%;
}
&.-primary {
color: $button-color-foreground;
background: $primary;
border-top: tall $primary-lighten-3;
border-bottom: tall $primary-darken-3;
&:hover {
background: $primary-darken-2;
border-top: tall $primary;
}
&.-active {
background: $primary;
border-bottom: tall $primary-lighten-3;
border-top: tall $primary-darken-3;
}
}
&.-success {
color: $button-color-foreground;
background: $success;
border-top: tall $success-lighten-2;
border-bottom: tall $success-darken-3;
&:hover {
background: $success-darken-2;
border-top: tall $success;
}
&.-active {
background: $success;
border-bottom: tall $success-lighten-2;
border-top: tall $success-darken-2;
}
}
&.-warning{
color: $button-color-foreground;
background: $warning;
border-top: tall $warning-lighten-2;
border-bottom: tall $warning-darken-3;
&:hover {
background: $warning-darken-2;
border-top: tall $warning;
}
&.-active {
background: $warning;
border-bottom: tall $warning-lighten-2;
border-top: tall $warning-darken-2;
}
}
&.-error {
color: $button-color-foreground;
background: $error;
border-top: tall $error-lighten-2;
border-bottom: tall $error-darken-3;
&:hover {
background: $error-darken-1;
border-top: tall $error;
}
&.-active {
background: $error;
border-bottom: tall $error-lighten-2;
border-top: tall $error-darken-2;
}
}
}
}
"""
BINDINGS = [Binding("enter", "press", "Press button", show=False)]
label: reactive[ContentText] = reactive[ContentText](Content.empty())
"""The text label that appears within the button."""
variant = reactive("default", init=False)
"""The variant name for the button."""
compact = reactive(False, toggle_class="-textual-compact")
"""Make the button compact (without borders)."""
flat = reactive(False)
"""Enable alternative flat button style."""
class Pressed(Message):
"""Event sent when a `Button` is pressed and there is no Button action.
Can be handled using `on_button_pressed` in a subclass of
[`Button`][textual.widgets.Button] or in a parent widget in the DOM.
"""
def __init__(self, button: Button) -> None:
self.button: Button = button
"""The button that was pressed."""
super().__init__()
@property
def control(self) -> Button:
"""An alias for [Pressed.button][textual.widgets.Button.Pressed.button].
This will be the same value as [Pressed.button][textual.widgets.Button.Pressed.button].
"""
return self.button
def __init__(
self,
label: ContentText | None = None,
variant: ButtonVariant = "default",
*,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
tooltip: RenderableType | None = None,
action: str | None = None,
compact: bool = False,
flat: bool = False,
):
"""Create a Button widget.
Args:
label: The text that appears within the button.
variant: The variant of the button.
name: The name of the button.
id: The ID of the button in the DOM.
classes: The CSS classes of the button.
disabled: Whether the button is disabled or not.
tooltip: Optional tooltip.
action: Optional action to run when clicked.
compact: Enable compact button style.
flat: Enable alternative flat look buttons.
"""
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
if label is None:
label = self.css_identifier_styled
self.variant = variant
self.flat = flat
self.compact = compact
self.set_reactive(Button.label, Content.from_text(label))
self.action = action
self.active_effect_duration = 0.2
"""Amount of time in seconds the button 'press' animation lasts."""
if tooltip is not None:
self.tooltip = tooltip
def get_content_width(self, container: Size, viewport: Size) -> int:
assert isinstance(self.label, Content)
try:
return max([cell_len(line) for line in self.label.plain.splitlines()]) + 2
except ValueError:
# Empty string label
return 2
def __rich_repr__(self) -> rich.repr.Result:
yield from super().__rich_repr__()
yield "variant", self.variant, "default"
def validate_variant(self, variant: str) -> str:
if variant not in _VALID_BUTTON_VARIANTS:
raise InvalidButtonVariant(
f"Valid button variants are {friendly_list(_VALID_BUTTON_VARIANTS)}"
)
return variant
def watch_variant(self, old_variant: str, variant: str):
self.remove_class(f"-{old_variant}")
self.add_class(f"-{variant}")
def watch_flat(self, flat: bool) -> None:
self.set_class(flat, "-style-flat")
self.set_class(not flat, "-style-default")
def validate_label(self, label: ContentText) -> Content:
"""Parse markup for self.label"""
return Content.from_text(label)
def render(self) -> RenderResult:
assert isinstance(self.label, Content)
return self.label
def post_render(
self, renderable: RenderableType, base_style: Style
) -> ConsoleRenderable:
return cast(ConsoleRenderable, renderable)
async def _on_click(self, event: events.Click) -> None:
event.stop()
if not self.has_class("-active"):
self.press()
def press(self) -> Self:
"""Animate the button and send the [Pressed][textual.widgets.Button.Pressed] message.
Can be used to simulate the button being pressed by a user.
Returns:
The button instance.
"""
if self.disabled or not self.display:
return self
# Manage the "active" effect:
self._start_active_affect()
# ...and let other components know that we've just been clicked:
if self.action is None:
self.post_message(Button.Pressed(self))
else:
self.call_later(
self.app.run_action, self.action, default_namespace=self._parent
)
return self
def _start_active_affect(self) -> None:
"""Start a small animation to show the button was clicked."""
if self.active_effect_duration > 0:
self.add_class("-active")
self.set_timer(
self.active_effect_duration, partial(self.remove_class, "-active")
)
def action_press(self) -> None:
"""Activate a press of the button."""
if not self.has_class("-active"):
self.press()
@classmethod
def success(
cls,
label: ContentText | None = None,
*,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
flat: bool = False,
) -> Button:
"""Utility constructor for creating a success Button variant.
Args:
label: The text that appears within the button.
name: The name of the button.
id: The ID of the button in the DOM.
classes: The CSS classes of the button.
disabled: Whether the button is disabled or not.
flat: Enable alternative flat look buttons.
Returns:
A [`Button`][textual.widgets.Button] widget of the 'success'
[variant][textual.widgets.button.ButtonVariant].
"""
return Button(
label=label,
variant="success",
name=name,
id=id,
classes=classes,
disabled=disabled,
flat=flat,
)
@classmethod
def warning(
cls,
label: ContentText | None = None,
*,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
flat: bool = False,
) -> Button:
"""Utility constructor for creating a warning Button variant.
Args:
label: The text that appears within the button.
name: The name of the button.
id: The ID of the button in the DOM.
classes: The CSS classes of the button.
disabled: Whether the button is disabled or not.
flat: Enable alternative flat look buttons.
Returns:
A [`Button`][textual.widgets.Button] widget of the 'warning'
[variant][textual.widgets.button.ButtonVariant].
"""
return Button(
label=label,
variant="warning",
name=name,
id=id,
classes=classes,
disabled=disabled,
flat=flat,
)
@classmethod
def error(
cls,
label: ContentText | None = None,
*,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
flat: bool = False,
) -> Button:
"""Utility constructor for creating an error Button variant.
Args:
label: The text that appears within the button.
name: The name of the button.
id: The ID of the button in the DOM.
classes: The CSS classes of the button.
disabled: Whether the button is disabled or not.
flat: Enable alternative flat look buttons.
Returns:
A [`Button`][textual.widgets.Button] widget of the 'error'
[variant][textual.widgets.button.ButtonVariant].
"""
return Button(
label=label,
variant="error",
name=name,
id=id,
classes=classes,
disabled=disabled,
flat=flat,
)