# Released under the MIT License. See LICENSE for details.
#
"""Doc-ui test page for judging window layouts.
The root test page browses here at each
:class:`bacommon.docui.v2.WindowLayout`; the page itself is the same
either way. It carries the things layouts stress: control rows (label to
value distance), titles and short button rows at every alignment, and
long rows, which in a narrow column scroll within it.
"""
from __future__ import annotations # Docs-generation hack.
from dataclasses import replace
from typing import TYPE_CHECKING, NamedTuple, assert_never
from bacommon.langstr import LangStrSpecValue
import bacommon.depiction as bdep
import bacommon.docui.v2 as dui2
if TYPE_CHECKING:
import bauiv1
import bacommon.docui.routes.docuitest
from bacommon.langstr import LangStrSpec
def _layout_description(layout: dui2.WindowLayout) -> str:
"""What a layout is for (shown on the page after its name)."""
# pylint: disable=too-many-return-statements
# (Other layouts are named by their enum names, so renames follow.)
wl = dui2.WindowLayout
match layout:
case dui2.WindowLayout.SMALL:
return (
'Use this layout for small dialogs or bits of content you'
' want to frame tightly. If you are showing enough to need'
' to scroll here, you should choose a bigger layout. This'
' layout provides the same narrow horizontal space at all'
' ui scales, so content can be tailored to fit it.'
)
case dui2.WindowLayout.SMALL_TALL:
return (
f'Has the same fixed width as {wl.SMALL.name} at all ui'
' scales, but has a larger fixed height on medium/large ui'
' scales. A good step up once content no longer fits on'
f' {wl.SMALL.name} without scrolling.'
)
case dui2.WindowLayout.SMALL_TALLER:
return (
f'Has the same fixed width as {wl.SMALL.name} at all ui'
' scales, but has a variable height that grows to fit your'
' screen on'
' medium/large ui scales. Use this if you are showing lots'
' of content where scrolling will always be necessary.'
)
case dui2.WindowLayout.WIDE:
return (
'Use this layout to show content designed to fill a'
' standard phone shaped screen area. This provides a fixed'
' width and mostly fixed height (possibly more on phone'
' screens) so content can be tailored to fit it.'
)
case dui2.WindowLayout.WIDE_TALL:
return (
f'Provides the same fixed width as {wl.WIDE.name}, but a'
' larger fixed amount of vertical space on medium and'
' large ui scales.'
)
case dui2.WindowLayout.WIDE_TALLER:
return (
f'Provides the same fixed width as {wl.WIDE.name}, but with'
' a height that expands to take advantage of all screen'
' sizes. Use this when you expect to have a lot of'
' scrollable wide content.'
)
case dui2.WindowLayout.WIDER:
return (
'This layout provides the same mostly-fixed height as'
f' {wl.WIDE.name}, but expands to take advantage of your'
' full screen width at all ui scales. Use this when showing'
' large amounts of horizontally scrollable content.'
)
case dui2.WindowLayout.LARGE:
return (
'This layout expands to use most of your screen area in all'
' ui scales, so use it only when you can actually fill that'
' space on a large screen. Good for things like stores and'
' inventories where there might be lots of vertical and'
' horizontal scrolling happening.'
)
case dui2.WindowLayout.VIEWER_NARROW:
return (
f'Like {wl.VIEWER.name}, but with a controls column two'
' thirds as wide, giving the viewer more of the room (and a'
' somewhat narrower window at medium and large ui scales).'
' Use this when a page has just a few simple controls'
' beside a big view.'
)
case dui2.WindowLayout.VIEWER:
return (
f'Provides the same fixed width as {wl.SMALL.name}, but with'
' a viewer alongside for showing content, on whichever side'
' the page asks for. A page with no viewer gets no pane:'
' its rows span the whole window instead.'
)
case _:
assert_never(layout)
def _lit(text: str) -> LangStrSpecValue:
# A dev-only page, so baked literals throughout.
return LangStrSpecValue.literal(text)
def layout_test_decos(debug: bool) -> list[bacommon.docui.v2.Decoration]:
"""Decorations for the test root page's layout-test buttons.
:meta private:
"""
from bauiv1 import _classicassets, _docuiv2testassets
strs = _docuiv2testassets.strings
return [
dui2.Image(
texture=_classicassets.textures.powerup_punch,
position=(-70, 0),
size=(40, 40),
h_align=dui2.HAlign.LEFT,
),
dui2.Image(
texture=_classicassets.textures.powerup_speed,
position=(0, 75),
size=(35, 35),
v_align=dui2.VAlign.TOP,
),
dui2.Text(
text=strs.layout.corner_tl.spec,
position=(-70, 75),
size=(50, 50),
h_align=dui2.HAlign.LEFT,
v_align=dui2.VAlign.TOP,
debug=debug,
),
dui2.Text(
text=strs.layout.corner_tr.spec,
position=(70, 75),
size=(50, 50),
h_align=dui2.HAlign.RIGHT,
v_align=dui2.VAlign.TOP,
debug=debug,
),
dui2.Text(
text=strs.layout.corner_bl.spec,
position=(-70, -75),
size=(50, 50),
h_align=dui2.HAlign.LEFT,
v_align=dui2.VAlign.BOTTOM,
debug=debug,
),
dui2.Text(
text=strs.layout.corner_br.spec,
position=(70, -75),
size=(50, 50),
h_align=dui2.HAlign.RIGHT,
v_align=dui2.VAlign.BOTTOM,
debug=debug,
),
]
def with_test_viewer(
viewer_side: dui2.ViewerSide | None,
theme: bacommon.docui.routes.docuitest.ThemeVariant,
*,
title: LangStrSpecValue,
rows: list[dui2.Row],
state: dict | None = None,
) -> dui2.Page:
"""A test page in ``theme``, with the test viewer on ``viewer_side``.
The viewer is a turning punch powerup box over a slightly darkened
background; at viewer layouts it sits on the given side, or with
``viewer_side`` None there's no viewer (the rows span the window).
Shared by the pages the root's 'Layout Tests' row opens, so each
looks the same at a picked side and theme. Other layouts ignore the
viewer.
:meta private:
"""
from bauiv1lib.docuitestthemes import themed_page
viewer: bdep.Depiction | None = None
side = dui2.ViewerSide.LEFT
if viewer_side is not None:
from bauiv1 import _classicassets
viewer = bdep.MeshViewerDepiction(
entries=[
bdep.MeshViewerEntry(
mesh=_classicassets.meshes.powerup,
texture=_classicassets.textures.powerup_punch,
)
]
)
side = viewer_side
return themed_page(
dui2.Page(
title=title,
state=state,
rows=rows,
viewer=viewer,
viewer_side=side,
# Mostly the window showing through, a touch darkened (the
# viewers page's 'Dark 25%').
viewer_background_color=(0.0, 0.0, 0.0, 0.25),
),
theme,
)
def _buttons(count: int) -> list[dui2.Button]:
return [
dui2.Button(
label=_lit(str(i + 1)),
size=(120, 80),
style=dui2.ButtonStyle.SQUARE,
)
for i in range(count)
]
[docs]
def test_page_window_layouts(
route: bacommon.docui.routes.docuitest.WindowLayouts,
) -> bacommon.docui.v2.Response:
"""Assorted rows for eyeballing a window layout."""
# pylint: disable=cyclic-import
import bacommon.docui.routes.docuitest as rt
from bauiv1lib.docuitestwidgets import _flavor_label
wstate = rt.WidgetTestState
arrived = route.get_state(wstate)
state = wstate() if arrived is None else arrived
debug = route.debug
left, center, right = (
dui2.HAlign.LEFT,
dui2.HAlign.CENTER,
dui2.HAlign.RIGHT,
)
def _rows(
title: str,
count: int,
align: dui2.HAlign,
footnote: str | None = None,
) -> list[dui2.Row]:
return [
dui2.TitleRow(_lit(title)),
dui2.ButtonRow(
title_align=align,
content_align=align,
debug=debug,
buttons=_buttons(count),
),
*([] if footnote is None else [dui2.FootnoteRow(_lit(footnote))]),
]
return dui2.Response(
page=with_test_viewer(
route.viewer_side,
route.theme,
title=_lit('Window Layouts'),
state=state.encode(),
rows=[
# Info about the layout: a row-less section with just a
# subtitle, which joins the heading below it in latching
# onto the first selectable row (no note: there's no
# selectable row above for one to latch onto).
*(
[]
if route.layout is None
else [
dui2.Section(
subtitle=_lit(
route.layout.name.replace('_', ' ').title()
+ ' - '
+ _layout_description(route.layout)
),
rows=[],
)
]
),
# A section of input rows, with a heading and a closing
# note (selecting its first row brings the heading into
# view; its last, the note).
dui2.Section(
title=_lit('Inputs'),
subtitle=_lit('A section heading and its subtitle.'),
footnote=_lit('A section note, under its last row.'),
debug=debug,
rows=[
# A row title under a section heading: the
# heading is larger, so the two read as levels.
dui2.TitleRow(_lit('A row title')),
wstate.checkbox_row(
lambda s: s.plain,
label=_lit('A checkbox'),
debug=debug,
),
wstate.choice_row(
lambda s: s.flavor,
choice_label=_flavor_label,
label=_lit('A choice'),
debug=debug,
),
dui2.FootnoteRow(_lit('Footnote under a control row.')),
wstate.slider_row(
lambda s: s.volume,
min_value=0.0,
max_value=1.0,
increment=0.05,
as_percent=True,
label=_lit('A slider'),
debug=debug,
),
wstate.text_input_row(
lambda s: s.text_medium,
label=_lit('A text input'),
debug=debug,
),
],
),
# A centered heading, with a header band, over the
# button rows.
dui2.Section(
title=_lit('Button Rows'),
title_align=center,
header_height=30.0,
header_decorations_center=[
dui2.Text(
text=_lit('(a header band)'),
position=(0.0, 0.0),
size=(300.0, 30.0),
scale=0.6,
color=(0.6, 0.74, 0.6, 1.0),
)
],
debug=debug,
rows=[
*_rows('Short row (left)', 3, left),
*_rows('Short row (center)', 3, center),
*_rows(
'Short row (right)',
3,
right,
footnote='Right-aligned footnote.',
),
*_rows(
'Long row (left)',
16,
left,
footnote=(
'Starts at the left; scrolls within its'
' area.'
),
),
*_rows('Long row (center)', 16, center),
*_rows(
'Long row (right)',
16,
right,
footnote=(
'Ends at the right; scrolls within its' ' area.'
),
),
],
),
dui2.ButtonRow(
center_content=True,
buttons=[
dui2.Button(
label=_lit('Hide Debug' if debug else 'Show Debug'),
size=(200, 60),
style=dui2.ButtonStyle.MEDIUM,
action=replace(route, debug=not debug).replace(),
),
],
),
],
)
)
class _Promise(NamedTuple):
"""A content-rect size a layout promises, along one axis."""
size: float
#: Exactly this (1 over scrolls) rather than at least this.
exact: bool
def _promised_width(layout: dui2.WindowLayout) -> _Promise | None:
"""The content-rect width a layout promises (at every ui-scale).
Viewer layouts' are for a page beside its viewer pane. None for
LARGE, which promises nothing: it gets most of the screen, whatever
that turns out to be.
"""
wl = dui2.WindowLayout
match layout:
case wl.SMALL | wl.SMALL_TALL | wl.SMALL_TALLER | wl.VIEWER:
return _Promise(dui2.SMALL_CONTENT_WIDTH, True)
case wl.VIEWER_NARROW:
return _Promise(dui2.NARROW_CONTENT_WIDTH, True)
case wl.WIDE | wl.WIDE_TALL | wl.WIDE_TALLER:
return _Promise(dui2.WIDE_CONTENT_WIDTH, True)
case wl.WIDER:
# (Follows the screen.)
return _Promise(dui2.WIDE_CONTENT_WIDTH, False)
case wl.LARGE:
return None
case _:
assert_never(layout)
def _promised_height(
layout: dui2.WindowLayout, small_ui: bool
) -> _Promise | None:
"""The content-rect height a layout promises at a ui-scale.
At small ui-scale windows fill the screen: wide and wider pages get
exactly theirs on the shortest screens (more on taller ones), and
the tall and taller layouts are just their base layouts. None for
LARGE (see :func:`_promised_width`).
"""
# pylint: disable=too-many-return-statements
wl = dui2.WindowLayout
if layout is wl.LARGE:
return None
if small_ui:
match layout:
case wl.WIDE | wl.WIDER:
return _Promise(dui2.WIDE_CONTENT_HEIGHT, True)
case wl.SMALL:
return _Promise(dui2.SMALL_CONTENT_HEIGHT, False)
case (
wl.SMALL_TALL
| wl.SMALL_TALLER
| wl.WIDE_TALL
| wl.WIDE_TALLER
| wl.VIEWER
| wl.VIEWER_NARROW
):
return _Promise(dui2.WIDE_CONTENT_HEIGHT, False)
case _:
assert_never(layout)
match layout:
case wl.SMALL:
return _Promise(dui2.SMALL_CONTENT_HEIGHT, True)
case wl.SMALL_TALL | wl.WIDE_TALL:
return _Promise(dui2.TALL_CONTENT_HEIGHT, True)
case wl.SMALL_TALLER | wl.WIDE_TALLER:
# (Follows the screen.)
return _Promise(dui2.TALL_CONTENT_HEIGHT, False)
case wl.WIDE | wl.WIDER:
return _Promise(dui2.WIDE_CONTENT_HEIGHT, True)
case wl.VIEWER | wl.VIEWER_NARROW:
return _Promise(dui2.WIDE_CONTENT_HEIGHT, False)
case _:
assert_never(layout)
#: The fit test page's vertical inset (the default, spelled out: its
#: rows get the promised content height less twice this).
_FIT_V_INSET = dui2.DEFAULT_V_INSET
def _fit_themed(
page: bacommon.docui.v2.Page,
theme: bacommon.docui.routes.docuitest.ThemeVariant,
) -> bacommon.docui.v2.Page:
"""A fit page in a theme, keeping the vertical inset it's sized by."""
from bauiv1lib.docuitestthemes import themed_page
return replace(themed_page(page, theme), v_inset=page.v_inset)
def _no_promise_page(
layout: dui2.WindowLayout,
uiscale: bauiv1.UIScale,
actual: tuple[float, float],
theme: bacommon.docui.routes.docuitest.ThemeVariant,
) -> bacommon.docui.v2.Response:
"""The fit page for a layout promising no size (LARGE).
Nothing to test: just what it says it is and what it got.
"""
return dui2.Response(
page=_fit_themed(
dui2.Page(
title=_lit(_layout_title(layout) + ' Fit'),
v_inset=_FIT_V_INSET,
rows=[
dui2.TitleRow(_lit('No promised size'), attach=False),
dui2.SubtitleRow(
_lit(
'Large makes no guarantees about space: it just'
' gets most of the screen, whatever that turns'
' out to be. Pages wanting a known size should'
' pick another layout.'
),
attach=False,
),
dui2.ButtonRow(
layout=dui2.ButtonRowLayout.FILL,
debug=True,
buttons=[
dui2.Button(
label=_lit(
f'This window gets {actual[0]:.1f}'
f' x {actual[1]:.1f}'
f' ({uiscale.name.lower()} ui-scale)'
),
label_scale=0.7,
size=(300.0, 80.0),
# (Wide art; it spans the row.)
style=dui2.ButtonStyle.LARGE,
action=dui2.Local(),
)
],
),
],
),
theme,
)
)
def test_page_layout_fit(
route: bacommon.docui.routes.docuitest.LayoutFit,
) -> bacommon.docui.v2.Response:
"""A row exactly as big as a layout promises its pages.
:meta private:
"""
from bauiv1lib.docui._frame import window_frames
from bauiv1lib.docui._layout import BUTTON_ROW_EDGE_INSET
from bauiv1lib.docui._panes import layout_has_viewer_pane
import bauiv1 as bui
layout = route.layout
viewer = layout_has_viewer_pane(layout)
side = dui2.ViewerSide.LEFT if viewer else None
def _measure() -> tuple[bui.UIScale, tuple[float, float]]:
# The content rect this window actually gets (to show beside
# the promise; frames are a logic-thread job).
uiscale = bui.app.ui_v1.uiscale
frame = window_frames(
layout, uiscale, dui2.ScrollEdgeStyle.FADE, close_style=False
)[side]
return uiscale, (frame.content_width, frame.content_height)
uiscale, actual = bui.logic_thread_submit(_measure).result(timeout=10.0)
small_ui = uiscale is bui.UIScale.SMALL
pwidth = _promised_width(layout)
pheight = _promised_height(layout, small_ui)
if pwidth is None or pheight is None:
return _no_promise_page(layout, uiscale, actual, route.theme)
def _over_label(promise: _Promise, vertical: bool) -> str:
if not promise.exact:
return '1 over\n(a minimum: may not\nscroll here);\ntap to fit'
if small_ui and vertical:
return (
'1 over\n(scrolls only on\nthe shortest screens\nat small'
' ui-scale);\ntap to fit'
)
return '1 over\n(should scroll);\ntap to fit'
# A hair under, so float rounding can't tip it into scrolling (or,
# when testing the edge, a hair over). The row's height is the
# promised content height less our vertical insets.
rowheight = (
pheight.size - 2.0 * _FIT_V_INSET + (1.0 if route.over else -0.5)
)
# A button row's h-scroll content is its buttons, spacing and
# padding plus the room its padding starts in from the content
# rect's edges; it fits when all that is no wider than the content
# rect (the page's horizontal inset doesn't come into it).
spacing = 15.0
rowwidth = pwidth.size + (1.0 if route.wide_over else -0.5)
# Square-button art draws a bit past its bounds; padding gives it
# room inside the row so the page edges don't clip it.
pad = 15.0
bheight = rowheight - 2.0 * pad
bwidth = (
rowwidth - 2.0 * BUTTON_ROW_EDGE_INSET - 2.0 * pad - 2.0 * spacing
) / 3
def _promise_text(promise: _Promise) -> str:
return ('' if promise.exact else '>= ') + f'{promise.size:.1f}'
return dui2.Response(
page=_fit_themed(
dui2.Page(
title=_lit(_layout_title(layout) + ' Fit'),
v_inset=_FIT_V_INSET,
viewer=bdep.BlankDepiction() if viewer else None,
viewer_side=dui2.ViewerSide.LEFT,
rows=[
dui2.ButtonRow(
padding_top=pad,
padding_bottom=pad,
padding_left=pad,
padding_right=pad,
button_spacing=spacing,
content_align=dui2.HAlign.CENTER,
debug=True,
buttons=[
dui2.Button(
label=_lit(
f'Width {rowwidth:.1f}: '
+ (
_over_label(pwidth, vertical=False)
if route.wide_over
else 'fits;\ntap to go 1 over'
)
),
label_scale=0.7,
size=(bwidth, bheight),
action=replace(
route, wide_over=not route.wide_over
).replace(),
),
dui2.Button(
label=_lit(
f'Height {rowheight:.1f}: '
+ (
_over_label(pheight, vertical=True)
if route.over
else 'fits;\ntap to go 1 over'
)
),
label_scale=0.7,
size=(bwidth, bheight),
action=replace(
route, over=not route.over
).replace(),
),
dui2.Button(
label=_lit(
f'Content promised\n{_promise_text(pwidth)}'
f' x {_promise_text(pheight)}'
f'\nthis window gets\n{actual[0]:.1f}'
f' x {actual[1]:.1f}'
f'\n(v_inset {_FIT_V_INSET:g};'
f' {uiscale.name.lower()} ui-scale)'
),
label_scale=0.6,
size=(bwidth, bheight),
),
],
)
],
),
route.theme,
)
)
[docs]
def test_page_bounds() -> bacommon.docui.v2.Response:
"""Button-style bounds tests (v2 mirror of '/boundstests')."""
from bauiv1 import _commonassets, _docuiv2testassets
strs = _docuiv2testassets.strings
def _nm(style: bacommon.docui.v2.ButtonStyle) -> LangStrSpec:
# Code-literal pass-through: renders the raw enum name
# verbatim in every locale.
return strs.common.code_literal(
text=f'{type(style).__name__}.{style.name}'
).spec
def _hello_rows(
title: LangStrSpec,
sizes: list[tuple[float, float]],
style: bacommon.docui.v2.ButtonStyle,
texture: bool = False,
) -> list[bacommon.docui.v2.Row]:
return [
dui2.TitleRow(title),
dui2.ButtonRow(
buttons=[
dui2.Button(
label=strs.layout.hello.spec,
size=size,
style=style,
texture=(
_commonassets.textures.white if texture else None
),
color=(1, 0, 0, 0.3) if texture else None,
debug=True,
)
for size in sizes
],
),
]
styles = dui2.ButtonStyle
return dui2.Response(
page=dui2.Page(
title=strs.layout.bounds_tests_title.spec,
rows=[
*_hello_rows(
_nm(styles.SQUARE),
[(300, 300), (200, 200), (100, 100)],
styles.SQUARE,
),
*_hello_rows(
_nm(styles.SQUARE_WIDE),
[(400, 200), (200, 250), (60, 100)],
styles.SQUARE_WIDE,
),
*_hello_rows(
strs.layout.background_texture.spec,
[(300, 300), (200, 200), (100, 100)],
styles.SQUARE,
texture=True,
),
*_hello_rows(
_nm(styles.TAB),
[(400, 100), (200, 50), (100, 60)],
styles.TAB,
),
*_hello_rows(
_nm(styles.LARGER),
[(500, 100), (200, 50), (100, 60)],
styles.LARGER,
),
*_hello_rows(
_nm(styles.LARGE),
[(400, 100), (200, 50), (100, 60)],
styles.LARGE,
),
*_hello_rows(
_nm(styles.MEDIUM),
[(300, 100), (200, 50), (100, 60)],
styles.MEDIUM,
),
*_hello_rows(
_nm(styles.SMALL),
[(200, 100), (200, 50), (100, 60)],
styles.SMALL,
),
*_hello_rows(
_nm(styles.BACK),
[(200, 100), (200, 50), (100, 60)],
styles.BACK,
),
*_hello_rows(
_nm(styles.BACK_SMALL),
[(200, 100), (200, 50), (100, 60)],
styles.BACK_SMALL,
),
],
)
)
def _layout_title(layout: dui2.WindowLayout) -> str:
"""A window layout's name, title-cased (``WIDE_TALL`` -> ``Wide Tall``)."""
return layout.name.replace('_', ' ').title()
def _side_name(side: dui2.ViewerSide | None) -> str:
"""A viewer pane placement's name (None for no pane)."""
return 'None' if side is None else side.name.title()
def layout_tests_rows(
state: bacommon.docui.routes.docuitest.RootState,
) -> list[dui2.Row]:
"""The root page's 'Layout Tests' row, at the picked layout.
Each button opens a test page at the layout picked in
:func:`layout_picker_rows`:
- Fade Scroll / Outline Scroll: the window layouts page (at the
picked viewer side too) in each scroll edge style; content
should sit in exactly the same place in both.
- Fit: the layout's promised content-rect size check (or a note
that it promises none).
- Widgets, Sections, Grids: those test pages, to see how they
behave at the layout's width (grids reflow to it).
All but Fit show the standard test viewer (see
:func:`with_test_viewer`) on the picked side, and all show their
pages in the theme picked in :func:`layout_theme_rows`.
:meta private:
"""
import bacommon.docui.routes.docuitest as rt
from bauiv1lib.docuitestthemes import theme_frame
layout = state.layout
side = state.viewer_side
theme = state.theme
background_color, cancel_button_color = theme_frame(theme)
def _button(
label: str,
route: bacommon.docui.routes.DocUIRoute,
scroll_edge_style: dui2.ScrollEdgeStyle | None = None,
) -> dui2.Button:
return dui2.Button(
label=LangStrSpecValue.literal(label),
size=(160, 80),
action=route.browse(
layout=layout,
scroll_edge_style=scroll_edge_style,
# (The theme's frame colors come with the link.)
background_color=background_color,
cancel_button_color=cancel_button_color,
),
)
return [
# Dev-only, so baked literals.
dui2.TitleRow(LangStrSpecValue.literal('Layout Tests')),
dui2.ButtonRow(
buttons=[
*(
_button(
label,
rt.WindowLayouts(
layout=layout, viewer_side=side, theme=theme
),
style,
)
for label, style in (
('Fade Scroll', dui2.ScrollEdgeStyle.FADE),
('Outline Scroll', dui2.ScrollEdgeStyle.OUTLINE),
)
),
_button('Fit', rt.LayoutFit(layout=layout, theme=theme)),
_button('Widgets', rt.Widgets(viewer_side=side, theme=theme)),
_button('Sections', rt.Sections(viewer_side=side, theme=theme)),
_button('Grids', rt.ButtonGrids(viewer_side=side, theme=theme)),
],
),
]
class _LayoutOption(NamedTuple):
"""One way to open a window: a layout and (at viewer layouts) a side."""
layout: dui2.WindowLayout
side: dui2.ViewerSide | None
label: str
size: tuple[float, float]
def _layout_options() -> list[_LayoutOption]:
"""Every layout, with a left/none/right option for viewer layouts.
Built from :class:`~bacommon.docui.v2.WindowLayout`, so a layout
added later shows up everywhere these are offered.
"""
from bauiv1lib.docui._panes import layout_has_viewer_pane
out: list[_LayoutOption] = []
for layout in dui2.WindowLayout:
name = _layout_title(layout)
if not layout_has_viewer_pane(layout):
out.append(_LayoutOption(layout, None, name, (120.0, 80.0)))
continue
for side in (dui2.ViewerSide.LEFT, None, dui2.ViewerSide.RIGHT):
out.append(
_LayoutOption(
layout, side, f'{name} ({_side_name(side)})', (200.0, 80.0)
)
)
return out
#: The picked button in the root page's 'Layout Test Options' rows.
_PICKED_COLOR = (0.35, 0.8, 1.0, 1.0)
#: :attr:`~bacommon.docui.v2.ButtonRow.button_spacing`'s default.
_DEFAULT_BUTTON_SPACING = dui2.ButtonRow(buttons=[]).button_spacing
def layout_picker_rows(
route: bacommon.docui.routes.docuitest.Root,
state: bacommon.docui.routes.docuitest.RootState,
) -> list[dui2.Row]:
"""The root page's 'Layout Test Options' layout row: pick a layout.
A button per layout option (see ``_layout_options()``), at 0.6
scale. Pressing one just stores it in the page's state (rows below
can open their pages at it); the picked one shows brightly.
:meta private:
"""
import bacommon.docui.routes.docuitest as rt
def _picked(option: _LayoutOption) -> bool:
# (A pick sets both, so they match together.)
return (
option.layout is state.layout and option.side is state.viewer_side
)
return [
# (Titles both this row and the themes row below it.)
dui2.TitleRow(LangStrSpecValue.literal('Layout Test Options')),
dui2.SubtitleRow(LangStrSpecValue.literal('Layout')),
dui2.ButtonRow(
buttons=[
dui2.Button(
label=LangStrSpecValue.literal(option.label),
size=option.size,
scale=0.6,
color=_PICKED_COLOR if _picked(option) else None,
# (Just updates our state; the page comes back with
# the new pick showing.)
action=rt.Root(debug=route.debug).replace(
sets=[
rt.RootState.assign(
lambda s: s.layout, option.layout
),
rt.RootState.assign(
lambda s: s.viewer_side, option.side
),
]
),
)
for option in _layout_options()
],
# (Tighter, to go with the smaller buttons.)
button_spacing=_DEFAULT_BUTTON_SPACING * 0.6,
),
]
def layout_theme_rows(
route: bacommon.docui.routes.docuitest.Root,
state: bacommon.docui.routes.docuitest.RootState,
) -> list[dui2.Row]:
"""The root page's 'Layout Test Options' themes row: pick a theme.
A button per :class:`~bacommon.docui.routes.docuitest.ThemeVariant`
(the page-themes test page's set), small and picked the same way as
in :func:`layout_picker_rows`; the 'Layout Tests' pages show in the
picked one.
:meta private:
"""
import bacommon.docui.routes.docuitest as rt
from bauiv1lib.docuitestthemes import theme_label
from bauiv1lib.docuitestwidgets import style_for_size
size = (150.0, 60.0)
return [
# (No title: the layout row's 'Layout Test Options' covers both.)
dui2.SubtitleRow(LangStrSpecValue.literal('Themes')),
dui2.ButtonRow(
buttons=[
dui2.Button(
label=LangStrSpecValue.literal(theme_label(theme)),
size=size,
style=style_for_size(size),
scale=0.6,
color=_PICKED_COLOR if theme is state.theme else None,
action=rt.Root(debug=route.debug).replace(
sets=[rt.RootState.assign(lambda s: s.theme, theme)]
),
)
for theme in rt.ThemeVariant
],
button_spacing=_DEFAULT_BUTTON_SPACING * 0.6,
),
]
# 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