Files
ai-app/iris/src/widget/position/span.rs
T
irisandClaude Fable 5.1 6102e0d4d9 iris: a dp length unit, resolved against density; crisp glyphs at physical size
Iris asked for this 2026-09-06 (IRIS_TODO.md, "a third length kind beside
relative and pixels ... a unit resolved against the display's density at
layout time"): before this, a Len was abs (physical pixels) or rel/rest
(a fraction of the parent), and the only way to make a design size look
the same physical size on a denser display was a single global multiply
applied after layout -- which the previous commit found is also what
made text blurry.

Len gains a `dp` field, resolved against a `density: f32` (physical
pixels per dp) now carried on UiRenderState/Painter
(`UiRenderState::set_density`/`density()`, `Painter::density()`) and
threaded through every `apply_rest`/`to_uivec2` call site. `len_fns::dp`
/ `Len::dp` construct one, exactly parallel to the existing `abs`/`rel`/
`rest`. A bare number is unaffected (still `abs`, physical pixels) --
`dp` is opt-in.

Text: `TextBuffer::shape` now takes `density` and multiplies
`font_size`/`line_height` (and any span override) by it before handing
them to parley, so the size that reaches the shaper and the rasteriser
(`TextData::place`) is the display's real physical size -- the atlas
holds a bitmap at the resolution it is actually shown at, instead of a
low-resolution one stretched afterward. `GlyphKey.size` already keys on
the resolved `font_size`, so a cache entry is naturally per physical size
with no further change. `TextData` also carries its own `density` copy
for `TextEditCtx::layout` (cursor movement/hit-testing), which shapes
text from an input callback with no `Painter` to read it from.

`Span::gap` and `Padding`'s four sides move from bare `f32` to `Len`, so
`.gap(dp(4))`/`.pad(dp(10))` work the same way any other size does; a
bare number still means physical pixels, unchanged.

Migrated transcript-ui's non-text sizes (row gap/padding, composer
padding) and one example to the new unit, per IRIS_TODO.md's "done when"
list. Android's own density (`DisplayMetrics.density`) is wired to both
copies in `new_peer`; the winit backend has no per-monitor density wired
up yet and stays at the default (1.0).

docs/IRIS.md, docs/LAYOUT.md and IRIS_TODO.md updated next.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 00:35:38 -04:00

162 lines
5.0 KiB
Rust

use crate::prelude::*;
use std::marker::PhantomData;
pub struct Span {
pub children: Vec<StrongWidget>,
pub dir: Dir,
/// A `Len` (not a bare `f32`) so `dp(4)` resolves against the display's
/// density the same way any other size in the tree does -- see
/// `Len::dp`'s field doc. Only the `abs` component (folded from `dp` at
/// draw time, `Widget::draw` below) is meaningful here; `rel`/`rest`
/// were never supported for a gap and still are not.
pub gap: Len,
}
impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis;
let gap = self.gap.apply_rest(painter.density()).abs;
// Phase 1: draw each child once, at the ambient (unmodified, full)
// region a size-only query used to see before this migration, to
// learn its length along the layout axis. This paints real
// primitives at a provisional slot; phase 2 below places each
// child for real via the normal `widget_within` dispatch, which
// only actually redraws it when that slot's *size* differs from
// this provisional one (most children: a resize, since the
// provisional slot is the whole span, not this child's share).
let lens: Vec<Len> = self
.children
.iter()
.map(|child| painter.widget(child).axis(axis))
.collect();
let gap_total = gap * self.children.len().saturating_sub(1) as f32;
let total = lens.iter().fold(Len::abs(gap_total), |s, &l| s + l);
// Phase 2: place each child for real, using the lengths just
// learned -- the same arithmetic this loop always used. The cross-
// axis length of *this* draw (used for `Span`'s own reported size
// below) falls out of each child's real, resolved-width `used`
// here for free -- this is what replaces `desired_ortho`'s former
// duplicate simulation of this same loop (see LAYOUT.md section 4).
let mut start = UiScalar::rel_min();
let mut ortho_len = Len::ZERO;
let mut ortho_mixed = false;
for (child, &len) in self.children.iter().zip(&lens) {
let mut span = UiSpan::FULL;
span.start = start;
if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.abs);
let rel_end = UiScalar::rel(len.rest / total.rest);
let end = (UiScalar::rel_max() + start) - offset;
start = rel_end.within(&start.to(end));
}
start.abs += len.abs;
start.rel += len.rel;
span.end = start;
let mut child_region = UiRegion::from_axis(axis, span, UiSpan::FULL);
if self.dir.sign == Sign::Neg {
child_region.flip(axis);
}
let used = painter.widget_within(child, child_region);
start.abs += gap;
let ortho = used.axis(!axis);
if ortho.rel > 0.0 || ortho.rest > 0.0 {
ortho_mixed = true;
} else {
ortho_len.abs = ortho_len.abs.max(ortho.abs);
}
}
if ortho_mixed {
ortho_len = Len::default();
}
let along = if total.rest == 0.0 && total.rel == 0.0 {
total
} else {
Len::default()
};
Size::from_axis(axis, along, ortho_len)
}
}
impl Span {
pub fn empty(dir: Dir) -> Self {
Self {
children: Vec::new(),
dir,
gap: Len::ZERO,
}
}
pub fn gap(mut self, gap: impl Into<Len>) -> Self {
self.gap = gap.into();
self
}
pub fn push(&mut self, w: StrongWidget) {
self.children.push(w);
}
pub fn pop(&mut self) -> Option<StrongWidget> {
self.children.pop()
}
}
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
pub children: Wa,
pub dir: Dir,
pub gap: Len,
_pd: PhantomData<(State, Tag)>,
}
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc>
for SpanBuilder<Rsc, LEN, Wa, Tag>
{
type Widget = Span;
#[track_caller]
fn run(self, rsc: &mut Rsc) -> Self::Widget {
Span {
children: self.children.add(rsc).arr.into_iter().collect(),
dir: self.dir,
gap: self.gap,
}
}
}
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
SpanBuilder<State, LEN, Wa, Tag>
{
pub fn new(children: Wa, dir: Dir) -> Self {
Self {
children,
dir,
gap: Len::ZERO,
_pd: PhantomData,
}
}
pub fn gap(mut self, gap: impl Into<Len>) -> Self {
self.gap = gap.into();
self
}
}
impl std::ops::Deref for Span {
type Target = Vec<StrongWidget>;
fn deref(&self) -> &Self::Target {
&self.children
}
}
impl std::ops::DerefMut for Span {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.children
}
}