# Released under the MIT License. See LICENSE for details.
#
"""Routes for the doc-ui test domain."""
from __future__ import annotations # Docs-generation hack.
from enum import Enum
from dataclasses import dataclass
from typing import TYPE_CHECKING, Annotated, override
from efro.dataclassio import ioprepped, IOAttrs
import bacommon.docui.v2 as dui2
from bacommon.docui.routes import (
DocUIRoute,
DocUILocalActionBase,
DocUIState,
PopupRoute,
family_members,
)
if TYPE_CHECKING:
import bacommon.docui.v2
[docs]
class TestRoute(DocUIRoute):
"""Family class for doc-ui test routes."""
[docs]
@override
@classmethod
def get_route_types(cls) -> tuple[type[DocUIRoute], ...]:
return family_members(AnyTestRoute)
[docs]
@override
@classmethod
def get_window_layout(cls) -> dui2.WindowLayout:
# Test pages are dense showcases built for the full-size
# window.
return dui2.WindowLayout.LARGE
[docs]
class ThemeVariant(Enum):
"""A look test pages can show their content in.
The page-themes test pages show each; the root page's 'Layout Test
Options' pick one for its 'Layout Tests' pages.
"""
DEFAULT = 'default'
POPUP_LOOK = 'popup_look'
BUTTON_COLOR = 'button_color'
ROLE_COLORS = 'role_colors'
TIGHT = 'tight'
ROOMY = 'roomy'
DARK = 'dark'
TEXT_STYLES = 'text_styles'
[docs]
@ioprepped
@dataclass
class Root(TestRoute, path='/'):
"""The root test page."""
#: Draw bounds and other debug bits.
debug: bool = False
#: Have the response include some client-effects.
test_effects: bool = False
#: Have the response include a local-action.
test_action: bool = False
[docs]
@ioprepped
@dataclass
class RootState(DocUIState, state_id='docuitest.root'):
"""State for the root test page."""
#: The window layout picked in its 'Layout Test Options' (for rows
#: below to open their pages at), and at viewer layouts which side
#: the viewer pane sits on (None for no pane).
layout: dui2.WindowLayout = dui2.WindowLayout.WIDE_TALL
viewer_side: dui2.ViewerSide | None = None
#: The theme picked in its 'Layout Test Options' (for rows below to
#: show their pages in).
theme: ThemeVariant = ThemeVariant.DEFAULT
[docs]
@ioprepped
@dataclass
class Test2(TestRoute, path='/test2'):
"""A second simple page."""
[docs]
@ioprepped
@dataclass
class Slow(TestRoute, path='/slow'):
"""A page that takes a while to load."""
[docs]
@ioprepped
@dataclass
class TimedActions(TestRoute, path='/timedactions'):
"""A page that counts down via timed-actions and then closes."""
val: int = 5
[docs]
@ioprepped
@dataclass
class DisplayItems(TestRoute, path='/displayitems'):
"""Display-item tests."""
debug: bool = False
[docs]
@ioprepped
@dataclass
class TextImages(TestRoute, path='/textimages'):
"""Text-with-images tests."""
debug: bool = False
[docs]
@ioprepped
@dataclass
class LiveTimes(TestRoute, path='/livetimes'):
"""Time-varying text (countdowns, elapsed times) that ticks itself."""
[docs]
@ioprepped
@dataclass
class TextWrapping(TestRoute, path='/textwrapping'):
"""Titles, subtitles and footnotes word-wrapped to their columns."""
debug: bool = False
[docs]
@ioprepped
@dataclass
class Animation(TestRoute, path='/animation'):
"""Decorations animated by keyframe client-effects."""
[docs]
@ioprepped
@dataclass
class Depictions(TestRoute, path='/depictions'):
"""Depiction tests."""
debug: bool = False
[docs]
@ioprepped
@dataclass
class Viewers(TestRoute, path='/viewers'):
"""Live viewer tests: good cases and bad data, one shown at a time.
The page's own controls switch cases, and the pane's style and
side (page state), in place.
"""
#: Which case the viewer pane shows (a test-page case name; unknown
#: names show the first).
case: str = ''
[docs]
@override
@classmethod
def get_window_layout(cls) -> dui2.WindowLayout:
return dui2.WindowLayout.VIEWER
[docs]
@ioprepped
@dataclass
class Names(TestRoute, path='/names'):
"""Name depiction tests (basic and capsule forms)."""
debug: bool = False
[docs]
@ioprepped
@dataclass
class NinePatch(TestRoute, path='/ninepatch'):
"""9-patch image tests (stretched and tile-fit, tinted)."""
debug: bool = False
[docs]
@ioprepped
@dataclass
class Glyphs(TestRoute, path='/glyphs'):
"""Custom glyphs against their measured bounds and anchor points."""
[docs]
@ioprepped
@dataclass
class GlyphsState(DocUIState, state_id='docuitest.glyphs'):
"""What the glyphs test page draws over its glyphs."""
crosshairs: bool = True
bounds: bool = True
[docs]
@ioprepped
@dataclass
class EmptyPage(TestRoute, path='/emptypage'):
"""A page with nothing on it."""
[docs]
@ioprepped
@dataclass
class BoundsTests(TestRoute, path='/boundstests'):
"""Button-style bounds tests."""
[docs]
class ControlRowKind(Enum):
"""Kinds of control row (for :class:`NavTest`)."""
CHECKBOX = 'checkbox'
TEXT_INPUT = 'text'
CHOICE = 'choice'
SLIDER = 'slider'
NUMBER = 'number'
COLOR = 'color'
BUTTON = 'button'
[docs]
@ioprepped
@dataclass
class NavTest(TestRoute, path='/navtest'):
"""A button row, then one control row of some kind as the last row.
For checking directional navigation on control rows: down from the
last row should reach the toolbars, left should reach the back
button, and right from a row's rightmost control should do nothing.
"""
kind: ControlRowKind = ControlRowKind.CHECKBOX
[docs]
@ioprepped
@dataclass
class Drill(TestRoute, path='/drill'):
"""The top of a drill-down; shows what was found at the bottom.
Exploring opens levels in windows of their own, several deep;
picking something at the bottom closes all of them at once and
hands the pick back here (see
:meth:`~bacommon.docui.routes.DocUIRoute.close_to`).
"""
[docs]
@ioprepped
@dataclass
class DrillLevel(TestRoute, path='/drill/level'):
"""One level of the drill-down, each in a window of its own."""
#: How deep this is (1 is the first level below the top).
depth: int = 1
#: Doors taken to get here, in order.
trail: str = ''
[docs]
@ioprepped
@dataclass
class DrillState(DocUIState, state_id='docuitest.drill'):
"""State for the drill-down's top page."""
#: What was picked at the bottom, handed back up (empty for
#: nothing yet).
found: str = ''
[docs]
@ioprepped
@dataclass
class PopupsMain(TestRoute, PopupRoute[PopupsState], path='/popups/main'):
"""The main test popup: acts on its parent every way there is."""
[docs]
@override
@classmethod
def get_popup_size(cls) -> tuple[float, float]:
# Lots to show.
return (600.0, 400.0)
[docs]
@ioprepped
@dataclass
class PopupsMainState(DocUIState, state_id='docuitest.popupsmain'):
"""The main test popup's own state (apart from its parent's)."""
#: Bumped by the popup replacing its own page.
bumps: int = 0
[docs]
@ioprepped
@dataclass
class ThemesState(DocUIState, state_id='docuitest.themes'):
"""State for the page-themes test pages (and their popups)."""
#: Backs the sample checkbox.
checked: bool = False
[docs]
@ioprepped
@dataclass
class Themes(TestRoute, path='/themes'):
"""Page themes: the same content shown with various look values."""
variant: ThemeVariant = ThemeVariant.DEFAULT
[docs]
@override
@classmethod
def get_window_layout(cls) -> dui2.WindowLayout:
# A column, so insets and spacing are easy to compare.
return dui2.WindowLayout.WIDE_TALL
[docs]
@ioprepped
@dataclass
class Sections(TestRoute, path='/sections'):
"""Sections: headings, notes, backings and spacing between them."""
debug: bool = False
#: See :attr:`WindowLayouts.viewer_side`.
viewer_side: dui2.ViewerSide | None = None
#: See :attr:`WindowLayouts.theme`.
theme: ThemeVariant = ThemeVariant.DEFAULT
[docs]
@override
@classmethod
def get_window_layout(cls) -> dui2.WindowLayout:
# A column, like the settings pages sections mostly live in.
return dui2.WindowLayout.SMALL_TALLER
[docs]
@ioprepped
@dataclass
class WindowLayouts(TestRoute, path='/windowlayouts'):
"""Assorted rows for judging window layouts (browse to it at each).
Control rows, titles and button rows at every alignment, and long
rows, which in a narrow column scroll within it.
"""
debug: bool = False
#: The layout we're being browsed to at, for the page to name (the
#: browse sets the actual layout; this is just for show).
layout: dui2.WindowLayout | None = None
#: Which side the page's viewer pane (a turning punch powerup box)
#: sits on at viewer layouts, or None for a page with no viewer
#: (its rows then span the window). Ignored at other layouts.
viewer_side: dui2.ViewerSide | None = None
#: The theme to show the page in (its frame colors come with the
#: link that opens it).
theme: ThemeVariant = ThemeVariant.DEFAULT
[docs]
@ioprepped
@dataclass
class LayoutFit(TestRoute, path='/layoutfit'):
"""A row exactly as big as a layout promises (browse at that layout).
``layout`` says which layout it's being shown in. The row fills the
content rect the layout promises (see
:data:`bacommon.docui.v2.SMALL_CONTENT_WIDTH` and friends), less the
page's insets, and should never scroll; with ``over`` /
``wide_over`` set it's a hair taller / wider, which should scroll
where the promise is exact (and may not where it's only a minimum).
Viewer layouts show a blank viewer pane, as their promises are for
a page beside one.
"""
over: bool = False
wide_over: bool = False
layout: dui2.WindowLayout = dui2.WindowLayout.WIDE_TALL
#: See :attr:`WindowLayouts.theme`. (The page keeps its own
#: vertical inset, which its row's size is figured from.)
theme: ThemeVariant = ThemeVariant.DEFAULT
[docs]
class Flavor(Enum):
"""Something to pick from on the widgets test page."""
VANILLA = 'v'
CHOCOLATE = 'c'
STRAWBERRY = 's'
MINT = 'm'
[docs]
class Size(Enum):
"""Something else to pick from on the widgets test page."""
S = 's'
M = 'm'
L = 'l'
XL = 'x'
[docs]
@ioprepped
@dataclass
class WebTestGet(TestRoute, path='/webtest/get'):
"""A page fetched from a web server via GET."""
[docs]
@ioprepped
@dataclass
class WebTestPost(
TestRoute, path='/webtest/post', method=dui2.RequestMethod.POST
):
"""A page fetched from a web server via POST."""
[docs]
@ioprepped
@dataclass
class CloudMsgTestGet(TestRoute, path='/cloudmsgtest/get'):
"""A page fetched through our cloud connection via GET."""
[docs]
@ioprepped
@dataclass
class CloudMsgTestPost(
TestRoute, path='/cloudmsgtest/post', method=dui2.RequestMethod.POST
):
"""A page fetched through our cloud connection via POST."""
# All routes in the family. Handlers can match against this with
# assert_never to be sure they cover everything.
AnyTestRoute = (
Root
| Test2
| Slow
| TimedActions
| DisplayItems
| TextImages
| LiveTimes
| TextWrapping
| Animation
| Depictions
| Viewers
| Names
| NinePatch
| Glyphs
| EmptyPage
| BoundsTests
| Widgets
| NavTest
| Drill
| DrillLevel
| Popups
| PopupsMain
| PopupsOther
| Themes
| ThemesPopup
| Sections
| ButtonGrids
| WindowLayouts
| LayoutFit
| WebTestGet
| WebTestPost
| CloudMsgTestGet
| CloudMsgTestPost
)
[docs]
class TestLocalAction(DocUILocalActionBase):
"""Family class for doc-ui test local-actions."""
[docs]
@override
@classmethod
def get_action_types(cls) -> tuple[type[DocUILocalActionBase], ...]:
return family_members(AnyTestLocalAction)
[docs]
@ioprepped
@dataclass
class TestAction(TestLocalAction, name='testaction'):
"""Show a message proving we got here."""
testparam: int
# All local-actions in the family (just the one for now).
[docs]
@ioprepped
@dataclass
class ShowVolume(TestLocalAction, name='showvolume'):
"""Show the volume slider's live value (fired while dragging)."""
AnyTestLocalAction = TestAction | ShowVolume
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self, TypeIs)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum