Compare commits

..
9 Commits
Author SHA1 Message Date
iris fa771a99eb update stuff 2026-08-03 20:33:59 -04:00
iris e817bc83af const trait keyword order 2026-06-23 21:30:09 -04:00
iris a648c62aa2 update 2026-04-15 20:31:52 -04:00
iris c118bb446b some potentially nice trait stuff 2026-03-15 21:07:06 -04:00
iris 1102dc7338 work 2026-02-26 19:18:27 -05:00
iris 1aadef0e7e fix Draw (redraw) 2026-02-21 00:19:39 -05:00
iris 426ff0adfc oop 2026-02-18 16:49:59 -05:00
iris dab6cf298a Merge branch 'work' of git.arirex.me:shadowcat/iris into work 2026-02-17 18:14:38 -05:00
iris 38d896d44d selector 2026-02-17 18:14:19 -05:00
117 changed files with 3585 additions and 15782 deletions

No files matched your search

Generated
+598 -761
View File
File diff suppressed because it is too large. Load diff
+8 -19
View File
@@ -3,15 +3,13 @@ name = "iris"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
[features]
layout-diagnostics = ["iris-core/layout-diagnostics"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
iris-core = { workspace = true } iris-core = { workspace = true }
iris-macro = { workspace = true } iris-macro = { workspace = true }
parley = { workspace = true } cosmic-text = { workspace = true }
unicode-segmentation = { workspace = true }
winit = { workspace = true } winit = { workspace = true }
arboard = { workspace = true, features = ["wayland-data-control"] } arboard = { workspace = true, features = ["wayland-data-control"] }
pollster = { workspace = true } pollster = { workspace = true }
@@ -23,31 +21,22 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] } tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
[workspace] [workspace]
members = ["core", "macro", "rig-input"] members = ["core", "macro"]
[profile.dev]
debug = 1
[profile.test]
debug = "line-tables-only"
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[workspace.dependencies] [workspace.dependencies]
pollster = "0.4.0" pollster = "1.0.1"
winit = "0.30.12" winit = "0.30.12"
wgpu = "30.0.1" wgpu = "30.0.0"
bytemuck = "1.23.1" bytemuck = "1.23.1"
image = "0.25.10" image = "0.25.6"
parley = "0.11.1" cosmic-text = "0.16.0"
swash = "0.2.10" unicode-segmentation = "1.12.0"
fxhash = "0.2.1" fxhash = "0.2.1"
log = "0.4.29"
arboard = "3.6.1" arboard = "3.6.1"
iris-core = { path = "core" } iris-core = { path = "core" }
iris-macro = { path = "macro" } iris-macro = { path = "macro" }
tokio = "1.49.0" tokio = "1.49.0"
wayland-client = "0.31.15"
wayland-protocols-wlr = { version = "0.3.12", features = ["client"] }
+25 -11
View File
@@ -1,6 +1,19 @@
images images
settings (sampler) settings (sampler)
consider typed TextureHandle<T> variants for distinct texture uses
text
figure out ways to speed up / what costs the most
resizing (per frame) is really slow (assuming painter isn't griefing)
j is weird / fix x offset
masks r just made to bare minimum work
scaling
could be just a simple scaling factor that multiplies abs
and need to ensure text uses raw abs and not scaled abs
naming? (pt, px)
want to keep (drawn) regions using px? or should I add another field to UiScalar/Vec
field could be best solution so redrawing stuff isn't needed & you can specify both as user
WidgetRef<W> or smth instead of Id WidgetRef<W> or smth instead of Id
enum that's either an Id or an actual concrete instance of W enum that's either an Id or an actual concrete instance of W
@@ -11,16 +24,17 @@ WidgetRef<W> or smth instead of Id
maybe introduce InnerWidget trait to allow for editors to expose & modify inner type maybe introduce InnerWidget trait to allow for editors to expose & modify inner type
maybe could also store a parent widget and keep using InnerWidget trait? unsure if possible maybe could also store a parent widget and keep using InnerWidget trait? unsure if possible
really weird limitation:
I don't think you can currently remove an element from a parent and put it in a child of the same parent
because it removes the unused children after the entire parent redraw
but the child gets drawn during that, so it will think the child is still active !!!
or something like that idk, maybe I need a special enum for parent that includes a undecided state where it may or may not get redrawn by the parent
or just do ref counting and ensure all drawn things == 1 afterwards (seems like best way)
ok so I'm removing the limit for now
don't forget I'm streaming
tags
vecs for each widget type? vecs for each widget type?
POTENTIAL BUG: closures that store IDs will not decrement the id!!! need to not increment id if moved into closure somehow??? wait no, need to decrement ID every time an event fn is added...... only if the id is used in it..?? POTENTIAL BUG: closures that store IDs will not decrement the id!!! need to not increment id if moved into closure somehow??? wait no, need to decrement ID every time an event fn is added...... only if the id is used in it..??
transforms on a move entry (scale + rotation)
an entry is a translation today; composing through one scales the rel
part and passes px through untouched, so fixed-size content and glyphs
do not follow a shortened entry
want a real transform per entry, resolved in resolve_move the way the
translation already is, so a whole subtree transforms with one buffer
write and no redraw
wanted for compose-style stretch at the end of a scroll area, and for
rotation generally
+2 -6
View File
@@ -3,14 +3,10 @@ name = "iris-core"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
[features]
layout-diagnostics = []
[dependencies] [dependencies]
winit = { workspace = true }
wgpu = { workspace = true } wgpu = { workspace = true }
bytemuck ={ workspace = true } bytemuck ={ workspace = true }
image = { workspace = true } image = { workspace = true }
parley = { workspace = true } cosmic-text = { workspace = true }
swash = { workspace = true }
fxhash = { workspace = true } fxhash = { workspace = true }
log = { workspace = true }
+1 -1
View File
@@ -1,4 +1,4 @@
use crate::{UiRsc, WeakWidget, WidgetIdFn, WidgetLike}; use crate::{UiRsc, WidgetIdFn, WidgetLike, WeakWidget};
pub trait WidgetAttr<Rsc, W: ?Sized> { pub trait WidgetAttr<Rsc, W: ?Sized> {
type Input; type Input;
+1 -6
View File
@@ -79,7 +79,6 @@ type EventData<Rsc, E> = (E, Rc<dyn for<'a> EventFn<Rsc, <E as Event>::Data<'a>>
pub struct TypeEventManager<Rsc: HasEvents, E: Event> { pub struct TypeEventManager<Rsc: HasEvents, E: Event> {
// TODO: reduce visiblity!! // TODO: reduce visiblity!!
pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>, pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>,
pub global: E::Global,
map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>, map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>,
} }
@@ -108,7 +107,6 @@ impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
fn default() -> Self { fn default() -> Self {
Self { Self {
active: Default::default(), active: Default::default(),
global: Default::default(),
map: Default::default(), map: Default::default(),
} }
} }
@@ -140,13 +138,11 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
pub fn run_fn<'a>( pub fn run_fn<'a>(
&mut self, &mut self,
id: impl IdLike, id: impl IdLike,
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) -> bool + 'a { ) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) + 'a {
let fs = self.map.get(&id.id()).cloned().unwrap_or_default(); let fs = self.map.get(&id.id()).cloned().unwrap_or_default();
move |ctx, rsc| { move |ctx, rsc| {
let mut consumed = false;
for (e, f) in fs { for (e, f) in fs {
if let Some(data) = e.should_run(&ctx.data) { if let Some(data) = e.should_run(&ctx.data) {
consumed |= e.consumes(&data);
f( f(
EventCtx { EventCtx {
state: ctx.state, state: ctx.state,
@@ -156,7 +152,6 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
) )
} }
} }
consumed
} }
} }
} }
-9
View File
@@ -9,19 +9,10 @@ pub use rsc::*;
pub trait Event: Sized + 'static + Clone { pub trait Event: Sized + 'static + Clone {
type Data<'a>: Clone = (); type Data<'a>: Clone = ();
type State: Default = (); type State: Default = ();
/// State the whole event type keeps, rather than one copy per widget.
type Global: Default = ();
#[allow(unused_variables)] #[allow(unused_variables)]
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> { fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
Some(data.clone()) Some(data.clone())
} }
/// Whether having run on this data uses up whatever triggered it, so
/// nothing further should see it.
#[allow(unused_variables)]
fn consumes(&self, data: &Self::Data<'_>) -> bool {
false
}
} }
pub trait EventLike { pub trait EventLike {
+1 -2
View File
@@ -21,13 +21,12 @@ pub trait HasEvents: Sized + UiRsc + HasState {
} }
pub trait RunEvents: HasEvents { pub trait RunEvents: HasEvents {
/// Whether anything that ran used up what triggered it.
fn run_event<E: EventLike>( fn run_event<E: EventLike>(
&mut self, &mut self,
id: impl IdLike, id: impl IdLike,
data: <E::Event as Event>::Data<'_>, data: <E::Event as Event>::Data<'_>,
state: &mut Self::State, state: &mut Self::State,
) -> bool { ) {
let f = self.events_mut().get_type::<E>().run_fn(id); let f = self.events_mut().get_type::<E>().run_fn(id);
f(EventCtx { state, data }, self) f(EventCtx { state, data }, self)
} }
-574
View File
@@ -1,574 +0,0 @@
use crate::{UiNum, util::Vec2};
use std::{
fmt::{Debug, Display, Formatter},
ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign},
};
/// A number held as a whole count of `1 / 2^SHIFT`.
///
/// Layout reaches one place by more than one route -- a box composed down the
/// chain, and the same box summed from what its children asked for -- and has
/// to decide whether the two are the same place. In floats they land a few
/// bits apart, which is a defect wherever the answer changes what is drawn
/// rather than where. Here adding and subtracting are exact, a multiply
/// drops to the step below, and a conversion between grids takes the nearest
/// one, so two routes to one place land on one number and everything
/// downstream compares for equality instead of for nearness.
///
/// `SHIFT` is the number of fractional bits, which is what makes the steps
/// divide a whole number: a power of two also converts to `f32` without
/// rounding while the value fits in its mantissa.
///
/// Arithmetic wraps at the ends of the range, the way the `i32` underneath
/// does. Saturating instead was measured at a twelfth of layout's
/// instructions -- five per add against one -- to keep the ordering of
/// coordinates two million pixels out, where nothing draws anyway. A value
/// off the end is a defect either way; wrapping makes it an obvious one.
/// Only [`Self::from_f32`] clamps, since a float has further to come from.
#[repr(transparent)]
#[derive(
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, bytemuck::Pod, bytemuck::Zeroable,
)]
pub struct Fixed<const SHIFT: u32>(i32);
/// A length or a coordinate in pixels, in steps of `1/1024`. Finer than
/// anything a display can show, and exact in `f32` up to 16,384 px, which is
/// what lets the same number reach the GPU.
pub type Px = Fixed<PX_SHIFT>;
/// How many bits of a pixel a [`Px`] keeps. One place, because [`PxVec2`]
/// and the shader's own decoding are the same grid or nothing lines up.
pub const PX_SHIFT: u32 = 10;
/// A share of what a box has left over, which is a weight beside its
/// siblings rather than a fraction of anything: a list divides its room by
/// the total of these, so the range has to hold a whole list's worth and the
/// precision only has to tell two weights apart.
pub type Weight = Fixed<16>;
/// A fraction of a box. Twenty-four bits of it, which matches `f32` around a
/// half and beats it above one -- where anchors actually sit -- and leaves
/// +/-128 of range, enough to sum a hundred children each asking for a whole
/// box. A `leftover` weight is not one of these: it is a share of what is
/// left rather than a fraction of anything, and it sums over a whole list.
pub type Rel = Fixed<REL_SHIFT>;
/// How many bits of a box a [`Rel`] keeps, beside [`PX_SHIFT`] and for the
/// same reason.
pub const REL_SHIFT: u32 = 24;
impl<const SHIFT: u32> Fixed<SHIFT> {
pub const ZERO: Self = Self(0);
pub const ONE: Self = Self::one();
/// The gap between neighbouring values, which is also how far apart two
/// numbers can be and still mean the same place.
pub const STEP: Self = Self(1);
/// Also what stands in for an unbounded end: compared against, never
/// added to, since arithmetic wraps past it.
pub const MIN: Self = Self(i32::MIN);
pub const MAX: Self = Self(i32::MAX);
const fn one() -> Self {
assert!(SHIFT < 31, "a Fixed needs a bit for the whole part");
Self(1 << SHIFT)
}
pub const fn from_raw(raw: i32) -> Self {
Self(raw)
}
/// The count of steps, for a caller that needs the representation rather
/// than the number.
pub const fn raw(self) -> i32 {
self.0
}
pub const fn from_int(v: i32) -> Self {
Self(v.wrapping_mul(Self::one().0))
}
/// Rounds to the nearest step, and clamps to the ends of the grid rather
/// than wrapping: this is where a number from outside arrives, and a float
/// has the range to be anywhere. A NaN has no nearest step and becomes
/// zero, which is a caller's mistake rather than a value worth carrying.
///
/// Half-away is written out rather than called through `f32::round`,
/// which is not `const`: a layout constant has to stay a constant.
pub const fn from_f32(v: f32) -> Self {
debug_assert!(!v.is_nan(), "a NaN has no place on the grid");
let scaled = v * Self::one().0 as f32;
// Above 2^23 an `f32` has no fractional part left to round, and
// adding a half there rounds the number itself up instead. The cast
// saturates at both ends and sends NaN to zero, which is the
// behaviour wanted at both.
const WHOLE: f32 = (1 << 23) as f32;
Self(match (scaled >= WHOLE, scaled <= -WHOLE, scaled < 0.0) {
(true, _, _) | (_, true, _) => scaled as i32,
(_, _, true) => (scaled - 0.5) as i32,
_ => (scaled + 0.5) as i32,
})
}
/// The first step at or above `v`, where [`Self::from_f32`] takes the
/// nearest one and is below it half the time. For a bound that has to
/// admit the value it came from: a measurement rounded down is a bound
/// that leaves out the thing it was measured from.
pub const fn ceil_from_f32(v: f32) -> Self {
let nearest = Self::from_f32(v);
match nearest.to_f32() < v {
true => nearest.next_up(),
false => nearest,
}
}
/// From a number as it is written in source -- `16`, `1.5` -- which is
/// the other place a value enters the grid.
pub fn from_num(v: impl UiNum) -> Self {
Self::from_f32(v.to_f32())
}
pub const fn to_f32(self) -> f32 {
self.0 as f32 / Self::one().0 as f32
}
/// The same value on another grid, rounded where the new one is coarser.
pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> {
Fixed(match TO >= SHIFT {
true => self.0 << (TO - SHIFT),
false => shift_round(self.0 as i64, SHIFT - TO) as i32,
})
}
pub const fn add(self, rhs: Self) -> Self {
Self(self.0.wrapping_add(rhs.0))
}
pub const fn sub(self, rhs: Self) -> Self {
Self(self.0.wrapping_sub(rhs.0))
}
pub const fn neg(self) -> Self {
Self(self.0.wrapping_neg())
}
/// Scaled by a number on any grid, which is how a length takes a fraction
/// of itself and keeps being a length: the product is measured in the
/// receiver's steps.
///
/// Dropped to the step below rather than taken to the nearest one
/// (Bryan, 2026-09-16), which costs a share a thousandth of a pixel of
/// its row -- less than an even number of pixels draws. Toward negative
/// infinity on both sides of zero, since that is a shift and nothing
/// else: a value and its negation therefore land different distances
/// from where they came, so a flipped span can sit a step from its
/// mirror image.
pub const fn mul<const BY: u32>(self, by: Fixed<BY>) -> Self {
Self(((self.0 as i64 * by.0 as i64) >> BY) as i32)
}
/// Repeated a whole number of times, which no grid rounds.
pub const fn mul_int(self, by: i32) -> Self {
Self(self.0.wrapping_mul(by))
}
/// Divided into a whole number of parts, rounded to the nearest step.
pub const fn div_int(self, by: i32) -> Self {
debug_assert!(by != 0, "no part of nothing");
if by == 0 {
return Self::ZERO;
}
Self(div_round(self.0 as i64, by as i64) as i32)
}
/// Divided by a number on any grid. A zero divisor is a caller bug -- a
/// box of no length has no fraction of itself -- and answers with the end
/// of the range so that a release build lays out something absurd rather
/// than dying.
pub const fn div<const BY: u32>(self, by: Fixed<BY>) -> Self {
debug_assert!(by.0 != 0, "dividing by a length of zero");
if by.0 == 0 {
return match self.0 < 0 {
true => Self::MIN,
false => Self::MAX,
};
}
Self(div_round((self.0 as i64) << BY, by.0 as i64) as i32)
}
/// `num / den` on *this* grid rather than on theirs, for weights coarser
/// than the share they divide.
pub const fn ratio<const OF: u32>(num: Fixed<OF>, den: Fixed<OF>) -> Self {
debug_assert!(den.0 != 0, "no part of a whole of nothing");
if den.0 == 0 {
return Self::ZERO;
}
Self(div_round((num.0 as i64) << SHIFT, den.0 as i64) as i32)
}
/// `from` and `to` a fraction of the way apart, the fraction being the
/// receiver -- the argument order [`crate::util::LerpUtil`] already uses.
pub const fn lerp<const OF: u32>(self, from: Fixed<OF>, to: Fixed<OF>) -> Fixed<OF> {
from.add(to.sub(from).mul(self))
}
pub const fn min(self, other: Self) -> Self {
match self.0 < other.0 {
true => self,
false => other,
}
}
pub const fn max(self, other: Self) -> Self {
match self.0 > other.0 {
true => self,
false => other,
}
}
pub const fn abs(self) -> Self {
Self(self.0.wrapping_abs())
}
pub const fn clamp(self, lo: Self, hi: Self) -> Self {
debug_assert!(lo.0 <= hi.0, "an empty clamp has no answer");
self.max(lo).min(hi)
}
/// The next value along, for an interval that must not admit its own
/// boundary. The step is the whole gap, so there is nothing to exclude
/// between this and the boundary itself.
pub const fn next_up(self) -> Self {
Self(self.0.wrapping_add(1))
}
pub const fn next_down(self) -> Self {
Self(self.0.wrapping_sub(1))
}
}
/// Back to a single step, rounding halves away from zero so that a value and
/// its negation round to the same distance.
const fn shift_round(v: i64, bits: u32) -> i64 {
let half = (1i64 << bits) >> 1;
match v < 0 {
true => -((-v + half) >> bits),
false => (v + half) >> bits,
}
}
const fn div_round(num: i64, den: i64) -> i64 {
let (q, rem) = (num / den, num % den);
match rem.unsigned_abs() * 2 >= den.unsigned_abs() {
true => match (num < 0) == (den < 0) {
true => q + 1,
false => q - 1,
},
false => q,
}
}
/// Toward positive infinity when `up`, toward negative infinity otherwise.
pub(crate) const fn div_toward(num: i64, den: i64, up: bool) -> i64 {
let (q, rem) = (num / den, num % den);
if rem == 0 {
return q;
}
match (rem < 0) == (den < 0) {
true => q + up as i64,
false => q - !up as i64,
}
}
/// Clamped to the ends, unlike a [`Fixed`]'s own arithmetic: a range of box
/// lengths that runs past `i32` really is unbounded.
pub(crate) const fn narrow(v: i64) -> i32 {
if v > i32::MAX as i64 {
return i32::MAX;
}
if v < i32::MIN as i64 {
return i32::MIN;
}
v as i32
}
const impl<const SHIFT: u32> Add for Fixed<SHIFT> {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Fixed::add(self, rhs)
}
}
const impl<const SHIFT: u32> Sub for Fixed<SHIFT> {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Fixed::sub(self, rhs)
}
}
const impl<const SHIFT: u32> Neg for Fixed<SHIFT> {
type Output = Self;
fn neg(self) -> Self {
Fixed::neg(self)
}
}
const impl<const SHIFT: u32> AddAssign for Fixed<SHIFT> {
fn add_assign(&mut self, rhs: Self) {
*self = Fixed::add(*self, rhs);
}
}
const impl<const SHIFT: u32> SubAssign for Fixed<SHIFT> {
fn sub_assign(&mut self, rhs: Self) {
*self = Fixed::sub(*self, rhs);
}
}
const impl<const SHIFT: u32, const BY: u32> Mul<Fixed<BY>> for Fixed<SHIFT> {
type Output = Self;
fn mul(self, rhs: Fixed<BY>) -> Self {
Fixed::mul(self, rhs)
}
}
const impl<const SHIFT: u32, const BY: u32> Div<Fixed<BY>> for Fixed<SHIFT> {
type Output = Self;
fn div(self, rhs: Fixed<BY>) -> Self {
Fixed::div(self, rhs)
}
}
impl<const SHIFT: u32> Display for Fixed<SHIFT> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.to_f32(), f)
}
}
/// Prints the number rather than the count of steps: a failing layout test
/// reports boxes, and `1126` is not a height anybody can read.
impl<const SHIFT: u32> Debug for Fixed<SHIFT> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.to_f32(), f)
}
}
/// Two of them, for the places a size or a position needs both axes: a
/// window, a box in pixels, a pointer. Held apart from [`crate::util::Vec2`]
/// because that one is what the GPU and the platform speak.
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct FixedVec2<const SHIFT: u32> {
pub x: Fixed<SHIFT>,
pub y: Fixed<SHIFT>,
}
pub type PxVec2 = FixedVec2<PX_SHIFT>;
impl<const SHIFT: u32> FixedVec2<SHIFT> {
pub const ZERO: Self = Self::splat(Fixed::ZERO);
pub const fn new(x: Fixed<SHIFT>, y: Fixed<SHIFT>) -> Self {
Self { x, y }
}
pub const fn splat(v: Fixed<SHIFT>) -> Self {
Self { x: v, y: v }
}
pub fn from_f32(v: Vec2) -> Self {
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
}
/// The first step at or above each part, for a measurement reported as a
/// box: what it occupies is not less than what was measured.
pub fn ceil_from_f32(v: Vec2) -> Self {
Self::new(Fixed::ceil_from_f32(v.x), Fixed::ceil_from_f32(v.y))
}
pub fn to_f32(self) -> Vec2 {
Vec2::new(self.x.to_f32(), self.y.to_f32())
}
pub const fn div_int(self, by: i32) -> Self {
Self::new(self.x.div_int(by), self.y.div_int(by))
}
pub const fn min(self, other: Self) -> Self {
Self::new(self.x.min(other.x), self.y.min(other.y))
}
pub const fn max(self, other: Self) -> Self {
Self::new(self.x.max(other.x), self.y.max(other.y))
}
}
// `impl_op!` names one concrete type, and this one is generic.
const impl<const SHIFT: u32> Add for FixedVec2<SHIFT> {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self::new(self.x.add(rhs.x), self.y.add(rhs.y))
}
}
const impl<const SHIFT: u32> Sub for FixedVec2<SHIFT> {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self::new(self.x.sub(rhs.x), self.y.sub(rhs.y))
}
}
const impl<const SHIFT: u32> AddAssign for FixedVec2<SHIFT> {
fn add_assign(&mut self, rhs: Self) {
*self = Add::add(*self, rhs);
}
}
const impl<const SHIFT: u32> SubAssign for FixedVec2<SHIFT> {
fn sub_assign(&mut self, rhs: Self) {
*self = Sub::sub(*self, rhs);
}
}
impl<const SHIFT: u32> Debug for FixedVec2<SHIFT> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl<const SHIFT: u32> Display for FixedVec2<SHIFT> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_sum_of_steps_does_not_drift() {
let mut at = Px::ZERO;
for _ in 0..20_000 {
at += Px::from_raw(3);
}
assert_eq!(at, Px::from_raw(60_000));
for _ in 0..20_000 {
at -= Px::from_raw(3);
}
assert_eq!(at, Px::ZERO);
}
#[test]
fn a_pixel_survives_the_trip_through_f32() {
for raw in [0, 1, -1, 64, -1000, 16_777_215, -16_777_215] {
let px = Px::from_raw(raw);
assert_eq!(Px::from_f32(px.to_f32()), px);
}
}
#[test]
fn a_fraction_of_a_length_is_a_length() {
let half = Px::from_int(100) * Rel::from_f32(0.5);
assert_eq!(half, Px::from_int(50));
assert_eq!(Px::from_int(100) * Rel::ONE, Px::from_int(100));
assert_eq!(Px::from_int(100) * Rel::ZERO, Px::ZERO);
}
/// Toward negative infinity on both sides of zero, which is what makes
/// it a shift rather than a shift and a sign branch -- and what makes a
/// value and its negation land different distances from where they came,
/// so a flipped span can sit a step from its mirror image.
#[test]
fn a_multiply_drops_to_the_step_below_on_both_sides_of_zero() {
// A step and a half of one, which has no step of its own.
let step_and_a_half = Rel::from_f32(1.5).div_int(Px::ONE.raw());
assert_eq!(Px::ONE * step_and_a_half, Px::from_raw(1));
assert_eq!(Px::ONE.neg() * step_and_a_half, Px::from_raw(-2));
}
/// A division rounds to the nearest step, so it cannot put back the
/// steps a truncating multiply dropped: a round trip comes back short,
/// never long, and by the few steps the two operations gave up.
#[test]
fn dividing_by_a_fraction_cannot_undo_a_truncating_multiply() {
let third = Rel::ONE / Rel::from_int(3);
let len = Px::from_int(300);
let back = len * third / third;
assert!(back <= len, "{back:?} is longer than {len:?}");
assert!(len - back <= Px::from_raw(3), "{back:?} against {len:?}");
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
}
/// The bound a greedy line break needs: the width it was measured at is
/// not on the grid, and the narrowest box the break still holds for is
/// the step at or above it, never the one below.
#[test]
fn a_ceiling_never_lands_below_the_number_it_came_from() {
let step = 1.0 / (1 << PX_SHIFT) as f32;
for n in 0..64 {
let v = 189.0 + n as f32 * step / 3.0;
let up = Px::ceil_from_f32(v);
assert!(up.to_f32() >= v, "{up:?} is below {v}");
assert!(
up.to_f32() - v < step,
"{up:?} is more than a step above {v}"
);
}
// An exact step is its own ceiling.
assert_eq!(Px::ceil_from_f32(189.5), Px::from_f32(189.5));
}
#[test]
fn a_number_from_outside_is_clamped_to_the_grid() {
assert_eq!(Px::from_f32(1e12), Px::MAX);
assert_eq!(Px::from_f32(-1e12), Px::MIN);
}
#[test]
fn a_coarser_grid_rounds_and_a_finer_one_does_not() {
// A third, which neither grid holds exactly.
let third = Rel::ONE / Rel::from_int(3);
assert_eq!(third.to_scale::<6>(), Fixed::<6>::from_raw(21));
let coarse = Fixed::<6>::from_raw(21);
assert_eq!(coarse.to_scale::<24>().to_scale::<6>(), coarse);
}
#[test]
fn lerp_takes_the_fraction_as_the_receiver() {
let (from, to) = (Px::from_int(10), Px::from_int(20));
assert_eq!(Rel::ZERO.lerp(from, to), from);
assert_eq!(Rel::ONE.lerp(from, to), to);
assert_eq!(Rel::from_f32(0.5).lerp(from, to), Px::from_int(15));
assert_eq!(Rel::from_f32(0.5).lerp(to, from), Px::from_int(15));
}
#[test]
fn a_ratio_is_finer_than_the_weights_it_divides() {
let (one, three) = (Weight::ONE, Weight::from_int(3));
// A third, which the weights' own grid could only hold to 1/65536.
assert_eq!(Rel::ratio(one, three), Rel::from_raw(5592405));
assert_eq!(Rel::ratio(three, three), Rel::ONE);
assert_eq!(Rel::ratio(Weight::ZERO, three), Rel::ZERO);
}
#[test]
fn nothing_sits_between_a_value_and_the_next_one() {
let at = Px::from_int(3);
assert_eq!(at.next_up().next_down(), at);
assert_eq!(at.next_up().raw() - at.raw(), 1);
assert!(at.next_down() < at && at < at.next_up());
}
#[test]
fn it_prints_the_number_rather_than_the_steps() {
assert_eq!(format!("{:?}", Px::from_f32(17.59375)), "17.59375");
assert_eq!(format!("{}", Px::from_int(-2)), "-2");
}
}
-485
View File
@@ -1,485 +0,0 @@
//! Opt-in counters and coarse timers for explaining CPU layout cost.
//!
//! Enable the `layout-diagnostics` feature. With it disabled, none of the
//! instrumentation is compiled into Iris. The retained rig in
//! `tests/layout_diagnostics.rs` is the ordinary entry point.
//!
//! Timers are inclusive: `update total` contains `full layout` or
//! `incremental layout`, and `text render` contains shaping and glyph
//! placement. They locate cost within one instrumented run and must not be
//! added together. Use an uninstrumented build under `perf` for final CPU
//! totals; counting every primitive and distinct widget deliberately perturbs
//! the instrumented run.
//!
//! Call [`trace_widget`] before a frame to retain the ordered constraint,
//! reuse, size, placement, and text events for one suspicious widget. The
//! selection is a set and survives [`take`] until cleared.
use crate::{Axis, LayoutLen, PxVec2, Size, UiRegion, WidgetId};
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
fmt::Write,
time::Instant,
};
#[derive(Clone, Copy)]
pub(crate) enum Counter {
Updates,
DrawRequests,
WidgetDraws,
RegionNodeDraws,
SizeReads,
HintHits,
HintMisses,
ReuseAttempts,
ReuseExact,
ReuseMoved,
ReuseDirty,
ReuseWrongParent,
ReuseRemapped,
ReuseOutside,
ReuseWrongLayer,
ReuseWrongNode,
QueuePops,
DepthReads,
LocalRedraws,
SizeChanges,
ReaderEdges,
PrimitiveWrites,
TextRenders,
TextShapeHits,
TextShapes,
TextBreaks,
GlyphPlacements,
OutsidePinnedLen,
OutsideRelBase,
OutsideRegion,
}
impl Counter {
const COUNT: usize = Self::OutsideRegion as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [
"updates",
"draw requests",
"widget draws",
"region-node draws",
"draw-result size reads",
"hint hits",
"hint misses",
"reuse attempts",
"reuse exact",
"reuse moved",
"reuse: dirty",
"reuse: wrong parent",
"reuse remapped",
"reuse: outside what it holds for",
"reuse: another layer",
"reuse: region-node choice changed",
"redraw queue pops",
"depth reads",
"local redraws",
"size changes",
"reader edges",
"primitive writes",
"text renders",
"text shape hits",
"text shapes",
"text line breaks",
"glyph placements",
"reuse outside: the length it was pinned to",
"reuse outside: a rel base",
"reuse outside: a region length",
];
}
#[derive(Clone, Copy)]
pub(crate) enum TimerKind {
Update,
FullLayout,
IncrementalLayout,
TextRender,
TextShape,
TextBreak,
GlyphPlacement,
}
impl TimerKind {
const COUNT: usize = Self::GlyphPlacement as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [
"update total",
"full layout",
"incremental layout",
"text render",
"text shape",
"text line break",
"glyph placement",
];
}
#[derive(Clone)]
pub struct Report {
counters: [u64; Counter::COUNT],
nanos: [u64; TimerKind::COUNT],
distinct_widgets: usize,
distinct_text_widgets: usize,
hot_widgets: Vec<Callsite>,
hot_text: Vec<Callsite>,
traces: Vec<TraceEvent>,
}
impl Default for Report {
fn default() -> Self {
Self {
counters: [0; Counter::COUNT],
nanos: [0; TimerKind::COUNT],
distinct_widgets: 0,
distinct_text_widgets: 0,
hot_widgets: Vec::new(),
hot_text: Vec::new(),
traces: Vec::new(),
}
}
}
impl Report {
pub fn counters(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
Counter::NAMES.into_iter().zip(self.counters)
}
/// Inclusive elapsed time accumulated for each targeted operation.
pub fn timings_ns(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
TimerKind::NAMES.into_iter().zip(self.nanos)
}
pub fn distinct_widgets(&self) -> usize {
self.distinct_widgets
}
pub fn distinct_text_widgets(&self) -> usize {
self.distinct_text_widgets
}
pub fn hot_widgets(&self) -> &[Callsite] {
&self.hot_widgets
}
pub fn hot_text(&self) -> &[Callsite] {
&self.hot_text
}
/// Ordered layout events for widgets selected with [`trace_widget`].
pub fn traces(&self) -> &[TraceEvent] {
&self.traces
}
/// Formats nonzero totals divided by `frames`.
pub fn per_frame(&self, frames: usize) -> String {
let divisor = frames.max(1) as f64;
let mut out = String::new();
for (name, value) in self.counters() {
if value != 0 {
let _ = writeln!(out, " {name:<27} {:>12.2}", value as f64 / divisor);
}
}
if self.distinct_widgets != 0 {
let _ = writeln!(
out,
" {:<27} {:>12}",
"distinct widgets", self.distinct_widgets
);
}
if self.distinct_text_widgets != 0 {
let _ = writeln!(
out,
" {:<27} {:>12}",
"distinct text widgets", self.distinct_text_widgets
);
}
for (name, nanos) in self.timings_ns() {
if nanos != 0 {
let ms = nanos as f64 / divisor / 1_000_000.0;
let _ = writeln!(out, " {name:<27} {ms:>12.3} ms");
}
}
if !self.hot_widgets.is_empty() {
let _ = writeln!(out, " hottest widget draws:");
for callsite in &self.hot_widgets {
let calls = callsite.calls as f64 / divisor;
let _ = writeln!(
out,
" {calls:>9.2} {:?} {}",
callsite.id, callsite.label
);
}
}
if !self.hot_text.is_empty() {
let _ = writeln!(out, " hottest text renders:");
for callsite in &self.hot_text {
let calls = callsite.calls as f64 / divisor;
let _ = writeln!(
out,
" {calls:>9.2} {:>3} widths {:?} {}",
callsite.distinct_widths, callsite.id, callsite.label
);
}
}
if !self.traces.is_empty() {
let _ = writeln!(out, " targeted layout trace:");
for event in &self.traces {
let _ = writeln!(out, " {event:?}");
}
}
out
}
}
#[derive(Clone)]
pub struct Callsite {
pub id: WidgetId,
pub label: String,
pub calls: u64,
pub distinct_widths: usize,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ReuseOutcome {
Exact,
Moved,
Dirty,
WrongParent,
WrongLayer,
Remapped,
Outside,
Undrawn,
}
/// One targeted layout event. Events are retained in execution order, making
/// repeated constraint paths visible without logging every widget globally.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TraceEvent {
DrawRequest {
id: WidgetId,
parent: Option<WidgetId>,
region: UiRegion,
pixel_size: PxVec2,
region_node: bool,
},
Reuse {
id: WidgetId,
outcome: ReuseOutcome,
},
SizeReported {
id: WidgetId,
size: Size,
},
RegionNode {
id: WidgetId,
parent: WidgetId,
region: UiRegion,
},
SizeRead {
id: WidgetId,
reader: WidgetId,
size: Size,
},
HintRead {
id: WidgetId,
reader: WidgetId,
axis: Axis,
hint: Option<LayoutLen>,
},
TextRendered {
id: WidgetId,
width: Option<f32>,
},
}
#[derive(Default)]
struct Calls {
label: String,
count: u64,
widths: HashSet<Option<u32>>,
}
#[derive(Default)]
struct Current {
report: Report,
widgets: HashMap<WidgetId, Calls>,
text_widgets: HashMap<WidgetId, Calls>,
traced: HashSet<WidgetId>,
}
thread_local! {
static CURRENT: RefCell<Current> = RefCell::new(Current::default());
}
pub(crate) fn bump(counter: Counter) {
CURRENT.with_borrow_mut(|current| current.report.counters[counter as usize] += 1);
}
pub(crate) fn draw_widget(id: WidgetId, label: &str) {
CURRENT.with_borrow_mut(|current| {
let calls = current.widgets.entry(id).or_default();
if calls.label.is_empty() {
calls.label = label.to_owned();
}
calls.count += 1;
});
}
/// Adds a widget to the targeted trace set. Selection survives [`take`]
/// until explicitly removed or cleared.
pub fn trace_widget(id: impl Into<WidgetId>) {
CURRENT.with_borrow_mut(|current| {
current.traced.insert(id.into());
});
}
pub fn untrace_widget(id: impl Into<WidgetId>) {
CURRENT.with_borrow_mut(|current| {
current.traced.remove(&id.into());
});
}
pub fn clear_traced_widgets() {
CURRENT.with_borrow_mut(|current| current.traced.clear());
}
fn trace(id: WidgetId, event: TraceEvent) {
CURRENT.with_borrow_mut(|current| {
if current.traced.contains(&id) {
current.report.traces.push(event);
}
});
}
pub(crate) fn draw_request(
id: WidgetId,
parent: Option<WidgetId>,
region: UiRegion,
pixel_size: PxVec2,
region_node: bool,
) {
trace(
id,
TraceEvent::DrawRequest {
id,
parent,
region,
pixel_size,
region_node,
},
);
}
pub(crate) fn reuse(id: WidgetId, outcome: ReuseOutcome) {
trace(id, TraceEvent::Reuse { id, outcome });
}
pub(crate) fn size_reported(id: WidgetId, size: Size) {
trace(id, TraceEvent::SizeReported { id, size });
}
pub(crate) fn region_node(id: WidgetId, parent: WidgetId, region: UiRegion) {
trace(id, TraceEvent::RegionNode { id, parent, region });
}
pub(crate) fn size_read(id: WidgetId, reader: WidgetId, size: Size) {
trace(id, TraceEvent::SizeRead { id, reader, size });
}
pub(crate) fn hint_read(id: WidgetId, reader: WidgetId, axis: Axis, hint: Option<LayoutLen>) {
trace(
id,
TraceEvent::HintRead {
id,
reader,
axis,
hint,
},
);
}
pub(crate) fn render_text(id: WidgetId, label: &str, width: Option<f32>) {
CURRENT.with_borrow_mut(|current| {
let calls = current.text_widgets.entry(id).or_default();
if calls.label.is_empty() {
calls.label = label.to_owned();
}
calls.count += 1;
calls.widths.insert(width.map(f32::to_bits));
if current.traced.contains(&id) {
current
.report
.traces
.push(TraceEvent::TextRendered { id, width });
}
});
}
pub(crate) struct Timer {
kind: TimerKind,
start: Instant,
}
pub(crate) fn timer(kind: TimerKind) -> Timer {
Timer {
kind,
start: Instant::now(),
}
}
impl Drop for Timer {
fn drop(&mut self) {
let nanos = self.start.elapsed().as_nanos().min(u64::MAX as u128) as u64;
CURRENT.with_borrow_mut(|current| current.report.nanos[self.kind as usize] += nanos);
}
}
/// Takes all diagnostics accumulated on this thread and resets them.
pub fn take() -> Report {
CURRENT.with_borrow_mut(|current| {
current.report.distinct_widgets = current.widgets.len();
current.report.distinct_text_widgets = current.text_widgets.len();
current.report.hot_widgets = hottest(&current.widgets);
current.report.hot_text = hottest(&current.text_widgets);
let report = std::mem::take(&mut current.report);
current.widgets.clear();
current.text_widgets.clear();
report
})
}
fn hottest(calls: &HashMap<WidgetId, Calls>) -> Vec<Callsite> {
let mut calls: Vec<_> = calls
.iter()
.map(|(&id, calls)| Callsite {
id,
label: calls.label.clone(),
calls: calls.count,
distinct_widths: calls.widths.len(),
})
.collect();
calls.sort_by(|a, b| b.calls.cmp(&a.calls).then_with(|| a.label.cmp(&b.label)));
calls.truncate(8);
calls
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn taking_a_report_resets_its_counters() {
let _ = take();
bump(Counter::Updates);
bump(Counter::Updates);
let report = take();
assert_eq!(report.counters().next(), Some(("updates", 2)));
assert!(take().counters().all(|(_, count)| count == 0));
}
}
+1 -5
View File
@@ -5,17 +5,14 @@
#![feature(unboxed_closures)] #![feature(unboxed_closures)]
#![feature(fn_traits)] #![feature(fn_traits)]
#![feature(const_destruct)] #![feature(const_destruct)]
#![feature(portable_simd)]
#![feature(associated_type_defaults)] #![feature(associated_type_defaults)]
#![feature(unsize)] #![feature(unsize)]
#![feature(coerce_unsized)] #![feature(coerce_unsized)]
#![feature(option_into_flat_iter)] #![feature(option_into_flat_iter)]
#[cfg(feature = "layout-diagnostics")]
pub mod layout_diagnostics;
mod attr; mod attr;
mod event; mod event;
mod fixed;
mod num; mod num;
mod orientation; mod orientation;
mod primitive; mod primitive;
@@ -27,7 +24,6 @@ pub mod util;
pub use attr::*; pub use attr::*;
pub use event::*; pub use event::*;
pub use fixed::*;
pub use num::*; pub use num::*;
pub use orientation::*; pub use orientation::*;
pub use primitive::*; pub use primitive::*;
+44 -77
View File
@@ -1,8 +1,8 @@
use crate::{Px, Rel}; use crate::vec2;
use super::*; use super::*;
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub struct Align { pub struct Align {
pub x: Option<AxisAlign>, pub x: Option<AxisAlign>,
pub y: Option<AxisAlign>, pub y: Option<AxisAlign>,
@@ -30,32 +30,20 @@ impl Align {
} }
} }
/// Where a widget sits in a box longer than it is. The default is the middle, #[derive(Clone, Copy, PartialEq, Eq)]
/// because the two edges are the ones that assume a direction: which of them pub enum AxisAlign {
/// is the near one depends on the writing system and on which way a container Neg,
/// runs, and the middle is the same either way. Center,
#[derive(Debug, Clone, Copy, PartialEq)] Pos,
pub struct AxisAlign(Rel);
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::from_f32(rel))
}
/// A fraction of the room left over, which is what the layout reads: the
/// three constants are the familiar places along it, not the only ones.
pub const fn rel(&self) -> Rel {
self.0
}
} }
impl Default for AxisAlign { impl AxisAlign {
fn default() -> Self { pub const fn rel(&self) -> f32 {
Self::CENTER match self {
Self::Neg => 0.0,
Self::Center => 0.5,
Self::Pos => 1.0,
}
} }
} }
@@ -65,60 +53,41 @@ pub struct CardinalAlign {
} }
impl CardinalAlign { impl CardinalAlign {
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::NEG); pub const LEFT: Self = Self::new(Axis::X, AxisAlign::Neg);
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::CENTER); pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::Center);
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::POS); pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::Pos);
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::NEG); pub const TOP: Self = Self::new(Axis::Y, AxisAlign::Neg);
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::CENTER); pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::Center);
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::POS); pub const BOT: Self = Self::new(Axis::Y, AxisAlign::Pos);
pub const fn new(axis: Axis, align: AxisAlign) -> Self { pub const fn new(axis: Axis, align: AxisAlign) -> Self {
Self { axis, align } Self { axis, align }
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Default)] #[derive(Clone, Copy, PartialEq, Eq)]
pub struct RegionAlign { pub struct RegionAlign {
pub x: AxisAlign, pub x: AxisAlign,
pub y: AxisAlign, pub y: AxisAlign,
} }
impl RegionAlign { impl RegionAlign {
/// Both axes at the near edge: the start of a box in its own orientation. pub const TOP_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Neg);
pub const NEAR: Self = Self { pub const TOP_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Neg);
x: AxisAlign::NEG, pub const TOP_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Neg);
y: 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 fn axis(&self, axis: Axis) -> AxisAlign { pub const BOT_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Pos);
match axis { pub const BOT_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Pos);
Axis::X => self.x, pub const BOT_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Pos);
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 { pub const fn new(x: AxisAlign, y: AxisAlign) -> Self {
Self { x, y } Self { x, y }
} }
pub const fn rel(&self) -> Vec2 {
vec2(self.x.rel(), self.y.rel())
}
} }
impl UiVec2 { impl UiVec2 {
@@ -171,15 +140,16 @@ impl Vec2 {
} }
} }
impl Len { impl UiScalar {
pub const fn align(&self, align: AxisAlign) -> UiSpan { pub const fn align(&self, align: AxisAlign) -> UiSpan {
let rel = align.rel(); let rel = align.rel();
let rest = Rel::ONE.sub(rel); let mut start = UiScalar::rel(rel);
let at = Len::from_parts(rel, Px::ZERO); start.abs -= self.abs * rel;
UiSpan { start.rel -= self.rel * rel;
start: Len::from_parts(at.rel.sub(self.rel.mul(rel)), at.px.sub(self.px.mul(rel))), let mut end = UiScalar::rel(rel);
end: Len::from_parts(at.rel.add(self.rel.mul(rest)), at.px.add(self.px.mul(rest))), end.abs += self.abs * (1.0 - rel);
} end.rel += self.rel * (1.0 - rel);
UiSpan { start, end }
} }
} }
@@ -195,8 +165,8 @@ impl From<RegionAlign> for Align {
impl From<Align> for RegionAlign { impl From<Align> for RegionAlign {
fn from(align: Align) -> Self { fn from(align: Align) -> Self {
Self { Self {
x: align.x.unwrap_or(AxisAlign::CENTER), x: align.x.unwrap_or(AxisAlign::Center),
y: align.y.unwrap_or(AxisAlign::CENTER), y: align.y.unwrap_or(AxisAlign::Center),
} }
} }
} }
@@ -219,10 +189,7 @@ impl From<CardinalAlign> for Align {
const impl From<RegionAlign> for UiVec2 { const impl From<RegionAlign> for UiVec2 {
fn from(align: RegionAlign) -> Self { fn from(align: RegionAlign) -> Self {
Self::new( Self::rel(align.rel())
Len::from_parts(align.x.rel(), Px::ZERO),
Len::from_parts(align.y.rel(), Px::ZERO),
)
} }
} }
+1 -36
View File
@@ -1,23 +1,11 @@
use super::*; use super::*;
use crate::{Fixed, FixedVec2};
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Eq, PartialEq)]
pub enum Axis { pub enum Axis {
X, X,
Y, Y,
} }
impl Axis {
/// A per-axis pair with `aligned` on this axis and `ortho` on the other,
/// which is what `from_axis` does for a vector.
pub fn pair<T>(self, aligned: T, ortho: T) -> [T; 2] {
match self {
Self::X => [aligned, ortho],
Self::Y => [ortho, aligned],
}
}
}
impl std::ops::Not for Axis { impl std::ops::Not for Axis {
type Output = Self; type Output = Self;
@@ -52,29 +40,6 @@ pub enum Sign {
Pos, Pos,
} }
impl<const SHIFT: u32> FixedVec2<SHIFT> {
pub const fn axis(&self, axis: Axis) -> Fixed<SHIFT> {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub const fn axis_mut(&mut self, axis: Axis) -> &mut Fixed<SHIFT> {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub const fn from_axis(axis: Axis, aligned: Fixed<SHIFT>, ortho: Fixed<SHIFT>) -> Self {
match axis {
Axis::X => Self::new(aligned, ortho),
Axis::Y => Self::new(ortho, aligned),
}
}
}
impl Vec2 { impl Vec2 {
pub fn axis(&self, axis: Axis) -> f32 { pub fn axis(&self, axis: Axis) -> f32 {
match axis { match axis {
+82 -122
View File
@@ -1,30 +1,22 @@
use super::*; use super::*;
use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op}; use crate::{UiNum, util::impl_op};
#[derive(Debug, Default, Clone, Copy, PartialEq)] #[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Size { pub struct Size {
pub x: LayoutLen, pub x: Len,
pub y: LayoutLen, pub y: Len,
} }
/// What a widget asks for along one axis: a [`Len`] -- pixels and a fraction #[derive(Debug, Clone, Copy, PartialEq)]
/// of the box it is given -- plus a share of whatever is left over once pub struct Len {
/// everything fixed has been taken. The parts add up rather than choosing pub abs: f32,
/// between one another. pub rel: f32,
/// pub rest: f32,
/// Only a container dividing its room can answer a share, so a length nobody
/// divides is a `Len`: a position, a padding, a cap, anything already
/// resolved.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LayoutLen {
pub px: Px,
pub rel: Rel,
pub leftover: Weight,
} }
impl<N: UiNum> From<N> for LayoutLen { impl<N: UiNum> From<N> for Len {
fn from(value: N) -> Self { fn from(value: N) -> Self {
LayoutLen::px(value.to_f32()) Len::abs(value.to_f32())
} }
} }
@@ -37,76 +29,52 @@ impl<Nx: UiNum, Ny: UiNum> From<(Nx, Ny)> for Size {
} }
} }
/// A length with no share in it is a length a container does not have to impl From<Len> for Size {
/// divide, which is one it can always give. fn from(value: Len) -> Self {
impl From<Len> for LayoutLen {
fn from(len: Len) -> Self {
Self {
px: len.px,
rel: len.rel,
leftover: Weight::ZERO,
}
}
}
impl From<LayoutLen> for Size {
fn from(value: LayoutLen) -> Self {
Self { x: value, y: value } Self { x: value, y: value }
} }
} }
impl Size { impl Size {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
x: LayoutLen::ZERO, x: Len::ZERO,
y: LayoutLen::ZERO, y: Len::ZERO,
}; };
pub const LEFTOVER: Self = Self { pub const REST: Self = Self {
x: LayoutLen::LEFTOVER, x: Len::REST,
y: LayoutLen::LEFTOVER, y: Len::REST,
}; };
/// From something measured outside layout -- a texture, a shaped line -- pub fn abs(v: Vec2) -> Self {
/// which is where a size in floats comes from.
pub fn px(v: Vec2) -> Self {
Self::from_px(PxVec2::from_f32(v))
}
pub const fn from_px(v: PxVec2) -> Self {
Self { Self {
x: LayoutLen { x: Len::abs(v.x),
px: v.x, y: Len::abs(v.y),
..LayoutLen::ZERO
},
y: LayoutLen {
px: v.y,
..LayoutLen::ZERO
},
} }
} }
pub fn rel(v: Vec2) -> Self { pub fn rel(v: Vec2) -> Self {
Self { Self {
x: LayoutLen::rel(v.x), x: Len::rel(v.x),
y: LayoutLen::rel(v.y), y: Len::rel(v.y),
} }
} }
pub fn leftover(v: Vec2) -> Self { pub fn rest(v: Vec2) -> Self {
Self { Self {
x: LayoutLen::leftover(v.x), x: Len::rest(v.x),
y: LayoutLen::leftover(v.y), y: Len::rest(v.y),
} }
} }
pub fn to_uivec2(self) -> UiVec2 { pub fn to_uivec2(self) -> UiVec2 {
UiVec2 { UiVec2 {
x: self.x.apply_leftover(), x: self.x.apply_rest(),
y: self.y.apply_leftover(), y: self.y.apply_rest(),
} }
} }
pub fn from_axis(axis: Axis, aligned: LayoutLen, ortho: LayoutLen) -> Self { pub fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self {
match axis { match axis {
Axis::X => Self { Axis::X => Self {
x: aligned, x: aligned,
@@ -119,73 +87,53 @@ impl Size {
} }
} }
pub fn axis(&self, axis: Axis) -> LayoutLen { pub fn axis(&self, axis: Axis) -> Len {
match axis { match axis {
Axis::X => self.x, Axis::X => self.x,
Axis::Y => self.y, Axis::Y => self.y,
} }
} }
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
} }
impl LayoutLen { impl Len {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
px: Px::ZERO, abs: 0.0,
rel: Rel::ZERO, rel: 0.0,
leftover: Weight::ZERO, rest: 0.0,
}; };
pub const LEFTOVER: Self = Self { pub const REST: Self = Self {
px: Px::ZERO, abs: 0.0,
rel: Rel::ZERO, rel: 0.0,
leftover: Weight::ONE, rest: 1.0,
}; };
/// The whole of what is left over counts as the whole box, which is what pub fn apply_rest(&self) -> UiScalar {
/// a length means to something that is not dividing a box between UiScalar {
/// siblings -- a scroll asking how long its content is. rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
pub fn apply_leftover(&self) -> Len { abs: self.abs,
let share = match self.leftover > Weight::ZERO {
true => Rel::ONE,
false => Rel::ZERO,
};
Len::from_parts(self.rel.add(share), self.px)
}
/// This length, given as a part of a box `len` long, as a part of the
/// box `len` is itself a part of. The share is untouched: it is a claim
/// on whoever divides the room, not a fraction of anything.
pub const fn within_len(self, len: Len) -> Self {
let part = Len::from_parts(self.rel, self.px).within_len(len);
Self {
px: part.px,
rel: part.rel,
leftover: self.leftover,
} }
} }
pub fn px(px: impl UiNum) -> Self { pub fn abs(abs: impl UiNum) -> Self {
Self { Self {
px: Px::from_num(px), abs: abs.to_f32(),
..Self::ZERO rel: 0.0,
rest: 0.0,
} }
} }
pub fn rel(rel: impl UiNum) -> Self { pub fn rel(rel: impl UiNum) -> Self {
Self { Self {
rel: Rel::from_num(rel), abs: 0.0,
..Self::ZERO rel: rel.to_f32(),
rest: 0.0,
} }
} }
pub fn leftover(ratio: impl UiNum) -> Self { pub fn rest(ratio: impl UiNum) -> Self {
Self { Self {
leftover: Weight::from_num(ratio), abs: 0.0,
..Self::ZERO rel: 0.0,
rest: ratio.to_f32(),
} }
} }
} }
@@ -193,26 +141,38 @@ impl LayoutLen {
pub mod len_fns { pub mod len_fns {
use super::*; use super::*;
pub fn px(px: impl UiNum) -> LayoutLen { pub fn abs(abs: impl UiNum) -> Len {
LayoutLen::px(px) Len {
abs: abs.to_f32(),
rel: 0.0,
rest: 0.0,
}
} }
pub fn rel(rel: impl UiNum) -> LayoutLen { pub fn rel(rel: impl UiNum) -> Len {
LayoutLen::rel(rel) Len {
abs: 0.0,
rel: rel.to_f32(),
rest: 0.0,
}
} }
pub fn leftover(ratio: impl UiNum) -> LayoutLen { pub fn rest(ratio: impl UiNum) -> Len {
LayoutLen::leftover(ratio) Len {
abs: 0.0,
rel: 0.0,
rest: ratio.to_f32(),
}
} }
} }
impl_op!(same LayoutLen Add add; px rel leftover); impl_op!(Len Add add; abs rel rest);
impl_op!(same LayoutLen Sub sub; px rel leftover); impl_op!(Len Sub sub; abs rel rest);
impl_op!(same Size Add add; x y); impl_op!(Size Add add; x y);
impl_op!(same Size Sub sub; x y); impl_op!(Size Sub sub; x y);
impl Default for LayoutLen { impl Default for Len {
fn default() -> Self { fn default() -> Self {
Self::leftover(1.0) Self::rest(1.0)
} }
} }
@@ -222,16 +182,16 @@ impl std::fmt::Display for Size {
} }
} }
impl std::fmt::Display for LayoutLen { impl std::fmt::Display for Len {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.px != Px::ZERO { if self.abs != 0.0 {
write!(f, "{} px;", self.px)?; write!(f, "{} abs;", self.abs)?;
} }
if self.rel != Rel::ZERO { if self.rel != 0.0 {
write!(f, "{} rel;", self.rel)?; write!(f, "{} rel;", self.rel)?;
} }
if self.leftover != Weight::ZERO { if self.rest != 0.0 {
write!(f, "{} leftover;", self.leftover)?; write!(f, "{} rest;", self.rest)?;
} }
Ok(()) Ok(())
} }
+142 -142
View File
@@ -1,46 +1,41 @@
use std::{fmt::Display, marker::Destruct}; use std::{fmt::Display, hash::Hash, marker::Destruct};
use super::*; use super::*;
use crate::{Px, PxVec2, Rel, UiNum, util::impl_op}; use crate::{
UiNum,
util::{LerpUtil, impl_op},
};
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct UiVec2 { pub struct UiVec2 {
pub x: Len, pub x: UiScalar,
pub y: Len, pub y: UiScalar,
} }
impl UiVec2 { impl UiVec2 {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
x: Len::ZERO, x: UiScalar::ZERO,
y: Len::ZERO, y: UiScalar::ZERO,
}; };
pub const fn new(x: Len, y: Len) -> Self { pub const fn new(x: UiScalar, y: UiScalar) -> Self {
Self { x, y } Self { x, y }
} }
pub const fn px(px: impl const Into<Vec2>) -> Self { pub const fn abs(abs: impl const Into<Vec2>) -> Self {
let px = px.into(); let abs = abs.into();
Self { Self {
x: Len::px(px.x), x: UiScalar::abs(abs.x),
y: Len::px(px.y), y: UiScalar::abs(abs.y),
}
}
/// From lengths already on the grid, with no fraction of a box.
pub const fn from_px(px: PxVec2) -> Self {
Self {
x: Len::from_parts(Rel::ZERO, px.x),
y: Len::from_parts(Rel::ZERO, px.y),
} }
} }
pub const fn rel(rel: impl const Into<Vec2>) -> Self { pub const fn rel(rel: impl const Into<Vec2>) -> Self {
let rel = rel.into(); let rel = rel.into();
Self { Self {
x: Len::rel(rel.x), x: UiScalar::rel(rel.x),
y: Len::rel(rel.y), y: UiScalar::rel(rel.y),
} }
} }
@@ -61,29 +56,37 @@ impl UiVec2 {
} }
} }
pub fn axis_mut(&mut self, axis: Axis) -> &mut Len { 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 {
match axis { match axis {
Axis::X => &mut self.x, Axis::X => &mut self.x,
Axis::Y => &mut self.y, Axis::Y => &mut self.y,
} }
} }
pub fn axis(&self, axis: Axis) -> Len { pub fn axis(&self, axis: Axis) -> UiScalar {
match axis { match axis {
Axis::X => self.x, Axis::X => self.x,
Axis::Y => self.y, Axis::Y => self.y,
} }
} }
/// Resolved against a box of `size`, which is where a fraction stops pub fn to_abs(&self, rel: Vec2) -> Vec2 {
/// being one and becomes a place. Vec2 {
pub fn to_px(&self, size: PxVec2) -> PxVec2 { x: self.x.to_abs(rel.x),
PxVec2::new(self.x.to_px(size.x), self.y.to_px(size.y)) y: self.y.to_abs(rel.y),
}
} }
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE); pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
pub const fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self { pub const fn from_axis(axis: Axis, aligned: UiScalar, ortho: UiScalar) -> Self {
match axis { match axis {
Axis::X => Self { Axis::X => Self {
x: aligned, x: aligned,
@@ -96,27 +99,34 @@ impl UiVec2 {
} }
} }
pub fn get_px(&self) -> Vec2 { pub fn get_abs(&self) -> Vec2 {
(self.x.px.to_f32(), self.y.px.to_f32()).into() (self.x.abs, self.y.abs).into()
} }
pub fn get_rel(&self) -> Vec2 { pub fn get_rel(&self) -> Vec2 {
(self.x.rel.to_f32(), self.y.rel.to_f32()).into() (self.x.rel, self.y.rel).into()
}
pub fn abs_mut(&mut self) -> Vec2View<'_> {
Vec2View {
x: &mut self.x.abs,
y: &mut self.y.abs,
}
} }
} }
impl Display for UiVec2 { impl Display for UiVec2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "rel{};px{}", self.get_rel(), self.get_px()) write!(f, "rel{};abs{}", self.get_rel(), self.get_abs())
} }
} }
impl_op!(same UiVec2 Add add; x y); impl_op!(UiVec2 Add add; x y);
impl_op!(same UiVec2 Sub sub; x y); impl_op!(UiVec2 Sub sub; x y);
const impl From<Vec2> for UiVec2 { const impl From<Vec2> for UiVec2 {
fn from(px: Vec2) -> Self { fn from(abs: Vec2) -> Self {
Self::px(px) Self::abs(abs)
} }
} }
@@ -124,149 +134,133 @@ const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2
where where
(T, U): const Destruct, (T, U): const Destruct,
{ {
fn from(px: (T, U)) -> Self { fn from(abs: (T, U)) -> Self {
Self::px(px) Self::abs(abs)
} }
} }
/// A length along one axis: a fraction of the box it is measured in plus an
/// offset, `rel * box + px`. A position is the same number -- the length from
/// the start of the box to the point -- which is why a [`UiSpan`] is two of
/// these. Both parts are fixed point, so composing one through a chain of
/// boxes rounds only where it multiplies, and lands on the same number as any
/// other route to the same place.
///
/// It carries no claim on what a container has left over. That is
/// [`crate::LayoutLen`], which is this plus a weight, and which means nothing
/// to anyone but whoever divides the room.
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, Default, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)]
pub struct Len { pub struct UiScalar {
pub rel: Rel, pub rel: f32,
pub px: Px, pub abs: f32,
} }
impl_op!(same Len Add add; rel px); impl Eq for UiScalar {}
impl_op!(same Len Sub sub; rel px); impl Hash for UiScalar {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
impl Len { state.write_u32(self.rel.to_bits());
pub const ZERO: Self = Self { state.write_u32(self.abs.to_bits());
rel: Rel::ZERO,
px: Px::ZERO,
};
pub const FULL: Self = Self {
rel: Rel::ONE,
px: Px::ZERO,
};
pub const fn new(rel: f32, px: f32) -> Self {
Self::from_parts(Rel::from_f32(rel), Px::from_f32(px))
} }
}
/// From parts already on the grid, rather than numbers to be put on it. impl_op!(UiScalar Add add; rel abs);
pub const fn from_parts(rel: Rel, px: Px) -> Self { impl_op!(UiScalar Sub sub; rel abs);
Self { rel, px }
impl UiScalar {
pub const ZERO: Self = Self { rel: 0.0, abs: 0.0 };
pub const FULL: Self = Self { rel: 1.0, abs: 0.0 };
pub const fn new(rel: f32, abs: f32) -> Self {
Self { rel, abs }
} }
pub const fn rel(rel: f32) -> Self { pub const fn rel(rel: f32) -> Self {
Self::from_parts(Rel::from_f32(rel), Px::ZERO) Self { rel, abs: 0.0 }
} }
pub const fn px(px: f32) -> Self { pub const fn abs(abs: f32) -> Self {
Self::from_parts(Rel::ZERO, Px::from_f32(px)) Self { rel: 0.0, abs }
} }
pub const fn rel_min() -> Self { pub const fn rel_min() -> Self {
Self::ZERO Self::new(0.0, 0.0)
} }
pub const fn rel_max() -> Self { pub const fn rel_max() -> Self {
Self::FULL Self::new(1.0, 0.0)
} }
pub const fn max(&self, other: Self) -> Self { pub const fn max(&self, other: Self) -> Self {
Self { Self {
rel: self.rel.max(other.rel), rel: self.rel.max(other.rel),
px: self.px.max(other.px), abs: self.abs.max(other.abs),
} }
} }
pub const fn min(&self, other: Self) -> Self { pub const fn min(&self, other: Self) -> Self {
Self { Self {
rel: self.rel.min(other.rel), rel: self.rel.min(other.rel),
px: self.px.min(other.px), abs: self.abs.min(other.abs),
} }
} }
/// Both parts by the same fraction, which is what a part of a length pub const fn offset(mut self, amt: f32) -> Self {
/// means when the length is part pixels and part a fraction of a box. self.abs += amt;
pub const fn scale(&self, by: Rel) -> Self {
Self {
rel: self.rel.mul(by),
px: self.px.mul(by),
}
}
pub const fn offset(mut self, amt: Px) -> Self {
self.px = self.px.add(amt);
self self
} }
pub const fn within(&self, span: &UiSpan) -> Self { pub const fn within(&self, span: &UiSpan) -> Self {
let anchor = self.rel.lerp(span.start.rel, span.end.rel);
let offset = self.abs + self.rel.lerp(span.start.abs, span.end.abs);
Self { Self {
rel: self.rel.lerp(span.start.rel, span.end.rel), rel: anchor,
px: self.px.add(self.rel.lerp(span.start.px, span.end.px)), abs: offset,
} }
} }
pub const fn within_len(&self, len: Len) -> Self { pub const fn outside(&self, span: &UiSpan) -> 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);
Self { rel, abs }
}
pub fn within_len(&self, len: UiScalar) -> Self {
self.within(&UiSpan { self.within(&UiSpan {
start: Len::ZERO, start: UiScalar::ZERO,
end: len, end: len,
}) })
} }
pub fn select_len(&self, len: Len) -> Self { pub fn select_len(&self, len: UiScalar) -> Self {
len.within_len(*self) len.within_len(*self)
} }
pub const fn flip(&mut self) { pub const fn flip(&mut self) {
self.rel = Rel::ONE.sub(self.rel); self.rel = 1.0 - self.rel;
self.px = self.px.neg(); self.abs = -self.abs;
} }
pub const fn to(&self, end: Self) -> UiSpan { pub const fn to(&self, end: Self) -> UiSpan {
UiSpan { start: *self, end } UiSpan { start: *self, end }
} }
/// Resolved against a box of `len`, which is the only place a fraction pub const fn to_abs(&self, rel: f32) -> f32 {
/// becomes a number of pixels. self.rel * rel + self.abs
pub const fn to_px(&self, len: Px) -> Px {
self.px.add(len.mul(self.rel))
} }
} }
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct UiSpan { pub struct UiSpan {
pub start: Len, pub start: UiScalar,
pub end: Len, pub end: UiScalar,
} }
impl UiSpan { impl UiSpan {
pub const FULL: Self = Self { pub const FULL: Self = Self {
start: Len::ZERO, start: UiScalar::ZERO,
end: Len::FULL, end: UiScalar::FULL,
}; };
pub const fn rel(rel: f32) -> Self { pub const fn rel(rel: f32) -> Self {
Self { Self {
start: Len::rel(rel), start: UiScalar::rel(rel),
end: Len::rel(rel), end: UiScalar::rel(rel),
} }
} }
pub const fn new(start: Len, end: Len) -> Self { pub const fn new(start: UiScalar, end: UiScalar) -> Self {
Self { start, end } Self { start, end }
} }
@@ -274,19 +268,14 @@ impl UiSpan {
self.start.flip(); self.start.flip();
self.end.flip(); self.end.flip();
std::mem::swap(&mut self.start.rel, &mut self.end.rel); std::mem::swap(&mut self.start.rel, &mut self.end.rel);
std::mem::swap(&mut self.start.px, &mut self.end.px); std::mem::swap(&mut self.start.abs, &mut self.end.abs);
} }
pub const fn shift(&mut self, offset: Len) { pub const fn shift(&mut self, offset: UiScalar) {
self.start += offset; self.start += offset;
self.end += offset; self.end += offset;
} }
/// Composing a box through the one it sits in, and the hottest line in
/// layout. It used to skip the multiplies where a span was the whole of
/// its parent or the parent the whole of its own; both come out of the
/// multiply unchanged anyway, and the body those comparisons cost was
/// what kept the inliner from taking this at all.
pub const fn within(&self, parent: &Self) -> Self { pub const fn within(&self, parent: &Self) -> Self {
Self { Self {
start: self.start.within(parent), start: self.start.within(parent),
@@ -294,17 +283,15 @@ impl UiSpan {
} }
} }
pub const fn len(&self) -> Len { pub const fn outside(&self, parent: &Self) -> Self {
self.end - self.start Self {
start: self.start.outside(parent),
end: self.end.outside(parent),
}
} }
/// Both ends by the same amount, which is what moving a box without pub const fn len(&self) -> UiScalar {
/// changing its length does to every part of it. self.end - self.start
pub const fn translated(self, by: Len) -> Self {
Self {
start: self.start + by,
end: self.end + by,
}
} }
} }
@@ -316,17 +303,6 @@ pub struct UiRegion {
} }
impl UiRegion { impl UiRegion {
/// Every part of the box by the same amount on each axis. Done to the
/// whole region rather than an end at a time, because that is what it is
/// -- and because four adds in a row are four adds, where four asked for
/// separately are four sequences.
pub const fn translated(self, x: Len, y: Len) -> Self {
Self {
x: self.x.translated(x),
y: self.y.translated(y),
}
}
pub const FULL: Self = Self { pub const FULL: Self = Self {
x: UiSpan::FULL, x: UiSpan::FULL,
y: UiSpan::FULL, y: UiSpan::FULL,
@@ -348,7 +324,14 @@ impl UiRegion {
y: self.y.within(&parent.y), y: self.y.within(&parent.y),
} }
} }
pub const fn axis(&self, axis: Axis) -> &UiSpan { pub const fn outside(&self, parent: &Self) -> Self {
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,
@@ -380,10 +363,10 @@ impl UiRegion {
self self
} }
pub fn to_px(&self, size: PxVec2) -> PixelRegion { pub fn to_px(&self, size: Vec2) -> PixelRegion {
PixelRegion { PixelRegion {
top_left: self.top_left().to_px(size), top_left: self.top_left().get_rel() * size + self.top_left().get_abs(),
bot_right: self.bot_right().to_px(size), bot_right: self.bot_right().get_rel() * size + self.bot_right().get_abs(),
} }
} }
@@ -438,21 +421,21 @@ impl Display for UiRegion {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug)]
pub struct PixelRegion { pub struct PixelRegion {
pub top_left: PxVec2, pub top_left: Vec2,
pub bot_right: PxVec2, pub bot_right: Vec2,
} }
impl PixelRegion { impl PixelRegion {
pub fn contains(&self, pos: PxVec2) -> bool { pub fn contains(&self, pos: Vec2) -> bool {
pos.x >= self.top_left.x pos.x >= self.top_left.x
&& pos.x <= self.bot_right.x && pos.x <= self.bot_right.x
&& pos.y >= self.top_left.y && pos.y >= self.top_left.y
&& pos.y <= self.bot_right.y && pos.y <= self.bot_right.y
} }
pub fn size(&self) -> PxVec2 { pub fn size(&self) -> Vec2 {
self.bot_right - self.top_left self.bot_right - self.top_left
} }
} }
@@ -462,3 +445,20 @@ impl Display for PixelRegion {
write!(f, "{} -> {}", self.top_left, self.bot_right) write!(f, "{} -> {}", self.top_left, self.bot_right)
} }
} }
pub struct Vec2View<'a> {
pub x: &'a mut f32,
pub y: &'a mut f32,
}
impl Vec2View<'_> {
pub fn set(&mut self, other: Vec2) {
*self.x = other.x;
*self.y = other.y;
}
pub fn add(&mut self, other: Vec2) {
*self.x += other.x;
*self.y += other.y;
}
}
-6
View File
@@ -10,12 +10,6 @@ pub struct Color<T> {
pub a: T, pub a: T,
} }
impl<T: ColorNum> Default for Color<T> {
fn default() -> Self {
Self::BLACK
}
}
impl<T: ColorNum> Color<T> { impl<T: ColorNum> Color<T> {
pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN); pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN);
pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX); pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX);
+3 -7
View File
@@ -1,7 +1,7 @@
use std::ops::{Index, IndexMut}; use std::ops::{Index, IndexMut};
use crate::{ use crate::{
render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst}, render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives},
util::to_mut, util::to_mut,
}; };
@@ -39,7 +39,7 @@ struct Child {
tail: usize, tail: usize,
} }
pub type DrawLayers = Layers<LayerDraws>; pub type PrimitiveLayers = Layers<Primitives>;
impl<T: Default> Layers<T> { impl<T: Default> Layers<T> {
pub fn new() -> Layers<T> { pub fn new() -> Layers<T> {
@@ -119,11 +119,7 @@ impl<T: Default> Layers<T> {
} }
} }
impl DrawLayers { impl PrimitiveLayers {
/// Inlined on purpose: it is one call per glyph, the innermost thing a
/// frame does, and whether the inliner takes it turns out to depend on
/// unrelated code elsewhere in the crate -- 12% of a resize frame.
#[inline]
pub fn write<P: Primitive>( pub fn write<P: Primitive>(
&mut self, &mut self,
layer: LayerId, layer: LayerId,
+148 -397
View File
@@ -1,97 +1,60 @@
#[cfg(feature = "layout-diagnostics")] use crate::{Align, RegionAlign, TextureHandle, Textures, UiColor, util::Vec2};
use crate::layout_diagnostics::{self as diag, Counter, TimerKind}; use cosmic_text::{
use crate::{ Attrs, AttrsList, Buffer, CacheKey, Color, Family, FontSystem, Metrics, Placement, SwashCache,
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, Px, PxVec2, RegionAlign, UiColor, SwashContent,
util::Vec2,
};
use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
};
use std::{
collections::VecDeque,
hash::{DefaultHasher, Hash, Hasher},
};
use swash::{
FontRef,
scale::{Render, ScaleContext, Source, StrikeWith},
zeno::{Format, Vector},
}; };
use image::{DynamicImage, GenericImageView, RgbaImage};
use std::simd::{Simd, num::SimdUint};
/// TODO: properly wrap this
pub mod text_lib {
pub use cosmic_text::*;
}
pub struct TextData { pub struct TextData {
pub font_ctx: FontContext, pub font_system: FontSystem,
pub layout_ctx: LayoutContext<UiColor>, pub swash_cache: SwashCache,
scale_ctx: ScaleContext, glyph_cache: Vec<(Placement, CacheKey, Color)>,
pub atlas: GlyphAtlas,
spare: VecDeque<Placed>,
} }
/// The glyphs of one text at one width. A buffer holds the ones it is drawn
/// as; these are the ones it had before, kept because a container measures a
/// child by drawing it in a box it may not keep, and so comes back to widths
/// it has already asked for.
struct Placed {
/// Where the glyphs land is a function of these three and nothing else,
/// so no widget or buffer identity is involved and two texts of the same
/// words share an answer.
text: String,
key: LayoutKey,
glyphs: RenderedText,
}
/// How many to keep. Bounding the whole store rather than each buffer is what
/// makes this a fixed cost instead of one a tree of ten thousand texts pays
/// ten thousand times; the re-asks come from laying out one subtree, so they
/// are close together and few are needed. Instructions over 500 resize frames
/// of `tests/revision_cost.rs`, both the repeating widths and the sweep that
/// cannot hit across frames: 13.7B at 32, 12.1B at 64, 10.4B and 12.1B at 128,
/// and nothing past that -- so 128, which is no worse in the case that never
/// repeats and better in the one that does.
const SPARE_PLACED: usize = 128;
impl Default for TextData { impl Default for TextData {
fn default() -> Self { fn default() -> Self {
Self { Self {
font_ctx: FontContext::new(), font_system: FontSystem::new(),
layout_ctx: LayoutContext::new(), swash_cache: SwashCache::new(),
scale_ctx: ScaleContext::new(), glyph_cache: Default::default(),
atlas: GlyphAtlas::default(),
spare: VecDeque::new(),
} }
} }
} }
#[derive(Clone, PartialEq)] #[derive(Clone, Copy)]
pub enum Family {
SansSerif,
Serif,
Monospace,
Named(String),
}
impl Family {
fn family(&self) -> FontFamily<'_> {
let name = match self {
Self::SansSerif => FontFamilyName::Generic(GenericFamily::SansSerif),
Self::Serif => FontFamilyName::Generic(GenericFamily::Serif),
Self::Monospace => FontFamilyName::Generic(GenericFamily::Monospace),
Self::Named(name) => FontFamilyName::Named(name.as_str().into()),
};
FontFamily::Single(name)
}
}
#[derive(Clone, PartialEq)]
pub struct TextAttrs { pub struct TextAttrs {
pub color: UiColor, pub color: UiColor,
pub font_size: f32, pub font_size: f32,
pub line_height: f32, pub line_height: f32,
pub family: Family, pub family: Family<'static>,
pub wrap: bool, pub wrap: bool,
/// inner alignment of text region (within where it's drawn)
pub align: RegionAlign, pub align: RegionAlign,
} }
pub const LINE_HEIGHT_MULT: f32 = 1.1; impl TextAttrs {
pub fn apply(&self, font_system: &mut FontSystem, buf: &mut Buffer, width: Option<f32>) {
buf.set_metrics_and_size(
font_system,
Metrics::new(self.font_size, self.line_height),
width,
None,
);
let attrs = Attrs::new().family(self.family);
let list = AttrsList::new(&attrs);
for line in &mut buf.lines {
line.set_attrs_list(list.clone());
}
}
}
pub type TextBuffer = Buffer;
impl Default for TextAttrs { impl Default for TextAttrs {
fn default() -> Self { fn default() -> Self {
@@ -107,334 +70,122 @@ impl Default for TextAttrs {
} }
} }
/// Keeps text and its corresponding layout from getting out of sync. pub const LINE_HEIGHT_MULT: f32 = 1.1;
pub struct TextBuffer {
text: String,
layout: Layout<UiColor>,
layout_key: Option<LayoutKey>,
/// The glyphs placed from `layout`, so drawing this text again at the
/// width it already has places them once.
placed: Option<RenderedText>,
}
#[derive(PartialEq)]
struct LayoutKey {
attrs: TextAttrs,
max_width: Option<f32>,
}
impl TextBuffer {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
layout: Layout::new(),
layout_key: None,
placed: None,
}
}
pub fn new_empty() -> Self {
Self::new("")
}
pub fn text(&self) -> &str {
&self.text
}
pub fn layout(&self) -> &Layout<UiColor> {
&self.layout
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn set_text(&mut self, text: impl Into<String>) {
let text = text.into();
if text != self.text {
self.text = text;
self.layout_key = None;
self.placed = None;
}
}
/// Invalidates the layout and returns the underlying string for editing.
pub fn edit(&mut self) -> &mut String {
self.layout_key = None;
self.placed = None;
&mut self.text
}
/// The glyphs of the shaping it is drawn as, once they are placed.
pub fn rendered(&self) -> Option<&RenderedText> {
self.placed.as_ref()
}
/// The width its shaping wraps at, and `None` where it does not wrap or
/// has not been shaped.
pub fn wrap_width(&self) -> Option<f32> {
self.layout_key.as_ref()?.max_width
}
/// Widths covered by the current line breaks, including a wider shaping
/// retained when a later draw requested a narrower box.
pub fn width_holds(&self) -> crate::Holds {
let Some(width) = self.wrap_width() else {
return crate::Holds::ANY;
};
let width = Px::from_f32(width);
let soft_wrapped = self.layout.lines().any(|line| {
matches!(
line.break_reason(),
parley::layout::BreakReason::Regular | parley::layout::BreakReason::Emergency
)
});
let upper = if soft_wrapped { width } else { Px::MAX };
crate::Holds::from(Px::ceil_from_f32(self.layout.width()).min(width)..=upper)
}
pub fn size(&self) -> Vec2 {
Vec2::new(self.layout.width(), self.layout.height())
}
pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option<f32>) {
let layout_key = LayoutKey {
attrs: attrs.clone(),
max_width: width,
};
if self.layout_key.as_ref() == Some(&layout_key) {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapeHits);
return;
}
// A greedy break at one width is the same break at every width down
// to the longest line it produced: each line still fits, and none can
// take a word that would not fit in the wider box. So the layout in
// hand already answers, and re-breaking would only be work.
//
// At the longest line exactly, with no margin below it. A narrower
// width really does break differently, so answering one from the
// break in hand is how a warm tree keeps lines a cold tree would
// never produce. The margin was here because a text reports the
// width it used and a parent hands that back; the report is the step
// at or above its longest line now, so what comes back fits.
if let Some(key) = &self.layout_key
&& key.attrs == *attrs
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
&& want <= broke_at
&& want >= self.layout.width()
{
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapeHits);
return;
}
let same_shaping = self
.layout_key
.as_ref()
.is_some_and(|key| key.attrs == *attrs);
let old_key = self.layout_key.replace(layout_key);
// The glyphs it holds are of the width it held, which the layout may
// well come back to.
if let Some(key) = old_key
&& let Some(glyphs) = self.placed.take()
{
data.keep_placed(Placed {
text: self.text.clone(),
key,
glyphs,
});
}
// Only the line breaking depends on the width: the shaped runs under
// it are a function of the text and the attrs, and parley re-breaks
// them in place. So a new width is a break, not a shaping.
if same_shaping {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextBreaks);
#[cfg(feature = "layout-diagnostics")]
let _break = diag::timer(TimerKind::TextBreak);
self.break_lines(width);
return;
}
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapes);
#[cfg(feature = "layout-diagnostics")]
let _shape = diag::timer(TimerKind::TextShape);
let mut builder = data
.layout_ctx
.ranged_builder(&mut data.font_ctx, &self.text, 1.0, true);
builder.push_default(StyleProperty::FontFamily(attrs.family.family()));
builder.push_default(StyleProperty::FontSize(attrs.font_size));
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
attrs.line_height,
)));
builder.push_default(StyleProperty::Brush(attrs.color));
builder.build_into(&mut self.layout, &self.text);
self.break_lines(width);
}
fn break_lines(&mut self, width: Option<f32>) {
self.layout.break_all_lines(width);
self.layout
.align(Alignment::Start, AlignmentOptions::default());
}
}
impl TextData { impl TextData {
pub fn place(&mut self, buffer: &TextBuffer) -> Vec<PlacedGlyph> { pub fn draw(
let mut placed = Vec::new();
for line in buffer.layout.lines() {
for item in line.items() {
let PositionedLayoutItem::GlyphRun(run) = item else {
continue;
};
let font = run.run().font();
let font_size = run.run().font_size();
let coords = run.run().normalized_coords();
let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize)
else {
continue;
};
let coords_hash = hash_coords(coords);
let font_id = font.data.id();
for glyph in run.positioned_glyphs() {
let subpixel = ((glyph.x.fract() * 4.0).round() as i32).rem_euclid(4) as u8;
let key = GlyphKey {
font: font_id,
glyph: glyph.id,
size: glyph_size_key(font_size),
subpixel,
coords: coords_hash,
};
let Some(entry) = self.glyph_entry(GlyphRaster {
key,
font: font_ref,
font_size,
coords,
subpixel,
glyph_id: glyph.id,
}) else {
continue;
};
placed.push(PlacedGlyph {
entry,
offset: PxVec2::new(
Px::from_int(glyph.x.floor() as i32 + entry.left),
Px::from_int(glyph.y.floor() as i32 - entry.top),
),
});
}
}
}
placed
}
fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option<GlyphEntry> {
if let Some(entry) = self.atlas.get(&glyph.key) {
return entry;
}
let mut scaler = self
.scale_ctx
.builder(glyph.font)
.size(glyph.font_size)
.hint(true)
.normalized_coords(glyph.coords)
.build();
let image = Render::new(&[
Source::ColorOutline(0),
Source::ColorBitmap(StrikeWith::BestFit),
Source::Outline,
])
.format(Format::Alpha)
.offset(Vector::new(glyph.subpixel as f32 / 4.0, 0.0))
.render(&mut scaler, glyph.glyph_id as u16);
if let Some(image) = image {
self.atlas.insert(glyph.key, &image)
} else {
self.atlas.insert_empty(glyph.key);
None
}
}
}
struct GlyphRaster<'a> {
key: GlyphKey,
font: FontRef<'a>,
font_size: f32,
coords: &'a [i16],
subpixel: u8,
glyph_id: u32,
}
fn hash_coords(coords: &[i16]) -> u64 {
let mut hasher = DefaultHasher::new();
coords.hash(&mut hasher);
hasher.finish()
}
const GLYPH_SIZE_STEPS_PER_PIXEL: f32 = 16.0;
fn glyph_size_key(font_size: f32) -> u32 {
(font_size * GLYPH_SIZE_STEPS_PER_PIXEL).round() as u32
}
pub struct RenderedText {
pub glyphs: Vec<PlacedGlyph>,
pub size: Vec2,
pub color: UiColor,
}
impl TextData {
/// The glyphs of this text at this width, taken out of what is kept.
fn take_placed(&mut self, text: &str, key: &LayoutKey) -> Option<RenderedText> {
// From the newest, since a re-ask is usually of something recent.
let at = self
.spare
.iter()
.rposition(|spare| spare.key == *key && spare.text == text)?;
self.spare.remove(at).map(|spare| spare.glyphs)
}
fn keep_placed(&mut self, placed: Placed) {
if self.spare.len() >= SPARE_PLACED {
self.spare.pop_front();
}
self.spare.push_back(placed);
}
pub fn render<'b>(
&mut self, &mut self,
buffer: &'b mut TextBuffer, buffer: &mut TextBuffer,
attrs: &TextAttrs, attrs: &TextAttrs,
width: Option<f32>, textures: &mut Textures,
) -> &'b RenderedText { ) -> RenderedText {
#[cfg(feature = "layout-diagnostics")] // TODO: either this or the layout stuff (or both) is super slow,
diag::bump(Counter::TextRenders); // should probably do texture packing and things if possible.
#[cfg(feature = "layout-diagnostics")] // very visible if you add just a couple of wrapping texts and resize window
let _render = diag::timer(TimerKind::TextRender); // should also be timed to figure out exactly what points need to be sped up
buffer.shape(self, attrs, width); // let mut pixels = HashMap::<_, [u8; 4]>::default();
// Only asked for when the buffer no longer holds them: taking one out let mut min_x = 0;
// of the store to then drop it would throw an answer away. let mut min_y = 0;
let placed = buffer.placed.take().or_else(|| { let mut max_x = 0;
let key = buffer.layout_key.as_ref()?; let mut max_y = 0;
self.take_placed(&buffer.text, key) let text_color = {
}); let c = attrs.color;
let placed = match placed { cosmic_text::Color::rgba(c.r, c.g, c.b, c.a)
Some(placed) => placed, };
None => { let mut max_width = 0.0f32;
#[cfg(feature = "layout-diagnostics")] let mut height = 0.0;
diag::bump(Counter::GlyphPlacements);
#[cfg(feature = "layout-diagnostics")] for run in buffer.layout_runs() {
let _place = diag::timer(TimerKind::GlyphPlacement); for glyph in run.glyphs.iter() {
RenderedText { let physical_glyph = glyph.physical((0., 0.), 1.0);
glyphs: self.place(buffer),
size: buffer.size(), let glyph_color = match glyph.color_opt {
color: attrs.color, Some(some) => some,
None => text_color,
};
if let Some(img) = self
.swash_cache
.get_image(&mut self.font_system, physical_glyph.cache_key)
{
let mut pos = img.placement;
pos.left += physical_glyph.x;
pos.top = physical_glyph.y + run.line_y as i32 - pos.top;
min_x = min_x.min(pos.left);
min_y = min_y.min(pos.top);
max_x = max_x.max(pos.left + pos.width as i32);
max_y = max_y.max(pos.top + pos.height as i32);
self.glyph_cache
.push((pos, physical_glyph.cache_key, glyph_color));
} }
} }
}; max_width = max_width.max(run.line_w);
buffer.placed.insert(placed) height += run.line_height;
}
let img_width = (max_x - min_x + 1) as u32;
let img_height = (max_y - min_y + 1) as u32;
let mut image = RgbaImage::new(img_width, img_height);
for (pos, key, color) in self.glyph_cache.drain(..) {
let img = self
.swash_cache
.get_image(&mut self.font_system, key)
.as_ref()
.unwrap();
let mut merge = |i, color: [u8; 4]| {
let i = i as i32;
let x = (i % pos.width as i32 + pos.left - min_x) as u32;
let y = (i / pos.width as i32 + pos.top - min_y) as u32;
let pixel = &mut image[(x, y)].0;
// TODO: no clue if proper alpha blending should be done
*pixel = Simd::from(color).saturating_add(Simd::from(*pixel)).into();
};
match img.content {
SwashContent::Mask => {
for (i, a) in img.data.iter().enumerate() {
let mut color = color.as_rgba();
color[3] = ((color[3] as u32 * *a as u32) / u8::MAX as u32) as u8;
merge(i, color);
}
}
SwashContent::SubpixelMask => todo!("subpixel mask text rendering"),
SwashContent::Color => {
let (colors, _) = img.data.as_chunks::<4>();
for (i, color) in colors.iter().enumerate() {
merge(i, *color);
}
}
}
}
let max_dim = 8192;
if image.width() > max_dim || image.height() > max_dim {
let width = image.width().min(max_dim);
let height = image.height().min(max_dim);
eprintln!(
"WARNING: image of size {:?} cropped to {:?} (texture too big)",
image.dimensions(),
(width, height)
);
image = image.view(0, 0, width, height).to_image();
}
RenderedText {
handle: textures.add(image),
top_left_offset: Vec2::new(min_x as f32, min_y as f32),
size: Vec2::new(max_width, height),
}
} }
} }
#[derive(Clone)]
pub struct RenderedText {
pub handle: TextureHandle,
pub top_left_offset: Vec2,
pub size: Vec2,
}
pub trait HasTextures {
fn add_texture(&mut self, image: DynamicImage) -> TextureHandle;
}
+16 -41
View File
@@ -1,4 +1,7 @@
use crate::util::{RefCounter, Vec2}; use crate::{
render::TexturePrimitive,
util::{RefCounter, Vec2},
};
use image::{DynamicImage, GenericImageView}; use image::{DynamicImage, GenericImageView};
use std::{ use std::{
ops::Index, ops::Index,
@@ -7,7 +10,7 @@ use std::{
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TextureHandle { pub struct TextureHandle {
slot: u32, inner: TexturePrimitive,
size: Vec2, size: Vec2,
counter: RefCounter, counter: RefCounter,
send: Sender<u32>, send: Sender<u32>,
@@ -26,26 +29,14 @@ pub struct Textures {
pub enum TextureUpdate<'a> { pub enum TextureUpdate<'a> {
Push(&'a DynamicImage), Push(&'a DynamicImage),
Set(u32, &'a DynamicImage), Set(u32, &'a DynamicImage),
Patch(u32, PatchRect, &'a DynamicImage),
Free(u32), Free(u32),
/// Added and freed before the renderer drained either update. It still has
/// to push a slot to stay lined up with `images`; `Free` then empties it.
PushFree, PushFree,
SetFree, SetFree,
} }
#[derive(Debug, Clone, Copy)]
pub struct PatchRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
enum Update { enum Update {
Push(u32), Push(u32),
Set(u32), Set(u32),
Patch(u32, PatchRect),
Free(u32), Free(u32),
} }
@@ -63,8 +54,14 @@ impl Textures {
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle { pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
let image = image.into(); let image = image.into();
let size = image.dimensions().into(); let size = image.dimensions().into();
let view_idx = self.push(image);
// 0 == default in renderer; TODO: actually create samplers here
let sampler_idx = 0;
TextureHandle { TextureHandle {
slot: self.push(image), inner: TexturePrimitive {
view_idx,
sampler_idx,
},
size, size,
counter: RefCounter::new(), counter: RefCounter::new(),
send: self.send.clone(), send: self.send.clone(),
@@ -84,23 +81,6 @@ impl Textures {
} }
} }
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
self.images[handle.slot as usize]
.as_mut()
.expect("texture was freed while still held")
}
/// Queue an upload of just `rect`, after writing it with `image_mut`.
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
self.updates.push(Update::Patch(handle.slot, rect));
}
/// How many textures are live, which is what a ui can ask; the renderer's
/// copies follow from the updates it drains.
pub fn count(&self) -> usize {
self.images.iter().flatten().count()
}
pub fn free(&mut self) { pub fn free(&mut self) {
for idx in self.recv.try_iter() { for idx in self.recv.try_iter() {
self.images[idx as usize] = None; self.images[idx as usize] = None;
@@ -119,19 +99,14 @@ impl Textures {
.as_ref() .as_ref()
.map(|img| TextureUpdate::Set(i, img)) .map(|img| TextureUpdate::Set(i, img))
.unwrap_or(TextureUpdate::SetFree), .unwrap_or(TextureUpdate::SetFree),
Update::Patch(i, rect) => self.images[i as usize]
.as_ref()
.map(|img| TextureUpdate::Patch(i, rect, img))
.unwrap_or(TextureUpdate::SetFree),
Update::Free(i) => TextureUpdate::Free(i), Update::Free(i) => TextureUpdate::Free(i),
}) })
} }
} }
impl TextureHandle { impl TextureHandle {
/// Index into `Textures`, and into the renderer's parallel slots. pub fn primitive(&self) -> TexturePrimitive {
pub fn slot(&self) -> u32 { self.inner
self.slot
} }
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
self.size self.size
@@ -141,7 +116,7 @@ impl TextureHandle {
impl Drop for TextureHandle { impl Drop for TextureHandle {
fn drop(&mut self) { fn drop(&mut self) {
if self.counter.drop() { if self.counter.drop() {
let _ = self.send.send(self.slot); let _ = self.send.send(self.inner.view_idx);
} }
} }
} }
@@ -150,7 +125,7 @@ impl Index<&TextureHandle> for Textures {
type Output = DynamicImage; type Output = DynamicImage;
fn index(&self, index: &TextureHandle) -> &Self::Output { fn index(&self, index: &TextureHandle) -> &Self::Output {
self.images[index.slot as usize].as_ref().unwrap() self.images[index.inner.view_idx as usize].as_ref().unwrap()
} }
} }
-247
View File
@@ -1,247 +0,0 @@
use crate::{
PatchRect, PxVec2,
util::{HashMap, Vec2},
};
use image::RgbaImage;
use swash::scale::image::{Content, Image};
/// Side of one page, and so of every layer of `render::page`'s array texture.
pub(crate) const PAGE: u32 = 1024;
/// Transparent margin kept around every glyph, so that sampling one cannot
/// pick up its neighbour along a shared edge.
const PAD: u32 = 1;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct GlyphKey {
pub font: u64,
pub glyph: u32,
/// Font size in 1/16 px, so sizes that round to the same pixels share a
/// raster instead of filling the atlas with near-duplicates.
pub size: u32,
/// Horizontal subpixel phase, in 1/4 px.
pub subpixel: u8,
/// Hash of the variation coordinates; a variable font at two weights is two
/// different sets of pixels from one glyph id.
pub coords: u64,
}
#[derive(Clone, Copy)]
pub struct GlyphEntry {
pub uv_min: Vec2,
pub uv_max: Vec2,
/// Offset from the glyph's pen position to the top-left of its pixels.
pub left: i32,
pub top: i32,
pub width: u32,
pub height: u32,
pub is_colored: bool,
/// Which atlas array layer this glyph is on.
pub layer: u32,
}
impl GlyphEntry {
const IS_COLORED: u32 = 1;
pub(crate) fn flags(&self) -> u32 {
if self.is_colored { Self::IS_COLORED } else { 0 }
}
}
struct Page {
image: RgbaImage,
x: u32,
y: u32,
shelf_height: u32,
}
/// A rectangle of one page the renderer has not uploaded yet.
#[derive(Clone, Copy)]
pub struct PageUpload {
pub layer: u32,
pub rect: PatchRect,
}
#[derive(Default)]
pub struct GlyphAtlas {
pages: Vec<Page>,
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
/// too, so it is not re-rasterised on every layout.
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
uploads: Vec<PageUpload>,
}
impl GlyphAtlas {
pub fn get(&self, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
self.entries.get(key).copied()
}
pub fn insert(&mut self, key: GlyphKey, image: &Image) -> Option<GlyphEntry> {
let w = image.placement.width;
let h = image.placement.height;
if w == 0 || h == 0 {
log::warn!(
"glyph {} in font {} rasterized at {w}x{h}; skipping it",
key.glyph,
key.font,
);
self.entries.insert(key, None);
return None;
}
if w > PAGE - PAD * 2 || h > PAGE - PAD * 2 {
log::warn!(
"glyph {} in font {} rasterized at {w}x{h}, too large for the {PAGE}x{PAGE} atlas; skipping it",
key.glyph,
key.font,
);
self.entries.insert(key, None);
return None;
}
let upload = self.allocate(w, h);
let PatchRect { x, y, .. } = upload.rect;
write_glyph(&mut self.pages[upload.layer as usize].image, image, x, y);
self.uploads.push(upload);
let scale = 1.0 / PAGE as f32;
let entry = GlyphEntry {
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
uv_max: Vec2::new((x + w) as f32 * scale, (y + h) as f32 * scale),
left: image.placement.left,
top: image.placement.top,
width: w,
height: h,
is_colored: matches!(image.content, Content::Color),
layer: upload.layer,
};
self.entries.insert(key, Some(entry));
Some(entry)
}
/// Reserves room for a `w` by `h` glyph, adding a page if none has it.
fn allocate(&mut self, w: u32, h: u32) -> PageUpload {
let rect = |x, y| PatchRect {
x,
y,
width: w,
height: h,
};
if let Some((i, (x, y))) = self
.pages
.iter_mut()
.enumerate()
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
{
return PageUpload {
layer: i as u32,
rect: rect(x, y),
};
}
self.pages.push(Page {
image: RgbaImage::new(PAGE, PAGE),
x: PAD + w + PAD,
y: PAD,
shelf_height: h + PAD,
});
PageUpload {
layer: self.pages.len() as u32 - 1,
rect: rect(PAD, PAD),
}
}
/// Drains what has been written since the last call, for the renderer to
/// upload. A new page needs nothing more: wgpu leaves the rest of a fresh
/// layer transparent, which is what an atlas wants.
pub fn uploads(&mut self) -> impl Iterator<Item = (PageUpload, &RgbaImage)> {
let pages = &self.pages;
self.uploads
.drain(..)
.map(|upload| (upload, &pages[upload.layer as usize].image))
}
pub fn insert_empty(&mut self, key: GlyphKey) {
self.entries.insert(key, None);
}
pub fn page_count(&self) -> u32 {
self.pages.len() as u32
}
pub fn glyph_count(&self) -> usize {
self.entries.len()
}
}
impl Page {
fn allocate(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
let need_w = w + PAD;
let need_h = h + PAD;
if self.x + need_w > PAGE {
if need_w + PAD > PAGE || self.y + self.shelf_height + need_h > PAGE {
return None;
}
self.y += self.shelf_height;
self.x = PAD;
self.shelf_height = 0;
} else if self.y + need_h > PAGE {
return None;
}
let position = (self.x, self.y);
self.x += need_w;
self.shelf_height = self.shelf_height.max(need_h);
Some(position)
}
}
/// Mask glyphs keep coverage in alpha so their raster can be tinted at draw time.
fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
let width = image.placement.width as usize;
let height = image.placement.height as usize;
let page_stride = page.width() as usize * 4;
let x = x as usize * 4;
let y = y as usize;
let page = page.as_mut();
for row in 0..height {
let start = (y + row) * page_stride + x;
let target = &mut page[start..start + width * 4];
match image.content {
Content::Color => {
let start = row * width * 4;
target.copy_from_slice(&image.data[start..start + width * 4]);
}
Content::Mask => {
let start = row * width;
for (target, &alpha) in target
.as_chunks_mut::<4>()
.0
.iter_mut()
.zip(&image.data[start..start + width])
{
target.copy_from_slice(&[255, 255, 255, alpha]);
}
}
Content::SubpixelMask => {
let start = row * width * 4;
for (target, source) in target
.as_chunks_mut::<4>()
.0
.iter_mut()
.zip(image.data[start..start + width * 4].as_chunks::<4>().0)
{
target.copy_from_slice(&[255, 255, 255, source[1]]);
}
}
}
}
}
#[derive(Clone, Copy)]
pub struct PlacedGlyph {
pub entry: GlyphEntry,
/// Whole pixels from the origin of the text to this glyph's top-left,
/// on the grid once here rather than on every frame that draws it.
pub offset: PxVec2,
}
+11 -51
View File
@@ -1,30 +1,31 @@
use crate::{UiRegion, util::Id, util::Vec2}; use crate::{UiRegion, util::Id};
use wgpu::*; use wgpu::*;
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct WindowUniform { pub struct WindowUniform {
pub dim: Vec2, pub width: f32,
pub height: f32,
} }
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PrimitiveInstance { pub struct PrimitiveInstance {
pub region: UiRegion, pub region: UiRegion,
pub binding: u32,
pub idx: u32,
pub mask_idx: MaskIdx, pub mask_idx: MaskIdx,
pub move_idx: MoveIdx,
} }
impl PrimitiveInstance { impl PrimitiveInstance {
// The region's four scalars, each a `Rel` beside a `Px`: whole counts const ATTRIBS: [VertexAttribute; 7] = vertex_attr_array![
// that the shader decodes, rather than the numbers themselves. 0 => Float32x2,
const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![ 1 => Float32x2,
0 => Sint32x2, 2 => Float32x2,
1 => Sint32x2, 3 => Float32x2,
2 => Sint32x2,
3 => Sint32x2,
4 => Uint32, 4 => Uint32,
5 => Uint32, 5 => Uint32,
6 => Uint32,
]; ];
pub fn desc() -> VertexBufferLayout<'static> { pub fn desc() -> VertexBufferLayout<'static> {
@@ -46,45 +47,4 @@ impl MaskIdx {
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Mask { pub struct Mask {
pub region: UiRegion, pub region: UiRegion,
pub move_idx: MoveIdx,
}
/// Its own type rather than another `Id<u32>`, because it sits beside
/// `MaskIdx` in an instance and the two must not be swappable.
#[repr(transparent)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MoveIdx(u32);
impl MoveIdx {
pub const NONE: Self = Self(u32::MAX);
pub(crate) fn slot(idx: usize) -> Self {
Self(idx as u32)
}
pub(crate) fn idx(self) -> usize {
self.0 as usize
}
}
/// One link of the chain a primitive's position is resolved through: the box
/// its contents are placed within, given in the coordinates of the slot it
/// names. Moving or resizing a subtree writes its own slot and nothing else.
///
/// The identity is `UiRegion::FULL`, not zero: a zeroed entry is a box of no
/// extent, which collapses everything under it to a point.
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct MoveOffset {
pub region: UiRegion,
pub parent: MoveIdx,
}
unsafe impl bytemuck::Pod for MoveOffset {}
unsafe impl bytemuck::Zeroable for MoveOffset {}
impl MoveOffset {
pub fn new(parent: MoveIdx, region: UiRegion) -> Self {
Self { region, parent }
}
} }
+196 -281
View File
@@ -1,94 +1,61 @@
use std::num::NonZero;
use crate::{ use crate::{
UiData, UiRenderState, UiData, UiRenderState,
render::{data::PrimitiveInstance, util::ArrBuf}, render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
util::{HashMap, Vec2}, util::HashMap,
}; };
use data::WindowUniform; use data::WindowUniform;
use wgpu::{ use wgpu::{
util::{BufferInitDescriptor, DeviceExt}, util::{BufferInitDescriptor, DeviceExt},
*, *,
}; };
use winit::dpi::PhysicalSize;
mod atlas;
mod data; mod data;
mod page;
mod primitive; mod primitive;
mod texture; mod texture;
mod util; mod util;
pub use atlas::*; pub use data::{Mask, MaskIdx};
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset};
pub use primitive::*; pub use primitive::*;
const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
fn module_source(wgsl: &str) -> String {
// The steps come from the same constants the CPU counts in, rather than
// a second copy of them written into the shader: a grid the two disagree
// about puts every coordinate somewhere else.
format!(
"const PX_STEP: f32 = 1.0 / {}.0;\nconst REL_STEP: f32 = 1.0 / {}.0;\n{PRELUDE}\n{wgsl}",
1u32 << crate::PX_SHIFT,
1u32 << crate::REL_SHIFT,
)
}
pub struct UiRenderNode { pub struct UiRenderNode {
shared_layout: BindGroupLayout, uniform_group: BindGroup,
shared_group: BindGroup, primitive_layout: BindGroupLayout,
format: TextureFormat, rsc_layout: BindGroupLayout,
rsc_group: BindGroup,
/// One per registered primitive, in id order. pipeline: RenderPipeline,
primitives: Vec<PrimitivePipeline>,
layers: HashMap<usize, RenderLayer>, layers: HashMap<usize, RenderLayer>,
active: Vec<usize>, active: Vec<usize>,
window_buffer: Buffer, window_buffer: Buffer,
textures: GpuTextures,
masks: ArrBuf<Mask>, masks: ArrBuf<Mask>,
moves: ArrBuf<MoveOffset>,
} }
struct RenderLayer { struct RenderLayer {
/// One per registered primitive, `None` where this layer draws none.
primitives: Vec<Option<ListBuffers>>,
}
/// What draws one registered primitive.
struct PrimitivePipeline {
data_layout: BindGroupLayout,
pipeline: RenderPipeline,
render: Box<dyn PrimitiveRender>,
}
/// One list's vertex buffer and the data its shader reads.
struct ListBuffers {
instance: ArrBuf<PrimitiveInstance>, instance: ArrBuf<PrimitiveInstance>,
data: ArrBuf<u8>, primitives: PrimitiveBuffers,
group: Option<BindGroup>, primitive_group: BindGroup,
/// What the primitive asked to keep per instance, if anything.
bindings: Vec<u32>,
} }
impl UiRenderNode { impl UiRenderNode {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) { pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
pass.set_bind_group(0, &self.shared_group, &[]); pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.uniform_group, &[]);
pass.set_bind_group(2, &self.rsc_group, &[]);
for i in &self.active { for i in &self.active {
let layer = &self.layers[i]; let layer = &self.layers[i];
for (id, list) in layer.primitives.iter().enumerate() { if layer.instance.len() == 0 {
let Some(list) = list else { continue }; continue;
let Some(group) = &list.group else { continue };
let primitive = &self.primitives[id];
pass.set_pipeline(&primitive.pipeline);
pass.set_bind_group(1, group, &[]);
pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
primitive.render.draw(
pass,
ListDraw {
instances: list.instance.len() as u32,
bindings: &list.bindings,
},
);
} }
pass.set_bind_group(1, &layer.primitive_group, &[]);
pass.set_vertex_buffer(0, layer.instance.buffer.slice(..));
pass.draw(0..4, 0..layer.instance.len() as u32);
} }
} }
@@ -99,157 +66,145 @@ impl UiRenderNode {
ui: &mut UiData, ui: &mut UiData,
ui_render: &mut UiRenderState, ui_render: &mut UiRenderState,
) { ) {
// Before the layers: each list is given its pipeline's data layout.
self.build_pipelines(device, queue, &ui.primitives);
self.active.clear(); self.active.clear();
for (i, draws) in ui_render.layers.iter_mut() { for (i, primitives) in ui_render.layers.iter_mut() {
self.active.push(i); self.active.push(i);
for change in draws.apply_free() { for change in primitives.apply_free() {
if let Some(inst) = ui_render.active.get_mut(&change.id) { if let Some(inst) = ui_render.active.get_mut(&change.id) {
for primitive in &mut inst.primitives { for h in &mut inst.primitives {
let h = &mut primitive.handle; if h.layer == i && h.inst_idx == change.old {
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
h.inst_idx = change.new; h.inst_idx = change.new;
break; break;
} }
} }
} }
} }
let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new); let rlayer = self.layers.entry(i).or_insert_with(|| {
if draws.updated { let primitives = PrimitiveBuffers::new(device);
let lists = draws.primitives(); let primitive_group =
// The zip would otherwise skip a list with no pipeline. Self::primitive_group(device, &self.primitive_layout, primitives.buffers());
assert!(lists.len() <= self.primitives.len()); RenderLayer {
rlayer.primitives.resize_with(lists.len(), || None); instance: ArrBuf::new(
for ((buffers, list), primitive) in rlayer device,
.primitives BufferUsages::VERTEX | BufferUsages::COPY_DST,
.iter_mut() "instance",
.zip(lists) ),
.zip(&self.primitives) primitives,
{ primitive_group,
let Some(list) = list else {
continue;
};
buffers
.get_or_insert_with(|| ListBuffers::new(device))
.update(device, queue, primitive, list);
} }
draws.updated = false; });
if primitives.updated {
rlayer
.instance
.update(device, queue, primitives.instances());
rlayer.primitives.update(device, queue, primitives.data());
rlayer.primitive_group = Self::primitive_group(
device,
&self.primitive_layout,
rlayer.primitives.buffers(),
);
primitives.updated = false;
} }
} }
for primitive in &mut self.primitives { let mut changed = false;
primitive.render.update(ui); changed |= self.textures.update(&mut ui.textures);
}
let mut regroup = false;
if ui.masks.changed { if ui.masks.changed {
ui.masks.changed = false; ui.masks.changed = false;
regroup |= self.masks.update(device, queue, &ui.masks[..]); self.masks.update(device, queue, &ui.masks[..]);
changed = true;
} }
if ui_render.moves.changed { if changed {
ui_render.moves.changed = false; self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks);
regroup |= self.moves.update(device, queue, ui_render.moves.entries());
}
if regroup {
self.shared_group = Self::shared_group(
device,
&self.shared_layout,
&self.window_buffer,
&self.masks,
&self.moves,
);
} }
} }
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) { pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &Queue) {
let size = size.into(); let slice = &[WindowUniform {
let slice = &[WindowUniform { dim: size }]; width: size.width as f32,
height: size.height as f32,
}];
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice)); queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
} }
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self { pub fn new(
let window_uniform = WindowUniform { device: &Device,
dim: Vec2::new(config.width as f32, config.height as f32), queue: &Queue,
}; config: &SurfaceConfiguration,
limits: UiLimits,
) -> Self {
let shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("UI Shape Shader"),
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
});
let window_uniform = WindowUniform::default();
let window_buffer = device.create_buffer_init(&BufferInitDescriptor { let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("window"), label: Some("window"),
contents: bytemuck::cast_slice(&[window_uniform]), contents: bytemuck::cast_slice(&[window_uniform]),
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
}); });
let shared_layout = Self::shared_layout(device); let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: Some("window"),
});
let uniform_group = Self::bind_group_0(device, &uniform_layout, &window_buffer);
let primitive_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &core::array::from_fn::<_, { PrimitiveBuffers::LEN }, _>(|i| {
BindGroupLayoutEntry {
binding: i as u32,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
}),
label: Some("primitive"),
});
let tex_manager = GpuTextures::new(device, queue);
let masks = ArrBuf::new( let masks = ArrBuf::new(
device, device,
BufferUsages::STORAGE | BufferUsages::COPY_DST, BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui masks", "ui masks",
); );
let moves = ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui move offsets",
);
let shared_group =
Self::shared_group(device, &shared_layout, &window_buffer, &masks, &moves);
Self { let rsc_layout = Self::rsc_layout(device, &limits);
shared_layout, let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
shared_group,
format: config.format,
primitives: Vec::new(),
window_buffer,
layers: HashMap::default(),
active: Vec::new(),
masks,
moves,
}
}
/// Compiles a pipeline for every primitive registered since the last call. let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
/// Sources only ever arrive at the end, so an id keeps its pipeline. label: Some("UI Shape Pipeline Layout"),
fn build_pipelines(&mut self, device: &Device, queue: &Queue, registry: &PrimitiveRegistry) { bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout].map(Some),
for source in &registry.sources()[self.primitives.len()..] { immediate_size: 0,
let render = (source.render)(device, queue);
let data_layout = Self::data_layout(device, source.stride);
let mut groups = vec![Some(&self.shared_layout), Some(&data_layout)];
groups.extend(render.layout().map(Some));
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some(source.label),
bind_group_layouts: &groups,
immediate_size: 0,
});
let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label);
self.primitives.push(PrimitivePipeline {
data_layout,
pipeline,
render,
});
}
}
fn pipeline(
device: &Device,
layout: &PipelineLayout,
format: TextureFormat,
wgsl: &str,
label: &str,
) -> RenderPipeline {
let module = device.create_shader_module(ShaderModuleDescriptor {
label: Some(label),
source: ShaderSource::Wgsl(module_source(wgsl).into()),
}); });
device.create_render_pipeline(&RenderPipelineDescriptor { let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some(label), label: Some("UI Shape Pipeline"),
layout: Some(layout), layout: Some(&pipeline_layout),
vertex: VertexState { vertex: VertexState {
module: &module, module: &shader,
entry_point: Some("vs_main"), entry_point: Some("vs_main"),
buffers: &[Some(PrimitiveInstance::desc())], buffers: &[Some(PrimitiveInstance::desc())],
compilation_options: Default::default(), compilation_options: Default::default(),
}, },
fragment: Some(FragmentState { fragment: Some(FragmentState {
module: &module, module: &shader,
entry_point: Some("fs_main"), entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState { targets: &[Some(ColorTargetState {
format, format: config.format,
blend: Some(BlendState::ALPHA_BLENDING), blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL, write_mask: ColorWrites::ALL,
})], })],
@@ -272,176 +227,136 @@ impl UiRenderNode {
}, },
multiview_mask: None, multiview_mask: None,
cache: None, cache: None,
});
Self {
uniform_group,
primitive_layout,
rsc_layout,
rsc_group,
pipeline,
window_buffer,
layers: HashMap::default(),
active: Vec::new(),
textures: tex_manager,
masks,
}
}
fn bind_group_0(
device: &Device,
layout: &BindGroupLayout,
window_buffer: &Buffer,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[BindGroupEntry {
binding: 0,
resource: window_buffer.as_entire_binding(),
}],
label: Some("ui window"),
}) })
} }
/// What every draw in the ui is given: the window, the masks and the fn primitive_group(
/// move chain every position is resolved through. device: &Device,
fn shared_layout(device: &Device) -> BindGroupLayout { layout: &BindGroupLayout,
buffers: [(u32, &Buffer); PrimitiveBuffers::LEN],
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &buffers.map(|(binding, buf)| BindGroupEntry {
binding,
resource: buf.as_entire_binding(),
}),
label: Some("ui primitives"),
})
}
fn rsc_layout(device: &Device, limits: &UiLimits) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor { device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[ entries: &[
BindGroupLayoutEntry { BindGroupLayoutEntry {
binding: 0, binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer { ty: BindingType::Texture {
ty: BufferBindingType::Uniform, sample_type: TextureSampleType::Float { filterable: false },
has_dynamic_offset: false, view_dimension: TextureViewDimension::D2,
min_binding_size: BufferSize::new(size_of::<WindowUniform>() as u64), multisampled: false,
}, },
count: None, count: Some(NonZero::new(limits.max_textures).unwrap()),
}, },
BindGroupLayoutEntry { BindGroupLayoutEntry {
binding: 1, binding: 1,
visibility: ShaderStages::FRAGMENT, visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer { ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
ty: BufferBindingType::Storage { read_only: true }, count: Some(NonZero::new(limits.max_samplers).unwrap()),
has_dynamic_offset: false,
min_binding_size: BufferSize::new(size_of::<Mask>() as u64),
},
count: None,
}, },
BindGroupLayoutEntry { BindGroupLayoutEntry {
binding: 2, binding: 2,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer { ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true }, ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false, has_dynamic_offset: false,
min_binding_size: BufferSize::new(size_of::<MoveOffset>() as u64), min_binding_size: None,
}, },
count: None, count: None,
}, },
], ],
label: Some("ui shared"), label: Some("ui rsc"),
}) })
} }
fn shared_group( fn rsc_group(
device: &Device, device: &Device,
layout: &BindGroupLayout, layout: &BindGroupLayout,
window: &Buffer, tex_manager: &GpuTextures,
masks: &ArrBuf<Mask>, masks: &ArrBuf<Mask>,
moves: &ArrBuf<MoveOffset>,
) -> BindGroup { ) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor { device.create_bind_group(&BindGroupDescriptor {
layout, layout,
entries: &[ entries: &[
BindGroupEntry { BindGroupEntry {
binding: 0, binding: 0,
resource: window.as_entire_binding(), resource: BindingResource::TextureViewArray(&tex_manager.views()),
}, },
BindGroupEntry { BindGroupEntry {
binding: 1, binding: 1,
resource: masks.buffer.as_entire_binding(), resource: BindingResource::SamplerArray(&tex_manager.samplers()),
}, },
BindGroupEntry { BindGroupEntry {
binding: 2, binding: 2,
resource: moves.buffer.as_entire_binding(), resource: masks.buffer.as_entire_binding(),
}, },
], ],
label: Some("ui shared"), label: Some("ui rsc"),
}) })
} }
/// Layout for a list of one primitive's data. Every size in the ui is pub fn view_count(&self) -> usize {
/// stated, so "is the buffer big enough for one entry?" is answered when self.textures.view_count()
/// the bind group is made; a `None` size is wgpu's to check on every draw.
fn data_layout(device: &Device, stride: u64) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: BufferSize::new(stride),
},
count: None,
}],
label: Some("ui primitive data"),
})
} }
} }
impl RenderLayer { pub struct UiLimits {
fn new() -> Self { max_textures: u32,
max_samplers: u32,
}
impl Default for UiLimits {
fn default() -> Self {
Self { Self {
primitives: Vec::new(), max_textures: 100000,
max_samplers: 1000,
} }
} }
} }
impl ListBuffers { impl UiLimits {
fn new(device: &Device) -> Self { pub fn max_binding_array_elements_per_shader_stage(&self) -> u32 {
Self { self.max_textures + self.max_samplers
instance: ArrBuf::new(
device,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
"instance",
),
data: ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"primitive data",
),
group: None,
bindings: Vec::new(),
}
} }
pub fn max_binding_array_sampler_elements_per_shader_stage(&self) -> u32 {
fn update( self.max_samplers
&mut self,
device: &Device,
queue: &Queue,
primitive: &PrimitivePipeline,
list: &InstanceList,
) {
self.bindings.clear();
primitive.render.instance_bindings(list, &mut self.bindings);
self.instance.update(device, queue, list.instances());
let resized = self.data.update(device, queue, list.data());
if list.instances().is_empty() {
self.group = None;
} else if resized || self.group.is_none() {
self.group = Some(device.create_bind_group(&BindGroupDescriptor {
layout: &primitive.data_layout,
entries: &[BindGroupEntry {
binding: 0,
resource: self.data.buffer.as_entire_binding(),
}],
label: Some("ui primitive data"),
}));
}
}
}
#[cfg(test)]
mod tests {
use super::module_source;
use wgpu::naga::{
front::wgsl,
valid::{Capabilities, ValidationFlags, Validator},
};
/// Every shader file, composed as the renderer composes it, parses and
/// validates with no device -- so an edit that breaks one fails here and
/// not in the first window opened.
#[test]
fn every_shader_validates() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src/render/shader");
let mut checked = 0;
for entry in std::fs::read_dir(dir).unwrap() {
let path = entry.unwrap().path();
if path.extension().is_none_or(|e| e != "wgsl") || path.ends_with("prelude.wgsl") {
continue;
}
let source = module_source(&std::fs::read_to_string(&path).unwrap());
let module = wgsl::parse_str(&source)
.unwrap_or_else(|e| panic!("{}: {}", path.display(), e.emit_to_string(&source)));
Validator::new(ValidationFlags::all(), Capabilities::all())
.validate(&module)
.unwrap_or_else(|e| panic!("{}: {e:?}", path.display()));
checked += 1;
}
assert!(checked > 0, "no shaders found in {dir}");
} }
} }
-156
View File
@@ -1,156 +0,0 @@
use wgpu::*;
use crate::{GlyphAtlas, UiData};
use super::{
atlas::PAGE,
primitive::{ListDraw, PrimitiveRender},
texture::{default_sampler, sampled_group, sampled_layout, write_region},
};
/// Draws glyphs from the atlas, which it owns: one array texture bound once
/// for a whole list, since every glyph in it reads the same pages.
pub struct GlyphRender {
pages: GpuPages,
layout: BindGroupLayout,
sampler: Sampler,
}
impl GlyphRender {
pub fn new(device: &Device, queue: &Queue) -> Self {
let layout = sampled_layout(device, TextureViewDimension::D2Array, "ui atlas");
let sampler = default_sampler(device);
Self {
pages: GpuPages::new(device, queue, &layout, &sampler),
layout,
sampler,
}
}
}
impl PrimitiveRender for GlyphRender {
fn layout(&self) -> Option<&BindGroupLayout> {
Some(&self.layout)
}
fn update(&mut self, ui: &mut UiData) {
self.pages
.update(&mut ui.text.atlas, &self.layout, &self.sampler);
}
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
pass.set_bind_group(2, self.pages.group(), &[]);
pass.draw(0..4, 0..list.instances);
}
}
/// The glyph atlas on the GPU: one array texture whose layers are the pages
/// `GlyphAtlas` packs.
///
/// One array rather than a texture per page because a layer index is ordinary
/// Vulkan 1.0 / GLES sampling, where a `binding_array` would need
/// `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack.
pub struct GpuPages {
device: Device,
queue: Queue,
texture: Texture,
group: BindGroup,
}
impl GpuPages {
pub fn new(
device: &Device,
queue: &Queue,
layout: &BindGroupLayout,
sampler: &Sampler,
) -> Self {
let texture = create_array(device, 1);
Self {
device: device.clone(),
queue: queue.clone(),
group: atlas_group(device, layout, &texture, sampler),
texture,
}
}
pub fn update(&mut self, atlas: &mut GlyphAtlas, layout: &BindGroupLayout, sampler: &Sampler) {
if atlas.page_count() > self.texture.depth_or_array_layers() {
self.grow(atlas.page_count(), layout, sampler);
}
for (upload, page) in atlas.uploads() {
let dst = TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: Origin3d {
x: upload.rect.x,
y: upload.rect.y,
z: upload.layer,
},
aspect: TextureAspect::All,
};
write_region(&self.queue, dst, page, upload.rect);
}
}
pub fn group(&self) -> &BindGroup {
&self.group
}
/// Doubles until `needed` fits and copies the old layers across GPU side.
/// The new texture stales the group, so that is rebuilt here.
fn grow(&mut self, needed: u32, layout: &BindGroupLayout, sampler: &Sampler) {
let old = self.texture.depth_or_array_layers();
let mut layers = old;
while layers < needed {
layers *= 2;
}
let texture = create_array(&self.device, layers);
let mut encoder = self
.device
.create_command_encoder(&CommandEncoderDescriptor {
label: Some("atlas grow"),
});
encoder.copy_texture_to_texture(
self.texture.as_image_copy(),
texture.as_image_copy(),
Extent3d {
width: PAGE,
height: PAGE,
depth_or_array_layers: old,
},
);
self.queue.submit(std::iter::once(encoder.finish()));
self.group = atlas_group(&self.device, layout, &texture, sampler);
self.texture = texture;
}
}
fn atlas_group(
device: &Device,
layout: &BindGroupLayout,
texture: &Texture,
sampler: &Sampler,
) -> BindGroup {
let view = texture.create_view(&TextureViewDescriptor {
dimension: Some(TextureViewDimension::D2Array),
..Default::default()
});
sampled_group(device, layout, &view, sampler, "ui atlas")
}
fn create_array(device: &Device, layers: u32) -> Texture {
device.create_texture(&TextureDescriptor {
label: Some("glyph atlas"),
size: Extent3d {
width: PAGE,
height: PAGE,
depth_or_array_layers: layers,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC,
view_formats: &[],
})
}
+205 -324
View File
@@ -1,321 +1,180 @@
use std::{any::TypeId, marker::PhantomData}; use std::ops::{Deref, DerefMut};
use crate::{ use crate::{
Color, TextureHandle, UiData, UiRegion, WidgetId, Color, UiRegion, WidgetId,
render::{ render::{
data::{MaskIdx, MoveIdx, PrimitiveInstance}, ArrBuf,
page::GlyphRender, data::{MaskIdx, PrimitiveInstance},
texture::ImageRender,
}, },
util::{HashMap, Vec2},
}; };
use bytemuck::Pod; use bytemuck::Pod;
use wgpu::{BindGroupLayout, Device, Queue, RenderPass}; use wgpu::*;
/// One instance of a primitive, laid out as the struct its shader reads. pub struct Primitives {
///
/// The type carries its own shader, so drawing one is all the wiring it needs:
/// its list, free list, buffers and pipeline follow from being registered.
pub trait Primitive: Pod + 'static {
/// Compiled after `prelude.wgsl`, which states what it declares and what
/// it is given.
const WGSL: &'static str;
/// Made once, the first time the renderer sees this primitive. It owns
/// whatever the shader samples and records the primitive's own draws; the
/// default owns nothing and draws every instance in one call.
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender>
where
Self: Sized,
{
let _ = (device, queue);
Box::new(Instanced)
}
}
/// The renderer's half of a primitive: what it samples, what it uploads, and
/// what draws it records.
///
/// Everything a draw shares -- the pipeline, the window and masks, the list's
/// own data and instance buffer -- is set before this is called. What is left
/// is what only this primitive knows: its group 2, and how many draws its
/// instances are.
pub trait PrimitiveRender {
/// The layout its shader reads at group 2. `None` for a primitive whose
/// shader samples nothing, whose pipeline then has no group 2 at all.
fn layout(&self) -> Option<&BindGroupLayout> {
None
}
/// Uploads whatever this primitive owns, once a frame, before any draw.
fn update(&mut self, ui: &mut UiData) {
let _ = ui;
}
/// Keeps what the primitive needs per instance at draw time, read from
/// the list's own data. A primitive that binds nothing per instance --
/// most of them -- leaves this empty and draws in one call.
fn instance_bindings(&self, list: &InstanceList, out: &mut Vec<u32>) {
let _ = (list, out);
}
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>);
}
/// What a `PrimitiveRender` draws: this list's instances, and whatever
/// `instance_bindings` kept for them.
pub struct ListDraw<'a> {
pub instances: u32,
pub bindings: &'a [u32],
}
/// The default: nothing sampled, every instance in one call.
pub struct Instanced;
impl PrimitiveRender for Instanced {
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
pass.draw(0..4, 0..list.instances);
}
}
/// Which registered primitive an instance is.
pub struct PrimitiveKind<P> {
id: u32,
_p: PhantomData<fn(P)>,
}
impl<P> PrimitiveKind<P> {
fn new(id: u32) -> Self {
Self {
id,
_p: PhantomData,
}
}
}
impl<P> Clone for PrimitiveKind<P> {
fn clone(&self) -> Self {
*self
}
}
impl<P> Copy for PrimitiveKind<P> {}
/// Every primitive a ui can draw, in the order they were first drawn.
#[derive(Default)]
pub struct PrimitiveRegistry {
kinds: Vec<PrimitiveSource>,
ids: HashMap<TypeId, u32>,
}
pub struct PrimitiveSource {
pub wgsl: &'static str,
pub label: &'static str,
/// Size of one instance's entry, stated as the data binding's minimum.
pub stride: u64,
pub render: fn(&Device, &Queue) -> Box<dyn PrimitiveRender>,
}
impl PrimitiveRegistry {
/// Registers `P` if this is the first time it has been drawn.
pub fn kind<P: Primitive>(&mut self) -> PrimitiveKind<P> {
let Self { kinds, ids } = self;
let id = *ids.entry(TypeId::of::<P>()).or_insert_with(|| {
kinds.push(PrimitiveSource {
wgsl: P::WGSL,
label: std::any::type_name::<P>(),
stride: size_of::<P>() as u64,
render: P::render,
});
kinds.len() as u32 - 1
});
PrimitiveKind::new(id)
}
pub fn sources(&self) -> &[PrimitiveSource] {
&self.kinds
}
}
/// One registered primitive's instances in one layer. Everything per-instance
/// rides here, so it stays in step through a `swap_remove`.
pub struct InstanceList {
instances: Vec<PrimitiveInstance>, instances: Vec<PrimitiveInstance>,
/// The widget each instance belongs to, for renumbering its handles.
assoc: Vec<WidgetId>, assoc: Vec<WidgetId>,
data: PrimitiveData,
free: Vec<usize>, free: Vec<usize>,
/// `stride` bytes of the primitive's own data per instance.
data: Vec<u8>,
/// From the type the list was made for, so a write is never checked.
stride: usize,
}
impl InstanceList {
fn new<P: Primitive>() -> Self {
Self {
instances: Vec::new(),
assoc: Vec::new(),
free: Vec::new(),
data: Vec::new(),
stride: size_of::<P>(),
}
}
pub fn instances(&self) -> &[PrimitiveInstance] {
&self.instances
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn stride(&self) -> usize {
self.stride
}
fn push(&mut self, id: WidgetId, inst: PrimitiveInstance, data: &[u8]) -> usize {
if let Some(i) = self.free.pop() {
self.instances[i] = inst;
self.assoc[i] = id;
self.data[i * self.stride..][..self.stride].copy_from_slice(data);
i
} else {
let i = self.instances.len();
self.instances.push(inst);
self.assoc.push(id);
self.data.extend_from_slice(data);
i
}
}
fn free(&mut self, i: usize) -> MaskIdx {
self.free.push(i);
self.instances[i].mask_idx
}
fn apply_free(&mut self, kind: u32) -> impl Iterator<Item = PrimitiveChange> {
self.free.sort_by(|a, b| b.cmp(a));
let instances = &mut self.instances;
let assoc = &mut self.assoc;
let data = &mut self.data;
let stride = self.stride;
self.free.drain(..).filter_map(move |i| {
instances.swap_remove(i);
assoc.swap_remove(i);
let last = instances.len();
data.copy_within(last * stride..(last + 1) * stride, i * stride);
data.truncate(last * stride);
if i == last {
return None;
}
let id = assoc[i];
Some(PrimitiveChange {
id,
kind,
old: last,
new: i,
})
})
}
}
/// Everything one layer draws, one list per registered primitive.
pub struct LayerDraws {
/// `None` until this layer draws that primitive, because only the write
/// knows the type the list is for.
primitives: Vec<Option<InstanceList>>,
pub updated: bool, pub updated: bool,
} }
impl Default for LayerDraws { impl Default for Primitives {
fn default() -> Self { fn default() -> Self {
Self { Self {
primitives: Vec::new(), instances: Default::default(),
assoc: Default::default(),
data: Default::default(),
free: Vec::new(),
updated: true, updated: true,
} }
} }
} }
impl LayerDraws { pub trait Primitive: Pod {
pub fn write<P: Primitive>( const BINDING: u32;
&mut self, fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self>;
layer: usize, }
PrimitiveInst {
kind, macro_rules! primitives {
id, ($($name:ident: $ty:ty => $binding:expr,)*) => {
primitive, #[derive(Default)]
region, pub struct PrimitiveData {
mask_idx, $(pub(crate) $name: PrimitiveVec<$ty>,)*
move_idx,
}: PrimitiveInst<P>,
) -> PrimitiveHandle {
self.updated = true;
// Grown on first use rather than sized from the registry, which a
// layer cannot see.
if self.primitives.len() <= kind.id as usize {
self.primitives.resize_with(kind.id as usize + 1, || None);
} }
let inst_idx = self.primitives[kind.id as usize]
.get_or_insert_with(InstanceList::new::<P>) pub struct PrimitiveBuffers {
.push( $($name: ArrBuf<$ty>,)*
id,
PrimitiveInstance {
region,
mask_idx,
move_idx,
},
bytemuck::bytes_of(&primitive),
);
PrimitiveHandle {
layer,
kind: kind.id,
inst_idx,
} }
}
pub fn primitives(&self) -> &[Option<InstanceList>] { impl PrimitiveBuffers {
&self.primitives pub fn update(&mut self, device: &Device, queue: &Queue, data: &PrimitiveData) {
} $(self.$name.update(device, queue, &data.$name);)*
}
}
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> { impl PrimitiveBuffers {
self.primitives pub const LEN: usize = primitives!(@count $($name)*);
.iter_mut() pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] {
.enumerate() [
.filter_map(|(kind, list)| Some((kind as u32, list.as_mut()?))) $((<$ty>::BINDING, &self.$name.buffer),)*
.flat_map(|(kind, list)| list.apply_free(kind)) ]
} }
pub fn new(device: &Device) -> Self {
Self {
$($name: ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
stringify!($name),
),)*
}
}
}
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { impl PrimitiveData {
self.updated = true; pub fn clear(&mut self) {
self.list(h).free(h.inst_idx) $(self.$name.clear();)*
} }
pub fn free(&mut self, binding: u32, idx: usize) {
match binding {
$(<$ty>::BINDING => self.$name.free(idx),)*
_ => unreachable!()
}
}
}
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion { $(
self.updated = true; unsafe impl bytemuck::Pod for $ty {}
&mut self.list(h).instances[h.inst_idx].region unsafe impl bytemuck::Zeroable for $ty {}
} impl Primitive for $ty {
const BINDING: u32 = $binding;
/// A handle is only ever made by `write`, which is what created the list. fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self> {
fn list(&mut self, h: &PrimitiveHandle) -> &mut InstanceList { &mut data.$name
self.primitives[h.kind as usize] }
.as_mut() }
.expect("handle names a primitive this layer never drew") )*
} };
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t),+) };
(@count $t:tt) => { 1 };
} }
pub struct PrimitiveInst<P> { pub struct PrimitiveInst<P> {
pub kind: PrimitiveKind<P>,
pub id: WidgetId, pub id: WidgetId,
pub primitive: P, pub primitive: P,
pub region: UiRegion, pub region: UiRegion,
pub mask_idx: MaskIdx, pub mask_idx: MaskIdx,
pub move_idx: MoveIdx, }
impl Primitives {
pub fn write<P: Primitive>(
&mut self,
layer: usize,
PrimitiveInst {
id,
primitive,
region,
mask_idx,
}: PrimitiveInst<P>,
) -> PrimitiveHandle {
self.updated = true;
let vec = P::vec(&mut self.data);
let i = vec.add(primitive);
let inst = PrimitiveInstance {
region,
idx: i as u32,
mask_idx,
binding: P::BINDING,
};
let inst_i = if let Some(i) = self.free.pop() {
self.instances[i] = inst;
self.assoc[i] = id;
i
} else {
let i = self.instances.len();
self.instances.push(inst);
self.assoc.push(id);
i
};
PrimitiveHandle::new::<P>(layer, inst_i, i)
}
/// returns (old index, new index)
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
self.free.sort_by(|a, b| b.cmp(a));
self.free.drain(..).filter_map(|i| {
self.instances.swap_remove(i);
self.assoc.swap_remove(i);
if i == self.instances.len() {
return None;
}
let id = self.assoc[i];
let old = self.instances.len();
Some(PrimitiveChange { id, old, new: i })
})
}
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self.updated = true;
self.data.free(h.binding, h.data_idx);
self.free.push(h.inst_idx);
self.instances[h.inst_idx].mask_idx
}
pub fn data(&self) -> &PrimitiveData {
&self.data
}
pub fn instances(&self) -> &Vec<PrimitiveInstance> {
&self.instances
}
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
self.updated = true;
&mut self.instances[h.inst_idx].region
}
} }
pub struct PrimitiveChange { pub struct PrimitiveChange {
pub id: WidgetId, pub id: WidgetId,
/// Which registered primitive's list moved, since they index separately.
pub kind: u32,
pub old: usize, pub old: usize,
pub new: usize, pub new: usize,
} }
@@ -323,12 +182,29 @@ pub struct PrimitiveChange {
#[derive(Debug)] #[derive(Debug)]
pub struct PrimitiveHandle { pub struct PrimitiveHandle {
pub layer: usize, pub layer: usize,
pub kind: u32,
pub inst_idx: usize, pub inst_idx: usize,
pub data_idx: usize,
pub binding: u32,
} }
impl PrimitiveHandle {
fn new<P: Primitive>(layer: usize, inst_idx: usize, data_idx: usize) -> Self {
Self {
layer,
inst_idx,
data_idx,
binding: P::BINDING,
}
}
}
primitives!(
rects: RectPrimitive => 0,
textures: TexturePrimitive => 1,
);
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Copy, Clone)]
pub struct RectPrimitive { pub struct RectPrimitive {
pub color: Color<u8>, pub color: Color<u8>,
pub radius: f32, pub radius: f32,
@@ -336,10 +212,6 @@ pub struct RectPrimitive {
pub inner_radius: f32, pub inner_radius: f32,
} }
impl Primitive for RectPrimitive {
const WGSL: &'static str = include_str!("shader/rect.wgsl");
}
impl RectPrimitive { impl RectPrimitive {
pub fn color(color: Color<u8>) -> Self { pub fn color(color: Color<u8>) -> Self {
Self { Self {
@@ -351,51 +223,60 @@ impl RectPrimitive {
} }
} }
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct GlyphPrimitive {
pub uv_min: Vec2,
pub uv_max: Vec2,
/// Which atlas array layer this glyph is on.
pub layer: u32,
pub color: Color<u8>,
pub flags: u32,
}
// Manual rather than derived: `Vec2`'s alignment leaves four bytes of padding
// here, which is how WGSL lays the struct out.
unsafe impl bytemuck::Pod for GlyphPrimitive {}
unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
impl Primitive for GlyphPrimitive {
const WGSL: &'static str = include_str!("shader/glyph.wgsl");
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender> {
Box::new(GlyphRender::new(device, queue))
}
}
/// One drawn image. Its shader reads nothing per instance; the slot names the
/// texture to bind for it.
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct TexturePrimitive { pub struct TexturePrimitive {
pub slot: u32, pub view_idx: u32,
pub sampler_idx: u32,
} }
impl Primitive for TexturePrimitive { pub struct PrimitiveVec<T> {
const WGSL: &'static str = include_str!("shader/texture.wgsl"); vec: Vec<T>,
free: Vec<usize>,
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender> {
Box::new(ImageRender::new(device, queue))
}
} }
impl From<&TextureHandle> for TexturePrimitive { impl<T> PrimitiveVec<T> {
fn from(handle: &TextureHandle) -> Self { pub fn new() -> Self {
Self { Self {
slot: handle.slot(), vec: Vec::new(),
free: Vec::new(),
} }
} }
pub fn add(&mut self, t: T) -> usize {
if let Some(i) = self.free.pop() {
self.vec[i] = t;
i
} else {
let i = self.vec.len();
self.vec.push(t);
i
}
}
pub fn free(&mut self, i: usize) {
self.free.push(i);
}
pub fn clear(&mut self) {
self.free.clear();
self.vec.clear();
}
}
impl<T> Default for PrimitiveVec<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> Deref for PrimitiveVec<T> {
type Target = Vec<T>;
fn deref(&self) -> &Self::Target {
&self.vec
}
}
impl<T> DerefMut for PrimitiveVec<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.vec
}
} }
+180
View File
@@ -0,0 +1,180 @@
enable wgpu_binding_array;
const RECT: u32 = 0u;
const TEXTURE: u32 = 1u;
@group(0) @binding(0)
var<uniform> window: WindowUniform;
@group(1) @binding(RECT)
var<storage> rects: array<Rect>;
@group(1) @binding(TEXTURE)
var<storage> textures: array<TextureInfo>;
struct Rect {
color: u32,
radius: f32,
thickness: f32,
inner_radius: f32,
}
struct TextureInfo {
view_idx: u32,
sampler_idx: u32,
}
struct Mask {
x: UiSpan,
y: UiSpan,
}
struct UiSpan {
start: UiScalar,
end: UiScalar,
}
struct UiScalar {
rel: f32,
abs: f32,
}
struct UiVec2 {
rel: vec2<f32>,
abs: vec2<f32>,
}
@group(2) @binding(0)
var views: binding_array<texture_2d<f32>>;
@group(2) @binding(1)
var samplers: binding_array<sampler>;
@group(2) @binding(2)
var<storage> masks: array<Mask>;
struct WindowUniform {
dim: vec2<f32>,
};
struct InstanceInput {
@location(0) x_start: vec2<f32>,
@location(1) x_end: vec2<f32>,
@location(2) y_start: vec2<f32>,
@location(3) y_end: vec2<f32>,
@location(4) binding: u32,
@location(5) idx: u32,
@location(6) mask_idx: u32,
}
struct VertexOutput {
@location(0) top_left: vec2<f32>,
@location(1) bot_right: vec2<f32>,
@location(2) uv: vec2<f32>,
@location(3) @interpolate(flat) binding: u32,
@location(4) @interpolate(flat) idx: u32,
@location(5) @interpolate(flat) mask_idx: u32,
@builtin(position) clip_position: vec4<f32>,
};
struct Region {
pos: vec2<f32>,
uv: vec2<f32>,
top_left: vec2<f32>,
bot_right: vec2<f32>,
}
@vertex
fn vs_main(
@builtin(vertex_index) vi: u32,
in: InstanceInput,
) -> VertexOutput {
var out: VertexOutput;
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
let size = bot_right - top_left;
let uv = vec2<f32>(
f32(vi % 2u),
f32(vi / 2u)
);
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
out.uv = uv;
out.binding = in.binding;
out.idx = in.idx;
out.top_left = top_left;
out.bot_right = bot_right;
out.mask_idx = in.mask_idx;
return out;
}
@fragment
fn fs_main(
in: VertexOutput
) -> @location(0) vec4<f32> {
let pos = in.clip_position.xy;
let region = Region(pos, in.uv, in.top_left, in.bot_right);
let i = in.idx;
var color: vec4<f32>;
switch in.binding {
case RECT: {
color = draw_rounded_rect(region, rects[i]);
}
case TEXTURE: {
color = draw_texture(region, textures[i]);
}
default: {
color = vec4(1.0, 0.0, 1.0, 1.0);
}
}
if in.mask_idx != 4294967295u {
let mask = masks[in.mask_idx];
let tl = UiVec2(vec2(mask.x.start.rel, mask.y.start.rel), vec2(mask.x.start.abs, mask.y.start.abs));
let br = UiVec2(vec2(mask.x.end.rel, mask.y.end.rel), vec2(mask.x.end.abs, mask.y.end.abs));
let top_left = floor(tl.rel * window.dim) + floor(tl.abs);
let bot_right = floor(br.rel * window.dim) + floor(br.abs);
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
color *= 0.0;
}
}
return color;
}
// TODO: this seems really inefficient (per frag indexing)?
fn draw_texture(region: Region, info: TextureInfo) -> vec4<f32> {
return textureSample(views[info.view_idx], samplers[info.sampler_idx], region.uv);
}
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
var color = unpack4x8unorm(rect.color);
let edge = 0.5;
let size = region.bot_right - region.top_left;
let corner = size / 2.0;
let center = region.top_left + corner;
let dist = distance_from_rect(region.pos, center, corner, rect.radius);
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
if rect.thickness > 0.0 {
let dist2 = distance_from_rect(region.pos, center, corner - rect.thickness, rect.inner_radius);
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
}
return color;
}
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
// vec from center to pixel
let p = pixel_pos - rect_center;
// vec from inner rect corner to pixel
let q = abs(p) - (rect_corner - radius);
return length(max(q, vec2(0.0))) - radius;
}
-33
View File
@@ -1,33 +0,0 @@
// Matches `GlyphEntry::IS_COLORED`.
const COLORED: u32 = 1u;
// The glyph atlas, whose array layers are its pages.
@group(2) @binding(0)
var atlas: texture_2d_array<f32>;
@group(2) @binding(1)
var samp: sampler;
struct GlyphInfo {
uv_min: vec2<f32>,
uv_max: vec2<f32>,
// Which layer of the atlas array this glyph's page is.
layer: u32,
color: u32,
flags: u32,
}
@group(1) @binding(0)
var<storage> glyphs: array<GlyphInfo>;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let g = glyphs[in.idx];
let uv = mix(g.uv_min, g.uv_max, in.uv);
let texel = textureSample(atlas, samp, uv, i32(g.layer));
if (g.flags & COLORED) != 0u {
return masked(in, texel);
}
var color = unpack4x8unorm(g.color);
color.a *= texel.a;
return masked(in, color);
}
-193
View File
@@ -1,193 +0,0 @@
// Prepended to every primitive's shader, which declares its own instance data
// as `var<storage> <name>: array<T>` at group 1 binding 0, and an `fs_main`
// shading one instance of it. What it samples, if anything, is bound at group
// 2: the texture at binding 0 and the sampler at binding 1.
@group(0) @binding(0)
var<uniform> window: WindowUniform;
@group(0) @binding(1)
var<storage> masks: array<Mask>;
@group(0) @binding(2)
var<storage> move_offsets: array<MoveOffset>;
struct WindowUniform {
dim: vec2<f32>,
};
struct Mask {
x: RawSpan,
y: RawSpan,
move_idx: u32,
}
struct MoveOffset {
x: RawSpan,
y: RawSpan,
parent: u32,
}
// `PX_STEP` and `REL_STEP` are prepended from `iris_core`'s own constants:
// what it stores is a whole count of each, both powers of two, so decoding
// is exact and the number here is the number the CPU decided.
// Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that
// composes to within half a step of a pixel boundary is on that boundary and
// belongs to the pixel above it. Flooring the product instead drops a pixel
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of
// `REL_STEP` as 383.99998, and five tabs each lose their last column.
//
// Taken over the whole coordinate, fraction and pixels summed, since a floor
// does not distribute over a sum: floored apart, a half of one and a half of
// the other lose the pixel the two together make.
fn snap_floor(v: vec2<f32>) -> vec2<f32> {
return floor(v + PX_STEP * 0.5);
}
struct RawScalar {
rel: i32,
px: i32,
}
struct RawSpan {
start: RawScalar,
end: RawScalar,
}
fn scalar_of(raw: RawScalar) -> Len {
return Len(f32(raw.rel) * REL_STEP, f32(raw.px) * PX_STEP);
}
fn span_of(raw: RawSpan) -> UiSpan {
return UiSpan(scalar_of(raw.start), scalar_of(raw.end));
}
fn scalar_of_pair(raw: vec2<i32>) -> Len {
return Len(f32(raw.x) * REL_STEP, f32(raw.y) * PX_STEP);
}
struct Region {
x: UiSpan,
y: UiSpan,
}
const MOVE_NONE: u32 = 4294967295u;
// Keep in step with `iris_core::CHAIN_LIMIT`. It bounds a malformed cycle
// rather than any real tree, and the CPU walk uses the same number so both
// resolve a deep one the same way.
const CHAIN_LIMIT: u32 = 64u;
// The same expression `Len::within` uses, in floats rather than on the
// CPU's grid: a move is resolved here so that scrolling a subtree writes one
// entry instead of walking it. What has to hold is that this agrees with
// itself frame to frame, not that it matches the CPU to the last bit.
fn scalar_within(s: Len, p: UiSpan) -> Len {
return Len(
p.start.rel + (p.end.rel - p.start.rel) * s.rel,
s.px + (p.start.px + (p.end.px - p.start.px) * s.rel),
);
}
fn span_within(s: UiSpan, p: UiSpan) -> UiSpan {
return UiSpan(scalar_within(s.start, p), scalar_within(s.end, p));
}
fn resolve_move(idx: u32, local: Region) -> Region {
var r = local;
var at = idx;
for (var step = 0u; step < CHAIN_LIMIT; step++) {
if at == MOVE_NONE {
break;
}
let entry = move_offsets[at];
r = Region(span_within(r.x, span_of(entry.x)), span_within(r.y, span_of(entry.y)));
at = entry.parent;
}
return r;
}
struct UiSpan {
start: Len,
end: Len,
}
struct Len {
rel: f32,
px: f32,
}
struct InstanceInput {
@location(0) x_start: vec2<i32>,
@location(1) x_end: vec2<i32>,
@location(2) y_start: vec2<i32>,
@location(3) y_end: vec2<i32>,
@location(4) mask_idx: u32,
@location(5) move_idx: u32,
}
struct VertexOutput {
@location(0) top_left: vec2<f32>,
@location(1) bot_right: vec2<f32>,
@location(2) uv: vec2<f32>,
@location(3) @interpolate(flat) mask_idx: u32,
@location(4) @interpolate(flat) idx: u32,
@builtin(position) clip_position: vec4<f32>,
};
@vertex
fn vs_main(
@builtin(vertex_index) vi: u32,
@builtin(instance_index) ii: u32,
in: InstanceInput,
) -> VertexOutput {
var out: VertexOutput;
let local = Region(
UiSpan(scalar_of_pair(in.x_start), scalar_of_pair(in.x_end)),
UiSpan(scalar_of_pair(in.y_start), scalar_of_pair(in.y_end)),
);
let r = resolve_move(in.move_idx, local);
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
let top_left_px = vec2(r.x.start.px, r.y.start.px);
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
let bot_right_px = vec2(r.x.end.px, r.y.end.px);
let top_left = snap_floor(top_left_rel * window.dim + top_left_px);
let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px);
let size = bot_right - top_left;
let uv = vec2<f32>(
f32(vi % 2u),
f32(vi / 2u)
);
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
out.uv = uv;
out.top_left = top_left;
out.bot_right = bot_right;
out.mask_idx = in.mask_idx;
out.idx = ii;
return out;
}
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
if in.mask_idx == 4294967295u {
return color;
}
let mask = masks[in.mask_idx];
// Its own chain, not the drawn primitive's, so a stationary viewport
// clips content that moves inside it.
let m = resolve_move(mask.move_idx, Region(span_of(mask.x), span_of(mask.y)));
let tl = vec2(m.x.start.rel, m.y.start.rel);
let tl_px = vec2(m.x.start.px, m.y.start.px);
let br = vec2(m.x.end.rel, m.y.end.rel);
let br_px = vec2(m.x.end.px, m.y.end.px);
let top_left = snap_floor(tl * window.dim + tl_px);
let bot_right = snap_floor(br * window.dim + br_px);
let pos = in.clip_position.xy;
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
return color * 0.0;
}
return color;
}
-39
View File
@@ -1,39 +0,0 @@
struct Rect {
color: u32,
radius: f32,
thickness: f32,
inner_radius: f32,
}
@group(1) @binding(0)
var<storage> rects: array<Rect>;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let rect = rects[in.idx];
var color = unpack4x8unorm(rect.color);
let edge = 0.5;
let size = in.bot_right - in.top_left;
let corner = size / 2.0;
let center = in.top_left + corner;
let pos = in.clip_position.xy;
let dist = distance_from_rect(pos, center, corner, rect.radius);
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
if rect.thickness > 0.0 {
let dist2 = distance_from_rect(pos, center, corner - rect.thickness, rect.inner_radius);
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
}
return masked(in, color);
}
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
// vec from center to pixel
let p = pixel_pos - rect_center;
// vec from inner rect corner to pixel
let q = abs(p) - (rect_corner - radius);
return length(max(q, vec2(0.0))) - radius;
}
-10
View File
@@ -1,10 +0,0 @@
// The image this instance draws, bound for it alone.
@group(2) @binding(0)
var image: texture_2d<f32>;
@group(2) @binding(1)
var samp: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return masked(in, textureSample(image, samp, in.uv));
}
+84 -198
View File
@@ -1,119 +1,59 @@
use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage}; use image::{DynamicImage, EncodableLayout};
use wgpu::{util::DeviceExt, *}; use wgpu::{util::DeviceExt, *};
use crate::{ use crate::{TextureUpdate, Textures};
PatchRect, TextureUpdate, Textures, UiData,
render::{
TexturePrimitive,
primitive::{ListDraw, PrimitiveRender},
},
};
/// Draws standalone images, which it owns. Each is its own texture, so each
/// instance binds its own and is a draw of its own.
pub struct ImageRender {
textures: GpuTextures,
layout: BindGroupLayout,
sampler: Sampler,
}
impl ImageRender {
pub fn new(device: &Device, queue: &Queue) -> Self {
Self {
textures: GpuTextures::new(device, queue),
layout: sampled_layout(device, TextureViewDimension::D2, "ui image"),
sampler: default_sampler(device),
}
}
}
impl PrimitiveRender for ImageRender {
fn layout(&self) -> Option<&BindGroupLayout> {
Some(&self.layout)
}
fn update(&mut self, ui: &mut UiData) {
self.textures
.update(&mut ui.textures, &self.layout, &self.sampler);
}
fn instance_bindings(&self, list: &super::InstanceList, out: &mut Vec<u32>) {
let slots = list
.data()
.chunks_exact(list.stride())
.map(|data| bytemuck::pod_read_unaligned::<TexturePrimitive>(data).slot);
out.extend(slots);
}
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
for (i, &slot) in list.bindings.iter().enumerate() {
let Some(image) = self.textures.group(slot) else {
continue;
};
pass.set_bind_group(2, image, &[]);
pass.draw(0..4, i as u32..i as u32 + 1);
}
}
}
/// The standalone images a ui draws, each its own texture and bind group --
/// unlike the glyph atlas in `super::page`, which is one array they share.
pub struct GpuTextures { pub struct GpuTextures {
device: Device, device: Device,
queue: Queue, queue: Queue,
slots: Vec<Option<ImageGpu>>, views: Vec<TextureView>,
} view_count: usize,
samplers: Vec<Sampler>,
struct ImageGpu { null_view: TextureView,
/// Kept for `patch`, which needs the texture rather than the view. no_views: Vec<TextureView>,
texture: Texture,
group: BindGroup,
} }
impl GpuTextures { impl GpuTextures {
pub fn new(device: &Device, queue: &Queue) -> Self { pub fn update(&mut self, textures: &mut Textures) -> bool {
Self { let mut changed = false;
device: device.clone(),
queue: queue.clone(),
slots: Vec::new(),
}
}
pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) {
for update in textures.updates() { for update in textures.updates() {
changed = true;
match update { match update {
TextureUpdate::Push(image) => { TextureUpdate::Push(image) => self.push(image),
let image = self.create(image, layout, sampler); TextureUpdate::Set(i, image) => self.set(i, image),
self.slots.push(Some(image)); TextureUpdate::SetFree => self.view_count += 1,
} TextureUpdate::Free(i) => self.free(i),
TextureUpdate::Set(i, image) => { TextureUpdate::PushFree => self.push_free(),
let image = self.create(image, layout, sampler);
self.slots[i as usize] = Some(image);
}
TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image),
TextureUpdate::PushFree => self.slots.push(None),
TextureUpdate::SetFree => {}
TextureUpdate::Free(i) => self.slots[i as usize] = None,
} }
} }
changed
}
fn set(&mut self, i: u32, image: &DynamicImage) {
self.view_count += 1;
let view = self.create_view(image);
self.views[i as usize] = view;
}
fn free(&mut self, i: u32) {
self.view_count -= 1;
self.views[i as usize] = self.null_view.clone();
}
fn push(&mut self, image: &DynamicImage) {
self.view_count += 1;
let view = self.create_view(image);
self.views.push(view);
}
fn push_free(&mut self) {
self.view_count += 1;
self.views.push(self.null_view.clone());
} }
pub fn group(&self, slot: u32) -> Option<&BindGroup> { fn create_view(&self, image: &DynamicImage) -> TextureView {
self.slots.get(slot as usize)?.as_ref().map(|i| &i.group) let image = image.to_rgba8();
} let (width, height) = image.dimensions();
fn create(
&self,
image: &DynamicImage,
layout: &BindGroupLayout,
sampler: &Sampler,
) -> ImageGpu {
let rgba = image.to_rgba8();
let (width, height) = rgba.dimensions();
let texture = self.device.create_texture_with_data( let texture = self.device.create_texture_with_data(
&self.queue, &self.queue,
&TextureDescriptor { &TextureDescriptor {
label: Some("image"), label: None,
size: Extent3d { size: Extent3d {
width, width,
height, height,
@@ -123,119 +63,65 @@ impl GpuTextures {
sample_count: 1, sample_count: 1,
dimension: TextureDimension::D2, dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm, format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, usage: TextureUsages::TEXTURE_BINDING,
view_formats: &[], view_formats: &[],
}, },
wgt::TextureDataOrder::MipMajor, wgt::TextureDataOrder::MipMajor,
rgba.as_bytes(), image.as_bytes(),
); );
let view = texture.create_view(&TextureViewDescriptor::default()); texture.create_view(&TextureViewDescriptor::default())
let group = sampled_group(&self.device, layout, &view, sampler, "ui image");
ImageGpu { texture, group }
} }
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) { pub fn new(device: &Device, queue: &Queue) -> Self {
let Some(Some(slot)) = self.slots.get(i as usize) else { let null_view = null_texture_view(device);
return; Self {
}; device: device.clone(),
let dst = TexelCopyTextureInfo { queue: queue.clone(),
texture: &slot.texture, views: Vec::new(),
mip_level: 0, samplers: vec![default_sampler(device)],
origin: Origin3d { no_views: vec![null_view.clone()],
x: rect.x, null_view,
y: rect.y, view_count: 0,
z: 0,
},
aspect: TextureAspect::All,
};
match image.as_rgba8() {
Some(rgba) => write_region(&self.queue, dst, rgba, rect),
// The texture is rgba8, so any other layout has to be converted --
// and converting the rectangle is cheaper than the whole image.
None => {
let sub = image
.view(rect.x, rect.y, rect.width, rect.height)
.to_image();
write_region(&self.queue, dst, &sub, PatchRect { x: 0, y: 0, ..rect });
}
} }
} }
}
pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) { pub fn views(&self) -> Vec<&TextureView> {
if rect.width == 0 || rect.height == 0 { if self.views.is_empty() {
return; &self.no_views
} else {
&self.views
}
.iter()
.by_ref()
.collect()
}
pub fn samplers(&self) -> Vec<&Sampler> {
self.samplers.iter().by_ref().collect()
}
pub fn view_count(&self) -> usize {
self.view_count
} }
let stride = src.width() * 4;
queue.write_texture(
dst,
src.as_bytes(),
TexelCopyBufferLayout {
offset: (rect.y * stride + rect.x * 4) as u64,
bytes_per_row: Some(stride),
rows_per_image: Some(rect.height),
},
Extent3d {
width: rect.width,
height: rect.height,
depth_or_array_layers: 1,
},
);
} }
/// What a primitive that samples binds: a texture, and the sampler that reads pub fn null_texture_view(device: &Device) -> TextureView {
/// it. device
pub fn sampled_group( .create_texture(&TextureDescriptor {
device: &Device, label: Some("null"),
layout: &BindGroupLayout, size: Extent3d {
view: &TextureView, width: 1,
sampler: &Sampler, height: 1,
label: &'static str, depth_or_array_layers: 1,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(view),
}, },
BindGroupEntry { mip_level_count: 1,
binding: 1, sample_count: 1,
resource: BindingResource::Sampler(sampler), dimension: TextureDimension::D2,
}, format: TextureFormat::Rgba8Unorm,
], usage: TextureUsages::TEXTURE_BINDING,
label: Some(label), view_formats: &[],
}) })
} .create_view(&TextureViewDescriptor::default())
/// The layout for one of those. The dimension differs -- the atlas is an
/// array of pages and an image is not -- and nothing else does.
pub fn sampled_layout(
device: &Device,
dimension: TextureViewDimension,
label: &'static str,
) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: dimension,
multisampled: false,
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: None,
},
],
label: Some(label),
})
} }
pub fn default_sampler(device: &Device) -> Sampler { pub fn default_sampler(device: &Device) -> Sampler {
+6 -10
View File
@@ -21,25 +21,17 @@ impl<T: Pod> ArrBuf<T> {
_pd: PhantomData, _pd: PhantomData,
} }
} }
/// Returns whether the `Buffer` was recreated, which stales any cached pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) {
/// `BindGroup` holding it. if self.len != data.len() {
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool {
let resized = self.len != data.len();
if resized {
self.len = data.len(); self.len = data.len();
self.buffer = self.buffer =
Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label); Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label);
} }
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data)); queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
resized
}
pub fn len(&self) -> usize {
self.len
} }
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer { fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
let mut size = size as u64; let mut size = size as u64;
if usage.contains(BufferUsages::STORAGE) { if usage.contains(BufferUsages::STORAGE) {
// A binding cannot be empty or under the layout's minimum.
size = size.max(std::mem::size_of::<T>() as u64); size = size.max(std::mem::size_of::<T>() as u64);
} }
device.create_buffer(&BufferDescriptor { device.create_buffer(&BufferDescriptor {
@@ -49,4 +41,8 @@ impl<T: Pod> ArrBuf<T> {
usage, usage,
}) })
} }
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.len
}
} }
+3 -76
View File
@@ -1,87 +1,14 @@
use crate::{ use crate::{LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId};
LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive,
Size, TextureHandle, UiRegion, UiVec2, WidgetId,
};
/// What is kept of a widget its parent has asked about. `drawn` says whether /// important non rendering data for retained drawing
/// it currently draws; one that does not is kept so that a change to it, or
/// under it, still reaches whoever asked.
#[derive(Debug)] #[derive(Debug)]
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
/// Where its drawing goes, in its region node's coordinates.
pub placement: UiRegion,
/// What a fraction declared or reported under this widget is a fraction
/// of, as a length of the window.
pub rel_base: UiVec2,
/// Where its drawing was put, and where it was asked. The two differ
/// where a container asks in one place and puts the answer in another --
/// a row measures from its cursor and puts the child in its slot. Each
/// carries the rel base that ask stated, so asking again from either is
/// the same question it was.
pub placed: PlaceDesc,
pub asked: PlaceDesc,
/// The box it was asked in, in the parent's region-node coordinates: the
/// box its drawing was made in and the one its contract is about. Its
/// drawing is placed elsewhere by re-expression, never by asking again.
pub region: UiRegion, pub region: UiRegion,
/// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet.
pub answer: Option<(Size, LayoutHolds)>,
/// Asked more than once in its parent's last draw -- measured in one box
/// and then asked in the one the parent decided. The parent's layout
/// rests on the first answer and its drawing on the last, so only the
/// parent can ask either again.
pub re_asked: bool,
/// What the widget reported, in window-unit lengths.
pub size: Size,
/// The window and region reads that this drawing holds for, and the
/// rel base and region it pinned.
pub holds: LayoutHolds,
pub drawn: bool,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
/// How far down the tree it was drawn, the root being 1. Carried down a
/// draw rather than worked out by walking up, so it is right for every
/// widget a frame visits and cannot drift while one is being drawn.
pub depth: usize,
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
/// Its primitives, each keeping the box it was written in -- in this pub primitives: Vec<PrimitiveHandle>,
/// widget's placement coordinates, which is what a move recomposes from.
pub primitives: Vec<RetainedPrimitive>,
/// An owned mask holds one reference independently of its primitives.
pub mask_region: Option<UiRegion>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>,
/// The movable region its primitives are positioned through: its own when
/// opted in, otherwise the nearest ancestor's.
pub move_idx: MoveIdx,
/// The declared lengths whoever drew this widget resolved into its rel base.
/// A change to one moves a box this widget cannot fix by drawing again,
/// and comparing them is what says so.
pub declared: [Option<LayoutLen>; 2],
/// Its alignment when it was last drawn, which a change to the property
/// is found against.
pub own_align: RegionAlign,
/// The movable region whose coordinates its placement is in when this
/// widget does not own a region node.
pub parent_move: MoveIdx,
/// The mask its drawing is clipped to: one it set itself, or the one it
/// inherited from whoever drew it.
pub mask: MaskIdx, pub mask: MaskIdx,
/// That inherited one. The two differ exactly where the widget set a
/// mask of its own, which is the one it owns and the one a move rewrites
/// -- and the one a redraw of it must not be handed back, since setting
/// a mask asserts there is none.
pub parent_mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
} }
impl ActiveData {
/// What it answered when its parent asked, where it has been asked at
/// all. Not `size`, which is what its last drawing reported: a drawing
/// re-expressed in the box that answer chose is not a second answer.
pub fn measured(&self) -> Option<Size> {
self.answer.map(|(size, _)| size)
}
}
+18
View File
@@ -0,0 +1,18 @@
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();
}
}
-178
View File
@@ -1,178 +0,0 @@
use crate::{Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow};
use std::ops::RangeInclusive;
/// The lengths of a box, in pixels, that one drawing of a widget holds for:
/// give the widget any box in this range and it draws the same thing and
/// reports the same size. A widget that never reads its box in pixels holds
/// for every length; one that does holds for the one it read unless it says
/// otherwise, and a parent holds for whatever keeps every child it asked
/// about or drew inside its own range.
///
/// The ends are lengths on the grid rather than floats with a tolerance
/// around them: a box offered back at the length a widget reported comes back
/// as the same number, so a range means what it says. The one place a range
/// is wider than the length it came from is [`Self::through`], and what it is
/// wider by is the floor that inverting a fraction undoes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Holds {
pub lo: Px,
pub hi: Px,
}
impl Holds {
pub const ANY: Self = Self {
lo: Px::MIN,
hi: Px::MAX,
};
pub const fn at(len: Px) -> Self {
Self { lo: len, hi: len }
}
pub const fn contains(&self, len: Px) -> bool {
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
}
pub const fn and(self, other: Self) -> Self {
Self {
lo: self.lo.max(other.lo),
hi: self.hi.min(other.hi),
}
}
/// What a box has to be for a part of it, `len` of the box long, to stay
/// in this range: the exact preimage of `px + floor(rel * box)`, which is
/// the one way a box in pixels is reached. A part with no relative extent
/// is a fixed length -- it was drawn at that length and any box keeps it
/// there.
///
/// The answer is an interval even where this range is a single length,
/// because the multiply on the way in drops to the step below and many
/// boxes therefore give one length. That is a floor rather than an
/// allowance: inverting it is two divisions and nothing else, and the
/// whole of a box maps back to itself.
pub const fn through(self, len: Len) -> Self {
if self.lo.raw() == Px::MIN.raw() && self.hi.raw() == Px::MAX.raw() {
return Self::ANY;
}
let rel = len.rel.raw() as i64;
if rel == 0 {
return Self::ANY;
}
let px = len.px.raw() as i64;
// `floor(rel * box) >= lo - px` is `rel * box >= (lo - px) << REL`, and
// `floor(rel * box) <= hi - px` is `rel * box < (hi - px + 1) << REL`.
let lo = (self.lo.raw() as i64 - px) << REL_SHIFT;
let hi = (((self.hi.raw() as i64 - px) + 1) << REL_SHIFT) - 1;
// Dividing by a negative fraction turns the ends around, so which
// bound each comes from is decided before dividing rather than by
// taking the min and max of four divisions.
match rel > 0 {
true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)),
false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)),
}
}
const fn raws(lo: i64, hi: i64) -> Self {
Self {
lo: Px::from_raw(narrow(lo)),
hi: Px::from_raw(narrow(hi)),
}
}
}
impl From<RangeInclusive<Px>> for Holds {
fn from(range: RangeInclusive<Px>) -> Self {
Self {
lo: *range.start(),
hi: *range.end(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Rel;
#[test]
fn an_unrestricted_range_stays_unrestricted_through_any_length() {
for rel in [-2.0, -0.5, 0.0, 0.5, 1.0, 2.0] {
for px in [-8, 0, 8] {
let len = Len::from_parts(Rel::from_f32(rel), Px::from_int(px));
assert_eq!(Holds::ANY.through(len), Holds::ANY);
}
}
}
#[test]
fn through_reverses_a_range_for_a_negative_fraction() {
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
let part = Len::from_parts(Rel::from_f32(-0.5), Px::from_int(10));
let holds = Holds::from(Px::from_int(20)..=Px::from_int(40)).through(part);
assert!(holds.contains(Px::from_int(-60)) && holds.contains(Px::from_int(-20)));
assert!(!holds.contains(Px::from_int(-61)) && !holds.contains(Px::from_int(-19)));
}
/// The case the widening is for: a part that holds only for the length it
/// was drawn at has to hold for the box it was drawn in, and a third of a
/// box is not a whole number of steps.
#[test]
fn a_part_maps_back_onto_the_box_it_was_measured_in() {
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
for box_len in (440..460).map(Px::from_int) {
let holds = Holds::at(part.to_px(box_len)).through(part);
assert!(holds.contains(box_len), "{box_len:?} left out by {holds:?}");
}
}
/// A widget handed the whole of its parent's box, with or without pixels
/// taken off it, has no fraction to invert: multiplying by one is exact
/// and taking the pixels off again is too, so the box maps back to
/// itself. Allowing for anything here compounded a step a level down a
/// chain of widgets each taking the whole of its parent.
#[test]
fn the_whole_of_a_box_maps_back_to_itself() {
let at = Px::from_int(956);
assert_eq!(Holds::at(at).through(Len::FULL), Holds::at(at));
let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8));
assert_eq!(
Holds::at(at).through(less_eight),
Holds::at(at + Px::from_int(8))
);
}
/// The range is the exact preimage at both ends, so a box one step
/// outside it really does give a length outside this range. What a wider
/// range costs is a drawing reused where it does not hold.
#[test]
fn a_box_one_step_outside_the_range_is_outside_it() {
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
let at = Px::from_int(300);
let holds = Holds::at(at).through(part);
for inside in [holds.lo, holds.hi] {
assert_eq!(part.to_px(inside), at, "{inside:?} left out of {holds:?}");
}
for outside in [holds.lo.next_down(), holds.hi.next_up()] {
assert_ne!(part.to_px(outside), at, "{outside:?} admitted by {holds:?}");
}
}
/// A truncating multiply only ever drops, so the step it needs allowing
/// for on the way in belongs at the top of the range and not the bottom.
#[test]
fn a_fraction_widens_further_up_than_down() {
let half = Len::from_parts(Rel::from_f32(0.5), Px::ZERO);
let holds = Holds::at(Px::from_int(100)).through(half);
let box_len = Px::from_int(200);
assert!(holds.hi - box_len > box_len - holds.lo, "{holds:?}");
}
#[test]
fn a_boundary_the_next_step_along_does_not_admit_it() {
let boundary = Px::from_int(10);
let above = Holds::from(boundary.next_up()..=Px::MAX);
assert!(!above.contains(boundary));
assert!(above.contains(boundary.next_up()));
}
}
-79
View File
@@ -1,79 +0,0 @@
use crate::{Axis, Holds, Len, PxVec2, UiRegion, UiVec2};
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// What one evaluation of a widget depends on: the window lengths its reads
/// hold for, the pixel lengths of its own box, and the symbolic lengths of
/// that box and of its rel base where either one is what it was expressed in.
///
/// The symbolic lengths are pins rather than ranges: a container places its
/// children as lengths of its rel base measured from where its own box starts,
/// so what it draws turns on that box's length and on nothing about where it
/// is. A box pin reaches the parent only where the box it pinned is the
/// parent's own; anywhere else the parent chose that length itself, and a
/// widget pinned this way is checked when it is re-placed.
///
/// A rel base pin says the answer or the drawing is a fraction of the rel base,
/// which is a different length wherever the rel base is a different one -- at
/// the same window size, so no range of window pixels can say it. A length
/// of the rel base that is only pixels is not one: it is that many pixels
/// whatever the rel base turns out to be.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds {
pub window: [Holds; 2],
pub rel_base: [Option<Len>; 2],
pub region: [Holds; 2],
pub region_len: [Option<Len>; 2],
}
impl LayoutHolds {
pub const ANY: Self = Self {
window: [Holds::ANY; 2],
rel_base: [None; 2],
region: [Holds::ANY; 2],
region_len: [None; 2],
};
pub fn and(self, other: Self) -> Self {
let mut result = Self::ANY;
for n in 0..2 {
result.window[n] = self.window[n].and(other.window[n]);
result.region[n] = self.region[n].and(other.region[n]);
debug_assert!(
self.region_len[n].is_none()
|| other.region_len[n].is_none()
|| self.region_len[n] == other.region_len[n]
);
debug_assert!(
self.rel_base[n].is_none()
|| other.rel_base[n].is_none()
|| self.rel_base[n] == other.rel_base[n]
);
result.region_len[n] = self.region_len[n].or(other.region_len[n]);
result.rel_base[n] = self.rel_base[n].or(other.rel_base[n]);
}
result
}
pub fn covers(self, other: Self) -> bool {
(0..2).all(|n| {
self.window[n].lo <= other.window[n].lo
&& self.window[n].hi >= other.window[n].hi
&& self.region[n].lo <= other.region[n].lo
&& self.region[n].hi >= other.region[n].hi
&& self.region_len[n].is_none_or(|len| other.region_len[n] == Some(len))
&& self.rel_base[n].is_none_or(|len| other.rel_base[n] == Some(len))
})
}
pub fn contains(self, window: PxVec2, rel_base: UiVec2, region: UiRegion) -> bool {
AXES.into_iter().all(|axis| {
let n = axis as usize;
let len = region.axis(axis).len();
self.window[n].contains(window.axis(axis))
&& self.rel_base[n].is_none_or(|pinned| pinned == rel_base.axis(axis))
&& self.region[n].contains(len.to_px(window.axis(axis)))
&& self.region_len[n].is_none_or(|pinned| pinned == len)
})
}
}
+6 -108
View File
@@ -1,126 +1,24 @@
use crate::{ use crate::{Mask, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena};
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, UiRegion, WeakWidget,
WidgetId, Widgets,
util::{Arena, Id, TrackedArena},
};
/// How far the shader will walk a move chain. It bounds a malformed cycle
/// rather than any real tree; `Moves::resolve` uses the same number so the
/// two agree on what a deep tree resolves to.
pub const CHAIN_LIMIT: u32 = 64;
mod active; mod active;
mod holds; mod cache;
mod layout_holds;
mod painter; mod painter;
mod place;
mod render_state; mod render_state;
mod size;
pub use active::*; pub use active::*;
pub use holds::*; pub use painter::Painter;
pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike};
pub use place::*;
pub use render_state::*; pub use render_state::*;
pub use size::*;
#[derive(Default)] #[derive(Default)]
pub struct UiData { pub struct UiData {
pub widgets: Widgets, pub widgets: Widgets,
/// Every primitive this ui can draw.
pub primitives: PrimitiveRegistry,
pub textures: Textures, pub textures: Textures,
pub text: TextData, pub text: TextData,
pub masks: TrackedArena<Mask, u32>, pub masks: TrackedArena<Mask, u32>,
} }
/// Where each widget's drawing sits relative to its parent's slot, so moving
/// a subtree writes one entry rather than every descendant's primitives.
#[derive(Default)]
pub struct Moves {
arena: Arena<MoveOffset, u32>,
pub changed: bool,
}
impl Moves {
pub fn push(&mut self, parent: MoveIdx, region: UiRegion) -> MoveIdx {
self.changed = true;
MoveIdx::slot(self.arena.push(MoveOffset::new(parent, region)).idx())
}
/// Re-points a slot at a different parent, for a widget drawn somewhere
/// else in the tree than it was.
pub fn set_parent(&mut self, idx: MoveIdx, parent: MoveIdx) {
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
if entry.parent != parent {
entry.parent = parent;
self.changed = true;
}
}
pub fn remove(&mut self, idx: MoveIdx) {
self.changed = true;
self.arena.remove(Id::preset(idx.idx() as u32));
}
/// Sets the box a slot's contents are placed within, itself given in the
/// coordinates of its parent slot.
pub fn set(&mut self, idx: MoveIdx, region: UiRegion) {
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
if entry.region != region {
entry.region = region;
self.changed = true;
}
}
/// The same walk the vertex shader does, in the same `Len` the shader is
/// handed, for asking where a drawing will actually land -- hit testing,
/// and nothing layout decides on. Layout threads its lengths down the
/// draw instead, so no box it compares is composed back up this chain.
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
let mut region = local;
self.walk(idx, |entry| region = region.within(entry));
region
}
fn walk(&self, idx: MoveIdx, mut step: impl FnMut(&UiRegion)) {
let mut at = idx;
for _ in 0..CHAIN_LIMIT {
if at == MoveIdx::NONE {
return;
}
let entry = &self.arena[at.idx()];
step(&entry.region);
at = entry.parent;
}
debug_assert!(
at == MoveIdx::NONE,
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
and the shader stops at the same depth"
);
}
/// How many slots a region in `idx` is composed through, which is what
/// the shader's walk costs per primitive.
pub fn depth(&self, idx: MoveIdx) -> usize {
let mut depth = 0;
let mut at = idx;
while at != MoveIdx::NONE && depth < CHAIN_LIMIT as usize {
at = self.arena[at.idx()].parent;
depth += 1;
}
depth
}
pub fn entries(&self) -> &[MoveOffset] {
&self.arena
}
pub fn clear(&mut self) {
self.changed = true;
self.arena = Arena::default();
}
}
pub trait UiRsc { pub trait UiRsc {
fn ui(&self) -> &UiData; fn ui(&self) -> &UiData;
fn ui_mut(&mut self) -> &mut UiData; fn ui_mut(&mut self) -> &mut UiData;
@@ -130,7 +28,7 @@ pub trait UiRsc {
#[allow(unused_variables)] #[allow(unused_variables)]
fn on_remove(&mut self, id: WidgetId) {} fn on_remove(&mut self, id: WidgetId) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn on_draw(&mut self, active: &ActiveData) {} fn on_draw(&mut self, active: &ActiveData, redrawn: bool) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn on_undraw(&mut self, active: &ActiveData) {} fn on_undraw(&mut self, active: &ActiveData) {}
+57 -659
View File
@@ -1,500 +1,122 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel, Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, Widget, WidgetId,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
render::{ util::Vec2,
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
TexturePrimitive,
},
ui::render_state::{DrawInfo, Placing},
}; };
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// makes your surfaces look pretty /// makes your surfaces look pretty
pub struct Painter<'a> { pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState, pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc, pub(super) rsc: &'a mut dyn UiRsc,
/// This widget's rel base, per axis: a length of the window, and what a
/// fraction it or anything under it declares or reports is a fraction
/// of. A length rather than a box, so padding can take from both the
/// rel base and the box without either becoming the other.
pub(super) rel_base: UiVec2,
/// The box this widget was asked in, in its region node's coordinates:
/// what it draws in, and what its children's places are parts of.
pub(super) region: UiRegion, pub(super) region: UiRegion,
/// The window in pixels. Frames and boxes become pixels against this one
/// unit, regardless of region-node boundaries.
pub(super) window: PxVec2,
pub(super) mask: MaskIdx, pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<RetainedPrimitive>, pub(super) primitives: Vec<PrimitiveHandle>,
pub(super) mask_region: Option<UiRegion>,
/// The previous drawing's owned mask, available for this draw to reclaim.
pub(super) mask_slot: Option<MaskIdx>,
/// Only children whose answers were read constrain this widget's answer.
pub(super) answer_under: LayoutHolds,
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>,
/// What this draw itself reads, as against what its children's drawings
/// hold for: every window and every length of its own region until it
/// reads one, then that one unless it says otherwise, and the rel base or
/// region length it read symbolically, each of which makes the drawing
/// hold for that length alone.
pub(super) own: LayoutHolds,
/// What each child's drawing depends on. Asking a child again replaces
/// its drawing, so it replaces this too rather than narrowing it.
pub(super) under: Vec<(WidgetId, LayoutHolds)>,
/// The movable region this widget's primitives are positioned through:
/// its own when opted in, otherwise the nearest ancestor's.
pub(super) move_idx: MoveIdx,
pub layer: usize, pub layer: usize,
/// The layer this widget was entered on, which its children's layers are
/// counted from however far `layer` has walked.
pub(super) own_layer: usize,
pub(super) depth: usize,
pub(super) id: WidgetId, pub(super) id: WidgetId,
} }
impl<'a> Painter<'a> { impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) { fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region);
}
/// Takes the kind, for a caller writing many of one primitive.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
self.write_resolved(kind, primitive, region, self.resolve(region));
}
/// A box in this widget's region, composed into its region node's
/// coordinates.
fn resolve(&self, region: UiRegion) -> UiRegion {
region.within(&self.region)
}
fn write_resolved<P: Primitive>(
&mut self,
kind: PrimitiveKind<P>,
primitive: P,
region: UiRegion,
resolved: UiRegion,
) {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PrimitiveWrites);
let h = self.state.layers.write( let h = self.state.layers.write(
self.layer, self.layer,
PrimitiveInst { PrimitiveInst {
kind,
id: self.id, id: self.id,
primitive, primitive,
region: resolved, region,
mask_idx: self.mask, mask_idx: self.mask,
move_idx: self.move_idx,
}, },
); );
self.push_primitive(RetainedPrimitive { handle: h, region });
}
fn push_primitive(&mut self, h: RetainedPrimitive) {
if self.mask != MaskIdx::NONE { if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask); self.rsc.ui_mut().masks.push_ref(self.mask);
} }
self.primitives.push(h); self.primitives.push(h);
} }
/// Writes a primitive over the whole of this widget's own box. /// Writes a primitive to be rendered
pub fn primitive(&mut self, primitive: impl PrimitiveLike) { pub fn primitive<P: Primitive>(&mut self, primitive: P) {
let primitive = primitive.into_primitive(self); self.primitive_at(primitive, self.region)
self.primitive_at(primitive, UiRegion::FULL)
} }
/// Writes a primitive in a part of this widget's own box, in that box's pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
/// coordinates. self.primitive_at(primitive, region.within(&self.region));
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, region);
} }
/// Sets a mask, in this widget's own box's coordinates.
pub fn set_mask(&mut self, region: UiRegion) { pub fn set_mask(&mut self, region: UiRegion) {
self.mask_region = Some(region);
assert!(self.mask == MaskIdx::NONE); assert!(self.mask == MaskIdx::NONE);
let resolved = self.resolve(region); self.mask = self.rsc.ui_mut().masks.push(Mask { region });
let move_idx = self.move_idx;
let mask = Mask {
region: resolved,
move_idx,
};
let masks = &mut self.rsc.ui_mut().masks;
self.mask = match self.mask_slot.take() {
Some(idx) => {
*masks.get_mut(idx) = mask;
idx
}
None => {
let idx = masks.push(mask);
// The owner keeps the slot alive even with no primitives.
masks.push_ref(idx);
idx
}
};
} }
/// Draws a widget in the whole of this widget's own box, with the rel /// Draws a widget within this widget's region.
/// base forwarded unchanged: what a container that is only a wrapper pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
/// around one child wants. self.widget_at(id, self.region);
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
self.widget_at(id, UiRegion::FULL)
} }
/// Resolves what the place says about the child's rel base into a length, /// Draws a widget somewhere within this one.
/// where that is this widget's own narrowed the way the region is. An /// Useful for drawing child widgets in select areas.
/// axis the region leaves whole is not read at all, so a wrapper that pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
/// only moves its child does not pin its drawing to a rel base. self.widget_at(id, region.within(&self.region));
fn state_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc {
for axis in AXES {
if let Some(span) = place.axis(axis).narrows_rel_base() {
let len = span.len();
let stated = (len != Len::FULL).then(|| len.within_len(self.rel_base(axis)));
*place.axis_mut(axis) = place.axis(axis).with_rel_base(stated);
}
}
place
} }
/// Asks a child, saying what its fractions are of and where it is asked. fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
/// self.children.push(id.id());
/// `place` says where the child goes and what its fractions are of: self.state.draw_inner(
/// see [`PlaceDesc`]. A `UiRegion` converts into the common case, which self.layer,
/// is a box of this widget's own with the answer placed inside it.
///
/// The child draws once, in the region that comes of it, and its answer
/// is placed inside that region by re-expressing the drawing. Nothing is
/// drawn again in a box an answer chose; a container that puts the
/// answer somewhere else says so with [`Self::place_at`].
pub fn widget_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
place: impl Into<PlaceDesc>,
) -> DrawResult<'s, 'a, W> {
let place = self.state_rel_base(place.into());
let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id());
let (rel_base, region) =
rel_base_and_region(self.region, self.rel_base, place, declared, align);
#[cfg(feature = "layout-diagnostics")]
if region_node {
diag::bump(Counter::RegionNodeDraws);
diag::region_node(id.id(), self.id, region);
}
// A child listed twice would be moved twice.
let re_asked = self.children.contains(&id.id());
if !re_asked {
self.children.push(id.id());
}
let px = rel_base.to_px(self.window);
let (size, answer_holds, holds) = self.state.draw_inner(
id.id(), id.id(),
DrawInfo { region,
layer: self.layer, Some(self.id),
parent: Some(self.id), self.mask,
depth: self.depth + 1,
parent_move: self.move_idx,
region_node,
mask: self.mask,
rel_base,
region,
placed: place,
asked: place,
re_asked,
px,
},
None, None,
self.rsc, self.rsc,
); );
let holds = self.in_parent(holds, region, place, declared);
let answer_holds = self.in_parent(answer_holds, region, place, declared);
match self.under.iter_mut().find(|(child, _)| *child == id.id()) {
Some((_, kept)) => *kept = holds,
None => self.under.push((id.id(), holds)),
}
DrawResult {
child: id,
painter: self,
size,
answer_holds,
}
} }
/// Takes back a child that was drawn only to find out how long it is. pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
/// Its drawing is dropped and it is not one of this widget's children self.textures.push(handle.clone());
/// this frame; what it answered is still something this widget asked. self.primitive_at(handle.primitive(), region.within(&self.region));
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.children.retain(|child| *child != id.id());
self.under.retain(|(child, _)| *child != id.id());
self.state.undraw_rec(id.id(), self.rsc);
} }
/// Puts a child in `place` of this widget's box, where that box is the pub fn texture(&mut self, handle: &TextureHandle) {
/// answer the child already gave: the drawing is re-expressed there self.textures.push(handle.clone());
/// rather than made again -- what a row does once it knows every slot, self.primitive(handle.primitive());
/// having measured each child from its cursor.
///
/// A child this draw has not asked about, and one whose rel base this
/// narrows, is asked here instead: there is no answer to re-express, or
/// the question has changed. So a container that places every child the
/// same way says it once, and which of the two happens is this widget's
/// business rather than the caller's.
pub fn place_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
place: impl Into<PlaceDesc>,
) -> DrawResult<'s, 'a, W> {
let place = self.state_rel_base(place.into());
let states_rel_base = AXES
.iter()
.any(|&axis| place.axis(axis).stated_rel_base().is_some());
if states_rel_base || !self.children.contains(&id.id()) {
return self.widget_at(id, place);
}
let at = self.placing();
self.state.place_in(id.id(), &at, place, self.rsc);
let active = &self.state.active[&id.id()];
let size = active.measured().unwrap_or(active.size);
DrawResult {
child: id,
painter: self,
size,
// Read where it was asked; moving it is not a second answer.
answer_holds: LayoutHolds::ANY,
}
} }
/// This widget as the thing its children are placed within. pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
fn placing(&self) -> Placing { self.textures.push(handle.clone());
Placing { self.primitive_at(handle.primitive(), region);
id: self.id,
region: self.region,
rel_base: self.rel_base,
window: self.window,
depth: self.depth,
move_idx: self.move_idx,
mask: self.mask,
}
} }
/// What a widget's rules declare its lengths to be, which whoever draws /// returns (handle, offset from top left)
/// it resolves into its rel base. Reading them depends on nothing -- the box pub fn render_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
/// that comes of them is kept on the child, and `redraw` compares it
/// there.
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] {
declared_lens(self.rsc.widgets(), id.id())
}
/// What a child says its length is without being drawn, if it can say,
/// as the length its draw would report: a fraction in it is resolved
/// against this widget's rel base, which is the rel base a child asked with
/// nothing narrowed gets. Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
let widgets = self.rsc.widgets();
// A rule is the answer where there is one: it wins over whatever the
// widget would draw, so it has to win over what the widget says too.
let hint = widgets.size_rules(id.id()).axis(axis).exact().or_else(|| {
widgets
.get_dyn(id.id())
.and_then(|widget| widget.size_hint(axis))
});
let rel_base = self.rel_base.axis(axis);
let resolved = hint.map(|hint| hint.within_len(rel_base));
#[cfg(feature = "layout-diagnostics")]
{
diag::hint_read(id.id(), self.id, axis, resolved);
diag::bump(match resolved {
Some(_) => Counter::HintHits,
None => Counter::HintMisses,
});
}
if let Some(hint) = hint {
self.depend_on(id);
// Resolving a fraction against this rel base makes this draw a
// function of the rel base's length. The fraction to ask about is
// the child's own: resolved against a rel base of pixels, none is
// left to see it by.
if hint.rel != Rel::ZERO {
self.own.rel_base[axis as usize] = Some(rel_base);
}
}
resolved
}
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id());
}
}
pub fn render_text<'b>(
&mut self,
buffer: &'b mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
) -> &'b RenderedText {
#[cfg(feature = "layout-diagnostics")]
diag::render_text(self.id, self.rsc.widgets().label(self.id), width);
let ui = self.rsc.ui_mut(); let ui = self.rsc.ui_mut();
ui.text.render(buffer, attrs, width) ui.text.draw(buffer, attrs, &mut ui.textures)
} }
/// Writes glyphs in the selected rel base or region coordinates. pub fn region(&self) -> UiRegion {
// TODO: merge the text methods into the primitive ones. self.region
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { }
// Glyph offsets and sizes are pixels, which compose additively.
// Only the shared origin needs composing through the region. pub fn size<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size {
let resolved = self.resolve(origin); self.size_ctx().size(id)
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>(); }
for glyph in text.glyphs.iter() {
let place = |mut region: UiRegion| { pub fn len_axis<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Len {
region.x.end = region.x.start; match axis {
region.y.end = region.y.start; Axis::X => self.size_ctx().width(id),
let mut region = region.offset(UiVec2::from_px(glyph.offset)); Axis::Y => self.size_ctx().height(id),
let size = PxVec2::new(
Px::from_int(glyph.entry.width as i32),
Px::from_int(glyph.entry.height as i32),
);
region.x.end = region.x.start.offset(size.x);
region.y.end = region.y.start.offset(size.y);
region
};
self.write_resolved(
kind,
GlyphPrimitive {
uv_min: glyph.entry.uv_min,
uv_max: glyph.entry.uv_max,
layer: glyph.entry.layer,
color: text.color,
flags: glyph.entry.flags(),
},
place(origin),
place(resolved),
);
} }
} }
/// The symbolic length of this widget's own box along one axis, in the pub fn output_size(&self) -> Vec2 {
/// lengths of its rel base that it places its children in. Reading it pins self.state.output_size
/// the drawing to that length -- and to nothing about where the box
/// starts, which is what lets a container move without being drawn
/// again. One axis at a time, because a container that divides one axis
/// holds for any length of the other.
pub fn region_len(&mut self, axis: Axis) -> Len {
let len = self.region.axis(axis).len();
self.own.region_len[axis as usize] = Some(len);
len
} }
/// This widget's rel base along one axis: what a fraction it or anything pub fn px_size(&mut self) -> Vec2 {
/// under it declares or reports is a fraction of. A container reads it self.region.size().to_abs(self.state.output_size)
/// to hand a length of it down -- padding, which takes its pixels off.
/// Reading it pins the drawing to that rel base, the way
/// [`Self::region_len`] pins it to the box.
pub fn rel_base(&mut self, axis: Axis) -> Len {
let len = self.rel_base.axis(axis);
self.own.rel_base[axis as usize] = Some(len);
len
}
/// 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)
}
/// Whether a rule beside this widget gives its length on `axis` outright,
/// which makes whatever it reports for that axis moot. A rule that only
/// bounds the length is not one of these: the answer is still the
/// widget's to give, and something still has to work it out.
///
/// The widget under a rule does not otherwise learn of it -- this is for
/// a container deciding whether reading its children across an axis is
/// worth anything, since reading one is also what makes its own size
/// depend on it.
pub fn has_exact_size(&self, axis: Axis) -> bool {
self.rsc
.widgets()
.size_rules(self.id)
.axis(axis)
.exact()
.is_some()
}
/// This widget's own 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) -> PxVec2 {
PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y))
}
/// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px {
let len = self.region.axis(axis).len();
let px = len.to_px(self.window.axis(axis));
let own = &mut self.own.region[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(px);
}
px
}
/// The lengths of this widget's own box on `axis` that what it is drawing
/// holds for -- the same primitives, in the same fractions and offsets
/// of the box, and the same reported size. A widget that read its length
/// in pixels holds for that one alone until it says otherwise.
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let len = self.region.axis(axis).len();
let holds = holds.into();
debug_assert!(
holds.contains(len.to_px(self.window.axis(axis))),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(),
self.id
);
self.own.region[axis as usize] = holds;
}
/// A window length in pixels, which is what every length in layout is
/// measured in. Reading one pins the drawing to this window wherever the
/// length is a fraction of it; one that is only pixels is that many
/// pixels in any window and pins nothing.
pub fn to_px(&mut self, len: Len, axis: Axis) -> Px {
let window = self.window.axis(axis);
if len.rel != Rel::ZERO {
let own = &mut self.own.window[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(window);
}
}
len.to_px(window)
}
/// The windows this drawing holds for, stated rather than taken: a
/// container that branched on a length in pixels says which side of the
/// boundary it was on, which is wider than the one window reading that
/// length pins, and replaces it.
pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into();
debug_assert!(
holds.contains(self.window.axis(axis)),
"'{}' ({:?}) says its drawing holds for windows that leave out this one",
self.label(),
self.id
);
self.own.window[axis as usize] = holds;
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
@@ -505,18 +127,6 @@ impl<'a> Painter<'a> {
self.layer = self.state.layers.child(self.layer); self.layer = self.state.layers.child(self.layer);
} }
/// The layer this widget's `n`th child draws on, addressed rather than
/// walked to. A container that measures one child by drawing it can ask
/// on the layer that child will end up on, and then the second ask is a
/// reuse rather than a second drawing on another layer.
pub fn child_layer_at(&mut self, n: usize) {
let mut at = self.state.layers.child(self.own_layer);
for _ in 0..n {
at = self.state.layers.next(at);
}
self.layer = at;
}
pub fn next_layer(&mut self) { pub fn next_layer(&mut self) {
self.layer = self.state.layers.next(self.layer); self.layer = self.state.layers.next(self.layer);
} }
@@ -528,220 +138,8 @@ impl<'a> Painter<'a> {
pub fn id(&self) -> &WidgetId { pub fn id(&self) -> &WidgetId {
&self.id &self.id
} }
}
/// A child that has just been drawn. Reading its size records that this pub fn size_ctx(&mut self) -> SizeCtx<'_> {
/// widget's own size depends on it; dropping it without reading draws the self.state.size_ctx(self.id, self.region.size(), self.rsc)
/// 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,
answer_holds: LayoutHolds,
}
impl<W: ?Sized> DrawResult<'_, '_, W> {
pub fn size(self) -> Size {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::SizeReads);
diag::size_read(self.child.id(), self.painter.id, self.size);
}
self.painter.depend_on(self.child);
self.painter.answer_under = self.painter.answer_under.and(self.answer_holds);
self.size
}
pub fn len(self, axis: Axis) -> LayoutLen {
self.size().axis(axis)
} }
} }
/// What `Painter::primitive` takes: a primitive, or something that yields one
/// and does whatever else drawing it needs.
pub trait PrimitiveLike {
type Primitive: Primitive;
fn into_primitive(self, painter: &mut Painter) -> Self::Primitive;
}
impl<P: Primitive> PrimitiveLike for P {
type Primitive = P;
fn into_primitive(self, _: &mut Painter) -> P {
self
}
}
impl PrimitiveLike for &TextureHandle {
type Primitive = TexturePrimitive;
/// Retains a share of the handle, so the slot the primitive names cannot
/// be freed and reused while it is still drawn.
fn into_primitive(self, painter: &mut Painter) -> TexturePrimitive {
painter.textures.push(self.clone());
self.into()
}
}
/// Moves what a child depends on into this widget's own terms: this
/// method's `impl` block is where a `Painter`'s own boxes are, so it takes
/// only what the child was asked with.
impl Painter<'_> {
/// Window ranges are already about the one unit and combine directly.
/// A rel base pin becomes this widget's own rel base wherever a length of it
/// is what reached the child; where only pixels did, no length of this
/// rel base can change the child's and the pin stops here.
///
/// A child's validity maps back through the part of this widget's box,
/// where the box the child was asked in is that part; a declared length
/// places the box inside the part instead, and then only that length
/// reaches the child. A narrowed rel base is not one of these: it decides
/// what fractions under the child mean and leaves the box the part it
/// was given.
fn in_parent(
&self,
holds: LayoutHolds,
region: UiRegion,
place: PlaceDesc,
declared: [Option<LayoutLen>; 2],
) -> LayoutHolds {
let mut result = LayoutHolds::ANY;
for axis in AXES {
let n = axis as usize;
// Every read became pixels against the window, so a range on
// it is already in this widget's terms.
result.window[n] = holds.window[n];
let at = *place.axis(axis);
let reaches = at.stated_rel_base().is_none()
&& !at.is_sized()
&& declared[n].is_none_or(|len| len.rel != Rel::ZERO);
result.rel_base[n] = holds.rel_base[n].and(reaches.then(|| self.rel_base.axis(axis)));
match (at.within_span(), declared[n].is_some()) {
// Its box is a part of this widget's own box, in that box's
// own lengths, so what it holds for maps back through that
// part into a range on this widget's box. A length it pinned
// is this widget's length less the part's pixels where the
// part is the whole of the box less pixels, which is the one
// shape that inverts exactly; any other part pins this
// widget's own length.
(Some(span), false) => {
let part_len = span.len();
result.region[n] = holds.region[n].through(part_len);
result.region_len[n] = holds.region_len[n].map(|pinned| match part_len.rel {
Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px),
_ => self.region.axis(axis).len(),
});
}
// Its box is a length this widget decided, from its own
// rel base or from a sibling's answer: no length of this
// widget's box reaches it, so what it holds for is a range
// on the window and none of it on that box.
_ => {
result.window[n] =
result.window[n].and(holds.region[n].through(region.axis(axis).len()));
}
}
}
result
}
}
/// What a widget declares a length of its box to be. `leftover` is not one: a
/// share of what is left over is only a length to the widget dividing one,
/// so it passes up in the size instead.
pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLen>; 2] {
let rules = widgets.size_rules(id);
let widget = widgets.get_dyn(id);
AXES.map(|axis| {
rules.axis(axis).declared().or_else(|| {
// A hint still narrows the box where no rule does, which is how a
// widget with a natural pixel size -- an image, a gap -- gets that
// size rather than the whole offer. That is the offer's business
// rather than a declaration's, and this falls away once a widget
// occupies its reported size inside the box it was offered.
widget
.and_then(|widget| widget.size_hint(axis))
.filter(|len| len.leftover == Weight::ZERO)
})
})
}
/// Whether what a widget reported along an axis is the whole of the box it
/// is in rather than a part to be placed inside it. 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 does too: the rule already gave
/// the region its length, and the rule's length is what the widget reports
/// there. And an axis the parent decided from the answer is
/// the answer already.
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
reported.leftover != Weight::ZERO || declared.is_some() || decided
}
/// Where a widget's drawing goes inside the part its parent gave it: what
/// it reported, on the side of the part its alignment says, and the whole
/// part wherever the answer fills it.
///
/// The length it reported is a length of its rel base, and the part is one too,
/// so this takes one from the other rather than composing it into the part.
/// That is what makes a fraction the same fraction wherever the part it is
/// placed in sits and however long it is -- the fraction is resolved once,
/// here, against the rel base it was reported of.
pub(crate) fn placement(
region: UiRegion,
size: Size,
declared: [Option<LayoutLen>; 2],
place: PlaceDesc,
align: RegionAlign,
) -> UiRegion {
let mut placed = region;
for axis in AXES {
let n = axis as usize;
let reported = size.axis(axis);
if fills(reported, declared[n], place.axis(axis).does_fill()) {
continue;
}
let len = Len::from_parts(reported.rel, reported.px);
let span = placed.axis_mut(axis);
span.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len;
}
placed
}
/// The rel base length and the box a child is asked in, in the coordinates the
/// widget asking draws in.
///
/// `own` is that widget's own box, and `place` what of it the child is
/// given, including any rel base it states -- a row's slot, or padding's rel
/// base less its pixels. That is a window length, like every other length
/// here, since a slot of a row is not a fraction of anything the row can
/// name. The child's declaration is a fraction of whichever reached it, and
/// is the only one that also places the box: a box the caller decided is
/// what `place` names.
pub(crate) fn rel_base_and_region(
own: UiRegion,
parent_rel_base: UiVec2,
place: PlaceDesc,
declared: [Option<LayoutLen>; 2],
align: RegionAlign,
) -> (UiVec2, UiRegion) {
let given = place.of(own, align);
let mut rel_base = parent_rel_base;
let mut region = given;
for axis in AXES {
let n = axis as usize;
let base = place
.axis(axis)
.stated_rel_base()
.unwrap_or_else(|| parent_rel_base.axis(axis));
let len = declared[n]
.map(|len| Len::from_parts(len.rel, len.px).within_len(base))
.unwrap_or(base);
*rel_base.axis_mut(axis) = len;
if declared[n].is_some() {
let slot = given.axis(axis);
let start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
*region.axis_mut(axis) = UiSpan::new(start, start + len);
}
}
(rel_base, region)
}
-249
View File
@@ -1,249 +0,0 @@
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan};
/// How a child's region along one axis comes from the region of the widget
/// asking, and what its fractions are of.
///
/// The three ways of saying a region are the three the geometry already has:
/// a span composed into the caller's box, a span shifted to where that box
/// starts, and a length placed in it by alignment. Which one is meant cannot
/// be read off the numbers, since two of them take the same span and apply
/// it differently, so it is said here.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlaceDescAxis {
span: PlaceSpan,
fills: bool,
rel_base: RelBase,
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum PlaceSpan {
Within(UiSpan),
Shifted(UiSpan),
Sized(Len),
}
/// What a child's fractions are of, where the caller has not named a length.
#[derive(Clone, Copy, Debug, PartialEq)]
enum RelBase {
/// The caller's own, unchanged.
Inherit,
/// The caller's own, narrowed the way the region is.
WithRegion,
/// This length of the window.
Len(Len),
}
impl PlaceDescAxis {
/// The whole of the caller's box.
pub const WHOLE: Self = Self::within(UiSpan::FULL);
/// `span` composed into the caller's own box, so it moves and scales
/// with it: [`UiSpan::within`], which is what a container that insets
/// one speaks. Taking eleven pixels off the end needs no length, where
/// saying the same thing in window lengths would make the container read
/// its own box -- and a box chosen from its own answer then feeds back
/// into the answer.
///
/// The child's rel base is narrowed the same way, so padding takes its
/// pixels off both and `rel(1)` under it fills the caller rather than
/// overflowing it.
pub const fn within(span: UiSpan) -> Self {
Self {
span: PlaceSpan::Within(span),
fills: false,
rel_base: RelBase::WithRegion,
}
}
/// `span` shifted to where the caller's own box starts: window lengths
/// along a cursor, which is what a container dividing room among its
/// children speaks. A child's report is a window length, so the cursor
/// that sums those reports is one too, and a moved box re-places every
/// child by re-adding its start, exactly.
///
/// The child's rel base passes through: how far along the cursor a child
/// sits says nothing about what a fraction under it is of.
pub const fn shifted(span: UiSpan) -> Self {
Self {
span: PlaceSpan::Shifted(span),
fills: false,
rel_base: RelBase::Inherit,
}
}
/// A box this long, placed in the caller's own by the child's alignment:
/// the rule that places an answer, with the length given from above
/// rather than reported. What a stack's sizing child decides for the
/// rest. It is the child's rel base too.
pub const fn sized(len: Len) -> Self {
Self {
span: PlaceSpan::Sized(len),
fills: false,
rel_base: RelBase::Len(len),
}
}
/// This region is the child's placement: its answer is not placed inside
/// it again. A container uses it where it hands back exactly what the
/// child asked for -- a row placing a child at the length it reported.
pub const fn fills(mut self) -> Self {
self.fills = true;
self
}
/// What the child's fractions are of, as a length of the window: a
/// resolved share, or a box a sibling's answer decided.
pub const fn rel_base(mut self, len: Len) -> Self {
self.rel_base = RelBase::Len(len);
self
}
/// Whether the region is the placement outright, rather than a box the
/// answer is placed inside.
pub(crate) const fn does_fill(self) -> bool {
self.fills
}
/// Where it lands in the coordinates `own` is in.
pub(crate) fn of(self, own: UiSpan, align: AxisAlign) -> UiSpan {
match self.span {
PlaceSpan::Within(span) => span.within(&own),
PlaceSpan::Shifted(mut span) => {
span.shift(own.start);
span
}
PlaceSpan::Sized(len) => {
let start = own.start + (own.len() - len).scale(align.rel());
UiSpan::new(start, start + len)
}
}
}
/// The child's rel base, where this says one outright. `None` forwards
/// the caller's own, and [`RelBase::WithRegion`] is resolved by whoever
/// can read that rel base, so it does not reach here.
pub(crate) const fn stated_rel_base(self) -> Option<Len> {
match self.rel_base {
RelBase::Len(len) => Some(len),
_ => None,
}
}
/// The length this narrows the caller's rel base by, where it does.
/// `None` leaves that rel base alone, and reading it is then a
/// dependency the caller does not take.
pub(crate) const fn narrows_rel_base(self) -> Option<UiSpan> {
match (self.rel_base, self.span) {
(RelBase::WithRegion, PlaceSpan::Within(span)) => Some(span),
_ => None,
}
}
/// The span it composes into the caller's box, where that is what it
/// does: the one case whose validity maps back through the part.
pub(crate) const fn within_span(self) -> Option<UiSpan> {
match self.span {
PlaceSpan::Within(span) => Some(span),
_ => None,
}
}
/// Whether the caller decided this length rather than a place along its
/// own box, which is what stops its length reaching the child at all.
pub(crate) const fn is_sized(self) -> bool {
matches!(self.span, PlaceSpan::Sized(_))
}
/// The same, with its rel base stated outright.
pub(crate) const fn with_rel_base(mut self, len: Option<Len>) -> Self {
self.rel_base = match len {
Some(len) => RelBase::Len(len),
None => RelBase::Inherit,
};
self
}
}
/// Where a child is asked, on both axes. A [`UiRegion`] converts into the
/// common case: that box of the caller's own, the answer placed inside it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlaceDesc {
pub x: PlaceDescAxis,
pub y: PlaceDescAxis,
}
impl PlaceDesc {
/// The whole of the caller's box, on both axes.
pub const WHOLE: Self = Self::splat(PlaceDescAxis::WHOLE);
pub const fn new(x: PlaceDescAxis, y: PlaceDescAxis) -> Self {
Self { x, y }
}
/// The same on both axes.
pub const fn splat(place: PlaceDescAxis) -> Self {
Self { x: place, y: place }
}
/// `aligned` on `axis` and `ortho` on the other, which is how a
/// container that divides one axis says what it is doing.
pub fn from_axis(axis: Axis, aligned: PlaceDescAxis, ortho: PlaceDescAxis) -> Self {
match axis {
Axis::X => Self::new(aligned, ortho),
Axis::Y => Self::new(ortho, aligned),
}
}
pub const fn axis(&self, axis: Axis) -> &PlaceDescAxis {
match axis {
Axis::X => &self.x,
Axis::Y => &self.y,
}
}
pub const fn axis_mut(&mut self, axis: Axis) -> &mut PlaceDescAxis {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
/// Both regions are the child's placement. See [`PlaceDescAxis::fills`].
pub const fn fills(self) -> Self {
Self::new(self.x.fills(), self.y.fills())
}
/// The child's rel base on one axis. See [`PlaceDescAxis::rel_base`].
pub const fn rel_base(mut self, axis: Axis, len: Len) -> Self {
*self.axis_mut(axis) = self.axis(axis).rel_base(len);
self
}
/// The box each axis names, in the coordinates `own` is in.
pub(crate) fn of(self, own: UiRegion, align: RegionAlign) -> UiRegion {
UiRegion::new(self.x.of(own.x, align.x), self.y.of(own.y, align.y))
}
}
impl From<UiRegion> for PlaceDesc {
fn from(region: UiRegion) -> Self {
Self::new(
PlaceDescAxis::within(region.x),
PlaceDescAxis::within(region.y),
)
}
}
impl From<PlaceDescAxis> for PlaceDesc {
fn from(place: PlaceDescAxis) -> Self {
Self::splat(place)
}
}
/// A primitive as it was written: its box in the widget's own box's
/// coordinates, which is what a move of that box re-composes from.
#[derive(Debug)]
pub struct RetainedPrimitive {
pub handle: PrimitiveHandle,
pub region: UiRegion,
}
+156 -1057
View File
File diff suppressed because it is too large. Load diff
+86
View File
@@ -0,0 +1,86 @@
use crate::{
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures,
UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
};
pub struct SizeCtx<'a> {
pub text: &'a mut TextData,
pub textures: &'a mut Textures,
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,
textures: self.textures,
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) -> RenderedText {
self.text.draw(buffer, attrs, self.textures)
}
pub fn label(&self, id: WidgetId) -> &String {
self.widgets.label(id)
}
}
-9
View File
@@ -34,10 +34,6 @@ impl<T, I: IdNum> Arena<T, I> {
self.tracker.free(id); self.tracker.free(id);
self.data[i] self.data[i]
} }
pub(crate) fn get_mut(&mut self, id: Id<I>) -> &mut T {
&mut self.data[id.idx()]
}
} }
impl<T, I: IdNum> Default for Arena<T, I> { impl<T, I: IdNum> Default for Arena<T, I> {
@@ -75,11 +71,6 @@ impl<T, I: IdNum> TrackedArena<T, I> {
self.refs[i.idx()] += 1; self.refs[i.idx()] += 1;
} }
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
self.changed = true;
self.inner.get_mut(id)
}
pub fn remove(&mut self, id: Id<I>) -> T pub fn remove(&mut self, id: Id<I>) -> T
where where
T: Copy, T: Copy,
+22 -29
View File
@@ -1,13 +1,34 @@
use std::ops::*;
pub const trait LerpUtil { 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;
} }
const impl LerpUtil for f32 { pub const trait DivOr {
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
fn lerp_inv(self, from: Self, to: Self) -> Self {
(self - from).div_or(to - from, from)
}
} }
macro_rules! impl_op { macro_rules! impl_op {
@@ -56,34 +77,6 @@ macro_rules! impl_op {
} }
} }
}; };
// Without the `f32` operations, for a type whose fields are not all the
// same kind of number: there is nothing a bare float means to a fraction
// and an offset at once.
(same $T:ident $op:ident $fn:ident $opa:ident $fna:ident; $($field:ident)*) => {
#[allow(non_snake_case)]
mod ${concat($T, _op_, $fn, _same_impl)} {
use super::*;
#[allow(unused_imports)]
use std::ops::*;
const impl $op for $T {
type Output = Self;
fn $fn(self, rhs: Self) -> Self::Output {
Self {
$($field: self.$field.$fn(rhs.$field),)*
}
}
}
const impl $opa for $T {
fn $fna(&mut self, rhs: Self) {
*self = self.$fn(rhs);
}
}
}
};
(same $T:ident $op:ident $fn:ident; $($field:ident)*) => {
impl_op!(same $T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
};
($T:ident $op:ident $fn:ident; $($field:ident)*) => { ($T:ident $op:ident $fn:ident; $($field:ident)*) => {
impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*); impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
}; };
+1 -1
View File
@@ -16,7 +16,7 @@ pub use id::*;
pub use math::*; pub use math::*;
pub use refcount::*; pub use refcount::*;
pub use slot::*; pub use slot::*;
pub(crate) use trust::*; pub use trust::*;
pub use typemap::*; pub use typemap::*;
pub use vec2::*; pub use vec2::*;
+1 -1
View File
@@ -1,4 +1,4 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SlotId { pub struct SlotId {
idx: u32, idx: u32,
genr: u32, genr: u32,
+7 -2
View File
@@ -1,10 +1,15 @@
#[allow(clippy::missing_safety_doc)] #[allow(clippy::missing_safety_doc)]
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T { pub unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
unsafe { std::mem::transmute::<&T, &T>(x) }
}
#[allow(clippy::missing_safety_doc)]
pub unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
unsafe { std::mem::transmute::<&mut T, &mut T>(x) } unsafe { std::mem::transmute::<&mut T, &mut T>(x) }
} }
#[allow(clippy::mut_from_ref, clippy::missing_safety_doc)] #[allow(clippy::mut_from_ref, clippy::missing_safety_doc)]
pub(crate) unsafe fn to_mut<T>(x: &T) -> &mut T { pub unsafe fn to_mut<T>(x: &T) -> &mut T {
#[allow(mutable_transmutes)] #[allow(mutable_transmutes)]
unsafe { unsafe {
std::mem::transmute::<&T, &mut T>(x) std::mem::transmute::<&T, &mut T>(x)
+11 -6
View File
@@ -1,11 +1,7 @@
use crate::util::impl_op; use crate::util::{DivOr, impl_op};
use std::{hash::Hash, ops::*}; use std::{hash::Hash, ops::*};
/// `align(8)` because that is WGSL's alignment for a `vec2<f32>`, so any GPU #[repr(C)]
/// struct holding one is laid out the way its shader reads it without having
/// to say so itself. Those structs still need a manual `unsafe impl Pod`,
/// since the trailing padding this introduces is what `derive(Pod)` refuses.
#[repr(C, align(8))]
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vec2 { pub struct Vec2 {
pub x: f32, pub x: f32,
@@ -71,6 +67,15 @@ 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;
+1 -7
View File
@@ -1,11 +1,8 @@
use crate::{RegionAlign, SizeRules, Widget}; use crate::Widget;
pub struct WidgetData { pub struct WidgetData {
pub widget: Box<dyn Widget>, pub widget: Box<dyn Widget>,
pub label: String, pub label: String,
pub(super) region_node: bool,
pub(super) size: SizeRules,
pub(super) align: RegionAlign,
/// dynamic borrow checking /// dynamic borrow checking
pub borrowed: bool, pub borrowed: bool,
} }
@@ -19,9 +16,6 @@ impl WidgetData {
Self { Self {
widget: Box::new(widget), widget: Box::new(widget),
label, label,
region_node: false,
size: SizeRules::default(),
align: RegionAlign::default(),
borrowed: false, borrowed: false,
} }
} }
+8 -1
View File
@@ -34,7 +34,7 @@ pub trait HasRoot {
pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> { pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> {
#[track_caller] #[track_caller]
fn add(self, state: &mut Rsc) -> WidgetArr<LEN>; fn add(self, rsc: &mut Rsc) -> WidgetArr<LEN>;
} }
impl<Rsc, const LEN: usize> WidgetArrLike<Rsc, LEN, ArrTag> for WidgetArr<LEN> { impl<Rsc, const LEN: usize> WidgetArrLike<Rsc, LEN, ArrTag> for WidgetArr<LEN> {
@@ -58,6 +58,13 @@ macro_rules! impl_widget_arr {
) )
} }
} }
impl<Rsc: UiRsc, $($W: WidgetLike<Rsc, $Tag>,$Tag,)*> IntoWidgetVec<Rsc, ($($Tag,)*), ArrTag> for ($($W,)*) {
fn into_vec(self, rsc: &mut Rsc) -> Vec<StrongWidget> {
#[allow(non_snake_case)]
let ($($W,)*) = self;
vec![$($W.add(rsc).upgrade(rsc),)*]
}
}
}; };
} }
+33 -16
View File
@@ -1,10 +1,9 @@
use crate::{Axis, LayoutLen, Painter, Size}; use crate::{Axis, AxisT, Len, Painter, SizeCtx, UiRsc};
use std::any::Any; use std::any::Any;
mod data; mod data;
mod handle; mod handle;
mod like; mod like;
mod size_rule;
mod tag; mod tag;
mod view; mod view;
mod widgets; mod widgets;
@@ -12,31 +11,36 @@ mod widgets;
pub use data::*; pub use data::*;
pub use handle::*; pub use handle::*;
pub use like::*; pub use like::*;
pub use size_rule::*;
pub use tag::*; pub use tag::*;
pub use view::*; pub use view::*;
pub use widgets::*; pub use widgets::*;
pub trait Widget: Any { pub trait Widget: Any {
/// Draws the widget, and returns what it used of the box it was given. fn draw(&mut self, painter: &mut Painter);
fn draw(&mut self, painter: &mut Painter) -> Size; fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len;
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len;
}
/// An exact length the widget can give without a painter or its children. pub trait WidgetAxisFns {
/// Optional, and saves a draw rather than changing one: a hint that fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len;
/// disagrees with the eventual draw fails a debug assertion. }
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
None impl<W: Widget + ?Sized> WidgetAxisFns for W {
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),
}
} }
} }
impl Widget for () { impl Widget for () {
/// A gap: nothing drawn, at the default length, so a span gives it a share. fn draw(&mut self, _: &mut Painter) {}
fn draw(&mut self, _: &mut Painter) -> Size { fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Size::default() Len::ZERO
} }
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> { Len::ZERO
Some(LayoutLen::default())
} }
} }
@@ -81,3 +85,16 @@ impl<State, F: FnOnce(&mut State) -> Option<StrongWidget>> WidgetOption<State> f
self(state) self(state)
} }
} }
pub trait IntoWidgetVec<Rsc, WTag, GTag> {
fn into_vec(self, rsc: &mut Rsc) -> Vec<StrongWidget>;
}
impl<Rsc: UiRsc, I: IntoIterator, Tag> IntoWidgetVec<Rsc, Tag, IterTag> for I
where
I::Item: WidgetLike<Rsc, Tag>,
{
fn into_vec(self, rsc: &mut Rsc) -> Vec<StrongWidget> {
self.into_iter().map(|w| w.add_strong(rsc).any()).collect()
}
}
-87
View File
@@ -1,87 +0,0 @@
use crate::{Axis, LayoutLen, Weight};
/// What a widget's length on one axis is, as a rule its parent applies where
/// it draws it rather than an answer the widget gives about itself.
///
/// A rule and a drawn size are not two opinions to reconcile: a rule wins on
/// the axis it names, and the `Size` returned by `draw` answers only the axes
/// with no rule. That is what lets a span divide its space around a length
/// nobody has drawn yet, and it is why a rule lives beside the widget rather
/// than inside it -- the widget under the rule never has to know about it.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum SizeRule {
/// Whatever the widget reports from drawing.
#[default]
Free,
/// This length, whatever the widget reports.
Exact(LayoutLen),
}
impl SizeRule {
/// The length this rule gives without the widget being drawn, if it can
/// give one. `leftover` is never among them: a share is a length only to
/// whoever divides one, so it passes up in the reported size instead and
/// is resolved there.
pub fn declared(&self) -> Option<LayoutLen> {
match self {
Self::Exact(len) if len.leftover == Weight::ZERO => Some(*len),
_ => None,
}
}
/// The length this rule gives outright, whatever the widget reports --
/// which makes the widget's answer on that axis moot. A share counts: it
/// is a length the widget's parent still has to divide, so it is exact
/// here and resolved there, unlike `declared`, which is only the ones
/// that give a box directly.
pub fn exact(&self) -> Option<LayoutLen> {
match self {
Self::Free => None,
Self::Exact(len) => Some(*len),
}
}
/// The length a widget reporting `reported` ends up with.
pub fn apply(&self, reported: LayoutLen) -> LayoutLen {
match self {
Self::Free => reported,
Self::Exact(len) => *len,
}
}
}
impl From<LayoutLen> for SizeRule {
fn from(len: LayoutLen) -> Self {
Self::Exact(len)
}
}
impl From<Option<LayoutLen>> for SizeRule {
fn from(len: Option<LayoutLen>) -> Self {
len.map_or(Self::Free, Self::Exact)
}
}
/// One rule per axis, which is how a widget carries a length on one axis and
/// leaves the other to whatever it draws.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct SizeRules {
pub x: SizeRule,
pub y: SizeRule,
}
impl SizeRules {
pub fn axis(&self, axis: Axis) -> SizeRule {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut SizeRule {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
}
+1
View File
@@ -62,3 +62,4 @@ impl<Rsc: UiRsc, V: WidgetView> WidgetLike<Rsc, ViewTag> for V {
} }
pub struct ArrTag; pub struct ArrTag;
pub struct IterTag;
+1 -67
View File
@@ -1,8 +1,7 @@
use std::sync::mpsc::{Receiver, Sender, channel}; use std::sync::mpsc::{Receiver, Sender, channel};
use crate::{ use crate::{
Axis, AxisAlign, IdLike, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget, IdLike, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
WidgetData, WidgetId,
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut}, util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
}; };
@@ -101,71 +100,6 @@ impl Widgets {
self.data_mut(id.id()).unwrap().label = label; self.data_mut(id.id()).unwrap().label = label;
} }
/// Whether this widget owns a movable retained region.
pub fn is_region_node(&self, id: impl IdLike) -> bool {
self.data(id).unwrap().region_node
}
/// Chooses whether this widget's retained drawing has one movable region
/// of its own. Changing the boundary redraws the subtree once so every
/// primitive names the right coordinate space.
pub fn set_region_node(&mut self, id: impl IdLike, region_node: bool) {
let id = id.id();
let data = self.data_mut(id).unwrap();
if data.region_node == region_node {
return;
}
data.region_node = region_node;
self.needs_redraw.insert(id);
}
/// The length rules whoever draws this widget applies to its box.
pub fn size_rules(&self, id: impl IdLike) -> SizeRules {
self.data(id).unwrap().size
}
/// Sets one axis's rule. The widget is marked rather than its parent
/// because the parent is not known here; `redraw` escalates a changed
/// declared length to whoever resolves it.
pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) {
let id = id.id();
let data = self.data_mut(id).unwrap();
if *data.size.axis_mut(axis) == rule {
return;
}
*data.size.axis_mut(axis) = rule;
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,
id: impl IdLike,
x: impl Into<SizeRule>,
y: impl Into<SizeRule>,
) {
let id = id.id();
self.set_size_rule(id, Axis::X, x.into());
self.set_size_rule(id, Axis::Y, y.into());
}
pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> { pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
self.vec.get_mut(id.id()) self.vec.get_mut(id.id())
} }
+1 -1
View File
@@ -10,7 +10,7 @@ struct State {
} }
impl DefaultAppState for State { impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self { fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
rect(Color::RED).set_root(rsc, &mut ui_state); rect(Color::RED).set_root(rsc, &mut ui_state);
Self { ui_state } Self { ui_state }
} }
-31
View File
@@ -1,31 +0,0 @@
//! The seeded random tree `tests/generated.rs` checks, drawn so it can be
//! looked at. `IRIS_SEED` and `IRIS_DEPTH` choose which one.
use iris::prelude::*;
use iris::random::Edits;
fn env(name: &str, fallback: u64) -> u64 {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(fallback)
}
fn main() {
DefaultApp::<State>::run();
}
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
}
impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
let seed = env("IRIS_SEED", 1);
let depth = env("IRIS_DEPTH", 4) as usize;
let (root, _) = iris::random::grow(rsc, seed, depth, &Edits::default());
ui_state.set_root(root);
Self { ui_state }
}
}
+10 -13
View File
@@ -1,3 +1,4 @@
use cosmic_text::Family;
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use winit::event::WindowEvent; use winit::event::WindowEvent;
@@ -15,31 +16,27 @@ pub struct Client {
} }
impl DefaultAppState for Client { impl DefaultAppState for Client {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self { fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
let rrect = rect(Color::WHITE).radius(20); let rrect = rect(Color::WHITE).radius(20);
let pad_test = ( let pad_test = (
rrect.color(Color::BLUE), rrect.color(Color::BLUE),
( (
// The square is one widget and the two shares of the row it
// sits centred in are another: a length is a property of a
// widget, so `.width` here would overwrite the `.sized`.
rrect rrect
.color(Color::RED) .color(Color::RED)
.sized((100, 100)) .sized((100, 100))
.center() .center()
.wrapper() .width(rest(2)),
.width(leftover(2)),
( (
rrect.color(Color::ORANGE), rrect.color(Color::ORANGE),
rrect.color(Color::LIME).pad(10.0), rrect.color(Color::LIME).pad(10.0),
) )
.span(Dir::RIGHT) .span(Dir::RIGHT)
.width(leftover(2)), .width(rest(2)),
rrect.color(Color::YELLOW), rrect.color(Color::YELLOW),
) )
.span(Dir::RIGHT) .span(Dir::RIGHT)
.pad(10) .pad(10)
.width(leftover(3)), .width(rest(3)),
) )
.span(Dir::RIGHT) .span(Dir::RIGHT)
.add(rsc); .add(rsc);
@@ -125,11 +122,11 @@ impl DefaultAppState for Client {
.add(rsc); .add(rsc);
let text_edit_scroll = ( let text_edit_scroll = (
msg_area.height(leftover(1)), msg_area.height(rest(1)),
( (
Rect::new(Color::WHITE.darker(0.9)), Rect::new(Color::WHITE.darker(0.9)),
( (
add_text.width(leftover(1)), add_text.width(rest(1)),
Rect::new(Color::GREEN) Rect::new(Color::GREEN)
.on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| { .on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| {
rsc.run_event::<Submit>(add_text, (), ctx.state); rsc.run_event::<Submit>(add_text, (), ctx.state);
@@ -147,7 +144,7 @@ impl DefaultAppState for Client {
.span(Dir::DOWN) .span(Dir::DOWN)
.add(rsc); .add(rsc);
let main = Wrapper::new().add(rsc); let main = WidgetPtr::empty().add(rsc);
let vals = Rc::new(RefCell::new((0, Vec::new()))); let vals = Rc::new(RefCell::new((0, Vec::new())));
let mut switch_button = |color, to: WeakWidget, label| { let mut switch_button = |color, to: WeakWidget, label| {
@@ -212,10 +209,10 @@ impl DefaultAppState for Client {
render: &mut UiRenderState, render: &mut UiRenderState,
) { ) {
let new = format!( let new = format!(
"widgets: {}\nactive: {}\ntextures: {}", "widgets: {}\nactive: {}\nviews: {}",
rsc.widgets().len(), rsc.widgets().len(),
render.active_widgets(), render.active_widgets(),
rsc.ui().textures.count(), self.ui_state.renderer.ui.view_count(),
); );
if new != *rsc.widgets()[self.info].content { if new != *rsc.widgets()[self.info].content {
*rsc.widgets_mut()[self.info].content = new; *rsc.widgets_mut()[self.info].content = new;
+1 -1
View File
@@ -11,7 +11,7 @@ struct State {
} }
impl DefaultAppState for State { impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self { fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
let rect = rect(Color::RED).add(rsc); let rect = rect(Color::RED).add(rsc);
rect.task_on(CursorSense::click(), async move |mut ctx| { rect.task_on(CursorSense::click(), async move |mut ctx| {
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
-67
View File
@@ -1,67 +0,0 @@
//! 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());
// Each one takes the whole width, because `text_align` puts the
// glyphs somewhere in the box the text is given and a text that
// reports the width of its own glyphs is given exactly that.
let label = |text: &str, align| wtext(text).size(24).text_align(align).width(rel(1.0));
let aligned = (
label("left", Align::LEFT),
label("centred", Align::H_CENTER),
label("right", 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 }
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ impl Test {
} }
impl DefaultAppState for State { impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self { fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
let test = Test::new(rsc); let test = Test::new(rsc);
test.on(CursorSense::click(), move |_, rsc| { test.on(CursorSense::click(), move |_, rsc| {
+1 -1
View File
@@ -6,7 +6,7 @@ edition.workspace = true
[dependencies] [dependencies]
proc-macro2 = "1.0.103" proc-macro2 = "1.0.103"
quote = "1.0.42" quote = "1.0.42"
syn = { version = "2.0.111", features = ["full"] } syn = { version = "3.0.3", features = ["full"] }
[lib] [lib]
proc-macro = true proc-macro = true
-16
View File
@@ -1,16 +0,0 @@
[package]
name = "rig-input"
version.workspace = true
edition.workspace = true
# Replays `.touch` recordings through Wayland's virtual-pointer protocol;
# headless sway has no input devices for coordinate-driving tools to move.
[[bin]]
name = "replay-touch"
path = "src/main.rs"
[dependencies]
# Share the harness parser so both ways of replaying read a file the same.
iris = { path = ".." }
wayland-client = { workspace = true }
wayland-protocols-wlr = { workspace = true }
-141
View File
@@ -1,141 +0,0 @@
use iris::harness::{TouchAction, TouchScript};
use std::time::Duration;
use wayland_client::protocol::wl_pointer::ButtonState;
use wayland_client::protocol::{wl_registry, wl_seat};
use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
};
const BTN_LEFT: u32 = 0x110;
const SETTLE: Duration = Duration::from_millis(200);
#[derive(Default)]
struct Globals {
seat: Option<wl_seat::WlSeat>,
manager: Option<ZwlrVirtualPointerManagerV1>,
}
impl Dispatch<wl_registry::WlRegistry, ()> for Globals {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
let wl_registry::Event::Global {
name,
interface,
version,
} = event
else {
return;
};
match interface.as_str() {
"wl_seat" => {
state.seat = Some(registry.bind(name, version.min(7), qh, ()));
}
"zwlr_virtual_pointer_manager_v1" => {
state.manager = Some(registry.bind(name, version.min(2), qh, ()));
}
_ => {}
}
}
}
delegate_noop!(Globals: ignore wl_seat::WlSeat);
delegate_noop!(Globals: ZwlrVirtualPointerManagerV1);
delegate_noop!(Globals: ZwlrVirtualPointerV1);
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let [width, height, path] = args.as_slice() else {
eprintln!("usage: replay-touch WIDTH HEIGHT FILE");
std::process::exit(2);
};
let (width, height) = (parse(width, "WIDTH"), parse(height, "HEIGHT"));
let text = std::fs::read_to_string(path)
.unwrap_or_else(|e| fail(&format!("could not read {path}: {e}")));
let script = TouchScript::parse(&text).unwrap_or_else(|e| fail(&e));
let conn = Connection::connect_to_env().unwrap_or_else(|e| {
fail(&format!(
"no wayland display ({e}); is WAYLAND_DISPLAY set?"
))
});
let mut queue = conn.new_event_queue();
let qh = queue.handle();
let display = conn.display();
display.get_registry(&qh, ());
let mut globals = Globals::default();
queue
.roundtrip(&mut globals)
.unwrap_or_else(|e| fail(&format!("wayland roundtrip failed: {e}")));
let manager = globals.manager.as_ref().unwrap_or_else(|| {
fail(
"this compositor does not offer zwlr_virtual_pointer_manager_v1, so a pointer cannot \
be synthesised; sway and every wlroots compositor do",
)
});
let pointer = manager.create_virtual_pointer(globals.seat.as_ref(), &qh, ());
// Put the pointer where the gesture starts and let the compositor
// settle before anything is pressed. Without this the press is
// dropped: sway has just learned about this pointer, and a button
// sent in the same breath as the motion that first puts it over a
// window arrives before there is a focused surface to send it to --
// winit sees `CursorEntered`, the moves and the *release*, never the
// press, so the gesture reads as a hover and nothing scrolls. Found
// by printing winit's own events; the settle is what fixed it.
if let Some(first) = script.samples.first() {
pointer.motion_absolute(0, first.pos.x as u32, first.pos.y as u32, width, height);
pointer.frame();
conn.flush()
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
std::thread::sleep(SETTLE);
}
let mut previous = 0;
for sample in &script.samples {
std::thread::sleep(Duration::from_millis(sample.t_ms - previous));
previous = sample.t_ms;
let t = sample.t_ms as u32;
pointer.motion_absolute(t, sample.pos.x as u32, sample.pos.y as u32, width, height);
pointer.frame();
// The button goes in a frame of its own, *after* the motion has
// been committed. Sent in the same frame as the motion that
// first puts the pointer over the window, sway drops it: the
// client sees `CursorEntered` and the moves but never a
// `MouseInput { state: Pressed }`, so the whole gesture reads as
// a hover and nothing scrolls. Found exactly that way, by
// printing winit's events.
let state = match sample.action {
TouchAction::Down => Some(ButtonState::Pressed),
TouchAction::Up => Some(ButtonState::Released),
TouchAction::Move => None,
};
if let Some(state) = state {
pointer.button(t, BTN_LEFT, state);
pointer.frame();
}
conn.flush()
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
}
pointer.destroy();
conn.flush().ok();
}
fn parse(text: &str, what: &str) -> u32 {
text.parse()
.unwrap_or_else(|_| fail(&format!("{what} is not a whole number: {text:?}")))
}
fn fail(message: &str) -> ! {
eprintln!("replay-touch: {message}");
std::process::exit(1);
}
-3
View File
@@ -1,3 +0,0 @@
[toolchain]
channel = "nightly"
components = ["clippy", "rustfmt"]
-14
View File
@@ -1,14 +0,0 @@
# The compositor `scripts/run-headless.sh` starts, so that an example has a
# surface where there is no display. Nothing here is meant to be looked at
# directly; `grim` is.
#
# No Xwayland: winit talks Wayland natively, so an X server is a second thing
# to go wrong for no gain.
xwayland disable
# The default output, overridden per run by `--mode`. Larger than the window
# an example opens, so nothing is scaled or clipped.
output HEADLESS-1 mode 1920x1200@60Hz
default_border none
focus_follows_mouse no
-182
View File
@@ -1,182 +0,0 @@
#!/bin/sh
# Run an iris example on a machine with no display.
#
# ./scripts/run-headless.sh tabs
# ./scripts/run-headless.sh tabs --shot /tmp/tabs.png --seconds 4
# ./scripts/run-headless.sh tabs --replay taps.touch --shot /tmp/tabs.png
# ./scripts/run-headless.sh app --dir ../elsewhere --mode 1080x2424@120Hz
#
# `--dir DIR` names the workspace to build in, defaulting to this one, so a
# project that depends on iris can be run through the same rig. `--bin` runs a
# crate binary rather than an example, and takes its own argv from
# `$RUN_HEADLESS_ARGS`, word-split on purpose.
#
# `--mode` sets the output, for running something at a size other than a
# desktop's -- a phone's, say. Set every run rather than only when it changes:
# the compositor is reused between runs, so a default-shaped run after a
# custom one would otherwise inherit the other's output and quietly screenshot
# the wrong size.
#
# `--resize WxH@Hz` changes the output under the app once it is up, then
# screenshots. A resize is its own case: what it has to match is a cold start
# at that size, byte for byte, and nothing in `cargo test` can see it.
#
# `--replay FILE` drives a `.touch` recording into the window through
# `replay-touch`, which reads it with the same parser `iris::harness` uses. A
# recording is `<ms> down|move|up <x> <y>` in the output's own pixels. With
# `--shot` it also writes `<shot>-before.png` from just before the gesture,
# since "it moved" is a claim about two pictures.
#
# What it supplies is a compositor for winit to open a surface on: a headless
# sway, and `grim` to screenshot it. Sway gets its own socket and runtime
# directory rather than joining whatever else is running, because it tiles --
# adding a window to someone else's compositor resizes theirs.
set -eu
need() {
command -v "$1" >/dev/null 2>&1 || {
echo "run-headless: $1 is not installed ($2)" >&2
exit 127
}
}
need sway "the compositor an example opens its window on"
need swaymsg "sway's control socket"
scripts=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$scripts/.." && pwd)
workdir="$root"
cd "$root"
run="${XDG_RUNTIME_DIR:-/tmp}/iris-headless"
seconds=3
shot=""
replay=""
resize=""
example=""
kind=example
mode=1920x1200@60Hz
while [ $# -gt 0 ]; do
case "$1" in
--shot) shot=$2; shift 2 ;;
--seconds) seconds=$2; shift 2 ;;
--bin) kind=bin; shift ;;
--mode) mode=$2; shift 2 ;;
--resize) resize=$2; shift 2 ;;
--replay) replay=$2; shift 2 ;;
--dir) workdir=$(cd "$2" && pwd); shift 2 ;;
--) shift; break ;;
*) example=$1; shift ;;
esac
done
[ -n "$example" ] || { echo "usage: $0 NAME [--bin] [--dir DIR] [--mode WxH@Hz] [--resize WxH@Hz] [--replay TOUCH] [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; }
[ -z "$replay" ] || [ -f "$replay" ] || { echo "run-headless: no touch script at $replay" >&2; exit 2; }
[ -z "$shot" ] || need grim "the screenshot --shot writes"
mkdir -p "$run"
export SWAYSOCK="$run/sway.sock"
# Named rather than left to sway's pid-based default, so a second run reuses
# this compositor instead of starting another beside it.
if ! swaymsg -t get_version >/dev/null 2>&1; then
rm -f "$SWAYSOCK"
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 LIBSEAT_BACKEND=noop \
setsid sway -c "$scripts/headless.conf" >"$run/sway.log" 2>&1 &
i=0
while [ $i -lt 20 ]; do
swaymsg -t get_version >/dev/null 2>&1 && break
i=$((i + 1)); sleep 0.5
done
swaymsg -t get_version >/dev/null 2>&1 || {
echo "run-headless: compositor did not start; see $run/sway.log" >&2
exit 1
}
fi
rm -f "$run/display"
swaymsg exec -- "sh -c 'printf %s \"\$WAYLAND_DISPLAY\" > $run/display'" >/dev/null
i=0
while [ $i -lt 20 ]; do
[ -s "$run/display" ] && break
i=$((i + 1)); sleep 0.5
done
[ -s "$run/display" ] || { echo "run-headless: could not read WAYLAND_DISPLAY" >&2; exit 1; }
WAYLAND_DISPLAY=$(cat "$run/display")
export WAYLAND_DISPLAY
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
swaymsg output HEADLESS-1 mode "$mode" >/dev/null
# The extent `replay-touch` positions against, so a script's coordinates
# are the output's own pixels.
out_w=${mode%x*}
out_h=${mode#*x}; out_h=${out_h%@*}
# Built before the app starts, so a compile error is not reported as a
# window that failed to move.
[ -z "$replay" ] || (cd "$root" && cargo build --bin replay-touch -p rig-input) >&2
cd "$workdir"
if [ "$kind" = bin ]; then
cargo build --bin "$example" "$@" >&2
bin="$workdir/target/debug/$example"
else
cargo build --example "$example" "$@" >&2
bin="$workdir/target/debug/examples/$example"
fi
# Deliberately word-split: this is the binary's own argv, not a single path.
# shellcheck disable=SC2086
"$bin" ${RUN_HEADLESS_ARGS:-} >"$run/$example.log" 2>&1 &
pid=$!
trap 'kill "$pid" 2>/dev/null || true' EXIT INT TERM
# Wait for the window to be mapped rather than for a number of seconds. A
# fixed sleep took an all-black screenshot the first time this ran, when sway
# had started in the same invocation and had not composited its output yet --
# which is indistinguishable from an app that draws nothing.
i=0
while [ $i -lt 40 ]; do
kill -0 "$pid" 2>/dev/null || break
swaymsg -t get_tree --raw 2>/dev/null | grep -q "\"pid\":$pid," && break
i=$((i + 1)); sleep 0.25
done
i=0
while [ $i -lt "$((seconds * 2))" ]; do
kill -0 "$pid" 2>/dev/null || break
i=$((i + 1)); sleep 0.5
done
if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then
swaymsg output HEADLESS-1 mode "$resize" >/dev/null
echo "run-headless: resized to $resize" >&2
sleep 2
fi
if [ -n "$replay" ] && kill -0 "$pid" 2>/dev/null; then
if [ -n "$shot" ]; then
grim "${shot%.png}-before.png"
echo "run-headless: wrote ${shot%.png}-before.png (before the gesture)" >&2
fi
"$root/target/debug/replay-touch" "$out_w" "$out_h" "$replay"
# A fling outlives the finger: the gesture's own last sample is not
# when the list stops. Long enough for Android's spline to settle
# (`FlingCalculator::duration` tops out around a second and a half).
sleep 2
fi
if kill -0 "$pid" 2>/dev/null; then
[ -n "$shot" ] && grim "$shot" && echo "run-headless: wrote $shot" >&2
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
status=0
else
wait "$pid" 2>/dev/null || status=$?
echo "run-headless: $example exited early (status ${status:-0})" >&2
status=${status:-1}
fi
echo "--- $example output ---" >&2
cat "$run/$example.log" >&2
exit "$status"
+4 -6
View File
@@ -15,10 +15,8 @@ where
let region = ctx.data.render.window_region(&id).unwrap(); let region = ctx.data.render.window_region(&id).unwrap();
let id_pos = region.top_left; let id_pos = region.top_left;
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left; let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
// The pointer arrives from the platform in floats; everything let pos = ctx.data.pos + container_pos - id_pos;
// it is compared against is on the grid. let size = region.size();
let pos = (PxVec2::from_f32(ctx.data.pos) + container_pos - id_pos).to_f32();
let size = region.size().to_f32();
select( select(
rsc, rsc,
ctx.data.render, ctx.data.render,
@@ -72,8 +70,8 @@ fn select(
if let Some(region) = render.window_region(&id) { if let Some(region) = render.window_region(&id) {
state.window.set_ime_allowed(true); state.window.set_ime_allowed(true);
state.window.set_ime_cursor_area( state.window.set_ime_cursor_area(
LogicalPosition::<f32>::from(region.top_left.to_f32().tuple()), LogicalPosition::<f32>::from(region.top_left.tuple()),
LogicalSize::<f32>::from(region.size().to_f32().tuple()), LogicalSize::<f32>::from(region.size().tuple()),
); );
} }
state.focus = Some(id); state.focus = Some(id);
+8
View File
@@ -7,3 +7,11 @@ impl Event for Submit {}
#[derive(Eq, PartialEq, Hash, Clone)] #[derive(Eq, PartialEq, Hash, Clone)]
pub struct Edited; pub struct Edited;
impl Event for Edited {} impl Event for Edited {}
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Draw;
impl Event for Draw {}
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Undraw;
impl Event for Undraw {}
+154 -67
View File
@@ -1,6 +1,10 @@
use crate::prelude::*; use crate::prelude::*;
use arboard::Clipboard; use arboard::Clipboard;
use std::{marker::PhantomData, sync::Arc, time::Instant}; use std::{
marker::{PhantomData, Sized},
sync::Arc,
time::Instant,
};
use winit::{ use winit::{
event::{Ime, WindowEvent}, event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy}, event_loop::{ActiveEventLoop, EventLoopProxy},
@@ -25,33 +29,24 @@ pub use sense::*;
pub use state::*; pub use state::*;
pub use task::*; pub use task::*;
/// Sends an application's own events to its event loop. It wraps the proxy pub struct EventSender<State: DefaultAppState> {
/// rather than being one because task updates travel the same way: what an proxy: EventLoopProxy<UiMainEvent<State>>,
/// application sends is its `Event`, not the loop's whole message type. }
pub struct Proxy<State: DefaultAppState>(EventLoopProxy<DefaultEvent<State>>);
impl<State: DefaultAppState> Clone for Proxy<State> { impl<State: DefaultAppState> Clone for EventSender<State> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self(self.0.clone()) Self {
proxy: self.proxy.clone(),
}
} }
} }
impl<State: DefaultAppState> Proxy<State> { impl<State: DefaultAppState> EventSender<State> {
pub fn send_event(&self, event: State::Event) { pub fn send(&self, event: State::Event) {
let _ = self.0.send_event(DefaultEvent::User(event)); let _ = self.proxy.send_event(UiMainEvent::App(event));
} }
} pub fn run(&self, f: impl MainCallback<State>) {
let _ = self.proxy.send_event(UiMainEvent::Callback(Box::new(f)));
/// What the event loop carries: the application's own events, and the
/// updates tasks send back to the ui thread.
pub enum DefaultEvent<State: DefaultAppState> {
User(State::Event),
Update(Box<dyn TaskUpdate<DefaultRsc<State>>>),
}
impl<State: DefaultAppState> TaskQueue<DefaultRsc<State>> for Proxy<State> {
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<State>>>) {
let _ = self.0.send_event(DefaultEvent::Update(update));
} }
} }
@@ -95,7 +90,7 @@ pub trait HasDefaultUiState: Sized + 'static {
pub trait DefaultAppState: HasDefaultUiState { pub trait DefaultAppState: HasDefaultUiState {
type Event: Send = (); type Event: Send = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self>) -> Self; fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self;
#[allow(unused_variables)] #[allow(unused_variables)]
fn event( fn event(
&mut self, &mut self,
@@ -119,23 +114,58 @@ pub trait DefaultAppState: HasDefaultUiState {
} }
} }
pub struct DefaultRsc<State: 'static> { pub struct DefaultRsc<State: 'static + DefaultAppState> {
pub ui: UiData, pub ui: UiData,
pub events: EventManager<Self>, pub events: EventManager<Self>,
pub tasks: Tasks<Self>, pub tasks: Tasks<Self>,
pub state: WidgetState, pub state: WidgetState,
pub widget_events: Vec<WidgetEvent>,
pub window_event: EventSender<State>,
_state: PhantomData<State>, _state: PhantomData<State>,
} }
impl<State> DefaultRsc<State> { pub struct WidgetEvent {
pub fn init(queue: Arc<dyn TaskQueue<Self>>) -> Self { id: WidgetId,
Self { ty: WidgetEventType,
ui: Default::default(), }
events: Default::default(),
tasks: Tasks::init(queue), pub enum WidgetEventType {
state: Default::default(), Draw,
_state: Default::default(), Undraw,
} Remove,
}
pub trait MainCallback<State>: FnOnce(&mut DefaultRsc<State>) + Sync + Send + 'static {}
impl<F: FnOnce(&mut DefaultRsc<State>) + Sync + Send + 'static, State> MainCallback<State> for F {}
pub enum UiMainEvent<State: DefaultAppState> {
RequestUpdate,
Callback(Box<dyn MainCallback<State>>),
App(State::Event),
}
impl<State: DefaultAppState> DefaultRsc<State> {
fn init(proxy: EventLoopProxy<UiMainEvent<State>>) -> (Self, TaskMsgReceiver<Self>) {
let window_event = EventSender {
proxy: proxy.clone(),
};
let (tasks, recv) = Tasks::init(move || {
if proxy.send_event(UiMainEvent::RequestUpdate).is_err() {
panic!("main thread blew up or smth");
}
});
(
Self {
ui: Default::default(),
events: Default::default(),
tasks,
widget_events: Default::default(),
state: Default::default(),
window_event,
_state: Default::default(),
},
recv,
)
} }
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> { pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
@@ -143,7 +173,7 @@ impl<State> DefaultRsc<State> {
} }
} }
impl<State> UiRsc for DefaultRsc<State> { impl<State: DefaultAppState> UiRsc for DefaultRsc<State> {
fn ui(&self) -> &UiData { fn ui(&self) -> &UiData {
&self.ui &self.ui
} }
@@ -152,25 +182,39 @@ impl<State> UiRsc for DefaultRsc<State> {
&mut self.ui &mut self.ui
} }
fn on_draw(&mut self, active: &ActiveData) { fn on_draw(&mut self, active: &ActiveData, redrawn: bool) {
self.events.draw(active); self.events.draw(active);
if !redrawn {
self.widget_events.push(WidgetEvent {
id: active.id,
ty: WidgetEventType::Draw,
});
}
} }
fn on_undraw(&mut self, active: &ActiveData) { fn on_undraw(&mut self, active: &ActiveData) {
self.events.undraw(active); self.events.undraw(active);
self.widget_events.push(WidgetEvent {
id: active.id,
ty: WidgetEventType::Undraw,
});
} }
fn on_remove(&mut self, id: WidgetId) { fn on_remove(&mut self, id: WidgetId) {
self.events.remove(id); self.events.remove(id);
self.state.remove(id); self.state.remove(id);
self.widget_events.push(WidgetEvent {
id,
ty: WidgetEventType::Remove,
});
} }
} }
impl<State: 'static> HasState for DefaultRsc<State> { impl<State: 'static + DefaultAppState> HasState for DefaultRsc<State> {
type State = State; type State = State;
} }
impl<State: 'static> HasEvents for DefaultRsc<State> { impl<State: 'static + DefaultAppState> HasEvents for DefaultRsc<State> {
fn events(&self) -> &EventManager<Self> { fn events(&self) -> &EventManager<Self> {
&self.events &self.events
} }
@@ -180,13 +224,13 @@ impl<State: 'static> HasEvents for DefaultRsc<State> {
} }
} }
impl<State: 'static> HasTasks for DefaultRsc<State> { impl<State: 'static + DefaultAppState> HasTasks for DefaultRsc<State> {
fn tasks_mut(&mut self) -> &mut Tasks<Self> { fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks &mut self.tasks
} }
} }
impl<State: 'static> HasWidgetState for DefaultRsc<State> { impl<State: 'static + DefaultAppState> HasWidgetState for DefaultRsc<State> {
fn widget_state(&self) -> &WidgetState { fn widget_state(&self) -> &WidgetState {
&self.state &self.state
} }
@@ -200,49 +244,62 @@ pub struct DefaultApp<State: DefaultAppState> {
rsc: DefaultRsc<State>, rsc: DefaultRsc<State>,
render: UiRenderState, render: UiRenderState,
state: State, state: State,
task_recv: TaskMsgReceiver<DefaultRsc<State>>,
} }
impl<State: DefaultAppState> AppState for DefaultApp<State> { impl<State: DefaultAppState> AppState for DefaultApp<State> {
type Event = DefaultEvent<State>; type Event = UiMainEvent<State>;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self { fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
let window = event_loop let window = event_loop
.create_window(State::window_attributes()) .create_window(State::window_attributes())
.unwrap(); .unwrap();
let default_state = DefaultUiState::new(window); let default_state = DefaultUiState::new(window);
let mut rsc = DefaultRsc::init(Arc::new(Proxy(proxy.clone()))); let (mut rsc, task_recv) = DefaultRsc::init(proxy);
let state = State::new(default_state, &mut rsc, Proxy(proxy)); let state = State::new(default_state, &mut rsc);
let render = UiRenderState::new(); let render = UiRenderState::new();
Self { rsc, state, render } Self {
rsc,
state,
render,
task_recv,
}
} }
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) { fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
match event { match event {
DefaultEvent::User(event) => self.state.event(event, &mut self.rsc, &mut self.render), UiMainEvent::RequestUpdate => {
DefaultEvent::Update(update) => update(&mut self.state, &mut self.rsc), self.check_updates();
}
UiMainEvent::App(event) => {
self.state.event(event, &mut self.rsc, &mut self.render);
}
UiMainEvent::Callback(f) => f(&mut self.rsc),
} }
self.request_redraw_if_needed();
} }
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) { fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let Self { rsc, render, state } = self; let Self {
rsc, render, state, ..
} = self;
// input handling
let ui_state = state.default_state_mut(); let ui_state = state.default_state_mut();
let input_changed = ui_state.input.event(&event); if ui_state.input.event(&event) {
let cursor_state = ui_state.cursor_state().clone(); let cursor_state = ui_state.cursor_state().clone();
let old = ui_state.focus; let old = ui_state.focus;
if cursor_state.buttons.left.is_start() { if cursor_state.buttons.left.is_start() {
ui_state.focus = None; ui_state.focus = None;
} }
if input_changed { let window_size = ui_state.window_size();
render.run_sensors(rsc, state, cursor_state); render.run_sensors(rsc, state, cursor_state, window_size);
if old != state.default_state().focus
&& let Some(old) = old
{
old.edit(rsc).deselect();
}
} }
let ui_state = state.default_state_mut(); let ui_state = state.default_state_mut();
if old != ui_state.focus
&& let Some(old) = old
{
old.edit(rsc).deselect();
}
match &event { match &event {
WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
@@ -251,7 +308,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.renderer.draw(); ui_state.renderer.draw();
} }
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
render.resize((size.width, size.height), rsc.widgets_mut()); render.resize((size.width, size.height));
ui_state.renderer.resize(size) ui_state.renderer.resize(size)
} }
WindowEvent::KeyboardInput { event, .. } => { WindowEvent::KeyboardInput { event, .. } => {
@@ -304,7 +361,8 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
_ => (), _ => (),
} }
state.window_event(event, rsc, render); state.window_event(event, rsc, render);
self.request_redraw_if_needed();
self.check_updates();
self.state.default_state_mut().input.end_frame(); self.state.default_state_mut().input.end_frame();
} }
@@ -314,9 +372,34 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
} }
impl<State: DefaultAppState> DefaultApp<State> { impl<State: DefaultAppState> DefaultApp<State> {
fn request_redraw_if_needed(&mut self) { pub fn check_updates(&mut self) {
let ui_state = self.state.default_state_mut(); let Self {
if self.render.needs_redraw(&ui_state.root, self.rsc.widgets()) { rsc,
render,
state,
task_recv,
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
let mut events = std::mem::take(&mut rsc.widget_events);
for event in events.drain(..) {
match event.ty {
WidgetEventType::Draw => {
rsc.run_event::<Draw>(event.id, (), state);
}
WidgetEventType::Undraw => {
rsc.run_event::<Undraw>(event.id, (), state);
}
_ => (),
}
}
rsc.widget_events = events;
let ui_state = state.default_state();
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
ui_state.renderer.window().request_redraw(); ui_state.renderer.window().request_redraw();
} }
} }
@@ -328,7 +411,9 @@ pub trait RscIdx<Rsc> {
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output; fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output;
} }
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for DefaultRsc<State> { impl<State: 'static + DefaultAppState, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I>
for DefaultRsc<State>
{
type Output = I::Output; type Output = I::Output;
fn index(&self, index: I) -> &Self::Output { fn index(&self, index: I) -> &Self::Output {
@@ -336,7 +421,9 @@ impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for Defaul
} }
} }
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::IndexMut<I> for DefaultRsc<State> { impl<State: 'static + DefaultAppState, I: RscIdx<DefaultRsc<State>>> std::ops::IndexMut<I>
for DefaultRsc<State>
{
fn index_mut(&mut self, index: I) -> &mut Self::Output { fn index_mut(&mut self, index: I) -> &mut Self::Output {
index.get_mut(self) index.get_mut(self)
} }
+21 -20
View File
@@ -1,4 +1,4 @@
use iris_core::{UiData, UiRenderNode, UiRenderState}; use iris_core::{UiData, UiLimits, UiRenderNode, UiRenderState};
use pollster::FutureExt; use pollster::FutureExt;
use std::sync::Arc; use std::sync::Arc;
use wgpu::*; use wgpu::*;
@@ -23,18 +23,9 @@ impl UiRenderer {
pub fn draw(&mut self) { pub fn draw(&mut self) {
let output = match self.surface.get_current_texture() { let output = match self.surface.get_current_texture() {
CurrentSurfaceTexture::Success(texture) => texture, CurrentSurfaceTexture::Success(v) => v,
CurrentSurfaceTexture::Suboptimal(texture) => { CurrentSurfaceTexture::Suboptimal(v) => v,
self.surface.configure(&self.device, &self.config); _ => panic!("failed"),
texture
}
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
self.surface.configure(&self.device, &self.config);
return;
}
CurrentSurfaceTexture::Timeout
| CurrentSurfaceTexture::Occluded
| CurrentSurfaceTexture::Validation => return,
}; };
let view = output let view = output
.texture .texture
@@ -58,7 +49,6 @@ impl UiRenderer {
} }
self.queue.submit(std::iter::once(encoder.finish())); self.queue.submit(std::iter::once(encoder.finish()));
self.window.pre_present_notify();
self.queue.present(output); self.queue.present(output);
} }
@@ -66,7 +56,7 @@ impl UiRenderer {
self.config.width = size.width; self.config.width = size.width;
self.config.height = size.height; self.config.height = size.height;
self.surface.configure(&self.device, &self.config); self.surface.configure(&self.device, &self.config);
self.ui.resize((size.width, size.height), &self.queue); self.ui.resize(size, &self.queue);
} }
fn create_encoder(device: &Device) -> CommandEncoder { fn create_encoder(device: &Device) -> CommandEncoder {
@@ -80,8 +70,10 @@ impl UiRenderer {
let instance = Instance::new(InstanceDescriptor { let instance = Instance::new(InstanceDescriptor {
backends: Backends::PRIMARY, backends: Backends::PRIMARY,
display: Some(Box::new(window.clone())), flags: Default::default(),
..InstanceDescriptor::new_without_display_handle() memory_budget_thresholds: Default::default(),
backend_options: Default::default(),
display: None,
}); });
let surface = instance let surface = instance
@@ -98,9 +90,18 @@ impl UiRenderer {
.block_on() .block_on()
.expect("Could not get adapter!"); .expect("Could not get adapter!");
let ui_limits = UiLimits::default();
let (device, queue) = adapter let (device, queue) = adapter
.request_device(&DeviceDescriptor { .request_device(&DeviceDescriptor {
required_features: Features::TEXTURE_BINDING_ARRAY
| Features::PARTIALLY_BOUND_BINDING_ARRAY
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
required_limits: Limits { required_limits: Limits {
max_binding_array_elements_per_shader_stage: ui_limits
.max_binding_array_elements_per_shader_stage(),
max_binding_array_sampler_elements_per_shader_stage: ui_limits
.max_binding_array_sampler_elements_per_shader_stage(),
max_buffer_size: 1 << 30, max_buffer_size: 1 << 30,
..Default::default() ..Default::default()
}, },
@@ -120,20 +121,20 @@ impl UiRenderer {
let config = SurfaceConfiguration { let config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT, usage: TextureUsages::RENDER_ATTACHMENT,
format: surface_format, format: surface_format,
color_space: SurfaceColorSpace::Auto,
width: size.width, width: size.width,
height: size.height, height: size.height,
present_mode: PresentMode::AutoVsync, present_mode: PresentMode::AutoNoVsync,
alpha_mode: surface_caps.alpha_modes[0], alpha_mode: surface_caps.alpha_modes[0],
desired_maximum_frame_latency: 2, desired_maximum_frame_latency: 2,
view_formats: vec![], view_formats: vec![],
color_space: Default::default(),
}; };
surface.configure(&device, &config); surface.configure(&device, &config);
let encoder = Self::create_encoder(&device); let encoder = Self::create_encoder(&device);
let ui = UiRenderNode::new(&device, &config); let ui = UiRenderNode::new(&device, &queue, &config, ui_limits);
Self { Self {
surface, surface,
+35 -103
View File
@@ -4,14 +4,14 @@ use std::{
rc::Rc, rc::Rc,
}; };
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
pub enum CursorButton { pub enum CursorButton {
Left, Left,
Right, Right,
Middle, Middle,
} }
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
pub enum CursorSense { pub enum CursorSense {
PressStart(CursorButton), PressStart(CursorButton),
Pressing(CursorButton), Pressing(CursorButton),
@@ -27,7 +27,7 @@ pub struct CursorSenses(Vec<CursorSense>);
impl Event for CursorSenses { impl Event for CursorSenses {
type Data<'a> = CursorData<'a>; type Data<'a> = CursorData<'a>;
type Global = Hovered; type State = SensorState;
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> { fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
if let Some(sense) = should_run(self, &data.cursor, data.hover) { if let Some(sense) = should_run(self, &data.cursor, data.hover) {
let mut data = data.clone(); let mut data = data.clone();
@@ -37,24 +37,6 @@ impl Event for CursorSenses {
None None
} }
} }
/// A press or a scroll is used up by whatever answered it, so it stops
/// there. Hovering is not: a cursor resting somewhere goes on resting.
fn consumes(&self, data: &Self::Data<'_>) -> bool {
!data.sense.position_only()
}
}
/// Who the cursor was inside, before and after an input. The difference is
/// whose hover has ended -- including a widget a higher layer has covered,
/// which the walk stops before reaching.
///
/// Two buffers that swap rather than one rebuilt, so an input allocates
/// nothing once they have grown.
#[derive(Default)]
pub struct Hovered {
was: Vec<WidgetId>,
now: Vec<WidgetId>,
} }
impl CursorSense { impl CursorSense {
@@ -70,12 +52,6 @@ impl CursorSense {
pub fn is_dragging(&self) -> bool { pub fn is_dragging(&self) -> bool {
matches!(self, CursorSense::Pressing(CursorButton::Left)) matches!(self, CursorSense::Pressing(CursorButton::Left))
} }
/// False if the sense is a button or a scroll, true if it is only about
/// where the cursor is.
fn position_only(&self) -> bool {
matches!(self, Self::HoverStart | Self::Hovering | Self::HoverEnd)
}
} }
#[derive(Default, Clone)] #[derive(Default, Clone)]
@@ -120,12 +96,6 @@ impl CursorButtons {
} }
impl CursorState { impl CursorState {
/// True if the cursor is only reporting where it is: no button and no
/// scroll this frame.
pub fn position_only(&self) -> bool {
self.scroll_delta == Vec2::ZERO && self.buttons.iter().all(|(_, state)| state.is_off())
}
pub fn end_frame(&mut self) { pub fn end_frame(&mut self) {
self.buttons.end_frame(); self.buttons.end_frame();
self.scroll_delta = Vec2::ZERO; self.scroll_delta = Vec2::ZERO;
@@ -153,6 +123,11 @@ pub struct Sensor<Ctx: HasEvents, Data> {
pub type SenseShape = UiRegion; pub type SenseShape = UiRegion;
#[derive(Default, Debug)]
pub struct SensorState {
pub hover: ActivationState,
}
#[derive(Clone)] #[derive(Clone)]
pub struct CursorData<'a> { pub struct CursorData<'a> {
/// where this widget was hit /// where this widget was hit
@@ -172,6 +147,7 @@ pub trait SensorUi {
rsc: &mut Rsc, rsc: &mut Rsc,
state: &mut Rsc::State, state: &mut Rsc::State,
cursor: CursorState, cursor: CursorState,
window_size: Vec2,
); );
} }
@@ -181,98 +157,54 @@ impl SensorUi for UiRenderState {
rsc: &mut Rsc, rsc: &mut Rsc,
state: &mut Rsc::State, state: &mut Rsc::State,
cursor: CursorState, cursor: CursorState,
window_size: Vec2,
) { ) {
// in order to remove this take, need to store active list in UiRenderState somehow // in order to remove this take, need to store active list in UiRenderState somehow
// this would probably be done through a generic parameter that adds yet another rsc / // this would probably be done through a generic parameter that adds yet another rsc /
// state like thing, but local to render state, and is passed to UiRsc events so you can // state like thing, but local to render state, and is passed to UiRsc events so you can
// update it there? // update it there?
let active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active); let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
let mut hovered = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().global);
hovered.now.clear();
let position_only = cursor.position_only();
let region_of = |id| self.window_region(&id);
for layer in self.layers.indices().rev() { for layer in self.layers.indices().rev() {
let mut consumed = false; let mut sensed = false;
for id in active.get(&layer).into_flat_iter().map(|(id, _)| *id) { for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
let Some(region) = region_of(id) else { let shape = self.active.get(id).unwrap().region;
continue; let region = shape.to_px(window_size);
}; let in_shape = cursor.exists && region.contains(cursor.pos);
if !cursor.exists || !region.contains(PxVec2::from_f32(cursor.pos)) { sensor.hover.update(in_shape);
if sensor.hover == ActivationState::Off {
continue; continue;
} }
hovered.now.push(id); sensed = true;
let hover = match hovered.was.contains(&id) {
true => ActivationState::On, let cursor = cursor.clone();
false => ActivationState::Start,
let data = CursorData {
pos: cursor.pos - region.top_left,
size: region.bot_right - region.top_left,
scroll_delta: cursor.scroll_delta,
hover: sensor.hover,
cursor,
// this does not have any meaning;
// might wanna set up Event to have a prepare stage
sense: CursorSense::Hovering,
render: self,
}; };
// A press or a scroll stops where something answered it, so a rsc.run_event::<CursorSense>(*id, data, state);
// button over a list does not swallow the list's scrolling.
consumed |= deliver(self, rsc, state, id, hover, &cursor, region);
// A cursor doing neither stops at whatever it is over, so
// hovering does not reach through.
consumed |= position_only;
} }
// Applied after the layer, never during it: senses on one layer do if sensed {
// not block each other.
if consumed {
break; break;
} }
} }
rsc.events_mut().get_type::<CursorSense>().active = active;
// Whatever the cursor was inside and is not now, whether it left or a
// layer above took the input before the walk reached it. A widget that
// stopped being drawn has no region to report and is simply dropped.
for &id in &hovered.was {
if !hovered.now.contains(&id)
&& let Some(region) = region_of(id)
{
deliver(self, rsc, state, id, ActivationState::End, &cursor, region);
}
}
std::mem::swap(&mut hovered.was, &mut hovered.now);
let senses = rsc.events_mut().get_type::<CursorSense>();
senses.active = active;
senses.global = hovered;
} }
} }
/// Runs one widget's cursor senses, and says whether they used up the input.
fn deliver<Rsc: HasEvents>(
render: &UiRenderState,
rsc: &mut Rsc,
state: &mut Rsc::State,
id: WidgetId,
hover: ActivationState,
cursor: &CursorState,
region: PixelRegion,
) -> bool {
let data = CursorData {
pos: cursor.pos - region.top_left.to_f32(),
size: region.size().to_f32(),
scroll_delta: cursor.scroll_delta,
hover,
cursor: cursor.clone(),
// this does not have any meaning;
// might wanna set up Event to have a prepare stage
sense: CursorSense::Hovering,
render,
};
rsc.run_event::<CursorSense>(id, data, state)
}
pub fn should_run( pub fn should_run(
senses: &CursorSenses, senses: &CursorSenses,
cursor: &CursorState, cursor: &CursorState,
hover: ActivationState, hover: ActivationState,
) -> Option<CursorSense> { ) -> Option<CursorSense> {
for sense in senses.iter() { for sense in senses.iter() {
// A widget the cursor is no longer inside senses only its position:
// the press that ended its hover landed on something else.
if !hover.is_on() && !sense.position_only() {
continue;
}
if match sense { if match sense {
CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(), CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(),
CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(), CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(),
+33 -16
View File
@@ -1,5 +1,11 @@
use iris_core::HasState; use iris_core::HasState;
use std::{pin::Pin, sync::Arc}; use std::{
pin::Pin,
sync::{
Arc,
mpsc::{Receiver as SyncReceiver, Sender as SyncSender, channel as sync_channel},
},
};
use tokio::{ use tokio::{
runtime::Runtime, runtime::Runtime,
sync::mpsc::{ sync::mpsc::{
@@ -8,51 +14,62 @@ use tokio::{
}, },
}; };
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {} pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {}
impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {} impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {}
/// Hands an update from a task to the thread that owns the ui. Delivery and
/// waking are one act: a host posts the update as a message its loop already
/// carries, so nothing has to wake the loop separately, or claim a redraw to
/// be looked at.
pub trait TaskQueue<Rsc: HasState>: Send + Sync + 'static {
fn send(&self, update: Box<dyn TaskUpdate<Rsc>>);
}
pub struct Tasks<Rsc: HasState> { pub struct Tasks<Rsc: HasState> {
start: AsyncSender<BoxTask>, start: AsyncSender<BoxTask>,
queue: Arc<dyn TaskQueue<Rsc>>, request_update: Arc<dyn Fn() + Send + Sync>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
} }
pub struct TaskCtx<Rsc: HasState> { pub struct TaskCtx<Rsc: HasState> {
queue: Arc<dyn TaskQueue<Rsc>>, send: TaskMsgSender<Rsc>,
} }
impl<Rsc: HasState> TaskCtx<Rsc> { impl<Rsc: HasState> TaskCtx<Rsc> {
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) { pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
self.queue.send(Box::new(f)); let _ = self.send.send(Box::new(f));
}
}
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
fn new(send: TaskMsgSender<Rsc>) -> Self {
Self { send }
} }
} }
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>; type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<Rsc: HasState> Tasks<Rsc> { impl<Rsc: HasState> Tasks<Rsc> {
pub fn init(queue: Arc<dyn TaskQueue<Rsc>>) -> Self { pub fn init(request_update: impl Fn() + 'static + Send + Sync) -> (Self, TaskMsgReceiver<Rsc>) {
let (start, start_recv) = async_channel(); let (start, start_recv) = async_channel();
let (msgs, msgs_recv) = sync_channel();
std::thread::spawn(|| { std::thread::spawn(|| {
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(listen(start_recv)) rt.block_on(listen(start_recv))
}); });
Self { start, queue } (
Self {
start,
msg_send: msgs,
request_update: Arc::new(request_update),
},
msgs_recv,
)
} }
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F) pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
where where
F::CallOnceFuture: Send, F::CallOnceFuture: Send,
{ {
let queue = self.queue.clone(); let send = self.msg_send.clone();
let request_update = self.request_update.clone();
let _ = self.start.send(Box::pin(async move { let _ = self.start.send(Box::pin(async move {
task(TaskCtx { queue }).await; task(TaskCtx::new(send)).await;
request_update();
})); }));
} }
} }
-333
View File
@@ -1,333 +0,0 @@
//! A ui with no window: build a tree, run frames, move a pointer, and read
//! back where widgets landed.
//!
//! It does not draw. A claim about pixels still needs a real surface.
use crate::prelude::*;
use std::{
sync::{
Arc,
mpsc::{Receiver, SyncSender, sync_channel},
},
time::Duration,
};
/// There is no loop here to post to, so updates queue until the test asks
/// for them.
struct Queue(SyncSender<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>);
impl TaskQueue<DefaultRsc<HarnessState>> for Queue {
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>) {
let _ = self.0.send(update);
}
}
/// `assert_eq!` for where a frame put a widget, written as its two corners.
#[macro_export]
macro_rules! assert_corners {
($harness:expr, $id:expr, ($x0:expr, $y0:expr), ($x1:expr, $y1:expr)) => {
assert_eq!(
$harness.region(&$id).expect("widget drew nothing"),
$crate::core::PixelRegion {
top_left: $crate::core::PxVec2::new(
$crate::core::Px::from_f32($x0 as f32),
$crate::core::Px::from_f32($y0 as f32),
),
bot_right: $crate::core::PxVec2::new(
$crate::core::Px::from_f32($x1 as f32),
$crate::core::Px::from_f32($y1 as f32),
),
}
);
};
}
pub use crate::assert_corners;
/// One replayed pointer sample, cut down to what a window delivers: where the
/// pointer is, and whether the button changed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TouchAction {
Down,
Move,
Up,
}
impl TouchAction {
fn parse(word: &str) -> Option<Self> {
match word {
"down" => Some(Self::Down),
"move" => Some(Self::Move),
"up" => Some(Self::Up),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct TouchSample {
pub t_ms: u64,
pub action: TouchAction,
pub pos: Vec2,
}
/// A recorded gesture, in the output's own pixels: `<ms> down|move|up <x> <y>`
/// a line, `#` to end of line ignored.
///
/// One parser for both ways of replaying a recording -- into a harness, and
/// into a real window -- so the two cannot read the same file differently.
pub struct TouchScript {
pub samples: Vec<TouchSample>,
}
impl TouchScript {
pub fn parse(text: &str) -> Result<Self, String> {
let mut samples: Vec<TouchSample> = Vec::new();
for (i, line) in text.lines().enumerate() {
let line = line.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let at = |what: &str| format!("touch script line {}: {what}: {line:?}", i + 1);
let mut words = line.split_whitespace();
let (Some(t), Some(action), Some(x), Some(y), None) = (
words.next(),
words.next(),
words.next(),
words.next(),
words.next(),
) else {
return Err(at("expected `t_ms action x y`"));
};
let t_ms: u64 = t.parse().map_err(|_| at("t_ms is not a whole number"))?;
let action =
TouchAction::parse(action).ok_or_else(|| at("action is not down/move/up"))?;
let x: f32 = x.parse().map_err(|_| at("x is not a number"))?;
let y: f32 = y.parse().map_err(|_| at("y is not a number"))?;
if let Some(last) = samples.last()
&& t_ms < last.t_ms
{
return Err(at("samples must be in time order"));
}
samples.push(TouchSample {
t_ms,
action,
pos: Vec2::new(x, y),
});
}
Ok(Self { samples })
}
}
#[derive(Default)]
pub struct HarnessState {
pub root: Option<StrongWidget>,
}
impl HasRoot for HarnessState {
fn set_root(&mut self, root: StrongWidget) {
self.root = Some(root);
}
}
pub struct Harness {
pub rsc: DefaultRsc<HarnessState>,
pub render: UiRenderState,
pub state: HarnessState,
updates: Receiver<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>,
cursor: CursorState,
}
impl Harness {
/// `size` is the output in physical pixels.
pub fn new(size: impl Into<Vec2>) -> Self {
// A `TaskQueue` must be `Sync`, which `mpsc::Sender` is not; the
// bound that comes with `SyncSender` is far past anything a test
// leaves unread.
let (send, updates) = sync_channel(1024);
let mut rsc = DefaultRsc::init(Arc::new(Queue(send)));
let mut render = UiRenderState::new();
render.resize(size, rsc.widgets_mut());
Self {
rsc,
render,
state: HarnessState::default(),
updates,
cursor: CursorState::default(),
}
}
pub fn size(&self) -> Vec2 {
self.render.output_size().to_f32()
}
pub fn resize(&mut self, size: impl Into<Vec2>) {
self.render.resize(size, self.rsc.widgets_mut());
}
/// Changes a length rule after the fact, the way `.width()` sets one.
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<LayoutLen>) {
self.rsc
.widgets_mut()
.set_size_rule(id, axis, SizeRule::Exact(len.into()));
}
/// Sets the root and lays it out, so a pointer event has something to hit.
pub fn set_root<T>(&mut self, widget: impl WidgetLike<DefaultRsc<HarnessState>, T>) {
widget.set_root(&mut self.rsc, &mut self.state);
self.frame();
}
pub fn needs_redraw(&self) -> bool {
self.render
.needs_redraw(&self.state.root, self.rsc.widgets())
}
pub fn apply_updates(&mut self) -> usize {
let mut applied = 0;
while let Ok(update) = self.updates.try_recv() {
update(&mut self.state, &mut self.rsc);
applied += 1;
}
applied
}
/// Waits for a task's first update, then applies everything waiting.
/// False if none arrived in time.
#[must_use]
pub fn await_update(&mut self, timeout: Duration) -> bool {
let Ok(update) = self.updates.recv_timeout(timeout) else {
return false;
};
update(&mut self.state, &mut self.rsc);
self.apply_updates();
true
}
/// Lays the tree out and builds its primitives.
pub fn frame(&mut self) {
self.apply_updates();
self.render.update(&self.state.root, &mut self.rsc);
}
/// Where the last frame put a widget, or `None` if it drew nothing.
pub fn region(&self, id: &impl IdLike) -> Option<PixelRegion> {
self.render.window_region(id)
}
pub fn move_to(&mut self, pos: impl Into<Vec2>) {
self.cursor.pos = pos.into();
self.cursor.exists = true;
self.sense();
}
pub fn leave(&mut self) {
self.cursor.exists = false;
self.sense();
}
pub fn press(&mut self, button: CursorButton) {
self.button(button).update(true);
self.sense();
}
pub fn release(&mut self, button: CursorButton) {
self.button(button).update(false);
self.sense();
}
/// A wheel carries no position, so this goes wherever the cursor was last
/// moved to -- nowhere, until it has been moved.
pub fn scroll(&mut self, delta: impl Into<Vec2>) {
self.cursor.scroll_delta = delta.into();
self.sense();
}
pub fn click(&mut self, pos: impl Into<Vec2>) {
self.move_to(pos);
self.press(CursorButton::Left);
self.release(CursorButton::Left);
}
/// Drives a recorded gesture through the harness.
pub fn replay(&mut self, script: &TouchScript) {
for sample in &script.samples {
match sample.action {
TouchAction::Down => {
self.move_to(sample.pos);
self.press(CursorButton::Left);
}
TouchAction::Move => self.move_to(sample.pos),
TouchAction::Up => {
self.move_to(sample.pos);
self.release(CursorButton::Left);
}
}
}
}
fn button(&mut self, button: CursorButton) -> &mut ActivationState {
let buttons = &mut self.cursor.buttons;
match button {
CursorButton::Left => &mut buttons.left,
CursorButton::Middle => &mut buttons.middle,
CursorButton::Right => &mut buttons.right,
}
}
/// Dispatches against the layout of the last frame, which is what a
/// window delivers input against too.
fn sense(&mut self) {
let cursor = self.cursor.clone();
self.render
.run_sensors(&mut self.rsc, &mut self.state, cursor);
self.cursor.end_frame();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(text: &str) -> Result<Vec<(u64, TouchAction, f32, f32)>, String> {
Ok(TouchScript::parse(text)?
.samples
.iter()
.map(|s| (s.t_ms, s.action, s.pos.x, s.pos.y))
.collect())
}
#[test]
fn a_recording_is_time_action_and_a_point() {
assert_eq!(
parse("0 down 10 20\n16 move 10.5 24\n32 up 10.5 24").unwrap(),
[
(0, TouchAction::Down, 10.0, 20.0),
(16, TouchAction::Move, 10.5, 24.0),
(32, TouchAction::Up, 10.5, 24.0),
]
);
}
#[test]
fn blank_lines_and_comments_are_not_samples() {
assert_eq!(
parse("# a flick\n\n 0 down 1 2 # the finger lands\n\n").unwrap(),
[(0, TouchAction::Down, 1.0, 2.0)]
);
}
#[test]
fn a_recording_that_goes_backwards_is_rejected() {
// Replay waits out the gap between samples, so time running backwards
// would silently become no wait at all.
let err = parse("16 down 1 2\n0 up 1 2").unwrap_err();
assert!(err.contains("time order"), "{err}");
}
#[test]
fn a_line_that_is_not_a_sample_says_which_line() {
let err = parse("0 down 1 2\n16 wiggle 1 2").unwrap_err();
assert!(err.contains("line 2"), "{err}");
assert!(err.contains("down/move/up"), "{err}");
}
}
+1 -2
View File
@@ -1,5 +1,6 @@
#![feature(unboxed_closures)] #![feature(unboxed_closures)]
#![feature(fn_traits)] #![feature(fn_traits)]
#![feature(gen_blocks)]
#![feature(associated_type_defaults)] #![feature(associated_type_defaults)]
#![feature(unsize)] #![feature(unsize)]
#![feature(option_into_flat_iter)] #![feature(option_into_flat_iter)]
@@ -7,8 +8,6 @@
pub mod default; pub mod default;
pub mod event; pub mod event;
pub mod harness;
pub mod random;
pub mod widget; pub mod widget;
pub use iris_core as core; pub use iris_core as core;
-931
View File
@@ -1,931 +0,0 @@
//! A seeded random widget tree, for tests and for looking at.
//!
//! One seed is one tree, on any machine and after any upgrade, so a test can
//! grow the same tree twice and a failing seed is reproduced by its number.
//! `examples/random.rs` draws one; `tests/generated.rs` checks that laying one
//! out again lands where growing it from scratch would.
use crate::prelude::*;
use std::collections::HashMap;
/// The declared lengths of one widget carrying a size rule, by axis.
pub type Lens = [Option<LayoutLen>; 2];
/// Where one widget carrying an alignment sits, by axis. `None` uses the
/// centered default.
pub type Aligns = [Option<AxisAlign>; 2];
/// What a test changes between two trees grown from the same seed, so the
/// warm one can be mutated and the cold one grown that way to begin with.
#[derive(Default)]
pub struct Edits {
/// Declared sizes, by the order the rules were put on.
pub sizes: HashMap<usize, Lens>,
/// Which children a span has, by the order the spans were made.
pub spans: HashMap<usize, SpanEdit>,
/// Alignments, by the order they were put on.
pub aligns: HashMap<usize, Aligns>,
/// Which widgets own a movable region, by the order they were offered
/// one. Region nodes change what a move writes and how deep a primitive's
/// chain is, so a tree that never grows one leaves both untested.
pub nodes: HashMap<usize, bool>,
/// Whether a [`Branch`] takes the side it would take at any measurement,
/// rather than the side the one it made says. The oracle wants the
/// measured side -- that is the whole point of a branch, and how a widget
/// believing a measurement a cold start would not have given it becomes a
/// different tree. A rig measuring cost wants this instead: a fixture
/// whose shape moves with the thing being measured cannot be compared
/// with itself across a change to it, and seed 1 at depth 8 went from 88
/// drawn widgets and 2,298 primitive writes a frame to 115 and 8,209
/// across fixed point, which is three and a half times the work behind a
/// number read as three and a half times the cost.
pub fixed_branches: bool,
}
#[derive(Default, Clone)]
pub struct SpanEdit {
/// Children to leave out, by index among the ones grown.
pub detach: Vec<usize>,
/// How many of the span's spares are in it, appended in order.
pub attach: usize,
}
/// xorshift64, written out rather than taken from a crate so that a seed
/// keeps meaning the same tree.
pub struct Rng(u64);
impl Rng {
pub fn new(seed: u64) -> Self {
Self(seed | 1)
}
pub fn bits(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
pub fn below(&mut self, n: usize) -> usize {
(self.bits() % n as u64) as usize
}
pub fn chance(&mut self) -> bool {
self.bits() & 1 == 0
}
}
const COLORS: [UiColor; 6] = [
UiColor::RED,
UiColor::GREEN,
UiColor::BLUE,
UiColor::YELLOW,
UiColor::CYAN,
UiColor::MAGENTA,
];
/// Leaves grown beside every span, for a test to put into it.
const SPARES: usize = 3;
const WORDS: &str = "Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer and not a setting.";
/// What growing a tree gives back: every widget in creation order, so two
/// trees from one seed line up index for index, and the declared sizes, which
/// are what a test changes to watch the change propagate.
#[derive(Default)]
pub struct Tree {
pub ids: Vec<WidgetId>,
pub sized: Vec<WidgetId>,
pub aligned: Vec<WidgetId>,
pub nodes: Vec<WidgetId>,
pub spans: Vec<Spanned>,
pub scrolls: Vec<WeakWidget<Scroll>>,
}
/// Branches on a child's measured length. Comparing boxes catches a widget
/// that moved; this catches one that believed a measurement a cold start
/// would not have given it, by turning that into a different tree. Its own
/// configuration never changes, so which side draws is a property of the
/// layout alone.
pub struct Branch {
pub probe: StrongWidget,
pub wide: StrongWidget,
pub narrow: StrongWidget,
pub threshold: f32,
}
impl Widget for Branch {
fn draw(&mut self, painter: &mut Painter) -> Size {
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = PlaceDescAxis::shifted(UiSpan::new(Len::ZERO, cut));
let measured = painter
.widget_at(&self.probe, PlaceDesc::new(PlaceDescAxis::WHOLE, top))
.len(Axis::X);
let len = measured.apply_leftover();
let px = painter.to_px(len, Axis::X);
// The range it actually branched on, said the way a container says
// one: pinning the window instead would redraw this widget on every
// resize, which is a fixture that never exercises reuse.
let threshold = Px::from_f32(self.threshold);
let holds = match px > threshold {
true => Holds::from(threshold + Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=threshold),
};
painter.window_holds(Axis::X, holds.through(len));
let below = PlaceDescAxis::shifted(UiSpan::new(cut, painter.region_len(Axis::Y)));
let place = PlaceDesc::new(PlaceDescAxis::WHOLE, below);
match px > threshold {
true => painter.widget_at(&self.wide, place),
false => painter.widget_at(&self.narrow, place),
};
Size::LEFTOVER
}
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
}
pub struct Spanned {
pub id: WeakWidget<Span>,
/// Everything made for this span that it does not hold -- spares never
/// attached and children detached alike. A widget belongs to one parent,
/// and one that belongs to nobody still has to be held here: dropping
/// the last share of it frees its id for the next widget to be given,
/// which puts two trees out of step.
pub spares: Vec<StrongWidget>,
/// How many children it was grown with, before any edit.
pub grown: usize,
}
/// A tree described rather than built: [`plan`] turns a seed into one of
/// these and [`build`] turns it into widgets, where growing did both at once.
///
/// The split is what makes a counterexample readable. A failing seed used to
/// be the entire record of one, because a grower that makes widgets as it
/// draws leaves nothing to take apart -- a shrinker could only grow its own
/// trees and hope to meet the same shape, which in practice it does not. A
/// plan is reduced by [`Plan::smaller`] and built again, so any seed that
/// fails can be cut down until what is left is small enough to read.
#[derive(Clone, Debug, PartialEq)]
pub struct Plan {
pub kind: Kind,
/// The declared size this widget carries. Whoever grows a widget offers
/// it one and the offer is taken or declined; a second offer to the same
/// widget is dropped, because two rules on one widget would settle in the
/// order they were applied rather than in grow order.
pub size: Option<Lens>,
/// The alignment it carries, under the same one-offer rule.
pub align: Option<Aligns>,
/// Whether it was offered a movable region of its own and what it
/// answered. `Some(false)` is an offer declined, which still uses up the
/// one offer, where `None` is an offer never made.
pub region_node: Option<bool>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Kind {
/// Wrapped and unwrapped text, because only one of them reads the width
/// it is given and so only one has to be drawn again for a new one.
Wrapped,
OneLine,
Rect {
color: usize,
alpha: u8,
},
/// Scrolling reads the pixel length of its box, which nothing else here
/// does, and gives its child a box longer than its own.
Scroll {
axis: Axis,
inner: Box<Plan>,
},
/// All three sides are grown either way, so a tree that draws one has the
/// same ids as a tree that draws another.
Branch {
probe: Box<Plan>,
wide: Box<Plan>,
narrow: Box<Plan>,
threshold: f32,
},
/// Each side its own, since a padding that is the same all round hides
/// anything that treats one edge differently from another.
Pad {
padding: [i32; 4],
inner: Box<Plan>,
},
Stack {
children: Vec<Plan>,
},
Span {
dir: usize,
gap: i32,
/// Grown for this span, in the order they are made.
children: Vec<Plan>,
/// Grown beside it whether or not they end up in it, so the widget
/// after them has the same id in a tree that leaves them out as in
/// one that puts them in.
spares: Vec<Plan>,
/// Which of `children` then `spares` are actually in the span, and
/// in what order -- kept apart from the two lists above so that a
/// tree which detaches, attaches or reorders its children still
/// makes the same widgets in the same order, and two builds line up
/// index for index. Anything not named here is built and held
/// rather than dropped, since freeing an id hands it to the next
/// widget and puts two trees out of step.
order: Vec<usize>,
},
}
impl Plan {
/// A widget carrying nothing anybody has offered it yet.
fn bare(kind: Kind) -> Self {
Self {
kind,
size: None,
align: None,
region_node: None,
}
}
/// How many widgets building it makes, spares and detached children
/// included, since those are made either way.
pub fn size(&self) -> usize {
1 + match &self.kind {
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => inner.size(),
Kind::Branch {
probe,
wide,
narrow,
..
} => probe.size() + wide.size() + narrow.size(),
Kind::Stack { children } => children.iter().map(Plan::size).sum(),
Kind::Span {
children, spares, ..
} => children.iter().chain(spares).map(Plan::size).sum(),
_ => 0,
}
}
/// The trees to try instead of this one when reducing a counterexample,
/// biggest cut first: a shrinker takes the first that still fails, so
/// offering "this subtree alone" before "this subtree with one child
/// fewer" is what gets from six hundred widgets to six rather than to
/// five hundred and ninety.
///
/// Every one of these is a tree the generator could have grown, so a
/// reduced plan is a counterexample in its own right rather than a
/// special case only the shrinker can make.
pub fn smaller(&self) -> Vec<Plan> {
let mut out = Vec::new();
// Standing in for the whole of it, which is the largest cut there is.
for kid in self.kids() {
out.push(kid.clone());
}
// Then what it carries, which costs nothing to put back if it was
// not the thing that mattered.
for dropped in [
self.region_node.map(|_| Plan {
region_node: None,
..self.clone()
}),
self.align.map(|_| Plan {
align: None,
..self.clone()
}),
self.size.map(|_| Plan {
size: None,
..self.clone()
}),
]
.into_iter()
.flatten()
{
out.push(dropped);
}
out.extend(self.kind.smaller().into_iter().map(|kind| Plan {
kind,
..self.clone()
}));
out
}
/// Visits every widget in the order [`build`] makes them, so a count
/// kept by the visitor indexes the same widget as the matching [`Tree`]
/// vector does.
pub fn walk_mut(&mut self, at: &mut impl FnMut(&mut Plan)) {
match &mut self.kind {
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => inner.walk_mut(at),
Kind::Branch {
probe,
wide,
narrow,
..
} => {
probe.walk_mut(at);
wide.walk_mut(at);
narrow.walk_mut(at);
}
Kind::Stack { children } => {
for child in children {
child.walk_mut(at);
}
}
Kind::Span {
children, spares, ..
} => {
for child in children.iter_mut().chain(spares) {
child.walk_mut(at);
}
}
_ => {}
}
at(self);
}
/// The same tree with `edits` applied, by the indices the generator would
/// have used for them.
///
/// [`plan`] resolves edits while drawing, which needs a seed. A scenario
/// needs them applied to a tree that already exists -- one it has built,
/// and one a shrinker may already have cut down, where no seed grows it
/// any more. Both routes take the same [`Edits`], so a case written
/// against one reads the same against the other.
pub fn edited(&self, edits: &Edits) -> Plan {
let mut out = self.clone();
let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0);
out.walk_mut(&mut |plan| {
if let Kind::Span {
children,
spares,
order,
..
} = &mut plan.kind
{
if let Some(edit) = edits.spans.get(&spans) {
*order = span_edited(order, children.len(), spares.len(), edit);
}
spans += 1;
}
if let Kind::Branch { threshold, .. } = &mut plan.kind
&& edits.fixed_branches
{
*threshold = f32::MIN;
}
if plan.size.is_some() {
if let Some(lens) = edits.sizes.get(&sized) {
plan.size = Some(*lens);
}
sized += 1;
}
if plan.align.is_some() {
if let Some(align) = edits.aligns.get(&aligned) {
plan.align = Some(*align);
}
aligned += 1;
}
if plan.region_node.is_some() {
if let Some(take) = edits.nodes.get(&nodes) {
plan.region_node = Some(*take);
}
nodes += 1;
}
});
out
}
fn kids(&self) -> Vec<&Plan> {
match &self.kind {
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => vec![inner],
Kind::Branch {
probe,
wide,
narrow,
..
} => vec![probe, wide, narrow],
Kind::Stack { children } => children.iter().collect(),
Kind::Span { children, .. } => children.iter().collect(),
_ => Vec::new(),
}
}
}
impl Kind {
/// Simplifications of the shape alone, leaving what the widget carries to
/// [`Plan::smaller`]. Replacing a node with one of its children is there
/// rather than here, since it answers with a whole `Plan`.
fn smaller(&self) -> Vec<Kind> {
let mut out = Vec::new();
/// One child reduced at a time, rebuilt into the same shape. Every
/// answer has the same number of children as it was given, so it is
/// for the shapes whose child count is part of what they are.
fn reduced(kids: &[Plan], rebuild: &dyn Fn(Vec<Plan>) -> Kind) -> Vec<Kind> {
let mut out = Vec::new();
for (i, kid) in kids.iter().enumerate() {
for small in kid.smaller() {
let mut next = kids.to_vec();
next[i] = small;
out.push(rebuild(next));
}
}
out
}
/// One child dropped, then [`reduced`]. For the shapes that hold any
/// number of children, where dropping one is the cut that matters.
fn each(kids: &[Plan], rebuild: &dyn Fn(Vec<Plan>) -> Kind) -> Vec<Kind> {
let mut out = Vec::new();
for i in 0..kids.len() {
if kids.len() > 1 {
let mut less = kids.to_vec();
less.remove(i);
out.push(rebuild(less));
}
}
out.extend(reduced(kids, rebuild));
out
}
match self {
// The one leaf that reads the width it is given, then the one
// that does not, then the one that measures nothing at all.
Kind::Wrapped => out.push(Kind::OneLine),
Kind::OneLine => out.push(Kind::Rect {
color: 0,
alpha: 255,
}),
Kind::Rect { .. } => {}
Kind::Scroll { axis, inner } => {
let axis = *axis;
out.extend(each(std::slice::from_ref(inner), &|mut k| Kind::Scroll {
axis,
inner: Box::new(k.remove(0)),
}));
}
Kind::Branch {
probe,
wide,
narrow,
threshold,
} => {
let threshold = *threshold;
// All three sides stay: a branch is the widget that draws
// one of two on a measurement, and one with a side missing
// is a different widget rather than a smaller one. Dropping
// the branch for a side is offered by `Plan::smaller`.
let sides = [(**probe).clone(), (**wide).clone(), (**narrow).clone()];
out.extend(reduced(&sides, &|k| Kind::Branch {
probe: Box::new(k[0].clone()),
wide: Box::new(k[1].clone()),
narrow: Box::new(k[2].clone()),
threshold,
}));
}
Kind::Pad { padding, inner } => {
let padding = *padding;
if padding != [0; 4] {
out.push(Kind::Pad {
padding: [0; 4],
inner: inner.clone(),
});
}
out.extend(each(std::slice::from_ref(inner), &|mut k| Kind::Pad {
padding,
inner: Box::new(k.remove(0)),
}));
}
Kind::Stack { children } => {
out.extend(each(children, &|children| Kind::Stack { children }))
}
Kind::Span {
dir,
gap,
children,
spares,
order,
} => {
let (dir, gap, n) = (*dir, *gap, children.len());
let span = |children: Vec<Plan>, spares: Vec<Plan>, order: Vec<usize>| Kind::Span {
dir,
gap,
children,
spares,
order,
};
let identity: Vec<usize> = (0..n).collect();
// An order the generator did not choose is part of the tree,
// so take that off before taking the tree apart.
if *order != identity {
out.push(span(children.clone(), spares.clone(), identity));
}
// Spares exist to be attached; with none attached they are
// widgets the span never holds.
if !spares.is_empty() && order.iter().all(|&i| i < n) {
out.push(span(children.clone(), Vec::new(), order.clone()));
}
if gap != 0 {
out.push(Kind::Span {
dir,
gap: 0,
children: children.clone(),
spares: spares.clone(),
order: order.clone(),
});
}
for k in 0..n {
if n > 1 {
let mut less = children.clone();
less.remove(k);
// Everything after it shifts down, spares included,
// since they are indexed past the children.
let order = order
.iter()
.filter(|&&i| i != k)
.map(|&i| if i > k { i - 1 } else { i })
.collect();
out.push(span(less, spares.clone(), order));
}
}
for (i, kid) in children.iter().enumerate() {
for small in kid.smaller() {
let mut next = children.clone();
next[i] = small;
out.push(span(next, spares.clone(), order.clone()));
}
}
}
}
out
}
}
/// A [`SpanEdit`] applied to the order a span already holds its children in.
///
/// `detach` names positions in that order and `attach` takes from the front
/// of what the span is not holding, both of which is what a test changing a
/// live span does -- so an edit means the same thing said to a tree and said
/// to the plan it was built from. On a span nobody has edited the order is
/// the children in the order they were grown, and this is then "leave these
/// out and put that many spares on the end".
fn span_edited(order: &[usize], children: usize, spares: usize, edit: &SpanEdit) -> Vec<usize> {
let mut detach = edit.detach.clone();
detach.sort_unstable();
detach.dedup();
let mut next: Vec<usize> = order
.iter()
.enumerate()
.filter(|(at, _)| !detach.contains(at))
.map(|(_, &which)| which)
.collect();
// What the span is not holding, in the order it hands them back: what it
// was already not holding first, in the order the widgets were made, and
// what this edit takes out after that, highest position first. A child
// just detached goes to the back rather than straight back in, which is
// what makes detaching one and attaching one a trade.
let mut free: Vec<usize> = (0..children + spares)
.filter(|i| !order.contains(i))
.collect();
free.extend(detach.iter().rev().filter_map(|&at| order.get(at).copied()));
next.extend(free.into_iter().take(edit.attach));
next
}
/// Plans the tree `seed` describes, `edits` replacing what it would otherwise
/// have given the widgets that carry them.
///
/// The edits are resolved here rather than at build time, so that a plan is
/// the whole of what a tree is and building one has nothing left to decide.
pub fn plan(seed: u64, depth: usize, edits: &Edits) -> Plan {
let mut sow = Sow {
rng: Rng::new(seed),
edits,
sized: 0,
aligned: 0,
nodes: 0,
spans: 0,
};
sow.node(depth)
}
/// Grows the tree `seed` describes, `edits` replacing the declared sizes it
/// would otherwise have given those wrappers.
pub fn grow<Rsc: UiRsc + 'static>(
rsc: &mut Rsc,
seed: u64,
depth: usize,
edits: &Edits,
) -> (StrongWidget, Tree) {
build(rsc, &plan(seed, depth, edits))
}
/// Draws a plan out of the random stream. Every draw happens in the order it
/// always has and before the decision it feeds, including the decisions that
/// are then dropped, because a seed has to keep meaning the same tree.
struct Sow<'a> {
rng: Rng,
edits: &'a Edits,
sized: usize,
aligned: usize,
nodes: usize,
spans: usize,
}
impl Sow<'_> {
fn leaf(&mut self) -> Plan {
Plan::bare(match self.rng.below(4) {
0 => Kind::Wrapped,
1 => Kind::OneLine,
_ => {
let color = self.rng.below(COLORS.len());
let alpha = (self.rng.below(5) * 63) as u8;
Kind::Rect { color, alpha }
}
})
}
fn len(&mut self) -> Option<LayoutLen> {
match self.rng.below(4) {
0 => Some(LayoutLen::px(20.0 + self.rng.below(180) as f32)),
1 => Some(LayoutLen::LEFTOVER),
_ => None,
}
}
fn align(&mut self) -> Aligns {
let axis = |s: &mut Self| match s.rng.below(4) {
0 => None,
1 => Some(AxisAlign::NEG),
2 => Some(AxisAlign::CENTER),
_ => Some(AxisAlign::POS),
};
let (x, y) = (axis(self), axis(self));
// Aligning on neither axis leaves the branch unexercised.
match x.is_none() && y.is_none() {
true => [Some(AxisAlign::CENTER), y],
false => [x, y],
}
}
/// A declared size over half the tree, kept where a test can change it.
fn sized(&mut self, inner: &mut Plan) {
let take = self.rng.chance();
let lens = [self.len(), self.len()];
if !take || inner.size.is_some() {
return;
}
let idx = self.sized;
self.sized += 1;
inner.size = Some(self.edits.sizes.get(&idx).copied().unwrap_or(lens));
}
/// An alignment over some of the tree, kept where a test can change it.
fn aligned(&mut self, inner: &mut Plan) {
let align = self.align();
if inner.align.is_some() {
return;
}
let idx = self.aligned;
self.aligned += 1;
inner.align = Some(self.edits.aligns.get(&idx).copied().unwrap_or(align));
}
/// A movable region of its own over some of the tree. What it changes is
/// how a move is written and how long a primitive's chain is, neither of
/// which any other branch here varies.
fn noded(&mut self, inner: &mut Plan) {
let take = self.rng.below(4) == 0;
if inner.region_node.is_some() {
return;
}
let idx = self.nodes;
self.nodes += 1;
inner.region_node = Some(self.edits.nodes.get(&idx).copied().unwrap_or(take));
}
fn offered(&mut self, inner: &mut Plan) {
self.sized(inner);
self.noded(inner);
}
fn node(&mut self, depth: usize) -> Plan {
if depth == 0 {
return self.leaf();
}
let positioned = self.rng.below(6);
if positioned == 0 {
let mut inner = self.node(depth - 1);
self.offered(&mut inner);
let axis = if self.rng.chance() { Axis::X } else { Axis::Y };
return Plan::bare(Kind::Scroll {
axis,
inner: Box::new(inner),
});
}
if positioned == 2 {
let probe = self.node(depth - 1);
let wide = self.node(depth - 1);
let narrow = self.node(depth - 1);
// Drawn either way, so the side a fixed branch takes is still a
// side the generator chose -- and it consumes the same randomness
// as a measured one, so the two grow the same ids.
let measured = self.rng.below(500) as f32;
let threshold = match self.edits.fixed_branches {
true => f32::MIN,
false => measured,
};
return Plan::bare(Kind::Branch {
probe: Box::new(probe),
wide: Box::new(wide),
narrow: Box::new(narrow),
threshold,
});
}
if positioned == 1 {
// Carries an alignment and makes no widget of its own, so the
// plan for it is the child it aligned.
let mut inner = self.node(depth - 1);
self.offered(&mut inner);
self.aligned(&mut inner);
return inner;
}
if self.rng.below(4) == 0 {
let mut inner = self.node(depth - 1);
self.offered(&mut inner);
let side = |s: &mut Self| s.rng.below(24) as i32;
let padding = [side(self), side(self), side(self), side(self)];
return Plan::bare(Kind::Pad {
padding,
inner: Box::new(inner),
});
}
let grown = 2 + self.rng.below(3);
let mut children = Vec::with_capacity(grown);
for _ in 0..grown {
let mut child = self.node(depth - 1);
self.offered(&mut child);
children.push(child);
}
if self.rng.chance() {
return Plan::bare(Kind::Stack { children });
}
let spares: Vec<Plan> = (0..SPARES).map(|_| self.leaf()).collect();
let idx = self.spans;
self.spans += 1;
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
let dir = self.rng.below(4);
// A row takes the height it is given rather than its tallest child,
// which is a rule beside it. Derived from an existing choice and
// consuming no randomness: a seed must keep growing the same tree
// when the generator gains another configuration.
let gap = self.rng.below(3) as i32 * 4;
let grown: Vec<usize> = (0..children.len()).collect();
let order = span_edited(&grown, children.len(), spares.len(), &edit);
Plan::bare(Kind::Span {
dir,
gap,
children,
spares,
order,
})
}
}
/// Builds a plan's widgets in the order it describes them, so two builds of
/// one plan line up index for index and their boxes can be compared.
pub fn build<Rsc: UiRsc + 'static>(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget, Tree) {
let mut build = Build {
rsc,
tree: Tree::default(),
};
let root = build.node(plan);
(root, build.tree)
}
struct Build<'a, Rsc> {
rsc: &'a mut Rsc,
tree: Tree,
}
impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
fn node(&mut self, plan: &Plan) -> StrongWidget {
let built = self.kind(&plan.kind);
let id = built.id();
if let Some(lens) = plan.size {
self.rsc
.ui_mut()
.widgets
.set_size_rules(id, lens[0], lens[1]);
self.tree.sized.push(id);
}
if let Some(align) = plan.align {
let widgets = &mut self.rsc.ui_mut().widgets;
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
widgets.set_alignment(id, axis, align.unwrap_or_default());
}
self.tree.aligned.push(id);
}
if let Some(take) = plan.region_node {
self.rsc.ui_mut().widgets.set_region_node(id, take);
self.tree.nodes.push(id);
}
built
}
fn kind(&mut self, kind: &Kind) -> StrongWidget {
let id: StrongWidget = match kind {
Kind::Wrapped => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
Kind::OneLine => wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add_strong(self.rsc),
Kind::Rect { color, alpha } => rect(COLORS[*color].alpha(*alpha)).add_strong(self.rsc),
Kind::Scroll { axis, inner } => {
let inner = self.node(inner);
let id = Scroll::new(inner, *axis).add(self.rsc);
self.tree.scrolls.push(id);
self.tree.ids.push(id.id());
return id.add_strong(self.rsc);
}
Kind::Branch {
probe,
wide,
narrow,
threshold,
} => {
let probe = self.node(probe);
let wide = self.node(wide);
let narrow = self.node(narrow);
let id = Branch {
probe,
wide,
narrow,
threshold: *threshold,
}
.add(self.rsc);
self.tree.ids.push(id.id());
return id.add_strong(self.rsc);
}
Kind::Pad { padding, inner } => {
let inner = self.node(inner);
let [left, right, top, bottom] = padding.map(Px::from_int);
let padding = Padding {
left,
right,
top,
bottom,
};
Pad { padding, inner }.add_strong(self.rsc)
}
Kind::Stack { children } => {
let children = children.iter().map(|c| self.node(c)).collect();
Stack {
children,
size: StackSize::Child(0),
}
.add_strong(self.rsc)
}
Kind::Span {
dir,
gap,
children,
spares,
order,
} => {
let grown = children.len();
// Every one of them is made, in this order, whether or not
// the span ends up holding it.
let made: Vec<StrongWidget> = children
.iter()
.chain(spares)
.map(|c| self.node(c))
.collect();
let mut left: Vec<Option<StrongWidget>> = made.into_iter().map(Some).collect();
let children: Vec<StrongWidget> = order
.iter()
.filter_map(|&i| left.get_mut(i).and_then(Option::take))
.collect();
// What the span does not hold is still held here: dropping
// the last share of a widget frees its id for the next one
// to be given, which puts two trees out of step.
let spares: Vec<StrongWidget> = left.into_iter().flatten().collect();
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][*dir % 4];
let id = Span {
children,
dir,
gap: Px::from_int(*gap),
}
.add(self.rsc);
if dir.axis == Axis::X {
self.rsc
.widgets_mut()
.set_size_rules(id, None, Some(LayoutLen::rel(1.0)));
}
self.tree.ids.push(id.id());
self.tree.spans.push(Spanned { id, spares, grown });
return id.add_strong(self.rsc);
}
};
self.tree.ids.push(id.id());
id
}
}
+8 -5
View File
@@ -6,13 +6,16 @@ pub struct Image {
} }
impl Widget for Image { impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
painter.primitive(&self.handle); painter.texture(&self.handle);
Size::px(self.handle.size())
} }
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> { fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Some(LayoutLen::px(self.handle.size().axis(axis))) Len::abs(self.handle.size().x)
}
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
Len::abs(self.handle.size().y)
} }
} }
+8 -10
View File
@@ -5,18 +5,16 @@ pub struct Masked {
} }
impl Widget for Masked { impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
painter.set_mask(UiRegion::FULL); painter.set_mask(painter.region());
painter.widget(&self.inner); painter.widget(&self.inner);
// What it occupies is its box, on both axes, for the reason `Scroll`
// reports the same: it clips what is inside to that box, so it can
// neither take less of one nor honestly ask for more. Passing the
// inner size up instead asks to be placed at a length it does not
// draw, and the framework would place the drawing it clipped away.
Size::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
Some(LayoutLen::LEFTOVER) ctx.width(&self.inner)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
} }
} }
+4 -2
View File
@@ -1,15 +1,17 @@
mod image; mod image;
mod mask; mod mask;
mod position; mod position;
mod ptr;
mod rect; mod rect;
mod text; mod text;
mod trait_fns; mod trait_fns;
mod wrapper; mod selector;
pub use image::*; pub use image::*;
pub use mask::*; pub use mask::*;
pub use position::*; pub use position::*;
pub use ptr::*;
pub use rect::*; pub use rect::*;
pub use text::*; pub use text::*;
pub use trait_fns::*; pub use trait_fns::*;
pub use wrapper::*; pub use selector::*;
+35
View File
@@ -0,0 +1,35 @@
use crate::prelude::*;
pub struct Aligned {
pub inner: StrongWidget,
pub align: Align,
}
impl Widget for Aligned {
fn draw(&mut self, painter: &mut Painter) {
let region = match self.align.tuple() {
(Some(x), Some(y)) => painter
.size(&self.inner)
.to_uivec2()
.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,
};
painter.widget_within(&self.inner, region);
}
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)
}
}
+10 -2
View File
@@ -6,10 +6,18 @@ pub struct LayerOffset {
} }
impl Widget for LayerOffset { impl Widget for LayerOffset {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
for _ in 0..self.offset { for _ in 0..self.offset {
painter.next_layer(); painter.next_layer();
} }
painter.widget(&self.inner).size() painter.widget(&self.inner);
}
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)
} }
} }
+48
View File
@@ -0,0 +1,48 @@
use crate::prelude::*;
pub struct MaxSize {
pub inner: StrongWidget,
pub x: 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 {
fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.inner);
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.apply_to_outer(ctx);
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
}
}
}
+6
View File
@@ -1,13 +1,19 @@
mod align;
mod layer; mod layer;
mod max_size;
mod offset; mod offset;
mod pad; mod pad;
mod scroll; mod scroll;
mod sized;
mod span; mod span;
mod stack; mod stack;
pub use align::*;
pub use layer::*; pub use layer::*;
pub use max_size::*;
pub use offset::*; pub use offset::*;
pub use pad::*; pub use pad::*;
pub use scroll::*; pub use scroll::*;
pub use sized::*;
pub use span::*; pub use span::*;
pub use stack::*; pub use stack::*;
+11 -4
View File
@@ -6,9 +6,16 @@ pub struct Offset {
} }
impl Widget for Offset { impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
painter let region = UiRegion::FULL.offset(self.amt);
.widget_at(&self.inner, UiRegion::FULL.offset(self.amt)) 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)
} }
} }
+52 -56
View File
@@ -6,50 +6,48 @@ pub struct Pad {
} }
impl Widget for Pad { impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
// The inner's own alignment, not the near edge. This reports the painter.widget_within(&self.inner, self.padding.region());
// inner's size plus the padding, so where the box is that answer the }
// inset box is exactly the inner and alignment has no room to move
// it; where the box is bigger -- a share of a row, a rule over this fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
// widget -- the slack is the inner's to sit in, and forcing the near let width = self.padding.left + self.padding.right;
// edge pinned it to a corner it had not asked for. let height = self.padding.top + self.padding.bottom;
// ctx.outer.x.abs -= width;
// Padding is an inset of both: it comes off the rel base, so `rel(1)` ctx.outer.y.abs -= height;
// under it fills this widget rather than overflowing it by the let mut size = ctx.width(&self.inner);
// padding, and it comes off the box, so what is drawn sits inside. size.abs += width;
// The two stay distinct -- the box can be narrower still, where a row size
// asked this widget in the room left, and a text wraps at that. }
let inner = painter.widget_at(&self.inner, self.padding.region()).size();
Size { fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
x: LayoutLen { let width = self.padding.left + self.padding.right;
px: inner.x.px + self.padding.left + self.padding.right, let height = self.padding.top + self.padding.bottom;
..inner.x ctx.outer.x.abs -= width;
}, ctx.outer.y.abs -= height;
y: LayoutLen { let mut size = ctx.height(&self.inner);
px: inner.y.px + self.padding.top + self.padding.bottom, size.abs += height;
..inner.y size
},
}
} }
} }
pub struct Padding { pub struct Padding {
pub left: Px, pub left: f32,
pub right: Px, pub right: f32,
pub top: Px, pub top: f32,
pub bottom: Px, pub bottom: f32,
} }
impl Padding { impl Padding {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
left: Px::ZERO, left: 0.0,
right: Px::ZERO, right: 0.0,
top: Px::ZERO, top: 0.0,
bottom: Px::ZERO, bottom: 0.0,
}; };
pub fn uniform(amt: impl UiNum) -> Self { pub fn uniform(amt: impl UiNum) -> Self {
let amt = Px::from_num(amt); let amt = amt.to_f32();
Self { Self {
left: amt, left: amt,
right: amt, right: amt,
@@ -57,82 +55,80 @@ impl Padding {
bottom: amt, bottom: amt,
} }
} }
/// `region` less this padding on each side. pub fn region(&self) -> UiRegion {
pub fn region_of(&self, mut region: UiRegion) -> UiRegion { let mut region = UiRegion::FULL;
region.x.start.px += self.left; region.x.start.abs += self.left;
region.y.start.px += self.top; region.y.start.abs += self.top;
region.x.end.px -= self.right; region.x.end.abs -= self.right;
region.y.end.px -= self.bottom; region.y.end.abs -= self.bottom;
region region
} }
pub fn region(&self) -> UiRegion {
self.region_of(UiRegion::FULL)
}
pub fn x(amt: impl UiNum) -> Self { pub fn x(amt: impl UiNum) -> Self {
let amt = Px::from_num(amt); let amt = amt.to_f32();
Self { Self {
left: amt, left: amt,
right: amt, right: amt,
..Self::ZERO top: 0.0,
bottom: 0.0,
} }
} }
pub fn y(amt: impl UiNum) -> Self { pub fn y(amt: impl UiNum) -> Self {
let amt = Px::from_num(amt); let amt = amt.to_f32();
Self { Self {
left: 0.0,
right: 0.0,
top: amt, top: amt,
bottom: amt, bottom: amt,
..Self::ZERO
} }
} }
pub fn top(amt: impl UiNum) -> Self { pub fn top(amt: impl UiNum) -> Self {
let mut s = Self::ZERO; let mut s = Self::ZERO;
s.top = Px::from_num(amt); s.top = amt.to_f32();
s s
} }
pub fn bottom(amt: impl UiNum) -> Self { pub fn bottom(amt: impl UiNum) -> Self {
let mut s = Self::ZERO; let mut s = Self::ZERO;
s.bottom = Px::from_num(amt); s.bottom = amt.to_f32();
s s
} }
pub fn left(amt: impl UiNum) -> Self { pub fn left(amt: impl UiNum) -> Self {
let mut s = Self::ZERO; let mut s = Self::ZERO;
s.left = Px::from_num(amt); s.left = amt.to_f32();
s s
} }
pub fn right(amt: impl UiNum) -> Self { pub fn right(amt: impl UiNum) -> Self {
let mut s = Self::ZERO; let mut s = Self::ZERO;
s.right = Px::from_num(amt); s.right = amt.to_f32();
s s
} }
pub fn with_top(mut self, amt: impl UiNum) -> Self { pub fn with_top(mut self, amt: impl UiNum) -> Self {
self.top = Px::from_num(amt); self.top = amt.to_f32();
self self
} }
pub fn with_bottom(mut self, amt: impl UiNum) -> Self { pub fn with_bottom(mut self, amt: impl UiNum) -> Self {
self.bottom = Px::from_num(amt); self.bottom = amt.to_f32();
self self
} }
pub fn with_left(mut self, amt: impl UiNum) -> Self { pub fn with_left(mut self, amt: impl UiNum) -> Self {
self.left = Px::from_num(amt); self.left = amt.to_f32();
self self
} }
pub fn with_right(mut self, amt: impl UiNum) -> Self { pub fn with_right(mut self, amt: impl UiNum) -> Self {
self.right = Px::from_num(amt); self.right = amt.to_f32();
self self
} }
} }
impl<T: UiNum> From<T> for Padding { impl<T: UiNum> From<T> for Padding {
fn from(amt: T) -> Self { fn from(amt: T) -> Self {
Self::uniform(amt) Self::uniform(amt.to_f32())
} }
} }
+28 -68
View File
@@ -3,78 +3,40 @@ use crate::prelude::*;
pub struct Scroll { pub struct Scroll {
inner: StrongWidget, inner: StrongWidget,
axis: Axis, axis: Axis,
amt: Px, amt: f32,
snap_end: bool, snap_end: bool,
container_len: Px, container_len: f32,
content_len: Px, content_len: f32,
} }
impl Widget for Scroll { impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let container_len = painter.px_len(self.axis); let output_len = painter.output_size().axis(self.axis);
// Asked in the whole viewport, then put at the scrolled offset. let container_len = painter.region().axis(self.axis).len();
let answer_len = painter let content_len = painter
.widget_at(&self.inner, PlaceDesc::WHOLE.fills()) .len_axis(&self.inner, self.axis)
.len(self.axis); .apply_rest()
let fixed = painter.to_px(Len::from_parts(answer_len.rel, answer_len.px), self.axis); .within_len(container_len)
self.container_len = container_len; .to_abs(output_len);
self.content_len = fixed.max(container_len); self.container_len = container_len.to_abs(output_len);
self.content_len = content_len;
if self.snap_end { if self.snap_end {
self.amt = self.content_len - self.container_len; self.amt = self.content_len - self.container_len;
} }
self.update_amt(); self.update_amt();
let align = painter.alignment().axis(self.axis);
// Content of a fixed length that fits sits at the start of any box it
// fits in -- but only anchored there. Anywhere else it is a part of
// the room left over, so it moves with every length the box takes and
// the drawing holds for that length alone. One scrolled part way sits
// where it is until the box shrinks past what is left of it. Kept to
// the end, it moves with every length.
let fixed_len = answer_len.rel == Rel::ZERO && answer_len.leftover == Weight::ZERO;
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG {
painter.holds(self.axis, fixed..=Px::MAX);
} else if fixed_len && !self.snap_end {
let left = self.content_len - self.amt;
painter.holds(self.axis, Px::MIN..=left);
}
// Content shorter than the viewport has room to sit in, and where it let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
// sits is this widget's own alignment -- the same property that would region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
// have placed the whole scroll in a box longer than it. painter.widget_within(&self.inner, region);
let slack = (self.container_len - self.content_len).max(Px::ZERO);
let anchor = slack.mul(align.rel());
// Content that fills the viewport and has not been scrolled is the
// viewport, and is handed back as it came. Writing the same box as
// its own length in pixels is the same box in another form, and the
// two do not round alike: a part centred in `rel 1` lands a step from
// one centred in `px 900`, since halving a difference is not halving
// each part of it.
let moved = anchor != Px::ZERO || self.amt != Px::ZERO;
let content = match moved || self.content_len != self.container_len {
true => {
let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
PlaceDescAxis::shifted(UiSpan::new(start, start.offset(self.content_len)))
}
false => PlaceDescAxis::WHOLE,
};
// The viewport is the inner's rel base, so a fraction it declares or
// reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it goes is the content
// box, scrolled: its drawing moved there, not made again there.
painter.place_at(
&self.inner,
PlaceDesc::from_axis(self.axis, content.fills(), PlaceDescAxis::WHOLE.fills()),
);
// What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for
// more. The content's length is what it scrolls through, not what it
// is.
Size::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
Some(LayoutLen::LEFTOVER) ctx.width(&self.inner)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
} }
} }
@@ -83,24 +45,22 @@ impl Scroll {
Self { Self {
inner, inner,
axis, axis,
amt: Px::ZERO, amt: 0.0,
snap_end: true, snap_end: true,
container_len: Px::ZERO, container_len: 0.0,
content_len: Px::ZERO, content_len: 0.0,
} }
} }
pub fn update_amt(&mut self) { pub fn update_amt(&mut self) {
self.amt = self.amt.max(Px::ZERO); self.amt = self.amt.max(0.0);
let len = (self.content_len - self.container_len).max(Px::ZERO); let len = (self.content_len - self.container_len).max(0.0);
self.amt = self.amt.min(len); self.amt = self.amt.min(len);
self.snap_end = self.amt == len; self.snap_end = self.amt == len;
} }
/// Scrolled by a distance the platform measures, which is the last place
/// a wheel notch or a finger is a float.
pub fn scroll(&mut self, amt: f32) { pub fn scroll(&mut self, amt: f32) {
self.amt -= Px::from_f32(amt); self.amt -= amt;
self.update_amt(); self.update_amt();
} }
} }
+34
View File
@@ -0,0 +1,34 @@
use crate::prelude::*;
pub struct Sized {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
impl Sized {
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 Sized {
fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.inner);
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.apply_to_outer(ctx);
self.x.unwrap_or_else(|| ctx.width(&self.inner))
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.apply_to_outer(ctx);
self.y.unwrap_or_else(|| ctx.height(&self.inner))
}
}
+129 -167
View File
@@ -4,167 +4,48 @@ use std::marker::PhantomData;
pub struct Span { pub struct Span {
pub children: Vec<StrongWidget>, pub children: Vec<StrongWidget>,
pub dir: Dir, pub dir: Dir,
pub gap: Px, pub gap: f32,
} }
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let axis = self.dir.axis; let total = self.len_sum(&mut painter.size_ctx());
// The row: this span's own box, as a length of the rel base its children let mut start = UiScalar::rel_min();
// are laid out against. Its start is nothing's business -- a slot is
// a length from it -- so what this reads is the length alone.
let far = painter.region_len(axis);
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(far - to, far - from),
};
// Across itself the child sits where its own alignment says, in the
// whole of the row: a span is what contains its children there, and
// nothing divides that axis.
let across = PlaceDescAxis::WHOLE;
// A length for every child before their final slots are chosen: from
// a hint where one says, and from drawing otherwise. The rel base passes
// through unchanged, so `rel(0.5)` is half the area this span was
// given whatever else is in it and wherever this child sits among
// them; what a drawn child is asked in is the room left from the
// cursor, because a text has to wrap at the width actually there.
let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children { for child in &self.children {
let len = match painter.size_hint(child, axis) { let mut span = UiSpan::FULL;
Some(len) => len, span.start = start;
None => { let len = painter.len_axis(child, self.dir.axis);
let room = PlaceDescAxis::shifted(along(cursor, far)); if len.rest > 0.0 {
painter let offset = UiScalar::new(total.rel, total.abs);
.widget_at(child, PlaceDesc::from_axis(axis, room, across)) let rel_end = UiScalar::rel(len.rest / total.rest);
.len(axis) let end = (UiScalar::rel_max() + start) - offset;
} start = rel_end.within(&start.to(end));
}; }
cursor.px += len.px + self.gap; start.abs += len.abs;
cursor.rel += len.rel; start.rel += len.rel;
lens.push(len); span.end = start;
let mut child_region = UiRegion::from_axis(self.dir.axis, span, UiSpan::FULL);
if self.dir.sign == Sign::Neg {
child_region.flip(self.dir.axis);
}
painter.widget_within(child, child_region);
start.abs += self.gap;
} }
let gaps = self
.gap
.mul_int(self.children.len().saturating_sub(1) as i32);
let total = lens.iter().fold(
LayoutLen {
px: gaps,
..LayoutLen::ZERO
},
|sum, len| sum + *len,
);
// What is left for the shares to divide: the row less everything
// fixed, as a length of the rel base rather than a number of pixels.
let room = far - Len::from_parts(total.rel, total.px);
// Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. Asked of `room`
// itself, and answered back through the same expression, so the
// boundary is the drawing's own and not a second way of finding it:
// the three cases a rounded division needed -- the fixed parts
// growing slower than the box, faster, or exactly with it -- are the
// sign of `room.rel`, which `through` already reads. What the
// generated oracle checks is the consequence, since which children
// exist at all turns on this.
let mut shares = false;
if total.leftover > Weight::ZERO {
shares = painter.to_px(room, axis) > Px::ZERO;
let holds = match shares {
true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO),
};
painter.window_holds(axis, holds.through(room));
}
// Across itself a span is as long as its longest child -- unless a
// rule beside it gives that length outright, and then reading them
// answers nothing and makes its size depend on theirs for it. A rule
// that only bounds the length does not count: the answer is still
// this span's to give.
let shrinks = !painter.has_exact_size(!axis);
// What the fixed parts and the gaps before here take, which is a sum
// of lengths and exact, and how much of the leftover weight is
// spoken for. A position is one from the other rather than a step
// from the last child: the share of the room is rounded, and taking
// each from the one before it would carry every rounding along the
// row.
let mut fixed = Len::rel_min();
let mut taken = Weight::ZERO;
let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO;
for (child, &len) in self.children.iter().zip(&lens) {
// A child asking for nothing but a part of what is left over,
// when nothing is, is not drawn at all. One that also asked for
// pixels or a fraction keeps those and overflows.
if len.leftover > Weight::ZERO && len.px == Px::ZERO && len.rel == Rel::ZERO && !shares
{
painter.undraw(child);
fixed.px += self.gap;
start = shared(fixed, taken, total.leftover, room);
continue;
}
let from = start;
if len.leftover > Weight::ZERO && shares {
taken += len.leftover;
}
fixed.px += len.px;
fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room);
// Along the row the span says where the child goes, and that slot
// is the child's box outright rather than something to place an
// answer inside again. A share is decided here and nowhere
// else: its slot narrows its rel base, and the child is asked in
// it, since a text wraps at the width it is actually given. A
// fixed child's slot is its own answer, so a drawing made in the
// room is put there as it is, and one not made yet is made here.
let slot = along(from, start);
let slot_place = PlaceDescAxis::shifted(slot).fills();
let mut place = PlaceDesc::from_axis(axis, slot_place, across);
if len.leftover > Weight::ZERO && shares {
place = place.rel_base(axis, slot.len());
}
let used = painter.place_at(child, place).len(!axis);
if shrinks {
// Choosing between a fixed and a relative length from the
// span's own eventual width admits multiple fixed points.
// A scalable child therefore makes Children scalable too;
// only fixed children are compared with one another.
if used.rel != Rel::ZERO || used.leftover != Weight::ZERO {
ortho = LayoutLen::LEFTOVER;
} else if ortho.leftover == Weight::ZERO {
ortho.px = ortho.px.max(used.px);
}
}
fixed.px += self.gap;
start = shared(fixed, taken, total.leftover, room);
}
// Carried whole rather than collapsed to one share: a span that sizes
// from its children does not resolve `leftover`, it passes the weight up,
// so nesting spans divides the same space rather than re-dividing a
// share of it. Four `leftover(1)` children under two spans under one span
// get a quarter each, which collapsing to `leftover(1)` per level does
// not give. Resolution happens at the nearest ancestor with a length,
// and the root always has one.
let along = total;
let ortho = match shrinks {
true => ortho,
false => LayoutLen::rel(1.0),
};
Size::from_axis(axis, along, ortho)
} }
}
/// Where a row has reached: everything fixed before this point, which is a fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
/// sum and exact, plus the share of the room the weights so far are worth, match self.dir.axis {
/// which is one rounding wherever it is asked for. Axis::X => self.desired_len(ctx),
fn shared(fixed: Len, taken: Weight, weight: Weight, room: Len) -> Len { Axis::Y => self.desired_ortho(ctx),
if taken == Weight::ZERO { }
return fixed; }
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),
}
} }
fixed + room.scale(Rel::ratio(taken, weight))
} }
impl Span { impl Span {
@@ -172,12 +53,12 @@ impl Span {
Self { Self {
children: Vec::new(), children: Vec::new(),
dir, dir,
gap: Px::ZERO, gap: 0.0,
} }
} }
pub fn gap(mut self, gap: impl UiNum) -> Self { pub fn gap(mut self, gap: impl UiNum) -> Self {
self.gap = Px::from_num(gap); self.gap = gap.to_f32();
self self
} }
@@ -188,44 +69,125 @@ 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<Children, Rsc, Tag, GTag> {
pub children: Wa, pub children: Children,
pub dir: Dir, pub dir: Dir,
pub gap: Px, pub gap: f32,
_pd: PhantomData<(State, Tag)>, _pd: PhantomData<(Rsc, Tag, GTag)>,
} }
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc> impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag> WidgetFnTrait<Rsc>
for SpanBuilder<Rsc, LEN, Wa, Tag> for SpanBuilder<Children, Rsc, Tag, GTag>
{ {
type Widget = Span; type Widget = Span;
#[track_caller] #[track_caller]
fn run(self, rsc: &mut Rsc) -> Self::Widget { fn run(self, rsc: &mut Rsc) -> Self::Widget {
Span { Span {
children: self.children.add(rsc).arr.into_iter().collect(), children: self.children.into_vec(rsc),
dir: self.dir, dir: self.dir,
gap: self.gap, gap: self.gap,
} }
} }
} }
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag>
SpanBuilder<State, LEN, Wa, Tag> SpanBuilder<Children, Rsc, Tag, GTag>
{ {
pub fn new(children: Wa, dir: Dir) -> Self { pub fn new(children: Children, dir: Dir) -> Self {
Self { Self {
children, children,
dir, dir,
gap: Px::ZERO, gap: 0.0,
_pd: PhantomData, _pd: PhantomData,
} }
} }
pub fn gap(mut self, gap: impl UiNum) -> Self { pub fn gap(mut self, gap: impl UiNum) -> Self {
self.gap = Px::from_num(gap); self.gap = gap.to_f32();
self self
} }
} }
+27 -53
View File
@@ -8,53 +8,29 @@ pub struct Stack {
} }
impl Widget for Stack { impl Widget for Stack {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let sizing = match self.size { let mut iter = self.children.iter();
StackSize::Default => None, if let Some(child) = iter.next() {
StackSize::Child(i) => Some(i), painter.child_layer();
}; painter.widget(child);
// Whichever child sizes the stack is given the stack's whole box -- }
// the stack is the length that child asked for, so placing that for child in iter {
// answer inside the box it decided would apply it twice. painter.next_layer();
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) { painter.widget(child);
// On the layer that child ends up on, so the ask below is a reuse
// rather than a second drawing of it somewhere else: a retained
// drawing belongs to the layer it was made on.
Some((i, child)) => {
painter.child_layer_at(i);
painter.widget_at(child, PlaceDesc::WHOLE.fills()).size()
}
None => Size::LEFTOVER,
};
// Every other child gets the box the sizing child decided: the
// stack is that length, so that is the box they are asked in, and a
// fraction under them is a fraction of it. A share leaves the axis
// to whoever gave the stack its box. Where a child sits in a box
// bigger than itself is its own business.
let on = |axis| {
let len = size.axis(axis);
match len.leftover == Weight::ZERO {
true => PlaceDescAxis::sized(Len::from_parts(len.rel, len.px)).fills(),
false => PlaceDescAxis::WHOLE,
}
};
let place = PlaceDesc::new(on(Axis::X), on(Axis::Y));
for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) {
continue;
}
painter.child_layer_at(i);
painter.widget_at(child, place);
} }
size
} }
/// Without a sizing child a stack is whatever box it is given, which it fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
/// can say without drawing anything.
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
match self.size { match self.size {
StackSize::Default => Some(LayoutLen::LEFTOVER), StackSize::Default => Len::default(),
StackSize::Child(_) => None, 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]),
} }
} }
} }
@@ -66,30 +42,28 @@ pub enum StackSize {
Child(usize), Child(usize),
} }
pub struct StackBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> { pub struct StackBuilder<Children, Rsc, Tag, GTag> {
pub children: Wa, pub children: Children,
pub size: StackSize, pub size: StackSize,
_pd: PhantomData<(State, Tag)>, _pd: PhantomData<(Rsc, Tag, GTag)>,
} }
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc> impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag> WidgetFnTrait<Rsc>
for StackBuilder<Rsc, LEN, Wa, Tag> for StackBuilder<Children, Rsc, Tag, GTag>
{ {
type Widget = Stack; type Widget = Stack;
#[track_caller] #[track_caller]
fn run(self, rsc: &mut Rsc) -> Self::Widget { fn run(self, rsc: &mut Rsc) -> Self::Widget {
Stack { Stack {
children: self.children.add(rsc).arr.into_iter().collect(), children: self.children.into_vec(rsc),
size: self.size, size: self.size,
} }
} }
} }
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag> StackBuilder<Children, Rsc, Tag, GTag> {
StackBuilder<State, LEN, Wa, Tag> pub fn new(children: Children) -> Self {
{
pub fn new(children: Wa) -> Self {
Self { Self {
children, children,
size: StackSize::default(), size: StackSize::default(),
+59
View File
@@ -0,0 +1,59 @@
use crate::prelude::*;
use std::marker::{Sized, Unsize};
pub struct WidgetPtr {
pub inner: Option<StrongWidget>,
}
impl Widget for WidgetPtr {
fn draw(&mut self, painter: &mut Painter) {
if let Some(id) = &self.inner {
painter.widget(id);
}
}
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
}
}
}
impl WidgetPtr {
pub fn new(widget: StrongWidget) -> Self {
Self {
inner: Some(widget),
}
}
pub fn empty() -> Self {
Self {
inner: Default::default(),
}
}
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) {
self.inner = Some(to)
}
pub fn replace<W: ?Sized + Unsize<dyn Widget>>(
&mut self,
to: StrongWidget<W>,
) -> Option<StrongWidget> {
self.inner.replace(to)
}
}
impl Default for WidgetPtr {
fn default() -> Self {
Self::empty()
}
}
+7 -4
View File
@@ -28,18 +28,21 @@ impl Rect {
} }
impl Widget for Rect { impl Widget for Rect {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
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::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> { fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Some(LayoutLen::LEFTOVER) Len::rest(1)
}
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
Len::rest(1)
} }
} }
+48
View File
@@ -0,0 +1,48 @@
use std::hash::Hash;
use iris_core::util::HashMap;
use crate::prelude::*;
pub struct WidgetSelector<T> {
current: (T, StrongWidget),
map: HashMap<T, StrongWidget>,
}
impl<T: Hash + Eq> WidgetSelector<T> {
pub fn new(key: T, widget: StrongWidget) -> Self {
Self {
current: (key, widget),
map: Default::default(),
}
}
pub fn set(&mut self, key: T, widget: StrongWidget) {
self.map.insert(key, widget);
}
pub fn select(&mut self, key: T) -> bool {
if let Some(val) = self.map.remove(&key) {
let mut new = (key, val);
std::mem::swap(&mut new, &mut self.current);
self.map.insert(new.0, new.1);
true
} else {
false
}
}
}
impl<T: 'static> Widget for WidgetSelector<T> {
fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.current.1);
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.width(&self.current.1)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.current.1)
}
}
+21 -6
View File
@@ -1,5 +1,6 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::PhantomData; use cosmic_text::{Attrs, Family, Metrics};
use std::marker::{PhantomData, Sized};
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> { pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
pub content: String, pub content: String,
@@ -19,7 +20,7 @@ impl<State, O, H: WidgetOption<State>> TextBuilder<State, O, H> {
self.attrs.color = color; self.attrs.color = color;
self self
} }
pub fn family(mut self, family: Family) -> Self { pub fn family(mut self, family: Family<'static>) -> Self {
self.attrs.family = family; self.attrs.family = family;
self self
} }
@@ -81,13 +82,19 @@ impl<Rsc: UiRsc> TextBuilderOutput<Rsc> for TextOutput {
state: &mut Rsc, state: &mut Rsc,
builder: TextBuilder<Rsc, Self, H>, builder: TextBuilder<Rsc, Self, H>,
) -> Self::Output { ) -> Self::Output {
let buf = TextBuffer::new(&builder.content); let mut buf = TextBuffer::new_empty(Metrics::new(
builder.attrs.font_size,
builder.attrs.line_height,
));
let hint = builder.hint.get(state); let hint = builder.hint.get(state);
let font_system = &mut state.ui_mut().text.font_system;
buf.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
let mut text = Text { let mut text = Text {
content: builder.content.into(), content: builder.content.into(),
view: TextView::new(buf, builder.attrs, hint), view: TextView::new(buf, builder.attrs, hint),
}; };
text.content.changed = false; text.content.changed = false;
builder.attrs.apply(font_system, &mut text.view.buf, None);
text text
} }
} }
@@ -103,11 +110,19 @@ impl<State: UiRsc> TextBuilderOutput<State> for TextEditOutput {
state: &mut State, state: &mut State,
builder: TextBuilder<State, Self, H>, builder: TextBuilder<State, Self, H>,
) -> Self::Output { ) -> Self::Output {
let buf = TextBuffer::new(&builder.content); let buf = TextBuffer::new_empty(Metrics::new(
TextEdit::new( builder.attrs.font_size,
builder.attrs.line_height,
));
let mut text = TextEdit::new(
TextView::new(buf, builder.attrs, builder.hint.get(state)), TextView::new(buf, builder.attrs, builder.hint.get(state)),
builder.output.mode, builder.output.mode,
) );
let font_system = &mut state.ui_mut().text.font_system;
text.buf
.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
builder.attrs.apply(font_system, &mut text.buf, None);
text
} }
} }
+403 -253
View File
@@ -1,30 +1,17 @@
use crate::prelude::*; use crate::prelude::*;
use iris_core::{TextData, UiColor}; use cosmic_text::{Affinity, Attrs, Cursor, FontSystem, LayoutRun, Motion};
use parley::{Affinity, Layout, Selection};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use unicode_segmentation::UnicodeSegmentation;
use winit::{ use winit::{
event::KeyEvent, event::KeyEvent,
keyboard::{Key, NamedKey}, keyboard::{Key, NamedKey},
}; };
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Motion {
Left,
Right,
LeftWord,
RightWord,
Up,
Down,
LineStart,
LineEnd,
}
pub struct TextEdit { pub struct TextEdit {
view: TextView, view: TextView,
/// `None` represents unfocused, which Parley's `Selection` cannot express. selection: TextSelection,
selection: Option<Selection>, history: Vec<(String, TextSelection)>,
history: Vec<(String, Option<Selection>)>, double_hit: Option<Cursor>,
double_hit: Option<usize>,
pub mode: EditMode, pub mode: EditMode,
} }
@@ -38,114 +25,230 @@ impl TextEdit {
pub fn new(view: TextView, mode: EditMode) -> Self { pub fn new(view: TextView, mode: EditMode) -> Self {
Self { Self {
view, view,
selection: None, selection: Default::default(),
history: Default::default(), history: Default::default(),
double_hit: None, double_hit: None,
mode, mode,
} }
} }
pub fn select_content(&self, start: Cursor, end: Cursor) -> String {
pub fn selected_text(&self) -> Option<String> { let (start, end) = sort_cursors(start, end);
let sel = self.selection?; let mut iter = self.buf.lines.iter().skip(start.line);
if sel.is_collapsed() { let first = iter.next().unwrap();
return None; if start.line == end.line {
first.text()[start.index..end.index].to_string()
} else {
let mut str = first.text()[start.index..].to_string();
for _ in (start.line + 1)..end.line {
str = str + "\n" + iter.next().unwrap().text();
}
let last = iter.next().unwrap();
str = str + "\n" + &last.text()[..end.index];
str
} }
Some(self.buf.text()[sel.text_range()].to_string())
} }
} }
impl Widget for TextEdit { impl Widget for TextEdit {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let base = painter.layer; let base = painter.layer;
painter.child_layer(); painter.child_layer();
let (_, size) = self.view.draw(painter); 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 size = vec2(1, self.attrs.line_height);
return size; match self.selection {
}; TextSelection::None => (),
let layout = self.view.buf.layout(); TextSelection::Pos(cursor) => {
if let Some(offset) = cursor_pos(cursor, &self.buf) {
// parley reports selection as boxes in layout space, so bidi and painter.primitive_within(
// wrapped lines come out right without this code knowing about either. RectPrimitive::color(Color::WHITE),
for (rect, _) in selection.geometry(layout) { size.align(Align::TOP_LEFT).offset(offset).within(&region),
let rect_size = vec2(rect.width() as f32, rect.height() as f32); );
let top_left = vec2(rect.x0 as f32, rect.y0 as f32); }
painter.primitive_within( }
RectPrimitive::color(Color::SKY), TextSelection::Span { start, end } => {
rect_size let (start, end) = sort_cursors(start, end);
.align(Align::TOP_LEFT) for (l, x, width) in iter_layout_lines(start, end, &self.buf) {
.offset(top_left) let top_left = vec2(x, self.attrs.line_height * l as f32);
.within(&region), painter.primitive_within(
); RectPrimitive::color(Color::SKY),
size.with_x(width)
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
);
}
if let Some(end_offset) = cursor_pos(end, &self.buf) {
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT)
.offset(end_offset)
.within(&region),
);
}
}
} }
}
let caret = selection.focus().geometry(layout, CARET_WIDTH); fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
let caret_size = vec2(caret.width() as f32, caret.height() as f32); self.view.desired_width(ctx)
let top_left = vec2(caret.x0 as f32, caret.y0 as f32); }
painter.primitive_within(
RectPrimitive::color(Color::WHITE), fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
caret_size self.view.desired_height(ctx)
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
);
size
} }
} }
const CARET_WIDTH: f32 = 1.0; /// provides top left + width
fn iter_layout_lines(
start: Cursor,
end: Cursor,
buf: &TextBuffer,
) -> impl Iterator<Item = (usize, f32, f32)> {
gen move {
let mut iter = buf.layout_runs().enumerate();
for (i, line) in iter.by_ref() {
if line.line_i == start.line
&& let Some(start_x) = index_x(&line, start.index)
{
if start.line == end.line
&& let Some(end_x) = index_x(&line, end.index)
{
yield (i, start_x, end_x - start_x);
return;
}
yield (i, start_x, line.line_w - start_x);
break;
}
}
for (i, line) in iter {
if line.line_i > end.line {
return;
}
if line.line_i == end.line
&& let Some(end_x) = index_x(&line, end.index)
{
yield (i, 0.0, end_x);
return;
}
yield (i, 0.0, line.line_w);
}
}
}
/// copied & modified from fn found in Editor in cosmic_text
/// returns x pos of a (non layout) index within an layout run
fn index_x(run: &LayoutRun, index: usize) -> Option<f32> {
for glyph in run.glyphs.iter() {
if index == glyph.start {
return Some(glyph.x);
} else if index > glyph.start && index < glyph.end {
// Guess x offset based on characters
let mut before = 0;
let mut total = 0;
let cluster = &run.text[glyph.start..glyph.end];
for (i, _) in cluster.grapheme_indices(true) {
if glyph.start + i < index {
before += 1;
}
total += 1;
}
let offset = glyph.w * (before as f32) / (total as f32);
return Some(glyph.x + offset);
}
}
None
}
/// returns top of line segment where cursor should visually select
fn cursor_pos(cursor: Cursor, buf: &TextBuffer) -> Option<Vec2> {
let mut prev = None;
for run in buf
.layout_runs()
.skip_while(|r| r.line_i < cursor.line)
.take_while(|r| r.line_i == cursor.line)
{
prev = Some(vec2(run.line_w, run.line_top));
if let Some(pos) = index_x(&run, cursor.index) {
return Some(vec2(pos, run.line_top));
}
}
prev
}
pub struct TextEditCtx<'a> { pub struct TextEditCtx<'a> {
pub text: &'a mut TextEdit, pub text: &'a mut TextEdit,
pub data: &'a mut TextData, pub font_system: &'a mut FontSystem,
} }
impl<'a> TextEditCtx<'a> { impl<'a> TextEditCtx<'a> {
fn layout(&mut self) -> &Layout<UiColor> {
let attrs = self.text.view.attrs.clone();
let width = self.text.view.wrap_width();
self.text.view.buf.shape(self.data, &attrs, width);
self.text.view.buf.layout()
}
fn clamp_selection_to_layout(&mut self) {
if let Some(sel) = self.text.selection {
let layout = self.layout();
self.text.selection = Some(sel.refresh(layout));
}
}
pub fn take(&mut self) -> String { pub fn take(&mut self) -> String {
let text = std::mem::take(self.text.view.buf.edit()); let text = self
self.text.selection = None; .text
.buf
.lines
.drain(..)
.map(|l| l.into_text())
.collect::<Vec<_>>()
.join("\n");
self.text
.buf
.set_text(self.font_system, "", &Attrs::new(), SHAPING, None);
self.text.selection.clear();
text text
} }
pub fn set(&mut self, text: &str) { pub fn set(&mut self, text: &str) {
let text = self.string(text); let text = self.string(text);
self.text.view.buf.set_text(text); self.text
self.text.selection = None; .buf
.set_text(self.font_system, &text, &Attrs::new(), SHAPING, None);
self.text.selection.clear();
} }
pub fn motion(&mut self, motion: Motion, select: bool) { pub fn motion(&mut self, motion: Motion, select: bool) {
let Some(sel) = self.text.selection else { if let TextSelection::Pos(cursor) = self.text.selection
return; && let Some(new) = self.buf_motion(cursor, motion)
}; {
let layout = self.layout(); if select {
let sel = apply_motion(sel, layout, motion, select); self.text.selection = TextSelection::Span {
self.text.selection = Some(sel); start: cursor,
end: new,
};
} else {
self.text.selection = TextSelection::Pos(new);
}
} else if let TextSelection::Span { start, end } = self.text.selection {
if select {
if let Some(cursor) = self.buf_motion(end, motion) {
self.text.selection = TextSelection::Span { start, end: cursor };
}
} else {
let (start, end) = sort_cursors(start, end);
let sel = &mut self.text.selection;
match motion {
Motion::Left | Motion::LeftWord => *sel = TextSelection::Pos(start),
Motion::Right | Motion::RightWord => *sel = TextSelection::Pos(end),
_ => {
if let Some(cursor) = self.buf_motion(end, motion) {
self.text.selection = TextSelection::Pos(cursor);
}
}
}
}
}
} }
/// Replace the `len` characters before the caret. This is the IME's
/// preedit path: it re-sends the whole composition each time.
pub fn replace(&mut self, len: usize, text: &str) { pub fn replace(&mut self, len: usize, text: &str) {
let text = self.string(text); let text = self.string(text);
for _ in 0..len { for _ in 0..len {
self.backspace(false); self.delete(false);
} }
self.insert_str(&text); self.insert_inner(&text, false);
} }
fn string(&self, text: &str) -> String { fn string(&self, text: &str) -> String {
@@ -158,176 +261,202 @@ impl<'a> TextEditCtx<'a> {
pub fn insert(&mut self, text: &str) { pub fn insert(&mut self, text: &str) {
let text = self.string(text); let text = self.string(text);
self.insert_str(&text); let mut lines = text.split('\n');
} let Some(first) = lines.next() else {
fn insert_str(&mut self, text: &str) {
if text.is_empty() {
return; return;
}
self.clear_span();
let at = match self.text.selection {
Some(sel) => sel.focus().index(),
None => return,
}; };
let at = at.min(self.text.view.buf.text().len()); self.insert_inner(first, true);
self.text.view.buf.edit().insert_str(at, text); for line in lines {
self.set_caret(at + text.len()); self.newline();
self.insert_inner(line, true);
}
} }
pub fn clear_span(&mut self) -> bool { pub fn clear_span(&mut self) -> bool {
let Some(sel) = self.text.selection else { if let TextSelection::Span { start, end } = self.text.selection {
return false; self.delete_between(start, end);
}; let (start, _) = sort_cursors(start, end);
if sel.is_collapsed() { self.text.selection = TextSelection::Pos(start);
return false; true
} else {
false
} }
let range = sel.text_range();
self.text.view.buf.edit().replace_range(range.clone(), "");
self.set_caret(range.start);
true
} }
fn set_caret(&mut self, index: usize) { pub fn delete_between(&mut self, start: Cursor, end: Cursor) {
let index = index.min(self.text.view.buf.text().len()); let lines = &mut self.text.view.buf.lines;
let layout = self.layout(); let (start, end) = sort_cursors(start, end);
self.text.selection = Some(Selection::from_byte_index( if start.line == end.line {
layout, let line = &mut lines[start.line];
index, let text = line.text();
Affinity::default(), let text = text[..start.index].to_string() + &text[end.index..];
)); edit_line(line, text);
} else {
// start
let start_text = lines[start.line].text()[..start.index].to_string();
let end_text = &lines[end.line].text()[end.index..];
let text = start_text + end_text;
edit_line(&mut lines[start.line], text);
}
// between
let range = (start.line + 1)..=end.line;
if !range.is_empty() {
lines.splice(range, None);
}
}
fn insert_inner(&mut self, text: &str, mov: bool) {
self.clear_span();
if let TextSelection::Pos(cursor) = &mut self.text.selection {
let line = &mut self.text.view.buf.lines[cursor.line];
let mut line_text = line.text().to_string();
line_text.insert_str(cursor.index, text);
edit_line(line, line_text);
if mov {
for _ in 0..text.chars().count() {
self.motion(Motion::Right, false);
}
}
}
} }
pub fn newline(&mut self) { pub fn newline(&mut self) {
if self.text.mode == EditMode::MultiLine { if self.text.mode == EditMode::SingleLine {
self.insert_str("\n"); return;
}
self.clear_span();
if let TextSelection::Pos(cursor) = &mut self.text.selection {
let lines = &mut self.text.view.buf.lines;
let line = &mut lines[cursor.line];
let new = line.split_off(cursor.index);
cursor.line += 1;
lines.insert(cursor.line, new);
cursor.index = 0;
} }
} }
pub fn backspace(&mut self, word: bool) { pub fn backspace(&mut self, word: bool) {
if self.clear_span() { if !self.clear_span()
return; && let TextSelection::Pos(cursor) = &mut self.text.selection
&& (cursor.index != 0 || cursor.line != 0)
{
self.motion(if word { Motion::LeftWord } else { Motion::Left }, false);
self.delete(word);
} }
let Some(sel) = self.text.selection else {
return;
};
let end = sel.focus().index();
if end == 0 {
return;
}
let layout = self.layout();
let start = if word {
sel.focus().previous_logical_word(layout).index()
} else {
let Some(cluster) = sel.focus().logical_clusters(layout)[0] else {
return;
};
let range = cluster.text_range();
if cluster.is_hard_line_break() || cluster.is_emoji() {
range.start
} else {
self.text.view.buf.text()[..range.end]
.char_indices()
.next_back()
.map_or(range.start, |(start, _)| start)
}
};
self.delete_range(start, end);
} }
pub fn delete(&mut self, word: bool) { pub fn delete(&mut self, word: bool) {
if self.clear_span() { if !self.clear_span()
return; && let TextSelection::Pos(cursor) = &mut self.text.selection
{
if word {
let start = *cursor;
if let Some(end) = self.buf_motion(start, Motion::RightWord) {
self.delete_between(start, end);
}
} else {
let lines = &mut self.text.view.buf.lines;
let line = &mut lines[cursor.line];
if cursor.index == line.text().len() {
if cursor.line == lines.len() - 1 {
return;
}
let add = lines.remove(cursor.line + 1).into_text();
let line = &mut lines[cursor.line];
let mut cur = line.text().to_string();
cur.push_str(&add);
edit_line(line, cur);
} else {
let mut text = line.text().to_string();
text.remove(cursor.index);
edit_line(line, text);
}
}
} }
let Some(sel) = self.text.selection else {
return;
};
let start = sel.focus().index();
if start >= self.text.view.buf.text().len() {
return;
}
let layout = self.layout();
let end = if word {
sel.focus().next_logical_word(layout).index()
} else {
let clusters = sel.focus().logical_clusters(layout);
let Some(cluster) = clusters[1].as_ref() else {
return;
};
cluster.text_range().end
};
self.delete_range(start, end);
} }
fn delete_range(&mut self, start: usize, end: usize) { fn buf_motion(&mut self, cursor: Cursor, motion: Motion) -> Option<Cursor> {
self.text.view.buf.edit().replace_range(start..end, ""); self.text
self.set_caret(start); .buf
.cursor_motion(self.font_system, cursor, None, motion)
.map(|r| r.0)
} }
pub fn select_all(&mut self) { pub fn select_word_at(&mut self, cursor: Cursor) {
let len = self.text.view.buf.text().len(); if let (Some(start), Some(end)) = (
if len == 0 { self.buf_motion(cursor, Motion::LeftWord),
return; self.buf_motion(cursor, Motion::RightWord),
) {
self.text.selection = TextSelection::Span { start, end };
}
}
pub fn select_line_at(&mut self, cursor: Cursor) {
let end = self.text.buf.lines[cursor.line].text().len();
self.text.selection = TextSelection::Span {
start: Cursor::new(cursor.line, 0),
end: Cursor::new(cursor.line, end),
} }
let layout = self.layout();
let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default());
let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default());
self.text.selection = Some(Selection::new(anchor, focus));
} }
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) { pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
let pos = pos let pos = pos - self.text.region().top_left().to_abs(size);
- self let hit = self.text.buf.hit(pos.x, pos.y);
.text let sel = &mut self.text.selection;
.region() match sel {
.top_left() TextSelection::None => {
.to_px(PxVec2::from_f32(size)) if !drag && let Some(hit) = hit {
.to_f32(); *sel = TextSelection::Pos(hit)
let prev_sel = self.text.selection; }
let prev_hit = self.text.double_hit;
let layout = self.layout();
let (selection, double_hit) = if drag {
let Some(selection) = prev_sel else {
return;
};
(selection.extend_to_point(layout, pos.x, pos.y), prev_hit)
} else {
let hit = Selection::from_point(layout, pos.x, pos.y);
let index = hit.focus().index();
// Successive clicks at one index select the word, then the line.
if recent && prev_hit == Some(index) {
(Selection::line_from_point(layout, pos.x, pos.y), None)
} else if recent && prev_sel.map(|s| s.focus().index()) == Some(index) {
(
Selection::word_from_point(layout, pos.x, pos.y),
Some(index),
)
} else {
(hit, None)
} }
}; TextSelection::Pos(pos) => match (hit, drag) {
(None, false) => *sel = TextSelection::None,
self.text.selection = Some(selection); (None, true) => (),
self.text.double_hit = double_hit; (Some(hit), false) => {
if recent && hit == *pos {
self.text.double_hit = Some(hit);
return self.select_word_at(hit);
} else {
*pos = hit
}
}
(Some(end), true) => *sel = TextSelection::Span { start: *pos, end },
},
TextSelection::Span { start, end } => match (hit, drag) {
(None, false) => *sel = TextSelection::None,
(None, true) => *sel = TextSelection::Pos(*start),
(Some(hit), false) => {
if recent
&& let Some(double) = self.text.double_hit
&& double == hit
{
return self.select_line_at(hit);
} else {
*sel = TextSelection::Pos(hit)
}
}
(Some(hit), true) => *end = hit,
},
}
if let TextSelection::Span { start, end } = sel
&& start == end
{
*sel = TextSelection::Pos(*start);
}
} }
pub fn deselect(&mut self) { pub fn deselect(&mut self) {
self.text.selection = None; self.text.selection = TextSelection::None;
self.text.double_hit = None;
} }
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult { pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
let old = (self.text.view.buf.text().to_string(), self.text.selection); let old = (self.text.content(), self.text.selection);
let mut undo = false; let mut undo = false;
let res = self.apply_event_inner(event, modifiers, &mut undo); let res = self.apply_event_inner(event, modifiers, &mut undo);
if undo { if undo && let Some((old, selection)) = self.text.history.pop() {
if let Some((old, selection)) = self.text.history.pop() { self.set(&old);
self.set(&old); self.text.selection = selection;
self.text.selection = selection; } else if self.text.content() != old.0 {
self.clamp_selection_to_layout();
}
} else if self.text.view.buf.text() != old.0 {
self.text.history.push(old); self.text.history.push(old);
} }
res res
@@ -352,25 +481,21 @@ impl<'a> TextEditCtx<'a> {
} }
} }
NamedKey::ArrowRight => { NamedKey::ArrowRight => {
let motion = if modifiers.control { if modifiers.control {
Motion::RightWord self.motion(Motion::RightWord, modifiers.shift)
} else { } else {
Motion::Right self.motion(Motion::Right, modifiers.shift)
}; }
self.motion(motion, modifiers.shift);
} }
NamedKey::ArrowLeft => { NamedKey::ArrowLeft => {
let motion = if modifiers.control { if modifiers.control {
Motion::LeftWord self.motion(Motion::LeftWord, modifiers.shift)
} else { } else {
Motion::Left self.motion(Motion::Left, modifiers.shift)
}; }
self.motion(motion, modifiers.shift);
} }
NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift), NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift),
NamedKey::ArrowDown => self.motion(Motion::Down, modifiers.shift), NamedKey::ArrowDown => self.motion(Motion::Down, modifiers.shift),
NamedKey::Home => self.motion(Motion::LineStart, modifiers.shift),
NamedKey::End => self.motion(Motion::LineEnd, modifiers.shift),
NamedKey::Escape => { NamedKey::Escape => {
self.deselect(); self.deselect();
return TextInputResult::Unfocus; return TextInputResult::Unfocus;
@@ -382,18 +507,34 @@ impl<'a> TextEditCtx<'a> {
match text.as_str() { match text.as_str() {
"v" => return TextInputResult::Paste, "v" => return TextInputResult::Paste,
"c" => { "c" => {
if let Some(content) = self.text.selected_text() { if let TextSelection::Span { start, end } = self.text.selection {
let content = self.text.select_content(start, end);
return TextInputResult::Copy(content); return TextInputResult::Copy(content);
} }
} }
"x" => { "x" => {
if let Some(content) = self.text.selected_text() { if let TextSelection::Span { start, end } = self.text.selection {
let content = self.text.select_content(start, end);
self.clear_span(); self.clear_span();
return TextInputResult::Copy(content); return TextInputResult::Copy(content);
} }
} }
"a" => self.select_all(), "a" => {
"z" => *undo = true, if !self.text.buf.lines[0].text().is_empty()
|| self.text.buf.lines.len() > 1
{
let lines = &self.text.buf.lines;
let last_line = lines.len() - 1;
let last_idx = lines[last_line].text().len();
self.text.selection = TextSelection::Span {
start: Cursor::new(0, 0),
end: Cursor::new(last_line, last_idx),
};
}
}
"z" => {
*undo = true;
}
_ => self.insert(text), _ => self.insert(text),
} }
} else { } else {
@@ -406,24 +547,6 @@ impl<'a> TextEditCtx<'a> {
} }
} }
fn apply_motion(
sel: Selection,
layout: &Layout<UiColor>,
motion: Motion,
extend: bool,
) -> Selection {
match motion {
Motion::Left => sel.previous_visual(layout, extend),
Motion::Right => sel.next_visual(layout, extend),
Motion::LeftWord => sel.previous_visual_word(layout, extend),
Motion::RightWord => sel.next_visual_word(layout, extend),
Motion::Up => sel.previous_line(layout, extend),
Motion::Down => sel.next_line(layout, extend),
Motion::LineStart => sel.line_start(layout, extend),
Motion::LineEnd => sel.line_end(layout, extend),
}
}
#[derive(Default)] #[derive(Default)]
pub struct Modifiers { pub struct Modifiers {
pub shift: bool, pub shift: bool,
@@ -446,6 +569,33 @@ pub enum TextInputResult {
Paste, Paste,
} }
#[derive(Debug, Default, Clone, Copy)]
pub enum TextSelection {
#[default]
None,
Pos(Cursor),
Span {
start: Cursor,
end: Cursor,
},
}
impl TextSelection {
pub fn clear(&mut self) {
match self {
TextSelection::None => (),
TextSelection::Pos(cursor) => {
cursor.line = 0;
cursor.index = 0;
cursor.affinity = Affinity::default();
}
TextSelection::Span { start: _, end: _ } => {
*self = TextSelection::None;
}
}
}
}
impl TextInputResult { impl TextInputResult {
pub fn unfocus(&self) -> bool { pub fn unfocus(&self) -> bool {
matches!(self, TextInputResult::Unfocus) matches!(self, TextInputResult::Unfocus)
@@ -475,7 +625,7 @@ impl<I: IdLike<Widget = TextEdit>> TextEditable for I {
let ui = ui.ui_mut(); let ui = ui.ui_mut();
TextEditCtx { TextEditCtx {
text: ui.widgets.get_mut(self).unwrap(), text: ui.widgets.get_mut(self).unwrap(),
data: &mut ui.text, font_system: &mut ui.text.font_system,
} }
} }
} }
+118 -56
View File
@@ -6,32 +6,34 @@ pub use edit::*;
use iris_core::util::MutDetect; use iris_core::util::MutDetect;
use crate::prelude::*; use crate::prelude::*;
use cosmic_text::{Attrs, BufferLine, Cursor, Metrics, Shaping};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
pub const SHAPING: Shaping = Shaping::Advanced;
pub struct Text { pub struct Text {
pub content: MutDetect<String>, pub content: MutDetect<String>,
view: TextView, view: TextView,
} }
pub struct TextView { pub struct TextView {
pub attrs: TextAttrs, pub attrs: MutDetect<TextAttrs>,
pub buf: TextBuffer, pub buf: MutDetect<TextBuffer>,
// cache
tex: Option<RenderedText>,
width: Option<f32>,
pub hint: Option<StrongWidget>, pub hint: Option<StrongWidget>,
} }
impl TextView {
fn is_empty(&self) -> bool {
self.buf.is_empty()
}
pub fn wrap_width(&self) -> Option<f32> {
self.buf.wrap_width()
}
}
impl TextView { impl TextView {
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self { pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
Self { attrs, buf, hint } Self {
attrs: attrs.into(),
buf: buf.into(),
tex: None,
width: None,
hint,
}
} }
/// region where the text should be draw /// region where the text should be draw
@@ -43,73 +45,133 @@ impl TextView {
.align(self.align) .align(self.align)
} }
/// The text shaped for the width it is drawn in. The buffer keeps its fn tex_region(&self, tex: &RenderedText) -> UiRegion {
/// answers under the attrs too, so changing those asks a new question let region = tex.size.align(self.align);
/// rather than invalidating anything. let dims = tex.handle.size();
fn render(&mut self, painter: &mut Painter) -> &RenderedText { let mut region = region.offset(tex.top_left_offset);
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X)); region.x.end = region.x.start + UiScalar::abs(dims.x);
// The shaper measures in floats, which is where a glyph advance comes region.y.end = region.y.start + UiScalar::abs(dims.y);
// from; what it answers goes back on the grid. region
painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32));
if width.is_some() {
painter.holds(Axis::X, self.buf.width_holds());
}
self.buf.rendered().expect("render_text placed the glyphs")
} }
fn render(&mut self, ctx: &mut SizeCtx) -> RenderedText {
let width = if self.attrs.wrap {
Some(ctx.px_size().x)
} else {
None
};
if width == self.width
&& let Some(tex) = &self.tex
&& !self.attrs.changed
&& !self.buf.changed
{
return tex.clone();
}
self.width = width;
let font_system = &mut ctx.text.font_system;
self.attrs.apply(font_system, &mut self.buf, width);
self.buf.shape_until_scroll(font_system, false);
let tex = ctx.draw_text(&mut self.buf, &self.attrs);
self.tex = Some(tex.clone());
self.attrs.changed = false;
self.buf.changed = false;
tex
}
pub fn tex(&self) -> Option<&RenderedText> { pub fn tex(&self) -> Option<&RenderedText> {
self.buf.rendered() self.tex.as_ref()
} }
/// Draws the text, and says where the glyphs went and what they use. pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) { if let Some(hint) = &self.hint
let align = self.align; && let [line] = &self.buf.lines[..]
if self.is_empty() && self.hint.is_some() { && line.text().is_empty()
let region = self.render(painter).size.align(align); {
let size = match &self.hint { ctx.width(hint)
Some(hint) => painter.widget(hint).size(), } else {
None => Size::ZERO, Len::abs(self.render(ctx).size.x)
};
return (region, size);
} }
}
let tex = self.render(painter); pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
let region = tex.size.align(align); if let Some(hint) = &self.hint
// The step at or above what the shaper measured, so a parent that && let [line] = &self.buf.lines[..]
// hands back the length this reports hands back a box the longest && line.text().is_empty()
// line fits in. Rounded to the nearest step it is half the time a {
// hair under that line, and the break made in it is not the break a ctx.height(hint)
// cold layout makes there. } else {
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size)); Len::abs(self.render(ctx).size.y)
painter.glyphs(tex, region); }
(region, size) }
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
let tex = self.render(&mut painter.size_ctx());
let region = self.tex_region(&tex);
if let Some(hint) = &self.hint
&& let [line] = &self.buf.lines[..]
&& line.text().is_empty()
{
painter.widget(hint);
} else {
painter.texture_within(&tex.handle, region);
}
region
} }
pub fn content(&self) -> String { pub fn content(&self) -> String {
self.buf.text().to_string() self.buf
.lines
.iter()
.map(|l| l.text())
.collect::<Vec<_>>()
.join("\n")
} }
} }
impl Text { impl Text {
pub fn new(content: impl Into<String>) -> Self { pub fn new(content: impl Into<String>) -> Self {
let content: String = content.into(); let attrs = TextAttrs::default();
let buf = TextBuffer::new_empty(Metrics::new(attrs.font_size, attrs.line_height));
Self { Self {
view: TextView::new(TextBuffer::new(&content), TextAttrs::default(), None), content: content.into().into(),
content: content.into(), view: TextView::new(buf, attrs, None),
} }
} }
fn update_buf(&mut self) { fn update_buf(&mut self, ctx: &mut SizeCtx) {
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(
&mut ctx.text.font_system,
&self.content,
&Attrs::new().family(self.view.attrs.family),
SHAPING,
None,
);
} }
} }
} }
impl Widget for Text { impl Widget for Text {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
self.update_buf(); self.update_buf(&mut painter.size_ctx());
self.view.draw(painter).1 self.view.draw(painter);
} }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.update_buf(ctx);
self.view.desired_width(ctx)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.update_buf(ctx);
self.view.desired_height(ctx)
}
}
pub fn sort_cursors(a: Cursor, b: Cursor) -> (Cursor, Cursor) {
let start = a.min(b);
let end = a.max(b);
(start, end)
}
pub fn edit_line(line: &mut BufferLine, text: String) {
line.set_text(text, line.ending(), line.attrs_list().clone());
} }
impl Deref for Text { impl Deref for Text {
+92 -62
View File
@@ -12,23 +12,14 @@ widget_trait! {
} }
} }
fn align(self, align: impl Into<Align>) -> impl WidgetIdFn<Rsc, WL::Widget> { fn align(self, align: impl Into<Align>) -> impl WidgetFn<Rsc, Aligned> {
// An axis left out keeps whatever it had, which is centered unless move |state| Aligned {
// something else set it. inner: self.add_strong(state),
let align = align.into(); align: align.into(),
move |state| {
let id = self.add(state);
let widgets = &mut state.ui_mut().widgets;
for (axis, align) in [(Axis::X, align.x), (Axis::Y, align.y)] {
if let Some(align) = align {
widgets.set_alignment(id, axis, align);
}
}
id
} }
} }
fn center(self) -> impl WidgetIdFn<Rsc, WL::Widget> { fn center(self) -> impl WidgetFn<Rsc, Aligned> {
self.align(Align::CENTER) self.align(Align::CENTER)
} }
@@ -40,46 +31,48 @@ widget_trait! {
} }
} }
fn region_node(self) -> impl WidgetIdFn<Rsc, WL::Widget> { fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, Sized> {
|state| {
let id = self.add(state);
state.ui_mut().widgets.set_region_node(id, true);
id
}
}
fn sized(self, size: impl Into<Size>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let size = size.into(); let size = size.into();
move |state| { move |state| Sized {
let id = self.add(state); inner: self.add_strong(state),
let widgets = &mut state.ui_mut().widgets; x: Some(size.x),
widgets.set_size_rule(id, Axis::X, SizeRule::Exact(size.x)); y: Some(size.y),
widgets.set_size_rule(id, Axis::Y, SizeRule::Exact(size.y));
id
} }
} }
fn width(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> { fn max_width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
let len = len.into(); let len = len.into();
move |state| { move |state| MaxSize {
let id = self.add(state); inner: self.add_strong(state),
state x: Some(len),
.ui_mut() y: None,
.widgets
.set_size_rule(id, Axis::X, SizeRule::Exact(len));
id
} }
} }
fn height(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> { fn max_height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
let len = len.into(); let len = len.into();
move |state| { move |state| MaxSize {
let id = self.add(state); inner: self.add_strong(state),
state x: None,
.ui_mut() y: Some(len),
.widgets }
.set_size_rule(id, Axis::Y, SizeRule::Exact(len)); }
id
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into();
move |state| Sized {
inner: self.add_strong(state),
x: Some(len),
y: None,
}
}
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into();
move |state| Sized {
inner: self.add_strong(state),
x: None,
y: Some(len),
} }
} }
@@ -92,9 +85,7 @@ widget_trait! {
fn scrollable(self) -> impl WidgetIdFn<Rsc, Scroll> where Rsc: HasEvents { fn scrollable(self) -> impl WidgetIdFn<Rsc, Scroll> where Rsc: HasEvents {
move |state| { move |state| {
let inner = self.add(state); Scroll::new(self.add_strong(state), Axis::Y)
state.ui_mut().widgets.set_region_node(inner, true);
Scroll::new(inner.upgrade(state), Axis::Y)
.on(CursorSense::Scroll, |ctx, rsc| { .on(CursorSense::Scroll, |ctx, rsc| {
let delta = ctx.data.scroll_delta.y * 50.0; let delta = ctx.data.scroll_delta.y * 50.0;
ctx.widget(rsc).scroll(delta); ctx.widget(rsc).scroll(delta);
@@ -134,28 +125,67 @@ widget_trait! {
|state| self.add(state) |state| self.add(state)
} }
// Named for the type it makes rather than as `wrapped`, which would read fn set_ptr(self, ptr: WeakWidget<WidgetPtr>, state: &mut Rsc) {
// as the text setting. `widget_trait!` takes no attributes, so what it is let id = self.add_strong(state);
// for is on `Wrapper` itself. state.ui_mut().widgets[ptr].inner = Some(id);
fn wrapper(self) -> impl WidgetFn<Rsc, Wrapper> {
|state| Wrapper {
inner: Some(self.add_strong(state)),
}
} }
} }
pub trait CoreWidgetArr<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> { pub trait CoreWidgetArr<Children, Rsc, Tag, GTag> {
fn span(self, dir: Dir) -> SpanBuilder<Rsc, LEN, Wa, Tag>; fn span(self, dir: Dir) -> SpanBuilder<Children, Rsc, Tag, GTag>;
fn stack(self) -> StackBuilder<Rsc, LEN, Wa, Tag>; fn stack(self) -> StackBuilder<Children, Rsc, Tag, GTag>;
} }
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag>
CoreWidgetArr<State, LEN, Wa, Tag> for Wa CoreWidgetArr<Children, Rsc, Tag, GTag> for Children
{ {
fn span(self, dir: Dir) -> SpanBuilder<State, LEN, Wa, Tag> { fn span(self, dir: Dir) -> SpanBuilder<Children, Rsc, Tag, GTag> {
SpanBuilder::new(self, dir) SpanBuilder::new(self, dir)
} }
fn stack(self) -> StackBuilder<State, LEN, Wa, Tag> { fn stack(self) -> StackBuilder<Children, Rsc, Tag, GTag> {
StackBuilder::new(self) StackBuilder::new(self)
} }
} }
pub trait RscFnMap<Rsc> {
type Input;
fn rsc_map<O>(
self,
f: impl Fn(Self::Input, &mut Rsc) -> O + Clone,
) -> impl Iterator<Item = impl FnOnce(&mut Rsc) -> O>;
}
impl<I: IntoIterator, Rsc> RscFnMap<Rsc> for I {
type Input = I::Item;
fn rsc_map<O>(
self,
f: impl Fn(Self::Input, &mut Rsc) -> O + Clone,
) -> impl Iterator<Item = impl FnOnce(&mut Rsc) -> O> {
self.into_iter().map(move |i| {
let f = f.clone();
move |rsc: &mut Rsc| f(i, rsc)
})
}
}
pub trait WidgetFnMap<Rsc: UiRsc> {
fn widget_map<O: WidgetLike<Rsc, Tag>, Tag>(
self,
f: impl Fn(WeakWidget) -> O + Clone,
) -> impl Iterator<Item = impl FnOnce(&mut Rsc) -> WeakWidget>;
}
impl<I: IntoIterator, Rsc: UiRsc> WidgetFnMap<Rsc> for I
where
I::Item: WidgetIdFn<Rsc>,
{
fn widget_map<O: WidgetLike<Rsc, Tag>, Tag>(
self,
f: impl Fn(WeakWidget) -> O + Clone,
) -> impl Iterator<Item = impl FnOnce(&mut Rsc) -> WeakWidget> {
self.into_iter().map(move |f2| {
let f = f.clone();
move |rsc: &mut Rsc| f(f2(rsc)).add(rsc) as WeakWidget
})
}
}
-50
View File
@@ -1,50 +0,0 @@
use crate::prelude::*;
use std::marker::Unsize;
/// One widget in a box of its own, doing as little as possible on the way:
/// it draws its child in the whole of its box and reports back what the child
/// said. It exists because a length and an alignment are properties of one
/// widget, so a widget cannot both be 100 wide and take two shares of a row
/// -- the two lengths need two widgets, and this is the smaller one.
///
/// Its child is optional so it can also be the swappable slot a tab bar
/// needs, which is what it was written for.
pub struct Wrapper {
pub inner: Option<StrongWidget>,
}
impl Widget for Wrapper {
fn draw(&mut self, painter: &mut Painter) -> Size {
match &self.inner {
Some(id) => painter.widget(id).size(),
None => Size::default(),
}
}
}
impl Wrapper {
pub fn new() -> Self {
Self::default()
}
pub fn empty() -> Self {
Self {
inner: Default::default(),
}
}
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) {
self.inner = Some(to)
}
pub fn replace<W: ?Sized + Unsize<dyn Widget>>(
&mut self,
to: StrongWidget<W>,
) -> Option<StrongWidget> {
self.inner.replace(to)
}
}
impl Default for Wrapper {
fn default() -> Self {
Self::empty()
}
}
-103
View File
@@ -1,103 +0,0 @@
//! A measurement that decides control flow.
//!
//! Comparing boxes catches a widget that moved. It does not catch a widget
//! that measured a child, believed a different answer from the one a cold
//! start would give, and took the other branch -- which is the same defect
//! arriving somewhere it cannot be ignored. A widget here branches on what it
//! measured, so a disagreement shows up as a different tree.
use iris::harness::Harness;
use iris::prelude::*;
/// Measures `probe` across `axis` and draws one of two children on the
/// answer. Its own configuration never changes, so which child is drawn is a
/// property of the layout alone.
struct BranchesOnMeasurement {
probe: StrongWidget,
wide: StrongWidget,
narrow: StrongWidget,
threshold: f32,
}
impl Widget for BranchesOnMeasurement {
fn draw(&mut self, painter: &mut Painter) -> Size {
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = PlaceDescAxis::shifted(UiSpan::new(Len::ZERO, cut));
let measured = painter
.widget_at(&self.probe, PlaceDesc::new(PlaceDescAxis::WHOLE, top))
.len(Axis::X);
let px = painter.to_px(measured.apply_leftover(), Axis::X);
let below = PlaceDescAxis::shifted(UiSpan::new(cut, painter.region_len(Axis::Y)));
let place = PlaceDesc::new(PlaceDescAxis::WHOLE, below);
match px > Px::from_f32(self.threshold) {
true => painter.widget_at(&self.wide, place),
false => painter.widget_at(&self.narrow, place),
};
Size::LEFTOVER
}
}
fn plant(h: &mut Harness, threshold: f32) -> (WidgetId, WidgetId) {
let words = "the quick brown fox jumps over the lazy dog and keeps running";
let probe = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let wide = rect(Color::RED).add(&mut h.rsc);
let narrow = rect(Color::BLUE).add(&mut h.rsc);
let branch = BranchesOnMeasurement {
probe: probe.add_strong(&mut h.rsc),
wide: wide.add_strong(&mut h.rsc),
narrow: narrow.add_strong(&mut h.rsc),
threshold,
}
.add(&mut h.rsc);
let side = rect(Color::GREEN).width(120).add(&mut h.rsc);
h.set_root((side, branch).span(Dir::RIGHT));
(wide.id(), narrow.id())
}
/// Which of the two branches drew, as a pair a test can compare.
fn taken(h: &Harness, wide: WidgetId, narrow: WidgetId) -> (bool, bool) {
(h.region(&wide).is_some(), h.region(&narrow).is_some())
}
#[test]
fn a_branch_taken_on_a_measurement_holds_across_repaints() {
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
let mut h = Harness::new((900, 600));
let (wide, narrow) = plant(&mut h, threshold);
let first = taken(&h, wide, narrow);
assert_ne!(first, (false, false), "threshold {threshold}: neither drew");
for frame in 0..4 {
h.rsc.widgets_mut().get_dyn_mut(wide);
h.rsc.widgets_mut().get_dyn_mut(narrow);
h.frame();
assert_eq!(
taken(&h, wide, narrow),
first,
"threshold {threshold}, repaint {frame}: the branch moved when nothing did"
);
}
}
}
#[test]
fn a_branch_taken_on_a_measurement_is_the_one_a_cold_start_takes() {
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
let mut warm = Harness::new((900, 600));
let (wide, narrow) = plant(&mut warm, threshold);
warm.resize((640, 480));
warm.frame();
warm.rsc.widgets_mut().get_dyn_mut(wide);
warm.frame();
let mut cold = Harness::new((640, 480));
let (cwide, cnarrow) = plant(&mut cold, threshold);
assert_eq!(
taken(&warm, wide, narrow),
taken(&cold, cwide, cnarrow),
"threshold {threshold}: warm and cold took different branches"
);
}
}
-45
View File
@@ -1,45 +0,0 @@
//! What a retained drawing costs in accuracy when it is moved instead of made
//! again. A subtree's stored regions are the only record of where it is, so a
//! move that works from the last answer rather than from the box it is now in
//! integrates its own rounding, and nothing later recomputes it. Re-expressing
//! each part as the same fraction of the new box is what keeps a long-lived
//! layout on the one a cold start produces.
use iris::harness::Harness;
use iris::prelude::*;
/// A row of a fixed height under a bar, so changing the bar's height moves the
/// row without changing the box it is given: the move path, repeatedly.
fn plant(h: &mut Harness, bar_height: f32) -> (WeakWidget<Rect>, WeakWidget<Rect>) {
let bar = rect(Color::RED).height(bar_height).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = (inner, rect(Color::GREEN)).span(Dir::RIGHT).height(100);
h.set_root((bar, row).span(Dir::DOWN));
(bar, inner)
}
/// Enough moves to pass the 0.05 physical pixels layout treats as the same
/// place, for a move that adds an offset to the last answer. Measured on this
/// fixture on 2026-09-15: adding the offset to both ends of a span shortened
/// the row by 0.071 over this many moves and by 0.712 over ten times as many,
/// growing with the count rather than settling. Placing the far end from the
/// near one instead left 0.069, because the length is re-derived either way.
const MOVES: usize = 20_000;
#[test]
fn a_subtree_moved_many_times_stays_where_a_cold_layout_puts_it() {
let mut warm = Harness::new((640, 900));
let (bar, inner) = plant(&mut warm, 40.0);
let mut height = 40.0;
for step in 0..MOVES {
height = 40.0 + (step % 300) as f32 * 0.37;
warm.set_len(bar, Axis::Y, height);
warm.frame();
}
let mut cold = Harness::new((640, 900));
let (_, cold_inner) = plant(&mut cold, height);
cold.frame();
assert_eq!(warm.region(&inner), cold.region(&cold_inner));
}
-29
View File
@@ -1,29 +0,0 @@
//! Whether measuring a widget and then giving it the length it reported is a
//! fixed point, which is what a span that sizes to its children needs.
use iris::harness::Harness;
use iris::prelude::*;
#[test]
fn a_wrapping_text_in_a_span_settles_on_one_width() {
let mut h = Harness::new((900, 600));
let words = "the quick brown fox jumps over the lazy dog and keeps on running \
until it reaches the end of a rather long line of text";
let t = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let filler = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((t, filler).span(Dir::RIGHT));
let mut widths = Vec::new();
for _ in 0..6 {
let r = h.region(&t.id()).unwrap();
widths.push(r.bot_right.x - r.top_left.x);
// Redrawing it changes nothing about the state, so nothing may move.
h.rsc.widgets_mut().get_dyn_mut(t.id());
h.frame();
}
println!("widths over six frames: {widths:?}");
assert!(
widths.windows(2).all(|w| w[0] == w[1]),
"a repaint that changed nothing moved it: {widths:?}"
);
}
-860
View File
@@ -1,860 +0,0 @@
//! Where a frame puts things, with no window to put them in.
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
/// A fixed 100 wide, and the rest of the 400 to its neighbour.
fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) {
let left = rect(Color::RED).width(100).add(&mut h.rsc);
let right = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
(left.id(), right.id())
}
#[test]
fn a_span_gives_each_child_the_width_it_asked_for() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
assert_corners!(h, left, (0, 0), (100, 200));
assert_corners!(h, right, (100, 0), (400, 200));
}
/// A span places each child in the room left after the one before, because a
/// text has to wrap at the width actually there, but the child's region is
/// the whole row. So two children asking for half each take the whole row
/// between them, however much of it was left when each was asked, and a third
/// overflows -- and a span passes its own region on unchanged, so a child of
/// a nested span asking for half asks for half of the same row.
#[test]
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
let mut h = Harness::new((400, 100));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let nested = (inner,).span(Dir::RIGHT).add(&mut h.rsc);
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
// The nested span is placed at the length it reported, and its own child
// asks for half of the row rather than half of that placement.
assert_corners!(h, nested, (200, 0), (400, 100));
assert_corners!(h, inner, (200, 0), (400, 100));
assert_corners!(h, tail, (400, 0), (500, 100));
}
/// The same fraction either way round: after a 100 px child in a 400 px row,
/// `rel(0.5)` is 100 to 300 whether the child's own rule says so or the child
/// drew half of what it was offered and reported that. Half the row, not half
/// of the 300 px left of it.
#[test]
fn a_reported_fraction_is_of_the_row_like_a_declared_one() {
let mut declaring = Harness::new((400, 100));
let head = rect(Color::RED).width(100).add(&mut declaring.rsc);
let declared = rect(Color::GREEN).width(rel(0.5)).add(&mut declaring.rsc);
declaring.set_root((head, declared).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(declaring, declared, (100, 0), (300, 100));
let mut reporting = Harness::new((400, 100));
let head = rect(Color::RED).width(100).add(&mut reporting.rsc);
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut reporting.rsc);
let reported = (inner,).span(Dir::RIGHT).add(&mut reporting.rsc);
reporting.set_root((head, reported).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(reporting, reported, (100, 0), (300, 100));
}
/// What the fraction a child reports is of and what box it is offered are
/// two different lengths, and only the first is the whole row: a text still
/// wraps at the room actually left after its neighbour, so the same
/// paragraph is taller where less of the row is left for it.
#[test]
fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
let paragraph = "Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer.";
let height_after = |head_width: i32| {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).width(head_width).add(&mut h.rsc);
let text = wtext(paragraph).size(16).wrap(true).add(&mut h.rsc);
h.set_root((head, text).span(Dir::RIGHT).width(rel(1.0)));
let region = h.region(&text).unwrap();
(region.bot_right.y - region.top_left.y).to_f32()
};
let (crowded, whole_row) = (height_after(300), height_after(0));
assert!(crowded > whole_row, "{crowded} against {whole_row}");
}
/// Padding is an inset: it narrows the frame a fraction resolves against and
/// adds itself back to the padded widget's reported length.
#[test]
fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() {
let mut h = Harness::new((400, 100));
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
// Ruled to the window: a root reporting a fraction of it is otherwise
// placed inside it by its own alignment, which is not what is under test.
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, inner, (10, 10), (200, 90));
assert_corners!(h, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (210, 0), (310, 100));
}
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer and not a setting.";
/// The worked example of what padding insets: in a 900 px row after a 24 px
/// icon, a `rel(1.0)` inside `pad(16)` is 900 - 32 and overflows the row by
/// the icon's width, while a wrapping text beside it is asked in the room
/// left, 900 - 24 - 32, and wraps there.
#[test]
fn padding_keeps_the_rel_base_distinct_from_the_room_left_in_a_row() {
let mut h = Harness::new((900, 200));
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let padded = fill.pad(16).add(&mut h.rsc);
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let fill_width = h.region(&fill).unwrap().size().x;
assert_eq!(fill_width, Px::from_int(868));
let mut h = Harness::new((900, 200));
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let padded = text.pad(16).add(&mut h.rsc);
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()];
let window = h.render.output_size().x;
let asked = active.region.x.len().to_px(window);
assert_eq!(active.rel_base.x.to_px(window), Px::from_int(868));
assert_eq!(asked, Px::from_int(844));
}
/// The other way round: a share inside padding. A slot is a length of the
/// row, which is already the padded width, so what the span decided reaches
/// the child as it stands -- taking the padding off a second time would make
/// `rel(1.0)` in the slot shorter than the slot.
#[test]
fn a_share_inside_padding_fills_the_slot_it_was_given() {
let mut h = Harness::new((900, 200));
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let first = Span {
children: vec![fill.add_strong(&mut h.rsc)],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.width(leftover(1))
.add(&mut h.rsc);
let second = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
let row = (first, second).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(row.pad(16));
assert_eq!(h.region(&first).unwrap().size().x, Px::from_int(434));
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(434));
}
/// The same padding in a share instead: the slot is 450, so both the
/// fraction and the wrap are the slot less the padding, and the two agree.
#[test]
fn padding_narrows_both_rel_base_and_box_inside_a_share() {
let mut h = Harness::new((900, 200));
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let padded = fill.pad(16).width(leftover(1)).add(&mut h.rsc);
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(418));
let mut h = Harness::new((900, 200));
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let padded = text.pad(16).width(leftover(1)).add(&mut h.rsc);
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()];
let window = h.render.output_size().x;
assert_eq!(active.rel_base.x.to_px(window), Px::from_int(418));
assert_eq!(active.region.x.len().to_px(window), Px::from_int(418));
}
#[test]
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
let mut h = Harness::new((400, 200));
let child = rect(Color::RED).height(40).add(&mut h.rsc);
let span = (child,).span(Dir::RIGHT).height(rel(1.0)).add(&mut h.rsc);
h.set_root(span);
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
}
#[test]
fn a_span_reports_its_tallest_fixed_child() {
let mut h = Harness::new((400, 200));
let short = rect(Color::RED).height(40).add(&mut h.rsc);
let tall = rect(Color::BLUE).height(70).add(&mut h.rsc);
let span = (short, tall).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(span);
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::px(70.0));
}
#[test]
fn resizing_relays_out_against_the_new_output() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
h.resize((800, 100));
assert!(h.needs_redraw());
h.frame();
assert_corners!(h, left, (0, 0), (100, 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));
// The span measures a child and then places it; listing it twice would
// move it twice. The span's own fixed total is shorter than the window,
// so the span is centred in it and everything under it carries that.
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, (150, 0), (350, 200));
h.set_len(left, Axis::X, 150);
h.frame();
assert_corners!(h, inner, (175, 0), (375, 200));
}
#[test]
fn alignment_accepts_an_arbitrary_fraction_and_changes_at_runtime() {
let mut h = Harness::new((400, 200));
let fixed = rect(Color::BLUE).sized((100, 100)).add(&mut h.rsc);
h.rsc
.widgets_mut()
.set_alignment(fixed, Axis::X, AxisAlign::new(0.25));
h.rsc
.widgets_mut()
.set_alignment(fixed, Axis::Y, AxisAlign::NEG);
h.set_root(fixed);
assert_corners!(h, fixed, (75, 0), (175, 100));
h.rsc
.widgets_mut()
.set_alignment(fixed, Axis::X, AxisAlign::new(0.75));
h.frame();
assert_corners!(h, fixed, (225, 0), (325, 100));
}
#[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 first asked in
// the whole box and then given 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.set_len(leaf, Axis::Y, 250);
h.frame();
assert_corners!(h, panel, (0, 0), (400, 250));
}
#[test]
fn a_moved_subtree_takes_its_children_with_it() {
let mut h = Harness::new((400, 400));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = inner.pad(10).height(40).region_node().add(&mut h.rsc);
// 80 of fixed rows in a 400 window, so the span takes 80 and sits in the
// middle of what it was given.
h.set_root((first, row).span(Dir::DOWN));
assert_corners!(h, inner, (10, 210), (390, 230));
h.set_len(first, Axis::Y, 80);
h.frame();
// The row opted into one movable region, so its descendants follow one
// entry rather than having their primitive regions rewritten.
assert_corners!(h, inner, (10, 230), (390, 250));
}
#[test]
fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() {
let mut h = Harness::new((400, 200));
let fixed = rect(Color::BLUE).width(50).add(&mut h.rsc);
let leftover = rect(Color::GREEN).add(&mut h.rsc);
let panel = (fixed, leftover).span(Dir::RIGHT).add(&mut h.rsc);
// Changing the bar's width is the only thing that changes the box the
// panel and everything under it was drawn for.
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, panel).span(Dir::RIGHT));
assert_corners!(h, fixed, (100, 0), (150, 200));
assert_corners!(h, leftover, (150, 0), (400, 200));
h.set_len(bar, Axis::X, 200);
h.frame();
// The panel's box is 100 shorter, so the fixed child is the same 50 wide
// against its new start and the one taking what is left absorbs the change.
assert_corners!(h, fixed, (200, 0), (250, 200));
assert_corners!(h, leftover, (250, 0), (400, 200));
}
#[test]
fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
let mut h = Harness::new((400, 200));
// The row is 40 tall whatever happens, which used to make its drawing
// impossible to take out of: recovering a fraction of a box needs a
// relative extent, and it has none on that axis.
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = inner.pad(10).height(40).add(&mut h.rsc);
let filler = rect(Color::GREEN).add(&mut h.rsc);
// This column is an item in a row, so it takes the width left for it
// rather than asking for a full row-width in addition to the bar.
let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, column).span(Dir::RIGHT));
assert_corners!(h, inner, (110, 10), (390, 30));
h.set_len(bar, Axis::X, 200);
h.frame();
assert_corners!(h, inner, (210, 10), (390, 30));
}
#[test]
fn only_a_region_node_lengthens_the_chain_and_it_can_be_removed() {
let mut h = Harness::new((400, 200));
let leaf = rect(Color::BLUE).add(&mut h.rsc);
let buried = leaf.pad(4).pad(4).pad(4).pad(4).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, buried).span(Dir::RIGHT));
let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 0, "the window is no entry");
h.rsc.widgets_mut().set_region_node(buried, true);
h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(
h.render.moves.depth(move_idx),
1,
"the opted-in widget's region alone"
);
h.rsc.widgets_mut().set_region_node(buried, false);
h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 0);
}
/// A span that sizes from its children passes their `leftover` weight up
/// than collapsing it to one share, so nesting divides the same space instead
/// of re-dividing a share of it.
#[test]
fn nested_spans_divide_the_space_once_however_deep_the_nesting_is() {
let mut h = Harness::new((400, 200));
let (a, b, c, d) = (
rect(Color::RED).add(&mut h.rsc),
rect(Color::BLUE).add(&mut h.rsc),
rect(Color::GREEN).add(&mut h.rsc),
rect(Color::WHITE).add(&mut h.rsc),
);
let left = (a, b).span(Dir::RIGHT).add(&mut h.rsc);
let right = (c, d).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
for (i, id) in [a, b, c, d].into_iter().enumerate() {
let x = i as f32 * 100.0;
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
}
}
/// The same space, unevenly nested: weights carried up mean a share is a
/// share of the whole, not of whatever branch a widget happens to sit in.
///
/// Each edge lands on the even division or one step below it, since a share
/// is a fraction of the room and a truncating multiply gives up what that
/// fraction does not divide. What stays exact is that each share starts
/// where the last one ended and the row ends at its own edge.
#[test]
fn an_uneven_nesting_still_gives_every_share_the_same_length() {
let mut h = Harness::new((400, 200));
let (a, b, c, d) = (
rect(Color::RED).add(&mut h.rsc),
rect(Color::BLUE).add(&mut h.rsc),
rect(Color::GREEN).add(&mut h.rsc),
rect(Color::WHITE).add(&mut h.rsc),
);
let one = (a,).span(Dir::RIGHT).add(&mut h.rsc);
let three = (b, c, d).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((one, three).span(Dir::RIGHT));
let mut start = Px::ZERO;
for (i, id) in [a, b, c, d].into_iter().enumerate() {
let got = h.region(&id).expect("widget drew nothing");
let even = Px::from_int((i as i32 + 1) * 100);
assert_eq!(got.top_left, PxVec2::new(start, Px::ZERO), "share {i}");
assert_eq!(got.bot_right.y, Px::from_int(200), "share {i}");
assert!(
got.bot_right.x == even || got.bot_right.x == even.next_down(),
"share {i} ends at {:?}, not {even:?}",
got.bot_right.x
);
start = got.bot_right.x;
}
assert_eq!(
start,
Px::from_int(400),
"the row stopped short of its edge"
);
}
/// However many ways a row is divided, the shares add up to the row: each
/// one is the fixed parts before it plus a share of the room, rather than a
/// step from where the last one ended, so the roundings do not accumulate
/// along it. Chained, two hundred of them ended a step short of the edge.
#[test]
fn a_row_of_equal_shares_fills_it_exactly() {
for n in [2usize, 3, 7, 64, 200] {
let mut h = Harness::new((1000, 100));
let mut ids = Vec::new();
let mut kids: Vec<StrongWidget> = Vec::new();
for _ in 0..n {
let kid = rect(Color::RED).add(&mut h.rsc);
ids.push(kid.id());
kids.push(kid.add_strong(&mut h.rsc));
}
let span = Span {
children: kids,
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.set_root(span);
h.frame();
for (i, id) in ids.iter().enumerate() {
let at = h.region(id).expect("a share drew nothing").top_left.x;
let want = Px::from_f32(1000.0 * (i as f32) / (n as f32));
assert!(
(at - want).abs() <= Px::STEP,
"{n} shares: the {i}th starts at {at:?}, not {want:?}"
);
}
let end = h.region(ids.last().unwrap()).unwrap().bot_right.x;
assert_eq!(end, Px::from_int(1000), "{n} shares do not reach the edge");
}
}
/// Where the shader puts an edge: the fraction resolved against the window
/// plus the pixel offset, taken to the boundary it composes to within half
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id];
let region = h.render.moves.resolve(active.move_idx, active.placement);
let dim = h.size().axis(axis);
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
let span = region.axis(axis);
(edge(span.start), edge(span.end))
}
fn hairline(h: &mut Harness, marks: &mut Vec<WidgetId>) -> StrongWidget {
let mark = rect(Color::RED).width(1).add_strong(&mut h.rsc);
marks.push(mark.id());
mark
}
fn share(h: &mut Harness, inner: StrongWidget, ratio: f32) -> StrongWidget {
h.set_len(&inner, Axis::X, LayoutLen::leftover(ratio));
inner
}
/// Shares in weights no binary fraction lands on, a padding on one branch
/// and not the other, so an edge falls near an integer as often as it can.
fn hairlines(h: &mut Harness, depth: usize, marks: &mut Vec<WidgetId>) -> StrongWidget {
let mut span = Span::empty(Dir::RIGHT);
if depth == 0 {
let left = rect(Color::BLUE).add_strong(&mut h.rsc);
let left = share(h, left, 3.0);
span.push(left);
let mark = hairline(h, marks);
span.push(mark);
let right = rect(Color::BLUE).add_strong(&mut h.rsc);
let right = share(h, right, 7.0);
span.push(right);
return span.add_strong(&mut h.rsc);
}
let first = hairlines(h, depth - 1, marks);
let first = share(h, first, 3.0);
span.push(first);
let second = hairlines(h, depth - 1, marks);
let second = Pad {
padding: Padding {
left: Px::from_int(3),
right: Px::from_int(7),
top: Px::ZERO,
bottom: Px::ZERO,
},
inner: second,
}
.add_strong(&mut h.rsc);
let second = share(h, second, 5.0);
span.push(second);
span.add_strong(&mut h.rsc)
}
/// A one-pixel line is a pixel wherever it is drawn. Both edges of a fixed
/// length share their box's fraction, so composing the chain moves them
/// together and the shader's `floor` cannot round the pixel between them
/// away -- only shift it. A separator that disappeared at one window size
/// would be a defect no size comparison catches.
#[test]
fn a_one_pixel_line_keeps_its_pixel_through_a_chain() {
let mut h = Harness::new((1920, 1200));
let mut marks = Vec::new();
let root = hairlines(&mut h, 4, &mut marks);
h.state.set_root(root);
h.frame();
assert_eq!(marks.len(), 16);
for size in [(1920, 1200), (1919, 1201), (997, 1003), (1367, 733)] {
h.resize(size);
h.frame();
for mark in &marks {
let (start, end) = drawn_edges(&h, *mark, Axis::X);
assert_eq!(end - start, 1.0, "at {size:?}, mark {mark:?}");
}
}
}
/// A span short of room takes it from its shares, which go to nothing and
/// then to nothing wider; the fixed lengths between them keep their pixels.
/// Collapsing those to make room would delete a separator the caller asked
/// for, which is worse than overflowing.
#[test]
fn a_span_out_of_room_shrinks_its_shares_and_not_its_fixed_lengths() {
let mut h = Harness::new((400, 20));
let mut marks = Vec::new();
let mut span = Span::empty(Dir::RIGHT);
for _ in 0..3 {
let share_of = rect(Color::BLUE).add_strong(&mut h.rsc);
let share_of = share(&mut h, share_of, 1.0);
span.push(share_of);
let mark = hairline(&mut h, &mut marks);
span.push(mark);
}
let root = span.add_strong(&mut h.rsc);
h.state.set_root(root);
h.frame();
for width in [400, 10, 3, 1] {
h.resize((width, 20));
h.frame();
for mark in &marks {
let (start, end) = drawn_edges(&h, *mark, Axis::X);
assert_eq!(end - start, 1.0, "at {width} wide, mark {mark:?}");
}
}
}
#[test]
fn only_a_pure_leftover_child_disappears_when_nothing_is_left() {
let mut h = Harness::new((100, 20));
let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
let leftover = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((fixed, leftover).span(Dir::RIGHT));
assert_corners!(h, fixed, (0, 0), (100, 20));
assert_eq!(h.region(&leftover), None);
// An undrawn child remains a dependency of the span, so making room for
// it draws it without rebuilding the tree.
h.set_len(fixed, Axis::X, 60);
h.frame();
assert_corners!(h, leftover, (60, 0), (100, 20));
let mut h = Harness::new((100, 20));
let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
let mixed = rect(Color::BLUE)
.width(LayoutLen::px(20) + LayoutLen::LEFTOVER)
.add(&mut h.rsc);
h.set_root((fixed, mixed).span(Dir::RIGHT));
// Pixels and fractions still overflow; only a child whose entire length
// is leftover is omitted.
assert_corners!(h, mixed, (100, 0), (120, 20));
}
#[test]
fn leftover_children_disappear_at_the_exact_fixed_content_boundary() {
let mut h = Harness::new((100, 100));
let first = rect(Color::RED).height(90).add(&mut h.rsc);
let a = rect(Color::GREEN).add(&mut h.rsc);
let b = rect(Color::BLUE).add(&mut h.rsc);
let inner = (a, b).span(Dir::DOWN).gap(4).add(&mut h.rsc);
h.set_root((first, inner).span(Dir::DOWN));
assert!(h.region(&a).is_some());
assert!(h.region(&b).is_some());
h.set_len(first, Axis::Y, 96.0);
h.frame();
assert!(h.region(&a).is_none());
assert!(h.region(&b).is_none());
}
/// **A stack child smaller than the stack sits where its own alignment
/// says.** `Stack` gives every child the box its sizing child defines and
/// used to force the near edge on all of them; that override is owed only to
/// the sizing child, which has already placed its own content in the box the
/// stack derived from its answer. Every other child is handed a box that owes
/// nothing to it, so where it sits in one bigger than itself is its own
/// business -- and with the override it could not be aligned at all, which is
/// what moved the `tabs` example's counters to the wrong corner.
#[test]
fn a_stack_child_smaller_than_the_stack_keeps_its_own_alignment() {
let mut h = Harness::new((400, 200));
let big = rect(Color::BLUE).add(&mut h.rsc);
let small = rect(Color::RED).sized((50, 50)).add(&mut h.rsc);
h.rsc
.widgets_mut()
.set_alignment(small.id(), Axis::X, AxisAlign::POS);
let (a, b) = (big.add_strong(&mut h.rsc), small.add_strong(&mut h.rsc));
let children: Vec<StrongWidget> = vec![a, b];
h.set_root(Stack {
children,
size: StackSize::Default,
});
assert_corners!(h, big, (0, 0), (400, 200));
// The far edge on X because it asked for it, the middle on Y because
// that is the default.
assert_corners!(h, small, (350, 75), (400, 125));
}
/// Five children of one span, buried under three containers that are each a
/// fraction of their parent so no length reaches the window without being
/// composed and rounded on the way. Returns each child's drawn width and
/// each gap between them, in pixels.
fn row_under_fractions(kid: Option<LayoutLen>, gap: f32, box_w: f32) -> (Vec<Px>, Vec<Px>) {
let mut h = Harness::new((box_w, 400.0));
let mut ids = Vec::new();
let mut kids: Vec<StrongWidget> = Vec::new();
for _ in 0..5 {
let r = rect(Color::RED).add(&mut h.rsc);
if let Some(len) = kid {
h.rsc
.widgets_mut()
.set_size_rule(r.id(), Axis::X, SizeRule::Exact(len));
}
ids.push(r.id());
kids.push(r.add_strong(&mut h.rsc));
}
let span = Span {
children: kids,
dir: Dir::RIGHT,
gap: Px::from_f32(gap),
}
.add(&mut h.rsc);
let a = (span.width(rel(0.9)),).span(Dir::RIGHT).add(&mut h.rsc);
let b = (a.width(rel(0.8)),).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((b.width(rel(0.7)),).span(Dir::RIGHT));
let boxes: Vec<_> = ids
.iter()
.map(|id| h.region(id).expect("a child drew nothing"))
.collect();
(
boxes.iter().map(|b| b.bot_right.x - b.top_left.x).collect(),
boxes
.windows(2)
.map(|p| p[1].top_left.x - p[0].bot_right.x)
.collect(),
)
}
/// **A length given in pixels is that many pixels, wherever it ends up.** A
/// gap and a declared width compose additively -- `Len::within` adds a part's
/// own pixels rather than scaling them, and both ends of a gap carry the same
/// fraction, so the multiply that rounds is the same on each -- which is why
/// nesting the row inside fractions of fractions cannot move them. Swept over
/// 2,100 box widths when this was written and exact at every one; five here,
/// including widths that divide badly by five.
#[test]
fn a_length_in_pixels_is_that_many_pixels_however_it_is_nested() {
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
let want = Px::from_int(7);
let (_, gaps) = row_under_fractions(None, 7.0, box_w);
assert!(
gaps.iter().all(|g| *g == want),
"box {box_w}: gaps between leftover children are {gaps:?}"
);
let (widths, gaps) = row_under_fractions(Some(LayoutLen::px(100.0)), 7.0, box_w);
assert!(
gaps.iter().all(|g| *g == want),
"box {box_w}: gaps between fixed children are {gaps:?}"
);
assert!(
widths.iter().all(|w| *w == Px::from_int(100)),
"box {box_w}: declared widths came out {widths:?}"
);
}
}
/// **Children asking for the same share of a row are not the same length**,
/// and this pins by how much rather than claiming they are equal. A position
/// is the quantity that gets rounded, so the row fills exactly and no two
/// children leave a seam; what that costs is a step or two between lengths
/// that were asked for identically. Exact composition would shrink the
/// spread, not remove it: five equal lengths cannot fill a row whose step
/// count is not a multiple of five.
#[test]
fn equal_shares_differ_by_at_most_two_steps_and_fill_the_row() {
for kid in [None, Some(LayoutLen::rel(0.2))] {
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
let (widths, gaps) = row_under_fractions(kid, 0.0, box_w);
let spread = *widths.iter().max().unwrap() - *widths.iter().min().unwrap();
assert!(
spread <= Px::from_raw(2),
"box {box_w}, {kid:?}: widths {widths:?} spread {spread:?}"
);
assert!(
gaps.iter().all(|g| *g == Px::ZERO),
"box {box_w}, {kid:?}: children left seams {gaps:?}"
);
}
}
}
#[test]
fn a_stack_sized_by_a_child_does_not_take_that_childs_fraction_twice() {
let mut h = Harness::new((400, 200));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let behind = rect(Color::BLUE).add(&mut h.rsc);
let stack = Stack {
children: vec![behind.add_strong(&mut h.rsc), half.add_strong(&mut h.rsc)],
size: StackSize::Child(1),
}
.add(&mut h.rsc);
h.set_root((stack,).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, stack, (0, 0), (200, 200));
assert_corners!(h, half, (0, 0), (200, 200));
assert_corners!(h, behind, (0, 0), (200, 200));
}
#[test]
fn a_fixed_child_is_centered_in_its_wrappers_share() {
let mut h = Harness::new((600, 300));
let leaf = rect(Color::RED).sized((100, 100)).center().add(&mut h.rsc);
let wrapper = leaf
.wrapper()
.width(leftover(2))
.height(rel(1.0))
.add(&mut h.rsc);
let other = rect(Color::BLUE).width(200).add(&mut h.rsc);
h.set_root((other, wrapper).span(Dir::RIGHT));
assert_corners!(h, wrapper, (200, 0), (600, 300));
assert_corners!(h, leaf, (350, 100), (450, 200));
h.resize((900, 400));
h.frame();
assert_corners!(h, wrapper, (200, 0), (900, 400));
assert_corners!(h, leaf, (500, 150), (600, 250));
}
/// The root's frame is the window and its rule is a fraction of that, which
/// is one resolution and not two: nothing above it narrowed anything.
#[test]
fn a_root_with_a_fraction_rule_is_that_fraction_of_the_window() {
let mut h = Harness::new((900, 200));
let root = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
h.set_root(root);
assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450));
}
#[test]
fn a_collapsed_share_keeps_the_gaps_before_the_next_slot() {
for dir in [Dir::RIGHT, Dir::LEFT, Dir::DOWN, Dir::UP] {
for collapsed in [1, 2] {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).add(&mut h.rsc);
h.set_len(head, dir.axis, 200);
let tail = rect(Color::BLUE).add(&mut h.rsc);
let tail_len = 200 - 10 * (collapsed + 1);
h.set_len(tail, dir.axis, tail_len);
let mut children: Vec<StrongWidget> = vec![head.add_strong(&mut h.rsc)];
let mut shares = Vec::new();
for _ in 0..collapsed {
let share = rect(Color::GREEN).add(&mut h.rsc);
shares.push(share);
children.push(share.add_strong(&mut h.rsc));
}
children.push(tail.add_strong(&mut h.rsc));
h.set_root(Span {
children,
dir,
gap: Px::from_int(10),
});
for share in shares {
assert!(h.region(&share).is_none());
}
let region = h.region(&tail).unwrap();
let (from, to) = match dir.sign {
Sign::Pos => (400 - tail_len, 400),
Sign::Neg => (0, tail_len),
};
assert_eq!(region.top_left.axis(dir.axis), Px::from_int(from));
assert_eq!(region.bot_right.axis(dir.axis), Px::from_int(to));
}
}
}
-121
View File
@@ -1,121 +0,0 @@
//! The tree a seed describes, as a value rather than as widgets.
//!
//! Two things have to hold for a plan to be worth having. Editing a plan has
//! to mean what growing with those edits means, or a scenario reads one thing
//! and the oracle another. And reducing a plan has to end, or a shrinker
//! searching for the smallest counterexample never returns.
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, plan};
use std::collections::HashMap;
fn some_edits(seed: u64, of: &Plan) -> Edits {
let mut rng = Rng::new(seed);
let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0);
let mut of = of.clone();
of.walk_mut(&mut |p| {
if matches!(p.kind, Kind::Span { .. }) {
spans += 1;
}
sized += p.size.is_some() as usize;
aligned += p.align.is_some() as usize;
nodes += p.region_node.is_some() as usize;
});
let pick =
|n: usize, rng: &mut Rng| -> Vec<usize> { (0..n).filter(|_| rng.chance()).collect() };
Edits {
sizes: pick(sized, &mut rng)
.into_iter()
.map(|i| (i, [Some(LayoutLen::LEFTOVER), None]))
.collect(),
aligns: pick(aligned, &mut rng)
.into_iter()
.map(|i| (i, [Some(AxisAlign::POS), None]))
.collect(),
nodes: pick(nodes, &mut rng)
.into_iter()
.map(|i| (i, true))
.collect(),
spans: pick(spans, &mut rng)
.into_iter()
.map(|i| {
(
i,
SpanEdit {
detach: vec![0],
attach: 2,
},
)
})
.collect::<HashMap<_, _>>(),
fixed_branches: false,
}
}
use iris::prelude::*;
/// The two routes to an edited tree are one tree. `plan` resolves edits out
/// of the random stream as it draws; `edited` puts them on a tree that
/// already exists, which is the only route a shrunk plan has, since no seed
/// grows one. A scenario written against either has to read the same.
#[test]
fn editing_a_plan_is_growing_one_with_those_edits() {
for seed in 1..=60 {
let bare = plan(seed, 5, &Edits::default());
let edits = some_edits(seed, &bare);
assert_eq!(
bare.edited(&edits),
plan(seed, 5, &edits),
"seed {seed}: edited and grown-with-edits disagree"
);
}
}
/// Every simplification is strictly smaller, so taking them in turn reaches a
/// fixed point instead of circling. A shrinker that can return to a tree it
/// has already tried does not stop.
#[test]
fn every_simplification_of_a_plan_is_smaller_than_it() {
for seed in 1..=60 {
let tree = plan(seed, 4, &Edits::default());
let mut queue = vec![tree];
let mut seen = 0;
while let Some(node) = queue.pop() {
seen += 1;
if seen > 400 {
break;
}
for small in node.smaller() {
assert!(
small.size() <= node.size(),
"seed {seed}: a simplification grew from {} to {}",
node.size(),
small.size()
);
if small.size() < node.size() {
queue.push(small);
}
}
}
}
}
/// Reducing until nothing reduces ends, and ends at something small enough to
/// read rather than at the tree it started from.
#[test]
fn reducing_a_plan_all_the_way_ends() {
for seed in 1..=30 {
let mut node = plan(seed, 5, &Edits::default());
let grown = node.size();
let mut steps = 0;
while let Some(next) = node.smaller().into_iter().next() {
node = next;
steps += 1;
assert!(steps < 10_000, "seed {seed}: reducing did not end");
}
assert!(
node.size() < grown.max(2),
"seed {seed}: reduced {grown} widgets to {}",
node.size()
);
}
}
-85
View File
@@ -1,85 +0,0 @@
//! Which widget an input reaches.
use std::{cell::RefCell, rc::Rc};
use iris::harness::{Harness, TouchScript};
use iris::prelude::*;
#[test]
fn a_press_reaches_only_the_widget_under_the_cursor() {
let mut h = Harness::new((400, 200));
let clicks = Rc::new(RefCell::new(Vec::new()));
let (on_left, on_right) = (clicks.clone(), clicks.clone());
let left = rect(Color::RED)
.width(100)
.on(CursorSense::click(), move |_, _| {
on_left.borrow_mut().push("left")
})
.add(&mut h.rsc);
let right = rect(Color::BLUE)
.on(CursorSense::click(), move |_, _| {
on_right.borrow_mut().push("right")
})
.add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
h.click((50, 100));
assert_eq!(*clicks.borrow(), ["left"]);
h.click((300, 100));
assert_eq!(*clicks.borrow(), ["left", "right"]);
}
#[test]
fn hover_ends_when_the_cursor_leaves_the_window() {
let mut h = Harness::new((400, 200));
let hovered = Rc::new(RefCell::new(0));
let ended = Rc::new(RefCell::new(0));
let (h_count, e_count) = (hovered.clone(), ended.clone());
let widget = rect(Color::RED)
.on(CursorSense::HoverStart, move |_, _| {
*h_count.borrow_mut() += 1
})
.on(CursorSense::HoverEnd, move |_, _| {
*e_count.borrow_mut() += 1
})
.add(&mut h.rsc);
h.set_root(widget);
h.move_to((200, 100));
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
// A second sample inside the same widget is not a second hover.
h.move_to((210, 100));
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
h.leave();
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1));
}
#[test]
fn a_recorded_gesture_presses_where_it_says() {
let mut h = Harness::new((400, 200));
let clicks = Rc::new(RefCell::new(Vec::new()));
let (on_left, on_right) = (clicks.clone(), clicks.clone());
let left = rect(Color::RED)
.width(100)
.on(CursorSense::click(), move |_, _| {
on_left.borrow_mut().push("left")
})
.add(&mut h.rsc);
let right = rect(Color::BLUE)
.on(CursorSense::click(), move |_, _| {
on_right.borrow_mut().push("right")
})
.add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
let script = TouchScript::parse("0 down 300 100\n80 up 300 100").unwrap();
h.replay(&script);
assert_eq!(*clicks.borrow(), ["right"]);
}
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more