Fix chat text render and chat shiftlock issues

Fixes 106, Fixes #127, Refs #116
This commit is contained in:
komaruworld committed 2026-09-12 14:45:29 +03:00
1 parent c523048518
commit 0548ebd253
13 files changed
+206 -16

No files matched your search

+4 -1
View File
@@ -526,7 +526,10 @@ RobloxInputDispatchResult RobloxInputRouter::HandleEvent(
RobloxInputSnapshot RobloxInputRouter::Snapshot() const {
std::lock_guard<std::mutex> lock(mutex_);
return snapshot_;
RobloxInputSnapshot snapshot = snapshot_;
const RobloxTextEditorSnapshot text = text_editor_.Snapshot();
snapshot.text_focus_generation = text.focused ? text.generation : 0;
return snapshot;
}
RobloxInputDispatchResult RobloxInputRouter::HandleMouseMotionLocked(
+2 -4
View File
@@ -14,8 +14,6 @@ namespace mocktail {
namespace runtime {
namespace {
constexpr int kMinimumNativeWidth = 32;
constexpr int kMinimumNativeHeight = 18;
constexpr char kPasswordBullet[] = "\xE2\x80\xA2";
void SecureClear(std::string* value) {
@@ -83,8 +81,8 @@ bool ValidateAndCountUtf8(const char* text, std::size_t size,
bool NativeGeometryIntersectsViewport(
const RobloxTextDisplayUpdate& update,
const RobloxTextOverlayViewport& viewport) {
if (!viewport.valid() || update.area_width < kMinimumNativeWidth ||
update.area_height < kMinimumNativeHeight) {
if (!viewport.valid() || update.area_width <= 0 ||
update.area_height <= 0) {
return false;
}
const int64_t right = static_cast<int64_t>(update.area_x) +
@@ -142,6 +142,9 @@ class ProductionTextInputBackend final
return runtime_->EndTextFocusSession(textbox_handle, generation,
notify_native);
}
bool IsTextFocusSessionActive(uint64_t generation) override {
return runtime_->Snapshot().text_focus_generation == generation;
}
Status ReplaceFocusedTextFromEngine(uint64_t generation,
std::string authoritative_utf8) override {
return runtime_->ReplaceFocusedTextFromEngine(
@@ -559,6 +562,44 @@ struct RobloxTextInputJniBridge::State {
return CompleteTerminalOnMainThread("queue");
}
// RbxKeyboard hides its own editor after Enter/Escape. The native side
// need not echo hideKeyboard, so reconcile the host editor before polling
// properties or replaying delayed text updates from that focus session.
if (applied_active &&
!backend->IsTextFocusSessionActive(applied_generation)) {
const uint64_t ended_generation = applied_generation;
if (!backend->RequestHideTextInput(ended_generation)) {
for (Command& command : pending) command.ClearSensitiveData();
{
std::lock_guard<std::mutex> lock(mutex);
EnterTerminalLocked("SDL text-input completion hide was rejected");
}
return CompleteTerminalOnMainThread("host-focus-end");
}
{
std::lock_guard<std::mutex> lock(mutex);
if (active_generation == ended_generation) {
desired_active = false;
active_handle = 0;
active_info = {};
active_geometry = {};
has_active_geometry = false;
geometry_refresh_pumps = 0;
}
}
for (auto it = pending.begin(); it != pending.end();) {
if (it->generation == ended_generation) {
it->ClearSensitiveData();
it = pending.erase(it);
} else {
++it;
}
}
applied_active = false;
applied_generation = 0;
applied_handle = 0;
}
AppendGeometryRefresh(&pending);
bool success = true;
@@ -27,6 +27,7 @@ class RobloxTextInputJniBridgeBackend {
virtual Status EndTextFocusSession(int64_t textbox_handle,
uint64_t generation,
bool notify_native) = 0;
virtual bool IsTextFocusSessionActive(uint64_t generation) = 0;
virtual Status ReplaceFocusedTextFromEngine(
uint64_t generation, std::string authoritative_utf8) = 0;
virtual Status QueryCurrentTextBoxInfo(
+4 -5
View File
@@ -431,13 +431,12 @@ Status RobloxTextSurfaceOverlay::RasterizeLocked() {
static_cast<std::size_t>(width * height * 4), 0);
const int frame_width = static_cast<int>(width);
const int frame_height = static_cast<int>(height);
// Use Roblox's horizontal text bounds directly. Inventing padding from
// the field's width or position shifts text away from its native origin
// and makes long lines scroll before they reach the field's actual edge.
// Use Roblox's text bounds directly. Extra padding shifts the native text
// origin, scrolls long lines too early and clips compact chat fields.
const int clip_left = 0;
const int clip_right = frame_width;
const int clip_top = 3;
const int clip_bottom = std::max(clip_top, frame_height - 3);
const int clip_top = 0;
const int clip_bottom = frame_height;
const int available_width = std::max(1, clip_right - clip_left);
const bool wrapped_layout =
presentation.multiline || presentation.text_wrapped;
+8 -2
View File
@@ -52,6 +52,9 @@ void WindowPointerCaptureOwner::ClearQuery() {
}
bool WindowPointerCaptureOwner::Pump(bool text_input_active) {
if (!focused_) {
return Apply(false, true);
}
MouseLockQueryCallback callback = nullptr;
void* context = nullptr;
{
@@ -93,9 +96,12 @@ bool WindowPointerCaptureOwner::Pump(bool text_input_active) {
const bool client_active = query_ok;
const bool native_capture_active = client_active && native_lock_active;
const bool right_drag_active = right_button_held_;
// Roblox can keep Shift Lock active while chat has text focus. Honor that
// native lock so camera motion stays relative and cannot leave the window.
// Text editing only suppresses the host's fallback RMB camera capture.
const bool capture_requested =
(right_drag_active || native_capture_active) && !text_input_active;
// Text input releases capture, but Roblox still draws its own cursor.
native_capture_active || (right_drag_active && !text_input_active);
// When capture is released, Roblox still draws its own cursor.
// Show the system pointer only when the native client cannot provide one.
const bool cursor_visible = !client_active && !capture_requested;
return Apply(capture_requested, cursor_visible);
+4 -1
View File
@@ -120,7 +120,10 @@ bool WindowTextInputOwner::Pump() {
}
if (command.type == CommandType::kHide) {
if (!active_ || active_generation_ != command.generation) {
// A hide can replace both a pending show and the hide of the previous
// editor. It must also stop that older editor if the new show was never
// pumped (Roblox briefly focuses an empty TextBox when closing chat).
if (!active_ || active_generation_ > command.generation) {
return true;
}
return StopAndClear();