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/fixed-box-source-20261007/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Fixed native box source

The native fixed/fill box resolver reads its own styles and supplied size,
viewport and fractions. It does not measure descendants. Previously its cache
generation still included descendant layout changes. The existing local style
dependency owner now supplies this distinction; the existing box cache binds
these results to the actual RenderStyles object and generation. It retains that
object so replacement and object lifetime cannot alias an unrelated source.

Intrinsic sizing, fractional width with greedy=False, custom style getters and
custom scalar resolvers retain full layout invalidation. Native spacing and
border inputs are required for the narrower source. Parent auto-size and parent
lifetime remain inputs. Held mutation-box admission remains first. Width,
viewport and fraction variants remain separate. Original extrema are restored
on cache hits. Repeated generation fields were removed from individual keys:
the owning cache already retires its previous generation.

The complete consumers are Widget._get_box_model and
box_depends_on_available_height; presentation release already retires the style
dependency and box sources. Related tests now check stale results and actual
cache lifetime instead of duplicating internal key shape. Existing Package
analysis parsed 250 production modules without omissions.

Final related controls: 34 passed in 2.63s, using this native source. Coverage
includes descendant changes, style-source replacement, intrinsic versus fixed
sizing, fractional greedy modes, custom Scalar/RenderStyles behavior, extrema
restoration and held geometry. An attempted Spacing-subclass control failed at
declaration because the actual accelerated Spacing is not subclassable; that
invalid control was removed. Its original failure log is preserved.

One original raw-driver resize App used source 623e07dd with the existing
installed Toad/Core runtime, without changing packages. It reached width 60,
released capture, exited zero and produced empty stderr. Final width appeared
at 993.66ms; matching earlier installed observation was 1057.70ms. Layout counts
were 69 versus 61. Different work and profiling overhead mean this does not
establish a reliable latency gain. Width frames still take roughly 100ms.
The subsequent source changes only decline non-native spacing/border inputs and
name the already-used tuple fields; the App's native-input path is unchanged.

Full App output and profiles remain at
/home/ts/.cache/agent-scratch/native-box-source-20261007. The original fixture
owns joined teardown; exact individual child births were not recorded. This is
source/App acceptance, not an installed or physical speed claim. There were no
provider calls, public inputs or package writes. Parent owns integration and
durable delivery. Required width-dependent rewrapping and arrangement remain.
212 changes: 212 additions & 0 deletions evidence/fixed-box-source-20261007/app-result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
{
"posted_ms": 706.3528039725497,
"frames": [
{
"ms": 111.03438097052276,
"width": 49,
"latest_requested_delta": 7
},
{
"ms": 230.6039429968223,
"width": 55,
"latest_requested_delta": 10
},
{
"ms": 345.3719209646806,
"width": 56,
"latest_requested_delta": 3
},
{
"ms": 463.6130550643429,
"width": 57,
"latest_requested_delta": 4
},
{
"ms": 578.8633780321106,
"width": 50,
"latest_requested_delta": 11
},
{
"ms": 689.6544770570472,
"width": 52,
"latest_requested_delta": 12
},
{
"ms": 877.9696580022573,
"width": 58,
"latest_requested_delta": 12
},
{
"ms": 993.6596649931744,
"width": 60,
"latest_requested_delta": 12
}
],
"requests": [
[
0.2652329858392477,
1
],
[
18.897947040386498,
2
],
[
37.19545598141849,
3
],
[
53.384628030471504,
4
],
[
66.79921899922192,
5
],
[
85.08271095342934,
6
],
[
103.35164598654956,
7
],
[
121.61416804883629,
8
],
[
136.9925830513239,
9
],
[
150.25750105269253,
10
],
[
168.59511495567858,
11
],
[
186.9223100366071,
12
],
[
205.22478804923594,
11
],
[
223.5255630221218,
10
],
[
242.8538060048595,
9
],
[
252.307447954081,
8
],
[
262.4928279547021,
7
],
[
278.1075610546395,
6
],
[
296.5192859992385,
5
],
[
314.88417205400765,
4
],
[
333.2842959789559,
3
],
[
351.663860026747,
2
],
[
369.79632498696446,
1
],
[
383.2449109759182,
0
],
[
401.6143069602549,
1
],
[
420.00607401132584,
2
],
[
438.38870397303253,
3
],
[
456.77275699563324,
4
],
[
475.2445960184559,
5
],
[
490.31689995899796,
6
],
[
503.6086189793423,
7
],
[
522.0020289998502,
8
],
[
540.3764940565452,
9
],
[
558.7385550606996,
10
],
[
577.0936830667779,
11
],
[
598.4561620280147,
12
]
],
"messages": {
"MouseMove": 16,
"CoreEventMessage": 40,
"Update": 465,
"Layout": 69,
"Resize": 80,
"InvokeLater": 42,
"Callback": 96,
"MouseUp": 2,
"TextSelected": 2,
"MouseRelease": 1,
"Click": 5,
"Compose": 153,
"Mount": 140,
"StateChanged": 66,
"TableOfContentsUpdated": 53
},
"widgets": 778,
"error": null,
"final_width": 60,
"boundary": "completed headless display; raw Driver ingress; not terminal pixels"
}
25 changes: 20 additions & 5 deletions src/textual/_measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from typing import TYPE_CHECKING, Callable, TypeVar

