Compare commits

..
Author SHA1 Message Date
iris-ai a123d13490 Merge remote-tracking branch 'upstream/main' into split/17-headless-rig 2026-09-14 02:48:37 -04:00
iris-ai deb9c1b6d7 Detect the rig's binaries, and take the machine out of its comments
The comments described the machine the rig was written on -- "this
machine has no display", "there is a real GPU here", an Android
emulator's GLX quirk -- which says nothing to anyone reading it from a
different checkout. What the reader needs is what the script supplies
and why, which is now all they get.

`sway`, `swaymsg` and, when `--shot` is passed, `grim` are checked up
front and named in the failure, rather than surfacing as a compositor
that would not start.

The `# shellcheck disable=SC2086 -- prose` directive did not parse, so
the suppression was not in effect; the prose moves to its own line.
Clean under shellcheck now.
2026-09-14 00:36:19 -04:00
iris 9d13f15bee Bring the headless rig into the repository
A rendering claim about iris was verified by hand from another checkout,
because the compositor script and the input replay lived in ai-app's
submodule and not here.

`scripts/run-headless.sh` starts a headless sway on its own socket, runs
an example against it and screenshots the result. `rig-input`'s
`replay-touch` drives a recorded gesture in through Wayland's virtual
pointer, since a headless compositor has no input device to move.

`iris::harness` gains the `.touch` parser, so a recording means the same
thing replayed into a harness as into a window rather than being read
twice by two parsers.

The ai-app copy's `--phone` became `--mode`, since which phone is not
iris's business; its `IRIS_SCALE` has nothing to hand a density to here,
so it waits for one.
2026-09-13 23:10:23 -04:00
31 changed files with 197 additions and 1280 deletions

No files matched your search

