Say when a drawing cannot be taken out of its box, rather than guessing
`lerp_inv` returns `Option`. Inverting a lerp over a range of zero length has no one answer, and `div_or`'s fallback picked one: the start of the range. Measured, that is not a wrong number so much as a plausible one -- taking a part out of a box fixed at the top of the window hands back exactly what went in, and out of a box fixed at the middle hands back the parent's own `rel` of 0.5 as if it were the child's fraction. Either way the caller cannot tell that nothing was recovered, which is the defect; the previous commit's claim that it "returns a rel of 0" is right only for the first case. `UiScalar::outside` and `UiSpan::outside` follow it to `Option`, and `Remap` is the answer at the region level: `new` says whether a drawing in one box can be put in another and `apply` then cannot fail, so `try_reuse` asks once for a whole subtree and `reusable` goes back to reporting only what the widget claims. That replaces the predicate the last commit put in `reusable`, which stated the same rule in a second place. `DivOr` existed only for the fallback and is gone, along with `UiRegion::outside` and `UiVec2::outside`, which had no callers once `Remap` owned the operation. `UiRegion::axis` took `&mut self` to return a shared reference; `Remap::new` needs it on a shared one. This does not redraw less. `Remap::new` refuses exactly what the predicate refused; what it buys is one statement of the rule and a remap that cannot half-apply. Counted on a resize, with the old wipe-everything for comparison: 20 padded rows, 101 widgets 101 draws -> 0 the `tabs` example 73 draws -> 0 the `text` example 49 draws -> 48 So the saving is whole where a resize does not change any widget's size, and nil in `text`, where both paragraphs rewrap to a different height and the relayout that forces reaches the root. The last commit's message oversold that case. Checked: fmt, clippy and 35 tests. `tabs` (with the image replay), `view` and `minimal` still byte-identical to `upstream/main`, `text` unchanged, and the live sway resize round trip still matches a cold start at each size.
This commit is contained in:
1 parent
984f482a7f
commit
53e61289f5
5 files changed
+106
-77
No files matched your search
+54
-22
@@ -56,13 +56,6 @@ impl UiVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn outside(&self, region: &UiRegion) -> UiVec2 {
|
|
||||||
UiVec2 {
|
|
||||||
x: self.x.outside(®ion.x),
|
|
||||||
y: self.y.outside(®ion.y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar {
|
pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => &mut self.x,
|
Axis::X => &mut self.x,
|
||||||
@@ -209,10 +202,12 @@ impl UiScalar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn outside(&self, span: &UiSpan) -> Self {
|
/// Undoes `within`, and `None` where the span has a fixed length: every
|
||||||
let rel = self.rel.lerp_inv(span.start.rel, span.end.rel);
|
/// fraction of it lands on the same `rel`, so none can be told apart.
|
||||||
|
pub fn outside(&self, span: &UiSpan) -> Option<Self> {
|
||||||
|
let rel = self.rel.lerp_inv(span.start.rel, span.end.rel)?;
|
||||||
let abs = self.abs - rel.lerp(span.start.abs, span.end.abs);
|
let abs = self.abs - rel.lerp(span.start.abs, span.end.abs);
|
||||||
Self { rel, abs }
|
Some(Self { rel, abs })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn within_len(&self, len: UiScalar) -> Self {
|
pub fn within_len(&self, len: UiScalar) -> Self {
|
||||||
@@ -283,10 +278,10 @@ impl UiSpan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn outside(&self, parent: &Self) -> Self {
|
pub fn outside(&self, parent: &Self) -> Option<Self> {
|
||||||
Self {
|
match (self.start.outside(parent), self.end.outside(parent)) {
|
||||||
start: self.start.outside(parent),
|
(Some(start), Some(end)) => Some(Self { start, end }),
|
||||||
end: self.end.outside(parent),
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,14 +319,7 @@ impl UiRegion {
|
|||||||
y: self.y.within(&parent.y),
|
y: self.y.within(&parent.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub const fn outside(&self, parent: &Self) -> Self {
|
pub const fn axis(&self, axis: Axis) -> &UiSpan {
|
||||||
Self {
|
|
||||||
x: self.x.outside(&parent.x),
|
|
||||||
y: self.y.outside(&parent.y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn axis(&mut self, axis: Axis) -> &UiSpan {
|
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => &self.x,
|
Axis::X => &self.x,
|
||||||
Axis::Y => &self.y,
|
Axis::Y => &self.y,
|
||||||
@@ -409,6 +397,50 @@ impl UiRegion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Taking a drawing out of one box and putting it in another, checked once
|
||||||
|
/// for a whole subtree so that applying it cannot fail.
|
||||||
|
///
|
||||||
|
/// A box of a fixed length holds each part as an offset from its start rather
|
||||||
|
/// than as a fraction of it, so those parts can be carried to a box of the
|
||||||
|
/// same length but never stretched to a different one.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub struct Remap {
|
||||||
|
from: UiRegion,
|
||||||
|
to: UiRegion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Remap {
|
||||||
|
pub fn new(from: UiRegion, to: UiRegion) -> Option<Self> {
|
||||||
|
[Axis::X, Axis::Y]
|
||||||
|
.into_iter()
|
||||||
|
.all(|axis| {
|
||||||
|
let (from, to) = (from.axis(axis), to.axis(axis));
|
||||||
|
from.start.rel != from.end.rel || from.len() == to.len()
|
||||||
|
})
|
||||||
|
.then_some(Self { from, to })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply(&self, region: UiRegion) -> UiRegion {
|
||||||
|
UiRegion {
|
||||||
|
x: Self::span(region.x, self.from.x, self.to.x),
|
||||||
|
y: Self::span(region.y, self.from.y, self.to.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span(span: UiSpan, from: UiSpan, to: UiSpan) -> UiSpan {
|
||||||
|
match span.outside(&from) {
|
||||||
|
Some(out) => out.within(&to),
|
||||||
|
// `new` admits this only where the two are the same length, so
|
||||||
|
// the difference between their starts is the whole move.
|
||||||
|
None => {
|
||||||
|
let mut span = span;
|
||||||
|
span.shift(to.start - from.start);
|
||||||
|
span
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for UiRegion {
|
impl Display for UiRegion {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
|
|||||||
+14
-23
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, OnResize, Painter, PixelRegion, Size,
|
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, OnResize, Painter, PixelRegion, Remap, Size,
|
||||||
StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
|
StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
|
||||||
util::{HashMap, HashSet, Vec2, forget_ref},
|
util::{HashMap, HashSet, Vec2, forget_ref},
|
||||||
};
|
};
|
||||||
@@ -183,38 +183,29 @@ impl UiRenderState {
|
|||||||
return Some(size);
|
return Some(size);
|
||||||
}
|
}
|
||||||
// TODO: epsilon?
|
// TODO: epsilon?
|
||||||
if old.size() == region.size() {
|
if old.size() != region.size() && !self.reusable(id, region, rsc) {
|
||||||
self.mov(id, old, region);
|
return None;
|
||||||
return Some(size);
|
|
||||||
}
|
}
|
||||||
if self.reusable(id, region, rsc) {
|
// Its drawing stands, if the new box can be reached from the old one.
|
||||||
// Its drawing stands; the new box is remapped into the primitives.
|
self.mov(id, &Remap::new(old, region)?);
|
||||||
self.mov(id, old, region);
|
Some(size)
|
||||||
return Some(size);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the widget can keep the drawing it has and be given `region`
|
/// Whether the widget can keep the drawing it has and be given `region`
|
||||||
/// instead, asked one axis at a time: a change on an axis it does not
|
/// instead, asked one axis at a time: a change on an axis it does not
|
||||||
/// depend on costs nothing, whatever it depends on elsewhere.
|
/// depend on costs nothing, whatever it depends on elsewhere.
|
||||||
fn reusable(&self, id: WidgetId, mut region: UiRegion, rsc: &dyn UiRsc) -> bool {
|
fn reusable(&self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> bool {
|
||||||
let Some(active) = self.active.get(&id) else {
|
let Some(active) = self.active.get(&id) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let Some(widget) = rsc.widgets().get_dyn(id) else {
|
let Some(widget) = rsc.widgets().get_dyn(id) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let mut old = active.region;
|
|
||||||
[Axis::X, Axis::Y].into_iter().all(|axis| {
|
[Axis::X, Axis::Y].into_iter().all(|axis| {
|
||||||
let offered = region.axis_mut(axis).len();
|
let offered = region.axis(axis).len();
|
||||||
let had = old.axis_mut(axis).len();
|
let had = active.region.axis(axis).len();
|
||||||
match widget.on_resize(axis) {
|
match widget.on_resize(axis) {
|
||||||
// Remapping out of the old box only scales while that box
|
OnResize::Scale => true,
|
||||||
// had a relative extent; inside a fixed one every part became
|
|
||||||
// an offset from its start, which `mov` carries but cannot
|
|
||||||
// stretch.
|
|
||||||
OnResize::Scale => offered == had || had.rel != 0.0,
|
|
||||||
// `Translate` is not acted on yet, and cannot be until a
|
// `Translate` is not acted on yet, and cannot be until a
|
||||||
// drawing can sit somewhere other than its box. `region` is
|
// drawing can sit somewhere other than its box. `region` is
|
||||||
// both the box a widget was given and the box its primitives
|
// both the box a widget was given and the box its primitives
|
||||||
@@ -237,17 +228,17 @@ impl UiRenderState {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
|
fn mov(&mut self, id: WidgetId, remap: &Remap) {
|
||||||
let active = self.active.get_mut(&id).unwrap();
|
let active = self.active.get_mut(&id).unwrap();
|
||||||
for h in &active.primitives {
|
for h in &active.primitives {
|
||||||
let region = self.layers[h.layer].region_mut(h);
|
let region = self.layers[h.layer].region_mut(h);
|
||||||
*region = region.outside(&from).within(&to);
|
*region = remap.apply(*region);
|
||||||
}
|
}
|
||||||
active.region = active.region.outside(&from).within(&to);
|
active.region = remap.apply(active.region);
|
||||||
// SAFETY: children cannot be recursive
|
// SAFETY: children cannot be recursive
|
||||||
let children = unsafe { forget_ref(&active.children) };
|
let children = unsafe { forget_ref(&active.children) };
|
||||||
for child in children {
|
for child in children {
|
||||||
self.mov(*child, from, to);
|
self.mov(*child, remap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-22
@@ -1,33 +1,21 @@
|
|||||||
use std::ops::*;
|
pub const trait LerpUtil: Sized {
|
||||||
|
|
||||||
pub const trait LerpUtil {
|
|
||||||
fn lerp(self, from: Self, to: Self) -> Self;
|
fn lerp(self, from: Self, to: Self) -> Self;
|
||||||
fn lerp_inv(self, from: Self, to: Self) -> Self;
|
fn lerp_inv(self, from: Self, to: Self) -> Option<Self>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const trait DivOr {
|
const impl LerpUtil for f32 {
|
||||||
fn div_or(self, rhs: Self, other: Self) -> Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
const impl DivOr for f32 {
|
|
||||||
fn div_or(self, rhs: Self, other: Self) -> Self {
|
|
||||||
let res = self / rhs;
|
|
||||||
if res.is_nan() { other } else { res }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const impl<
|
|
||||||
T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy,
|
|
||||||
> LerpUtil for T
|
|
||||||
{
|
|
||||||
/// linear interpolation
|
/// linear interpolation
|
||||||
/// from * (1.0 - self) + to * self
|
/// from * (1.0 - self) + to * self
|
||||||
fn lerp(self, from: Self, to: Self) -> Self {
|
fn lerp(self, from: Self, to: Self) -> Self {
|
||||||
from + (to - from) * self
|
from + (to - from) * self
|
||||||
}
|
}
|
||||||
/// inverse of lerp
|
/// inverse of lerp, and `None` where `from` and `to` are the same point:
|
||||||
fn lerp_inv(self, from: Self, to: Self) -> Self {
|
/// every input lerps to it, so there is no one answer to come back to.
|
||||||
(self - from).div_or(to - from, from)
|
fn lerp_inv(self, from: Self, to: Self) -> Option<Self> {
|
||||||
|
match to == from {
|
||||||
|
true => None,
|
||||||
|
false => Some((self - from) / (to - from)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-10
@@ -1,4 +1,4 @@
|
|||||||
use crate::util::{DivOr, impl_op};
|
use crate::util::impl_op;
|
||||||
use std::{hash::Hash, ops::*};
|
use std::{hash::Hash, ops::*};
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -67,15 +67,6 @@ impl_op!(Vec2 Sub sub; x y);
|
|||||||
impl_op!(Vec2 Mul mul; x y);
|
impl_op!(Vec2 Mul mul; x y);
|
||||||
impl_op!(Vec2 Div div; x y);
|
impl_op!(Vec2 Div div; x y);
|
||||||
|
|
||||||
const impl DivOr for Vec2 {
|
|
||||||
fn div_or(self, rhs: Self, other: Self) -> Self {
|
|
||||||
Self {
|
|
||||||
x: self.x.div_or(rhs.x, other.x),
|
|
||||||
y: self.y.div_or(rhs.y, other.y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Neg for Vec2 {
|
impl Neg for Vec2 {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
//! What a drawing can be taken out of, and what it cannot.
|
||||||
|
|
||||||
|
use iris::core::{Remap, UiRegion, UiScalar, UiSpan};
|
||||||
|
|
||||||
|
/// A box `size` tall whose top is `rel` of the way down the window.
|
||||||
|
fn fixed(rel: f32, size: f32) -> UiRegion {
|
||||||
|
UiRegion::new(
|
||||||
|
UiSpan::FULL,
|
||||||
|
UiSpan::new(UiScalar { rel, abs: 0.0 }, UiScalar { rel, abs: size }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fixed_box_can_be_carried_but_not_stretched() {
|
||||||
|
let from = fixed(0.0, 164.0);
|
||||||
|
assert!(Remap::new(from, UiRegion::FULL).is_none());
|
||||||
|
assert!(Remap::new(from, fixed(0.5, 164.0)).is_some());
|
||||||
|
assert!(Remap::new(from, fixed(0.0, 98.0)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_relative_box_can_be_stretched_to_any_other() {
|
||||||
|
let remap = Remap::new(UiRegion::FULL, fixed(0.0, 98.0)).expect("relative boxes remap");
|
||||||
|
// A part that filled the window keeps filling what replaced it, which is
|
||||||
|
// exactly what `outside` could not say for a box of a fixed length.
|
||||||
|
assert_eq!(remap.apply(UiRegion::FULL), fixed(0.0, 98.0));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user