use crate::{ Axis, Color, Len, MoveOffset, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, render::{ Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive, PrimitiveHandle, PrimitiveInst, RectPrimitive, }, ui::render_state::Retained, util::Vec2, }; pub struct Painter<'a> { pub(super) state: &'a mut UiRenderState, pub(super) rsc: &'a mut dyn UiRsc, pub(super) region: UiRegion, pub(super) mask: MaskIdx, pub(super) move_slot: MoveIdx, pub(super) child_move_slot: Option, /// This widget's retained mask slot. pub(super) own_mask: MaskIdx, pub(super) textures: Vec, pub(super) primitives: Vec, /// Previous handles, consumed in draw order and freed if left over. pub(super) recycle: std::iter::Peekable>, pub(super) children: Vec, pub(super) reuse_child_sizes: bool, pub layer: usize, pub(super) id: WidgetId, } impl<'a> Painter<'a> { fn primitive_at(&mut self, primitive: P, region: UiRegion) { self.write_primitive(primitive, region, Drawn::Yes); } /// The next handle from the previous draw, if it can hold what is /// about to be written: same kind of primitive, same layer, and the /// same answer to "does a layer's draw order name it". /// /// **Consumed strictly in order, and one mismatch ends recycling for /// the rest of the draw.** A widget's `draw` is a function of its own /// state, so a redraw writes the same sequence of primitives in the /// same order in the overwhelmingly common case; searching the /// remainder for a match would turn an O(1) step into an O(primitives) /// one to rescue a case that means the widget's content changed shape /// 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 drawn_matches = (h.pos == NOT_DRAWN) == (drawn == Drawn::No); if h.binding != binding || h.layer != self.layer || !drawn_matches { return None; } self.recycle.next() } /// The one path every primitive this widget owns goes through -- /// drawn or, for a mask's shape, only referenced. fn write_primitive( &mut self, primitive: P, region: UiRegion, drawn: Drawn, ) -> u32 { let inst = PrimitiveInst { id: self.id, primitive, region, mask_idx: self.mask, move_idx: self.move_slot, }; let h = match self.take_recycled(P::BINDING, drawn) { Some(h) => { self.state.primitives.recycle(&h, inst); h } None => self.state.write_primitive(self.layer, drawn, inst), }; if self.mask != MaskIdx::NONE { self.rsc.ui_mut().masks.push_ref(self.mask); } let slot = h.slot; self.own(h); slot } /// Take ownership of a handle this widget just wrote. /// /// The one place a `PrimitiveHandle` enters `self.primitives`, and so /// the one place that can keep `Primitives::handle_index` in step with /// where it lands -- which is what `UiRenderState::apply_free` reads /// instead of scanning this vec. Anything that writes a primitive /// without coming through here leaves that index unset, and its /// position in a layer's draw order stops being renumbered. fn own(&mut self, h: PrimitiveHandle) { self.state .primitives .set_handle_index(h.slot, self.primitives.len() as u32); self.primitives.push(h); } /// Writes a primitive to be rendered pub fn primitive(&mut self, primitive: P) { self.primitive_at(primitive, self.region) } pub fn primitive_within(&mut self, primitive: P, region: UiRegion) { self.primitive_at(primitive, region.within(&self.region)); } /// Clip everything this widget draws, itself and its descendants, to /// `region`. One call per widget; a widget drawn inside another /// widget's mask nests instead -- the new mask chains to the inherited /// one (`Mask::parent`) and the fragment stage multiplies both /// coverages, which is what lets a transcript row's code fence clip /// to itself *and* to the list it scrolls inside. /// /// The clip is a **primitive**, not a rectangle copied into the mask: /// this writes an undrawn `RectPrimitive` at `region` and points the /// mask at it, so the fragment stage evaluates the same rounded-rect /// coverage a drawn rect gets. See LAYOUT.md's "Masks with a shape". /// /// The slot is allocated once and **rewritten in place** on every /// later draw rather than pushed again, because a descendant whose own /// region did not change is not redrawn (`draw_inner`'s fast path) and /// so keeps pointing at whichever slot it was drawn under. See /// `ActiveData::own_mask` for what pushing a fresh one cost. pub fn set_mask(&mut self, region: UiRegion) { let shape = self.write_primitive(RectPrimitive::color(Color::NONE), region, Drawn::No); self.set_mask_to(shape); } /// Clip everything this widget draws after this call to `shape`'s /// own shape -- the first primitive `shape`'s subtree drew, which /// must already have been drawn this frame /// (`UiRenderState::first_primitive`). What `.masked_by()` uses to /// clip a container's content to the rounded background it draws, /// with no radius argument anywhere that could fall out of step with /// the one being drawn. pub fn set_mask_to_widget(&mut self, shape: &StrongWidget) { let slot = self.state.first_primitive(shape.id()).unwrap_or_else(|| { panic!( "'{}' was given as a mask's shape but drew no primitive, so there is nothing to \ clip to", self.rsc.widgets().label(shape.id()), ) }); self.set_mask_to(slot); } /// Points this widget's mask at a primitive that has already been /// written -- the shared half of [`Self::set_mask`]. fn set_mask_to(&mut self, shape: u32) { // `assert!`, not `debug_assert!`: one comparison per widget draw, // and the second call silently *replacing* the first is a widget // drawn unclipped -- which reaches the screen and nothing says so. // Every build anybody runs here is release // (review, 2026-09-07). assert!( self.own_mask == MaskIdx::NONE || self.mask != self.own_mask, "set_mask called twice while drawing one widget: the second would replace the first \ rather than nest inside it", ); // A glyph would need a CPU-side alpha plane for the hit test to // agree with the shader, and a standalone image a bind-group // switch the fragment stage cannot make -- see `Mask::primitive`. // Named here rather than left to the shader, which would read a // rect that is not there and clip to nothing. let binding = self.state.primitives.instance(shape).binding; assert_eq!( binding, RectPrimitive::BINDING, "a mask's shape must be a rect primitive; primitive {shape} is binding {binding}", ); let parent = self.mask; let mask = Mask { primitive: shape, parent, }; let old_parent = if self.own_mask == MaskIdx::NONE { let slot = self.rsc.ui_mut().masks.push(mask); // The one ref this widget holds on its own slot, so the slot // outlives any single frame's primitives; released in // `UiRenderState::remove`'s `undraw` branch. self.rsc.ui_mut().masks.push_ref(slot); self.own_mask = slot; MaskIdx::NONE } else { let old = self.rsc.ui().masks[self.own_mask.idx()].parent; *self.rsc.ui_mut().masks.get_mut(self.own_mask) = mask; old }; // The chain link's own ref, taken before the old one is dropped so // that re-chaining to the same slot cannot free it in between. // Released here when the link changes, and in // `UiRenderState::remove` when this widget's slot goes. if old_parent != parent { if parent != MaskIdx::NONE { self.rsc.ui_mut().masks.push_ref(parent); } if old_parent != MaskIdx::NONE { self.rsc.ui_mut().masks.remove(old_parent); } } self.mask = self.own_mask; } /// Draws a widget within this widget's region, returning the size it /// reported using. pub fn widget(&mut self, id: &StrongWidget) -> Size { self.widget_at(id, self.region) } /// Draws a widget somewhere within this one. /// Useful for drawing child widgets in select areas. pub fn widget_within(&mut self, id: &StrongWidget, region: UiRegion) -> Size { self.widget_at(id, region.within(&self.region)) } /// Translate this widget's children as one retained subtree, in output /// pixels. The first call must happen before drawing a child, because the /// slot becomes the parent of every direct child's ordinary move slot. /// Once retained, it may be updated later in a redraw (for example after /// measuring a changed child). All deeper descendants inherit it and the /// CPU hit-test walk resolves the same translation as the shader. /// /// This offsets the child coordinate space, not this widget: its own /// primitives and hit region remain fixed. Once allocated, the boundary /// stays in the chain across redraws; set it to zero to return children to /// their unshifted positions. pub fn set_child_offset(&mut self, offset: Vec2) { let slot = match self.child_move_slot { Some(slot) => slot, None => { assert!( self.children.is_empty(), "a child offset must be created before drawing a child" ); let parent = self.move_slot.idx() as u32; let slot = self .rsc .ui_mut() .move_offsets .push(MoveOffset::new([offset.x, offset.y], parent)); // One ref for this widget's ownership and one on the // up-link. Direct children take their own refs when their // move slots are allocated. self.rsc.ui_mut().move_offsets.push_ref(slot); self.rsc.ui_mut().move_offsets.push_ref(self.move_slot); self.child_move_slot = Some(slot); return; } }; let next = [offset.x, offset.y]; if self.rsc.ui().move_offsets[slot.idx()].delta != next { self.rsc.ui_mut().move_offsets.get_mut(slot).delta = next; self.state.note_move(); } } pub fn known_len(&self, id: &StrongWidget, axis: Axis) -> Option { if let Some(len) = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis) { return Some(len.fold_dp(self.density())); } if !self.reuse_child_sizes || self.rsc.widgets().needs_redraw.contains(&id.id()) { return None; } self.state.active.get(&id.id()).map(|a| a.size.axis(axis)) } fn widget_at(&mut self, id: &StrongWidget, region: UiRegion) -> Size { self.children.push(id.id()); // Passed directly rather than looked up from `self.active`: this // widget's own `ActiveData` (which would carry its `move_slot`) is // not inserted there until *after* its own `Widget::draw` returns, // so a lookup here -- for a child drawn partway through that same // call -- would always find nothing. `self.move_slot` is this // widget's own slot, already known, and always correct regardless // of insertion order. See `UiRenderState::move_parent_of`. let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot); self.state.draw_inner( self.layer, id.id(), region, Some(self.id), parent_move_slot.idx() as u32, self.mask, Retained::default(), self.rsc, ) } /// Place an already-drawn child's used area, redrawing only if its size changes. pub fn place(&mut self, id: &StrongWidget, region: UiRegion) -> Size { let region = region.within(&self.region); let retained = self .state .active .get(&id.id()) .map(|active| (active.layer, active.mask)); if let Some(size) = self.state.place(id.id(), region, self.rsc) { size } else if let Some((layer, mask)) = retained { self.children.push(id.id()); self.rsc.widgets_mut().needs_redraw.insert(id.id()); let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot); self.state.draw_inner( layer, id.id(), region, Some(self.id), parent_move_slot.idx() as u32, mask, Retained::default(), self.rsc, ) } else { self.widget_at(id, region) } } pub fn place_used( &mut self, id: &StrongWidget, used: Size, within: UiRegion, ) -> Size { let region = self.fit_region(used, within); self.place(id, region) } pub fn fit_region(&mut self, used: Size, mut within: UiRegion) -> UiRegion { let mut region = used .to_uivec2(self.density()) .align(RegionAlign::TOP_LEFT) .within(&within); let output = self.output_size(); for axis in [Axis::X, Axis::Y] { let mut actual = region.within(&self.region); let mut available = within.within(&self.region); if actual.axis(axis).len().to_abs(output.axis(axis)) > available.axis(axis).len().to_abs(output.axis(axis)) { *region.axis_mut(axis) = *within.axis(axis); } } region } pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) { self.textures.push(handle.clone()); self.write_image(handle.image_index(), region.within(&self.region)); } pub fn texture(&mut self, handle: &TextureHandle) { self.textures.push(handle.clone()); self.write_image(handle.image_index(), self.region); } pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) { self.textures.push(handle.clone()); self.write_image(handle.image_index(), region); } /// A standalone image draws with its own bind group rather than sharing /// the layer's one instanced draw, so it goes through /// `Primitives::write_image` instead of `primitive_at`/`Primitive::vec`. fn write_image(&mut self, texture_idx: u32, region: UiRegion) { let h = match self.take_recycled(IMAGE_BINDING, Drawn::Yes) { Some(h) => { self.state.primitives.recycle_image( &h, self.id, texture_idx, region, self.mask, self.move_slot, ); h } None => self.state.write_image( self.layer, self.id, texture_idx, region, self.mask, self.move_slot, ), }; if self.mask != MaskIdx::NONE { self.rsc.ui_mut().masks.push_ref(self.mask); } self.own(h); } pub fn render_text( &mut self, buffer: &mut TextBuffer, attrs: &TextAttrs, width: Option, ) -> RenderedText { let density = self.state.density; // Counted here rather than in `TextView::render`, which returns // its memoized layout without reaching this -- so this counts // shapes, not requests. `UiRenderState::take_counters`. self.state.shape_count += 1; let ui = self.rsc.ui_mut(); ui.text .render(buffer, attrs, width, &mut ui.textures, density) } /// Which glyph atlas the glyphs handed out right now belong to -- /// what a widget caching a [`RenderedText`] across frames has to /// compare against before re-emitting it (`GlyphAtlas::clear`). pub fn atlas_generation(&mut self) -> u64 { self.rsc.ui_mut().text.atlas.generation() } /// Draw a laid-out string: one quad per glyph, all sampling the atlas. /// /// `origin` is where the text's top-left goes; every glyph is placed at an /// absolute pixel offset from it, so re-drawing after a resize is this loop /// and nothing else. pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { // A caller re-emitting quads placed against an atlas that has since // been cleared draws every glyph from coordinates now holding // something else. Caught at the submission rather than on screen, // where it reads as fragments of unrelated letters. `assert_eq!` // for R1's reason: two integers per laid-out string, not per // glyph, and the failure is unreadable text on a release build. assert_eq!( text.generation, self.atlas_generation(), "glyphs placed against atlas generation {} submitted against {}: the holder did not \ re-render after the atlas was cleared", text.generation, self.atlas_generation(), ); let flags_for = |is_color| { if is_color { GlyphPrimitive::IS_COLOR } else { 0 } }; for glyph in text.glyphs.iter() { let mut region = origin; region.x.end = region.x.start; region.y.end = region.y.start; let mut region = region.offset(UiVec2::abs(glyph.offset)); region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32); region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32); self.primitive_at( GlyphPrimitive::new( glyph.entry.uv_min, glyph.entry.uv_max, glyph.entry.layer, glyph.color, flags_for(glyph.entry.is_color), ), region, ); } } pub fn region(&self) -> UiRegion { self.region } pub fn output_size(&self) -> Vec2 { self.state.output_size } /// Physical pixels per `dp` -- see `UiRenderState::density`'s field /// doc. What `Len::dp`'s `apply_rest` call resolves against. pub fn density(&self) -> f32 { self.state.density } pub fn px_size(&mut self) -> Vec2 { self.region.size().to_abs(self.state.output_size) } pub fn text_data(&mut self) -> &mut TextData { &mut self.rsc.ui_mut().text } pub fn child_layer(&mut self) { self.layer = self.state.layers.child(self.layer); } pub fn next_layer(&mut self) { self.layer = self.state.layers.next(self.layer); } pub fn label(&self) -> &str { &self.rsc.widgets().data(self.id).unwrap().label } pub fn id(&self) -> &WidgetId { &self.id } }