from textual.css.scalar import Scalar, Unit
from textual.css.styles import RenderStyles
from textual.geometry import Spacing

if TYPE_CHECKING:
from textual.widget import Widget
Expand Down Expand Up @@ -229,10 +231,13 @@ def decorate(function: Function) -> Function:
return decorate


def _local_box_inputs(widget: Widget) -> tuple[bool, bool, bool]:
def _local_box_inputs(widget: Widget) -> tuple[bool, bool, bool, bool]:
"""Resolve local scalar dependencies once per owning style generation."""
styles = widget.styles
revision = styles._cache_key
if type(styles) is not RenderStyles:
return True, False, False, False
# Retain the actual source so replacement cannot reuse an unrelated epoch.
revision = id(styles), styles, styles._cache_key
cached = widget.__dict__.get("_height_style_dependency_cache")
if cached is not None and cached[0] == revision:
return cached[1]
Expand All @@ -244,11 +249,11 @@ def _local_box_inputs(widget: Widget) -> tuple[bool, bool, bool]:
# Compile this check at the style-plan boundary, not each cache hit.
# A custom scalar resolver need not obey its nominal unit's inputs.
if type(scalar).resolve is not Scalar.resolve:
result = (True, False, False)
result = (True, False, False, False)
break
unit = scalar.percent_unit if scalar.unit is Unit.PERCENT else scalar.unit
if unit is Unit.HEIGHT:
result = (True, False, False)
result = (True, False, False, False)
break
else:
result = (
Expand All @@ -258,6 +263,16 @@ def _local_box_inputs(widget: Widget) -> tuple[bool, bool, bool]:
or (max_height is not None and not max_height.is_cells),
width is not None and (width.is_auto or width.is_fraction),
height is not None and height.is_auto,
all(scalar is None or type(scalar) is Scalar for scalar in (
width, height, min_width, max_width, min_height, max_height,
))
and (width is None or not width.is_auto)
and (height is None or not height.is_auto)
and all(type(spacing) is Spacing and all(
type(cell) in (int, bool) for cell in spacing
) for spacing in (styles.margin, styles.padding))
and all(type(edge) is tuple and type(edge[0]) is str
for edge in styles.border),
)
widget._height_style_dependency_cache = revision, result
return result
Expand All @@ -267,7 +282,7 @@ def box_depends_on_available_height(widget: Widget) -> bool:
"""Conservative proof for the native box resolver, including its extrema."""
if not widget._native_box_measurement:
return True
depends, content_width, content_height = _local_box_inputs(widget)
depends, content_width, content_height, _ = _local_box_inputs(widget)
if depends:
return True
if content_width and widget._content_width_dependency.depends(widget):
Expand Down
21 changes: 16 additions & 5 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
from textual._extrema import Extrema
from textual._measurement import (
CONTEXT_HEIGHT, INDEPENDENT_HEIGHT, NATIVE_WIDGET_HEIGHT, NATIVE_WIDGET_WIDTH,
HeightDependency, _arrangement_wrapper_uses_height,
HeightDependency, _arrangement_wrapper_uses_height, _local_box_inputs,
box_depends_on_available_height, height_dependency,
)
from textual.message_pump import MessagePump
Expand Down Expand Up @@ -531,7 +531,7 @@ def __init__(
self._repaint_regions: set[Region] = set()

self._box_model_cache: LRUCache[object, tuple[BoxModel, Extrema]] = LRUCache(16)
self._box_model_revision: tuple[int | None, ...] | None = None
self._box_model_revision: tuple[object, ...] | None = None

# Cache the auto content dimensions
self._content_width_cache: tuple[object, int] = (None, 0)
Expand Down Expand Up @@ -1958,10 +1958,21 @@ def _get_box_model(
# never descend through an actively changing child to size siblings.
return box
nodes = self.__dict__.get("_nodes")
revision = (self._layout_updates,
revision: tuple[object, ...] = (self._layout_updates,
nodes._updates if nodes is not None else 0)
if self._native_box_measurement:
_, content_width, _, styles_only = _local_box_inputs(self)
local_styles = styles_only and (greedy or not content_width)
else:
local_styles = False
if local_styles:
# Fixed/fill native boxes never query child content. Their source
# is the original local style owner, not descendant layout epochs.
styles = self.styles
revision = (id(styles), styles, styles._cache_key)
cache_container, cache_height_fraction = container, height_fraction
if not self._box_depends_on_available_height():
independent_height = not self._box_depends_on_available_height()
if local_styles or independent_height:
# The class/method/layout declarations proved these two inputs unused.
# Include the immediate parent's actual auto-size inputs before idle,
# and retire prior epochs rather than accumulating stale aliases.
Expand All @@ -1971,6 +1982,7 @@ def _get_box_model(
parent is not None and parent.styles.is_auto_width,
parent is not None and parent.styles.is_auto_height,
)
if independent_height:
cache_container = container.with_height(0)
cache_height_fraction = Fraction(0)
if revision != self._box_model_revision:
Expand All @@ -1989,7 +2001,6 @@ def _get_box_model(
cache_height_fraction,
constrain_width,
greedy,
*revision,
)
if cached_measurement := self._box_model_cache.get(cache_key):
# Alignment consumes the same original constraints that produced
Expand Down
Loading
Loading