iris: Widget::draw reports the size it used, replacing desired_width/height
Implements LAYOUT.md end to end: one fn draw(&mut self, &mut Painter) -> Size replaces draw + desired_width/desired_height on every widget in iris/src/widget/, SizeCtx and Cache are deleted, and a moved widget (Scroll, Offset) costs one move_offsets write resolved by a shared resolve_move WGSL function in both shader stages -- O(1) regardless of how many primitives are in its subtree, measured at 500 in the new iris/src/layout_tests.rs (a plain unit test: UiRenderState touches no GPU or window). Five real bugs surfaced only by diffing iris/run-headless.sh screenshots against the pre-change tree and are written up in LAYOUT.md's "Deviations found during implementation": Aligned's provisional draw composing painter.region() a second time through widget_within; Sized/ MaxSize reporting a capped size while still painting their child unconstrained (fine under the old two-pass model, wrong once a parent like Aligned draws before knowing the final size); a widget's move_offsets parent link being unreadable from self.active while its own ActiveData is still mid-construction; Painter::reposition needing the child's *painted* footprint (its reported size, top-left anchored) rather than its offered region; and a widget's move slot needing to be reused in place across redraws, with its delta reset, rather than reallocated. All four iris/examples render pixel-identical to the pre-change tree. cargo fmt/clippy/test clean across the workspace (18 tests: 14 pre-existing plus 4 new). Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
This commit is contained in:
1 parent
e0a473e090
commit
1a6599e1b2
36 files changed
+1200
-593
No files matched your search
@@ -2,7 +2,7 @@ use std::ops::{Index, IndexMut};
|
||||
|
||||
use crate::{
|
||||
UiRegion, WidgetId,
|
||||
render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives},
|
||||
render::{MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives},
|
||||
util::to_mut,
|
||||
};
|
||||
|
||||
@@ -140,8 +140,9 @@ impl PrimitiveLayers {
|
||||
texture_idx: u32,
|
||||
region: UiRegion,
|
||||
mask_idx: MaskIdx,
|
||||
move_idx: MoveIdx,
|
||||
) -> PrimitiveHandle {
|
||||
self[layer].write_image(layer, id, texture_idx, region, mask_idx)
|
||||
self[layer].write_image(layer, id, texture_idx, region, mask_idx, move_idx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,11 @@ pub struct PrimitiveInstance {
|
||||
pub binding: u32,
|
||||
pub idx: u32,
|
||||
pub mask_idx: MaskIdx,
|
||||
pub move_idx: MoveIdx,
|
||||
}
|
||||
|
||||
impl PrimitiveInstance {
|
||||
const ATTRIBS: [VertexAttribute; 7] = vertex_attr_array![
|
||||
const ATTRIBS: [VertexAttribute; 8] = vertex_attr_array![
|
||||
0 => Float32x2,
|
||||
1 => Float32x2,
|
||||
2 => Float32x2,
|
||||
@@ -26,6 +27,7 @@ impl PrimitiveInstance {
|
||||
4 => Uint32,
|
||||
5 => Uint32,
|
||||
6 => Uint32,
|
||||
7 => Uint32,
|
||||
];
|
||||
|
||||
pub fn desc() -> VertexBufferLayout<'static> {
|
||||
@@ -43,8 +45,48 @@ impl MaskIdx {
|
||||
pub const NONE: Self = Self::preset(u32::MAX);
|
||||
}
|
||||
|
||||
pub type MoveIdx = Id<u32>;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Mask {
|
||||
pub region: UiRegion,
|
||||
/// The mask-owning widget's own move slot -- resolved in the fragment
|
||||
/// shader against the same chain the vertex shader walks for a
|
||||
/// primitive's own corners, so a mask and the content clipped by it
|
||||
/// can move independently. See LAYOUT.md section 2b.
|
||||
pub move_idx: MoveIdx,
|
||||
}
|
||||
|
||||
/// One widget's cumulative on-screen translation, and the slot of the
|
||||
/// ancestor to add on top of it. `parent == u32::MAX` ends the chain. A
|
||||
/// pure abs-pixel delta, not a general `UiRegion` remap -- sufficient for
|
||||
/// every call site that moves a widget (`Scroll`, `Offset`) since both are
|
||||
/// translations of an already-drawn subtree. See LAYOUT.md section 2.
|
||||
///
|
||||
/// `_pad` matches WGSL's storage-buffer layout for `MoveOffset`: `delta` is
|
||||
/// a `vec2<f32>`, which gives the struct an 8-byte alignment and rounds its
|
||||
/// WGSL size up to 16 bytes even though `delta` + `parent` only total 12 --
|
||||
/// the same trap `GlyphPrimitive` documents below. `bytemuck` does not
|
||||
/// check this for us, and getting it wrong is a wgpu validation panic at
|
||||
/// draw time ("buffer bound ... with size 12 where the shader expects 16"),
|
||||
/// not a compile error.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct MoveOffset {
|
||||
pub delta: [f32; 2],
|
||||
pub parent: u32,
|
||||
_pad: u32,
|
||||
}
|
||||
|
||||
impl MoveOffset {
|
||||
pub const NONE_PARENT: u32 = u32::MAX;
|
||||
|
||||
pub fn new(delta: [f32; 2], parent: u32) -> Self {
|
||||
Self {
|
||||
delta,
|
||||
parent,
|
||||
_pad: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ mod texture;
|
||||
mod util;
|
||||
|
||||
pub use atlas::*;
|
||||
pub use data::{Mask, MaskIdx};
|
||||
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset};
|
||||
pub use primitive::*;
|
||||
|
||||
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
||||
@@ -34,6 +34,7 @@ pub struct UiRenderNode {
|
||||
window_buffer: Buffer,
|
||||
textures: GpuTextures,
|
||||
masks: ArrBuf<Mask>,
|
||||
move_offsets: ArrBuf<MoveOffset>,
|
||||
}
|
||||
|
||||
struct RenderLayer {
|
||||
@@ -156,14 +157,28 @@ impl UiRenderNode {
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let moves_resized = if ui.move_offsets.changed {
|
||||
ui.move_offsets.changed = false;
|
||||
self.move_offsets
|
||||
.update(device, queue, &ui.move_offsets[..])
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let rebuild_main = self.textures.update(
|
||||
&mut ui.textures,
|
||||
&self.rsc_layout,
|
||||
&self.masks,
|
||||
masks_resized,
|
||||
&self.move_offsets,
|
||||
masks_resized || moves_resized,
|
||||
);
|
||||
if rebuild_main {
|
||||
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks);
|
||||
self.rsc_group = Self::rsc_group(
|
||||
device,
|
||||
&self.rsc_layout,
|
||||
&self.textures,
|
||||
&self.masks,
|
||||
&self.move_offsets,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,9 +244,14 @@ impl UiRenderNode {
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"ui masks",
|
||||
);
|
||||
let move_offsets = ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"ui move offsets",
|
||||
);
|
||||
|
||||
let rsc_layout = Self::rsc_layout(device);
|
||||
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
|
||||
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks, &move_offsets);
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some("UI Shape Pipeline Layout"),
|
||||
@@ -287,6 +307,7 @@ impl UiRenderNode {
|
||||
active: Vec::new(),
|
||||
textures: tex_manager,
|
||||
masks,
|
||||
move_offsets,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +386,16 @@ impl UiRenderNode {
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 4,
|
||||
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some("ui rsc"),
|
||||
})
|
||||
@@ -377,6 +408,7 @@ impl UiRenderNode {
|
||||
layout: &BindGroupLayout,
|
||||
tex_manager: &GpuTextures,
|
||||
masks: &ArrBuf<Mask>,
|
||||
move_offsets: &ArrBuf<MoveOffset>,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
@@ -397,6 +429,10 @@ impl UiRenderNode {
|
||||
binding: 3,
|
||||
resource: masks.buffer.as_entire_binding(),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 4,
|
||||
resource: move_offsets.buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label: Some("ui rsc"),
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
Color, UiRegion, WidgetId,
|
||||
render::{
|
||||
ArrBuf,
|
||||
data::{MaskIdx, PrimitiveInstance},
|
||||
data::{MaskIdx, MoveIdx, PrimitiveInstance},
|
||||
},
|
||||
};
|
||||
use bytemuck::Pod;
|
||||
@@ -138,6 +138,7 @@ pub struct PrimitiveInst<P> {
|
||||
pub primitive: P,
|
||||
pub region: UiRegion,
|
||||
pub mask_idx: MaskIdx,
|
||||
pub move_idx: MoveIdx,
|
||||
}
|
||||
|
||||
impl Primitives {
|
||||
@@ -149,6 +150,7 @@ impl Primitives {
|
||||
primitive,
|
||||
region,
|
||||
mask_idx,
|
||||
move_idx,
|
||||
}: PrimitiveInst<P>,
|
||||
) -> PrimitiveHandle {
|
||||
self.updated = true;
|
||||
@@ -158,6 +160,7 @@ impl Primitives {
|
||||
region,
|
||||
idx: i as u32,
|
||||
mask_idx,
|
||||
move_idx,
|
||||
binding: P::BINDING,
|
||||
};
|
||||
let inst_i = if let Some(i) = self.free.pop() {
|
||||
@@ -184,12 +187,14 @@ impl Primitives {
|
||||
texture_idx: u32,
|
||||
region: UiRegion,
|
||||
mask_idx: MaskIdx,
|
||||
move_idx: MoveIdx,
|
||||
) -> PrimitiveHandle {
|
||||
self.updated = true;
|
||||
let inst = PrimitiveInstance {
|
||||
region,
|
||||
idx: texture_idx,
|
||||
mask_idx,
|
||||
move_idx,
|
||||
binding: IMAGE_BINDING,
|
||||
};
|
||||
let inst_i = if let Some(i) = self.image_free.pop() {
|
||||
|
||||
@@ -33,6 +33,14 @@ struct GlyphInfo {
|
||||
struct Mask {
|
||||
x: UiSpan,
|
||||
y: UiSpan,
|
||||
move_idx: u32,
|
||||
}
|
||||
|
||||
/// One widget's cumulative on-screen translation and the slot of the
|
||||
/// ancestor to add on top of it. Mirrors `MoveOffset` in data.rs.
|
||||
struct MoveOffset {
|
||||
delta: vec2<f32>,
|
||||
parent: u32,
|
||||
}
|
||||
|
||||
struct UiSpan {
|
||||
@@ -66,6 +74,31 @@ var image_texture: texture_2d<f32>;
|
||||
var samp: sampler;
|
||||
@group(2) @binding(3)
|
||||
var<storage> masks: array<Mask>;
|
||||
@group(2) @binding(4)
|
||||
var<storage> move_offsets: array<MoveOffset>;
|
||||
|
||||
// A move chain more than this deep means something else is wrong (an
|
||||
// accidental cycle) -- kept in step with `MOVE_CHAIN_LIMIT` in
|
||||
// render_state.rs, which walks the identical bound on the CPU side for
|
||||
// hit-testing. Bounded so a malformed chain cannot hang the GPU.
|
||||
const MOVE_CHAIN_LIMIT: u32 = 16u;
|
||||
|
||||
/// Sums the pixel delta along the parent chain starting at `idx`, shared by
|
||||
/// the vertex stage (a primitive's own corners) and the fragment stage (its
|
||||
/// mask's corners) so the walk is written once. See LAYOUT.md section 2b.
|
||||
fn resolve_move(idx: u32) -> vec2<f32> {
|
||||
var total = vec2<f32>(0.0, 0.0);
|
||||
var i = idx;
|
||||
for (var step = 0u; step < MOVE_CHAIN_LIMIT; step++) {
|
||||
let entry = move_offsets[i];
|
||||
total += entry.delta;
|
||||
if entry.parent == 4294967295u {
|
||||
break;
|
||||
}
|
||||
i = entry.parent;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
struct WindowUniform {
|
||||
dim: vec2<f32>,
|
||||
@@ -79,6 +112,7 @@ struct InstanceInput {
|
||||
@location(4) binding: u32,
|
||||
@location(5) idx: u32,
|
||||
@location(6) mask_idx: u32,
|
||||
@location(7) move_idx: u32,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@@ -110,8 +144,9 @@ fn vs_main(
|
||||
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 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 move_delta = resolve_move(in.move_idx);
|
||||
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs) + move_delta;
|
||||
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs) + move_delta;
|
||||
let size = bot_right - top_left;
|
||||
|
||||
let uv = vec2<f32>(
|
||||
@@ -154,11 +189,12 @@ fn fs_main(
|
||||
}
|
||||
if in.mask_idx != 4294967295u {
|
||||
let mask = masks[in.mask_idx];
|
||||
let mask_delta = resolve_move(mask.move_idx);
|
||||
let tl = UiVec2(vec2(mask.x.start.rel, mask.y.start.rel), vec2(mask.x.start.abs, mask.y.start.abs));
|
||||
let br = UiVec2(vec2(mask.x.end.rel, mask.y.end.rel), vec2(mask.x.end.abs, mask.y.end.abs));
|
||||
|
||||
let top_left = floor(tl.rel * window.dim) + floor(tl.abs);
|
||||
let bot_right = floor(br.rel * window.dim) + floor(br.abs);
|
||||
let top_left = floor(tl.rel * window.dim) + floor(tl.abs) + mask_delta;
|
||||
let bot_right = floor(br.rel * window.dim) + floor(br.abs) + mask_delta;
|
||||
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
||||
color *= 0.0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use image::{DynamicImage, EncodableLayout, GenericImageView};
|
||||
use wgpu::{util::DeviceExt, *};
|
||||
|
||||
use crate::{Mask, PatchRect, TextureKind, TextureUpdate, Textures, render::util::ArrBuf};
|
||||
use crate::{
|
||||
Mask, MoveOffset, PatchRect, TextureKind, TextureUpdate, Textures, render::util::ArrBuf,
|
||||
};
|
||||
|
||||
use super::atlas::PAGE;
|
||||
|
||||
@@ -73,21 +75,23 @@ impl GpuTextures {
|
||||
textures: &mut Textures,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
move_offsets: &ArrBuf<MoveOffset>,
|
||||
masks_resized: bool,
|
||||
) -> bool {
|
||||
let mut rebuild_main = masks_resized;
|
||||
if masks_resized {
|
||||
// The masks buffer just moved, so every bind group holding a
|
||||
// reference to it -- one per live standalone image -- is stale.
|
||||
self.rebuild_image_bind_groups(rsc_layout, masks);
|
||||
// The masks or move-offsets buffer just moved, so every bind
|
||||
// group holding a reference to either -- one per live
|
||||
// standalone image -- is stale.
|
||||
self.rebuild_image_bind_groups(rsc_layout, masks, move_offsets);
|
||||
}
|
||||
for update in textures.updates() {
|
||||
match update {
|
||||
TextureUpdate::Push(kind, image) => {
|
||||
rebuild_main |= self.push(kind, image, rsc_layout, masks);
|
||||
rebuild_main |= self.push(kind, image, rsc_layout, masks, move_offsets);
|
||||
}
|
||||
TextureUpdate::Set(kind, i, image) => {
|
||||
rebuild_main |= self.set(kind, i, image, rsc_layout, masks);
|
||||
rebuild_main |= self.set(kind, i, image, rsc_layout, masks, move_offsets);
|
||||
}
|
||||
// A patch changes texture contents, not which layer or bind
|
||||
// group exists, so it never asks for a rebuild -- rebuilding
|
||||
@@ -107,8 +111,9 @@ impl GpuTextures {
|
||||
image: &DynamicImage,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
move_offsets: &ArrBuf<MoveOffset>,
|
||||
) -> bool {
|
||||
let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout, masks);
|
||||
let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout, masks, move_offsets);
|
||||
self.slots.push(slot);
|
||||
rebuilt
|
||||
}
|
||||
@@ -120,8 +125,9 @@ impl GpuTextures {
|
||||
image: &DynamicImage,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
move_offsets: &ArrBuf<MoveOffset>,
|
||||
) -> bool {
|
||||
let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout, masks);
|
||||
let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout, masks, move_offsets);
|
||||
self.slots[i as usize] = slot;
|
||||
rebuilt
|
||||
}
|
||||
@@ -132,16 +138,17 @@ impl GpuTextures {
|
||||
image: &DynamicImage,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
move_offsets: &ArrBuf<MoveOffset>,
|
||||
) -> (Slot, bool) {
|
||||
match kind {
|
||||
TextureKind::Image => {
|
||||
let gpu = self.create_image(image, rsc_layout, masks);
|
||||
let gpu = self.create_image(image, rsc_layout, masks, move_offsets);
|
||||
(Slot::Image(gpu), false)
|
||||
}
|
||||
TextureKind::Page { layer } => {
|
||||
let mut rebuilt = false;
|
||||
if layer >= self.array_capacity {
|
||||
self.grow_array(rsc_layout, masks);
|
||||
self.grow_array(rsc_layout, masks, move_offsets);
|
||||
rebuilt = true;
|
||||
}
|
||||
self.write_full_layer(layer, image);
|
||||
@@ -229,7 +236,12 @@ impl GpuTextures {
|
||||
/// copies the old layers across GPU-side -- no readback. Recreates the
|
||||
/// array's view, which invalidates every bind group that referenced it,
|
||||
/// so this also rebuilds all of them before returning.
|
||||
fn grow_array(&mut self, rsc_layout: &BindGroupLayout, masks: &ArrBuf<Mask>) {
|
||||
fn grow_array(
|
||||
&mut self,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
move_offsets: &ArrBuf<MoveOffset>,
|
||||
) {
|
||||
let new_capacity = self.array_capacity * 2;
|
||||
let new_texture = Self::create_array_texture(&self.device, new_capacity);
|
||||
if self.page_count > 0 {
|
||||
@@ -265,10 +277,15 @@ impl GpuTextures {
|
||||
..Default::default()
|
||||
});
|
||||
self.array_capacity = new_capacity;
|
||||
self.rebuild_image_bind_groups(rsc_layout, masks);
|
||||
self.rebuild_image_bind_groups(rsc_layout, masks, move_offsets);
|
||||
}
|
||||
|
||||
fn rebuild_image_bind_groups(&mut self, rsc_layout: &BindGroupLayout, masks: &ArrBuf<Mask>) {
|
||||
fn rebuild_image_bind_groups(
|
||||
&mut self,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
move_offsets: &ArrBuf<MoveOffset>,
|
||||
) {
|
||||
for slot in &mut self.slots {
|
||||
if let Slot::Image(gpu) = slot {
|
||||
gpu.bind_group = Self::make_image_bind_group(
|
||||
@@ -278,6 +295,7 @@ impl GpuTextures {
|
||||
&gpu.view,
|
||||
&self.sampler,
|
||||
masks,
|
||||
move_offsets,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -288,6 +306,7 @@ impl GpuTextures {
|
||||
image: &DynamicImage,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
move_offsets: &ArrBuf<MoveOffset>,
|
||||
) -> ImageGpu {
|
||||
let rgba = image.to_rgba8();
|
||||
let (width, height) = rgba.dimensions();
|
||||
@@ -318,6 +337,7 @@ impl GpuTextures {
|
||||
&view,
|
||||
&self.sampler,
|
||||
masks,
|
||||
move_offsets,
|
||||
);
|
||||
ImageGpu {
|
||||
texture,
|
||||
@@ -336,6 +356,7 @@ impl GpuTextures {
|
||||
image_view: &TextureView,
|
||||
sampler: &Sampler,
|
||||
masks: &ArrBuf<Mask>,
|
||||
move_offsets: &ArrBuf<MoveOffset>,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout: rsc_layout,
|
||||
@@ -356,6 +377,10 @@ impl GpuTextures {
|
||||
binding: 3,
|
||||
resource: masks.buffer.as_entire_binding(),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 4,
|
||||
resource: move_offsets.buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label: Some("ui rsc image"),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId};
|
||||
use crate::{LayerId, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId};
|
||||
|
||||
/// important non rendering data for retained drawing
|
||||
#[derive(Debug)]
|
||||
@@ -11,4 +11,14 @@ pub struct ActiveData {
|
||||
pub children: Vec<WidgetId>,
|
||||
pub mask: MaskIdx,
|
||||
pub layer: LayerId,
|
||||
/// What `Widget::draw` returned the last time this widget was actually
|
||||
/// drawn -- read by a parent placing this widget again without
|
||||
/// redrawing it, replacing `Cache.size`'s old role. See LAYOUT.md
|
||||
/// section 5.
|
||||
pub size: Size,
|
||||
/// This widget's slot in `UiData::move_offsets`, assigned on its first
|
||||
/// draw and kept for the rest of its life (redraws reuse it in place
|
||||
/// so a retained child's `parent` link never goes stale). See
|
||||
/// LAYOUT.md section 2.
|
||||
pub move_slot: MoveIdx,
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
use crate::{BothAxis, Len, UiVec2, WidgetId, util::HashMap};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Cache {
|
||||
pub size: BothAxis<HashMap<WidgetId, (UiVec2, Len)>>,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
pub fn remove(&mut self, id: WidgetId) {
|
||||
self.size.x.remove(&id);
|
||||
self.size.y.remove(&id);
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.size.x.clear();
|
||||
self.size.y.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
use crate::{Mask, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena};
|
||||
use crate::{
|
||||
Mask, MoveOffset, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
||||
};
|
||||
|
||||
mod active;
|
||||
mod cache;
|
||||
mod painter;
|
||||
mod render_state;
|
||||
mod size;
|
||||
|
||||
pub use active::*;
|
||||
pub use painter::Painter;
|
||||
pub use render_state::*;
|
||||
pub use size::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UiData {
|
||||
@@ -17,6 +16,12 @@ pub struct UiData {
|
||||
pub textures: Textures,
|
||||
pub text: TextData,
|
||||
pub masks: TrackedArena<Mask, u32>,
|
||||
/// One entry per widget ever drawn, forming the parent-linked chain
|
||||
/// `resolve_move` walks in both shader stages. Allocated once on a
|
||||
/// widget's first draw and reused for every later redraw of the same
|
||||
/// id (never reallocated), so a retained descendant's `parent` index
|
||||
/// never goes stale -- see LAYOUT.md section 2.
|
||||
pub move_offsets: TrackedArena<MoveOffset, u32>,
|
||||
}
|
||||
|
||||
pub trait UiRsc {
|
||||
|
||||
+69
-29
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
|
||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
|
||||
render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||
RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion,
|
||||
UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
|
||||
render::{GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||
util::Vec2,
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct Painter<'a> {
|
||||
|
||||
pub(super) region: UiRegion,
|
||||
pub(super) mask: MaskIdx,
|
||||
pub(super) move_slot: MoveIdx,
|
||||
pub(super) textures: Vec<TextureHandle>,
|
||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||
pub(super) children: Vec<WidgetId>,
|
||||
@@ -28,6 +29,7 @@ impl<'a> Painter<'a> {
|
||||
primitive,
|
||||
region,
|
||||
mask_idx: self.mask,
|
||||
move_idx: self.move_slot,
|
||||
},
|
||||
);
|
||||
if self.mask != MaskIdx::NONE {
|
||||
@@ -48,31 +50,80 @@ impl<'a> Painter<'a> {
|
||||
|
||||
pub fn set_mask(&mut self, region: UiRegion) {
|
||||
assert!(self.mask == MaskIdx::NONE);
|
||||
self.mask = self.rsc.ui_mut().masks.push(Mask { region });
|
||||
self.mask = self.rsc.ui_mut().masks.push(Mask {
|
||||
region,
|
||||
move_idx: self.move_slot,
|
||||
});
|
||||
}
|
||||
|
||||
/// Draws a widget within this widget's region.
|
||||
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
|
||||
self.widget_at(id, self.region);
|
||||
/// Draws a widget within this widget's region, returning the size it
|
||||
/// reported using.
|
||||
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) -> Size {
|
||||
self.widget_at(id, self.region)
|
||||
}
|
||||
|
||||
/// Draws a widget somewhere within this one.
|
||||
/// Useful for drawing child widgets in select areas.
|
||||
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
|
||||
self.widget_at(id, region.within(&self.region));
|
||||
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) -> Size {
|
||||
self.widget_at(id, region.within(&self.region))
|
||||
}
|
||||
|
||||
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
|
||||
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) -> Size {
|
||||
self.children.push(id.id());
|
||||
// Passed directly rather than looked up from `self.active`: this
|
||||
// widget's own `ActiveData` (which would carry its `move_slot`) is
|
||||
// not inserted there until *after* its own `Widget::draw` returns,
|
||||
// so a lookup here -- for a child drawn partway through that same
|
||||
// call -- would always find nothing. `self.move_slot` is this
|
||||
// widget's own slot, already known, and always correct regardless
|
||||
// of insertion order. See `UiRenderState::move_parent_of`.
|
||||
self.state.draw_inner(
|
||||
self.layer,
|
||||
id.id(),
|
||||
region,
|
||||
Some(self.id),
|
||||
self.move_slot.idx() as u32,
|
||||
self.mask,
|
||||
None,
|
||||
None,
|
||||
self.rsc,
|
||||
);
|
||||
self.state
|
||||
.active
|
||||
.get(&id.id())
|
||||
.map(|a| a.size)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Move an already-drawn child from wherever it currently sits to
|
||||
/// `region` (resolved against this widget's own region, matching
|
||||
/// `widget_within`) without a second draw -- an O(1) offset write via
|
||||
/// `UiRenderState::mov`. For a container that draws a child
|
||||
/// provisionally to learn its size (e.g. `Aligned`) and then places it
|
||||
/// for real. Only valid when the target keeps the child's drawn size;
|
||||
/// if the shape actually changes, the normal `widget_within` dispatch
|
||||
/// (which detects that from the stored region) does the right thing
|
||||
/// instead.
|
||||
pub fn reposition<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
|
||||
let region = region.within(&self.region);
|
||||
self.state.reposition(id.id(), region, self.rsc);
|
||||
}
|
||||
|
||||
/// Draw `child` at a provisional region to learn its size under one
|
||||
/// axis's worth of assumption, discard everything it wrote, then draw
|
||||
/// it again at the region that assumption produced. For the rare
|
||||
/// parent that cannot pick an offered size without already knowing the
|
||||
/// answer. Twice the cost of one `draw`; every other case in this file
|
||||
/// avoids it.
|
||||
pub fn draw_twice<W: ?Sized>(
|
||||
&mut self,
|
||||
id: &StrongWidget<W>,
|
||||
first: UiRegion,
|
||||
second: impl FnOnce(Size) -> UiRegion,
|
||||
) -> Size {
|
||||
let used = self.widget_within(id, first);
|
||||
let region = second(used);
|
||||
self.widget_within(id, region)
|
||||
}
|
||||
|
||||
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||
@@ -94,10 +145,14 @@ impl<'a> Painter<'a> {
|
||||
/// the layer's one instanced draw, so it goes through
|
||||
/// `Primitives::write_image` instead of `primitive_at`/`Primitive::vec`.
|
||||
fn write_image(&mut self, texture_idx: u32, region: UiRegion) {
|
||||
let h = self
|
||||
.state
|
||||
.layers
|
||||
.write_image(self.layer, self.id, texture_idx, region, self.mask);
|
||||
let h = self.state.layers.write_image(
|
||||
self.layer,
|
||||
self.id,
|
||||
texture_idx,
|
||||
region,
|
||||
self.mask,
|
||||
self.move_slot,
|
||||
);
|
||||
if self.mask != MaskIdx::NONE {
|
||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
||||
}
|
||||
@@ -151,17 +206,6 @@ impl<'a> Painter<'a> {
|
||||
self.region
|
||||
}
|
||||
|
||||
pub fn size<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size {
|
||||
self.size_ctx().size(id)
|
||||
}
|
||||
|
||||
pub fn len_axis<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Len {
|
||||
match axis {
|
||||
Axis::X => self.size_ctx().width(id),
|
||||
Axis::Y => self.size_ctx().height(id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_size(&self) -> Vec2 {
|
||||
self.state.output_size
|
||||
}
|
||||
@@ -189,8 +233,4 @@ impl<'a> Painter<'a> {
|
||||
pub fn id(&self) -> &WidgetId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn size_ctx(&mut self) -> SizeCtx<'_> {
|
||||
self.state.size_ctx(self.id, self.region.size(), self.rsc)
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,60 @@
|
||||
use crate::{
|
||||
ActiveData, Axis, IdLike, MaskIdx, Painter, PixelRegion, PrimitiveLayers, SizeCtx,
|
||||
ActiveData, IdLike, MaskIdx, MoveIdx, Painter, PixelRegion, PrimitiveLayers, RegionAlign,
|
||||
StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
|
||||
ui::cache::Cache,
|
||||
util::{HashMap, HashSet, Vec2, forget_ref},
|
||||
render::MoveOffset,
|
||||
util::{HashMap, HashSet, Id, Vec2},
|
||||
};
|
||||
|
||||
pub struct UiRenderState {
|
||||
pub active: HashMap<WidgetId, ActiveData>,
|
||||
pub layers: PrimitiveLayers,
|
||||
pub(super) output_size: Vec2,
|
||||
pub cache: Cache,
|
||||
|
||||
old_root: Option<WidgetId>,
|
||||
resized: bool,
|
||||
draw_started: HashSet<WidgetId>,
|
||||
|
||||
/// `Widget::draw` calls and `Primitives::region_mut` rewrites since the
|
||||
/// last `take_counters`. LAYOUT.md section 8's pass conditions are
|
||||
/// stated in terms of these two: an unchanged frame must cost 0 of
|
||||
/// each, and moving one widget must cost 0 draws and 0 rewrites
|
||||
/// regardless of how many primitives are in its subtree.
|
||||
draw_count: u64,
|
||||
region_mut_count: u64,
|
||||
mov_count: u64,
|
||||
}
|
||||
|
||||
/// A move chain more than this deep would mean something else is wrong
|
||||
/// (an accidental cycle) -- see `resolve_move` in shader.wgsl, which walks
|
||||
/// the identical bound and must be kept in step with this constant.
|
||||
pub const MOVE_CHAIN_LIMIT: usize = 16;
|
||||
|
||||
impl UiRenderState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active: Default::default(),
|
||||
layers: Default::default(),
|
||||
cache: Default::default(),
|
||||
output_size: Vec2::ZERO,
|
||||
old_root: None,
|
||||
resized: false,
|
||||
draw_started: Default::default(),
|
||||
draw_count: 0,
|
||||
region_mut_count: 0,
|
||||
mov_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads and zeroes the (draws, region_mut rewrites, move_offsets
|
||||
/// writes) counters -- call once per frame before `update()` to
|
||||
/// measure exactly that frame, per LAYOUT.md section 8.
|
||||
pub fn take_counters(&mut self) -> (u64, u64, u64) {
|
||||
(
|
||||
std::mem::take(&mut self.draw_count),
|
||||
std::mem::take(&mut self.region_mut_count),
|
||||
std::mem::take(&mut self.mov_count),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||
self.output_size = size.into();
|
||||
self.resized = true;
|
||||
@@ -65,10 +91,37 @@ impl UiRenderState {
|
||||
self.clear(rsc);
|
||||
// free all resources & cache
|
||||
if let Some(id) = root {
|
||||
self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, None, rsc);
|
||||
self.draw_inner(
|
||||
0,
|
||||
id.id(),
|
||||
UiRegion::FULL,
|
||||
None,
|
||||
MoveOffset::NONE_PARENT,
|
||||
MaskIdx::NONE,
|
||||
None,
|
||||
None,
|
||||
rsc,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The slot an *already-active* widget's `move_offsets` entry chains
|
||||
/// to, read back from `self.active`. Only valid where the parent is
|
||||
/// guaranteed to already be in `self.active` -- true for `redraw()`,
|
||||
/// which targets a widget that was fully drawn on some earlier update,
|
||||
/// but **not** for a widget being drawn as part of its own parent's
|
||||
/// `Widget::draw` call: that parent's `ActiveData` is not inserted
|
||||
/// until its `draw` returns (below), so a child drawn partway through
|
||||
/// it would always read back "no parent" here. `Painter::widget_at`
|
||||
/// avoids that trap by passing its own already-known `move_slot`
|
||||
/// straight through instead of asking `self.active` to look it up.
|
||||
fn move_parent_of(&self, parent: Option<WidgetId>) -> u32 {
|
||||
parent
|
||||
.and_then(|p| self.active.get(&p))
|
||||
.map(|p| p.move_slot.idx() as u32)
|
||||
.unwrap_or(MoveOffset::NONE_PARENT)
|
||||
}
|
||||
|
||||
// TODO: should prolly make a DrawInfo struct or smth for everything other than rsc
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn draw_inner(
|
||||
@@ -77,11 +130,14 @@ impl UiRenderState {
|
||||
id: WidgetId,
|
||||
region: UiRegion,
|
||||
parent: Option<WidgetId>,
|
||||
parent_move_slot: u32,
|
||||
mask: MaskIdx,
|
||||
old_children: Option<Vec<WidgetId>>,
|
||||
old_move_slot: Option<MoveIdx>,
|
||||
rsc: &mut dyn UiRsc,
|
||||
) {
|
||||
let mut old_children = old_children.unwrap_or_default();
|
||||
let mut old_move_slot = old_move_slot;
|
||||
if let Some(active) = self.active.get_mut(&id)
|
||||
&& !rsc.widgets().needs_redraw.contains(&id)
|
||||
{
|
||||
@@ -91,21 +147,69 @@ impl UiRenderState {
|
||||
} else if active.region.size() == region.size() {
|
||||
// TODO: epsilon?
|
||||
let from = active.region;
|
||||
self.mov(id, from, region);
|
||||
self.mov(id, from, region, rsc);
|
||||
return;
|
||||
} else if rsc
|
||||
.widgets()
|
||||
.get_dyn(id)
|
||||
.map(|w| w.is_size_independent())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// The offered region changed shape, but this widget's own
|
||||
// drawn output does not depend on it (a fixed-size leaf) --
|
||||
// rewrite its own primitives' regions in place (O(primitives
|
||||
// owned directly by this widget, which for a leaf is O(1))
|
||||
// instead of redrawing. See LAYOUT.md section 3.
|
||||
let from = active.region;
|
||||
for h in &active.primitives {
|
||||
let r = self.layers[h.layer].region_mut(h);
|
||||
*r = r.outside(&from).within(®ion);
|
||||
self.region_mut_count += 1;
|
||||
}
|
||||
active.region = region;
|
||||
return;
|
||||
}
|
||||
// if not, then maintain resize and track old children to remove unneeded
|
||||
let active = self.remove(id, false, rsc).unwrap();
|
||||
old_children = active.children;
|
||||
old_move_slot = Some(active.move_slot);
|
||||
}
|
||||
|
||||
// draw widget
|
||||
self.draw_started.insert(id);
|
||||
|
||||
let move_slot = match old_move_slot {
|
||||
// Reused across a real redraw of the same id: the fresh
|
||||
// geometry this draw is about to write is placed at its
|
||||
// correct absolute position by `region` itself, so any delta
|
||||
// accumulated before this redraw is now stale and would
|
||||
// double-offset it if left in place. The chain link (`parent`)
|
||||
// is untouched -- the logical parent has not changed.
|
||||
Some(slot) => {
|
||||
let entry = rsc.ui_mut().move_offsets.get_mut(slot);
|
||||
entry.delta = [0.0, 0.0];
|
||||
slot
|
||||
}
|
||||
None => {
|
||||
let slot = rsc
|
||||
.ui_mut()
|
||||
.move_offsets
|
||||
.push(MoveOffset::new([0.0, 0.0], parent_move_slot));
|
||||
rsc.ui_mut().move_offsets.push_ref(slot);
|
||||
if parent_move_slot != MoveOffset::NONE_PARENT {
|
||||
rsc.ui_mut()
|
||||
.move_offsets
|
||||
.push_ref(Id::preset(parent_move_slot));
|
||||
}
|
||||
slot
|
||||
}
|
||||
};
|
||||
|
||||
let mut painter = Painter {
|
||||
state: self,
|
||||
region,
|
||||
mask,
|
||||
move_slot,
|
||||
layer,
|
||||
id,
|
||||
textures: Vec::new(),
|
||||
@@ -115,7 +219,8 @@ impl UiRenderState {
|
||||
};
|
||||
|
||||
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
|
||||
widget.draw(&mut painter);
|
||||
painter.state.draw_count += 1;
|
||||
let size = widget.draw(&mut painter);
|
||||
drop(widget);
|
||||
|
||||
let Painter {
|
||||
@@ -123,6 +228,7 @@ impl UiRenderState {
|
||||
rsc: _,
|
||||
region,
|
||||
mask,
|
||||
move_slot,
|
||||
textures,
|
||||
primitives,
|
||||
children,
|
||||
@@ -140,6 +246,8 @@ impl UiRenderState {
|
||||
children,
|
||||
mask,
|
||||
layer,
|
||||
size,
|
||||
move_slot,
|
||||
};
|
||||
|
||||
// remove old children that weren't kept
|
||||
@@ -153,18 +261,66 @@ impl UiRenderState {
|
||||
self.active.insert(id, active);
|
||||
}
|
||||
|
||||
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
|
||||
let active = self.active.get_mut(&id).unwrap();
|
||||
for h in &active.primitives {
|
||||
let region = self.layers[h.layer].region_mut(h);
|
||||
*region = region.outside(&from).within(&to);
|
||||
}
|
||||
active.region = active.region.outside(&from).within(&to);
|
||||
// SAFETY: children cannot be recursive
|
||||
let children = unsafe { forget_ref(&active.children) };
|
||||
for child in children {
|
||||
self.mov(*child, from, to);
|
||||
}
|
||||
/// O(1): write the delta for this widget's own slot in
|
||||
/// `move_offsets`. No primitive is touched and there is no recursion --
|
||||
/// every descendant's primitive references this slot transitively
|
||||
/// through the parent chain the shader walks (`resolve_move`), so it
|
||||
/// picks the new delta up for free. See LAYOUT.md section 2.
|
||||
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion, rsc: &mut dyn UiRsc) {
|
||||
let Some(active) = self.active.get_mut(&id) else {
|
||||
return;
|
||||
};
|
||||
let slot = active.move_slot;
|
||||
active.region = to;
|
||||
let from_px = from.top_left().to_abs(self.output_size);
|
||||
let to_px = to.top_left().to_abs(self.output_size);
|
||||
let delta = to_px - from_px;
|
||||
let entry = rsc.ui_mut().move_offsets.get_mut(slot);
|
||||
entry.delta[0] += delta.x;
|
||||
entry.delta[1] += delta.y;
|
||||
self.mov_count += 1;
|
||||
}
|
||||
|
||||
/// Move an already-active widget to `to`. Used by `Painter::reposition`,
|
||||
/// for a parent that drew a child provisionally (at the whole region it
|
||||
/// was offered) and now knows where the child actually belongs.
|
||||
///
|
||||
/// Unlike `mov` (called by `draw_inner`'s own dispatch, where the
|
||||
/// *offered* region really did move and `active.region` already tracks
|
||||
/// it), the child here was not offered a smaller region -- it was
|
||||
/// offered everything and chose, on its own, to occupy only
|
||||
/// `active.size` of it. By convention every widget in this crate that
|
||||
/// does that anchors its own content at the top-left of whatever it
|
||||
/// was given (`Rect`/`Image`/`Sized`/`MaxSize` -- see their `draw`
|
||||
/// bodies), so that is where this assumes the child was actually
|
||||
/// painted, not `active.region` itself (which is the *offered* box,
|
||||
/// usually bigger). A nested `Aligned` whose own child is not top-left
|
||||
/// anchored -- i.e. `Aligned` wrapping `Aligned` -- is the one shape
|
||||
/// this does not cover; none of iris's widgets or examples build that
|
||||
/// today. See LAYOUT.md's "Rejected, and why" / deviations for the
|
||||
/// full reasoning.
|
||||
///
|
||||
/// The delta is overwritten, not accumulated like `mov`'s: `from` is
|
||||
/// recomputed fresh from `active.size`/`active.region` every call, so
|
||||
/// repeating the same `reposition` (e.g. an unrelated redraw elsewhere
|
||||
/// re-running this widget's parent without its own layout changing)
|
||||
/// must land on the same answer, not drift further each time.
|
||||
pub(super) fn reposition(&mut self, id: WidgetId, to: UiRegion, rsc: &mut dyn UiRsc) {
|
||||
let Some(active) = self.active.get(&id) else {
|
||||
return;
|
||||
};
|
||||
let from = active
|
||||
.size
|
||||
.to_uivec2()
|
||||
.align(RegionAlign::TOP_LEFT)
|
||||
.within(&active.region);
|
||||
let slot = active.move_slot;
|
||||
let from_px = from.top_left().to_abs(self.output_size);
|
||||
let to_px = to.top_left().to_abs(self.output_size);
|
||||
let delta = to_px - from_px;
|
||||
let entry = rsc.ui_mut().move_offsets.get_mut(slot);
|
||||
entry.delta = [delta.x, delta.y];
|
||||
self.mov_count += 1;
|
||||
}
|
||||
|
||||
/// NOTE: instance textures are cleared and self.textures freed
|
||||
@@ -180,6 +336,18 @@ impl UiRenderState {
|
||||
active.textures.clear();
|
||||
rsc.ui_mut().textures.free();
|
||||
if undraw {
|
||||
// Permanent removal: retire this widget's own move slot
|
||||
// (the self-ownership ref taken when it was allocated) and
|
||||
// the up-link ref it held on its parent's slot -- read from
|
||||
// the arena entry itself, not from `active.parent`, since
|
||||
// the parent's own `ActiveData` may already be gone by the
|
||||
// time a deep descendant is retired (see LAYOUT.md
|
||||
// section 2's lifecycle note).
|
||||
let parent_slot = rsc.ui_mut().move_offsets[active.move_slot.idx()].parent;
|
||||
rsc.ui_mut().move_offsets.remove(active.move_slot);
|
||||
if parent_slot != MoveOffset::NONE_PARENT {
|
||||
rsc.ui_mut().move_offsets.remove(Id::preset(parent_slot));
|
||||
}
|
||||
rsc.on_undraw(active);
|
||||
}
|
||||
}
|
||||
@@ -187,7 +355,6 @@ impl UiRenderState {
|
||||
}
|
||||
|
||||
fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
|
||||
self.cache.remove(id);
|
||||
let inst = self.remove(id, true, rsc);
|
||||
if let Some(inst) = &inst {
|
||||
for c in &inst.children {
|
||||
@@ -201,7 +368,6 @@ impl UiRenderState {
|
||||
for (_, active) in self.active.drain() {
|
||||
rsc.on_undraw(&active);
|
||||
}
|
||||
self.cache.clear();
|
||||
self.layers.clear();
|
||||
rsc.widgets_mut().needs_redraw.clear();
|
||||
rsc.free();
|
||||
@@ -261,8 +427,43 @@ impl UiRenderState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
||||
let region = self.active.get(&id.id())?.region;
|
||||
/// `active[id].region`, corrected by every `move_offsets` delta between
|
||||
/// `id` and the root -- the CPU-side twin of the vertex shader's chain
|
||||
/// walk, over the same arena, so the two cannot disagree about where a
|
||||
/// widget is. O(chain depth), not O(primitives). See LAYOUT.md
|
||||
/// section 2b.
|
||||
pub fn resolved_region(&self, id: &impl IdLike, rsc: &dyn UiRsc) -> Option<UiRegion> {
|
||||
let active = self.active.get(&id.id())?;
|
||||
let delta = self.resolve_move_chain(active.move_slot, rsc);
|
||||
Some(active.region.offset(UiVec2::abs(delta)))
|
||||
}
|
||||
|
||||
/// The plain-Rust twin of `resolve_move` in shader.wgsl: sums the
|
||||
/// pixel delta along the parent chain starting at `slot`. Both walks
|
||||
/// share `MOVE_CHAIN_LIMIT` as their bound so the two cannot disagree
|
||||
/// about where the chain ends.
|
||||
fn resolve_move_chain(&self, mut slot: MoveIdx, rsc: &dyn UiRsc) -> Vec2 {
|
||||
let offsets = &rsc.ui().move_offsets;
|
||||
let mut delta = Vec2::ZERO;
|
||||
for i in 0..MOVE_CHAIN_LIMIT {
|
||||
let entry = &offsets[slot.idx()];
|
||||
delta.x += entry.delta[0];
|
||||
delta.y += entry.delta[1];
|
||||
if entry.parent == MoveOffset::NONE_PARENT {
|
||||
return delta;
|
||||
}
|
||||
slot = Id::preset(entry.parent);
|
||||
debug_assert!(
|
||||
i + 1 < MOVE_CHAIN_LIMIT,
|
||||
"move offset chain exceeded MOVE_CHAIN_LIMIT; a widget's `parent` link is \
|
||||
probably cyclic"
|
||||
);
|
||||
}
|
||||
delta
|
||||
}
|
||||
|
||||
pub fn window_region(&self, id: &impl IdLike, rsc: &dyn UiRsc) -> Option<PixelRegion> {
|
||||
let region = self.resolved_region(id, rsc)?;
|
||||
Some(region.to_px(self.output_size))
|
||||
}
|
||||
|
||||
@@ -270,21 +471,6 @@ impl UiRenderState {
|
||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
self.draw_started.remove(&id);
|
||||
// check if parent depends on the desired size of this, if so then redraw it first
|
||||
for axis in [Axis::X, Axis::Y] {
|
||||
if let Some(&(outer, old)) = self.cache.size.axis_dyn(axis).get(&id)
|
||||
&& let Some(current) = self.active.get(&id)
|
||||
&& let Some(pid) = current.parent
|
||||
{
|
||||
self.cache.size.axis_dyn(axis).remove(&id);
|
||||
let new = self.size_ctx(id, outer, rsc).len_axis(id, axis);
|
||||
self.cache.size.axis_dyn(axis).insert(id, (outer, new));
|
||||
if new != old {
|
||||
self.redraw(pid, rsc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.draw_started.contains(&id) {
|
||||
return;
|
||||
}
|
||||
@@ -292,34 +478,35 @@ impl UiRenderState {
|
||||
let Some(active) = self.remove(id, false, rsc) else {
|
||||
return;
|
||||
};
|
||||
let old_size = active.size;
|
||||
let parent = active.parent;
|
||||
// `old_move_slot` being `Some` below means the slot is reused in
|
||||
// place rather than freshly parented, so this is only reached for
|
||||
// logging/clarity's sake, never actually used to link a new slot.
|
||||
let parent_move_slot = self.move_parent_of(parent);
|
||||
|
||||
self.draw_inner(
|
||||
active.layer,
|
||||
id,
|
||||
active.region,
|
||||
active.parent,
|
||||
parent,
|
||||
parent_move_slot,
|
||||
active.mask,
|
||||
Some(active.children),
|
||||
Some(active.move_slot),
|
||||
rsc,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn size_ctx<'b>(
|
||||
&'b mut self,
|
||||
source: WidgetId,
|
||||
outer: UiVec2,
|
||||
rsc: &'b mut dyn UiRsc,
|
||||
) -> SizeCtx<'b> {
|
||||
let ui = rsc.ui_mut();
|
||||
SizeCtx {
|
||||
source,
|
||||
cache: &mut self.cache,
|
||||
text: &mut ui.text,
|
||||
textures: &mut ui.textures,
|
||||
widgets: &ui.widgets,
|
||||
outer,
|
||||
output_size: self.output_size,
|
||||
id: source,
|
||||
// If this widget's own reported size changed, its parent's layout
|
||||
// (which placed it using the old size) is now stale and needs to
|
||||
// relay out too. Checked after the real draw, not before it --
|
||||
// there is no query left that answers "what size would this be"
|
||||
// without actually drawing (LAYOUT.md section 5).
|
||||
if let Some(pid) = parent {
|
||||
let new_size = self.active.get(&id).map(|a| a.size);
|
||||
if new_size != Some(old_size) {
|
||||
self.redraw(pid, rsc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
use crate::{
|
||||
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures,
|
||||
UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
||||
};
|
||||
|
||||
pub struct SizeCtx<'a> {
|
||||
pub text: &'a mut TextData,
|
||||
pub textures: &'a mut Textures,
|
||||
pub(super) source: WidgetId,
|
||||
pub(super) widgets: &'a Widgets,
|
||||
pub(super) cache: &'a mut Cache,
|
||||
/// TODO: should this be pub? rn used for sized
|
||||
pub outer: UiVec2,
|
||||
pub(super) output_size: Vec2,
|
||||
pub(super) id: WidgetId,
|
||||
}
|
||||
|
||||
impl SizeCtx<'_> {
|
||||
pub fn id(&self) -> &WidgetId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn source(&self) -> &WidgetId {
|
||||
&self.source
|
||||
}
|
||||
|
||||
pub(super) fn len_inner<A: const AxisT>(&mut self, id: WidgetId) -> Len {
|
||||
if let Some((_, len)) = self.cache.size.axis::<A>().get(&id) {
|
||||
return *len;
|
||||
}
|
||||
let len = self
|
||||
.widgets
|
||||
.get_dyn_dynamic(id)
|
||||
.desired_len::<A>(&mut SizeCtx {
|
||||
text: self.text,
|
||||
textures: self.textures,
|
||||
source: self.source,
|
||||
widgets: self.widgets,
|
||||
cache: self.cache,
|
||||
outer: self.outer,
|
||||
output_size: self.output_size,
|
||||
id,
|
||||
});
|
||||
self.cache.size.axis::<A>().insert(id, (self.outer, len));
|
||||
len
|
||||
}
|
||||
|
||||
pub fn width(&mut self, id: impl IdLike) -> Len {
|
||||
self.len_inner::<XAxis>(id.id())
|
||||
}
|
||||
|
||||
pub fn height(&mut self, id: impl IdLike) -> Len {
|
||||
self.len_inner::<YAxis>(id.id())
|
||||
}
|
||||
|
||||
pub fn len_axis(&mut self, id: impl IdLike, axis: Axis) -> Len {
|
||||
match axis {
|
||||
Axis::X => self.width(id),
|
||||
Axis::Y => self.height(id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&mut self, id: impl IdLike) -> Size {
|
||||
let id = id.id();
|
||||
Size {
|
||||
x: self.width(id),
|
||||
y: self.height(id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn px_size(&mut self) -> Vec2 {
|
||||
self.outer.to_abs(self.output_size)
|
||||
}
|
||||
|
||||
pub fn output_size(&mut self) -> Vec2 {
|
||||
self.output_size
|
||||
}
|
||||
|
||||
pub fn draw_text(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
self.text.render(buffer, attrs, width, self.textures)
|
||||
}
|
||||
|
||||
pub fn label(&self, id: WidgetId) -> &String {
|
||||
self.widgets.label(id)
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,15 @@ impl<T, I: IdNum> TrackedArena<T, I> {
|
||||
self.refs[i.idx()] += 1;
|
||||
}
|
||||
|
||||
/// Mutable access to an existing entry, for the rare case (the move
|
||||
/// offset chain) where an already-allocated slot is updated in place
|
||||
/// rather than replaced. Marks the arena changed so the GPU copy is
|
||||
/// re-uploaded.
|
||||
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
||||
self.changed = true;
|
||||
&mut self.inner.data[id.idx()]
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: Id<I>) -> T
|
||||
where
|
||||
T: Copy,
|
||||
|
||||
+17
-20
@@ -1,4 +1,4 @@
|
||||
use crate::{Axis, AxisT, Len, Painter, SizeCtx};
|
||||
use crate::{Painter, Size};
|
||||
use std::any::Any;
|
||||
|
||||
mod data;
|
||||
@@ -16,31 +16,28 @@ pub use view::*;
|
||||
pub use widgets::*;
|
||||
|
||||
pub trait Widget: Any {
|
||||
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;
|
||||
}
|
||||
/// Draw within `painter.region()` (the space the parent offered) and
|
||||
/// report how much of it was actually used, per axis.
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size;
|
||||
|
||||
pub trait WidgetAxisFns {
|
||||
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len;
|
||||
}
|
||||
|
||||
impl<W: Widget + ?Sized> WidgetAxisFns for W {
|
||||
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
match A::get() {
|
||||
Axis::X => self.desired_width(ctx),
|
||||
Axis::Y => self.desired_height(ctx),
|
||||
}
|
||||
/// True if `draw`'s output (both the primitives it writes and the
|
||||
/// `Size` it returns) is the same for any `painter.region()` of the
|
||||
/// same *content* -- an icon, a fixed-size rect, an already-decoded
|
||||
/// image at its natural size. Default `false` (redraw on any change to
|
||||
/// the offered region) because assuming independence wrongly produces
|
||||
/// a stale draw; a widget must opt in. See LAYOUT.md.
|
||||
fn is_size_independent(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for () {
|
||||
fn draw(&mut self, _: &mut Painter) {}
|
||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||
Len::ZERO
|
||||
fn draw(&mut self, _: &mut Painter) -> Size {
|
||||
Size::ZERO
|
||||
}
|
||||
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
||||
Len::ZERO
|
||||
|
||||
fn is_size_independent(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user