Source code for bauiv1lib.docui._panes

# Released under the MIT License. See LICENSE for details.
#
"""Doc-ui viewer panes: which one a page gets, and the room around it."""

from __future__ import annotations  # Docs-generation hack.

from dataclasses import dataclass
from typing import TYPE_CHECKING, assert_never

import bacommon.docui.v2 as dui2
import bauiv1 as bui

if TYPE_CHECKING:
    from bauiv1lib.docui._frame import WindowFrame
    from bauiv1lib.docui._pagelook import PageLook


def viewer_insets_v(uiscale: bui.UIScale) -> tuple[float, float]:
    """How far a viewer pane's bottom and top edges sit in from the
    content rect's (negative: out past it).

    At small ui-scale it reaches out a little past content toward the
    toolbars (still clear of them: the pane's own frame and rounded
    corners already read as spacing). All tuned by eye.
    """
    match uiscale:
        case bui.UIScale.SMALL:
            return (-15.0, -15.0)
        case bui.UIScale.MEDIUM | bui.UIScale.LARGE:
            # The whole pane a touch lower.
            return (-5.0, 5.0)
        case _:
            assert_never(uiscale)


def viewer_pane_rect(
    frame: WindowFrame, uiscale: bui.UIScale
) -> tuple[float, float, float, float]:
    """Where a window frame's viewer pane goes (x, y, width, height).

    Beside its scroll area, spanning its content rect's height with its
    edges nudged per ui-scale (see :func:`viewer_insets_v`).
    (Meaningless for a frame with no pane.)
    """
    inset_bottom, inset_top = viewer_insets_v(uiscale)
    return (
        frame.viewer_left,
        frame.content_bottom + inset_bottom,
        frame.viewer_width,
        frame.content_height - inset_bottom - inset_top,
    )


def edge_fades(
    frame: WindowFrame, look: PageLook
) -> tuple[float, float, float, float] | None:
    """How far a frame's scroll-area edge fades reach in for a page.

    (left, right, bottom, top), in from the extend rect's edges; None
    for no fades. Each is the frame's fade length on that side (see
    :class:`~bauiv1lib.docui._frame.WindowFrame`), shortened to end by
    the page's inset line (the side's extend plus the page's horizontal
    or vertical inset), so content at rest is never faded. A shortened
    fade is squeezed, not cut off: it still runs all the way from clear
    to solid.
    """
    if frame.fade_lengths is None:
        return None
    fleft, fright, fbottom, ftop = frame.fade_lengths
    eleft, eright, ebottom, etop = frame.extends
    h_inset, v_inset = look.h_inset, look.v_inset
    return (
        max(0.0, min(fleft, eleft + h_inset)),
        max(0.0, min(fright, eright + h_inset)),
        max(0.0, min(fbottom, ebottom + v_inset)),
        max(0.0, min(ftop, etop + v_inset)),
    )


def apply_edge_fades(
    scrollwidget: bui.Widget, fades: tuple[float, float, float, float] | None
) -> None:
    """Give a scroll widget edge fades (see :func:`edge_fades`)."""
    if fades is None:
        return
    fleft, fright, fbottom, ftop = fades
    bui.scrollwidget(
        edit=scrollwidget,
        edge_style='fade',
        edge_fade=(fleft, fbottom, fright, ftop),
    )


def layout_has_viewer_pane(layout: dui2.WindowLayout) -> bool:
    """Whether a layout makes room for a viewer pane (if a page has one)."""
    match layout:
        case dui2.WindowLayout.VIEWER | dui2.WindowLayout.VIEWER_NARROW:
            return True
        case (
            dui2.WindowLayout.LARGE
            | dui2.WindowLayout.SMALL
            | dui2.WindowLayout.SMALL_TALL
            | dui2.WindowLayout.SMALL_TALLER
            | dui2.WindowLayout.WIDE
            | dui2.WindowLayout.WIDE_TALL
            | dui2.WindowLayout.WIDE_TALLER
            | dui2.WindowLayout.WIDER
        ):
            return False
        case _:
            assert_never(layout)


