From 4a12554bf4e197aa9c1f6e46d8218b226e13c8e3 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Thu, 30 Apr 2026 00:06:27 -0600 Subject: [PATCH] UI: Mobile keyboard fixes, safe area padding, & more --- extern/aurora | 2 +- res/rml/window.rcss | 20 +++++++++ src/dusk/ui/component.cpp | 2 +- src/dusk/ui/string_button.cpp | 34 +++++++++++--- src/dusk/ui/string_button.hpp | 4 +- src/dusk/ui/window.cpp | 84 +++++++++++++++++++++++++++++++++++ src/dusk/ui/window.hpp | 13 ++++++ src/m_Do/m_Do_main.cpp | 4 +- 8 files changed, 152 insertions(+), 11 deletions(-) diff --git a/extern/aurora b/extern/aurora index 1fed316829..a2c431d084 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 1fed316829004084aeae75e8a4c4315bd33e4890 +Subproject commit a2c431d0840404aef49a808a2f95a97bf45aad38 diff --git a/res/rml/window.rcss b/res/rml/window.rcss index 6830060116..6f4cf5b777 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -46,6 +46,8 @@ button { display: flex; flex: 0 0 64dp; height: 64dp; + min-width: 0; + overflow: auto hidden; background-color: rgba(217, 217, 217, 10%); font-family: "Fira Sans Condensed"; font-weight: bold; @@ -55,9 +57,11 @@ button { } .window .tab-bar .tab { + flex: 0 0 auto; padding: 0 24dp; line-height: 64dp; opacity: 0.25; + white-space: nowrap; decorator: vertical-gradient(#c2a42d00 #c2a42d00); transition: decorator 0.1s linear-in-out, opacity 0.1s linear-in-out; } @@ -144,6 +148,22 @@ scrollbarvertical sliderbar:active { background-color: rgba(194, 164, 45, 80%); } +scrollbarhorizontal { + height: 0; +} + +scrollbarhorizontal sliderarrowdec, +scrollbarhorizontal sliderarrowinc { + width: 0; + height: 0; +} + +scrollbarhorizontal slidertrack, +scrollbarhorizontal sliderbar { + width: 0; + height: 0; +} + .section-heading { font-weight: bold; text-transform: uppercase; diff --git a/src/dusk/ui/component.cpp b/src/dusk/ui/component.cpp index 6435309a57..363bf524bf 100644 --- a/src/dusk/ui/component.cpp +++ b/src/dusk/ui/component.cpp @@ -17,7 +17,7 @@ bool Component::focus() { if (mRoot->Focus(true)) { mRoot->ScrollIntoView(Rml::ScrollIntoViewOptions{ Rml::ScrollAlignment::Center, - Rml::ScrollAlignment::Nearest, + Rml::ScrollAlignment::Center, Rml::ScrollBehavior::Smooth, Rml::ScrollParentage::Closest, }); diff --git a/src/dusk/ui/string_button.cpp b/src/dusk/ui/string_button.cpp index 60b80b437e..8fef0ed1ba 100644 --- a/src/dusk/ui/string_button.cpp +++ b/src/dusk/ui/string_button.cpp @@ -1,5 +1,7 @@ #include "string_button.hpp" +#include + namespace dusk::ui { BaseStringButton::BaseStringButton(Rml::Element* parent, Props props) @@ -12,6 +14,12 @@ void BaseStringButton::update() { if (mPendingStopEditing) { stop_editing(mPendingCommit, mPendingRefocusRoot); } + if (mPendingInputFocusFrames > 0) { + --mPendingInputFocusFrames; + if (mPendingInputFocusFrames == 0) { + focus_input(); + } + } ControlledSelectButton::update(); } @@ -37,10 +45,9 @@ void BaseStringButton::start_editing() { // Hide value element mValueElem->SetProperty(Rml::PropertyId::Visibility, Rml::Style::Visibility::Hidden); - // Focus and select text within input - mInputElem->Focus(true); - const int end = static_cast(Rml::StringUtilities::LengthUTF8(mInputElem->GetValue())); - mInputElem->SetSelectionRange(0, end); + // RmlUi lays out the new input during render. Wait one full frame before focusing it so + // mobile keyboard placement gets a valid caret rectangle. + mPendingInputFocusFrames = 2; // Mark button as selected to indicate "active" set_selected(true); @@ -84,11 +91,26 @@ bool BaseStringButton::handle_nav_command(NavCommand cmd) { return false; } -void BaseStringButton::stop_editing(bool commit, bool refocusRoot) { +void BaseStringButton::focus_input() { if (mInputElem == nullptr) { return; } + + aurora::rmlui::set_input_type( + mType == "number" ? aurora::rmlui::InputType::Number : aurora::rmlui::InputType::Text); + + if (mInputElem->Focus(true)) { + const int end = static_cast(Rml::StringUtilities::LengthUTF8(mInputElem->GetValue())); + mInputElem->SetSelectionRange(0, end); + } +} + +void BaseStringButton::stop_editing(bool commit, bool refocusRoot) { mPendingStopEditing = false; + mPendingInputFocusFrames = 0; + if (mInputElem == nullptr) { + return; + } if (commit) { set_value(mInputElem->GetValue()); } @@ -109,4 +131,4 @@ StringButton::StringButton(Rml::Element* parent, Props props) : BaseStringButton(parent, {.key = std::move(props.key), .maxLength = props.maxLength}), mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)) {} -} // namespace dusk::ui \ No newline at end of file +} // namespace dusk::ui diff --git a/src/dusk/ui/string_button.hpp b/src/dusk/ui/string_button.hpp index 7fbd7350dc..303fe19772 100644 --- a/src/dusk/ui/string_button.hpp +++ b/src/dusk/ui/string_button.hpp @@ -24,12 +24,14 @@ protected: virtual void set_value(Rml::String value) = 0; private: + void focus_input(); void stop_editing(bool commit = true, bool refocusRoot = false); Rml::ElementFormControlInput* mInputElem = nullptr; std::vector > mInputListeners; Rml::String mType; int mMaxLength; + int mPendingInputFocusFrames = 0; bool mPendingStopEditing = false; bool mPendingCommit = true; bool mPendingRefocusRoot = false; @@ -59,4 +61,4 @@ private: std::function mSetValue; }; -} // namespace dusk::ui \ No newline at end of file +} // namespace dusk::ui diff --git a/src/dusk/ui/window.cpp b/src/dusk/ui/window.cpp index 84e36a0396..2b6669e558 100644 --- a/src/dusk/ui/window.cpp +++ b/src/dusk/ui/window.cpp @@ -1,13 +1,67 @@ #include "window.hpp" #include +#include +#include "aurora/lib/window.hpp" #include "aurora/rmlui.hpp" #include "button.hpp" #include "magic_enum.hpp" #include "ui.hpp" +#include +#include + namespace dusk::ui { +namespace { + +float base_body_padding(Rml::Context* context) noexcept { + if (context == nullptr) { + return 64.0f; + } + const float dpRatio = std::max(context->GetDensityIndependentPixelRatio(), 0.001f); + const float heightDp = static_cast(context->GetDimensions().y) / dpRatio; + if (heightDp <= 640.0f) { + return 16.0f * dpRatio; + } + return 64.0f * dpRatio; +} + +Window::Insets safe_area_insets(Rml::Context* context) noexcept { + if (context == nullptr) { + return {}; + } + + auto* window = aurora::window::get_sdl_window(); + if (window == nullptr) { + return {}; + } + + const AuroraWindowSize windowSize = aurora::window::get_window_size(); + if (windowSize.width == 0 || windowSize.height == 0) { + return {}; + } + + SDL_Rect safeRect{}; + if (!SDL_GetWindowSafeArea(window, &safeRect)) { + return {}; + } + + const Rml::Vector2i contextSize = context->GetDimensions(); + const float scaleX = static_cast(contextSize.x) / static_cast(windowSize.width); + const float scaleY = static_cast(contextSize.y) / static_cast(windowSize.height); + + const float safeRight = static_cast(safeRect.x + safeRect.w); + const float safeBottom = static_cast(safeRect.y + safeRect.h); + return { + .top = std::max(0.0f, static_cast(safeRect.y)) * scaleY, + .right = std::max(0.0f, static_cast(windowSize.width) - safeRight) * scaleX, + .bottom = std::max(0.0f, static_cast(windowSize.height) - safeBottom) * scaleY, + .left = std::max(0.0f, static_cast(safeRect.x)) * scaleX, + }; +} + +} // namespace Window::Window() { auto* context = aurora::rmlui::get_context(); @@ -74,11 +128,41 @@ void Window::hide() { } void Window::update() { + update_safe_area(); for (const auto& component : mContentComponents) { component->update(); } } +void Window::update_safe_area() noexcept { + if (mDocument == nullptr) { + return; + } + + Rml::Context* context = mDocument->GetContext(); + const float basePadding = base_body_padding(context); + Insets safeInsets = safe_area_insets(context); + safeInsets = { + std::round(std::max(basePadding, safeInsets.top)), + std::round(std::max(basePadding, safeInsets.right)), + std::round(std::max(basePadding, safeInsets.bottom)), + std::round(std::max(basePadding, safeInsets.left)), + }; + if (safeInsets == mBodyPadding) { + return; + } + + mBodyPadding = safeInsets; + mDocument->SetProperty( + Rml::PropertyId::PaddingTop, Rml::Property(safeInsets.top, Rml::Unit::PX)); + mDocument->SetProperty( + Rml::PropertyId::PaddingRight, Rml::Property(safeInsets.right, Rml::Unit::PX)); + mDocument->SetProperty( + Rml::PropertyId::PaddingBottom, Rml::Property(safeInsets.bottom, Rml::Unit::PX)); + mDocument->SetProperty( + Rml::PropertyId::PaddingLeft, Rml::Property(safeInsets.left, Rml::Unit::PX)); +} + bool Window::set_active_tab(int index) { if (index < 0 || index >= mTabs.size() || index == mSelectedTabIndex) { return false; diff --git a/src/dusk/ui/window.hpp b/src/dusk/ui/window.hpp index 33be77af1a..cfa29ebd4e 100644 --- a/src/dusk/ui/window.hpp +++ b/src/dusk/ui/window.hpp @@ -17,6 +17,17 @@ public: std::unique_ptr