Compare commits

..
Author SHA1 Message Date
irisandClaude Opus 5 0a5eb2d984 Build on wgpu 30
Two majors, and the renderer is under everything else that is left to
extract, so it goes before the slices that would otherwise be written twice.
`image` 0.25.6 -> 0.25.10 rides along; `winit` stays on 0.30.12, since 0.31
is only a prerelease.

What the API asked for:

- An instance now takes the display it will present on, and GLES on Wayland
  needs it -- so the window the surface is made from is handed over with it.
- `get_current_texture` returns a `CurrentSurfaceTexture` rather than a
  `Result`, which replaces an `unwrap` that would have panicked on a resize
  or an occluded window: reconfigure when the surface is outdated, lost or
  suboptimal, and skip the frame when there is nothing to draw into.
- Presenting moved to the queue, after `pre_present_notify` as before.
- Bind group layouts and vertex buffer layouts are sparse now, so a pipeline
  states `Some(layout)` per slot.

Verified with the three render checks and `tests/draw_cost.rs`: identical
pictures, and 33.6/167/587/2855 us per frame at 8/64/256/1024 layers against
33.3/161/588/2903 on wgpu 28.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 18:57:52 -04:00
26 changed files with 173 additions and 782 deletions

No files matched your search

+1 -6
View File
@@ -79,7 +79,6 @@ type EventData<Rsc, E> = (E, Rc<dyn for<'a> EventFn<Rsc, <E as Event>::Data<'a>>
pub struct TypeEventManager<Rsc: HasEvents, E: Event> { pub struct TypeEventManager<Rsc: HasEvents, E: Event> {
// TODO: reduce visiblity!! // TODO: reduce visiblity!!
pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>, pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>,
pub global: E::Global,
map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>, map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>,
} }
@@ -108,7 +107,6 @@ impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
fn default() -> Self { fn default() -> Self {
Self { Self {
active: Default::default(), active: Default::default(),
global: Default::default(),
map: Default::default(), map: Default::default(),
} }
} }
@@ -140,13 +138,11 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
pub fn run_fn<'a>( pub fn run_fn<'a>(
&mut self, &mut self,
id: impl IdLike, id: impl IdLike,
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) -> bool + 'a { ) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) + 'a {
let fs = self.map.get(&id.id()).cloned().unwrap_or_default(); let fs = self.map.get(&id.id()).cloned().unwrap_or_default();
move |ctx, rsc| { move |ctx, rsc| {
let mut consumed = false;
for (e, f) in fs { for (e, f) in fs {
if let Some(data) = e.should_run(&ctx.data) { if let Some(data) = e.should_run(&ctx.data) {
consumed |= e.consumes(&data);
f( f(
EventCtx { EventCtx {
state: ctx.state, state: ctx.state,
@@ -156,7 +152,6 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
) )
} }
} }
consumed
} }
} }
} }
-9
View File
@@ -9,19 +9,10 @@ pub use rsc::*;
pub trait Event: Sized + 'static + Clone { pub trait Event: Sized + 'static + Clone {
type Data<'a>: Clone = (); type Data<'a>: Clone = ();
type State: Default = (); type State: Default = ();
/// State the whole event type keeps, rather than one copy per widget.
type Global: Default = ();
#[allow(unused_variables)] #[allow(unused_variables)]
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> { fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
Some(data.clone()) Some(data.clone())
} }
/// Whether having run on this data uses up whatever triggered it, so
/// nothing further should see it.
#[allow(unused_variables)]
fn consumes(&self, data: &Self::Data<'_>) -> bool {
false
}
} }
pub trait EventLike { pub trait EventLike {
+1 -2
View File
@@ -21,13 +21,12 @@ pub trait HasEvents: Sized + UiRsc + HasState {
} }
pub trait RunEvents: HasEvents { pub trait RunEvents: HasEvents {
/// Whether anything that ran used up what triggered it.
fn run_event<E: EventLike>( fn run_event<E: EventLike>(
&mut self, &mut self,
id: impl IdLike, id: impl IdLike,
data: <E::Event as Event>::Data<'_>, data: <E::Event as Event>::Data<'_>,
state: &mut Self::State, state: &mut Self::State,
) -> bool { ) {
let f = self.events_mut().get_type::<E>().run_fn(id); let f = self.events_mut().get_type::<E>().run_fn(id);
f(EventCtx { state, data }, self) f(EventCtx { state, data }, self)
} }
+1 -1
View File
@@ -421,7 +421,7 @@ impl Display for UiRegion {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug)]
pub struct PixelRegion { pub struct PixelRegion {
pub top_left: Vec2, pub top_left: Vec2,
pub bot_right: Vec2, pub bot_right: Vec2,
-4
View File
@@ -34,10 +34,6 @@ impl UiRenderState {
self.resized = true; 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) { 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 // safety mechanism for memory leaks; might wanna return a result instead so user can
// decide whether to panic or not // decide whether to panic or not
+5 -1
View File
@@ -10,7 +10,11 @@ struct State {
} }
impl DefaultAppState for State { impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self { fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
rect(Color::RED).set_root(rsc, &mut ui_state); rect(Color::RED).set_root(rsc, &mut ui_state);
Self { ui_state } Self { ui_state }
} }
+5 -1
View File
@@ -15,7 +15,11 @@ pub struct Client {
} }
impl DefaultAppState for Client { impl DefaultAppState for Client {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self { fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let rrect = rect(Color::WHITE).radius(20); let rrect = rect(Color::WHITE).radius(20);
let pad_test = ( let pad_test = (
rrect.color(Color::BLUE), rrect.color(Color::BLUE),
+5 -1
View File
@@ -11,7 +11,11 @@ struct State {
} }
impl DefaultAppState for State { impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self { fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let rect = rect(Color::RED).add(rsc); let rect = rect(Color::RED).add(rsc);
rect.task_on(CursorSense::click(), async move |mut ctx| { rect.task_on(CursorSense::click(), async move |mut ctx| {
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
+5 -1
View File
@@ -36,7 +36,11 @@ impl Test {
} }
impl DefaultAppState for State { impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self { fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let test = Test::new(rsc); let test = Test::new(rsc);
test.on(CursorSense::click(), move |_, rsc| { test.on(CursorSense::click(), move |_, rsc| {
+49 -62
View File
@@ -1,6 +1,10 @@
use crate::prelude::*; use crate::prelude::*;
use arboard::Clipboard; use arboard::Clipboard;
use std::{marker::PhantomData, sync::Arc, time::Instant}; use std::{
marker::{PhantomData, Sized},
sync::Arc,
time::Instant,
};
use winit::{ use winit::{
event::{Ime, WindowEvent}, event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy}, event_loop::{ActiveEventLoop, EventLoopProxy},
@@ -25,35 +29,7 @@ pub use sense::*;
pub use state::*; pub use state::*;
pub use task::*; pub use task::*;
/// Sends an application's own events to its event loop. It wraps the proxy pub type Proxy<Event> = EventLoopProxy<Event>;
/// 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 struct DefaultUiState { pub struct DefaultUiState {
pub root: Option<StrongWidget>, pub root: Option<StrongWidget>,
@@ -94,8 +70,9 @@ pub trait HasDefaultUiState: Sized + 'static {
} }
pub trait DefaultAppState: HasDefaultUiState { pub trait DefaultAppState: HasDefaultUiState {
type Event: Send = (); type Event = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self>) -> Self; fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>)
-> Self;
#[allow(unused_variables)] #[allow(unused_variables)]
fn event( fn event(
&mut self, &mut self,
@@ -128,14 +105,18 @@ pub struct DefaultRsc<State: 'static> {
} }
impl<State> DefaultRsc<State> { impl<State> DefaultRsc<State> {
pub fn init(queue: Arc<dyn TaskQueue<Self>>) -> Self { fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) {
Self { let (tasks, recv) = Tasks::init(window);
ui: Default::default(), (
events: Default::default(), Self {
tasks: Tasks::init(queue), ui: Default::default(),
state: Default::default(), events: Default::default(),
_state: Default::default(), tasks,
} state: Default::default(),
_state: Default::default(),
},
recv,
)
} }
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> { pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
@@ -200,32 +181,43 @@ pub struct DefaultApp<State: DefaultAppState> {
rsc: DefaultRsc<State>, rsc: DefaultRsc<State>,
render: UiRenderState, render: UiRenderState,
state: State, state: State,
task_recv: TaskMsgReceiver<DefaultRsc<State>>,
} }
impl<State: DefaultAppState> AppState for DefaultApp<State> { impl<State: DefaultAppState> AppState for DefaultApp<State> {
type Event = DefaultEvent<State>; type Event = State::Event;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self { fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
let window = event_loop let window = event_loop
.create_window(State::window_attributes()) .create_window(State::window_attributes())
.unwrap(); .unwrap();
let default_state = DefaultUiState::new(window); let default_state = DefaultUiState::new(window);
let mut rsc = DefaultRsc::init(Arc::new(Proxy(proxy.clone()))); let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone());
let state = State::new(default_state, &mut rsc, Proxy(proxy)); let state = State::new(default_state, &mut rsc, proxy);
let render = UiRenderState::new(); let render = UiRenderState::new();
Self { rsc, state, render } Self {
rsc,
state,
render,
task_recv,
}
} }
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) { fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
match event { self.state.event(event, &mut self.rsc, &mut self.render);
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();
} }
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) { fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let Self { rsc, render, state } = self; let Self {
rsc,
render,
state,
task_recv,
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
let ui_state = state.default_state_mut(); let ui_state = state.default_state_mut();
let input_changed = ui_state.input.event(&event); let input_changed = ui_state.input.event(&event);
@@ -235,7 +227,8 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.focus = None; ui_state.focus = None;
} }
if input_changed { 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(); let ui_state = state.default_state_mut();
if old != ui_state.focus if old != ui_state.focus
@@ -304,8 +297,11 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
_ => (), _ => (),
} }
state.window_event(event, rsc, render); state.window_event(event, rsc, render);
self.request_redraw_if_needed(); let ui_state = self.state.default_state_mut();
self.state.default_state_mut().input.end_frame(); if render.needs_redraw(&ui_state.root, rsc.widgets()) {
ui_state.renderer.window().request_redraw();
}
ui_state.input.end_frame();
} }
fn exit(&mut self) { 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> { pub trait RscIdx<Rsc> {
type Output; type Output;
fn get(self, rsc: &Rsc) -> &Self::Output; fn get(self, rsc: &Rsc) -> &Self::Output;
+5
View File
@@ -24,6 +24,8 @@ impl UiRenderer {
pub fn draw(&mut self) { pub fn draw(&mut self) {
let output = match self.surface.get_current_texture() { let output = match self.surface.get_current_texture() {
CurrentSurfaceTexture::Success(texture) => texture, CurrentSurfaceTexture::Success(texture) => texture,
// Still drawable; the surface has just changed under us, and
// reconfiguring is what the next frame wants rather than this one.
CurrentSurfaceTexture::Suboptimal(texture) => { CurrentSurfaceTexture::Suboptimal(texture) => {
self.surface.configure(&self.device, &self.config); self.surface.configure(&self.device, &self.config);
texture texture
@@ -32,6 +34,7 @@ impl UiRenderer {
self.surface.configure(&self.device, &self.config); self.surface.configure(&self.device, &self.config);
return; return;
} }
// Nothing to draw into this frame.
CurrentSurfaceTexture::Timeout CurrentSurfaceTexture::Timeout
| CurrentSurfaceTexture::Occluded | CurrentSurfaceTexture::Occluded
| CurrentSurfaceTexture::Validation => return, | CurrentSurfaceTexture::Validation => return,
@@ -78,6 +81,8 @@ impl UiRenderer {
pub fn new(window: Arc<Window>) -> Self { pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size(); let size = window.inner_size();
// The display handle is what GLES needs to present on Wayland, and it
// has to be the one the surface is made from.
let instance = Instance::new(InstanceDescriptor { let instance = Instance::new(InstanceDescriptor {
backends: Backends::PRIMARY, backends: Backends::PRIMARY,
display: Some(Box::new(window.clone())), display: Some(Box::new(window.clone())),
+35 -103
View File
@@ -4,14 +4,14 @@ use std::{
rc::Rc, rc::Rc,
}; };
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
pub enum CursorButton { pub enum CursorButton {
Left, Left,
Right, Right,
Middle, Middle,
} }
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
pub enum CursorSense { pub enum CursorSense {
PressStart(CursorButton), PressStart(CursorButton),
Pressing(CursorButton), Pressing(CursorButton),
@@ -27,7 +27,7 @@ pub struct CursorSenses(Vec<CursorSense>);
impl Event for CursorSenses { impl Event for CursorSenses {
type Data<'a> = CursorData<'a>; type Data<'a> = CursorData<'a>;
type Global = Hovered; type State = SensorState;
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> { fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
if let Some(sense) = should_run(self, &data.cursor, data.hover) { if let Some(sense) = should_run(self, &data.cursor, data.hover) {
let mut data = data.clone(); let mut data = data.clone();
@@ -37,24 +37,6 @@ impl Event for CursorSenses {
None None
} }
} }
/// A press or a scroll is used up by whatever answered it, so it stops
/// there. Hovering is not: a cursor resting somewhere goes on resting.
fn consumes(&self, data: &Self::Data<'_>) -> bool {
!data.sense.position_only()
}
}
/// Who the cursor was inside, before and after an input. The difference is
/// whose hover has ended -- including a widget a higher layer has covered,
/// which the walk stops before reaching.
///
/// Two buffers that swap rather than one rebuilt, so an input allocates
/// nothing once they have grown.
#[derive(Default)]
pub struct Hovered {
was: Vec<WidgetId>,
now: Vec<WidgetId>,
} }
impl CursorSense { impl CursorSense {
@@ -70,12 +52,6 @@ impl CursorSense {
pub fn is_dragging(&self) -> bool { pub fn is_dragging(&self) -> bool {
matches!(self, CursorSense::Pressing(CursorButton::Left)) matches!(self, CursorSense::Pressing(CursorButton::Left))
} }
/// False if the sense is a button or a scroll, true if it is only about
/// where the cursor is.
fn position_only(&self) -> bool {
matches!(self, Self::HoverStart | Self::Hovering | Self::HoverEnd)
}
} }
#[derive(Default, Clone)] #[derive(Default, Clone)]
@@ -120,12 +96,6 @@ impl CursorButtons {
} }
impl CursorState { impl CursorState {
/// True if the cursor is only reporting where it is: no button and no
/// scroll this frame.
pub fn position_only(&self) -> bool {
self.scroll_delta == Vec2::ZERO && self.buttons.iter().all(|(_, state)| state.is_off())
}
pub fn end_frame(&mut self) { pub fn end_frame(&mut self) {
self.buttons.end_frame(); self.buttons.end_frame();
self.scroll_delta = Vec2::ZERO; self.scroll_delta = Vec2::ZERO;
@@ -153,6 +123,11 @@ pub struct Sensor<Ctx: HasEvents, Data> {
pub type SenseShape = UiRegion; pub type SenseShape = UiRegion;
#[derive(Default, Debug)]
pub struct SensorState {
pub hover: ActivationState,
}
#[derive(Clone)] #[derive(Clone)]
pub struct CursorData<'a> { pub struct CursorData<'a> {
/// where this widget was hit /// where this widget was hit
@@ -172,6 +147,7 @@ pub trait SensorUi {
rsc: &mut Rsc, rsc: &mut Rsc,
state: &mut Rsc::State, state: &mut Rsc::State,
cursor: CursorState, cursor: CursorState,
window_size: Vec2,
); );
} }
@@ -181,98 +157,54 @@ impl SensorUi for UiRenderState {
rsc: &mut Rsc, rsc: &mut Rsc,
state: &mut Rsc::State, state: &mut Rsc::State,
cursor: CursorState, cursor: CursorState,
window_size: Vec2,
) { ) {
// in order to remove this take, need to store active list in UiRenderState somehow // in order to remove this take, need to store active list in UiRenderState somehow
// this would probably be done through a generic parameter that adds yet another rsc / // this would probably be done through a generic parameter that adds yet another rsc /
// state like thing, but local to render state, and is passed to UiRsc events so you can // state like thing, but local to render state, and is passed to UiRsc events so you can
// update it there? // update it there?
let active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active); let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
let mut hovered = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().global);
hovered.now.clear();
let position_only = cursor.position_only();
let region_of = |id| self.window_region(&id);
for layer in self.layers.indices().rev() { for layer in self.layers.indices().rev() {
let mut consumed = false; let mut sensed = false;
for id in active.get(&layer).into_flat_iter().map(|(id, _)| *id) { for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
let Some(region) = region_of(id) else { let shape = self.active.get(id).unwrap().region;
continue; let region = shape.to_px(window_size);
}; let in_shape = cursor.exists && region.contains(cursor.pos);
if !cursor.exists || !region.contains(cursor.pos) { sensor.hover.update(in_shape);
if sensor.hover == ActivationState::Off {
continue; continue;
} }
hovered.now.push(id); sensed = true;
let hover = match hovered.was.contains(&id) {
true => ActivationState::On, let cursor = cursor.clone();
false => ActivationState::Start,
let data = CursorData {
pos: cursor.pos - region.top_left,
size: region.bot_right - region.top_left,
scroll_delta: cursor.scroll_delta,
hover: sensor.hover,
cursor,
// this does not have any meaning;
// might wanna set up Event to have a prepare stage
sense: CursorSense::Hovering,
render: self,
}; };
// A press or a scroll stops where something answered it, so a rsc.run_event::<CursorSense>(*id, data, state);
// button over a list does not swallow the list's scrolling.
consumed |= deliver(self, rsc, state, id, hover, &cursor, region);
// A cursor doing neither stops at whatever it is over, so
// hovering does not reach through.
consumed |= position_only;
} }
// Applied after the layer, never during it: senses on one layer do if sensed {
// not block each other.
if consumed {
break; break;
} }
} }
rsc.events_mut().get_type::<CursorSense>().active = active;
// Whatever the cursor was inside and is not now, whether it left or a
// layer above took the input before the walk reached it. A widget that
// stopped being drawn has no region to report and is simply dropped.
for &id in &hovered.was {
if !hovered.now.contains(&id)
&& let Some(region) = region_of(id)
{
deliver(self, rsc, state, id, ActivationState::End, &cursor, region);
}
}
std::mem::swap(&mut hovered.was, &mut hovered.now);
let senses = rsc.events_mut().get_type::<CursorSense>();
senses.active = active;
senses.global = hovered;
} }
} }
/// Runs one widget's cursor senses, and says whether they used up the input.
fn deliver<Rsc: HasEvents>(
render: &UiRenderState,
rsc: &mut Rsc,
state: &mut Rsc::State,
id: WidgetId,
hover: ActivationState,
cursor: &CursorState,
region: PixelRegion,
) -> bool {
let data = CursorData {
pos: cursor.pos - region.top_left,
size: region.bot_right - region.top_left,
scroll_delta: cursor.scroll_delta,
hover,
cursor: cursor.clone(),
// this does not have any meaning;
// might wanna set up Event to have a prepare stage
sense: CursorSense::Hovering,
render,
};
rsc.run_event::<CursorSense>(id, data, state)
}
pub fn should_run( pub fn should_run(
senses: &CursorSenses, senses: &CursorSenses,
cursor: &CursorState, cursor: &CursorState,
hover: ActivationState, hover: ActivationState,
) -> Option<CursorSense> { ) -> Option<CursorSense> {
for sense in senses.iter() { for sense in senses.iter() {
// A widget the cursor is no longer inside senses only its position:
// the press that ended its hover landed on something else.
if !hover.is_on() && !sense.position_only() {
continue;
}
if match sense { if match sense {
CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(), CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(),
CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(), CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(),
+34 -16
View File
@@ -1,5 +1,11 @@
use iris_core::HasState; use iris_core::HasState;
use std::{pin::Pin, sync::Arc}; use std::{
pin::Pin,
sync::{
Arc,
mpsc::{Receiver as SyncReceiver, Sender as SyncSender, channel as sync_channel},
},
};
use tokio::{ use tokio::{
runtime::Runtime, runtime::Runtime,
sync::mpsc::{ sync::mpsc::{
@@ -7,52 +13,64 @@ use tokio::{
unbounded_channel as async_channel, 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 {} pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {}
impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {} impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {}
/// Hands an update from a task to the thread that owns the ui. Delivery and
/// waking are one act: a host posts the update as a message its loop already
/// carries, so nothing has to wake the loop separately, or claim a redraw to
/// be looked at.
pub trait TaskQueue<Rsc: HasState>: Send + Sync + 'static {
fn send(&self, update: Box<dyn TaskUpdate<Rsc>>);
}
pub struct Tasks<Rsc: HasState> { pub struct Tasks<Rsc: HasState> {
start: AsyncSender<BoxTask>, start: AsyncSender<BoxTask>,
queue: Arc<dyn TaskQueue<Rsc>>, window: Arc<Window>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
} }
pub struct TaskCtx<Rsc: HasState> { pub struct TaskCtx<Rsc: HasState> {
queue: Arc<dyn TaskQueue<Rsc>>, send: TaskMsgSender<Rsc>,
} }
impl<Rsc: HasState> TaskCtx<Rsc> { impl<Rsc: HasState> TaskCtx<Rsc> {
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) { pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
self.queue.send(Box::new(f)); let _ = self.send.send(Box::new(f));
}
}
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
fn new(send: TaskMsgSender<Rsc>) -> Self {
Self { send }
} }
} }
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>; type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<Rsc: HasState> Tasks<Rsc> { impl<Rsc: HasState> Tasks<Rsc> {
pub fn init(queue: Arc<dyn TaskQueue<Rsc>>) -> Self { pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) {
let (start, start_recv) = async_channel(); let (start, start_recv) = async_channel();
let (msgs, msgs_recv) = sync_channel();
std::thread::spawn(|| { std::thread::spawn(|| {
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(listen(start_recv)) rt.block_on(listen(start_recv))
}); });
Self { start, queue } (
Self {
start,
msg_send: msgs,
window,
},
msgs_recv,
)
} }
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F) pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
where where
F::CallOnceFuture: Send, F::CallOnceFuture: Send,
{ {
let queue = self.queue.clone(); let send = self.msg_send.clone();
let window = self.window.clone();
let _ = self.start.send(Box::pin(async move { let _ = self.start.send(Box::pin(async move {
task(TaskCtx { queue }).await; task(TaskCtx::new(send)).await;
window.request_redraw();
})); }));
} }
} }
-180
View File
@@ -1,180 +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;
#[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);
}
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();
}
}
-1
View File
@@ -7,7 +7,6 @@
pub mod default; pub mod default;
pub mod event; pub mod event;
pub mod harness;
pub mod widget; pub mod widget;
pub use iris_core as core; pub use iris_core as core;
+2 -2
View File
@@ -4,7 +4,7 @@ mod max_size;
mod offset; mod offset;
mod pad; mod pad;
mod scroll; mod scroll;
mod set_size; mod sized;
mod span; mod span;
mod stack; mod stack;
@@ -14,6 +14,6 @@ pub use max_size::*;
pub use offset::*; pub use offset::*;
pub use pad::*; pub use pad::*;
pub use scroll::*; pub use scroll::*;
pub use set_size::*; pub use sized::*;
pub use span::*; pub use span::*;
pub use stack::*; pub use stack::*;
@@ -1,12 +1,12 @@
use crate::prelude::*; use crate::prelude::*;
pub struct SetSize { pub struct Sized {
pub inner: StrongWidget, pub inner: StrongWidget,
pub x: Option<Len>, pub x: Option<Len>,
pub y: Option<Len>, pub y: Option<Len>,
} }
impl SetSize { impl Sized {
fn apply_to_outer(&self, ctx: &mut SizeCtx) { fn apply_to_outer(&self, ctx: &mut SizeCtx) {
if let Some(x) = self.x { if let Some(x) = self.x {
ctx.outer.x.select_len(x.apply_rest()); ctx.outer.x.select_len(x.apply_rest());
@@ -17,7 +17,7 @@ impl SetSize {
} }
} }
impl Widget for SetSize { impl Widget for Sized {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.inner); painter.widget(&self.inner);
} }
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::Unsize; use std::marker::{Sized, Unsize};
pub struct WidgetPtr { pub struct WidgetPtr {
pub inner: Option<StrongWidget>, pub inner: Option<StrongWidget>,
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::PhantomData; use std::marker::{PhantomData, Sized};
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> { pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
pub content: String, pub content: String,
+6 -6
View File
@@ -31,9 +31,9 @@ widget_trait! {
} }
} }
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> { fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, Sized> {
let size = size.into(); let size = size.into();
move |state| SetSize { move |state| Sized {
inner: self.add_strong(state), inner: self.add_strong(state),
x: Some(size.x), x: Some(size.x),
y: Some(size.y), y: Some(size.y),
@@ -58,18 +58,18 @@ widget_trait! {
} }
} }
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> { fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into(); let len = len.into();
move |state| SetSize { move |state| Sized {
inner: self.add_strong(state), inner: self.add_strong(state),
x: Some(len), x: Some(len),
y: None, y: None,
} }
} }
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> { fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into(); let len = len.into();
move |state| SetSize { move |state| Sized {
inner: self.add_strong(state), inner: self.add_strong(state),
x: None, x: None,
y: Some(len), y: Some(len),
+14 -13
View File
@@ -4,19 +4,20 @@
//! //!
//! cargo test --release --test draw_cost -- --ignored --nocapture //! cargo test --release --test draw_cost -- --ignored --nocapture
//! //!
//! Wall time is the wrong number to read for anything under a few percent -- //! **Read the instruction count, not the clock.** Wall time here swings by 2x
//! it varied by 2x between runs of one unchanged binary where instructions //! between runs of one binary on this machine -- more under `cargo test` than
//! retired varied by 0.1%. Count those instead: //! run directly -- while instructions retired are stable to 0.1%:
//! //!
//! perf stat -e instructions:u target/release/.../draw_cost-* --ignored //! perf stat -e instructions:u target/release/.../draw_cost-* --ignored
//! //!
//! That is how `PrimitiveRender` was measured against a match in the renderer: //! Measured that way on 2026-09-13, drawing each primitive through its own
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording //! `PrimitiveRender` rather than a match in the renderer costs **6
//! one. //! 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 //! The instance is leaked on purpose. Dropping the last one makes the Vulkan
//! when the last one drops, which can fault as a thread that used it exits -- //! loader unload Mesa's ICD, which faults when a thread that touched Vulkan
//! and every test runs on a spawned thread. //! exits -- and libtest runs every test on a spawned thread.
use std::time::Instant; use std::time::Instant;
@@ -29,13 +30,13 @@ use wgpu::{Color as GpuColor, *};
const SIZE: u32 = 1024; const SIZE: u32 = 1024;
const FRAMES: u32 = 200; const FRAMES: u32 = 200;
/// Reported as the best of this many batches, since the mean moves by more /// Reported as the best of this many batches. The mean moves by 15% between
/// than the thing being measured. /// runs on this machine, which is more than the thing being measured.
const BATCHES: u32 = 8; const BATCHES: u32 = 8;
fn gpu() -> Option<(Device, Queue)> { fn gpu() -> Option<(Device, Queue)> {
// Probed rather than assumed: there may be no Vulkan adapter, and GL is // Probed rather than assumed: this machine's Vulkan device comes and goes,
// what is left when there is not. // and GL is what is left when it is gone.
let all = Instance::new(InstanceDescriptor::new_without_display_handle()); let all = Instance::new(InstanceDescriptor::new_without_display_handle());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default())) let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{ {
-34
View File
@@ -1,34 +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));
}
-60
View File
@@ -1,60 +0,0 @@
//! Which widget an input reaches.
use std::{cell::RefCell, rc::Rc};
use iris::harness::Harness;
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));
}
-225
View File
@@ -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"
);
}
-26
View File
@@ -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));
}
-23
View File
@@ -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);
}