Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dc7614ae6 | ||
|
|
e865467a3f | ||
|
|
23376aef25 | ||
|
|
36fec09d11 | ||
|
|
5494642dec | ||
|
|
8cac927438 | ||
|
|
827d317f41 | ||
|
|
e53ce585e6 | ||
|
|
f3fd9417d4 | ||
|
|
3ab9c922fd | ||
|
|
71ba3723ff | ||
|
|
0a14df2cc3 | ||
|
|
0e7076a01c | ||
|
|
f62131eecf | ||
|
|
028521b419 |
No files matched your search
@@ -79,6 +79,7 @@ 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>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +108,7 @@ 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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,11 +140,13 @@ 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) + 'a {
|
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) -> bool + '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,
|
||||||
@@ -152,6 +156,7 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
consumed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,10 +9,19 @@ pub use rsc::*;
|
|||||||
pub trait Event: Sized + 'static + Clone {
|
pub trait Event: Sized + 'static + Clone {
|
||||||
type Data<'a>: Clone = ();
|
type Data<'a>: Clone = ();
|
||||||
type State: Default = ();
|
type State: Default = ();
|
||||||
|
/// State the whole event type keeps, rather than one copy per widget.
|
||||||
|
type Global: Default = ();
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
||||||
Some(data.clone())
|
Some(data.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether having run on this data uses up whatever triggered it, so
|
||||||
|
/// nothing further should see it.
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn consumes(&self, data: &Self::Data<'_>) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait EventLike {
|
pub trait EventLike {
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ 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
-2
@@ -235,8 +235,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
ui_state.focus = None;
|
ui_state.focus = None;
|
||||||
}
|
}
|
||||||
if input_changed {
|
if input_changed {
|
||||||
let window_size = ui_state.window_size();
|
render.run_sensors(rsc, state, cursor_state);
|
||||||
render.run_sensors(rsc, state, cursor_state, window_size);
|
|
||||||
}
|
}
|
||||||
let ui_state = state.default_state_mut();
|
let ui_state = state.default_state_mut();
|
||||||
if old != ui_state.focus
|
if old != ui_state.focus
|
||||||
|
|||||||
+99
-31
@@ -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),
|
||||||
@@ -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 State = SensorState;
|
type Global = Hovered;
|
||||||
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,6 +37,24 @@ 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 {
|
||||||
@@ -52,6 +70,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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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)]
|
||||||
@@ -96,6 +120,12 @@ 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;
|
||||||
@@ -123,11 +153,6 @@ 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
|
||||||
@@ -147,7 +172,6 @@ pub trait SensorUi {
|
|||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
state: &mut Rsc::State,
|
state: &mut Rsc::State,
|
||||||
cursor: CursorState,
|
cursor: CursorState,
|
||||||
window_size: Vec2,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,46 +181,85 @@ 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 mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
|
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);
|
||||||
|
|
||||||
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 in active.get(&layer).into_flat_iter().map(|(id, _)| *id) {
|
||||||
let shape = self.active.get(id).unwrap().region;
|
let Some(region) = region_of(id) else {
|
||||||
let region = shape.to_px(window_size);
|
continue;
|
||||||
let in_shape = cursor.exists && region.contains(cursor.pos);
|
};
|
||||||
sensor.hover.update(in_shape);
|
if !cursor.exists || !region.contains(cursor.pos) {
|
||||||
if sensor.hover == ActivationState::Off {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
sensed = true;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let cursor = cursor.clone();
|
// 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 {
|
let data = CursorData {
|
||||||
pos: cursor.pos - region.top_left,
|
pos: cursor.pos - region.top_left,
|
||||||
size: region.bot_right - region.top_left,
|
size: region.bot_right - region.top_left,
|
||||||
scroll_delta: cursor.scroll_delta,
|
scroll_delta: cursor.scroll_delta,
|
||||||
hover: sensor.hover,
|
hover,
|
||||||
cursor,
|
cursor: cursor.clone(),
|
||||||
// this does not have any meaning;
|
// this does not have any meaning;
|
||||||
// might wanna set up Event to have a prepare stage
|
// might wanna set up Event to have a prepare stage
|
||||||
sense: CursorSense::Hovering,
|
sense: CursorSense::Hovering,
|
||||||
render: self,
|
render,
|
||||||
};
|
};
|
||||||
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(
|
pub fn should_run(
|
||||||
@@ -205,6 +268,11 @@ pub fn should_run(
|
|||||||
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(),
|
||||||
|
|||||||
+1
-2
@@ -173,9 +173,8 @@ impl Harness {
|
|||||||
/// window delivers input against too.
|
/// window delivers input against too.
|
||||||
fn sense(&mut self) {
|
fn sense(&mut self) {
|
||||||
let cursor = self.cursor.clone();
|
let cursor = self.cursor.clone();
|
||||||
let size = self.render.output_size();
|
|
||||||
self.render
|
self.render
|
||||||
.run_sensors(&mut self.rsc, &mut self.state, cursor, size);
|
.run_sensors(&mut self.rsc, &mut self.state, cursor);
|
||||||
self.cursor.end_frame();
|
self.cursor.end_frame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
//! 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user