diff --git a/code/framework/src/gui/cef/renderer_app.cpp b/code/framework/src/gui/cef/renderer_app.cpp index 1db4347ac..6acd0da04 100644 --- a/code/framework/src/gui/cef/renderer_app.cpp +++ b/code/framework/src/gui/cef/renderer_app.cpp @@ -54,6 +54,12 @@ namespace Framework::GUI::CEF { CefRefPtr handler = new CallEventHandler(browser); CefRefPtr func = CefV8Value::CreateFunction("callEvent", handler); global->SetValue("callEvent", func, V8_PROPERTY_ATTRIBUTE_NONE); + + // Each document starts with nothing focused; without this the dedupe in + // OnFocusedNodeChanged would swallow the first focus after a same-process navigation. + if (frame && frame->IsMain()) { + _inputFocus[browser->GetIdentifier()] = false; + } } // The DOM is only reachable here, so the browser process cannot decide this for itself. @@ -62,7 +68,9 @@ namespace Framework::GUI::CEF { return; } - const bool typing = node && node->GetType() == CefDOMNode::Type::DOM_NODE_TYPE_ELEMENT && node->GetFormControlElementType() != CefDOMNode::FormControlType::DOM_FORM_CONTROL_TYPE_UNSUPPORTED; + // IsEditable: text inputs, textareas and contenteditable. A form-control test would count + // buttons, checkboxes and selects as typing, and miss rich-text editors. + const bool typing = node && node->IsEditable(); bool &previous = _inputFocus[browser->GetIdentifier()]; if (previous == typing) { diff --git a/code/framework/src/gui/cef/renderer_app.h b/code/framework/src/gui/cef/renderer_app.h index 414486114..07bf32b78 100644 --- a/code/framework/src/gui/cef/renderer_app.h +++ b/code/framework/src/gui/cef/renderer_app.h @@ -30,7 +30,7 @@ namespace Framework::GUI::CEF { class RendererApp final: public CefApp, public CefRenderProcessHandler { private: - // browser id -> form control focused; a renderer process can host several browsers + // browser id -> editable node focused; a renderer process can host several browsers std::unordered_map _inputFocus; public: diff --git a/code/framework/src/gui/manager.cpp b/code/framework/src/gui/manager.cpp index 50a37c689..2559a2bc2 100644 --- a/code/framework/src/gui/manager.cpp +++ b/code/framework/src/gui/manager.cpp @@ -578,6 +578,16 @@ namespace Framework::GUI { } } + bool Manager::IsAnyTextInputFocused() const { + std::scoped_lock lock(_renderMutex); + for (const auto &view : _views) { + if (view->HasFocus() && view->ShouldDisplay() && view->IsTextInputFocused()) { + return true; + } + } + return false; + } + bool Manager::IsAnyViewFocused() const { std::scoped_lock lock(_renderMutex); for (const auto &view : _views) { diff --git a/code/framework/src/gui/manager.h b/code/framework/src/gui/manager.h index ed556fa82..af5a53173 100644 --- a/code/framework/src/gui/manager.h +++ b/code/framework/src/gui/manager.h @@ -81,6 +81,11 @@ namespace Framework::GUI { void CleanupViews(); bool IsAnyViewFocused() const; + + // Whether a shown, focused view has the caret in an editable node, as opposed to merely + // holding focus. The predicate input gating wants: a focused HUD must not eat keybinds or + // mute voice. + bool IsAnyTextInputFocused() const; bool IsAnyGCViewFocused() const; std::vector GetAllViews() const; diff --git a/code/framework/src/gui/view.cpp b/code/framework/src/gui/view.cpp index c09fe9ea2..0ff4900fc 100644 --- a/code/framework/src/gui/view.cpp +++ b/code/framework/src/gui/view.cpp @@ -124,6 +124,16 @@ namespace Framework::GUI { _created = true; } + if (data.event == ViewEvent::InputFocusChange) { + _textInputFocused = data.focused; + } + + // A new document starts with nothing focused; a cross-process navigation never reports + // the old caret going away. + if (data.event == ViewEvent::LoadingStart && data.isMainFrame) { + _textInputFocused = false; + } + // window object exists from main-frame load start; bind before anything talks to the page if (data.event == ViewEvent::LoadingStart && data.isMainFrame && _sdk && _browser) { (void)_sdk->Init(_browser); diff --git a/code/framework/src/gui/view.h b/code/framework/src/gui/view.h index 8d27a329c..215ab511f 100644 --- a/code/framework/src/gui/view.h +++ b/code/framework/src/gui/view.h @@ -61,8 +61,9 @@ namespace Framework::GUI { // CPU renderer fallback std::vector _pixelData; - bool _gpuAccelerated = false; - bool _hasFocus = false; + bool _gpuAccelerated = false; + bool _hasFocus = false; + bool _textInputFocused = false; int _x; int _y; int _z; @@ -109,6 +110,13 @@ namespace Framework::GUI { } } + // The DOM half of focus: an editable node holds the caret. Keys only reach a page while + // HasFocus() && ShouldDisplay(), so gate on all three. Not cleared by Focus(false): the DOM + // keeps its caret across it, and reports no transition when the view is refocused. + bool IsTextInputFocused() const { + return _textInputFocused; + } + bool HasFocus() const { return _hasFocus; } diff --git a/code/framework/src/integrations/client/instance.cpp b/code/framework/src/integrations/client/instance.cpp index 8b2f04216..6d36b44d2 100644 --- a/code/framework/src/integrations/client/instance.cpp +++ b/code/framework/src/integrations/client/instance.cpp @@ -385,7 +385,8 @@ namespace Framework::Integrations::Client { if (!_chatBox.IsSessionActive() || _chatBox.IsInputActive()) { return false; } - if (_webManager && _webManager->IsAnyViewFocused()) { + // Text entry, not view focus: a focused HUD or menu leaves keybinds and push-to-talk alone. + if (_webManager && _webManager->IsAnyTextInputFocused()) { return false; } #ifdef _WIN32 @@ -565,9 +566,10 @@ namespace Framework::Integrations::Client { { FW_PROFILE_SCOPE_N("Client::Voice"); - // The chat box and web views belong to the framework, so it enforces this itself - // rather than trusting every mod to remember. - _voiceClient.SetInputSuppressed(_chatBox.IsInputActive() || (_webManager && _webManager->IsAnyViewFocused())); + // The chat box and web views belong to the framework, so it enforces this itself rather + // than trusting every mod to remember. Text entry only: a focused HUD or menu must not + // mute the player. + _voiceClient.SetInputSuppressed(_chatBox.IsInputActive() || (_webManager && _webManager->IsAnyTextInputFocused())); // Speaker positions come from the replicated entity set, as the server's voice // router gets them: an owner GUID means a player-controlled entity. Done here so diff --git a/code/framework/src/integrations/client/scripting/builtins/web.cpp b/code/framework/src/integrations/client/scripting/builtins/web.cpp index 97d7a7729..da128c7eb 100644 --- a/code/framework/src/integrations/client/scripting/builtins/web.cpp +++ b/code/framework/src/integrations/client/scripting/builtins/web.cpp @@ -369,7 +369,7 @@ namespace Framework::Integrations::Client::Scripting::Builtins { {{"cursor", "string", "CSS-style cursor name, or \"custom\" for shapes without one."}, {"cursorType", "number", "Raw CEF cursor type."}}, "Dispatched when an owned view's requested cursor shape changes."); describe(Framework::GUI::ViewEvent::Tooltip, "BrowserTooltipEvent", "A page wants to display a tooltip.", {{"text", "string", "Tooltip text; empty when the tooltip is dismissed."}}, "Dispatched when an owned view requests a tooltip; windowless rendering draws none, so the script must."); - describe(Framework::GUI::ViewEvent::InputFocusChange, "BrowserInputFocusChangeEvent", "A form control inside a page gained or lost focus.", {{"focused", "boolean", "True while the page holds keyboard input."}}, + describe(Framework::GUI::ViewEvent::InputFocusChange, "BrowserInputFocusChangeEvent", "An editable element inside a page gained or lost focus.", {{"focused", "boolean", "True while the page holds keyboard input."}}, "Dispatched when focus enters or leaves an editable element of an owned view; use it to stop routing keys to the game."); describe(Framework::GUI::ViewEvent::ResourceBlocked, "BrowserResourceBlockedEvent", "A web view refused a request.", {{"url", "string", "URL that was refused."}, {"domain", "string", "Host component of that URL, empty when unparsable."}, {"reason", "\"cross-origin\" | \"invalid-url\" | \"host-filter\" | \"foreign-event\"", "Why the request was refused."}}, diff --git a/code/framework/src/voice/client/voice_client.h b/code/framework/src/voice/client/voice_client.h index af5fc5559..d936f505e 100644 --- a/code/framework/src/voice/client/voice_client.h +++ b/code/framework/src/voice/client/voice_client.h @@ -247,7 +247,7 @@ namespace Framework::Voice { void RemoveSpeaker(uint64_t speaker); // Blocks transmission regardless of push-to-talk, cutting the release delay short. Set by - // the client Instance while its chat box has the caret or a web view holds focus. + // the client Instance while its chat box or a web view's editable node has the caret. void SetInputSuppressed(bool suppressed); // The mod-owned half of the same block: window focus, a game menu, locked controls. Ored