Skip to content

Commit

Permalink
ButtonState and ToggleState enums are now literal types.
Browse files Browse the repository at this point in the history
Added "disallowed" state to ButtonState.
Added theming for disallowed buttons and menu items.
  • Loading branch information
salt-die committed Mar 4, 2024
1 parent 7355bf3 commit 38c43da
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 288 deletions.
18 changes: 11 additions & 7 deletions examples/advanced/figfonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from batgrl.app import App
from batgrl.colors import BLACK, RED, WHITE, gradient, lerp_colors
from batgrl.figfont import FIGFont
from batgrl.gadgets.behaviors.button_behavior import ButtonBehavior, ButtonState
from batgrl.gadgets.behaviors.button_behavior import ButtonBehavior
from batgrl.gadgets.text import Text

ASSETS = Path(__file__).parent.parent / "assets"
Expand Down Expand Up @@ -73,24 +73,26 @@ async def _drip(self):

class TextButton(ButtonBehavior, Text):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._dark_red = lerp_colors(RED, BLACK, 0.35)
self._color_task = None
self._grad = gradient(WHITE, RED, 20)
self._dark_red = lerp_colors(RED, BLACK, 0.35)
self._i = 0
super().__init__(**kwargs)

def on_add(self):
self._color_task = asyncio.create_task(self._to_white())
super().on_add()

def on_remove(self):
self._color_task.cancel()
if self._color_task is not None:
self._color_task.cancel()
super().on_remove()

async def _to_red(self):
self.canvas["fg_color"] = self._grad[self._i]
while self._i < len(self._grad) - 1:
self._i += 1
if self.state is not ButtonState.DOWN:
if self.button_state != "down":
self.canvas["fg_color"] = self._grad[self._i]
await asyncio.sleep(0.01)

Expand All @@ -102,12 +104,14 @@ async def _to_white(self):
await asyncio.sleep(0.01)

def update_hover(self):
self._color_task.cancel()
if self._color_task is not None:
self._color_task.cancel()
self._color_task = asyncio.create_task(self._to_red())
self.canvas["fg_color"] = self._grad[self._i]

def update_normal(self):
self._color_task.cancel()
if self._color_task is not None:
self._color_task.cancel()
self._color_task = asyncio.create_task(self._to_white())
self.canvas["fg_color"] = self._grad[self._i]

Expand Down
20 changes: 4 additions & 16 deletions examples/basic/buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from batgrl.gadgets.flat_toggle import FlatToggle
from batgrl.gadgets.grid_layout import GridLayout
from batgrl.gadgets.text import Text
from batgrl.gadgets.toggle_button import ToggleButton, ToggleState
from batgrl.gadgets.toggle_button import ToggleButton


class ButtonApp(App):
Expand All @@ -19,7 +19,7 @@ def callback():

def toggle_button_callback(i):
def callback(state):
prefix = "un" if state is ToggleState.OFF else ""
prefix = "un" if state == "off" else ""
display.add_str(f"Button {i + 1} {prefix}toggled!".ljust(20))

return callback
Expand All @@ -36,13 +36,10 @@ def callback(state):
horizontal_spacing=1,
)

# Buttons
grid_layout.add_gadgets(
Button(size=(1, 10), label=f"Button {i + 1}", callback=button_callback(i))
for i in range(5)
)

# Independent toggle buttons
grid_layout.add_gadgets(
ToggleButton(
size=(1, 12),
Expand All @@ -51,18 +48,15 @@ def callback(state):
)
for i in range(5, 10)
)

# Grouped radio buttons
grid_layout.add_gadgets(
ToggleButton(
size=(1, 12),
group="a",
group=0,
label=f"Button {i + 1}",
callback=toggle_button_callback(i),
)
for i in range(10, 15)
)

grid_layout.size = grid_layout.minimum_grid_size

flat_grid = GridLayout(
Expand All @@ -72,20 +66,14 @@ def callback(state):
orientation="lr-tb",
horizontal_spacing=1,
)

# Independent flat toggles
flat_grid.add_gadgets(
FlatToggle(callback=toggle_button_callback(i)) for i in range(15, 20)
)

# Grouped flat toggles
flat_grid.add_gadgets(
FlatToggle(group="b", callback=toggle_button_callback(i))
FlatToggle(group=1, callback=toggle_button_callback(i))
for i in range(20, 25)
)

flat_grid.size = flat_grid.minimum_grid_size

self.add_gadgets(display, grid_layout, flat_grid)


Expand Down
2 changes: 1 addition & 1 deletion examples/basic/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def inner(toggle_state):
)
)

self.children[-2].children[1].item_disabled = True
self.children[-2].children[1].button_state = "disallowed"


