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),)),
),
)
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
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Rects
|
A recording describing the rectangular areas of the
|
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.
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
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.
bottom_edge
The cells along the bottom row of the region, left to right.
left_edge
The cells along the left column of the region, top to bottom.
right_edge
The cells along the right column of the region, top to bottom.
counterweight.geometry.Position
dataclass
A cell's position, x columns right of and y rows below the top-left corner of the screen or of a box.