Browse Source

Implement input focus behavior in HTML5

 - Key and mouse events are only consumed if canvas is focused
 - NOTIFICATION_WM_MOUSE_ENTER, _MOUSE_EXIT, _FOCUS_IN and _FOCUS_OUT are
   emitted
L. Krause 8 years ago
parent
commit
ca28c068c0

+ 1 - 1
platform/javascript/SCsub

@@ -19,7 +19,7 @@ javascript_objects = []
 for x in javascript_files:
     javascript_objects.append(env_javascript.Object(x))
 
-env.Append(LINKFLAGS=["-s", "EXPORTED_FUNCTIONS=\"['_main','_audio_server_mix_function','_main_after_fs_sync']\""])
+env.Append(LINKFLAGS=["-s", "EXPORTED_FUNCTIONS=\"['_main','_audio_server_mix_function','_main_after_fs_sync','_send_notification']\""])
 env.Append(LINKFLAGS=["--shell-file", '"platform/javascript/godot_shell.html"'])
 
 # output file name without file extension

+ 5 - 1
platform/javascript/godot_shell.html

@@ -83,6 +83,10 @@
 			color: white;
 		}
 
+		#canvas:focus {
+			outline: none;
+		}
+
 
 		/* Status display
 		 * ============== */
@@ -147,7 +151,7 @@ $GODOT_HEAD_INCLUDE
 </head>
 <body>
 	<div id="container">
-		<canvas id="canvas" width="640" height="480" onclick="canvas.ownerDocument.defaultView.focus();" oncontextmenu="event.preventDefault();">
+		<canvas id="canvas" width="640" height="480" tabindex="0" oncontextmenu="event.preventDefault();">
 			HTML5 canvas appears to be unsupported in the current browser.<br />
 			Please try updating or use a different browser.
 		</canvas>

+ 88 - 29
platform/javascript/os_javascript.cpp

