Repository: oomek/attractplus
Pull request #120: Update mouse movement handling:
Author: login: Chadnaut
Created: 2025-04-01T15:34:23Z | Merged: 2025-04-23T22:11:37Z

- Separate axis monitoring to prevent direction blocking
- Capture pointer movements outside the window to prevent lost directions
- Pointer no longer confined to threshold region

Basically changed ALL the MouseMoved handling - requires thorough testing!


Diff:
diff --git a/src/fe_input.cpp b/src/fe_input.cpp
index 6f5c617c0..4144b6572 100644
--- a/src/fe_input.cpp
+++ b/src/fe_input.cpp
@@ -40,6 +40,8 @@ namespace
 	bool g_touch_moved=false;
 
 	int g_wheel_delta=0;
+	sf::Vector2i g_mouse_pos;
+	sf::Vector2i g_mouse_thresh;
 
 	static std::vector< int > g_joyfemap( sf::Joystick::Count, 0 );
 
@@ -124,6 +126,12 @@ namespace
 	}
 };
 
+void FeInputSingle::init_mouse_window( FeWindow &wnd )
+{
+	g_mouse_pos = sf::Mouse::getPosition( wnd.get_win() );
+	g_mouse_thresh = sf::Vector2i( 0, 0 );
+}
+
 // Needs to stay aligned with sf::Keyboard
 //
 const char *FeInputSingle::keyStrings[] =
@@ -292,7 +300,7 @@ FeInputSingle::FeInputSingle( Type t, int c )
 {
 }
 
