diff --git a/core/src/render/data.rs b/core/src/render/data.rs index d5b53a6..517b694 100644 --- a/core/src/render/data.rs +++ b/core/src/render/data.rs @@ -65,13 +65,16 @@ impl MoveIdx { } } -/// One link of the chain a primitive's position is resolved through: a -/// translation in physical pixels, and the slot it is relative to. Moving a -/// subtree writes its own slot and nothing else. +/// 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 delta: Vec2, + pub region: UiRegion, pub parent: MoveIdx, } @@ -81,7 +84,7 @@ unsafe impl bytemuck::Zeroable for MoveOffset {} impl MoveOffset { pub fn root(parent: MoveIdx) -> Self { Self { - delta: Vec2::ZERO, + region: UiRegion::FULL, parent, } } diff --git a/core/src/render/shader/prelude.wgsl b/core/src/render/shader/prelude.wgsl index 3fc9cf2..42bd824 100644 --- a/core/src/render/shader/prelude.wgsl +++ b/core/src/render/shader/prelude.wgsl @@ -21,28 +21,45 @@ struct Mask { } struct MoveOffset { - delta: vec2, + 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 resolve_move(idx: u32) -> vec2 { - var total = vec2(0.0, 0.0); +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]; - total += entry.delta; + r = Region(span_within(r.x, entry.x), span_within(r.y, entry.y)); at = entry.parent; } - return total; + return r; } struct UiSpan { @@ -81,14 +98,18 @@ fn vs_main( ) -> VertexOutput { var out: VertexOutput; - let top_left_rel = vec2(in.x_start.x, in.y_start.x); - let top_left_abs = vec2(in.x_start.y, in.y_start.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 local = Region( + UiSpan(UiScalar(in.x_start.x, in.x_start.y), UiScalar(in.x_end.x, in.x_end.y)), + UiSpan(UiScalar(in.y_start.x, in.y_start.y), UiScalar(in.y_end.x, 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 moved = resolve_move(in.move_idx); - let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs + moved); - let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs + moved); + 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 size = bot_right - top_left; let uv = vec2( @@ -111,16 +132,16 @@ fn masked(in: VertexOutput, color: vec4) -> vec4 { return color; } let mask = masks[in.mask_idx]; - let tl = vec2(mask.x.start.rel, mask.y.start.rel); - let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs); - let br = vec2(mask.x.end.rel, mask.y.end.rel); - let br_abs = vec2(mask.x.end.abs, mask.y.end.abs); - // Its own chain, not the drawn primitive's, so a stationary viewport // clips content that moves inside it. - let moved = resolve_move(mask.move_idx); - let top_left = floor(tl * window.dim) + floor(tl_abs + moved); - let bot_right = floor(br * window.dim) + floor(br_abs + moved); + let m = resolve_move(mask.move_idx, Region(mask.x, mask.y)); + let tl = vec2(m.x.start.rel, m.y.start.rel); + 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 bot_right = floor(br * window.dim) + floor(br_abs); let pos = in.clip_position.xy; if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y { return color * 0.0; diff --git a/core/src/ui/mod.rs b/core/src/ui/mod.rs index 5bdd6cb..881dad5 100644 --- a/core/src/ui/mod.rs +++ b/core/src/ui/mod.rs @@ -1,7 +1,7 @@ use crate::{ - Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, - Widgets, - util::{Arena, Id, TrackedArena, Vec2}, + Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, UiRegion, WeakWidget, + WidgetId, Widgets, + util::{Arena, Id, TrackedArena}, }; /// How far the shader will walk a move chain. It bounds a malformed cycle @@ -46,26 +46,27 @@ impl Moves { self.arena.remove(Id::preset(idx.idx() as u32)); } - /// Sets a slot's translation, in physical pixels, relative to its parent. - pub fn set(&mut self, idx: MoveIdx, delta: Vec2) { + /// 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.delta != delta { - entry.delta = delta; + if entry.region != region { + entry.region = region; self.changed = true; } } - /// The translation a primitive in `idx` has accumulated, which is the - /// same walk the vertex shader does. - pub fn resolve(&self, idx: MoveIdx) -> Vec2 { - let mut total = Vec2::ZERO; + /// 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 total; + return region; } let entry = self.arena[at.idx()]; - total += entry.delta; + region = region.within(&entry.region); at = entry.parent; } debug_assert!( @@ -73,7 +74,7 @@ impl Moves { "a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \ and the shader stops at the same depth" ); - total + region } pub fn entries(&self) -> &[MoveOffset] { diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 4ae56ae..6d0e2b3 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -110,7 +110,7 @@ impl UiRenderState { // draw widget let move_idx = self.move_slot(id, parent); - self.moves.set(move_idx, Vec2::ZERO); + self.moves.set(move_idx, UiRegion::FULL); rsc.widgets_mut().needs_redraw.remove(&id); self.draw_started.insert(id); @@ -211,7 +211,7 @@ impl UiRenderState { // coordinates its parent drew, so the chain above applies alike. let moved = region.to_px(self.output_size).top_left - old.to_px(self.output_size).top_left; - self.moves.set(slot, moved); + self.moves.set(slot, UiRegion::FULL.offset(moved)); return Some(size); } if !self.reusable(id, region, rsc) || !old.stretchable() { @@ -269,7 +269,7 @@ impl UiRenderState { *region = region.stretch(&from, &to); } active.region = active.region.stretch(&from, &to); - self.moves.set(active.move_idx, Vec2::ZERO); + self.moves.set(active.move_idx, UiRegion::FULL); // SAFETY: children cannot be recursive let children = unsafe { forget_ref(&active.children) }; for child in children { @@ -367,12 +367,8 @@ impl UiRenderState { /// chain above has moved since, which is the walk the vertex shader does. pub fn window_region(&self, id: &impl IdLike) -> Option { let active = self.active.get(&id.id())?; - let moved = self.moves.resolve(active.move_idx); - let region = active.region.to_px(self.output_size); - Some(PixelRegion { - top_left: region.top_left + moved, - bot_right: region.bot_right + moved, - }) + let region = self.moves.resolve(active.move_idx, active.region); + Some(region.to_px(self.output_size)) } /// redraws a widget that's currently active (drawn)