[docs] @dataclass(frozen=True) class PaneMode: """The viewer pane a page gets: which side it sits on, whether it's framed. (Its background isn't part of this; a pane takes a new one in place.) """ side: dui2.ViewerSide frame_style: dui2.ViewerFrameStyle
def page_pane_mode( layout: dui2.WindowLayout, page: dui2.Page | None ) -> PaneMode | None: """The viewer pane a page gets in a window at a layout, if any. Only pages with a viewer get one, and only at layouts with room for it (see :attr:`bacommon.docui.v2.Page.viewer`). """ if page is None or page.viewer is None: return None if not layout_has_viewer_pane(layout): return None return PaneMode(side=page.viewer_side, frame_style=page.viewer_frame_style) @dataclass(frozen=True) class ScrollGeometry: """What a page is laid out against (see page prep). Its frame's content rect size, extends and page-arrow insets (see :class:`~bauiv1lib.docui._frame.WindowFrame`). """ content_width: float content_height: float extends: tuple[float, float, float, float] page_arrow_insets: tuple[float, float] @classmethod def from_frame(cls, frame: WindowFrame) -> ScrollGeometry: """A window (or popup) frame's.""" return cls( content_width=frame.content_width, content_height=frame.content_height, extends=frame.extends, page_arrow_insets=frame.page_arrow_insets, ) @dataclass(frozen=True) class PageGeometries: """A window's scroll areas for each pane its pages may ask for. Built in the logic thread and handed to background page prep, which picks the one for the page it gets (see :meth:`for_page`); the window then lays itself out the same way when the page arrives. """ layout: dui2.WindowLayout no_pane: ScrollGeometry left_pane: ScrollGeometry right_pane: ScrollGeometry def for_pane(self, pane: PaneMode | None) -> ScrollGeometry: """The scroll area beside a pane (or with none).""" if pane is None: return self.no_pane match pane.side: case dui2.ViewerSide.LEFT: return self.left_pane case dui2.ViewerSide.RIGHT: return self.right_pane case _: assert_never(pane.side) def pane_for_page(self, page: dui2.Page) -> PaneMode | None: """The pane a page gets (see :func:`page_pane_mode`).""" return page_pane_mode(self.layout, page) def page_geometries( layout: dui2.WindowLayout, frames: dict[dui2.ViewerSide | None, WindowFrame], ) -> PageGeometries: """A window's page geometries, from its frame for each pane side.""" return PageGeometries( layout=layout, no_pane=ScrollGeometry.from_frame(frames[None]), left_pane=ScrollGeometry.from_frame(frames[dui2.ViewerSide.LEFT]), right_pane=ScrollGeometry.from_frame(frames[dui2.ViewerSide.RIGHT]), ) def scroll_widget_rect( frame: WindowFrame, ) -> tuple[float, float, float, float]: """Where a window's scroll widget goes (x, y, width, height). Its frame's content rect, reaching out by its extends (its extend rect). """ ext_left, ext_right, ext_bottom, ext_top = frame.extends return ( frame.content_left - ext_left, frame.content_bottom - ext_bottom, frame.content_width + ext_left + ext_right, frame.content_height + ext_bottom + ext_top, ) def window_edge_fades( frame: WindowFrame, *, uiscale: bui.UIScale, toolbar_visibility: str, look: PageLook, ) -> tuple[float, float, float, float] | None: """How far a window's scroll-area edge fades reach in (or None). Its frame's for its page (see :func:`edge_fades`), less any at the bottom at small ui-scale under a minimal toolbar, which has nothing there to fade toward. """ fades = edge_fades(frame, look) if ( fades is not None and uiscale is bui.UIScale.SMALL and toolbar_visibility == 'menu_minimal' ): fleft, fright, _fbottom, ftop = fades return (fleft, fright, 0.0, ftop) return fades # 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