Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3fd9417d4 | ||
|
|
3ab9c922fd | ||
|
|
71ba3723ff | ||
|
|
0a14df2cc3 | ||
|
|
0e7076a01c | ||
|
|
f62131eecf | ||
|
|
028521b419 |
No files matched your search
@@ -135,6 +135,11 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What this widget registered, without running any of it.
|
||||||
|
pub fn registered(&self, id: WidgetId) -> impl Iterator<Item = &E> {
|
||||||
|
self.map.get(&id).into_iter().flatten().map(|(e, _)| e)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run_fn<'a>(
|
pub fn run_fn<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: impl IdLike,
|
id: impl IdLike,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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| {
|
||||||
|
|||||||
+42
-60
@@ -25,35 +25,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 +66,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 +101,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 +177,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);
|
||||||
@@ -305,8 +293,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) {
|
||||||
@@ -314,15 +305,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;
|
||||||
|
|||||||
+45
-7
@@ -4,14 +4,14 @@ use std::{
|
|||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum CursorButton {
|
pub enum CursorButton {
|
||||||
Left,
|
Left,
|
||||||
Right,
|
Right,
|
||||||
Middle,
|
Middle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum CursorSense {
|
pub enum CursorSense {
|
||||||
PressStart(CursorButton),
|
PressStart(CursorButton),
|
||||||
Pressing(CursorButton),
|
Pressing(CursorButton),
|
||||||
@@ -39,6 +39,19 @@ impl Event for CursorSenses {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CursorSenses {
|
||||||
|
/// Whether a widget with these senses stops the input reaching the layer
|
||||||
|
/// below it. Where the cursor rests stops at the top layer that is under
|
||||||
|
/// it; something happening to the cursor stops only at a widget that
|
||||||
|
/// answers to that, so a click-only child does not swallow a scroll.
|
||||||
|
fn consumes(&self, cursor: &CursorState, hover: ActivationState, momentary: bool) -> bool {
|
||||||
|
if !momentary {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
should_run(self, cursor, hover).is_some_and(|sense| sense.is_momentary())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl CursorSense {
|
impl CursorSense {
|
||||||
pub fn click() -> Self {
|
pub fn click() -> Self {
|
||||||
Self::PressStart(CursorButton::Left)
|
Self::PressStart(CursorButton::Left)
|
||||||
@@ -52,6 +65,12 @@ 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this sense is about something happening to the cursor, rather
|
||||||
|
/// than about where it rests.
|
||||||
|
fn is_momentary(&self) -> bool {
|
||||||
|
!matches!(self, Self::HoverStart | Self::Hovering | Self::HoverEnd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
@@ -96,6 +115,11 @@ impl CursorButtons {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CursorState {
|
impl CursorState {
|
||||||
|
/// Whether anything is happening to the cursor beyond where it rests.
|
||||||
|
pub fn has_momentary_input(&self) -> bool {
|
||||||
|
self.scroll_delta != Vec2::ZERO || self.buttons.iter().any(|(_, 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;
|
||||||
@@ -164,8 +188,9 @@ impl SensorUi for UiRenderState {
|
|||||||
// 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 mut 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 momentary = cursor.has_momentary_input();
|
||||||
for layer in self.layers.indices().rev() {
|
for layer in self.layers.indices().rev() {
|
||||||
let mut sensed = false;
|
let mut consumed = false;
|
||||||
for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
|
for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
|
||||||
let shape = self.active.get(id).unwrap().region;
|
let shape = self.active.get(id).unwrap().region;
|
||||||
let region = shape.to_px(window_size);
|
let region = shape.to_px(window_size);
|
||||||
@@ -174,9 +199,22 @@ impl SensorUi for UiRenderState {
|
|||||||
if sensor.hover == ActivationState::Off {
|
if sensor.hover == ActivationState::Off {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
sensed = true;
|
// A widget the cursor has left still hears its hover ending,
|
||||||
|
// but a press or a scroll landing elsewhere is not its input.
|
||||||
let cursor = cursor.clone();
|
let cursor = match in_shape {
|
||||||
|
true => cursor.clone(),
|
||||||
|
false => CursorState {
|
||||||
|
pos: cursor.pos,
|
||||||
|
exists: cursor.exists,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
consumed = consumed
|
||||||
|
|| rsc
|
||||||
|
.events_mut()
|
||||||
|
.get_type::<CursorSense>()
|
||||||
|
.registered(*id)
|
||||||
|
.any(|senses| senses.consumes(&cursor, sensor.hover, momentary));
|
||||||
|
|
||||||
let data = CursorData {
|
let data = CursorData {
|
||||||
pos: cursor.pos - region.top_left,
|
pos: cursor.pos - region.top_left,
|
||||||
@@ -191,7 +229,7 @@ impl SensorUi for UiRenderState {
|
|||||||
};
|
};
|
||||||
rsc.run_event::<CursorSense>(*id, data, state);
|
rsc.run_event::<CursorSense>(*id, data, state);
|
||||||
}
|
}
|
||||||
if sensed {
|
if consumed {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-16
@@ -1,5 +1,11 @@
|
|||||||
use iris_core::HasState;
|
use iris_core::HasState;
|
||||||
use std::{pin::Pin, sync::Arc};
|
use std::{
|
||||||
|
pin::Pin,
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
mpsc::{Receiver as SyncReceiver, Sender as SyncSender, channel as sync_channel},
|
||||||
|
},
|
||||||
|
};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
runtime::Runtime,
|
runtime::Runtime,
|
||||||
sync::mpsc::{
|
sync::mpsc::{
|
||||||
@@ -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();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-181
@@ -1,181 +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();
|
|
||||||
let size = self.render.output_size();
|
|
||||||
self.render
|
|
||||||
.run_sensors(&mut self.rsc, &mut self.state, cursor, size);
|
|
||||||
self.cursor.end_frame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
//! Input across layers: what stops at a layer, what passes through it, and
|
||||||
|
//! where hovering stops. These drive `run_sensors` directly, which needs no
|
||||||
|
//! GPU and no window.
|
||||||
|
|
||||||
|
use iris::prelude::*;
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
struct SenseRsc {
|
||||||
|
ui: UiData,
|
||||||
|
events: EventManager<SenseRsc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiRsc for SenseRsc {
|
||||||
|
fn ui(&self) -> &UiData {
|
||||||
|
&self.ui
|
||||||
|
}
|
||||||
|
fn ui_mut(&mut self) -> &mut UiData {
|
||||||
|
&mut self.ui
|
||||||
|
}
|
||||||
|
fn on_draw(&mut self, active: &ActiveData) {
|
||||||
|
self.events.draw(active);
|
||||||
|
}
|
||||||
|
fn on_undraw(&mut self, active: &ActiveData) {
|
||||||
|
self.events.undraw(active);
|
||||||
|
}
|
||||||
|
fn on_remove(&mut self, id: WidgetId) {
|
||||||
|
self.events.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasState for SenseRsc {
|
||||||
|
type State = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasEvents for SenseRsc {
|
||||||
|
fn events(&self) -> &EventManager<Self> {
|
||||||
|
&self.events
|
||||||
|
}
|
||||||
|
fn events_mut(&mut self) -> &mut EventManager<Self> {
|
||||||
|
&mut self.events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Ui {
|
||||||
|
rsc: SenseRsc,
|
||||||
|
render: UiRenderState,
|
||||||
|
state: (),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ui {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
rsc: SenseRsc {
|
||||||
|
ui: UiData::default(),
|
||||||
|
events: EventManager::default(),
|
||||||
|
},
|
||||||
|
render: UiRenderState::new(),
|
||||||
|
state: (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn listen<W: Widget + ?core::marker::Sized + 'static>(
|
||||||
|
&mut self,
|
||||||
|
widget: &StrongWidget<W>,
|
||||||
|
senses: impl Into<CursorSenses>,
|
||||||
|
) -> Fired {
|
||||||
|
let fired = Fired::default();
|
||||||
|
let sink = fired.clone();
|
||||||
|
self.rsc
|
||||||
|
.register_event(widget.weak(), senses.into(), move |ctx, _rsc| {
|
||||||
|
sink.0.borrow_mut().push(ctx.data.sense)
|
||||||
|
});
|
||||||
|
fired
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stacks the widgets bottom first, each on its own layer, and lays them
|
||||||
|
/// out in a square window.
|
||||||
|
fn stack(&mut self, children: Vec<StrongWidget>) {
|
||||||
|
let root = self
|
||||||
|
.rsc
|
||||||
|
.ui
|
||||||
|
.widgets
|
||||||
|
.add_strong(Stack {
|
||||||
|
children,
|
||||||
|
size: StackSize::default(),
|
||||||
|
})
|
||||||
|
.any();
|
||||||
|
self.render.resize((WINDOW, WINDOW));
|
||||||
|
self.render.update(&root, &mut self.rsc);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor(&mut self, at: (f32, f32)) -> CursorState {
|
||||||
|
CursorState {
|
||||||
|
pos: at.into(),
|
||||||
|
exists: true,
|
||||||
|
buttons: Default::default(),
|
||||||
|
scroll_delta: Vec2::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&mut self, cursor: CursorState) {
|
||||||
|
self.render.run_sensors(
|
||||||
|
&mut self.rsc,
|
||||||
|
&mut self.state,
|
||||||
|
cursor,
|
||||||
|
(WINDOW, WINDOW).into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn full(ui: &mut Ui) -> StrongWidget<Rect> {
|
||||||
|
rect(UiColor::WHITE).add_strong(&mut ui.rsc)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_stops_at_the_topmost_widget() {
|
||||||
|
let mut ui = Ui::new();
|
||||||
|
let (bottom, middle, top) = (full(&mut ui), full(&mut ui), full(&mut ui));
|
||||||
|
let bottom_hover = ui.listen(&bottom, CursorSense::HoverStart);
|
||||||
|
let middle_hover = ui.listen(&middle, CursorSense::HoverStart);
|
||||||
|
let top_hover = ui.listen(&top, CursorSense::HoverStart);
|
||||||
|
ui.stack(vec![bottom.any(), middle.any(), top.any()]);
|
||||||
|
|
||||||
|
let cursor = ui.cursor((50.0, 50.0));
|
||||||
|
ui.run(cursor);
|
||||||
|
|
||||||
|
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 ui = Ui::new();
|
||||||
|
let (list, button, overlay) = (full(&mut ui), full(&mut ui), full(&mut ui));
|
||||||
|
let scrolled = ui.listen(&list, CursorSense::Scroll);
|
||||||
|
let clicked = ui.listen(&button, CursorSense::click());
|
||||||
|
let overlay_clicked = ui.listen(&overlay, CursorSense::click());
|
||||||
|
ui.stack(vec![list.any(), button.any(), overlay.any()]);
|
||||||
|
|
||||||
|
let mut cursor = ui.cursor((50.0, 50.0));
|
||||||
|
cursor.scroll_delta = (0.0, 10.0).into();
|
||||||
|
ui.run(cursor);
|
||||||
|
|
||||||
|
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 only_the_topmost_listener_takes_a_press() {
|
||||||
|
let mut ui = Ui::new();
|
||||||
|
let (below, above) = (full(&mut ui), full(&mut ui));
|
||||||
|
let below_clicked = ui.listen(&below, CursorSense::click());
|
||||||
|
let above_clicked = ui.listen(&above, CursorSense::click());
|
||||||
|
ui.stack(vec![below.any(), above.any()]);
|
||||||
|
|
||||||
|
let mut cursor = ui.cursor((50.0, 50.0));
|
||||||
|
cursor.buttons.left = ActivationState::Start;
|
||||||
|
ui.run(cursor);
|
||||||
|
|
||||||
|
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 ui = Ui::new();
|
||||||
|
let list = full(&mut ui);
|
||||||
|
// The row above the list covers it, but only its left half is the button.
|
||||||
|
let button = full(&mut ui);
|
||||||
|
let gap = full(&mut ui);
|
||||||
|
let list_clicked = ui.listen(&list, CursorSense::click());
|
||||||
|
let button_clicked = ui.listen(&button, CursorSense::click());
|
||||||
|
let row = ui.rsc.ui.widgets.add_strong(Span {
|
||||||
|
children: vec![button.any(), gap.any()],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: 0.0,
|
||||||
|
});
|
||||||
|
ui.stack(vec![list.any(), row.any()]);
|
||||||
|
|
||||||
|
let mut on_button = ui.cursor((20.0, 50.0));
|
||||||
|
on_button.buttons.left = ActivationState::Start;
|
||||||
|
ui.run(on_button);
|
||||||
|
assert_eq!(button_clicked.take(), [CursorSense::click()]);
|
||||||
|
assert_eq!(list_clicked.take(), []);
|
||||||
|
|
||||||
|
let mut beside_it = ui.cursor((80.0, 50.0));
|
||||||
|
beside_it.buttons.left = ActivationState::Start;
|
||||||
|
ui.run(beside_it);
|
||||||
|
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 ui = Ui::new();
|
||||||
|
let widget = full(&mut ui);
|
||||||
|
let hover = ui.listen(&widget, CursorSense::HoverStart | CursorSense::HoverEnd);
|
||||||
|
ui.stack(vec![widget.any()]);
|
||||||
|
|
||||||
|
let cursor = ui.cursor((50.0, 50.0));
|
||||||
|
ui.run(cursor);
|
||||||
|
assert_eq!(hover.take(), [CursorSense::HoverStart]);
|
||||||
|
|
||||||
|
let mut gone = ui.cursor((50.0, 50.0));
|
||||||
|
gone.exists = false;
|
||||||
|
ui.run(gone);
|
||||||
|
assert_eq!(hover.take(), [CursorSense::HoverEnd]);
|
||||||
|
}
|
||||||
@@ -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