RUST.md: E4 ticked with the screenshot path, the exact commands against app/ui-sandbox.sh, and the streaming-duplication bug the screenshot found; "Where things stand" moved E4 out of "in flight" into its own done bullet. IRIS.md: transcript_ui::build_tree, the public API change transcript-ui gained for this. CLIENT_CORE.md: client_core::config's table row and its correspondence note. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
235 lines
12 KiB
Markdown
235 lines
12 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: `transcript_ui::build_tree` (RUST.md's E4)
|
|
|
|
`transcript_ui::build` claimed the whole window (`ui_state.set_root(tree)`)
|
|
as its last step, which is right for a window that *is* the transcript
|
|
screen (the winit example, an eventual Android cdylib) and wrong for the
|
|
desktop app, which puts a session list beside it. `build_tree` is `build`
|
|
minus that last step: it returns `(TranscriptScreen, StrongWidget)` instead
|
|
of just `TranscriptScreen`, and the caller decides where the tree goes —
|
|
into `ui_state.set_root`, or into a `WidgetPtr` alongside something else
|
|
(`iris/desktop-app`'s `rebuild_transcript`). `build` is now one line calling
|
|
`build_tree` and doing the `set_root` itself, so existing callers are
|
|
unaffected.
|
|
|
|
```rust
|
|
// before, and still available, for a caller that wants to *be* the window:
|
|
let screen = transcript_ui::build(rsc, &mut ui_state, rows);
|
|
|
|
// new, for a caller embedding the screen beside something else:
|
|
let (screen, tree) = transcript_ui::build_tree(rsc, rows);
|
|
some_widget_ptr(rsc).set(tree);
|
|
```
|
|
|
|
## 2026-09-05: `SpanStyle`, per-range text styling (RUST.md's I5)
|
|
|
|
A `TextBuffer` used to have exactly one style (`TextAttrs`: colour, size,
|
|
family, ...) for its whole string, applied via `push_default` into parley's
|
|
ranged builder. `SpanStyle` is a second, optional layer: a byte range plus
|
|
whichever of colour/family/font size/bold/italic/underline it overrides,
|
|
pushed with parley's own `push(property, range)` instead. Why: a transcript
|
|
row's markdown (a heading, **bold**, `inline code`, a link) all inside one
|
|
wrapped paragraph needs each to carry its own look while the paragraph
|
|
still wraps and selects as a single buffer — the thing `masonry`'s
|
|
`TextArea` cannot do (`StyleSet` is one style for the whole editor,
|
|
`text_area.rs:43-44`'s `// TODO: RichTextInput`), and the reason this
|
|
existed at all.
|
|
|
|
```rust
|
|
let (text, spans) = transcript_ui::markdown::render_markdown(src, 16.0);
|
|
wtext(text)
|
|
.spans(spans) // new: TextBuilder::spans, on both Text and TextEdit
|
|
.editable(EditMode::MultiLine)
|
|
.add(rsc);
|
|
```
|
|
|
|
Two things a widget author should know before reaching for it:
|
|
|
|
- **Call `.spans()` before or after `.editable()`, both work** — the field
|
|
lives on `TextBuilder` itself, not either output type, and both
|
|
`TextOutput::run` and `TextEditOutput::run` apply it to the buffer via
|
|
`TextBuffer::set_spans`. **These two call sites are a pair**: adding a
|
|
third `TextBuilderOutput` impl without also calling `set_spans` there
|
|
reproduces the exact bug this box shipped once already (spans silently
|
|
dropped for `TextEdit`, found only by screenshotting, not by any test —
|
|
`markdown.rs`'s own unit tests check string/range logic, which is
|
|
correct in isolation and proves nothing about whether the render path
|
|
ever sees it).
|
|
- **Colour is now per-glyph, not per-buffer.** `PlacedGlyph` gained a
|
|
`color: UiColor` field (from parley's own per-run `Style::brush`), and
|
|
`Painter::glyphs` draws each glyph in its own colour instead of
|
|
`RenderedText::color` uniformly. `RenderedText::color` still exists (the
|
|
buffer's *base* colour, for a caller that wants it as a whole, e.g. to
|
|
tint a cursor) but no longer drives what a glyph actually renders as.
|
|
|
|
## 2026-09-05: accessibility names via AccessKit (RUST.md's I4)
|
|
|
|
`.label()` (already in `trait_fns.rs`, previously unused anywhere in-tree)
|
|
is now load-bearing: it's the one thing that puts a widget in the AccessKit
|
|
tree `iris_core::ui::access::AccessTree` builds and both backends push
|
|
out. A widget author who wants a control to be findable by name (and
|
|
tappable by name, through `ui-trace`/a real screen reader) calls `.label()`
|
|
on it; nothing else is required, and a widget nobody labels is invisible
|
|
to this system at zero cost, not just zero UI.
|
|
|
|
```rust
|
|
let button = rect(Color::LIME)
|
|
.on(CursorSense::click(), move |_, rsc| { ... })
|
|
.label("Add task"); // now findable by uiautomator/AccessKit as "Add task"
|
|
```
|
|
|
|
Two new things a widget author might touch directly:
|
|
|
|
- **`Widget::access_role(&self) -> accesskit::Role`**, default `Unknown`.
|
|
Override it if your widget has a real platform equivalent —
|
|
`TextEdit` now returns `TextInput`/`MultilineTextInput` by `EditMode`.
|
|
Only consulted for a widget that also has a `.label()`; an unlabelled
|
|
widget's `access_role` is never called.
|
|
- **`Widgets::named() -> impl Iterator<Item = WidgetId>`** — every widget
|
|
with an explicit label, for anything else that wants to walk the same
|
|
set `AccessTree` does.
|
|
|
|
Nothing about `Painter`, `draw`, or the layout/move machinery changed —
|
|
this sits entirely beside them, reading `resolved_region`'s output rather
|
|
than participating in producing it.
|
|
|
|
## 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.
|