Repository: Textualize/textual

Issue:
Activated button animations
The Button widget should have a visual indicator that it was clicked. Material buttons do this. The reasoning is that if an action doesn't immediately update the UI, the user tends to click again to make sure it works.

I'm thinking a brief pulse animation, or 500ms flash. Something subtly enough to not be irritating, but still convey the active state.

this may be achieved by adding an `-active` class to the Button to allow a dev to define the active animation.


Discussion:
"Fried pulse animation"? 😄
I think I meant "brief" 🤷‍♂️ 

Resolving patch:
diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py
--- a/src/textual/widgets/_button.py
+++ b/src/textual/widgets/_button.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import sys
+from functools import partial
 from typing import cast
 
 if sys.version_info >= (3, 8):
@@ -41,6 +42,8 @@ class Button(Widget, can_focus=True):
         margin: 1 0;
         align: center middle;
         text-style: bold;
+
+        transition: background 0.1;/* for "active" effect */
     }
 
     Button:hover {
@@ -49,6 +52,10 @@ class Button(Widget, can_focus=True):
         border: tall $primary-lighten-1;
     }
 
+    Button.-active {
+        background: $primary-lighten-1;
+    }
+
     .-dark-mode Button {
         background: $background;
         color: $primary-lighten-2;
@@ -59,6 +66,10 @@ class Button(Widget, can_focus=True):
         background: $surface;
     }
 
+    .-dark-mode Button.-active {
+        background: $background-lighten-3;
+    }
+
     /* Success variant */
     Button.-success {
         background: $success;
@@ -72,6 +83,10 @@ class Button(Widget, can_focus=True):
         border: tall $success-lighten-2;
     }
 
+    Button.-success.-active {
+        background: $success-lighten-1;
+    }
+
     .-dark-mode Button.-success {
         background: $success;
         color: $text-success;
@@ -84,6 +99,10 @@ class Button(Widget, can_focus=True):
         border: tall $success-lighten-3;
     }
 
+    .-dark-mode Button.-success.-active {
+        background: $success-lighten-1;
+    }
+
     /* Warning variant */
     Button.-warning {
         background: $warning;
@@ -97,6 +116,10 @@ class Button(Widget, can_focus=True):
         border: tall $warning-lighten-3;
     }
 
+    Button.-warning.-active {
+        background: $warning;
+    }
+
     .-dark-mode Button.-warning {
         background: $warning;
         color: $text-warning;
@@ -109,6 +132,10 @@ class Button(Widget, can_focus=True):
         border: tall $warning-lighten-3;
     }
 
+    .-dark-mode Button.-warning.-active {
+        background: $warning-lighten-1;
+    }
+
     /* Error variant */
     Button.-error {
         background: $error;
@@ -122,6 +149,10 @@ class Button(Widget, can_focus=True):
         border: tall $error-lighten-3;
     }
 
+    Button.-error.-active {
+        background: $error;
+    }
+
     .-dark-mode Button.-error {
         background: $error;
         color: $text-error;
@@ -134,11 +165,18 @@ class Button(Widget, can_focus=True):
         border: tall $error-lighten-3;
     }
 
+    .-dark-mode Button.-error.-active {
+        background: $error;
+    }
+
     App.-show-focus Button:focus {
         tint: $accent 20%;
     }
     """
 
+    ACTIVE_EFFECT_DURATION = 0.3
+    """When buttons are clicked they get the `-active` class for this duration (in seconds)"""
+
     class Pressed(Message, bubble=True):
         @property
         def button(self) -> Button:
@@ -199,8 +237,15 @@ def render(self) -> RenderableType:
 
     async def on_click(self, event: events.Click) -> None:
         event.stop()
-        if not self.disabled:
-            await self.emit(Button.Pressed(self))
+        if self.disabled:
+            return
+        # Manage the "active" effect:
+        self.add_class("-active")
+        self.set_timer(
+            self.ACTIVE_EFFECT_DURATION, partial(self.remove_class, "-active")
+        )
+        # ...and let other components know that we've just been clicked:
+        await self.emit(Button.Pressed(self))
 
     async def on_key(self, event: events.Key) -> None:
         if event.key == "enter" and not self.disabled:


Test patch: