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
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
- **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
redraw has finished, then the host schedules the next platform frame before
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**
(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;
impl Default for TextAttrs {
@@ -701,10 +710,16 @@ impl TextBuffer {
width: Option<f32>,
density: f32,
) {
let shape_attrs = TextShapeAttrs::from(attrs);
if self.shaped.as_ref() == Some(&(shape_attrs.clone(), width, density)) {
if self
.shaped
.as_ref()
.is_some_and(|(old, old_width, old_density)| {
old.matches(attrs) && *old_width == width && *old_density == density
})
{
return;
}
let shape_attrs = TextShapeAttrs::from(attrs);
let base_family = data.resolve_family(&attrs.family);
let span_families: Vec<Option<String>> = self
.spans
+8
View File
@@ -34,6 +34,14 @@ pub struct TextureHandle {
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
/// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped
pub struct Textures {
+15 -5
View File
@@ -112,6 +112,14 @@ impl WgpuErrorLog {
pub fn snapshot(&self) -> Vec<String> {
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 {
@@ -206,11 +214,13 @@ impl UiRenderNode {
rlayer.order.update(device, queue, entries, dirty);
let (entries, dirty) = order.images_for_upload();
rlayer.images.update(device, queue, entries, dirty);
rlayer.image_tex_indices = order
.images()
.iter()
.map(|&slot| ui_render.primitives.instance(slot).idx)
.collect();
rlayer.image_tex_indices.clear();
rlayer.image_tex_indices.extend(
order
.images()
.iter()
.map(|&slot| ui_render.primitives.instance(slot).idx),
);
order.updated = false;
}
}
+21 -16
View File
@@ -450,19 +450,26 @@ impl LayerOrder {
}
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.order,
&mut self.order_dirty,
false,
changes,
);
changes.extend(Self::apply_free_list(
Self::apply_free_list(
&mut self.image_free,
&mut self.images,
&mut self.images_dirty,
true,
));
changes
changes,
);
}
pub fn order_for_upload(&mut self) -> (&[u32], &mut Dirty) {
@@ -478,22 +485,20 @@ impl LayerOrder {
list: &mut Vec<u32>,
dirty: &mut Dirty,
is_image: bool,
) -> Vec<OrderChange> {
changes: &mut Vec<OrderChange>,
) {
free.sort_by(|a, b| b.cmp(a));
free.drain(..)
.filter_map(|pos| {
list.swap_remove(pos);
if pos == list.len() {
return None;
}
for pos in free.drain(..) {
list.swap_remove(pos);
if pos != list.len() {
dirty.mark(pos);
Some(OrderChange {
changes.push(OrderChange {
slot: list[pos],
is_image,
pos,
})
})
.collect()
});
}
}
}
pub fn order(&self) -> &Vec<u32> {
@@ -523,7 +528,7 @@ pub enum Drawn {
pub const NOT_DRAWN: usize = usize::MAX;
#[derive(Debug)]
#[derive(Clone, Copy, Debug)]
pub struct PrimitiveHandle {
pub layer: usize,
pub pos: usize,
+2 -2
View File
@@ -90,13 +90,13 @@ impl<T: Pod> ArrBuf<T> {
}
self.len = data.len();
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(
&self.buffer,
range.start as BufferAddress * stride,
bytemuck::cast_slice(&data[range]),
);
}
});
dirty.clear();
reallocated
}
+57 -22
View File
@@ -10,11 +10,11 @@ fn node_id(id: WidgetId) -> NodeId {
NodeId(id.as_u64())
}
#[derive(Clone, PartialEq)]
struct Entry {
name: String,
role: Role,
bounds: PixelRegion,
seen: u64,
}
fn entry_node(entry: &Entry) -> Node {
@@ -37,6 +37,7 @@ fn entry_node(entry: &Entry) -> Node {
#[derive(Default)]
pub struct AccessTree {
known: HashMap<WidgetId, Entry>,
generation: u64,
rebuilds: u64,
}
@@ -45,12 +46,17 @@ impl AccessTree {
Self::default()
}
fn collect(
widgets: &Widgets,
render: &UiRenderState,
rsc: &dyn UiRsc,
) -> HashMap<WidgetId, Entry> {
let mut current = HashMap::default();
/// Refresh the retained accessibility state and report whether it changed.
/// Existing entries are updated in place so an ordinary frame allocates
/// nothing, including one where only bounds changed.
pub fn refresh(&mut self, widgets: &Widgets, render: &UiRenderState, rsc: &dyn UiRsc) -> bool {
self.generation = self.generation.wrapping_add(1);
if self.generation == 0 {
self.known.clear();
self.generation = 1;
}
let generation = self.generation;
let mut changed = false;
for id in widgets.named() {
let Some(bounds) = render.window_region(&id, rsc) else {
continue;
@@ -58,16 +64,42 @@ impl AccessTree {
let Some(widget) = widgets.get_dyn(id) else {
continue;
};
current.insert(
id,
Entry {
name: widgets.label(id).clone(),
role: widget.access_role(),
bounds,
},
);
let name = widgets.label(id);
let role = widget.access_role();
match self.known.get_mut(&id) {
Some(entry) => {
if entry.name != *name {
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
@@ -84,13 +116,14 @@ impl AccessTree {
render: &UiRenderState,
rsc: &dyn UiRsc,
) -> Option<TreeUpdate> {
let current = Self::collect(widgets, render, rsc);
if current == self.known {
if !self.refresh(widgets, render, rsc) {
return None;
}
self.known = current.clone();
self.rebuilds += 1;
Some(build_update(&current))
Some(self.tree_update())
}
pub fn tree_update(&self) -> TreeUpdate {
build_update(&self.known)
}
/// 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
/// client that has since detached and reattached).
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 {
+4
View File
@@ -9,12 +9,16 @@ pub struct ActiveData {
pub region: UiRegion,
pub parent: Option<WidgetId>,
pub textures: Vec<TextureHandle>,
pub(crate) spare_textures: Vec<TextureHandle>,
/// Paint slots retained by this draw. The GPU primitive stores only the
/// slot index, so these handles are what prevent a live primitive from
/// observing a recycled paint.
pub paints: Vec<PaintId>,
pub(crate) spare_paints: Vec<PaintId>,
pub primitives: Vec<PrimitiveHandle>,
pub(crate) spare_primitives: Vec<PrimitiveHandle>,
pub children: Vec<WidgetId>,
pub(crate) spare_children: Vec<WidgetId>,
pub size_dependencies: Vec<WidgetId>,
pub mask: MaskIdx,
/// The widget's retained mask slot, or `MaskIdx::NONE`.
+4 -6
View File
@@ -122,13 +122,11 @@ impl Ui {
update(&mut text)?;
text.invalidate_all()
};
let active: Vec<WidgetId> = {
let mut active = owners;
{
let render = self.render_state.get();
owners
.into_iter()
.filter(|owner| render.active.contains_key(owner))
.collect()
};
active.retain(|owner| render.active.contains_key(owner));
}
self.data.widgets.needs_redraw.extend(active);
Ok(())
}
+28 -9
View File
@@ -21,9 +21,12 @@ pub struct Painter<'a> {
pub(super) child_move_slot: Option<MoveIdx>,
pub(super) own_mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>,
pub(super) recycle_textures: Vec<TextureHandle>,
pub(super) paints: Vec<PaintId>,
pub(super) recycle_paints: Vec<PaintId>,
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) size_dependencies: Vec<WidgetId>,
pub(super) size: Option<Size>,
@@ -87,12 +90,13 @@ impl<'a> Painter<'a> {
/// anyway. Stopping is also what keeps the invariant simple: every
/// handle from `recycled` on is untouched and gets freed together.
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);
if h.binding != binding || h.layer != self.layer || !drawn_matches {
return None;
}
self.recycle.next()
self.recycle_at += 1;
Some(h)
}
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.
pub fn paint(&mut self, paint: &PaintId) -> u32 {
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()
}
pub fn paint_value(&mut self, paint: &mut crate::PaintValue) -> u32 {
let paint = paint.resolve(&mut self.rsc.ui_mut().paints).clone();
self.paint(&paint)
let paint = paint.resolve(&mut self.rsc.ui_mut().paints);
self.paint(paint)
}
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) {
self.textures.push(handle.clone());
self.retain_texture(handle);
self.write_image(handle.image_index(), region.within(&self.region));
}
pub fn texture(&mut self, handle: &TextureHandle) {
self.textures.push(handle.clone());
self.retain_texture(handle);
self.write_image(handle.image_index(), self.region);
}
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);
}
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) {
let h = match self.take_recycled(IMAGE_BINDING, Drawn::Yes) {
Some(h) => {
+82 -21
View File
@@ -5,8 +5,8 @@ use crate::{
ActiveData, Axis, ChildOrder, IdLike, MaskIdx, MoveIdx, Painter, PixelRegion, PrimitiveLayers,
RegionAlign, Size, StrongWidget, UiRegion, UiRsc, UiVec2, Widget, WidgetId, Widgets,
render::{
Drawn, MoveOffset, NOT_DRAWN, Primitive, PrimitiveHandle, PrimitiveInst, Primitives,
RectPrimitive, rounded_rect_coverage,
Drawn, MoveOffset, NOT_DRAWN, OrderChange, Primitive, PrimitiveHandle, PrimitiveInst,
Primitives, RectPrimitive, rounded_rect_coverage,
},
util::{HashMap, HashSet, Id, Vec2},
};
@@ -30,6 +30,7 @@ pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>,
pub primitives: Primitives,
pub layers: PrimitiveLayers,
order_changes: Vec<OrderChange>,
pub(super) output_size: Vec2,
/// Physical pixels per `dp` -- see `LayoutLen::dp`'s field doc. `1.0` (an
/// unscaled display) until a backend that knows its own density calls
@@ -80,11 +81,17 @@ pub struct UiRenderState {
pub(crate) struct Retained {
pub region: Option<UiRegion>,
pub children: Vec<WidgetId>,
pub spare_children: Vec<WidgetId>,
pub move_slot: Option<MoveIdx>,
pub child_move_slot: Option<MoveIdx>,
pub own_mask: MaskIdx,
pub textures: Vec<crate::TextureHandle>,
pub spare_textures: Vec<crate::TextureHandle>,
pub primitives: Vec<PrimitiveHandle>,
pub spare_primitives: Vec<PrimitiveHandle>,
pub paints: Vec<crate::PaintId>,
pub spare_paints: Vec<crate::PaintId>,
pub size_dependencies: Vec<WidgetId>,
}
impl Default for Retained {
@@ -92,11 +99,17 @@ impl Default for Retained {
Self {
region: None,
children: Vec::new(),
spare_children: Vec::new(),
move_slot: None,
child_move_slot: None,
own_mask: MaskIdx::NONE,
textures: Vec::new(),
spare_textures: Vec::new(),
primitives: Vec::new(),
spare_primitives: Vec::new(),
paints: Vec::new(),
spare_paints: Vec::new(),
size_dependencies: Vec::new(),
}
}
}
@@ -109,6 +122,7 @@ impl UiRenderState {
active: Default::default(),
primitives: Default::default(),
layers: Default::default(),
order_changes: Vec::new(),
output_size: Vec2::ZERO,
density: 1.0,
old_root: None,
@@ -184,8 +198,11 @@ impl UiRenderState {
}
fn apply_free(&mut self) {
let mut changes = std::mem::take(&mut self.order_changes);
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 Some(idx) = self.primitives.handle_index(change.slot) else {
continue;
@@ -204,6 +221,7 @@ impl UiRenderState {
}
}
}
self.order_changes = changes;
self.primitives.release_freed();
}
@@ -380,11 +398,17 @@ impl UiRenderState {
let Retained {
region: mut old_region,
children: mut old_children,
spare_children: mut children,
move_slot: mut old_move_slot,
mut child_move_slot,
mut own_mask,
textures: mut recycle_textures,
spare_textures: mut textures,
primitives: mut recycle,
paints: _old_paints,
spare_primitives: mut primitives,
paints: mut recycle_paints,
spare_paints: mut paints,
mut size_dependencies,
} = retained;
let dirty = rsc.widgets_mut().needs_redraw.remove(&id);
let requires_exact_region = rsc
@@ -434,10 +458,17 @@ impl UiRenderState {
let active = self.remove(id, false, true, rsc).unwrap();
old_region = Some(active.region);
old_children = active.children;
children = active.spare_children;
old_move_slot = Some(active.move_slot);
child_move_slot = active.child_move_slot;
own_mask = active.own_mask;
recycle_textures = active.textures;
textures = active.spare_textures;
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) {
let layer_changed = self
.active
@@ -449,12 +480,25 @@ impl UiRenderState {
}
old_region = Some(active.region);
old_children = active.children;
children = active.spare_children;
old_move_slot = Some(active.move_slot);
child_move_slot = active.child_move_slot;
own_mask = active.own_mask;
recycle_textures = active.textures;
textures = active.spare_textures;
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);
debug_assert!(
!reentrant,
@@ -481,12 +525,15 @@ impl UiRenderState {
own_mask,
layer,
id,
textures: Vec::new(),
paints: Vec::new(),
primitives: Vec::new(),
recycle: recycle.into_iter().peekable(),
children: Vec::new(),
size_dependencies: Vec::new(),
textures,
recycle_textures,
paints,
recycle_paints,
primitives,
recycle,
recycle_at: 0,
children,
size_dependencies,
size: None,
reuse_child_sizes,
rsc,
@@ -534,9 +581,12 @@ impl UiRenderState {
child_move_slot,
own_mask,
textures,
mut recycle_textures,
paints,
mut recycle_paints,
primitives,
recycle,
mut recycle,
recycle_at,
children,
size_dependencies,
size: _,
@@ -545,18 +595,31 @@ impl UiRenderState {
id,
} = painter;
for h in recycle {
self.free_primitive(&h);
for h in &recycle[recycle_at..] {
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 {
id,
region,
parent,
textures,
spare_textures: recycle_textures,
paints,
spare_paints: recycle_paints,
primitives,
spare_primitives: recycle,
children,
spare_children: old_children,
size_dependencies,
mask: inherited_mask,
layer: inherited_layer,
@@ -567,12 +630,6 @@ impl UiRenderState {
move_applied: Vec2::ZERO,
};
for c in &old_children {
if !active.children.contains(c) {
self.remove_rec(*c, rsc);
}
}
rsc.on_draw(&active);
self.active.insert(id, active);
size
@@ -713,8 +770,6 @@ impl UiRenderState {
}
}
Self::remask_shape_users(&self.active, id, active.own_mask, &active.primitives, rsc);
active.textures.clear();
rsc.ui_mut().textures.free();
if undraw {
// A captured widget that goes away mid-gesture (LazySpan's
// virtualisation retiring a row, a rebuild) must not leave
@@ -1137,11 +1192,17 @@ impl UiRenderState {
Retained {
region: Some(active.region),
children: active.children,
spare_children: active.spare_children,
move_slot: Some(active.move_slot),
child_move_slot: active.child_move_slot,
own_mask: active.own_mask,
textures: active.textures,
spare_textures: active.spare_textures,
primitives: active.primitives,
spare_primitives: active.spare_primitives,
paints: active.paints,
spare_paints: active.spare_paints,
size_dependencies: active.size_dependencies,
},
rsc,
);
+25 -5
View File
@@ -62,10 +62,24 @@ impl Dirty {
}
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 {
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() {
let mut bits = *word;
while bits != 0 {
@@ -75,14 +89,20 @@ impl Dirty {
if start >= len {
break;
}
match ranges.last_mut() {
match pending.as_mut() {
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);
}
}
ranges
if let Some(range) = pending {
visit(range);
}
}
pub fn clear(&mut self) {
+12 -4
View File
@@ -8,10 +8,18 @@ pub struct WidgetData {
impl WidgetData {
pub fn new<W: Widget>(widget: W) -> Self {
let mut label = std::any::type_name::<W>().to_string();
if let (Some(first), Some(last)) = (label.find(":"), label.rfind(":")) {
label = label.split_at(first).0.to_string() + "::" + label.split_at(last + 1).1;
}
let name = std::any::type_name::<W>();
let label = match (name.find("::"), name.rfind("::")) {
(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 {
widget: Box::new(widget),
label,
+9 -4
View File
@@ -379,7 +379,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
frame_diagnostics.paints_resized,
frame_diagnostics.atlas_pages_grown_prev,
frame_diagnostics.image_bind_group_creates_prev,
renderer.wgpu_errors.snapshot().len(),
renderer.wgpu_errors.len(),
);
}
let mut parts = renderer.draw();
@@ -410,13 +410,18 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
}
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.ui().render_state().get(),
&self.rsc,
) {
);
if access_changed {
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| {
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 = render_state.get();
crate::diagnostics::log_frame(&render_state, frame_start, parts, animating);
if let Some(tree_update) = ui_state.access.update(rsc.widgets(), &render_state, rsc)
{
ui_state.access_adapter.update_if_active(|| tree_update);
if ui_state.access.refresh(rsc.widgets(), &render_state, rsc) {
let access = &ui_state.access;
ui_state
.access_adapter
.update_if_active(|| access.tree_update());
}
}
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.
if cursor.cancelled {
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();
pointer.press_origin = None;
pointer.drag_axis = None;
for id in told {
if let Some(id) = captured {
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;
return;
}
@@ -442,15 +441,16 @@ impl SensorUi for UiRenderState {
pointer.captured = requests.holder();
match pointer.captured {
Some(winner) => {
let losers: Vec<WidgetId> = pointer
.pressed
.iter()
.copied()
.filter(|&id| id != winner)
.collect();
pointer.pressed.retain(|&id| id == winner);
for loser in losers {
deliver_cancel(self, rsc, state, loser, &cursor, window_size, &requests);
let mut winner_was_pressed = false;
for id in pointer.pressed.drain(..) {
if id == winner {
winner_was_pressed = true;
} else {
deliver_cancel(self, rsc, state, id, &cursor, window_size, &requests);
}
}
if winner_was_pressed {
pointer.pressed.push(winner);
}
}
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 gap = self.gap.apply_rest(painter.density()).abs;
let mut lens: Vec<Option<LayoutLen>> = self
.children
.iter()
.map(|child| painter.known_len(child, axis))
.collect();
let mut drawn = vec![false; self.children.len()];
let gap_total = gap * self.children.len().saturating_sub(1) as f32;
let mut measured = Vec::with_capacity(self.children.len());
let mut total = LayoutLen::abs(gap_total);
let mut cursor = UiScalar::rel_min();
for (i, child) in self.children.iter().enumerate() {
let len = match lens[i] {
Some(len) => len,
for child in &self.children {
let (len, drawn) = match painter.known_len(child, axis) {
Some(len) => (len, false),
None => {
let mut slot = UiSpan::new(cursor, UiScalar::rel_max());
if self.dir.sign == Sign::Neg {
@@ -35,47 +31,25 @@ impl Widget for Span {
}
let region = UiRegion::from_axis(axis, slot, UiSpan::FULL);
let len = painter.widget_within(child, region).size().axis(axis);
lens[i] = Some(len);
drawn[i] = true;
len
(len, true)
}
};
measured.push((len, drawn));
total += len;
cursor.abs += len.abs + gap;
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 ortho_len = LayoutLen::ZERO;
let mut ortho_mixed = false;
let mut placed = Vec::with_capacity(self.children.len());
for (i, (child, &len)) in self.children.iter().zip(&lens).enumerate() {
let mut span = UiSpan::FULL;
span.start = start;
if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.abs);
let rel_end = UiScalar::rel(len.rest / total.rest);
let end = (UiScalar::rel_max() + start) - offset;
start = rel_end.within(&start.to(end));
}
start.abs += len.abs;
start.rel += len.rel;
span.end = start;
let mut child_region = UiRegion::from_axis(axis, span, UiSpan::FULL);
if self.dir.sign == Sign::Neg {
child_region.flip(axis);
}
let used = if drawn[i] {
for (child, &(len, drawn)) in self.children.iter().zip(&measured) {
let child_region = child_region(axis, self.dir.sign, total, gap, &mut start, len);
let used = if drawn {
painter.place(child, child_region).size()
} else {
painter.widget_within(child, child_region).size()
};
placed.push(child_region);
start.abs += gap;
let ortho = used.axis(!axis);
if ortho.rel > 0.0 || ortho.rest > 0.0 {
@@ -90,7 +64,9 @@ impl Widget for Span {
let ortho = ortho_len
.apply_rest(painter.density())
.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;
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 {
pub fn empty(dir: Dir) -> 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 mut used = Vec::with_capacity(self.children.len());
let mut iter = self.children.iter();
if let Some(child) = iter.next() {
painter.child_layer();
used.push(match region {
let mut used = known
.is_none()
.then(|| Vec::with_capacity(self.children.len()));
let mut selected = known.unwrap_or_default();
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(),
None => painter.widget(child).size(),
});
}
for child in iter {
painter.next_layer();
used.push(match region {
Some(region) => painter.widget_within(child, region).size(),
None => painter.widget(child).size(),
});
};
if matches!(self.size, StackSize::Child(target) if target == i) {
selected = child_size;
}
if let Some(used) = &mut used {
used.push(child_size);
}
}
let size = match self.size {
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);
for (child, child_size) in self.children.iter().zip(used) {
let child_region = child_size
+12 -13
View File
@@ -188,16 +188,18 @@ impl TextView {
self.overflow = OverflowState::default();
let available = painter.px_size().x.max(0.0);
let excess = (tex.size.x - available).max(0.0);
let attrs = self.text.attrs().clone();
if excess <= 0.0 || matches!(attrs.overflow, TextOverflow::Visible | TextOverflow::Wrap) {
let (overflow, overflow_position) = {
let attrs = self.text.attrs();
(attrs.overflow, attrs.overflow_position)
};
if excess <= 0.0 || matches!(overflow, TextOverflow::Visible | TextOverflow::Wrap) {
return None;
}
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
.overflow_position
let mut pan = overflow_position
.resolve(painter.density())
.to_abs(excess)
.clamp(0.0, excess);
@@ -229,10 +231,7 @@ impl TextView {
}
pan = pan.clamp(0.0, excess);
}
let old_pan = attrs
.overflow_position
.resolve(painter.density())
.to_abs(excess);
let old_pan = overflow_position.resolve(painter.density()).to_abs(excess);
if pan != old_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.pan = pan;
self.overflow.content_end = available;
if attrs.overflow != TextOverflow::Ellipsis {
if overflow != TextOverflow::Ellipsis {
return None;
}
@@ -287,14 +286,14 @@ impl TextView {
return painter.widget(hint).size();
}
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 {
let within = tex.size.align(attrs.align).within(&painter.region());
let within = tex.size.align(align).within(&painter.region());
painter.glyphs(&tex, within);
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);
if self.overflow.suppress_content
&& self
+13 -13
View File
@@ -312,15 +312,16 @@ impl SelectionController {
}
fn deselect(&mut self, rsc: &mut impl UiRsc) {
let mut ids = std::mem::take(&mut self.selected);
if let Some((anchor, _)) = self.anchor.take()
&& !ids.contains(&anchor)
{
ids.push(anchor);
}
for id in ids {
let anchor = self.anchor.take().map(|(id, _)| id);
let anchor_was_selected = anchor.is_some_and(|anchor| self.selected.contains(&anchor));
for id in self.selected.drain(..) {
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) {
@@ -352,8 +353,7 @@ impl SelectionController {
(focus_at, anchor_at)
};
let old = std::mem::take(&mut self.selected);
for old_id in old {
for &old_id in &self.selected {
if !self.order[lo..=hi].contains(&old_id) {
Self::with_text(rsc, old_id, |text| text.deselect());
}
@@ -375,7 +375,8 @@ impl SelectionController {
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>(
@@ -486,12 +487,11 @@ impl<Rsc: HasEvents> Controller<Rsc> for SelectionController {
.map(CommandResult::Copy)
.unwrap_or(CommandResult::Unused),
Command::SelectAll => {
let order = self.order.clone();
self.deselect(rsc);
for &id in &order {
for &id in &self.order {
Self::with_text(rsc, id, |text| text.select_all());
}
self.selected = order;
self.selected.extend_from_slice(&self.order);
CommandResult::Used
}
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);
}