class Button(Child):
- def __init__(self, parent, rect, value, callback, is_active=False):
+ def __init__(self, parent, rect, value, callback, highlight=False):
super().__init__(parent)
self.rect = rect
self.value = value
self.callback = callback
- self.is_active = is_active
+ self.highlight = highlight
self.pushed = False
def draw_value(self, color):
def draw(self):
if not self.pushed:
- value_color = "lime" if self.is_active else "gray"
+ value_color = "lime" if self.highlight else "gray"
colors = ("black", value_color, value_color)
else:
colors = ("darkgray", "lightgray", "black")
class Child(EventMethodDispatcher):
- def __init__(self, parent):
- self._parent = parent
- if parent is not None:
- parent.children.append(self)
-
- @property
- def parent(self):
- return self._parent
-
- @parent.setter
- def parent(self, parent):
- if self._parent is not None:
- self._parent.children.remove(self)
- self._parent = parent
- if parent is not None:
- parent.children.append(self)
- if "root" in self.__dict__:
- self.__dict__.pop("root")
+ def __init__(self, parent, enabled=True):
+ self.parent = parent
+ parent.children.append(self)
+ self.enabled = enabled
@cached_property
def root(self):
from .button import Button
from .modal import Modal
-from .parent import Parent
class DropDownMenu(Modal):
class DropDown(Button):
- def __init__(self, parent, rect, value, entries, callback, is_active=False):
+ def __init__(self, parent, rect, value, entries, callback, highlight=False):
self.dropdown_menu = DropDownMenu(parent, rect, entries, callback)
- super().__init__(parent, rect, value, self.dropdown_menu.activate, is_active)
+ super().__init__(parent, rect, value, self.dropdown_menu.activate, highlight)
-import pygame
+from .root import Root
-class FocusRootMixin:
- surf: pygame.Surface
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.focus_stack: list[FocusableMixin | FocusRootMixin] = [self]
-
- @property
- def active(self):
- return self.focus_stack[-1] is self
-
- def handle_event(self, ev):
- focused = self.focus_stack[-1]
- (super() if focused is self else focused).handle_event(ev)
-
-
-class FocusableMixin:
- root: FocusRootMixin
+class Focusable:
+ root: Root
+ dirty: bool
@property
- def active(self):
+ def focused(self):
return self.root.focus_stack[-1] is self
def activate(self):
- focus_stack = self.root.focus_stack
- assert self not in focus_stack
- focus_stack.append(self)
+ assert self not in self.root.focus_stack
+ self.root.focus_stack.append(self)
self.dirty = True
def deactivate(self):
- focus_stack = self.root.focus_stack
- assert self.active
- focus_stack.pop()
+ assert self.focused
+ self.root.focus_stack.pop()
self.dirty = True
if self.pushed:
pygame.draw.rect(self.surf, "honeydew4", self.rect)
self.shape.draw(self.surf, "black" if self.pushed else "white")
- if self.is_active:
+ if self.highlight:
color = "lime"
elif self.pushed:
color = "red"
import pygame
from .child import Child
-from .focus import FocusableMixin
+from .focus import Focusable
from .parent import Parent
-class Modal(FocusableMixin, Parent, Child):
+class Modal(Focusable, Parent, Child):
def __init__(self, parent):
- super().__init__(parent)
- self.draw = self._check_active(self.draw)
- self.update = self._check_active(self.update)
- self.handle_event = self._check_active(self.handle_event)
- self.activate = self._check_active(self.activate, False)
- self.deactivate = self._check_active(self.deactivate)
+ super().__init__(parent, False)
+ self.draw = self._check_enabled(self.draw)
+ self.update = self._check_enabled(self.update)
+ self.handle_event = self._check_enabled(self.handle_event)
+ self.activate = self._check_enabled(self.activate, False)
+ self.deactivate = self._check_enabled(self.deactivate)
- def _check_active(self, method, cmp=True):
+ def _check_enabled(self, method, cmp=True):
def inner(*args, **kwargs):
- if self.active is cmp:
+ if self.enabled is cmp:
return method(*args, **kwargs)
return None
return inner
def activate(self):
super().activate()
+ self.enabled = True
self.root.stop_event = True
self.dirty = True
def deactivate(self):
super().deactivate()
+ self.enabled = False
self.root.stop_event = True
self.dirty = True
if not self.running or self.stop_event:
return
for child in self.children:
+ if not child.enabled:
+ continue
child.handle_event(ev)
if not self.running or self.stop_event:
break
def update(self):
+ s = super()
+ if hasattr(s, "update"):
+ s.update()
for child in self.children:
- child.update()
+ if child.enabled:
+ child.update()
def draw(self):
+ s = super()
+ if hasattr(s, "draw"):
+ s.draw()
for child in self.children:
- child.draw()
+ if child.enabled:
+ child.draw()
import pygame
from .parent import Parent
-from .focus import FocusRootMixin
-class Root(FocusRootMixin, Parent):
+class Root(Parent):
BACKGROUND_COLOR: pygame.Color
def __init__(self, surf, font=None):
+ from .child import Child
+
super().__init__()
self.font = font
self.running = True
self.clock = pygame.time.Clock()
self.stop_event = False
self.root = self
+ self.focus_stack: list[Root | Child] = [self]
def handle_quit(self, _=None):
self.running = False
KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: handle_quit}}
+ @property
+ def focused(self):
+ return self.focus_stack[-1] is self
+
def handle_event(self, ev):
if ev.type in (pygame.WINDOWEXPOSED, pygame.ACTIVEEVENT):
self.dirty = True
return
- super().handle_event(ev)
+ focused = self.focus_stack[-1]
+ if focused is self:
+ focused = super()
+ focused.handle_event(ev)
def draw(self):
if hasattr(self, "BACKGROUND_COLOR"):
DELAY_MS = 500
REPEAT_MS = 100
- def __init__(self, parent, rect, value, callback, is_active=False):
+ def __init__(self, parent, rect, value, callback, highlight=False):
self._pushed = False
- super().__init__(parent, rect, value, callback, is_active)
+ super().__init__(parent, rect, value, callback, highlight)
self.repeat_ts = None
@property
class TabBar(Parent, Child):
- def __init__(self, parent, rect, labels, groups, active):
+ def __init__(self, parent, rect, labels, groups, current_tab):
super().__init__(parent)
self.labels = labels
self.groups = groups
- self.active = active
+ self.current_tab = current_tab
num_labels = len(labels)
self.buttons = [
Button(
def update_children(self, i=None):
if i is not None:
- if self.active == i:
+ if self.current_tab == i:
return
- self.active = i
+ self.current_tab = i
for i, (button, group) in enumerate(zip(self.buttons, self.groups)):
- is_group_active = i == self.active
- if button.is_active != is_group_active:
- button.is_active = is_group_active
+ is_current_tab = i == self.current_tab
+ if button.highlight != is_current_tab:
+ button.highlight = is_current_tab
self.dirty = True
for item in group:
if item.parent is not self:
item.parent = self
- is_child_active = item in self.children
- if is_group_active == is_child_active:
+ is_child_enabled = item.enabled
+ if is_current_tab == is_child_enabled:
continue
- if is_group_active:
- self.children.append(item)
- elif is_child_active:
- self.children.remove(item)
+ if is_current_tab:
+ item.enabled = True
+ elif is_child_enabled:
+ item.enabled = False
self.dirty = True
assert len(self.children) == len(set(self.children))
import pygame
from .child import Child
-from .focus import FocusableMixin
+from .focus import Focusable
@dataclass
return pos
-class TextInput(FocusableMixin, Child):
+class TextInput(Focusable, Child):
def __init__(self, parent, rect, callback, value="", value_filter=None):
super().__init__(parent)
self.rect = rect
def draw(self):
pygame.draw.rect(self.surf, "black", self.rect)
- if self.active:
+ if self.focused:
fs = self.get_font_surface()
else:
fs = self.font.render(self.value, True, "gray")
self.cursor.pos = self.pos_from_offset(
ev.pos[0] - self.rect.left - 16 + self.offset
)
- elif self.active:
+ elif self.focused:
self.deactivate(True)
@contextmanager
self.cursor.pos = pos
def activate(self):
- if self.active:
+ if self.focused:
return
super().activate()
self.cursor = Cursor(self.value)
def deactivate(self, restore=False):
- if not self.active:
+ if not self.focused:
return
if restore:
self.value = self.cursor.old_value
}
def handle_keydown(self, ev):
- if not self.active:
+ if not self.focused:
return
if (key_method := self.get_key_method(ev.key, ev.mod)) is not None:
key_callback = key_method
return
with self.check_dirty():
key_callback()
- if self.active:
+ if self.focused:
self.cursor.press_key(key_callback, ev.key)
def handle_keyup(self, ev):
for device_type, value in settings_per_device_type.items():
if value is not None:
self.xinput_conf.update_by_type(device_type, value)
- self.single_button.is_active = self.xrandr_conf.is_active("single")
- self.double_button.is_active = self.xrandr_conf.is_active("double")
- self.vertical_button.is_active = self.xrandr_conf.is_active("vertical")
+ self.single_button.highlight = self.xrandr_conf.is_active("single")
+ self.double_button.highlight = self.xrandr_conf.is_active("double")
+ self.vertical_button.highlight = self.xrandr_conf.is_active("vertical")
self.dirty = True
def quit(self):