iris: eliminate retained-frame heap churn

This commit is contained in:
iris committed 2026-09-12 22:44:03 -04:00
1 parent badd758d9b
commit 715c86e7e2
22 files changed
+527 -196

No files matched your search

+6
View File
@@ -215,6 +215,12 @@ widget observed during that same draw; the next draw replaces the list, so a
dependency disappears as soon as the widget stops reading it. Both fields dependency disappears as soon as the widget stops reading it. Both fields
have `ActiveData`'s existing lifecycle through `remove`/`remove_rec`. have `ActiveData`'s existing lifecycle through `remove`/`remove_rec`.
Retained draw output uses two buffers per collection. A redraw clears and
fills the spare child, primitive, texture, and paint buffers while consuming
the current buffers for reuse, then swaps their roles. Stable redraws therefore
reuse vector capacity and move matching resource handles instead of allocating
new collections or changing resource reference counts each frame.
### 6. Rejected alternatives ### 6. Rejected alternatives
- **A flat (non-chained) per-subtree offset table**, Iris's literal - **A flat (non-chained) per-subtree offset table**, Iris's literal
+3 -1
View File
@@ -819,7 +819,9 @@ reads it during `draw` and calls `request_next_frame` while it remains active.
Iris stages the requesting widget's invalidation until the current retained Iris stages the requesting widget's invalidation until the current retained
redraw has finished, then the host schedules the next platform frame before redraw has finished, then the host schedules the next platform frame before
renderer update or swapchain acquisition. `Widget::draw` still returns unit, renderer update or swapchain acquisition. `Widget::draw` still returns unit,
and `Widget` has no tick callback. and `Widget` has no tick callback. Retained draw records alternate between two
buffers for children, primitives, textures, and paints, so steady frame-driven
redraws reuse their storage and matching resource handles.
**Iris ships no fonts, and font families are application-named strings** **Iris ships no fonts, and font families are application-named strings**
(2026-09-12). Applications register their own font bytes on `Ui` and select (2026-09-12). Applications register their own font bytes on `Ui` and select
+17 -2
View File
@@ -617,6 +617,15 @@ impl From<&TextAttrs> for TextShapeAttrs {
} }
} }
impl TextShapeAttrs {
fn matches(&self, attrs: &TextAttrs) -> bool {
self.color == attrs.color
&& self.font_size == attrs.font_size
&& self.line_height == attrs.line_height
&& self.family == attrs.family
}
}
pub const LINE_HEIGHT_MULT: f32 = 1.1; pub const LINE_HEIGHT_MULT: f32 = 1.1;
impl Default for TextAttrs { impl Default for TextAttrs {
@@ -701,10 +710,16 @@ impl TextBuffer {
width: Option<f32>, width: Option<f32>,
density: f32, density: f32,
) { ) {
let shape_attrs = TextShapeAttrs::from(attrs); if self
if self.shaped.as_ref() == Some(&(shape_attrs.clone(), width, density)) { .shaped
.as_ref()
.is_some_and(|(old, old_width, old_density)| {
old.matches(attrs) && *old_width == width && *old_density == density
})
{
return; return;
} }
let shape_attrs = TextShapeAttrs::from(attrs);
let base_family = data.resolve_family(&attrs.family); let base_family = data.resolve_family(&attrs.family);
let span_families: Vec<Option<String>> = self let span_families: Vec<Option<String>> = self
.spans .spans
+8
View File
@@ -34,6 +34,14 @@ pub struct TextureHandle {
rsc: RscHandle<TextureRsc>, rsc: RscHandle<TextureRsc>,
} }
impl PartialEq for TextureHandle {
fn eq(&self, other: &Self) -> bool {
self.rsc.id() == other.rsc.id()
}
}
impl Eq for TextureHandle {}
/// a texture manager for a ui /// a texture manager for a ui
/// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped /// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped
pub struct Textures { pub struct Textures {
+15 -5
View File
@@ -112,6 +112,14 @@ impl WgpuErrorLog {
pub fn snapshot(&self) -> Vec<String> { pub fn snapshot(&self) -> Vec<String> {
self.errors.lock().unwrap().iter().cloned().collect() self.errors.lock().unwrap().iter().cloned().collect()
} }
pub fn len(&self) -> usize {
self.errors.lock().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.errors.lock().unwrap().is_empty()
}
} }
pub struct UiRenderNode { pub struct UiRenderNode {
@@ -206,11 +214,13 @@ impl UiRenderNode {
rlayer.order.update(device, queue, entries, dirty); rlayer.order.update(device, queue, entries, dirty);
let (entries, dirty) = order.images_for_upload(); let (entries, dirty) = order.images_for_upload();
rlayer.images.update(device, queue, entries, dirty); rlayer.images.update(device, queue, entries, dirty);
rlayer.image_tex_indices = order rlayer.image_tex_indices.clear();
.images() rlayer.image_tex_indices.extend(
.iter() order
.map(|&slot| ui_render.primitives.instance(slot).idx) .images()
.collect(); .iter()
.map(|&slot| ui_render.primitives.instance(slot).idx),
);
order.updated = false; order.updated = false;
} }
} }
+21 -16
View File
@@ -450,19 +450,26 @@ impl LayerOrder {
} }
pub fn apply_free(&mut self) -> Vec<OrderChange> { pub fn apply_free(&mut self) -> Vec<OrderChange> {
let mut changes = Self::apply_free_list( let mut changes = Vec::new();
self.apply_free_into(&mut changes);
changes
}
pub(crate) fn apply_free_into(&mut self, changes: &mut Vec<OrderChange>) {
Self::apply_free_list(
&mut self.free, &mut self.free,
&mut self.order, &mut self.order,
&mut self.order_dirty, &mut self.order_dirty,
false, false,
changes,
); );
changes.extend(Self::apply_free_list( Self::apply_free_list(
&mut self.image_free, &mut self.image_free,
&mut self.images, &mut self.images,
&mut self.images_dirty, &mut self.images_dirty,
true, true,
)); changes,
changes );
} }
pub fn order_for_upload(&mut self) -> (&[u32], &mut Dirty) { pub fn order_for_upload(&mut self) -> (&[u32], &mut Dirty) {
@@ -478,22 +485,20 @@ impl LayerOrder {
list: &mut Vec<u32>, list: &mut Vec<u32>,
dirty: &mut Dirty, dirty: &mut Dirty,
is_image: bool, is_image: bool,
) -> Vec<OrderChange> { changes: &mut Vec<OrderChange>,
) {
free.sort_by(|a, b| b.cmp(a)); free.sort_by(|a, b| b.cmp(a));
free.drain(..) for pos in free.drain(..) {
.filter_map(|pos| { list.swap_remove(pos);
list.swap_remove(pos); if pos != list.len() {
if pos == list.len() {
return None;
}
dirty.mark(pos); dirty.mark(pos);
Some(OrderChange { changes.push(OrderChange {
slot: list[pos], slot: list[pos],
is_image, is_image,
pos, pos,
}) });
}) }
.collect() }
} }
pub fn order(&self) -> &Vec<u32> { pub fn order(&self) -> &Vec<u32> {
@@ -523,7 +528,7 @@ pub enum Drawn {
pub const NOT_DRAWN: usize = usize::MAX; pub const NOT_DRAWN: usize = usize::MAX;
#[derive(Debug)] #[derive(Clone, Copy, Debug)]
pub struct PrimitiveHandle { pub struct PrimitiveHandle {
pub layer: usize, pub layer: usize,
pub pos: usize, pub pos: usize,
+2 -2
View File
@@ -90,13 +90,13 @@ impl<T: Pod> ArrBuf<T> {
} }
self.len = data.len(); self.len = data.len();
let stride = std::mem::size_of::<T>() as BufferAddress; let stride = std::mem::size_of::<T>() as BufferAddress;
for range in dirty.ranges(data.len(), Self::MERGE_GAP) { dirty.for_each_range(data.len(), Self::MERGE_GAP, |range| {
queue.write_buffer( queue.write_buffer(
&self.buffer, &self.buffer,
range.start as BufferAddress * stride, range.start as BufferAddress * stride,
bytemuck::cast_slice(&data[range]), bytemuck::cast_slice(&data[range]),
); );
} });
dirty.clear(); dirty.clear();
reallocated reallocated
} }
+57 -22
View File
@@ -10,11 +10,11 @@ fn node_id(id: WidgetId) -> NodeId {
NodeId(id.as_u64()) NodeId(id.as_u64())
} }
#[derive(Clone, PartialEq)]
struct Entry { struct Entry {
name: String, name: String,
role: Role, role: Role,
bounds: PixelRegion, bounds: PixelRegion,
seen: u64,
} }
fn entry_node(entry: &Entry) -> Node { fn entry_node(entry: &Entry) -> Node {
@@ -37,6 +37,7 @@ fn entry_node(entry: &Entry) -> Node {
#[derive(Default)] #[derive(Default)]
pub struct AccessTree { pub struct AccessTree {
known: HashMap<WidgetId, Entry>, known: HashMap<WidgetId, Entry>,
generation: u64,
rebuilds: u64, rebuilds: u64,
} }
@@ -45,12 +46,17 @@ impl AccessTree {
Self::default() Self::default()
} }
fn collect( /// Refresh the retained accessibility state and report whether it changed.
widgets: &Widgets, /// Existing entries are updated in place so an ordinary frame allocates
render: &UiRenderState, /// nothing, including one where only bounds changed.
rsc: &dyn UiRsc, pub fn refresh(&mut self, widgets: &Widgets, render: &UiRenderState, rsc: &dyn UiRsc) -> bool {
) -> HashMap<WidgetId, Entry> { self.generation = self.generation.wrapping_add(1);
let mut current = HashMap::default(); if self.generation == 0 {
self.known.clear();
self.generation = 1;
}
let generation = self.generation;
let mut changed = false;
for id in widgets.named() { for id in widgets.named() {
let Some(bounds) = render.window_region(&id, rsc) else { let Some(bounds) = render.window_region(&id, rsc) else {
continue; continue;
@@ -58,16 +64,42 @@ impl AccessTree {
let Some(widget) = widgets.get_dyn(id) else { let Some(widget) = widgets.get_dyn(id) else {
continue; continue;
}; };
current.insert( let name = widgets.label(id);
id, let role = widget.access_role();
Entry { match self.known.get_mut(&id) {
name: widgets.label(id).clone(), Some(entry) => {
role: widget.access_role(), if entry.name != *name {
bounds, entry.name.clone_from(name);
}, changed = true;
); }
if entry.role != role || entry.bounds != bounds {
entry.role = role;
entry.bounds = bounds;
changed = true;
}
entry.seen = generation;
}
None => {
self.known.insert(
id,
Entry {
name: name.clone(),
role,
bounds,
seen: generation,
},
);
changed = true;
}
}
} }
current let old_len = self.known.len();
self.known.retain(|_, entry| entry.seen == generation);
changed |= self.known.len() != old_len;
if changed {
self.rebuilds += 1;
}
changed
} }
/// Walks `widgets.named()`, looks up each one's current screen bounds /// Walks `widgets.named()`, looks up each one's current screen bounds
@@ -84,13 +116,14 @@ impl AccessTree {
render: &UiRenderState, render: &UiRenderState,
rsc: &dyn UiRsc, rsc: &dyn UiRsc,
) -> Option<TreeUpdate> { ) -> Option<TreeUpdate> {
let current = Self::collect(widgets, render, rsc); if !self.refresh(widgets, render, rsc) {
if current == self.known {
return None; return None;
} }
self.known = current.clone(); Some(self.tree_update())
self.rebuilds += 1; }
Some(build_update(&current))
pub fn tree_update(&self) -> TreeUpdate {
build_update(&self.known)
} }
/// The unconditional twin of `update`, for a platform adapter's /// The unconditional twin of `update`, for a platform adapter's
@@ -100,7 +133,9 @@ impl AccessTree {
/// meant to answer (it may have already sent this same snapshot to a /// meant to answer (it may have already sent this same snapshot to a
/// client that has since detached and reattached). /// client that has since detached and reattached).
pub fn build_full(widgets: &Widgets, render: &UiRenderState, rsc: &dyn UiRsc) -> TreeUpdate { pub fn build_full(widgets: &Widgets, render: &UiRenderState, rsc: &dyn UiRsc) -> TreeUpdate {
build_update(&Self::collect(widgets, render, rsc)) let mut tree = Self::new();
tree.refresh(widgets, render, rsc);
tree.tree_update()
} }
pub fn take_rebuilds(&mut self) -> u64 { pub fn take_rebuilds(&mut self) -> u64 {
+4
View File
@@ -9,12 +9,16 @@ pub struct ActiveData {
pub region: UiRegion, pub region: UiRegion,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
pub(crate) spare_textures: Vec<TextureHandle>,
/// Paint slots retained by this draw. The GPU primitive stores only the /// Paint slots retained by this draw. The GPU primitive stores only the
/// slot index, so these handles are what prevent a live primitive from /// slot index, so these handles are what prevent a live primitive from
/// observing a recycled paint. /// observing a recycled paint.
pub paints: Vec<PaintId>, pub paints: Vec<PaintId>,
pub(crate) spare_paints: Vec<PaintId>,
pub primitives: Vec<PrimitiveHandle>, pub primitives: Vec<PrimitiveHandle>,
pub(crate) spare_primitives: Vec<PrimitiveHandle>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
pub(crate) spare_children: Vec<WidgetId>,
pub size_dependencies: Vec<WidgetId>, pub size_dependencies: Vec<WidgetId>,
pub mask: MaskIdx, pub mask: MaskIdx,
/// The widget's retained mask slot, or `MaskIdx::NONE`. /// The widget's retained mask slot, or `MaskIdx::NONE`.
+4 -6
View File
@@ -122,13 +122,11 @@ impl Ui {
update(&mut text)?; update(&mut text)?;
text.invalidate_all() text.invalidate_all()
}; };
let active: Vec<WidgetId> = { let mut active = owners;
{
let render = self.render_state.get(); let render = self.render_state.get();
owners active.retain(|owner| render.active.contains_key(owner));
.into_iter() }
.filter(|owner| render.active.contains_key(owner))
.collect()
};
self.data.widgets.needs_redraw.extend(active); self.data.widgets.needs_redraw.extend(active);
Ok(()) Ok(())
} }
+28 -9
View File
@@ -21,9 +21,12 @@ pub struct Painter<'a> {
pub(super) child_move_slot: Option<MoveIdx>, pub(super) child_move_slot: Option<MoveIdx>,
pub(super) own_mask: MaskIdx, pub(super) own_mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) recycle_textures: Vec<TextureHandle>,
pub(super) paints: Vec<PaintId>, pub(super) paints: Vec<PaintId>,
pub(super) recycle_paints: Vec<PaintId>,
pub(super) primitives: Vec<PrimitiveHandle>, pub(super) primitives: Vec<PrimitiveHandle>,
pub(super) recycle: std::iter::Peekable<std::vec::IntoIter<PrimitiveHandle>>, pub(super) recycle: Vec<PrimitiveHandle>,
pub(super) recycle_at: usize,
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
pub(super) size_dependencies: Vec<WidgetId>, pub(super) size_dependencies: Vec<WidgetId>,
pub(super) size: Option<Size>, pub(super) size: Option<Size>,
@@ -87,12 +90,13 @@ impl<'a> Painter<'a> {
/// anyway. Stopping is also what keeps the invariant simple: every /// anyway. Stopping is also what keeps the invariant simple: every
/// handle from `recycled` on is untouched and gets freed together. /// handle from `recycled` on is untouched and gets freed together.
fn take_recycled(&mut self, binding: u32, drawn: Drawn) -> Option<PrimitiveHandle> { fn take_recycled(&mut self, binding: u32, drawn: Drawn) -> Option<PrimitiveHandle> {
let h = self.recycle.peek()?; let h = *self.recycle.get(self.recycle_at)?;
let drawn_matches = (h.pos == NOT_DRAWN) == (drawn == Drawn::No); let drawn_matches = (h.pos == NOT_DRAWN) == (drawn == Drawn::No);
if h.binding != binding || h.layer != self.layer || !drawn_matches { if h.binding != binding || h.layer != self.layer || !drawn_matches {
return None; return None;
} }
self.recycle.next() self.recycle_at += 1;
Some(h)
} }
fn write_primitive<P: Primitive>( fn write_primitive<P: Primitive>(
@@ -139,14 +143,18 @@ impl<'a> Painter<'a> {
/// primitive and retains the handle for exactly as long as that draw. /// primitive and retains the handle for exactly as long as that draw.
pub fn paint(&mut self, paint: &PaintId) -> u32 { pub fn paint(&mut self, paint: &PaintId) -> u32 {
if !self.paints.contains(paint) { if !self.paints.contains(paint) {
self.paints.push(paint.clone()); if let Some(i) = self.recycle_paints.iter().position(|old| old == paint) {
self.paints.push(self.recycle_paints.swap_remove(i));
} else {
self.paints.push(paint.clone());
}
} }
paint.slot() paint.slot()
} }
pub fn paint_value(&mut self, paint: &mut crate::PaintValue) -> u32 { pub fn paint_value(&mut self, paint: &mut crate::PaintValue) -> u32 {
let paint = paint.resolve(&mut self.rsc.ui_mut().paints).clone(); let paint = paint.resolve(&mut self.rsc.ui_mut().paints);
self.paint(&paint) self.paint(paint)
} }
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) { pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
@@ -387,20 +395,31 @@ impl<'a> Painter<'a> {
} }
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) { pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
self.textures.push(handle.clone()); self.retain_texture(handle);
self.write_image(handle.image_index(), region.within(&self.region)); self.write_image(handle.image_index(), region.within(&self.region));
} }
pub fn texture(&mut self, handle: &TextureHandle) { pub fn texture(&mut self, handle: &TextureHandle) {
self.textures.push(handle.clone()); self.retain_texture(handle);
self.write_image(handle.image_index(), self.region); self.write_image(handle.image_index(), self.region);
} }
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) { pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
self.textures.push(handle.clone()); self.retain_texture(handle);
self.write_image(handle.image_index(), region); self.write_image(handle.image_index(), region);
} }
fn retain_texture(&mut self, handle: &TextureHandle) {
if self.textures.contains(handle) {
return;
}
if let Some(i) = self.recycle_textures.iter().position(|old| old == handle) {
self.textures.push(self.recycle_textures.swap_remove(i));
} else {
self.textures.push(handle.clone());
}
}
fn write_image(&mut self, texture_idx: u32, region: UiRegion) { fn write_image(&mut self, texture_idx: u32, region: UiRegion) {
let h = match self.take_recycled(IMAGE_BINDING, Drawn::Yes) { let h = match self.take_recycled(IMAGE_BINDING, Drawn::Yes) {
Some(h) => { Some(h) => {
+82 -21
View File
@@ -5,8 +5,8 @@ use crate::{
ActiveData, Axis, ChildOrder, IdLike, MaskIdx, MoveIdx, Painter, PixelRegion, PrimitiveLayers, ActiveData, Axis, ChildOrder, IdLike, MaskIdx, MoveIdx, Painter, PixelRegion, PrimitiveLayers,
RegionAlign, Size, StrongWidget, UiRegion, UiRsc, UiVec2, Widget, WidgetId, Widgets, RegionAlign, Size, StrongWidget, UiRegion, UiRsc, UiVec2, Widget, WidgetId, Widgets,
render::{ render::{
Drawn, MoveOffset, NOT_DRAWN, Primitive, PrimitiveHandle, PrimitiveInst, Primitives, Drawn, MoveOffset, NOT_DRAWN, OrderChange, Primitive, PrimitiveHandle, PrimitiveInst,
RectPrimitive, rounded_rect_coverage, Primitives, RectPrimitive, rounded_rect_coverage,
}, },
util::{HashMap, HashSet, Id, Vec2}, util::{HashMap, HashSet, Id, Vec2},
}; };
@@ -30,6 +30,7 @@ pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>, pub active: HashMap<WidgetId, ActiveData>,
pub primitives: Primitives, pub primitives: Primitives,
pub layers: PrimitiveLayers, pub layers: PrimitiveLayers,
order_changes: Vec<OrderChange>,
pub(super) output_size: Vec2, pub(super) output_size: Vec2,
/// Physical pixels per `dp` -- see `LayoutLen::dp`'s field doc. `1.0` (an /// Physical pixels per `dp` -- see `LayoutLen::dp`'s field doc. `1.0` (an
/// unscaled display) until a backend that knows its own density calls /// unscaled display) until a backend that knows its own density calls
@@ -80,11 +81,17 @@ pub struct UiRenderState {
pub(crate) struct Retained { pub(crate) struct Retained {
pub region: Option<UiRegion>, pub region: Option<UiRegion>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
pub spare_children: Vec<WidgetId>,
pub move_slot: Option<MoveIdx>, pub move_slot: Option<MoveIdx>,
pub child_move_slot: Option<MoveIdx>, pub child_move_slot: Option<MoveIdx>,
pub own_mask: MaskIdx, pub own_mask: MaskIdx,
pub textures: Vec<crate::TextureHandle>,
pub spare_textures: Vec<crate::TextureHandle>,
pub primitives: Vec<PrimitiveHandle>, pub primitives: Vec<PrimitiveHandle>,
pub spare_primitives: Vec<PrimitiveHandle>,
pub paints: Vec<crate::PaintId>, pub paints: Vec<crate::PaintId>,
pub spare_paints: Vec<crate::PaintId>,
pub size_dependencies: Vec<WidgetId>,
} }
impl Default for Retained { impl Default for Retained {
@@ -92,11 +99,17 @@ impl Default for Retained {
Self { Self {
region: None, region: None,
children: Vec::new(), children: Vec::new(),
spare_children: Vec::new(),
move_slot: None, move_slot: None,
child_move_slot: None, child_move_slot: None,
own_mask: MaskIdx::NONE, own_mask: MaskIdx::NONE,
textures: Vec::new(),
spare_textures: Vec::new(),
primitives: Vec::new(), primitives: Vec::new(),
spare_primitives: Vec::new(),
paints: Vec::new(), paints: Vec::new(),
spare_paints: Vec::new(),
size_dependencies: Vec::new(),
} }
} }
} }
@@ -109,6 +122,7 @@ impl UiRenderState {
active: Default::default(), active: Default::default(),
primitives: Default::default(), primitives: Default::default(),
layers: Default::default(), layers: Default::default(),
order_changes: Vec::new(),
output_size: Vec2::ZERO, output_size: Vec2::ZERO,
density: 1.0, density: 1.0,
old_root: None, old_root: None,
@@ -184,8 +198,11 @@ impl UiRenderState {
} }
fn apply_free(&mut self) { fn apply_free(&mut self) {
let mut changes = std::mem::take(&mut self.order_changes);
for (layer, order) in self.layers.iter_mut() { for (layer, order) in self.layers.iter_mut() {
for change in order.apply_free() { changes.clear();
order.apply_free_into(&mut changes);
for change in changes.drain(..) {
let owner = self.primitives.owner(change.slot); let owner = self.primitives.owner(change.slot);
let Some(idx) = self.primitives.handle_index(change.slot) else { let Some(idx) = self.primitives.handle_index(change.slot) else {
continue; continue;
@@ -204,6 +221,7 @@ impl UiRenderState {
} }
} }
} }
self.order_changes = changes;
self.primitives.release_freed(); self.primitives.release_freed();
} }
@@ -380,11 +398,17 @@ impl UiRenderState {
let Retained { let Retained {
region: mut old_region, region: mut old_region,
children: mut old_children, children: mut old_children,
spare_children: mut children,
move_slot: mut old_move_slot, move_slot: mut old_move_slot,
mut child_move_slot, mut child_move_slot,
mut own_mask, mut own_mask,
textures: mut recycle_textures,
spare_textures: mut textures,
primitives: mut recycle, primitives: mut recycle,
paints: _old_paints, spare_primitives: mut primitives,
paints: mut recycle_paints,
spare_paints: mut paints,
mut size_dependencies,
} = retained; } = retained;
let dirty = rsc.widgets_mut().needs_redraw.remove(&id); let dirty = rsc.widgets_mut().needs_redraw.remove(&id);
let requires_exact_region = rsc let requires_exact_region = rsc
@@ -434,10 +458,17 @@ impl UiRenderState {
let active = self.remove(id, false, true, rsc).unwrap(); let active = self.remove(id, false, true, rsc).unwrap();
old_region = Some(active.region); old_region = Some(active.region);
old_children = active.children; old_children = active.children;
children = active.spare_children;
old_move_slot = Some(active.move_slot); old_move_slot = Some(active.move_slot);
child_move_slot = active.child_move_slot; child_move_slot = active.child_move_slot;
own_mask = active.own_mask; own_mask = active.own_mask;
recycle_textures = active.textures;
textures = active.spare_textures;
recycle = active.primitives; recycle = active.primitives;
primitives = active.spare_primitives;
recycle_paints = active.paints;
paints = active.spare_paints;
size_dependencies = active.size_dependencies;
} else if self.active.contains_key(&id) { } else if self.active.contains_key(&id) {
let layer_changed = self let layer_changed = self
.active .active
@@ -449,12 +480,25 @@ impl UiRenderState {
} }
old_region = Some(active.region); old_region = Some(active.region);
old_children = active.children; old_children = active.children;
children = active.spare_children;
old_move_slot = Some(active.move_slot); old_move_slot = Some(active.move_slot);
child_move_slot = active.child_move_slot; child_move_slot = active.child_move_slot;
own_mask = active.own_mask; own_mask = active.own_mask;
recycle_textures = active.textures;
textures = active.spare_textures;
recycle = active.primitives; recycle = active.primitives;
primitives = active.spare_primitives;
recycle_paints = active.paints;
paints = active.spare_paints;
size_dependencies = active.size_dependencies;
} }
textures.clear();
paints.clear();
primitives.clear();
children.clear();
size_dependencies.clear();
let reentrant = !self.draw_started.insert(id); let reentrant = !self.draw_started.insert(id);
debug_assert!( debug_assert!(
!reentrant, !reentrant,
@@ -481,12 +525,15 @@ impl UiRenderState {
own_mask, own_mask,
layer, layer,
id, id,
textures: Vec::new(), textures,
paints: Vec::new(), recycle_textures,
primitives: Vec::new(), paints,
recycle: recycle.into_iter().peekable(), recycle_paints,
children: Vec::new(), primitives,
size_dependencies: Vec::new(), recycle,
recycle_at: 0,
children,
size_dependencies,
size: None, size: None,
reuse_child_sizes, reuse_child_sizes,
rsc, rsc,
@@ -534,9 +581,12 @@ impl UiRenderState {
child_move_slot, child_move_slot,
own_mask, own_mask,
textures, textures,
mut recycle_textures,
paints, paints,
mut recycle_paints,
primitives, primitives,
recycle, mut recycle,
recycle_at,
children, children,
size_dependencies, size_dependencies,
size: _, size: _,
@@ -545,18 +595,31 @@ impl UiRenderState {
id, id,
} = painter; } = painter;
for h in recycle { for h in &recycle[recycle_at..] {
self.free_primitive(&h); self.free_primitive(h);
} }
for c in &old_children {
if !children.contains(c) {
self.remove_rec(*c, rsc);
}
}
recycle.clear();
recycle_textures.clear();
recycle_paints.clear();
old_children.clear();
let active = ActiveData { let active = ActiveData {
id, id,
region, region,
parent, parent,
textures, textures,
spare_textures: recycle_textures,
paints, paints,
spare_paints: recycle_paints,
primitives, primitives,
spare_primitives: recycle,
children, children,
spare_children: old_children,
size_dependencies, size_dependencies,
mask: inherited_mask, mask: inherited_mask,
layer: inherited_layer, layer: inherited_layer,
@@ -567,12 +630,6 @@ impl UiRenderState {
move_applied: Vec2::ZERO, move_applied: Vec2::ZERO,
}; };
for c in &old_children {
if !active.children.contains(c) {
self.remove_rec(*c, rsc);
}
}
rsc.on_draw(&active); rsc.on_draw(&active);
self.active.insert(id, active); self.active.insert(id, active);
size size
@@ -713,8 +770,6 @@ impl UiRenderState {
} }
} }
Self::remask_shape_users(&self.active, id, active.own_mask, &active.primitives, rsc); Self::remask_shape_users(&self.active, id, active.own_mask, &active.primitives, rsc);
active.textures.clear();
rsc.ui_mut().textures.free();
if undraw { if undraw {
// A captured widget that goes away mid-gesture (LazySpan's // A captured widget that goes away mid-gesture (LazySpan's
// virtualisation retiring a row, a rebuild) must not leave // virtualisation retiring a row, a rebuild) must not leave
@@ -1137,11 +1192,17 @@ impl UiRenderState {
Retained { Retained {
region: Some(active.region), region: Some(active.region),
children: active.children, children: active.children,
spare_children: active.spare_children,
move_slot: Some(active.move_slot), move_slot: Some(active.move_slot),
child_move_slot: active.child_move_slot, child_move_slot: active.child_move_slot,
own_mask: active.own_mask, own_mask: active.own_mask,
textures: active.textures,
spare_textures: active.spare_textures,
primitives: active.primitives, primitives: active.primitives,
spare_primitives: active.spare_primitives,
paints: active.paints, paints: active.paints,
spare_paints: active.spare_paints,
size_dependencies: active.size_dependencies,
}, },
rsc, rsc,
); );
+25 -5
View File
@@ -62,10 +62,24 @@ impl Dirty {
} }
pub fn ranges(&self, len: usize, gap: usize) -> Vec<Range<usize>> { pub fn ranges(&self, len: usize, gap: usize) -> Vec<Range<usize>> {
let mut ranges = Vec::new();
self.for_each_range(len, gap, |range| ranges.push(range));
ranges
}
pub(crate) fn for_each_range(
&self,
len: usize,
gap: usize,
mut visit: impl FnMut(Range<usize>),
) {
if self.all { if self.all {
return Vec::from_iter((len > 0).then_some(0..len)); if len > 0 {
visit(0..len);
}
return;
} }
let mut ranges: Vec<Range<usize>> = Vec::new(); let mut pending: Option<Range<usize>> = None;
for (w, word) in self.words.iter().enumerate() { for (w, word) in self.words.iter().enumerate() {
let mut bits = *word; let mut bits = *word;
while bits != 0 { while bits != 0 {
@@ -75,14 +89,20 @@ impl Dirty {
if start >= len { if start >= len {
break; break;
} }
match ranges.last_mut() { match pending.as_mut() {
Some(last) if start - last.end <= gap => last.end = end, Some(last) if start - last.end <= gap => last.end = end,
_ => ranges.push(start..end), _ => {
if let Some(range) = pending.replace(start..end) {
visit(range);
}
}
} }
bits &= !(((1u128 << run) - 1) as u64) << (start - w * 64); bits &= !(((1u128 << run) - 1) as u64) << (start - w * 64);
} }
} }
ranges if let Some(range) = pending {
visit(range);
}
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
+12 -4
View File
@@ -8,10 +8,18 @@ pub struct WidgetData {
impl WidgetData { impl WidgetData {
pub fn new<W: Widget>(widget: W) -> Self { pub fn new<W: Widget>(widget: W) -> Self {
let mut label = std::any::type_name::<W>().to_string(); let name = std::any::type_name::<W>();
if let (Some(first), Some(last)) = (label.find(":"), label.rfind(":")) { let label = match (name.find("::"), name.rfind("::")) {
label = label.split_at(first).0.to_string() + "::" + label.split_at(last + 1).1; (Some(first), Some(last)) => {
} let suffix = &name[last + 2..];
let mut label = String::with_capacity(first + 2 + suffix.len());
label.push_str(&name[..first]);
label.push_str("::");
label.push_str(suffix);
label
}
_ => name.to_owned(),
};
Self { Self {
widget: Box::new(widget), widget: Box::new(widget),
label, label,
+9 -4
View File
@@ -379,7 +379,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
frame_diagnostics.paints_resized, frame_diagnostics.paints_resized,
frame_diagnostics.atlas_pages_grown_prev, frame_diagnostics.atlas_pages_grown_prev,
frame_diagnostics.image_bind_group_creates_prev, frame_diagnostics.image_bind_group_creates_prev,
renderer.wgpu_errors.snapshot().len(), renderer.wgpu_errors.len(),
); );
} }
let mut parts = renderer.draw(); let mut parts = renderer.draw();
@@ -410,13 +410,18 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
} }
let ui_state = self.state.android_state_mut(); let ui_state = self.state.android_state_mut();
if let Some(tree_update) = ui_state.access.update( let access_changed = ui_state.access.refresh(
self.rsc.widgets(), self.rsc.widgets(),
&self.rsc.ui().render_state().get(), &self.rsc.ui().render_state().get(),
&self.rsc, &self.rsc,
) { );
if access_changed {
let ui_state = self.state.android_state_mut(); let ui_state = self.state.android_state_mut();
if let Some(events) = ui_state.access_adapter.update_if_active(|| tree_update) { let access = &ui_state.access;
if let Some(events) = ui_state
.access_adapter
.update_if_active(|| access.tree_update())
{
ctx.push_dynamic_deferred_callback(move |env, view| { ctx.push_dynamic_deferred_callback(move |env, view| {
raise_if_enabled(env, view, events); raise_if_enabled(env, view, events);
}); });
+5 -3
View File
@@ -269,9 +269,11 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
let render_state = rsc.ui.render_state(); let render_state = rsc.ui.render_state();
let render_state = render_state.get(); let render_state = render_state.get();
crate::diagnostics::log_frame(&render_state, frame_start, parts, animating); crate::diagnostics::log_frame(&render_state, frame_start, parts, animating);
if let Some(tree_update) = ui_state.access.update(rsc.widgets(), &render_state, rsc) if ui_state.access.refresh(rsc.widgets(), &render_state, rsc) {
{ let access = &ui_state.access;
ui_state.access_adapter.update_if_active(|| tree_update); ui_state
.access_adapter
.update_if_active(|| access.tree_update());
} }
} }
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
+16 -16
View File
@@ -328,18 +328,17 @@ impl SensorUi for UiRenderState {
// Platform cancellation reaches every tracker and produces no other sense. // Platform cancellation reaches every tracker and produces no other sense.
if cursor.cancelled { if cursor.cancelled {
let captured = pointer.captured.take(); let captured = pointer.captured.take();
let mut told: Vec<WidgetId> = captured.into_iter().collect();
for id in pointer.pressed.drain(..) {
if Some(id) != captured {
told.push(id);
}
}
requests.release(); requests.release();
pointer.press_origin = None; pointer.press_origin = None;
pointer.drag_axis = None; pointer.drag_axis = None;
for id in told { if let Some(id) = captured {
deliver_cancel(self, rsc, state, id, &cursor, window_size, &requests); deliver_cancel(self, rsc, state, id, &cursor, window_size, &requests);
} }
for id in pointer.pressed.drain(..) {
if Some(id) != captured {
deliver_cancel(self, rsc, state, id, &cursor, window_size, &requests);
}
}
rsc.events_mut().get_type::<CursorSense>().global = pointer; rsc.events_mut().get_type::<CursorSense>().global = pointer;
return; return;
} }
@@ -442,15 +441,16 @@ impl SensorUi for UiRenderState {
pointer.captured = requests.holder(); pointer.captured = requests.holder();
match pointer.captured { match pointer.captured {
Some(winner) => { Some(winner) => {
let losers: Vec<WidgetId> = pointer let mut winner_was_pressed = false;
.pressed for id in pointer.pressed.drain(..) {
.iter() if id == winner {
.copied() winner_was_pressed = true;
.filter(|&id| id != winner) } else {
.collect(); deliver_cancel(self, rsc, state, id, &cursor, window_size, &requests);
pointer.pressed.retain(|&id| id == winner); }
for loser in losers { }
deliver_cancel(self, rsc, state, loser, &cursor, window_size, &requests); if winner_was_pressed {
pointer.pressed.push(winner);
} }
} }
None if !button_down => pointer.pressed.clear(), None if !button_down => pointer.pressed.clear(),
+42 -39
View File
@@ -17,17 +17,13 @@ impl Widget for Span {
let axis = self.dir.axis; let axis = self.dir.axis;
let gap = self.gap.apply_rest(painter.density()).abs; let gap = self.gap.apply_rest(painter.density()).abs;
let mut lens: Vec<Option<LayoutLen>> = self let gap_total = gap * self.children.len().saturating_sub(1) as f32;
.children let mut measured = Vec::with_capacity(self.children.len());
.iter() let mut total = LayoutLen::abs(gap_total);
.map(|child| painter.known_len(child, axis))
.collect();
let mut drawn = vec![false; self.children.len()];
let mut cursor = UiScalar::rel_min(); let mut cursor = UiScalar::rel_min();
for (i, child) in self.children.iter().enumerate() { for child in &self.children {
let len = match lens[i] { let (len, drawn) = match painter.known_len(child, axis) {
Some(len) => len, Some(len) => (len, false),
None => { None => {
let mut slot = UiSpan::new(cursor, UiScalar::rel_max()); let mut slot = UiSpan::new(cursor, UiScalar::rel_max());
if self.dir.sign == Sign::Neg { if self.dir.sign == Sign::Neg {
@@ -35,47 +31,25 @@ impl Widget for Span {
} }
let region = UiRegion::from_axis(axis, slot, UiSpan::FULL); let region = UiRegion::from_axis(axis, slot, UiSpan::FULL);
let len = painter.widget_within(child, region).size().axis(axis); let len = painter.widget_within(child, region).size().axis(axis);
lens[i] = Some(len); (len, true)
drawn[i] = true;
len
} }
}; };
measured.push((len, drawn));
total += len;
cursor.abs += len.abs + gap; cursor.abs += len.abs + gap;
cursor.rel += len.rel; cursor.rel += len.rel;
} }
let lens: Vec<LayoutLen> = lens.into_iter().map(Option::unwrap).collect();
let gap_total = gap * self.children.len().saturating_sub(1) as f32;
let total = lens.iter().fold(LayoutLen::abs(gap_total), |s, &l| s + l);
let mut start = UiScalar::rel_min(); let mut start = UiScalar::rel_min();
let mut ortho_len = LayoutLen::ZERO; let mut ortho_len = LayoutLen::ZERO;
let mut ortho_mixed = false; let mut ortho_mixed = false;
let mut placed = Vec::with_capacity(self.children.len()); for (child, &(len, drawn)) in self.children.iter().zip(&measured) {
for (i, (child, &len)) in self.children.iter().zip(&lens).enumerate() { let child_region = child_region(axis, self.dir.sign, total, gap, &mut start, len);
let mut span = UiSpan::FULL; let used = if drawn {
span.start = start;
if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.abs);
let rel_end = UiScalar::rel(len.rest / total.rest);
let end = (UiScalar::rel_max() + start) - offset;
start = rel_end.within(&start.to(end));
}
start.abs += len.abs;
start.rel += len.rel;
span.end = start;
let mut child_region = UiRegion::from_axis(axis, span, UiSpan::FULL);
if self.dir.sign == Sign::Neg {
child_region.flip(axis);
}
let used = if drawn[i] {
painter.place(child, child_region).size() painter.place(child, child_region).size()
} else { } else {
painter.widget_within(child, child_region).size() painter.widget_within(child, child_region).size()
}; };
placed.push(child_region);
start.abs += gap;
let ortho = used.axis(!axis); let ortho = used.axis(!axis);
if ortho.rel > 0.0 || ortho.rest > 0.0 { if ortho.rel > 0.0 || ortho.rest > 0.0 {
@@ -90,7 +64,9 @@ impl Widget for Span {
let ortho = ortho_len let ortho = ortho_len
.apply_rest(painter.density()) .apply_rest(painter.density())
.align(AxisAlign::Neg); .align(AxisAlign::Neg);
for (child, mut region) in self.children.iter().zip(placed) { let mut start = UiScalar::rel_min();
for (child, &(len, _)) in self.children.iter().zip(&measured) {
let mut region = child_region(axis, self.dir.sign, total, gap, &mut start, len);
*region.axis_mut(!axis) = ortho; *region.axis_mut(!axis) = ortho;
painter.place(child, region); painter.place(child, region);
} }
@@ -106,6 +82,33 @@ impl Widget for Span {
} }
} }
fn child_region(
axis: Axis,
sign: Sign,
total: LayoutLen,
gap: f32,
start: &mut UiScalar,
len: LayoutLen,
) -> UiRegion {
let mut span = UiSpan::FULL;
span.start = *start;
if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.abs);
let rel_end = UiScalar::rel(len.rest / total.rest);
let end = (UiScalar::rel_max() + *start) - offset;
*start = rel_end.within(&start.to(end));
}
start.abs += len.abs;
start.rel += len.rel;
span.end = *start;
start.abs += gap;
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
if sign == Sign::Neg {
region.flip(axis);
}
region
}
impl Span { impl Span {
pub fn empty(dir: Dir) -> Self { pub fn empty(dir: Dir) -> Self {
Self { Self {
+20 -15
View File
@@ -20,27 +20,32 @@ impl Widget for Stack {
}), }),
}; };
let region = known.map(|size| size.to_uivec2(density).align(RegionAlign::TOP_LEFT)); let region = known.map(|size| size.to_uivec2(density).align(RegionAlign::TOP_LEFT));
let mut used = Vec::with_capacity(self.children.len()); let mut used = known
let mut iter = self.children.iter(); .is_none()
if let Some(child) = iter.next() { .then(|| Vec::with_capacity(self.children.len()));
painter.child_layer(); let mut selected = known.unwrap_or_default();
used.push(match region { for (i, child) in self.children.iter().enumerate() {
if i == 0 {
painter.child_layer();
} else {
painter.next_layer();
}
let child_size = match region {
Some(region) => painter.widget_within(child, region).size(), Some(region) => painter.widget_within(child, region).size(),
None => painter.widget(child).size(), None => painter.widget(child).size(),
}); };
} if matches!(self.size, StackSize::Child(target) if target == i) {
for child in iter { selected = child_size;
painter.next_layer(); }
used.push(match region { if let Some(used) = &mut used {
Some(region) => painter.widget_within(child, region).size(), used.push(child_size);
None => painter.widget(child).size(), }
});
} }
let size = match self.size { let size = match self.size {
StackSize::Default => Size::default(), StackSize::Default => Size::default(),
StackSize::Child(i) => used.get(i).copied().unwrap_or_default(), StackSize::Child(_) => selected,
}; };
if known.is_none() { if let Some(used) = used {
let final_region = size.to_uivec2(density).align(RegionAlign::TOP_LEFT); let final_region = size.to_uivec2(density).align(RegionAlign::TOP_LEFT);
for (child, child_size) in self.children.iter().zip(used) { for (child, child_size) in self.children.iter().zip(used) {
let child_region = child_size let child_region = child_size
+12 -13
View File
@@ -188,16 +188,18 @@ impl TextView {
self.overflow = OverflowState::default(); self.overflow = OverflowState::default();
let available = painter.px_size().x.max(0.0); let available = painter.px_size().x.max(0.0);
let excess = (tex.size.x - available).max(0.0); let excess = (tex.size.x - available).max(0.0);
let attrs = self.text.attrs().clone(); let (overflow, overflow_position) = {
if excess <= 0.0 || matches!(attrs.overflow, TextOverflow::Visible | TextOverflow::Wrap) { let attrs = self.text.attrs();
(attrs.overflow, attrs.overflow_position)
};
if excess <= 0.0 || matches!(overflow, TextOverflow::Visible | TextOverflow::Wrap) {
return None; return None;
} }
let marker = let marker =
(attrs.overflow == TextOverflow::Ellipsis).then(|| painter.render_ellipsis(&self.text)); (overflow == TextOverflow::Ellipsis).then(|| painter.render_ellipsis(&self.text));
let mut pan = attrs let mut pan = overflow_position
.overflow_position
.resolve(painter.density()) .resolve(painter.density())
.to_abs(excess) .to_abs(excess)
.clamp(0.0, excess); .clamp(0.0, excess);
@@ -229,10 +231,7 @@ impl TextView {
} }
pan = pan.clamp(0.0, excess); pan = pan.clamp(0.0, excess);
} }
let old_pan = attrs let old_pan = overflow_position.resolve(painter.density()).to_abs(excess);
.overflow_position
.resolve(painter.density())
.to_abs(excess);
if pan != old_pan { if pan != old_pan {
self.text.set_overflow_position(Len::abs(pan)); self.text.set_overflow_position(Len::abs(pan));
} }
@@ -242,7 +241,7 @@ impl TextView {
self.overflow.viewport = vec2(available, tex.size.y); self.overflow.viewport = vec2(available, tex.size.y);
self.overflow.pan = pan; self.overflow.pan = pan;
self.overflow.content_end = available; self.overflow.content_end = available;
if attrs.overflow != TextOverflow::Ellipsis { if overflow != TextOverflow::Ellipsis {
return None; return None;
} }
@@ -287,14 +286,14 @@ impl TextView {
return painter.widget(hint).size(); return painter.widget(hint).size();
} }
let marker = self.prepare_overflow(painter, &tex, follow_caret); let marker = self.prepare_overflow(painter, &tex, follow_caret);
let attrs = self.text.attrs().clone(); let align = self.text.attrs().align;
if !self.overflow.active { if !self.overflow.active {
let within = tex.size.align(attrs.align).within(&painter.region()); let within = tex.size.align(align).within(&painter.region());
painter.glyphs(&tex, within); painter.glyphs(&tex, within);
return Size::abs(tex.size); return Size::abs(tex.size);
} }
let viewport = self.overflow.viewport.align(attrs.align); let viewport = self.overflow.viewport.align(align);
let marker_highlight = painter.paint(&PaintId::SKY); let marker_highlight = painter.paint(&PaintId::SKY);
if self.overflow.suppress_content if self.overflow.suppress_content
&& self && self
+13 -13
View File
@@ -312,15 +312,16 @@ impl SelectionController {
} }
fn deselect(&mut self, rsc: &mut impl UiRsc) { fn deselect(&mut self, rsc: &mut impl UiRsc) {
let mut ids = std::mem::take(&mut self.selected); let anchor = self.anchor.take().map(|(id, _)| id);
if let Some((anchor, _)) = self.anchor.take() let anchor_was_selected = anchor.is_some_and(|anchor| self.selected.contains(&anchor));
&& !ids.contains(&anchor) for id in self.selected.drain(..) {
{
ids.push(anchor);
}
for id in ids {
Self::with_text(rsc, id, |text| text.deselect()); Self::with_text(rsc, id, |text| text.deselect());
} }
if let Some(anchor) = anchor
&& !anchor_was_selected
{
Self::with_text(rsc, anchor, |text| text.deselect());
}
} }
fn begin(&mut self, rsc: &mut impl UiRsc, id: WidgetId, pos: Vec2, size: Vec2) { fn begin(&mut self, rsc: &mut impl UiRsc, id: WidgetId, pos: Vec2, size: Vec2) {
@@ -352,8 +353,7 @@ impl SelectionController {
(focus_at, anchor_at) (focus_at, anchor_at)
}; };
let old = std::mem::take(&mut self.selected); for &old_id in &self.selected {
for old_id in old {
if !self.order[lo..=hi].contains(&old_id) { if !self.order[lo..=hi].contains(&old_id) {
Self::with_text(rsc, old_id, |text| text.deselect()); Self::with_text(rsc, old_id, |text| text.deselect());
} }
@@ -375,7 +375,8 @@ impl SelectionController {
text.select_between(start, end); text.select_between(start, end);
}); });
} }
self.selected = self.order[lo..=hi].to_vec(); self.selected.clear();
self.selected.extend_from_slice(&self.order[lo..=hi]);
} }
pub fn drag<Rsc: HasEvents>( pub fn drag<Rsc: HasEvents>(
@@ -486,12 +487,11 @@ impl<Rsc: HasEvents> Controller<Rsc> for SelectionController {
.map(CommandResult::Copy) .map(CommandResult::Copy)
.unwrap_or(CommandResult::Unused), .unwrap_or(CommandResult::Unused),
Command::SelectAll => { Command::SelectAll => {
let order = self.order.clone();
self.deselect(rsc); self.deselect(rsc);
for &id in &order { for &id in &self.order {
Self::with_text(rsc, id, |text| text.select_all()); Self::with_text(rsc, id, |text| text.select_all());
} }
self.selected = order; self.selected.extend_from_slice(&self.order);
CommandResult::Used CommandResult::Used
} }
Command::Escape => { Command::Escape => {
+126
View File
@@ -0,0 +1,126 @@
use iris::prelude::*;
use std::{
alloc::{GlobalAlloc, Layout, System},
cell::Cell,
time::{Duration, Instant},
};
struct CountingAllocator;
thread_local! {
static TRACKING: Cell<bool> = const { Cell::new(false) };
static ALLOCATIONS: Cell<usize> = const { Cell::new(0) };
}
fn note_allocation() {
TRACKING.with(|tracking| {
if tracking.get() {
ALLOCATIONS.with(|allocations| allocations.set(allocations.get() + 1));
}
});
}
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc(layout) };
note_allocation();
ptr
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc_zeroed(layout) };
note_allocation();
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let ptr = unsafe { System.realloc(ptr, layout, new_size) };
note_allocation();
ptr
}
}
#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;
fn allocations_during(run: impl FnOnce()) -> usize {
ALLOCATIONS.with(|allocations| allocations.set(0));
TRACKING.with(|tracking| tracking.set(true));
run();
TRACKING.with(|tracking| tracking.set(false));
ALLOCATIONS.with(Cell::get)
}
struct TestRsc {
ui: Ui,
}
impl UiRsc for TestRsc {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
}
struct FrameRequester;
impl Widget for FrameRequester {
fn draw(&mut self, painter: &mut Painter) {
let paint = painter.paint(&PaintId::WHITE);
painter.primitive(RectPrimitive::color(paint));
painter.request_next_frame();
painter.set_size(Size::REST);
}
}
#[test]
fn a_warmed_self_redraw_allocates_nothing() {
let mut rsc = TestRsc { ui: Ui::default() };
let root = rsc.ui.widgets.add_strong(FrameRequester).any();
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
let start = Instant::now();
for frame in 0..3 {
assert!(render.update_at(&root, &mut rsc, start + Duration::from_millis(frame * 16),));
}
let allocations = allocations_during(|| {
assert!(render.update_at(&root, &mut rsc, start + Duration::from_millis(48),));
});
assert_eq!(allocations, 0);
}
#[test]
fn refreshing_retained_accessibility_allocates_nothing() {
let mut rsc = TestRsc { ui: Ui::default() };
let leaf = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
rsc.ui.widgets.set_label(&leaf, "named".to_owned());
let offset = rsc.ui.widgets.add_strong(Offset {
inner: leaf.any(),
amt: UiVec2::ZERO,
});
let offset_id = offset.weak();
let root = offset.any();
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
render.update(&root, &mut rsc);
let mut access = AccessTree::new();
assert!(access.refresh(rsc.widgets(), &render, &rsc));
rsc.ui.widgets.get_mut(&offset_id).unwrap().amt = UiVec2::abs(Vec2::new(20.0, 0.0));
render.update(&root, &mut rsc);
let moved = allocations_during(|| assert!(access.refresh(rsc.widgets(), &render, &rsc)));
assert_eq!(moved, 0);
let unchanged = allocations_during(|| assert!(!access.refresh(rsc.widgets(), &render, &rsc)));
assert_eq!(unchanged, 0);
}