diff --git a/core/src/primitive/text.rs b/core/src/primitive/text.rs index 1b44c5e..e8a41fe 100644 --- a/core/src/primitive/text.rs +++ b/core/src/primitive/text.rs @@ -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, 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> = self .spans diff --git a/core/src/primitive/texture.rs b/core/src/primitive/texture.rs index c4943db..461c33f 100644 --- a/core/src/primitive/texture.rs +++ b/core/src/primitive/texture.rs @@ -34,6 +34,14 @@ pub struct TextureHandle { rsc: RscHandle, } +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 { diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index 35d8c4c..b5ebe84 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -112,6 +112,14 @@ impl WgpuErrorLog { pub fn snapshot(&self) -> Vec { 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; } } diff --git a/core/src/render/primitive.rs b/core/src/render/primitive.rs index 1d8ffd3..d7950d2 100644 --- a/core/src/render/primitive.rs +++ b/core/src/render/primitive.rs @@ -450,19 +450,26 @@ impl LayerOrder { } pub fn apply_free(&mut self) -> Vec { - 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) { + 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, dirty: &mut Dirty, is_image: bool, - ) -> Vec { + changes: &mut Vec, + ) { 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 { @@ -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, diff --git a/core/src/render/util/mod.rs b/core/src/render/util/mod.rs index c87ed7f..305fec1 100644 --- a/core/src/render/util/mod.rs +++ b/core/src/render/util/mod.rs @@ -90,13 +90,13 @@ impl ArrBuf { } self.len = data.len(); let stride = std::mem::size_of::() 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 } diff --git a/core/src/ui/access.rs b/core/src/ui/access.rs index d8c8306..a7bfe12 100644 --- a/core/src/ui/access.rs +++ b/core/src/ui/access.rs @@ -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, + generation: u64, rebuilds: u64, } @@ -45,12 +46,17 @@ impl AccessTree { Self::default() } - fn collect( - widgets: &Widgets, - render: &UiRenderState, - rsc: &dyn UiRsc, - ) -> HashMap { - 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 { - 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(¤t)) + 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 { diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index 0d47649..05901e6 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -9,12 +9,16 @@ pub struct ActiveData { pub region: UiRegion, pub parent: Option, pub textures: Vec, + pub(crate) spare_textures: Vec, /// 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, + pub(crate) spare_paints: Vec, pub primitives: Vec, + pub(crate) spare_primitives: Vec, pub children: Vec, + pub(crate) spare_children: Vec, pub size_dependencies: Vec, pub mask: MaskIdx, /// The widget's retained mask slot, or `MaskIdx::NONE`. diff --git a/core/src/ui/mod.rs b/core/src/ui/mod.rs index 433ca33..b66f7d7 100644 --- a/core/src/ui/mod.rs +++ b/core/src/ui/mod.rs @@ -122,13 +122,11 @@ impl Ui { update(&mut text)?; text.invalidate_all() }; - let active: Vec = { + 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(()) } diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 012ad20..49f532c 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -21,9 +21,12 @@ pub struct Painter<'a> { pub(super) child_move_slot: Option, pub(super) own_mask: MaskIdx, pub(super) textures: Vec, + pub(super) recycle_textures: Vec, pub(super) paints: Vec, + pub(super) recycle_paints: Vec, pub(super) primitives: Vec, - pub(super) recycle: std::iter::Peekable>, + pub(super) recycle: Vec, + pub(super) recycle_at: usize, pub(super) children: Vec, pub(super) size_dependencies: Vec, pub(super) size: Option, @@ -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 { - 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( @@ -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(&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) => { diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 662a626..d47f7dc 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -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, pub primitives: Primitives, pub layers: PrimitiveLayers, + order_changes: Vec, 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, pub children: Vec, + pub spare_children: Vec, pub move_slot: Option, pub child_move_slot: Option, pub own_mask: MaskIdx, + pub textures: Vec, + pub spare_textures: Vec, pub primitives: Vec, + pub spare_primitives: Vec, pub paints: Vec, + pub spare_paints: Vec, + pub size_dependencies: Vec, } 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, ); diff --git a/core/src/util/dirty.rs b/core/src/util/dirty.rs index 1beda01..beb074a 100644 --- a/core/src/util/dirty.rs +++ b/core/src/util/dirty.rs @@ -62,10 +62,24 @@ impl Dirty { } pub fn ranges(&self, len: usize, gap: usize) -> Vec> { + 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), + ) { if self.all { - return Vec::from_iter((len > 0).then_some(0..len)); + if len > 0 { + visit(0..len); + } + return; } - let mut ranges: Vec> = Vec::new(); + let mut pending: Option> = 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) { diff --git a/core/src/widget/data.rs b/core/src/widget/data.rs index b0fe4dd..bdc6485 100644 --- a/core/src/widget/data.rs +++ b/core/src/widget/data.rs @@ -8,10 +8,18 @@ pub struct WidgetData { impl WidgetData { pub fn new(widget: W) -> Self { - let mut label = std::any::type_name::().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::(); + 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, diff --git a/src/android/view.rs b/src/android/view.rs index 8686b0e..69208ff 100644 --- a/src/android/view.rs +++ b/src/android/view.rs @@ -379,7 +379,7 @@ impl IrisViewPeer { 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 IrisViewPeer { } 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); }); diff --git a/src/desktop/mod.rs b/src/desktop/mod.rs index 953dfd3..d7faa06 100644 --- a/src/desktop/mod.rs +++ b/src/desktop/mod.rs @@ -269,9 +269,11 @@ impl AppState for DesktopApp { 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) => { diff --git a/src/rsc/sense.rs b/src/rsc/sense.rs index 3a80307..fae2321 100644 --- a/src/rsc/sense.rs +++ b/src/rsc/sense.rs @@ -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 = 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::().global = pointer; return; } @@ -442,15 +441,16 @@ impl SensorUi for UiRenderState { pointer.captured = requests.holder(); match pointer.captured { Some(winner) => { - let losers: Vec = 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(), diff --git a/src/widget/layout/span.rs b/src/widget/layout/span.rs index 96a1ffc..d44da86 100644 --- a/src/widget/layout/span.rs +++ b/src/widget/layout/span.rs @@ -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> = 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 = 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 { diff --git a/src/widget/layout/stack.rs b/src/widget/layout/stack.rs index 807fdd2..6d5407f 100644 --- a/src/widget/layout/stack.rs +++ b/src/widget/layout/stack.rs @@ -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 diff --git a/src/widget/text/mod.rs b/src/widget/text/mod.rs index 5cd9ad4..745cc88 100644 --- a/src/widget/text/mod.rs +++ b/src/widget/text/mod.rs @@ -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 diff --git a/src/widget/text/selection.rs b/src/widget/text/selection.rs index c22664e..fc5227b 100644 --- a/src/widget/text/selection.rs +++ b/src/widget/text/selection.rs @@ -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( @@ -486,12 +487,11 @@ impl Controller 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 => { diff --git a/tests/allocation.rs b/tests/allocation.rs new file mode 100644 index 0000000..1d4e3c7 --- /dev/null +++ b/tests/allocation.rs @@ -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 = const { Cell::new(false) }; + static ALLOCATIONS: Cell = 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); +}