Files
ai-app/IRIS.md
T
irisandClaude Sonnet 3a9208f38b RUST.md, IRIS.md: record I3 -- List built and benchmarked, emulator step named
Ticks I3's box with the numbers (all flat across N as required),
updates "Where things stand", and adds IRIS.md's public-API entry for
List plus the fill-shaped-background lesson. The remaining emulator
comparison against transcript-bench.sh needs List wired into an actual
transcript/session screen (closer to I5's scope than I3's), so it's
recorded as the next step with the exact command rather than left
silently undone.

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
2026-09-05 06:27:58 -04:00

141 lines
7.8 KiB
Markdown

# iris: notable public API changes
For Iris to read on her own time. Each entry is a change to iris's public
surface that a widget author or app author would notice: a trait method
added, removed or re-shaped; a type that callers construct differently; a
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
it helps judge the change without the session that made it. Newest first.
## 2026-09-05: `List`, a virtualised bottom-anchored list (RUST.md's I3)
A new widget, `iris::widget::List` (`iris/src/widget/list.rs` -- read its
module doc first), for the transcript's kind of screen: variable-height
rows, keyed by a `u64`, composed only while visible, moved rather than
re-laid-out on scroll, a scroll anchor that survives a row inserted above
it, "more" sentinels at each end, and "hold the edge nearest the tap" when
a row's height changes (`note_tap`, resolved in the layout pass).
```rust
let mut list = List::new(Axis::Y);
list.push_back(ListRow::new(key, row_widget)); // O(1)
list.push_front(ListRow::new(older_key, row)); // O(1), anchor unaffected
list.set_more_before(Some(spinner_widget)); // sentinel, drawn at the edge
list.note_tap(viewport_y); // before mutating a row's height
let (top, bottom) = list.extent(key).unwrap(); // last frame's on-screen box, if visible
```
Built entirely out of existing primitives (`Painter::widget`/`widget_within`/
`reposition`/`draw_twice`, and `draw_inner`'s own old-children diffing) --
no new mechanism was added to the render core for it. One correctness
lesson worth reading even for other widgets: a row that fills whatever
region it is offered (`Rect`, `is_size_independent`) cannot be measured at
a throwaway oversized region and then merely `reposition`ed into place --
`reposition` only ever writes an offset, never a size, so the oversized
primitive stays oversized. `List` fixes this by caching each row's real
height once measured and placing an already-known row directly at its
exact box; see `list.rs`'s `place` for the full reasoning and
`a_fill_shaped_background_is_not_left_oversized` for the regression test.
## 2026-09-05: a second backend (android-view), and what moved to make room for it
RUST.md's I2. Three changes a widget or app author would notice, all in
service of the same thing: `default` (winit) and the new `android`
(android-view) backends sharing what does not depend on windowing.
- **`Selector`/`Selectable`'s bound changed from `Rsc::State:
HasDefaultUiState` to `Rsc::State: FocusHost`** (new trait, `attr.rs`).
`HasDefaultUiState` still exists and still works — `default/attr.rs` now
implements `FocusHost` for anything that has it — so a winit app's
existing code is unaffected. An Android app implements `FocusHost` via
`HasAndroidUiState` instead. Affects only an app that referenced
`HasDefaultUiState` directly at a `Selectable`/`Selector` call site
rather than through `.attr::<Selectable>(())`, which nothing in-tree
does.
- **`Tasks::init` takes `Arc<dyn RequestRedraw>` instead of
`Arc<winit::window::Window>`.** `RequestRedraw` (`task.rs`) is one method,
`fn request_redraw(&self)`; `winit::window::Window` implements it
(`default/render.rs`), so `Tasks::init(window)` at a call site is
unchanged by inference. Only matters if something constructed a `Tasks`
directly rather than through `DefaultRsc`/`AndroidRsc`.
- **`TextEdit::apply_event`/`TextInputResult` are `#[cfg(not(target_os =
"android"))]`** — they take a `winit::event::KeyEvent`, which does not
exist on Android; `android/input.rs` drives the same primitives
(`backspace`/`delete`/`motion`/`insert`, all still unconditional) from
`ndk::event::Keycode` directly instead. New unconditional getters on the
way: `TextEdit::text()`/`selection_range()`/`caret()`, and
`TextEditCtx::delete_byte_range`/`set_cursor_byte` — the primitives
`android/ime.rs`'s `InputConnection` bridge needed and that were not
previously exposed publicly.
## 2026-09-04: `Widget::draw` reports the size it used; `desired_width`/`desired_height` are gone
A widget used to implement three methods (`draw`, `desired_width`,
`desired_height`); it now implements one, `fn draw(&mut self, painter: &mut
Painter) -> Size`, which draws into `painter.region()` and returns how much
of it was used. Why: the two extra methods routinely re-simulated what
`draw` was about to do anyway (`Span::desired_ortho` copied its own draw
loop to get cross-axis sizing right) — one visit per widget per frame
instead of up to three. A container that needs a child's size before
placing it (alignment, centering) draws the child once at a provisional
region, reads the returned `Size`, and calls the new `Painter::reposition`
to move it into its final spot — an O(1) offset write, not a second draw. A
widget whose drawn output never depends on the size it's given (a
fixed-size `Rect`, a decoded `Image`) overrides the new `fn
is_size_independent(&self) -> bool { false }` to `true`, which skips
redrawing it when only its offered region changes shape.
```rust
// before
fn draw(&mut self, painter: &mut Painter) { /* ... */ }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { /* ... */ }
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { /* ... */ }
// after
fn draw(&mut self, painter: &mut Painter) -> Size { /* ... */ }
```
`SizeCtx` and `Cache` are gone with it — see `LAYOUT.md` for the full
design, the move-offset mechanism this shipped alongside, and the file
list.
## 2026-09-04: texture pipeline rebuilt off the binding array
`Textures`/`TextureHandle`, `GlyphPrimitive`, and `UiRenderNode::new` all
changed shape. Why: the old pipeline bound every texture ever drawn in one
`binding_array<texture_2d<f32>>` and asked every device, unconditionally,
for `VK_EXT_descriptor_indexing` — a real share of Android GPUs lack it,
and it failed outright on the Android emulator's software Vulkan. See
TEXTURES.md's "Recommended shape" and "Implemented, 2026-09-04".
- **`UiRenderNode::new` drops its `limits: UiLimits` parameter, and
`UiLimits` is gone.** Before: `UiRenderNode::new(&device, &queue,
&config, UiLimits::default())`. After: `UiRenderNode::new(&device,
&queue, &config)`. Nothing replaces it — there are no more
binding-array limits to size.
- **`src/default/render.rs`'s device request asks for no features and no
binding-array limits.** Before: `required_features:
Features::TEXTURE_BINDING_ARRAY | Features::PARTIALLY_BOUND_BINDING_ARRAY
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING`
plus two `max_binding_array_*` limits. After: `Features::empty()` (the
`DeviceDescriptor` default) and only `max_buffer_size` set, which was
never about the binding array.
- **`TextureHandle` has no `primitive()` method any more**; a caller
outside `iris` shouldn't have been calling it (it fed the old renderer's
internals), but if something did: use `image_index()` for a standalone
image's bind-group index. There is no equivalent for a page — a page has
no bind group of its own now, see below.
- **`GlyphPrimitive` has no public constructor from a struct literal.**
Before: `GlyphPrimitive { uv_min, uv_max, view_idx, sampler_idx, color,
flags }`. After: `GlyphPrimitive::new(uv_min, uv_max, layer, color,
flags)` — one `layer` (the shared atlas array's layer) instead of a
`view_idx`/`sampler_idx` pair, since a page is now a layer of one array
texture rather than its own bound texture.
- **A widget author drawing images is unaffected**: `Painter::texture`/
`texture_at`/`texture_within` and `Textures::add` keep their signatures.
What changed underneath is that each standalone image now gets its own
`wgpu::BindGroup` and draw call instead of a slot in the shared array —
invisible from the widget API, visible only in `UiRenderNode`'s internals
and in `iris`'s device requirements.