mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 23:00:46 +02:00
When Roblox asks for a keyboard (showKeyboard), a world-locked panel opens below eye level as an OpenXR quad layer. Controllers point at keys and the trigger types; text and key presses go through the same platform events as the desktop keyboard, so the focused TextBox edits identically. The layout is US QWERTY without the function row or Ctrl/Alt, with one-shot Shift, Caps Lock, Tab, Backspace auto-repeat, Enter and Close. A trigger aimed at the panel is withheld from Roblox so a key press is not also a click. A text field above the keys mirrors the focused TextBox, whose desktop overlay never reaches the eye images. MOCKTAIL_VR_KEYBOARD=0 turns it off.
204 lines
6.9 KiB
C++
204 lines
6.9 KiB
C++
#ifndef MOCKTAIL_RUNTIME_ROBLOX_TEXT_EDITOR_H_
|
|
#define MOCKTAIL_RUNTIME_ROBLOX_TEXT_EDITOR_H_
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <deque>
|
|
#include <mutex>
|
|
#include <string>
|
|
|
|
#include "mocktail/platform/platform_runtime.h"
|
|
#include "mocktail/platform/text_clipboard.h"
|
|
#include "mocktail/status.h"
|
|
|
|
namespace mocktail {
|
|
namespace runtime {
|
|
|
|
struct RobloxTextFocusSession {
|
|
int64_t textbox_handle = 0;
|
|
uint64_t generation = 0;
|
|
std::string initial_text;
|
|
bool manual_focus_release = false;
|
|
bool multiline = false;
|
|
int32_t area_x = 0;
|
|
int32_t area_y = 0;
|
|
int32_t area_width = 0;
|
|
int32_t area_height = 0;
|
|
float font_size = 0.0F;
|
|
int32_t x_alignment = 0;
|
|
int32_t y_alignment = 1;
|
|
int32_t text_color = 0;
|
|
int32_t font = 0;
|
|
int32_t text_input_type = 0;
|
|
int32_t return_key_type = 0;
|
|
bool text_wrapped = false;
|
|
};
|
|
|
|
// Non-text properties returned by NativeGLInterface.nativeGetTextBoxInfo.
|
|
// Keeping them separate from the focus session lets the Android property
|
|
// callback refresh layout without replacing text or creating a new editor
|
|
// generation.
|
|
struct RobloxTextFocusProperties {
|
|
bool manual_focus_release = false;
|
|
bool multiline = false;
|
|
int32_t area_x = 0;
|
|
int32_t area_y = 0;
|
|
int32_t area_width = 0;
|
|
int32_t area_height = 0;
|
|
float font_size = 0.0F;
|
|
int32_t x_alignment = 0;
|
|
int32_t y_alignment = 1;
|
|
int32_t text_color = 0;
|
|
int32_t font = 0;
|
|
int32_t text_input_type = 0;
|
|
int32_t return_key_type = 0;
|
|
bool text_wrapped = false;
|
|
};
|
|
|
|
enum class RobloxTextDisplayEvent { kShow, kUpdate, kHide };
|
|
|
|
// Sensitive, synchronous display snapshot. Observers must copy what they need
|
|
// during the call, never log `utf8`, and securely clear any retained copy.
|
|
struct RobloxTextDisplayUpdate {
|
|
RobloxTextDisplayEvent event = RobloxTextDisplayEvent::kHide;
|
|
uint64_t generation = 0;
|
|
int32_t area_x = 0;
|
|
int32_t area_y = 0;
|
|
int32_t area_width = 0;
|
|
int32_t area_height = 0;
|
|
float font_size = 0.0F;
|
|
bool multiline = false;
|
|
int32_t x_alignment = 0;
|
|
int32_t y_alignment = 1;
|
|
int32_t text_color = 0;
|
|
int32_t font = 0;
|
|
int32_t text_input_type = 0;
|
|
bool text_wrapped = false;
|
|
const char* utf8 = nullptr;
|
|
std::size_t utf8_size = 0;
|
|
int32_t cursor_utf16 = 0;
|
|
int32_t selection_begin_utf16 = 0;
|
|
int32_t selection_end_utf16 = 0;
|
|
};
|
|
|
|
struct RobloxTextDisplaySink {
|
|
using UpdateFn = void (*)(void* context,
|
|
const RobloxTextDisplayUpdate& update);
|
|
|
|
void* context = nullptr;
|
|
UpdateFn update = nullptr;
|
|
};
|
|
|
|
// Process-wide second observer of the focused TextBox display, called
|
|
// synchronously after the surface overlay with the same sensitivity contract.
|
|
// The VR keyboard uses it to show the text being typed in the headset, where
|
|
// the desktop overlay is not visible. Passing an empty sink clears it.
|
|
void SetRobloxTextDisplayMirror(RobloxTextDisplaySink sink);
|
|
|
|
struct RobloxTextSink {
|
|
using SyncFn = Status (*)(void* context, const char* utf8, std::size_t size,
|
|
int32_t cursor_utf16);
|
|
using PassTextFn = Status (*)(void* context, int64_t textbox_handle,
|
|
const char* utf8, std::size_t size,
|
|
bool finished, int32_t cursor_utf16);
|
|
using HandleFn = Status (*)(void* context, int64_t textbox_handle);
|
|
|
|
void* context = nullptr;
|
|
SyncFn sync = nullptr;
|
|
PassTextFn pass_text = nullptr;
|
|
HandleFn return_pressed = nullptr;
|
|
HandleFn release_focus = nullptr;
|
|
};
|
|
|
|
struct RobloxTextEditResult {
|
|
bool handled = false;
|
|
bool dispatched = false;
|
|
bool session_ended = false;
|
|
Status status = Status::Ok();
|
|
};
|
|
|
|
struct RobloxTextEditorSnapshot {
|
|
bool focused = false;
|
|
int64_t textbox_handle = 0;
|
|
uint64_t generation = 0;
|
|
std::size_t text_bytes = 0;
|
|
int32_t cursor_utf16 = 0;
|
|
int32_t selection_begin_utf16 = 0;
|
|
int32_t selection_end_utf16 = 0;
|
|
uint64_t committed_edits = 0;
|
|
uint64_t native_errors = 0;
|
|
};
|
|
|
|
// Thread-safe UTF-8 shadow editor for one Roblox TextBox focus generation.
|
|
// Text content is intentionally exposed only as a byte count in snapshots.
|
|
class RobloxTextEditor final {
|
|
public:
|
|
explicit RobloxTextEditor(RobloxTextSink sink,
|
|
RobloxTextDisplaySink display_sink = {},
|
|
platform::TextClipboard* clipboard = nullptr);
|
|
|
|
RobloxTextEditor(const RobloxTextEditor&) = delete;
|
|
RobloxTextEditor& operator=(const RobloxTextEditor&) = delete;
|
|
|
|
Status BeginFocusSession(RobloxTextFocusSession session);
|
|
Status EndFocusSession(int64_t textbox_handle, uint64_t generation,
|
|
bool notify_native);
|
|
Status ReplaceFocusedTextFromEngine(uint64_t generation,
|
|
std::string authoritative_utf8);
|
|
Status UpdateFocusProperties(uint64_t generation,
|
|
const RobloxTextFocusProperties& properties);
|
|
RobloxTextEditResult HandleTextInput(const platform::TextInputEvent& event);
|
|
RobloxTextEditResult HandleTextEditing(
|
|
const platform::TextEditingEvent& event);
|
|
RobloxTextEditResult HandleKey(const platform::KeyEvent& event);
|
|
RobloxTextEditorSnapshot Snapshot() const;
|
|
|
|
private:
|
|
RobloxTextEditResult ReplaceLocked(std::size_t begin, std::size_t end,
|
|
const std::string& replacement);
|
|
RobloxTextEditResult ReplaceCompositionLocked(
|
|
const platform::TextEditingEvent& event);
|
|
RobloxTextEditResult MoveCursorLocked(std::size_t byte_offset,
|
|
bool extend_selection);
|
|
RobloxTextEditResult SelectAllLocked();
|
|
RobloxTextEditResult CopySelectionLocked(bool cut);
|
|
RobloxTextEditResult PasteLocked();
|
|
RobloxTextEditResult FinishLocked();
|
|
RobloxTextEditResult ReleaseLocked();
|
|
Status SyncLocked();
|
|
Status PassLocked(bool finished);
|
|
void RememberPendingNativeEchoLocked();
|
|
bool ConsumePendingNativeEchoLocked(const std::string& text);
|
|
void ClearPendingNativeEchoesLocked();
|
|
void PublishDisplayLocked(RobloxTextDisplayEvent event);
|
|
void UpdateSelectionSnapshotLocked();
|
|
std::size_t SelectionBeginLocked() const;
|
|
std::size_t SelectionEndLocked() const;
|
|
bool HasSelectionLocked() const;
|
|
void ClearLocked();
|
|
|
|
const RobloxTextSink sink_;
|
|
const RobloxTextDisplaySink display_sink_;
|
|
platform::TextClipboard* const clipboard_;
|
|
mutable std::mutex mutex_;
|
|
RobloxTextFocusSession session_;
|
|
std::string text_;
|
|
std::size_t cursor_byte_ = 0;
|
|
std::size_t selection_anchor_byte_ = 0;
|
|
std::size_t composition_begin_byte_ = 0;
|
|
std::size_t composition_end_byte_ = 0;
|
|
std::size_t composition_original_cursor_byte_ = 0;
|
|
std::size_t composition_original_anchor_byte_ = 0;
|
|
std::string composition_replaced_text_;
|
|
bool composition_active_ = false;
|
|
uint32_t active_shortcuts_ = 0;
|
|
std::deque<std::string> pending_native_echoes_;
|
|
std::size_t pending_native_echo_bytes_ = 0;
|
|
RobloxTextEditorSnapshot snapshot_;
|
|
};
|
|
|
|
} // namespace runtime
|
|
} // namespace mocktail
|
|
|
|
#endif // MOCKTAIL_RUNTIME_ROBLOX_TEXT_EDITOR_H_
|