Merge remote-tracking branch 'upstream/main' into split/17-headless-rig

This commit is contained in:
iris-ai committed 2026-09-14 02:48:37 -04:00
commit a123d13490
29 files changed
+895 -612

No files matched your search

+55 -23
View File
@@ -56,13 +56,6 @@ impl UiVec2 {
} }
} }
pub const fn outside(&self, region: &UiRegion) -> UiVec2 {
UiVec2 {
x: self.x.outside(&region.x),
y: self.y.outside(&region.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,11 +278,11 @@ impl UiSpan {
} }
} }
pub const fn outside(&self, parent: &Self) -> Self { pub fn outside(&self, parent: &Self) -> Option<Self> {
Self { Some(Self {
start: self.start.outside(parent), start: self.start.outside(parent)?,
end: self.end.outside(parent), end: self.end.outside(parent)?,
} })
} }
pub const fn len(&self) -> UiScalar { pub const fn len(&self) -> UiScalar {
@@ -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!(
+7 -1
View File
@@ -1,14 +1,20 @@
use crate::{LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId}; use crate::{LayerId, MaskIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId};
/// important non rendering data for retained drawing /// important non rendering data for retained drawing
#[derive(Debug)] #[derive(Debug)]
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
pub region: UiRegion, pub region: UiRegion,
/// What the widget said it used of `region`, the last time it drew.
pub size: Size,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>, pub primitives: Vec<PrimitiveHandle>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>,
/// Whether it read the output's size, and so is wrong when that changes.
pub reads_output: bool,
pub mask: MaskIdx, pub mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
} }
-18
View File
@@ -1,18 +0,0 @@
use crate::{BothAxis, Len, UiVec2, WidgetId, util::HashMap};
#[derive(Default)]
pub struct Cache {
pub size: BothAxis<HashMap<WidgetId, (UiVec2, Len)>>,
}
impl Cache {
pub fn remove(&mut self, id: WidgetId) {
self.size.x.remove(&id);
self.size.y.remove(&id);
}
pub fn clear(&mut self) {
self.size.x.clear();
self.size.y.clear();
}
}
-3
View File
@@ -3,15 +3,12 @@ use crate::{
}; };
mod active; mod active;
mod cache;
mod painter; mod painter;
mod render_state; mod render_state;
mod size;
pub use active::*; pub use active::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::{Painter, PrimitiveLike};
pub use render_state::*; pub use render_state::*;
pub use size::*;
#[derive(Default)] #[derive(Default)]
pub struct UiData { pub struct UiData {
+69 -24
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData, Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind, GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
TexturePrimitive, TexturePrimitive,
@@ -18,6 +18,9 @@ pub struct Painter<'a> {
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>, pub(super) primitives: Vec<PrimitiveHandle>,
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>,
pub(super) reads_output: bool,
pub layer: usize, pub layer: usize,
pub(super) id: WidgetId, pub(super) id: WidgetId,
} }
@@ -68,19 +71,31 @@ impl<'a> Painter<'a> {
} }
/// Draws a widget within this widget's region. /// Draws a widget within this widget's region.
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) { pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
self.widget_at(id, self.region); self.widget_at(id, self.region)
} }
/// Draws a widget somewhere within this one. /// Draws a widget somewhere within this one. Drawing one a second time
/// Useful for drawing child widgets in select areas. /// gives it a new box, keeping the drawing it already has where it can.
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) { pub fn widget_within<'s, W: ?Sized>(
self.widget_at(id, region.within(&self.region)); &'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
let region = region.within(&self.region);
self.widget_at(id, region)
} }
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) { fn widget_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
// A child listed twice would be moved twice.
if !self.children.contains(&id.id()) {
self.children.push(id.id()); self.children.push(id.id());
self.state.draw_inner( }
let size = self.state.draw_inner(
self.layer, self.layer,
id.id(), id.id(),
region, region,
@@ -89,6 +104,25 @@ impl<'a> Painter<'a> {
None, None,
self.rsc, self.rsc,
); );
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 hint = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis)?;
self.depend_on_size(id);
Some(hint)
}
fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id());
}
} }
pub fn render_text( pub fn render_text(
@@ -129,22 +163,17 @@ impl<'a> Painter<'a> {
self.region self.region
} }
pub fn size<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size { /// The output's size in pixels. A widget that reads it draws again when
self.size_ctx().size(id) /// the output changes, since nothing else can put that right.
} pub fn output_size(&mut self) -> Vec2 {
self.reads_output = true;
pub fn len_axis<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Len {
match axis {
Axis::X => self.size_ctx().width(id),
Axis::Y => self.size_ctx().height(id),
}
}
pub fn output_size(&self) -> Vec2 {
self.state.output_size self.state.output_size
} }
/// This widget's box in pixels. Resolved against the output's size, so a
/// widget that reads it draws again when the output changes.
pub fn px_size(&mut self) -> Vec2 { pub fn px_size(&mut self) -> Vec2 {
self.reads_output = true;
self.region.size().to_abs(self.state.output_size) self.region.size().to_abs(self.state.output_size)
} }
@@ -167,9 +196,25 @@ impl<'a> Painter<'a> {
pub fn id(&self) -> &WidgetId { pub fn id(&self) -> &WidgetId {
&self.id &self.id
} }
}
pub fn size_ctx(&mut self) -> SizeCtx<'_> { /// A child that has just been drawn. Reading its size records that this
self.state.size_ctx(self.id, self.region.size(), self.rsc) /// 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 {
self.painter.depend_on_size(self.child);
self.size
}
pub fn len(self, axis: Axis) -> Len {
self.size().axis(axis)
} }
} }
+123 -61
View File
@@ -1,7 +1,6 @@
use crate::{ use crate::{
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget, ActiveData, Axis, DrawLayers, IdLike, MaskIdx, OnResize, Painter, PixelRegion, Remap, Size,
UiRegion, UiRsc, UiVec2, WidgetId, Widgets, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
ui::cache::Cache,
util::{HashMap, HashSet, Vec2, forget_ref}, util::{HashMap, HashSet, Vec2, forget_ref},
}; };
@@ -9,7 +8,6 @@ pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>, pub active: HashMap<WidgetId, ActiveData>,
pub layers: DrawLayers, pub layers: DrawLayers,
pub(super) output_size: Vec2, pub(super) output_size: Vec2,
pub cache: Cache,
old_root: Option<WidgetId>, old_root: Option<WidgetId>,
resized: bool, resized: bool,
@@ -21,7 +19,6 @@ impl UiRenderState {
Self { Self {
active: Default::default(), active: Default::default(),
layers: Default::default(), layers: Default::default(),
cache: Default::default(),
output_size: Vec2::ZERO, output_size: Vec2::ZERO,
old_root: None, old_root: None,
resized: false, resized: false,
@@ -56,11 +53,21 @@ impl UiRenderState {
); );
} }
let root = root.into(); let root = root.into();
if self.needs_full_redraw(root) { if self.root_changed(root) {
self.redraw_all(root, rsc); self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id()); self.old_root = root.map(|r| r.id());
} else if self.resized {
// A region is a fraction of the output plus an offset, resolved
// against the window in the shader, so a resize moves the whole
// drawing on its own. Only a widget that read pixels can be wrong.
for (&id, active) in &self.active {
if active.reads_output {
rsc.widgets_mut().needs_redraw.insert(id);
}
}
}
self.resized = false; self.resized = false;
} else if rsc.widgets().has_updates() { if rsc.widgets().has_updates() {
self.redraw_updates(rsc); self.redraw_updates(rsc);
} }
} }
@@ -84,19 +91,11 @@ impl UiRenderState {
mask: MaskIdx, mask: MaskIdx,
old_children: Option<Vec<WidgetId>>, old_children: Option<Vec<WidgetId>>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) { ) -> Size {
let mut old_children = old_children.unwrap_or_default(); let mut old_children = old_children.unwrap_or_default();
if let Some(active) = self.active.get_mut(&id) if self.active.contains_key(&id) {
&& !rsc.widgets().needs_redraw.contains(&id) if let Some(size) = self.try_reuse(id, region, rsc) {
{ return size;
// check to see if we can skip drawing first
if active.region == region {
return;
} else if active.region.size() == region.size() {
// TODO: epsilon?
let from = active.region;
self.mov(id, from, region);
return;
} }
// if not, then maintain resize and track old children to remove unneeded // if not, then maintain resize and track old children to remove unneeded
let active = self.remove(id, false, rsc).unwrap(); let active = self.remove(id, false, rsc).unwrap();
@@ -104,6 +103,7 @@ impl UiRenderState {
} }
// draw widget // draw widget
rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.insert(id); self.draw_started.insert(id);
let mut painter = Painter { let mut painter = Painter {
@@ -115,11 +115,13 @@ impl UiRenderState {
textures: Vec::new(), textures: Vec::new(),
primitives: Vec::new(), primitives: Vec::new(),
children: Vec::new(), children: Vec::new(),
size_deps: Vec::new(),
reads_output: false,
rsc, rsc,
}; };
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id); let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
widget.draw(&mut painter); let size = widget.draw(&mut painter);
drop(widget); drop(widget);
let Painter { let Painter {
@@ -130,18 +132,29 @@ impl UiRenderState {
textures, textures,
primitives, primitives,
children, children,
size_deps,
reads_output,
layer, layer,
id, id,
} = painter; } = painter;
debug_assert!(
Self::hints_agree(id, size, rsc),
"'{}' ({id:?}) drew a size its size_hint disagrees with",
rsc.widgets().label(id)
);
// add to active // add to active
let active = ActiveData { let active = ActiveData {
id, id,
region, region,
size,
parent, parent,
textures, textures,
primitives, primitives,
children, children,
size_deps,
reads_output,
mask, mask,
layer, layer,
}; };
@@ -155,19 +168,77 @@ impl UiRenderState {
rsc.on_draw(&active); rsc.on_draw(&active);
self.active.insert(id, active); self.active.insert(id, active);
size
} }
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) { /// The drawing a widget already has, kept for a new box if the box has not
/// changed in a way it depends on.
fn try_reuse(&mut self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> Option<Size> {
if rsc.widgets().needs_redraw.contains(&id) {
return None;
}
let active = self.active.get(&id)?;
let (size, old) = (active.size, active.region);
if old == region {
return Some(size);
}
// TODO: epsilon?
if old.size() != region.size() && !self.reusable(id, region, rsc) {
return None;
}
// Its drawing stands, if the new box can be reached from the old one.
self.mov(id, &Remap::new(old, region)?);
Some(size)
}
/// 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
/// depend on costs nothing, whatever it depends on elsewhere.
fn reusable(&self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> bool {
let Some(active) = self.active.get(&id) else {
return false;
};
let Some(widget) = rsc.widgets().get_dyn(id) else {
return false;
};
[Axis::X, Axis::Y].into_iter().all(|axis| {
let offered = region.axis(axis).len();
let had = active.region.axis(axis).len();
match widget.on_resize(axis) {
OnResize::Scale => true,
// `Translate` is not acted on yet, and cannot be until a
// drawing can sit somewhere other than its box. `region` is
// both the box a widget was given and the box its primitives
// are in, and `mov` remaps from it -- so carrying a drawing at
// its old size while the box grows makes the next move stretch
// it. The offset chain is what separates the two.
OnResize::Translate | OnResize::Redraw => offered == had,
}
})
}
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {
let Some(widget) = rsc.widgets().get_dyn(id) else {
return true;
};
[Axis::X, Axis::Y].into_iter().all(|axis| {
widget
.size_hint(axis)
.is_none_or(|hint| hint == size.axis(axis))
})
}
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);
} }
} }
@@ -191,7 +262,6 @@ impl UiRenderState {
} }
fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> { fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
self.cache.remove(id);
let inst = self.remove(id, true, rsc); let inst = self.remove(id, true, rsc);
if let Some(inst) = &inst { if let Some(inst) = &inst {
for c in &inst.children { for c in &inst.children {
@@ -205,7 +275,6 @@ impl UiRenderState {
for (_, active) in self.active.drain() { for (_, active) in self.active.drain() {
rsc.on_undraw(&active); rsc.on_undraw(&active);
} }
self.cache.clear();
self.layers.clear(); self.layers.clear();
rsc.widgets_mut().needs_redraw.clear(); rsc.widgets_mut().needs_redraw.clear();
rsc.free(); rsc.free();
@@ -222,17 +291,12 @@ impl UiRenderState {
root.into().map(|r| r.id()) != self.old_root root.into().map(|r| r.id()) != self.old_root
} }
// Scheduling and drawing must use the same full-redraw predicate.
fn needs_full_redraw<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
self.root_changed(root) || self.resized
}
pub fn needs_redraw<'a>( pub fn needs_redraw<'a>(
&self, &self,
root: impl Into<Option<&'a StrongWidget>>, root: impl Into<Option<&'a StrongWidget>>,
widgets: &Widgets, widgets: &Widgets,
) -> bool { ) -> bool {
self.needs_full_redraw(root) || widgets.has_updates() self.root_changed(root) || self.resized || widgets.has_updates()
} }
pub fn active_widgets(&self) -> usize { pub fn active_widgets(&self) -> usize {
@@ -265,22 +329,19 @@ impl UiRenderState {
/// redraws a widget that's currently active (drawn) /// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.remove(&id); self.draw_started.remove(&id);
// check if parent depends on the desired size of this, if so then redraw it first // Whoever read this widget's size may be a different size now, so the
for axis in [Axis::X, Axis::Y] { // highest reader is what draws. Everything between the two is marked
if let Some(&(outer, old)) = self.cache.size.axis_dyn(axis).get(&id) // as well: their own boxes have not changed, so the mark is the only
&& let Some(current) = self.active.get(&id) // thing stopping the draw reusing its way past this widget.
&& let Some(pid) = current.parent if let Some(top) = self.mark_readers(id, rsc) {
{ self.redraw(top, rsc);
self.cache.size.axis_dyn(axis).remove(&id); // Cleared by that draw if it reached here; if it did not, this is
let new = self.size_ctx(id, outer, rsc).len_axis(id, axis); // no longer drawn and asking again would not end.
self.cache.size.axis_dyn(axis).insert(id, (outer, new)); rsc.widgets_mut().needs_redraw.remove(&id);
if new != old { return;
self.redraw(pid, rsc);
}
}
} }
rsc.widgets_mut().needs_redraw.remove(&id);
if self.draw_started.contains(&id) { if self.draw_started.contains(&id) {
return; return;
@@ -301,22 +362,23 @@ impl UiRenderState {
); );
} }
pub(super) fn size_ctx<'b>( /// The furthest ancestor that read this widget's size, directly or through
&'b mut self, /// widgets that did the same, marking everything below it on the way.
source: WidgetId, fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<WidgetId> {
outer: UiVec2, let mut top = None;
rsc: &'b mut dyn UiRsc, let mut at = id;
) -> SizeCtx<'b> { while let Some(active) = self.active.get(&at)
let ui = rsc.ui_mut(); && let Some(parent) = active.parent
SizeCtx { && self
source, .active
cache: &mut self.cache, .get(&parent)
text: &mut ui.text, .is_some_and(|p| p.size_deps.contains(&at))
widgets: &ui.widgets, {
outer, rsc.widgets_mut().needs_redraw.insert(at);
output_size: self.output_size, top = Some(parent);
id: source, at = parent;
} }
top
} }
} }
-89
View File
@@ -1,89 +0,0 @@
use crate::{
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2,
WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
};
pub struct SizeCtx<'a> {
pub text: &'a mut TextData,
pub(super) source: WidgetId,
pub(super) widgets: &'a Widgets,
pub(super) cache: &'a mut Cache,
/// TODO: should this be pub? rn used for sized
pub outer: UiVec2,
pub(super) output_size: Vec2,
pub(super) id: WidgetId,
}
impl SizeCtx<'_> {
pub fn id(&self) -> &WidgetId {
&self.id
}
pub fn source(&self) -> &WidgetId {
&self.source
}
pub(super) fn len_inner<A: const AxisT>(&mut self, id: WidgetId) -> Len {
if let Some((_, len)) = self.cache.size.axis::<A>().get(&id) {
return *len;
}
let len = self
.widgets
.get_dyn_dynamic(id)
.desired_len::<A>(&mut SizeCtx {
text: self.text,
source: self.source,
widgets: self.widgets,
cache: self.cache,
outer: self.outer,
output_size: self.output_size,
id,
});
self.cache.size.axis::<A>().insert(id, (self.outer, len));
len
}
pub fn width(&mut self, id: impl IdLike) -> Len {
self.len_inner::<XAxis>(id.id())
}
pub fn height(&mut self, id: impl IdLike) -> Len {
self.len_inner::<YAxis>(id.id())
}
pub fn len_axis(&mut self, id: impl IdLike, axis: Axis) -> Len {
match axis {
Axis::X => self.width(id),
Axis::Y => self.height(id),
}
}
pub fn size(&mut self, id: impl IdLike) -> Size {
let id = id.id();
Size {
x: self.width(id),
y: self.height(id),
}
}
pub fn px_size(&mut self) -> Vec2 {
self.outer.to_abs(self.output_size)
}
pub fn output_size(&mut self) -> Vec2 {
self.output_size
}
pub fn draw_text(
&mut self,
buffer: &mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
) -> RenderedText {
self.text.render(buffer, attrs, width)
}
pub fn label(&self, id: WidgetId) -> &String {
self.widgets.label(id)
}
}
+10 -22
View File
@@ -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
View File
@@ -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;
+32 -19
View File
@@ -1,4 +1,4 @@
use crate::{Axis, AxisT, Len, Painter, SizeCtx}; use crate::{Axis, Len, Painter, Size};
use std::any::Any; use std::any::Any;
mod data; mod data;
@@ -15,32 +15,45 @@ pub use tag::*;
pub use view::*; pub use view::*;
pub use widgets::*; pub use widgets::*;
/// What may be done to a widget's drawing when the box it was given changes
/// on this axis, instead of drawing it again. Asked per axis, because wrapped
/// text reads the width it is offered and not the height.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OnResize {
Scale,
Translate,
#[default]
Redraw,
}
pub trait Widget: Any { pub trait Widget: Any {
fn draw(&mut self, painter: &mut Painter); /// Draws the widget, and returns what it used of the box it was given.
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len; fn draw(&mut self, painter: &mut Painter) -> Size;
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len;
}
pub trait WidgetAxisFns { /// An exact length the widget can give without a painter or its children.
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len; /// Optional, and saves a draw rather than changing one: a hint that
} /// disagrees with the eventual draw fails a debug assertion.
fn size_hint(&self, _axis: Axis) -> Option<Len> {
impl<W: Widget + ?Sized> WidgetAxisFns for W { None
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len {
match A::get() {
Axis::X => self.desired_width(ctx),
Axis::Y => self.desired_height(ctx),
} }
fn on_resize(&self, _axis: Axis) -> OnResize {
OnResize::default()
} }
} }
impl Widget for () { impl Widget for () {
fn draw(&mut self, _: &mut Painter) {} /// A gap: nothing drawn, at the default length, so a span gives it a share.
fn desired_width(&mut self, _: &mut SizeCtx) -> Len { fn draw(&mut self, _: &mut Painter) -> Size {
Len::ZERO Size::default()
} }
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
Len::ZERO fn size_hint(&self, _axis: Axis) -> Option<Len> {
Some(Len::default())
}
fn on_resize(&self, _axis: Axis) -> OnResize {
OnResize::Scale
} }
} }
+63
View File
@@ -0,0 +1,63 @@
//! Text sizing: wrapped text reads the width it is offered, fixed text does
//! not, and both report a height their container lays out around.
use iris::prelude::*;
fn main() {
DefaultApp::<State>::run();
}
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
}
const SAMPLE: &str = "Wrapping shapes one source into as many lines as its container \
leaves room for, so the height of a paragraph is an answer rather than a setting, and \
the same words in a narrower box come back taller. Resize the window and watch the \
text below reflow into a different number of lines while nothing about it changes.";
impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
let panel = || rect(Color::WHITE.darker(0.85));
let wrapped = wtext(SAMPLE)
.size(28)
.wrap(true)
.text_align(Align::LEFT)
.pad(16)
.background(panel());
let aligned = (
wtext("left").size(24).text_align(Align::LEFT),
wtext("centred").size(24).text_align(Align::CENTER),
wtext("right").size(24).text_align(Align::RIGHT),
)
.span(Dir::DOWN)
.gap(8)
.pad(16)
.background(panel());
// The same words in half the width, which is a different number of
// lines and so a different height. A declared width only holds along
// a span's own axis, hence the row.
let narrow = (
wtext(SAMPLE)
.size(20)
.wrap(true)
.pad(16)
.background(panel())
.align(Align::TOP)
.width(rel(0.5)),
rect(Color::WHITE.darker(0.95)),
)
.span(Dir::RIGHT);
(wrapped, aligned, narrow)
.span(Dir::DOWN)
.gap(12)
.pad(12)
.set_root(rsc, &mut ui_state);
Self { ui_state }
}
}
+6 -5
View File
@@ -6,16 +6,17 @@ pub struct Image {
} }
impl Widget for Image { impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.primitive(&self.handle); painter.primitive(&self.handle);
Size::abs(self.handle.size())
} }
fn desired_width(&mut self, _: &mut SizeCtx) -> Len { fn size_hint(&self, axis: Axis) -> Option<Len> {
Len::abs(self.handle.size().x) Some(Len::abs(self.handle.size().axis(axis)))
} }
fn desired_height(&mut self, _: &mut SizeCtx) -> Len { fn on_resize(&self, _: Axis) -> OnResize {
Len::abs(self.handle.size().y) OnResize::Scale
} }
} }
+5 -8
View File
@@ -5,16 +5,13 @@ pub struct Masked {
} }
impl Widget for Masked { impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region()); painter.set_mask(painter.region());
painter.widget(&self.inner); painter.widget(&self.inner).size()
} }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { /// It clips to the box it was given, not to the part its child used.
ctx.width(&self.inner) fn on_resize(&self, _: Axis) -> OnResize {
} OnResize::Redraw
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
} }
} }
+8 -21
View File
@@ -6,30 +6,17 @@ pub struct Aligned {
} }
impl Widget for Aligned { impl Widget for Aligned {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
// Drawn where it may be too big, then given its aligned box once its
// size is known.
let size = painter.widget(&self.inner).size();
let region = match self.align.tuple() { let region = match self.align.tuple() {
(Some(x), Some(y)) => painter (Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
.size(&self.inner) (Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
.to_uivec2() (None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
.align(RegionAlign { x, y }),
(Some(x), None) => {
let x = painter.size_ctx().width(&self.inner).apply_rest().align(x);
UiRegion::new(x, UiSpan::FULL)
}
(None, Some(y)) => {
let y = painter.size_ctx().height(&self.inner).apply_rest().align(y);
UiRegion::new(UiSpan::FULL, y)
}
(None, None) => UiRegion::FULL, (None, None) => UiRegion::FULL,
}; };
painter.widget_within(&self.inner, region); painter.widget_within(&self.inner, region);
} size
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.width(&self.inner)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
} }
} }
+2 -10
View File
@@ -6,18 +6,10 @@ pub struct LayerOffset {
} }
impl Widget for LayerOffset { impl Widget for LayerOffset {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
for _ in 0..self.offset { for _ in 0..self.offset {
painter.next_layer(); painter.next_layer();
} }
painter.widget(&self.inner); painter.widget(&self.inner).size()
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.width(&self.inner)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
} }
} }
+13 -36
View File
@@ -6,43 +6,20 @@ pub struct MaxSize {
pub y: Option<Len>, pub y: Option<Len>,
} }
impl MaxSize {
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
if let Some(x) = self.x {
ctx.outer.x.select_len(x.apply_rest());
}
if let Some(y) = self.y {
ctx.outer.y.select_len(y.apply_rest());
}
}
}
impl Widget for MaxSize { impl Widget for MaxSize {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget(&self.inner); let child = painter.widget(&self.inner).size();
} let output = painter.output_size();
Size {
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { x: capped(child.x, self.x, output.x),
self.apply_to_outer(ctx); y: capped(child.y, self.y, output.y),
let width = ctx.width(&self.inner);
if let Some(x) = self.x {
let width_px = width.apply_rest().to_abs(ctx.output_size().x);
let x_px = x.apply_rest().to_abs(ctx.output_size().x);
if width_px > x_px { x } else { width }
} else {
width
}
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.apply_to_outer(ctx);
let height = ctx.height(&self.inner);
if let Some(y) = self.y {
let height_px = height.apply_rest().to_abs(ctx.output_size().y);
let y_px = y.apply_rest().to_abs(ctx.output_size().y);
if height_px > y_px { y } else { height }
} else {
height
} }
} }
} }
fn capped(len: Len, max: Option<Len>, output: f32) -> Len {
match max {
Some(max) if len.apply_rest().to_abs(output) > max.apply_rest().to_abs(output) => max,
_ => len,
}
}
+2 -10
View File
@@ -6,16 +6,8 @@ pub struct Offset {
} }
impl Widget for Offset { impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
let region = UiRegion::FULL.offset(self.amt); let region = UiRegion::FULL.offset(self.amt);
painter.widget_within(&self.inner, region); painter.widget_within(&self.inner, region).size()
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.width(&self.inner)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
} }
} }
+13 -21
View File
@@ -6,28 +6,20 @@ pub struct Pad {
} }
impl Widget for Pad { impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_within(&self.inner, self.padding.region()); let inner = painter
.widget_within(&self.inner, self.padding.region())
.size();
Size {
x: Len {
abs: inner.x.abs + self.padding.left + self.padding.right,
..inner.x
},
y: Len {
abs: inner.y.abs + self.padding.top + self.padding.bottom,
..inner.y
},
} }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
let width = self.padding.left + self.padding.right;
let height = self.padding.top + self.padding.bottom;
ctx.outer.x.abs -= width;
ctx.outer.y.abs -= height;
let mut size = ctx.width(&self.inner);
size.abs += width;
size
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
let width = self.padding.left + self.padding.right;
let height = self.padding.top + self.padding.bottom;
ctx.outer.x.abs -= width;
ctx.outer.y.abs -= height;
let mut size = ctx.height(&self.inner);
size.abs += height;
size
} }
} }
+7 -11
View File
@@ -10,11 +10,14 @@ pub struct Scroll {
} }
impl Widget for Scroll { impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
let output_len = painter.output_size().axis(self.axis); let output_len = painter.output_size().axis(self.axis);
let container_len = painter.region().axis(self.axis).len(); let container_len = painter.region().axis(self.axis).len();
let content_len = painter // Drawn in the whole container to learn its length, then placed at
.len_axis(&self.inner, self.axis) // the scrolled offset.
let child = painter.widget(&self.inner).size();
let content_len = child
.axis(self.axis)
.apply_rest() .apply_rest()
.within_len(container_len) .within_len(container_len)
.to_abs(output_len); .to_abs(output_len);
@@ -29,14 +32,7 @@ impl Widget for Scroll {
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0)); let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
painter.widget_within(&self.inner, region); painter.widget_within(&self.inner, region);
} child
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.width(&self.inner)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
} }
} }
+12 -20
View File
@@ -6,29 +6,21 @@ pub struct SetSize {
pub y: Option<Len>, pub y: Option<Len>,
} }
impl SetSize {
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
if let Some(x) = self.x {
ctx.outer.x.select_len(x.apply_rest());
}
if let Some(y) = self.y {
ctx.outer.y.select_len(y.apply_rest());
}
}
}
impl Widget for SetSize { impl Widget for SetSize {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget(&self.inner); let child = painter.widget(&self.inner).size();
Size {
x: self.x.unwrap_or(child.x),
y: self.y.unwrap_or(child.y),
}
} }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { /// A declared axis is known without looking at the child, which is what
self.apply_to_outer(ctx); /// lets a span lay out around `.height(rest(1))` without drawing it.
self.x.unwrap_or_else(|| ctx.width(&self.inner)) fn size_hint(&self, axis: Axis) -> Option<Len> {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
} }
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.apply_to_outer(ctx);
self.y.unwrap_or_else(|| ctx.height(&self.inner))
} }
} }
+32 -101
View File
@@ -8,13 +8,27 @@ pub struct Span {
} }
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
let total = self.len_sum(&mut painter.size_ctx()); let axis = self.dir.axis;
// A length for every child before any is placed: from its own hint
// where it has one, and from drawing it where it does not.
let lens: Vec<Len> = self
.children
.iter()
.map(|child| match painter.size_hint(child, axis) {
Some(len) => len,
None => painter.widget(child).len(axis),
})
.collect();
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
let total = lens.iter().fold(Len::abs(gap), |sum, len| sum + *len);
let mut start = UiScalar::rel_min(); let mut start = UiScalar::rel_min();
for child in &self.children { let mut ortho = Len::ZERO;
for (child, len) in self.children.iter().zip(&lens) {
let mut span = UiSpan::FULL; let mut span = UiSpan::FULL;
span.start = start; span.start = start;
let len = painter.len_axis(child, self.dir.axis);
if len.rest > 0.0 { if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.abs); let offset = UiScalar::new(total.rel, total.abs);
let rel_end = UiScalar::rel(len.rest / total.rest); let rel_end = UiScalar::rel(len.rest / total.rest);
@@ -24,27 +38,25 @@ impl Widget for Span {
start.abs += len.abs; start.abs += len.abs;
start.rel += len.rel; start.rel += len.rel;
span.end = start; span.end = start;
let mut child_region = UiRegion::from_axis(self.dir.axis, span, UiSpan::FULL); let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
if self.dir.sign == Sign::Neg { if self.dir.sign == Sign::Neg {
child_region.flip(self.dir.axis); region.flip(axis);
}
let used = painter.widget_within(child, region).size().axis(!axis);
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
if used.rel > 0.0 || used.rest > 0.0 {
ortho = Len::REST;
} else if ortho.rest == 0.0 {
ortho.abs = ortho.abs.max(used.abs);
} }
painter.widget_within(child, child_region);
start.abs += self.gap; start.abs += self.gap;
} }
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { let along = match total.rest == 0.0 && total.rel == 0.0 {
match self.dir.axis { true => total,
Axis::X => self.desired_len(ctx), false => Len::default(),
Axis::Y => self.desired_ortho(ctx), };
} Size::from_axis(axis, along, ortho)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
match self.dir.axis {
Axis::X => self.desired_ortho(ctx),
Axis::Y => self.desired_len(ctx),
}
} }
} }
@@ -69,87 +81,6 @@ impl Span {
pub fn pop(&mut self) -> Option<StrongWidget> { pub fn pop(&mut self) -> Option<StrongWidget> {
self.children.pop() self.children.pop()
} }
fn len_sum(&mut self, ctx: &mut SizeCtx) -> Len {
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
self.children.iter().fold(Len::abs(gap), |mut s, id| {
// it's tempting to subtract the abs & rel from the ctx outer,
// but that would create inconsistent sizing if you put
// a rest first vs last & only speed up in one direction.
// I think this is only solvable by restricting how you can
// compute size, bc currently you need child to define parent's
// sectioning and you need parent's sectioning to define child.
// Fortunately, that doesn't matter in most cases
let len = ctx.len_axis(id, self.dir.axis);
s += len;
s
})
}
fn desired_len(&mut self, ctx: &mut SizeCtx) -> Len {
let len = self.len_sum(ctx);
if len.rest == 0.0 && len.rel == 0.0 {
len
} else {
Len::default()
}
}
fn desired_ortho(&mut self, ctx: &mut SizeCtx) -> Len {
// this is a weird hack to get text wrapping to work properly when in a downward span
// the correct solution here is to add a function to widget that lets them
// request that ctx.outer has an axis "resolved" before checking the other,
// and panicking or warning if two request opposite axis (unsolvable in that case)
let outer = ctx.outer.axis(self.dir.axis);
if self.dir.axis == Axis::X {
// so....... this literally copies draw so that the lengths are correctly set in the
// context, which makes this slow and not cool
let total = self.len_sum(ctx);
let mut start = UiScalar::rel_min();
let mut ortho_len = Len::ZERO;
for child in &self.children {
let mut span = UiSpan::FULL;
span.start = start;
let len = ctx.len_axis(child, self.dir.axis);
if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.abs);
let rel_end = UiScalar::rel(len.rest / total.rest);
let end = (UiScalar::rel_max() + start) - offset;
start = rel_end.within(&start.to(end));
}
start.abs += len.abs;
start.rel += len.rel;
span.end = start;
let scalar = span.len();
*ctx.outer.axis_mut(self.dir.axis) = outer.select_len(scalar);
let ortho = ctx.len_axis(child, !self.dir.axis);
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
if ortho.rel > 0.0 || ortho.rest > 0.0 {
ortho_len.rest = 1.0;
ortho_len.abs = 0.0;
break;
}
ortho_len.abs = ortho_len.abs.max(ortho.abs);
start.abs += self.gap;
}
ortho_len
} else {
let mut ortho_len = Len::ZERO;
let ortho = !self.dir.axis;
for child in &self.children {
let len = ctx.len_axis(child, ortho);
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
if len.rel > 0.0 || len.rest > 0.0 {
ortho_len.rest = 1.0;
ortho_len.abs = 0.0;
break;
}
ortho_len.abs = ortho_len.abs.max(len.abs);
}
ortho_len
}
}
} }
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> { pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
+16 -21
View File
@@ -8,30 +8,25 @@ pub struct Stack {
} }
impl Widget for Stack { impl Widget for Stack {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
let mut iter = self.children.iter(); let sizing = match self.size {
if let Some(child) = iter.next() { StackSize::Default => None,
painter.child_layer(); StackSize::Child(i) => Some(i),
painter.widget(child); };
let mut size = Size::default();
for (i, child) in self.children.iter().enumerate() {
match i {
0 => painter.child_layer(),
_ => painter.next_layer(),
} }
for child in iter { let drawn = painter.widget(child);
painter.next_layer(); // Only the child that sizes the stack is read, so the others
painter.widget(child); // changing size does not redraw it.
if sizing == Some(i) {
size = drawn.size();
} }
} }
size
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
match self.size {
StackSize::Default => Len::default(),
StackSize::Child(i) => ctx.width(&self.children[i]),
}
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
match self.size {
StackSize::Default => Len::default(),
StackSize::Child(i) => ctx.height(&self.children[i]),
}
} }
} }
+4 -19
View File
@@ -6,25 +6,10 @@ pub struct WidgetPtr {
} }
impl Widget for WidgetPtr { impl Widget for WidgetPtr {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
if let Some(id) = &self.inner { match &self.inner {
painter.widget(id); Some(id) => painter.widget(id).size(),
} None => Size::default(),
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
if let Some(id) = &self.inner {
ctx.width(id)
} else {
Len::ZERO
}
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
if let Some(id) = &self.inner {
ctx.height(id)
} else {
Len::ZERO
} }
} }
} }
+7 -5
View File
@@ -28,21 +28,23 @@ impl Rect {
} }
impl Widget for Rect { impl Widget for Rect {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.primitive(RectPrimitive { painter.primitive(RectPrimitive {
color: self.color, color: self.color,
radius: self.radius, radius: self.radius,
thickness: self.thickness, thickness: self.thickness,
inner_radius: self.inner_radius, inner_radius: self.inner_radius,
}); });
Size::REST
} }
fn desired_width(&mut self, _: &mut SizeCtx) -> Len { fn size_hint(&self, _: Axis) -> Option<Len> {
Len::rest(1) Some(Len::REST)
} }
fn desired_height(&mut self, _: &mut SizeCtx) -> Len { /// Its box is its primitive's own region, so a new one is written there.
Len::rest(1) fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
} }
} }
+16 -13
View File
@@ -55,44 +55,47 @@ impl TextEdit {
} }
impl Widget for TextEdit { impl Widget for TextEdit {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
let base = painter.layer; let base = painter.layer;
painter.child_layer(); painter.child_layer();
self.view.draw(painter); let (_, size) = self.view.draw(painter);
painter.layer = base; painter.layer = base;
let region = self.region(); let region = self.region();
let Some(selection) = self.selection else { let Some(selection) = self.selection else {
return; return size;
}; };
let layout = self.view.buf.layout(); let layout = self.view.buf.layout();
// parley reports selection as boxes in layout space, so bidi and // parley reports selection as boxes in layout space, so bidi and
// wrapped lines come out right without this code knowing about either. // wrapped lines come out right without this code knowing about either.
for (rect, _) in selection.geometry(layout) { for (rect, _) in selection.geometry(layout) {
let size = vec2(rect.width() as f32, rect.height() as f32); let rect_size = vec2(rect.width() as f32, rect.height() as f32);
let top_left = vec2(rect.x0 as f32, rect.y0 as f32); let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
painter.primitive_within( painter.primitive_within(
RectPrimitive::color(Color::SKY), RectPrimitive::color(Color::SKY),
size.align(Align::TOP_LEFT).offset(top_left).within(&region), rect_size
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
); );
} }
let caret = selection.focus().geometry(layout, CARET_WIDTH); let caret = selection.focus().geometry(layout, CARET_WIDTH);
let size = vec2(caret.width() as f32, caret.height() as f32); let caret_size = vec2(caret.width() as f32, caret.height() as f32);
let top_left = vec2(caret.x0 as f32, caret.y0 as f32); let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
painter.primitive_within( painter.primitive_within(
RectPrimitive::color(Color::WHITE), RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(top_left).within(&region), caret_size
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
); );
size
} }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { fn on_resize(&self, axis: Axis) -> OnResize {
self.view.desired_width(ctx) self.view.on_resize(axis)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.view.desired_height(ctx)
} }
} }
+36 -41
View File
@@ -52,15 +52,15 @@ impl TextView {
.align(self.align) .align(self.align)
} }
fn render(&mut self, ctx: &mut SizeCtx) -> &RenderedText { fn render(&mut self, painter: &mut Painter) -> &RenderedText {
let width = if self.attrs.wrap { let width = if self.attrs.wrap {
Some(ctx.px_size().x) Some(painter.px_size().x)
} else { } else {
None None
}; };
if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed { if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
self.width = width; self.width = width;
self.tex = Some(ctx.draw_text(&mut self.buf, &self.attrs, width)); self.tex = Some(painter.render_text(&mut self.buf, &self.attrs, width));
self.attrs.changed = false; self.attrs.changed = false;
self.buf.changed = false; self.buf.changed = false;
} }
@@ -69,39 +69,40 @@ impl TextView {
pub fn tex(&self) -> Option<&RenderedText> { pub fn tex(&self) -> Option<&RenderedText> {
self.tex.as_ref() self.tex.as_ref()
} }
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { /// Draws the text, and says where the glyphs went and what they use.
if self.is_empty() pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) {
&& let Some(hint) = &self.hint
{
ctx.width(hint)
} else {
Len::abs(self.render(ctx).size.x)
}
}
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
if self.is_empty()
&& let Some(hint) = &self.hint
{
ctx.height(hint)
} else {
Len::abs(self.render(ctx).size.y)
}
}
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
let align = self.align; let align = self.align;
if self.is_empty() && self.hint.is_some() { if self.is_empty() && self.hint.is_some() {
let region = self.render(&mut painter.size_ctx()).size.align(align); let region = self.render(painter).size.align(align);
if let Some(hint) = &self.hint { let size = match &self.hint {
painter.widget(hint); Some(hint) => painter.widget(hint).size(),
} None => Size::ZERO,
return region; };
return (region, size);
} }
let tex = self.render(&mut painter.size_ctx()); let tex = self.render(painter);
let region = tex.size.align(align); let region = tex.size.align(align);
let size = Size::abs(tex.size);
let within = region.within(&painter.region()); let within = region.within(&painter.region());
painter.glyphs(tex, within); painter.glyphs(tex, within);
region (region, size)
}
/// Wrapping reads the width it is offered, so a wider box reshapes it and
/// a taller one does not. Alignment matters too, and separately: glyphs
/// anchored to the start of an axis stay put when that extent changes,
/// but centred or end-aligned ones move even though the shaping stands.
pub fn on_resize(&self, axis: Axis) -> OnResize {
let reshapes = axis == Axis::X && self.attrs.wrap;
let anchored = match axis {
Axis::X => self.align.x,
Axis::Y => self.align.y,
} == AxisAlign::Neg;
match reshapes || !anchored {
true => OnResize::Redraw,
false => OnResize::Translate,
}
} }
pub fn content(&self) -> String { pub fn content(&self) -> String {
@@ -117,7 +118,7 @@ impl Text {
content: content.into(), content: content.into(),
} }
} }
fn update_buf(&mut self, _ctx: &mut SizeCtx) { fn update_buf(&mut self) {
if self.content.changed { if self.content.changed {
self.content.changed = false; self.content.changed = false;
self.view.buf.set_text(self.content.as_str()); self.view.buf.set_text(self.content.as_str());
@@ -126,19 +127,13 @@ impl Text {
} }
impl Widget for Text { impl Widget for Text {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
self.update_buf(&mut painter.size_ctx()); self.update_buf();
self.view.draw(painter); self.view.draw(painter).1
} }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { fn on_resize(&self, axis: Axis) -> OnResize {
self.update_buf(ctx); self.view.on_resize(axis)
self.view.desired_width(ctx)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.update_buf(ctx);
self.view.desired_height(ctx)
} }
} }
+77
View File
@@ -32,3 +32,80 @@ fn resizing_relays_out_against_the_new_output() {
assert_corners!(h, left, (0, 0), (100, 100)); assert_corners!(h, left, (0, 0), (100, 100));
assert_corners!(h, right, (100, 0), (800, 100)); assert_corners!(h, right, (100, 0), (800, 100));
} }
#[test]
fn an_empty_widget_takes_a_share_of_a_span() {
let mut h = Harness::new((400, 200));
let gap = ().add(&mut h.rsc);
let right = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((gap, right).span(Dir::RIGHT));
assert_corners!(h, gap, (0, 0), (300, 200));
assert_corners!(h, right, (300, 0), (400, 200));
}
#[test]
fn a_child_drawn_twice_moves_once() {
let mut h = Harness::new((400, 200));
// `Aligned` draws its child twice; listing it twice would move it twice.
let inner = rect(Color::BLUE).add(&mut h.rsc);
let centered = inner.center().width(200).add(&mut h.rsc);
let left = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((left, centered).span(Dir::RIGHT));
assert_corners!(h, inner, (100, 0), (300, 200));
h.rsc[left].x = Some(Len::abs(150));
h.frame();
assert_corners!(h, inner, (150, 0), (350, 200));
}
#[test]
fn a_resize_lands_where_a_cold_start_would() {
let build = |h: &mut Harness| {
let para = wtext(
"Wrapping shapes one source into as many lines as its container leaves room \
for, so the height of a paragraph is an answer rather than a setting.",
)
.size(20)
.wrap(true)
.pad(16)
.add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
let root = (para, below).span(Dir::DOWN).pad(12);
h.set_root(root);
(para, below)
};
let mut cold = Harness::new((900, 1200));
let (cold_para, cold_below) = build(&mut cold);
let mut resized = Harness::new((1920, 1200));
let (para, below) = build(&mut resized);
resized.resize((900, 1200));
resized.frame();
assert_eq!(resized.region(&para), cold.region(&cold_para), "paragraph");
assert_eq!(resized.region(&below), cold.region(&cold_below), "below");
}
#[test]
fn a_fixed_box_is_drawn_again_rather_than_stretched() {
let mut h = Harness::new((400, 400));
// The panel fills a stack sized by its sibling, so it is drawn in the
// whole box and then placed in the shorter one. Reusing it in that fixed
// box afterwards would leave it whatever height it happened to have.
let panel = rect(Color::BLUE).add(&mut h.rsc);
let leaf = rect(Color::RED).height(100).add(&mut h.rsc);
let stack = (panel, leaf)
.stack()
.size(StackSize::Child(1))
.add(&mut h.rsc);
h.set_root(stack.align(Align::TOP));
assert_corners!(h, panel, (0, 0), (400, 100));
h.rsc[leaf].y = Some(Len::abs(250));
h.frame();
assert_corners!(h, panel, (0, 0), (400, 250));
}
+27
View File
@@ -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));
}
+252
View File
@@ -0,0 +1,252 @@
//! What a second frame draws again, and what it keeps.
use std::{cell::Cell, rc::Rc};
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
/// A leaf that counts its draws and reports whatever size it is given, so a
/// test can see what the retained path skipped.
struct Counted {
draws: Rc<Cell<usize>>,
size: Size,
dependence: OnResize,
}
impl Widget for Counted {
fn draw(&mut self, _: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
self.size
}
fn on_resize(&self, _: Axis) -> OnResize {
self.dependence
}
}
struct Counts(Rc<Cell<usize>>);
impl Counts {
fn get(&self) -> usize {
self.0.get()
}
}
fn counted(h: &mut Harness, size: Size, dependence: OnResize) -> (WeakWidget<Counted>, Counts) {
let draws = Rc::new(Cell::new(0));
let id = Counted {
draws: draws.clone(),
size,
dependence,
}
.add(&mut h.rsc);
(id, Counts(draws))
}
/// A fixed-width leaf beside one that takes the rest, so changing the first
/// hands the second a different box without the output changing.
fn pair(h: &mut Harness, rest: OnResize) -> (WeakWidget<Counted>, Counts, WidgetId) {
let (first, _) = counted(h, Size::from((100, 200)), OnResize::Translate);
let (second, draws) = counted(h, Size::REST, rest);
h.set_root((first, second).span(Dir::RIGHT));
(first, draws, second.id())
}
#[test]
fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, OnResize::Scale);
let settled = draws.get();
assert_corners!(h, second, (100, 0), (400, 200));
h.rsc[first].size = Size::from((150, 200));
h.frame();
assert_eq!(
draws.get(),
settled,
"its box is a field to write, not a reason to draw"
);
assert_corners!(h, second, (150, 0), (400, 200));
}
#[test]
fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, OnResize::Redraw);
let settled = draws.get();
h.rsc[first].size = Size::from((150, 200));
h.frame();
// Twice: once for the span to measure it, once for its real box. A child
// that can hint its length is spared the first, and a smaller number here
// means someone has made that cheaper rather than broken it.
assert_eq!(draws.get(), settled + 2);
assert_corners!(h, second, (150, 0), (400, 200));
}
#[test]
fn a_span_child_that_declares_its_length_is_drawn_once() {
let mut h = Harness::new((400, 200));
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
// The span takes one child's length from its hint and has to draw the
// other to find out, so only the second is drawn before it is placed.
let hinted = told.width(100).add(&mut h.rsc);
h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1);
assert_eq!(
asked_draws.get(),
2,
"drawn to be measured, then again to be placed"
);
}
#[test]
fn a_span_relays_out_when_a_child_it_measured_changes() {
let mut h = Harness::new((400, 200));
let (first, _, second) = pair(&mut h, OnResize::Translate);
h.rsc[first].size = Size::from((250, 200));
h.frame();
assert_corners!(h, first, (0, 0), (250, 200));
assert_corners!(h, second, (250, 0), (400, 200));
}
#[test]
fn a_placed_child_survives_the_next_frame() {
let mut h = Harness::new((400, 200));
// Both children declare a length, so the span places them from their hints
// rather than drawing them to find out.
let top = rect(Color::RED).height(80).add(&mut h.rsc);
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
h.set_root((top, bottom).span(Dir::DOWN));
h.rsc.widgets_mut().get_dyn_mut(top.id());
h.frame();
assert_corners!(h, top, (0, 0), (400, 80));
assert_corners!(h, bottom, (0, 80), (400, 200));
}
/// Lays its child out from the hint alone, never reading what it drew.
struct FromHint {
inner: StrongWidget,
}
impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let mut region = UiRegion::FULL;
region.y.end = region.y.start.offset(len.abs);
painter.widget_within(&self.inner, region);
Size::REST
}
}
#[test]
fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::RED).height(80).add(&mut h.rsc);
let parent = FromHint {
inner: inner.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
h.set_root(parent);
assert_corners!(h, inner, (0, 0), (400, 80));
h.rsc[inner].y = Some(Len::abs(120));
h.frame();
assert_corners!(h, inner, (0, 0), (400, 120));
}
/// Reads the output's size, which nothing but its own draw can put right.
struct ReadsOutput {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsOutput {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
Size::abs(painter.output_size() / 4.0)
}
}
#[test]
fn a_resize_does_not_redraw_what_the_shader_can_move() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
assert!(h.needs_redraw());
h.frame();
assert_eq!(
draws.get(),
settled,
"its box is the same fraction of a different output"
);
assert_corners!(h, leaf, (0, 0), (800, 100));
}
#[test]
fn a_resize_redraws_what_read_the_output() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsOutput {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
h.frame();
assert_eq!(draws.get(), settled + 1);
}
#[test]
fn narrowing_the_output_reflows_text_and_relays_out_around_it() {
let mut h = Harness::new((600, 400));
let para = wtext(
"Wrapping shapes one source into as many lines as its container leaves \
room for, so the height of a paragraph is an answer rather than a setting.",
)
.size(20)
.wrap(true)
.add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((para, below).span(Dir::DOWN));
let top = h.region(&below).expect("drew nothing").top_left.y;
h.resize((300, 400));
h.frame();
let lower = h.region(&below).expect("drew nothing").top_left.y;
assert!(lower > top, "same words, half the width: {top} -> {lower}");
}
#[test]
fn a_change_two_levels_under_its_reader_still_reaches_it() {
let mut h = Harness::new((400, 400));
// Every wrapper up to the outer pad read the size below it, so the outer
// pad is what draws again -- and the span it hands the box to is the same
// size as before, which is what lets a draw reuse its way past the leaf.
let (leaf, _) = counted(&mut h, Size::abs((100, 100).into()), OnResize::Redraw);
let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).pad(12));
assert_corners!(h, below, (12, 132), (388, 388));
h.rsc[leaf].size = Size::abs((100, 200).into());
h.frame();
assert_corners!(h, below, (12, 232), (388, 388));
}