+59
View File
@@ -202,6 +202,14 @@ impl UiScalar {
} }
} }
/// Undoes `within`, and `None` where the span has a fixed length: every
/// fraction of it lands on the same `rel`, so none can be told apart.
pub fn outside(&self, span: &UiSpan) -> Option<Self> {
let rel = self.rel.lerp_inv(span.start.rel, span.end.rel)?;
let abs = self.abs - rel.lerp(span.start.abs, span.end.abs);
Some(Self { rel, abs })
}
pub fn within_len(&self, len: UiScalar) -> Self { pub fn within_len(&self, len: UiScalar) -> Self {
self.within(&UiSpan { self.within(&UiSpan {
start: UiScalar::ZERO, start: UiScalar::ZERO,
@@ -270,6 +278,13 @@ impl UiSpan {
} }
} }
pub fn outside(&self, parent: &Self) -> Option<Self> {
Some(Self {
start: self.start.outside(parent)?,
end: self.end.outside(parent)?,
})
}
pub const fn len(&self) -> UiScalar { pub const fn len(&self) -> UiScalar {
self.end - self.start self.end - self.start
} }
@@ -382,6 +397,50 @@ impl UiRegion {
} }
} }
/// Taking a drawing out of one box and putting it in another, checked once
/// for a whole subtree so that applying it cannot fail.
///
/// A box of a fixed length holds each part as an offset from its start rather
/// than as a fraction of it, so those parts can be carried to a box of the
/// same length but never stretched to a different one.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Remap {
from: UiRegion,
to: UiRegion,
}
impl Remap {
pub fn new(from: UiRegion, to: UiRegion) -> Option<Self> {
[Axis::X, Axis::Y]
.into_iter()
.all(|axis| {
let (from, to) = (from.axis(axis), to.axis(axis));
from.start.rel != from.end.rel || from.len() == to.len()
})
.then_some(Self { from, to })
}
pub fn apply(&self, region: UiRegion) -> UiRegion {
UiRegion {
x: Self::span(region.x, self.from.x, self.to.x),
y: Self::span(region.y, self.from.y, self.to.y),
}
}
fn span(span: UiSpan, from: UiSpan, to: UiSpan) -> UiSpan {
match span.outside(&from) {
Some(out) => out.within(&to),
// `new` admits this only where the two are the same length, so
// the difference between their starts is the whole move.
None => {
let mut span = span;
span.shift(to.start - from.start);
span
}
}
}
}
impl Display for UiRegion { impl Display for UiRegion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
+4 -46
View File
@@ -1,10 +1,11 @@
use crate::{UiRegion, util::Id, util::Vec2}; use crate::{UiRegion, util::Id};
use wgpu::*; use wgpu::*;
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct WindowUniform { pub struct WindowUniform {
pub dim: Vec2, pub width: f32,
pub height: f32,
} }
#[repr(C)] #[repr(C)]
@@ -12,17 +13,15 @@ pub struct WindowUniform {
pub struct PrimitiveInstance { pub struct PrimitiveInstance {
pub region: UiRegion, pub region: UiRegion,
pub mask_idx: MaskIdx, pub mask_idx: MaskIdx,
pub move_idx: MoveIdx,
} }
impl PrimitiveInstance { impl PrimitiveInstance {
const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![ const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![
0 => Float32x2, 0 => Float32x2,
1 => Float32x2, 1 => Float32x2,
2 => Float32x2, 2 => Float32x2,
3 => Float32x2, 3 => Float32x2,
4 => Uint32, 4 => Uint32,
5 => Uint32,
]; ];
pub fn desc() -> VertexBufferLayout<'static> { pub fn desc() -> VertexBufferLayout<'static> {
@@ -44,45 +43,4 @@ impl MaskIdx {
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Mask { pub struct Mask {
pub region: UiRegion, pub region: UiRegion,
pub move_idx: MoveIdx,
}
/// Its own type rather than another `Id<u32>`, because it sits beside
/// `MaskIdx` in an instance and the two must not be swappable.
#[repr(transparent)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MoveIdx(u32);
impl MoveIdx {
pub const NONE: Self = Self(u32::MAX);
pub(crate) fn slot(idx: usize) -> Self {
Self(idx as u32)
}
pub(crate) fn idx(self) -> usize {
self.0 as usize
}
}
/// One link of the chain a primitive's position is resolved through: the box
/// its contents are placed within, given in the coordinates of the slot it
/// names. Moving or resizing a subtree writes its own slot and nothing else.
///
/// The identity is `UiRegion::FULL`, not zero: a zeroed entry is a box of no
/// extent, which collapses everything under it to a point.
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct MoveOffset {
pub region: UiRegion,
pub parent: MoveIdx,
}
unsafe impl bytemuck::Pod for MoveOffset {}
unsafe impl bytemuck::Zeroable for MoveOffset {}
impl MoveOffset {
pub fn new(parent: MoveIdx, region: UiRegion) -> Self {
Self { region, parent }
}
} }
+11 -38
View File
@@ -17,7 +17,7 @@ mod texture;
mod util; mod util;
pub use atlas::*; pub use atlas::*;
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset}; pub use data::{Mask, MaskIdx};
pub use primitive::*; pub use primitive::*;
const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
@@ -34,7 +34,6 @@ pub struct UiRenderNode {
active: Vec<usize>, active: Vec<usize>,
window_buffer: Buffer, window_buffer: Buffer,
masks: ArrBuf<Mask>, masks: ArrBuf<Mask>,
moves: ArrBuf<MoveOffset>,
} }
struct RenderLayer { struct RenderLayer {
@@ -128,35 +127,32 @@ impl UiRenderNode {
for primitive in &mut self.primitives { for primitive in &mut self.primitives {
primitive.render.update(ui); primitive.render.update(ui);
} }
let mut regroup = false;
if ui.masks.changed { if ui.masks.changed {
ui.masks.changed = false; ui.masks.changed = false;
regroup |= self.masks.update(device, queue, &ui.masks[..]); if self.masks.update(device, queue, &ui.masks[..]) {
}
if ui_render.moves.changed {
ui_render.moves.changed = false;
regroup |= self.moves.update(device, queue, ui_render.moves.entries());
}
if regroup {
self.shared_group = Self::shared_group( self.shared_group = Self::shared_group(
device, device,
&self.shared_layout, &self.shared_layout,
&self.window_buffer, &self.window_buffer,
&self.masks, &self.masks,
&self.moves,
); );
} }
} }
}
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) { pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
let size = size.into(); let size = size.into();
let slice = &[WindowUniform { dim: size }]; let slice = &[WindowUniform {
width: size.x,
height: size.y,
}];
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice)); queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
} }
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self { pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
let window_uniform = WindowUniform { let window_uniform = WindowUniform {
dim: Vec2::new(config.width as f32, config.height as f32), width: config.width as f32,
height: config.height as f32,
}; };
let window_buffer = device.create_buffer_init(&BufferInitDescriptor { let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("window"), label: Some("window"),
@@ -170,13 +166,7 @@ impl UiRenderNode {
BufferUsages::STORAGE | BufferUsages::COPY_DST, BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui masks", "ui masks",
); );
let moves = ArrBuf::new( let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui move offsets",
);
let shared_group =
Self::shared_group(device, &shared_layout, &window_buffer, &masks, &moves);
Self { Self {
shared_layout, shared_layout,
@@ -187,7 +177,6 @@ impl UiRenderNode {
layers: HashMap::default(), layers: HashMap::default(),
active: Vec::new(), active: Vec::new(),
masks, masks,
moves,
} }
} }
@@ -263,8 +252,7 @@ impl UiRenderNode {
}) })
} }
/// What every draw in the ui is given: the window, the masks and the /// What every draw in the ui is given: the window and the masks.
/// move chain every position is resolved through.
fn shared_layout(device: &Device) -> BindGroupLayout { fn shared_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor { device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[ entries: &[
@@ -288,16 +276,6 @@ impl UiRenderNode {
}, },
count: None, count: None,
}, },
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: BufferSize::new(size_of::<MoveOffset>() as u64),
},
count: None,
},
], ],
label: Some("ui shared"), label: Some("ui shared"),
}) })
@@ -308,7 +286,6 @@ impl UiRenderNode {
layout: &BindGroupLayout, layout: &BindGroupLayout,
window: &Buffer, window: &Buffer,
masks: &ArrBuf<Mask>, masks: &ArrBuf<Mask>,
moves: &ArrBuf<MoveOffset>,
) -> BindGroup { ) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor { device.create_bind_group(&BindGroupDescriptor {
layout, layout,
@@ -321,10 +298,6 @@ impl UiRenderNode {
binding: 1, binding: 1,
resource: masks.buffer.as_entire_binding(), resource: masks.buffer.as_entire_binding(),
}, },
BindGroupEntry {
binding: 2,
resource: moves.buffer.as_entire_binding(),
},
], ],
label: Some("ui shared"), label: Some("ui shared"),
}) })
+5 -11
View File
@@ -3,7 +3,7 @@ use std::{any::TypeId, marker::PhantomData};
use crate::{ use crate::{
Color, TextureHandle, UiData, UiRegion, WidgetId, Color, TextureHandle, UiData, UiRegion, WidgetId,
render::{ render::{
data::{MaskIdx, MoveIdx, PrimitiveInstance}, data::{MaskIdx, PrimitiveInstance},
page::GlyphRender, page::GlyphRender,
texture::ImageRender, texture::ImageRender,
}, },
@@ -246,7 +246,6 @@ impl LayerDraws {
primitive, primitive,
region, region,
mask_idx, mask_idx,
move_idx,
}: PrimitiveInst<P>, }: PrimitiveInst<P>,
) -> PrimitiveHandle { ) -> PrimitiveHandle {
self.updated = true; self.updated = true;
@@ -259,11 +258,7 @@ impl LayerDraws {
.get_or_insert_with(InstanceList::new::<P>) .get_or_insert_with(InstanceList::new::<P>)
.push( .push(
id, id,
PrimitiveInstance { PrimitiveInstance { region, mask_idx },
region,
mask_idx,
move_idx,
},
bytemuck::bytes_of(&primitive), bytemuck::bytes_of(&primitive),
); );
PrimitiveHandle { PrimitiveHandle {
@@ -309,7 +304,6 @@ pub struct PrimitiveInst<P> {
pub primitive: P, pub primitive: P,
pub region: UiRegion, pub region: UiRegion,
pub mask_idx: MaskIdx, pub mask_idx: MaskIdx,
pub move_idx: MoveIdx,
} }
pub struct PrimitiveChange { pub struct PrimitiveChange {
@@ -353,7 +347,7 @@ impl RectPrimitive {
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph /// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects. /// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
#[repr(C)] #[repr(C, align(8))]
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct GlyphPrimitive { pub struct GlyphPrimitive {
pub uv_min: Vec2, pub uv_min: Vec2,
@@ -364,8 +358,8 @@ pub struct GlyphPrimitive {
pub flags: u32, pub flags: u32,
} }
// Manual rather than derived: `Vec2`'s alignment leaves four bytes of padding // Manual rather than derived: the align(8) leaves four bytes of padding, which
// here, which is how WGSL lays the struct out. // is how WGSL lays the struct out.
unsafe impl bytemuck::Pod for GlyphPrimitive {} unsafe impl bytemuck::Pod for GlyphPrimitive {}
unsafe impl bytemuck::Zeroable for GlyphPrimitive {} unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
impl Primitive for GlyphPrimitive { impl Primitive for GlyphPrimitive {
+8 -62
View File
@@ -7,8 +7,6 @@
var<uniform> window: WindowUniform; var<uniform> window: WindowUniform;
@group(0) @binding(1) @group(0) @binding(1)
var<storage> masks: array<Mask>; var<storage> masks: array<Mask>;
@group(0) @binding(2)
var<storage> move_offsets: array<MoveOffset>;
struct WindowUniform { struct WindowUniform {
dim: vec2<f32>, dim: vec2<f32>,
@@ -17,49 +15,6 @@ struct WindowUniform {
struct Mask { struct Mask {
x: UiSpan, x: UiSpan,
y: UiSpan, y: UiSpan,
move_idx: u32,
}
struct MoveOffset {
x: UiSpan,
y: UiSpan,
parent: u32,
}
struct Region {
x: UiSpan,
y: UiSpan,
}
const MOVE_NONE: u32 = 4294967295u;
// Keep in step with `iris_core::CHAIN_LIMIT`. It bounds a malformed cycle
// rather than any real tree, and the CPU walk uses the same number so both
// resolve a deep one the same way.
const CHAIN_LIMIT: u32 = 64u;
fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar {
return UiScalar(
mix(p.start.rel, p.end.rel, s.rel),
s.abs + mix(p.start.abs, p.end.abs, s.rel),
);
}
fn span_within(s: UiSpan, p: UiSpan) -> UiSpan {
return UiSpan(scalar_within(s.start, p), scalar_within(s.end, p));
}
fn resolve_move(idx: u32, local: Region) -> Region {
var r = local;
var at = idx;
for (var step = 0u; step < CHAIN_LIMIT; step++) {
if at == MOVE_NONE {
break;
}
let entry = move_offsets[at];
r = Region(span_within(r.x, entry.x), span_within(r.y, entry.y));
at = entry.parent;
}
return r;
} }
struct UiSpan { struct UiSpan {
@@ -78,7 +33,6 @@ struct InstanceInput {
@location(2) y_start: vec2<f32>, @location(2) y_start: vec2<f32>,
@location(3) y_end: vec2<f32>, @location(3) y_end: vec2<f32>,
@location(4) mask_idx: u32, @location(4) mask_idx: u32,
@location(5) move_idx: u32,
} }
struct VertexOutput { struct VertexOutput {
@@ -98,15 +52,10 @@ fn vs_main(
) -> VertexOutput { ) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
let local = Region( let top_left_rel = vec2(in.x_start.x, in.y_start.x);
UiSpan(UiScalar(in.x_start.x, in.x_start.y), UiScalar(in.x_end.x, in.x_end.y)), let top_left_abs = vec2(in.x_start.y, in.y_start.y);
UiSpan(UiScalar(in.y_start.x, in.y_start.y), UiScalar(in.y_end.x, in.y_end.y)), let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
); let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
let r = resolve_move(in.move_idx, local);
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
let top_left_abs = vec2(r.x.start.abs, r.y.start.abs);
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
let bot_right_abs = vec2(r.x.end.abs, r.y.end.abs);
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs); let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs); let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
@@ -132,13 +81,10 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
return color; return color;
} }
let mask = masks[in.mask_idx]; let mask = masks[in.mask_idx];
// Its own chain, not the drawn primitive's, so a stationary viewport let tl = vec2(mask.x.start.rel, mask.y.start.rel);
// clips content that moves inside it. let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
let m = resolve_move(mask.move_idx, Region(mask.x, mask.y)); let br = vec2(mask.x.end.rel, mask.y.end.rel);
let tl = vec2(m.x.start.rel, m.y.start.rel); let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
let tl_abs = vec2(m.x.start.abs, m.y.start.abs);
let br = vec2(m.x.end.rel, m.y.end.rel);
let br_abs = vec2(m.x.end.abs, m.y.end.abs);
let top_left = floor(tl * window.dim) + floor(tl_abs); let top_left = floor(tl * window.dim) + floor(tl_abs);
let bot_right = floor(br * window.dim) + floor(br_abs); let bot_right = floor(br * window.dim) + floor(br_abs);
+1 -6
View File
@@ -1,4 +1,4 @@
use crate::{LayerId, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId}; use crate::{LayerId, MaskIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId};
/// important non rendering data for retained drawing /// important non rendering data for retained drawing
#[derive(Debug)] #[derive(Debug)]
@@ -15,11 +15,6 @@ pub struct ActiveData {
pub size_deps: Vec<WidgetId>, pub size_deps: Vec<WidgetId>,
/// Whether it read the output's size, and so is wrong when that changes. /// Whether it read the output's size, and so is wrong when that changes.
pub reads_output: bool, pub reads_output: bool,
/// The slot its primitives are positioned through: its own if its parent
/// placed it, otherwise the nearest ancestor that has one.
pub move_idx: MoveIdx,
/// The slot `region` is given in, which is whatever its parent drew in.
pub parent_move: MoveIdx,
pub mask: MaskIdx, pub mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
} }
+1 -90
View File
@@ -1,14 +1,7 @@
use crate::{ use crate::{
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, UiRegion, WeakWidget, Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
WidgetId, Widgets,
util::{Arena, Id, TrackedArena},
}; };
/// How far the shader will walk a move chain. It bounds a malformed cycle
/// rather than any real tree; `Moves::resolve` uses the same number so the
/// two agree on what a deep tree resolves to.
pub const CHAIN_LIMIT: u32 = 64;
mod active; mod active;
mod painter; mod painter;
mod render_state; mod render_state;
@@ -27,88 +20,6 @@ pub struct UiData {
pub masks: TrackedArena<Mask, u32>, pub masks: TrackedArena<Mask, u32>,
} }
/// Where each widget's drawing sits relative to its parent's slot, so moving
/// a subtree writes one entry rather than every descendant's primitives.
#[derive(Default)]
pub struct Moves {
arena: Arena<MoveOffset, u32>,
pub changed: bool,
}
impl Moves {
pub fn push(&mut self, parent: MoveIdx, region: UiRegion) -> MoveIdx {
self.changed = true;
MoveIdx::slot(self.arena.push(MoveOffset::new(parent, region)).idx())
}
/// Re-points a slot at a different parent, for a widget drawn somewhere
/// else in the tree than it was.
pub fn set_parent(&mut self, idx: MoveIdx, parent: MoveIdx) {
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
if entry.parent != parent {
entry.parent = parent;
self.changed = true;
}
}
pub fn remove(&mut self, idx: MoveIdx) {
self.changed = true;
self.arena.remove(Id::preset(idx.idx() as u32));
}
/// Sets the box a slot's contents are placed within, itself given in the
/// coordinates of its parent slot.
pub fn set(&mut self, idx: MoveIdx, region: UiRegion) {
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
if entry.region != region {
entry.region = region;
self.changed = true;
}
}
/// Composes a region held in `idx`'s coordinates down the chain, which is
/// the same walk the vertex shader does.
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
let mut region = local;
let mut at = idx;
for _ in 0..CHAIN_LIMIT {
if at == MoveIdx::NONE {
return region;
}
let entry = self.arena[at.idx()];
region = region.within(&entry.region);
at = entry.parent;
}
debug_assert!(
at == MoveIdx::NONE,
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
and the shader stops at the same depth"
);
region
}
/// How many slots a region in `idx` is composed through, which is what
/// the shader's walk costs per primitive.
pub fn depth(&self, idx: MoveIdx) -> usize {
let mut depth = 0;
let mut at = idx;
while at != MoveIdx::NONE && depth < CHAIN_LIMIT as usize {
at = self.arena[at.idx()].parent;
depth += 1;
}
depth
}
pub fn entries(&self) -> &[MoveOffset] {
&self.arena
}
pub fn clear(&mut self) {
self.changed = true;
self.arena = Arena::default();
}
}
pub trait UiRsc { pub trait UiRsc {
fn ui(&self) -> &UiData; fn ui(&self) -> &UiData;
fn ui_mut(&mut self) -> &mut UiData; fn ui_mut(&mut self) -> &mut UiData;
+10 -39
View File
@@ -2,8 +2,8 @@ use crate::{
Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
PrimitiveKind, TexturePrimitive, TexturePrimitive,
}, },
util::Vec2, util::Vec2,
}; };
@@ -13,7 +13,6 @@ pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState, pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc, pub(super) rsc: &'a mut dyn UiRsc,
/// This widget's box, in the coordinates of `move_idx`.
pub(super) region: UiRegion, pub(super) region: UiRegion,
pub(super) mask: MaskIdx, pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
@@ -22,9 +21,6 @@ pub struct Painter<'a> {
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>, pub(super) size_deps: Vec<WidgetId>,
pub(super) reads_output: bool, pub(super) reads_output: bool,
/// The slot this widget's primitives are positioned through: its own if
/// its parent placed it, otherwise the nearest ancestor that has one.
pub(super) move_idx: MoveIdx,
pub layer: usize, pub layer: usize,
pub(super) id: WidgetId, pub(super) id: WidgetId,
} }
@@ -45,7 +41,6 @@ impl<'a> Painter<'a> {
primitive, primitive,
region, region,
mask_idx: self.mask, mask_idx: self.mask,
move_idx: self.move_idx,
}, },
); );
self.push_primitive(h); self.push_primitive(h);
@@ -72,47 +67,29 @@ impl<'a> Painter<'a> {
pub fn set_mask(&mut self, region: UiRegion) { pub fn set_mask(&mut self, region: UiRegion) {
assert!(self.mask == MaskIdx::NONE); assert!(self.mask == MaskIdx::NONE);
self.mask = self.rsc.ui_mut().masks.push(Mask { self.mask = self.rsc.ui_mut().masks.push(Mask { region });
region,
move_idx: self.move_idx,
});
} }
/// Draws a widget within this widget's region. /// Draws a widget within this widget's region.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> { pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
self.widget_at(id, self.region, false) self.widget_at(id, self.region)
} }
/// Draws a widget somewhere within this one. /// Draws a widget somewhere within this one. Drawing one a second time
/// gives it a new box, keeping the drawing it already has where it can.
pub fn widget_within<'s, W: ?Sized>( pub fn widget_within<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let region = region.within(&self.region); let region = region.within(&self.region);
self.widget_at(id, region, false) self.widget_at(id, region)
}
/// Draws a child this widget decides the box of, and may decide again
/// once it knows what the child came to. The child gets a slot of its
/// own, so placing it a second time writes one entry however much it
/// drew -- moved or resized alike, since everything under the slot is
/// held as a fraction of its box. A child drawn any other way has no slot
/// and can only be given a different box by drawing again.
pub fn place<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
let region = region.within(&self.region);
self.widget_at(id, region, true)
} }
fn widget_at<'s, W: ?Sized>( fn widget_at<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
slotted: bool,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
// A child listed twice would be moved twice. // A child listed twice would be moved twice.
if !self.children.contains(&id.id()) { if !self.children.contains(&id.id()) {
@@ -123,8 +100,6 @@ impl<'a> Painter<'a> {
id.id(), id.id(),
region, region,
Some(self.id), Some(self.id),
self.move_idx,
slotted,
self.mask, self.mask,
None, None,
self.rsc, self.rsc,
@@ -184,8 +159,6 @@ impl<'a> Painter<'a> {
} }
} }
/// This widget's box, in the coordinates its own primitives are written
/// in -- so a region composed `within` it may be drawn directly.
pub fn region(&self) -> UiRegion { pub fn region(&self) -> UiRegion {
self.region self.region
} }
@@ -197,13 +170,11 @@ impl<'a> Painter<'a> {
self.state.output_size self.state.output_size
} }
/// This widget's box in pixels. Resolved against the output's size and /// This widget's box in pixels. Resolved against the output's size, so a
/// the boxes it sits within, so a widget that reads it draws again when /// widget that reads it draws again when the output changes.
/// the output changes.
pub fn px_size(&mut self) -> Vec2 { pub fn px_size(&mut self) -> Vec2 {
self.reads_output = true; self.reads_output = true;
let region = self.state.moves.resolve(self.move_idx, self.region); self.region.size().to_abs(self.state.output_size)
region.size().to_abs(self.state.output_size)
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
+46 -141
View File
@@ -1,11 +1,9 @@
use crate::{ use crate::{
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion, ActiveData, Axis, DrawLayers, IdLike, MaskIdx, OnResize, Painter, PixelRegion, Remap, Size,
Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
util::{HashMap, HashSet, Vec2}, util::{HashMap, HashSet, Vec2, forget_ref},
}; };
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
pub struct UiRenderState { pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>, pub active: HashMap<WidgetId, ActiveData>,
pub layers: DrawLayers, pub layers: DrawLayers,
@@ -14,10 +12,6 @@ pub struct UiRenderState {
old_root: Option<WidgetId>, old_root: Option<WidgetId>,
resized: bool, resized: bool,
draw_started: HashSet<WidgetId>, draw_started: HashSet<WidgetId>,
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
/// replaces that while its children go on pointing at the slot.
slots: HashMap<WidgetId, MoveIdx>,
pub moves: Moves,
} }
impl UiRenderState { impl UiRenderState {
@@ -29,8 +23,6 @@ impl UiRenderState {
old_root: None, old_root: None,
resized: false, resized: false,
draw_started: Default::default(), draw_started: Default::default(),
slots: Default::default(),
moves: Default::default(),
} }
} }
@@ -84,17 +76,7 @@ impl UiRenderState {
self.clear(rsc); self.clear(rsc);
// free all resources & cache // free all resources & cache
if let Some(id) = root { if let Some(id) = root {
self.draw_inner( self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, None, rsc);
0,
id.id(),
UiRegion::FULL,
None,
MoveIdx::NONE,
false,
MaskIdx::NONE,
None,
rsc,
);
} }
} }
@@ -106,15 +88,13 @@ impl UiRenderState {
id: WidgetId, id: WidgetId,
region: UiRegion, region: UiRegion,
parent: Option<WidgetId>, parent: Option<WidgetId>,
parent_move: MoveIdx,
slotted: bool,
mask: MaskIdx, mask: MaskIdx,
old_children: Option<Vec<WidgetId>>, old_children: Option<Vec<WidgetId>>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> Size { ) -> Size {
let mut old_children = old_children.unwrap_or_default(); let mut old_children = old_children.unwrap_or_default();
if self.active.contains_key(&id) { if self.active.contains_key(&id) {
if let Some(size) = self.try_reuse(id, region, parent_move, rsc) { if let Some(size) = self.try_reuse(id, region, rsc) {
return size; return size;
} }
// if not, then maintain resize and track old children to remove unneeded // if not, then maintain resize and track old children to remove unneeded
@@ -123,21 +103,12 @@ impl UiRenderState {
} }
// draw widget // draw widget
let (move_idx, local) = match slotted {
// Its box becomes its slot's, so it draws in the slot's own
// coordinates and the box it was given is one entry to rewrite.
true => (self.move_slot(id, parent_move, region), UiRegion::FULL),
false => {
self.drop_slot(id);
(parent_move, region)
}
};
rsc.widgets_mut().needs_redraw.remove(&id); rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.insert(id); self.draw_started.insert(id);
let mut painter = Painter { let mut painter = Painter {
state: self, state: self,
region: local, region,
mask, mask,
layer, layer,
id, id,
@@ -146,7 +117,6 @@ impl UiRenderState {
children: Vec::new(), children: Vec::new(),
size_deps: Vec::new(), size_deps: Vec::new(),
reads_output: false, reads_output: false,
move_idx,
rsc, rsc,
}; };
@@ -157,14 +127,13 @@ impl UiRenderState {
let Painter { let Painter {
state: _, state: _,
rsc: _, rsc: _,
region: _, region,
mask, mask,
textures, textures,
primitives, primitives,
children, children,
size_deps, size_deps,
reads_output, reads_output,
move_idx,
layer, layer,
id, id,
} = painter; } = painter;
@@ -186,8 +155,6 @@ impl UiRenderState {
children, children,
size_deps, size_deps,
reads_output, reads_output,
move_idx,
parent_move,
mask, mask,
layer, layer,
}; };
@@ -204,114 +171,49 @@ impl UiRenderState {
size size
} }
/// The slot a widget's box is held in, made on its first placed draw and
/// kept until it stops being drawn -- a redraw replaces its `ActiveData`
/// while descendants go on naming the slot.
fn move_slot(&mut self, id: WidgetId, parent: MoveIdx, region: UiRegion) -> MoveIdx {
if let Some(&idx) = self.slots.get(&id) {
self.moves.set_parent(idx, parent);
self.moves.set(idx, region);
return idx;
}
let idx = self.moves.push(parent, region);
self.slots.insert(id, idx);
idx
}
/// Gives up a slot a widget no longer needs, because it is drawn somewhere
/// that does not place it. Its descendants name it, so this is only
/// reached where they are about to be drawn again.
fn drop_slot(&mut self, id: WidgetId) {
if let Some(idx) = self.slots.remove(&id) {
self.moves.remove(idx);
}
}
/// The drawing a widget already has, kept for a new box if the box has not /// The drawing a widget already has, kept for a new box if the box has not
/// changed in a way it depends on. /// changed in a way it depends on.
fn try_reuse( fn try_reuse(&mut self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> Option<Size> {
&mut self,
id: WidgetId,
region: UiRegion,
parent_move: MoveIdx,
rsc: &mut dyn UiRsc,
) -> Option<Size> {
if rsc.widgets().needs_redraw.contains(&id) { if rsc.widgets().needs_redraw.contains(&id) {
return None; return None;
} }
let active = self.active.get(&id)?; let active = self.active.get(&id)?;
// Drawn somewhere else in the tree: its box is in coordinates it no let (size, old) = (active.size, active.region);
// longer sits in, and its slot names the wrong parent.
if active.parent_move != parent_move {
return None;
}
let (size, old, slot) = (active.size, active.region, active.move_idx);
if old == region { if old == region {
return Some(size); return Some(size);
} }
// Only a placed widget can be given a different box without drawing // TODO: epsilon?
// again: everything it drew is a fraction of its slot's box, so one if old.size() != region.size() && !self.reusable(id, region, rsc) {
// entry says where all of it went.
if slot == parent_move {
return None; return None;
} }
let mut changed = [false; 2]; // Its drawing stands, if the new box can be reached from the old one.
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) { self.mov(id, &Remap::new(old, region)?);
*c = region.axis(axis).len() != old.axis(axis).len();
}
if changed.iter().any(|&c| c) {
let widget = rsc.widgets().get_dyn(id)?;
let redraws = AXES
.into_iter()
.zip(changed)
.any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale);
if redraws {
return None;
}
}
// Anything under it that has to be drawn again is drawn by drawing
// this, because whatever reads that widget's size sits in between and
// has to lay out around whatever it comes to.
if changed.iter().any(|&c| c) && self.redraws_under(id, changed, rsc) {
return None;
}
self.moves.set(slot, region);
self.active.get_mut(&id).unwrap().region = region;
Some(size) Some(size)
} }
/// Whether anything under `id` would have to be drawn again for the box /// Whether the widget can keep the drawing it has and be given `region`
/// it is a fraction of changing length, `changed` saying which axes of /// instead, asked one axis at a time: a change on an axis it does not
/// that box did. /// depend on costs nothing, whatever it depends on elsewhere.
/// fn reusable(&self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> bool {
/// A part of a box with no relative extent on an axis is a fixed length,
/// held as offsets from that box's start, and composing anything into it
/// leaves no relative extent either. So a widget whose own box did not
/// change length has no descendant whose box did, and the walk stops
/// there -- an 80-wide child of a widened row is not asked at all.
fn redraws_under(&self, id: WidgetId, changed: [bool; 2], rsc: &dyn UiRsc) -> bool {
let Some(active) = self.active.get(&id) else { let Some(active) = self.active.get(&id) else {
return false; return false;
}; };
active.children.iter().any(|&child| { let Some(widget) = rsc.widgets().get_dyn(id) else {
let Some(data) = self.active.get(&child) else {
return false; return false;
}; };
let mut own = changed; [Axis::X, Axis::Y].into_iter().all(|axis| {
for (axis, c) in AXES.into_iter().zip(own.iter_mut()) { let offered = region.axis(axis).len();
*c &= data.region.axis(axis).len().rel != 0.0; let had = active.region.axis(axis).len();
match widget.on_resize(axis) {
OnResize::Scale => true,
// `Translate` is not acted on yet, and cannot be until a
// drawing can sit somewhere other than its box. `region` is
// both the box a widget was given and the box its primitives
// are in, and `mov` remaps from it -- so carrying a drawing at
// its old size while the box grows makes the next move stretch
// it. The offset chain is what separates the two.
OnResize::Translate | OnResize::Redraw => offered == had,
} }
if !own.iter().any(|&c| c) {
return false;
}
let redraws = match rsc.widgets().get_dyn(child) {
Some(widget) => AXES
.into_iter()
.zip(own)
.any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale),
None => true,
};
redraws || self.redraws_under(child, own, rsc)
}) })
} }
@@ -319,13 +221,27 @@ impl UiRenderState {
let Some(widget) = rsc.widgets().get_dyn(id) else { let Some(widget) = rsc.widgets().get_dyn(id) else {
return true; return true;
}; };
AXES.into_iter().all(|axis| { [Axis::X, Axis::Y].into_iter().all(|axis| {
widget widget
.size_hint(axis) .size_hint(axis)
.is_none_or(|hint| hint == size.axis(axis)) .is_none_or(|hint| hint == size.axis(axis))
}) })
} }
fn mov(&mut self, id: WidgetId, remap: &Remap) {
let active = self.active.get_mut(&id).unwrap();
for h in &active.primitives {
let region = self.layers[h.layer].region_mut(h);
*region = remap.apply(*region);
}
active.region = remap.apply(active.region);
// SAFETY: children cannot be recursive
let children = unsafe { forget_ref(&active.children) };
for child in children {
self.mov(*child, remap);
}
}
/// NOTE: instance textures are cleared and self.textures freed /// NOTE: instance textures are cleared and self.textures freed
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> { fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
let mut active = self.active.remove(&id); let mut active = self.active.remove(&id);
@@ -352,10 +268,6 @@ impl UiRenderState {
self.remove_rec(*c, rsc); self.remove_rec(*c, rsc);
} }
} }
// After the descendants, whose slots name this one as their parent.
if let Some(idx) = self.slots.remove(&id) {
self.moves.remove(idx);
}
inst inst
} }
@@ -363,8 +275,6 @@ impl UiRenderState {
for (_, active) in self.active.drain() { for (_, active) in self.active.drain() {
rsc.on_undraw(&active); rsc.on_undraw(&active);
} }
self.slots.clear();
self.moves.clear();
self.layers.clear(); self.layers.clear();
rsc.widgets_mut().needs_redraw.clear(); rsc.widgets_mut().needs_redraw.clear();
rsc.free(); rsc.free();
@@ -412,11 +322,8 @@ impl UiRenderState {
} }
} }
/// Where a widget is on screen: its box composed through the boxes it
/// sits within, which is the walk the vertex shader does.
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> { pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let active = self.active.get(&id.id())?; let region = self.active.get(&id.id())?.region;
let region = self.moves.resolve(active.parent_move, active.region);
Some(region.to_px(self.output_size)) Some(region.to_px(self.output_size))
} }
@@ -449,8 +356,6 @@ impl UiRenderState {
id, id,
active.region, active.region,
active.parent, active.parent,
active.parent_move,
active.move_idx != active.parent_move,
active.mask, active.mask,
Some(active.children), Some(active.children),
rsc, rsc,
-4
View File
@@ -34,10 +34,6 @@ impl<T, I: IdNum> Arena<T, I> {
self.tracker.free(id); self.tracker.free(id);
self.data[i] self.data[i]
} }
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
&mut self.data[id.idx()]
}
} }
impl<T, I: IdNum> Default for Arena<T, I> { impl<T, I: IdNum> Default for Arena<T, I> {
+10 -1
View File
@@ -1,5 +1,6 @@
pub const trait LerpUtil { pub const trait LerpUtil: Sized {
fn lerp(self, from: Self, to: Self) -> Self; fn lerp(self, from: Self, to: Self) -> Self;
fn lerp_inv(self, from: Self, to: Self) -> Option<Self>;
} }
const impl LerpUtil for f32 { const impl LerpUtil for f32 {
@@ -8,6 +9,14 @@ const impl LerpUtil for f32 {
fn lerp(self, from: Self, to: Self) -> Self { fn lerp(self, from: Self, to: Self) -> Self {
from + (to - from) * self from + (to - from) * self
} }
/// inverse of lerp, and `None` where `from` and `to` are the same point:
/// every input lerps to it, so there is no one answer to come back to.
fn lerp_inv(self, from: Self, to: Self) -> Option<Self> {
match to == from {
true => None,
false => Some((self - from) / (to - from)),
}
}
} }
macro_rules! impl_op { macro_rules! impl_op {
+5
View File
@@ -1,3 +1,8 @@
#[allow(clippy::missing_safety_doc)]
pub(crate) unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
unsafe { std::mem::transmute::<&T, &T>(x) }
}
#[allow(clippy::missing_safety_doc)] #[allow(clippy::missing_safety_doc)]
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T { pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
unsafe { std::mem::transmute::<&mut T, &mut T>(x) } unsafe { std::mem::transmute::<&mut T, &mut T>(x) }
+1 -5
View File
@@ -1,11 +1,7 @@
use crate::util::impl_op; use crate::util::impl_op;
use std::{hash::Hash, ops::*}; use std::{hash::Hash, ops::*};
/// `align(8)` because that is WGSL's alignment for a `vec2<f32>`, so any GPU #[repr(C)]
/// struct holding one is laid out the way its shader reads it without having
/// to say so itself. Those structs still need a manual `unsafe impl Pod`,
/// since the trailing padding this introduces is what `derive(Pod)` refuses.
#[repr(C, align(8))]
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vec2 { pub struct Vec2 {
pub x: f32, pub x: f32,
-31
View File
@@ -1,31 +0,0 @@
//! The seeded random tree `tests/generated.rs` checks, drawn so it can be
//! looked at. `IRIS_SEED` and `IRIS_DEPTH` choose which one.
use iris::prelude::*;
use std::collections::HashMap;
fn env(name: &str, fallback: u64) -> u64 {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(fallback)
}
fn main() {
DefaultApp::<State>::run();
}
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
}
impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
let seed = env("IRIS_SEED", 1);
let depth = env("IRIS_DEPTH", 4) as usize;
let (root, _) = iris::random::grow(rsc, seed, depth, &HashMap::new());
ui_state.set_root(root);
Self { ui_state }
}
}
-1
View File
@@ -8,7 +8,6 @@
pub mod default; pub mod default;
pub mod event; pub mod event;
pub mod harness; pub mod harness;
pub mod random;
pub mod widget; pub mod widget;
pub use iris_core as core; pub use iris_core as core;
-161
View File
@@ -1,161 +0,0 @@
//! A seeded random widget tree, for tests and for looking at.
//!
//! One seed is one tree, on any machine and after any upgrade, so a test can
//! grow the same tree twice and a failing seed is reproduced by its number.
//! `examples/random.rs` draws one; `tests/generated.rs` checks that laying one
//! out again lands where growing it from scratch would.
use crate::prelude::*;
use std::collections::HashMap;
/// The declared lengths of one `SetSize`, by axis.
pub type Lens = [Option<Len>; 2];
/// xorshift64, written out rather than taken from a crate so that a seed
/// keeps meaning the same tree.
pub struct Rng(u64);
impl Rng {
pub fn new(seed: u64) -> Self {
Self(seed | 1)
}
pub fn bits(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
pub fn below(&mut self, n: usize) -> usize {
(self.bits() % n as u64) as usize
}
pub fn chance(&mut self) -> bool {
self.bits() & 1 == 0
}
}
const COLORS: [UiColor; 6] = [
UiColor::RED,
UiColor::GREEN,
UiColor::BLUE,
UiColor::YELLOW,
UiColor::CYAN,
UiColor::MAGENTA,
];
const WORDS: &str = "Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer and not a setting.";
/// What growing a tree gives back: every widget in creation order, so two
/// trees from one seed line up index for index, and the declared sizes, which
/// are what a test changes to watch the change propagate.
#[derive(Default)]
pub struct Tree {
pub ids: Vec<WidgetId>,
pub sized: Vec<WeakWidget<SetSize>>,
}
/// Grows the tree `seed` describes, `edits` replacing the declared sizes it
/// would otherwise have given those wrappers.
pub fn grow<Rsc: UiRsc + 'static>(
rsc: &mut Rsc,
seed: u64,
depth: usize,
edits: &HashMap<usize, Lens>,
) -> (StrongWidget, Tree) {
let mut grow = Grow {
rsc,
rng: Rng::new(seed),
tree: Tree::default(),
edits,
};
let root = grow.node(depth);
(root, grow.tree)
}
struct Grow<'a, Rsc> {
rsc: &'a mut Rsc,
rng: Rng,
tree: Tree,
edits: &'a HashMap<usize, Lens>,
}
impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
fn leaf(&mut self) -> StrongWidget {
let id: StrongWidget = match self.rng.below(4) {
// Wrapped and unwrapped, because only one of them reads the width
// it is given and so only one has to be drawn again for a new one.
0 => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
1 => wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add_strong(self.rsc),
_ => {
let color = COLORS[self.rng.below(COLORS.len())];
let alpha = (self.rng.below(5) * 63) as u8;
rect(color.alpha(alpha)).add_strong(self.rsc)
}
};
self.tree.ids.push(id.id());
id
}
fn len(&mut self) -> Option<Len> {
match self.rng.below(4) {
0 => Some(Len::abs(20.0 + self.rng.below(180) as f32)),
1 => Some(Len::REST),
_ => None,
}
}
/// A declared size over half the tree, kept where a test can change it.
fn sized(&mut self, inner: StrongWidget) -> StrongWidget {
if !self.rng.chance() {
return inner;
}
let idx = self.tree.sized.len();
let lens = [self.len(), self.len()];
let lens = self.edits.get(&idx).copied().unwrap_or(lens);
let id = SetSize {
inner,
x: lens[0],
y: lens[1],
}
.add(self.rsc);
self.tree.sized.push(id);
self.tree.ids.push(id.id());
id.add_strong(self.rsc)
}
fn node(&mut self, depth: usize) -> StrongWidget {
if depth == 0 {
return self.leaf();
}
let count = 2 + self.rng.below(2);
let mut children = Vec::with_capacity(count);
for _ in 0..count {
let child = self.node(depth - 1);
children.push(self.sized(child));
}
let id: StrongWidget = match self.rng.below(3) {
0 => Stack {
children,
size: StackSize::Child(0),
}
.add_strong(self.rsc),
_ => {
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][self.rng.below(4)];
Span {
children,
dir,
gap: self.rng.below(3) as f32 * 4.0,
}
.add_strong(self.rsc)
}
};
self.tree.ids.push(id.id());
id
}
}
+2 -8
View File
@@ -9,20 +9,14 @@ impl Widget for Aligned {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
// Drawn where it may be too big, then given its aligned box once its // Drawn where it may be too big, then given its aligned box once its
// size is known. // size is known.
let size = painter.place(&self.inner, UiRegion::FULL).size(); let size = painter.widget(&self.inner).size();
let region = match self.align.tuple() { let region = match self.align.tuple() {
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }), (Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL), (Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)), (None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
(None, None) => UiRegion::FULL, (None, None) => UiRegion::FULL,
}; };
painter.place(&self.inner, region); painter.widget_within(&self.inner, region);
size size
} }
/// The aligned box is a fraction of its own, so the child keeps its
/// length and stays against the edge it was aligned to.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
-4
View File
@@ -12,8 +12,4 @@ impl Widget for LayerOffset {
} }
painter.widget(&self.inner).size() painter.widget(&self.inner).size()
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
-4
View File
@@ -10,8 +10,4 @@ impl Widget for Offset {
let region = UiRegion::FULL.offset(self.amt); let region = UiRegion::FULL.offset(self.amt);
painter.widget_within(&self.inner, region).size() painter.widget_within(&self.inner, region).size()
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
-6
View File
@@ -21,12 +21,6 @@ impl Widget for Pad {
}, },
} }
} }
/// The padding is an offset from each edge, so a longer box pads the same
/// amount and the child takes the rest.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
pub struct Padding { pub struct Padding {
+3 -3
View File
@@ -12,10 +12,10 @@ pub struct Scroll {
impl Widget for Scroll { impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let output_len = painter.output_size().axis(self.axis); let output_len = painter.output_size().axis(self.axis);
let container_len = UiScalar::abs(painter.px_size().axis(self.axis)); let container_len = painter.region().axis(self.axis).len();
// Drawn in the whole container to learn its length, then placed at // Drawn in the whole container to learn its length, then placed at
// the scrolled offset. // the scrolled offset.
let child = painter.place(&self.inner, UiRegion::FULL).size(); let child = painter.widget(&self.inner).size();
let content_len = child let content_len = child
.axis(self.axis) .axis(self.axis)
.apply_rest() .apply_rest()
@@ -31,7 +31,7 @@ impl Widget for Scroll {
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0)); let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
painter.place(&self.inner, region); painter.widget_within(&self.inner, region);
child child
} }
} }
-4
View File
@@ -23,8 +23,4 @@ impl Widget for SetSize {
Axis::Y => self.y, Axis::Y => self.y,
} }
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
+2 -8
View File
@@ -17,7 +17,7 @@ impl Widget for Span {
.iter() .iter()
.map(|child| match painter.size_hint(child, axis) { .map(|child| match painter.size_hint(child, axis) {
Some(len) => len, Some(len) => len,
None => painter.place(child, UiRegion::FULL).len(axis), None => painter.widget(child).len(axis),
}) })
.collect(); .collect();
@@ -42,7 +42,7 @@ impl Widget for Span {
if self.dir.sign == Sign::Neg { if self.dir.sign == Sign::Neg {
region.flip(axis); region.flip(axis);
} }
let used = painter.place(child, region).size().axis(!axis); let used = painter.widget_within(child, region).size().axis(!axis);
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels // TODO: rel shouldn't do this, but no easy way before actually calculating pixels
if used.rel > 0.0 || used.rest > 0.0 { if used.rel > 0.0 || used.rest > 0.0 {
ortho = Len::REST; ortho = Len::REST;
@@ -58,12 +58,6 @@ impl Widget for Span {
}; };
Size::from_axis(axis, along, ortho) Size::from_axis(axis, along, ortho)
} }
/// Every child is placed in fractions and offsets of the span's own box,
/// so a longer box holds the same layout and the children follow it.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
impl Span { impl Span {
-4
View File
@@ -28,10 +28,6 @@ impl Widget for Stack {
} }
size size
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
-224
View File
@@ -1,224 +0,0 @@
//! What the vertex shader's move-chain walk costs, against how deep the chain
//! is. Every active widget owns a slot, so the depth a primitive resolves
//! through is its depth in the widget tree.
//!
//! cargo test --release --test chain_cost -- --ignored --nocapture
//!
//! Timed on the GPU with timestamp queries rather than by the clock: wall time
//! here varied by 2x between runs of one unchanged binary. The pass is
//! submitted and waited on, so this is the GPU's cost and not the recording
//! loop's -- which is what `draw_cost.rs` measures instead.
//!
//! The instances are two pixels wide so that vertex work dominates; a chain
//! walk that does not show up against small quads will not show up against
//! anything.
//!
//! The instance is leaked deliberately, for the reason `draw_cost.rs` gives.
use iris::prelude::*;
use iris_core::{
MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode, UiRenderState,
UiScalar, UiSpan,
};
use wgpu::{Color as GpuColor, *};
const SIZE: u32 = 1024;
const INSTANCES: usize = 200_000;
const FRAMES: u32 = 20;
/// Reported as the best of this many batches, since the mean moves by more
/// than the thing being measured.
const BATCHES: u32 = 8;
fn gpu() -> Option<(Device, Queue, f32)> {
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
let instance: &'static Instance = Box::leak(Box::new(instance));
let adapter =
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
println!("no timestamp queries on {:?}", adapter.get_info().name);
return None;
}
println!("adapter: {:?}", adapter.get_info().name);
let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor {
required_features: Features::TIMESTAMP_QUERY,
..Default::default()
}))
.ok()?;
let period = queue.get_timestamp_period();
Some((device, queue, period))
}
fn config(format: TextureFormat) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: SIZE,
height: SIZE,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
/// A chain `depth` slots long, and instances that all resolve through its end.
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
let kind = ui.primitives.kind::<RectPrimitive>();
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
let mut slot = MoveIdx::NONE;
for _ in 0..depth {
slot = render.moves.push(slot, UiRegion::FULL);
}
let px = |v: f32| UiScalar { rel: 0.0, abs: v };
for i in 0..INSTANCES {
let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
let y = (i / (SIZE as usize / 2)) as f32;
render.layers.write(
0,
PrimitiveInst {
kind,
id,
primitive: RectPrimitive::color(UiColor::WHITE),
region: UiRegion::new(
UiSpan::new(px(x), px(x + 2.0)),
UiSpan::new(px(y), px(y + 1.0)),
),
mask_idx: MaskIdx::NONE,
move_idx: slot,
},
);
}
}
/// Nanoseconds the pass took on the GPU, best of `BATCHES`.
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
let format = TextureFormat::Bgra8Unorm;
let mut node = UiRenderNode::new(device, &config(format));
let mut ui = UiData::default();
let mut render = UiRenderState::new();
fill(&mut ui, &mut render, depth);
node.update(device, queue, &mut ui, &mut render);
let target = device.create_texture(&TextureDescriptor {
label: Some("chain cost"),
size: Extent3d {
width: SIZE,
height: SIZE,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format,
usage: TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = target.create_view(&TextureViewDescriptor::default());
let queries = device.create_query_set(&QuerySetDescriptor {
label: Some("chain cost"),
ty: QueryType::Timestamp,
count: 2,
});
let resolved = device.create_buffer(&BufferDescriptor {
label: Some("resolved"),
size: 16,
usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let readback = device.create_buffer(&BufferDescriptor {
label: Some("readback"),
size: 16,
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let frame = || {
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
{
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
label: None,
color_attachments: &[Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(GpuColor::BLACK),
store: StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: Some(RenderPassTimestampWrites {
query_set: &queries,
beginning_of_pass_write_index: Some(0),
end_of_pass_write_index: Some(1),
}),
occlusion_query_set: None,
multiview_mask: None,
});
node.draw(pass);
}
encoder.resolve_query_set(&queries, 0..2, &resolved, 0);
encoder.copy_buffer_to_buffer(&resolved, 0, &readback, 0, 16);
queue.submit(Some(encoder.finish()));
let slice = readback.slice(..);
slice.map_async(MapMode::Read, |_| {});
let _ = device.poll(PollType::Wait {
submission_index: None,
timeout: None,
});
let ns = {
let view = slice.get_mapped_range().expect("timestamps did not map");
let stamps: [u64; 2] = [
u64::from_le_bytes(view[..8].try_into().unwrap()),
u64::from_le_bytes(view[8..16].try_into().unwrap()),
];
(stamps[1].saturating_sub(stamps[0])) as f64 * period as f64
};
readback.unmap();
ns
};
frame();
let mut best = f64::MAX;
for _ in 0..BATCHES {
let mut total = 0.0;
for _ in 0..FRAMES {
total += frame();
}
best = best.min(total / FRAMES as f64);
}
best
}
#[test]
#[ignore = "measurement, not a check"]
fn chain_cost_by_depth() {
let Some((device, queue, period)) = gpu() else {
println!("no gpu with timestamps; nothing measured");
return;
};
println!("{INSTANCES} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
let mut base = None;
for depth in [1, 2, 4, 8, 16, 32, 64] {
let ns = pass_cost(&device, &queue, period, depth);
let base = *base.get_or_insert(ns);
println!(
"depth {depth:>3}: {:>9.1} us {:+6.1}% against depth 1",
ns / 1000.0,
(ns - base) / base * 100.0
);
}
}
+2 -5
View File
@@ -22,8 +22,8 @@ use std::time::Instant;
use iris::prelude::*; use iris::prelude::*;
use iris_core::{ use iris_core::{
GlyphPrimitive, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, TextureHandle, GlyphPrimitive, MaskIdx, PrimitiveInst, RectPrimitive, TextureHandle, TexturePrimitive, UiData,
TexturePrimitive, UiData, UiRegion, UiRenderNode, UiRenderState, UiRegion, UiRenderNode, UiRenderState,
}; };
use wgpu::{Color as GpuColor, *}; use wgpu::{Color as GpuColor, *};
@@ -95,7 +95,6 @@ fn fill(
primitive: RectPrimitive::color(UiColor::WHITE), primitive: RectPrimitive::color(UiColor::WHITE),
region: UiRegion::FULL, region: UiRegion::FULL,
mask_idx: MaskIdx::NONE, mask_idx: MaskIdx::NONE,
move_idx: MoveIdx::NONE,
}, },
); );
render.layers.write( render.layers.write(
@@ -112,7 +111,6 @@ fn fill(
}, },
region: UiRegion::FULL, region: UiRegion::FULL,
mask_idx: MaskIdx::NONE, mask_idx: MaskIdx::NONE,
move_idx: MoveIdx::NONE,
}, },
); );
} }
@@ -125,7 +123,6 @@ fn fill(
primitive: TexturePrimitive::from(h), primitive: TexturePrimitive::from(h),
region: UiRegion::FULL, region: UiRegion::FULL,
mask_idx: MaskIdx::NONE, mask_idx: MaskIdx::NONE,
move_idx: MoveIdx::NONE,
}, },
); );
} }
-171
View File
@@ -1,171 +0,0 @@
//! Random trees, checked against building the same tree cold.
//!
//! A frame reaches its layout by keeping most of the last one: slots
//! rewritten, some widgets drawn again, the rest untouched. The property here
//! is that what comes out is the tree a cold start would have produced, so
//! anything the retained path carried over that it should not have shows up
//! as a difference in somebody's box.
//!
//! `iris::random` grows the tree and `examples/random.rs` draws one. A seed is
//! the whole reproduction; `a_long_run_of_seeds_agrees` is the ignored sweep
//! for when it is worth spending the time.
use std::collections::HashMap;
use iris::harness::Harness;
use iris::prelude::*;
use iris::random::{Lens, Rng, Tree, grow};
const DEPTH: usize = 4;
/// Seeds whose trees agree. The ones left out are `a_wrapping_child_of_a_row`
/// below, which is a defect older than the chain.
const SEEDS: [u64; 6] = [2, 3, 4, 5, 8, 9];
fn plant(h: &mut Harness, seed: u64, edits: &HashMap<usize, Lens>) -> Tree {
let (root, tree) = grow(&mut h.rsc, seed, DEPTH, edits);
h.state.root = Some(root);
h.frame();
tree
}
/// Changes a few of the declared sizes, and says which, so the cold tree can
/// be grown with the same ones.
fn edit(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap<usize, Lens> {
let mut edits = HashMap::new();
for _ in 0..4 {
let idx = rng.below(tree.sized.len());
let lens = [
Some(Len::abs(20.0 + rng.below(180) as f32)),
Some(Len::abs(20.0 + rng.below(180) as f32)),
];
edits.insert(idx, lens);
let sized = &mut h.rsc[tree.sized[idx]];
sized.x = lens[0];
sized.y = lens[1];
}
edits
}
/// Every widget in one tree against the matching widget in the other. A
/// mismatch prints the widget's ancestry, marking the ones that own a slot,
/// since where two trees disagree is rarely where the cause is.
fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, &Tree)) {
let ((wh, wt), (ch, ct)) = (warm, cold);
assert_eq!(wt.ids.len(), ct.ids.len(), "seed {seed}: different trees");
let mut drawn = 0;
let mut wrong = 0;
for (i, (&w, &c)) in wt.ids.iter().zip(&ct.ids).enumerate() {
let (got, want) = (wh.region(&w), ch.region(&c));
drawn += usize::from(got.is_some());
if got == want {
continue;
}
wrong += 1;
if wrong <= 3 {
let mut chain = Vec::new();
let mut at = Some(w);
while let Some(id) = at {
let active = &wh.render.active[&id];
let slot = match active.move_idx == active.parent_move {
true => "",
false => "*",
};
chain.push(format!("{}{slot}", wh.rsc.widgets().label(id)));
at = active.parent;
}
println!(
"seed {seed} after {what}: widget {i}\n warm {got:?}\n cold {want:?}\n {}",
chain.join(" < ")
);
}
}
assert!(drawn > 0, "seed {seed}: nothing was drawn");
assert_eq!(wrong, 0, "seed {seed}: {wrong} widgets differ after {what}");
}
fn changed_size(seed: u64) {
let mut warm = Harness::new((900, 1200));
let grown = plant(&mut warm, seed, &HashMap::new());
let mut rng = Rng::new(seed ^ 0x5eed);
let edits = edit(&mut warm, &grown, &mut rng);
warm.frame();
let mut cold = Harness::new((900, 1200));
let same = plant(&mut cold, seed, &edits);
assert_same(seed, "a size change", (&warm, &grown), (&cold, &same));
}
fn resized(seed: u64) {
let mut warm = Harness::new((1920, 1200));
let grown = plant(&mut warm, seed, &HashMap::new());
warm.resize((640, 900));
warm.frame();
let mut cold = Harness::new((640, 900));
let same = plant(&mut cold, seed, &HashMap::new());
assert_same(seed, "a resize", (&warm, &grown), (&cold, &same));
}
fn resized_then_changed(seed: u64) {
let mut warm = Harness::new((1920, 1200));
let grown = plant(&mut warm, seed, &HashMap::new());
warm.resize((640, 900));
warm.frame();
let mut rng = Rng::new(seed ^ 0xb0a7);
let edits = edit(&mut warm, &grown, &mut rng);
warm.frame();
let mut cold = Harness::new((640, 900));
let same = plant(&mut cold, seed, &edits);
let what = "a resize then a size change";
assert_same(seed, what, (&warm, &grown), (&cold, &same));
}
#[test]
fn a_changed_size_lands_where_growing_it_that_way_would() {
SEEDS.into_iter().for_each(changed_size);
}
#[test]
fn a_resize_lands_where_starting_at_that_size_would() {
SEEDS.into_iter().for_each(resized);
}
#[test]
fn a_size_change_after_a_resize_lands_the_same_way() {
SEEDS.into_iter().for_each(resized_then_changed);
}
/// Reproduces a divergence that predates the position chain: laying a tree out
/// again does not always land where growing it cold does.
///
/// Every one seen so far is a wrapping text on a span's *own* axis, where the
/// two draws do not agree. The span measures the child in the whole box, the
/// child shapes to that width and reports the width it used, the span then
/// places it in exactly that width -- which is a length change, so the child
/// shapes again, and its longest line is shorter than the box it was just
/// given. Each pass narrows it, so where the tree ends up depends on how many
/// passes it has had, and a warm tree has had a different number from a cold
/// one. Layout is supposed to be a function of the state alone.
///
/// A span whose axis is not the wrap axis is stable, which is every real
/// column of text, and why nothing else has run into this.
///
/// 7 of these 90 diverge on `db1751f`, before the chain; 30 do with it, since
/// a placed child reaches the second shaping more often. Both numbers are the
/// same defect, and it wants fixing where the two draws meet -- LAYOUT.md §4 --
/// rather than anywhere in the chain.
#[test]
#[ignore = "known divergence, and the reproduction for fixing it"]
fn a_wrapping_child_of_a_row_settles_somewhere_else_each_time() {
for seed in 1..=30 {
changed_size(seed);
resized(seed);
resized_then_changed(seed);
}
}
-73
View File
@@ -109,76 +109,3 @@ fn a_fixed_box_is_drawn_again_rather_than_stretched() {
assert_corners!(h, panel, (0, 0), (400, 250)); assert_corners!(h, panel, (0, 0), (400, 250));
} }
#[test]
fn a_moved_subtree_takes_its_children_with_it() {
let mut h = Harness::new((400, 400));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = inner.pad(10).height(40).add(&mut h.rsc);
h.set_root((first, row).span(Dir::DOWN));
assert_corners!(h, inner, (10, 50), (390, 70));
h.rsc[first].y = Some(Len::abs(80));
h.frame();
// The row is the same shape somewhere else, so one slot moved it and
// `inner`'s own region was never rewritten.
assert_corners!(h, inner, (10, 90), (390, 110));
}
#[test]
fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() {
let mut h = Harness::new((400, 200));
let fixed = rect(Color::BLUE).width(50).add(&mut h.rsc);
let rest = rect(Color::GREEN).add(&mut h.rsc);
let panel = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
// Changing the bar's width is the only thing that changes the box the
// panel and everything under it was drawn for.
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, panel).span(Dir::RIGHT));
assert_corners!(h, fixed, (100, 0), (150, 200));
assert_corners!(h, rest, (150, 0), (400, 200));
h.rsc[bar].x = Some(Len::abs(200));
h.frame();
// The panel's box is 100 shorter, so the fixed child is the same 50 wide
// against its new start and the one taking the rest absorbs the change.
assert_corners!(h, fixed, (200, 0), (250, 200));
assert_corners!(h, rest, (250, 0), (400, 200));
}
#[test]
fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
let mut h = Harness::new((400, 200));
// The row is 40 tall whatever happens, which used to make its drawing
// impossible to take out of: recovering a fraction of a box needs a
// relative extent, and it has none on that axis.
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = inner.pad(10).height(40).add(&mut h.rsc);
let filler = rect(Color::GREEN).add(&mut h.rsc);
let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, column).span(Dir::RIGHT));
assert_corners!(h, inner, (110, 10), (390, 30));
h.rsc[bar].x = Some(Len::abs(200));
h.frame();
assert_corners!(h, inner, (210, 10), (390, 30));
}
#[test]
fn only_a_container_that_places_its_children_lengthens_the_chain() {
let mut h = Harness::new((400, 200));
let leaf = rect(Color::BLUE).add(&mut h.rsc);
// Four widgets between the span and the leaf, none of which places what
// it draws, so all of them share the span's slot.
let buried = leaf.pad(4).pad(4).pad(4).pad(4).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, buried).span(Dir::RIGHT));
let slot = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(slot), 1, "one span above the leaf");
}
+27
View File
@@ -0,0 +1,27 @@
//! What a drawing can be taken out of, and what it cannot.
use iris::core::{Remap, UiRegion, UiScalar, UiSpan};
/// A box `size` tall whose top is `rel` of the way down the window.
fn fixed(rel: f32, size: f32) -> UiRegion {
UiRegion::new(
UiSpan::FULL,
UiSpan::new(UiScalar { rel, abs: 0.0 }, UiScalar { rel, abs: size }),
)
}
#[test]
fn a_fixed_box_can_be_carried_but_not_stretched() {
let from = fixed(0.0, 164.0);
assert!(Remap::new(from, UiRegion::FULL).is_none());
assert!(Remap::new(from, fixed(0.5, 164.0)).is_some());
assert!(Remap::new(from, fixed(0.0, 98.0)).is_none());
}
#[test]
fn a_relative_box_can_be_stretched_to_any_other() {
let remap = Remap::new(UiRegion::FULL, fixed(0.0, 98.0)).expect("relative boxes remap");
// A part that filled the window keeps filling what replaced it, which is
// exactly what `outside` could not say for a box of a fixed length.
assert_eq!(remap.apply(UiRegion::FULL), fixed(0.0, 98.0));
}
-43
View File
@@ -1,43 +0,0 @@
//! What re-placing a subtree costs per frame, as a load for a counter rather
//! than a check. A span of 200 fixed-height rows, five primitives each, with
//! the row above them changing height every frame, so every row below is
//! offered a box the same shape somewhere else.
//!
//! cargo test --release --test replace_cost -- --ignored
//! perf stat -e instructions:u target/release/.../replace_cost-* --ignored
//!
//! Wall time is the wrong number here; see `draw_cost.rs`. Measured on
//! 2026-09-14 at 1.98M instructions per frame, against 2.38M for rewriting
//! each row's regions instead and 7.13M for redrawing them.
use iris::harness::Harness;
use iris::prelude::*;
const ROWS: usize = 200;
const FRAMES: usize = 200;
#[test]
#[ignore = "measurement, not a check"]
fn replacing_rows_every_frame() {
let mut h = Harness::new((1920, 1200));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let mut span = Span::empty(Dir::DOWN);
span.push(first.add_strong(&mut h.rsc));
for i in 0..ROWS {
let row = (
rect(Color::BLUE.darker(i as f32 / (ROWS * 2) as f32)),
rect(Color::GREEN).pad(2),
wtext("row").size(16).pad(2),
)
.span(Dir::RIGHT)
.pad(4)
.height(40)
.add(&mut h.rsc);
span.push(row.add_strong(&mut h.rsc));
}
h.set_root(span);
for i in 0..FRAMES {
h.rsc[first].y = Some(Len::abs(40.0 + (i % 2) as f32));
h.frame();
}
}
-87
View File
@@ -250,90 +250,3 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
assert_corners!(h, below, (12, 232), (388, 388)); assert_corners!(h, below, (12, 232), (388, 388));
} }
/// Claims its drawing survives its box changing length, and has a child so
/// that the walk looking for what does not has one to reach.
struct Stretchy {
inner: StrongWidget,
draws: Rc<Cell<usize>>,
}
impl Widget for Stretchy {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.widget(&self.inner).size()
}
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
}
#[test]
fn stretching_a_subtree_carries_the_children_in_it() {
let mut h = Harness::new((400, 400));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let outer = Stretchy {
inner: inner.add_strong(&mut h.rsc),
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root((first, outer).span(Dir::DOWN));
let settled = draws.get();
assert_corners!(h, inner, (0, 40), (400, 400));
h.rsc[first].y = Some(Len::abs(80));
h.frame();
assert_eq!(
draws.get(),
settled,
"its drawing follows its box, rather than being made again"
);
assert_corners!(h, outer, (0, 80), (400, 400));
assert_corners!(h, inner, (0, 80), (400, 400));
}
#[test]
fn a_widened_row_redraws_what_reads_its_length_and_nothing_else() {
let mut h = Harness::new((400, 200));
// What a transcript row is: something whose shaping depends on the width
// it is given, beside something that only has to be the right shape.
let (wraps, wrap_draws) = counted(&mut h, Size::REST, OnResize::Redraw);
let (backing, back_draws) = counted(&mut h, Size::REST, OnResize::Scale);
let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, row).span(Dir::RIGHT));
let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get());
h.rsc[bar].x = Some(Len::abs(200));
h.frame();
// The span reads every child's size, so redrawing one takes the span
// with it -- and the span then measures and places the redrawn child.
assert!(wrap_draws.get() > settled_wrap, "reads the width it got");
assert_eq!(back_draws.get(), settled_back, "only has to be the shape");
assert_corners!(h, backing, (200, 0), (300, 200));
assert_corners!(h, wraps, (300, 0), (400, 200));
}
#[test]
fn a_fixed_length_child_is_not_redrawn_when_the_box_around_it_grows() {
let mut h = Harness::new((400, 200));
// It would be drawn again for a width it does not have: its own box is
// a fixed 80 wherever the row's edges end up.
let (fixed, draws) = counted(&mut h, Size::from((80, 200)), OnResize::Redraw);
let (rest, _) = counted(&mut h, Size::REST, OnResize::Scale);
let row = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, row).span(Dir::RIGHT));
let settled = draws.get();
h.rsc[bar].x = Some(Len::abs(200));
h.frame();
assert_eq!(draws.get(), settled, "its own length did not change");
assert_corners!(h, fixed, (200, 0), (280, 200));
}