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

271 lines
8.3 KiB
Python

"""Provides the base code and implementations of toggle widgets.
In particular it provides `Checkbox`, `RadioButton` and `RadioSet`.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar
from rich.console import RenderableType
from textual.binding import Binding, BindingType
from textual.content import Content, ContentText
from textual.events import Click
from textual.geometry import Size
from textual.message import Message
from textual.reactive import reactive
from textual.style import Style
from textual.widgets._static import Static
if TYPE_CHECKING:
from typing_extensions import Self
class ToggleButton(Static, can_focus=True):
"""Base toggle button widget.
Warning:
`ToggleButton` should be considered to be an internal class; it
exists to serve as the common core of [Checkbox][textual.widgets.Checkbox] and
[RadioButton][textual.widgets.RadioButton].
"""
ALLOW_SELECT = False
BINDINGS: ClassVar[list[BindingType]] = [
Binding("enter,space", "toggle_button", "Toggle", show=False),
]
"""
| Key(s) | Description |
| :- | :- |
| enter, space | Toggle the value. |
"""
COMPONENT_CLASSES: ClassVar[set[str]] = {
"toggle--button",
"toggle--label",
}
"""
| Class | Description |
| :- | :- |
| `toggle--button` | Targets the toggle button itself. |
| `toggle--label` | Targets the text label of the toggle button. |
"""
DEFAULT_CSS = """
ToggleButton {
width: auto;
border: tall $border-blurred;
padding: 0 1;
background: $surface;
text-wrap: nowrap;
text-overflow: ellipsis;
&.-textual-compact {
border: none !important;
padding: 0;
&:focus {
border: tall $border;
background-tint: $foreground 5%;
& > .toggle--label {
color: $block-cursor-foreground;
background: $block-cursor-background;
text-style: $block-cursor-text-style;
}
}
}
& > .toggle--button {
color: $panel-darken-2;
background: $panel;
}
&.-on > .toggle--button {
color: $text-success;
background: $panel;
}
&:focus {
border: tall $border;
background-tint: $foreground 5%;
& > .toggle--label {
color: $block-cursor-foreground;
background: $block-cursor-background;
text-style: $block-cursor-text-style;
}
}
&:blur:hover {
& > .toggle--label {
background: $block-hover-background;
}
}
}
"""
BUTTON_LEFT: str = ""
"""The character used for the left side of the toggle button."""
BUTTON_INNER: str = "X"
"""The character used for the inside of the button."""
BUTTON_RIGHT: str = ""
"""The character used for the right side of the toggle button."""
value: reactive[bool] = reactive(False, init=False)
"""The value of the button. `True` for on, `False` for off."""
compact: reactive[bool] = reactive(False, toggle_class="-textual-compact")
"""Enable compact display?"""
def __init__(
self,
label: ContentText = "",
value: bool = False,
button_first: bool = True,
*,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
tooltip: RenderableType | None = None,
compact: bool = False,
) -> None:
"""Initialise the toggle.
Args:
label: The label for the toggle.
value: The initial value of the toggle.
button_first: Should the button come before the label, or after?
name: The name of the toggle.
id: The ID of the toggle in the DOM.
classes: The CSS classes of the toggle.
disabled: Whether the button is disabled or not.
tooltip: RenderableType | None = None,
compact: Show a compact button.
"""
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
self._button_first = button_first
# NOTE: Don't send a Changed message in response to the initial set.
with self.prevent(self.Changed):
self.value = value
self._label = self._make_label(label)
if tooltip is not None:
self.tooltip = tooltip
self.compact = compact
def _make_label(self, label: ContentText) -> Content:
"""Make label content.
Args:
label: The source value for the label.
Returns:
A `Content` rendering of the label for use in the button.
"""
label = Content.from_text(label).first_line.rstrip()
return label
@property
def label(self) -> Content:
"""The label associated with the button."""
return self._label
@label.setter
def label(self, label: ContentText) -> None:
self._label = self._make_label(label)
self.refresh(layout=True)
@property
def _button(self) -> Content:
"""The button, reflecting the current value."""
# Grab the button style.
button_style = self.get_visual_style("toggle--button")
# Building the style for the side characters. Note that this is
# sensitive to the type of character used, so pay attention to
# BUTTON_LEFT and BUTTON_RIGHT.
side_style = Style(
foreground=button_style.background,
background=self.background_colors[1],
)
return Content.assemble(
(self.BUTTON_LEFT, side_style),
(self.BUTTON_INNER, button_style),
(self.BUTTON_RIGHT, side_style),
)
def render(self) -> Content:
"""Render the content of the widget.
Returns:
The content to render for the widget.
"""
button = self._button
label_style = self.get_visual_style("toggle--label")
label = self._label.pad(1, 1).stylize_before(label_style)
if self._button_first:
content = Content.assemble(button, label)
else:
content = Content.assemble(label, button)
return content
def get_content_width(self, container: Size, viewport: Size) -> int:
return (
self._button.get_optimal_width(self.styles, 0)
+ (2 if self._label else 0)
+ self._label.get_optimal_width(self.styles, 0)
)
def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
return 1
def toggle(self) -> Self:
"""Toggle the value of the widget.
Returns:
The `ToggleButton` instance.
"""
self.value = not self.value
return self
def action_toggle_button(self) -> None:
"""Toggle the value of the widget when called as an action.
This would normally be used for a keyboard binding.
"""
self.toggle()
async def _on_click(self, _: Click) -> None:
"""Toggle the value of the widget when clicked with the mouse."""
self.toggle()
class Changed(Message):
"""Posted when the value of the toggle button changes."""
def __init__(self, toggle_button: ToggleButton, value: bool) -> None:
"""Initialise the message.
Args:
toggle_button: The toggle button sending the message.
value: The value of the toggle button.
"""
super().__init__()
self._toggle_button = toggle_button
"""A reference to the toggle button that was changed."""
self.value = value
"""The value of the toggle button after the change."""
def watch_value(self) -> None:
"""React to the value being changed.
When triggered, the CSS class `-on` is applied to the widget if
`value` has become `True`, or it is removed if it has become
`False`. Subsequently a related `Changed` event will be posted.
"""
self.set_class(self.value, "-on")
self.post_message(self.Changed(self, self.value))