Make alignment a widget property
This commit is contained in:
1 parent
8220a78d4a
commit
d3b0ebf90c
21 files changed
+823
-300
No files matched your search
@@ -40,6 +40,7 @@ pub(crate) enum Counter {
|
||||
ReuseWrongParent,
|
||||
ReuseRemapped,
|
||||
ReuseOutside,
|
||||
PlaceRedraws,
|
||||
QueuePops,
|
||||
DepthReads,
|
||||
LocalRedraws,
|
||||
@@ -72,6 +73,7 @@ impl Counter {
|
||||
"reuse: wrong parent",
|
||||
"reuse remapped",
|
||||
"reuse: outside what it holds for",
|
||||
"placed by redrawing",
|
||||
"redraw queue pops",
|
||||
"depth reads",
|
||||
"local redraws",
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::vec2;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub struct Align {
|
||||
pub x: Option<AxisAlign>,
|
||||
pub y: Option<AxisAlign>,
|
||||
@@ -30,20 +30,30 @@ impl Align {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AxisAlign {
|
||||
Neg,
|
||||
Center,
|
||||
Pos,
|
||||
}
|
||||
/// Where a widget sits in a box longer than it is. The default is the middle,
|
||||
/// because the two edges are the ones that assume a direction: which of them
|
||||
/// is the near one depends on the writing system and on which way a container
|
||||
/// runs, and the middle is the same either way.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct AxisAlign(f32);
|
||||
|
||||
impl AxisAlign {
|
||||
pub const NEG: Self = Self::new(0.0);
|
||||
pub const CENTER: Self = Self::new(0.5);
|
||||
pub const POS: Self = Self::new(1.0);
|
||||
|
||||
pub const fn new(rel: f32) -> Self {
|
||||
Self(rel)
|
||||
}
|
||||
|
||||
pub const fn rel(&self) -> f32 {
|
||||
match self {
|
||||
Self::Neg => 0.0,
|
||||
Self::Center => 0.5,
|
||||
Self::Pos => 1.0,
|
||||
}
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AxisAlign {
|
||||
fn default() -> Self {
|
||||
Self::CENTER
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,34 +63,57 @@ pub struct CardinalAlign {
|
||||
}
|
||||
|
||||
impl CardinalAlign {
|
||||
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::Neg);
|
||||
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::Center);
|
||||
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::Pos);
|
||||
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::Neg);
|
||||
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::Center);
|
||||
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::Pos);
|
||||
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::NEG);
|
||||
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::CENTER);
|
||||
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::POS);
|
||||
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::NEG);
|
||||
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::CENTER);
|
||||
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::POS);
|
||||
|
||||
pub const fn new(axis: Axis, align: AxisAlign) -> Self {
|
||||
Self { axis, align }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct RegionAlign {
|
||||
pub x: AxisAlign,
|
||||
pub y: AxisAlign,
|
||||
}
|
||||
|
||||
impl RegionAlign {
|
||||
pub const TOP_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Neg);
|
||||
pub const TOP_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Neg);
|
||||
pub const TOP_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Neg);
|
||||
pub const CENTER_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Center);
|
||||
pub const CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Center);
|
||||
pub const CENTER_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Center);
|
||||
pub const BOT_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Pos);
|
||||
pub const BOT_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Pos);
|
||||
pub const BOT_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Pos);
|
||||
/// Both axes at the near edge. What a container passes as an override for
|
||||
/// a child it is going to position itself.
|
||||
pub const NEAR: Self = Self {
|
||||
x: AxisAlign::NEG,
|
||||
y: AxisAlign::NEG,
|
||||
};
|
||||
|
||||
pub fn axis(&self, axis: Axis) -> AxisAlign {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut AxisAlign {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RegionAlign {
|
||||
pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG);
|
||||
pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG);
|
||||
pub const TOP_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::NEG);
|
||||
pub const CENTER_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::CENTER);
|
||||
pub const CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::CENTER);
|
||||
pub const CENTER_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::CENTER);
|
||||
pub const BOT_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::POS);
|
||||
pub const BOT_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::POS);
|
||||
pub const BOT_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::POS);
|
||||
|
||||
pub const fn new(x: AxisAlign, y: AxisAlign) -> Self {
|
||||
Self { x, y }
|
||||
@@ -165,8 +198,8 @@ impl From<RegionAlign> for Align {
|
||||
impl From<Align> for RegionAlign {
|
||||
fn from(align: Align) -> Self {
|
||||
Self {
|
||||
x: align.x.unwrap_or(AxisAlign::Center),
|
||||
y: align.y.unwrap_or(AxisAlign::Center),
|
||||
x: align.x.unwrap_or(AxisAlign::CENTER),
|
||||
y: align.y.unwrap_or(AxisAlign::CENTER),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,15 @@ impl UiScalar {
|
||||
}
|
||||
}
|
||||
|
||||
/// Both channels by the same factor, which is what a fraction of a
|
||||
/// length means when the length is part pixels and part a share.
|
||||
pub const fn scale(&self, by: f32) -> Self {
|
||||
Self {
|
||||
rel: self.rel * by,
|
||||
px: self.px * by,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn offset(mut self, amt: f32) -> Self {
|
||||
self.px += amt;
|
||||
self
|
||||
|
||||
+13
-2
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
Holds, LayerId, Len, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId,
|
||||
Holds, LayerId, Len, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle,
|
||||
UiRegion, WidgetId,
|
||||
};
|
||||
|
||||
/// What is kept of a widget its parent has asked about. `drawn` says whether
|
||||
@@ -8,6 +9,7 @@ use crate::{
|
||||
#[derive(Debug)]
|
||||
pub struct ActiveData {
|
||||
pub id: WidgetId,
|
||||
/// The box its drawing is in, in `parent_move`'s coordinates.
|
||||
pub region: UiRegion,
|
||||
/// The box its parent first asked about it in, as a part of the box the
|
||||
/// parent was itself asked in. Any later box it was given was decided
|
||||
@@ -18,7 +20,7 @@ pub struct ActiveData {
|
||||
pub answer: (Size, [Holds; 2]),
|
||||
/// What the widget said it used of its box, the last time it drew.
|
||||
pub size: Size,
|
||||
/// The pixel lengths of its box, per axis, that its drawing and `size`
|
||||
/// The pixel lengths of `region`, per axis, that its drawing and `size`
|
||||
/// hold for.
|
||||
pub holds: [Holds; 2],
|
||||
pub drawn: bool,
|
||||
@@ -39,6 +41,15 @@ pub struct ActiveData {
|
||||
/// A change to one moves a box this widget cannot fix by drawing again,
|
||||
/// and comparing them is what says so.
|
||||
pub declared: [Option<Len>; 2],
|
||||
/// The alignment its parent asked it with. A local redraw repeats that
|
||||
/// question, including an override chosen by a container.
|
||||
pub align: RegionAlign,
|
||||
/// Whether that alignment was the parent's override rather than the
|
||||
/// widget's own property.
|
||||
pub align_override: bool,
|
||||
/// Its own alignment when it was last drawn. A change to the property is
|
||||
/// found against this even when its parent overrode the alignment.
|
||||
pub own_align: RegionAlign,
|
||||
/// The movable region whose coordinates `region` uses.
|
||||
pub parent_move: MoveIdx,
|
||||
pub mask: MaskIdx,
|
||||
|
||||
+99
-28
@@ -1,8 +1,8 @@
|
||||
#[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,
|
||||
Axis, Holds, Len, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer,
|
||||
TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, Widgets,
|
||||
render::{
|
||||
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
||||
PrimitiveKind, TexturePrimitive,
|
||||
@@ -99,16 +99,7 @@ impl<'a> Painter<'a> {
|
||||
|
||||
/// 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)
|
||||
self.widget_within(id, UiRegion::FULL)
|
||||
}
|
||||
|
||||
/// What a widget's rules declare its lengths to be, which whoever draws
|
||||
@@ -127,19 +118,45 @@ impl<'a> Painter<'a> {
|
||||
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>(
|
||||
/// Draws a widget somewhere within this one. `region` is in this widget's
|
||||
/// own coordinates, and the child's declared lengths are still to be
|
||||
/// taken from it. Where the child's drawing sits inside what it is given
|
||||
/// is the child's alignment, applied where the child is drawn, so a
|
||||
/// container positions a child either by handing it a box of exactly its
|
||||
/// length or by leaving it room and letting its alignment decide.
|
||||
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, None)
|
||||
}
|
||||
|
||||
/// Draws a widget with an alignment chosen by its container rather than
|
||||
/// the widget's property. Containers use this when the box they hand down
|
||||
/// already expresses the size they report around the child.
|
||||
pub fn widget_aligned<'s, W: ?Sized>(
|
||||
&'s mut self,
|
||||
id: &'s StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
align: RegionAlign,
|
||||
) -> DrawResult<'s, 'a, W> {
|
||||
self.widget_at(id, region, Some(align))
|
||||
}
|
||||
|
||||
fn widget_at<'s, W: ?Sized>(
|
||||
&'s mut self,
|
||||
id: &'s StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
align_override: Option<RegionAlign>,
|
||||
) -> DrawResult<'s, 'a, W> {
|
||||
let region_node = self.rsc.widgets().is_region_node(id.id());
|
||||
let declared = self.declared_lens(id);
|
||||
let align = align_override.unwrap_or_else(|| self.rsc.widgets().alignment(id.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),
|
||||
true => declared_box(region, declared, align),
|
||||
false => region,
|
||||
};
|
||||
let within = match local == UiRegion::FULL {
|
||||
@@ -161,7 +178,10 @@ impl<'a> Painter<'a> {
|
||||
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(
|
||||
// The answer and what it holds for, both about the box asked in. The
|
||||
// child's record may say something else once its drawing has been
|
||||
// placed: a drawing made again in its placed box holds for that box.
|
||||
let (size, holds) = self.state.draw_inner(
|
||||
id.id(),
|
||||
within,
|
||||
DrawInfo {
|
||||
@@ -173,19 +193,18 @@ impl<'a> Painter<'a> {
|
||||
mask: self.mask,
|
||||
offer,
|
||||
offered_px: self.px_within_offer(offer),
|
||||
align: align_override,
|
||||
},
|
||||
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);
|
||||
self.state.active.get_mut(&id.id()).unwrap().answer = (size, holds);
|
||||
}
|
||||
// Whatever the child's drawing holds for keeps this one to the boxes
|
||||
// Whatever the child's answer 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()));
|
||||
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
||||
}
|
||||
DrawResult {
|
||||
child: id,
|
||||
@@ -232,7 +251,8 @@ impl<'a> Painter<'a> {
|
||||
region: UiRegion,
|
||||
) -> Option<Len> {
|
||||
let declared = self.declared_lens(child);
|
||||
let local = declared_box(region, declared);
|
||||
let align = self.rsc.widgets().alignment(child.id());
|
||||
let local = declared_box(region, declared, align);
|
||||
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()) {
|
||||
@@ -326,6 +346,20 @@ impl<'a> Painter<'a> {
|
||||
self.region
|
||||
}
|
||||
|
||||
/// Where this widget sits in a box longer than the length it takes. A
|
||||
/// widget that positions its own content reads it to place that content
|
||||
/// the way the box around it would have placed the widget.
|
||||
pub fn alignment(&self) -> RegionAlign {
|
||||
self.rsc.widgets().alignment(self.id)
|
||||
}
|
||||
|
||||
/// The part of this widget's box that something of `size` takes, at the
|
||||
/// near edge. A container that reports one child's size gives every child
|
||||
/// this, so what it draws is inside what it says it occupies.
|
||||
pub fn box_of(&self, size: Size) -> UiRegion {
|
||||
placed_box(UiRegion::FULL, size, RegionAlign::NEAR, [None; 2])
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -455,15 +489,52 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<Len>; 2]
|
||||
})
|
||||
}
|
||||
|
||||
/// The box a drawing occupies: the size the widget reported, on the side of
|
||||
/// the box it was asked in that its alignment says. An axis reported as a
|
||||
/// share fills, because a share is a length only to whoever divides one, and
|
||||
/// whoever did is the one that handed down this box. A declared axis is
|
||||
/// left alone too: `declared_box` already placed it, in the parent's box,
|
||||
/// and the rule's length is what the widget reports there.
|
||||
///
|
||||
/// A reported fraction is a fraction of the box the widget drew in, where a
|
||||
/// declared one is a fraction of the box its parent handed down -- a span
|
||||
/// reporting `rel(1.0)` means all of what it was given, whatever that was a
|
||||
/// fraction of. So this scales by the box rather than composing into it.
|
||||
pub(crate) fn placed_box(
|
||||
region: UiRegion,
|
||||
size: Size,
|
||||
align: RegionAlign,
|
||||
declared: [Option<Len>; 2],
|
||||
) -> UiRegion {
|
||||
let mut placed = region;
|
||||
for (axis, declared) in AXES.into_iter().zip(declared) {
|
||||
let reported = size.axis(axis);
|
||||
if reported.leftover != 0.0 || declared.is_some() {
|
||||
continue;
|
||||
}
|
||||
let span = placed.axis_mut(axis);
|
||||
let len = span.len().scale(reported.rel) + UiScalar::px(reported.px);
|
||||
span.start += (span.len() - len).scale(align.axis(axis).rel());
|
||||
span.end = span.start + len;
|
||||
}
|
||||
placed
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// fraction of a length means a fraction of that one, and puts what is left
|
||||
/// over on the side its alignment says. A caller that already reserved the
|
||||
/// space hands back the same length, so this is the identity for it.
|
||||
pub(crate) fn declared_box(
|
||||
mut region: UiRegion,
|
||||
declared: [Option<Len>; 2],
|
||||
align: RegionAlign,
|
||||
) -> 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);
|
||||
let len = UiScalar::new(len.rel, len.px);
|
||||
span.start += (span.len() - len).scale(align.axis(axis).rel());
|
||||
span.end = span.start + len;
|
||||
}
|
||||
region
|
||||
}
|
||||
+221
-55
@@ -1,9 +1,10 @@
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
||||
use crate::ui::painter::declared_lens;
|
||||
use crate::ui::painter::{declared_box, declared_lens, placed_box};
|
||||
use crate::{
|
||||
ActiveData, Axis, DrawLayers, Holds, IdLike, Len, MaskIdx, MoveIdx, Moves, Painter,
|
||||
PixelRegion, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId, Widgets,
|
||||
PixelRegion, RegionAlign, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId,
|
||||
Widgets,
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
|
||||
@@ -23,6 +24,9 @@ pub(super) struct DrawInfo {
|
||||
/// that box in pixels.
|
||||
pub offer: UiRegion,
|
||||
pub offered_px: Vec2,
|
||||
/// A container's answer for where the widget sits. `None` uses the
|
||||
/// widget's own property.
|
||||
pub align: Option<RegionAlign>,
|
||||
}
|
||||
|
||||
pub struct UiRenderState {
|
||||
@@ -39,6 +43,13 @@ pub struct UiRenderState {
|
||||
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
|
||||
/// replaces that while its children go on pointing at the slot.
|
||||
slots: HashMap<WidgetId, MoveIdx>,
|
||||
/// Answers invalidated by a declared-length change below them. These are
|
||||
/// replaced even when retained placement means the redraw is not at the
|
||||
/// old offer.
|
||||
answer_invalid: crate::util::HashSet<WidgetId>,
|
||||
/// Whether this frame contains a declared-length change, so any dirty
|
||||
/// dependent replaces its answer too.
|
||||
replace_answers: bool,
|
||||
pub moves: Moves,
|
||||
}
|
||||
|
||||
@@ -50,6 +61,8 @@ impl UiRenderState {
|
||||
output_size: Vec2::ZERO,
|
||||
old_root: None,
|
||||
slots: Default::default(),
|
||||
answer_invalid: Default::default(),
|
||||
replace_answers: false,
|
||||
moves: Default::default(),
|
||||
root_move: MoveIdx::NONE,
|
||||
resized: false,
|
||||
@@ -91,6 +104,7 @@ impl UiRenderState {
|
||||
mask: MaskIdx::NONE,
|
||||
offer: UiRegion::FULL,
|
||||
offered_px: self.output_size,
|
||||
align: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,12 +145,15 @@ impl UiRenderState {
|
||||
// anything dirty settles, so that whatever a new output draws
|
||||
// again is drawn once, in the box it will have.
|
||||
let info = self.root_info();
|
||||
self.draw_inner(root.id(), UiRegion::FULL, info, None, rsc);
|
||||
let region = Self::root_region(root.id(), rsc.widgets());
|
||||
let answer = self.draw_inner(root.id(), region, info, None, rsc);
|
||||
self.active.get_mut(&root.id()).unwrap().answer = answer;
|
||||
}
|
||||
self.resized = false;
|
||||
if rsc.widgets().has_updates() {
|
||||
self.redraw_updates(rsc);
|
||||
}
|
||||
self.replace_answers = false;
|
||||
self.free(rsc);
|
||||
}
|
||||
|
||||
@@ -148,10 +165,19 @@ impl UiRenderState {
|
||||
self.write_root();
|
||||
if let Some(id) = root {
|
||||
let info = self.root_info();
|
||||
self.draw_inner(id.id(), UiRegion::FULL, info, None, rsc);
|
||||
let region = Self::root_region(id.id(), rsc.widgets());
|
||||
self.draw_inner(id.id(), region, info, None, rsc);
|
||||
}
|
||||
}
|
||||
|
||||
fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion {
|
||||
declared_box(
|
||||
UiRegion::FULL,
|
||||
declared_lens(widgets, id),
|
||||
widgets.alignment(id),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn draw_inner(
|
||||
&mut self,
|
||||
id: WidgetId,
|
||||
@@ -159,7 +185,7 @@ impl UiRenderState {
|
||||
info: DrawInfo,
|
||||
mut old: Option<ActiveData>,
|
||||
rsc: &mut dyn UiRsc,
|
||||
) -> Size {
|
||||
) -> (Size, [Holds; 2]) {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
{
|
||||
diag::bump(Counter::DrawRequests);
|
||||
@@ -171,15 +197,82 @@ impl UiRenderState {
|
||||
info.region_node,
|
||||
);
|
||||
}
|
||||
if self.active.contains_key(&id) {
|
||||
if let Some(size) = self.try_reuse(id, region, info, rsc) {
|
||||
return size;
|
||||
let own_align = rsc.widgets().alignment(id);
|
||||
let align = info.align.unwrap_or(own_align);
|
||||
let replace_answer = self.answer_invalid.remove(&id)
|
||||
|| (self.replace_answers
|
||||
&& (rsc.widgets().needs_redraw.contains(&id)
|
||||
|| self.dirty_size_under(id, rsc.widgets())));
|
||||
let retained = match replace_answer {
|
||||
true => None,
|
||||
false => self
|
||||
.retained_answer(id, region, info, rsc.widgets())
|
||||
.or_else(|| self.try_reuse(id, region, info, rsc)),
|
||||
};
|
||||
let answer = retained.unwrap_or_else(|| {
|
||||
if old.is_none() {
|
||||
old = self.remove(id, false, rsc);
|
||||
}
|
||||
// if not, then maintain resize and track old children to remove unneeded
|
||||
old = self.remove(id, false, rsc);
|
||||
self.draw_at(id, region, info, align, old.take(), rsc)
|
||||
});
|
||||
|
||||
let declared = declared_lens(rsc.widgets(), id);
|
||||
// A near-edge override means the caller already chose this box from
|
||||
// the child's answer. Applying the answer again would compound the
|
||||
// placement; it is also how the second, final ask terminates.
|
||||
let placed = match info.align == Some(RegionAlign::NEAR) {
|
||||
true => region,
|
||||
false => placed_box(region, answer.0, align, declared),
|
||||
};
|
||||
let placed_info = DrawInfo {
|
||||
align: Some(RegionAlign::NEAR),
|
||||
..info
|
||||
};
|
||||
// The symbolic box can be unchanged while its parent slot changed
|
||||
// pixel size. Reuse checks the resolved box even in that case.
|
||||
if self.try_reuse(id, placed, placed_info, rsc).is_none() {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::PlaceRedraws);
|
||||
let old = self.remove(id, false, rsc);
|
||||
self.draw_at(id, placed, placed_info, RegionAlign::NEAR, old, rsc);
|
||||
}
|
||||
|
||||
// draw widget
|
||||
// The answer is only reusable while both parts of the operation are:
|
||||
// what the widget reported in the offered box, and what it drew in
|
||||
// the box its report selected. Express the latter's contract back in
|
||||
// terms of the offered box before handing it to the parent.
|
||||
let drawing_holds = self.active[&id].holds;
|
||||
let mut settled = answer;
|
||||
for axis in AXES {
|
||||
let reported = answer.0.axis(axis);
|
||||
let placed_len = match reported.leftover != 0.0 || declared[axis as usize].is_some() {
|
||||
true => UiScalar::FULL,
|
||||
false => UiScalar::new(reported.rel, reported.px),
|
||||
};
|
||||
settled.1[axis as usize] =
|
||||
settled.1[axis as usize].and(drawing_holds[axis as usize].through(placed_len));
|
||||
}
|
||||
|
||||
let active = self.active.get_mut(&id).unwrap();
|
||||
active.offer = info.offer;
|
||||
active.answer = settled;
|
||||
active.align = align;
|
||||
active.align_override = info.align.is_some();
|
||||
active.own_align = own_align;
|
||||
active.depth = info.depth;
|
||||
settled
|
||||
}
|
||||
|
||||
/// Calls a widget's `draw` and keeps what it drew in `region`.
|
||||
fn draw_at(
|
||||
&mut self,
|
||||
id: WidgetId,
|
||||
region: UiRegion,
|
||||
info: DrawInfo,
|
||||
align: RegionAlign,
|
||||
old: Option<ActiveData>,
|
||||
rsc: &mut dyn UiRsc,
|
||||
) -> (Size, [Holds; 2]) {
|
||||
let (move_idx, local, retired_move) = match info.region_node {
|
||||
// Its box becomes its movable region, so it draws in that
|
||||
// region's coordinates and its box is one entry to rewrite.
|
||||
@@ -295,6 +388,7 @@ impl UiRenderState {
|
||||
mask,
|
||||
offer: UiRegion::FULL,
|
||||
offered_px: px,
|
||||
align: None,
|
||||
},
|
||||
rsc,
|
||||
);
|
||||
@@ -302,7 +396,6 @@ impl UiRenderState {
|
||||
}
|
||||
}
|
||||
|
||||
// add to active
|
||||
let active = ActiveData {
|
||||
id,
|
||||
region,
|
||||
@@ -318,8 +411,10 @@ impl UiRenderState {
|
||||
primitives,
|
||||
children,
|
||||
size_deps,
|
||||
// Written by whoever draws it, which is what resolves them.
|
||||
declared: [None; 2],
|
||||
declared: declared_lens(rsc.widgets(), id),
|
||||
align,
|
||||
align_override: info.align.is_some(),
|
||||
own_align: rsc.widgets().alignment(id),
|
||||
move_idx,
|
||||
parent_move: info.parent_move,
|
||||
mask,
|
||||
@@ -327,7 +422,7 @@ impl UiRenderState {
|
||||
};
|
||||
rsc.on_draw(&active);
|
||||
self.active.insert(id, active);
|
||||
size
|
||||
(size, holds)
|
||||
}
|
||||
|
||||
/// Keeps a region node's entry across redraws because descendants retain
|
||||
@@ -358,9 +453,9 @@ impl UiRenderState {
|
||||
.to_px(self.output_size)
|
||||
}
|
||||
|
||||
/// A clean, drawn widget's retained size, if its drawing holds for a box
|
||||
/// of `px`. This observes the answer only; it does not move or otherwise
|
||||
/// reuse the widget's drawing.
|
||||
/// A clean widget's retained answer, if that answer holds for a box of
|
||||
/// `px`. This does not move its drawing, which may already be in the box
|
||||
/// that answer placed it in.
|
||||
pub(super) fn retained_size(
|
||||
&self,
|
||||
id: WidgetId,
|
||||
@@ -372,8 +467,38 @@ impl UiRenderState {
|
||||
return None;
|
||||
}
|
||||
let active = self.active.get(&id)?;
|
||||
let valid = active.drawn && active.parent_move == parent_move && active.holds_at(px);
|
||||
valid.then_some((active.size, active.holds))
|
||||
let (size, holds) = active.answer;
|
||||
let valid = active.drawn
|
||||
&& active.parent_move == parent_move
|
||||
&& holds[0].contains(px.x)
|
||||
&& holds[1].contains(px.y);
|
||||
valid.then_some((size, holds))
|
||||
}
|
||||
|
||||
/// The answer to an ask can be retained independently of where its
|
||||
/// drawing ended up. Alignment is exactly that case: the first box is the
|
||||
/// question and the smaller placed box holds the drawing.
|
||||
fn retained_answer(
|
||||
&self,
|
||||
id: WidgetId,
|
||||
region: UiRegion,
|
||||
info: DrawInfo,
|
||||
widgets: &Widgets,
|
||||
) -> Option<(Size, [Holds; 2])> {
|
||||
if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) {
|
||||
return None;
|
||||
}
|
||||
let active = self.active.get(&id)?;
|
||||
let has_region_node = active.move_idx != active.parent_move;
|
||||
if !active.drawn
|
||||
|| has_region_node != info.region_node
|
||||
|| active.parent_move != info.parent_move
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let px = self.px_of(info.parent_move, region);
|
||||
let (size, holds) = active.answer;
|
||||
(holds[0].contains(px.x) && holds[1].contains(px.y)).then_some((size, holds))
|
||||
}
|
||||
|
||||
/// Whether anything whose size this widget's own size was read from is
|
||||
@@ -388,29 +513,42 @@ impl UiRenderState {
|
||||
})
|
||||
}
|
||||
|
||||
/// The pixel size of the box a widget was first asked about in, composed
|
||||
/// through the boxes its ancestors were asked in.
|
||||
fn offered_px(&self, id: WidgetId) -> Vec2 {
|
||||
let Some(active) = self.active.get(&id) else {
|
||||
return self.output_size;
|
||||
/// The first box a widget was asked about, re-expressed in the coordinate
|
||||
/// space its drawing uses. Keeping the relative box and composing it
|
||||
/// again avoids rebuilding a shifted box from rounded pixel lengths.
|
||||
fn offered_region(&self, id: WidgetId) -> UiRegion {
|
||||
let active = &self.active[&id];
|
||||
let parent_region = match active.parent.and_then(|id| self.active.get(&id)) {
|
||||
Some(parent) if parent.move_idx == active.parent_move => {
|
||||
if parent.move_idx == parent.parent_move {
|
||||
self.offered_region(parent.id)
|
||||
} else {
|
||||
UiRegion::FULL
|
||||
}
|
||||
}
|
||||
_ => UiRegion::FULL,
|
||||
};
|
||||
let parent = match active.parent {
|
||||
Some(parent) => self.offered_px(parent),
|
||||
None => self.output_size,
|
||||
let mut offered = match active.offer == UiRegion::FULL {
|
||||
true => parent_region,
|
||||
false => active.offer.within(&parent_region),
|
||||
};
|
||||
let size = active.offer.size();
|
||||
Vec2::new(size.x.to_px(parent.x), size.y.to_px(parent.y))
|
||||
for axis in AXES {
|
||||
if active.declared[axis as usize].is_some() {
|
||||
*offered.axis_mut(axis) = *active.region.axis(axis);
|
||||
}
|
||||
}
|
||||
offered
|
||||
}
|
||||
|
||||
/// The drawing a widget already has, kept for a new box if it holds for
|
||||
/// that box.
|
||||
/// Reuses the actual drawing in a new box if its retained contract holds
|
||||
/// there. Answers retained from a different ask are handled separately.
|
||||
fn try_reuse(
|
||||
&mut self,
|
||||
id: WidgetId,
|
||||
region: UiRegion,
|
||||
info: DrawInfo,
|
||||
rsc: &mut dyn UiRsc,
|
||||
) -> Option<Size> {
|
||||
) -> Option<(Size, [Holds; 2])> {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::ReuseAttempts);
|
||||
if rsc.widgets().needs_redraw.contains(&id) {
|
||||
@@ -453,8 +591,12 @@ impl UiRenderState {
|
||||
return None;
|
||||
}
|
||||
let moved = active.region != region;
|
||||
let (size, old_region, slot, mask) =
|
||||
(active.size, active.region, active.move_idx, info.mask);
|
||||
let (answer, old_region, slot, mask) = (
|
||||
(active.size, active.holds),
|
||||
active.region,
|
||||
active.move_idx,
|
||||
info.mask,
|
||||
);
|
||||
if moved {
|
||||
if has_region_node {
|
||||
self.moves.set(slot, region);
|
||||
@@ -487,7 +629,7 @@ impl UiRenderState {
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(size)
|
||||
Some(answer)
|
||||
}
|
||||
|
||||
/// Re-expresses an ordinary retained subtree in a new parent region.
|
||||
@@ -610,6 +752,9 @@ impl UiRenderState {
|
||||
size_deps: Vec::new(),
|
||||
move_idx: info.parent_move,
|
||||
declared: [None; 2],
|
||||
align: RegionAlign::default(),
|
||||
align_override: false,
|
||||
own_align: rsc.widgets().alignment(id),
|
||||
parent_move: info.parent_move,
|
||||
mask: info.mask,
|
||||
layer: info.layer,
|
||||
@@ -624,6 +769,8 @@ impl UiRenderState {
|
||||
}
|
||||
}
|
||||
self.slots.clear();
|
||||
self.answer_invalid.clear();
|
||||
self.replace_answers = false;
|
||||
self.moves.clear();
|
||||
self.root_move = MoveIdx::NONE;
|
||||
self.layers.clear();
|
||||
@@ -638,6 +785,7 @@ impl UiRenderState {
|
||||
rsc.on_remove(id);
|
||||
self.remove(id, true, rsc);
|
||||
self.drop_slot(id);
|
||||
self.answer_invalid.remove(&id);
|
||||
}
|
||||
rsc.ui_mut().textures.free();
|
||||
}
|
||||
@@ -748,9 +896,19 @@ impl UiRenderState {
|
||||
// to draw -- with the mark left on, so the parent draws it rather
|
||||
// than keeping it.
|
||||
let declared_changed = declared_lens(rsc.widgets(), id) != active.declared;
|
||||
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
|
||||
if let Some(parent) = active.parent
|
||||
&& (declared_changed || !active.drawn)
|
||||
&& (declared_changed || alignment_changed || !active.drawn)
|
||||
{
|
||||
if declared_changed {
|
||||
self.replace_answers = true;
|
||||
let mut at = Some(id);
|
||||
while let Some(next) = at {
|
||||
self.answer_invalid.insert(next);
|
||||
rsc.widgets_mut().needs_redraw.insert(next);
|
||||
at = self.active[&next].parent;
|
||||
}
|
||||
}
|
||||
rsc.widgets_mut().needs_redraw.insert(id);
|
||||
self.redraw(parent, rsc);
|
||||
// Whatever the parent did not draw again is nothing it holds now.
|
||||
@@ -762,15 +920,27 @@ impl UiRenderState {
|
||||
}
|
||||
let region = active.region;
|
||||
let region_node = active.parent.is_some() && rsc.widgets().is_region_node(id);
|
||||
let offered_px = self.offered_px(id);
|
||||
let asked_in = match active.parent {
|
||||
Some(_) => self.offered_region(id),
|
||||
None => Self::root_region(id, rsc.widgets()),
|
||||
};
|
||||
let offered_px = self.px_of(active.parent_move, asked_in);
|
||||
let at_offer = same_px(self.px_of(active.parent_move, region), offered_px);
|
||||
// Asked again where its parent asked: a box decided from its own
|
||||
// answer gives that answer back whatever the content now says. Only
|
||||
// a region node can be redrawn away from its current box without
|
||||
// first involving the parent that chose that box.
|
||||
let parent_must_place = active.parent.is_some()
|
||||
&& (!region_node || active.align_override)
|
||||
&& !same_pixel_region(
|
||||
self.moves
|
||||
.resolve(active.parent_move, region)
|
||||
.to_px(self.output_size),
|
||||
self.moves
|
||||
.resolve(active.parent_move, asked_in)
|
||||
.to_px(self.output_size),
|
||||
);
|
||||
// An independently positioned region node can redraw at its offer
|
||||
// and move its slot to its own placement. Every other widget needs
|
||||
// its parent to reproduce a different final position.
|
||||
if let Some(parent) = active.parent
|
||||
&& !region_node
|
||||
&& !at_offer
|
||||
&& parent_must_place
|
||||
{
|
||||
rsc.widgets_mut().needs_redraw.insert(id);
|
||||
self.redraw(parent, rsc);
|
||||
@@ -786,23 +956,15 @@ impl UiRenderState {
|
||||
mask: active.mask,
|
||||
offer: active.offer,
|
||||
offered_px,
|
||||
align: active.align_override.then_some(active.align),
|
||||
};
|
||||
let (was_answer, was) = (active.answer, (active.size, active.holds));
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::LocalRedraws);
|
||||
|
||||
let mut asked_in = region;
|
||||
if !at_offer {
|
||||
for axis in AXES {
|
||||
let span = asked_in.axis_mut(axis);
|
||||
span.end = span.start + UiScalar::px(offered_px.axis(axis));
|
||||
}
|
||||
}
|
||||
let old = self.remove(id, false, rsc);
|
||||
let size = self.draw_inner(id, asked_in, info, old, rsc);
|
||||
let active = self.active.get_mut(&id).unwrap();
|
||||
let answer = (size, active.holds);
|
||||
active.answer = answer;
|
||||
let answer = self.draw_inner(id, asked_in, info, old, rsc);
|
||||
self.active.get_mut(&id).unwrap().answer = answer;
|
||||
let Some(parent) = info.parent else {
|
||||
return;
|
||||
};
|
||||
@@ -834,6 +996,10 @@ fn same_px(a: Vec2, b: Vec2) -> bool {
|
||||
Holds::at(a.x).contains(b.x) && Holds::at(a.y).contains(b.y)
|
||||
}
|
||||
|
||||
fn same_pixel_region(a: PixelRegion, b: PixelRegion) -> bool {
|
||||
same_px(a.top_left, b.top_left) && same_px(a.bot_right, b.bot_right)
|
||||
}
|
||||
|
||||
/// A retained region rewritten from one parent box into another. A fixed
|
||||
/// source extent can be translated but cannot recover fractions for a resize.
|
||||
#[derive(Clone, Copy)]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::{SizeRules, Widget};
|
||||
use crate::{RegionAlign, SizeRules, Widget};
|
||||
|
||||
pub struct WidgetData {
|
||||
pub widget: Box<dyn Widget>,
|
||||
pub label: String,
|
||||
pub(super) region_node: bool,
|
||||
pub(super) size: SizeRules,
|
||||
pub(super) align: RegionAlign,
|
||||
/// dynamic borrow checking
|
||||
pub borrowed: bool,
|
||||
}
|
||||
@@ -20,6 +21,7 @@ impl WidgetData {
|
||||
label,
|
||||
region_node: false,
|
||||
size: SizeRules::default(),
|
||||
align: RegionAlign::default(),
|
||||
borrowed: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
|
||||
use crate::{
|
||||
Axis, IdLike, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
|
||||
Axis, AxisAlign, IdLike, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget,
|
||||
WidgetData, WidgetId,
|
||||
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
||||
};
|
||||
|
||||
@@ -136,6 +137,23 @@ impl Widgets {
|
||||
self.needs_redraw.insert(id);
|
||||
}
|
||||
|
||||
/// Where this widget sits in a box longer than the length it takes.
|
||||
pub fn alignment(&self, id: impl IdLike) -> RegionAlign {
|
||||
self.data(id).unwrap().align
|
||||
}
|
||||
|
||||
/// Sets one axis's alignment. Which box a widget ends up in is its
|
||||
/// parent's to decide, so this is escalated the way a length rule is.
|
||||
pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) {
|
||||
let id = id.id();
|
||||
let data = self.data_mut(id).unwrap();
|
||||
if *data.align.axis_mut(axis) == align {
|
||||
return;
|
||||
}
|
||||
*data.align.axis_mut(axis) = align;
|
||||
self.needs_redraw.insert(id);
|
||||
}
|
||||
|
||||
/// Both axes at once, for a caller holding a pair.
|
||||
pub fn set_size_rules(
|
||||
&mut self,
|
||||
|
||||
Reference in new issue
Block a user