Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91b71b97dc | ||
|
|
b9c4856e3f | ||
|
|
79dcc156c9 | ||
|
|
c0fcc0345c | ||
|
|
444a2cd138 | ||
|
|
23fb71ee56 | ||
|
|
01a9b8633d | ||
|
|
29d390da52 | ||
|
|
7b318e3271 | ||
|
|
89491a5949 | ||
|
|
a08f61a80c | ||
|
|
4d9839f380 | ||
|
|
b3d3da5dab | ||
|
|
f5864da3c4 | ||
|
|
0106257be0 | ||
|
|
bafaa1db6d |
No files matched your search
Generated
+245
-228
File diff suppressed because it is too large.
Load diff
+3
-5
@@ -20,7 +20,7 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
|
||||
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
|
||||
|
||||
[workspace]
|
||||
members = ["core", "macro", "rig-input"]
|
||||
members = ["core", "macro"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
@@ -29,9 +29,9 @@ edition = "2024"
|
||||
[workspace.dependencies]
|
||||
pollster = "0.4.0"
|
||||
winit = "0.30.12"
|
||||
wgpu = "30.0.1"
|
||||
wgpu = "28.0.0"
|
||||
bytemuck = "1.23.1"
|
||||
image = "0.25.10"
|
||||
image = "0.25.6"
|
||||
parley = "0.11.1"
|
||||
swash = "0.2.10"
|
||||
fxhash = "0.2.1"
|
||||
@@ -40,5 +40,3 @@ arboard = "3.6.1"
|
||||
iris-core = { path = "core" }
|
||||
iris-macro = { path = "macro" }
|
||||
tokio = "1.49.0"
|
||||
wayland-client = "0.31.15"
|
||||
wayland-protocols-wlr = { version = "0.3.12", features = ["client"] }
|
||||
@@ -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> {
|
||||
// TODO: reduce visiblity!!
|
||||
pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>,
|
||||
pub global: E::Global,
|
||||
map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>,
|
||||
}
|
||||
|
||||
@@ -108,7 +107,6 @@ impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active: Default::default(),
|
||||
global: Default::default(),
|
||||
map: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -140,13 +138,11 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
|
||||
pub fn run_fn<'a>(
|
||||
&mut self,
|
||||
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();
|
||||
move |ctx, rsc| {
|
||||
let mut consumed = false;
|
||||
for (e, f) in fs {
|
||||
if let Some(data) = e.should_run(&ctx.data) {
|
||||
consumed |= e.consumes(&data);
|
||||
f(
|
||||
EventCtx {
|
||||
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 {
|
||||
type Data<'a>: Clone = ();
|
||||
type State: Default = ();
|
||||
/// State the whole event type keeps, rather than one copy per widget.
|
||||
type Global: Default = ();
|
||||
#[allow(unused_variables)]
|
||||
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
||||
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 {
|
||||
|
||||
@@ -21,13 +21,12 @@ pub trait HasEvents: Sized + UiRsc + HasState {
|
||||
}
|
||||
|
||||
pub trait RunEvents: HasEvents {
|
||||
/// Whether anything that ran used up what triggered it.
|
||||
fn run_event<E: EventLike>(
|
||||
&mut self,
|
||||
id: impl IdLike,
|
||||
data: <E::Event as Event>::Data<'_>,
|
||||
state: &mut Self::State,
|
||||
) -> bool {
|
||||
) {
|
||||
let f = self.events_mut().get_type::<E>().run_fn(id);
|
||||
f(EventCtx { state, data }, self)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
@@ -202,6 +209,12 @@ impl UiScalar {
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn outside(&self, span: &UiSpan) -> Self {
|
||||
let rel = self.rel.lerp_inv(span.start.rel, span.end.rel);
|
||||
let abs = self.abs - rel.lerp(span.start.abs, span.end.abs);
|
||||
Self { rel, abs }
|
||||
}
|
||||
|
||||
pub fn within_len(&self, len: UiScalar) -> Self {
|
||||
self.within(&UiSpan {
|
||||
start: UiScalar::ZERO,
|
||||
@@ -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 {
|
||||
self.end - self.start
|
||||
}
|
||||
@@ -304,7 +324,14 @@ impl UiRegion {
|
||||
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 {
|
||||
Axis::X => &self.x,
|
||||
Axis::Y => &self.y,
|
||||
@@ -394,7 +421,7 @@ impl Display for UiRegion {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub struct PixelRegion {
|
||||
pub top_left: Vec2,
|
||||
pub bot_right: Vec2,
|
||||
|
||||
+4
-46
@@ -1,10 +1,11 @@
|
||||
use crate::{UiRegion, util::Id, util::Vec2};
|
||||
use crate::{UiRegion, util::Id};
|
||||
use wgpu::*;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||
pub struct WindowUniform {
|
||||
pub dim: Vec2,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
@@ -12,17 +13,15 @@ pub struct WindowUniform {
|
||||
pub struct PrimitiveInstance {
|
||||
pub region: UiRegion,
|
||||
pub mask_idx: MaskIdx,
|
||||
pub move_idx: MoveIdx,
|
||||
}
|
||||
|
||||
impl PrimitiveInstance {
|
||||
const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![
|
||||
const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![
|
||||
0 => Float32x2,
|
||||
1 => Float32x2,
|
||||
2 => Float32x2,
|
||||
3 => Float32x2,
|
||||
4 => Uint32,
|
||||
5 => Uint32,
|
||||
];
|
||||
|
||||
pub fn desc() -> VertexBufferLayout<'static> {
|
||||
@@ -44,45 +43,4 @@ impl MaskIdx {
|
||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Mask {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
+14
-41
@@ -17,7 +17,7 @@ mod texture;
|
||||
mod util;
|
||||
|
||||
pub use atlas::*;
|
||||
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset};
|
||||
pub use data::{Mask, MaskIdx};
|
||||
pub use primitive::*;
|
||||
|
||||
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
||||
@@ -34,7 +34,6 @@ pub struct UiRenderNode {
|
||||
active: Vec<usize>,
|
||||
window_buffer: Buffer,
|
||||
masks: ArrBuf<Mask>,
|
||||
moves: ArrBuf<MoveOffset>,
|
||||
}
|
||||
|
||||
struct RenderLayer {
|
||||
@@ -128,35 +127,32 @@ impl UiRenderNode {
|
||||
for primitive in &mut self.primitives {
|
||||
primitive.render.update(ui);
|
||||
}
|
||||
let mut regroup = false;
|
||||
if ui.masks.changed {
|
||||
ui.masks.changed = false;
|
||||
regroup |= self.masks.update(device, queue, &ui.masks[..]);
|
||||
}
|
||||
if ui_render.moves.changed {
|
||||
ui_render.moves.changed = false;
|
||||
regroup |= self.moves.update(device, queue, ui_render.moves.entries());
|
||||
}
|
||||
if regroup {
|
||||
if self.masks.update(device, queue, &ui.masks[..]) {
|
||||
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) {
|
||||
let size = size.into();
|
||||
let slice = &[WindowUniform { dim: size }];
|
||||
let slice = &[WindowUniform {
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
}];
|
||||
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
||||
}
|
||||
|
||||
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
|
||||
let window_uniform = WindowUniform {
|
||||
dim: Vec2::new(config.width as f32, config.height as f32),
|
||||
width: config.width as f32,
|
||||
height: config.height as f32,
|
||||
};
|
||||
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
||||
label: Some("window"),
|
||||
@@ -170,13 +166,7 @@ impl UiRenderNode {
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"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);
|
||||
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
|
||||
|
||||
Self {
|
||||
shared_layout,
|
||||
@@ -187,7 +177,6 @@ impl UiRenderNode {
|
||||
layers: HashMap::default(),
|
||||
active: Vec::new(),
|
||||
masks,
|
||||
moves,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,8 +186,8 @@ impl UiRenderNode {
|
||||
for source in ®istry.sources()[self.primitives.len()..] {
|
||||
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 mut groups = vec![&self.shared_layout, &data_layout];
|
||||
groups.extend(render.layout());
|
||||
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some(source.label),
|
||||
bind_group_layouts: &groups,
|
||||
@@ -230,7 +219,7 @@ impl UiRenderNode {
|
||||
vertex: VertexState {
|
||||
module: &module,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[Some(PrimitiveInstance::desc())],
|
||||
buffers: &[PrimitiveInstance::desc()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
@@ -263,8 +252,7 @@ impl UiRenderNode {
|
||||
})
|
||||
}
|
||||
|
||||
/// What every draw in the ui is given: the window, the masks and the
|
||||
/// move chain every position is resolved through.
|
||||
/// What every draw in the ui is given: the window and the masks.
|
||||
fn shared_layout(device: &Device) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[
|
||||
@@ -288,16 +276,6 @@ impl UiRenderNode {
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: BufferSize::new(size_of::<MoveOffset>() as u64),
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some("ui shared"),
|
||||
})
|
||||
@@ -308,7 +286,6 @@ impl UiRenderNode {
|
||||
layout: &BindGroupLayout,
|
||||
window: &Buffer,
|
||||
masks: &ArrBuf<Mask>,
|
||||
moves: &ArrBuf<MoveOffset>,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
@@ -321,10 +298,6 @@ impl UiRenderNode {
|
||||
binding: 1,
|
||||
resource: masks.buffer.as_entire_binding(),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: moves.buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label: Some("ui shared"),
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{any::TypeId, marker::PhantomData};
|
||||
use crate::{
|
||||
Color, TextureHandle, UiData, UiRegion, WidgetId,
|
||||
render::{
|
||||
data::{MaskIdx, MoveIdx, PrimitiveInstance},
|
||||
data::{MaskIdx, PrimitiveInstance},
|
||||
page::GlyphRender,
|
||||
texture::ImageRender,
|
||||
},
|
||||
@@ -246,7 +246,6 @@ impl LayerDraws {
|
||||
primitive,
|
||||
region,
|
||||
mask_idx,
|
||||
move_idx,
|
||||
}: PrimitiveInst<P>,
|
||||
) -> PrimitiveHandle {
|
||||
self.updated = true;
|
||||
@@ -259,11 +258,7 @@ impl LayerDraws {
|
||||
.get_or_insert_with(InstanceList::new::<P>)
|
||||
.push(
|
||||
id,
|
||||
PrimitiveInstance {
|
||||
region,
|
||||
mask_idx,
|
||||
move_idx,
|
||||
},
|
||||
PrimitiveInstance { region, mask_idx },
|
||||
bytemuck::bytes_of(&primitive),
|
||||
);
|
||||
PrimitiveHandle {
|
||||
@@ -309,7 +304,6 @@ pub struct PrimitiveInst<P> {
|
||||
pub primitive: P,
|
||||
pub region: UiRegion,
|
||||
pub mask_idx: MaskIdx,
|
||||
pub move_idx: MoveIdx,
|
||||
}
|
||||
|
||||
pub struct PrimitiveChange {
|
||||
@@ -353,7 +347,7 @@ 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, align(8))]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct GlyphPrimitive {
|
||||
pub uv_min: Vec2,
|
||||
@@ -364,8 +358,8 @@ pub struct GlyphPrimitive {
|
||||
pub flags: u32,
|
||||
}
|
||||
|
||||
// Manual rather than derived: `Vec2`'s alignment leaves four bytes of padding
|
||||
// here, which is how WGSL lays the struct out.
|
||||
// Manual rather than derived: the align(8) leaves four bytes of padding, which
|
||||
// is how WGSL lays the struct out.
|
||||
unsafe impl bytemuck::Pod for GlyphPrimitive {}
|
||||
unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
|
||||
impl Primitive for GlyphPrimitive {
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
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>,
|
||||
@@ -17,49 +15,6 @@ struct WindowUniform {
|
||||
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;
|
||||
|
||||
fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar {
|
||||
return UiScalar(
|
||||
mix(p.start.rel, p.end.rel, s.rel),
|
||||
s.abs + mix(p.start.abs, p.end.abs, 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 {
|
||||
@@ -78,7 +33,6 @@ struct InstanceInput {
|
||||
@location(2) y_start: vec2<f32>,
|
||||
@location(3) y_end: vec2<f32>,
|
||||
@location(4) mask_idx: u32,
|
||||
@location(5) move_idx: u32,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@@ -98,15 +52,10 @@ fn vs_main(
|
||||
) -> 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_abs = vec2(r.x.start.abs, r.y.start.abs);
|
||||
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
|
||||
let bot_right_abs = vec2(r.x.end.abs, r.y.end.abs);
|
||||
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);
|
||||
@@ -132,13 +81,10 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
||||
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_abs = vec2(m.x.start.abs, m.y.start.abs);
|
||||
let br = vec2(m.x.end.rel, m.y.end.rel);
|
||||
let br_abs = vec2(m.x.end.abs, m.y.end.abs);
|
||||
let tl = vec2(mask.x.start.rel, mask.y.start.rel);
|
||||
let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
|
||||
let br = vec2(mask.x.end.rel, mask.y.end.rel);
|
||||
let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
|
||||
|
||||
let top_left = floor(tl * window.dim) + floor(tl_abs);
|
||||
let bot_right = floor(br * window.dim) + floor(br_abs);
|
||||
|
||||
+1
-12
@@ -1,25 +1,14 @@
|
||||
use crate::{LayerId, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId};
|
||||
use crate::{LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId};
|
||||
|
||||
/// important non rendering data for retained drawing
|
||||
#[derive(Debug)]
|
||||
pub struct ActiveData {
|
||||
pub id: WidgetId,
|
||||
pub region: UiRegion,
|
||||
/// What the widget said it used of `region`, the last time it drew.
|
||||
pub size: Size,
|
||||
pub parent: Option<WidgetId>,
|
||||
pub textures: Vec<TextureHandle>,
|
||||
pub primitives: Vec<PrimitiveHandle>,
|
||||
pub children: Vec<WidgetId>,
|
||||
/// The children whose size this widget read while drawing.
|
||||
pub size_deps: Vec<WidgetId>,
|
||||
/// Whether it read the output's size, and so is wrong when that changes.
|
||||
pub reads_output: bool,
|
||||
/// 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 slot `region` is given in, which is whatever its parent drew in.
|
||||
pub parent_move: MoveIdx,
|
||||
pub mask: MaskIdx,
|
||||
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();
|
||||
}
|
||||
}
|
||||
+4
-90
@@ -1,21 +1,17 @@
|
||||
use crate::{
|
||||
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, UiRegion, WeakWidget,
|
||||
WidgetId, Widgets,
|
||||
util::{Arena, Id, TrackedArena},
|
||||
Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::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 cache;
|
||||
mod painter;
|
||||
mod render_state;
|
||||
mod size;
|
||||
|
||||
pub use active::*;
|
||||
pub use painter::{Painter, PrimitiveLike};
|
||||
pub use render_state::*;
|
||||
pub use size::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UiData {
|
||||
@@ -27,88 +23,6 @@ pub struct UiData {
|
||||
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 {
|
||||
fn ui(&self) -> &UiData;
|
||||
fn ui_mut(&mut self) -> &mut UiData;
|
||||
|
||||
+27
-101
@@ -1,9 +1,9 @@
|
||||
use crate::{
|
||||
Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
|
||||
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
|
||||
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
|
||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
|
||||
render::{
|
||||
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
||||
PrimitiveKind, TexturePrimitive,
|
||||
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
|
||||
TexturePrimitive,
|
||||
},
|
||||
util::Vec2,
|
||||
};
|
||||
@@ -13,18 +13,11 @@ pub struct Painter<'a> {
|
||||
pub(super) state: &'a mut UiRenderState,
|
||||
pub(super) rsc: &'a mut dyn UiRsc,
|
||||
|
||||
/// This widget's box, in the coordinates of `move_idx`.
|
||||
pub(super) region: UiRegion,
|
||||
pub(super) mask: MaskIdx,
|
||||
pub(super) textures: Vec<TextureHandle>,
|
||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||
pub(super) children: Vec<WidgetId>,
|
||||
/// The children whose size this widget read while drawing.
|
||||
pub(super) size_deps: Vec<WidgetId>,
|
||||
pub(super) reads_output: bool,
|
||||
/// 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(super) id: WidgetId,
|
||||
}
|
||||
@@ -45,7 +38,6 @@ impl<'a> Painter<'a> {
|
||||
primitive,
|
||||
region,
|
||||
mask_idx: self.mask,
|
||||
move_idx: self.move_idx,
|
||||
},
|
||||
);
|
||||
self.push_primitive(h);
|
||||
@@ -72,82 +64,31 @@ impl<'a> Painter<'a> {
|
||||
|
||||
pub fn set_mask(&mut self, region: UiRegion) {
|
||||
assert!(self.mask == MaskIdx::NONE);
|
||||
self.mask = self.rsc.ui_mut().masks.push(Mask {
|
||||
region,
|
||||
move_idx: self.move_idx,
|
||||
});
|
||||
self.mask = self.rsc.ui_mut().masks.push(Mask { 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> {
|
||||
self.widget_at(id, self.region, false)
|
||||
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
|
||||
self.widget_at(id, self.region);
|
||||
}
|
||||
|
||||
/// Draws a widget somewhere within this one.
|
||||
pub fn widget_within<'s, W: ?Sized>(
|
||||
&'s mut self,
|
||||
id: &'s StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
) -> DrawResult<'s, 'a, W> {
|
||||
let region = region.within(&self.region);
|
||||
self.widget_at(id, region, false)
|
||||
/// Useful for drawing child widgets in select areas.
|
||||
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
|
||||
self.widget_at(id, region.within(&self.region));
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let region = region.within(&self.region);
|
||||
self.widget_at(id, region, true)
|
||||
}
|
||||
|
||||
fn widget_at<'s, W: ?Sized>(
|
||||
&'s mut self,
|
||||
id: &'s StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
slotted: bool,
|
||||
) -> DrawResult<'s, 'a, W> {
|
||||
// A child listed twice would be moved twice.
|
||||
if !self.children.contains(&id.id()) {
|
||||
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
|
||||
self.children.push(id.id());
|
||||
}
|
||||
let size = self.state.draw_inner(
|
||||
self.state.draw_inner(
|
||||
self.layer,
|
||||
id.id(),
|
||||
region,
|
||||
Some(self.id),
|
||||
self.move_idx,
|
||||
slotted,
|
||||
self.mask,
|
||||
None,
|
||||
self.rsc,
|
||||
);
|
||||
DrawResult {
|
||||
child: id,
|
||||
painter: self,
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
/// What a child says its length is without being drawn, if it can say.
|
||||
/// Asking counts as reading its size.
|
||||
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
|
||||
let hint = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis)?;
|
||||
self.depend_on_size(id);
|
||||
Some(hint)
|
||||
}
|
||||
|
||||
fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||
if !self.size_deps.contains(&child.id()) {
|
||||
self.size_deps.push(child.id());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_text(
|
||||
@@ -184,26 +125,27 @@ impl<'a> Painter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
self.region
|
||||
}
|
||||
|
||||
/// The output's size in pixels. A widget that reads it draws again when
|
||||
/// the output changes, since nothing else can put that right.
|
||||
pub fn output_size(&mut self) -> Vec2 {
|
||||
self.reads_output = true;
|
||||
pub fn size<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size {
|
||||
self.size_ctx().size(id)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
self.reads_output = true;
|
||||
let region = self.state.moves.resolve(self.move_idx, self.region);
|
||||
region.size().to_abs(self.state.output_size)
|
||||
self.region.size().to_abs(self.state.output_size)
|
||||
}
|
||||
|
||||
pub fn text_data(&mut self) -> &mut TextData {
|
||||
@@ -225,25 +167,9 @@ impl<'a> Painter<'a> {
|
||||
pub fn id(&self) -> &WidgetId {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
||||
/// A child that has just been drawn. Reading its size records that this
|
||||
/// widget's own size depends on it; dropping it without reading draws the
|
||||
/// child and leaves the parent independent of what it came to.
|
||||
pub struct DrawResult<'p, 'a, W: ?Sized> {
|
||||
painter: &'p mut Painter<'a>,
|
||||
child: &'p StrongWidget<W>,
|
||||
size: Size,
|
||||
}
|
||||
|
||||
impl<W: ?Sized> DrawResult<'_, '_, W> {
|
||||
pub fn size(self) -> Size {
|
||||
self.painter.depend_on_size(self.child);
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn len(self, axis: Axis) -> Len {
|
||||
self.size().axis(axis)
|
||||
pub fn size_ctx(&mut self) -> SizeCtx<'_> {
|
||||
self.state.size_ctx(self.id, self.region.size(), self.rsc)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+72
-233
@@ -1,23 +1,19 @@
|
||||
use crate::{
|
||||
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion,
|
||||
Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
|
||||
util::{HashMap, HashSet, Vec2},
|
||||
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget,
|
||||
UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
|
||||
ui::cache::Cache,
|
||||
util::{HashMap, HashSet, Vec2, forget_ref},
|
||||
};
|
||||
|
||||
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
||||
|
||||
pub struct UiRenderState {
|
||||
pub active: HashMap<WidgetId, ActiveData>,
|
||||
pub layers: DrawLayers,
|
||||
pub(super) output_size: Vec2,
|
||||
pub cache: Cache,
|
||||
|
||||
old_root: Option<WidgetId>,
|
||||
resized: bool,
|
||||
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 {
|
||||
@@ -25,12 +21,11 @@ impl UiRenderState {
|
||||
Self {
|
||||
active: Default::default(),
|
||||
layers: Default::default(),
|
||||
cache: Default::default(),
|
||||
output_size: Vec2::ZERO,
|
||||
old_root: None,
|
||||
resized: false,
|
||||
draw_started: Default::default(),
|
||||
slots: Default::default(),
|
||||
moves: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +34,6 @@ impl UiRenderState {
|
||||
self.resized = true;
|
||||
}
|
||||
|
||||
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) {
|
||||
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
||||
// decide whether to panic or not
|
||||
@@ -61,21 +52,11 @@ impl UiRenderState {
|
||||
);
|
||||
}
|
||||
let root = root.into();
|
||||
if self.root_changed(root) {
|
||||
if self.needs_full_redraw(root) {
|
||||
self.redraw_all(root, rsc);
|
||||
self.old_root = root.map(|r| r.id());
|
||||
} else if self.resized {
|
||||
// A region is a fraction of the output plus an offset, resolved
|
||||
// against the window in the shader, so a resize moves the whole
|
||||
// drawing on its own. Only a widget that read pixels can be wrong.
|
||||
for (&id, active) in &self.active {
|
||||
if active.reads_output {
|
||||
rsc.widgets_mut().needs_redraw.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.resized = false;
|
||||
if rsc.widgets().has_updates() {
|
||||
} else if rsc.widgets().has_updates() {
|
||||
self.redraw_updates(rsc);
|
||||
}
|
||||
}
|
||||
@@ -84,17 +65,7 @@ impl UiRenderState {
|
||||
self.clear(rsc);
|
||||
// free all resources & cache
|
||||
if let Some(id) = root {
|
||||
self.draw_inner(
|
||||
0,
|
||||
id.id(),
|
||||
UiRegion::FULL,
|
||||
None,
|
||||
MoveIdx::NONE,
|
||||
false,
|
||||
MaskIdx::NONE,
|
||||
None,
|
||||
rsc,
|
||||
);
|
||||
self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, None, rsc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,16 +77,22 @@ impl UiRenderState {
|
||||
id: WidgetId,
|
||||
region: UiRegion,
|
||||
parent: Option<WidgetId>,
|
||||
parent_move: MoveIdx,
|
||||
slotted: bool,
|
||||
mask: MaskIdx,
|
||||
old_children: Option<Vec<WidgetId>>,
|
||||
rsc: &mut dyn UiRsc,
|
||||
) -> Size {
|
||||
) {
|
||||
let mut old_children = old_children.unwrap_or_default();
|
||||
if self.active.contains_key(&id) {
|
||||
if let Some(size) = self.try_reuse(id, region, parent_move, rsc) {
|
||||
return size;
|
||||
if let Some(active) = self.active.get_mut(&id)
|
||||
&& !rsc.widgets().needs_redraw.contains(&id)
|
||||
{
|
||||
// check to see if we can skip drawing first
|
||||
if active.region == region {
|
||||
return;
|
||||
} else if active.region.size() == region.size() {
|
||||
// TODO: epsilon?
|
||||
let from = active.region;
|
||||
self.mov(id, from, region);
|
||||
return;
|
||||
}
|
||||
// if not, then maintain resize and track old children to remove unneeded
|
||||
let active = self.remove(id, false, rsc).unwrap();
|
||||
@@ -123,71 +100,44 @@ impl UiRenderState {
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
};
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
self.draw_started.insert(id);
|
||||
|
||||
let mut painter = Painter {
|
||||
state: self,
|
||||
region: local,
|
||||
region,
|
||||
mask,
|
||||
layer,
|
||||
id,
|
||||
textures: Vec::new(),
|
||||
primitives: Vec::new(),
|
||||
children: Vec::new(),
|
||||
size_deps: Vec::new(),
|
||||
reads_output: false,
|
||||
move_idx,
|
||||
rsc,
|
||||
};
|
||||
|
||||
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
|
||||
let size = widget.draw(&mut painter);
|
||||
widget.draw(&mut painter);
|
||||
drop(widget);
|
||||
|
||||
let Painter {
|
||||
state: _,
|
||||
rsc: _,
|
||||
region: _,
|
||||
region,
|
||||
mask,
|
||||
textures,
|
||||
primitives,
|
||||
children,
|
||||
size_deps,
|
||||
reads_output,
|
||||
move_idx,
|
||||
layer,
|
||||
id,
|
||||
} = 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
|
||||
let active = ActiveData {
|
||||
id,
|
||||
region,
|
||||
size,
|
||||
parent,
|
||||
textures,
|
||||
primitives,
|
||||
children,
|
||||
size_deps,
|
||||
reads_output,
|
||||
move_idx,
|
||||
parent_move,
|
||||
mask,
|
||||
layer,
|
||||
};
|
||||
@@ -201,129 +151,20 @@ impl UiRenderState {
|
||||
|
||||
rsc.on_draw(&active);
|
||||
self.active.insert(id, active);
|
||||
size
|
||||
}
|
||||
|
||||
/// The slot a widget's box is held in, made on its first placed draw and
|
||||
/// 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;
|
||||
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
|
||||
let active = self.active.get_mut(&id).unwrap();
|
||||
for h in &active.primitives {
|
||||
let region = self.layers[h.layer].region_mut(h);
|
||||
*region = region.outside(&from).within(&to);
|
||||
}
|
||||
let idx = self.moves.push(parent, region);
|
||||
self.slots.insert(id, idx);
|
||||
idx
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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 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,
|
||||
parent_move: MoveIdx,
|
||||
rsc: &mut dyn UiRsc,
|
||||
) -> Option<Size> {
|
||||
if rsc.widgets().needs_redraw.contains(&id) {
|
||||
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 {
|
||||
return None;
|
||||
}
|
||||
let (size, old, slot) = (active.size, active.region, active.move_idx);
|
||||
if old == region {
|
||||
return Some(size);
|
||||
}
|
||||
// Only a placed widget can be given a different box without drawing
|
||||
// again: everything it drew is a fraction of its slot's box, so one
|
||||
// entry says where all of it went.
|
||||
if slot == parent_move {
|
||||
return None;
|
||||
}
|
||||
let mut changed = [false; 2];
|
||||
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) {
|
||||
*c = region.axis(axis).len() != old.axis(axis).len();
|
||||
}
|
||||
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);
|
||||
if redraws {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// 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 whatever it comes to.
|
||||
if changed.iter().any(|&c| c) && self.redraws_under(id, changed, rsc) {
|
||||
return None;
|
||||
}
|
||||
self.moves.set(slot, region);
|
||||
self.active.get_mut(&id).unwrap().region = region;
|
||||
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 {
|
||||
let Some(active) = self.active.get(&id) else {
|
||||
return false;
|
||||
};
|
||||
active.children.iter().any(|&child| {
|
||||
let Some(data) = self.active.get(&child) else {
|
||||
return false;
|
||||
};
|
||||
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 = match rsc.widgets().get_dyn(child) {
|
||||
Some(widget) => AXES
|
||||
.into_iter()
|
||||
.zip(own)
|
||||
.any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale),
|
||||
None => true,
|
||||
};
|
||||
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
|
||||
@@ -346,16 +187,13 @@ impl UiRenderState {
|
||||
}
|
||||
|
||||
fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
|
||||
self.cache.remove(id);
|
||||
let inst = self.remove(id, true, rsc);
|
||||
if let Some(inst) = &inst {
|
||||
for c in &inst.children {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -363,8 +201,7 @@ impl UiRenderState {
|
||||
for (_, active) in self.active.drain() {
|
||||
rsc.on_undraw(&active);
|
||||
}
|
||||
self.slots.clear();
|
||||
self.moves.clear();
|
||||
self.cache.clear();
|
||||
self.layers.clear();
|
||||
rsc.widgets_mut().needs_redraw.clear();
|
||||
rsc.free();
|
||||
@@ -381,12 +218,17 @@ impl UiRenderState {
|
||||
root.into().map(|r| r.id()) != self.old_root
|
||||
}
|
||||
|
||||
// Scheduling and drawing must use the same full-redraw predicate.
|
||||
fn needs_full_redraw<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
|
||||
self.root_changed(root) || self.resized
|
||||
}
|
||||
|
||||
pub fn needs_redraw<'a>(
|
||||
&self,
|
||||
root: impl Into<Option<&'a StrongWidget>>,
|
||||
widgets: &Widgets,
|
||||
) -> bool {
|
||||
self.root_changed(root) || self.resized || widgets.has_updates()
|
||||
self.needs_full_redraw(root) || widgets.has_updates()
|
||||
}
|
||||
|
||||
pub fn active_widgets(&self) -> usize {
|
||||
@@ -412,29 +254,29 @@ impl UiRenderState {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let active = self.active.get(&id.id())?;
|
||||
let region = self.moves.resolve(active.parent_move, active.region);
|
||||
let region = self.active.get(&id.id())?.region;
|
||||
Some(region.to_px(self.output_size))
|
||||
}
|
||||
|
||||
/// redraws a widget that's currently active (drawn)
|
||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
self.draw_started.remove(&id);
|
||||
// Whoever read this widget's size may be a different size now, so the
|
||||
// highest reader is what draws. Everything between the two is marked
|
||||
// as well: their own boxes have not changed, so the mark is the only
|
||||
// thing stopping the draw reusing its way past this widget.
|
||||
if let Some(top) = self.mark_readers(id, rsc) {
|
||||
self.redraw(top, rsc);
|
||||
// Cleared by that draw if it reached here; if it did not, this is
|
||||
// no longer drawn and asking again would not end.
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
return;
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
|
||||
if self.draw_started.contains(&id) {
|
||||
return;
|
||||
@@ -449,31 +291,28 @@ impl UiRenderState {
|
||||
id,
|
||||
active.region,
|
||||
active.parent,
|
||||
active.parent_move,
|
||||
active.move_idx != active.parent_move,
|
||||
active.mask,
|
||||
Some(active.children),
|
||||
rsc,
|
||||
);
|
||||
}
|
||||
|
||||
/// The furthest ancestor that read this widget's size, directly or through
|
||||
/// widgets that did the same, marking everything below it on the way.
|
||||
fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<WidgetId> {
|
||||
let mut top = None;
|
||||
let mut at = id;
|
||||
while let Some(active) = self.active.get(&at)
|
||||
&& let Some(parent) = active.parent
|
||||
&& self
|
||||
.active
|
||||
.get(&parent)
|
||||
.is_some_and(|p| p.size_deps.contains(&at))
|
||||
{
|
||||
rsc.widgets_mut().needs_redraw.insert(at);
|
||||
top = Some(parent);
|
||||
at = parent;
|
||||
pub(super) fn size_ctx<'b>(
|
||||
&'b mut self,
|
||||
source: WidgetId,
|
||||
outer: UiVec2,
|
||||
rsc: &'b mut dyn UiRsc,
|
||||
) -> SizeCtx<'b> {
|
||||
let ui = rsc.ui_mut();
|
||||
SizeCtx {
|
||||
source,
|
||||
cache: &mut self.cache,
|
||||
text: &mut ui.text,
|
||||
widgets: &ui.widgets,
|
||||
outer,
|
||||
output_size: self.output_size,
|
||||
id: source,
|
||||
}
|
||||
top
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use crate::{
|
||||
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2,
|
||||
WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
||||
};
|
||||
|
||||
pub struct SizeCtx<'a> {
|
||||
pub text: &'a mut TextData,
|
||||
pub(super) source: WidgetId,
|
||||
pub(super) widgets: &'a Widgets,
|
||||
pub(super) cache: &'a mut Cache,
|
||||
/// TODO: should this be pub? rn used for sized
|
||||
pub outer: UiVec2,
|
||||
pub(super) output_size: Vec2,
|
||||
pub(super) id: WidgetId,
|
||||
}
|
||||
|
||||
impl SizeCtx<'_> {
|
||||
pub fn id(&self) -> &WidgetId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn source(&self) -> &WidgetId {
|
||||
&self.source
|
||||
}
|
||||
|
||||
pub(super) fn len_inner<A: const AxisT>(&mut self, id: WidgetId) -> Len {
|
||||
if let Some((_, len)) = self.cache.size.axis::<A>().get(&id) {
|
||||
return *len;
|
||||
}
|
||||
let len = self
|
||||
.widgets
|
||||
.get_dyn_dynamic(id)
|
||||
.desired_len::<A>(&mut SizeCtx {
|
||||
text: self.text,
|
||||
source: self.source,
|
||||
widgets: self.widgets,
|
||||
cache: self.cache,
|
||||
outer: self.outer,
|
||||
output_size: self.output_size,
|
||||
id,
|
||||
});
|
||||
self.cache.size.axis::<A>().insert(id, (self.outer, len));
|
||||
len
|
||||
}
|
||||
|
||||
pub fn width(&mut self, id: impl IdLike) -> Len {
|
||||
self.len_inner::<XAxis>(id.id())
|
||||
}
|
||||
|
||||
pub fn height(&mut self, id: impl IdLike) -> Len {
|
||||
self.len_inner::<YAxis>(id.id())
|
||||
}
|
||||
|
||||
pub fn len_axis(&mut self, id: impl IdLike, axis: Axis) -> Len {
|
||||
match axis {
|
||||
Axis::X => self.width(id),
|
||||
Axis::Y => self.height(id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&mut self, id: impl IdLike) -> Size {
|
||||
let id = id.id();
|
||||
Size {
|
||||
x: self.width(id),
|
||||
y: self.height(id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn px_size(&mut self) -> Vec2 {
|
||||
self.outer.to_abs(self.output_size)
|
||||
}
|
||||
|
||||
pub fn output_size(&mut self) -> Vec2 {
|
||||
self.output_size
|
||||
}
|
||||
|
||||
pub fn draw_text(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
self.text.render(buffer, attrs, width)
|
||||
}
|
||||
|
||||
pub fn label(&self, id: WidgetId) -> &String {
|
||||
self.widgets.label(id)
|
||||
}
|
||||
}
|
||||
@@ -34,10 +34,6 @@ impl<T, I: IdNum> Arena<T, I> {
|
||||
self.tracker.free(id);
|
||||
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> {
|
||||
|
||||
+22
-1
@@ -1,13 +1,34 @@
|
||||
use std::ops::*;
|
||||
|
||||
pub const trait LerpUtil {
|
||||
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
|
||||
/// from * (1.0 - self) + to * self
|
||||
fn lerp(self, from: Self, to: Self) -> 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 {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
#[allow(clippy::missing_safety_doc)]
|
||||
pub(crate) unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
|
||||
unsafe { std::mem::transmute::<&T, &T>(x) }
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_safety_doc)]
|
||||
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
|
||||
unsafe { std::mem::transmute::<&mut 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::*};
|
||||
|
||||
/// `align(8)` because that is WGSL's alignment for a `vec2<f32>`, so any GPU
|
||||
/// 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))]
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Vec2 {
|
||||
pub x: f32,
|
||||
@@ -71,6 +67,15 @@ impl_op!(Vec2 Sub sub; x y);
|
||||
impl_op!(Vec2 Mul mul; 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 {
|
||||
type Output = Self;
|
||||
|
||||
|
||||
+18
-31
@@ -1,4 +1,4 @@
|
||||
use crate::{Axis, Len, Painter, Size};
|
||||
use crate::{Axis, AxisT, Len, Painter, SizeCtx};
|
||||
use std::any::Any;
|
||||
|
||||
mod data;
|
||||
@@ -15,45 +15,32 @@ pub use tag::*;
|
||||
pub use view::*;
|
||||
pub use widgets::*;
|
||||
|
||||
/// What may be done to a widget's drawing when the box it was given changes
|
||||
/// on this axis, instead of drawing it again. Asked per axis, because wrapped
|
||||
/// text reads the width it is offered and not the height.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum OnResize {
|
||||
Scale,
|
||||
Translate,
|
||||
#[default]
|
||||
Redraw,
|
||||
pub trait Widget: Any {
|
||||
fn draw(&mut self, painter: &mut Painter);
|
||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len;
|
||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len;
|
||||
}
|
||||
|
||||
pub trait Widget: Any {
|
||||
/// Draws the widget, and returns what it used of the box it was given.
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size;
|
||||
pub trait WidgetAxisFns {
|
||||
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len;
|
||||
}
|
||||
|
||||
/// An exact length the widget can give without a painter or its children.
|
||||
/// Optional, and saves a draw rather than changing one: a hint that
|
||||
/// disagrees with the eventual draw fails a debug assertion.
|
||||
fn size_hint(&self, _axis: Axis) -> Option<Len> {
|
||||
None
|
||||
impl<W: Widget + ?Sized> WidgetAxisFns for W {
|
||||
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
match A::get() {
|
||||
Axis::X => self.desired_width(ctx),
|
||||
Axis::Y => self.desired_height(ctx),
|
||||
}
|
||||
|
||||
fn on_resize(&self, _axis: Axis) -> OnResize {
|
||||
OnResize::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for () {
|
||||
/// A gap: nothing drawn, at the default length, so a span gives it a share.
|
||||
fn draw(&mut self, _: &mut Painter) -> Size {
|
||||
Size::default()
|
||||
fn draw(&mut self, _: &mut Painter) {}
|
||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||
Len::ZERO
|
||||
}
|
||||
|
||||
fn size_hint(&self, _axis: Axis) -> Option<Len> {
|
||||
Some(Len::default())
|
||||
}
|
||||
|
||||
fn on_resize(&self, _axis: Axis) -> OnResize {
|
||||
OnResize::Scale
|
||||
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
||||
Len::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -10,7 +10,11 @@ struct 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>,
|
||||
_: Proxy<Self::Event>,
|
||||
) -> Self {
|
||||
rect(Color::RED).set_root(rsc, &mut 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 std::collections::HashMap;
|
||||
|
||||
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, &HashMap::new());
|
||||
ui_state.set_root(root);
|
||||
Self { ui_state }
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,11 @@ pub struct 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>,
|
||||
_: Proxy<Self::Event>,
|
||||
) -> Self {
|
||||
let rrect = rect(Color::WHITE).radius(20);
|
||||
let pad_test = (
|
||||
rrect.color(Color::BLUE),
|
||||
|
||||
+5
-1
@@ -11,7 +11,11 @@ struct 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>,
|
||||
_: Proxy<Self::Event>,
|
||||
) -> Self {
|
||||
let rect = rect(Color::RED).add(rsc);
|
||||
rect.task_on(CursorSense::click(), async move |mut ctx| {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -36,7 +36,11 @@ impl Test {
|
||||
}
|
||||
|
||||
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>,
|
||||
_: Proxy<Self::Event>,
|
||||
) -> Self {
|
||||
let test = Test::new(rsc);
|
||||
|
||||
test.on(CursorSense::click(), move |_, rsc| {
|
||||
|
||||
@@ -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,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,170 +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.
|
||||
#
|
||||
# `--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=""
|
||||
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 ;;
|
||||
--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] [--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 "$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"
|
||||
+44
-57
@@ -1,6 +1,10 @@
|
||||
use crate::prelude::*;
|
||||
use arboard::Clipboard;
|
||||
use std::{marker::PhantomData, sync::Arc, time::Instant};
|
||||
use std::{
|
||||
marker::{PhantomData, Sized},
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
};
|
||||
use winit::{
|
||||
event::{Ime, WindowEvent},
|
||||
event_loop::{ActiveEventLoop, EventLoopProxy},
|
||||
@@ -25,35 +29,7 @@ pub use sense::*;
|
||||
pub use state::*;
|
||||
pub use task::*;
|
||||
|
||||
/// Sends an application's own events to its event loop. It wraps the proxy
|
||||
/// rather than being one because task updates travel the same way: what an
|
||||
/// 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> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: DefaultAppState> Proxy<State> {
|
||||
pub fn send_event(&self, event: State::Event) {
|
||||
let _ = self.0.send_event(DefaultEvent::User(event));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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));
|
||||
}
|
||||
}
|
||||
pub type Proxy<Event> = EventLoopProxy<Event>;
|
||||
|
||||
pub struct DefaultUiState {
|
||||
pub root: Option<StrongWidget>,
|
||||
@@ -94,8 +70,9 @@ pub trait HasDefaultUiState: Sized + 'static {
|
||||
}
|
||||
|
||||
pub trait DefaultAppState: HasDefaultUiState {
|
||||
type Event: Send = ();
|
||||
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self>) -> Self;
|
||||
type Event = ();
|
||||
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>)
|
||||
-> Self;
|
||||
#[allow(unused_variables)]
|
||||
fn event(
|
||||
&mut self,
|
||||
@@ -128,14 +105,18 @@ pub struct DefaultRsc<State: 'static> {
|
||||
}
|
||||
|
||||
impl<State> DefaultRsc<State> {
|
||||
pub fn init(queue: Arc<dyn TaskQueue<Self>>) -> Self {
|
||||
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) {
|
||||
let (tasks, recv) = Tasks::init(window);
|
||||
(
|
||||
Self {
|
||||
ui: Default::default(),
|
||||
events: Default::default(),
|
||||
tasks: Tasks::init(queue),
|
||||
tasks,
|
||||
state: Default::default(),
|
||||
_state: Default::default(),
|
||||
}
|
||||
},
|
||||
recv,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
|
||||
@@ -200,32 +181,43 @@ pub struct DefaultApp<State: DefaultAppState> {
|
||||
rsc: DefaultRsc<State>,
|
||||
render: UiRenderState,
|
||||
state: State,
|
||||
task_recv: TaskMsgReceiver<DefaultRsc<State>>,
|
||||
}
|
||||
|
||||
impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
type Event = DefaultEvent<State>;
|
||||
type Event = State::Event;
|
||||
|
||||
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
|
||||
let window = event_loop
|
||||
.create_window(State::window_attributes())
|
||||
.unwrap();
|
||||
let default_state = DefaultUiState::new(window);
|
||||
let mut rsc = DefaultRsc::init(Arc::new(Proxy(proxy.clone())));
|
||||
let state = State::new(default_state, &mut rsc, Proxy(proxy));
|
||||
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone());
|
||||
let state = State::new(default_state, &mut rsc, proxy);
|
||||
let render = UiRenderState::new();
|
||||
Self { rsc, state, render }
|
||||
Self {
|
||||
rsc,
|
||||
state,
|
||||
render,
|
||||
task_recv,
|
||||
}
|
||||
}
|
||||
|
||||
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
|
||||
match event {
|
||||
DefaultEvent::User(event) => self.state.event(event, &mut self.rsc, &mut self.render),
|
||||
DefaultEvent::Update(update) => update(&mut self.state, &mut self.rsc),
|
||||
}
|
||||
self.request_redraw_if_needed();
|
||||
self.state.event(event, &mut self.rsc, &mut self.render);
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
|
||||
let Self { rsc, render, state } = self;
|
||||
let Self {
|
||||
rsc,
|
||||
render,
|
||||
state,
|
||||
task_recv,
|
||||
} = self;
|
||||
|
||||
for update in task_recv.try_iter() {
|
||||
update(state, rsc);
|
||||
}
|
||||
|
||||
let ui_state = state.default_state_mut();
|
||||
let input_changed = ui_state.input.event(&event);
|
||||
@@ -235,7 +227,8 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
ui_state.focus = None;
|
||||
}
|
||||
if input_changed {
|
||||
render.run_sensors(rsc, state, cursor_state);
|
||||
let window_size = ui_state.window_size();
|
||||
render.run_sensors(rsc, state, cursor_state, window_size);
|
||||
}
|
||||
let ui_state = state.default_state_mut();
|
||||
if old != ui_state.focus
|
||||
@@ -304,8 +297,11 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
_ => (),
|
||||
}
|
||||
state.window_event(event, rsc, render);
|
||||
self.request_redraw_if_needed();
|
||||
self.state.default_state_mut().input.end_frame();
|
||||
let ui_state = self.state.default_state_mut();
|
||||
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
|
||||
ui_state.renderer.window().request_redraw();
|
||||
}
|
||||
ui_state.input.end_frame();
|
||||
}
|
||||
|
||||
fn exit(&mut self) {
|
||||
@@ -313,15 +309,6 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: DefaultAppState> DefaultApp<State> {
|
||||
fn request_redraw_if_needed(&mut self) {
|
||||
let ui_state = self.state.default_state_mut();
|
||||
if self.render.needs_redraw(&ui_state.root, self.rsc.widgets()) {
|
||||
ui_state.renderer.window().request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RscIdx<Rsc> {
|
||||
type Output;
|
||||
fn get(self, rsc: &Rsc) -> &Self::Output;
|
||||
|
||||
+4
-20
@@ -22,20 +22,7 @@ impl UiRenderer {
|
||||
}
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
let output = match self.surface.get_current_texture() {
|
||||
CurrentSurfaceTexture::Success(texture) => texture,
|
||||
CurrentSurfaceTexture::Suboptimal(texture) => {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
texture
|
||||
}
|
||||
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
return;
|
||||
}
|
||||
CurrentSurfaceTexture::Timeout
|
||||
| CurrentSurfaceTexture::Occluded
|
||||
| CurrentSurfaceTexture::Validation => return,
|
||||
};
|
||||
let output = self.surface.get_current_texture().unwrap();
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&TextureViewDescriptor::default());
|
||||
@@ -59,7 +46,7 @@ impl UiRenderer {
|
||||
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.window.pre_present_notify();
|
||||
self.queue.present(output);
|
||||
output.present();
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
|
||||
@@ -78,10 +65,9 @@ impl UiRenderer {
|
||||
pub fn new(window: Arc<Window>) -> Self {
|
||||
let size = window.inner_size();
|
||||
|
||||
let instance = Instance::new(InstanceDescriptor {
|
||||
let instance = Instance::new(&InstanceDescriptor {
|
||||
backends: Backends::PRIMARY,
|
||||
display: Some(Box::new(window.clone())),
|
||||
..InstanceDescriptor::new_without_display_handle()
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let surface = instance
|
||||
@@ -93,7 +79,6 @@ impl UiRenderer {
|
||||
power_preference: PowerPreference::default(),
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
apply_limit_buckets: false,
|
||||
})
|
||||
.block_on()
|
||||
.expect("Could not get adapter!");
|
||||
@@ -120,7 +105,6 @@ impl UiRenderer {
|
||||
let config = SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_format,
|
||||
color_space: SurfaceColorSpace::Auto,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: PresentMode::AutoVsync,
|
||||
|
||||
+31
-99
@@ -4,14 +4,14 @@ use std::{
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum CursorButton {
|
||||
Left,
|
||||
Right,
|
||||
Middle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum CursorSense {
|
||||
PressStart(CursorButton),
|
||||
Pressing(CursorButton),
|
||||
@@ -27,7 +27,7 @@ pub struct CursorSenses(Vec<CursorSense>);
|
||||
|
||||
impl Event for CursorSenses {
|
||||
type Data<'a> = CursorData<'a>;
|
||||
type Global = Hovered;
|
||||
type State = SensorState;
|
||||
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
||||
if let Some(sense) = should_run(self, &data.cursor, data.hover) {
|
||||
let mut data = data.clone();
|
||||
@@ -37,24 +37,6 @@ impl Event for CursorSenses {
|
||||
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 {
|
||||
@@ -70,12 +52,6 @@ impl CursorSense {
|
||||
pub fn is_dragging(&self) -> bool {
|
||||
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)]
|
||||
@@ -120,12 +96,6 @@ impl CursorButtons {
|
||||
}
|
||||
|
||||
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) {
|
||||
self.buttons.end_frame();
|
||||
self.scroll_delta = Vec2::ZERO;
|
||||
@@ -153,6 +123,11 @@ pub struct Sensor<Ctx: HasEvents, Data> {
|
||||
|
||||
pub type SenseShape = UiRegion;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct SensorState {
|
||||
pub hover: ActivationState,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CursorData<'a> {
|
||||
/// where this widget was hit
|
||||
@@ -172,6 +147,7 @@ pub trait SensorUi {
|
||||
rsc: &mut Rsc,
|
||||
state: &mut Rsc::State,
|
||||
cursor: CursorState,
|
||||
window_size: Vec2,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,85 +157,46 @@ impl SensorUi for UiRenderState {
|
||||
rsc: &mut Rsc,
|
||||
state: &mut Rsc::State,
|
||||
cursor: CursorState,
|
||||
window_size: Vec2,
|
||||
) {
|
||||
// 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 /
|
||||
// state like thing, but local to render state, and is passed to UiRsc events so you can
|
||||
// update it there?
|
||||
let 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);
|
||||
|
||||
let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
|
||||
for layer in self.layers.indices().rev() {
|
||||
let mut consumed = false;
|
||||
for id in active.get(&layer).into_flat_iter().map(|(id, _)| *id) {
|
||||
let Some(region) = region_of(id) else {
|
||||
continue;
|
||||
};
|
||||
if !cursor.exists || !region.contains(cursor.pos) {
|
||||
let mut sensed = false;
|
||||
for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
|
||||
let shape = self.active.get(id).unwrap().region;
|
||||
let region = shape.to_px(window_size);
|
||||
let in_shape = cursor.exists && region.contains(cursor.pos);
|
||||
sensor.hover.update(in_shape);
|
||||
if sensor.hover == ActivationState::Off {
|
||||
continue;
|
||||
}
|
||||
hovered.now.push(id);
|
||||
let hover = match hovered.was.contains(&id) {
|
||||
true => ActivationState::On,
|
||||
false => ActivationState::Start,
|
||||
};
|
||||
// A press or a scroll stops where something answered it, so a
|
||||
// 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
|
||||
// not block each other.
|
||||
if consumed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
sensed = true;
|
||||
|
||||
// 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 cursor = cursor.clone();
|
||||
|
||||
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(),
|
||||
hover: sensor.hover,
|
||||
cursor,
|
||||
// this does not have any meaning;
|
||||
// might wanna set up Event to have a prepare stage
|
||||
sense: CursorSense::Hovering,
|
||||
render,
|
||||
render: self,
|
||||
};
|
||||
rsc.run_event::<CursorSense>(id, data, state)
|
||||
rsc.run_event::<CursorSense>(*id, data, state);
|
||||
}
|
||||
if sensed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
rsc.events_mut().get_type::<CursorSense>().active = active;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_run(
|
||||
@@ -268,11 +205,6 @@ pub fn should_run(
|
||||
hover: ActivationState,
|
||||
) -> Option<CursorSense> {
|
||||
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 {
|
||||
CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(),
|
||||
CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(),
|
||||
|
||||
+34
-16
@@ -1,5 +1,11 @@
|
||||
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::{
|
||||
runtime::Runtime,
|
||||
sync::mpsc::{
|
||||
@@ -7,52 +13,64 @@ use tokio::{
|
||||
unbounded_channel as async_channel,
|
||||
},
|
||||
};
|
||||
use winit::window::Window;
|
||||
|
||||
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 {}
|
||||
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> {
|
||||
start: AsyncSender<BoxTask>,
|
||||
queue: Arc<dyn TaskQueue<Rsc>>,
|
||||
window: Arc<Window>,
|
||||
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
|
||||
}
|
||||
|
||||
pub struct TaskCtx<Rsc: HasState> {
|
||||
queue: Arc<dyn TaskQueue<Rsc>>,
|
||||
send: TaskMsgSender<Rsc>,
|
||||
}
|
||||
|
||||
impl<Rsc: HasState> TaskCtx<Rsc> {
|
||||
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>>;
|
||||
|
||||
impl<Rsc: HasState> Tasks<Rsc> {
|
||||
pub fn init(queue: Arc<dyn TaskQueue<Rsc>>) -> Self {
|
||||
pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) {
|
||||
let (start, start_recv) = async_channel();
|
||||
let (msgs, msgs_recv) = sync_channel();
|
||||
std::thread::spawn(|| {
|
||||
let rt = Runtime::new().unwrap();
|
||||
rt.block_on(listen(start_recv))
|
||||
});
|
||||
Self { start, queue }
|
||||
(
|
||||
Self {
|
||||
start,
|
||||
msg_send: msgs,
|
||||
window,
|
||||
},
|
||||
msgs_recv,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
|
||||
where
|
||||
F::CallOnceFuture: Send,
|
||||
{
|
||||
let queue = self.queue.clone();
|
||||
let send = self.msg_send.clone();
|
||||
let window = self.window.clone();
|
||||
let _ = self.start.send(Box::pin(async move {
|
||||
task(TaskCtx { queue }).await;
|
||||
task(TaskCtx::new(send)).await;
|
||||
window.request_redraw();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
-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}");
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
pub mod default;
|
||||
pub mod event;
|
||||
pub mod harness;
|
||||
pub mod random;
|
||||
pub mod widget;
|
||||
|
||||
pub use iris_core as core;
|
||||
|
||||
-161
@@ -1,161 +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];
|
||||
|
||||
/// 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,
|
||||
];
|
||||
|
||||
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>>,
|
||||
}
|
||||
|
||||
/// 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: &HashMap<usize, Lens>,
|
||||
) -> (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 HashMap<usize, Lens>,
|
||||
}
|
||||
|
||||
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::abs(20.0 + self.rng.below(180) as f32)),
|
||||
1 => Some(Len::REST),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.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 count = 2 + self.rng.below(2);
|
||||
let mut children = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let child = self.node(depth - 1);
|
||||
children.push(self.sized(child));
|
||||
}
|
||||
let id: StrongWidget = match self.rng.below(3) {
|
||||
0 => Stack {
|
||||
children,
|
||||
size: StackSize::Child(0),
|
||||
}
|
||||
.add_strong(self.rsc),
|
||||
_ => {
|
||||
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][self.rng.below(4)];
|
||||
Span {
|
||||
children,
|
||||
dir,
|
||||
gap: self.rng.below(3) as f32 * 4.0,
|
||||
}
|
||||
.add_strong(self.rsc)
|
||||
}
|
||||
};
|
||||
self.tree.ids.push(id.id());
|
||||
id
|
||||
}
|
||||
}
|
||||
+5
-6
@@ -6,17 +6,16 @@ pub struct Image {
|
||||
}
|
||||
|
||||
impl Widget for Image {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
painter.primitive(&self.handle);
|
||||
Size::abs(self.handle.size())
|
||||
}
|
||||
|
||||
fn size_hint(&self, axis: Axis) -> Option<Len> {
|
||||
Some(Len::abs(self.handle.size().axis(axis)))
|
||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||
Len::abs(self.handle.size().x)
|
||||
}
|
||||
|
||||
fn on_resize(&self, _: Axis) -> OnResize {
|
||||
OnResize::Scale
|
||||
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
||||
Len::abs(self.handle.size().y)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-5
@@ -5,13 +5,16 @@ pub struct 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.widget(&self.inner).size()
|
||||
painter.widget(&self.inner);
|
||||
}
|
||||
|
||||
/// It clips to the box it was given, not to the part its child used.
|
||||
fn on_resize(&self, _: Axis) -> OnResize {
|
||||
OnResize::Redraw
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -6,23 +6,30 @@ pub struct Aligned {
|
||||
}
|
||||
|
||||
impl Widget for Aligned {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
// Drawn where it may be too big, then given its aligned box once its
|
||||
// size is known.
|
||||
let size = painter.place(&self.inner, UiRegion::FULL).size();
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
let region = match self.align.tuple() {
|
||||
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
|
||||
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
|
||||
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
|
||||
(Some(x), Some(y)) => painter
|
||||
.size(&self.inner)
|
||||
.to_uivec2()
|
||||
.align(RegionAlign { x, y }),
|
||||
(Some(x), None) => {
|
||||
let x = painter.size_ctx().width(&self.inner).apply_rest().align(x);
|
||||
UiRegion::new(x, UiSpan::FULL)
|
||||
}
|
||||
(None, Some(y)) => {
|
||||
let y = painter.size_ctx().height(&self.inner).apply_rest().align(y);
|
||||
UiRegion::new(UiSpan::FULL, y)
|
||||
}
|
||||
(None, None) => UiRegion::FULL,
|
||||
};
|
||||
painter.place(&self.inner, region);
|
||||
size
|
||||
painter.widget_within(&self.inner, region);
|
||||
}
|
||||
|
||||
/// The aligned box is a fraction of its own, so the child keeps its
|
||||
/// length and stays against the edge it was aligned to.
|
||||
fn on_resize(&self, _: Axis) -> OnResize {
|
||||
OnResize::Scale
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,18 @@ pub struct 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 {
|
||||
painter.next_layer();
|
||||
}
|
||||
painter.widget(&self.inner).size()
|
||||
painter.widget(&self.inner);
|
||||
}
|
||||
|
||||
fn on_resize(&self, _: Axis) -> OnResize {
|
||||
OnResize::Scale
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -6,20 +6,43 @@ pub struct MaxSize {
|
||||
pub y: Option<Len>,
|
||||
}
|
||||
|
||||
impl Widget for MaxSize {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let child = painter.widget(&self.inner).size();
|
||||
let output = painter.output_size();
|
||||
Size {
|
||||
x: capped(child.x, self.x, output.x),
|
||||
y: capped(child.y, self.y, output.y),
|
||||
impl MaxSize {
|
||||
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
|
||||
if let Some(x) = self.x {
|
||||
ctx.outer.x.select_len(x.apply_rest());
|
||||
}
|
||||
if let Some(y) = self.y {
|
||||
ctx.outer.y.select_len(y.apply_rest());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn capped(len: Len, max: Option<Len>, output: f32) -> Len {
|
||||
match max {
|
||||
Some(max) if len.apply_rest().to_abs(output) > max.apply_rest().to_abs(output) => max,
|
||||
_ => len,
|
||||
impl Widget for MaxSize {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
painter.widget(&self.inner);
|
||||
}
|
||||
|
||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
self.apply_to_outer(ctx);
|
||||
let width = ctx.width(&self.inner);
|
||||
if let Some(x) = self.x {
|
||||
let width_px = width.apply_rest().to_abs(ctx.output_size().x);
|
||||
let x_px = x.apply_rest().to_abs(ctx.output_size().x);
|
||||
if width_px > x_px { x } else { width }
|
||||
} else {
|
||||
width
|
||||
}
|
||||
}
|
||||
|
||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
self.apply_to_outer(ctx);
|
||||
let height = ctx.height(&self.inner);
|
||||
if let Some(y) = self.y {
|
||||
let height_px = height.apply_rest().to_abs(ctx.output_size().y);
|
||||
let y_px = y.apply_rest().to_abs(ctx.output_size().y);
|
||||
if height_px > y_px { y } else { height }
|
||||
} else {
|
||||
height
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ mod max_size;
|
||||
mod offset;
|
||||
mod pad;
|
||||
mod scroll;
|
||||
mod set_size;
|
||||
mod sized;
|
||||
mod span;
|
||||
mod stack;
|
||||
|
||||
@@ -14,6 +14,6 @@ pub use max_size::*;
|
||||
pub use offset::*;
|
||||
pub use pad::*;
|
||||
pub use scroll::*;
|
||||
pub use set_size::*;
|
||||
pub use sized::*;
|
||||
pub use span::*;
|
||||
pub use stack::*;
|
||||
@@ -6,12 +6,16 @@ pub struct 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);
|
||||
painter.widget_within(&self.inner, region).size()
|
||||
painter.widget_within(&self.inner, region);
|
||||
}
|
||||
|
||||
fn on_resize(&self, _: Axis) -> OnResize {
|
||||
OnResize::Scale
|
||||
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)
|
||||
}
|
||||
}
|
||||
+20
-18
@@ -6,26 +6,28 @@ pub struct Pad {
|
||||
}
|
||||
|
||||
impl Widget for Pad {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let inner = painter
|
||||
.widget_within(&self.inner, self.padding.region())
|
||||
.size();
|
||||
Size {
|
||||
x: Len {
|
||||
abs: inner.x.abs + self.padding.left + self.padding.right,
|
||||
..inner.x
|
||||
},
|
||||
y: Len {
|
||||
abs: inner.y.abs + self.padding.top + self.padding.bottom,
|
||||
..inner.y
|
||||
},
|
||||
}
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
painter.widget_within(&self.inner, self.padding.region());
|
||||
}
|
||||
|
||||
/// The padding is an offset from each edge, so a longer box pads the same
|
||||
/// amount and the child takes the rest.
|
||||
fn on_resize(&self, _: Axis) -> OnResize {
|
||||
OnResize::Scale
|
||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
let width = self.padding.left + self.padding.right;
|
||||
let height = self.padding.top + self.padding.bottom;
|
||||
ctx.outer.x.abs -= width;
|
||||
ctx.outer.y.abs -= height;
|
||||
let mut size = ctx.width(&self.inner);
|
||||
size.abs += width;
|
||||
size
|
||||
}
|
||||
|
||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
let width = self.padding.left + self.padding.right;
|
||||
let height = self.padding.top + self.padding.bottom;
|
||||
ctx.outer.x.abs -= width;
|
||||
ctx.outer.y.abs -= height;
|
||||
let mut size = ctx.height(&self.inner);
|
||||
size.abs += height;
|
||||
size
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,11 @@ pub struct 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_size().axis(self.axis);
|
||||
let container_len = UiScalar::abs(painter.px_size().axis(self.axis));
|
||||
// Drawn in the whole container to learn its length, then placed at
|
||||
// the scrolled offset.
|
||||
let child = painter.place(&self.inner, UiRegion::FULL).size();
|
||||
let content_len = child
|
||||
.axis(self.axis)
|
||||
let container_len = painter.region().axis(self.axis).len();
|
||||
let content_len = painter
|
||||
.len_axis(&self.inner, self.axis)
|
||||
.apply_rest()
|
||||
.within_len(container_len)
|
||||
.to_abs(output_len);
|
||||
@@ -31,8 +28,15 @@ impl Widget for Scroll {
|
||||
|
||||
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);
|
||||
painter.place(&self.inner, region);
|
||||
child
|
||||
painter.widget_within(&self.inner, region);
|
||||
}
|
||||
|
||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
ctx.width(&self.inner)
|
||||
}
|
||||
|
||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
ctx.height(&self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +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 {
|
||||
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))
|
||||
}
|
||||
}
|
||||
+100
-37
@@ -8,27 +8,13 @@ pub struct Span {
|
||||
}
|
||||
|
||||
impl Widget for Span {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let axis = self.dir.axis;
|
||||
// A length for every child before any is placed: from its own hint
|
||||
// where it has one, and from drawing it where it does not.
|
||||
let lens: Vec<Len> = self
|
||||
.children
|
||||
.iter()
|
||||
.map(|child| match painter.size_hint(child, axis) {
|
||||
Some(len) => len,
|
||||
None => painter.place(child, UiRegion::FULL).len(axis),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
|
||||
let total = lens.iter().fold(Len::abs(gap), |sum, len| sum + *len);
|
||||
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
let total = self.len_sum(&mut painter.size_ctx());
|
||||
let mut start = UiScalar::rel_min();
|
||||
let mut ortho = Len::ZERO;
|
||||
for (child, len) in self.children.iter().zip(&lens) {
|
||||
for child in &self.children {
|
||||
let mut span = UiSpan::FULL;
|
||||
span.start = start;
|
||||
let len = painter.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);
|
||||
@@ -38,31 +24,27 @@ impl Widget for Span {
|
||||
start.abs += len.abs;
|
||||
start.rel += len.rel;
|
||||
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 {
|
||||
region.flip(axis);
|
||||
}
|
||||
let used = painter.place(child, region).size().axis(!axis);
|
||||
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
|
||||
if used.rel > 0.0 || used.rest > 0.0 {
|
||||
ortho = Len::REST;
|
||||
} else if ortho.rest == 0.0 {
|
||||
ortho.abs = ortho.abs.max(used.abs);
|
||||
child_region.flip(self.dir.axis);
|
||||
}
|
||||
painter.widget_within(child, child_region);
|
||||
start.abs += self.gap;
|
||||
}
|
||||
|
||||
let along = match total.rest == 0.0 && total.rel == 0.0 {
|
||||
true => total,
|
||||
false => Len::default(),
|
||||
};
|
||||
Size::from_axis(axis, along, ortho)
|
||||
}
|
||||
|
||||
/// Every child is placed in fractions and offsets of the span's own box,
|
||||
/// so a longer box holds the same layout and the children follow it.
|
||||
fn on_resize(&self, _: Axis) -> OnResize {
|
||||
OnResize::Scale
|
||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
match self.dir.axis {
|
||||
Axis::X => self.desired_len(ctx),
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +69,87 @@ impl Span {
|
||||
pub fn pop(&mut self) -> Option<StrongWidget> {
|
||||
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> {
|
||||
|
||||
@@ -8,29 +8,30 @@ pub struct Stack {
|
||||
}
|
||||
|
||||
impl Widget for Stack {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let sizing = match self.size {
|
||||
StackSize::Default => None,
|
||||
StackSize::Child(i) => Some(i),
|
||||
};
|
||||
let mut size = Size::default();
|
||||
for (i, child) in self.children.iter().enumerate() {
|
||||
match i {
|
||||
0 => painter.child_layer(),
|
||||
_ => painter.next_layer(),
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
let mut iter = self.children.iter();
|
||||
if let Some(child) = iter.next() {
|
||||
painter.child_layer();
|
||||
painter.widget(child);
|
||||
}
|
||||
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();
|
||||
for child in iter {
|
||||
painter.next_layer();
|
||||
painter.widget(child);
|
||||
}
|
||||
}
|
||||
size
|
||||
}
|
||||
|
||||
fn on_resize(&self, _: Axis) -> OnResize {
|
||||
OnResize::Scale
|
||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
match self.size {
|
||||
StackSize::Default => Len::default(),
|
||||
StackSize::Child(i) => ctx.width(&self.children[i]),
|
||||
}
|
||||
}
|
||||
|
||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
match self.size {
|
||||
StackSize::Default => Len::default(),
|
||||
StackSize::Child(i) => ctx.height(&self.children[i]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-5
@@ -1,15 +1,30 @@
|
||||
use crate::prelude::*;
|
||||
use std::marker::Unsize;
|
||||
use std::marker::{Sized, Unsize};
|
||||
|
||||
pub struct WidgetPtr {
|
||||
pub inner: Option<StrongWidget>,
|
||||
}
|
||||
|
||||
impl Widget for WidgetPtr {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
match &self.inner {
|
||||
Some(id) => painter.widget(id).size(),
|
||||
None => Size::default(),
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
if let Some(id) = &self.inner {
|
||||
painter.widget(id);
|
||||
}
|
||||
}
|
||||
|
||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
if let Some(id) = &self.inner {
|
||||
ctx.width(id)
|
||||
} else {
|
||||
Len::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
if let Some(id) = &self.inner {
|
||||
ctx.height(id)
|
||||
} else {
|
||||
Len::ZERO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-7
@@ -28,23 +28,21 @@ impl Rect {
|
||||
}
|
||||
|
||||
impl Widget for Rect {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
painter.primitive(RectPrimitive {
|
||||
color: self.color,
|
||||
radius: self.radius,
|
||||
thickness: self.thickness,
|
||||
inner_radius: self.inner_radius,
|
||||
});
|
||||
Size::REST
|
||||
}
|
||||
|
||||
fn size_hint(&self, _: Axis) -> Option<Len> {
|
||||
Some(Len::REST)
|
||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||
Len::rest(1)
|
||||
}
|
||||
|
||||
/// Its box is its primitive's own region, so a new one is written there.
|
||||
fn on_resize(&self, _: Axis) -> OnResize {
|
||||
OnResize::Scale
|
||||
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
||||
Len::rest(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
use std::marker::PhantomData;
|
||||
use std::marker::{PhantomData, Sized};
|
||||
|
||||
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
|
||||
pub content: String,
|
||||
|
||||
+13
-16
@@ -55,47 +55,44 @@ impl TextEdit {
|
||||
}
|
||||
|
||||
impl Widget for TextEdit {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
let base = painter.layer;
|
||||
painter.child_layer();
|
||||
let (_, size) = self.view.draw(painter);
|
||||
self.view.draw(painter);
|
||||
painter.layer = base;
|
||||
let region = self.region();
|
||||
|
||||
let Some(selection) = self.selection else {
|
||||
return size;
|
||||
return;
|
||||
};
|
||||
let layout = self.view.buf.layout();
|
||||
|
||||
// parley reports selection as boxes in layout space, so bidi and
|
||||
// wrapped lines come out right without this code knowing about either.
|
||||
for (rect, _) in selection.geometry(layout) {
|
||||
let rect_size = vec2(rect.width() as f32, rect.height() as f32);
|
||||
let 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),
|
||||
rect_size
|
||||
.align(Align::TOP_LEFT)
|
||||
.offset(top_left)
|
||||
.within(®ion),
|
||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||
);
|
||||
}
|
||||
|
||||
let caret = selection.focus().geometry(layout, CARET_WIDTH);
|
||||
let caret_size = vec2(caret.width() as f32, caret.height() as f32);
|
||||
let 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.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||
);
|
||||
size
|
||||
}
|
||||
|
||||
fn on_resize(&self, axis: Axis) -> OnResize {
|
||||
self.view.on_resize(axis)
|
||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
self.view.desired_width(ctx)
|
||||
}
|
||||
|
||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
self.view.desired_height(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+41
-36
@@ -52,15 +52,15 @@ impl TextView {
|
||||
.align(self.align)
|
||||
}
|
||||
|
||||
fn render(&mut self, painter: &mut Painter) -> &RenderedText {
|
||||
fn render(&mut self, ctx: &mut SizeCtx) -> &RenderedText {
|
||||
let width = if self.attrs.wrap {
|
||||
Some(painter.px_size().x)
|
||||
Some(ctx.px_size().x)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
|
||||
self.width = width;
|
||||
self.tex = Some(painter.render_text(&mut self.buf, &self.attrs, width));
|
||||
self.tex = Some(ctx.draw_text(&mut self.buf, &self.attrs, width));
|
||||
self.attrs.changed = false;
|
||||
self.buf.changed = false;
|
||||
}
|
||||
@@ -69,40 +69,39 @@ impl TextView {
|
||||
pub fn tex(&self) -> Option<&RenderedText> {
|
||||
self.tex.as_ref()
|
||||
}
|
||||
/// Draws the text, and says where the glyphs went and what they use.
|
||||
pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) {
|
||||
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
if self.is_empty()
|
||||
&& let Some(hint) = &self.hint
|
||||
{
|
||||
ctx.width(hint)
|
||||
} else {
|
||||
Len::abs(self.render(ctx).size.x)
|
||||
}
|
||||
}
|
||||
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
if self.is_empty()
|
||||
&& let Some(hint) = &self.hint
|
||||
{
|
||||
ctx.height(hint)
|
||||
} else {
|
||||
Len::abs(self.render(ctx).size.y)
|
||||
}
|
||||
}
|
||||
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
|
||||
let align = self.align;
|
||||
if self.is_empty() && self.hint.is_some() {
|
||||
let region = self.render(painter).size.align(align);
|
||||
let size = match &self.hint {
|
||||
Some(hint) => painter.widget(hint).size(),
|
||||
None => Size::ZERO,
|
||||
};
|
||||
return (region, size);
|
||||
let region = self.render(&mut painter.size_ctx()).size.align(align);
|
||||
if let Some(hint) = &self.hint {
|
||||
painter.widget(hint);
|
||||
}
|
||||
return region;
|
||||
}
|
||||
|
||||
let tex = self.render(painter);
|
||||
let tex = self.render(&mut painter.size_ctx());
|
||||
let region = tex.size.align(align);
|
||||
let size = Size::abs(tex.size);
|
||||
let within = region.within(&painter.region());
|
||||
painter.glyphs(tex, within);
|
||||
(region, size)
|
||||
}
|
||||
|
||||
/// Wrapping reads the width it is offered, so a wider box reshapes it and
|
||||
/// a taller one does not. Alignment matters too, and separately: glyphs
|
||||
/// anchored to the start of an axis stay put when that extent changes,
|
||||
/// but centred or end-aligned ones move even though the shaping stands.
|
||||
pub fn on_resize(&self, axis: Axis) -> OnResize {
|
||||
let reshapes = axis == Axis::X && self.attrs.wrap;
|
||||
let anchored = match axis {
|
||||
Axis::X => self.align.x,
|
||||
Axis::Y => self.align.y,
|
||||
} == AxisAlign::Neg;
|
||||
match reshapes || !anchored {
|
||||
true => OnResize::Redraw,
|
||||
false => OnResize::Translate,
|
||||
}
|
||||
region
|
||||
}
|
||||
|
||||
pub fn content(&self) -> String {
|
||||
@@ -118,7 +117,7 @@ impl Text {
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
fn update_buf(&mut self) {
|
||||
fn update_buf(&mut self, _ctx: &mut SizeCtx) {
|
||||
if self.content.changed {
|
||||
self.content.changed = false;
|
||||
self.view.buf.set_text(self.content.as_str());
|
||||
@@ -127,13 +126,19 @@ impl Text {
|
||||
}
|
||||
|
||||
impl Widget for Text {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.update_buf();
|
||||
self.view.draw(painter).1
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
self.update_buf(&mut painter.size_ctx());
|
||||
self.view.draw(painter);
|
||||
}
|
||||
|
||||
fn on_resize(&self, axis: Axis) -> OnResize {
|
||||
self.view.on_resize(axis)
|
||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
self.update_buf(ctx);
|
||||
self.view.desired_width(ctx)
|
||||
}
|
||||
|
||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
self.update_buf(ctx);
|
||||
self.view.desired_height(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
move |state| SetSize {
|
||||
move |state| Sized {
|
||||
inner: self.add_strong(state),
|
||||
x: Some(size.x),
|
||||
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();
|
||||
move |state| SetSize {
|
||||
move |state| Sized {
|
||||
inner: self.add_strong(state),
|
||||
x: Some(len),
|
||||
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();
|
||||
move |state| SetSize {
|
||||
move |state| Sized {
|
||||
inner: self.add_strong(state),
|
||||
x: None,
|
||||
y: Some(len),
|
||||
|
||||
@@ -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, abs: 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
|
||||
);
|
||||
}
|
||||
}
|
||||
+19
-22
@@ -4,45 +4,46 @@
|
||||
//!
|
||||
//! 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:
|
||||
//! **Read the instruction count, not the clock.** Wall time here swings by 2x
|
||||
//! between runs of one binary on this machine -- more under `cargo test` than
|
||||
//! run directly -- while instructions retired are stable to 0.1%:
|
||||
//!
|
||||
//! 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.
|
||||
//! Measured that way on 2026-09-13, drawing each primitive through its own
|
||||
//! `PrimitiveRender` rather than a match in the renderer costs **6
|
||||
//! instructions per list drawn**, which is 0.1% of a frame at both 256 and
|
||||
//! 1024 layers. Recording one list into the pass costs wgpu ~5,400.
|
||||
//!
|
||||
//! 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.
|
||||
//! The instance is leaked on purpose. Dropping the last one makes the Vulkan
|
||||
//! loader unload Mesa's ICD, which faults when a thread that touched Vulkan
|
||||
//! exits -- and libtest runs every test 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,
|
||||
GlyphPrimitive, MaskIdx, 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.
|
||||
/// Reported as the best of this many batches. The mean moves by 15% between
|
||||
/// runs on this machine, which is 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());
|
||||
// Probed rather than assumed: this machine's Vulkan device comes and goes,
|
||||
// and GL is what is left when it is gone.
|
||||
let all = Instance::new(&InstanceDescriptor::default());
|
||||
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
||||
{
|
||||
Ok(_) => all,
|
||||
Err(_) => Instance::new(InstanceDescriptor {
|
||||
Err(_) => Instance::new(&InstanceDescriptor {
|
||||
backends: Backends::GL,
|
||||
..InstanceDescriptor::new_without_display_handle()
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
// Leaked rather than dropped: see the note at the top of the file.
|
||||
@@ -57,7 +58,6 @@ fn config(format: TextureFormat) -> SurfaceConfiguration {
|
||||
SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format,
|
||||
color_space: SurfaceColorSpace::Auto,
|
||||
width: SIZE,
|
||||
height: SIZE,
|
||||
present_mode: PresentMode::Fifo,
|
||||
@@ -95,7 +95,6 @@ fn fill(
|
||||
primitive: RectPrimitive::color(UiColor::WHITE),
|
||||
region: UiRegion::FULL,
|
||||
mask_idx: MaskIdx::NONE,
|
||||
move_idx: MoveIdx::NONE,
|
||||
},
|
||||
);
|
||||
render.layers.write(
|
||||
@@ -112,7 +111,6 @@ fn fill(
|
||||
},
|
||||
region: UiRegion::FULL,
|
||||
mask_idx: MaskIdx::NONE,
|
||||
move_idx: MoveIdx::NONE,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -125,7 +123,6 @@ fn fill(
|
||||
primitive: TexturePrimitive::from(h),
|
||||
region: UiRegion::FULL,
|
||||
mask_idx: MaskIdx::NONE,
|
||||
move_idx: MoveIdx::NONE,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,171 +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::{Lens, Rng, Tree, grow};
|
||||
|
||||
const DEPTH: usize = 4;
|
||||
/// Seeds whose trees agree. The ones left out are `a_wrapping_child_of_a_row`
|
||||
/// below, which is a defect older than the chain.
|
||||
const SEEDS: [u64; 6] = [2, 3, 4, 5, 8, 9];
|
||||
|
||||
fn plant(h: &mut Harness, seed: u64, edits: &HashMap<usize, Lens>) -> Tree {
|
||||
let (root, tree) = grow(&mut h.rsc, seed, DEPTH, edits);
|
||||
h.state.root = Some(root);
|
||||
h.frame();
|
||||
tree
|
||||
}
|
||||
|
||||
/// 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());
|
||||
let lens = [
|
||||
Some(Len::abs(20.0 + rng.below(180) as f32)),
|
||||
Some(Len::abs(20.0 + rng.below(180) as f32)),
|
||||
];
|
||||
edits.insert(idx, lens);
|
||||
let sized = &mut h.rsc[tree.sized[idx]];
|
||||
sized.x = lens[0];
|
||||
sized.y = lens[1];
|
||||
}
|
||||
edits
|
||||
}
|
||||
|
||||
/// 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());
|
||||
if 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}", wh.rsc.widgets().label(id)));
|
||||
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, &HashMap::new());
|
||||
|
||||
let mut rng = Rng::new(seed ^ 0x5eed);
|
||||
let edits = edit(&mut warm, &grown, &mut rng);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let same = plant(&mut cold, seed, &edits);
|
||||
|
||||
assert_same(seed, "a size change", (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
fn resized(seed: u64) {
|
||||
let mut warm = Harness::new((1920, 1200));
|
||||
let grown = plant(&mut warm, seed, &HashMap::new());
|
||||
warm.resize((640, 900));
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let same = plant(&mut cold, seed, &HashMap::new());
|
||||
|
||||
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, &HashMap::new());
|
||||
warm.resize((640, 900));
|
||||
warm.frame();
|
||||
|
||||
let mut rng = Rng::new(seed ^ 0xb0a7);
|
||||
let edits = edit(&mut warm, &grown, &mut rng);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let same = plant(&mut cold, seed, &edits);
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
/// Reproduces a divergence that predates the position chain: laying a tree out
|
||||
/// again does not always land where growing it cold does.
|
||||
///
|
||||
/// Every one seen so far is a wrapping text on a span's *own* axis, where the
|
||||
/// two draws do not agree. The span measures the child in the whole box, the
|
||||
/// child shapes to that width and reports the width it used, the span then
|
||||
/// places it in exactly that width -- which is a length change, so the child
|
||||
/// shapes again, and its longest line is shorter than the box it was just
|
||||
/// given. Each pass narrows it, so where the tree ends up depends on how many
|
||||
/// passes it has had, and a warm tree has had a different number from a cold
|
||||
/// one. Layout is supposed to be a function of the state alone.
|
||||
///
|
||||
/// A span whose axis is not the wrap axis is stable, which is every real
|
||||
/// column of text, and why nothing else has run into this.
|
||||
///
|
||||
/// 7 of these 90 diverge on `db1751f`, before the chain; 30 do with it, since
|
||||
/// a placed child reaches the second shaping more often. Both numbers are the
|
||||
/// same defect, and it wants fixing where the two draws meet -- LAYOUT.md §4 --
|
||||
/// rather than anywhere in the chain.
|
||||
#[test]
|
||||
#[ignore = "known divergence, and the reproduction for fixing it"]
|
||||
fn a_wrapping_child_of_a_row_settles_somewhere_else_each_time() {
|
||||
for seed in 1..=30 {
|
||||
changed_size(seed);
|
||||
resized(seed);
|
||||
resized_then_changed(seed);
|
||||
}
|
||||
}
|
||||
-184
@@ -1,184 +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::abs(150));
|
||||
h.frame();
|
||||
|
||||
assert_corners!(h, inner, (150, 0), (350, 200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resize_lands_where_a_cold_start_would() {
|
||||
let build = |h: &mut Harness| {
|
||||
let para = wtext(
|
||||
"Wrapping shapes one source into as many lines as its container leaves room \
|
||||
for, so the height of a paragraph is an answer rather than a setting.",
|
||||
)
|
||||
.size(20)
|
||||
.wrap(true)
|
||||
.pad(16)
|
||||
.add(&mut h.rsc);
|
||||
let below = rect(Color::RED).add(&mut h.rsc);
|
||||
let root = (para, below).span(Dir::DOWN).pad(12);
|
||||
h.set_root(root);
|
||||
(para, below)
|
||||
};
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let (cold_para, cold_below) = build(&mut cold);
|
||||
|
||||
let mut resized = Harness::new((1920, 1200));
|
||||
let (para, below) = build(&mut resized);
|
||||
resized.resize((900, 1200));
|
||||
resized.frame();
|
||||
|
||||
assert_eq!(resized.region(¶), cold.region(&cold_para), "paragraph");
|
||||
assert_eq!(resized.region(&below), cold.region(&cold_below), "below");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fixed_box_is_drawn_again_rather_than_stretched() {
|
||||
let mut h = Harness::new((400, 400));
|
||||
// The panel fills a stack sized by its sibling, so it is drawn in the
|
||||
// whole box and then placed in the shorter one. Reusing it in that fixed
|
||||
// box afterwards would leave it whatever height it happened to have.
|
||||
let panel = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let leaf = rect(Color::RED).height(100).add(&mut h.rsc);
|
||||
let stack = (panel, leaf)
|
||||
.stack()
|
||||
.size(StackSize::Child(1))
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(stack.align(Align::TOP));
|
||||
assert_corners!(h, panel, (0, 0), (400, 100));
|
||||
|
||||
h.rsc[leaf].y = Some(Len::abs(250));
|
||||
h.frame();
|
||||
|
||||
assert_corners!(h, panel, (0, 0), (400, 250));
|
||||
}
|
||||
|
||||
#[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::abs(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::abs(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::abs(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), 1, "one span above the leaf");
|
||||
}
|
||||
@@ -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::abs(40.0 + (i % 2) as f32));
|
||||
h.frame();
|
||||
}
|
||||
}
|
||||
@@ -1,339 +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();
|
||||
|
||||
// Twice: once for the span to measure it, once for its real box. A child
|
||||
// that can hint its length is spared the first, and a smaller number here
|
||||
// means someone has made that cheaper rather than broken it.
|
||||
assert_eq!(draws.get(), settled + 2);
|
||||
assert_corners!(h, second, (150, 0), (400, 200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_span_child_that_declares_its_length_is_drawn_once() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
|
||||
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
|
||||
// The span takes one child's length from its hint and has to draw the
|
||||
// other to find out, so only the second is drawn before it is placed.
|
||||
let hinted = told.width(100).add(&mut h.rsc);
|
||||
h.set_root((hinted, asked).span(Dir::RIGHT));
|
||||
|
||||
assert_eq!(told_draws.get(), 1);
|
||||
assert_eq!(
|
||||
asked_draws.get(),
|
||||
2,
|
||||
"drawn to be measured, then again to be placed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_span_relays_out_when_a_child_it_measured_changes() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (first, _, second) = pair(&mut h, OnResize::Translate);
|
||||
|
||||
h.rsc[first].size = Size::from((250, 200));
|
||||
h.frame();
|
||||
|
||||
assert_corners!(h, first, (0, 0), (250, 200));
|
||||
assert_corners!(h, second, (250, 0), (400, 200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_placed_child_survives_the_next_frame() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
// Both children declare a length, so the span places them from their hints
|
||||
// rather than drawing them to find out.
|
||||
let top = rect(Color::RED).height(80).add(&mut h.rsc);
|
||||
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
|
||||
h.set_root((top, bottom).span(Dir::DOWN));
|
||||
|
||||
h.rsc.widgets_mut().get_dyn_mut(top.id());
|
||||
h.frame();
|
||||
|
||||
assert_corners!(h, top, (0, 0), (400, 80));
|
||||
assert_corners!(h, bottom, (0, 80), (400, 200));
|
||||
}
|
||||
|
||||
/// Lays its child out from the hint alone, never reading what it drew.
|
||||
struct FromHint {
|
||||
inner: StrongWidget,
|
||||
}
|
||||
|
||||
impl Widget for FromHint {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
|
||||
let mut region = UiRegion::FULL;
|
||||
region.y.end = region.y.start.offset(len.abs);
|
||||
painter.widget_within(&self.inner, region);
|
||||
Size::REST
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let inner = rect(Color::RED).height(80).add(&mut h.rsc);
|
||||
let parent = FromHint {
|
||||
inner: inner.add_strong(&mut h.rsc),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(parent);
|
||||
assert_corners!(h, inner, (0, 0), (400, 80));
|
||||
|
||||
h.rsc[inner].y = Some(Len::abs(120));
|
||||
h.frame();
|
||||
|
||||
assert_corners!(h, inner, (0, 0), (400, 120));
|
||||
}
|
||||
|
||||
/// Reads the output's size, which nothing but its own draw can put right.
|
||||
struct ReadsOutput {
|
||||
draws: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Widget for ReadsOutput {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.draws.set(self.draws.get() + 1);
|
||||
Size::abs(painter.output_size() / 4.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resize_does_not_redraw_what_the_shader_can_move() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw);
|
||||
h.set_root(leaf);
|
||||
let settled = draws.get();
|
||||
|
||||
h.resize((800, 100));
|
||||
assert!(h.needs_redraw());
|
||||
h.frame();
|
||||
|
||||
assert_eq!(
|
||||
draws.get(),
|
||||
settled,
|
||||
"its box is the same fraction of a different output"
|
||||
);
|
||||
assert_corners!(h, leaf, (0, 0), (800, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resize_redraws_what_read_the_output() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let leaf = ReadsOutput {
|
||||
draws: draws.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(leaf);
|
||||
let settled = draws.get();
|
||||
|
||||
h.resize((800, 100));
|
||||
h.frame();
|
||||
|
||||
assert_eq!(draws.get(), settled + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn narrowing_the_output_reflows_text_and_relays_out_around_it() {
|
||||
let mut h = Harness::new((600, 400));
|
||||
let para = wtext(
|
||||
"Wrapping shapes one source into as many lines as its container leaves \
|
||||
room for, so the height of a paragraph is an answer rather than a setting.",
|
||||
)
|
||||
.size(20)
|
||||
.wrap(true)
|
||||
.add(&mut h.rsc);
|
||||
let below = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_root((para, below).span(Dir::DOWN));
|
||||
let top = h.region(&below).expect("drew nothing").top_left.y;
|
||||
|
||||
h.resize((300, 400));
|
||||
h.frame();
|
||||
|
||||
let lower = h.region(&below).expect("drew nothing").top_left.y;
|
||||
assert!(lower > top, "same words, half the width: {top} -> {lower}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_change_two_levels_under_its_reader_still_reaches_it() {
|
||||
let mut h = Harness::new((400, 400));
|
||||
// Every wrapper up to the outer pad read the size below it, so the outer
|
||||
// pad is what draws again -- and the span it hands the box to is the same
|
||||
// size as before, which is what lets a draw reuse its way past the leaf.
|
||||
let (leaf, _) = counted(&mut h, Size::abs((100, 100).into()), OnResize::Redraw);
|
||||
let padded = leaf.pad(10).add(&mut h.rsc);
|
||||
let below = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_root((padded, below).span(Dir::DOWN).pad(12));
|
||||
assert_corners!(h, below, (12, 132), (388, 388));
|
||||
|
||||
h.rsc[leaf].size = Size::abs((100, 200).into());
|
||||
h.frame();
|
||||
|
||||
assert_corners!(h, below, (12, 232), (388, 388));
|
||||
}
|
||||
|
||||
/// 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::abs(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::abs(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_fixed_length_child_is_not_redrawn_when_the_box_around_it_grows() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
// It would be drawn again for a width it does not have: its own box is
|
||||
// a fixed 80 wherever the row's edges end up.
|
||||
let (fixed, draws) = counted(&mut h, Size::from((80, 200)), OnResize::Redraw);
|
||||
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::abs(200));
|
||||
h.frame();
|
||||
|
||||
assert_eq!(draws.get(), settled, "its own length did not change");
|
||||
assert_corners!(h, fixed, (200, 0), (280, 200));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user