Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions evidence/grid-track-inputs-20261007/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Grid measurement inputs

`GridLayout.arrange` queries content width and outer-size width extrema only
for auto columns without column spans. It queries content height and outer-size
height extrema only for auto rows without row spans. Final boxes and offsets
receive the resolved cell size. `GridHeight` previously treated all four
operations as incoming-height inputs even when the tracks never called them.

The existing dependency declaration now follows those actual query branches.
The original Widget arrangement resource can reuse measurement placements at
another available height when the grid does not read that height. Width,
viewport, child membership, layout/style publication and optimal mode remain
in the original resource key. Fractional/height-relative rows, height-relative
columns, actual auto-track measurements/extrema, custom layout hooks and
dock/split/overlay placement retain their original dependencies. No compositor,
held-root, clipping or hit-map decision changes.

Source census: 250 production modules parsed, zero omissions, using the existing
refactor-audit Package. The supply/consumer family is GridLayout.arrange,
Layout.__init_subclass__, GridHeight, NativeLayoutHeight and
Widget._arrangement_depends_on_available_height/arrange. There is one changed
production owner, GridHeight; no new retained resource or event cache.

Fourteen affected grid controls passed in 1.07 seconds. New real App controls
compare reused placements against fresh placement and retain custom incoming-
height behavior when the corresponding track changes to auto. The first
height-control assertion incorrectly expected a fixed-height child to fill
the larger auto track; it was corrected to make the child auto-height when
testing that dynamic measurement. Production was unchanged by that correction.

One existing raw-Driver sidebar App check passed with the existing installed
WorkerStatic declaration cohort and native source override. It completed 36
packets, 625 final widgets, width 60, no reported App error. Raw result and
profile remain at:

`/home/ts/.cache/agent-scratch/sidebar-drag-hotpath-20261007/grid-track-input-source-drag/`

Compared with the supplied installed profile, arrangement calls were 760 to
661 and box calls 721 to 643, but root reflows were also 11 to 9. Inclusive root
arrangement was 412 to 337 ms and box time 262 to 244 ms; these overlap. Final
headless paint was 1121 to 937 ms, while posting took 712 to 805 ms. This is
not a reliable overall latency improvement or terminal-pixel acceptance.
The new declaration is source-qualified and the affected App path works;
installation belongs to Parent's integration. Transcript remeasurement at
genuinely changed widths remains required.
30 changes: 21 additions & 9 deletions src/textual/_measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,20 +211,32 @@ def depends(self, widget: Widget, *, greedy: bool = True) -> bool:
or any(_scalar_uses_height(row, height_fraction=True) for row in rows)
or any(_scalar_uses_height(column) for column in columns)):
return True
auto_rows = not rows or any(row.is_auto for row in rows)
auto_columns = any(column.is_auto for column in columns)
for child in widget.displayed_children:
child_styles = child.styles
# Auto cells pass the outer size directly to content measurement
# and extrema. Final boxes/offsets receive the already-resolved cell
# size, so fractional child boxes are safe once tracks are proven.
if child._content_height_dependency.depends(child):
return True
if child._content_width_dependency.depends(child):
return True
if any(scalar is not None and _scalar_uses_height(scalar) for scalar in (
child_styles.min_width, child_styles.max_width,
child_styles.min_height, child_styles.max_height,
)):
return True
# Only auto tracks ask for content or resolve extrema against the
# incoming outer size. Fixed/fractional columns and fixed rows
# resolve their cell sizes first; final child boxes read those cell
# sizes, not the outer height. An unused custom measurement must
# not make measurement and placement arrange the same grid twice.
if auto_rows and child_styles.row_span == 1:
if child._content_height_dependency.depends(child):
return True
if any(scalar is not None and _scalar_uses_height(scalar) for scalar in (
child_styles.min_height, child_styles.max_height,
)):
return True
if auto_columns and child_styles.column_span == 1:
if child._content_width_dependency.depends(child):
return True
if any(scalar is not None and _scalar_uses_height(scalar) for scalar in (
child_styles.min_width, child_styles.max_width,
)):
return True
return False


Expand Down
44 changes: 44 additions & 0 deletions tests/test_height_independent_arrangement.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,50 @@ async def test_grid_auto_track_extrema_keep_outer_height_dependency(field, value
assert parent.arrange(Size(40, 100)) is not first


@pytest.mark.parametrize("unused_axis", ["width", "height"])
async def test_grid_reuses_only_when_outer_measurement_is_not_called(unused_axis):
class WidthFromHeight(Static):
def get_content_width(self, container, viewport):
return container.height

class HeightFromHeight(Static):
def get_content_height(self, container, viewport, width):
return container.height

app = App()
async with app.run_test(size=(80, 24)) as pilot:
child = (WidthFromHeight if unused_axis == "width" else HeightFromHeight)(
"wrapping text " * 10
)
child.styles.width = "1fr"
child.styles.height = "auto" if unused_axis == "width" else 2
parent = VerticalGroup(child)
parent.styles.layout = "grid"
parent.styles.height = "auto"
parent.styles.grid_columns = "1fr"
parent.styles.grid_rows = "auto" if unused_axis == "width" else "2"
await app.mount(parent)
await pilot.pause()

first = parent.arrange(Size(40, 10))
assert parent.arrange(Size(40, 100)) is first
# Reuse must agree with a fresh original placement, not just its key.
parent._clear_arrangement_cache()
assert parent.arrange(Size(40, 100)).placements == first.placements
assert parent.arrange(Size(30, 100)) is not first

# The exact same custom method becomes a real outer-size input as
# soon as the corresponding auto track actually calls it.
if unused_axis == "width":
parent.styles.grid_columns = "auto"
else:
parent.styles.grid_rows = "auto"
child.styles.height = "auto"
dependent = parent.arrange(Size(40, 10))
assert parent.arrange(Size(40, 100)) is not dependent
assert parent.arrange(Size(40, 100)).placements != dependent.placements


async def test_local_style_projection_covers_all_raw_mutation_boundaries():
app = App()
async with app.run_test() as pilot:
Expand Down
Loading