Compare commits
9
Commits
9644971daf
..
work
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa771a99eb | ||
|
|
e817bc83af | ||
|
|
a648c62aa2 | ||
|
|
c118bb446b | ||
|
|
1102dc7338 | ||
|
|
1aadef0e7e | ||
|
|
426ff0adfc | ||
|
|
dab6cf298a | ||
|
|
38d896d44d |
No files matched your search
Generated
+598
-761
File diff suppressed because it is too large.
Load diff
+8
-13
@@ -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,25 +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"]
|
||||||
|
|
||||||
[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"] }
|
|
||||||
@@ -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,6 +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..??
|
||||||
+2
-6
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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,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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,488 +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, Len, Size, UiRegion, WidgetId, util::Vec2};
|
|
||||||
use std::{
|
|
||||||
cell::RefCell,
|
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
fmt::Write,
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub(crate) enum Counter {
|
|
||||||
Updates,
|
|
||||||
ResizeDependents,
|
|
||||||
DrawRequests,
|
|
||||||
WidgetDraws,
|
|
||||||
PlaceCalls,
|
|
||||||
SizeReads,
|
|
||||||
HintHits,
|
|
||||||
HintMisses,
|
|
||||||
RetainedSizeHits,
|
|
||||||
ReuseAttempts,
|
|
||||||
ReuseExact,
|
|
||||||
ReuseMoved,
|
|
||||||
ReuseDirty,
|
|
||||||
ReuseWrongParent,
|
|
||||||
ReuseUnslotted,
|
|
||||||
ReuseOwnResize,
|
|
||||||
ReuseDescendantResize,
|
|
||||||
ResizeChecks,
|
|
||||||
ResizeCheckChildren,
|
|
||||||
QueuePops,
|
|
||||||
DepthReads,
|
|
||||||
EagerReaderRedraws,
|
|
||||||
LocalRedraws,
|
|
||||||
SizeChanges,
|
|
||||||
ReaderEdges,
|
|
||||||
PrimitiveWrites,
|
|
||||||
TextRenders,
|
|
||||||
TextShapeHits,
|
|
||||||
TextShapes,
|
|
||||||
TextBreaks,
|
|
||||||
GlyphPlacements,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Counter {
|
|
||||||
const COUNT: usize = Self::GlyphPlacements as usize + 1;
|
|
||||||
|
|
||||||
const NAMES: [&'static str; Self::COUNT] = [
|
|
||||||
"updates",
|
|
||||||
"resize dependents",
|
|
||||||
"draw requests",
|
|
||||||
"widget draws",
|
|
||||||
"place calls",
|
|
||||||
"draw-result size reads",
|
|
||||||
"hint hits",
|
|
||||||
"hint misses",
|
|
||||||
"retained size hits",
|
|
||||||
"reuse attempts",
|
|
||||||
"reuse exact",
|
|
||||||
"reuse moved",
|
|
||||||
"reuse: dirty",
|
|
||||||
"reuse: wrong parent",
|
|
||||||
"reuse: unslotted",
|
|
||||||
"reuse: own resize",
|
|
||||||
"reuse: descendant resize",
|
|
||||||
"resize checks",
|
|
||||||
"resize children checked",
|
|
||||||
"redraw queue pops",
|
|
||||||
"depth reads",
|
|
||||||
"eager reader redraws",
|
|
||||||
"local redraws",
|
|
||||||
"size changes",
|
|
||||||
"reader edges",
|
|
||||||
"primitive writes",
|
|
||||||
"text renders",
|
|
||||||
"text shape hits",
|
|
||||||
"text shapes",
|
|
||||||
"text line breaks",
|
|
||||||
"glyph placements",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub(crate) enum TimerKind {
|
|
||||||
Update,
|
|
||||||
FullLayout,
|
|
||||||
ResizeMarking,
|
|
||||||
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",
|
|
||||||
"resize marking",
|
|
||||||
"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,
|
|
||||||
Unslotted,
|
|
||||||
OwnResize,
|
|
||||||
DescendantResize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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: Vec2,
|
|
||||||
slotted: bool,
|
|
||||||
},
|
|
||||||
Reuse {
|
|
||||||
id: WidgetId,
|
|
||||||
outcome: ReuseOutcome,
|
|
||||||
},
|
|
||||||
SizeReported {
|
|
||||||
id: WidgetId,
|
|
||||||
size: Size,
|
|
||||||
},
|
|
||||||
Placed {
|
|
||||||
id: WidgetId,
|
|
||||||
parent: WidgetId,
|
|
||||||
region: UiRegion,
|
|
||||||
},
|
|
||||||
SizeRead {
|
|
||||||
id: WidgetId,
|
|
||||||
reader: WidgetId,
|
|
||||||
size: Size,
|
|
||||||
},
|
|
||||||
HintRead {
|
|
||||||
id: WidgetId,
|
|
||||||
reader: WidgetId,
|
|
||||||
axis: Axis,
|
|
||||||
hint: Option<Len>,
|
|
||||||
},
|
|
||||||
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: Vec2,
|
|
||||||
slotted: bool,
|
|
||||||
) {
|
|
||||||
trace(
|
|
||||||
id,
|
|
||||||
TraceEvent::DrawRequest {
|
|
||||||
id,
|
|
||||||
parent,
|
|
||||||
region,
|
|
||||||
pixel_size,
|
|
||||||
slotted,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 placed(id: WidgetId, parent: WidgetId, region: UiRegion) {
|
|
||||||
trace(id, TraceEvent::Placed { 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<Len>) {
|
|
||||||
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(¤t.widgets);
|
|
||||||
current.report.hot_text = hottest(¤t.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
-3
@@ -5,14 +5,12 @@
|
|||||||
#![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 num;
|
mod num;
|
||||||
|
|||||||
@@ -144,10 +144,10 @@ 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 mut start = UiScalar::rel(rel);
|
let mut start = UiScalar::rel(rel);
|
||||||
start.px -= self.px * rel;
|
start.abs -= self.abs * rel;
|
||||||
start.rel -= self.rel * rel;
|
start.rel -= self.rel * rel;
|
||||||
let mut end = UiScalar::rel(rel);
|
let mut end = UiScalar::rel(rel);
|
||||||
end.px += self.px * (1.0 - rel);
|
end.abs += self.abs * (1.0 - rel);
|
||||||
end.rel += self.rel * (1.0 - rel);
|
end.rel += self.rel * (1.0 - rel);
|
||||||
UiSpan { start, end }
|
UiSpan { start, end }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
pub enum Axis {
|
pub enum Axis {
|
||||||
X,
|
X,
|
||||||
Y,
|
Y,
|
||||||
|
|||||||
+20
-20
@@ -9,14 +9,14 @@ pub struct Size {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Len {
|
pub struct Len {
|
||||||
pub px: f32,
|
pub abs: f32,
|
||||||
pub rel: f32,
|
pub rel: f32,
|
||||||
pub rest: f32,
|
pub rest: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<N: UiNum> From<N> for Len {
|
impl<N: UiNum> From<N> for Len {
|
||||||
fn from(value: N) -> Self {
|
fn from(value: N) -> Self {
|
||||||
Len::px(value.to_f32())
|
Len::abs(value.to_f32())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +46,10 @@ impl Size {
|
|||||||
y: Len::REST,
|
y: Len::REST,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn px(v: Vec2) -> Self {
|
pub fn abs(v: Vec2) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: Len::px(v.x),
|
x: Len::abs(v.x),
|
||||||
y: Len::px(v.y),
|
y: Len::abs(v.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,13 +97,13 @@ impl Size {
|
|||||||
|
|
||||||
impl Len {
|
impl Len {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
px: 0.0,
|
abs: 0.0,
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const REST: Self = Self {
|
pub const REST: Self = Self {
|
||||||
px: 0.0,
|
abs: 0.0,
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: 1.0,
|
rest: 1.0,
|
||||||
};
|
};
|
||||||
@@ -111,27 +111,27 @@ impl Len {
|
|||||||
pub fn apply_rest(&self) -> UiScalar {
|
pub fn apply_rest(&self) -> UiScalar {
|
||||||
UiScalar {
|
UiScalar {
|
||||||
rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
|
rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
|
||||||
px: self.px,
|
abs: self.abs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn px(px: impl UiNum) -> Self {
|
pub fn abs(abs: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
px: px.to_f32(),
|
abs: abs.to_f32(),
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rel(rel: impl UiNum) -> Self {
|
pub fn rel(rel: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
px: 0.0,
|
abs: 0.0,
|
||||||
rel: rel.to_f32(),
|
rel: rel.to_f32(),
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rest(ratio: impl UiNum) -> Self {
|
pub fn rest(ratio: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
px: 0.0,
|
abs: 0.0,
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: ratio.to_f32(),
|
rest: ratio.to_f32(),
|
||||||
}
|
}
|
||||||
@@ -141,31 +141,31 @@ impl Len {
|
|||||||
pub mod len_fns {
|
pub mod len_fns {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn px(px: impl UiNum) -> Len {
|
pub fn abs(abs: impl UiNum) -> Len {
|
||||||
Len {
|
Len {
|
||||||
px: px.to_f32(),
|
abs: abs.to_f32(),
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rel(rel: impl UiNum) -> Len {
|
pub fn rel(rel: impl UiNum) -> Len {
|
||||||
Len {
|
Len {
|
||||||
px: 0.0,
|
abs: 0.0,
|
||||||
rel: rel.to_f32(),
|
rel: rel.to_f32(),
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rest(ratio: impl UiNum) -> Len {
|
pub fn rest(ratio: impl UiNum) -> Len {
|
||||||
Len {
|
Len {
|
||||||
px: 0.0,
|
abs: 0.0,
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: ratio.to_f32(),
|
rest: ratio.to_f32(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_op!(Len Add add; px rel rest);
|
impl_op!(Len Add add; abs rel rest);
|
||||||
impl_op!(Len Sub sub; px rel rest);
|
impl_op!(Len Sub sub; abs rel rest);
|
||||||
|
|
||||||
impl_op!(Size Add add; x y);
|
impl_op!(Size Add add; x y);
|
||||||
impl_op!(Size Sub sub; x y);
|
impl_op!(Size Sub sub; x y);
|
||||||
@@ -184,8 +184,8 @@ impl std::fmt::Display for Size {
|
|||||||
|
|
||||||
impl std::fmt::Display for Len {
|
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 != 0.0 {
|
if self.abs != 0.0 {
|
||||||
write!(f, "{} px;", self.px)?;
|
write!(f, "{} abs;", self.abs)?;
|
||||||
}
|
}
|
||||||
if self.rel != 0.0 {
|
if self.rel != 0.0 {
|
||||||
write!(f, "{} rel;", self.rel)?;
|
write!(f, "{} rel;", self.rel)?;
|
||||||
|
|||||||
+67
-40
@@ -23,11 +23,11 @@ impl UiVec2 {
|
|||||||
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: UiScalar::px(px.x),
|
x: UiScalar::abs(abs.x),
|
||||||
y: UiScalar::px(px.y),
|
y: UiScalar::abs(abs.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +56,13 @@ impl UiVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn outside(&self, region: &UiRegion) -> UiVec2 {
|
||||||
|
UiVec2 {
|
||||||
|
x: self.x.outside(®ion.x),
|
||||||
|
y: self.y.outside(®ion.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar {
|
pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => &mut self.x,
|
Axis::X => &mut self.x,
|
||||||
@@ -70,10 +77,10 @@ impl UiVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_px(&self, rel: Vec2) -> Vec2 {
|
pub fn to_abs(&self, rel: Vec2) -> Vec2 {
|
||||||
Vec2 {
|
Vec2 {
|
||||||
x: self.x.to_px(rel.x),
|
x: self.x.to_abs(rel.x),
|
||||||
y: self.y.to_px(rel.y),
|
y: self.y.to_abs(rel.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +99,8 @@ impl UiVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_px(&self) -> Vec2 {
|
pub fn get_abs(&self) -> Vec2 {
|
||||||
(self.x.px, self.y.px).into()
|
(self.x.abs, self.y.abs).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rel(&self) -> Vec2 {
|
pub fn get_rel(&self) -> Vec2 {
|
||||||
@@ -102,15 +109,15 @@ impl UiVec2 {
|
|||||||
|
|
||||||
pub fn abs_mut(&mut self) -> Vec2View<'_> {
|
pub fn abs_mut(&mut self) -> Vec2View<'_> {
|
||||||
Vec2View {
|
Vec2View {
|
||||||
x: &mut self.x.px,
|
x: &mut self.x.abs,
|
||||||
y: &mut self.y.px,
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,8 +125,8 @@ impl_op!(UiVec2 Add add; x y);
|
|||||||
impl_op!(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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,8 +134,8 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,34 +143,34 @@ where
|
|||||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)]
|
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)]
|
||||||
pub struct UiScalar {
|
pub struct UiScalar {
|
||||||
pub rel: f32,
|
pub rel: f32,
|
||||||
pub px: f32,
|
pub abs: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for UiScalar {}
|
impl Eq for UiScalar {}
|
||||||
impl Hash for UiScalar {
|
impl Hash for UiScalar {
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
state.write_u32(self.rel.to_bits());
|
state.write_u32(self.rel.to_bits());
|
||||||
state.write_u32(self.px.to_bits());
|
state.write_u32(self.abs.to_bits());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_op!(UiScalar Add add; rel px);
|
impl_op!(UiScalar Add add; rel abs);
|
||||||
impl_op!(UiScalar Sub sub; rel px);
|
impl_op!(UiScalar Sub sub; rel abs);
|
||||||
|
|
||||||
impl UiScalar {
|
impl UiScalar {
|
||||||
pub const ZERO: Self = Self { rel: 0.0, px: 0.0 };
|
pub const ZERO: Self = Self { rel: 0.0, abs: 0.0 };
|
||||||
pub const FULL: Self = Self { rel: 1.0, px: 0.0 };
|
pub const FULL: Self = Self { rel: 1.0, abs: 0.0 };
|
||||||
|
|
||||||
pub const fn new(rel: f32, px: f32) -> Self {
|
pub const fn new(rel: f32, abs: f32) -> Self {
|
||||||
Self { rel, px }
|
Self { rel, abs }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel(rel: f32) -> Self {
|
pub const fn rel(rel: f32) -> Self {
|
||||||
Self { rel, px: 0.0 }
|
Self { rel, abs: 0.0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn px(px: f32) -> Self {
|
pub const fn abs(abs: f32) -> Self {
|
||||||
Self { rel: 0.0, px }
|
Self { rel: 0.0, abs }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel_min() -> Self {
|
pub const fn rel_min() -> Self {
|
||||||
@@ -177,31 +184,37 @@ impl UiScalar {
|
|||||||
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn offset(mut self, amt: f32) -> Self {
|
pub const fn offset(mut self, amt: f32) -> Self {
|
||||||
self.px += amt;
|
self.abs += 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 anchor = self.rel.lerp(span.start.rel, span.end.rel);
|
||||||
let offset = self.px + self.rel.lerp(span.start.px, span.end.px);
|
let offset = self.abs + self.rel.lerp(span.start.abs, span.end.abs);
|
||||||
Self {
|
Self {
|
||||||
rel: anchor,
|
rel: anchor,
|
||||||
px: offset,
|
abs: offset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
pub fn within_len(&self, len: UiScalar) -> Self {
|
||||||
self.within(&UiSpan {
|
self.within(&UiSpan {
|
||||||
start: UiScalar::ZERO,
|
start: UiScalar::ZERO,
|
||||||
@@ -215,15 +228,15 @@ impl UiScalar {
|
|||||||
|
|
||||||
pub const fn flip(&mut self) {
|
pub const fn flip(&mut self) {
|
||||||
self.rel = 1.0 - self.rel;
|
self.rel = 1.0 - self.rel;
|
||||||
self.px = -self.px;
|
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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn to_px(&self, rel: f32) -> f32 {
|
pub const fn to_abs(&self, rel: f32) -> f32 {
|
||||||
self.rel * rel + self.px
|
self.rel * rel + self.abs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +268,7 @@ 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: UiScalar) {
|
pub const fn shift(&mut self, offset: UiScalar) {
|
||||||
@@ -270,6 +283,13 @@ impl UiSpan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn outside(&self, parent: &Self) -> Self {
|
||||||
|
Self {
|
||||||
|
start: self.start.outside(parent),
|
||||||
|
end: self.end.outside(parent),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn len(&self) -> UiScalar {
|
pub const fn len(&self) -> UiScalar {
|
||||||
self.end - self.start
|
self.end - self.start
|
||||||
}
|
}
|
||||||
@@ -304,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,
|
||||||
@@ -338,8 +365,8 @@ impl UiRegion {
|
|||||||
|
|
||||||
pub fn to_px(&self, size: Vec2) -> PixelRegion {
|
pub fn to_px(&self, size: Vec2) -> PixelRegion {
|
||||||
PixelRegion {
|
PixelRegion {
|
||||||
top_left: self.top_left().get_rel() * size + self.top_left().get_px(),
|
top_left: self.top_left().get_rel() * size + self.top_left().get_abs(),
|
||||||
bot_right: self.bot_right().get_rel() * size + self.bot_right().get_px(),
|
bot_right: self.bot_right().get_rel() * size + self.bot_right().get_abs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,7 +421,7 @@ impl Display for UiRegion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug)]
|
||||||
pub struct PixelRegion {
|
pub struct PixelRegion {
|
||||||
pub top_left: Vec2,
|
pub top_left: Vec2,
|
||||||
pub bot_right: Vec2,
|
pub bot_right: Vec2,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
-382
@@ -1,96 +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, RegionAlign, UiColor, util::Vec2,
|
SwashContent,
|
||||||
};
|
|
||||||
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 {
|
||||||
@@ -106,320 +70,122 @@ impl Default for TextAttrs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How far below the longest line a width may fall and still be answered by
|
pub const LINE_HEIGHT_MULT: f32 = 1.1;
|
||||||
/// the break in hand. A parent that offers a child the length it reported
|
|
||||||
/// composes that length back through the box chain, so the two differ in the
|
|
||||||
/// last bits -- and at exactly the longest line, that decides whether a line
|
|
||||||
/// fits. Sub-pixel, so no break it admits is one a reader could see.
|
|
||||||
const BREAK_EPSILON_PX: f32 = 0.05;
|
|
||||||
|
|
||||||
/// Keeps text and its corresponding layout from getting out of sync.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 a chance to
|
|
||||||
// disagree with itself -- which is what happens when a parent offers
|
|
||||||
// a child the length that child just reported, and the two land
|
|
||||||
// either side of a float.
|
|
||||||
if let Some(key) = &self.layout_key
|
|
||||||
&& key.attrs == *attrs
|
|
||||||
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
|
|
||||||
&& want <= broke_at
|
|
||||||
&& want + BREAK_EPSILON_PX >= 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: Vec2::new(
|
|
||||||
glyph.x.floor() + entry.left as f32,
|
|
||||||
glyph.y.floor() - entry.top as f32,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,245 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
PatchRect,
|
|
||||||
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,
|
|
||||||
pub offset: Vec2,
|
|
||||||
}
|
|
||||||
+7
-45
@@ -1,28 +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 {
|
||||||
const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![
|
const ATTRIBS: [VertexAttribute; 7] = vertex_attr_array![
|
||||||
0 => Float32x2,
|
0 => Float32x2,
|
||||||
1 => Float32x2,
|
1 => Float32x2,
|
||||||
2 => Float32x2,
|
2 => Float32x2,
|
||||||
3 => Float32x2,
|
3 => Float32x2,
|
||||||
4 => Uint32,
|
4 => Uint32,
|
||||||
5 => Uint32,
|
5 => Uint32,
|
||||||
|
6 => Uint32,
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn desc() -> VertexBufferLayout<'static> {
|
pub fn desc() -> VertexBufferLayout<'static> {
|
||||||
@@ -44,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 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+195
-272
@@ -1,87 +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 {
|
|
||||||
format!("{PRELUDE}\n{wgsl}")
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,156 +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 h in &mut inst.primitives {
|
for h in &mut inst.primitives {
|
||||||
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
|
if h.layer == i && 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 ®istry.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,
|
||||||
})],
|
})],
|
||||||
@@ -264,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}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,153 +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: UiSpan,
|
|
||||||
y: UiSpan,
|
|
||||||
move_idx: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MoveOffset {
|
|
||||||
x: UiSpan,
|
|
||||||
y: UiSpan,
|
|
||||||
parent: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Written the way `UiScalar::within` writes it rather than as `mix`, so the
|
|
||||||
// CPU and the shader compose a position with the same arithmetic and answer
|
|
||||||
// the same thing about where a widget is.
|
|
||||||
fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar {
|
|
||||||
return UiScalar(
|
|
||||||
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, entry.x), span_within(r.y, entry.y));
|
|
||||||
at = entry.parent;
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UiSpan {
|
|
||||||
start: UiScalar,
|
|
||||||
end: UiScalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UiScalar {
|
|
||||||
rel: f32,
|
|
||||||
px: 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) 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(UiScalar(in.x_start.x, in.x_start.y), UiScalar(in.x_end.x, in.x_end.y)),
|
|
||||||
UiSpan(UiScalar(in.y_start.x, in.y_start.y), UiScalar(in.y_end.x, in.y_end.y)),
|
|
||||||
);
|
|
||||||
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 = floor(top_left_rel * window.dim) + floor(top_left_px);
|
|
||||||
let bot_right = floor(bot_right_rel * window.dim) + floor(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(mask.x, 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 = floor(tl * window.dim) + floor(tl_px);
|
|
||||||
let bot_right = floor(br * window.dim) + floor(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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+1
-37
@@ -1,50 +1,14 @@
|
|||||||
use crate::{
|
use crate::{LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId};
|
||||||
LayerId, Len, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId,
|
|
||||||
util::Vec2,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// important non rendering data for retained drawing
|
/// important non rendering data for retained drawing
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ActiveData {
|
pub struct ActiveData {
|
||||||
pub id: WidgetId,
|
pub id: WidgetId,
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
/// What the widget said it used of `region`, the last time it drew.
|
|
||||||
pub size: Size,
|
|
||||||
/// The pixel size of the box it drew against. `region` alone cannot say:
|
|
||||||
/// it is a fraction of a slot's box, and the same fraction of a box that
|
|
||||||
/// has since changed is a different number of pixels.
|
|
||||||
pub px: Vec2,
|
|
||||||
/// The pixel size of the box its parent first asked about it in, before
|
|
||||||
/// knowing what it came to. `px` may be a box derived from that answer,
|
|
||||||
/// and a size measured there is only the same answer asked again.
|
|
||||||
pub offered_px: Vec2,
|
|
||||||
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>,
|
||||||
pub primitives: Vec<PrimitiveHandle>,
|
pub primitives: Vec<PrimitiveHandle>,
|
||||||
pub children: Vec<WidgetId>,
|
pub children: Vec<WidgetId>,
|
||||||
/// The children whose size this widget read while drawing.
|
|
||||||
pub size_deps: Vec<WidgetId>,
|
|
||||||
/// Offered pixel axes which flowed into this widget's reported size,
|
|
||||||
/// directly or through a child size it read.
|
|
||||||
pub size_box_inputs: [bool; 2],
|
|
||||||
/// Output axes read while producing `size`, distinct from the widget's
|
|
||||||
/// own box when that box has a fixed pixel length.
|
|
||||||
pub size_output_inputs: [bool; 2],
|
|
||||||
/// The output dimensions against which those dependencies were observed.
|
|
||||||
pub output_px: Vec2,
|
|
||||||
/// The slot its primitives are positioned through: its own if its parent
|
|
||||||
/// placed it, otherwise the nearest ancestor that has one.
|
|
||||||
pub move_idx: MoveIdx,
|
|
||||||
/// The declared lengths whoever drew this widget resolved into its box.
|
|
||||||
/// A change to one moves a box this widget cannot fix by drawing again,
|
|
||||||
/// and comparing them is what says so.
|
|
||||||
pub declared: [Option<Len>; 2],
|
|
||||||
/// The slot `region` is given in, which is whatever its parent drew in.
|
|
||||||
pub parent_move: MoveIdx,
|
|
||||||
pub mask: MaskIdx,
|
pub mask: MaskIdx,
|
||||||
pub layer: LayerId,
|
pub layer: LayerId,
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-96
@@ -1,114 +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 cache;
|
||||||
mod painter;
|
mod painter;
|
||||||
mod render_state;
|
mod render_state;
|
||||||
|
mod size;
|
||||||
|
|
||||||
pub use active::*;
|
pub use active::*;
|
||||||
pub use painter::{Painter, PrimitiveLike};
|
pub use painter::Painter;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Composes a region held in `idx`'s coordinates down the chain, which is
|
|
||||||
/// the same walk the vertex shader does.
|
|
||||||
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
|
|
||||||
let mut region = local;
|
|
||||||
let mut at = idx;
|
|
||||||
for _ in 0..CHAIN_LIMIT {
|
|
||||||
if at == MoveIdx::NONE {
|
|
||||||
return region;
|
|
||||||
}
|
|
||||||
let entry = self.arena[at.idx()];
|
|
||||||
region = region.within(&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"
|
|
||||||
);
|
|
||||||
region
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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;
|
||||||
@@ -118,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) {}
|
||||||
|
|
||||||
|
|||||||
+41
-356
@@ -1,12 +1,7 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
use crate::layout_diagnostics::{self as diag, Counter};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
|
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
|
||||||
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
|
TextureHandle, UiRegion, UiRenderState, UiRsc, Widget, WidgetId,
|
||||||
render::{
|
render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||||
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
|
||||||
PrimitiveKind, TexturePrimitive,
|
|
||||||
},
|
|
||||||
util::Vec2,
|
util::Vec2,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -15,53 +10,26 @@ 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 box, in the coordinates of `move_idx`.
|
|
||||||
pub(super) region: UiRegion,
|
pub(super) region: UiRegion,
|
||||||
pub(super) mask: MaskIdx,
|
pub(super) mask: MaskIdx,
|
||||||
pub(super) textures: Vec<TextureHandle>,
|
pub(super) textures: Vec<TextureHandle>,
|
||||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||||
pub(super) children: Vec<WidgetId>,
|
pub(super) children: Vec<WidgetId>,
|
||||||
/// The children asked about so far, so the first box each was asked
|
|
||||||
/// about is the one recorded as its offer.
|
|
||||||
pub(super) offered: Vec<WidgetId>,
|
|
||||||
/// The children whose size this widget read while drawing.
|
|
||||||
pub(super) size_deps: Vec<WidgetId>,
|
|
||||||
/// Offered pixel axes which can affect the size this draw reports.
|
|
||||||
pub(super) size_box_inputs: [bool; 2],
|
|
||||||
pub(super) size_output_inputs: [bool; 2],
|
|
||||||
/// The slot this widget's primitives are positioned through: its own if
|
|
||||||
/// its parent placed it, otherwise the nearest ancestor that has one.
|
|
||||||
pub(super) move_idx: MoveIdx,
|
|
||||||
pub layer: usize,
|
pub 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) {
|
|
||||||
#[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,
|
region,
|
||||||
mask_idx: self.mask,
|
mask_idx: self.mask,
|
||||||
move_idx: self.move_idx,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.push_primitive(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_primitive(&mut self, h: PrimitiveHandle) {
|
|
||||||
if self.mask != MaskIdx::NONE {
|
if self.mask != MaskIdx::NONE {
|
||||||
// TODO: I have no clue if this works at all :joy:
|
// 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);
|
||||||
@@ -70,302 +38,85 @@ impl<'a> Painter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Writes a primitive to be rendered
|
/// 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, self.region)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
|
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
||||||
let primitive = primitive.into_primitive(self);
|
|
||||||
self.primitive_at(primitive, region.within(&self.region));
|
self.primitive_at(primitive, region.within(&self.region));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_mask(&mut self, region: UiRegion) {
|
pub fn set_mask(&mut self, region: UiRegion) {
|
||||||
assert!(self.mask == MaskIdx::NONE);
|
assert!(self.mask == MaskIdx::NONE);
|
||||||
self.mask = self.rsc.ui_mut().masks.push(Mask {
|
self.mask = self.rsc.ui_mut().masks.push(Mask { region });
|
||||||
region,
|
|
||||||
move_idx: self.move_idx,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a widget within this widget's region.
|
/// Draws a widget within this widget's region.
|
||||||
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
|
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
|
||||||
let declared = self.declared_lens(id);
|
self.widget_at(id, self.region);
|
||||||
// Composing `FULL` through a box is not quite the identity in f32,
|
|
||||||
// so a child with nothing declared keeps the box it would have had.
|
|
||||||
let region = match declared.iter().any(Option::is_some) {
|
|
||||||
true => declared_box(UiRegion::FULL, declared).within(&self.region),
|
|
||||||
false => self.region,
|
|
||||||
};
|
|
||||||
self.widget_at(id, region, false, declared)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a widget somewhere within this one.
|
/// Draws a widget somewhere within this one.
|
||||||
pub fn widget_within<'s, W: ?Sized>(
|
/// Useful for drawing child widgets in select areas.
|
||||||
&'s mut self,
|
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
|
||||||
id: &'s StrongWidget<W>,
|
self.widget_at(id, region.within(&self.region));
|
||||||
region: UiRegion,
|
|
||||||
) -> DrawResult<'s, 'a, W> {
|
|
||||||
let declared = self.declared_lens(id);
|
|
||||||
let region = declared_box(region, declared).within(&self.region);
|
|
||||||
self.widget_at(id, region, false, declared)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a widget declares its lengths to be, which whoever draws it
|
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
|
||||||
/// resolves into its box. `rest` is not among them: a share of what is
|
self.children.push(id.id());
|
||||||
/// left over is only a length to the widget dividing one, so it passes
|
self.state.draw_inner(
|
||||||
/// up in the size instead. Reading it depends on nothing -- the box that
|
|
||||||
/// comes of it is kept on the child, and `redraw` compares it there.
|
|
||||||
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<Len>; 2] {
|
|
||||||
let Some(widget) = self.rsc.widgets().get_dyn(id.id()) else {
|
|
||||||
return [None; 2];
|
|
||||||
};
|
|
||||||
[Axis::X, Axis::Y].map(|axis| declared_len(widget, axis))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draws a child this widget decides the box of, and may decide again
|
|
||||||
/// once it knows what the child came to. The child gets a slot of its
|
|
||||||
/// own, so placing it a second time writes one entry however much it
|
|
||||||
/// drew -- moved or resized alike, since everything under the slot is
|
|
||||||
/// held as a fraction of its box. A child drawn any other way has no slot
|
|
||||||
/// and can only be given a different box by drawing again.
|
|
||||||
pub fn place<'s, W: ?Sized>(
|
|
||||||
&'s mut self,
|
|
||||||
id: &'s StrongWidget<W>,
|
|
||||||
region: UiRegion,
|
|
||||||
) -> DrawResult<'s, 'a, W> {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::PlaceCalls);
|
|
||||||
let declared = self.declared_lens(id);
|
|
||||||
let region = declared_box(region, declared).within(&self.region);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::placed(id.id(), self.id, region);
|
|
||||||
self.widget_at(id, region, true, declared)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn widget_at<'s, W: ?Sized>(
|
|
||||||
&'s mut self,
|
|
||||||
id: &'s StrongWidget<W>,
|
|
||||||
region: UiRegion,
|
|
||||||
slotted: bool,
|
|
||||||
declared: [Option<Len>; 2],
|
|
||||||
) -> DrawResult<'s, 'a, W> {
|
|
||||||
// A child listed twice would be moved twice.
|
|
||||||
if !self.children.contains(&id.id()) {
|
|
||||||
self.children.push(id.id());
|
|
||||||
}
|
|
||||||
let size = self.state.draw_inner(
|
|
||||||
self.layer,
|
self.layer,
|
||||||
id.id(),
|
id.id(),
|
||||||
region,
|
region,
|
||||||
Some(self.id),
|
Some(self.id),
|
||||||
self.depth + 1,
|
|
||||||
self.move_idx,
|
|
||||||
slotted,
|
|
||||||
self.mask,
|
self.mask,
|
||||||
None,
|
None,
|
||||||
self.rsc,
|
self.rsc,
|
||||||
);
|
);
|
||||||
self.offer(id.id(), region);
|
|
||||||
if let Some(active) = self.state.active.get_mut(&id.id()) {
|
|
||||||
active.declared = declared;
|
|
||||||
}
|
|
||||||
DrawResult {
|
|
||||||
child: id,
|
|
||||||
painter: self,
|
|
||||||
size,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a child says its length is without being drawn, if it can say.
|
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||||
/// Asking counts as reading its size.
|
self.textures.push(handle.clone());
|
||||||
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
|
self.primitive_at(handle.primitive(), region.within(&self.region));
|
||||||
let hint = self
|
|
||||||
.rsc
|
|
||||||
.widgets()
|
|
||||||
.get_dyn(id.id())
|
|
||||||
.and_then(|widget| widget.size_hint(axis));
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::hint_read(id.id(), self.id, axis, hint);
|
|
||||||
match hint {
|
|
||||||
Some(hint) => {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::HintHits);
|
|
||||||
self.depend_on_hint(id);
|
|
||||||
Some(hint)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::HintMisses);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A retained child length valid under the region it is about to be
|
pub fn texture(&mut self, handle: &TextureHandle) {
|
||||||
/// offered. Unlike a hint, this is contextual: it is kept only when none
|
self.textures.push(handle.clone());
|
||||||
/// of the offered pixel axes which produced it changed.
|
self.primitive(handle.primitive());
|
||||||
pub fn known_len<W: ?Sized>(
|
|
||||||
&mut self,
|
|
||||||
child: &StrongWidget<W>,
|
|
||||||
axis: Axis,
|
|
||||||
region: UiRegion,
|
|
||||||
) -> Option<Len> {
|
|
||||||
let region = region.within(&self.region);
|
|
||||||
self.offer(child.id(), region);
|
|
||||||
if let Some(hint) = self.size_hint(child, axis) {
|
|
||||||
return Some(hint);
|
|
||||||
}
|
|
||||||
self.retained_size(child, region)
|
|
||||||
.map(|size| size.axis(axis))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `region` in this widget's own coordinates.
|
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||||
fn retained_size<W: ?Sized>(
|
self.textures.push(handle.clone());
|
||||||
&mut self,
|
self.primitive_at(handle.primitive(), region);
|
||||||
child: &StrongWidget<W>,
|
|
||||||
region: UiRegion,
|
|
||||||
) -> Option<Size> {
|
|
||||||
let (size, box_inputs, output_inputs) =
|
|
||||||
self.state
|
|
||||||
.retained_size(child.id(), region, self.move_idx, self.rsc.widgets())?;
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::RetainedSizeHits);
|
|
||||||
self.depend_on_size_inputs(child, box_inputs, output_inputs);
|
|
||||||
Some(size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Records the box a child was first asked about in this draw. Any later
|
/// returns (handle, offset from top left)
|
||||||
/// box this draw gives it was decided knowing its answer, so a size the
|
pub fn render_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
|
||||||
/// child measures there is not an answer to this widget's question.
|
|
||||||
fn offer(&mut self, child: WidgetId, region: UiRegion) {
|
|
||||||
if self.offered.contains(&child) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.offered.push(child);
|
|
||||||
let px = self.state.px_of(self.move_idx, region);
|
|
||||||
if let Some(active) = self.state.active.get_mut(&child) {
|
|
||||||
active.offered_px = px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Depends on a length the child gave without being drawn. A hint is
|
|
||||||
/// context-free, so this depends on the child but on no pixel axis.
|
|
||||||
fn depend_on_hint<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
|
||||||
self.depend_on_size_inputs(child, [false; 2], [false; 2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Depends on a size the child produced by drawing, which carries
|
|
||||||
/// whatever the child read to produce it.
|
|
||||||
fn depend_on_drawn_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
|
||||||
let (box_inputs, output_inputs) = self
|
|
||||||
.state
|
|
||||||
.active
|
|
||||||
.get(&child.id())
|
|
||||||
.map_or(([false; 2], [false; 2]), |active| {
|
|
||||||
(active.size_box_inputs, active.size_output_inputs)
|
|
||||||
});
|
|
||||||
self.depend_on_size_inputs(child, box_inputs, output_inputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn depend_on_size_inputs<W: ?Sized>(
|
|
||||||
&mut self,
|
|
||||||
child: &StrongWidget<W>,
|
|
||||||
box_inputs: [bool; 2],
|
|
||||||
output_inputs: [bool; 2],
|
|
||||||
) {
|
|
||||||
if !self.size_deps.contains(&child.id()) {
|
|
||||||
self.size_deps.push(child.id());
|
|
||||||
}
|
|
||||||
for (own, child) in self.size_box_inputs.iter_mut().zip(box_inputs) {
|
|
||||||
*own |= child;
|
|
||||||
}
|
|
||||||
for (own, child) in self.size_output_inputs.iter_mut().zip(output_inputs) {
|
|
||||||
*own |= child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: merge the text methods into the primitive ones.
|
|
||||||
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
|
||||||
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
|
|
||||||
for glyph in text.glyphs.iter() {
|
|
||||||
let mut region = origin;
|
|
||||||
region.x.end = region.x.start;
|
|
||||||
region.y.end = region.y.start;
|
|
||||||
let mut region = region.offset(UiVec2::px(glyph.offset));
|
|
||||||
region.x.end = region.x.start + UiScalar::px(glyph.entry.width as f32);
|
|
||||||
region.y.end = region.y.start + UiScalar::px(glyph.entry.height as f32);
|
|
||||||
self.write(
|
|
||||||
kind,
|
|
||||||
GlyphPrimitive {
|
|
||||||
uv_min: glyph.entry.uv_min,
|
|
||||||
uv_max: glyph.entry.uv_max,
|
|
||||||
layer: glyph.entry.layer,
|
|
||||||
color: text.color,
|
|
||||||
flags: glyph.entry.flags(),
|
|
||||||
},
|
|
||||||
region,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This widget's box, in the coordinates its own primitives are written
|
|
||||||
/// in -- so a region composed `within` it may be drawn directly.
|
|
||||||
pub fn region(&self) -> UiRegion {
|
pub fn region(&self) -> UiRegion {
|
||||||
self.region
|
self.region
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The output's size in pixels. A widget that reads it draws again when
|
pub fn size<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size {
|
||||||
/// the output changes, since nothing else can put that right.
|
self.size_ctx().size(id)
|
||||||
pub fn output_size(&mut self) -> Vec2 {
|
}
|
||||||
self.size_output_inputs = [true; 2];
|
|
||||||
|
pub fn len_axis<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Len {
|
||||||
|
match axis {
|
||||||
|
Axis::X => self.size_ctx().width(id),
|
||||||
|
Axis::Y => self.size_ctx().height(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output_size(&self) -> Vec2 {
|
||||||
self.state.output_size
|
self.state.output_size
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One axis of the output in pixels. Prefer this to [`Self::output_size`]
|
|
||||||
/// when the other axis cannot affect the size this widget reports.
|
|
||||||
pub fn output_len(&mut self, axis: Axis) -> f32 {
|
|
||||||
self.size_output_inputs[axis as usize] = true;
|
|
||||||
self.state.output_size.axis(axis)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This widget's box in pixels. Resolved against the output's size and
|
|
||||||
/// the boxes it sits within, so a widget that reads it draws again when
|
|
||||||
/// the output changes.
|
|
||||||
pub fn px_size(&mut self) -> Vec2 {
|
pub fn px_size(&mut self) -> Vec2 {
|
||||||
self.size_box_inputs = [true; 2];
|
self.region.size().to_abs(self.state.output_size)
|
||||||
let region = self.state.moves.resolve(self.move_idx, self.region);
|
|
||||||
region.size().to_px(self.state.output_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One axis of this widget's box in pixels. Prefer this to
|
|
||||||
/// [`Self::px_size`] when the other axis cannot affect the reported size.
|
|
||||||
pub fn px_len(&mut self, axis: Axis) -> f32 {
|
|
||||||
self.size_box_inputs[axis as usize] = true;
|
|
||||||
self.px_len_for_draw(axis)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One axis of this widget's box in pixels, for a draw whose reported
|
|
||||||
/// size does not follow from it -- a clamp or a position. Nothing records
|
|
||||||
/// the read, so a size that does depend on it would go stale.
|
|
||||||
pub fn px_len_for_draw(&self, axis: Axis) -> f32 {
|
|
||||||
let region = self.state.moves.resolve(self.move_idx, self.region);
|
|
||||||
region
|
|
||||||
.size()
|
|
||||||
.axis(axis)
|
|
||||||
.to_px(self.state.output_size.axis(axis))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_data(&mut self) -> &mut TextData {
|
pub fn text_data(&mut self) -> &mut TextData {
|
||||||
@@ -387,74 +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,
|
|
||||||
}
|
|
||||||
|
|
||||||
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_drawn_size(self.child);
|
|
||||||
self.size
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn len(self, axis: Axis) -> Len {
|
|
||||||
self.size().axis(axis)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What `Painter::primitive` takes: a primitive, or something that yields one
|
|
||||||
/// and does whatever else drawing it needs.
|
|
||||||
pub trait PrimitiveLike {
|
|
||||||
type Primitive: Primitive;
|
|
||||||
fn into_primitive(self, painter: &mut Painter) -> Self::Primitive;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<P: Primitive> PrimitiveLike for P {
|
|
||||||
type Primitive = P;
|
|
||||||
fn into_primitive(self, _: &mut Painter) -> P {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrimitiveLike for &TextureHandle {
|
|
||||||
type Primitive = TexturePrimitive;
|
|
||||||
|
|
||||||
/// Retains a share of the handle, so the slot the primitive names cannot
|
|
||||||
/// be freed and reused while it is still drawn.
|
|
||||||
fn into_primitive(self, painter: &mut Painter) -> TexturePrimitive {
|
|
||||||
painter.textures.push(self.clone());
|
|
||||||
self.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What a widget declares a length of its box to be. `rest` 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_len(widget: &dyn Widget, axis: Axis) -> Option<Len> {
|
|
||||||
widget.size_hint(axis).filter(|len| len.rest == 0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes a widget's declared lengths in the box `region` is given in, since a
|
|
||||||
/// fraction of a length means a fraction of that one. A caller that already
|
|
||||||
/// reserved the space hands back the same length, so this is the identity
|
|
||||||
/// for it.
|
|
||||||
fn declared_box(mut region: UiRegion, declared: [Option<Len>; 2]) -> UiRegion {
|
|
||||||
for (axis, len) in [Axis::X, Axis::Y].into_iter().zip(declared) {
|
|
||||||
let Some(len) = len else { continue };
|
|
||||||
let span = region.axis_mut(axis);
|
|
||||||
span.end = span.start + UiScalar::new(len.rel, len.px);
|
|
||||||
}
|
|
||||||
region
|
|
||||||
}
|
|
||||||
+91
-660
@@ -1,41 +1,19 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
|
||||||
use crate::ui::painter::declared_len;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion,
|
ActiveData, Axis, IdLike, MaskIdx, Painter, PixelRegion, PrimitiveLayers, SizeCtx,
|
||||||
Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId, Widgets,
|
StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
|
||||||
util::{HashMap, HashSet, Vec2},
|
ui::cache::Cache,
|
||||||
|
util::{HashMap, HashSet, Vec2, forget_ref},
|
||||||
};
|
};
|
||||||
|
|
||||||
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
|
||||||
const LAYOUT_EPSILON_PX: f32 = 0.05;
|
|
||||||
|
|
||||||
fn pixel_len_changed(old: f32, new: f32) -> bool {
|
|
||||||
(old - new).abs() > LAYOUT_EPSILON_PX
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UiRenderState {
|
pub struct UiRenderState {
|
||||||
pub active: HashMap<WidgetId, ActiveData>,
|
pub active: HashMap<WidgetId, ActiveData>,
|
||||||
pub layers: DrawLayers,
|
pub layers: PrimitiveLayers,
|
||||||
pub(super) output_size: Vec2,
|
pub(super) output_size: Vec2,
|
||||||
|
pub cache: Cache,
|
||||||
|
|
||||||
old_root: Option<WidgetId>,
|
old_root: Option<WidgetId>,
|
||||||
/// The slot every chain bottoms out in, holding the output as a box.
|
resized: bool,
|
||||||
root_move: MoveIdx,
|
|
||||||
/// Widgets whose reported size depends on the root box rather than on
|
|
||||||
/// their own, so nothing below them changing length can reach them.
|
|
||||||
root_readers: HashSet<WidgetId>,
|
|
||||||
/// Content/state dirtiness whose retained size cannot answer a layout
|
|
||||||
/// question until that widget has drawn again.
|
|
||||||
invalid_sizes: HashSet<WidgetId>,
|
|
||||||
/// What has already been drawn during the pass under way, so a widget
|
|
||||||
/// reached by redrawing an ancestor is not drawn again on its own
|
|
||||||
/// account. Emptied when the pass ends.
|
|
||||||
draw_started: HashSet<WidgetId>,
|
draw_started: HashSet<WidgetId>,
|
||||||
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
|
|
||||||
/// replaces that while its children go on pointing at the slot.
|
|
||||||
slots: HashMap<WidgetId, MoveIdx>,
|
|
||||||
pub moves: Moves,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiRenderState {
|
impl UiRenderState {
|
||||||
@@ -43,64 +21,20 @@ impl UiRenderState {
|
|||||||
Self {
|
Self {
|
||||||
active: Default::default(),
|
active: Default::default(),
|
||||||
layers: Default::default(),
|
layers: Default::default(),
|
||||||
|
cache: Default::default(),
|
||||||
output_size: Vec2::ZERO,
|
output_size: Vec2::ZERO,
|
||||||
old_root: None,
|
old_root: None,
|
||||||
invalid_sizes: Default::default(),
|
resized: false,
|
||||||
draw_started: Default::default(),
|
draw_started: Default::default(),
|
||||||
slots: Default::default(),
|
|
||||||
moves: Default::default(),
|
|
||||||
root_move: MoveIdx::NONE,
|
|
||||||
root_readers: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The window as a box, so a chain bottoms out in one rather than in a
|
|
||||||
/// multiplication applied after it. Composing through a box held in
|
|
||||||
/// pixels leaves everything below it in pixels, which is why nothing
|
|
||||||
/// downstream has to know the output's size to resolve a position.
|
|
||||||
fn write_root(&mut self) {
|
|
||||||
let region = UiRegion::new(
|
|
||||||
UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.x)),
|
|
||||||
UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.y)),
|
|
||||||
);
|
|
||||||
match self.root_move == MoveIdx::NONE {
|
|
||||||
true => self.root_move = self.moves.push(MoveIdx::NONE, region),
|
|
||||||
false => self.moves.set(self.root_move, region),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||||
self.output_size = size.into();
|
self.output_size = size.into();
|
||||||
self.write_root();
|
self.resized = true;
|
||||||
}
|
|
||||||
|
|
||||||
/// Which axes of the root widget's box are no longer the ones the root
|
|
||||||
/// slot holds, which is all a resize now is: one slot written, found by
|
|
||||||
/// the same comparison every other box change is found by.
|
|
||||||
fn root_axes_changed(&self) -> [bool; 2] {
|
|
||||||
let Some(active) = self.old_root.and_then(|root| self.active.get(&root)) else {
|
|
||||||
return [false; 2];
|
|
||||||
};
|
|
||||||
let px = self.px_of(active.parent_move, active.region);
|
|
||||||
let mut changed = [false; 2];
|
|
||||||
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) {
|
|
||||||
*c = pixel_len_changed(active.px.axis(axis), px.axis(axis));
|
|
||||||
}
|
|
||||||
changed
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn output_size(&self) -> Vec2 {
|
|
||||||
self.output_size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
|
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::Updates);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
let _update = diag::timer(TimerKind::Update);
|
|
||||||
self.invalid_sizes.clear();
|
|
||||||
self.invalid_sizes
|
|
||||||
.extend(rsc.widgets().needs_redraw.iter().copied());
|
|
||||||
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
||||||
// decide whether to panic or not
|
// decide whether to panic or not
|
||||||
if !rsc.widgets().waiting.is_empty() {
|
if !rsc.widgets().waiting.is_empty() {
|
||||||
@@ -118,77 +52,20 @@ impl UiRenderState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let root = root.into();
|
let root = root.into();
|
||||||
if self.root_changed(root) {
|
if self.root_changed(root) || self.resized {
|
||||||
self.redraw_all(root, rsc);
|
self.redraw_all(root, rsc);
|
||||||
self.old_root = root.map(|r| r.id());
|
self.old_root = root.map(|r| r.id());
|
||||||
} else if self.root_axes_changed().iter().any(|&c| c) {
|
self.resized = false;
|
||||||
// Every box is a part of the root box, so writing it is a box
|
} else if rsc.widgets().has_updates() {
|
||||||
// that changed length like any other. Offering the root widget
|
|
||||||
// its box again puts that through `try_reuse`, which answers per
|
|
||||||
// axis and lets `redraws_under` price the subtree -- rather than
|
|
||||||
// marking it, which would redraw it whichever axis moved. What
|
|
||||||
// that cannot reach is a widget whose size came from the root box
|
|
||||||
// instead of its own, since its own box need not have changed.
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
let _marking = diag::timer(TimerKind::ResizeMarking);
|
|
||||||
let changed = self.root_axes_changed();
|
|
||||||
for id in self.root_readers.clone() {
|
|
||||||
let reads = self
|
|
||||||
.active
|
|
||||||
.get(&id)
|
|
||||||
.map_or([false; 2], |active| active.size_output_inputs);
|
|
||||||
if !AXES
|
|
||||||
.into_iter()
|
|
||||||
.zip(changed)
|
|
||||||
.any(|(axis, c)| c && reads[axis as usize])
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::ResizeDependents);
|
|
||||||
rsc.widgets_mut().needs_redraw.insert(id);
|
|
||||||
}
|
|
||||||
if let Some(root) = root {
|
|
||||||
self.draw_inner(
|
|
||||||
0,
|
|
||||||
root.id(),
|
|
||||||
UiRegion::FULL,
|
|
||||||
None,
|
|
||||||
1,
|
|
||||||
self.root_move,
|
|
||||||
false,
|
|
||||||
MaskIdx::NONE,
|
|
||||||
None,
|
|
||||||
rsc,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rsc.widgets().has_updates() {
|
|
||||||
self.redraw_updates(rsc);
|
self.redraw_updates(rsc);
|
||||||
}
|
}
|
||||||
self.invalid_sizes.clear();
|
|
||||||
self.draw_started.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
|
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
let _layout = diag::timer(TimerKind::FullLayout);
|
|
||||||
self.clear(rsc);
|
self.clear(rsc);
|
||||||
// free all resources & cache
|
// free all resources & cache
|
||||||
self.write_root();
|
|
||||||
if let Some(id) = root {
|
if let Some(id) = root {
|
||||||
self.draw_inner(
|
self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, None, rsc);
|
||||||
0,
|
|
||||||
id.id(),
|
|
||||||
UiRegion::FULL,
|
|
||||||
None,
|
|
||||||
1,
|
|
||||||
self.root_move,
|
|
||||||
false,
|
|
||||||
MaskIdx::NONE,
|
|
||||||
None,
|
|
||||||
rsc,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,122 +77,73 @@ impl UiRenderState {
|
|||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
parent: Option<WidgetId>,
|
parent: Option<WidgetId>,
|
||||||
depth: usize,
|
|
||||||
parent_move: MoveIdx,
|
|
||||||
slotted: bool,
|
|
||||||
mask: MaskIdx,
|
mask: MaskIdx,
|
||||||
mut old: Option<ActiveData>,
|
old_children: Option<Vec<WidgetId>>,
|
||||||
rsc: &mut dyn UiRsc,
|
rsc: &mut dyn UiRsc,
|
||||||
) -> Size {
|
) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
let mut redrawn = old_children.is_some();
|
||||||
|
let mut old_children = old_children.unwrap_or_default();
|
||||||
|
if let Some(active) = self.active.get_mut(&id)
|
||||||
|
&& !rsc.widgets().needs_redraw.contains(&id)
|
||||||
{
|
{
|
||||||
diag::bump(Counter::DrawRequests);
|
redrawn = true;
|
||||||
diag::draw_request(id, parent, region, self.px_of(parent_move, region), slotted);
|
// check to see if we can skip drawing first
|
||||||
}
|
if active.region == region {
|
||||||
if self.active.contains_key(&id) {
|
return;
|
||||||
if let Some(size) = self.try_reuse(id, region, depth, parent_move, rsc) {
|
} else if active.region.size() == region.size() {
|
||||||
return size;
|
// TODO: epsilon?
|
||||||
|
let from = active.region;
|
||||||
|
self.mov(id, from, region);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// if not, then maintain resize and track old children to remove unneeded
|
// if not, then maintain resize and track old children to remove unneeded
|
||||||
old = self.remove(id, false, rsc);
|
let active = self.remove(id, false, rsc).unwrap();
|
||||||
|
old_children = active.children;
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw widget
|
// draw widget
|
||||||
let (move_idx, local) = match slotted {
|
|
||||||
// Its box becomes its slot's, so it draws in the slot's own
|
|
||||||
// coordinates and the box it was given is one entry to rewrite.
|
|
||||||
true => (self.move_slot(id, parent_move, region), UiRegion::FULL),
|
|
||||||
false => {
|
|
||||||
self.drop_slot(id);
|
|
||||||
(parent_move, region)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let px = self.px_of(move_idx, local);
|
|
||||||
// Drawn again in a box its parent already decided: the offer is the
|
|
||||||
// one recorded when the parent first asked, not this box.
|
|
||||||
let (old_children, offered_px) = match old {
|
|
||||||
Some(old) => (old.children, old.offered_px),
|
|
||||||
None => (Vec::new(), px),
|
|
||||||
};
|
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
|
||||||
self.draw_started.insert(id);
|
self.draw_started.insert(id);
|
||||||
|
|
||||||
let mut painter = Painter {
|
let mut painter = Painter {
|
||||||
state: self,
|
state: self,
|
||||||
region: local,
|
region,
|
||||||
mask,
|
mask,
|
||||||
layer,
|
layer,
|
||||||
id,
|
id,
|
||||||
textures: Vec::new(),
|
textures: Vec::new(),
|
||||||
primitives: Vec::new(),
|
primitives: Vec::new(),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
offered: Vec::new(),
|
|
||||||
size_deps: Vec::new(),
|
|
||||||
depth,
|
|
||||||
size_box_inputs: [false; 2],
|
|
||||||
size_output_inputs: [false; 2],
|
|
||||||
move_idx,
|
|
||||||
rsc,
|
rsc,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
diag::bump(Counter::WidgetDraws);
|
|
||||||
diag::draw_widget(id, painter.rsc.widgets().label(id));
|
|
||||||
}
|
|
||||||
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
|
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
|
||||||
let size = widget.draw(&mut painter);
|
widget.draw(&mut painter);
|
||||||
drop(widget);
|
drop(widget);
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::size_reported(id, size);
|
|
||||||
|
|
||||||
let Painter {
|
let Painter {
|
||||||
state: _,
|
state: _,
|
||||||
rsc: _,
|
rsc: _,
|
||||||
region: _,
|
region,
|
||||||
mask,
|
mask,
|
||||||
textures,
|
textures,
|
||||||
primitives,
|
primitives,
|
||||||
children,
|
children,
|
||||||
offered: _,
|
|
||||||
size_deps,
|
|
||||||
size_box_inputs,
|
|
||||||
size_output_inputs,
|
|
||||||
move_idx,
|
|
||||||
layer,
|
layer,
|
||||||
depth: _,
|
|
||||||
id,
|
id,
|
||||||
} = painter;
|
} = painter;
|
||||||
|
|
||||||
debug_assert!(
|
|
||||||
Self::hints_agree(id, size, rsc),
|
|
||||||
"'{}' ({id:?}) drew a size its size_hint disagrees with",
|
|
||||||
rsc.widgets().label(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
// add to active
|
// add to active
|
||||||
let active = ActiveData {
|
let active = ActiveData {
|
||||||
id,
|
id,
|
||||||
region,
|
region,
|
||||||
size,
|
|
||||||
px,
|
|
||||||
offered_px,
|
|
||||||
parent,
|
parent,
|
||||||
depth,
|
|
||||||
textures,
|
textures,
|
||||||
primitives,
|
primitives,
|
||||||
children,
|
children,
|
||||||
size_deps,
|
|
||||||
size_box_inputs,
|
|
||||||
size_output_inputs,
|
|
||||||
// Written by whoever draws it, which is what resolves them.
|
|
||||||
declared: [None; 2],
|
|
||||||
output_px: self.output_size,
|
|
||||||
move_idx,
|
|
||||||
parent_move,
|
|
||||||
mask,
|
mask,
|
||||||
layer,
|
layer,
|
||||||
};
|
};
|
||||||
|
|
||||||
// remove old children that weren't kept
|
// remove old children that weren't kept
|
||||||
for c in &old_children {
|
for c in &old_children {
|
||||||
if !active.children.contains(c) {
|
if !active.children.contains(c) {
|
||||||
@@ -323,272 +151,22 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match active.size_output_inputs.iter().any(|&reads| reads) {
|
rsc.on_draw(&active, redrawn);
|
||||||
true => self.root_readers.insert(id),
|
|
||||||
false => self.root_readers.remove(&id),
|
|
||||||
};
|
|
||||||
rsc.on_draw(&active);
|
|
||||||
self.active.insert(id, active);
|
self.active.insert(id, active);
|
||||||
self.invalid_sizes.remove(&id);
|
|
||||||
size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The slot a widget's box is held in, made on its first placed draw and
|
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
|
||||||
/// kept until it stops being drawn -- a redraw replaces its `ActiveData`
|
|
||||||
/// while descendants go on naming the slot.
|
|
||||||
fn move_slot(&mut self, id: WidgetId, parent: MoveIdx, region: UiRegion) -> MoveIdx {
|
|
||||||
if let Some(&idx) = self.slots.get(&id) {
|
|
||||||
self.moves.set_parent(idx, parent);
|
|
||||||
self.moves.set(idx, region);
|
|
||||||
return idx;
|
|
||||||
}
|
|
||||||
let idx = self.moves.push(parent, region);
|
|
||||||
self.slots.insert(id, idx);
|
|
||||||
idx
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gives up a slot a widget no longer needs, because it is drawn somewhere
|
|
||||||
/// that does not place it. Its descendants name it, so this is only
|
|
||||||
/// reached where they are about to be drawn again.
|
|
||||||
fn drop_slot(&mut self, id: WidgetId) {
|
|
||||||
if let Some(idx) = self.slots.remove(&id) {
|
|
||||||
self.moves.remove(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The pixel size of a region held in `slot`'s coordinates.
|
|
||||||
pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> Vec2 {
|
|
||||||
self.moves
|
|
||||||
.resolve(slot, region)
|
|
||||||
.size()
|
|
||||||
.to_px(self.output_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A clean widget's retained size, when the offered pixel axes which
|
|
||||||
/// produced that answer are unchanged. This observes the old answer only;
|
|
||||||
/// it does not move or otherwise reuse the widget's drawing.
|
|
||||||
pub(super) fn retained_size(
|
|
||||||
&self,
|
|
||||||
id: WidgetId,
|
|
||||||
region: UiRegion,
|
|
||||||
parent_move: MoveIdx,
|
|
||||||
widgets: &Widgets,
|
|
||||||
) -> Option<(Size, [bool; 2], [bool; 2])> {
|
|
||||||
if self.size_is_invalid(id, widgets) || self.dirty_size_under(id, widgets) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let active = self.active.get(&id)?;
|
|
||||||
if active.parent_move != parent_move {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let px = self.px_of(parent_move, region);
|
|
||||||
let valid_box = AXES
|
|
||||||
.into_iter()
|
|
||||||
.zip(active.size_box_inputs)
|
|
||||||
.all(|(axis, depends)| {
|
|
||||||
!depends || !pixel_len_changed(active.px.axis(axis), px.axis(axis))
|
|
||||||
});
|
|
||||||
let valid_output =
|
|
||||||
AXES.into_iter()
|
|
||||||
.zip(active.size_output_inputs)
|
|
||||||
.all(|(axis, depends)| {
|
|
||||||
!depends
|
|
||||||
|| !pixel_len_changed(
|
|
||||||
active.output_px.axis(axis),
|
|
||||||
self.output_size.axis(axis),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
(valid_box && valid_output).then_some((
|
|
||||||
active.size,
|
|
||||||
active.size_box_inputs,
|
|
||||||
active.size_output_inputs,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn size_is_invalid(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
|
||||||
self.invalid_sizes.contains(&id) || widgets.needs_redraw.contains(&id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
|
||||||
self.active.get(&id).is_some_and(|active| {
|
|
||||||
active.size_deps.iter().any(|child| {
|
|
||||||
self.size_is_invalid(*child, widgets) || self.dirty_size_under(*child, widgets)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The drawing a widget already has, kept for a new box if the box has not
|
|
||||||
/// changed in a way it depends on.
|
|
||||||
fn try_reuse(
|
|
||||||
&mut self,
|
|
||||||
id: WidgetId,
|
|
||||||
region: UiRegion,
|
|
||||||
depth: usize,
|
|
||||||
parent_move: MoveIdx,
|
|
||||||
rsc: &mut dyn UiRsc,
|
|
||||||
) -> Option<Size> {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::ReuseAttempts);
|
|
||||||
// Only its own dirtiness, not anything dirty under it that could
|
|
||||||
// change the size this hands back. What makes that safe is the order
|
|
||||||
// `redraw_updates` settles in, and nothing else: by the time a reader
|
|
||||||
// draws, everything dirty below it has been drawn and has propagated.
|
|
||||||
// Draw in another order and this returns a stale size -- measured, on
|
|
||||||
// seed 2 of `tests/generated.rs`.
|
|
||||||
if rsc.widgets().needs_redraw.contains(&id) {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
diag::bump(Counter::ReuseDirty);
|
|
||||||
diag::reuse(id, ReuseOutcome::Dirty);
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let active = self.active.get(&id)?;
|
|
||||||
// Drawn somewhere else in the tree: its box is in coordinates it no
|
|
||||||
// longer sits in, and its slot names the wrong parent.
|
|
||||||
if active.parent_move != parent_move {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
diag::bump(Counter::ReuseWrongParent);
|
|
||||||
diag::reuse(id, ReuseOutcome::WrongParent);
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let (size, old_region, slot, old_px) =
|
|
||||||
(active.size, active.region, active.move_idx, active.px);
|
|
||||||
// In pixels, because `region` is a fraction of a slot's box and that
|
|
||||||
// box may be what changed -- an unchanged fraction of a box half the
|
|
||||||
// size is half the widget.
|
|
||||||
let px = self.px_of(parent_move, region);
|
|
||||||
let mut changed = [false; 2];
|
|
||||||
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) {
|
|
||||||
*c = pixel_len_changed(old_px.axis(axis), px.axis(axis));
|
|
||||||
}
|
|
||||||
if !changed.iter().any(|&c| c) && old_region == region {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
diag::bump(Counter::ReuseExact);
|
|
||||||
diag::reuse(id, ReuseOutcome::Exact);
|
|
||||||
}
|
|
||||||
self.keep_depth(id, depth);
|
|
||||||
return Some(size);
|
|
||||||
}
|
|
||||||
// Only a placed widget can be given a different *region* without
|
|
||||||
// drawing again: it has an entry of its own to say where it went,
|
|
||||||
// where an unslotted one shares its parent's and has nothing to
|
|
||||||
// write. Its parent's box changing length is not that -- everything
|
|
||||||
// it drew is a fraction of that box, so the slot already above it
|
|
||||||
// carries the change and `on_resize` below decides whether the
|
|
||||||
// drawing survives it.
|
|
||||||
if slot == parent_move && old_region != region {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
diag::bump(Counter::ReuseUnslotted);
|
|
||||||
diag::reuse(id, ReuseOutcome::Unslotted);
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if changed.iter().any(|&c| c) {
|
|
||||||
let widget = rsc.widgets().get_dyn(id)?;
|
|
||||||
let redraws = AXES
|
|
||||||
.into_iter()
|
|
||||||
.zip(changed)
|
|
||||||
.any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale);
|
|
||||||
// Anything under it that has to be drawn again is drawn by drawing
|
|
||||||
// this, because whatever reads that widget's size sits in between
|
|
||||||
// and has to lay out around what it comes to.
|
|
||||||
if redraws {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
diag::bump(Counter::ReuseOwnResize);
|
|
||||||
diag::reuse(id, ReuseOutcome::OwnResize);
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if self.redraws_under(id, changed, rsc) {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
diag::bump(Counter::ReuseDescendantResize);
|
|
||||||
diag::reuse(id, ReuseOutcome::DescendantResize);
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if slot != parent_move {
|
|
||||||
self.moves.set(slot, region);
|
|
||||||
}
|
|
||||||
self.keep_depth(id, depth);
|
|
||||||
let active = self.active.get_mut(&id).unwrap();
|
let active = self.active.get_mut(&id).unwrap();
|
||||||
active.region = region;
|
for h in &active.primitives {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
let region = self.layers[h.layer].region_mut(h);
|
||||||
{
|
*region = region.outside(&from).within(&to);
|
||||||
diag::bump(Counter::ReuseMoved);
|
}
|
||||||
diag::reuse(id, ReuseOutcome::Moved);
|
active.region = active.region.outside(&from).within(&to);
|
||||||
|
// SAFETY: children cannot be recursive
|
||||||
|
let children = unsafe { forget_ref(&active.children) };
|
||||||
|
for child in children {
|
||||||
|
self.mov(*child, from, to);
|
||||||
}
|
}
|
||||||
Some(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether anything under `id` would have to be drawn again for the box
|
|
||||||
/// it is a fraction of changing length, `changed` saying which axes of
|
|
||||||
/// that box did.
|
|
||||||
///
|
|
||||||
/// A part of a box with no relative extent on an axis is a fixed length,
|
|
||||||
/// held as offsets from that box's start, and composing anything into it
|
|
||||||
/// leaves no relative extent either. So a widget whose own box did not
|
|
||||||
/// change length has no descendant whose box did, and the walk stops
|
|
||||||
/// there -- an 80-wide child of a widened row is not asked at all.
|
|
||||||
fn redraws_under(&self, id: WidgetId, changed: [bool; 2], rsc: &dyn UiRsc) -> bool {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::ResizeChecks);
|
|
||||||
let Some(active) = self.active.get(&id) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let size_deps = &active.size_deps;
|
|
||||||
active.children.iter().any(|&child| {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::ResizeCheckChildren);
|
|
||||||
let Some(data) = self.active.get(&child) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let Some(widget) = rsc.widgets().get_dyn(child) else {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
// What it drew to learn this child's size was the child in *this*
|
|
||||||
// box, so a different box is a different answer -- unless the
|
|
||||||
// child gave an exact one without being drawn at all.
|
|
||||||
if size_deps.contains(&child) {
|
|
||||||
let measured = AXES
|
|
||||||
.into_iter()
|
|
||||||
.zip(changed)
|
|
||||||
.any(|(axis, c)| c && widget.size_hint(axis).is_none());
|
|
||||||
if measured {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut own = changed;
|
|
||||||
for (axis, c) in AXES.into_iter().zip(own.iter_mut()) {
|
|
||||||
*c &= data.region.axis(axis).len().rel != 0.0;
|
|
||||||
}
|
|
||||||
if !own.iter().any(|&c| c) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let redraws = AXES
|
|
||||||
.into_iter()
|
|
||||||
.zip(own)
|
|
||||||
.any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale);
|
|
||||||
redraws || self.redraws_under(child, own, rsc)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {
|
|
||||||
let Some(widget) = rsc.widgets().get_dyn(id) else {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
AXES.into_iter().all(|axis| {
|
|
||||||
widget
|
|
||||||
.size_hint(axis)
|
|
||||||
.is_none_or(|hint| hint == size.axis(axis))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NOTE: instance textures are cleared and self.textures freed
|
/// NOTE: instance textures are cleared and self.textures freed
|
||||||
@@ -611,16 +189,13 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
|
fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
|
||||||
|
self.cache.remove(id);
|
||||||
let inst = self.remove(id, true, rsc);
|
let inst = self.remove(id, true, rsc);
|
||||||
if let Some(inst) = &inst {
|
if let Some(inst) = &inst {
|
||||||
for c in &inst.children {
|
for c in &inst.children {
|
||||||
self.remove_rec(*c, rsc);
|
self.remove_rec(*c, rsc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// After the descendants, whose slots name this one as their parent.
|
|
||||||
if let Some(idx) = self.slots.remove(&id) {
|
|
||||||
self.moves.remove(idx);
|
|
||||||
}
|
|
||||||
inst
|
inst
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,70 +203,19 @@ impl UiRenderState {
|
|||||||
for (_, active) in self.active.drain() {
|
for (_, active) in self.active.drain() {
|
||||||
rsc.on_undraw(&active);
|
rsc.on_undraw(&active);
|
||||||
}
|
}
|
||||||
self.slots.clear();
|
self.cache.clear();
|
||||||
self.moves.clear();
|
|
||||||
self.root_move = MoveIdx::NONE;
|
|
||||||
self.layers.clear();
|
self.layers.clear();
|
||||||
self.invalid_sizes.clear();
|
|
||||||
self.draw_started.clear();
|
|
||||||
rsc.widgets_mut().needs_redraw.clear();
|
rsc.widgets_mut().needs_redraw.clear();
|
||||||
rsc.free();
|
rsc.free();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
|
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
while let Some(&id) = rsc.widgets().needs_redraw.iter().next() {
|
||||||
let _layout = diag::timer(TimerKind::IncrementalLayout);
|
|
||||||
// A reader's answer is only valid after every dirty size it reads has
|
|
||||||
// settled, and taking the deepest first is what arranges that --
|
|
||||||
// `try_reuse` hands back a retained size without asking whether
|
|
||||||
// anything dirty sits under it, so this order is load-bearing for the
|
|
||||||
// answer and not only for the cost. Equal-depth widgets are
|
|
||||||
// independent, so their order does not matter. Resize dirtiness already marks whole reader chains, so
|
|
||||||
// choosing their shallowest roots coalesces descendants that share a
|
|
||||||
// reader and gives each changing box its final constraints first.
|
|
||||||
while let Some(id) = {
|
|
||||||
let dirty = rsc.widgets().needs_redraw.iter().copied();
|
|
||||||
dirty.max_by_key(|&id| self.depth(id))
|
|
||||||
} {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::QueuePops);
|
|
||||||
self.redraw(id, rsc);
|
self.redraw(id, rsc);
|
||||||
}
|
}
|
||||||
rsc.free();
|
rsc.free();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keeps a reused widget's depth current, since being reused is being
|
|
||||||
/// visited: only a subtree nobody looked at can hold a stale one.
|
|
||||||
fn keep_depth(&mut self, id: WidgetId, depth: usize) {
|
|
||||||
if let Some(active) = self.active.get_mut(&id) {
|
|
||||||
active.depth = depth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn depth(&self, id: WidgetId) -> usize {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::DepthReads);
|
|
||||||
let depth = self.active.get(&id).map_or(1, |active| active.depth);
|
|
||||||
debug_assert_eq!(
|
|
||||||
depth,
|
|
||||||
self.walked_depth(id),
|
|
||||||
"a widget's kept depth is not the one its ancestry says"
|
|
||||||
);
|
|
||||||
depth
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What the kept depth is checked against, and the only thing that reads
|
|
||||||
/// the ancestry to find one.
|
|
||||||
fn walked_depth(&self, id: WidgetId) -> usize {
|
|
||||||
let mut depth = 0;
|
|
||||||
let mut at = Some(id);
|
|
||||||
while let Some(id) = at {
|
|
||||||
at = self.active.get(&id).and_then(|active| active.parent);
|
|
||||||
depth += 1;
|
|
||||||
}
|
|
||||||
depth
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
|
pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
|
||||||
root.into().map(|r| r.id()) != self.old_root
|
root.into().map(|r| r.id()) != self.old_root
|
||||||
}
|
}
|
||||||
@@ -701,9 +225,7 @@ impl UiRenderState {
|
|||||||
root: impl Into<Option<&'a StrongWidget>>,
|
root: impl Into<Option<&'a StrongWidget>>,
|
||||||
widgets: &Widgets,
|
widgets: &Widgets,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
self.root_changed(root)
|
self.root_changed(root) || widgets.has_updates()
|
||||||
|| self.root_axes_changed().iter().any(|&c| c)
|
|
||||||
|| widgets.has_updates()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn active_widgets(&self) -> usize {
|
pub fn active_widgets(&self) -> usize {
|
||||||
@@ -718,75 +240,40 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug_layers(&self) {
|
pub fn debug_layers(&self) {
|
||||||
for ((idx, depth), draws) in self.layers.iter_depth() {
|
for ((idx, depth), primitives) in self.layers.iter_depth() {
|
||||||
let indent = " ".repeat(depth * 2);
|
let indent = " ".repeat(depth * 2);
|
||||||
let counts: Vec<String> = draws
|
let len = primitives.instances().len();
|
||||||
.primitives()
|
print!("{indent}{idx}: {len} primitives");
|
||||||
.iter()
|
if len >= 1 {
|
||||||
.map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string())
|
print!(" ({})", primitives.instances()[0].binding);
|
||||||
.collect();
|
}
|
||||||
println!("{indent}{idx}: [{}]", counts.join(", "));
|
println!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where a widget is on screen: its box composed through the boxes it
|
|
||||||
/// sits within, which is the walk the vertex shader does.
|
|
||||||
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
||||||
let active = self.active.get(&id.id())?;
|
let region = self.active.get(&id.id())?.region;
|
||||||
let region = self.moves.resolve(active.parent_move, active.region);
|
|
||||||
Some(region.to_px(self.output_size))
|
Some(region.to_px(self.output_size))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// redraws a widget that's currently active (drawn)
|
/// redraws a widget that's currently active (drawn)
|
||||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
||||||
self.draw_started.remove(&id);
|
|
||||||
if rsc.widgets().needs_redraw.contains(&id) {
|
|
||||||
self.invalid_sizes.insert(id);
|
|
||||||
}
|
|
||||||
// A widget can only answer whether its size changed by drawing in the
|
|
||||||
// box its parent chose. If that box changed in pixels, its retained
|
|
||||||
// placement is stale and the highest size reader must choose the new
|
|
||||||
// box first. The same holds when the box was decided from the
|
|
||||||
// widget's own answer: measuring there again can only repeat it,
|
|
||||||
// whatever the content now says. Otherwise the widget can draw
|
|
||||||
// locally, and its readers only matter if the returned size actually
|
|
||||||
// changed.
|
|
||||||
let box_changed = self.active.get(&id).is_some_and(|active| {
|
|
||||||
let px = self.px_of(active.parent_move, active.region);
|
|
||||||
AXES.into_iter()
|
|
||||||
.any(|axis| pixel_len_changed(active.px.axis(axis), px.axis(axis)))
|
|
||||||
});
|
|
||||||
// A declared length is resolved into this widget's box by whoever
|
|
||||||
// drew it, so a change to one moves a box this widget cannot fix by
|
|
||||||
// drawing again, however its own size comes out. Compared rather
|
|
||||||
// than assumed: a widget dirtied for any other reason declares what
|
|
||||||
// it declared before, and redrawing its parent for that costs 17%.
|
|
||||||
let declared_changed = self.active.get(&id).is_some_and(|active| {
|
|
||||||
rsc.widgets().get_dyn(id).is_some_and(|widget| {
|
|
||||||
AXES.into_iter()
|
|
||||||
.zip(active.declared)
|
|
||||||
.any(|(axis, was)| declared_len(widget, axis) != was)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
let top = match box_changed {
|
|
||||||
true => self.top_reader(id),
|
|
||||||
false => None,
|
|
||||||
}
|
|
||||||
.or_else(|| self.derived_box_reader(id))
|
|
||||||
.or_else(|| {
|
|
||||||
declared_changed
|
|
||||||
.then(|| self.active.get(&id).and_then(|active| active.parent))
|
|
||||||
.flatten()
|
|
||||||
});
|
|
||||||
if let Some(top) = top {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::EagerReaderRedraws);
|
|
||||||
self.mark_below(id, top, rsc);
|
|
||||||
self.redraw(top, rsc);
|
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
|
self.draw_started.remove(&id);
|
||||||
|
// check if parent depends on the desired size of this, if so then redraw it first
|
||||||
|
for axis in [Axis::X, Axis::Y] {
|
||||||
|
if let Some(&(outer, old)) = self.cache.size.axis_dyn(axis).get(&id)
|
||||||
|
&& let Some(current) = self.active.get(&id)
|
||||||
|
&& let Some(pid) = current.parent
|
||||||
|
{
|
||||||
|
self.cache.size.axis_dyn(axis).remove(&id);
|
||||||
|
let new = self.size_ctx(id, outer, rsc).len_axis(id, axis);
|
||||||
|
self.cache.size.axis_dyn(axis).insert(id, (outer, new));
|
||||||
|
if new != old {
|
||||||
|
self.redraw(pid, rsc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if self.draw_started.contains(&id) {
|
if self.draw_started.contains(&id) {
|
||||||
return;
|
return;
|
||||||
@@ -795,90 +282,34 @@ impl UiRenderState {
|
|||||||
let Some(active) = self.remove(id, false, rsc) else {
|
let Some(active) = self.remove(id, false, rsc) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::LocalRedraws);
|
|
||||||
|
|
||||||
let old_size = active.size;
|
self.draw_inner(
|
||||||
let size = self.draw_inner(
|
|
||||||
active.layer,
|
active.layer,
|
||||||
id,
|
id,
|
||||||
active.region,
|
active.region,
|
||||||
active.parent,
|
active.parent,
|
||||||
active.depth,
|
|
||||||
active.parent_move,
|
|
||||||
active.move_idx != active.parent_move,
|
|
||||||
active.mask,
|
active.mask,
|
||||||
Some(active),
|
Some(active.children),
|
||||||
rsc,
|
rsc,
|
||||||
);
|
);
|
||||||
|
|
||||||
if size != old_size {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::SizeChanges);
|
|
||||||
if let Some(parent) = self.active.get(&id).and_then(|active| active.parent)
|
|
||||||
&& self
|
|
||||||
.active
|
|
||||||
.get(&parent)
|
|
||||||
.is_some_and(|active| active.size_deps.contains(&id))
|
|
||||||
{
|
|
||||||
// Propagate one dependency edge at a time. If drawing the reader
|
|
||||||
// does not change its own size, nothing above it can observe this.
|
|
||||||
rsc.widgets_mut().needs_redraw.insert(parent);
|
|
||||||
self.invalid_sizes.insert(parent);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::ReaderEdges);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The highest reader up the chain that gave what it read a box other
|
pub(super) fn size_ctx<'b>(
|
||||||
/// than the one it asked in, on an axis this widget's size reads. Above
|
&'b mut self,
|
||||||
/// it every box is a constraint rather than an answer. It is the highest
|
source: WidgetId,
|
||||||
/// and not the nearest because a pass-through hands a derived box down
|
outer: UiVec2,
|
||||||
/// unchanged.
|
rsc: &'b mut dyn UiRsc,
|
||||||
fn derived_box_reader(&self, id: WidgetId) -> Option<WidgetId> {
|
) -> SizeCtx<'b> {
|
||||||
let reads = self.active.get(&id)?.size_box_inputs;
|
let ui = rsc.ui_mut();
|
||||||
let mut top = None;
|
SizeCtx {
|
||||||
for (active, parent) in self.reader_chain(id) {
|
source,
|
||||||
let px = self.px_of(active.parent_move, active.region);
|
cache: &mut self.cache,
|
||||||
if AXES.into_iter().zip(reads).any(|(axis, r)| {
|
text: &mut ui.text,
|
||||||
r && pixel_len_changed(active.offered_px.axis(axis), px.axis(axis))
|
textures: &mut ui.textures,
|
||||||
}) {
|
widgets: &ui.widgets,
|
||||||
top = Some(parent);
|
outer,
|
||||||
}
|
output_size: self.output_size,
|
||||||
}
|
id: source,
|
||||||
top
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The furthest ancestor that read this widget's size, directly or through
|
|
||||||
/// widgets that did the same.
|
|
||||||
fn top_reader(&self, id: WidgetId) -> Option<WidgetId> {
|
|
||||||
self.reader_chain(id).last().map(|(_, parent)| parent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Each widget from `id` upward whose parent read its size, with that
|
|
||||||
/// parent.
|
|
||||||
fn reader_chain(&self, id: WidgetId) -> impl Iterator<Item = (&ActiveData, WidgetId)> {
|
|
||||||
let mut at = Some(id);
|
|
||||||
std::iter::from_fn(move || {
|
|
||||||
let active = self.active.get(&at?)?;
|
|
||||||
let parent = active.parent?;
|
|
||||||
let read = self.active.get(&parent)?.size_deps.contains(&active.id);
|
|
||||||
at = read.then_some(parent);
|
|
||||||
read.then_some((active, parent))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Marks everything from `id` up to, and not including, `top`, so that
|
|
||||||
/// drawing `top` draws each of them rather than reusing it.
|
|
||||||
fn mark_below(&self, id: WidgetId, top: WidgetId, rsc: &mut dyn UiRsc) {
|
|
||||||
let mut at = id;
|
|
||||||
while at != top {
|
|
||||||
rsc.widgets_mut().needs_redraw.insert(at);
|
|
||||||
let Some(parent) = self.active.get(&at).and_then(|active| active.parent) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
at = parent;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 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> {
|
||||||
|
|||||||
+22
-1
@@ -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 {
|
||||||
|
|||||||
@@ -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,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
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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),)*]
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-35
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Axis, Len, Painter, Size};
|
use crate::{Axis, AxisT, Len, Painter, SizeCtx, UiRsc};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
mod data;
|
mod data;
|
||||||
@@ -15,48 +15,32 @@ pub use tag::*;
|
|||||||
pub use view::*;
|
pub use view::*;
|
||||||
pub use widgets::*;
|
pub use widgets::*;
|
||||||
|
|
||||||
/// What may be done to a widget's drawing when the box it was given changes
|
pub trait Widget: Any {
|
||||||
/// on this axis, instead of drawing it again. Asked per axis, because wrapped
|
fn draw(&mut self, painter: &mut Painter);
|
||||||
/// text reads the width it is offered and not the height.
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len;
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len;
|
||||||
pub enum OnResize {
|
|
||||||
Scale,
|
|
||||||
/// Reserved: nothing reads this yet, so a widget saying it is redrawn.
|
|
||||||
/// Keeping an unchanged drawing in a bigger box needs the widget to say
|
|
||||||
/// *where* in that box it should sit, which is the alignment work.
|
|
||||||
Translate,
|
|
||||||
#[default]
|
|
||||||
Redraw,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Widget: Any {
|
pub trait WidgetAxisFns {
|
||||||
/// Draws the widget, and returns what it used of the box it was given.
|
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len;
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size;
|
}
|
||||||
|
|
||||||
/// An exact length the widget can give without a painter or its children.
|
impl<W: Widget + ?Sized> WidgetAxisFns for W {
|
||||||
/// 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.
|
match A::get() {
|
||||||
fn size_hint(&self, _axis: Axis) -> Option<Len> {
|
Axis::X => self.desired_width(ctx),
|
||||||
None
|
Axis::Y => self.desired_height(ctx),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _axis: Axis) -> OnResize {
|
|
||||||
OnResize::default()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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<Len> {
|
Len::ZERO
|
||||||
Some(Len::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_resize(&self, _axis: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,3 +62,4 @@ impl<Rsc: UiRsc, V: WidgetView> WidgetLike<Rsc, ViewTag> for V {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct ArrTag;
|
pub struct ArrTag;
|
||||||
|
pub struct IterTag;
|
||||||
+1
-1
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,7 +16,7 @@ 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),
|
||||||
@@ -143,7 +144,7 @@ impl DefaultAppState for Client {
|
|||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let main = WidgetPtr::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| {
|
||||||
@@ -208,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
@@ -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;
|
||||||
|
|||||||
@@ -1,63 +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());
|
|
||||||
|
|
||||||
let aligned = (
|
|
||||||
wtext("left").size(24).text_align(Align::LEFT),
|
|
||||||
wtext("centred").size(24).text_align(Align::CENTER),
|
|
||||||
wtext("right").size(24).text_align(Align::RIGHT),
|
|
||||||
)
|
|
||||||
.span(Dir::DOWN)
|
|
||||||
.gap(8)
|
|
||||||
.pad(16)
|
|
||||||
.background(panel());
|
|
||||||
|
|
||||||
// The same words in half the width, which is a different number of
|
|
||||||
// lines and so a different height. A declared width only holds along
|
|
||||||
// a span's own axis, hence the row.
|
|
||||||
let narrow = (
|
|
||||||
wtext(SAMPLE)
|
|
||||||
.size(20)
|
|
||||||
.wrap(true)
|
|
||||||
.pad(16)
|
|
||||||
.background(panel())
|
|
||||||
.align(Align::TOP)
|
|
||||||
.width(rel(0.5)),
|
|
||||||
rect(Color::WHITE.darker(0.95)),
|
|
||||||
)
|
|
||||||
.span(Dir::RIGHT);
|
|
||||||
|
|
||||||
(wrapped, aligned, narrow)
|
|
||||||
.span(Dir::DOWN)
|
|
||||||
.gap(12)
|
|
||||||
.pad(12)
|
|
||||||
.set_root(rsc, &mut ui_state);
|
|
||||||
Self { ui_state }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -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
@@ -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
|
||||||
@@ -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 }
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[toolchain]
|
|
||||||
channel = "nightly"
|
|
||||||
components = ["clippy", "rustfmt"]
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
@@ -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 {}
|
||||||
+153
-66
@@ -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 => {
|
||||||
@@ -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
@@ -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
@@ -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(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,
|
|
||||||
size: region.bot_right - region.top_left,
|
|
||||||
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
@@ -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();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-320
@@ -1,320 +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::util::Vec2::new($x0 as f32, $y0 as f32),
|
|
||||||
bot_right: $crate::core::util::Vec2::new($x1 as 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 rsc = DefaultRsc::init(Arc::new(Queue(send)));
|
|
||||||
let mut render = UiRenderState::new();
|
|
||||||
render.resize(size);
|
|
||||||
Self {
|
|
||||||
rsc,
|
|
||||||
render,
|
|
||||||
state: HarnessState::default(),
|
|
||||||
updates,
|
|
||||||
cursor: CursorState::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn size(&self) -> Vec2 {
|
|
||||||
self.render.output_size()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
|
||||||
self.render.resize(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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
@@ -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;
|
||||||
|
|||||||
-317
@@ -1,317 +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 `SetSize`, by axis.
|
|
||||||
pub type Lens = [Option<Len>; 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 `SetSize` wrappers were made.
|
|
||||||
pub sizes: HashMap<usize, Lens>,
|
|
||||||
/// Which children a span has, by the order the spans were made.
|
|
||||||
pub spans: HashMap<usize, SpanEdit>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<WeakWidget<SetSize>>,
|
|
||||||
pub spans: Vec<Spanned>,
|
|
||||||
pub scrolls: Vec<WeakWidget<Scroll>>,
|
|
||||||
/// Children a `SpanEdit` took out, held so that dropping the last share
|
|
||||||
/// of one does not free its id for the next widget to be given -- which
|
|
||||||
/// would put the two trees' `ids` out of step.
|
|
||||||
pub detached: Vec<StrongWidget>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 mut top = UiRegion::FULL;
|
|
||||||
top.y.end = top.y.start.offset(40.0);
|
|
||||||
let measured = painter.place(&self.probe, top).len(Axis::X);
|
|
||||||
let px = measured.apply_rest().to_px(painter.px_len(Axis::X));
|
|
||||||
|
|
||||||
let mut rest = UiRegion::FULL;
|
|
||||||
rest.y.start = rest.y.start.offset(40.0);
|
|
||||||
match px > self.threshold {
|
|
||||||
true => painter.place(&self.wide, rest),
|
|
||||||
false => painter.place(&self.narrow, rest),
|
|
||||||
};
|
|
||||||
Size::REST
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Spanned {
|
|
||||||
pub id: WeakWidget<Span>,
|
|
||||||
/// Leaves grown with the span whether or not they end up in it, so both
|
|
||||||
/// trees make the same widgets in the same order either way. Attaching
|
|
||||||
/// one moves it out of here: a widget belongs to one parent, and one that
|
|
||||||
/// belongs to nobody still has to be held or it reads as a leak.
|
|
||||||
pub spares: Vec<StrongWidget>,
|
|
||||||
/// How many children it was grown with, before any edit.
|
|
||||||
pub grown: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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) {
|
|
||||||
let mut grow = Grow {
|
|
||||||
rsc,
|
|
||||||
rng: Rng::new(seed),
|
|
||||||
tree: Tree::default(),
|
|
||||||
edits,
|
|
||||||
};
|
|
||||||
let root = grow.node(depth);
|
|
||||||
(root, grow.tree)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Grow<'a, Rsc> {
|
|
||||||
rsc: &'a mut Rsc,
|
|
||||||
rng: Rng,
|
|
||||||
tree: Tree,
|
|
||||||
edits: &'a Edits,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
|
|
||||||
fn leaf(&mut self) -> StrongWidget {
|
|
||||||
let id: StrongWidget = match self.rng.below(4) {
|
|
||||||
// Wrapped and unwrapped, because only one of them reads the width
|
|
||||||
// it is given and so only one has to be drawn again for a new one.
|
|
||||||
0 => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
|
|
||||||
1 => wtext("one line, overflowing whatever it is given")
|
|
||||||
.size(16)
|
|
||||||
.wrap(false)
|
|
||||||
.add_strong(self.rsc),
|
|
||||||
_ => {
|
|
||||||
let color = COLORS[self.rng.below(COLORS.len())];
|
|
||||||
let alpha = (self.rng.below(5) * 63) as u8;
|
|
||||||
rect(color.alpha(alpha)).add_strong(self.rsc)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn len(&mut self) -> Option<Len> {
|
|
||||||
match self.rng.below(4) {
|
|
||||||
0 => Some(Len::px(20.0 + self.rng.below(180) as f32)),
|
|
||||||
1 => Some(Len::REST),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn align(&mut self) -> Align {
|
|
||||||
let mut axis = || match self.rng.below(4) {
|
|
||||||
0 => None,
|
|
||||||
1 => Some(AxisAlign::Neg),
|
|
||||||
2 => Some(AxisAlign::Center),
|
|
||||||
_ => Some(AxisAlign::Pos),
|
|
||||||
};
|
|
||||||
let (mut x, y) = (axis(), axis());
|
|
||||||
// Aligning on neither axis is just another transparent wrapper and
|
|
||||||
// would leave this branch unexercised.
|
|
||||||
if x.is_none() && y.is_none() {
|
|
||||||
x = Some(AxisAlign::Center);
|
|
||||||
}
|
|
||||||
Align { x, y }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A declared size over half the tree, kept where a test can change it.
|
|
||||||
fn sized(&mut self, inner: StrongWidget) -> StrongWidget {
|
|
||||||
if !self.rng.chance() {
|
|
||||||
return inner;
|
|
||||||
}
|
|
||||||
let idx = self.tree.sized.len();
|
|
||||||
let lens = [self.len(), self.len()];
|
|
||||||
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens);
|
|
||||||
let id = SetSize {
|
|
||||||
inner,
|
|
||||||
x: lens[0],
|
|
||||||
y: lens[1],
|
|
||||||
}
|
|
||||||
.add(self.rsc);
|
|
||||||
self.tree.sized.push(id);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
id.add_strong(self.rsc)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn node(&mut self, depth: usize) -> StrongWidget {
|
|
||||||
if depth == 0 {
|
|
||||||
return self.leaf();
|
|
||||||
}
|
|
||||||
let positioned = self.rng.below(6);
|
|
||||||
if positioned == 0 {
|
|
||||||
// Scrolling reads the pixel length of its box, which nothing
|
|
||||||
// else here does, and gives its child a box longer than its own.
|
|
||||||
let inner = self.node(depth - 1);
|
|
||||||
let inner = self.sized(inner);
|
|
||||||
let axis = if self.rng.chance() { Axis::X } else { Axis::Y };
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
if positioned == 2 {
|
|
||||||
// Both sides are grown either way, so a tree that draws one has
|
|
||||||
// the same ids as a tree that draws the other.
|
|
||||||
let probe = self.node(depth - 1);
|
|
||||||
let wide = self.node(depth - 1);
|
|
||||||
let narrow = self.node(depth - 1);
|
|
||||||
let threshold = self.rng.below(500) as f32;
|
|
||||||
let id = Branch {
|
|
||||||
probe,
|
|
||||||
wide,
|
|
||||||
narrow,
|
|
||||||
threshold,
|
|
||||||
}
|
|
||||||
.add(self.rsc);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
return id.add_strong(self.rsc);
|
|
||||||
}
|
|
||||||
if positioned == 1 {
|
|
||||||
let inner = self.node(depth - 1);
|
|
||||||
let inner = self.sized(inner);
|
|
||||||
let id = Aligned {
|
|
||||||
inner,
|
|
||||||
align: self.align(),
|
|
||||||
}
|
|
||||||
.add_strong(self.rsc);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
if self.rng.below(4) == 0 {
|
|
||||||
let inner = self.node(depth - 1);
|
|
||||||
let inner = self.sized(inner);
|
|
||||||
// Each side its own, since a padding that is the same all round
|
|
||||||
// hides anything that treats one edge differently from another.
|
|
||||||
let mut side = || self.rng.below(24) as f32;
|
|
||||||
let padding = Padding {
|
|
||||||
left: side(),
|
|
||||||
right: side(),
|
|
||||||
top: side(),
|
|
||||||
bottom: side(),
|
|
||||||
};
|
|
||||||
let id = Pad { padding, inner }.add_strong(self.rsc);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
let grown = 2 + self.rng.below(3);
|
|
||||||
let mut children = Vec::with_capacity(grown);
|
|
||||||
for _ in 0..grown {
|
|
||||||
let child = self.node(depth - 1);
|
|
||||||
children.push(self.sized(child));
|
|
||||||
}
|
|
||||||
if self.rng.chance() {
|
|
||||||
let id = Stack {
|
|
||||||
children,
|
|
||||||
size: StackSize::Child(0),
|
|
||||||
}
|
|
||||||
.add_strong(self.rsc);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
// Grown either way, so the widget after them has the same id in a
|
|
||||||
// tree that leaves them out as in one that puts them in.
|
|
||||||
let mut spares: Vec<StrongWidget> = (0..SPARES).map(|_| self.leaf()).collect();
|
|
||||||
let idx = self.tree.spans.len();
|
|
||||||
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
|
|
||||||
// Highest first, so an index means the same child however many of its
|
|
||||||
// neighbours are going too.
|
|
||||||
let mut detach = edit.detach.clone();
|
|
||||||
detach.sort_unstable();
|
|
||||||
for j in detach.into_iter().rev() {
|
|
||||||
if j < children.len() {
|
|
||||||
self.tree.detached.push(children.remove(j));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let attach = edit.attach.min(spares.len());
|
|
||||||
children.extend(spares.drain(..attach));
|
|
||||||
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][self.rng.below(4)];
|
|
||||||
let id = Span {
|
|
||||||
children,
|
|
||||||
dir,
|
|
||||||
gap: self.rng.below(3) as f32 * 4.0,
|
|
||||||
}
|
|
||||||
.add(self.rsc);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
self.tree.spans.push(Spanned { id, spares, grown });
|
|
||||||
id.add_strong(self.rsc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+6
-7
@@ -6,17 +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<Len> {
|
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||||
Some(Len::px(self.handle.size().axis(axis)))
|
Len::abs(self.handle.size().x)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
||||||
OnResize::Scale
|
Len::abs(self.handle.size().y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-5
@@ -5,13 +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(painter.region());
|
painter.set_mask(painter.region());
|
||||||
painter.widget(&self.inner).size()
|
painter.widget(&self.inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// It clips to the box it was given, not to the part its child used.
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
ctx.width(&self.inner)
|
||||||
OnResize::Redraw
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
ctx.height(&self.inner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ mod ptr;
|
|||||||
mod rect;
|
mod rect;
|
||||||
mod text;
|
mod text;
|
||||||
mod trait_fns;
|
mod trait_fns;
|
||||||
|
mod selector;
|
||||||
|
|
||||||
pub use image::*;
|
pub use image::*;
|
||||||
pub use mask::*;
|
pub use mask::*;
|
||||||
@@ -13,3 +14,4 @@ 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 selector::*;
|
||||||
@@ -6,37 +6,30 @@ pub struct Aligned {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Aligned {
|
impl Widget for Aligned {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
let known = match self.align.tuple() {
|
|
||||||
(Some(_), Some(_)) => painter
|
|
||||||
.known_len(&self.inner, Axis::X, UiRegion::FULL)
|
|
||||||
.zip(painter.known_len(&self.inner, Axis::Y, UiRegion::FULL))
|
|
||||||
.map(|(x, y)| Size { x, y }),
|
|
||||||
(Some(_), None) => painter
|
|
||||||
.known_len(&self.inner, Axis::X, UiRegion::FULL)
|
|
||||||
.map(|x| Size { x, y: Len::REST }),
|
|
||||||
(None, Some(_)) => painter
|
|
||||||
.known_len(&self.inner, Axis::Y, UiRegion::FULL)
|
|
||||||
.map(|y| Size { x: Len::REST, y }),
|
|
||||||
(None, None) => Some(Size::REST),
|
|
||||||
};
|
|
||||||
// Drawn where it may be too big only when the aligned axes are not
|
|
||||||
// already known, then given its aligned box once its size is known.
|
|
||||||
let had_size = known.is_some();
|
|
||||||
let size = known.unwrap_or_else(|| painter.place(&self.inner, UiRegion::FULL).size());
|
|
||||||
let region = match self.align.tuple() {
|
let region = match self.align.tuple() {
|
||||||
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
|
(Some(x), Some(y)) => painter
|
||||||
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
|
.size(&self.inner)
|
||||||
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
|
.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,
|
(None, None) => UiRegion::FULL,
|
||||||
};
|
};
|
||||||
let placed = painter.place(&self.inner, region).size();
|
painter.widget_within(&self.inner, region);
|
||||||
if had_size { placed } else { size }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The aligned box is a fraction of its own, so the child keeps its
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
/// length and stays against the edge it was aligned to.
|
ctx.width(&self.inner)
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
}
|
||||||
OnResize::Scale
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
ctx.height(&self.inner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,14 +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 on_resize(&self, _: Axis) -> OnResize {
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
OnResize::Scale
|
ctx.width(&self.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
ctx.height(&self.inner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,20 +6,43 @@ pub struct MaxSize {
|
|||||||
pub y: Option<Len>,
|
pub y: Option<Len>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for MaxSize {
|
impl MaxSize {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
|
||||||
let child = painter.widget(&self.inner).size();
|
if let Some(x) = self.x {
|
||||||
let output = painter.output_size();
|
ctx.outer.x.select_len(x.apply_rest());
|
||||||
Size {
|
}
|
||||||
x: capped(child.x, self.x, output.x),
|
if let Some(y) = self.y {
|
||||||
y: capped(child.y, self.y, output.y),
|
ctx.outer.y.select_len(y.apply_rest());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn capped(len: Len, max: Option<Len>, output: f32) -> Len {
|
impl Widget for MaxSize {
|
||||||
match max {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
Some(max) if len.apply_rest().to_px(output) > max.apply_rest().to_px(output) => max,
|
painter.widget(&self.inner);
|
||||||
_ => len,
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ mod max_size;
|
|||||||
mod offset;
|
mod offset;
|
||||||
mod pad;
|
mod pad;
|
||||||
mod scroll;
|
mod scroll;
|
||||||
mod set_size;
|
mod sized;
|
||||||
mod span;
|
mod span;
|
||||||
mod stack;
|
mod stack;
|
||||||
|
|
||||||
@@ -14,6 +14,6 @@ pub use max_size::*;
|
|||||||
pub use offset::*;
|
pub use offset::*;
|
||||||
pub use pad::*;
|
pub use pad::*;
|
||||||
pub use scroll::*;
|
pub use scroll::*;
|
||||||
pub use set_size::*;
|
pub use sized::*;
|
||||||
pub use span::*;
|
pub use span::*;
|
||||||
pub use stack::*;
|
pub use stack::*;
|
||||||
@@ -6,12 +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) {
|
||||||
let region = UiRegion::FULL.offset(self.amt);
|
let region = UiRegion::FULL.offset(self.amt);
|
||||||
painter.widget_within(&self.inner, region).size()
|
painter.widget_within(&self.inner, region);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
OnResize::Scale
|
ctx.width(&self.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
ctx.height(&self.inner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+24
-22
@@ -6,26 +6,28 @@ 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) {
|
||||||
let inner = painter
|
painter.widget_within(&self.inner, self.padding.region());
|
||||||
.widget_within(&self.inner, self.padding.region())
|
|
||||||
.size();
|
|
||||||
Size {
|
|
||||||
x: Len {
|
|
||||||
px: inner.x.px + self.padding.left + self.padding.right,
|
|
||||||
..inner.x
|
|
||||||
},
|
|
||||||
y: Len {
|
|
||||||
px: inner.y.px + self.padding.top + self.padding.bottom,
|
|
||||||
..inner.y
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The padding is an offset from each edge, so a longer box pads the same
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
/// amount and the child takes the rest.
|
let width = self.padding.left + self.padding.right;
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
let height = self.padding.top + self.padding.bottom;
|
||||||
OnResize::Scale
|
ctx.outer.x.abs -= width;
|
||||||
|
ctx.outer.y.abs -= height;
|
||||||
|
let mut size = ctx.width(&self.inner);
|
||||||
|
size.abs += width;
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
let width = self.padding.left + self.padding.right;
|
||||||
|
let height = self.padding.top + self.padding.bottom;
|
||||||
|
ctx.outer.x.abs -= width;
|
||||||
|
ctx.outer.y.abs -= height;
|
||||||
|
let mut size = ctx.height(&self.inner);
|
||||||
|
size.abs += height;
|
||||||
|
size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,10 +57,10 @@ impl Padding {
|
|||||||
}
|
}
|
||||||
pub fn region(&self) -> UiRegion {
|
pub fn region(&self) -> UiRegion {
|
||||||
let mut region = UiRegion::FULL;
|
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 x(amt: impl UiNum) -> Self {
|
pub fn x(amt: impl UiNum) -> Self {
|
||||||
|
|||||||
@@ -10,21 +10,15 @@ pub struct Scroll {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Scroll {
|
impl Widget for Scroll {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
let output_len = painter.output_len(self.axis);
|
let output_len = painter.output_size().axis(self.axis);
|
||||||
// Its size is its content's, whatever box that is scrolled within.
|
let container_len = painter.region().axis(self.axis).len();
|
||||||
let container_len = UiScalar::px(painter.px_len_for_draw(self.axis));
|
let content_len = painter
|
||||||
// Draw in the whole container only when its scrolling-axis length is
|
.len_axis(&self.inner, self.axis)
|
||||||
// not already known, then place it at the scrolled offset.
|
|
||||||
let known_len = painter.known_len(&self.inner, self.axis, UiRegion::FULL);
|
|
||||||
let measured = known_len.is_none();
|
|
||||||
let child = measured.then(|| painter.place(&self.inner, UiRegion::FULL).size());
|
|
||||||
let content_len = known_len
|
|
||||||
.unwrap_or_else(|| child.unwrap().axis(self.axis))
|
|
||||||
.apply_rest()
|
.apply_rest()
|
||||||
.within_len(container_len)
|
.within_len(container_len)
|
||||||
.to_px(output_len);
|
.to_abs(output_len);
|
||||||
self.container_len = container_len.to_px(output_len);
|
self.container_len = container_len.to_abs(output_len);
|
||||||
self.content_len = content_len;
|
self.content_len = content_len;
|
||||||
|
|
||||||
if self.snap_end {
|
if self.snap_end {
|
||||||
@@ -34,8 +28,15 @@ impl Widget for Scroll {
|
|||||||
|
|
||||||
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
|
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
|
||||||
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
||||||
let placed = painter.place(&self.inner, region).size();
|
painter.widget_within(&self.inner, region);
|
||||||
child.unwrap_or(placed)
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
pub struct SetSize {
|
|
||||||
pub inner: StrongWidget,
|
|
||||||
pub x: Option<Len>,
|
|
||||||
pub y: Option<Len>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for SetSize {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
||||||
// Nothing to apply: a declared length is taken where this widget is
|
|
||||||
// drawn, so the box it has already is that length, and `rest` is a
|
|
||||||
// share only whoever divides a length can work out. Both reach them
|
|
||||||
// through `size_hint`.
|
|
||||||
let child = painter.widget(&self.inner).size();
|
|
||||||
Size {
|
|
||||||
x: self.x.unwrap_or(child.x),
|
|
||||||
y: self.y.unwrap_or(child.y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A declared axis is known without looking at the child, which is what
|
|
||||||
/// lets a span lay out around `.height(rest(1))` without drawing it.
|
|
||||||
fn size_hint(&self, axis: Axis) -> Option<Len> {
|
|
||||||
match axis {
|
|
||||||
Axis::X => self.x,
|
|
||||||
Axis::Y => self.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
+112
-61
@@ -8,73 +8,43 @@ pub struct Span {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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());
|
||||||
// A length for every child before any is placed: from its own hint
|
|
||||||
// where it has one, and from drawing it where it does not.
|
|
||||||
let mut cursor = UiScalar::rel_min();
|
|
||||||
let mut lens = Vec::with_capacity(self.children.len());
|
|
||||||
for child in &self.children {
|
|
||||||
let mut span = UiSpan::new(cursor, UiScalar::rel_max());
|
|
||||||
if self.dir.sign == Sign::Neg {
|
|
||||||
span.flip();
|
|
||||||
}
|
|
||||||
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
|
||||||
let len = match painter.known_len(child, axis, region) {
|
|
||||||
Some(len) => len,
|
|
||||||
None => painter.place(child, region).len(axis),
|
|
||||||
};
|
|
||||||
cursor.px += len.px + self.gap;
|
|
||||||
cursor.rel += len.rel;
|
|
||||||
lens.push(len);
|
|
||||||
}
|
|
||||||
|
|
||||||
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
|
|
||||||
let total = lens.iter().fold(Len::px(gap), |sum, len| sum + *len);
|
|
||||||
|
|
||||||
let mut start = UiScalar::rel_min();
|
let mut start = UiScalar::rel_min();
|
||||||
let mut ortho = Len::ZERO;
|
for child in &self.children {
|
||||||
for (child, len) in self.children.iter().zip(&lens) {
|
|
||||||
let mut span = UiSpan::FULL;
|
let mut span = UiSpan::FULL;
|
||||||
span.start = start;
|
span.start = start;
|
||||||
|
let len = painter.len_axis(child, self.dir.axis);
|
||||||
if len.rest > 0.0 {
|
if len.rest > 0.0 {
|
||||||
let offset = UiScalar::new(total.rel, total.px);
|
let offset = UiScalar::new(total.rel, total.abs);
|
||||||
let rel_end = UiScalar::rel(len.rest / total.rest);
|
let rel_end = UiScalar::rel(len.rest / total.rest);
|
||||||
let end = (UiScalar::rel_max() + start) - offset;
|
let end = (UiScalar::rel_max() + start) - offset;
|
||||||
start = rel_end.within(&start.to(end));
|
start = rel_end.within(&start.to(end));
|
||||||
}
|
}
|
||||||
start.px += len.px;
|
start.abs += len.abs;
|
||||||
start.rel += len.rel;
|
start.rel += len.rel;
|
||||||
span.end = start;
|
span.end = start;
|
||||||
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
let mut child_region = UiRegion::from_axis(self.dir.axis, span, UiSpan::FULL);
|
||||||
if self.dir.sign == Sign::Neg {
|
if self.dir.sign == Sign::Neg {
|
||||||
region.flip(axis);
|
child_region.flip(self.dir.axis);
|
||||||
}
|
}
|
||||||
let used = painter.place(child, region).size().axis(!axis);
|
painter.widget_within(child, child_region);
|
||||||
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
|
start.abs += self.gap;
|
||||||
if used.rel > 0.0 || used.rest > 0.0 {
|
|
||||||
ortho = Len::REST;
|
|
||||||
} else if ortho.rest == 0.0 {
|
|
||||||
ortho.px = ortho.px.max(used.px);
|
|
||||||
}
|
|
||||||
start.px += self.gap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carried whole rather than collapsed to one share: a span that sizes
|
|
||||||
// from its children does not resolve `rest`, it passes the weight up,
|
|
||||||
// so nesting spans divides the same space rather than re-dividing a
|
|
||||||
// share of it. Four `rest(1)` children under two spans under one span
|
|
||||||
// get a quarter each, which collapsing to `rest(1)` per level does
|
|
||||||
// not give. Resolution happens at the nearest ancestor with a length,
|
|
||||||
// and the root always has one.
|
|
||||||
let along = total;
|
|
||||||
Size::from_axis(axis, along, ortho)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every child is placed in fractions and offsets of the span's own box,
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
/// so a longer box holds the same layout and the children follow it.
|
match self.dir.axis {
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
Axis::X => self.desired_len(ctx),
|
||||||
OnResize::Scale
|
Axis::Y => self.desired_ortho(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,34 +69,115 @@ 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: f32,
|
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,
|
||||||
|
|||||||
@@ -8,29 +8,30 @@ 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);
|
||||||
let mut size = Size::default();
|
}
|
||||||
for (i, child) in self.children.iter().enumerate() {
|
for child in iter {
|
||||||
match i {
|
painter.next_layer();
|
||||||
0 => painter.child_layer(),
|
painter.widget(child);
|
||||||
_ => painter.next_layer(),
|
|
||||||
}
|
|
||||||
let drawn = painter.widget(child);
|
|
||||||
// Only the child that sizes the stack is read, so the others
|
|
||||||
// changing size does not redraw it.
|
|
||||||
if sizing == Some(i) {
|
|
||||||
size = drawn.size();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
OnResize::Scale
|
match self.size {
|
||||||
|
StackSize::Default => Len::default(),
|
||||||
|
StackSize::Child(i) => ctx.width(&self.children[i]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
match self.size {
|
||||||
|
StackSize::Default => Len::default(),
|
||||||
|
StackSize::Child(i) => ctx.height(&self.children[i]),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,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(),
|
||||||
|
|||||||
+24
-7
@@ -1,22 +1,39 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::marker::Unsize;
|
use std::marker::{Sized, Unsize};
|
||||||
|
|
||||||
pub struct WidgetPtr {
|
pub struct WidgetPtr {
|
||||||
pub inner: Option<StrongWidget>,
|
pub inner: Option<StrongWidget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for WidgetPtr {
|
impl Widget for WidgetPtr {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
match &self.inner {
|
if let Some(id) = &self.inner {
|
||||||
Some(id) => painter.widget(id).size(),
|
painter.widget(id);
|
||||||
None => Size::default(),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
if let Some(id) = &self.inner {
|
||||||
|
ctx.width(id)
|
||||||
|
} else {
|
||||||
|
Len::ZERO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
if let Some(id) = &self.inner {
|
||||||
|
ctx.height(id)
|
||||||
|
} else {
|
||||||
|
Len::ZERO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetPtr {
|
impl WidgetPtr {
|
||||||
pub fn new() -> Self {
|
pub fn new(widget: StrongWidget) -> Self {
|
||||||
Self::default()
|
Self {
|
||||||
|
inner: Some(widget),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn empty() -> Self {
|
pub fn empty() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
+5
-7
@@ -28,23 +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::REST
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size_hint(&self, _: Axis) -> Option<Len> {
|
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||||
Some(Len::REST)
|
Len::rest(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Its box is its primitive's own region, so a new one is written there.
|
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
Len::rest(1)
|
||||||
OnResize::Scale
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+402
-250
@@ -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,118 +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(®ion),
|
||||||
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(®ion),
|
painter.primitive_within(
|
||||||
);
|
RectPrimitive::color(Color::SKY),
|
||||||
|
size.with_x(width)
|
||||||
|
.align(Align::TOP_LEFT)
|
||||||
|
.offset(top_left)
|
||||||
|
.within(®ion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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(®ion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let caret = selection.focus().geometry(layout, CARET_WIDTH);
|
|
||||||
let caret_size = vec2(caret.width() as f32, caret.height() as f32);
|
|
||||||
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
|
||||||
painter.primitive_within(
|
|
||||||
RectPrimitive::color(Color::WHITE),
|
|
||||||
caret_size
|
|
||||||
.align(Align::TOP_LEFT)
|
|
||||||
.offset(top_left)
|
|
||||||
.within(®ion),
|
|
||||||
);
|
|
||||||
size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, axis: Axis) -> OnResize {
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
self.view.on_resize(axis)
|
self.view.desired_width(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
self.view.desired_height(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -162,170 +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 - self.text.region().top_left().to_px(size);
|
let pos = pos - self.text.region().top_left().to_abs(size);
|
||||||
let prev_sel = self.text.selection;
|
let hit = self.text.buf.hit(pos.x, pos.y);
|
||||||
let prev_hit = self.text.double_hit;
|
let sel = &mut self.text.selection;
|
||||||
|
match sel {
|
||||||
let layout = self.layout();
|
TextSelection::None => {
|
||||||
let (selection, double_hit) = if drag {
|
if !drag && let Some(hit) = hit {
|
||||||
let Some(selection) = prev_sel else {
|
*sel = TextSelection::Pos(hit)
|
||||||
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
|
||||||
@@ -350,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;
|
||||||
@@ -380,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 {
|
||||||
@@ -404,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,
|
||||||
@@ -444,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)
|
||||||
@@ -473,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+111
-63
@@ -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,87 +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);
|
||||||
|
region.x.end = region.x.start + UiScalar::abs(dims.x);
|
||||||
|
region.y.end = region.y.start + UiScalar::abs(dims.y);
|
||||||
|
region
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, ctx: &mut SizeCtx) -> RenderedText {
|
||||||
let width = if self.attrs.wrap {
|
let width = if self.attrs.wrap {
|
||||||
Some(painter.px_len(Axis::X))
|
Some(ctx.px_size().x)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
painter.render_text(&mut self.buf, &self.attrs, width)
|
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);
|
|
||||||
let region = tex.size.align(align);
|
|
||||||
let size = Size::px(tex.size);
|
|
||||||
let within = region.within(&painter.region());
|
|
||||||
painter.glyphs(tex, within);
|
|
||||||
(region, size)
|
|
||||||
}
|
}
|
||||||
|
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
/// Wrapping reads the width it is offered, so a wider box reshapes it and
|
if let Some(hint) = &self.hint
|
||||||
/// a taller one does not. Alignment matters too, and separately: glyphs
|
&& let [line] = &self.buf.lines[..]
|
||||||
/// anchored to the start of an axis stay put when that extent changes,
|
&& line.text().is_empty()
|
||||||
/// but centred or end-aligned ones move even though the shaping stands.
|
{
|
||||||
pub fn on_resize(&self, axis: Axis) -> OnResize {
|
ctx.height(hint)
|
||||||
let reshapes = axis == Axis::X && self.attrs.wrap;
|
} else {
|
||||||
let anchored = match axis {
|
Len::abs(self.render(ctx).size.y)
|
||||||
Axis::X => self.align.x,
|
|
||||||
Axis::Y => self.align.y,
|
|
||||||
} == AxisAlign::Neg;
|
|
||||||
match reshapes || !anchored {
|
|
||||||
true => OnResize::Redraw,
|
|
||||||
false => OnResize::Translate,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 on_resize(&self, axis: Axis) -> OnResize {
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
self.view.on_resize(axis)
|
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 {
|
||||||
|
|||||||
+56
-13
@@ -31,9 +31,9 @@ widget_trait! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> {
|
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, Sized> {
|
||||||
let size = size.into();
|
let size = size.into();
|
||||||
move |state| SetSize {
|
move |state| Sized {
|
||||||
inner: self.add_strong(state),
|
inner: self.add_strong(state),
|
||||||
x: Some(size.x),
|
x: Some(size.x),
|
||||||
y: Some(size.y),
|
y: Some(size.y),
|
||||||
@@ -58,18 +58,18 @@ widget_trait! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
|
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
|
||||||
let len = len.into();
|
let len = len.into();
|
||||||
move |state| SetSize {
|
move |state| Sized {
|
||||||
inner: self.add_strong(state),
|
inner: self.add_strong(state),
|
||||||
x: Some(len),
|
x: Some(len),
|
||||||
y: None,
|
y: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
|
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
|
||||||
let len = len.into();
|
let len = len.into();
|
||||||
move |state| SetSize {
|
move |state| Sized {
|
||||||
inner: self.add_strong(state),
|
inner: self.add_strong(state),
|
||||||
x: None,
|
x: None,
|
||||||
y: Some(len),
|
y: Some(len),
|
||||||
@@ -131,18 +131,61 @@ widget_trait! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
//! What the vertex shader's move-chain walk costs, against how deep the chain
|
|
||||||
//! is. Every active widget owns a slot, so the depth a primitive resolves
|
|
||||||
//! through is its depth in the widget tree.
|
|
||||||
//!
|
|
||||||
//! cargo test --release --test chain_cost -- --ignored --nocapture
|
|
||||||
//!
|
|
||||||
//! Timed on the GPU with timestamp queries rather than by the clock: wall time
|
|
||||||
//! here varied by 2x between runs of one unchanged binary. The pass is
|
|
||||||
//! submitted and waited on, so this is the GPU's cost and not the recording
|
|
||||||
//! loop's -- which is what `draw_cost.rs` measures instead.
|
|
||||||
//!
|
|
||||||
//! The instances are two pixels wide so that vertex work dominates; a chain
|
|
||||||
//! walk that does not show up against small quads will not show up against
|
|
||||||
//! anything.
|
|
||||||
//!
|
|
||||||
//! The instance is leaked deliberately, for the reason `draw_cost.rs` gives.
|
|
||||||
|
|
||||||
use iris::prelude::*;
|
|
||||||
use iris_core::{
|
|
||||||
MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode, UiRenderState,
|
|
||||||
UiScalar, UiSpan,
|
|
||||||
};
|
|
||||||
use wgpu::{Color as GpuColor, *};
|
|
||||||
|
|
||||||
const SIZE: u32 = 1024;
|
|
||||||
const INSTANCES: usize = 200_000;
|
|
||||||
const FRAMES: u32 = 20;
|
|
||||||
/// Reported as the best of this many batches, since the mean moves by more
|
|
||||||
/// than the thing being measured.
|
|
||||||
const BATCHES: u32 = 8;
|
|
||||||
|
|
||||||
fn gpu() -> Option<(Device, Queue, f32)> {
|
|
||||||
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
|
|
||||||
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
|
||||||
{
|
|
||||||
Ok(_) => all,
|
|
||||||
Err(_) => Instance::new(InstanceDescriptor {
|
|
||||||
backends: Backends::GL,
|
|
||||||
..InstanceDescriptor::new_without_display_handle()
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
let instance: &'static Instance = Box::leak(Box::new(instance));
|
|
||||||
let adapter =
|
|
||||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
|
|
||||||
if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
|
|
||||||
println!("no timestamp queries on {:?}", adapter.get_info().name);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
println!("adapter: {:?}", adapter.get_info().name);
|
|
||||||
let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor {
|
|
||||||
required_features: Features::TIMESTAMP_QUERY,
|
|
||||||
..Default::default()
|
|
||||||
}))
|
|
||||||
.ok()?;
|
|
||||||
let period = queue.get_timestamp_period();
|
|
||||||
Some((device, queue, period))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config(format: TextureFormat) -> SurfaceConfiguration {
|
|
||||||
SurfaceConfiguration {
|
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
|
||||||
format,
|
|
||||||
color_space: SurfaceColorSpace::Auto,
|
|
||||||
width: SIZE,
|
|
||||||
height: SIZE,
|
|
||||||
present_mode: PresentMode::Fifo,
|
|
||||||
desired_maximum_frame_latency: 2,
|
|
||||||
alpha_mode: CompositeAlphaMode::Auto,
|
|
||||||
view_formats: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A chain `depth` slots long, and instances that all resolve through its end.
|
|
||||||
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
|
||||||
let kind = ui.primitives.kind::<RectPrimitive>();
|
|
||||||
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
|
||||||
|
|
||||||
let mut slot = MoveIdx::NONE;
|
|
||||||
for _ in 0..depth {
|
|
||||||
slot = render.moves.push(slot, UiRegion::FULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
let px = |v: f32| UiScalar { rel: 0.0, px: v };
|
|
||||||
for i in 0..INSTANCES {
|
|
||||||
let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
|
|
||||||
let y = (i / (SIZE as usize / 2)) as f32;
|
|
||||||
render.layers.write(
|
|
||||||
0,
|
|
||||||
PrimitiveInst {
|
|
||||||
kind,
|
|
||||||
id,
|
|
||||||
primitive: RectPrimitive::color(UiColor::WHITE),
|
|
||||||
region: UiRegion::new(
|
|
||||||
UiSpan::new(px(x), px(x + 2.0)),
|
|
||||||
UiSpan::new(px(y), px(y + 1.0)),
|
|
||||||
),
|
|
||||||
mask_idx: MaskIdx::NONE,
|
|
||||||
move_idx: slot,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Nanoseconds the pass took on the GPU, best of `BATCHES`.
|
|
||||||
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
|
|
||||||
let format = TextureFormat::Bgra8Unorm;
|
|
||||||
let mut node = UiRenderNode::new(device, &config(format));
|
|
||||||
let mut ui = UiData::default();
|
|
||||||
let mut render = UiRenderState::new();
|
|
||||||
fill(&mut ui, &mut render, depth);
|
|
||||||
node.update(device, queue, &mut ui, &mut render);
|
|
||||||
|
|
||||||
let target = device.create_texture(&TextureDescriptor {
|
|
||||||
label: Some("chain cost"),
|
|
||||||
size: Extent3d {
|
|
||||||
width: SIZE,
|
|
||||||
height: SIZE,
|
|
||||||
depth_or_array_layers: 1,
|
|
||||||
},
|
|
||||||
mip_level_count: 1,
|
|
||||||
sample_count: 1,
|
|
||||||
dimension: TextureDimension::D2,
|
|
||||||
format,
|
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
|
||||||
view_formats: &[],
|
|
||||||
});
|
|
||||||
let view = target.create_view(&TextureViewDescriptor::default());
|
|
||||||
|
|
||||||
let queries = device.create_query_set(&QuerySetDescriptor {
|
|
||||||
label: Some("chain cost"),
|
|
||||||
ty: QueryType::Timestamp,
|
|
||||||
count: 2,
|
|
||||||
});
|
|
||||||
let resolved = device.create_buffer(&BufferDescriptor {
|
|
||||||
label: Some("resolved"),
|
|
||||||
size: 16,
|
|
||||||
usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC,
|
|
||||||
mapped_at_creation: false,
|
|
||||||
});
|
|
||||||
let readback = device.create_buffer(&BufferDescriptor {
|
|
||||||
label: Some("readback"),
|
|
||||||
size: 16,
|
|
||||||
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
|
|
||||||
mapped_at_creation: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
let frame = || {
|
|
||||||
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
|
|
||||||
{
|
|
||||||
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
|
||||||
label: None,
|
|
||||||
color_attachments: &[Some(RenderPassColorAttachment {
|
|
||||||
view: &view,
|
|
||||||
resolve_target: None,
|
|
||||||
ops: Operations {
|
|
||||||
load: LoadOp::Clear(GpuColor::BLACK),
|
|
||||||
store: StoreOp::Store,
|
|
||||||
},
|
|
||||||
depth_slice: None,
|
|
||||||
})],
|
|
||||||
depth_stencil_attachment: None,
|
|
||||||
timestamp_writes: Some(RenderPassTimestampWrites {
|
|
||||||
query_set: &queries,
|
|
||||||
beginning_of_pass_write_index: Some(0),
|
|
||||||
end_of_pass_write_index: Some(1),
|
|
||||||
}),
|
|
||||||
occlusion_query_set: None,
|
|
||||||
multiview_mask: None,
|
|
||||||
});
|
|
||||||
node.draw(pass);
|
|
||||||
}
|
|
||||||
encoder.resolve_query_set(&queries, 0..2, &resolved, 0);
|
|
||||||
encoder.copy_buffer_to_buffer(&resolved, 0, &readback, 0, 16);
|
|
||||||
queue.submit(Some(encoder.finish()));
|
|
||||||
|
|
||||||
let slice = readback.slice(..);
|
|
||||||
slice.map_async(MapMode::Read, |_| {});
|
|
||||||
let _ = device.poll(PollType::Wait {
|
|
||||||
submission_index: None,
|
|
||||||
timeout: None,
|
|
||||||
});
|
|
||||||
let ns = {
|
|
||||||
let view = slice.get_mapped_range().expect("timestamps did not map");
|
|
||||||
let stamps: [u64; 2] = [
|
|
||||||
u64::from_le_bytes(view[..8].try_into().unwrap()),
|
|
||||||
u64::from_le_bytes(view[8..16].try_into().unwrap()),
|
|
||||||
];
|
|
||||||
(stamps[1].saturating_sub(stamps[0])) as f64 * period as f64
|
|
||||||
};
|
|
||||||
readback.unmap();
|
|
||||||
ns
|
|
||||||
};
|
|
||||||
|
|
||||||
frame();
|
|
||||||
let mut best = f64::MAX;
|
|
||||||
for _ in 0..BATCHES {
|
|
||||||
let mut total = 0.0;
|
|
||||||
for _ in 0..FRAMES {
|
|
||||||
total += frame();
|
|
||||||
}
|
|
||||||
best = best.min(total / FRAMES as f64);
|
|
||||||
}
|
|
||||||
best
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "measurement, not a check"]
|
|
||||||
fn chain_cost_by_depth() {
|
|
||||||
let Some((device, queue, period)) = gpu() else {
|
|
||||||
println!("no gpu with timestamps; nothing measured");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
println!("{INSTANCES} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
|
|
||||||
let mut base = None;
|
|
||||||
for depth in [1, 2, 4, 8, 16, 32, 64] {
|
|
||||||
let ns = pass_cost(&device, &queue, period, depth);
|
|
||||||
let base = *base.get_or_insert(ns);
|
|
||||||
println!(
|
|
||||||
"depth {depth:>3}: {:>9.1} us {:+6.1}% against depth 1",
|
|
||||||
ns / 1000.0,
|
|
||||||
(ns - base) / base * 100.0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +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 mut top = UiRegion::FULL;
|
|
||||||
top.y.end = top.y.start.offset(40.0);
|
|
||||||
let measured = painter.place(&self.probe, top).len(Axis::X);
|
|
||||||
let px = measured.apply_rest().to_px(painter.px_len(Axis::X));
|
|
||||||
|
|
||||||
let mut rest = UiRegion::FULL;
|
|
||||||
rest.y.start = rest.y.start.offset(40.0);
|
|
||||||
match px > self.threshold {
|
|
||||||
true => painter.place(&self.wide, rest),
|
|
||||||
false => painter.place(&self.narrow, rest),
|
|
||||||
};
|
|
||||||
Size::REST
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
//! What one frame of `UiRenderNode::draw` costs on the CPU, against the number
|
|
||||||
//! of layers it walks. Recording only: the pass is built and dropped without
|
|
||||||
//! being submitted, so this is the loop's cost and not the GPU's.
|
|
||||||
//!
|
|
||||||
//! cargo test --release --test draw_cost -- --ignored --nocapture
|
|
||||||
//!
|
|
||||||
//! Wall time is the wrong number to read for anything under a few percent --
|
|
||||||
//! it varied by 2x between runs of one unchanged binary where instructions
|
|
||||||
//! retired varied by 0.1%. Count those instead:
|
|
||||||
//!
|
|
||||||
//! perf stat -e instructions:u target/release/.../draw_cost-* --ignored
|
|
||||||
//!
|
|
||||||
//! That is how `PrimitiveRender` was measured against a match in the renderer:
|
|
||||||
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
|
|
||||||
//! one.
|
|
||||||
//!
|
|
||||||
//! The instance is leaked deliberately. A Vulkan loader may unload the driver
|
|
||||||
//! when the last one drops, which can fault as a thread that used it exits --
|
|
||||||
//! and every test runs on a spawned thread.
|
|
||||||
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use iris::prelude::*;
|
|
||||||
use iris_core::{
|
|
||||||
GlyphPrimitive, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, TextureHandle,
|
|
||||||
TexturePrimitive, UiData, UiRegion, UiRenderNode, UiRenderState,
|
|
||||||
};
|
|
||||||
use wgpu::{Color as GpuColor, *};
|
|
||||||
|
|
||||||
const SIZE: u32 = 1024;
|
|
||||||
const FRAMES: u32 = 200;
|
|
||||||
/// Reported as the best of this many batches, since the mean moves by more
|
|
||||||
/// than the thing being measured.
|
|
||||||
const BATCHES: u32 = 8;
|
|
||||||
|
|
||||||
fn gpu() -> Option<(Device, Queue)> {
|
|
||||||
// Probed rather than assumed: there may be no Vulkan adapter, and GL is
|
|
||||||
// what is left when there is not.
|
|
||||||
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
|
|
||||||
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
|
||||||
{
|
|
||||||
Ok(_) => all,
|
|
||||||
Err(_) => Instance::new(InstanceDescriptor {
|
|
||||||
backends: Backends::GL,
|
|
||||||
..InstanceDescriptor::new_without_display_handle()
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
// Leaked rather than dropped: see the note at the top of the file.
|
|
||||||
let instance: &'static Instance = Box::leak(Box::new(instance));
|
|
||||||
let adapter =
|
|
||||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
|
|
||||||
println!("adapter: {:?}", adapter.get_info());
|
|
||||||
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config(format: TextureFormat) -> SurfaceConfiguration {
|
|
||||||
SurfaceConfiguration {
|
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
|
||||||
format,
|
|
||||||
color_space: SurfaceColorSpace::Auto,
|
|
||||||
width: SIZE,
|
|
||||||
height: SIZE,
|
|
||||||
present_mode: PresentMode::Fifo,
|
|
||||||
desired_maximum_frame_latency: 2,
|
|
||||||
alpha_mode: CompositeAlphaMode::Auto,
|
|
||||||
view_formats: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every layer draws all three primitives, so the renderer takes a different
|
|
||||||
/// path for each list it walks -- which is the case a single-primitive layer
|
|
||||||
/// would never exercise. Images are bound per instance, so there are few.
|
|
||||||
fn fill(
|
|
||||||
ui: &mut UiData,
|
|
||||||
render: &mut UiRenderState,
|
|
||||||
layers: usize,
|
|
||||||
per_layer: usize,
|
|
||||||
) -> Vec<TextureHandle> {
|
|
||||||
let rect = ui.primitives.kind::<RectPrimitive>();
|
|
||||||
let glyph = ui.primitives.kind::<GlyphPrimitive>();
|
|
||||||
let texture = ui.primitives.kind::<TexturePrimitive>();
|
|
||||||
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
|
||||||
let handles: Vec<_> = (0..4)
|
|
||||||
.map(|_| ui.textures.add(image::RgbaImage::new(4, 4)))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut layer = 0;
|
|
||||||
for _ in 0..layers {
|
|
||||||
for _ in 0..per_layer {
|
|
||||||
render.layers.write(
|
|
||||||
layer,
|
|
||||||
PrimitiveInst {
|
|
||||||
kind: rect,
|
|
||||||
id,
|
|
||||||
primitive: RectPrimitive::color(UiColor::WHITE),
|
|
||||||
region: UiRegion::FULL,
|
|
||||||
mask_idx: MaskIdx::NONE,
|
|
||||||
move_idx: MoveIdx::NONE,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
render.layers.write(
|
|
||||||
layer,
|
|
||||||
PrimitiveInst {
|
|
||||||
kind: glyph,
|
|
||||||
id,
|
|
||||||
primitive: GlyphPrimitive {
|
|
||||||
uv_min: vec2(0.0, 0.0),
|
|
||||||
uv_max: vec2(1.0, 1.0),
|
|
||||||
layer: 0,
|
|
||||||
color: UiColor::WHITE,
|
|
||||||
flags: 0,
|
|
||||||
},
|
|
||||||
region: UiRegion::FULL,
|
|
||||||
mask_idx: MaskIdx::NONE,
|
|
||||||
move_idx: MoveIdx::NONE,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for h in &handles[..2] {
|
|
||||||
render.layers.write(
|
|
||||||
layer,
|
|
||||||
PrimitiveInst {
|
|
||||||
kind: texture,
|
|
||||||
id,
|
|
||||||
primitive: TexturePrimitive::from(h),
|
|
||||||
region: UiRegion::FULL,
|
|
||||||
mask_idx: MaskIdx::NONE,
|
|
||||||
move_idx: MoveIdx::NONE,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
layer = render.layers.next(layer);
|
|
||||||
}
|
|
||||||
handles
|
|
||||||
}
|
|
||||||
|
|
||||||
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
|
|
||||||
let format = TextureFormat::Bgra8Unorm;
|
|
||||||
let mut node = UiRenderNode::new(device, &config(format));
|
|
||||||
let mut ui = UiData::default();
|
|
||||||
let mut render = UiRenderState::new();
|
|
||||||
let _handles = fill(&mut ui, &mut render, layers, per_layer);
|
|
||||||
node.update(device, queue, &mut ui, &mut render);
|
|
||||||
|
|
||||||
let target = device.create_texture(&TextureDescriptor {
|
|
||||||
label: Some("draw cost"),
|
|
||||||
size: Extent3d {
|
|
||||||
width: SIZE,
|
|
||||||
height: SIZE,
|
|
||||||
depth_or_array_layers: 1,
|
|
||||||
},
|
|
||||||
mip_level_count: 1,
|
|
||||||
sample_count: 1,
|
|
||||||
dimension: TextureDimension::D2,
|
|
||||||
format,
|
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
|
||||||
view_formats: &[],
|
|
||||||
});
|
|
||||||
let view = target.create_view(&TextureViewDescriptor::default());
|
|
||||||
|
|
||||||
let record = |frames: u32| {
|
|
||||||
let start = Instant::now();
|
|
||||||
for _ in 0..frames {
|
|
||||||
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
|
|
||||||
{
|
|
||||||
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
|
||||||
color_attachments: &[Some(RenderPassColorAttachment {
|
|
||||||
view: &view,
|
|
||||||
resolve_target: None,
|
|
||||||
ops: Operations {
|
|
||||||
load: LoadOp::Clear(GpuColor::BLACK),
|
|
||||||
store: StoreOp::Store,
|
|
||||||
},
|
|
||||||
depth_slice: None,
|
|
||||||
})],
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
node.draw(pass);
|
|
||||||
}
|
|
||||||
drop(encoder.finish());
|
|
||||||
}
|
|
||||||
start.elapsed().as_secs_f64() / frames as f64
|
|
||||||
};
|
|
||||||
record(FRAMES / 4);
|
|
||||||
(0..BATCHES)
|
|
||||||
.map(|_| record(FRAMES))
|
|
||||||
.fold(f64::MAX, f64::min)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "measurement, not a check"]
|
|
||||||
fn draw_cost_by_layer_count() {
|
|
||||||
let Some((device, queue)) = gpu() else {
|
|
||||||
panic!("no wgpu device; see the this-machine-graphics notes");
|
|
||||||
};
|
|
||||||
println!(
|
|
||||||
"layers, each 8 rects + 8 glyphs + 2 images: us/frame (us per layer), best of {BATCHES}"
|
|
||||||
);
|
|
||||||
let base = frame_cost(&device, &queue, 1, 8) * 1e6;
|
|
||||||
for layers in [8, 64, 256, 1024] {
|
|
||||||
let per_frame = frame_cost(&device, &queue, layers, 8) * 1e6;
|
|
||||||
// Net of the empty pass, which is the same in any version of this.
|
|
||||||
println!(
|
|
||||||
"{layers:>5}: {per_frame:8.1} us ({:.3} us)",
|
|
||||||
(per_frame - base).max(0.0) / layers as f64
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,468 +0,0 @@
|
|||||||
//! Random trees, checked against building the same tree cold.
|
|
||||||
//!
|
|
||||||
//! A frame reaches its layout by keeping most of the last one: slots
|
|
||||||
//! rewritten, some widgets drawn again, the rest untouched. The property here
|
|
||||||
//! is that what comes out is the tree a cold start would have produced, so
|
|
||||||
//! anything the retained path carried over that it should not have shows up
|
|
||||||
//! as a difference in somebody's box.
|
|
||||||
//!
|
|
||||||
//! `iris::random` grows the tree and `examples/random.rs` draws one. A seed is
|
|
||||||
//! the whole reproduction; `a_long_run_of_seeds_agrees` is the ignored sweep
|
|
||||||
//! for when it is worth spending the time.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use iris::harness::Harness;
|
|
||||||
use iris::prelude::*;
|
|
||||||
use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow};
|
|
||||||
|
|
||||||
/// How deep the generator branches. The generator widens two to four ways per
|
|
||||||
/// level, so depth is exponential in width and a deep narrow tree is not
|
|
||||||
/// reachable by raising this -- it buys more overlap between dependency
|
|
||||||
/// paths, not more ancestry.
|
|
||||||
fn depth() -> usize {
|
|
||||||
env("IRIS_GENERATED_DEPTH", 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|
||||||
std::env::var(name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse().ok())
|
|
||||||
.unwrap_or(fallback)
|
|
||||||
}
|
|
||||||
const SEEDS: [u64; 7] = [1, 2, 3, 5, 8, 13, 98];
|
|
||||||
const REGION_EPSILON_PX: f32 = 0.05;
|
|
||||||
|
|
||||||
fn same_coordinate(got: f32, want: f32) -> bool {
|
|
||||||
(got - want).abs() <= REGION_EPSILON_PX
|
|
||||||
}
|
|
||||||
|
|
||||||
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
|
|
||||||
match (got, want) {
|
|
||||||
(Some(got), Some(want)) => {
|
|
||||||
same_coordinate(got.top_left.x, want.top_left.x)
|
|
||||||
&& same_coordinate(got.top_left.y, want.top_left.y)
|
|
||||||
&& same_coordinate(got.bot_right.x, want.bot_right.x)
|
|
||||||
&& same_coordinate(got.bot_right.y, want.bot_right.y)
|
|
||||||
}
|
|
||||||
(None, None) => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree {
|
|
||||||
let (root, tree) = grow(&mut h.rsc, seed, depth(), edits);
|
|
||||||
h.state.root = Some(root);
|
|
||||||
h.frame();
|
|
||||||
tree
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resize_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
|
|
||||||
let lens = [
|
|
||||||
Some(Len::px(20.0 + rng.below(180) as f32)),
|
|
||||||
Some(Len::px(20.0 + rng.below(180) as f32)),
|
|
||||||
];
|
|
||||||
let sized = &mut h.rsc[tree.sized[idx]];
|
|
||||||
sized.x = lens[0];
|
|
||||||
sized.y = lens[1];
|
|
||||||
lens
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Changes a few of the declared sizes, and says which, so the cold tree can
|
|
||||||
/// be grown with the same ones.
|
|
||||||
fn edit(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap<usize, Lens> {
|
|
||||||
let mut edits = HashMap::new();
|
|
||||||
for _ in 0..4 {
|
|
||||||
let idx = rng.below(tree.sized.len());
|
|
||||||
edits.insert(idx, resize_one(h, tree, idx, rng));
|
|
||||||
}
|
|
||||||
edits
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every declared size at once, so every reader of a size in the tree has a
|
|
||||||
/// changed descendant in the same frame and the whole dirty set has to settle
|
|
||||||
/// together.
|
|
||||||
fn edit_every(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap<usize, Lens> {
|
|
||||||
(0..tree.sized.len())
|
|
||||||
.map(|idx| (idx, resize_one(h, tree, idx, rng)))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A way of changing what a span holds. Each is a shape worth its own case:
|
|
||||||
/// taking a child out of the middle is not the same as emptying a span, and
|
|
||||||
/// adding one is not the same as adding three.
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
enum Shuffle {
|
|
||||||
/// Every other child, so what is left is interleaved with what went.
|
|
||||||
EveryOther,
|
|
||||||
/// Everything but the first, which is the last step before empty.
|
|
||||||
AllButFirst,
|
|
||||||
/// Three more on the end at once.
|
|
||||||
AddThree,
|
|
||||||
/// The first out and three more on, so the count moves both ways.
|
|
||||||
SwapForThree,
|
|
||||||
/// One out of the middle and one on the end.
|
|
||||||
TradeOne,
|
|
||||||
}
|
|
||||||
|
|
||||||
const SHUFFLES: [Shuffle; 5] = [
|
|
||||||
Shuffle::EveryOther,
|
|
||||||
Shuffle::AllButFirst,
|
|
||||||
Shuffle::AddThree,
|
|
||||||
Shuffle::SwapForThree,
|
|
||||||
Shuffle::TradeOne,
|
|
||||||
];
|
|
||||||
|
|
||||||
impl Shuffle {
|
|
||||||
fn of(self, grown: usize) -> SpanEdit {
|
|
||||||
let all = |step: usize, from: usize| (from..grown).step_by(step).collect();
|
|
||||||
match self {
|
|
||||||
Self::EveryOther => SpanEdit {
|
|
||||||
detach: all(2, 0),
|
|
||||||
attach: 0,
|
|
||||||
},
|
|
||||||
Self::AllButFirst => SpanEdit {
|
|
||||||
detach: all(1, 1),
|
|
||||||
attach: 0,
|
|
||||||
},
|
|
||||||
Self::AddThree => SpanEdit {
|
|
||||||
detach: Vec::new(),
|
|
||||||
attach: 3,
|
|
||||||
},
|
|
||||||
Self::SwapForThree => SpanEdit {
|
|
||||||
detach: vec![0],
|
|
||||||
attach: 3,
|
|
||||||
},
|
|
||||||
Self::TradeOne => SpanEdit {
|
|
||||||
detach: vec![grown / 2],
|
|
||||||
attach: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Applies `shuffle` to every third span, and says what it did so the cold
|
|
||||||
/// tree can be grown that way. The widgets it takes out are given back: the
|
|
||||||
/// last share of one must outlive the comparison, or its id is handed to
|
|
||||||
/// something else and the two trees stop lining up.
|
|
||||||
fn reshuffle(
|
|
||||||
h: &mut Harness,
|
|
||||||
tree: &mut Tree,
|
|
||||||
shuffle: Shuffle,
|
|
||||||
) -> (HashMap<usize, SpanEdit>, Vec<StrongWidget>) {
|
|
||||||
let mut edits = HashMap::new();
|
|
||||||
let mut detached = Vec::new();
|
|
||||||
for (idx, span) in tree.spans.iter_mut().enumerate().step_by(3) {
|
|
||||||
let span_edit = shuffle.of(span.grown);
|
|
||||||
let mut take = span_edit.detach.clone();
|
|
||||||
take.sort_unstable();
|
|
||||||
let children = &mut h.rsc[span.id].children;
|
|
||||||
// Highest first, so an index means the same child however many of
|
|
||||||
// its neighbours are going too.
|
|
||||||
for j in take.into_iter().rev() {
|
|
||||||
if j < children.len() {
|
|
||||||
detached.push(children.remove(j));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let attach = span_edit.attach.min(span.spares.len());
|
|
||||||
children.extend(span.spares.drain(..attach));
|
|
||||||
edits.insert(idx, span_edit);
|
|
||||||
}
|
|
||||||
(edits, detached)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What a widget was configured with, so a tree the generator found can be
|
|
||||||
/// written out by hand. A fuzz failure is a lead; the fast test that replaces
|
|
||||||
/// it has to be buildable from what the failure printed.
|
|
||||||
fn describe(id: WidgetId, h: &Harness) -> String {
|
|
||||||
let label = h.rsc.widgets().label(id).to_string();
|
|
||||||
let Some(widget) = h.rsc.widgets().get_dyn(id) else {
|
|
||||||
return label;
|
|
||||||
};
|
|
||||||
let any: &dyn std::any::Any = widget;
|
|
||||||
let len = |l: &Option<Len>| match l {
|
|
||||||
Some(l) => format!("{l}"),
|
|
||||||
None => "-".into(),
|
|
||||||
};
|
|
||||||
if let Some(w) = any.downcast_ref::<SetSize>() {
|
|
||||||
return format!("SetSize{{x:{},y:{}}}", len(&w.x), len(&w.y));
|
|
||||||
}
|
|
||||||
if let Some(w) = any.downcast_ref::<Span>() {
|
|
||||||
let sign = if w.dir.sign == Sign::Neg { "-" } else { "+" };
|
|
||||||
return format!(
|
|
||||||
"Span{{dir:{:?}{sign},gap:{},n:{}}}",
|
|
||||||
w.dir.axis,
|
|
||||||
w.gap,
|
|
||||||
w.children.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(w) = any.downcast_ref::<Pad>() {
|
|
||||||
let p = &w.padding;
|
|
||||||
return format!(
|
|
||||||
"Pad{{l:{},r:{},t:{},b:{}}}",
|
|
||||||
p.left, p.right, p.top, p.bottom
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(w) = any.downcast_ref::<Aligned>() {
|
|
||||||
let a = |v: Option<AxisAlign>| match v {
|
|
||||||
None => "-",
|
|
||||||
Some(AxisAlign::Neg) => "neg",
|
|
||||||
Some(AxisAlign::Center) => "mid",
|
|
||||||
Some(AxisAlign::Pos) => "pos",
|
|
||||||
};
|
|
||||||
return format!("Aligned{{x:{},y:{}}}", a(w.align.x), a(w.align.y));
|
|
||||||
}
|
|
||||||
if let Some(w) = any.downcast_ref::<Stack>() {
|
|
||||||
return format!("Stack{{n:{}}}", w.children.len());
|
|
||||||
}
|
|
||||||
label
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every widget in one tree against the matching widget in the other. A
|
|
||||||
/// mismatch prints the widget's ancestry, marking the ones that own a slot,
|
|
||||||
/// since where two trees disagree is rarely where the cause is.
|
|
||||||
fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, &Tree)) {
|
|
||||||
let ((wh, wt), (ch, ct)) = (warm, cold);
|
|
||||||
assert_eq!(wt.ids.len(), ct.ids.len(), "seed {seed}: different trees");
|
|
||||||
let mut drawn = 0;
|
|
||||||
let mut wrong = 0;
|
|
||||||
for (i, (&w, &c)) in wt.ids.iter().zip(&ct.ids).enumerate() {
|
|
||||||
let (got, want) = (wh.region(&w), ch.region(&c));
|
|
||||||
drawn += usize::from(got.is_some());
|
|
||||||
// This oracle cares where rasterization lands, not whether equivalent
|
|
||||||
// arithmetic produced the same f32. Keep the tolerance to one
|
|
||||||
// twentieth of a physical pixel, while whether a widget drew remains
|
|
||||||
// exact.
|
|
||||||
if same_region(got, want) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
wrong += 1;
|
|
||||||
if wrong <= 3 {
|
|
||||||
let mut chain = Vec::new();
|
|
||||||
let mut at = Some(w);
|
|
||||||
while let Some(id) = at {
|
|
||||||
let active = &wh.render.active[&id];
|
|
||||||
let slot = match active.move_idx == active.parent_move {
|
|
||||||
true => "",
|
|
||||||
false => "*",
|
|
||||||
};
|
|
||||||
chain.push(format!("{}{slot}", describe(id, wh)));
|
|
||||||
at = active.parent;
|
|
||||||
}
|
|
||||||
println!(
|
|
||||||
"seed {seed} after {what}: widget {i}\n warm {got:?}\n cold {want:?}\n {}",
|
|
||||||
chain.join(" < ")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(drawn > 0, "seed {seed}: nothing was drawn");
|
|
||||||
assert_eq!(wrong, 0, "seed {seed}: {wrong} widgets differ after {what}");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn changed_size(seed: u64) {
|
|
||||||
let mut warm = Harness::new((900, 1200));
|
|
||||||
let grown = plant(&mut warm, seed, &Edits::default());
|
|
||||||
// Not every tree grows a declared size to change.
|
|
||||||
if grown.sized.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rng = Rng::new(seed ^ 0x5eed);
|
|
||||||
let sizes = edit(&mut warm, &grown, &mut rng);
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
let mut cold = Harness::new((900, 1200));
|
|
||||||
let same = plant(
|
|
||||||
&mut cold,
|
|
||||||
seed,
|
|
||||||
&Edits {
|
|
||||||
sizes,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_same(seed, "a size change", (&warm, &grown), (&cold, &same));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reshuffled(seed: u64, shuffle: Shuffle) {
|
|
||||||
let mut warm = Harness::new((900, 1200));
|
|
||||||
let mut grown = plant(&mut warm, seed, &Edits::default());
|
|
||||||
// Some seeds grow nothing but wrappers, and a shuffle with no span to
|
|
||||||
// shuffle is not the same thing as one that had no effect. A span behind
|
|
||||||
// a branch nobody took is the same kind of nothing: it is not drawn, so
|
|
||||||
// shuffling it cannot move anything.
|
|
||||||
let shuffles = grown
|
|
||||||
.spans
|
|
||||||
.iter()
|
|
||||||
.step_by(3)
|
|
||||||
.any(|span| warm.region(&span.id.id()).is_some());
|
|
||||||
if !shuffles {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let before: Vec<_> = grown.ids.iter().map(|id| warm.region(id)).collect();
|
|
||||||
|
|
||||||
let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle);
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
// Or the two trees would agree for want of anything having happened.
|
|
||||||
let after = grown.ids.iter().map(|id| warm.region(id));
|
|
||||||
let moved = before.iter().zip(after).filter(|(a, b)| *a != b).count();
|
|
||||||
assert!(moved > 0, "seed {seed}: {shuffle:?} changed nothing");
|
|
||||||
|
|
||||||
let mut cold = Harness::new((900, 1200));
|
|
||||||
let same = plant(
|
|
||||||
&mut cold,
|
|
||||||
seed,
|
|
||||||
&Edits {
|
|
||||||
spans,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let what = format!("{shuffle:?}");
|
|
||||||
assert_same(seed, &what, (&warm, &grown), (&cold, &same));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn changed_every_size(seed: u64) {
|
|
||||||
let mut warm = Harness::new((900, 1200));
|
|
||||||
let grown = plant(&mut warm, seed, &Edits::default());
|
|
||||||
if grown.sized.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rng = Rng::new(seed ^ 0xa11);
|
|
||||||
let sizes = edit_every(&mut warm, &grown, &mut rng);
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
let mut cold = Harness::new((900, 1200));
|
|
||||||
let same = plant(
|
|
||||||
&mut cold,
|
|
||||||
seed,
|
|
||||||
&Edits {
|
|
||||||
sizes,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_same(seed, "every size at once", (&warm, &grown), (&cold, &same));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Marks a spread of widgets for redraw at once. Nothing changes, so no box
|
|
||||||
/// may either; what this exercises is the order a frame settles a dirty set
|
|
||||||
/// in, which the other cases reach one dependency path at a time.
|
|
||||||
fn repainted_together(seed: u64) {
|
|
||||||
let mut warm = Harness::new((900, 1200));
|
|
||||||
let grown = plant(&mut warm, seed, &Edits::default());
|
|
||||||
for &id in grown.ids.iter().step_by(5) {
|
|
||||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
|
||||||
}
|
|
||||||
assert!(
|
|
||||||
!warm.rsc.widgets().needs_redraw.is_empty(),
|
|
||||||
"seed {seed}: nothing was marked"
|
|
||||||
);
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
let mut cold = Harness::new((900, 1200));
|
|
||||||
let same = plant(&mut cold, seed, &Edits::default());
|
|
||||||
|
|
||||||
let what = "many repaints at once";
|
|
||||||
assert_same(seed, what, (&warm, &grown), (&cold, &same));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resized(seed: u64) {
|
|
||||||
let mut warm = Harness::new((1920, 1200));
|
|
||||||
let grown = plant(&mut warm, seed, &Edits::default());
|
|
||||||
warm.resize((640, 900));
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
let mut cold = Harness::new((640, 900));
|
|
||||||
let same = plant(&mut cold, seed, &Edits::default());
|
|
||||||
|
|
||||||
assert_same(seed, "a resize", (&warm, &grown), (&cold, &same));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resized_then_changed(seed: u64) {
|
|
||||||
let mut warm = Harness::new((1920, 1200));
|
|
||||||
let grown = plant(&mut warm, seed, &Edits::default());
|
|
||||||
if grown.sized.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
warm.resize((640, 900));
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
let mut rng = Rng::new(seed ^ 0xb0a7);
|
|
||||||
let sizes = edit(&mut warm, &grown, &mut rng);
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
let mut cold = Harness::new((640, 900));
|
|
||||||
let same = plant(
|
|
||||||
&mut cold,
|
|
||||||
seed,
|
|
||||||
&Edits {
|
|
||||||
sizes,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let what = "a resize then a size change";
|
|
||||||
assert_same(seed, what, (&warm, &grown), (&cold, &same));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_changed_size_lands_where_growing_it_that_way_would() {
|
|
||||||
SEEDS.into_iter().for_each(changed_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn every_size_changing_at_once_lands_where_growing_it_that_way_would() {
|
|
||||||
SEEDS.into_iter().for_each(changed_every_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn many_widgets_redrawing_at_once_leaves_every_box_where_it_was() {
|
|
||||||
SEEDS.into_iter().for_each(repainted_together);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_resize_lands_where_starting_at_that_size_would() {
|
|
||||||
SEEDS.into_iter().for_each(resized);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_size_change_after_a_resize_lands_the_same_way() {
|
|
||||||
SEEDS.into_iter().for_each(resized_then_changed);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
|
|
||||||
for shuffle in SHUFFLES {
|
|
||||||
for seed in SEEDS {
|
|
||||||
reshuffled(seed, shuffle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The same property over a hundred seeds and every scenario. What it has
|
|
||||||
/// found so far was never where the trees disagreed: a text measured in a box
|
|
||||||
/// it was not going to get, and a widget re-measured in a box its own answer
|
|
||||||
/// had decided. `tests/shrink.rs` is how a seed from here becomes a tree
|
|
||||||
/// small enough to read.
|
|
||||||
#[test]
|
|
||||||
#[ignore = "a hundred seeds, rather than the seven the others check"]
|
|
||||||
fn a_long_run_of_seeds_agrees() {
|
|
||||||
let seeds = std::env::var("IRIS_GENERATED_SEED")
|
|
||||||
.ok()
|
|
||||||
.and_then(|seed| seed.parse().ok())
|
|
||||||
.map(|seed| seed..=seed)
|
|
||||||
.unwrap_or_else(|| 1..=env("IRIS_GENERATED_SEEDS", 100));
|
|
||||||
for seed in seeds {
|
|
||||||
changed_size(seed);
|
|
||||||
changed_every_size(seed);
|
|
||||||
repainted_together(seed);
|
|
||||||
resized(seed);
|
|
||||||
resized_then_changed(seed);
|
|
||||||
for shuffle in SHUFFLES {
|
|
||||||
reshuffled(seed, shuffle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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:?}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-351
@@ -1,351 +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));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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));
|
|
||||||
// `Aligned` draws its child twice; listing it twice would move it twice.
|
|
||||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
let centered = inner.center().width(200).add(&mut h.rsc);
|
|
||||||
let left = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((left, centered).span(Dir::RIGHT));
|
|
||||||
assert_corners!(h, inner, (100, 0), (300, 200));
|
|
||||||
|
|
||||||
h.rsc[left].x = Some(Len::px(150));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, inner, (150, 0), (350, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_resize_lands_where_a_cold_start_would() {
|
|
||||||
let build = |h: &mut Harness| {
|
|
||||||
let para = wtext(
|
|
||||||
"Wrapping shapes one source into as many lines as its container leaves room \
|
|
||||||
for, so the height of a paragraph is an answer rather than a setting.",
|
|
||||||
)
|
|
||||||
.size(20)
|
|
||||||
.wrap(true)
|
|
||||||
.pad(16)
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let below = rect(Color::RED).add(&mut h.rsc);
|
|
||||||
let root = (para, below).span(Dir::DOWN).pad(12);
|
|
||||||
h.set_root(root);
|
|
||||||
(para, below)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut cold = Harness::new((900, 1200));
|
|
||||||
let (cold_para, cold_below) = build(&mut cold);
|
|
||||||
|
|
||||||
let mut resized = Harness::new((1920, 1200));
|
|
||||||
let (para, below) = build(&mut resized);
|
|
||||||
resized.resize((900, 1200));
|
|
||||||
resized.frame();
|
|
||||||
|
|
||||||
assert_eq!(resized.region(¶), cold.region(&cold_para), "paragraph");
|
|
||||||
assert_eq!(resized.region(&below), cold.region(&cold_below), "below");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_fixed_box_is_drawn_again_rather_than_stretched() {
|
|
||||||
let mut h = Harness::new((400, 400));
|
|
||||||
// The panel fills a stack sized by its sibling, so it is drawn in the
|
|
||||||
// whole box and then placed in the shorter one. Reusing it in that fixed
|
|
||||||
// box afterwards would leave it whatever height it happened to have.
|
|
||||||
let panel = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
let leaf = rect(Color::RED).height(100).add(&mut h.rsc);
|
|
||||||
let stack = (panel, leaf)
|
|
||||||
.stack()
|
|
||||||
.size(StackSize::Child(1))
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(stack.align(Align::TOP));
|
|
||||||
assert_corners!(h, panel, (0, 0), (400, 100));
|
|
||||||
|
|
||||||
h.rsc[leaf].y = Some(Len::px(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).add(&mut h.rsc);
|
|
||||||
h.set_root((first, row).span(Dir::DOWN));
|
|
||||||
assert_corners!(h, inner, (10, 50), (390, 70));
|
|
||||||
|
|
||||||
h.rsc[first].y = Some(Len::px(80));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
// The row is the same shape somewhere else, so one slot moved it and
|
|
||||||
// `inner`'s own region was never rewritten.
|
|
||||||
assert_corners!(h, inner, (10, 90), (390, 110));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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 rest = rect(Color::GREEN).add(&mut h.rsc);
|
|
||||||
let panel = (fixed, rest).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, rest, (150, 0), (400, 200));
|
|
||||||
|
|
||||||
h.rsc[bar].x = Some(Len::px(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 the rest absorbs the change.
|
|
||||||
assert_corners!(h, fixed, (200, 0), (250, 200));
|
|
||||||
assert_corners!(h, rest, (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);
|
|
||||||
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.rsc[bar].x = Some(Len::px(200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, inner, (210, 10), (390, 30));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn only_a_container_that_places_its_children_lengthens_the_chain() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let leaf = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
// Four widgets between the span and the leaf, none of which places what
|
|
||||||
// it draws, so all of them share the span's slot.
|
|
||||||
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 slot = h.render.active[&leaf.id()].parent_move;
|
|
||||||
assert_eq!(
|
|
||||||
h.render.moves.depth(slot),
|
|
||||||
2,
|
|
||||||
"the span above the leaf, and the root the window is held in"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A span that sizes from its children passes their `rest` weight up rather
|
|
||||||
/// 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.
|
|
||||||
#[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));
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Where the shader puts an edge: the two parts of a scalar are floored
|
|
||||||
/// apart, so a fraction and a pixel offset snap independently.
|
|
||||||
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
|
|
||||||
let active = &h.render.active[&id];
|
|
||||||
let region = h.render.moves.resolve(active.parent_move, active.region);
|
|
||||||
let dim = h.size().axis(axis);
|
|
||||||
let edge = |s: UiScalar| (s.rel * dim).floor() + s.px.floor();
|
|
||||||
let span = region.axis(axis);
|
|
||||||
(edge(span.start), edge(span.end))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hairline(h: &mut Harness, marks: &mut Vec<WidgetId>) -> StrongWidget {
|
|
||||||
let inner = rect(Color::RED).add_strong(&mut h.rsc);
|
|
||||||
let mark = SetSize {
|
|
||||||
inner,
|
|
||||||
x: Some(Len::px(1.0)),
|
|
||||||
y: None,
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc);
|
|
||||||
marks.push(mark.id());
|
|
||||||
mark
|
|
||||||
}
|
|
||||||
|
|
||||||
fn share(h: &mut Harness, inner: StrongWidget, ratio: f32) -> StrongWidget {
|
|
||||||
SetSize {
|
|
||||||
inner,
|
|
||||||
x: Some(Len::rest(ratio)),
|
|
||||||
y: None,
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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: 3.0,
|
|
||||||
right: 7.0,
|
|
||||||
top: 0.0,
|
|
||||||
bottom: 0.0,
|
|
||||||
},
|
|
||||||
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:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
//! Retained CPU-layout diagnostics on one reproducible random tree.
|
|
||||||
//!
|
|
||||||
//! Counters and phase timers:
|
|
||||||
//!
|
|
||||||
//! cargo test --release --features layout-diagnostics \
|
|
||||||
//! --test layout_diagnostics -- --ignored --nocapture
|
|
||||||
//!
|
|
||||||
//! Uninstrumented hardware totals for one phase:
|
|
||||||
//!
|
|
||||||
//! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \
|
|
||||||
//! -e cycles:u,instructions:u cargo test --release \
|
|
||||||
//! --test layout_diagnostics -- --ignored --nocapture
|
|
||||||
//!
|
|
||||||
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
|
|
||||||
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
|
|
||||||
//! `IRIS_DIRTY` how many widgets `many` marks at once.
|
|
||||||
|
|
||||||
use iris::harness::Harness;
|
|
||||||
use iris::prelude::*;
|
|
||||||
use iris::random::{Edits, Tree, grow};
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
|
|
||||||
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
#[test]
|
|
||||||
fn a_selected_widget_retains_its_layout_events() {
|
|
||||||
use iris::core::layout_diagnostics::{self as diagnostics, TraceEvent};
|
|
||||||
|
|
||||||
diagnostics::clear_traced_widgets();
|
|
||||||
let _ = diagnostics::take();
|
|
||||||
let mut harness = Harness::new((400, 200));
|
|
||||||
let leaf = rect(Color::RED).add(&mut harness.rsc);
|
|
||||||
let other = rect(Color::BLUE).add(&mut harness.rsc);
|
|
||||||
let root = (leaf, other).span(Dir::RIGHT).add(&mut harness.rsc);
|
|
||||||
harness.set_root(root);
|
|
||||||
diagnostics::trace_widget(leaf.id());
|
|
||||||
let _ = diagnostics::take();
|
|
||||||
|
|
||||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(root.id());
|
|
||||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf.id());
|
|
||||||
harness.frame();
|
|
||||||
|
|
||||||
let report = diagnostics::take();
|
|
||||||
assert!(
|
|
||||||
report
|
|
||||||
.traces()
|
|
||||||
.iter()
|
|
||||||
.any(|event| matches!(event, TraceEvent::Placed { id, .. } if *id == leaf.id()))
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
report
|
|
||||||
.traces()
|
|
||||||
.iter()
|
|
||||||
.any(|event| matches!(event, TraceEvent::DrawRequest { id, .. } if *id == leaf.id()))
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
report
|
|
||||||
.traces()
|
|
||||||
.iter()
|
|
||||||
.any(|event| matches!(event, TraceEvent::SizeRead { id, .. } if *id == leaf.id()))
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
report
|
|
||||||
.traces()
|
|
||||||
.iter()
|
|
||||||
.any(|event| matches!(event, TraceEvent::SizeReported { id, .. } if *id == leaf.id()))
|
|
||||||
);
|
|
||||||
diagnostics::clear_traced_widgets();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|
||||||
std::env::var(name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse().ok())
|
|
||||||
.unwrap_or(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
fn trace_selected(tree: &Tree) {
|
|
||||||
let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let index = value
|
|
||||||
.parse::<usize>()
|
|
||||||
.expect("IRIS_TRACE_INDEX must be a tree.ids index");
|
|
||||||
let id = tree.ids[index];
|
|
||||||
iris::core::layout_diagnostics::trace_widget(id);
|
|
||||||
println!("tracing tree.ids[{index}] = {id:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "layout-diagnostics"))]
|
|
||||||
fn trace_selected(_: &Tree) {}
|
|
||||||
|
|
||||||
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
|
||||||
let mut harness = Harness::new(OUTPUT);
|
|
||||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
|
||||||
harness.state.root = Some(root);
|
|
||||||
harness.frame();
|
|
||||||
println!(
|
|
||||||
"fixture: seed {seed}, depth {depth}, {} widgets, {} active",
|
|
||||||
tree.ids.len(),
|
|
||||||
harness.render.active_widgets()
|
|
||||||
);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
let _ = iris::core::layout_diagnostics::take();
|
|
||||||
(harness, tree)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
|
|
||||||
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
||||||
let frames = elapsed.len();
|
|
||||||
// The worst frame is the stutter somebody sees, so it goes beside the
|
|
||||||
// median; p99 says whether it is the load or a single interruption.
|
|
||||||
println!(
|
|
||||||
"{label}: {frames} frame(s), min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
|
|
||||||
max {:.3} ms, total {:.1} ms",
|
|
||||||
elapsed[0],
|
|
||||||
elapsed[frames / 2],
|
|
||||||
elapsed[frames * 99 / 100],
|
|
||||||
elapsed[frames - 1],
|
|
||||||
elapsed.iter().sum::<f64>(),
|
|
||||||
);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
let diagnostics = iris::core::layout_diagnostics::take();
|
|
||||||
print!("{}", diagnostics.per_frame(frames));
|
|
||||||
for callsite in diagnostics.hot_text().iter().take(3) {
|
|
||||||
let mut ancestry = Vec::new();
|
|
||||||
let mut id = Some(callsite.id);
|
|
||||||
while let Some(widget) = id {
|
|
||||||
ancestry.push(_harness.rsc.widgets().label(widget).as_str());
|
|
||||||
id = _harness
|
|
||||||
.render
|
|
||||||
.active
|
|
||||||
.get(&widget)
|
|
||||||
.and_then(|active| active.parent);
|
|
||||||
}
|
|
||||||
println!(" text ancestry: {}", ancestry.join(" < "));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(
|
|
||||||
label: &str,
|
|
||||||
frames: usize,
|
|
||||||
harness: &mut Harness,
|
|
||||||
mut change: impl FnMut(&mut Harness, usize),
|
|
||||||
) {
|
|
||||||
let mut elapsed = Vec::with_capacity(frames);
|
|
||||||
for frame in 0..frames {
|
|
||||||
change(harness, frame);
|
|
||||||
let start = Instant::now();
|
|
||||||
harness.frame();
|
|
||||||
elapsed.push(start.elapsed().as_secs_f64() * 1_000.0);
|
|
||||||
}
|
|
||||||
report(label, elapsed, harness);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "measurement, not a check"]
|
|
||||||
fn layout_cost() {
|
|
||||||
let seed = env("IRIS_SEED", 1_u64);
|
|
||||||
let depth = env("IRIS_DEPTH", 7_usize);
|
|
||||||
let frames = env("IRIS_FRAMES", 100_usize);
|
|
||||||
assert!(frames > 0, "IRIS_FRAMES must be greater than zero");
|
|
||||||
let phase = env("IRIS_PHASE", String::from("all"));
|
|
||||||
assert!(
|
|
||||||
["all", "cold", "repaint", "many", "size", "scroll", "resize"].contains(&phase.as_str()),
|
|
||||||
"unknown IRIS_PHASE {phase:?}"
|
|
||||||
);
|
|
||||||
let selected = |name| phase == "all" || phase == name;
|
|
||||||
|
|
||||||
if selected("cold") {
|
|
||||||
let mut harness = Harness::new(OUTPUT);
|
|
||||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
|
||||||
harness.state.root = Some(root);
|
|
||||||
println!(
|
|
||||||
"fixture: seed {seed}, depth {depth}, {} widgets",
|
|
||||||
tree.ids.len()
|
|
||||||
);
|
|
||||||
trace_selected(&tree);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
let _ = iris::core::layout_diagnostics::take();
|
|
||||||
run("cold", 1, &mut harness, |_, _| {});
|
|
||||||
drop(tree);
|
|
||||||
}
|
|
||||||
|
|
||||||
if selected("repaint") {
|
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
|
||||||
trace_selected(&tree);
|
|
||||||
let leaf = tree.ids[0];
|
|
||||||
run("repaint", frames, &mut harness, move |harness, _| {
|
|
||||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if selected("many") {
|
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
|
||||||
trace_selected(&tree);
|
|
||||||
// Spread through the tree rather than taken from one subtree, so the
|
|
||||||
// dependency paths the frame settles overlap.
|
|
||||||
let wanted = env("IRIS_DIRTY", 32_usize).max(1);
|
|
||||||
let step = (tree.ids.len() / wanted).max(1);
|
|
||||||
let dirty: Vec<_> = tree.ids.iter().copied().step_by(step).collect();
|
|
||||||
println!("marking {} of {} widgets", dirty.len(), tree.ids.len());
|
|
||||||
run("many", frames, &mut harness, move |harness, _| {
|
|
||||||
for &id in &dirty {
|
|
||||||
harness.rsc.widgets_mut().get_dyn_mut(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if selected("size") {
|
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
|
||||||
trace_selected(&tree);
|
|
||||||
let sized = tree.sized[0];
|
|
||||||
run("size", frames, &mut harness, move |harness, frame| {
|
|
||||||
harness.rsc[sized].x = Some(Len::px(100.0 + (frame % 2) as f32 * 40.0));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if selected("scroll") {
|
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
|
||||||
trace_selected(&tree);
|
|
||||||
let scroll = tree.scrolls[0];
|
|
||||||
run("scroll", frames, &mut harness, move |harness, frame| {
|
|
||||||
harness.rsc[scroll].scroll(if frame % 2 == 0 { 12.0 } else { -12.0 });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if selected("resize") {
|
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
|
||||||
trace_selected(&tree);
|
|
||||||
run("resize", frames, &mut harness, |harness, frame| {
|
|
||||||
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
|
|
||||||
});
|
|
||||||
drop(tree);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"]);
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
//! Input across layers: what stops at a layer, what passes through it, and
|
|
||||||
//! where hovering stops.
|
|
||||||
|
|
||||||
use std::{cell::RefCell, rc::Rc};
|
|
||||||
|
|
||||||
use iris::harness::Harness;
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
const WINDOW: f32 = 100.0;
|
|
||||||
|
|
||||||
/// Every sense that has fired on one widget since it was last read.
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
struct Fired(Rc<RefCell<Vec<CursorSense>>>);
|
|
||||||
|
|
||||||
impl Fired {
|
|
||||||
fn take(&self) -> Vec<CursorSense> {
|
|
||||||
std::mem::take(&mut self.0.borrow_mut())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A widget filling whatever it is given, recording the senses it is sent.
|
|
||||||
fn listener(h: &mut Harness, senses: impl Into<CursorSenses>) -> (WeakWidget<Rect>, Fired) {
|
|
||||||
let fired = Fired::default();
|
|
||||||
let record = fired.clone();
|
|
||||||
let id = rect(Color::WHITE)
|
|
||||||
.on(senses.into(), move |ctx, _| {
|
|
||||||
record.0.borrow_mut().push(ctx.data.sense)
|
|
||||||
})
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
(id, fired)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A widget with no senses of its own, to leave a gap beside one that has.
|
|
||||||
fn blank(h: &mut Harness) -> WeakWidget<Rect> {
|
|
||||||
rect(Color::WHITE).add(&mut h.rsc)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn harness() -> Harness {
|
|
||||||
Harness::new((WINDOW, WINDOW))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hover_stops_at_the_topmost_widget() {
|
|
||||||
let mut h = harness();
|
|
||||||
let (bottom, bottom_hover) = listener(&mut h, CursorSense::HoverStart);
|
|
||||||
let (middle, middle_hover) = listener(&mut h, CursorSense::HoverStart);
|
|
||||||
let (top, top_hover) = listener(&mut h, CursorSense::HoverStart);
|
|
||||||
h.set_root((bottom, middle, top).stack());
|
|
||||||
|
|
||||||
h.move_to((50, 50));
|
|
||||||
|
|
||||||
assert_eq!(top_hover.take(), [CursorSense::HoverStart]);
|
|
||||||
assert_eq!(
|
|
||||||
middle_hover.take(),
|
|
||||||
[],
|
|
||||||
"hover is not shared with a layer below"
|
|
||||||
);
|
|
||||||
assert_eq!(bottom_hover.take(), []);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_scroll_passes_through_every_widget_that_does_not_want_it() {
|
|
||||||
let mut h = harness();
|
|
||||||
let (list, scrolled) = listener(&mut h, CursorSense::Scroll);
|
|
||||||
let (button, clicked) = listener(&mut h, CursorSense::click());
|
|
||||||
let (overlay, overlay_clicked) = listener(&mut h, CursorSense::click());
|
|
||||||
h.set_root((list, button, overlay).stack());
|
|
||||||
|
|
||||||
h.move_to((50, 50));
|
|
||||||
h.scroll((0, 10));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
scrolled.take(),
|
|
||||||
[CursorSense::Scroll],
|
|
||||||
"two layers of click-only widgets do not stop a scroll"
|
|
||||||
);
|
|
||||||
assert_eq!(clicked.take(), []);
|
|
||||||
assert_eq!(overlay_clicked.take(), []);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hovering_a_button_above_does_not_stop_a_later_scroll() {
|
|
||||||
let mut h = harness();
|
|
||||||
let (list, scrolled) = listener(&mut h, CursorSense::Scroll);
|
|
||||||
let (button, _clicked) = listener(&mut h, CursorSense::click());
|
|
||||||
h.set_root((list, button).stack());
|
|
||||||
|
|
||||||
// The hover arrives in its own frame, as a window delivers it.
|
|
||||||
h.move_to((50, 50));
|
|
||||||
assert_eq!(scrolled.take(), []);
|
|
||||||
|
|
||||||
h.scroll((0, 10));
|
|
||||||
assert_eq!(
|
|
||||||
scrolled.take(),
|
|
||||||
[CursorSense::Scroll],
|
|
||||||
"a hover already resting on the button must not consume the wheel"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn only_the_topmost_listener_takes_a_press() {
|
|
||||||
let mut h = harness();
|
|
||||||
let (below, below_clicked) = listener(&mut h, CursorSense::click());
|
|
||||||
let (above, above_clicked) = listener(&mut h, CursorSense::click());
|
|
||||||
h.set_root((below, above).stack());
|
|
||||||
|
|
||||||
h.click((50, 50));
|
|
||||||
|
|
||||||
assert_eq!(above_clicked.take(), [CursorSense::click()]);
|
|
||||||
assert_eq!(below_clicked.take(), [], "one press goes to one widget");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_press_beside_the_button_reaches_the_layer_below() {
|
|
||||||
let mut h = harness();
|
|
||||||
let (list, list_clicked) = listener(&mut h, CursorSense::click());
|
|
||||||
// The row above the list covers it, but only its left half is the button.
|
|
||||||
let (button, button_clicked) = listener(&mut h, CursorSense::click());
|
|
||||||
let row = (button, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
h.set_root((list, row).stack());
|
|
||||||
|
|
||||||
h.click((20, 50));
|
|
||||||
assert_eq!(button_clicked.take(), [CursorSense::click()]);
|
|
||||||
assert_eq!(list_clicked.take(), []);
|
|
||||||
|
|
||||||
h.click((80, 50));
|
|
||||||
assert_eq!(button_clicked.take(), [], "the cursor is not on the button");
|
|
||||||
assert_eq!(
|
|
||||||
list_clicked.take(),
|
|
||||||
[CursorSense::click()],
|
|
||||||
"a press beside the button belongs to what is under it"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn leaving_a_widget_still_ends_its_hover() {
|
|
||||||
let mut h = harness();
|
|
||||||
let (widget, hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
|
|
||||||
h.set_root(widget);
|
|
||||||
|
|
||||||
h.move_to((50, 50));
|
|
||||||
assert_eq!(hover.take(), [CursorSense::HoverStart]);
|
|
||||||
|
|
||||||
h.leave();
|
|
||||||
assert_eq!(hover.take(), [CursorSense::HoverEnd]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn leaving_a_widget_does_not_block_the_layer_below() {
|
|
||||||
let mut h = harness();
|
|
||||||
let (below, below_hover) = listener(&mut h, CursorSense::HoverStart);
|
|
||||||
// Only the left half of the layer above is a widget, so the cursor can
|
|
||||||
// leave it without leaving the one underneath.
|
|
||||||
let (above, above_hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
|
|
||||||
let row = (above, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
h.set_root((below, row).stack());
|
|
||||||
|
|
||||||
h.move_to((20, 50));
|
|
||||||
assert_eq!(above_hover.take(), [CursorSense::HoverStart]);
|
|
||||||
assert_eq!(below_hover.take(), [], "the layer above is over it");
|
|
||||||
|
|
||||||
h.move_to((80, 50));
|
|
||||||
assert_eq!(above_hover.take(), [CursorSense::HoverEnd]);
|
|
||||||
assert_eq!(
|
|
||||||
below_hover.take(),
|
|
||||||
[CursorSense::HoverStart],
|
|
||||||
"ending a hover above must not stop the hover below"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn covering_a_widget_ends_its_hover() {
|
|
||||||
let mut h = harness();
|
|
||||||
let (below, below_hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
|
|
||||||
let (above, above_hover) = listener(&mut h, CursorSense::HoverStart);
|
|
||||||
let row = (above, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
h.set_root((below, row).stack());
|
|
||||||
|
|
||||||
h.move_to((80, 50));
|
|
||||||
assert_eq!(below_hover.take(), [CursorSense::HoverStart]);
|
|
||||||
|
|
||||||
h.move_to((20, 50));
|
|
||||||
assert_eq!(above_hover.take(), [CursorSense::HoverStart]);
|
|
||||||
assert_eq!(
|
|
||||||
below_hover.take(),
|
|
||||||
[CursorSense::HoverEnd],
|
|
||||||
"a widget covered by one that took the input is no longer hovered"
|
|
||||||
);
|
|
||||||
|
|
||||||
h.move_to((80, 50));
|
|
||||||
assert_eq!(
|
|
||||||
below_hover.take(),
|
|
||||||
[CursorSense::HoverStart],
|
|
||||||
"uncovering it hovers it again"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hover_starts_and_ends_once_each() {
|
|
||||||
let mut h = harness();
|
|
||||||
// Only the left half is the widget, so the cursor can leave it without
|
|
||||||
// leaving the window.
|
|
||||||
let (widget, hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
|
|
||||||
let row = (widget, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
h.set_root(row);
|
|
||||||
|
|
||||||
h.move_to((20, 50));
|
|
||||||
assert_eq!(hover.take(), [CursorSense::HoverStart]);
|
|
||||||
|
|
||||||
h.move_to((30, 50));
|
|
||||||
assert_eq!(hover.take(), [], "staying inside is not a second start");
|
|
||||||
|
|
||||||
h.move_to((80, 50));
|
|
||||||
assert_eq!(hover.take(), [CursorSense::HoverEnd]);
|
|
||||||
|
|
||||||
h.move_to((90, 50));
|
|
||||||
assert_eq!(hover.take(), [], "an ended hover does not end again");
|
|
||||||
|
|
||||||
h.move_to((20, 50));
|
|
||||||
assert_eq!(
|
|
||||||
hover.take(),
|
|
||||||
[CursorSense::HoverStart],
|
|
||||||
"re-entering starts it"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
//! What re-placing a subtree costs per frame, as a load for a counter rather
|
|
||||||
//! than a check. A span of 200 fixed-height rows, five primitives each, with
|
|
||||||
//! the row above them changing height every frame, so every row below is
|
|
||||||
//! offered a box the same shape somewhere else.
|
|
||||||
//!
|
|
||||||
//! cargo test --release --test replace_cost -- --ignored
|
|
||||||
//! perf stat -e instructions:u target/release/.../replace_cost-* --ignored
|
|
||||||
//!
|
|
||||||
//! Wall time is the wrong number here; see `draw_cost.rs`. Measured on
|
|
||||||
//! 2026-09-14 at 1.98M instructions per frame, against 2.38M for rewriting
|
|
||||||
//! each row's regions instead and 7.13M for redrawing them.
|
|
||||||
|
|
||||||
use iris::harness::Harness;
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
const ROWS: usize = 200;
|
|
||||||
const FRAMES: usize = 200;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "measurement, not a check"]
|
|
||||||
fn replacing_rows_every_frame() {
|
|
||||||
let mut h = Harness::new((1920, 1200));
|
|
||||||
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
|
||||||
let mut span = Span::empty(Dir::DOWN);
|
|
||||||
span.push(first.add_strong(&mut h.rsc));
|
|
||||||
for i in 0..ROWS {
|
|
||||||
let row = (
|
|
||||||
rect(Color::BLUE.darker(i as f32 / (ROWS * 2) as f32)),
|
|
||||||
rect(Color::GREEN).pad(2),
|
|
||||||
wtext("row").size(16).pad(2),
|
|
||||||
)
|
|
||||||
.span(Dir::RIGHT)
|
|
||||||
.pad(4)
|
|
||||||
.height(40)
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
span.push(row.add_strong(&mut h.rsc));
|
|
||||||
}
|
|
||||||
h.set_root(span);
|
|
||||||
for i in 0..FRAMES {
|
|
||||||
h.rsc[first].y = Some(Len::px(40.0 + (i % 2) as f32));
|
|
||||||
h.frame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,471 +0,0 @@
|
|||||||
//! What a second frame draws again, and what it keeps.
|
|
||||||
|
|
||||||
use std::{cell::Cell, rc::Rc};
|
|
||||||
|
|
||||||
use iris::harness::{Harness, assert_corners};
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
/// A leaf that counts its draws and reports whatever size it is given, so a
|
|
||||||
/// test can see what the retained path skipped.
|
|
||||||
struct Counted {
|
|
||||||
draws: Rc<Cell<usize>>,
|
|
||||||
size: Size,
|
|
||||||
dependence: OnResize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Counted {
|
|
||||||
fn draw(&mut self, _: &mut Painter) -> Size {
|
|
||||||
self.draws.set(self.draws.get() + 1);
|
|
||||||
self.size
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
self.dependence
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Counts(Rc<Cell<usize>>);
|
|
||||||
|
|
||||||
impl Counts {
|
|
||||||
fn get(&self) -> usize {
|
|
||||||
self.0.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn counted(h: &mut Harness, size: Size, dependence: OnResize) -> (WeakWidget<Counted>, Counts) {
|
|
||||||
let draws = Rc::new(Cell::new(0));
|
|
||||||
let id = Counted {
|
|
||||||
draws: draws.clone(),
|
|
||||||
size,
|
|
||||||
dependence,
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
(id, Counts(draws))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A fixed-width leaf beside one that takes the rest, so changing the first
|
|
||||||
/// hands the second a different box without the output changing.
|
|
||||||
fn pair(h: &mut Harness, rest: OnResize) -> (WeakWidget<Counted>, Counts, WidgetId) {
|
|
||||||
let (first, _) = counted(h, Size::from((100, 200)), OnResize::Translate);
|
|
||||||
let (second, draws) = counted(h, Size::REST, rest);
|
|
||||||
h.set_root((first, second).span(Dir::RIGHT));
|
|
||||||
(first, draws, second.id())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (first, draws, second) = pair(&mut h, OnResize::Scale);
|
|
||||||
let settled = draws.get();
|
|
||||||
assert_corners!(h, second, (100, 0), (400, 200));
|
|
||||||
|
|
||||||
h.rsc[first].size = Size::from((150, 200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
draws.get(),
|
|
||||||
settled,
|
|
||||||
"its box is a field to write, not a reason to draw"
|
|
||||||
);
|
|
||||||
assert_corners!(h, second, (150, 0), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (first, draws, second) = pair(&mut h, OnResize::Redraw);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
h.rsc[first].size = Size::from((150, 200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
// The preceding fixed child makes the remaining box this child's real
|
|
||||||
// box, so measuring it also draws it in its final place.
|
|
||||||
assert_eq!(draws.get(), settled + 1);
|
|
||||||
assert_corners!(h, second, (150, 0), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_span_child_that_declares_its_length_is_drawn_once() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
|
|
||||||
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
|
|
||||||
// The span takes one child's length from its hint and has to draw the
|
|
||||||
// other to find out, so only the second is drawn before it is placed.
|
|
||||||
let hinted = told.width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((hinted, asked).span(Dir::RIGHT));
|
|
||||||
|
|
||||||
assert_eq!(told_draws.get(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
asked_draws.get(),
|
|
||||||
2,
|
|
||||||
"drawn to be measured, then again to be placed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_span_relays_out_when_a_child_it_measured_changes() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (first, _, second) = pair(&mut h, OnResize::Translate);
|
|
||||||
|
|
||||||
h.rsc[first].size = Size::from((250, 200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, first, (0, 0), (250, 200));
|
|
||||||
assert_corners!(h, second, (250, 0), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_repaint_that_keeps_its_size_does_not_relay_out() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (first, draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
|
|
||||||
let (second, _) = counted(&mut h, Size::REST, OnResize::Translate);
|
|
||||||
h.set_root((first, second).span(Dir::RIGHT));
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
// Taking mutable access is the ordinary content-change signal. This
|
|
||||||
// widget returns the same size, so the parent has nothing to lay out.
|
|
||||||
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_eq!(draws.get(), settled + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_placed_child_survives_the_next_frame() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
// Both children declare a length, so the span places them from their hints
|
|
||||||
// rather than drawing them to find out.
|
|
||||||
let top = rect(Color::RED).height(80).add(&mut h.rsc);
|
|
||||||
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
|
|
||||||
h.set_root((top, bottom).span(Dir::DOWN));
|
|
||||||
|
|
||||||
h.rsc.widgets_mut().get_dyn_mut(top.id());
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, top, (0, 0), (400, 80));
|
|
||||||
assert_corners!(h, bottom, (0, 80), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lays its child out from the hint alone, never reading what it drew.
|
|
||||||
struct FromHint {
|
|
||||||
inner: StrongWidget,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for FromHint {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
||||||
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
|
|
||||||
let mut region = UiRegion::FULL;
|
|
||||||
region.y.end = region.y.start.offset(len.px);
|
|
||||||
painter.widget_within(&self.inner, region);
|
|
||||||
Size::REST
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let inner = rect(Color::RED).height(80).add(&mut h.rsc);
|
|
||||||
let parent = FromHint {
|
|
||||||
inner: inner.add_strong(&mut h.rsc),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(parent);
|
|
||||||
assert_corners!(h, inner, (0, 0), (400, 80));
|
|
||||||
|
|
||||||
h.rsc[inner].y = Some(Len::px(120));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, inner, (0, 0), (400, 120));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads the output's size, which nothing but its own draw can put right.
|
|
||||||
struct ReadsOutput {
|
|
||||||
draws: Rc<Cell<usize>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for ReadsOutput {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
||||||
self.draws.set(self.draws.get() + 1);
|
|
||||||
Size::px(painter.output_size() / 4.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads the output across one axis only, and says so: its drawing follows
|
|
||||||
/// a taller box on its own, so only a wider one is worth a draw.
|
|
||||||
struct ReadsWidth {
|
|
||||||
draws: Rc<Cell<usize>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for ReadsWidth {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
||||||
self.draws.set(self.draws.get() + 1);
|
|
||||||
Size::px((painter.output_len(Axis::X) / 4.0, 20.0).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_resize(&self, axis: Axis) -> OnResize {
|
|
||||||
match axis {
|
|
||||||
Axis::X => OnResize::Redraw,
|
|
||||||
Axis::Y => OnResize::Scale,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_resize_does_not_redraw_what_the_shader_can_move() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Scale);
|
|
||||||
h.set_root(leaf);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
h.resize((800, 100));
|
|
||||||
assert!(h.needs_redraw());
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
draws.get(),
|
|
||||||
settled,
|
|
||||||
"a scaling drawing follows its box, and the output is one"
|
|
||||||
);
|
|
||||||
assert_corners!(h, leaf, (0, 0), (800, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The output is the root of the box chain, so a resize is a box that changed
|
|
||||||
/// length and `OnResize` answers for it -- there is not a second rule for the
|
|
||||||
/// window. A drawing that does not scale is redrawn whichever box moved.
|
|
||||||
#[test]
|
|
||||||
fn a_resize_redraws_what_does_not_scale() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw);
|
|
||||||
h.set_root(leaf);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
h.resize((800, 100));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_eq!(draws.get(), settled + 1, "its box is a different length");
|
|
||||||
assert_corners!(h, leaf, (0, 0), (800, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_resize_redraws_what_read_the_output() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let draws = Rc::new(Cell::new(0));
|
|
||||||
let leaf = ReadsOutput {
|
|
||||||
draws: draws.clone(),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(leaf);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
h.resize((800, 100));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_eq!(draws.get(), settled + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_resize_only_redraws_read_output_axes() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let draws = Rc::new(Cell::new(0));
|
|
||||||
let leaf = ReadsWidth {
|
|
||||||
draws: draws.clone(),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(leaf);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
h.resize((400, 300));
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled, "height was never read");
|
|
||||||
|
|
||||||
h.resize((800, 300));
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled + 1, "width changes its answer");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn subpixel_resize_changes_accumulate_from_the_last_layout() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let draws = Rc::new(Cell::new(0));
|
|
||||||
let leaf = ReadsWidth {
|
|
||||||
draws: draws.clone(),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(leaf);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
for width in [400.02, 400.04, 400.05] {
|
|
||||||
h.resize((width, 200.0));
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled);
|
|
||||||
}
|
|
||||||
|
|
||||||
h.resize((400.06, 200.0));
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn subpixel_box_changes_accumulate_from_the_last_draw() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (first, draws, _) = pair(&mut h, OnResize::Redraw);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
for width in [100.02, 100.04, 100.05] {
|
|
||||||
h.rsc[first].size.x = Len::px(width);
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled);
|
|
||||||
}
|
|
||||||
|
|
||||||
h.rsc[first].size.x = Len::px(100.06);
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reporting_the_same_output_size_does_not_start_a_resize() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let draws = Rc::new(Cell::new(0));
|
|
||||||
let leaf = ReadsOutput {
|
|
||||||
draws: draws.clone(),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(leaf);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
h.resize((400, 200));
|
|
||||||
|
|
||||||
assert!(!h.needs_redraw());
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn narrowing_the_output_reflows_text_and_relays_out_around_it() {
|
|
||||||
let mut h = Harness::new((600, 400));
|
|
||||||
let para = wtext(
|
|
||||||
"Wrapping shapes one source into as many lines as its container leaves \
|
|
||||||
room for, so the height of a paragraph is an answer rather than a setting.",
|
|
||||||
)
|
|
||||||
.size(20)
|
|
||||||
.wrap(true)
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let below = rect(Color::RED).add(&mut h.rsc);
|
|
||||||
h.set_root((para, below).span(Dir::DOWN));
|
|
||||||
let top = h.region(&below).expect("drew nothing").top_left.y;
|
|
||||||
|
|
||||||
h.resize((300, 400));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
let lower = h.region(&below).expect("drew nothing").top_left.y;
|
|
||||||
assert!(lower > top, "same words, half the width: {top} -> {lower}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_change_two_levels_under_its_reader_still_reaches_it() {
|
|
||||||
let mut h = Harness::new((400, 400));
|
|
||||||
// Every wrapper up to the outer pad read the size below it, so the outer
|
|
||||||
// pad is what draws again -- and the span it hands the box to is the same
|
|
||||||
// size as before, which is what lets a draw reuse its way past the leaf.
|
|
||||||
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), OnResize::Redraw);
|
|
||||||
let padded = leaf.pad(10).add(&mut h.rsc);
|
|
||||||
let below = rect(Color::RED).add(&mut h.rsc);
|
|
||||||
h.set_root((padded, below).span(Dir::DOWN).pad(12));
|
|
||||||
assert_corners!(h, below, (12, 132), (388, 388));
|
|
||||||
|
|
||||||
h.rsc[leaf].size = Size::px((100, 200).into());
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, below, (12, 232), (388, 388));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Claims its drawing survives its box changing length, and has a child so
|
|
||||||
/// that the walk looking for what does not has one to reach.
|
|
||||||
struct Stretchy {
|
|
||||||
inner: StrongWidget,
|
|
||||||
draws: Rc<Cell<usize>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Stretchy {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
||||||
self.draws.set(self.draws.get() + 1);
|
|
||||||
painter.widget(&self.inner).size()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn stretching_a_subtree_carries_the_children_in_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 draws = Rc::new(Cell::new(0));
|
|
||||||
let outer = Stretchy {
|
|
||||||
inner: inner.add_strong(&mut h.rsc),
|
|
||||||
draws: draws.clone(),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root((first, outer).span(Dir::DOWN));
|
|
||||||
let settled = draws.get();
|
|
||||||
assert_corners!(h, inner, (0, 40), (400, 400));
|
|
||||||
|
|
||||||
h.rsc[first].y = Some(Len::px(80));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
draws.get(),
|
|
||||||
settled,
|
|
||||||
"its drawing follows its box, rather than being made again"
|
|
||||||
);
|
|
||||||
assert_corners!(h, outer, (0, 80), (400, 400));
|
|
||||||
assert_corners!(h, inner, (0, 80), (400, 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_widened_row_redraws_what_reads_its_length_and_nothing_else() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
// What a transcript row is: something whose shaping depends on the width
|
|
||||||
// it is given, beside something that only has to be the right shape.
|
|
||||||
let (wraps, wrap_draws) = counted(&mut h, Size::REST, OnResize::Redraw);
|
|
||||||
let (backing, back_draws) = counted(&mut h, Size::REST, OnResize::Scale);
|
|
||||||
let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((bar, row).span(Dir::RIGHT));
|
|
||||||
let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get());
|
|
||||||
|
|
||||||
h.rsc[bar].x = Some(Len::px(200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
// The span reads every child's size, so redrawing one takes the span
|
|
||||||
// with it -- and the span then measures and places the redrawn child.
|
|
||||||
assert!(wrap_draws.get() > settled_wrap, "reads the width it got");
|
|
||||||
assert_eq!(back_draws.get(), settled_back, "only has to be the shape");
|
|
||||||
assert_corners!(h, backing, (200, 0), (300, 200));
|
|
||||||
assert_corners!(h, wraps, (300, 0), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_declared_length_child_is_not_redrawn_when_the_box_around_it_grows() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
// Its box is a fixed 80 wherever the row's edges end up, so drawing it
|
|
||||||
// again would be for a width it does not have. The declared width is what
|
|
||||||
// lets the span say that without drawing it: a width the span learnt by
|
|
||||||
// drawing the child in its own box is only an answer for that box.
|
|
||||||
let (counter, draws) = counted(&mut h, Size::from((80, 200)), OnResize::Redraw);
|
|
||||||
let fixed = counter.width(80).add(&mut h.rsc);
|
|
||||||
let (rest, _) = counted(&mut h, Size::REST, OnResize::Scale);
|
|
||||||
let row = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((bar, row).span(Dir::RIGHT));
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
h.rsc[bar].x = Some(Len::px(200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_eq!(draws.get(), settled, "its own length did not change");
|
|
||||||
assert_corners!(h, fixed, (200, 0), (280, 200));
|
|
||||||
}
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
//! What a resize frame costs and what it holds, on a tree the revision before
|
|
||||||
//! #16 also builds.
|
|
||||||
//!
|
|
||||||
//! Deliberately written in the API subset `43ce8c7` and this branch share, so
|
|
||||||
//! the same source can be dropped into an old worktree and measured there:
|
|
||||||
//! that is the only like-for-like comparison with the code the retained
|
|
||||||
//! layout replaced. The random tree cannot carry one, because the generator
|
|
||||||
//! itself changed with the work.
|
|
||||||
//!
|
|
||||||
//! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \
|
|
||||||
//! -- --ignored --nocapture resize_cost
|
|
||||||
//! ROWS=2000 cargo test --release --test revision_cost \
|
|
||||||
//! -- --ignored --nocapture text_memory
|
|
||||||
//!
|
|
||||||
//! Wall time on this machine varies with CPU frequency; take the number from
|
|
||||||
//! `perf stat -e instructions:u` on the test binary directly.
|
|
||||||
|
|
||||||
use iris::harness::Harness;
|
|
||||||
use iris::prelude::*;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
/// xorshift64, so one seed is one set of paragraphs on any machine.
|
|
||||||
struct Rng(u64);
|
|
||||||
|
|
||||||
impl Rng {
|
|
||||||
fn bits(&mut self) -> u64 {
|
|
||||||
self.0 ^= self.0 << 13;
|
|
||||||
self.0 ^= self.0 >> 7;
|
|
||||||
self.0 ^= self.0 << 17;
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
fn below(&mut self, n: usize) -> usize {
|
|
||||||
(self.bits() % n as u64) as usize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const WORDS: [&str; 24] = [
|
|
||||||
"wrapping",
|
|
||||||
"shapes",
|
|
||||||
"one",
|
|
||||||
"source",
|
|
||||||
"into",
|
|
||||||
"as",
|
|
||||||
"many",
|
|
||||||
"lines",
|
|
||||||
"as",
|
|
||||||
"the",
|
|
||||||
"box",
|
|
||||||
"leaves",
|
|
||||||
"room",
|
|
||||||
"for",
|
|
||||||
"paragraph",
|
|
||||||
"height",
|
|
||||||
"answer",
|
|
||||||
"setting",
|
|
||||||
"container",
|
|
||||||
"width",
|
|
||||||
"before",
|
|
||||||
"knows",
|
|
||||||
"measured",
|
|
||||||
"again",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// A run of its own words, so nothing here is fast for two texts being the
|
|
||||||
/// same string.
|
|
||||||
fn words(rng: &mut Rng, least: usize, most: usize) -> String {
|
|
||||||
let words = least + rng.below(most - least);
|
|
||||||
let mut out = String::new();
|
|
||||||
for _ in 0..words {
|
|
||||||
if !out.is_empty() {
|
|
||||||
out.push(' ');
|
|
||||||
}
|
|
||||||
out.push_str(WORDS[rng.below(WORDS.len())]);
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
const OUTPUT: (f32, f32) = (900.0, 1200.0);
|
|
||||||
|
|
||||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|
||||||
std::env::var(name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse().ok())
|
|
||||||
.unwrap_or(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A row of a fixed-width rect beside a column of one wrapping and one
|
|
||||||
/// overflowing text: the shape that makes a container measure a child in a
|
|
||||||
/// box it will not keep.
|
|
||||||
fn build(h: &mut Harness, rows: usize) -> Vec<WidgetId> {
|
|
||||||
let mut rng = Rng(1);
|
|
||||||
let mut paragraphs = Vec::new();
|
|
||||||
let mut col = Span::empty(Dir::DOWN);
|
|
||||||
for _ in 0..rows {
|
|
||||||
let mut row = Span::empty(Dir::RIGHT);
|
|
||||||
row.push(rect(Color::RED).width(Len::px(40.0)).add_strong(&mut h.rsc));
|
|
||||||
let mut body = Span::empty(Dir::DOWN);
|
|
||||||
let para = wtext(words(&mut rng, 12, 52))
|
|
||||||
.size(16)
|
|
||||||
.wrap(true)
|
|
||||||
.add_strong(&mut h.rsc);
|
|
||||||
paragraphs.push(para.id());
|
|
||||||
body.push(para);
|
|
||||||
body.push(
|
|
||||||
// Short, or its unwrapped width decides the row and the
|
|
||||||
// paragraph beside it never wraps.
|
|
||||||
wtext(words(&mut rng, 2, 6))
|
|
||||||
.size(16)
|
|
||||||
.wrap(false)
|
|
||||||
.add_strong(&mut h.rsc),
|
|
||||||
);
|
|
||||||
row.push(body.add_strong(&mut h.rsc));
|
|
||||||
col.push(row.add_strong(&mut h.rsc));
|
|
||||||
}
|
|
||||||
let root = col.add(&mut h.rsc);
|
|
||||||
h.set_root(root);
|
|
||||||
paragraphs
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "measurement, not a check"]
|
|
||||||
fn resize_cost() {
|
|
||||||
let rows = env("ROWS", 40_usize);
|
|
||||||
let frames = env("FRAMES", 500_usize);
|
|
||||||
let mut h = Harness::new(OUTPUT);
|
|
||||||
let paragraphs = build(&mut h, rows);
|
|
||||||
// What it cost is only half the comparison: the old code is cheaper
|
|
||||||
// partly because it wraps at the container's whole width rather than the
|
|
||||||
// part left beside the rect, and draws past the edge of the output.
|
|
||||||
println!("output width {}", OUTPUT.0);
|
|
||||||
for (at, id) in paragraphs.iter().enumerate().take(3) {
|
|
||||||
println!("paragraph {at}: {:?}", h.region(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Two widths in turn is the friendly case for anything that remembers an
|
|
||||||
// answer, so `SWEEP=1` never repeats one -- a drag rather than a toggle.
|
|
||||||
let sweep = env("SWEEP", 0_usize) != 0;
|
|
||||||
let mut elapsed = Vec::with_capacity(frames);
|
|
||||||
for frame in 0..frames {
|
|
||||||
let narrower = match sweep {
|
|
||||||
true => (frame % 256) as f32,
|
|
||||||
false => ((frame + 1) % 2) as f32 * 8.0,
|
|
||||||
};
|
|
||||||
h.resize((OUTPUT.0 - narrower, OUTPUT.1));
|
|
||||||
let start = Instant::now();
|
|
||||||
h.frame();
|
|
||||||
elapsed.push(start.elapsed().as_secs_f64() * 1000.0);
|
|
||||||
}
|
|
||||||
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
||||||
println!(
|
|
||||||
"resize: {frames} frames, min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
|
|
||||||
max {:.3} ms, total {:.1} ms",
|
|
||||||
elapsed[0],
|
|
||||||
elapsed[frames / 2],
|
|
||||||
elapsed[frames * 99 / 100],
|
|
||||||
elapsed[frames - 1],
|
|
||||||
elapsed.iter().sum::<f64>()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn kb(field: &str) -> u64 {
|
|
||||||
std::fs::read_to_string("/proc/self/status")
|
|
||||||
.unwrap()
|
|
||||||
.lines()
|
|
||||||
.find(|line| line.starts_with(field))
|
|
||||||
.and_then(|line| line.split_whitespace().nth(1)?.parse().ok())
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn report(label: &str) {
|
|
||||||
println!(
|
|
||||||
"{label:24} rss {:>7} kB peak {:>7} kB",
|
|
||||||
kb("VmRSS:"),
|
|
||||||
kb("VmHWM:")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run this one on its own: the figures are the whole process's.
|
|
||||||
#[test]
|
|
||||||
#[ignore = "measurement, not a check"]
|
|
||||||
fn text_memory() {
|
|
||||||
let rows = env("ROWS", 2000_usize);
|
|
||||||
report("before");
|
|
||||||
let mut h = Harness::new(OUTPUT);
|
|
||||||
let paragraphs = build(&mut h, rows);
|
|
||||||
report("after cold frame");
|
|
||||||
for frame in 0..40 {
|
|
||||||
h.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
|
|
||||||
h.frame();
|
|
||||||
}
|
|
||||||
report("after 40 resizes");
|
|
||||||
// Settled: the output holds still and one leaf repaints per frame.
|
|
||||||
for _ in 0..10 {
|
|
||||||
let _ = h.rsc.widgets_mut().get_dyn_mut(paragraphs[0]);
|
|
||||||
h.frame();
|
|
||||||
}
|
|
||||||
report("after settling");
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
//! Scrolling moves content and stops at its ends.
|
|
||||||
|
|
||||||
use iris::harness::{Harness, assert_corners};
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
// Twice the window's height, so there is 200 to scroll.
|
|
||||||
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
|
||||||
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
|
||||||
h.set_root((top, bottom).span(Dir::DOWN).scrollable());
|
|
||||||
h.move_to((200, 100));
|
|
||||||
|
|
||||||
// `Scroll` starts snapped to the end.
|
|
||||||
assert_corners!(h, top, (0, -200), (400, 0));
|
|
||||||
|
|
||||||
// The handler scales a wheel line by 50.
|
|
||||||
h.scroll((0, 1));
|
|
||||||
h.frame();
|
|
||||||
assert_corners!(h, top, (0, -150), (400, 50));
|
|
||||||
|
|
||||||
h.scroll((0, 10));
|
|
||||||
h.frame();
|
|
||||||
assert_corners!(h, top, (0, 0), (400, 200));
|
|
||||||
}
|
|
||||||
-525
@@ -1,525 +0,0 @@
|
|||||||
//! A property test that shrinks its own counterexample.
|
|
||||||
//!
|
|
||||||
//! `generated.rs` reproduces a failure from a seed, but a seed is not a lead
|
|
||||||
//! anybody can read: the tree is hundreds of widgets, and reconstructing the
|
|
||||||
//! part that matters by hand has failed every time it has been tried. This
|
|
||||||
//! grows trees it can take apart, so a failure is reduced to the smallest
|
|
||||||
//! tree that still shows it and printed as something to write a fast test
|
|
||||||
//! from.
|
|
||||||
//!
|
|
||||||
//! cargo test --release --test shrink -- --ignored --nocapture
|
|
||||||
//!
|
|
||||||
//! `SHRINK_SEEDS` how many trees to try, `SHRINK_DEPTH` how deep to grow
|
|
||||||
//! them, `SHRINK_CASE` which scenario. It is a fuzzer: run it once the
|
|
||||||
//! ordinary tests pass, and turn what it finds into a test of its own rather
|
|
||||||
//! than leaving a seed as the record.
|
|
||||||
|
|
||||||
use iris::harness::Harness;
|
|
||||||
use iris::prelude::*;
|
|
||||||
use iris::random::{Branch, Rng};
|
|
||||||
|
|
||||||
/// The same two leaves `iris::random` grows, since only one of them reads the
|
|
||||||
/// width it is given and that is the difference that matters.
|
|
||||||
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.",
|
|
||||||
];
|
|
||||||
|
|
||||||
const ONE_LINE: &str = "one line, overflowing whatever it is given";
|
|
||||||
|
|
||||||
const OUTER: (f32, f32) = (1920.0, 1200.0);
|
|
||||||
const INNER: (f32, f32) = (640.0, 900.0);
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
enum Node {
|
|
||||||
/// Words taken from [`WORDS`], and whether it wraps.
|
|
||||||
Text(usize, bool),
|
|
||||||
/// The leaf that overflows whatever box it is given rather than wrapping.
|
|
||||||
OneLine,
|
|
||||||
Rect,
|
|
||||||
/// Direction, gap, children in creation order, and the order they are
|
|
||||||
/// attached in -- separate so a tree that reorders its children
|
|
||||||
/// still makes the same widgets in the same order, and two
|
|
||||||
/// builds line up index for index.
|
|
||||||
Span(bool, f32, Vec<Node>, Vec<usize>),
|
|
||||||
Stack(Vec<Node>),
|
|
||||||
Pad(f32, Box<Node>),
|
|
||||||
Aligned(u8, u8, Box<Node>),
|
|
||||||
Sized(Option<Len>, Option<Len>, Box<Node>),
|
|
||||||
Scroll(bool, Box<Node>),
|
|
||||||
Branch(Box<Node>, Box<Node>, Box<Node>, f32),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn axis_align(v: u8) -> Option<AxisAlign> {
|
|
||||||
match v % 4 {
|
|
||||||
0 => None,
|
|
||||||
1 => Some(AxisAlign::Neg),
|
|
||||||
2 => Some(AxisAlign::Center),
|
|
||||||
_ => Some(AxisAlign::Pos),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dir(down: bool) -> Dir {
|
|
||||||
if down { Dir::DOWN } else { Dir::RIGHT }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Node {
|
|
||||||
/// Builds into `h`, pushing every id in tree order, so two builds of one
|
|
||||||
/// node line up index for index and their boxes can be compared.
|
|
||||||
fn build(
|
|
||||||
&self,
|
|
||||||
h: &mut Harness,
|
|
||||||
out: &mut Vec<WidgetId>,
|
|
||||||
spans: &mut Vec<WeakWidget<Span>>,
|
|
||||||
) -> StrongWidget {
|
|
||||||
let id: StrongWidget = match self {
|
|
||||||
Node::Text(words, wrap) => {
|
|
||||||
let n = (*words).clamp(1, WORDS.len());
|
|
||||||
wtext(WORDS[..n].join(" "))
|
|
||||||
.size(16)
|
|
||||||
.wrap(*wrap)
|
|
||||||
.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
Node::OneLine => wtext(ONE_LINE).size(16).wrap(false).add_strong(&mut h.rsc),
|
|
||||||
Node::Rect => rect(Color::RED).add_strong(&mut h.rsc),
|
|
||||||
Node::Span(down, gap, kids, order) => {
|
|
||||||
let mut built: Vec<_> = kids.iter().map(|k| Some(k.build(h, out, spans))).collect();
|
|
||||||
// `order` is a permutation, so each is taken exactly once.
|
|
||||||
let children = order
|
|
||||||
.iter()
|
|
||||||
.map(|&i| built[i].take().expect("order repeats an index"))
|
|
||||||
.collect();
|
|
||||||
let handle = Span {
|
|
||||||
children,
|
|
||||||
dir: dir(*down),
|
|
||||||
gap: *gap,
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
spans.push(handle);
|
|
||||||
handle.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
Node::Stack(kids) => {
|
|
||||||
let children = kids.iter().map(|k| k.build(h, out, spans)).collect();
|
|
||||||
Stack {
|
|
||||||
children,
|
|
||||||
size: StackSize::Child(0),
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
Node::Pad(p, kid) => {
|
|
||||||
let inner = kid.build(h, out, spans);
|
|
||||||
Pad {
|
|
||||||
padding: Padding {
|
|
||||||
left: *p,
|
|
||||||
right: *p,
|
|
||||||
top: *p,
|
|
||||||
bottom: *p,
|
|
||||||
},
|
|
||||||
inner,
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
Node::Aligned(x, y, kid) => {
|
|
||||||
let inner = kid.build(h, out, spans);
|
|
||||||
Aligned {
|
|
||||||
inner,
|
|
||||||
align: Align {
|
|
||||||
x: axis_align(*x),
|
|
||||||
y: axis_align(*y),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
Node::Sized(x, y, kid) => {
|
|
||||||
let inner = kid.build(h, out, spans);
|
|
||||||
SetSize {
|
|
||||||
inner,
|
|
||||||
x: *x,
|
|
||||||
y: *y,
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
Node::Scroll(down, kid) => {
|
|
||||||
let inner = kid.build(h, out, spans);
|
|
||||||
let axis = if *down { Axis::Y } else { Axis::X };
|
|
||||||
Scroll::new(inner, axis).add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
Node::Branch(probe, a, b, at) => {
|
|
||||||
let probe = probe.build(h, out, spans);
|
|
||||||
let wide = a.build(h, out, spans);
|
|
||||||
let narrow = b.build(h, out, spans);
|
|
||||||
Branch {
|
|
||||||
probe,
|
|
||||||
wide,
|
|
||||||
narrow,
|
|
||||||
threshold: *at,
|
|
||||||
}
|
|
||||||
.add_strong(&mut h.rsc)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
out.push(id.id());
|
|
||||||
id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn size(&self) -> usize {
|
|
||||||
1 + match self {
|
|
||||||
Node::Text(..) | Node::OneLine | Node::Rect => 0,
|
|
||||||
Node::Span(_, _, kids, _) | Node::Stack(kids) => kids.iter().map(Node::size).sum(),
|
|
||||||
Node::Pad(_, k)
|
|
||||||
| Node::Aligned(_, _, k)
|
|
||||||
| Node::Sized(_, _, k)
|
|
||||||
| Node::Scroll(_, k) => k.size(),
|
|
||||||
Node::Branch(p, a, b, _) => p.size() + a.size() + b.size(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every one-step simplification: a wrapper replaced by what it wrapped, a
|
|
||||||
/// child dropped, a length or a word count reduced. Ordered cheapest-first
|
|
||||||
/// so the greedy walk takes the biggest bites early.
|
|
||||||
fn smaller(&self) -> Vec<Node> {
|
|
||||||
let mut out = Vec::new();
|
|
||||||
let leaf = Node::Rect;
|
|
||||||
match self {
|
|
||||||
Node::Text(words, wrap) => {
|
|
||||||
if *words > 1 {
|
|
||||||
out.push(Node::Text(words / 2, *wrap));
|
|
||||||
out.push(Node::Text(words - 1, *wrap));
|
|
||||||
}
|
|
||||||
if *wrap {
|
|
||||||
out.push(Node::Text(*words, false));
|
|
||||||
}
|
|
||||||
out.push(leaf);
|
|
||||||
}
|
|
||||||
Node::OneLine => out.push(Node::Rect),
|
|
||||||
Node::Rect => {}
|
|
||||||
Node::Span(down, gap, kids, order) => {
|
|
||||||
out.extend(order.iter().map(|&i| kids[i].clone()));
|
|
||||||
for i in 0..kids.len() {
|
|
||||||
if kids.len() > 1 {
|
|
||||||
let mut less = kids.clone();
|
|
||||||
less.remove(i);
|
|
||||||
let order = (0..less.len()).collect();
|
|
||||||
out.push(Node::Span(*down, *gap, less, order));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if *gap != 0.0 {
|
|
||||||
out.push(Node::Span(*down, 0.0, kids.clone(), order.clone()));
|
|
||||||
}
|
|
||||||
for (i, kid) in kids.iter().enumerate() {
|
|
||||||
for small in kid.smaller() {
|
|
||||||
let mut next = kids.clone();
|
|
||||||
next[i] = small;
|
|
||||||
out.push(Node::Span(*down, *gap, next, order.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Node::Stack(kids) => {
|
|
||||||
out.extend(kids.iter().cloned());
|
|
||||||
for i in 0..kids.len() {
|
|
||||||
if kids.len() > 1 {
|
|
||||||
let mut less = kids.clone();
|
|
||||||
less.remove(i);
|
|
||||||
out.push(Node::Stack(less));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (i, kid) in kids.iter().enumerate() {
|
|
||||||
for small in kid.smaller() {
|
|
||||||
let mut next = kids.clone();
|
|
||||||
next[i] = small;
|
|
||||||
out.push(Node::Stack(next));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Node::Pad(p, kid) => {
|
|
||||||
out.push((**kid).clone());
|
|
||||||
if *p != 0.0 {
|
|
||||||
out.push(Node::Pad(0.0, kid.clone()));
|
|
||||||
}
|
|
||||||
out.extend(
|
|
||||||
kid.smaller()
|
|
||||||
.into_iter()
|
|
||||||
.map(|k| Node::Pad(*p, Box::new(k))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Node::Aligned(x, y, kid) => {
|
|
||||||
out.push((**kid).clone());
|
|
||||||
for (nx, ny) in [(0, *y), (*x, 0)] {
|
|
||||||
if (nx, ny) != (*x, *y) {
|
|
||||||
out.push(Node::Aligned(nx, ny, kid.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.extend(
|
|
||||||
kid.smaller()
|
|
||||||
.into_iter()
|
|
||||||
.map(|k| Node::Aligned(*x, *y, Box::new(k))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Node::Sized(x, y, kid) => {
|
|
||||||
out.push((**kid).clone());
|
|
||||||
if x.is_some() {
|
|
||||||
out.push(Node::Sized(None, *y, kid.clone()));
|
|
||||||
}
|
|
||||||
if y.is_some() {
|
|
||||||
out.push(Node::Sized(*x, None, kid.clone()));
|
|
||||||
}
|
|
||||||
out.extend(
|
|
||||||
kid.smaller()
|
|
||||||
.into_iter()
|
|
||||||
.map(|k| Node::Sized(*x, *y, Box::new(k))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Node::Scroll(down, kid) => {
|
|
||||||
out.push((**kid).clone());
|
|
||||||
out.extend(
|
|
||||||
kid.smaller()
|
|
||||||
.into_iter()
|
|
||||||
.map(|k| Node::Scroll(*down, Box::new(k))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Node::Branch(p, a, b, at) => {
|
|
||||||
out.push((**p).clone());
|
|
||||||
out.push((**a).clone());
|
|
||||||
out.push((**b).clone());
|
|
||||||
for small in p.smaller() {
|
|
||||||
out.push(Node::Branch(Box::new(small), a.clone(), b.clone(), *at));
|
|
||||||
}
|
|
||||||
for small in a.smaller() {
|
|
||||||
out.push(Node::Branch(p.clone(), Box::new(small), b.clone(), *at));
|
|
||||||
}
|
|
||||||
for small in b.smaller() {
|
|
||||||
out.push(Node::Branch(p.clone(), a.clone(), Box::new(small), *at));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A declared size over about half the tree, the way `iris::random` puts them
|
|
||||||
/// in: on the way into every child rather than as a node kind of its own, so
|
|
||||||
/// readers of a size are dense rather than occasional.
|
|
||||||
fn sized(rng: &mut Rng, inner: Node) -> Node {
|
|
||||||
if !rng.chance() {
|
|
||||||
return inner;
|
|
||||||
}
|
|
||||||
let len = |rng: &mut Rng| match rng.below(4) {
|
|
||||||
0 => Some(Len::px(20.0 + rng.below(180) as f32)),
|
|
||||||
1 => Some(Len::REST),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
Node::Sized(len(rng), len(rng), Box::new(inner))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn grow(rng: &mut Rng, depth: usize) -> Node {
|
|
||||||
if depth == 0 {
|
|
||||||
return match rng.below(4) {
|
|
||||||
0 => Node::Text(1 + rng.below(WORDS.len()), true),
|
|
||||||
1 => Node::OneLine,
|
|
||||||
_ => Node::Rect,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let len = |rng: &mut Rng| match rng.below(4) {
|
|
||||||
0 => Some(Len::px(20.0 + rng.below(180) as f32)),
|
|
||||||
1 => Some(Len::REST),
|
|
||||||
2 => Some(Len::rel(0.25 + rng.below(3) as f32 * 0.25)),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let kid = |rng: &mut Rng| {
|
|
||||||
let inner = grow(rng, depth - 1);
|
|
||||||
sized(rng, inner)
|
|
||||||
};
|
|
||||||
match rng.below(8) {
|
|
||||||
0 => Node::Scroll(rng.chance(), Box::new(kid(rng))),
|
|
||||||
1 => Node::Aligned(rng.below(4) as u8, rng.below(4) as u8, Box::new(kid(rng))),
|
|
||||||
2 => Node::Pad(rng.below(24) as f32, Box::new(kid(rng))),
|
|
||||||
3 => Node::Sized(len(rng), len(rng), Box::new(kid(rng))),
|
|
||||||
4 => Node::Branch(
|
|
||||||
Box::new(kid(rng)),
|
|
||||||
Box::new(kid(rng)),
|
|
||||||
Box::new(kid(rng)),
|
|
||||||
rng.below(500) as f32,
|
|
||||||
),
|
|
||||||
5 => Node::Stack((0..2 + rng.below(2)).map(|_| kid(rng)).collect()),
|
|
||||||
_ => {
|
|
||||||
let kids: Vec<_> = (0..2 + rng.below(3)).map(|_| kid(rng)).collect();
|
|
||||||
let order = (0..kids.len()).collect();
|
|
||||||
Node::Span(rng.chance(), rng.below(3) as f32 * 4.0, kids, order)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
|
||||||
enum Case {
|
|
||||||
Resize,
|
|
||||||
Repaint,
|
|
||||||
ResizeRepaint,
|
|
||||||
Reorder,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every span's children rotated by one, as a tree rather than as a change:
|
|
||||||
/// what a warm frame reaches by moving them has to be where growing them that
|
|
||||||
/// way lands.
|
|
||||||
fn reordered(node: &Node) -> Node {
|
|
||||||
match node {
|
|
||||||
Node::Span(down, gap, kids, order) => {
|
|
||||||
let kids = kids.iter().map(reordered).collect::<Vec<_>>();
|
|
||||||
let mut order = order.clone();
|
|
||||||
order.rotate_left(1);
|
|
||||||
Node::Span(*down, *gap, kids, order)
|
|
||||||
}
|
|
||||||
Node::Stack(kids) => Node::Stack(kids.iter().map(reordered).collect()),
|
|
||||||
Node::Pad(p, k) => Node::Pad(*p, Box::new(reordered(k))),
|
|
||||||
Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(reordered(k))),
|
|
||||||
Node::Sized(x, y, k) => Node::Sized(*x, *y, Box::new(reordered(k))),
|
|
||||||
Node::Scroll(d, k) => Node::Scroll(*d, Box::new(reordered(k))),
|
|
||||||
Node::Branch(p, a, b, at) => Node::Branch(
|
|
||||||
Box::new(reordered(p)),
|
|
||||||
Box::new(reordered(a)),
|
|
||||||
Box::new(reordered(b)),
|
|
||||||
*at,
|
|
||||||
),
|
|
||||||
leaf => leaf.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Runs one scenario warm and cold and says where they disagree.
|
|
||||||
fn diverges(node: &Node, case: Case) -> Option<String> {
|
|
||||||
let resizes = matches!(case, Case::Resize | Case::ResizeRepaint);
|
|
||||||
let repaints = matches!(case, Case::Repaint | Case::ResizeRepaint);
|
|
||||||
let start = if resizes { OUTER } else { INNER };
|
|
||||||
let mut warm = Harness::new(start);
|
|
||||||
let mut warm_ids = Vec::new();
|
|
||||||
let mut warm_spans = Vec::new();
|
|
||||||
let root = node.build(&mut warm, &mut warm_ids, &mut warm_spans);
|
|
||||||
warm.state.root = Some(root);
|
|
||||||
// The frame that makes it warm: without it there is nothing retained and
|
|
||||||
// the comparison is two cold starts agreeing with each other.
|
|
||||||
warm.frame();
|
|
||||||
if resizes {
|
|
||||||
warm.resize(INNER);
|
|
||||||
warm.frame();
|
|
||||||
}
|
|
||||||
if repaints {
|
|
||||||
for &id in &warm_ids {
|
|
||||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
|
||||||
}
|
|
||||||
warm.frame();
|
|
||||||
}
|
|
||||||
if case == Case::Reorder {
|
|
||||||
for span in &warm_spans {
|
|
||||||
warm.rsc[*span].children.rotate_left(1);
|
|
||||||
}
|
|
||||||
warm.frame();
|
|
||||||
}
|
|
||||||
|
|
||||||
// What the warm tree was moved into, grown that way from the start.
|
|
||||||
let want = match case {
|
|
||||||
Case::Reorder => reordered(node),
|
|
||||||
_ => node.clone(),
|
|
||||||
};
|
|
||||||
let mut cold = Harness::new(INNER);
|
|
||||||
let mut cold_ids = Vec::new();
|
|
||||||
let mut cold_spans = Vec::new();
|
|
||||||
let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans);
|
|
||||||
cold.state.root = Some(root);
|
|
||||||
cold.frame();
|
|
||||||
|
|
||||||
for (i, (&w, &c)) in warm_ids.iter().zip(&cold_ids).enumerate() {
|
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
||||||
let same = match (got, want) {
|
|
||||||
(Some(g), Some(c)) => {
|
|
||||||
let d = |a: f32, b: f32| (a - b).abs() <= 0.05;
|
|
||||||
d(g.top_left.x, c.top_left.x)
|
|
||||||
&& d(g.top_left.y, c.top_left.y)
|
|
||||||
&& d(g.bot_right.x, c.bot_right.x)
|
|
||||||
&& d(g.bot_right.y, c.bot_right.y)
|
|
||||||
}
|
|
||||||
(None, None) => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
if !same {
|
|
||||||
return Some(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes the first simplification that still fails, until none does.
|
|
||||||
fn shrink(mut node: Node, case: Case) -> Node {
|
|
||||||
loop {
|
|
||||||
let Some(next) = node
|
|
||||||
.smaller()
|
|
||||||
.into_iter()
|
|
||||||
.find(|small| diverges(small, case).is_some())
|
|
||||||
else {
|
|
||||||
return node;
|
|
||||||
};
|
|
||||||
node = next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|
||||||
std::env::var(name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.parse().ok())
|
|
||||||
.unwrap_or(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "a fuzzer; run it once the ordinary tests pass"]
|
|
||||||
fn no_grown_tree_lays_out_differently_warm_than_cold() {
|
|
||||||
let seeds: u64 = env("SHRINK_SEEDS", 400);
|
|
||||||
let depth: usize = env("SHRINK_DEPTH", 5);
|
|
||||||
let case = match env("SHRINK_CASE", String::from("resize")).as_str() {
|
|
||||||
"repaint" => Case::Repaint,
|
|
||||||
"resize-repaint" => Case::ResizeRepaint,
|
|
||||||
"reorder" => Case::Reorder,
|
|
||||||
_ => Case::Resize,
|
|
||||||
};
|
|
||||||
|
|
||||||
for seed in 1..=seeds {
|
|
||||||
let node = grow(&mut Rng::new(seed), depth);
|
|
||||||
let Some(how) = diverges(&node, case) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let small = shrink(node.clone(), case);
|
|
||||||
println!(
|
|
||||||
"seed {seed}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
|
|
||||||
node.size(),
|
|
||||||
small.size()
|
|
||||||
);
|
|
||||||
panic!("seed {seed} lays out differently warm than cold");
|
|
||||||
}
|
|
||||||
let sizes: Vec<usize> = (1..=seeds)
|
|
||||||
.map(|seed| grow(&mut Rng::new(seed), depth).size())
|
|
||||||
.collect();
|
|
||||||
let total: usize = sizes.iter().sum();
|
|
||||||
println!(
|
|
||||||
"{seeds} trees at depth {depth} agree: {} widgets total, largest {}",
|
|
||||||
total,
|
|
||||||
sizes.iter().max().copied().unwrap_or(0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
//! What a background task can change, and how it gets back to the ui.
|
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use iris::harness::Harness;
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_task_update_reaches_the_tree() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let widget = rect(Color::RED).add(&mut h.rsc);
|
|
||||||
h.set_root(widget.task_on(CursorSense::click(), async move |mut ctx| {
|
|
||||||
ctx.update(move |_, rsc| widget(rsc).color = Color::BLUE);
|
|
||||||
}));
|
|
||||||
|
|
||||||
h.click((200, 100));
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
h.await_update(Duration::from_secs(5)),
|
|
||||||
"the task sent no update"
|
|
||||||
);
|
|
||||||
assert_eq!(h.rsc[widget].color, Color::BLUE);
|
|
||||||
}
|
|
||||||
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user