-FeInputSingle::FeInputSingle( const sf::Event &e, const sf::IntRect &mc_rect, const int joy_thresh )
+FeInputSingle::FeInputSingle( const sf::Event &e, const int mouse_thresh, const int joy_thresh )
 	: m_type( Unsupported ),
 	m_code( 0 )
 {
@@ -377,28 +385,39 @@ FeInputSingle::FeInputSingle( const sf::Event &e, const sf::IntRect &mc_rect, co
 
 	else if ( e.is<sf::Event::MouseMoved>() )
 	{
-		const auto* event = e.getIf<sf::Event::MouseMoved>();
-		if ( event )
+		const auto* mv = e.getIf<sf::Event::MouseMoved>();
+		if ( mv )
 		{
-			if ( event->position.x < mc_rect.position.x )
+			// find delta from previous position
+			sf::Vector2i delta = mv->position - g_mouse_pos;
+			g_mouse_pos = mv->position;
+
+			// if delta reverses direction then reset its thresh position to zero
+			// - improves responsiveness, preventing 2x travel to the opposite threshold
+			if (( g_mouse_thresh.x < 0 && delta.x > 0 ) || ( g_mouse_thresh.x > 0 && delta.x < 0 )) g_mouse_thresh.x = 0;
+			if (( g_mouse_thresh.y < 0 && delta.y > 0 ) || ( g_mouse_thresh.y > 0 && delta.y < 0 )) g_mouse_thresh.y = 0;
+			g_mouse_thresh += delta;
+
+			// calculate distance each axis is from the threshold
+			sf::Vector2i threshold = sf::Vector2i(
+				abs(g_mouse_thresh.x) - mouse_thresh,
+				abs(g_mouse_thresh.y) - mouse_thresh
+			);
+
+			// only a single direction can be reported at a time
+			// - choose the direction furthest from the threshold, or x if equal
+			// - the remaining direction will (likely) be reported next loop
+			if ( threshold.x >= 0 && threshold.x >= threshold.y )
 			{
 				m_type = Mouse;
-				m_code = MouseLeft;
+				m_code = delta.x > 0 ? MouseRight : MouseLeft;
+				g_mouse_thresh.x %= mouse_thresh;
 			}
-			else if ( event->position.y < mc_rect.position.y )
+			else if ( threshold.y >= 0 )
 			{
 				m_type = Mouse;
-				m_code = MouseUp;
-			}
-			else if ( event->position.x >= mc_rect.position.x + mc_rect.size.x )
-			{
-				m_type = Mouse;
-				m_code = MouseRight;
-			}
-			else if ( event->position.y >= mc_rect.position.y + mc_rect.size.y )
-			{
-				m_type = Mouse;
-				m_code = MouseDown;
+				m_code = delta.y > 0 ? MouseDown : MouseUp;
+				g_mouse_thresh.y %= mouse_thresh;
 			}
 		}
 	}
@@ -804,17 +823,6 @@ std::string FeInputMapEntry::as_string() const
 	return retval;
 }
 
-bool FeInputMapEntry::has_mouse_move() const
-{
-	for ( std::set < FeInputSingle >::const_iterator it=inputs.begin(); it != inputs.end(); ++it )
-	{
-		if ( (*it).is_mouse_move() )
-			return true;
-	}
-
-	return false;
-}
-
 FeMapping::FeMapping( FeInputMap::Command cmd )
 	: command( cmd )
 {
@@ -946,8 +954,7 @@ const char *FeInputMap::commandDispStrings[] =
 };
 
 FeInputMap::FeInputMap()
-	: m_defaults( (int)Select ),
-	m_mmove_count( 0 )
+	: m_defaults( (int)Select )
 {
 	// Set default actions for the "UI" commands (Back, Up, Down, Left, Right)
 	//
@@ -1148,9 +1155,9 @@ FeInputMap::Command FeInputMap::get_command_from_tracked_keys( const int joy_thr
 	return retval;
 }
 
-FeInputMap::Command FeInputMap::map_input( const sf::Event &e, const sf::IntRect &mc_rect, const int joy_thresh )
+FeInputMap::Command FeInputMap::map_input( const sf::Event &e, const int mouse_thresh, const int joy_thresh )
 {
-	FeInputSingle index( e, mc_rect, joy_thresh );
+	FeInputSingle index( e, mouse_thresh, joy_thresh );
 
 	if ( e.is<sf::Event::Closed>() )
 	{
@@ -1187,7 +1194,7 @@ FeInputMap::Command FeInputMap::map_input( const sf::Event &e, const sf::IntRect
 		else if ( const auto* mouse = e.getIf<sf::Event::MouseButtonReleased>() )
 			te = sf::Event::MouseButtonPressed{ mouse->button };
 
-		FeInputSingle tt( te, mc_rect, joy_thresh );
+		FeInputSingle tt( te, mouse_thresh, joy_thresh );
 		if ( m_tracked_keys.find( tt ) != m_tracked_keys.end() )
 		{
 			FeInputMap::Command temp = get_command_from_tracked_keys( joy_thresh );
@@ -1336,9 +1343,6 @@ void FeInputMap::set_mapping( const FeMapping &mapping )
 		//
 		if ( m_inputs[i].command == cmd )
 		{
-			if ( m_inputs[i].has_mouse_move() )
-				m_mmove_count--;
-
 			m_inputs.erase( m_inputs.begin() + i );
 			continue;
 		}
@@ -1367,9 +1371,6 @@ void FeInputMap::set_mapping( const FeMapping &mapping )
 		if (!new_entry.inputs.empty())
 		{
 			m_inputs.push_back( new_entry );
-
-			if ( new_entry.has_mouse_move() )
-				m_mmove_count++;
 		}
 	}
 
@@ -1435,9 +1436,6 @@ int FeInputMap::process_setting( const std::string &setting,
 
 	m_inputs.push_back( new_entry );
 
-	if ( new_entry.has_mouse_move() )
-		m_mmove_count++;
-
 	return 0;
 }
 
diff --git a/src/fe_input.hpp b/src/fe_input.hpp
index cb0e7d006..069458eff 100644
--- a/src/fe_input.hpp
+++ b/src/fe_input.hpp
@@ -73,7 +73,7 @@ class FeInputSingle
 	FeInputSingle( Type t, int code );
 
 	// Construct from an SFML event
-	FeInputSingle( const sf::Event &ev, const sf::IntRect &mc_rect, const int joy_thresh );
+	FeInputSingle( const sf::Event &ev, const int mouse_thresh, const int joy_thresh );
 
 	// Construct from a config string
 	FeInputSingle( const std::string &str );
@@ -90,6 +90,8 @@ class FeInputSingle
 	// false otherwise.  Works for keys, buttons and joystick axes.  Does not work for mouse moves or wheels.
 	bool get_current_state( int joy_thresh ) const;
 
+	void init_mouse_window( FeWindow &wnd );
+
 	// Return the current position of the input that this object represents.
 	// Works for joystick axes
 	int get_current_pos( FeWindow &w ) const;
@@ -189,7 +191,7 @@ class FeInputMap : public FeBaseConfigurable
 
 	FeInputMap();
 
-	Command map_input( const sf::Event &, const sf::IntRect &mc_rect, const int joy_thresh );
+	Command map_input( const sf::Event &, const int mouse_thresh, const int joy_thresh );
 
 	Command input_conflict_check( const FeInputMapEntry &e );
 
@@ -219,7 +221,6 @@ class FeInputMap : public FeBaseConfigurable
 		const std::string &fn );
 
 	void save( nowide::ofstream & ) const;
-	bool has_mouse_moves() const { return ( m_mmove_count > 0 ); };
 
 	static Command string_to_command( const std::string &s );
 
@@ -256,8 +257,6 @@ class FeInputMap : public FeBaseConfigurable
 
 	// Config for mapping joysticks by name to specific id #s
 	std::vector< std::pair< int, std::string > > m_joy_config;
-
-	int m_mmove_count; // counter of whether mouse moves are mapped
 };
 
 class FeInputMapEntry
@@ -271,7 +270,6 @@ class FeInputMapEntry
 		const FeInputSingle &trigger=FeInputSingle() ) const;
 
 	std::string as_string() const;
-	bool has_mouse_move() const;
 
 	std::set < FeInputSingle > inputs;
 	FeInputMap::Command command;
diff --git a/src/fe_overlay.cpp b/src/fe_overlay.cpp
index bd30b50d5..f5ea5a22c 100644
--- a/src/fe_overlay.cpp
+++ b/src/fe_overlay.cpp
@@ -792,7 +792,7 @@ void FeOverlay::input_map_dialog(
 	// Make sure the appropriate mouse capture variables are set, in case
 	// the user has just changed the mouse threshold
 	//
-	m_feSettings.init_mouse_capture( m_screen_size.x, m_screen_size.y );
+	m_feSettings.init_mouse_threshold( m_screen_size.x, m_screen_size.y );
 
 	message.setSize( m_screen_size.x, m_screen_size.y );
 	message.setString( msg_str );
@@ -814,9 +814,9 @@ void FeOverlay::input_map_dialog(
 	bool multi_mode=false; // flag if we are checking for multiple inputs.
 	bool done=false;
 
-	sf::IntRect mc_rect;
+	int mouse_thresh;
 	int joy_thresh;
-	m_feSettings.get_input_config_metrics( mc_rect, joy_thresh );
+	m_feSettings.get_input_config_metrics( mouse_thresh, joy_thresh );
 
 	std::set < std::pair<int,int> > joystick_moves;
 	FeInputMapEntry entry;
@@ -838,7 +838,7 @@ void FeOverlay::input_map_dialog(
 					done = true;
 				else
 				{
-					FeInputSingle single( ev.value(), mc_rect, joy_thresh );
+					FeInputSingle single( ev.value(), mouse_thresh, joy_thresh );
 					if ( single.get_type() != FeInputSingle::Unsupported )
 					{
 						if (( ev->is<sf::Event::KeyPressed>() )
@@ -1439,15 +1439,6 @@ bool FeOverlay::event_loop( FeEventLoopCtx &ctx )
 						&& ( c == ctx.extra_exit ))
 					c = FeInputMap::Exit;
 
-				if ( const auto* mv = ev->getIf<sf::Event::MouseMoved>() )
-				{
-					if ( m_feSettings.test_mouse_reset( mv->position.x, mv->position.y ))
-					{
-						sf::Vector2u s = m_wnd.get_win().getSize();
-						sf::Mouse::setPosition( sf::Vector2i( s.x / 2, s.y / 2 ), m_wnd.get_win() );
-					}
-				}
-
 				switch( c )
 				{
 				case FeInputMap::Configure:
diff --git a/src/fe_settings.cpp b/src/fe_settings.cpp
index 225626327..00688456e 100644
--- a/src/fe_settings.cpp
+++ b/src/fe_settings.cpp
@@ -909,7 +909,7 @@ void FeSettings::load_state()
 FeInputMap::Command FeSettings::map_input( const std::optional<sf::Event> &e )
 {
 	if ( e.has_value() )
-		return m_inputmap.map_input( e.value(), m_mousecap_rect, m_joy_thresh );
+		return m_inputmap.map_input( e.value(), m_window_mouse_thresh, m_joy_thresh );
 	else
 		return FeInputMap::LAST_COMMAND;
 }
@@ -919,9 +919,9 @@ FeInputMap::Command FeSettings::input_conflict_check( const FeInputMapEntry &e )
 	return m_inputmap.input_conflict_check( e );
 }
 
-void FeSettings::get_input_config_metrics( sf::IntRect &mousecap_rect, int &joy_thresh )
+void FeSettings::get_input_config_metrics( int &mouse_thresh, int &joy_thresh )
 {
-	mousecap_rect = m_mousecap_rect;
+	mouse_thresh = m_window_mouse_thresh;
 	joy_thresh = m_joy_thresh;
 }
 
@@ -940,21 +940,9 @@ bool FeSettings::get_current_state( FeInputMap::Command c )
 	return m_inputmap.get_current_state( c, m_joy_thresh );
 }
 
-void FeSettings::init_mouse_capture( int window_x, int window_y )
+void FeSettings::init_mouse_threshold( int window_x, int window_y )
 {
-	int radius = window_x * m_mouse_thresh / 400;
-	int centre_x = window_x / 2;
-	int centre_y = window_y / 2;
-
-	m_mousecap_rect.position.x = centre_x - radius;
-	m_mousecap_rect.position.y = centre_y - radius;
-	m_mousecap_rect.size.x = radius * 2;
-	m_mousecap_rect.size.y = radius * 2;
-}
-
-bool FeSettings::test_mouse_reset( int mouse_x, int mouse_y ) const
-{
-	return (( m_inputmap.has_mouse_moves() ) && ( !m_mousecap_rect.contains({ mouse_x, mouse_y })));
+	m_window_mouse_thresh = window_x * m_mouse_thresh / 400;
 }
 
 int FeSettings::get_filter_index_from_offset( int offset ) const
diff --git a/src/fe_settings.hpp b/src/fe_settings.hpp
index e5e867456..9b0a49da2 100644
--- a/src/fe_settings.hpp
+++ b/src/fe_settings.hpp
@@ -210,7 +210,7 @@ class FeSettings : public FeBaseConfigurable
 	FeLayoutInfo m_intro_params;
 	FeLayoutInfo m_current_layout_params; // copy of current layout params (w/ per display params as well)
 	FeLayoutInfo m_display_menu_per_display_params; // stores only the 'per_display' params for the display menu
-	sf::IntRect m_mousecap_rect;
+	int m_window_mouse_thresh;
 
 	int m_current_display; // -1 if we are currently showing the 'displays menu' w/ custom layout
 	FeBaseConfigurable *m_current_config_object;
@@ -319,7 +319,7 @@ class FeSettings : public FeBaseConfigurable
 
 	FeInputMap::Command map_input( const std::optional<sf::Event> &e );
 
-	void get_input_config_metrics( sf::IntRect &mousecap_rect, int &joy_thresh );
+	void get_input_config_metrics( int &mouse_thresh, int &joy_thresh );
 	FeInputMap::Command input_conflict_check( const FeInputMapEntry &e );
 
 	// for use with Up, Down, Left, Right, Back commands to get what they are actually mapped to
@@ -397,8 +397,7 @@ class FeSettings : public FeBaseConfigurable
 	bool select_last_launch();
 	bool is_last_launch( int filter_offset, int index_offset );
 	int get_joy_thresh() const { return m_joy_thresh; }
-	void init_mouse_capture( int window_x, int window_y );
-	bool test_mouse_reset( int mouse_x, int mouse_y ) const;
+	void init_mouse_threshold( int window_x, int window_y );
 
 	// prepares for emulator launch by setting various tracking variables (last launch, etc)
 	// and determining the correct executable, command line parameters and working directory to use
diff --git a/src/fe_vm.cpp b/src/fe_vm.cpp
index 25c3f5152..a1a91d04e 100644
--- a/src/fe_vm.cpp
+++ b/src/fe_vm.cpp
@@ -1221,6 +1221,8 @@ bool FeVM::on_new_layout()
 			<< " (" << filename << ")" << std::endl;
 	}
 
+	FeInputSingle().init_mouse_window( m_window );
+
 	// To avoid frame delay of nested surfaces we have to sort them here
 	// so the most nested ones are redrawn first
 	std::stable_sort( m_texturePool.begin(), m_texturePool.end(), nesting_compare );
diff --git a/src/fe_window.cpp b/src/fe_window.cpp
index dff72993a..ce96bded7 100644
--- a/src/fe_window.cpp
+++ b/src/fe_window.cpp
@@ -459,11 +459,7 @@ void FeWindow::initial_create()
 	SetWindowLongPtr( hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>( CustomWndProc ));
 #endif
 
-	m_fes.init_mouse_capture( wsize.x, wsize.y );
-
-	// Only mess with the mouse position if mouse moves mapped
-	if ( m_fes.test_mouse_reset( 0, 0 ) )
-		sf::Mouse::setPosition( sf::Vector2i( wsize.x / 2, wsize.y / 2 ), *m_window );
+	m_fes.init_mouse_threshold( wsize.x, wsize.y );
 
 	std::vector<unsigned char> logo_data = base64_decode( _binary_resources_images_Logo_png );
 	if ( m_logo_image.loadFromMemory( logo_data.data(), logo_data.size() ));
@@ -795,5 +791,33 @@ void FeWindow::draw( const sf::Drawable &d, const sf::RenderStates &r )
 
 const std::optional<sf::Event> FeWindow::pollEvent()
 {
-	return m_window->pollEvent();
+	// if mouse is outside the window hijack the event
+	if ( m_mouse_outside )
+	{
+		sf::Vector2i pos = sf::Mouse::getPosition( *m_window );
+		if ( pos != m_mouse_pos )
+		{
+			m_mouse_pos = pos;
+			return sf::Event::MouseMoved({ m_mouse_pos });
+		}
+	}
+
+	// the actual window event
+	std::optional<sf::Event> ev = m_window->pollEvent();
+
+	// store the mouse position, and whether the mouse is over the window
+	if ( ev.has_value() )
+	{
+		if ( ev->is<sf::Event::MouseMoved>() )
+		{
+			m_mouse_pos = ev->getIf<sf::Event::MouseMoved>()->position;
+			m_mouse_outside = false;
+		}
+		else if ( ev->is<sf::Event::MouseLeft>() )
+		{
+			m_mouse_outside = true;
+		}
+	}
+
+	return ev;
 }
diff --git a/src/fe_window.hpp b/src/fe_window.hpp
index d85909f5a..cbb8254dc 100644
--- a/src/fe_window.hpp
+++ b/src/fe_window.hpp
@@ -51,6 +51,8 @@ class FeWindow
 	void check_for_sleep();
 #endif
 	int m_win_mode;
+	bool m_mouse_outside = true;
+	sf::Vector2i m_mouse_pos;
 	sf::Image m_logo_image;
 
 public:
diff --git a/src/main.cpp b/src/main.cpp
index 734a8aa54..7056db0c5 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -357,19 +357,6 @@ int main(int argc, char *argv[])
 				if ( ev->is<sf::Event::Closed>() )
 					exit_selected = true;
 
-				else if ( ev->is<sf::Event::MouseMoved>() )
-				{
-					const auto* mov = ev->getIf<sf::Event::MouseMoved>();
-					if ( mov && feSettings.test_mouse_reset( mov->position.x, mov->position.y ))
-					{
-						// We reset the mouse if we are capturing it and it has moved
-						// outside of its bounding box
-						//
-						sf::Vector2u s = window.get_win().getSize();
-						sf::Mouse::setPosition( sf::Vector2i( s.x / 2, s.y / 2 ), window.get_win() );
-					}
-				}
-
 				else if ( ev->is<sf::Event::KeyReleased>() ||
 							ev->is<sf::Event::MouseButtonReleased>() ||
 							ev->is<sf::Event::JoystickButtonReleased>() )


Reviews:
user: login: Chadnaut
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T15:35:48Z
user: login: Chadnaut
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T15:36:50Z
user: login: Chadnaut
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T15:37:28Z
user: login: oomek
state: APPROVED
author_association: OWNER
submitted_at: 2025-04-23T15:38:38Z

Review comments:
diff_hunk: @@ -377,28 +385,39 @@ FeInputSingle::FeInputSingle( const sf::Event &e, const sf::IntRect &mc_rect, co
 
 	else if ( e.is<sf::Event::MouseMoved>() )
 	{
-		const auto* event = e.getIf<sf::Event::MouseMoved>();
-		if ( event )
+		const auto* mv = e.getIf<sf::Event::MouseMoved>();
+		if ( mv )
path: src/fe_input.cpp
user: login: Chadnaut
body: Big change to MouseMoved here. Cannot use MouseMovedRaw (no mac support).
created_at: 2025-04-01T15:35:48Z
author_association: COLLABORATOR
diff_hunk: @@ -742,5 +738,33 @@ void FeWindow::draw( const sf::Drawable &d, const sf::RenderStates &r )
 
 const std::optional<sf::Event> FeWindow::pollEvent()
 {
-	return m_window->pollEvent();
+	// if mouse is outside the window hijack the event
+	if ( m_mouse_outside )
path: src/fe_window.cpp
user: login: Chadnaut
body: Here's the trick - pollEvent is hijacked to report MouseMoved events when the cursor is outside the window.
created_at: 2025-04-01T15:36:50Z
author_association: COLLABORATOR
diff_hunk: @@ -1444,15 +1444,6 @@ bool FeOverlay::event_loop( FeEventLoopCtx &ctx )
 						&& ( c == ctx.extra_exit ))
 					c = FeInputMap::Exit;
 
-				if ( const auto* mv = ev->getIf<sf::Event::MouseMoved>() )
-				{
-					if ( m_feSettings.test_mouse_reset( mv->position.x, mv->position.y ))
-					{
-						sf::Vector2u s = m_wnd.get_win().getSize();
-						sf::Mouse::setPosition( sf::Vector2i( s.x / 2, s.y / 2 ), m_wnd.get_win() );
path: src/fe_overlay.cpp
user: login: Chadnaut
body: No need to lock pointer to capture region anymore.
created_at: 2025-04-01T15:37:28Z
author_association: COLLABORATOR
diff_hunk: @@ -64,7 +64,7 @@ jobs:
         sudo apt-get install -y ./am/extlibs/mxe/mxe-x86-64-w64-mingw32.static-ffmpeg_7.0.2.deb
         sudo ln -s /usr/lib/mxe/usr/bin/x86_64-w64-mingw32.static-gcc /usr/lib/mxe/usr/bin/x86_64-w64-mingw32.static-cc
         sudo mv /usr/lib/mxe/usr/x86_64-pc-linux-gnu/bin/cmake /usr/lib/mxe/usr/x86_64-pc-linux-gnu/bin/cmake.original || true
-        sudo ln -sf $(which cmake) /usr/lib/mxe/usr/x86_64-pc-linux-gnu/bin/cmake
+        sudo ln -sf /usr/bin/cmake /usr/lib/mxe/usr/x86_64-pc-linux-gnu/bin/cmake
path: .github/workflows/Windows.yml
user: login: oomek
body: Could you push this cmake fix to master as a commit please and squash and rebase this pr? I also need to rebase the utf8 fix branch.
created_at: 2025-04-02T01:15:13Z
author_association: OWNER

PR comments:
user: login: oomek
created_at: 2025-04-01T17:37:28Z
body: While you're at mouse events would you please take a look at this PR?
https://github.com/oomek/attractplus/pull/108/files
author_association: OWNER
user: login: Chadnaut
created_at: 2025-04-02T00:55:59Z
body: I should have added that up front - this PR is specifically to fix that issue
- https://github.com/oomek/attractplus/issues/107
- https://github.com/oomek/attractplus/pull/108/files
author_association: COLLABORATOR
user: login: Chadnaut
created_at: 2025-04-22T15:05:03Z
body: @oomek Why was this branch deleted?
author_association: COLLABORATOR
user: login: oomek
created_at: 2025-04-22T15:11:22Z
body: I didn't
author_association: OWNER
user: login: oomek
created_at: 2025-04-22T15:16:27Z
body: My fault. Restored, fetch
author_association: OWNER
user: login: oomek
created_at: 2025-04-23T11:48:12Z
body: Testing. I've had no idea that the mouse pointer is locked to the centre of the screen when the mouse movement is bound to Up/Down controls. Looking good so far. Can you please explain the other issues it fixes, I'm not able to reproduce.
author_association: OWNER
user: login: Chadnaut
created_at: 2025-04-23T12:57:17Z
body: 1. The priority of the mouse moved event feels inconsistent

The old capture region is a small box in the center of the window.
When the mouse escapes this box it triggers a direction, and the mouse is reset to the center.

If you move the mouse diagonally, it will hit one edge of the box before the other, then reset.
For example, the "right" direction hits moments before the "up" direction, and all "up" progress is lost when reset.

2. If I spin the spinner too fast, it gets "stuck"

When the mouse is moved quickly it has a chance to escape the window.
Although AM still tracks it, it doesn't trigger the capture region reset.


author_association: COLLABORATOR
user: login: Chadnaut
created_at: 2025-04-23T13:28:10Z
body: To reproduce:
1. Set the mouse up/down to control game prev/next.
2. Move mouse up/right - but more right than up. "Right" triggers a cursor reset, causing to "up" to never register.
3. Mouse the mouse very fast to escape the window. Up/down no longer registers prev/next.
author_association: COLLABORATOR