506 lines
20 KiB
Rust
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
|
|
}
|
|
}
|