Skip to content

use_rects

use_rects tells a component where the element it returns is on the screen, as Rects: the regions of its content, padding, border and margin boxes, and how much of it its ancestors' clips leave visible. A component can fit what it shows to the space layout gives it, as a CSS container query does. Here each label shortens a path to fit its own width:

def shortened(path: str, width: int) -> str:
    """`path` with as many leading directories as it takes to fit in `width` cells replaced by `…`."""
    parts = path.split("/")
    for start in range(len(parts)):
        candidate = "/".join(("…", *parts[start:]) if start else parts)
        if len(candidate) <= width:
            return candidate
    return parts[-1][:width]


@component
def path_label(path: str) -> Text:
    width = use_rects().content.width
    return Text(
        content=shortened(path, width), style=full_width | min_width(0) | text_wrap_none | text_color("amber", 400)
    )


@component
def split() -> Div:
    left_width, set_left_width = use_state(24)

    def on_input(event: InputEvent, _: Receiver) -> InputResponse:
        match event:
            case KeyPressed(key=Key.Left):
                set_left_width(lambda width: max(width - 4, 8))
                return ()
            case KeyPressed(key=Key.Right):
                set_left_width(lambda width: min(width + 4, 40))
                return ()
            case _:
                return None

    pane = border | border_lightrounded | pad_x(1)

    return Div(
        style=row,
        on_input=on_input,
        children=(
            Div(style=pane | width(left_width), children=(path_label(PATH),)),
            Div(style=pane | fill(1), children=(path_label(PATH),)),
        ),
    )

A wide pane showing the whole path, and a narrow one showing only focus.py

╭──────────────────────────────────╮╭──────────╮
│ src/counterweight/focus.py       ││ focus.py │
╰──────────────────────────────────╯╰──────────╯

Left and Right move the border between the panes, and each label follows its pane's width:

The label's width comes from its pane, never from its text, so the path it shows can't change the width it reads. full_width stretches it across its pane, and min_width(0) lets it be narrower than its text (see Content sets a minimum size).

Use use_hovered for detecting hover state

use_rects is a low-level hook. For the common use case of detecting whether the mouse is hovering over an element, use the higher-level use_hovered hook instead.

Measuring another element

Given an ElementHandle, use_rects reports the rectangles of the element with that handle instead of the calling component's own top-level element, as a React component measures an element through a ref and a ResizeObserver. That's how a component measures an element a child component draws, such as the viewport of a scroll_area it passes the handle to as viewport. The handle comes from use_state(ElementHandle), so it's the same on every render. A handle that names no visible element reports empty rectangles, as a component's first render does.

Measuring the room around an element

Rects.screen is the region the app lays out in, in the same coordinates as the element's own boxes: the terminal in FullScreen, and the region in Inline. Beside the element's border box, it says how much room there is on each side of it, as an IntersectionObserverEntry carries rootBounds, the viewport, beside boundingClientRect. A popup that opens below its field while it fits there, and above it otherwise, decides from it:

rects = use_rects()
below = rects.screen.bottom - rects.border.bottom
above = rects.border.top - rects.screen.top
opens_above = below < shown_rows and above > below

There's no anchor positioning to flip the popup in layout, as CSS's position-try-fallbacks: flip-block does, so the decision comes from the last layout, and the popup moves when the component renders again, before the frame is painted. The screen is the same for every element, and a component that reads it renders again when it changes, even when none of its own boxes move: after the terminal resizes, and in Inline, whenever the root's height does.

When it renders again

use_rects reports the rectangles as of the last layout, and a component that calls it renders again as soon as any of the rectangles it read move, whether its own state changed or something else, such as a sibling growing, moved them. That happens before the frame is painted, as a browser delivers ResizeObserver notifications between layout and paint: the render loop lays the tree out, re-renders the components whose rectangles moved, and lays it out again, until nothing they read moves. So in the example, the labels fit their new widths in the frame that moves the border between the panes, and a component that mounts reads empty rectangles in its first render but is drawn from its real ones.

A component whose size depends on the rectangles it reads should settle: one that grows whenever it reads its own size never does. A frame re-renders a few times at most before it paints anyway, leaving the rest to the next frame and logging a warning, which it doesn't repeat until a frame settles, and with app's check_purity on, the app raises UnsettledMeasurements instead.

Browsers bound that loop differently: each round of ResizeObserver notifications reaches only the observers of elements deeper in the tree than the shallowest the last round reached, so a chain of elements nested inside each other settles however long it is, but an element observed again at the same depth waits for the next frame, with the "ResizeObserver loop completed with undelivered notifications" error. A component re-renders when any of the rectangles it read move, so one that lays out its rows from its own width, and so changes its own height, as text_area does, would wait for the next frame every time it mounted. Counting passes lets it settle on its second, at the cost of cutting off a longer chain of components that each measure the last.

