diff --git a/src/events/global_input.rs b/src/events/global_input.rs new file mode 100644 index 000000000..19543a215 --- /dev/null +++ b/src/events/global_input.rs @@ -0,0 +1,158 @@ +//! Handles all of the global input events and state. +//! The core of this module is the `GlobalInput` struct. It is responsible for aggregating +//! and interpreting raw input events into high-level semantic events. + +use events::{InputState, UiEvent, MouseClick, MouseDrag, Scroll, InputProvider}; +use input::MouseButton; +use position::{Point, Scalar}; +use widget::Index; + +/// Global input event handler that also implements `InputProvider`. The `Ui` passes all events +/// to it's `GlobalInput` instance, which aggregates and interprets the events to provide +/// so-called 'high-level' events to widgets. This input gets reset after every update by the `Ui`. +pub struct GlobalInput { + /// The `InputState` as it was at the end of the last update cycle. + pub start_state: InputState, + /// The most recent `InputState`, with updates from handling all the events + /// this update cycle + pub current_state: InputState, + events: Vec, + drag_threshold: Scalar, +} + +/// Iterator over global `UiEvent`s. Unlike the `WidgetInputEventIterator`, this will +/// never filter out any events, and all coordinates will be reative to the (0,0) origin +/// of the window. +pub type GlobalInputEventIterator<'a> = ::std::slice::Iter<'a, UiEvent>; + +impl <'a> InputProvider<'a, GlobalInputEventIterator<'a>> for GlobalInput { + fn all_events(&'a self) -> GlobalInputEventIterator { + self.events.iter() + } + + fn current_state(&'a self) -> &'a InputState { + &self.current_state + } + + fn mouse_button_down(&self, button: MouseButton) -> Option { + self.current_state().mouse_buttons.get(button).map(|_| { + self.mouse_position() + }) + } +} + +impl GlobalInput { + + /// Returns a fresh new `GlobalInput` + pub fn new(drag_threshold: Scalar) -> GlobalInput { + GlobalInput{ + events: Vec::new(), + drag_threshold: drag_threshold, + start_state: InputState::new(), + current_state: InputState::new(), + } + } + + /// Adds a new event and updates the internal state. + pub fn push_event(&mut self, event: UiEvent) { + use input::Input::{Release, Move}; + use input::Motion::MouseRelative; + use input::Motion::MouseScroll; + use input::Button::Mouse; + + let maybe_new_event = match event { + UiEvent::Raw(Release(Mouse(button))) => self.handle_mouse_release(button), + UiEvent::Raw(Move(MouseRelative(x, y))) => self.handle_mouse_move([x, y]), + UiEvent::Raw(Move(MouseScroll(x, y))) => self.mouse_scroll(x, y), + _ => None + }; + + self.current_state.update(&event); + self.events.push(event); + if let Some(new_event) = maybe_new_event { + self.push_event(new_event); + } + } + + /// Called at the end of every update cycle in order to prepare the `GlobalInput` to + /// handle events for the next one. + pub fn reset(&mut self) { + self.events.clear(); + self.start_state = self.current_state.clone(); + } + + /// Returns the most up to date position of the mouse + pub fn mouse_position(&self) -> Point { + self.current_state.mouse_position + } + + /// Returns the input state as it was after the last update + pub fn starting_state(&self) -> &InputState { + &self.start_state + } + + /// Returns the most up to date info on which widget is capturing the mouse + pub fn currently_capturing_mouse(&self) -> Option { + self.current_state.widget_capturing_mouse + } + + /// Returns the most up to date info on which widget is capturing the keyboard + pub fn currently_capturing_keyboard(&self) -> Option { + self.current_state.widget_capturing_keyboard + } + + + fn mouse_scroll(&self, x: f64, y: f64) -> Option { + Some(UiEvent::Scroll(Scroll{ + x: x, + y: y, + modifiers: self.current_state.modifiers + })) + } + + fn handle_mouse_move(&self, move_to: Point) -> Option { + self.current_state.mouse_buttons.pressed_button().and_then(|btn_and_point| { + if self.is_drag(btn_and_point.1, move_to) { + Some(UiEvent::MouseDrag(MouseDrag{ + button: btn_and_point.0, + start: btn_and_point.1, + end: move_to, + in_progress: true, + modifier: self.current_state.modifiers + })) + } else { + None + } + }) + } + + fn handle_mouse_release(&self, button: MouseButton) -> Option { + self.current_state.mouse_buttons.get(button).map(|point| { + if self.is_drag(point, self.current_state.mouse_position) { + UiEvent::MouseDrag(MouseDrag{ + button: button, + start: point, + end: self.current_state.mouse_position, + modifier: self.current_state.modifiers, + in_progress: false + }) + } else { + UiEvent::MouseClick(MouseClick { + button: button, + location: point, + modifier: self.current_state.modifiers + }) + } + }) + } + + fn is_drag(&self, a: Point, b: Point) -> bool { + distance_between(a, b) > self.drag_threshold + } +} + +fn distance_between(a: Point, b: Point) -> Scalar { + let dx_2 = (a[0] - b[0]).powi(2); + let dy_2 = (a[1] - b[1]).powi(2); + (dx_2 + dy_2).abs().sqrt() +} diff --git a/src/events/input_provider.rs b/src/events/input_provider.rs new file mode 100644 index 000000000..e14c21221 --- /dev/null +++ b/src/events/input_provider.rs @@ -0,0 +1,254 @@ +//! Contains the `InputProvider` trait, which is used to provide input events to widgets. + +use events::{UiEvent, Scroll, MouseClick, MouseDrag, InputState}; +use input::{Input, Button}; +use input::keyboard::Key; +use input::mouse::MouseButton; +use position::Point; + + +/// Trait for something that provides events to be consumed by a widget. +/// Provides a bunch of convenience methods for filtering out specific types of events. +pub trait InputProvider<'a, T: Iterator> { + /// This is the only method that needs to be implemented. + /// Just provided a reference to a `Vec` that contains + /// all the events for this update cycle. + fn all_events(&'a self) -> T; + + /// Returns the current input state. The returned state is assumed to be up to + /// date with all of the events so far. + fn current_state(&'a self) -> &'a InputState; + + /// If the given mouse button is currently pressed, returns the current position of the mouse. + /// Otherwise, returns `None` + fn mouse_button_down(&'a self, button: MouseButton) -> Option; + + ////////////////////////////////////////////////// + // Methods that just check the stream of events // + ////////////////////////////////////////////////// + + /// Returns a `String` containing _all_ the text that was entered since + /// the last update cycle. + fn text_just_entered(&'a self) -> Option { + let all_text: String = self.all_events().filter_map(|evt| { + match *evt { + UiEvent::Raw(Input::Text(ref text)) => Some(text), + _ => None + } + }).fold(String::new(), |acc, item| { + acc + item + }); + + if all_text.is_empty() { + None + } else { + Some(all_text) + } + } + + /// Returns all of the `Key`s that were released since the last update. + fn keys_just_released(&'a self) -> KeysJustReleased<'a, T> { + KeysJustReleased{ + event_iter: self.all_events(), + lifetime: ::std::marker::PhantomData + } + } + + /// Returns all of the keyboard `Key`s that were pressed since the last update. + fn keys_just_pressed(&'a self) -> KeysJustPressed<'a, T> { + KeysJustPressed { + event_iter: self.all_events(), + lifetime: ::std::marker::PhantomData + } + } + + /// Returns all of the `MouseButton`s that were pressed since the last update. + fn mouse_buttons_just_pressed(&'a self) -> MouseButtonsJustPressed<'a, T> { + MouseButtonsJustPressed { + event_iter: self.all_events(), + lifetime: ::std::marker::PhantomData + } + } + + /// Returns all of the `MouseButton`s that were released since the last update. + fn mouse_buttons_just_released(&'a self) -> MouseButtonsJustReleased<'a, T> { + MouseButtonsJustReleased { + event_iter: self.all_events(), + lifetime: ::std::marker::PhantomData + } + } + + /// Returns a `Scroll` struct if any scrolling was done since the last update. + /// If multiple raw scroll events occured since the last update (which could very well + /// happen if the user is scrolling quickly), then the `Scroll` returned will represent an + /// aggregate total of all the scrolling. + fn scroll(&'a self) -> Option { + self.all_events().filter_map(|evt| { + match *evt { + UiEvent::Scroll(scroll) => Some(scroll), + _ => None + } + }).fold(None, |maybe_scroll, scroll| { + if maybe_scroll.is_some() { + maybe_scroll.map(|acc| { + Scroll{ + x: acc.x + scroll.x, + y: acc.y + scroll.y, + modifiers: scroll.modifiers + } + }) + } else { + Some(scroll) + } + }) + } + + /// Convenience method to call `mouse_drag`, passing in `MouseButton::Left`. + /// Saves widgets from having to `use input::mouse::MouseButton` if all they care + /// about is the left mouse button. + fn mouse_left_drag(&'a self) -> Option { + self.mouse_drag(MouseButton::Left) + } + + /// Returns a `MouseDrag` if one has occured involving the given mouse button. + /// If multiple raw mouse movement events have + /// occured since the last update (which will happen if the user moves the mouse quickly), + /// then the returned `MouseDrag` will be only the _most recent_ one, which will contain + /// the most recent mouse position. + fn mouse_drag(&'a self, button: MouseButton) -> Option { + self.all_events().filter_map(|evt| { + match *evt { + UiEvent::MouseDrag(drag_evt) if drag_evt.button == button => Some(drag_evt), + _ => None + } + }).last() + } + + /// Convenience method to call `mouse_click`, passing in passing in `MouseButton::Left`. + /// Saves widgets from having to `use input::mouse::MouseButton` if all they care + /// about is the left mouse button. + fn mouse_left_click(&'a self) -> Option { + self.mouse_click(MouseButton::Left) + } + + /// Convenience method to call `mouse_click`, passing in passing in `MouseButton::Right`. + /// Saves widgets from having to `use input::mouse::MouseButton` if all they care + /// about is the left mouse button. + fn mouse_right_click(&'a self) -> Option { + self.mouse_click(MouseButton::Right) + } + + /// Returns a `MouseClick` if one has occured with the given mouse button. + /// A _click_ is determined to have occured if a mouse button was pressed and subsequently + /// released while the mouse was in roughly the same place. + fn mouse_click(&'a self, button: MouseButton) -> Option { + self.all_events().filter_map(|evt| { + match *evt { + UiEvent::MouseClick(click) if click.button == button => Some(click), + _ => None + } + }).next() + } + + ///////////////////////////////////////////////////// + // Methods that just check the current input state // + ///////////////////////////////////////////////////// + + /// Convenience method for checking if the Left mouse button is down. + /// Returns mouse position if the Left mouse button is currently pressed, otherwise `None`. + fn mouse_left_button_down(&'a self) -> Option { + self.mouse_button_down(MouseButton::Left) + } + + /// Convenience method for checking if the Right mouse button is down. + /// Returns mouse position if the Right mouse button is currently pressed, otherwise `None`. + fn mouse_right_button_down(&'a self) -> Option { + self.mouse_button_down(MouseButton::Right) + } + + /// Convenience method for returning the current mouse position. + fn mouse_position(&'a self) -> Point { + self.current_state().mouse_position + } + +} + +/// An Iterator over `input::keyboard::Key`s that were just released. +#[derive(Debug)] +pub struct KeysJustReleased<'a, T: Iterator + Sized> { + event_iter: T, + lifetime: ::std::marker::PhantomData<&'a ()> +} + +impl<'a, T> Iterator for KeysJustReleased<'a, T> where T: Iterator + Sized { + type Item = Key; + + fn next(&mut self) -> Option { + while let Some(event) = self.event_iter.next() { + if let UiEvent::Raw(Input::Release(Button::Keyboard(key))) = *event { + return Some(key); + } + } + None + } +} + +/// An Iterator over `input::keyboard::Key`s that were just pressed. +#[derive(Debug)] +pub struct KeysJustPressed<'a, T: Iterator + Sized> { + event_iter: T, + lifetime: ::std::marker::PhantomData<&'a ()> +} + +impl<'a, T> Iterator for KeysJustPressed<'a, T> where T: Iterator + Sized { + type Item = Key; + + fn next(&mut self) -> Option { + while let Some(event) = self.event_iter.next() { + if let UiEvent::Raw(Input::Press(Button::Keyboard(key))) = *event { + return Some(key); + } + } + None + } +} + +/// An Iterator over `input::mouse::MouseButton`s that were just pressed. +#[derive(Debug)] +pub struct MouseButtonsJustPressed<'a, T: Iterator + Sized> { + event_iter: T, + lifetime: ::std::marker::PhantomData<&'a ()> +} + +impl<'a, T> Iterator for MouseButtonsJustPressed<'a, T> where T: Iterator + Sized { + type Item = MouseButton; + + fn next(&mut self) -> Option { + while let Some(event) = self.event_iter.next() { + if let UiEvent::Raw(Input::Press(Button::Mouse(mouse_button))) = *event { + return Some(mouse_button); + } + } + None + } +} + +/// An Iterator over `input::mouse::MouseButton`s that were just released. +#[derive(Debug)] +pub struct MouseButtonsJustReleased<'a, T: Iterator + Sized> { + event_iter: T, + lifetime: ::std::marker::PhantomData<&'a ()> +} + +impl<'a, T> Iterator for MouseButtonsJustReleased<'a, T> where T: Iterator + Sized { + type Item = MouseButton; + + fn next(&mut self) -> Option { + while let Some(event) = self.event_iter.next() { + if let UiEvent::Raw(Input::Release(Button::Mouse(mouse_button))) = *event { + return Some(mouse_button); + } + } + None + } +} diff --git a/src/events/input_state.rs b/src/events/input_state.rs new file mode 100644 index 000000000..2c25ecd73 --- /dev/null +++ b/src/events/input_state.rs @@ -0,0 +1,217 @@ +//! Everything related to storing the state of user input. This includes the state of any +//! buttons on either the keyboard or the mouse, as well as the position of the mouse. +//! It also includes which widgets, if any, are capturing the keyboard and mouse. +//! This module exists mostly to support the `events::InputProvider` trait. + +use input::MouseButton; +use input::keyboard::{NO_MODIFIER, ModifierKey, Key}; +use position::Point; +use widget::Index; +use events::UiEvent; + +/// The max total number of buttons on a mouse. +pub const NUM_MOUSE_BUTTONS: usize = 9; + +/// Describes the position of the mouse when the button was pressed. Will be +/// `None` if the mouse button is currently in the up position. +pub type ButtonDownPosition = Option; + +/// Holds the current state of user input. This includes the state of all buttons on +/// the keyboard and mouse, as well as the position of the mouse. It also includes which +/// widgets, if any, are capturing keyboard and mouse input. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct InputState { + /// A map that stores the up/down state of each button. If the button is down, then + /// it stores the position of the mouse when the button was first pressed. + pub mouse_buttons: ButtonMap, + /// The current position of the mouse. + pub mouse_position: Point, + /// Which widget, if any, is currently capturing the keyboard + pub widget_capturing_keyboard: Option, + /// Which widget, if any, is currently capturing the mouse + pub widget_capturing_mouse: Option, + /// Which modifier keys are being held down. + pub modifiers: ModifierKey, +} + +impl InputState { + /// Returns a fresh new input state + pub fn new() -> InputState { + InputState{ + mouse_buttons: ButtonMap::new(), + mouse_position: [0.0, 0.0], + widget_capturing_keyboard: None, + widget_capturing_mouse: None, + modifiers: NO_MODIFIER, + } + } + + /// Updates the input state based on an event. + pub fn update(&mut self, event: &UiEvent) { + use input::{Button, Motion, Input}; + + match *event { + UiEvent::Raw(Input::Press(Button::Mouse(mouse_button))) => { + self.mouse_buttons.set(mouse_button, Some(self.mouse_position)); + }, + UiEvent::Raw(Input::Release(Button::Mouse(mouse_button))) => { + self.mouse_buttons.set(mouse_button, None); + }, + UiEvent::Raw(Input::Move(Motion::MouseRelative(x, y))) => { + self.mouse_position = [x, y]; + }, + UiEvent::Raw(Input::Press(Button::Keyboard(key))) => { + get_modifier(key).map(|modifier| self.modifiers.insert(modifier)); + }, + UiEvent::Raw(Input::Release(Button::Keyboard(key))) => { + get_modifier(key).map(|modifier| self.modifiers.remove(modifier)); + }, + UiEvent::WidgetCapturesKeyboard(idx) => { + self.widget_capturing_keyboard = Some(idx); + }, + UiEvent::WidgetUncapturesKeyboard(_) => { + self.widget_capturing_keyboard = None; + }, + UiEvent::WidgetCapturesMouse(idx) => { + self.widget_capturing_mouse = Some(idx); + }, + UiEvent::WidgetUncapturesMouse(_) => { + self.widget_capturing_mouse = None; + }, + _ => {} + } + } + + /// Returns a copy of the InputState relative to the given `position::Point` + pub fn relative_to(&self, xy: Point) -> InputState { + InputState { + mouse_position: ::vecmath::vec2_sub(self.mouse_position, xy), + mouse_buttons: self.mouse_buttons.relative_to(xy), + ..*self + } + } +} + +fn get_modifier(key: Key) -> Option { + use input::keyboard::{CTRL, SHIFT, ALT, GUI}; + + match key { + Key::LCtrl | Key::RCtrl => Some(CTRL), + Key::LShift | Key::RShift => Some(SHIFT), + Key::LAlt | Key::RAlt => Some(ALT), + Key::LGui | Key::RGui => Some(GUI), + _ => None + } +} + +/// Stores the state of all mouse buttons. If the mouse button is down, +/// it stores the position of the mouse when the button was pressed +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct ButtonMap { + button_states: [ButtonDownPosition; NUM_MOUSE_BUTTONS] +} + +impl ButtonMap { + /// Returns a new button map with all states set to `None` + pub fn new() -> ButtonMap { + ButtonMap{ + button_states: [None; NUM_MOUSE_BUTTONS] + } + } + + /// Sets the state of a specific `MouseButton` + pub fn set(&mut self, button: MouseButton, point: ButtonDownPosition) { + let idx = ButtonMap::button_idx(button); + self.button_states[idx] = point; + } + + /// Returns the state of a mouse button + pub fn get(&self, button: MouseButton) -> ButtonDownPosition { + self.button_states[ButtonMap::button_idx(button)] + } + + /// Returns the current state of a mouse button, leaving `None` in its place + pub fn take(&mut self, button: MouseButton) -> ButtonDownPosition { + self.button_states[ButtonMap::button_idx(button)].take() + } + + /// If any mouse buttons are currently pressed, will return a tuple containing + /// both the `MouseButton` that is pressed and the `Point` describing the location of the + /// mouse when it was pressed. + pub fn pressed_button(&self) -> Option<(MouseButton, Point)> { + self.button_states.iter().enumerate().filter(|idx_and_state| idx_and_state.1.is_some()) + .map(|idx_and_state| + (ButtonMap::idx_to_button(idx_and_state.0), idx_and_state.1.unwrap())) + .next() + } + + /// Returns a copy of the ButtonMap relative to the given `Point` + pub fn relative_to(&self, xy: Point) -> ButtonMap { + let mut relative_buttons = ButtonMap::new(); + for (idx, state) in self.button_states.iter().enumerate() { + relative_buttons.button_states[idx] = state.map(|position| { + [position[0] - xy[0], position[1] - xy[1]] + }); + } + relative_buttons + } + + fn idx_to_button(idx: usize) -> MouseButton { + MouseButton::from(idx as u32) + } + fn button_idx(button: MouseButton) -> usize { + u32::from(button) as usize + } + +} + + + +#[test] +fn pressed_button_returns_none_if_no_buttons_are_pressed() { + let map = ButtonMap::new(); + let pressed = map.pressed_button(); + assert!(pressed.is_none()); +} + +#[test] +fn pressed_button_should_return_first_pressed_button() { + let mut map = ButtonMap::new(); + + map.set(MouseButton::Right, Some([3.0, 3.0])); + map.set(MouseButton::X1, Some([5.4, 4.5])); + + let pressed = map.pressed_button(); + assert_eq!(Some((MouseButton::Right, [3.0, 3.0])), pressed); +} + +#[test] +fn button_down_should_store_the_point() { + let mut map = ButtonMap::new(); + let point: ButtonDownPosition = Some([2.0, 5.0]); + map.set(MouseButton::Left, point); + + assert_eq!(point, map.get(MouseButton::Left)); +} + +#[test] +fn take_resets_and_returns_current_state() { + let mut map = ButtonMap::new(); + let point: ButtonDownPosition = Some([2.0, 5.0]); + map.set(MouseButton::Left, point); + + let taken = map.take(MouseButton::Left); + assert_eq!(point, taken); + assert!(map.get(MouseButton::Left).is_none()); +} + +#[test] +fn input_state_should_be_made_relative_to_a_given_point() { + let mut state = InputState::new(); + state.mouse_position = [50.0, -10.0]; + state.mouse_buttons.set(MouseButton::Middle, Some([-20.0, -10.0])); + + let relative_state = state.relative_to([20.0, 20.0]); + assert_eq!([30.0, -30.0], relative_state.mouse_position); + assert_eq!(Some([-40.0, -30.0]), relative_state.mouse_buttons.get(MouseButton::Middle)); +} diff --git a/src/events/mod.rs b/src/events/mod.rs new file mode 100644 index 000000000..0317cbe48 --- /dev/null +++ b/src/events/mod.rs @@ -0,0 +1,30 @@ +//! This module contains all the logic for handling input events and providing them to widgets. +//! All user input is provided to the `Ui` in the form of `input::Input` events, which are continuously +//! polled from the backend window implementation. These raw input events tent to be fairly low level. +//! The `Ui` passes each of these events off to it's `GlobalInput`, which keeps track of the state of +//! affairs for the entire `Ui`. `GlobalInput` will also aggregate the low level events into higher +//! level ones. For instance, two events indicating that a mouse button was pressed then released +//! would cause a new `UiEvent::MouseClick` to be generated. This saves individual widgets from +//! having to interpret these themselves, thus freeing them from also having to store input state. +//! +//! Whenever there's an update, all of the events that have occured since the last update will be +//! available for widgets to process. This is where the `InputProvider` trait comes in. The +//! `InputProvider` trait provides many methods for conveniently filtering events that a widget would +//! like to handle. There are two things that implement this trait. The first is `GlobalInput`, and +//! the second is `WidgetInput`. `WidgetInput` is used to provide input events to a specific widget. +//! It filters events that don't apply to the widget, and all events provided by `WidgetIput` will +//! have all coordinates in the widget's own local coordinate system. `GlobalInput`, on the other hand, +//! will never filter out any events, and will always provide them with coordinates relative to the +//! window. + +pub mod ui_event; +pub mod input_state; +pub mod widget_input; +pub mod global_input; +pub mod input_provider; + +pub use self::input_state::{InputState, ButtonMap}; +pub use self::global_input::{GlobalInputEventIterator, GlobalInput}; +pub use self::widget_input::{WidgetInputEventIterator, WidgetInput}; +pub use self::ui_event::{UiEvent, MouseClick, MouseDrag, Scroll}; +pub use self::input_provider::InputProvider; diff --git a/src/events/ui_event.rs b/src/events/ui_event.rs new file mode 100644 index 000000000..f01e9ab9b --- /dev/null +++ b/src/events/ui_event.rs @@ -0,0 +1,309 @@ +//! Contains all the structs and enums to describe all of the input events that `Widget`s +//! can handle. The core of this module is the `UiEvent` enum, which encapsulates all +//! of those events. + +use input::{Input, MouseButton, Motion, Button}; +use input::keyboard::ModifierKey; +use position::Point; +use vecmath::vec2_sub; +use widget::Index; + +/// Enum containing all the events that `Widget`s can listen for. +#[derive(Clone, PartialEq, Debug)] +pub enum UiEvent { + /// Represents a raw `input::Input` event + Raw(Input), + /// Represents a mouse button being pressed and subsequently released while the + /// mouse stayed in roughly the same place. + MouseClick(MouseClick), + /// Represents a mouse button being pressed and a subsequent movement of the mouse. + MouseDrag(MouseDrag), + /// This is a generic scroll event. This is different from the `input::Movement::MouseScroll` + /// event in several aspects. For one, it does not necessarily have to get created by a + /// mouse wheel, it could be generated from a keypress, or as a response to handling some + /// other event. Secondly, it contains a field holding the `input::keyboard::ModifierKey` + /// that was held while the scroll occured. + Scroll(Scroll), + /// Indicates that the given widget is starting to capture the mouse. + WidgetCapturesMouse(Index), + /// Indicates that the given widget is losing mouse capture. + WidgetUncapturesMouse(Index), + /// Indicates that the given widget is starting to capture the keyboard. + WidgetCapturesKeyboard(Index), + /// Indicates that the given widget is losing keyboard capture. + WidgetUncapturesKeyboard(Index), +} + +/// Contains all the relevant information for a mouse drag. +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct MouseDrag { + /// Which mouse button was being held during the drag + pub button: MouseButton, + /// The origin of the drag. This will always be the position of the mouse whenever the + /// button was first pressed + pub start: Point, + /// The end position of the mouse. If `in_progress` is true, then subsequent `MouseDrag` + /// events may be created with a new `end` as the mouse continues to move. + pub end: Point, + /// Which modifier keys are being held during the mouse drag. + pub modifier: ModifierKey, + /// Indicates whether the mouse button is still being held down. If it is, then + /// `in_progress` will be `true` and more `MouseDrag` events can likely be expected. + pub in_progress: bool, +} + +/// Contains all the relevant information for a mouse click. +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct MouseClick { + /// Which mouse button was clicked + pub button: MouseButton, + /// The location of the click + pub location: Point, + /// Which modifier keys, if any, that were being held down when the user clicked + pub modifier: ModifierKey, +} + +/// Holds all the relevant information about a scroll event +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct Scroll { + /// The amount of scroll along the x axis. + pub x: f64, + /// The amount of scroll along the y axis. + pub y: f64, + /// Which modifier keys, if any, that were being held down while the scroll occured + pub modifiers: ModifierKey, +} + +impl MouseClick { + /// Returns a copy of the MouseClick relative to the given `position::Point` + pub fn relative_to(&self, xy: Point) -> MouseClick { + MouseClick{ + location: vec2_sub(self.location, xy), + ..*self + } + } +} + +impl MouseDrag { + /// Returns a copy of the MouseDrag relative to the given `position::Point` + pub fn relative_to(&self, xy: Point) -> MouseDrag { + MouseDrag{ + start: vec2_sub(self.start, xy), + end: vec2_sub(self.end, xy), + ..*self + } + } +} + +impl UiEvent { + /// Returns a copy of the UiEvent relative to the given `position::Point` + pub fn relative_to(&self, xy: Point) -> Self { + use self::UiEvent::{MouseClick, MouseDrag, Raw}; + match *self { + MouseClick(click) => MouseClick(click.relative_to(xy)), + MouseDrag(drag) => MouseDrag(drag.relative_to(xy)), + Raw(ref raw_input) => { + Raw(match *raw_input { + Input::Move(Motion::MouseRelative(x, y)) => + Input::Move(Motion::MouseRelative(x - xy[0], y - xy[1])), + Input::Move(Motion::MouseCursor(x, y)) => + Input::Move(Motion::MouseCursor(x - xy[0], y - xy[1])), + ref other_input => other_input.clone() + }) + }, + ref other_event => other_event.clone() + } + } + + /// Returns `true` if this event is related to the mouse. Note that just because this method + /// returns true does not mean that the event necessarily came from the mouse. + /// A `UiEvent::Scroll` is considered to be both a mouse and a keyboard event. + pub fn is_mouse_event(&self) -> bool { + match *self { + UiEvent::Raw(Input::Press(Button::Mouse(_))) => true, + UiEvent::Raw(Input::Release(Button::Mouse(_))) => true, + UiEvent::Raw(Input::Move(Motion::MouseCursor(_, _))) => true, + UiEvent::Raw(Input::Move(Motion::MouseRelative(_, _))) => true, + UiEvent::Raw(Input::Move(Motion::MouseScroll(_, _))) => true, + UiEvent::MouseClick(_) => true, + UiEvent::MouseDrag(_) => true, + UiEvent::Scroll(_) => true, + _ => false + } + } + + /// Returns `true` if this event is related to the keyboard. Note that just because this method + /// returns true does not mean that the event necessarily came from the keyboard. + /// A `UiEvent::Scroll` is considered to be both a mouse and a keyboard event. + pub fn is_keyboard_event(&self) -> bool { + match *self { + UiEvent::Raw(Input::Press(Button::Keyboard(_))) => true, + UiEvent::Raw(Input::Release(Button::Keyboard(_))) => true, + UiEvent::Raw(Input::Text(_)) => true, + UiEvent::Scroll(_) => true, + _ => false + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use input::{Input, MouseButton, Motion, Button, JoystickAxisArgs}; + use input::keyboard::{self, Key, NO_MODIFIER}; + + // We'll see if this approach causes problems later on down the road... + #[test] + fn scroll_event_shoulbe_be_both_a_mouse_and_keyboard_event() { + let scroll_event = UiEvent::Scroll(Scroll{ + x: 0.0, + y: 0.0, + modifiers: NO_MODIFIER + }); + assert!(scroll_event.is_mouse_event()); + assert!(scroll_event.is_keyboard_event()); + } + + #[test] + fn is_keyboard_event_should_be_true_for_all_keyboard_events() { + let keyboard_events = vec![ + UiEvent::Raw(Input::Press(Button::Keyboard(Key::L))), + UiEvent::Raw(Input::Release(Button::Keyboard(Key::L))), + UiEvent::Raw(Input::Text("wha?".to_string())), + ]; + for event in keyboard_events { + assert!(event.is_keyboard_event(), format!("{:?} should be a keyboard event", event)); + } + + let non_keyboard_events = vec![ + UiEvent::Raw(Input::Press(Button::Mouse(MouseButton::Left))), + UiEvent::Raw(Input::Release(Button::Mouse(MouseButton::Left))), + UiEvent::MouseClick(MouseClick{ + button: MouseButton::Left, + location: [0.0, 0.0], + modifier: NO_MODIFIER + }), + UiEvent::MouseDrag(MouseDrag{ + button: MouseButton::Left, + start: [0.0, 0.0], + end: [0.0, 0.0], + modifier: NO_MODIFIER, + in_progress: true, + }), + UiEvent::Raw(Input::Move(Motion::MouseCursor(2.0, 3.0))), + UiEvent::Raw(Input::Move(Motion::MouseRelative(2.0, 3.0))), + UiEvent::Raw(Input::Move(Motion::MouseScroll(3.5, 6.5))), + ]; + + for event in non_keyboard_events { + assert!(!event.is_keyboard_event(), format!("{:?} should not be a keyboard event", event)); + } + } + + #[test] + fn is_mouse_event_should_be_true_for_all_mouse_events() { + let mouse_events = vec![ + UiEvent::Raw(Input::Press(Button::Mouse(MouseButton::Left))), + UiEvent::Raw(Input::Release(Button::Mouse(MouseButton::Left))), + UiEvent::MouseClick(MouseClick{ + button: MouseButton::Left, + location: [0.0, 0.0], + modifier: NO_MODIFIER + }), + UiEvent::MouseDrag(MouseDrag{ + button: MouseButton::Left, + start: [0.0, 0.0], + end: [0.0, 0.0], + modifier: NO_MODIFIER, + in_progress: true, + }), + UiEvent::Raw(Input::Move(Motion::MouseCursor(2.0, 3.0))), + UiEvent::Raw(Input::Move(Motion::MouseRelative(2.0, 3.0))), + UiEvent::Raw(Input::Move(Motion::MouseScroll(3.5, 6.5))), + ]; + for event in mouse_events { + assert!(event.is_mouse_event(), format!("{:?}.is_mouse_event() == false", event)); + } + + let non_mouse_events = vec![ + UiEvent::Raw(Input::Press(Button::Keyboard(Key::G))), + UiEvent::Raw(Input::Release(Button::Keyboard(Key::G))), + UiEvent::Raw(Input::Move(Motion::JoystickAxis(JoystickAxisArgs{ + id: 0, + axis: 0, + position: 0f64 + }))), + UiEvent::Raw(Input::Text("rust is brown".to_string())), + UiEvent::Raw(Input::Resize(0, 0)), + UiEvent::Raw(Input::Focus(true)), + UiEvent::Raw(Input::Cursor(true)), + ]; + for event in non_mouse_events { + assert!(!event.is_mouse_event(), format!("{:?}.is_mouse_event() == true", event)); + } + } + + #[test] + fn mouse_click_should_be_made_relative() { + let original = UiEvent::MouseClick(MouseClick{ + button: MouseButton::Middle, + location: [30.0, -80.0], + modifier: keyboard::SHIFT + }); + let relative = original.relative_to([10.0, 20.0]); + + if let UiEvent::MouseClick(click) = relative { + assert_eq!([20.0, -100.0], click.location); + assert_eq!(MouseButton::Middle, click.button); + assert_eq!(keyboard::SHIFT, click.modifier); + } else { + panic!("expected a mouse click"); + } + } + + #[test] + fn mouse_drage_should_be_made_relative() { + let original = UiEvent::MouseDrag(MouseDrag{ + start: [20.0, 5.0], + end: [50.0, 1.0], + button: MouseButton::Left, + modifier: keyboard::CTRL, + in_progress: false + }); + + let relative = original.relative_to([-5.0, 5.0]); + if let UiEvent::MouseDrag(drag) = relative { + assert_eq!([25.0, 0.0], drag.start); + assert_eq!([55.0, -4.0], drag.end); + assert_eq!(MouseButton::Left, drag.button); + assert_eq!(keyboard::CTRL, drag.modifier); + assert!(!drag.in_progress); + } else { + panic!("expected to get a drag event"); + } + } + + #[test] + fn mouse_cursor_should_be_made_relative() { + let original = UiEvent::Raw(Input::Move(Motion::MouseCursor(-44.0, 55.0))); + let relative = original.relative_to([4.0, 5.0]); + if let UiEvent::Raw(Input::Move(Motion::MouseCursor(x, y))) = relative { + assert_eq!(-48.0, x); + assert_eq!(50.0, y); + } else { + panic!("expected a mouse move event"); + } + } + + #[test] + fn mouse_relative_motion_should_be_made_relative() { + let original = UiEvent::Raw(Input::Move(Motion::MouseRelative(-2.0, -4.0))); + let relative = original.relative_to([3.0, 3.0]); + if let UiEvent::Raw(Input::Move(Motion::MouseRelative(x, y))) = relative { + assert_eq!(-5.0, x); + assert_eq!(-7.0, y); + } else { + panic!("expected a mouse relative motion event"); + } + } +} diff --git a/src/events/widget_input.rs b/src/events/widget_input.rs new file mode 100644 index 000000000..6e87b0b17 --- /dev/null +++ b/src/events/widget_input.rs @@ -0,0 +1,172 @@ +//! Contains all the logic for filtering input events and making them relative to widgets. +//! The core of this module is the `WidgetInput::for_widget` method, which creates an +//! `InputProvider` that provides input events for a specific widget. + +use widget::Index; +use events::{InputState, + UiEvent, + GlobalInput, + GlobalInputEventIterator, + InputProvider, + MouseClick, + MouseDrag, +}; +use position::{Point, Rect}; +use input::mouse::MouseButton; + +/// Holds any events meant to be given to a `Widget`. This is what widgets will interface with +/// when handling events in their `update` method. All events returned from methods on `WidgetInput` +/// will be relative to the widget's own (0,0) origin. Additionally, `WidgetInput` will not provide +/// mouse or keyboard events that do not directly pertain to the widget. +pub struct WidgetInput<'a> { + global_input: &'a GlobalInput, + current_state: InputState, + widget_area: Rect, + widget_idx: Index, +} + +impl<'a> WidgetInput<'a> { + /// Returns a `WidgetInput` with events specifically for the given widget. + /// Filters out only the events that directly pertain to the widget. + /// All events will also be made relative to the widget's own (0,0) origin. + pub fn for_widget<'g>(widget: Index, widget_area: Rect, global_input: &'g GlobalInput) -> WidgetInput<'g> { + WidgetInput { + global_input: &global_input, + widget_area: widget_area, + widget_idx: widget, + current_state: global_input.current_state.relative_to(widget_area.xy()) + } + } + + /// Returns true if the mouse is currently over the widget, otherwise false + pub fn mouse_is_over_widget(&self) -> bool { + self.point_is_over(self.mouse_position()) + } + + /// If the mouse is over the widget and no other widget is capturing the mouse, then + /// this will return the position of the mouse relative to the widget. Otherwise, it + /// will return `None` + pub fn maybe_mouse_position(&self) -> Option { + if self.mouse_is_over_widget() { + Some(self.mouse_position()) + } else { + None + } + } + + fn point_is_over(&self, point: Point) -> bool { + self.widget_relative_rect().is_over(point) + } + + fn widget_relative_rect(&self) -> Rect { + let widget_dim = self.widget_area.dim(); + Rect::from_xy_dim([0.0, 0.0], widget_dim) + } +} + +/// Alows iterating over events for a specific widget. All events provided by this Iterator +/// will be filtered, so that input intended for other widgets is excluded. In addition, +/// all mouse events will have their coordinates relative to the widget's own (0,0) origin. +pub struct WidgetInputEventIterator<'a> { + global_event_iter: GlobalInputEventIterator<'a>, + current_state: InputState, + widget_area: Rect, + widget_idx: Index, +} + +impl<'a> Iterator for WidgetInputEventIterator<'a> { + type Item = &'a UiEvent; + + fn next(&mut self) -> Option<&'a UiEvent> { + self.global_event_iter.next().and_then(|event| { + self.current_state.update(event); + if should_provide_event(self.widget_idx, self.widget_area, event, &self.current_state) { + Some(event) + } else { + self.next() + } + }) + } + +} + + +impl<'a> InputProvider<'a, WidgetInputEventIterator<'a>> for WidgetInput<'a> { + + fn all_events(&'a self) -> WidgetInputEventIterator<'a> { + WidgetInputEventIterator{ + global_event_iter: self.global_input.all_events(), + current_state: self.global_input.start_state.relative_to(self.widget_area.xy()), + widget_area: self.widget_area, + widget_idx: self.widget_idx, + } + } + + fn current_state(&'a self) -> &'a InputState { + &self.current_state + } + + fn mouse_click(&'a self, button: MouseButton) -> Option { + self.all_events().filter_map(|event| { + match *event { + UiEvent::MouseClick(click) if click.button == button => { + Some(click.relative_to(self.widget_area.xy())) + }, + _ => None + } + }).next() + } + + fn mouse_drag(&'a self, button: MouseButton) -> Option { + self.all_events().filter_map(|evt| { + match *evt { + UiEvent::MouseDrag(drag_evt) if drag_evt.button == button => { + Some(drag_evt.relative_to(self.widget_area.xy())) + }, + _ => None + } + }).last() + } + + fn mouse_button_down(&self, button: MouseButton) -> Option { + self.current_state().mouse_buttons.get(button).iter().filter(|_| { + self.current_state().widget_capturing_mouse.map(|capturing| { + capturing == self.widget_idx + }).unwrap_or_else(|| self.mouse_is_over_widget()) + }).map(|pt| *pt).next() + } +} + +fn should_provide_event(widget: Index, + widget_area: Rect, + event: &UiEvent, + current_state: &InputState) -> bool { + let is_keyboard = event.is_keyboard_event(); + let is_mouse = event.is_mouse_event(); + + (is_keyboard && current_state.widget_capturing_keyboard == Some(widget)) + || (is_mouse && should_provide_mouse_event(widget, widget_area, event, current_state)) + || (!is_keyboard && !is_mouse) +} + +fn should_provide_mouse_event(widget: Index, + widget_area: Rect, + event: &UiEvent, + current_state: &InputState) -> bool { + let capturing_mouse = current_state.widget_capturing_mouse; + match capturing_mouse { + Some(idx) if idx == widget => true, + None => mouse_event_is_over_widget(widget_area, event, current_state), + _ => false + } +} + +fn mouse_event_is_over_widget(widget_area: Rect, event: &UiEvent, current_state: &InputState) -> bool { + match *event { + UiEvent::MouseClick(click) => widget_area.is_over(click.location), + UiEvent::MouseDrag(drag) => { + widget_area.is_over(drag.start) || widget_area.is_over(drag.end) + }, + _ => widget_area.is_over(current_state.mouse_position) + } +} diff --git a/src/lib.rs b/src/lib.rs index 730c96198..8803df029 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -86,6 +86,8 @@ pub use widget::Index as WidgetIndex; pub use widget::Kind as WidgetKind; pub use widget::State as WidgetState; +pub mod events; + pub mod backend; mod background; @@ -102,6 +104,9 @@ mod ui; pub mod utils; mod widget; +#[cfg(test)] +mod tests; + @@ -110,10 +115,10 @@ mod widget; /// This is the recommended way of generating `WidgetId`s as it greatly lessens the chances of /// making errors when adding or removing widget ids. /// -/// Each Widget must have its own unique identifier so that the `Ui` can keep track of its state +/// Each Widget must have its own unique identifier so that the `Ui` can keep track of its state /// between updates. /// -/// To make this easier, we provide the `widget_ids` macro, which generates a unique `WidgetId` for +/// To make this easier, we provide the `widget_ids` macro, which generates a unique `WidgetId` for /// each identifier given in the list. /// /// The `with n` syntax reserves `n` number of `WidgetId`s for that identifier rather than just one. diff --git a/src/tests/global_input.rs b/src/tests/global_input.rs new file mode 100644 index 000000000..dc4cbaca3 --- /dev/null +++ b/src/tests/global_input.rs @@ -0,0 +1,294 @@ +use input::Button::Keyboard; +use input::keyboard::{self, ModifierKey, Key}; +use input::Button::Mouse; +use input::mouse::MouseButton; +use input::{Input, Motion}; +use position::Scalar; +use events::{UiEvent, MouseClick, MouseDrag, Scroll, InputProvider, GlobalInput}; +use widget::{Id, Index}; + +#[test] +fn resetting_input_should_set_starting_state_to_current_state() { + let mut input = GlobalInput::new(0.0); + input.push_event(UiEvent::Raw(Input::Press(Keyboard(Key::LShift)))); + input.push_event(UiEvent::Raw(Input::Move(Motion::MouseScroll(0.0, 50.0)))); + + let expected_start_state = input.current_state.clone(); + input.reset(); + assert_eq!(expected_start_state, input.start_state); +} + +#[test] +fn resetting_input_should_clear_out_all_events() { + let mut input = GlobalInput::new(0.0); + input.push_event(UiEvent::Raw(Input::Press(Keyboard(Key::LShift)))); + input.push_event(UiEvent::Raw(Input::Move(Motion::MouseScroll(0.0, 50.0)))); + input.reset(); + assert!(input.all_events().next().is_none()); +} + +#[test] +fn scroll_events_should_have_modifier_keys() { + let mut input = GlobalInput::new(0.0); + + input.push_event(UiEvent::Raw(Input::Press(Keyboard(Key::LShift)))); + input.push_event(UiEvent::Raw(Input::Move(Motion::MouseScroll(0.0, 50.0)))); + + let scroll = input.scroll().expect("expected to get a scroll event"); + assert_eq!(keyboard::SHIFT, scroll.modifiers); +} + +#[test] +fn global_input_should_track_widget_currently_capturing_keyboard() { + let mut input = GlobalInput::new(0.0); + + let idx: Index = Index::Public(Id(5)); + input.push_event(UiEvent::WidgetCapturesKeyboard(idx)); + + assert_eq!(Some(idx), input.currently_capturing_keyboard()); + + input.push_event(UiEvent::WidgetUncapturesKeyboard(idx)); + assert!(input.currently_capturing_keyboard().is_none()); + + let new_idx: Index = Index::Public(Id(5)); + input.push_event(UiEvent::WidgetCapturesKeyboard(new_idx)); + assert_eq!(Some(new_idx), input.currently_capturing_keyboard()); +} + +#[test] +fn global_input_should_track_widget_currently_capturing_mouse() { + let mut input = GlobalInput::new(0.0); + + let idx: Index = Index::Public(Id(5)); + input.push_event(UiEvent::WidgetCapturesMouse(idx)); + + assert_eq!(Some(idx), input.currently_capturing_mouse()); + + input.push_event(UiEvent::WidgetUncapturesMouse(idx)); + assert!(input.currently_capturing_mouse().is_none()); + + let new_idx: Index = Index::Public(Id(5)); + input.push_event(UiEvent::WidgetCapturesMouse(new_idx)); + assert_eq!(Some(new_idx), input.currently_capturing_mouse()); +} + +#[test] +fn global_input_should_track_current_mouse_position() { + let mut input = GlobalInput::new(0.0); + + input.push_event(mouse_move_event(50.0, 77.7)); + assert_eq!([50.0, 77.7], input.mouse_position()); +} + +#[test] +fn mouse_button_down_should_return_current_mouse_position_if_button_is_pressed() { + let mut input = GlobalInput::new(0.0); + + input.push_event(mouse_move_event(50.0, 77.7)); + input.push_event(UiEvent::Raw(Input::Press(Mouse(MouseButton::Left)))); + + assert_eq!(Some([50.0, 77.7]), input.mouse_button_down(MouseButton::Left)); +} + +#[test] +fn mouse_button_down_should_return_none_if_button_is_not_pressed() { + let pressed_button = MouseButton::Middle; + let non_pressed_button = MouseButton::Right; + + let mut input = GlobalInput::new(0.0); + input.push_event(UiEvent::Raw(Input::Press(Mouse(pressed_button)))); + + assert!(input.mouse_button_down(non_pressed_button).is_none()); +} + +#[test] +fn entered_text_should_be_aggregated_from_multiple_events() { + let mut input = GlobalInput::new(0.0); + + input.push_event(UiEvent::Raw(Input::Text("Phil ".to_string()))); + input.push_event(UiEvent::Raw(Input::Text("is a".to_string()))); + input.push_event(UiEvent::Raw(Input::Text("wesome".to_string()))); + + let actual_text = input.text_just_entered().expect("expected to get a String, got None"); + assert_eq!("Phil is awesome".to_string(), actual_text); +} + +#[test] +fn drag_event_should_still_be_created_if_reset_is_called_between_press_and_release() { + let mut input = GlobalInput::new(4.0); + + input.push_event(UiEvent::Raw(Input::Press(Mouse(MouseButton::Left)))); + input.push_event(mouse_move_event(50.0, 77.7)); + input.reset(); + input.push_event(UiEvent::Raw(Input::Release(Mouse(MouseButton::Left)))); + + assert!(input.mouse_left_drag().is_some()); +} + +#[test] +fn click_event_should_still_be_created_if_reset_is_called_between_press_and_release() { + let mut input = GlobalInput::new(4.0); + + input.push_event(UiEvent::Raw(Input::Press(Mouse(MouseButton::Left)))); + input.reset(); + input.push_event(UiEvent::Raw(Input::Release(Mouse(MouseButton::Left)))); + + assert!(input.mouse_left_click().is_some()); +} + +#[test] +fn no_events_should_be_returned_after_reset_is_called() { + let mut input = GlobalInput::new(0.0); + input.push_event(UiEvent::Raw(Input::Press(Keyboard(Key::RShift)))); + input.push_event(UiEvent::Raw(Input::Move(Motion::MouseScroll(7.0, 88.5)))); + input.push_event(UiEvent::Raw(Input::Press(Mouse(MouseButton::Left)))); + input.push_event(mouse_move_event(60.0, 30.0)); + input.push_event(UiEvent::Raw(Input::Release(Mouse(MouseButton::Left)))); + + input.reset(); + + assert!(input.all_events().next().is_none()); +} + +#[test] +fn drag_with_modifer_key_should_include_modifiers_in_drag_event() { + use input::keyboard::SHIFT; + + let mut input = GlobalInput::new(4.0); + input.push_event(UiEvent::Raw(Input::Press(Keyboard(Key::RShift)))); + input.push_event(UiEvent::Raw(Input::Move(Motion::MouseScroll(7.0, 88.5)))); + + let scroll = input.scroll().expect("expected a scroll event"); + + assert_eq!(SHIFT, scroll.modifiers); +} + +#[test] +fn click_with_modifier_key_should_include_modifiers_in_click_event() { + use input::keyboard::CTRL; + + let mut input = GlobalInput::new(4.0); + input.push_event(UiEvent::Raw(Input::Press(Keyboard(Key::LCtrl)))); + input.push_event(UiEvent::Raw(Input::Press(Mouse(MouseButton::Left)))); + input.push_event(UiEvent::Raw(Input::Release(Mouse(MouseButton::Left)))); + + let click = input.mouse_left_click().expect("expected mouse left click event"); + let expected = MouseClick { + button: MouseButton::Left, + location: [0.0, 0.0], + modifier: CTRL + }; + assert_eq!(expected, click); +} + +#[test] +fn high_level_scroll_event_should_be_created_from_a_raw_mouse_scroll() { + let mut input = GlobalInput::new(0.0); + + input.push_event(UiEvent::Raw(Input::Move(Motion::MouseScroll(10.0, 33.0)))); + + let expected_scroll = Scroll{ + x: 10.0, + y: 33.0, + modifiers: ModifierKey::default() + }; + let actual_scroll = input.scroll().expect("expected a scroll event"); + assert_eq!(expected_scroll, actual_scroll); +} + +#[test] +fn mouse_button_pressed_moved_released_creates_final_drag_event() { + let mut input = GlobalInput::new(4.0); + + input.push_event(UiEvent::Raw(Input::Press(Mouse(MouseButton::Left)))); + input.push_event(mouse_move_event(20.0, 10.0)); + input.push_event(UiEvent::Raw(Input::Release(Mouse(MouseButton::Left)))); + + let expected_drag = MouseDrag{ + button: MouseButton::Left, + start: [0.0, 0.0], + end: [20.0, 10.0], + modifier: ModifierKey::default(), + in_progress: false + }; + let mouse_drag = input.mouse_drag(MouseButton::Left).expect("Expected to get a mouse drag event"); + assert_eq!(expected_drag, mouse_drag); +} + +#[test] +fn mouse_button_pressed_then_moved_creates_drag_event() { + let mut input = GlobalInput::new(4.0); + + let press = UiEvent::Raw(Input::Press(Mouse(MouseButton::Left))); + let mouse_move = mouse_move_event(20.0, 10.0); + input.push_event(press.clone()); + input.push_event(mouse_move.clone()); + + let expected_drag = MouseDrag{ + button: MouseButton::Left, + start: [0.0, 0.0], + end: [20.0, 10.0], + modifier: ModifierKey::default(), + in_progress: true + }; + let mouse_drag = input.mouse_drag(MouseButton::Left).expect("Expected to get a mouse drag event"); + assert_eq!(expected_drag, mouse_drag); +} + +#[test] +fn mouse_click_position_should_be_mouse_position_when_pressed() { + let mut input = GlobalInput::new(4.0); + + input.push_event(mouse_move_event(4.0, 5.0)); + input.push_event(UiEvent::Raw(Input::Press(Mouse(MouseButton::Left)))); + input.push_event(mouse_move_event(5.0, 5.0)); + input.push_event(UiEvent::Raw(Input::Release(Mouse(MouseButton::Left)))); + + let expected_click = MouseClick { + button: MouseButton::Left, + location: [4.0, 5.0], + modifier: ModifierKey::default() + }; + let actual_click = input.mouse_click(MouseButton::Left).expect("expected a mouse click event"); + + assert_eq!(expected_click, actual_click); + +} + +#[test] +fn mouse_button_pressed_then_released_should_create_mouse_click_event() { + let mut input = GlobalInput::new(4.0); + + let press = UiEvent::Raw(Input::Press(Mouse(MouseButton::Left))); + let release = UiEvent::Raw(Input::Release(Mouse(MouseButton::Left))); + input.push_event(press.clone()); + input.push_event(release.clone()); + + let expected_click = MouseClick { + button: MouseButton::Left, + location: [0.0, 0.0], + modifier: ModifierKey::default() + }; + let actual_click = input.mouse_click(MouseButton::Left).expect("expected a mouse click event"); + + assert_eq!(expected_click, actual_click); +} + +#[test] +fn all_events_should_return_all_inputs_in_order() { + let mut input = GlobalInput::new(0.0); + + let evt1 = UiEvent::Raw(Input::Press(Keyboard(Key::Z))); + input.push_event(evt1.clone()); + let evt2 = UiEvent::Raw(Input::Press(Keyboard(Key::A))); + input.push_event(evt2.clone()); + + let results = input.all_events().collect::>(); + assert_eq!(2, results.len()); + assert_eq!(evt1, *results[0]); + assert_eq!(evt2, *results[1]); +} + +fn mouse_move_event(x: Scalar, y: Scalar) -> UiEvent { + UiEvent::Raw(Input::Move(Motion::MouseRelative(x, y))) +} diff --git a/src/tests/input_provider.rs b/src/tests/input_provider.rs new file mode 100644 index 000000000..218579447 --- /dev/null +++ b/src/tests/input_provider.rs @@ -0,0 +1,202 @@ +use events::{UiEvent, Scroll, MouseClick, MouseDrag, InputState, InputProvider}; +use input::{Input, Button}; +use input::keyboard::{Key, ModifierKey, NO_MODIFIER}; +use input::mouse::MouseButton; +use position::Point; + +#[test] +fn mouse_position_should_return_mouse_position_from_current_state() { + let position = [5.0, 7.0]; + let mut input_state = InputState::new(); + input_state.mouse_position = position; + let input = ProviderImpl::with_input_state(input_state); + assert_eq!(position, input.mouse_position()); +} + +#[test] +fn mouse_button_down_should_return_true_if_button_is_pressed() { + let mut input_state = InputState::new(); + input_state.mouse_buttons.set(MouseButton::Right, Some([0.0, 0.0])); + let input = ProviderImpl::with_input_state(input_state); + assert_eq!(Some([0.0, 0.0]), input.mouse_button_down(MouseButton::Right)); + assert!(input.mouse_left_button_down().is_none()); +} + +#[test] +fn mouse_button_releases_should_be_collected_into_a_vec() { + use input::mouse::MouseButton::{Left, Right, Middle}; + let input = ProviderImpl::with_events(vec![ + UiEvent::Raw(Input::Release(Button::Mouse(Left))), + UiEvent::Raw(Input::Release(Button::Mouse(Middle))), + UiEvent::Raw(Input::Release(Button::Mouse(Right))), + ]); + + let expected = vec![Left, Middle, Right]; + assert_eq!(expected, input.mouse_buttons_just_released().collect::>()); +} + +#[test] +fn mouse_button_presses_should_be_collected_into_a_vec() { + use input::mouse::MouseButton::{Left, Right, Middle}; + let input = ProviderImpl::with_events(vec![ + UiEvent::Raw(Input::Press(Button::Mouse(Left))), + UiEvent::Raw(Input::Press(Button::Mouse(Middle))), + UiEvent::Raw(Input::Press(Button::Mouse(Right))), + ]); + + let expected = vec![Left, Middle, Right]; + assert_eq!(expected, input.mouse_buttons_just_pressed().collect::>()); +} + +#[test] +fn key_releases_should_be_collected_into_a_vec() { + let input = ProviderImpl::with_events(vec![ + UiEvent::Raw(Input::Release(Button::Keyboard(Key::LShift))), + UiEvent::Raw(Input::Release(Button::Keyboard(Key::H))), + UiEvent::Raw(Input::Release(Button::Keyboard(Key::I))), + UiEvent::Raw(Input::Release(Button::Keyboard(Key::J))), + ]); + + let expected = vec![Key::LShift, Key::H, Key::I, Key::J]; + assert_eq!(expected, input.keys_just_released().collect::>()); +} + +#[test] +fn key_presses_should_be_collected_into_a_vec() { + let input = ProviderImpl::with_events(vec![ + UiEvent::Raw(Input::Press(Button::Keyboard(Key::LShift))), + UiEvent::Raw(Input::Press(Button::Keyboard(Key::H))), + UiEvent::Raw(Input::Press(Button::Keyboard(Key::I))), + UiEvent::Raw(Input::Press(Button::Keyboard(Key::J))), + ]); + + let expected = vec![Key::LShift, Key::H, Key::I, Key::J]; + assert_eq!(expected, input.keys_just_pressed().collect::>()); +} + +#[test] +fn mouse_clicks_should_be_filtered_by_mouse_button() { + let input = ProviderImpl::with_events(vec![ + UiEvent::MouseClick(MouseClick{ + button: MouseButton::Left, + location: [50.0, 40.0], + modifier: NO_MODIFIER + }), + UiEvent::MouseClick(MouseClick{ + button: MouseButton::Right, + location: [70.0, 30.0], + modifier: NO_MODIFIER + }), + UiEvent::MouseClick(MouseClick{ + button: MouseButton::Middle, + location: [90.0, 20.0], + modifier: NO_MODIFIER + }), + ]); + + let r_click = input.mouse_click(MouseButton::Right).expect("expected a right click event"); + assert_eq!(MouseButton::Right, r_click.button); + let l_click = input.mouse_click(MouseButton::Left).expect("expected a left click event"); + assert_eq!(MouseButton::Left, l_click.button); + let m_click = input.mouse_click(MouseButton::Middle).expect("expected a middle click event"); + assert_eq!(MouseButton::Middle, m_click.button); + +} + +#[test] +fn only_the_last_drag_event_should_be_returned() { + let input = ProviderImpl::with_events(vec![ + drag_event(MouseButton::Left, [20.0, 10.0], [30.0, 20.0]), + drag_event(MouseButton::Left, [20.0, 10.0], [40.0, 30.0]), + drag_event(MouseButton::Left, [20.0, 10.0], [50.0, 40.0]) + ]); + + let expected_drag = MouseDrag{ + button: MouseButton::Left, + start: [20.0, 10.0], + end: [50.0, 40.0], + modifier: NO_MODIFIER, + in_progress: true + }; + let actual_drag = input.mouse_left_drag().expect("expected a mouse drag event"); + assert_eq!(expected_drag, actual_drag); +} + +#[test] +fn scroll_events_should_be_aggregated_into_one_when_scroll_is_called() { + let input = ProviderImpl::with_events(vec![ + scroll_event(10.0, 33.0), + scroll_event(10.0, 33.0), + scroll_event(10.0, 33.0) + ]); + + let expected_scroll = Scroll { + x: 30.0, + y: 99.0, + modifiers: ModifierKey::default() + }; + + let actual = input.scroll().expect("expected a scroll event"); + assert_eq!(expected_scroll, actual); +} + +fn drag_event(mouse_button: MouseButton, start: Point, end: Point) -> UiEvent { + UiEvent::MouseDrag(MouseDrag{ + button: mouse_button, + start: start, + end: end, + modifier: NO_MODIFIER, + in_progress: true + }) +} + +fn scroll_event(x: f64, y: f64) -> UiEvent { + UiEvent::Scroll(Scroll{ + x: x, + y: y, + modifiers: NO_MODIFIER + }) +} + +/// This is just a basic struct that implements the `InputProvider` Trait so that +/// the default trait methods are easy to test +struct ProviderImpl{ + events: Vec, + current_state: InputState, +} + +impl ProviderImpl { + fn new(events: Vec, state: InputState) -> ProviderImpl { + ProviderImpl{ + events: events, + current_state: state, + } + } + + fn with_events(events: Vec) -> ProviderImpl { + ProviderImpl::new(events, InputState::new()) + } + + fn with_input_state(state: InputState) -> ProviderImpl { + ProviderImpl::new(vec!(), state) + } +} + +pub type TestInputEventIterator<'a> = ::std::slice::Iter<'a, UiEvent>; + + +impl<'a> InputProvider<'a, TestInputEventIterator<'a>> for ProviderImpl { + fn all_events(&'a self) -> TestInputEventIterator<'a> { + self.events.iter() + } + + fn current_state(&self) -> &InputState { + &self.current_state + } + + fn mouse_button_down(&self, button: MouseButton) -> Option { + self.current_state().mouse_buttons.get(button).map(|_| { + self.mouse_position() + }) + } +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 000000000..e95d3df25 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,4 @@ +mod global_input; +mod widget_input; +mod input_provider; +mod ui; diff --git a/src/tests/ui.rs b/src/tests/ui.rs new file mode 100644 index 000000000..945e3b54a --- /dev/null +++ b/src/tests/ui.rs @@ -0,0 +1,205 @@ +use events::InputProvider; +use events::ui_event::UiEvent; +use ::{Ui, + Theme, + CharacterCache, + Labelable, + Canvas, + Color, + Positionable, + Colorable, + Sizeable, + Widget}; +use input::{Input, Motion, Button, self}; +use input::keyboard::Key; +use input::mouse::MouseButton; +use graphics::ImageSize; +use graphics::character::Character; +use graphics::types::FontSize; +use widget::{Index, self}; +use widget::button::Button as ButtonWidget; +use position::Point; + +#[test] +fn ui_should_reset_global_input_after_widget_are_set() { + let mut ui = windowless_ui(); + ui.win_w = 250.0; + ui.win_h = 300.0; + + const CANVAS_ID: widget::Id = widget::Id(0); + const BUTTON_ID: widget::Id = widget::Id(1); + + move_mouse_to_widget(Index::Public(BUTTON_ID), &mut ui); + left_click_mouse(&mut ui); + + assert!(ui.global_input.all_events().next().is_some()); + ui.set_widgets(|ui| { + + Canvas::new() + .color(Color::Rgba(1.0, 1.0, 1.0, 1.0)) + .set(CANVAS_ID, ui); + ButtonWidget::new() + .w_h(100.0, 200.0) + .label("MyButton") + .react(|| {}) + .bottom_right_of(CANVAS_ID) + .set(BUTTON_ID, ui); + }); + + assert!(ui.global_input.all_events().next().is_none()); +} + +#[test] +fn ui_should_push_capturing_event_when_mouse_button_is_pressed_over_a_widget() { + let mut ui = windowless_ui(); + ui.win_w = 250.0; + ui.win_h = 300.0; + + const CANVAS_ID: widget::Id = widget::Id(0); + const BUTTON_ID: widget::Id = widget::Id(1); + ui.set_widgets(|ui| { + + Canvas::new() + .color(Color::Rgba(1.0, 1.0, 1.0, 1.0)) + .set(CANVAS_ID, ui); + ButtonWidget::new() + .w_h(100.0, 200.0) + .label("MyButton") + .react(|| {}) + .bottom_right_of(CANVAS_ID) + .set(BUTTON_ID, ui); + }); + + let button_idx = Index::Public(BUTTON_ID); + move_mouse_to_widget(button_idx, &mut ui); + press_mouse_button(MouseButton::Left, &mut ui); + + let expected_capture_event = UiEvent::WidgetCapturesKeyboard(button_idx); + assert_event_was_pushed(&ui, expected_capture_event); + + // Now click somewhere on the background and widget should uncapture + release_mouse_button(MouseButton::Left, &mut ui); + move_mouse_to_abs_coordinates(1.0, 1.0, &mut ui); + press_mouse_button(MouseButton::Left, &mut ui); + + let expected_uncapture_event = UiEvent::WidgetUncapturesKeyboard(button_idx); + assert_event_was_pushed(&ui, expected_uncapture_event); +} + +#[test] +fn ui_should_convert_mouse_cursor_event_into_mouse_relative_event() { + let mut ui = windowless_ui(); + ui.win_w = 150.0; + ui.win_h = 200.0; + + // MouseCursor event contains location in window coordinates, which + // use the upper left corner as the origin. + ui.handle_event(&Input::Move(Motion::MouseCursor(5.0, 140.0))); + + // MouseRelative events contain location coordinates where the center of the window is the origin. + let expected_relative_event = UiEvent::Raw( + Input::Move(Motion::MouseRelative(-70.0, -40.0)) + ); + assert_event_was_pushed(&ui, expected_relative_event); +} + +#[test] +fn ui_should_push_input_events_to_aggregator() { + let mut ui = windowless_ui(); + + test_handling_basic_input_event(&mut ui, Input::Press(input::Button::Keyboard(Key::LCtrl))); + test_handling_basic_input_event(&mut ui, Input::Release(input::Button::Keyboard(Key::LCtrl))); + test_handling_basic_input_event(&mut ui, Input::Text("my string".to_string())); + test_handling_basic_input_event(&mut ui, Input::Resize(55, 99)); + test_handling_basic_input_event(&mut ui, Input::Focus(true)); + test_handling_basic_input_event(&mut ui, Input::Cursor(true)); +} + +fn left_click_mouse(ui: &mut Ui) { + press_mouse_button(MouseButton::Left, ui); + release_mouse_button(MouseButton::Left, ui); +} + +fn release_mouse_button(button: MouseButton, ui: &mut Ui) { + let event = Input::Release(Button::Mouse(button)); + ui.handle_event(&event); +} + +fn press_mouse_button(button: MouseButton, ui: &mut Ui) { + let event = Input::Press(Button::Mouse(button)); + ui.handle_event(&event); +} + +fn move_mouse_to_widget(widget_idx: Index, ui: &mut Ui) { + ui.xy_of(widget_idx).map(|point| { + let abs_xy = to_window_coordinates(point, ui); + move_mouse_to_abs_coordinates(abs_xy[0], abs_xy[1], ui); + }); +} + +fn move_mouse_to_abs_coordinates(x: f64, y: f64, ui: &mut Ui) { + ui.handle_event(&Input::Move(Motion::MouseCursor(x, y))); +} + +fn test_handling_basic_input_event(ui: &mut Ui, event: Input) { + ui.handle_event(&event); + assert_event_was_pushed(ui, UiEvent::Raw(event)); +} + +fn assert_event_was_pushed(ui: &Ui, event: UiEvent) { + let found = ui.global_input.all_events().find(|evt| **evt == event); + assert!(found.is_some(), + format!("expected to find event: {:?} in: \nall_events: {:?}", + event, + ui.global_input.all_events().collect::>())); +} + +fn to_window_coordinates(xy: Point, ui: &Ui) -> Point { + let x = (ui.win_w / 2.0) + xy[0]; + let y = (ui.win_h / 2.0) - xy[1]; + [x, y] +} + +fn windowless_ui() -> Ui { + let theme = Theme::default(); + let cc = MockCharacterCache::new(); + Ui::new(cc, theme) +} + +struct MockImageSize { + w: u32, + h: u32, +} + +impl ImageSize for MockImageSize { + fn get_size(&self) -> (u32, u32) { + (self.w, self.h) + } +} +struct MockCharacterCache{ + my_char: Character +} + +impl MockCharacterCache { + fn new() -> MockCharacterCache { + MockCharacterCache { + my_char: Character{ + offset: [0.0, 0.0], + size: [14.0, 22.0], + texture: MockImageSize{ + w: 14, + h: 22 + } + } + } + } +} + +impl CharacterCache for MockCharacterCache { + type Texture = MockImageSize; + + fn character(&mut self, _font_size: FontSize, _ch: char) -> &Character { + &self.my_char + } + +} diff --git a/src/tests/widget_input.rs b/src/tests/widget_input.rs new file mode 100644 index 000000000..e96b1383a --- /dev/null +++ b/src/tests/widget_input.rs @@ -0,0 +1,204 @@ +use input::{Input, Motion, Button}; +use input::keyboard::NO_MODIFIER; +use input::mouse::MouseButton; +use events::{UiEvent, MouseClick, GlobalInput, WidgetInput, InputProvider}; +use widget::{Index, Id}; +use position::Rect; + +#[test] +fn mouse_button_down_should_return_none_if_mouse_is_not_over_widget() { + let widget_area = Rect::from_corners([10.0, 10.0], [50.0, 50.0]); + let mut global_input = GlobalInput::new(4.0); + // mouse position stays at (0,0) + global_input.push_event(UiEvent::Raw(Input::Press(Button::Mouse(MouseButton::Left)))); + + let widget_input = WidgetInput::for_widget(Index::Public(Id(2)), widget_area, &global_input); + + assert!(widget_input.mouse_left_button_down().is_none()); +} + +#[test] +fn mouse_button_down_should_return_none_if_another_widget_is_capturing_mouse() { + let widget_area = Rect::from_corners([10.0, 10.0], [50.0, 50.0]); + let mut global_input = GlobalInput::new(4.0); + global_input.push_event(UiEvent::WidgetCapturesMouse(Index::Public(Id(999)))); + global_input.push_event(UiEvent::Raw(Input::Move(Motion::MouseRelative(30.0, 30.0)))); + global_input.push_event(UiEvent::Raw(Input::Press(Button::Mouse(MouseButton::Left)))); + + let widget_input = WidgetInput::for_widget(Index::Public(Id(2)), widget_area, &global_input); + + assert!(widget_input.mouse_left_button_down().is_none()); +} + +#[test] +fn mouse_button_down_should_return_current_mouse_position_if_mouse_is_over_widget() { + let widget_area = Rect::from_corners([10.0, 10.0], [50.0, 50.0]); + let mut global_input = GlobalInput::new(4.0); + global_input.push_event(UiEvent::Raw(Input::Move(Motion::MouseRelative(30.0, 30.0)))); + global_input.push_event(UiEvent::Raw(Input::Press(Button::Mouse(MouseButton::Left)))); + + let widget_input = WidgetInput::for_widget(Index::Public(Id(2)), widget_area, &global_input); + + assert_eq!(Some([0.0, 0.0]), widget_input.mouse_left_button_down()); +} + +#[test] +fn maybe_mouse_position_should_return_position_if_mouse_is_over_the_widget() { + let widget_area = Rect::from_corners([10.0, 10.0], [50.0, 50.0]); + let mut global_input = GlobalInput::new(4.0); + global_input.push_event(UiEvent::Raw(Input::Move(Motion::MouseRelative(30.0, 30.0)))); + + let widget_input = WidgetInput::for_widget(Index::Public(Id(2)), widget_area, &global_input); + + let maybe_mouse_position = widget_input.maybe_mouse_position(); + assert_eq!(Some([0.0, 0.0]), maybe_mouse_position); +} + +#[test] +fn maybe_mouse_position_should_return_none_if_mouse_is_not_over_the_widget() { + let widget_area = Rect::from_corners([10.0, 10.0], [50.0, 50.0]); + let mut global_input = GlobalInput::new(4.0); + global_input.push_event(UiEvent::Raw(Input::Move(Motion::MouseRelative(-10.0, -10.0)))); + + let widget_input = WidgetInput::for_widget(Index::Public(Id(2)), widget_area, &global_input); + + let maybe_mouse_position = widget_input.maybe_mouse_position(); + assert!(maybe_mouse_position.is_none()); +} + +#[test] +fn mouse_is_over_widget_should_be_true_if_mouse_is_over_the_widget_area() { + let widget_area = Rect::from_corners([10.0, 10.0], [50.0, 50.0]); + let mut global_input = GlobalInput::new(4.0); + global_input.push_event(UiEvent::Raw(Input::Move(Motion::MouseRelative(30.0, 30.0)))); + + let widget_input = WidgetInput::for_widget(Index::Public(Id(2)), widget_area, &global_input); + + assert!(widget_input.mouse_is_over_widget()); +} + +#[test] +fn mouse_is_over_widget_should_be_false_if_mouse_is_not_over_widget() { + let widget_area = Rect::from_corners([10.0, 10.0], [50.0, 50.0]); + let mut global_input = GlobalInput::new(4.0); + global_input.push_event(UiEvent::Raw(Input::Move(Motion::MouseRelative(90.0, 90.0)))); + + let widget_input = WidgetInput::for_widget(Index::Public(Id(2)), widget_area, &global_input); + + assert!(!widget_input.mouse_is_over_widget()); +} + +#[test] +fn input_state_should_be_provided_relative_to_the_widget_area() { + let widget_area = Rect::from_corners([10.0, 10.0], [50.0, 50.0]); + let mut global_input = GlobalInput::new(4.0); + global_input.push_event(UiEvent::Raw(Input::Move(Motion::MouseRelative(30.0, 30.0)))); + + let widget_input = WidgetInput::for_widget(Index::Public(Id(2)), widget_area, &global_input); + + assert_eq!([0.0, 0.0], widget_input.mouse_position()); +} + +#[test] +fn scroll_events_should_be_provided_if_widget_captures_mouse_but_not_keyboard() { + let mut global_input = GlobalInput::new(4.0); + let widget = Index::Public(Id(1)); + global_input.push_event(UiEvent::WidgetCapturesMouse(widget)); + global_input.push_event(UiEvent::Raw(Input::Move(Motion::MouseScroll(0.0, -76.0)))); + + let some_rect = Rect::from_corners([5.0, 5.0], [40.0, 40.0]); + let widget_input = WidgetInput::for_widget(widget, some_rect, &global_input); + assert!(widget_input.scroll().is_some()); +} + +#[test] +fn scroll_events_should_be_provided_if_widget_captures_keyboard_but_not_mouse() { + let mut global_input = GlobalInput::new(4.0); + let widget = Index::Public(Id(1)); + global_input.push_event(UiEvent::WidgetCapturesKeyboard(widget)); + global_input.push_event(UiEvent::Raw(Input::Move(Motion::MouseScroll(0.0, -76.0)))); + + let some_rect = Rect::from_corners([5.0, 5.0], [40.0, 40.0]); + let widget_input = WidgetInput::for_widget(widget, some_rect, &global_input); + assert!(widget_input.scroll().is_some()); +} + +#[test] +fn widget_input_should_provide_any_mouse_events_over_the_widgets_area_if_nothing_is_capturing_mouse() { + let mut global_input = GlobalInput::new(4.0); + global_input.push_event(UiEvent::MouseClick(MouseClick{ + button: MouseButton::Left, + location: [10.0, 10.0], + modifier: NO_MODIFIER + })); + assert!(global_input.currently_capturing_mouse().is_none()); + + let widget = Index::Public(Id(4)); + let widget_area = Rect::from_corners([0.0, 0.0], [40.0, 40.0]); + let widget_input = WidgetInput::for_widget(widget, widget_area, &global_input); + + widget_input.mouse_left_click().expect("Expected to get a mouse click event"); + + let another_widget = Index::Public(Id(7)); + let another_area = Rect::from_corners([-20.0, -20.0], [0.0, 0.0]); + let another_widget_input = WidgetInput::for_widget(another_widget, another_area, &global_input); + + assert!(another_widget_input.mouse_left_click().is_none()); +} + +#[test] +fn widget_input_should_only_provide_keyboard_input_to_widget_that_has_focus() { + let mut global_input = GlobalInput::new(4.0); + + let some_rect = Rect::from_corners([0.0, 0.0], [40.0, 40.0]); + let widget_4 = Index::Public(Id(4)); + global_input.push_event(UiEvent::WidgetCapturesKeyboard(widget_4)); + global_input.push_event(UiEvent::Raw(Input::Text("some text".to_string()))); + + let widget_4_input = WidgetInput::for_widget(widget_4, some_rect, &global_input); + let widget_4_text = widget_4_input.text_just_entered(); + assert_eq!(Some("some text".to_string()), widget_4_text); + + let another_widget_input = WidgetInput::for_widget(Index::Public(Id(7)), + some_rect, + &global_input); + assert!(another_widget_input.text_just_entered().is_none()); +} + +#[test] +fn mouse_clicks_should_be_relative_to_widget_position() { + let idx = Index::Public(Id(5)); + let mut global_input = GlobalInput::new(4.0); + global_input.push_event(UiEvent::MouseClick(MouseClick{ + button: MouseButton::Left, + location: [10.0, 10.0], + modifier: NO_MODIFIER + })); + + let rect = Rect::from_corners([0.0, 0.0], [20.0, 20.0]); + let widget_input = WidgetInput::for_widget(idx, rect, &global_input); + let widget_click = widget_input.mouse_left_click().expect("widget click should not be null"); + assert_eq!([0.0, 0.0], widget_click.location); +} + +#[test] +fn mouse_drags_should_be_relative_to_widget_position() { + use events::MouseDrag; + + let idx = Index::Public(Id(5)); + let mut global_input = GlobalInput::new(4.0); + global_input.push_event(UiEvent::MouseDrag(MouseDrag{ + button: MouseButton::Left, + start: [5.0, 5.0], + end: [10.0, 10.0], + modifier: NO_MODIFIER, + in_progress: false + })); + + let rect = Rect::from_corners([0.0, 0.0], [20.0, 20.0]); + let widget_input = WidgetInput::for_widget(idx, rect, &global_input); + let drag = widget_input.mouse_left_drag().expect("expected a mouse drag event"); + assert_eq!([-5.0, -5.0], drag.start); + assert_eq!([0.0, 0.0], drag.end); + +} diff --git a/src/theme.rs b/src/theme.rs index 598d44144..afe3f74d2 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -40,6 +40,9 @@ pub struct Theme { pub maybe_scrollbar: Option, /// Unique styling for each widget, index-able by the **Widget::kind**. pub widget_styling: HashMap<&'static str, WidgetDefault>, + /// Mouse Drag distance threshold determines the minimum distance from the mouse-down point + /// that the mouse must move before starting a drag operation. + pub mouse_drag_threshold: Scalar, } /// The defaults for a specific widget. @@ -69,6 +72,8 @@ impl WidgetDefault { } } +/// This is the default value that is used for `Theme::mouse_drag_threshold`. +pub const DEFAULT_MOUSE_DRAG_THRESHOLD: Scalar = 4.0; impl Theme { @@ -89,6 +94,7 @@ impl Theme { font_size_small: 12, maybe_scrollbar: None, widget_styling: HashMap::new(), + mouse_drag_threshold: DEFAULT_MOUSE_DRAG_THRESHOLD, } } @@ -110,4 +116,3 @@ impl Theme { } } - diff --git a/src/ui.rs b/src/ui.rs index e3f6dfb38..75438f7b7 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -7,6 +7,8 @@ use graph::{self, Graph, NodeIndex}; use mouse::{self, Mouse}; use input; use input::{ + Input, + Motion, GenericEvent, MouseCursorEvent, MouseScrollEvent, @@ -21,6 +23,7 @@ use std::collections::HashSet; use std::io::Write; use theme::Theme; use widget::{self, Widget}; +use ::events::{UiEvent, InputProvider, GlobalInput, WidgetInput}; /// Indicates whether or not the Mouse has been captured by a widget. @@ -51,6 +54,8 @@ pub struct Ui { pub win_w: f64, /// Window height. pub win_h: f64, + /// Handles aggregation of events and providing them to Widgets + pub global_input: GlobalInput, /// The Widget cache, storing state for all widgets. widget_graph: Graph, /// The latest received mouse state. @@ -143,6 +148,7 @@ impl Ui { { let window = widget_graph.add_placeholder(); let prev_updated_widgets = updated_widgets.clone(); + let mouse_drag_threshold = theme.mouse_drag_threshold; Ui { widget_graph: widget_graph, theme: theme, @@ -166,9 +172,25 @@ impl Ui { depth_order: depth_order, updated_widgets: updated_widgets, prev_updated_widgets: prev_updated_widgets, + global_input: GlobalInput::new(mouse_drag_threshold), } } + /// Returns a `WidgetInput` for the given widget + pub fn widget_input>(&self, widget: I) -> WidgetInput { + let idx = widget.into(); + + // If there's no rectangle for a given widget, then we use one with zero area. + // This means that the resulting `WidgetInput` will not include any mouse events + // unless it has captured the mouse, since none will have occured over that area. + let rect = self.rect_of(idx).unwrap_or_else(|| { + let right_edge = self.win_w / 2.0; + let bottom_edge = self.win_h / 2.0; + Rect::from_xy_dim([right_edge, bottom_edge], [0.0, 0.0]) + }); + WidgetInput::for_widget(idx, rect, &self.global_input) + } + /// The **Rect** for the widget at the given index. /// /// Returns `None` if there is no widget for the given index. @@ -243,8 +265,10 @@ impl Ui { /// Handle game events and update the state. pub fn handle_event(&mut self, event: &E) { + use input::{CursorEvent, FocusEvent}; event.resize(|w, h| { + self.global_input.push_event(UiEvent::Raw(Input::Resize(w, h))); self.win_w = w as f64; self.win_h = h as f64; self.needs_redraw(); @@ -257,10 +281,19 @@ impl Ui { event.mouse_cursor(|x, y| { // Convert mouse coords to (0, 0) origin. - self.mouse.xy = [x - self.win_w / 2.0, -(y - self.win_h / 2.0)]; + let center_origin_point = [x - self.win_w / 2.0, -(y - self.win_h / 2.0)]; + self.mouse.xy = center_origin_point; + self.global_input.push_event(UiEvent::Raw( + Input::Move( + Motion::MouseRelative(center_origin_point[0], center_origin_point[1]) + ) + )); }); event.mouse_scroll(|x, y| { + self.global_input.push_event(UiEvent::Raw( + Input::Move(Motion::MouseScroll(x, y)) + )); self.mouse.scroll.x += x; self.mouse.scroll.y += y; }); @@ -269,8 +302,13 @@ impl Ui { use input::Button; use input::MouseButton::{Left, Middle, Right}; + self.global_input.push_event(UiEvent::Raw( + Input::Press(button_type) + )); + match button_type { Button::Mouse(button) => { + self.widget_under_mouse_captures_keyboard(); let mouse_button = match button { Left => &mut self.mouse.left, Right => &mut self.mouse.right, @@ -288,6 +326,11 @@ impl Ui { event.release(|button_type| { use input::Button; use input::MouseButton::{Left, Middle, Right}; + + self.global_input.push_event(UiEvent::Raw( + Input::Release(button_type) + )); + match button_type { Button::Mouse(button) => { let mouse_button = match button { @@ -305,10 +348,38 @@ impl Ui { }); event.text(|text| { - self.text_just_entered.push(text.to_string()) + self.text_just_entered.push(text.to_string()); + self.global_input.push_event(UiEvent::Raw(Input::Text(text.to_string()))); + }); + + event.focus(|focus| { + self.global_input.push_event(UiEvent::Raw(Input::Focus(focus))); + }); + + event.cursor(|cursor| { + self.global_input.push_event(UiEvent::Raw(Input::Cursor(cursor))); }); } + fn widget_under_mouse_captures_keyboard(&mut self) { + use graph::algo::pick_widget; + + let mouse_xy = self.global_input.mouse_position(); + let widget_under_mouse = + pick_widget(&self.widget_graph, &self.depth_order.indices, mouse_xy); + let currently_capturing_keyboard = self.global_input.currently_capturing_keyboard(); + + if currently_capturing_keyboard.is_some() + && currently_capturing_keyboard != widget_under_mouse { + self.global_input.push_event( + UiEvent::WidgetUncapturesKeyboard(currently_capturing_keyboard.unwrap()) + ); + } + + if let Some(idx) = widget_under_mouse { + self.global_input.push_event(UiEvent::WidgetCapturesKeyboard(idx)); + } + } /// Get the centred xy coords for some given `Dimension`s, `Position` and alignment. /// @@ -518,6 +589,9 @@ impl Ui { self.mouse.middle.reset_pressed_and_released(); self.mouse.right.reset_pressed_and_released(); self.mouse.unknown.reset_pressed_and_released(); + + // reset the global input state + self.global_input.reset(); } @@ -647,7 +721,7 @@ pub fn infer_parent_from_position(ui: &Ui, x_pos: Position, y_pos: Positio /// Attempts to infer the parent of a widget from its *x*/*y* `Position`s and the current state of /// the `Ui`. -/// +/// /// If no parent can be inferred via the `Position`s, the `maybe_current_parent_idx` will be used. /// /// If `maybe_current_parent_idx` is `None`, the `Ui`'s `window` widget will be used. @@ -711,7 +785,7 @@ pub fn get_mouse_state(ui: &Ui, idx: widget::Index) -> Option { Some(Capturing::Captured(captured_idx)) => if idx == captured_idx { Some(ui.mouse) } else { None }, _ => - if Some(idx) == ui.maybe_widget_under_mouse + if Some(idx) == ui.maybe_widget_under_mouse || Some(idx) == ui.maybe_top_scrollable_widget_under_mouse { Some(ui.mouse) } else { @@ -725,7 +799,7 @@ pub fn get_mouse_state(ui: &Ui, idx: widget::Index) -> Option { /// Indicate that the widget with the given widget::Index has captured the mouse. /// /// Returns true if the mouse was successfully captured. -/// +/// /// Returns false if the mouse was already captured. pub fn mouse_captured_by(ui: &mut Ui, idx: widget::Index) -> bool { // If the mouse isn't already captured, set idx as the capturing widget. @@ -842,4 +916,3 @@ pub fn post_update_cache(ui: &mut Ui, widget: widget::PostUpdateCache(ui: &mut Ui, color: Color) { ui.maybe_background_color = Some(color); } - diff --git a/src/widget/button.rs b/src/widget/button.rs index 051d9b731..b2c8d9bf2 100644 --- a/src/widget/button.rs +++ b/src/widget/button.rs @@ -8,13 +8,13 @@ use { FramedRectangle, IndexSlot, Labelable, - Mouse, Positionable, Scalar, Text, Widget, }; use widget; +use events::InputProvider; /// A pressable button widget whose reaction is triggered upon release. @@ -54,44 +54,8 @@ widget_style!{ pub struct State { rectangle_idx: IndexSlot, label_idx: IndexSlot, - interaction: Interaction, } -/// Represents an interaction with the Button widget. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum Interaction { - Normal, - Highlighted, - Clicked, -} - - -impl Interaction { - /// Alter the widget color depending on the state. - fn color(&self, color: Color) -> Color { - match *self { - Interaction::Normal => color, - Interaction::Highlighted => color.highlighted(), - Interaction::Clicked => color.clicked(), - } - } -} - - -/// Check the current state of the button. -fn get_new_interaction(is_over: bool, prev: Interaction, mouse: Mouse) -> Interaction { - use mouse::ButtonPosition::{Down, Up}; - use self::Interaction::{Normal, Highlighted, Clicked}; - match (is_over, prev, mouse.left.position) { - (true, Normal, Down) => Normal, - (true, _, Down) => Clicked, - (true, _, Up) => Highlighted, - (false, Clicked, Down) => Clicked, - _ => Normal, - } -} - - impl<'a, F> Button<'a, F> { /// Create a button context to be built upon. @@ -109,7 +73,6 @@ impl<'a, F> Button<'a, F> { pub react { maybe_react = Some(F) } pub enabled { enabled = bool } } - } @@ -135,7 +98,6 @@ impl<'a, F> Widget for Button<'a, F> State { rectangle_idx: IndexSlot::new(), label_idx: IndexSlot::new(), - interaction: Interaction::Normal, } } @@ -146,50 +108,36 @@ impl<'a, F> Widget for Button<'a, F> /// Update the state of the Button. fn update(self, args: widget::UpdateArgs) { let widget::UpdateArgs { idx, state, style, rect, mut ui, .. } = args; - let Button { enabled, maybe_label, maybe_react, .. } = self; - let maybe_mouse = ui.input().maybe_mouse; - - // Check whether or not a new interaction has occurred. - let new_interaction = match (enabled, maybe_mouse) { - (false, _) | (true, None) => Interaction::Normal, - (true, Some(mouse)) => { - let is_over = rect.is_over(mouse.xy); - get_new_interaction(is_over, state.view().interaction, mouse) - }, - }; - - // Capture the mouse if it was clicked, uncapture if it was released. - match (state.view().interaction, new_interaction) { - (Interaction::Highlighted, Interaction::Clicked) => { ui.capture_mouse(); }, - (Interaction::Clicked, Interaction::Highlighted) | - (Interaction::Clicked, Interaction::Normal) => { ui.uncapture_mouse(); }, - _ => (), - } - // If the mouse was released over button, react. - if let (Interaction::Clicked, Interaction::Highlighted) = - (state.view().interaction, new_interaction) { - if let Some(react) = maybe_react { - react() + let button_color = { + let input = ui.widget_input(); + if input.mouse_left_click().is_some() { + self.maybe_react.map(|react_function| react_function()); } - } + + let style_color = style.color(ui.theme()); + input.mouse_left_button_down().map(|_| { + style_color.clicked() + }).or_else(|| { + input.maybe_mouse_position().map(|_| style_color.highlighted()) + }).unwrap_or(style_color) + }; // FramedRectangle widget. let rectangle_idx = state.view().rectangle_idx.get(&mut ui); let dim = rect.dim(); let frame = style.frame(ui.theme()); - let color = new_interaction.color(style.color(ui.theme())); let frame_color = style.frame_color(ui.theme()); FramedRectangle::new(dim) .middle_of(idx) .graphics_for(idx) - .color(color) + .color(button_color) .frame(frame) .frame_color(frame_color) .set(rectangle_idx, &mut ui); // Label widget. - if let Some(label) = maybe_label { + if let Some(label) = self.maybe_label { let label_idx = state.view().label_idx.get(&mut ui); let color = style.label_color(ui.theme()); let font_size = style.label_font_size(ui.theme()); @@ -201,11 +149,6 @@ impl<'a, F> Widget for Button<'a, F> .set(label_idx, &mut ui); } - // If there has been a change in interaction, set the new one. - if state.view().interaction != new_interaction { - state.update(|state| state.interaction = new_interaction); - } - } } diff --git a/src/widget/drop_down_list.rs b/src/widget/drop_down_list.rs index aedff36e6..a3bfa4345 100644 --- a/src/widget/drop_down_list.rs +++ b/src/widget/drop_down_list.rs @@ -17,6 +17,7 @@ use ::{ Sizeable, }; use widget::{self, Widget}; +use events::InputProvider; /// The index of a selected item. @@ -153,10 +154,6 @@ impl<'a, F> Widget for DropDownList<'a, F> where fn update(mut self, args: widget::UpdateArgs) { let widget::UpdateArgs { idx, state, rect, style, mut ui, .. } = args; - let (global_mouse, window_dim) = { - let input = ui.input(); - (input.global_mouse, input.window_dim) - }; let frame = style.frame(ui.theme()); let num_strings = self.strings.len(); @@ -184,43 +181,38 @@ impl<'a, F> Widget for DropDownList<'a, F> where state.update(|state| state.buttons = new_buttons); } - // Determine the new menu state by checking whether or not any of our Button's reactions - // are triggered. + // Act on the current menu state and determine what the next one will be. + // new_menu_state is what we will be getting passed next frame let new_menu_state = match state.view().menu_state { - // If closed, we only want the button at the selected index to be drawn. MenuState::Closed => { - let buttons = &state.view().buttons; - // Get the button index and the label for the closed menu's button. + let buttons = &state.view().buttons; let (button_idx, label) = selected .map(|i| (buttons[i].0, &self.strings[i][..])) .unwrap_or_else(|| (buttons[0].0, self.maybe_label.unwrap_or(""))); - // Use the pre-existing button widget to act as our button. let mut was_clicked = false; { + // use the pre-existing Button widget let mut button = Button::new() - .xy(rect.xy()) - .wh(rect.dim()) - .label(label) - .parent(idx) - .react(|| was_clicked = true); - let is_selected = false; - button.style = style.button_style(is_selected); + .xy(rect.xy()) + .wh(rect.dim()) + .label(label) + .parent(idx) + .react(|| {was_clicked = true}); + button.style = style.button_style(false); button.set(button_idx, &mut ui); } - // If the closed menu was clicked, we want to open it. + // If the button was clicked, then open, otherwise stay closed if was_clicked { MenuState::Open } else { MenuState::Closed } }, - - // Otherwise if open, we want to set all the buttons that would be currently visible. MenuState::Open => { - + // Otherwise if open, we want to set all the buttons that would be currently visible. let (xy, dim) = rect.xy_dim(); let max_visible_height = { - let bottom_win_y = (-window_dim[1]) / 2.0; + let bottom_win_y = (-ui.window_dim()[1]) / 2.0; const WINDOW_PADDING: Scalar = 20.0; let max = xy[1] + dim[1] / 2.0 - bottom_win_y - WINDOW_PADDING; style.maybe_max_visible_height(ui.theme()).map(|max_height| { @@ -260,25 +252,27 @@ impl<'a, F> Widget for DropDownList<'a, F> where button.set(button_node_idx, &mut ui); } - // If one of the buttons was clicked, we want to close the menu. - if let Some(i) = was_clicked { + let mouse_pressed_elsewhere = ui.global_input.mouse_buttons_just_pressed().next().is_some() + && !canvas_rect.is_over(ui.global_input.mouse_position()); - // If we were given some react function, we'll call it. + // Determine the new menu state + if let Some(i) = was_clicked { + // If one of the buttons was clicked, we want to close the menu. if let Some(ref mut react) = self.maybe_react { + // If we were given some react function, we'll call it. *self.selected = selected; - react(self.selected, i, &self.strings[i]) + react(self.selected, i, &self.strings[i]); } - MenuState::Closed - // Otherwise if the mouse was released somewhere else we should close the menu. - } else if global_mouse.left.was_just_pressed - && !canvas_rect.is_over(global_mouse.xy) { + } else if mouse_pressed_elsewhere { + // if a mouse button was pressed somewhere else, then close the menu MenuState::Closed + } else { + // Otherwise, we just keep the menu open MenuState::Open } - }, - + } }; if state.view().menu_state != new_menu_state { diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 91174abfe..c8b5513a3 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -6,6 +6,7 @@ use std::fmt::Debug; use theme::{self, Theme}; use time::precise_time_ns; use ui::{self, Ui, UserInput}; +use events::{GlobalInput, WidgetInput}; pub use self::id::Id; pub use self::index::Index; @@ -810,7 +811,7 @@ pub trait Widget: Sized { /// For all following occasions, the pre-existing cached state will be compared and updated. /// /// Note that this is a very imperative, mutation oriented segment of code. We try to move as much -/// imperativeness and mutation out of the users hands and into this function as possible, so that +/// imperativeness and mutation out of the users hands and into this function as possible, so that /// users have a clear, consise, purely functional `Widget` API. As a result, we try to keep this /// as verbosely annotated as possible. If anything is unclear, feel free to post an issue or PR /// with concerns/improvements to the github repo. @@ -1120,11 +1121,34 @@ impl<'a, C> UiCell<'a, C> { /// A reference to the `Ui`'s `GlyphCache`. pub fn glyph_cache(&self) -> &GlyphCache { &self.ui.glyph_cache } + /// Returns the dimensions of the window + pub fn window_dim(&self) -> Dimensions { + [self.ui.win_w, self.ui.win_h] + } + /// A struct representing the user input that has occurred since the last update. pub fn input(&self) -> UserInput { ui::user_input(self.ui, self.idx) } + /// Returns an immutable reference to the `GlobalInput` of the `Ui`. All coordinates + /// here will be relative to the center of the window. + pub fn global_input(&self) -> &GlobalInput { + &self.ui.global_input + } + + /// Returns a `WidgetInput` with input events for the widget. + /// All coordinates in the `WidgetInput` will be relative to the current widget. + pub fn widget_input(&self) -> WidgetInput { + self.widget_input_for(self.idx) + } + + /// Returns a `WidgetInput` with input events for the widget. + /// All coordinates in the `WidgetInput` will be relative to the given widget. + pub fn widget_input_for>(&self, widget: I) -> WidgetInput { + self.ui.widget_input(widget.into()) + } + /// A struct representing the user input that has occurred since the last update for the /// `Widget` with the given index.. pub fn input_for>(&self, idx: I) -> UserInput { @@ -1372,7 +1396,7 @@ impl Sizeable for W where W: Widget { // } // }; // } -// +// // style_retrieval! { // fn_name: color, // member: maybe_color,