if __name__ == "__main__":
Expand Down
3 changes: 2 additions & 1 deletion src/batgrl/colors/color_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,10 @@ class ColorTheme(TypedDict, total=False):
button_normal: ColorPair
button_hover: ColorPair
button_press: ColorPair
button_disallowed: ColorPair
menu_item_hover: ColorPair
menu_item_selected: ColorPair
menu_item_disabled: ColorPair
menu_item_disallowed: ColorPair
titlebar_normal: ColorPair
titlebar_inactive: ColorPair
data_table_sort_indicator: ColorPair
Expand Down
3 changes: 2 additions & 1 deletion src/batgrl/colors/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,10 @@
"button_normal": {"fg": "dde4ed", "bg": "2a3ca0"},
"button_hover": {"fg": "fff0f6", "bg": "3248c0"},
"button_press": {"fg": "fff0f6", "bg": "c4a219"},
"button_disallowed": {"fg": "272b40", "bg": "070c25"},
"menu_item_hover": {"fg": "f2babc", "bg": "111834"},
"menu_item_selected": {"fg": "ecf3ff", "bg": "1b244b"},
"menu_item_disabled": {"fg": "272b40", "bg": "070c25"},
"menu_item_disallowed": {"fg": "272b40", "bg": "070c25"},
"titlebar_normal": {"fg": "ffe0df", "bg": "070c25"},
"titlebar_inactive": {"fg": "7d6b71", "bg": "070c25"},
"data_table_sort_indicator": {"fg": "ecf3ff", "bg": "070c25"},
Expand Down
100 changes: 52 additions & 48 deletions src/batgrl/gadgets/behaviors/button_behavior.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
"""Button behavior for a gadget."""
from enum import Enum
from typing import Literal

from ...io import MouseEventType

__all__ = ["ButtonState", "ButtonBehavior"]


class ButtonState(str, Enum):
"""
State of a button gadget.
:class:`ButtonState` is one of "normal", "hover", "down".
"""

NORMAL = "normal"
HOVER = "hover"
DOWN = "down"
ButtonState = Literal["normal", "hover", "down", "disallowed"]
"""Button behavior states."""


class ButtonBehavior:
"""
Button behavior for a gadget.
A button has three states: 'normal', 'hover', and 'down'.
A button has four states: "normal", "hover", "down", and "disallowed".
When a button's state changes one of the following methods are called:
:meth:`update_normal`, :meth:`update_hover`, and :meth:`update_down`.
- :meth:`update_normal`
- :meth:`update_hover`
- :meth:`update_down`
- :meth:`update_disallowed`
When a button is released, the :meth:`on_release` method is called.
Expand All @@ -38,74 +32,84 @@ class ButtonBehavior:
----------
always_release : bool
Whether a mouse up event outside the button will trigger it.
state : ButtonState
Current button state. One of `NORMAL`, `HOVER`, `DOWN`.
button_state : ButtonState
Current button state.
Methods
-------
on_release()
Triggered when a button is released.
update_normal()
Paint the normal state.
update_hover()
Paint the hover state.
update_down()
Paint the down state.
on_release()
Triggered when a button is released.
update_disallowed()
Paint the disallowed state.
"""

def __init__(self, *, always_release: bool = False, **kwargs):
super().__init__(**kwargs)
self.always_release = always_release
self.state = ButtonState.NORMAL

def on_add(self):
"""Paint normal state."""
super().on_add()
self._normal()

def _normal(self):
self.state = ButtonState.NORMAL
self.update_normal()

def _hover(self):
self.state = ButtonState.HOVER
self.update_hover()

def _down(self):
self.state = ButtonState.DOWN
self.update_down()

def on_mouse(self, mouse_event):
self.button_state: ButtonState = "normal"

@property
def button_state(self) -> ButtonState:
"""Current button state."""
return self._button_state

@button_state.setter
def button_state(self, button_state: ButtonState):
dispatch = {
"normal": self.update_normal,
"hover": self.update_hover,
"down": self.update_down,
"disallowed": self.update_disallowed,
}
if button_state not in dispatch:
button_state = "normal"

self._button_state = button_state
dispatch[button_state]()

def on_mouse(self, mouse_event) -> bool | None:
"""Determine button state from mouse event."""
if self.button_state == "disallowed":
return False

if super().on_mouse(mouse_event):
return True

collides = self.collides_point(mouse_event.position)

if mouse_event.event_type is MouseEventType.MOUSE_DOWN:
if collides:
self._down()
self.button_state = "down"
return True

elif (
mouse_event.event_type is MouseEventType.MOUSE_UP
and self.state is ButtonState.DOWN
and self.button_state == "down"
):
if collides:
self._hover()
self.on_release()
self.button_state = "hover"
return True

self._normal()
self.button_state = "normal"

if self.always_release:
self.on_release()
return True

if not collides and self.state is ButtonState.HOVER:
self._normal()
elif collides and self.state is ButtonState.NORMAL:
self._hover()
if not collides and self.button_state == "hover":
self.button_state = "normal"
elif collides and self.button_state == "normal":
self.button_state = "hover"

def on_release(self):
"""Triggered when button is released."""

def update_normal(self):
"""Paint the normal state."""
Expand All @@ -116,5 +120,5 @@ def update_hover(self):
def update_down(self):
"""Paint the down state."""

def on_release(self):
"""Triggered when button is released."""
def update_disallowed(self):
"""Paint the disallowed state."""
Loading

0 comments on commit 38c43da

Please sign in to comment.