API

counterweight.hooks.use_rects

use_rects(handle: ElementHandle | None = None) -> Rects
PARAMETER DESCRIPTION
handle

The element to report on, by the ElementHandle passed to its handle, as a React component measures another element through a ref, or None for the calling component's top-level element.

TYPE: ElementHandle | None DEFAULT: None

RETURNS DESCRIPTION
Rects

A recording describing the rectangular areas of the content, padding, border, and margin of the element as of the last layout. In a component's first render, and for a handle that named no visible element in the last layout, the returned rectangles will all be positioned at the top-left corner of the screen with 0 width and height. A component that calls use_rects renders again as soon as any of the rectangles it read move, before the frame that moved them is painted.

counterweight.hooks.UnsettledMeasurements

Raised when check_purity is on and the components whose use_rects or use_scroll reads went stale still read stale ones after the frame re-rendered them as often as it does before painting, usually because a component's size depends on what it measures, so it never settles.

counterweight.geometry.Rects dataclass

Rects(
    *,
    content: Region,
    padding: Region,
    border: Region,
    margin: Region,
    clip: Region | None,
    visible: Region | None,
    screen: Region,
)

Where an element is on the screen: the regions its box model occupies, from its content box out to its margin box, and how much of it its ancestors' clips leave visible.

The boxes are whole, never cut down by the clip, so content is always the region text wraps to, even when part of it is clipped away.

content instance-attribute

content: Region

padding instance-attribute

padding: Region

border instance-attribute

border: Region

margin instance-attribute

margin: Region

clip instance-attribute

clip: Region | None

The region the element's ancestors let it draw in: its parent's clip (the screen, for the root), cut down on each axis where the parent's overflow is Hidden, Clip or Scroll to the parent's padding box, or its border box when the element's clip_to is "border". It's the screen when the element's clip_to is "screen", whatever its ancestors clip. It's None when nothing is left, so the element draws nothing.

visible instance-attribute

visible: Region | None

The part of the border box inside clip, or None when none of it is.

screen instance-attribute

screen: Region

The region the app lays the element out in, from (0, 0), in the coordinates of the element's other regions: the terminal in FullScreen, and in Inline, the region, as tall as the root's content. It's what clip_to="screen" clips to, and the root's clip, so a component can see how much room there is beyond its own boxes, as a popup that flips to the side with room does, as an IntersectionObserverEntry's rootBounds shows the viewport beside the element's boundingClientRect. It's the same for every element in a frame.

counterweight.geometry.Region dataclass

Region(*, left: int, top: int, right: int, bottom: int)

A rectangular area of terminal cells.

The edges are half-open: left and top are the first column and row inside the region, while right and bottom are the first column and row outside it. So width and height count cells, and a region with zero (or negative) width or height is empty.

left instance-attribute

left: int

top instance-attribute

top: int

right instance-attribute

right: int

bottom instance-attribute

bottom: int

width property

width: int

height property

height: int

is_empty property

is_empty: bool

top_left property

top_left: Position

The top-left cell of the region.

top_right property

top_right: Position

The top-right cell of the region.

bottom_left property

bottom_left: Position

The bottom-left cell of the region.

bottom_right property

bottom_right: Position

The bottom-right cell of the region.

__post_init__

__post_init__() -> None

__hash__

__hash__() -> int

contains

contains(position: Position) -> bool

positions

positions() -> Iterator[Position]

Every cell in the region, row by row.

top_edge

top_edge() -> Iterator[Position]

The cells along the top row of the region, left to right.

bottom_edge

bottom_edge() -> Iterator[Position]

The cells along the bottom row of the region, left to right.

left_edge

left_edge() -> Iterator[Position]

The cells along the left column of the region, top to bottom.

right_edge

right_edge() -> Iterator[Position]

The cells along the right column of the region, top to bottom.

intersection

intersection(other: Region) -> Region | None

The cells shared by both regions, or None if they share no cells. It's this region itself when other contains all of it, so callers can tell by identity.

counterweight.geometry.Position dataclass

Position(x: int, y: int)

A cell's position, x columns right of and y rows below the top-left corner of the screen or of a box.

x instance-attribute

x: int

y instance-attribute

y: int

__add__

__add__(other: Position) -> Position

__sub__

__sub__(other: Position) -> Position

fill_to

fill_to(other: Position) -> Iterator[Position]

Every position in the rectangle with this position and other at opposite corners, both included.