"""BottomControlsStrip: light-grey hint bar pinned to the bottom of the screen.
Solitaire, Casual Crusade, Claustrowordia, and Tiny Yurts all hand-rolled the
same widget: a 32–40 px light-grey rectangle pinned to the bottom edge, listing
"DRAG X · PRESS Y · …" hint segments separated by middle-dots, with the active
hint optionally bolded. This ships that widget as a first-class Control.
Example::
strip = BottomControlsStrip(hints=["DRAG cards onto piles", "DOUBLE-CLICK to auto-play", "ESC menu"])
strip.place_bottom_strip(height=32) # see Control.place_bottom_strip
root.add_child(strip)
"""
from __future__ import annotations
from ..descriptors import Property
from ..math.types import Vec2
from .core import Control
__all__ = ["BottomControlsStrip"]
[docs]
class BottomControlsStrip(Control):
"""Pinned hint strip rendering "SEG · SEG · …" centred over a light-grey bg.
Properties
----------
hints
Tuple of short hint strings. Each segment is rendered separated by a
middle-dot glyph. Re-assignment redraws.
font_size
Text size in px (default 13).
text_colour
RGBA tuple for the hint text (default near-black).
bg_colour
RGBA tuple for the strip background (default light grey ``#E6E6E6``).
Layout
------
The strip is a normal :class:`Control`: pair it with
:meth:`Control.place_bottom_strip` (or its top-strip companion) for the
canonical anchored placement.
"""
_draw_caching = True
font_size = Property(13.0, range=(8, 32), hint="Hint text size")
separator = Property(" \u00b7 ", hint="Inter-segment glyph (middle-dot by default)")
def __init__(
self,
hints: list[str] | tuple[str, ...] = (),
*,
bg_colour: tuple[float, float, float, float] = (0.90, 0.90, 0.92, 1.0),
text_colour: tuple[float, float, float, float] = (0.10, 0.10, 0.12, 1.0),
**kwargs,
):
super().__init__(**kwargs)
self._hints: tuple[str, ...] = tuple(hints)
self._bg_colour = tuple(bg_colour)
self._text_colour = tuple(text_colour)
self.size = Vec2(self.size_x, 32.0)
# --- properties --------------------------------------------------------
@property
def hints(self) -> tuple[str, ...]:
return self._hints
[docs]
@hints.setter
def hints(self, value):
self._hints = tuple(value)
self.queue_redraw()
@property
def bg_colour(self):
return self._bg_colour
[docs]
@bg_colour.setter
def bg_colour(self, value):
from ..properties import Colour
coerced = Colour.coerce(value, name="BottomControlsStrip.bg_colour")
if coerced is None:
# The strip renders directly to ``draw_rect`` and has no theme key
# to fall back on, so ``None`` cannot be resolved. Fail loudly per
# the engine-wide "raw draw callsites resolve or fail loudly" rule.
raise TypeError(
"BottomControlsStrip.bg_colour does not support None: it has no theme fallback; "
"assign an RGBA tuple instead."
)
self._bg_colour = coerced
self.queue_redraw()
@property
def text_colour(self):
return self._text_colour
[docs]
@text_colour.setter
def text_colour(self, value):
from ..properties import Colour
coerced = Colour.coerce(value, name="BottomControlsStrip.text_colour")
if coerced is None:
# See ``bg_colour`` setter: no theme fallback, fail loudly.
raise TypeError(
"BottomControlsStrip.text_colour does not support None: it has no theme fallback; "
"assign an RGBA tuple instead."
)
self._text_colour = coerced
self.queue_redraw()
# --- rendering ---------------------------------------------------------
[docs]
def on_draw(self, renderer):
x, y, w, h = self.get_global_rect()
renderer.draw_rect((x, y), (w, h), colour=self._bg_colour, filled=True)
if not self._hints:
return
text = self.separator.join(self._hints)
scale = self.font_size / 14.0
text_w = renderer.text_width(text, scale)
text_x = x + (w - text_w) / 2
text_y = y + (h - self.font_size) / 2
renderer.draw_text(text, (text_x, text_y), colour=self._text_colour, scale=scale)
[docs]
def get_minimum_size(self) -> Vec2:
return Vec2(max(self.min_size.x, 80.0), max(self.min_size.y, 24.0))