flat buttons

This commit is contained in:
Will McGugan
2025-09-01 17:52:06 +01:00
parent f59e89fca9
commit 44129f0e4b
7 changed files with 186 additions and 85 deletions

View File

@@ -24,6 +24,22 @@ class ButtonsApp(App[str]):
Button.warning("Warning!", disabled=True),
Button.error("Error!", disabled=True),
),
VerticalScroll(
Static("Flat Buttons", classes="header"),
Button("Default", flat=True),
Button("Primary!", variant="primary", flat=True),
Button.success("Success!", flat=True),
Button.warning("Warning!", flat=True),
Button.error("Error!", flat=True),
),
VerticalScroll(
Static("Disabled Flat Buttons", classes="header"),
Button("Default", disabled=True, flat=True),
Button("Primary!", variant="primary", disabled=True, flat=True),
Button.success("Success!", disabled=True, flat=True),
Button.warning("Warning!", disabled=True, flat=True),
Button.error("Error!", disabled=True, flat=True),
),
)
def on_button_pressed(self, event: Button.Pressed) -> None:

View File

@@ -87,6 +87,11 @@ BORDER_CHARS: dict[
("", " ", ""),
("", "", ""),
),
"block": (
("", "", ""),
("", " ", ""),
("", "", ""),
),
"hkey": (
("", "", ""),
(" ", " ", " "),
@@ -190,6 +195,11 @@ BORDER_LOCATIONS: dict[
(0, 0, 0),
(0, 0, 0),
),
"block": (
(1, 1, 1),
(0, 0, 0),
(1, 1, 1),
),
"hkey": (
(0, 0, 0),
(0, 0, 0),

View File

@@ -41,6 +41,7 @@ from typing import (
Generic,
Iterable,
Iterator,
Mapping,
NamedTuple,
Sequence,
TextIO,
@@ -3969,12 +3970,17 @@ class App(Generic[ReturnType], DOMNode):
)
def _parse_action(
self, action: str | ActionParseResult, default_namespace: DOMNode
self,
action: str | ActionParseResult,
default_namespace: DOMNode,
namespaces: Mapping[str, DOMNode] | None = None,
) -> tuple[DOMNode, str, tuple[object, ...]]:
"""Parse an action.
Args:
action: An action string.
default_namespace: Namespace to user when none is supplied in the action.
namespaces: Mapping of namespaces.
Raises:
ActionError: If there are any errors parsing the action string.
@@ -3987,8 +3993,10 @@ class App(Generic[ReturnType], DOMNode):
else:
destination, action_name, params = actions.parse(action)
action_target: DOMNode | None = None
if destination:
action_target: DOMNode | None = (
None if namespaces is None else namespaces.get(destination)
)
if destination and action_target is None:
if destination not in self._action_targets:
raise ActionError(f"Action namespace {destination} is not known")
action_target = getattr(self, destination, None)
@@ -4021,6 +4029,7 @@ class App(Generic[ReturnType], DOMNode):
self,
action: str | ActionParseResult,
default_namespace: DOMNode | None = None,
namespaces: Mapping[str, DOMNode] | None = None,
) -> bool:
"""Perform an [action](/guide/actions).
@@ -4030,12 +4039,13 @@ class App(Generic[ReturnType], DOMNode):
action: Action encoded in a string.
default_namespace: Namespace to use if not provided in the action,
or None to use app.
namespaces: Mapping of namespaces.
Returns:
True if the event has been handled.
"""
action_target, action_name, params = self._parse_action(
action, self if default_namespace is None else default_namespace
action, self if default_namespace is None else default_namespace, namespaces
)
if action_target.check_action(action_name, params):
return await self._dispatch_action(action_target, action_name, params)

View File

@@ -24,6 +24,7 @@ VALID_BORDER: Final = {
"tall",
"tab",
"thick",
"block",
"vkey",
"wide",
}

View File

@@ -16,6 +16,7 @@ EdgeType = Literal[
"round",
"solid",
"thick",
"block",
"double",
"dashed",
"heavy",

View File

@@ -19,6 +19,7 @@ from typing import (
Collection,
Generator,
Iterable,
Mapping,
NamedTuple,
Sequence,
TypeVar,
@@ -4304,13 +4305,16 @@ class Widget(DOMNode):
self._layout_cache[cache_key] = visual
return visual
async def run_action(self, action: str) -> None:
async def run_action(
self, action: str, namespaces: Mapping[str, DOMNode] | None = None
) -> None:
"""Perform a given action, with this widget as the default namespace.
Args:
action: Action encoded as a string.
namespaces: Mapping of namespaces.
"""
await self.app.run_action(action, self)
await self.app.run_action(action, self, namespaces)
def post_message(self, message: Message) -> bool:
"""Post a message to this widget.

View File

@@ -50,109 +50,149 @@ class Button(Widget, can_focus=True):
Button {
width: auto;
min-width: 16;
height: auto;
color: $button-foreground;
background: $surface;
border: none;
border-top: tall $surface-lighten-1;
border-bottom: tall $surface-darken-1;
height:auto;
line-pad: 1;
text-align: center;
content-align: center middle;
text-style: bold;
line-pad: 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 {
&.-style-flat {
color: auto 90%;
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;
border: block $surface;
&:hover {
background: $primary-darken-2;
border-top: tall $primary;
opacity: 90%;
}
&:focus {
text-style: $button-focus-text-style;
}
&.-active {
background: $surface;
border: block $surface;
tint: $background 30%;
}
&.-primary {
background: $primary-muted;
border: block $primary-muted;
}
&.-success {
background: $success-muted;
border: block $success-muted;
}
&.-warning {
background: $warning-muted;
border: block $warning-muted;
}
&.-error {
background: $error-muted;
border: block $error-muted;
}
}
&.-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.4;
}
&: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-bottom: tall $primary-lighten-3;
border-top: tall $primary-darken-3;
}
}
border-top: tall $primary-lighten-3;
border-bottom: 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: $primary-darken-2;
border-top: tall $primary;
}
&:hover {
background: $success-darken-2;
border-top: tall $success;
&.-active {
background: $primary;
border-bottom: tall $primary-lighten-3;
border-top: tall $primary-darken-3;
}
}
&.-active {
&.-success {
color: $button-color-foreground;
background: $success;
border-bottom: tall $success-lighten-2;
border-top: tall $success-darken-2;
}
}
border-top: tall $success-lighten-2;
border-bottom: tall $success-darken-3;
&.-warning{
color: $button-color-foreground;
background: $warning;
border-top: tall $warning-lighten-2;
border-bottom: tall $warning-darken-3;
&:hover {
background: $success-darken-2;
border-top: tall $success;
}
&:hover {
background: $warning-darken-2;
border-top: tall $warning;
&.-active {
background: $success;
border-bottom: tall $success-lighten-2;
border-top: tall $success-darken-2;
}
}
&.-active {
&.-warning{
color: $button-color-foreground;
background: $warning;
border-bottom: tall $warning-lighten-2;
border-top: tall $warning-darken-2;
}
}
border-top: tall $warning-lighten-2;
border-bottom: tall $warning-darken-3;
&.-error {
color: $button-color-foreground;
background: $error;
border-top: tall $error-lighten-2;
border-bottom: tall $error-darken-3;
&:hover {
background: $warning-darken-2;
border-top: tall $warning;
}
&:hover {
background: $error-darken-1;
border-top: tall $error;
&.-active {
background: $warning;
border-bottom: tall $warning-lighten-2;
border-top: tall $warning-darken-2;
}
}
&.-active {
&.-error {
color: $button-color-foreground;
background: $error;
border-bottom: tall $error-lighten-2;
border-top: tall $error-darken-2;
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;
}
}
}
}
@@ -169,6 +209,9 @@ class Button(Widget, can_focus=True):
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.
@@ -201,6 +244,7 @@ class Button(Widget, can_focus=True):
tooltip: RenderableType | None = None,
action: str | None = None,
compact: bool = False,
flat: bool = False,
):
"""Create a Button widget.
@@ -214,6 +258,7 @@ class Button(Widget, can_focus=True):
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)
@@ -224,6 +269,7 @@ class Button(Widget, can_focus=True):
self.variant = variant
self.action = action
self.compact = compact
self.flat = flat
self.active_effect_duration = 0.2
"""Amount of time in seconds the button 'press' animation lasts."""
@@ -253,6 +299,10 @@ class Button(Widget, can_focus=True):
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)
@@ -314,6 +364,7 @@ class Button(Widget, can_focus=True):
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
flat: bool = False,
) -> Button:
"""Utility constructor for creating a success Button variant.
@@ -324,6 +375,7 @@ class Button(Widget, can_focus=True):
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'
@@ -336,6 +388,7 @@ class Button(Widget, can_focus=True):
id=id,
classes=classes,
disabled=disabled,
flat=flat,
)
@classmethod
@@ -347,6 +400,7 @@ class Button(Widget, can_focus=True):
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
flat: bool = False,
) -> Button:
"""Utility constructor for creating a warning Button variant.
@@ -357,6 +411,7 @@ class Button(Widget, can_focus=True):
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'
@@ -369,6 +424,7 @@ class Button(Widget, can_focus=True):
id=id,
classes=classes,
disabled=disabled,
flat=flat,
)
@classmethod
@@ -380,6 +436,7 @@ class Button(Widget, can_focus=True):
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
flat: bool = False,
) -> Button:
"""Utility constructor for creating an error Button variant.
@@ -390,6 +447,7 @@ class Button(Widget, can_focus=True):
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'
@@ -402,4 +460,5 @@ class Button(Widget, can_focus=True):
id=id,
classes=classes,
disabled=disabled,
flat=flat,
)