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>
162 lines
5.0 KiB
Rust
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
|
|
}
|
|
}
|