Files
iris/core/src/ui/painter.rs
T

506 lines
20 KiB
Rust

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<MoveIdx>,
/// This widget's retained mask slot.
pub(super) own_mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>,
/// Previous handles, consumed in draw order and freed if left over.
pub(super) recycle: std::iter::Peekable<std::vec::IntoIter<PrimitiveHandle>>,
pub(super) children: Vec<WidgetId>,
pub(super) reuse_child_sizes: bool,
pub layer: usize,
pub(super) id: WidgetId,
}
impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(&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<PrimitiveHandle> {
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<P: 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<P: Primitive>(&mut self, primitive: P) {
self.primitive_at(primitive, self.region)
}
pub fn primitive_within<P: Primitive>(&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<W: ?Sized>(&mut self, shape: &StrongWidget<W>) {
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<W: ?Sized>(&mut self, id: &StrongWidget<W>) -> 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<W: ?Sized>(&mut self, id: &StrongWidget<W>, 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<W: ?Sized>(&self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
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<W: ?Sized>(&mut self, id: &StrongWidget<W>, 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<W: ?Sized>(&mut self, id: &StrongWidget<W>, 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<W: ?Sized>(
&mut self,
id: &StrongWidget<W>,
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<f32>,
) -> 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
}
}