`.width()` built a `SetSize` whose whole job was to answer `size_hint`, so every declared length cost a widget, an `ActiveData` and a link of chain to say one number. It is now a `SizeRule` per axis on `WidgetData`, beside `region_node`, resolved by `Painter` where the widget is drawn. `SetSize` and `MaxSize` are gone; `MaxSize` had no caller but its own builders. That settles which of two answers is the size. A rule wins on the axis it names and the `Size` returned by `draw` answers the rest, applied once in `draw_inner` rather than by each widget that could carry one -- so the widget under a rule never learns of it. `Painter::size_hint` reads the rule first for the same reason: a rule that beats what a widget would draw has to beat what it says about itself. `declared_lens` still falls back to a non-leftover `size_hint`, which is how an image or a gap gets its own pixel size rather than the whole offer. That is the offer's business rather than a declaration's, and it falls away when a widget occupies its reported size inside the box it was offered. `known` and `declared` are separate because a share is a length to whoever divides one and not to whoever composes a box: `.width(leftover(3))` is known without drawing but cannot narrow anything. Checked: fmt, clippy, 85 tests, and 100 generated seeds agreeing warm against cold in 67.6 s. `minimal`, `text` and `view` render byte-identical at 1920x1200; `tabs` differs only in the widget count it prints about itself, which is two wrapper types smaller.
470 lines
17 KiB
Rust
470 lines
17 KiB
Rust
#[cfg(feature = "layout-diagnostics")]
|
|
use crate::layout_diagnostics::{self as diag, Counter};
|
|
use crate::{
|
|
Axis, Holds, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
|
|
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, Widgets,
|
|
render::{
|
|
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
|
PrimitiveKind, TexturePrimitive,
|
|
},
|
|
ui::render_state::DrawInfo,
|
|
util::Vec2,
|
|
};
|
|
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
|
|
|
/// makes your surfaces look pretty
|
|
pub struct Painter<'a> {
|
|
pub(super) state: &'a mut UiRenderState,
|
|
pub(super) rsc: &'a mut dyn UiRsc,
|
|
|
|
/// This widget's box, in the coordinates of `move_idx`.
|
|
pub(super) region: UiRegion,
|
|
pub(super) mask: MaskIdx,
|
|
pub(super) textures: Vec<TextureHandle>,
|
|
pub(super) primitives: Vec<PrimitiveHandle>,
|
|
pub(super) children: Vec<WidgetId>,
|
|
/// The children asked about so far, so the first box each was asked in
|
|
/// is the one recorded as its offer.
|
|
pub(super) offered: Vec<WidgetId>,
|
|
/// The box this widget was first asked about in, in pixels.
|
|
pub(super) offered_px: Vec2,
|
|
/// Whether this draw is in that box, which makes the questions it asks
|
|
/// the ones a cold layout asks and their answers the ones to keep.
|
|
pub(super) at_offer: bool,
|
|
/// The children whose size this widget read while drawing.
|
|
pub(super) size_deps: Vec<WidgetId>,
|
|
/// What this draw itself read of its box in pixels, per axis: every
|
|
/// length until it reads one, then that one, unless it says otherwise.
|
|
pub(super) own: [Holds; 2],
|
|
/// What the children it asked about and drew keep it to.
|
|
pub(super) under: [Holds; 2],
|
|
/// The movable region this widget's primitives are positioned through:
|
|
/// its own when opted in, otherwise the nearest ancestor's.
|
|
pub(super) move_idx: MoveIdx,
|
|
pub layer: usize,
|
|
pub(super) depth: usize,
|
|
pub(super) id: WidgetId,
|
|
}
|
|
|
|
impl<'a> Painter<'a> {
|
|
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
|
let kind = self.rsc.ui_mut().primitives.kind::<P>();
|
|
self.write(kind, primitive, region);
|
|
}
|
|
|
|
/// Takes the kind, for a caller writing many of one primitive.
|
|
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::PrimitiveWrites);
|
|
let h = self.state.layers.write(
|
|
self.layer,
|
|
PrimitiveInst {
|
|
kind,
|
|
id: self.id,
|
|
primitive,
|
|
region,
|
|
mask_idx: self.mask,
|
|
move_idx: self.move_idx,
|
|
},
|
|
);
|
|
self.push_primitive(h);
|
|
}
|
|
|
|
fn push_primitive(&mut self, h: PrimitiveHandle) {
|
|
if self.mask != MaskIdx::NONE {
|
|
// TODO: I have no clue if this works at all :joy:
|
|
self.rsc.ui_mut().masks.push_ref(self.mask);
|
|
}
|
|
self.primitives.push(h);
|
|
}
|
|
|
|
/// Writes a primitive to be rendered
|
|
pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
|
|
let primitive = primitive.into_primitive(self);
|
|
self.primitive_at(primitive, self.region)
|
|
}
|
|
|
|
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
|
|
let primitive = primitive.into_primitive(self);
|
|
self.primitive_at(primitive, region.within(&self.region));
|
|
}
|
|
|
|
pub fn set_mask(&mut self, region: UiRegion) {
|
|
assert!(self.mask == MaskIdx::NONE);
|
|
self.mask = self.rsc.ui_mut().masks.push(Mask {
|
|
region,
|
|
move_idx: self.move_idx,
|
|
});
|
|
}
|
|
|
|
/// Draws a widget within this widget's region.
|
|
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
|
|
self.widget_at(id, UiRegion::FULL)
|
|
}
|
|
|
|
/// Draws a widget somewhere within this one.
|
|
pub fn widget_within<'s, W: ?Sized>(
|
|
&'s mut self,
|
|
id: &'s StrongWidget<W>,
|
|
region: UiRegion,
|
|
) -> DrawResult<'s, 'a, W> {
|
|
self.widget_at(id, region)
|
|
}
|
|
|
|
/// What a widget's rules declare its lengths to be, which whoever draws
|
|
/// it resolves into its box. Reading them depends on nothing -- the box
|
|
/// that comes of them is kept on the child, and `redraw` compares it
|
|
/// there.
|
|
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<Len>; 2] {
|
|
declared_lens(self.rsc.widgets(), id.id())
|
|
}
|
|
|
|
/// Takes back a child that was drawn only to find out how long it is.
|
|
/// Its drawing is dropped and it is not one of this widget's children
|
|
/// this frame; what it answered is still something this widget asked.
|
|
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
|
|
self.children.retain(|child| *child != id.id());
|
|
self.state.undraw_rec(id.id(), self.rsc);
|
|
}
|
|
|
|
/// `region` in this widget's own coordinates, and with the child's
|
|
/// declared lengths still to be taken.
|
|
fn widget_at<'s, W: ?Sized>(
|
|
&'s mut self,
|
|
id: &'s StrongWidget<W>,
|
|
region: UiRegion,
|
|
) -> DrawResult<'s, 'a, W> {
|
|
let region_node = self.rsc.widgets().is_region_node(id.id());
|
|
let declared = self.declared_lens(id);
|
|
// Composing `FULL` through a box is not quite the identity in f32,
|
|
// so a child with nothing declared keeps the box it would have had.
|
|
let local = match declared.iter().any(Option::is_some) {
|
|
true => declared_box(region, declared),
|
|
false => region,
|
|
};
|
|
let within = match local == UiRegion::FULL {
|
|
true => self.region,
|
|
false => local.within(&self.region),
|
|
};
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
if region_node {
|
|
diag::bump(Counter::RegionNodeDraws);
|
|
diag::region_node(id.id(), self.id, within);
|
|
}
|
|
// A child listed twice would be moved twice.
|
|
if !self.children.contains(&id.id()) {
|
|
self.children.push(id.id());
|
|
}
|
|
let first_ask = self.offer(id.id());
|
|
let offer = match first_ask {
|
|
true => local,
|
|
false => self.state.active.get(&id.id()).map_or(local, |a| a.offer),
|
|
};
|
|
let answers_offer = self.at_offer && local == offer;
|
|
let size = self.state.draw_inner(
|
|
id.id(),
|
|
within,
|
|
DrawInfo {
|
|
layer: self.layer,
|
|
parent: Some(self.id),
|
|
depth: self.depth + 1,
|
|
parent_move: self.move_idx,
|
|
region_node,
|
|
mask: self.mask,
|
|
offer,
|
|
offered_px: self.px_within_offer(offer),
|
|
},
|
|
None,
|
|
self.rsc,
|
|
);
|
|
let active = self.state.active.get_mut(&id.id()).unwrap();
|
|
active.declared = declared;
|
|
if answers_offer {
|
|
active.answer = (active.size, active.holds);
|
|
}
|
|
// Whatever the child's drawing holds for keeps this one to the boxes
|
|
// that give the child a length inside it.
|
|
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
|
|
*under = under.and(active.holds[axis as usize].through(local.axis(axis).len()));
|
|
}
|
|
DrawResult {
|
|
child: id,
|
|
painter: self,
|
|
size,
|
|
}
|
|
}
|
|
|
|
/// What a child says its length is without being drawn, if it can say.
|
|
/// Asking counts as reading its size.
|
|
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
|
|
let widgets = self.rsc.widgets();
|
|
// A rule is the answer where there is one: it wins over whatever the
|
|
// widget would draw, so it has to win over what the widget says too.
|
|
let hint = widgets.size_rules(id.id()).axis(axis).known().or_else(|| {
|
|
widgets
|
|
.get_dyn(id.id())
|
|
.and_then(|widget| widget.size_hint(axis))
|
|
});
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::hint_read(id.id(), self.id, axis, hint);
|
|
match hint {
|
|
Some(hint) => {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::HintHits);
|
|
self.depend_on(id);
|
|
Some(hint)
|
|
}
|
|
None => {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::HintMisses);
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A child's length in the box it is about to be offered, if it can be
|
|
/// had without drawing it: from its hint, or from a drawing it already
|
|
/// has that holds for that box.
|
|
pub fn known_len<W: ?Sized>(
|
|
&mut self,
|
|
child: &StrongWidget<W>,
|
|
axis: Axis,
|
|
region: UiRegion,
|
|
) -> Option<Len> {
|
|
let declared = self.declared_lens(child);
|
|
let local = declared_box(region, declared);
|
|
let within = local.within(&self.region);
|
|
let first_ask = self.offer(child.id());
|
|
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
|
|
active.offer = local;
|
|
}
|
|
if let Some(hint) = self.size_hint(child, axis) {
|
|
return Some(hint);
|
|
}
|
|
let px = self.state.px_of(self.move_idx, within);
|
|
let (size, holds) =
|
|
self.state
|
|
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?;
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::RetainedSizeHits);
|
|
self.depend_on(child);
|
|
if first_ask {
|
|
let active = self.state.active.get_mut(&child.id()).unwrap();
|
|
active.answer = (size, holds);
|
|
}
|
|
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
|
|
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
|
}
|
|
Some(size.axis(axis))
|
|
}
|
|
|
|
/// Whether this is the first box a child is asked about in during a draw
|
|
/// that is itself in the box it was asked in -- the question a cold
|
|
/// layout asks, whose answer is the one to keep.
|
|
fn offer(&mut self, child: WidgetId) -> bool {
|
|
if !self.at_offer || self.offered.contains(&child) {
|
|
return false;
|
|
}
|
|
self.offered.push(child);
|
|
true
|
|
}
|
|
|
|
/// The pixel size of a part of the box this widget was asked in.
|
|
fn px_within_offer(&self, local: UiRegion) -> Vec2 {
|
|
let size = local.size();
|
|
Vec2::new(
|
|
size.x.to_px(self.offered_px.x),
|
|
size.y.to_px(self.offered_px.y),
|
|
)
|
|
}
|
|
|
|
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
|
if !self.size_deps.contains(&child.id()) {
|
|
self.size_deps.push(child.id());
|
|
}
|
|
}
|
|
|
|
pub fn render_text<'b>(
|
|
&mut self,
|
|
buffer: &'b mut TextBuffer,
|
|
attrs: &TextAttrs,
|
|
width: Option<f32>,
|
|
) -> &'b RenderedText {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::render_text(self.id, self.rsc.widgets().label(self.id), width);
|
|
let ui = self.rsc.ui_mut();
|
|
ui.text.render(buffer, attrs, width)
|
|
}
|
|
|
|
// TODO: merge the text methods into the primitive ones.
|
|
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
|
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
|
|
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::px(glyph.offset));
|
|
region.x.end = region.x.start + UiScalar::px(glyph.entry.width as f32);
|
|
region.y.end = region.y.start + UiScalar::px(glyph.entry.height as f32);
|
|
self.write(
|
|
kind,
|
|
GlyphPrimitive {
|
|
uv_min: glyph.entry.uv_min,
|
|
uv_max: glyph.entry.uv_max,
|
|
layer: glyph.entry.layer,
|
|
color: text.color,
|
|
flags: glyph.entry.flags(),
|
|
},
|
|
region,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// This widget's box, in the coordinates its own primitives are written
|
|
/// in -- so a region composed `within` it may be drawn directly.
|
|
pub fn region(&self) -> UiRegion {
|
|
self.region
|
|
}
|
|
|
|
/// This widget's box in pixels. Reading it makes the drawing one that
|
|
/// holds for this box only, until `holds` says how far it goes.
|
|
pub fn px_size(&mut self) -> Vec2 {
|
|
let px = self.state.px_of(self.move_idx, self.region);
|
|
for (own, len) in self.own.iter_mut().zip([px.x, px.y]) {
|
|
if *own == Holds::ANY {
|
|
*own = Holds::at(len);
|
|
}
|
|
}
|
|
px
|
|
}
|
|
|
|
/// One axis of this widget's box in pixels. Prefer this to
|
|
/// [`Self::px_size`] when the other axis cannot affect the drawing.
|
|
pub fn px_len(&mut self, axis: Axis) -> f32 {
|
|
let len = self.state.px_of(self.move_idx, self.region).axis(axis);
|
|
let own = &mut self.own[axis as usize];
|
|
if *own == Holds::ANY {
|
|
*own = Holds::at(len);
|
|
}
|
|
len
|
|
}
|
|
|
|
/// The lengths of this widget's box on `axis` that what it is drawing
|
|
/// holds for -- the same primitives, in the same fractions and offsets
|
|
/// of the box, and the same reported size. A widget that read its
|
|
/// length in pixels holds for that one alone until it says otherwise.
|
|
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
|
|
let holds = holds.into();
|
|
debug_assert!(
|
|
holds.contains(self.state.px_of(self.move_idx, self.region).axis(axis)),
|
|
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
|
|
self.label(),
|
|
self.id
|
|
);
|
|
self.own[axis as usize] = holds;
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
/// A child that has just been drawn. Reading its size records that this
|
|
/// widget's own size depends on it; dropping it without reading draws the
|
|
/// child and leaves the parent independent of what it came to.
|
|
pub struct DrawResult<'p, 'a, W: ?Sized> {
|
|
painter: &'p mut Painter<'a>,
|
|
child: &'p StrongWidget<W>,
|
|
size: Size,
|
|
}
|
|
|
|
impl<W: ?Sized> DrawResult<'_, '_, W> {
|
|
pub fn size(self) -> Size {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
diag::bump(Counter::SizeReads);
|
|
diag::size_read(self.child.id(), self.painter.id, self.size);
|
|
}
|
|
self.painter.depend_on(self.child);
|
|
self.size
|
|
}
|
|
|
|
pub fn len(self, axis: Axis) -> Len {
|
|
self.size().axis(axis)
|
|
}
|
|
}
|
|
|
|
/// What `Painter::primitive` takes: a primitive, or something that yields one
|
|
/// and does whatever else drawing it needs.
|
|
pub trait PrimitiveLike {
|
|
type Primitive: Primitive;
|
|
fn into_primitive(self, painter: &mut Painter) -> Self::Primitive;
|
|
}
|
|
|
|
impl<P: Primitive> PrimitiveLike for P {
|
|
type Primitive = P;
|
|
fn into_primitive(self, _: &mut Painter) -> P {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl PrimitiveLike for &TextureHandle {
|
|
type Primitive = TexturePrimitive;
|
|
|
|
/// Retains a share of the handle, so the slot the primitive names cannot
|
|
/// be freed and reused while it is still drawn.
|
|
fn into_primitive(self, painter: &mut Painter) -> TexturePrimitive {
|
|
painter.textures.push(self.clone());
|
|
self.into()
|
|
}
|
|
}
|
|
|
|
/// What a widget declares a length of its box to be. `leftover` is not one: a
|
|
/// share of what is left over is only a length to the widget dividing one,
|
|
/// so it passes up in the size instead.
|
|
pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<Len>; 2] {
|
|
let rules = widgets.size_rules(id);
|
|
let widget = widgets.get_dyn(id);
|
|
AXES.map(|axis| {
|
|
rules.axis(axis).declared().or_else(|| {
|
|
// A hint still narrows the box where no rule does, which is how a
|
|
// widget with a natural pixel size -- an image, a gap -- gets that
|
|
// size rather than the whole offer. That is the offer's business
|
|
// rather than a declaration's, and this falls away once a widget
|
|
// occupies its reported size inside the box it was offered.
|
|
widget
|
|
.and_then(|widget| widget.size_hint(axis))
|
|
.filter(|len| len.leftover == 0.0)
|
|
})
|
|
})
|
|
}
|
|
|
|
/// Takes a widget's declared lengths in the box `region` is given in, since a
|
|
/// fraction of a length means a fraction of that one. A caller that already
|
|
/// reserved the space hands back the same length, so this is the identity
|
|
/// for it.
|
|
fn declared_box(mut region: UiRegion, declared: [Option<Len>; 2]) -> UiRegion {
|
|
for (axis, len) in AXES.into_iter().zip(declared) {
|
|
let Some(len) = len else { continue };
|
|
let span = region.axis_mut(axis);
|
|
span.end = span.start + UiScalar::new(len.rel, len.px);
|
|
}
|
|
region
|
|
}
|