diff --git a/sandbox/buttons.css b/sandbox/buttons.css index 77ff5c379..cedf20ded 100644 --- a/sandbox/buttons.css +++ b/sandbox/buttons.css @@ -1,12 +1,4 @@ -#foo { - text-style: underline; - background: rebeccapurple; -} - -*:focus { - tint: yellow 50%; -} - -#foo:hover { - background: greenyellow; +Button { + padding-left: 1; + padding-right: 1; } diff --git a/sandbox/buttons.py b/sandbox/buttons.py index c90fe6b53..4fd49ccb9 100644 --- a/sandbox/buttons.py +++ b/sandbox/buttons.py @@ -1,21 +1,30 @@ +from textual import layout, events from textual.app import App, ComposeResult - from textual.widgets import Button -from textual import layout class ButtonsApp(App[str]): def compose(self) -> ComposeResult: yield layout.Vertical( - Button("foo", id="foo"), - Button("bar", id="bar"), - Button("baz", id="baz"), + Button("default", id="foo"), + Button.success("success", id="bar"), + Button.warning("warning", id="baz"), + Button.error("error", id="baz"), ) def handle_pressed(self, event: Button.Pressed) -> None: self.app.bell() - self.log("pressed", event.button.id) - self.exit(event.button.id) + print("pressed", event.button.id) + + async def on_key(self, event: events.Key) -> None: + await self.dispatch_key(event) + + def key_a(self): + print(f"text-success: {self.stylesheet.variables.get('text-success')}") + print( + f"text-success-darken-1: {self.stylesheet.variables.get('text-success-darken-1')}" + ) + self.dark = not self.dark app = ButtonsApp( diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index ca766e2ef..e733f77dd 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -416,7 +416,6 @@ class MessagePump: Args: event (events.Key): A key event. """ - key_method = getattr(self, f"key_{event.key}", None) if key_method is not None: if await invoke(key_method, event): diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index d93ebd4b6..075b7972b 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -1,51 +1,130 @@ from __future__ import annotations -from typing import cast +from typing import cast, Literal from rich.console import RenderableType -from rich.style import Style from rich.text import Text, TextType from .. import events +from ..css._error_tools import friendly_list from ..message import Message from ..reactive import Reactive from ..widget import Widget +ButtonVariant = Literal["default", "success", "warning", "error"] +_VALID_BUTTON_VARIANTS = {"default", "success", "warning", "error"} + + +class InvalidButtonVariant(Exception): + pass + class Button(Widget, can_focus=True): """A simple clickable button.""" CSS = """ - - Button { width: auto; height: 3; + background: $primary; color: $text-primary; - content-align: center middle; border: tall $primary-lighten-3; - + + content-align: center middle; margin: 1 0; align: center middle; text-style: bold; } + Button:hover { + background: $primary-darken-2; + color: $text-primary-darken-2; + border: tall $primary-lighten-1; + } + .-dark-mode Button { - border: tall white $primary-lighten-2; - color: $primary-lighten-2; background: $background; + color: $primary-lighten-2; + border: tall white $primary-lighten-2; } .-dark-mode Button:hover { background: $surface; } + /* Success variant */ + Button.-success { + background: $success; + color: $text-success; + border: tall $success-lighten-3; + } - Button:hover { - background:$primary-darken-2; - color: $text-primary-darken-2; - border: tall $primary-lighten-1; + Button.-success:hover { + background: $success-darken-1; + color: $text-success-darken-1; + border: tall $success-lighten-2; /* TODO: This shouldn't be necessary?? */ + } + + .-dark-mode Button.-success { + background: $success; + color: $text-success; + border: tall $success-lighten-3; + } + + .-dark-mode Button.-success:hover { + background: $success-darken-1; + color: $text-success-darken-1; + border: tall $success-lighten-3; + } + + /* Warning variant */ + Button.-warning { + background: $warning; + color: $text-warning; + border: tall $warning-lighten-3; + } + + Button.-warning:hover { + background: $warning-darken-1; + color: $text-warning-darken-1; + border: tall $warning-lighten-3; + } + + .-dark-mode Button.-warning { + background: $warning; + color: $text-warning; + border: tall $warning-lighten-3; + } + + .-dark-mode Button.-warning:hover { + background: $warning-darken-1; + color: $text-warning-darken-1; + border: tall $warning-lighten-3; + } + + Button.-error { + background: $error; + color: $text-error; + border: tall $error-lighten-3; + } + + Button.-error:hover { + background: $error-darken-1; + color: $text-error-darken-1; + border: tall $error-lighten-3; + } + + .-dark-mode Button.-error { + background: $error; + color: $text-error; + border: tall $error-lighten-3; + } + + .-dark-mode Button.-error:hover { + background: $error-darken-1; + color: $text-error-darken-1; + border: tall $error-lighten-3; } App.-show-focus Button:focus { @@ -63,11 +142,22 @@ class Button(Widget, can_focus=True): self, label: TextType | None = None, disabled: bool = False, + variant: ButtonVariant = "default", *, name: str | None = None, id: str | None = None, classes: str | None = None, ): + """Create a Button widget. + + Args: + label (str): The text that appears within the button. + disabled (bool): Whether the button is disabled or not. + variant (ButtonVariant): 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. + """ super().__init__(name=name, id=id, classes=classes) if label is None: @@ -79,6 +169,15 @@ class Button(Widget, can_focus=True): if disabled: self.add_class("-disabled") + if variant in _VALID_BUTTON_VARIANTS: + if variant != "default": + self.add_class(f"-{variant}") + + else: + raise InvalidButtonVariant( + f"Valid button variants are {friendly_list(_VALID_BUTTON_VARIANTS)}" + ) + label: Reactive[RenderableType] = Reactive("") def validate_label(self, label: RenderableType) -> RenderableType: @@ -100,3 +199,93 @@ class Button(Widget, can_focus=True): async def on_key(self, event: events.Key) -> None: if event.key == "enter" and not self.disabled: await self.emit(Button.Pressed(self)) + + @staticmethod + def success( + label: TextType | None = None, + disabled: bool = False, + *, + name: str | None = None, + id: str | None = None, + classes: str | None = None, + ) -> Button: + """Utility constructor for creating a success Button variant. + + Args: + label (str): The text that appears within the button. + disabled (bool): Whether the button is disabled or not. + name: The name of the button. + id: The ID of the button in the DOM. + classes: The CSS classes of the button. + + Returns: + Button: A Button widget of the 'success' variant. + """ + return Button( + label=label, + disabled=disabled, + variant="success", + name=name, + id=id, + classes=classes, + ) + + @staticmethod + def warning( + label: TextType | None = None, + disabled: bool = False, + *, + name: str | None = None, + id: str | None = None, + classes: str | None = None, + ) -> Button: + """Utility constructor for creating a warning Button variant. + + Args: + label (str): The text that appears within the button. + disabled (bool): Whether the button is disabled or not. + name: The name of the button. + id: The ID of the button in the DOM. + classes: The CSS classes of the button. + + Returns: + Button: A Button widget of the 'warning' variant. + """ + return Button( + label=label, + disabled=disabled, + variant="warning", + name=name, + id=id, + classes=classes, + ) + + @staticmethod + def error( + label: TextType | None = None, + disabled: bool = False, + *, + name: str | None = None, + id: str | None = None, + classes: str | None = None, + ) -> Button: + """Utility constructor for creating an error Button variant. + + Args: + label (str): The text that appears within the button. + disabled (bool): Whether the button is disabled or not. + name: The name of the button. + id: The ID of the button in the DOM. + classes: The CSS classes of the button. + + Returns: + Button: A Button widget of the 'error' variant. + """ + return Button( + label=label, + disabled=disabled, + variant="error", + name=name, + id=id, + classes=classes, + )