@@ -145,6 +145,31 @@ static EM_BOOL _fullscreen_change_callback(int event_type, const EmscriptenFulls
 
 static InputDefault *_input;
 
+static bool is_canvas_focused() {
+
+	/* clang-format off */
+	return EM_ASM_INT_V(
+		return document.activeElement == Module.canvas;
+	);
+	/* clang-format on */
+}
+
+static void focus_canvas() {
+
+	/* clang-format off */
+	EM_ASM(
+		Module.canvas.focus();
+	);
+	/* clang-format on */
+}
+
+static bool _cursor_inside_canvas = true;
+
+static bool is_cursor_inside_canvas() {
+
+	return _cursor_inside_canvas;
+}
+
 static EM_BOOL _mousebutton_callback(int event_type, const EmscriptenMouseEvent *mouse_event, void *user_data) {
 
 	ERR_FAIL_COND_V(event_type != EMSCRIPTEN_EVENT_MOUSEDOWN && event_type != EMSCRIPTEN_EVENT_MOUSEUP, false);
@@ -164,26 +189,42 @@ static EM_BOOL _mousebutton_callback(int event_type, const EmscriptenMouseEvent
 	}
 
 	int mask = _input->get_mouse_button_mask();
-	if (ev->is_pressed())
+	if (ev->is_pressed()) {
+		// since the event is consumed, focus manually
+		if (!is_canvas_focused()) {
+			focus_canvas();
+		}
 		mask |= 1 << ev->get_button_index();
-	else
+	} else if (mask & (1 << ev->get_button_index())) {
 		mask &= ~(1 << ev->get_button_index());
+	} else {
+		// release event, but press was outside the canvas, so ignore
+		return false;
+	}
 	ev->set_button_mask(mask >> 1);
 
 	_input->parse_input_event(ev);
+	// prevent selection dragging
 	return true;
 }
 
 static EM_BOOL _mousemove_callback(int event_type, const EmscriptenMouseEvent *mouse_event, void *user_data) {
 
 	ERR_FAIL_COND_V(event_type != EMSCRIPTEN_EVENT_MOUSEMOVE, false);
+	OS_JavaScript *os = static_cast<OS_JavaScript *>(user_data);
+	int input_mask = _input->get_mouse_button_mask();
+	Point2 pos = Point2(mouse_event->canvasX, mouse_event->canvasY);
+	// outside the canvas, only read mouse movement if dragging started inside
+	// the canvas; imitating desktop app behaviour
+	if (!is_cursor_inside_canvas() && !input_mask)
+		return false;
 
 	Ref<InputEventMouseMotion> ev;
 	ev.instance();
 	dom2godot_mod(mouse_event, ev);
-	ev->set_button_mask(_input->get_mouse_button_mask() >> 1);
+	ev->set_button_mask(input_mask >> 1);
 
-	ev->set_position(Point2(mouse_event->canvasX, mouse_event->canvasY));
+	ev->set_position(pos);
 	ev->set_global_position(ev->get_position());
 
 	ev->set_relative(_input->get_mouse_position() - ev->get_position());
@@ -191,12 +232,20 @@ static EM_BOOL _mousemove_callback(int event_type, const EmscriptenMouseEvent *m
 	ev->set_speed(_input->get_last_mouse_speed());
 
 	_input->parse_input_event(ev);
-	return true;
+	// don't suppress mouseover/leave events
+	return false;
 }
 
 static EM_BOOL _wheel_callback(int event_type, const EmscriptenWheelEvent *wheel_event, void *user_data) {
 
 	ERR_FAIL_COND_V(event_type != EMSCRIPTEN_EVENT_WHEEL, false);
+	if (!is_canvas_focused()) {
+		if (is_cursor_inside_canvas()) {
+			focus_canvas();
+		} else {
+			return false;
+		}
+	}
 
 	Ref<InputEventMouseButton> ev;
 	ev.instance();
@@ -387,6 +436,15 @@ static EM_BOOL joy_callback_func(int p_type, const EmscriptenGamepadEvent *p_eve
 	return false;
 }
 
+extern "C" {
+void send_notification(int notif) {
+	if (notif == MainLoop::NOTIFICATION_WM_MOUSE_ENTER || notif == MainLoop::NOTIFICATION_WM_MOUSE_EXIT) {
+		_cursor_inside_canvas = notif == MainLoop::NOTIFICATION_WM_MOUSE_ENTER;
+	}
+	OS_JavaScript::get_singleton()->get_main_loop()->notification(notif);
+}
+}
+
 void OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) {
 
 	print_line("Init OS");
@@ -465,17 +523,17 @@ void OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, i
 	EM_CHECK(ev)
 
 	EMSCRIPTEN_RESULT result;
-	SET_EM_CALLBACK("#canvas", mousemove, _mousemove_callback)
+	SET_EM_CALLBACK("#window", mousemove, _mousemove_callback)
 	SET_EM_CALLBACK("#canvas", mousedown, _mousebutton_callback)
-	SET_EM_CALLBACK("#canvas", mouseup, _mousebutton_callback)
-	SET_EM_CALLBACK("#canvas", wheel, _wheel_callback)
-	SET_EM_CALLBACK("#canvas", touchstart, _touchpress_callback)
-	SET_EM_CALLBACK("#canvas", touchmove, _touchmove_callback)
-	SET_EM_CALLBACK("#canvas", touchend, _touchpress_callback)
-	SET_EM_CALLBACK("#canvas", touchcancel, _touchpress_callback)
-	SET_EM_CALLBACK(NULL, keydown, _keydown_callback)
-	SET_EM_CALLBACK(NULL, keypress, _keypress_callback)
-	SET_EM_CALLBACK(NULL, keyup, _keyup_callback)
+	SET_EM_CALLBACK("#window", mouseup, _mousebutton_callback)
+	SET_EM_CALLBACK("#window", wheel, _wheel_callback)
+	SET_EM_CALLBACK("#window", touchstart, _touchpress_callback)
+	SET_EM_CALLBACK("#window", touchmove, _touchmove_callback)
+	SET_EM_CALLBACK("#window", touchend, _touchpress_callback)
+	SET_EM_CALLBACK("#window", touchcancel, _touchpress_callback)
+	SET_EM_CALLBACK("#canvas", keydown, _keydown_callback)
+	SET_EM_CALLBACK("#canvas", keypress, _keypress_callback)
+	SET_EM_CALLBACK("#canvas", keyup, _keyup_callback)
 	SET_EM_CALLBACK(NULL, resize, _browser_resize_callback)
 	SET_EM_CALLBACK(NULL, fullscreenchange, _fullscreen_change_callback)
 	SET_EM_CALLBACK_NODATA(gamepadconnected, joy_callback_func)
@@ -485,6 +543,21 @@ void OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, i
 #undef SET_EM_CALLBACK
 #undef EM_CHECK
 
+	/* clang-format off */
+	EM_ASM_ARGS({
+		const send_notification = Module.cwrap('send_notification', null, ['number']);
+		const notifs = arguments;
+		(['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, i) {
+			Module.canvas.addEventListener(event, send_notification.bind(this, notifs[i]));
+		});
+	},
+		MainLoop::NOTIFICATION_WM_MOUSE_ENTER,
+		MainLoop::NOTIFICATION_WM_MOUSE_EXIT,
+		MainLoop::NOTIFICATION_WM_FOCUS_IN,
+		MainLoop::NOTIFICATION_WM_FOCUS_OUT
+	);
+/* clang-format on */
+
 #ifdef JAVASCRIPT_EVAL_ENABLED
 	javascript_eval = memnew(JavaScript);
 	GlobalConfig::get_singleton()->add_singleton(GlobalConfig::Singleton("JavaScript", javascript_eval));
@@ -777,20 +850,6 @@ void OS_JavaScript::main_loop_end() {
 		main_loop->finish();
 }
 
-void OS_JavaScript::main_loop_focusout() {
-
-	if (main_loop)
-		main_loop->notification(MainLoop::NOTIFICATION_WM_FOCUS_OUT);
-	//audio_driver_javascript.set_pause(true);
-}
-
-void OS_JavaScript::main_loop_focusin() {
-
-	if (main_loop)
-		main_loop->notification(MainLoop::NOTIFICATION_WM_FOCUS_IN);
-	//audio_driver_javascript.set_pause(false);
-}
-
 void OS_JavaScript::process_accelerometer(const Vector3 &p_accelerometer) {
 
 	input->set_accelerometer(p_accelerometer);