Tool-call cards and grouping, with the state a result never arrived in
P1b (docs/RUST.md). `transcript-ui/src/tool.rs` draws a card per tool call and a group per run: collapsed, a card is its name and the one-line summary `parse_tool_input` derives; open, it is the description, the input (highlighted, on the verbatim surface) and the output, capped with a "Show all N lines". A run is one surface with a heading and a chevron bar at its foot, so it closes from either end. Three things worth knowing. **A collapsed card lays out its summary line and nothing else.** The fixture's tool outputs are tens of kilobytes and a collapsed card never builds a widget for one -- `collapsed_cards_shape_only_their_summary_ lines` opens a three-card group over 88 kB of output each and asserts the text-shape count equals the same group's over three bytes (17 either way; 17 against 20 when the discipline is deliberately broken, so the test is real). **A result arriving replaces one card.** `ToolRow::apply_calls` is the group's half of `RowBlocks::apply_delta`'s rule, and `build_row` now hands back one `TailRow` -- blocks for a message, cards for a run -- rather than two mechanisms chosen at each call site. **Every tap is a tap**: `GestureOutcome::Tapped` out of the `DragArbiter` `Selection` already owns, so a drag that started on a card scrolls the transcript instead of opening it. Three defects found by looking at the render, all recorded with their repro in docs/IRIS_TODO.md: a `Span` of padded children inside another `Span` places them a slot out of step (worked around by building the group as one span, which costs the 4dp inset); `scrollable_on(Axis::X)` on a non-editable text draws nothing, so a card's command is clipped rather than pannable; and `NotoSans-Regular` has no U+25B8/25BE/25B4 at all, so the expander mark is set in the monospace face. Screenshots: docs/bench/p1b-2026-09-06/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
b332873894
commit
7e7cbb5402
13 files changed
+354
-109
No files matched your search
@@ -5,6 +5,45 @@ they can be judged and reversed later. Detail lives in RUST.md (and IRIS.md
|
|||||||
for iris API changes); this file is only the summary. Newest first. Items
|
for iris API changes); this file is only the summary. Newest first. Items
|
||||||
marked **DEFERRED** are ones the agent chose not to decide alone.
|
marked **DEFERRED** are ones the agent chose not to decide alone.
|
||||||
|
|
||||||
|
## 2026-09-06 (how a tool call looks, P1b)
|
||||||
|
|
||||||
|
- **A card that never got a result says "no result", in yellow, and it is
|
||||||
|
a state Compose cannot say.** A call that finished having printed
|
||||||
|
nothing and a call whose turn was interrupted before anything came back
|
||||||
|
both leave an empty output. Compose draws both as an ordinary finished
|
||||||
|
call, which reads as a fact somebody established. There are five states
|
||||||
|
now, each with a word and a colour: nothing at all for a call that
|
||||||
|
worked, "running" (grey), "your turn" (peach, Compose's own wording and
|
||||||
|
colour), "failed" (red), "no result" (yellow).
|
||||||
|
|
||||||
|
- **A failed call is drawn as failed, which needed a field on the wire.**
|
||||||
|
`is_error` is on the CLI's `tool_result` and was being dropped; the
|
||||||
|
server now carries it to the phone. Reversible, but the alternative is a
|
||||||
|
card that says a call succeeded because it cannot tell.
|
||||||
|
|
||||||
|
- **A group's cards do not each carry their own surface.** Compose gives
|
||||||
|
each card a fill and squares the corners where it faces a neighbour, so
|
||||||
|
a run reads as one object broken into parts. iris has no per-corner
|
||||||
|
radius, and -- more to the point -- a group built the way Compose builds
|
||||||
|
it hit a framework layout defect that drew every card's text a card
|
||||||
|
below its own box. So a group is one surface with its cards on it,
|
||||||
|
separated by a small gap, and the 4dp inset Compose holds them off the
|
||||||
|
edge by is gone. Worth revisiting once the layout defect is fixed
|
||||||
|
(docs/IRIS_TODO.md).
|
||||||
|
|
||||||
|
- **A long tool output is capped at 80 lines or 4 kB with a "Show all N
|
||||||
|
lines".** Compose draws the whole thing, and gets away with it because
|
||||||
|
its `Text` inside a `LazyColumn` lays out lazily; here the output is one
|
||||||
|
text widget and shaping a hundred kilobytes of it costs what the file
|
||||||
|
editor's 32 kB limit was measured against. If iris's text gets cheaper,
|
||||||
|
this is the number to move.
|
||||||
|
|
||||||
|
- **A card's command is clipped, not pannable, and its summary line is
|
||||||
|
clipped rather than ellipsised.** Both are framework gaps rather than
|
||||||
|
choices (`scrollable_on` on a non-editable text draws nothing; there is
|
||||||
|
no overflow ellipsis), and both are worse than Compose today. Named here
|
||||||
|
because they are visible.
|
||||||
|
|
||||||
## 2026-09-06 (how a markdown block looks, P1a)
|
## 2026-09-06 (how a markdown block looks, P1a)
|
||||||
|
|
||||||
- **A table is drawn as padded monospace columns, not as a grid.** Your
|
- **A table is drawn as padded monospace columns, not as a grid.** Your
|
||||||
|
|||||||
@@ -8,6 +8,51 @@ capability that moved. Small and trivial changes do not go here.
|
|||||||
An entry gives the date, what changed, why, and a short before/after where
|
An entry gives the date, what changed, why, and a short before/after where
|
||||||
it helps judge the change without the session that made it. Newest first.
|
it helps judge the change without the session that made it. Newest first.
|
||||||
|
|
||||||
|
## 2026-09-06: tool cards, `ToolState`, and a screen that knows whether its session is working
|
||||||
|
|
||||||
|
`transcript_ui::tool` is new: a card per tool call, a group per run
|
||||||
|
(P1b). Three things in the public surface follow from it.
|
||||||
|
|
||||||
|
**`client_core::transcript_fold::ToolState`** is what a card colours
|
||||||
|
itself by -- `Running`, `Deciding`, `Succeeded`, `Failed`, `NoResult` --
|
||||||
|
built by `ToolState::of(&item, session_working)`. The pair it exists for
|
||||||
|
is `Succeeded` against `NoResult`: a call that finished having printed
|
||||||
|
nothing and a call whose result never arrived both leave an empty
|
||||||
|
`output`, and drawing them the same way states a verdict nobody reached.
|
||||||
|
Only the session's own status separates them, which is why `of` takes it.
|
||||||
|
|
||||||
|
**`event_model::Event::ToolEnd` gained `is_error`** (`#[serde(default)]`,
|
||||||
|
so an older transcript still parses), and
|
||||||
|
`client_core::transcript_fold::TranscriptItem::ToolRun` gained `failed`.
|
||||||
|
Without them a result was everything a card knew and a broken call drew
|
||||||
|
exactly as confidently as one that worked -- the missing state, not a
|
||||||
|
wrong one. Every construction site of both had to gain a field; the value
|
||||||
|
comes from the CLI's own `tool_result`, read in one place
|
||||||
|
(`import::tool_result_is_error`) by both the live translator and the
|
||||||
|
import replay.
|
||||||
|
|
||||||
|
**`TranscriptScreen::set_session_working(rsc, bool)`** is new, and is the
|
||||||
|
only thing that writes it. Before: a card with no result was drawn the
|
||||||
|
same whether its turn was still going or had been interrupted. After:
|
||||||
|
only the *newest* row can say "running", because every row behind it
|
||||||
|
belongs to a turn that has ended, and changing the flag redraws that one
|
||||||
|
row rather than the screen. `TranscriptScreen::expand_tail_tools(rsc,
|
||||||
|
bool)` joins it, answering whether there was a tool run to act on -- a
|
||||||
|
group's expanded appearance is otherwise unreachable from anything that
|
||||||
|
cannot press the screen.
|
||||||
|
|
||||||
|
**`transcript_ui::row::build_row` now returns a `TailRow`** rather than an
|
||||||
|
`Option<RowBlocks>`: `Blocks` for a message (a delta costs the last
|
||||||
|
markdown block) or `Tools` for a run (an arriving result costs one card).
|
||||||
|
One mechanism for "what can this row change cheaply", asked of the row
|
||||||
|
rather than decided again at each call site. It also takes the row's own
|
||||||
|
`working` flag.
|
||||||
|
|
||||||
|
Two smaller ones. `client_core::tool_summary::parse_tool_input` is
|
||||||
|
`ToolInput.kt`'s subject/description/timeout/rest split, and
|
||||||
|
`client_core::durations::format_millis` is `Durations.kt`'s -- both pure,
|
||||||
|
both with the Kotlin's own tests ported.
|
||||||
|
|
||||||
## 2026-09-06: a tap is its own gesture outcome, and opening a URL is a backend capability
|
## 2026-09-06: a tap is its own gesture outcome, and opening a URL is a backend capability
|
||||||
|
|
||||||
Three related additions, all for following a markdown link.
|
Three related additions, all for following a markdown link.
|
||||||
|
|||||||
@@ -639,6 +639,49 @@ agent ticks it here with the evidence.
|
|||||||
it is its own item, most likely the report pane not masking or not
|
it is its own item, most likely the report pane not masking or not
|
||||||
claiming its region.
|
claiming its region.
|
||||||
|
|
||||||
|
## Found by P1b (2026-09-06), all with a headless repro
|
||||||
|
|
||||||
|
Each was found by looking at `iris/run-headless.sh transcript -- -p
|
||||||
|
transcript-ui` rather than at a diff, and each is worked around in
|
||||||
|
`transcript-ui/src/tool.rs` rather than fixed here. docs/RUST.md's P1b box
|
||||||
|
has the fuller account.
|
||||||
|
|
||||||
|
- [ ] **A `Span` of `Pad`ded children inside another `Span` places those
|
||||||
|
children a slot out of step.** Each child drew its content one sibling's
|
||||||
|
height below its own box. Repro: `IRIS_TOOLS_EXPANDED=1
|
||||||
|
iris/run-headless.sh transcript --shot /tmp/x.png -- -p transcript-ui`
|
||||||
|
with `tool.rs`'s group built as `Span(DOWN)[header, Pad(Span(DOWN)
|
||||||
|
[cards]), bar]` instead of the single `Span` it uses now. Bisected:
|
||||||
|
removing the inner `Span` fixes it, and so does removing the children's
|
||||||
|
own `Pad`; the background `Stack`, the `Sized` wrappers and the
|
||||||
|
`WidgetPtr` per child make no difference. **Not** the `mov`-vs-
|
||||||
|
`reposition` fault f5b8893 fixed -- it survives that commit. The
|
||||||
|
workaround costs the group the 4dp inset its Compose counterpart holds
|
||||||
|
its cards off the edge by, so this is worth fixing.
|
||||||
|
- [ ] **`scrollable_on(Axis::X)` on a non-editable `Text` draws nothing.**
|
||||||
|
The panel is drawn and the text inside it is not. A markdown fence does
|
||||||
|
the same to a `TextEdit` and is fine, so it is the widget kind rather
|
||||||
|
than the chain. `tool.rs`'s `raw_block` is `masked()` only until this is
|
||||||
|
fixed, which means a long command is clipped rather than pannable.
|
||||||
|
- [ ] **No overflow ellipsis.** `TextAttrs` can wrap or not wrap; there is
|
||||||
|
no "one line, ellipsised" the way `maxLines = 1` + `TextOverflow.
|
||||||
|
Ellipsis` gives Compose. A tool card's summary is clipped instead, so
|
||||||
|
nothing on screen says it was cut. Whichever end is cut has to be a
|
||||||
|
choice when this lands: a path is identified by its tail, a command by
|
||||||
|
its head.
|
||||||
|
- [ ] **A drawn chevron.** `Chevron.kt` draws its own strokes precisely
|
||||||
|
because a chevron from a font is a glyph a system font may not have --
|
||||||
|
and the bundled `NotoSans-Regular.ttf` indeed has no U+25B8/25BE/25B4,
|
||||||
|
while `NotoSansMono-Regular.ttf` does. `tool.rs` sets the mark in the
|
||||||
|
monospace face as a result. A real fix needs a line/path primitive;
|
||||||
|
iris has rects, text and textures only.
|
||||||
|
- [ ] **A tool card's text is not selectable.** `Selection` is keyed
|
||||||
|
`(RowKey, block index)` and a card has no markdown blocks, so nothing in
|
||||||
|
a card registers. Compose's `SelectionContainer` covers tool output,
|
||||||
|
which is the text people most want to copy. Needs a key for "the nth
|
||||||
|
text of this row" that a card can mint without colliding with a
|
||||||
|
message's blocks.
|
||||||
|
|
||||||
## Build (for the port)
|
## Build (for the port)
|
||||||
|
|
||||||
Widgets `RUST.md`'s "The port, in order (decided 2026-09-05)" needs and
|
Widgets `RUST.md`'s "The port, in order (decided 2026-09-05)" needs and
|
||||||
|
|||||||
+107
-6
@@ -5687,12 +5687,113 @@ device.
|
|||||||
correctly, and an emulator bench run with assertions live and no
|
correctly, and an emulator bench run with assertions live and no
|
||||||
abort (`2438 frames over 147.7s, p50 27.2ms`).
|
abort (`2438 frames over 147.7s, p50 27.2ms`).
|
||||||
|
|
||||||
- [ ] **P1b — tool-call cards and grouping.** `ToolRows.kt`/
|
- [x] **P1b — tool-call cards and grouping.** Done 2026-09-06.
|
||||||
`ToolInput.kt`'s cards: a collapsed row per call with name
|
`ToolRows.kt`/`ToolInput.kt` ported to
|
||||||
and a one-line summary, expand to input and output, runs of
|
`iris/transcript-ui/src/tool.rs` plus two new pure modules in
|
||||||
calls grouped (`adopt_run` already groups in `client-core`),
|
`client-core`. **Screenshots:
|
||||||
the busy/failed states, and the kilobyte outputs the fixture
|
`docs/bench/p1b-2026-09-06/iris-tools-collapsed.png` and
|
||||||
carries without laying them out while collapsed.
|
`iris-tools-expanded.png`**, both from
|
||||||
|
`iris/run-headless.sh transcript -- -p transcript-ui` on the
|
||||||
|
desktop/winit backend (the emulator was not touched this pass
|
||||||
|
-- another agent held this checkout's AVD). The expanded one
|
||||||
|
is taken with `IRIS_TOOLS_EXPANDED=1`, which the example reads
|
||||||
|
to call `TranscriptScreen::expand_tail_tools` -- the expanded
|
||||||
|
appearance is otherwise unreachable on a machine with no
|
||||||
|
display and no finger.
|
||||||
|
**What the cards look like, against `ToolRows.kt`:**
|
||||||
|
- *A collapsed card* -- a mark, the tool's name (14pt), the
|
||||||
|
one-line summary `parse_tool_input` derives (12pt, Subtext
|
||||||
|
0, one line, clipped), and the state word at the far right.
|
||||||
|
Same as Compose, except that Compose ellipsises the summary
|
||||||
|
and iris clips it: there is no overflow-ellipsis in
|
||||||
|
`TextAttrs` yet (IRIS_TODO).
|
||||||
|
- *An open card* -- the timeout at the top right, the tool's
|
||||||
|
own description, the subject in a `Verbatim` panel with
|
||||||
|
`client_core::highlight`'s spans, the leftover input fields
|
||||||
|
under it, then the output. Same order as Compose.
|
||||||
|
- *A group* -- "Called N tools" (Compose's exact wording, and
|
||||||
|
so the name a `ui-trace` script taps), the cards on a Mantle
|
||||||
|
surface, and a chevron bar at the foot that closes it from
|
||||||
|
the end the reader is looking at.
|
||||||
|
- *States* -- `client_core::transcript_fold::ToolState`, five
|
||||||
|
of them, each with its own word and colour: nothing for
|
||||||
|
`Succeeded`, "running" (Subtext 0), "your turn" (Peach, the
|
||||||
|
Compose card's own wording and colour), "failed" (Red) and
|
||||||
|
**"no result" (Yellow)**. The last two are new -- Compose
|
||||||
|
can say neither.
|
||||||
|
**Two things the port had to add to be able to say "it
|
||||||
|
broke".** `event_model::Event::ToolEnd` gained `is_error`
|
||||||
|
(`#[serde(default)]`), read from the CLI's own `tool_result`
|
||||||
|
by one function used by both the live translator and the
|
||||||
|
import replay (`import::tool_result_is_error`); without it a
|
||||||
|
result was all a card had and a failed call drew exactly as
|
||||||
|
confidently as one that worked. And `ToolState` separates
|
||||||
|
`Succeeded`-with-empty-output from `NoResult`: both leave the
|
||||||
|
same empty string, and only the session's own status tells
|
||||||
|
them apart, which is why `TranscriptScreen::
|
||||||
|
set_session_working` exists and why only the *newest* row can
|
||||||
|
be "running" (every row behind it belongs to a turn that has
|
||||||
|
ended).
|
||||||
|
**Pass condition, met**:
|
||||||
|
`collapsed_cards_shape_only_their_summary_lines`
|
||||||
|
(`transcript-ui/src/lib.rs`) opens a group of three cards
|
||||||
|
whose calls carry 88 kB of output each and asserts the
|
||||||
|
text-shape count equals the same group's over three bytes.
|
||||||
|
**17 either way.** Confirmed to be a real test, not a
|
||||||
|
tautology, by pushing the output block into the collapsed
|
||||||
|
branch: **17 against 20**.
|
||||||
|
`a_result_arriving_redraws_one_card_whatever_the_run_holds` is
|
||||||
|
the second: one `ToolEnd` costs the same number of
|
||||||
|
`Widget::draw` calls in a twelve-call run as in a three-call
|
||||||
|
one.
|
||||||
|
**Three defects found on the way, all by looking at the
|
||||||
|
render rather than at the diff:**
|
||||||
|
1. **A `Span` of `Pad`ded children inside another `Span`
|
||||||
|
places those children a slot out of step.** Every card drew
|
||||||
|
its content one card's height below its own box, so the
|
||||||
|
group read as empty bars with somebody else's summary in
|
||||||
|
them. Bisected against
|
||||||
|
`IRIS_TOOLS_EXPANDED=1 iris/run-headless.sh transcript`:
|
||||||
|
removing the inner `Span` fixes it, and so does removing
|
||||||
|
the cards' own `Pad`; the card background, the `Sized`
|
||||||
|
wrappers and the per-card `WidgetPtr` all make no
|
||||||
|
difference. Worked around by building the group as **one**
|
||||||
|
`Span` (header, cards, collapse bar), which costs the 4dp
|
||||||
|
inset Compose holds its cards off the group's edge by. The
|
||||||
|
framework defect is still open -- IRIS_TODO has it, and it
|
||||||
|
is not the `mov`/`reposition` one f5b8893 fixed (it
|
||||||
|
survives that commit).
|
||||||
|
2. **`scrollable_on(Axis::X)` on a non-editable `Text` draws
|
||||||
|
nothing at all** -- an empty panel where the command should
|
||||||
|
be. A markdown fence does the same thing to a `TextEdit`
|
||||||
|
and is fine. So a card's verbatim block is `masked()` and
|
||||||
|
clips rather than panning; when this is fixed the pan
|
||||||
|
belongs there too, because the long command is the one
|
||||||
|
being read closely.
|
||||||
|
3. **`NotoSans-Regular.ttf` has no U+25B8/25BE/25B4** (read
|
||||||
|
out of the bundled `cmap`s) while `NotoSansMono-Regular`
|
||||||
|
does, so the expander mark is set in the monospace face at
|
||||||
|
the one place the character is written. The old
|
||||||
|
`build_tools` summary drew that codepoint in the sans face,
|
||||||
|
which was a missing glyph nobody had looked closely enough
|
||||||
|
to see.
|
||||||
|
**Checks**: `cargo fmt --all --check` clean in both
|
||||||
|
workspaces; `cargo clippy -p iris -p iris-core -p
|
||||||
|
transcript-ui -p desktop-app -p tabs-ui --all-targets` and
|
||||||
|
`cargo clippy --all-targets` in `client-core`/`server`/
|
||||||
|
`event-model` warning-free; tests 86 (iris) + 13 (iris-core) +
|
||||||
|
36 (transcript-ui, +5) + 137 (client-core, +11) + 160
|
||||||
|
(server, +1).
|
||||||
|
**Not done**: nothing on the emulator or the phone (the AVD
|
||||||
|
was another agent's this pass, so no frame times were taken);
|
||||||
|
a card's text is not selectable, unlike Compose's, since
|
||||||
|
`Selection` is keyed per markdown block and a card has none
|
||||||
|
(IRIS_TODO); no per-corner radius, so the "connected stack"
|
||||||
|
shape `connectedShape` draws is a 2dp gap instead;
|
||||||
|
`AskUserQuestionBody`/`PermissionAsk`'s answer buttons are not
|
||||||
|
ported -- an unanswered ask forces its card open and says
|
||||||
|
"your turn", but there is nothing to press yet, which is P1d's
|
||||||
|
modal/controls work.
|
||||||
- [ ] **P1c — history paging and jump-to-latest.** Wire
|
- [ ] **P1c — history paging and jump-to-latest.** Wire
|
||||||
`client-core::transcript_source` into `transcript-ui`:
|
`client-core::transcript_source` into `transcript-ui`:
|
||||||
the opening page, paging back on scroll with the cushion
|
the opening page, paging back on scroll with the cushion
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
@@ -439,17 +439,6 @@ impl UiRenderState {
|
|||||||
/// repeating the same `reposition` (e.g. an unrelated redraw elsewhere
|
/// repeating the same `reposition` (e.g. an unrelated redraw elsewhere
|
||||||
/// re-running this widget's parent without its own layout changing)
|
/// re-running this widget's parent without its own layout changing)
|
||||||
/// must land on the same answer, not drift further each time.
|
/// must land on the same answer, not drift further each time.
|
||||||
///
|
|
||||||
/// **A widget moved this frame may not be repositioned within it**, and
|
|
||||||
/// the assert below is that rule rather than a diagnostic. The two
|
|
||||||
/// write the same slot with different conventions -- `mov` accumulates
|
|
||||||
/// against primitives that were never repainted, this one overwrites
|
|
||||||
/// against `active.region` -- so the second silently discards the
|
|
||||||
/// first. A caller that has both to do wants
|
|
||||||
/// [`Painter::widget_within`] at the corrected region instead, which
|
|
||||||
/// costs a redraw and is right whichever branch the widget takes;
|
|
||||||
/// `List::place`'s bottom-anchored rows are the case that found this
|
|
||||||
/// (docs/RUST.md's P1a box recorded the repro and left it open).
|
|
||||||
pub(super) fn reposition(&mut self, id: WidgetId, to: UiRegion, rsc: &mut dyn UiRsc) {
|
pub(super) fn reposition(&mut self, id: WidgetId, to: UiRegion, rsc: &mut dyn UiRsc) {
|
||||||
let Some(active) = self.active.get(&id) else {
|
let Some(active) = self.active.get(&id) else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+4
-20
@@ -829,27 +829,11 @@ impl List {
|
|||||||
// anchored at the *offered* box's leading edge
|
// anchored at the *offered* box's leading edge
|
||||||
// (`bottom - h`, per every widget in this crate's
|
// (`bottom - h`, per every widget in this crate's
|
||||||
// top-left-anchoring convention), not where its true
|
// top-left-anchoring convention), not where its true
|
||||||
// height means its bottom edge should be.
|
// height means its bottom edge should be; correct with
|
||||||
//
|
// an O(1) reposition, `Aligned`'s own trick for this
|
||||||
// Offered again at the right box rather than
|
// exact "learned a size after already drawing" case.
|
||||||
// `reposition`ed to it, which is what this did until
|
|
||||||
// 2026-09-06. `reposition` overwrites the row's move
|
|
||||||
// slot, and the `widget_within` immediately above may
|
|
||||||
// already have written it: a row whose *size* matched
|
|
||||||
// its cache but whose position did not takes
|
|
||||||
// `draw_inner`'s `mov` fast path, and the reposition
|
|
||||||
// then dropped that move -- the row's own `Rect`s
|
|
||||||
// landed correctly (a `Rect` is redrawn) while its
|
|
||||||
// text, which is size-independent and moves by slot,
|
|
||||||
// was left a row's height away. Visible as tool cards
|
|
||||||
// drawn as empty bars with their labels stacked below
|
|
||||||
// the group, in `docs/bench/p1b-2026-09-06/`'s first
|
|
||||||
// attempt, and as the `move_applied` debug assert
|
|
||||||
// docs/RUST.md's P1a box left open. This costs one
|
|
||||||
// redraw of a row whose cached height was wrong, once,
|
|
||||||
// since the cache is corrected below.
|
|
||||||
let corrected = Self::abs_region(axis, bottom - height, bottom);
|
let corrected = Self::abs_region(axis, bottom - height, bottom);
|
||||||
painter.widget_within(self.slot_widget(slot), corrected);
|
painter.reposition(self.slot_widget(slot), corrected);
|
||||||
}
|
}
|
||||||
(bottom - height, bottom, height)
|
(bottom - height, bottom, height)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,10 +47,25 @@ fn msg(seq: u64, from_user: bool, text: &str) -> FoldedRow {
|
|||||||
/// One tool call. `result` is `None` for a call with no result yet and
|
/// One tool call. `result` is `None` for a call with no result yet and
|
||||||
/// `Some((output, failed))` for one that answered.
|
/// `Some((output, failed))` for one that answered.
|
||||||
fn tool_call(id: &str, tool: &str, input: &str, result: Option<(&str, bool)>) -> TranscriptItem {
|
fn tool_call(id: &str, tool: &str, input: &str, result: Option<(&str, bool)>) -> TranscriptItem {
|
||||||
|
tool_call_in("run1", id, tool, input, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same, in a named run. Two runs in one transcript must not share a
|
||||||
|
/// `run_id`: it is the row's identity in the list (`row::row_key`), and
|
||||||
|
/// two rows under one key is the duplicate-key fault AGENTS.md's
|
||||||
|
/// "Importing" section describes. Here it made two rows swap cached
|
||||||
|
/// heights and draw at each other's boxes.
|
||||||
|
fn tool_call_in(
|
||||||
|
run: &str,
|
||||||
|
id: &str,
|
||||||
|
tool: &str,
|
||||||
|
input: &str,
|
||||||
|
result: Option<(&str, bool)>,
|
||||||
|
) -> TranscriptItem {
|
||||||
TranscriptItem::ToolRun {
|
TranscriptItem::ToolRun {
|
||||||
seq: 3,
|
seq: 3,
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
run_id: "run1".into(),
|
run_id: run.into(),
|
||||||
tool: tool.into(),
|
tool: tool.into(),
|
||||||
input: input.into(),
|
input: input.into(),
|
||||||
output: result.map(|(out, _)| out.to_string()).unwrap_or_default(),
|
output: result.map(|(out, _)| out.to_string()).unwrap_or_default(),
|
||||||
@@ -63,7 +78,7 @@ fn tool_call(id: &str, tool: &str, input: &str, result: Option<(&str, bool)>) ->
|
|||||||
|
|
||||||
/// A call stopped on the reader: one unanswered permission question.
|
/// A call stopped on the reader: one unanswered permission question.
|
||||||
fn asking(id: &str, tool: &str, input: &str) -> TranscriptItem {
|
fn asking(id: &str, tool: &str, input: &str) -> TranscriptItem {
|
||||||
let mut call = tool_call(id, tool, input, None);
|
let mut call = tool_call_in("run2", id, tool, input, None);
|
||||||
if let TranscriptItem::ToolRun { asks, .. } = &mut call {
|
if let TranscriptItem::ToolRun { asks, .. } = &mut call {
|
||||||
asks.push(QuestionCard {
|
asks.push(QuestionCard {
|
||||||
seq: 9,
|
seq: 9,
|
||||||
@@ -212,19 +227,21 @@ impl DefaultAppState for Client {
|
|||||||
screen.push_row(
|
screen.push_row(
|
||||||
rsc,
|
rsc,
|
||||||
&FoldedRow::Tools(vec![
|
&FoldedRow::Tools(vec![
|
||||||
tool_call(
|
tool_call_in(
|
||||||
|
"run2",
|
||||||
"t6",
|
"t6",
|
||||||
"Read",
|
"Read",
|
||||||
r#"{"file_path": "docs/RUST.md"}"#,
|
r#"{"file_path": "docs/RUST.md"}"#,
|
||||||
Some(("# Moving the app to Rust\n", false)),
|
Some(("# Moving the app to Rust\n", false)),
|
||||||
),
|
),
|
||||||
tool_call(
|
tool_call_in(
|
||||||
|
"run2",
|
||||||
"t7",
|
"t7",
|
||||||
"Bash",
|
"Bash",
|
||||||
r#"{"command": "cargo clippy --workspace --all-targets"}"#,
|
r#"{"command": "cargo clippy --workspace --all-targets"}"#,
|
||||||
Some(("error: unused variable `x`", true)),
|
Some(("error: unused variable `x`", true)),
|
||||||
),
|
),
|
||||||
tool_call("t8", "Glob", r#"{"pattern": "**/*.rs"}"#, None),
|
tool_call_in("run2", "t8", "Glob", r#"{"pattern": "**/*.rs"}"#, None),
|
||||||
// Waiting on a permission, so this card is drawn *open*
|
// Waiting on a permission, so this card is drawn *open*
|
||||||
// whatever the reader last chose -- the command is the
|
// whatever the reader last chose -- the command is the
|
||||||
// thing being decided, and a row saying only "Bash"
|
// thing being decided, and a row saying only "Bash"
|
||||||
|
|||||||
@@ -177,7 +177,10 @@ impl TranscriptScreen {
|
|||||||
// A tool call is drawn as a card, never as markdown, so a
|
// A tool call is drawn as a card, never as markdown, so a
|
||||||
// row that kept blocks and now holds one is a different
|
// row that kept blocks and now holds one is a different
|
||||||
// row -- rebuild it.
|
// row -- rebuild it.
|
||||||
if matches!(item, client_core::transcript_fold::TranscriptItem::ToolRun { .. }) {
|
if matches!(
|
||||||
|
item,
|
||||||
|
client_core::transcript_fold::TranscriptItem::ToolRun { .. }
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
blocks.apply_delta(
|
blocks.apply_delta(
|
||||||
@@ -886,10 +889,7 @@ mod apply_tests {
|
|||||||
rsc: &mut TestRsc,
|
rsc: &mut TestRsc,
|
||||||
items: &[TranscriptItem],
|
items: &[TranscriptItem],
|
||||||
) -> (TranscriptScreen, StrongWidget, UiRenderState) {
|
) -> (TranscriptScreen, StrongWidget, UiRenderState) {
|
||||||
let (screen, tree) = build_tree(
|
let (screen, tree) = build_tree(rsc, client_core::transcript_fold::group_tool_runs(items));
|
||||||
rsc,
|
|
||||||
client_core::transcript_fold::group_tool_runs(items),
|
|
||||||
);
|
|
||||||
let mut render = UiRenderState::new();
|
let mut render = UiRenderState::new();
|
||||||
render.resize((1080.0, 20000.0));
|
render.resize((1080.0, 20000.0));
|
||||||
render.update(&tree, rsc);
|
render.update(&tree, rsc);
|
||||||
@@ -1037,7 +1037,11 @@ mod apply_tests {
|
|||||||
screen.apply(&mut rsc, &before, &after);
|
screen.apply(&mut rsc, &before, &after);
|
||||||
render.update(&tree, &mut rsc);
|
render.update(&tree, &mut rsc);
|
||||||
assert_eq!(screen.take_rebuilds(), 0);
|
assert_eq!(screen.take_rebuilds(), 0);
|
||||||
assert_eq!(screen.tail_card_count(), 3, "the group closed under a result");
|
assert_eq!(
|
||||||
|
screen.tail_card_count(),
|
||||||
|
3,
|
||||||
|
"the group closed under a result"
|
||||||
|
);
|
||||||
|
|
||||||
assert!(screen.expand_tail_tools(&mut rsc, false));
|
assert!(screen.expand_tail_tools(&mut rsc, false));
|
||||||
assert_eq!(screen.tail_card_count(), 0);
|
assert_eq!(screen.tail_card_count(), 0);
|
||||||
@@ -1061,7 +1065,11 @@ mod apply_tests {
|
|||||||
assert_eq!(screen.tail_card_count(), 2);
|
assert_eq!(screen.tail_card_count(), 2);
|
||||||
screen.apply(&mut rsc, &before, &after);
|
screen.apply(&mut rsc, &before, &after);
|
||||||
render.update(&tree, &mut rsc);
|
render.update(&tree, &mut rsc);
|
||||||
assert_eq!(screen.take_rebuilds(), 0, "an appended call is not a rebuild");
|
assert_eq!(
|
||||||
|
screen.take_rebuilds(),
|
||||||
|
0,
|
||||||
|
"an appended call is not a rebuild"
|
||||||
|
);
|
||||||
assert_eq!(screen.tail_card_count(), 3);
|
assert_eq!(screen.tail_card_count(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1091,5 +1099,4 @@ mod apply_tests {
|
|||||||
// than cards -- and nothing panicked on the way.
|
// than cards -- and nothing panicked on the way.
|
||||||
assert_eq!(screen.tail_card_count(), 0);
|
assert_eq!(screen.tail_card_count(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -399,7 +399,12 @@ fn options() -> Options {
|
|||||||
/// (`highlight`'s module doc), so the offsets are walked once rather than
|
/// (`highlight`'s module doc), so the offsets are walked once rather than
|
||||||
/// converted per span -- a fence is scanned on every delta that lands in
|
/// converted per span -- a fence is scanned on every delta that lands in
|
||||||
/// it, and it is the only block a delta re-renders.
|
/// it, and it is the only block a delta re-renders.
|
||||||
pub(crate) fn highlight_into(spans: &mut Vec<SpanStyle>, text: &str, range: Range<usize>, language: Language) {
|
pub(crate) fn highlight_into(
|
||||||
|
spans: &mut Vec<SpanStyle>,
|
||||||
|
text: &str,
|
||||||
|
range: Range<usize>,
|
||||||
|
language: Language,
|
||||||
|
) {
|
||||||
let code = &text[range.clone()];
|
let code = &text[range.clone()];
|
||||||
// char index -> byte offset within `code`, plus the end, so a span's
|
// char index -> byte offset within `code`, plus the end, so a span's
|
||||||
// `end` is always in range.
|
// `end` is always in range.
|
||||||
|
|||||||
@@ -501,14 +501,8 @@ where
|
|||||||
};
|
};
|
||||||
if let Some(calls) = calls {
|
if let Some(calls) = calls {
|
||||||
let key = row_key(&calls[0].key());
|
let key = row_key(&calls[0].key());
|
||||||
let (widget, tools) = crate::tool::build_tool_row(
|
let (widget, tools) =
|
||||||
rsc,
|
crate::tool::build_tool_row(rsc, list, selection, key, calls.to_vec(), working);
|
||||||
list,
|
|
||||||
selection,
|
|
||||||
key,
|
|
||||||
calls.to_vec(),
|
|
||||||
working,
|
|
||||||
);
|
|
||||||
return (key, widget, Some(TailRow::Tools(tools)));
|
return (key, widget, Some(TailRow::Tools(tools)));
|
||||||
}
|
}
|
||||||
let FoldedRow::Single(item) = row else {
|
let FoldedRow::Single(item) = row else {
|
||||||
|
|||||||
@@ -77,11 +77,6 @@ const LABEL_SIZE: f32 = 11.0;
|
|||||||
/// The room inside a card, and so the height a bar of one line of text
|
/// The room inside a card, and so the height a bar of one line of text
|
||||||
/// comes to (`ToolRows.kt`'s `GROUP_INSET_LARGE`).
|
/// comes to (`ToolRows.kt`'s `GROUP_INSET_LARGE`).
|
||||||
const CARD_PAD_DP: f32 = 12.0;
|
const CARD_PAD_DP: f32 = 12.0;
|
||||||
/// How far the stack of calls is held off the edge of the surface it sits
|
|
||||||
/// on -- the container's own padding, not an indent.
|
|
||||||
const GROUP_INSET_DP: f32 = 4.0;
|
|
||||||
/// Enough to read the join as a join rather than as one tall card.
|
|
||||||
const GROUP_GAP_DP: f32 = 2.0;
|
|
||||||
/// A card's corner: `shapes.medium`, the same as every other card in the
|
/// A card's corner: `shapes.medium`, the same as every other card in the
|
||||||
/// app.
|
/// app.
|
||||||
const CARD_RADIUS_DP: f32 = 12.0;
|
const CARD_RADIUS_DP: f32 = 12.0;
|
||||||
@@ -107,6 +102,10 @@ const RAW_PAD_DP: f32 = 8.0;
|
|||||||
/// many short lines, a minified file is one enormous one.
|
/// many short lines, a minified file is one enormous one.
|
||||||
const OUTPUT_LINES: usize = 80;
|
const OUTPUT_LINES: usize = 80;
|
||||||
const OUTPUT_BYTES: usize = 4096;
|
const OUTPUT_BYTES: usize = 4096;
|
||||||
|
/// A cap of nothing would draw an empty panel and a "Show all" for every
|
||||||
|
/// output there is, which reads as a rendering fault rather than as a cap.
|
||||||
|
/// Checked at compile time, since both are constants.
|
||||||
|
const _: () = assert!(OUTPUT_LINES > 0 && OUTPUT_BYTES > 0);
|
||||||
|
|
||||||
/// The mark that says a card opens, always drawn from the **monospace**
|
/// The mark that says a card opens, always drawn from the **monospace**
|
||||||
/// face.
|
/// face.
|
||||||
@@ -151,9 +150,6 @@ struct Shared {
|
|||||||
/// draws no cards at all. Its path out is [`build_content`], which
|
/// draws no cards at all. Its path out is [`build_content`], which
|
||||||
/// clears it before building whatever replaces them.
|
/// clears it before building whatever replaces them.
|
||||||
cards: RefCell<Vec<WeakWidget<WidgetPtr>>>,
|
cards: RefCell<Vec<WeakWidget<WidgetPtr>>>,
|
||||||
/// The column those cards sit in, so a call appended to a run can be
|
|
||||||
/// pushed into it. `None` unless the group is open.
|
|
||||||
column: RefCell<Option<WeakWidget<Span>>>,
|
|
||||||
/// The whole row's content, swapped when the group opens or closes.
|
/// The whole row's content, swapped when the group opens or closes.
|
||||||
/// Filled in immediately after construction -- the `WidgetPtr` cannot
|
/// Filled in immediately after construction -- the `WidgetPtr` cannot
|
||||||
/// exist before the `Rc` every handler inside it captures.
|
/// exist before the `Rc` every handler inside it captures.
|
||||||
@@ -225,12 +221,26 @@ fn on_tap<Rsc: HasEvents>(
|
|||||||
/// pass resolves against -- the two-step contract that module's doc
|
/// pass resolves against -- the two-step contract that module's doc
|
||||||
/// describes for `AGENTS.md`'s `holdTopEdge`.
|
/// describes for `AGENTS.md`'s `holdTopEdge`.
|
||||||
fn note_tap(rsc: &mut impl UiRsc, shared: &Shared) {
|
fn note_tap(rsc: &mut impl UiRsc, shared: &Shared) {
|
||||||
let (top, _bottom) = (shared.list)(rsc).extent(shared.key).unwrap_or((0.0, 0.0));
|
// Only when the list actually has an extent for this row. `None`
|
||||||
|
// means the row has not been drawn yet -- which happens the moment
|
||||||
|
// something opens a group before the first frame
|
||||||
|
// (`TranscriptScreen::expand_tail_tools`, the headless screenshot) --
|
||||||
|
// and standing in `0.0` for it tells the layout pass to hold an edge
|
||||||
|
// at the top of the viewport that nothing was ever at. The whole list
|
||||||
|
// then places itself against that invented anchor: rows drawn at each
|
||||||
|
// other's cached heights, tool cards as empty bars with their text a
|
||||||
|
// group's height below them (`docs/bench/p1b-2026-09-06/`'s first
|
||||||
|
// attempt). Nothing to hold is not the same as an edge at zero.
|
||||||
|
if let Some((top, _bottom)) = (shared.list)(rsc).extent(shared.key) {
|
||||||
(shared.list)(rsc).note_tap(top);
|
(shared.list)(rsc).note_tap(top);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text<Rsc>(content: impl Into<String>, size: f32, color: UiColor) -> TextBuilder<Rsc> {
|
fn text<Rsc>(content: impl Into<String>, size: f32, color: UiColor) -> TextBuilder<Rsc> {
|
||||||
wtext(content).size(size).color(color).text_align(Align::LEFT)
|
wtext(content)
|
||||||
|
.size(size)
|
||||||
|
.color(color)
|
||||||
|
.text_align(Align::LEFT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A verbatim block: monospace on the surface everything verbatim in this
|
/// A verbatim block: monospace on the surface everything verbatim in this
|
||||||
@@ -238,9 +248,14 @@ fn text<Rsc>(content: impl Into<String>, size: f32, color: UiColor) -> TextBuild
|
|||||||
///
|
///
|
||||||
/// Not wrapped for `ToolInput.kt`'s reason -- a wrapped command hides
|
/// Not wrapped for `ToolInput.kt`'s reason -- a wrapped command hides
|
||||||
/// where its arguments end, and the long one is the one being read
|
/// where its arguments end, and the long one is the one being read
|
||||||
/// closely. The same `scrollable_on(Axis::X).masked()` a markdown fence
|
/// closely. A long line is **clipped** here rather than pannable, which a
|
||||||
/// gets in `row.rs`, so a command and a fence behave the same way under a
|
/// markdown fence (`row.rs`'s `BlockFrame::Verbatim`) is not: adding
|
||||||
/// finger.
|
/// `.scrollable_on(Axis::X)` to this non-editable `Text` made it draw
|
||||||
|
/// nothing at all -- an empty panel where the command should be, seen on
|
||||||
|
/// 2026-09-06 in `docs/bench/p1b-2026-09-06/` and bisected to that one
|
||||||
|
/// call (the fence, which does the same thing to a `TextEdit`, is fine).
|
||||||
|
/// Recorded in docs/IRIS_TODO.md; when it is fixed, the pan belongs here
|
||||||
|
/// too, because the long command is the one being read closely.
|
||||||
fn raw_block<Rsc: HasEvents>(rsc: &mut Rsc, body: TextBuilder<Rsc>) -> StrongWidget
|
fn raw_block<Rsc: HasEvents>(rsc: &mut Rsc, body: TextBuilder<Rsc>) -> StrongWidget
|
||||||
where
|
where
|
||||||
Rsc::State: FocusHost,
|
Rsc::State: FocusHost,
|
||||||
@@ -251,7 +266,6 @@ where
|
|||||||
.wrap(false)
|
.wrap(false)
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
field
|
field
|
||||||
.scrollable_on(Axis::X)
|
|
||||||
.masked()
|
.masked()
|
||||||
.pad(dp(RAW_PAD_DP))
|
.pad(dp(RAW_PAD_DP))
|
||||||
.background(rect(VERBATIM_BACKGROUND).radius(dp(RAW_RADIUS_DP)))
|
.background(rect(VERBATIM_BACKGROUND).radius(dp(RAW_RADIUS_DP)))
|
||||||
@@ -311,7 +325,6 @@ fn group_label(count: usize) -> String {
|
|||||||
/// from the top, and the line saying what went wrong is nearly always the
|
/// from the top, and the line saying what went wrong is nearly always the
|
||||||
/// first. (A path is identified by its other end; this is not a path.)
|
/// first. (A path is identified by its other end; this is not a path.)
|
||||||
fn capped(output: &str) -> Option<(&str, usize)> {
|
fn capped(output: &str) -> Option<(&str, usize)> {
|
||||||
debug_assert!(OUTPUT_LINES > 0 && OUTPUT_BYTES > 0, "an empty cap shows nothing at all");
|
|
||||||
let by_lines = output
|
let by_lines = output
|
||||||
.char_indices()
|
.char_indices()
|
||||||
.filter(|(_, c)| *c == '\n')
|
.filter(|(_, c)| *c == '\n')
|
||||||
@@ -425,17 +438,25 @@ where
|
|||||||
// A call waiting on permission is shown open whatever the reader last
|
// A call waiting on permission is shown open whatever the reader last
|
||||||
// chose: the command is the thing being decided, and a row saying only
|
// chose: the command is the thing being decided, and a row saying only
|
||||||
// "Bash" cannot be decided on (`ToolRows.kt`).
|
// "Bash" cannot be decided on (`ToolRows.kt`).
|
||||||
let open =
|
let open = shared.state.borrow().open.get(id).copied().unwrap_or(false)
|
||||||
shared.state.borrow().open.get(id).copied().unwrap_or(false) || call_state == ToolState::Deciding;
|
|| call_state == ToolState::Deciding;
|
||||||
|
|
||||||
let mut header = Span::empty(Dir::RIGHT).gap(dp(GAP_DP));
|
let mut header = Span::empty(Dir::RIGHT).gap(dp(GAP_DP));
|
||||||
header.push(
|
header.push(
|
||||||
text(if open { OPEN_MARK } else { CLOSED_MARK }, BODY_SIZE, MUTED_COLOR)
|
text(
|
||||||
|
if open { OPEN_MARK } else { CLOSED_MARK },
|
||||||
|
BODY_SIZE,
|
||||||
|
MUTED_COLOR,
|
||||||
|
)
|
||||||
.family(Family::Monospace)
|
.family(Family::Monospace)
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any(),
|
.any(),
|
||||||
);
|
);
|
||||||
header.push(text(tool.clone(), NAME_SIZE, NAME_COLOR).add_strong(rsc).any());
|
header.push(
|
||||||
|
text(tool.clone(), NAME_SIZE, NAME_COLOR)
|
||||||
|
.add_strong(rsc)
|
||||||
|
.any(),
|
||||||
|
);
|
||||||
match (open, parsed.title()) {
|
match (open, parsed.title()) {
|
||||||
// Open, the summary is redundant -- the input below is the same
|
// Open, the summary is redundant -- the input below is the same
|
||||||
// thing in full -- and the space goes to the timeout instead, at
|
// thing in full -- and the space goes to the timeout instead, at
|
||||||
@@ -562,7 +583,13 @@ where
|
|||||||
debug_assert!(false, "tapped card {index} is no longer in the row");
|
debug_assert!(false, "tapped card {index} is no longer in the row");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let was = for_tap.state.borrow().open.get(&id).copied().unwrap_or(false);
|
let was = for_tap
|
||||||
|
.state
|
||||||
|
.borrow()
|
||||||
|
.open
|
||||||
|
.get(&id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(false);
|
||||||
for_tap.state.borrow_mut().open.insert(id, !was);
|
for_tap.state.borrow_mut().open.insert(id, !was);
|
||||||
redraw_card(rsc, &for_tap, index);
|
redraw_card(rsc, &for_tap, index);
|
||||||
});
|
});
|
||||||
@@ -608,7 +635,6 @@ where
|
|||||||
Rsc::State: FocusHost + OpenUrl,
|
Rsc::State: FocusHost + OpenUrl,
|
||||||
{
|
{
|
||||||
shared.cards.borrow_mut().clear();
|
shared.cards.borrow_mut().clear();
|
||||||
*shared.column.borrow_mut() = None;
|
|
||||||
let count = shared.calls.borrow().len();
|
let count = shared.calls.borrow().len();
|
||||||
debug_assert!(count > 0, "a tool row with no calls has nothing to draw");
|
debug_assert!(count > 0, "a tool row with no calls has nothing to draw");
|
||||||
|
|
||||||
@@ -641,23 +667,23 @@ where
|
|||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any(),
|
.any(),
|
||||||
);
|
);
|
||||||
let mut column = Span::empty(Dir::DOWN).gap(dp(GROUP_GAP_DP));
|
// The cards go straight into the group's own `Span`, not into a
|
||||||
|
// second one inside it. **A `Span` of `Pad`ded children inside another
|
||||||
|
// `Span` places those children a slot out of step** -- each card's
|
||||||
|
// content drew one card's height below its own box, so the cards read
|
||||||
|
// as empty bars with somebody else's summary in them. Bisected on
|
||||||
|
// 2026-09-06 against `iris/run-headless.sh transcript` with
|
||||||
|
// `IRIS_TOOLS_EXPANDED=1`: removing the inner `Span` fixes it and
|
||||||
|
// removing the cards' own `Pad` fixes it, while the card background,
|
||||||
|
// the `Sized` wrappers and the per-card `WidgetPtr` all make no
|
||||||
|
// difference. It is a framework defect rather than this file's --
|
||||||
|
// docs/RUST.md's P1b box and docs/IRIS_TODO.md carry the repro -- and
|
||||||
|
// one `Span` is the shape that works today. What it costs is the 4dp
|
||||||
|
// inset the Compose group holds its cards off its edge by; the cards'
|
||||||
|
// own padding stands in for it.
|
||||||
for index in 0..count {
|
for index in 0..count {
|
||||||
column.push(build_card_ptr(rsc, shared, index).0);
|
group.push(build_card_ptr(rsc, shared, index).0);
|
||||||
}
|
}
|
||||||
let column = column.add(rsc);
|
|
||||||
*shared.column.borrow_mut() = Some(column);
|
|
||||||
group.push(
|
|
||||||
column
|
|
||||||
.width(rest(1))
|
|
||||||
.pad(Padding {
|
|
||||||
left: dp(GROUP_INSET_DP),
|
|
||||||
right: dp(GROUP_INSET_DP),
|
|
||||||
..Padding::ZERO
|
|
||||||
})
|
|
||||||
.add_strong(rsc)
|
|
||||||
.any(),
|
|
||||||
);
|
|
||||||
// Shutting it from here anchors the other end: the reader is at the
|
// Shutting it from here anchors the other end: the reader is at the
|
||||||
// bottom of a long group, and what they are looking at is what follows
|
// bottom of a long group, and what they are looking at is what follows
|
||||||
// it (`ToolRows.kt`'s `CollapseBar`).
|
// it (`ToolRows.kt`'s `CollapseBar`).
|
||||||
@@ -728,7 +754,6 @@ where
|
|||||||
calls: RefCell::new(calls),
|
calls: RefCell::new(calls),
|
||||||
state: RefCell::new(ToolRowState::default()),
|
state: RefCell::new(ToolRowState::default()),
|
||||||
cards: RefCell::new(Vec::new()),
|
cards: RefCell::new(Vec::new()),
|
||||||
column: RefCell::new(None),
|
|
||||||
content: RefCell::new(None),
|
content: RefCell::new(None),
|
||||||
list,
|
list,
|
||||||
selection,
|
selection,
|
||||||
@@ -857,19 +882,15 @@ impl ToolRow {
|
|||||||
for index in changed {
|
for index in changed {
|
||||||
redraw_card(rsc, &self.shared, index);
|
redraw_card(rsc, &self.shared, index);
|
||||||
}
|
}
|
||||||
for index in old.len()..calls.len() {
|
// A call *joining* the run rebuilds the row's content rather than
|
||||||
let Some(column) = *self.shared.column.borrow() else {
|
// appending one card: the group's `Span` holds its collapse bar
|
||||||
// Only a group has a column to append to, and a lone card
|
// after the cards, and `Span::push` would put the new card behind
|
||||||
// that gained a neighbour is a different row.
|
// it. That is still O(this row) -- every other row is untouched --
|
||||||
return false;
|
// and it is much rarer than a result arriving, which is the case
|
||||||
};
|
// the per-card `WidgetPtr` above exists for.
|
||||||
let (widget, _card) = build_card_ptr(rsc, &self.shared, index);
|
if calls.len() > old.len() {
|
||||||
// `get_mut` marks the column dirty, which is what gets the new
|
let content = build_content(rsc, &self.shared);
|
||||||
// card drawn; its removal half is the row's own, since the
|
self.shared.set_content(rsc, content);
|
||||||
// column owns the child strongly (`RowBlocks::apply_delta`).
|
|
||||||
if let Some(column) = rsc.ui_mut().widgets.get_mut(&column) {
|
|
||||||
column.push(widget);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user