diff --git a/include/rex/ui/imgui_drawer.h b/include/rex/ui/imgui_drawer.h index ace4dd2..0e79e7e 100644 --- a/include/rex/ui/imgui_drawer.h +++ b/include/rex/ui/imgui_drawer.h @@ -77,8 +77,12 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer { void RenderDrawLists(ImDrawData* data, UIDrawContext& ui_draw_context); + void AddDialogImpl(ImGuiDialog* dialog); + void RemoveDialogImpl(ImGuiDialog* dialog); + void ClearInput(); void OnKey(KeyEvent& e, bool is_down); + static int MouseEventButtonToImGui(const MouseEvent& e); void UpdateMousePosition(float x, float y); void SwitchToPhysicalMouseAndUpdateMousePosition(const MouseEvent& e); @@ -108,6 +112,15 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer { // detaching the presenter. std::unique_ptr font_texture_; + // Bit mask of ImGui mouse buttons the drawer has seen pressed, used for + // window mouse capture bookkeeping (io.MouseDown can't be used - input is + // queued via the ImGui event API and only reflected at NewFrame). + uint32_t mouse_buttons_down_ = 0; + + // Whether the platform window has been asked to activate text input + // (character event delivery / IME) because a text widget is active. + bool text_input_active_ = false; + // If there's an active pointer, the ImGui mouse is controlled by this touch. // If it's TouchEvent::kPointerIDNone, the ImGui mouse is controlled by the // mouse. diff --git a/include/rex/ui/window.h b/include/rex/ui/window.h index 4bf2d7c..36ef5ae 100644 --- a/include/rex/ui/window.h +++ b/include/rex/ui/window.h @@ -311,6 +311,14 @@ class Window { // the cursor instantly. void SetCursorVisibility(CursorVisibility new_cursor_visibility); + // Enables or disables platform text input (character event delivery / IME) + // while a text widget is active. Characters are delivered to input listeners + // via OnKeyChar. On some platforms character events are always delivered + // (Win32 WM_CHAR, GTK) and this is a no-op; on SDL, character events are + // only delivered while text input is started, so this must be kept in sync + // by whoever hosts text widgets (done by ImGuiDrawer). + virtual void SetTextInputActive(bool /*active*/) {} + bool HasFocus() const { return HasActualState() ? has_focus_ : false; } // May be applied in a delayed way or dropped at all, HasFocus will not // necessarily be true immediately. diff --git a/include/rex/ui/window_sdl.h b/include/rex/ui/window_sdl.h index ad98b21..2e8c734 100644 --- a/include/rex/ui/window_sdl.h +++ b/include/rex/ui/window_sdl.h @@ -25,6 +25,8 @@ class SDLWindow final : public Window { static void HandleSDLEvent(const SDL_Event& event); + void SetTextInputActive(bool active) override; + protected: uint32_t GetLatestDpiImpl() const override; bool OpenImpl() override; @@ -51,6 +53,8 @@ class SDLWindow final : public Window { WindowDestructionReceiver& destruction_receiver); void HandleMouseWheel(const SDL_MouseWheelEvent& event, WindowDestructionReceiver& destruction_receiver); + void HandleTextInput(const SDL_TextInputEvent& event, + WindowDestructionReceiver& destruction_receiver); void RevealAutoHiddenCursor(); void SetCursorAutoHideTimer(); void RemoveCursorAutoHideTimer(); diff --git a/src/kernel/xam/xam_ui.cpp b/src/kernel/xam/xam_ui.cpp index 1ff5944..f08e31e 100644 --- a/src/kernel/xam/xam_ui.cpp +++ b/src/kernel/xam/xam_ui.cpp @@ -377,7 +377,9 @@ class KeyboardInputDialog : public XamDialog { } if (ImGui::InputText("##body", text_buffer_.data(), text_buffer_.size(), ImGuiInputTextFlags_EnterReturnsTrue)) { - text_ = std::string(text_buffer_.data(), text_buffer_.size()); + // Note: constructing from the pointer only - the buffer is + // NUL-padded to max_length, and (data, size) would keep the padding. + text_ = std::string(text_buffer_.data()); cancelled_ = false; ImGui::CloseCurrentPopup(); Close(); @@ -390,7 +392,7 @@ class KeyboardInputDialog : public XamDialog { } } if (ImGui::Button("OK")) { - text_ = std::string(text_buffer_.data(), text_buffer_.size()); + text_ = std::string(text_buffer_.data()); cancelled_ = false; ImGui::CloseCurrentPopup(); Close(); diff --git a/src/ui/imgui_drawer.cpp b/src/ui/imgui_drawer.cpp index b27de00..3b42f46 100644 --- a/src/ui/imgui_drawer.cpp +++ b/src/ui/imgui_drawer.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include @@ -44,11 +45,11 @@ ImGuiDrawer::~ImGuiDrawer() { SetPresenter(nullptr); if (!dialogs_.empty()) { window_->RemoveInputListener(this); - if (internal_state_) { - ImGui::SetCurrentContext(internal_state_); - if (touch_pointer_id_ == TouchEvent::kPointerIDNone && ImGui::IsAnyMouseDown()) { - window_->ReleaseMouse(); - } + if (touch_pointer_id_ == TouchEvent::kPointerIDNone && mouse_buttons_down_) { + window_->ReleaseMouse(); + } + if (text_input_active_) { + window_->SetTextInputActive(false); } } if (internal_state_) { @@ -59,6 +60,20 @@ ImGuiDrawer::~ImGuiDrawer() { void ImGuiDrawer::AddDialog(ImGuiDialog* dialog) { assert_not_null(dialog); + // Dialogs may be constructed on kernel/guest threads (XamShow*UI dispatch); + // the dialog list, the window input listener list and the presenter UI + // drawer list are all UI-thread state, so marshal to the UI thread. If the + // UI loop is already gone (shutdown), skip - the dialog will never be drawn + // and the dispatch path deletes it. + WindowedAppContext& app_context = window_->app_context(); + if (!app_context.IsInUIThread()) { + app_context.CallInUIThreadSynchronous([this, dialog]() { AddDialogImpl(dialog); }); + return; + } + AddDialogImpl(dialog); +} + +void ImGuiDrawer::AddDialogImpl(ImGuiDialog* dialog) { // Check if already added. if (std::find(dialogs_.cbegin(), dialogs_.cend(), dialog) != dialogs_.cend()) { return; @@ -78,6 +93,15 @@ void ImGuiDrawer::AddDialog(ImGuiDialog* dialog) { void ImGuiDrawer::RemoveDialog(ImGuiDialog* dialog) { assert_not_null(dialog); + WindowedAppContext& app_context = window_->app_context(); + if (!app_context.IsInUIThread()) { + app_context.CallInUIThreadSynchronous([this, dialog]() { RemoveDialogImpl(dialog); }); + return; + } + RemoveDialogImpl(dialog); +} + +void ImGuiDrawer::RemoveDialogImpl(ImGuiDialog* dialog) { auto it = std::find(dialogs_.cbegin(), dialogs_.cend(), dialog); if (it == dialogs_.cend()) { return; @@ -265,9 +289,19 @@ std::optional ImGuiDrawer::VirtualKeyToImGuiKey(VirtualKey vkey) { {ui::VirtualKey::kF11, ImGuiKey_F11}, {ui::VirtualKey::kF12, ImGuiKey_F12}, // Modifiers + // Win32 sends the generic VKs (WM_KEYDOWN gives VK_SHIFT etc.), SDL and + // GTK send the sided ones - both must be mapped. {ui::VirtualKey::kShift, ImGuiKey_LeftShift}, {ui::VirtualKey::kControl, ImGuiKey_LeftCtrl}, {ui::VirtualKey::kMenu, ImGuiKey_LeftAlt}, + {ui::VirtualKey::kLShift, ImGuiKey_LeftShift}, + {ui::VirtualKey::kRShift, ImGuiKey_RightShift}, + {ui::VirtualKey::kLControl, ImGuiKey_LeftCtrl}, + {ui::VirtualKey::kRControl, ImGuiKey_RightCtrl}, + {ui::VirtualKey::kLMenu, ImGuiKey_LeftAlt}, + {ui::VirtualKey::kRMenu, ImGuiKey_RightAlt}, + {ui::VirtualKey::kLWin, ImGuiKey_LeftSuper}, + {ui::VirtualKey::kRWin, ImGuiKey_RightSuper}, {ui::VirtualKey::kCapital, ImGuiKey_CapsLock}, {ui::VirtualKey::kNumLock, ImGuiKey_NumLock}, {ui::VirtualKey::kScroll, ImGuiKey_ScrollLock}, @@ -402,7 +436,15 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) { if (reset_mouse_position_after_next_frame_) { reset_mouse_position_after_next_frame_ = false; - io.MousePos = ImVec2(-FLT_MAX, -FLT_MAX); + io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); + } + + // Keep the platform text input state in sync with whether a text widget is + // active (SDL only delivers character events while text input is started). + bool want_text_input = io.WantTextInput && !dialogs_.empty(); + if (want_text_input != text_input_active_) { + text_input_active_ = want_text_input; + window_->SetTextInputActive(want_text_input); } // Detaching is deferred if the last dialog is removed during drawing, perform @@ -476,30 +518,40 @@ void ImGuiDrawer::OnKeyChar(KeyEvent& e) { } } +int ImGuiDrawer::MouseEventButtonToImGui(const MouseEvent& e) { + switch (e.button()) { + case rex::ui::MouseEvent::Button::kLeft: + return 0; + case rex::ui::MouseEvent::Button::kRight: + return 1; + default: + // Ignored. + return -1; + } +} + void ImGuiDrawer::OnMouseDown(MouseEvent& e) { SwitchToPhysicalMouseAndUpdateMousePosition(e); auto& io = GetIO(); - int button = -1; - switch (e.button()) { - case rex::ui::MouseEvent::Button::kLeft: { - button = 0; - break; - } - case rex::ui::MouseEvent::Button::kRight: { - button = 1; - break; - } - default: { - // Ignored. - break; - } - } - if (button >= 0 && button < std::size(io.MouseDown)) { - if (!io.MouseDown[button]) { - if (!ImGui::IsAnyMouseDown()) { + int button = MouseEventButtonToImGui(e); + if (button >= 0) { + if (!(mouse_buttons_down_ & (UINT32_C(1) << button))) { + if (!mouse_buttons_down_) { window_->CaptureMouse(); } - io.MouseDown[button] = true; + mouse_buttons_down_ |= UINT32_C(1) << button; + } + // Queue rather than write io.MouseDown directly: the event queue keeps the + // position-then-button ordering and spreads a same-frame press+release + // over multiple NewFrames, so clicks register at the position they + // actually happened at and fast clicks aren't lost when the UI frame rate + // lags behind input. + io.AddMouseButtonEvent(button, true); + if (io.WantCaptureMouse) { + // Keep presses over the UI out of lower-Z listeners (game input, + // keybinds). Releases are deliberately never eaten so lower listeners + // can't be left with a stuck button. + e.set_handled(true); } } } @@ -511,35 +563,25 @@ void ImGuiDrawer::OnMouseMove(MouseEvent& e) { void ImGuiDrawer::OnMouseUp(MouseEvent& e) { SwitchToPhysicalMouseAndUpdateMousePosition(e); auto& io = GetIO(); - int button = -1; - switch (e.button()) { - case rex::ui::MouseEvent::Button::kLeft: { - button = 0; - break; - } - case rex::ui::MouseEvent::Button::kRight: { - button = 1; - break; - } - default: { - // Ignored. - break; - } - } - if (button >= 0 && button < std::size(io.MouseDown)) { - if (io.MouseDown[button]) { - io.MouseDown[button] = false; - if (!ImGui::IsAnyMouseDown()) { + int button = MouseEventButtonToImGui(e); + if (button >= 0) { + if (mouse_buttons_down_ & (UINT32_C(1) << button)) { + mouse_buttons_down_ &= ~(UINT32_C(1) << button); + if (!mouse_buttons_down_) { window_->ReleaseMouse(); } } + io.AddMouseButtonEvent(button, false); } } void ImGuiDrawer::OnMouseWheel(MouseEvent& e) { SwitchToPhysicalMouseAndUpdateMousePosition(e); auto& io = GetIO(); - io.MouseWheel += float(e.scroll_y()) / float(MouseEvent::kScrollPerDetent); + io.AddMouseWheelEvent(0.0f, float(e.scroll_y()) / float(MouseEvent::kScrollPerDetent)); + if (io.WantCaptureMouse) { + e.set_handled(true); + } } void ImGuiDrawer::OnTouchEvent(TouchEvent& e) { @@ -550,8 +592,13 @@ void ImGuiDrawer::OnTouchEvent(TouchEvent& e) { // The latest pointer needs to be controlling the ImGui mouse. if (touch_pointer_id_ == TouchEvent::kPointerIDNone) { // Switching from the mouse to touch input. - if (ImGui::IsAnyMouseDown()) { - std::memset(io.MouseDown, 0, sizeof(io.MouseDown)); + if (mouse_buttons_down_) { + for (int button = 0; button < 32; ++button) { + if (mouse_buttons_down_ & (UINT32_C(1) << button)) { + io.AddMouseButtonEvent(button, false); + } + } + mouse_buttons_down_ = 0; window_->ReleaseMouse(); } } @@ -563,27 +610,32 @@ void ImGuiDrawer::OnTouchEvent(TouchEvent& e) { } UpdateMousePosition(e.x(), e.y()); if (action == TouchEvent::Action::kUp || action == TouchEvent::Action::kCancel) { - io.MouseDown[0] = false; + io.AddMouseButtonEvent(0, false); touch_pointer_id_ = TouchEvent::kPointerIDNone; // Make sure that after a touch, the ImGui mouse isn't hovering over // anything. reset_mouse_position_after_next_frame_ = true; } else { - io.MouseDown[0] = true; + io.AddMouseButtonEvent(0, true); reset_mouse_position_after_next_frame_ = false; } } void ImGuiDrawer::ClearInput() { auto& io = GetIO(); - if (touch_pointer_id_ == TouchEvent::kPointerIDNone && ImGui::IsAnyMouseDown()) { + if (touch_pointer_id_ == TouchEvent::kPointerIDNone && mouse_buttons_down_) { window_->ReleaseMouse(); } - io.MousePos = ImVec2(-FLT_MAX, -FLT_MAX); - std::memset(io.MouseDown, 0, sizeof(io.MouseDown)); + mouse_buttons_down_ = 0; + io.ClearEventsQueue(); io.ClearInputKeys(); + io.ClearInputMouse(); touch_pointer_id_ = TouchEvent::kPointerIDNone; reset_mouse_position_after_next_frame_ = false; + if (text_input_active_) { + text_input_active_ = false; + window_->SetTextInputActive(false); + } } void ImGuiDrawer::OnKey(KeyEvent& e, bool is_down) { @@ -594,35 +646,47 @@ void ImGuiDrawer::OnKey(KeyEvent& e, bool is_down) { } switch (virtual_key) { case VirtualKey::kShift: - io.KeyShift = is_down; + case VirtualKey::kLShift: + case VirtualKey::kRShift: + io.AddKeyEvent(ImGuiMod_Shift, is_down); break; case VirtualKey::kControl: - io.KeyCtrl = is_down; + case VirtualKey::kLControl: + case VirtualKey::kRControl: + io.AddKeyEvent(ImGuiMod_Ctrl, is_down); break; case VirtualKey::kMenu: - // FIXME(Triang3l): Doesn't work in xenia-ui-window-demo. - io.KeyAlt = is_down; + case VirtualKey::kLMenu: + case VirtualKey::kRMenu: + io.AddKeyEvent(ImGuiMod_Alt, is_down); break; case VirtualKey::kLWin: - io.KeySuper = is_down; + case VirtualKey::kRWin: + io.AddKeyEvent(ImGuiMod_Super, is_down); break; default: break; } + // While a text field is active, keep key presses away from app keybinds and + // the game (typing a name must not toggle overlays or drive MnK input). + // Releases always propagate so lower listeners can't get stuck keys. + if (is_down && io.WantTextInput) { + e.set_handled(true); + } } void ImGuiDrawer::UpdateMousePosition(float x, float y) { auto& io = GetIO(); float physical_to_logical = float(window_->GetMediumDpi()) / float(window_->GetDpi()); - io.MousePos.x = x * physical_to_logical; - io.MousePos.y = y * physical_to_logical; + io.AddMousePosEvent(x * physical_to_logical, y * physical_to_logical); } void ImGuiDrawer::SwitchToPhysicalMouseAndUpdateMousePosition(const MouseEvent& e) { if (touch_pointer_id_ != TouchEvent::kPointerIDNone) { touch_pointer_id_ = TouchEvent::kPointerIDNone; auto& io = GetIO(); - std::memset(io.MouseDown, 0, sizeof(io.MouseDown)); + // Release the ImGui touch-driven button. + io.AddMouseButtonEvent(0, false); // Nothing needs to be done regarding CaptureMouse and ReleaseMouse - all // buttons as well as mouse capture have been released when switching to // touch input, the mouse is never captured during touch input, and now diff --git a/src/ui/window_sdl.cpp b/src/ui/window_sdl.cpp index 0cb7202..e410212 100644 --- a/src/ui/window_sdl.cpp +++ b/src/ui/window_sdl.cpp @@ -376,6 +376,9 @@ void SDLWindow::HandleSDLEvent(const SDL_Event& event) { case SDL_EVENT_KEY_UP: window_id = event.key.windowID; break; + case SDL_EVENT_TEXT_INPUT: + window_id = event.text.windowID; + break; case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: window_id = event.button.windowID; @@ -589,6 +592,9 @@ void SDLWindow::HandleEvent(const SDL_Event& event) { case SDL_EVENT_KEY_UP: HandleKey(event.key, false, destruction_receiver); break; + case SDL_EVENT_TEXT_INPUT: + HandleTextInput(event.text, destruction_receiver); + break; case SDL_EVENT_MOUSE_BUTTON_DOWN: HandleMouseButton(event.button, true, destruction_receiver); break; @@ -662,6 +668,68 @@ void SDLWindow::HandleMouseMotion(const SDL_MouseMotionEvent& event, OnMouseMove(e, destruction_receiver); } +void SDLWindow::HandleTextInput(const SDL_TextInputEvent& event, + WindowDestructionReceiver& destruction_receiver) { + if (!event.text) { + return; + } + SDL_Keymod mod = SDL_GetModState(); + const uint8_t* s = reinterpret_cast(event.text); + while (*s) { + // Decode one UTF-8 codepoint. + uint32_t codepoint = 0; + uint32_t continuation_count = 0; + uint8_t lead = *s++; + if (lead < 0x80) { + codepoint = lead; + } else if ((lead & 0xE0) == 0xC0) { + codepoint = lead & 0x1F; + continuation_count = 1; + } else if ((lead & 0xF0) == 0xE0) { + codepoint = lead & 0x0F; + continuation_count = 2; + } else if ((lead & 0xF8) == 0xF0) { + codepoint = lead & 0x07; + continuation_count = 3; + } else { + // Stray continuation or invalid lead byte - skip. + continue; + } + bool valid = true; + for (uint32_t i = 0; i < continuation_count; ++i) { + uint8_t continuation = *s; + if ((continuation & 0xC0) != 0x80) { + valid = false; + break; + } + codepoint = (codepoint << 6) | (continuation & 0x3F); + ++s; + } + if (!valid || !codepoint) { + continue; + } + // Mirrors the Win32 WM_CHAR path - the character is carried in the + // KeyEvent's virtual key field. + KeyEvent e(this, VirtualKey(codepoint), 1, false, (mod & SDL_KMOD_SHIFT) != 0, + (mod & SDL_KMOD_CTRL) != 0, (mod & SDL_KMOD_ALT) != 0, (mod & SDL_KMOD_GUI) != 0); + OnKeyChar(e, destruction_receiver); + if (destruction_receiver.IsWindowDestroyedOrClosed()) { + return; + } + } +} + +void SDLWindow::SetTextInputActive(bool active) { + if (!window_) { + return; + } + if (active) { + SDL_StartTextInput(window_); + } else { + SDL_StopTextInput(window_); + } +} + void SDLWindow::HandleMouseWheel(const SDL_MouseWheelEvent& event, WindowDestructionReceiver& destruction_receiver) { RevealAutoHiddenCursor();