Roblox backend: hidden TextBox proxy for text input
A single off-screen TextBox holds focus while an imgui text field is active, so the OS delivers typing (any layout/unicode), paste and copy/cut. Its text mirrors imgui selection (fully selected) so Ctrl+C/X copy exactly that; deletions apply as an imgui Backspace; Ctrl is released around pasted characters. Focus is captured after the click is released, kept on clicks inside imgui, ends on clicks outside. KeyCode text kept as fallback. SetTextProxyDebug for logging.
This commit is contained in:
@@ -60,6 +60,7 @@ local InstMethods = {}
|
||||
local InstMT = {}
|
||||
InstMT.__index = function(t, k)
|
||||
if k == "Parent" then return rawget(t, "_parent") end
|
||||
local pv = rawget(t, "_props")[k]; if pv ~= nil then return pv end
|
||||
local m = InstMethods[k]; if m then return m end
|
||||
for _, c in ipairs(rawget(t, "_children")) do if c.Name == k then return c end end
|
||||
return nil
|
||||
@@ -71,13 +72,18 @@ InstMT.__newindex = function(t, k, v)
|
||||
rawset(t, "_parent", v)
|
||||
if v then table.insert(rawget(v, "_children"), t) end
|
||||
else
|
||||
rawset(t, k, v)
|
||||
local props = rawget(t, "_props")
|
||||
local old = props[k]
|
||||
props[k] = v
|
||||
local sigs = rawget(t, "_propsigs")
|
||||
if sigs and sigs[k] and old ~= v then sigs[k]:Fire() end
|
||||
end
|
||||
end
|
||||
local function newInst(class, name, parent)
|
||||
local o = setmetatable({ ClassName = class, Name = name or class, _children = {} }, InstMT)
|
||||
local o = setmetatable({ _children = {}, _props = { ClassName = class, Name = name or class } }, InstMT)
|
||||
if class == "ScreenGui" or class == "Frame" or class == "ImageLabel" then o.AbsoluteSize = v2(SCREEN_W, SCREEN_H) end
|
||||
o.Visible = true
|
||||
if class == "TextBox" then rawget(o, "_props").FocusLost = Signal() end
|
||||
if class == "BindableEvent" then local sig = Signal(); rawset(o, "Event", sig); rawset(o, "Fire", function(_, ...) sig:Fire(...) end) end
|
||||
if parent then o.Parent = parent end
|
||||
return o
|
||||
@@ -95,6 +101,14 @@ function InstMethods:IsA(class)
|
||||
return self.ClassName == class
|
||||
end
|
||||
function InstMethods:Destroy() self.Parent = nil end
|
||||
local focused_box = nil
|
||||
function InstMethods:GetPropertyChangedSignal(prop)
|
||||
local sigs = rawget(self, "_propsigs") or {}; rawset(self, "_propsigs", sigs)
|
||||
sigs[prop] = sigs[prop] or Signal(); return sigs[prop]
|
||||
end
|
||||
function InstMethods:CaptureFocus() focused_box = self end
|
||||
function InstMethods:_loseFocus() if focused_box == self then focused_box = nil; local fl = rawget(self, "_props").FocusLost; if fl then fl:Fire(false) end end end
|
||||
function InstMethods:ReleaseFocus() self:_loseFocus() end
|
||||
|
||||
-- Services
|
||||
local mouse = v2(0, 0)
|
||||
@@ -103,6 +117,7 @@ local UIS = {
|
||||
InputBegan = Signal(), InputEnded = Signal(), InputChanged = Signal(), WindowFocused = Signal(), WindowFocusReleased = Signal(),
|
||||
}
|
||||
function UIS:GetMouseLocation() return mouse end
|
||||
function UIS:GetFocusedTextBox() return focused_box end
|
||||
function UIS:IsKeyDown(kc) return keys_down[kc] == true end
|
||||
|
||||
local RunService = { Heartbeat = Signal(), Stepped = Signal(), PreAnimation = Signal(), RenderStepped = Signal() }
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
-- Text proxy: typing/paste arrive through the hidden TextBox, and its text mirrors the InputText selection (for copy).
|
||||
local G = require(game:GetService("ReplicatedStorage"):WaitForChild("ImGui"))
|
||||
local ImGui, ImVec2 = G.ImGui, G.ImVec2
|
||||
local UIS = game:GetService("UserInputService")
|
||||
ImGui.CreateContext(); G.ImGui_ImplRoblox.Init()
|
||||
local io = ImGui.GetIO()
|
||||
local buf = ImGui.NewTextBuffer("", 64)
|
||||
local proxy
|
||||
local f = 0
|
||||
game:GetService("RunService").Stepped:Connect(function()
|
||||
f += 1
|
||||
G.ImGui_ImplRoblox.NewFrame()
|
||||
proxy = proxy or game:GetService("Players").LocalPlayer.PlayerGui.ImGui.TextProxy
|
||||
if f == 3 then io:AddMousePosEvent(60, 45); io:AddMouseButtonEvent(0, true) end
|
||||
if f == 4 then io:AddMouseButtonEvent(0, false) end
|
||||
if f == 8 then assert(UIS:GetFocusedTextBox() == proxy, "proxy focused"); proxy.Text = "abc" end -- typed
|
||||
if f == 10 then io:AddKeyEvent(G.ImGuiMod_Ctrl, true); proxy.Text = "hello world" end -- pasted with Ctrl held
|
||||
if f == 11 then io:AddKeyEvent(G.ImGuiMod_Ctrl, false) end -- pasted
|
||||
if f == 13 then io:AddKeyEvent(G.ImGuiMod_Ctrl, true); io:AddKeyEvent(G.ImGuiKey.A, true) end -- select all
|
||||
if f == 14 then io:AddKeyEvent(G.ImGuiKey.A, false); io:AddKeyEvent(G.ImGuiMod_Ctrl, false) end
|
||||
ImGui.NewFrame()
|
||||
ImGui.SetNextWindowPos(ImVec2(10, 10)); ImGui.SetNextWindowSize(ImVec2(300, 100))
|
||||
ImGui.Begin("T"); ImGui.InputText("##t", buf, 64); ImGui.End()
|
||||
ImGui.Render(); G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
|
||||
if f == 12 then local st = ImGui.GetCurrentContext().InputTextState -- (buf itself is written on validation/deactivation)
|
||||
local got = G.ImGui._ByteArrayToString(st.TextA.Data, 1, st.TextLen + 1)
|
||||
assert(got == "abchello world", "text: " .. got) end
|
||||
if f == 15 then proxy:_loseFocus() end
|
||||
-- keys while the proxy reports focus but receives nothing (Roblox silently not delivering): must still type
|
||||
if f == 20 then
|
||||
io:AddMousePosEvent(60, 45); io:AddMouseButtonEvent(0, true)
|
||||
end
|
||||
if f == 21 then io:AddMouseButtonEvent(0, false) end
|
||||
if f == 24 then
|
||||
assert(UIS:GetFocusedTextBox() == proxy, "proxy focused again")
|
||||
local k = { UserInputType = Enum.UserInputType.Keyboard, KeyCode = Enum.KeyCode.Z, Position = { X = 0, Y = 0, Z = 0 } }
|
||||
UIS.InputBegan:Fire(k, true); UIS.InputEnded:Fire(k, true)
|
||||
end -- click inside imgui (mouse is over the field): edit must survive
|
||||
if f == 16 then
|
||||
assert(ImGui.GetCurrentContext().ActiveId ~= 0, "edit ended by a click inside imgui")
|
||||
assert(UIS:GetFocusedTextBox() == proxy, "proxy not recaptured")
|
||||
assert(proxy.Text == "abchello world", "selection mirror: " .. tostring(proxy.Text))
|
||||
io:AddMousePosEvent(900, 900) -- move off imgui, then the user clicks into the game
|
||||
end
|
||||
if f == 17 then proxy:_loseFocus() end
|
||||
if f == 19 then
|
||||
assert(ImGui.GetCurrentContext().ActiveId == 0, "field still active after focus lost")
|
||||
assert(UIS:GetFocusedTextBox() == nil, "proxy recaptured")
|
||||
assert(ImGui.TextBufferToString(buf) == "abchello world", "buf: " .. ImGui.TextBufferToString(buf))
|
||||
end
|
||||
-- click inside the field with a selection (Roblox drops focus on press, we recapture on release): selection must survive
|
||||
if f == 28 then io:AddKeyEvent(G.ImGuiMod_Ctrl, true); io:AddKeyEvent(G.ImGuiKey.A, true) end
|
||||
if f == 29 then io:AddKeyEvent(G.ImGuiKey.A, false); io:AddKeyEvent(G.ImGuiMod_Ctrl, false) end
|
||||
if f == 31 then proxy:_loseFocus() end
|
||||
if f == 34 then
|
||||
local st = ImGui.GetCurrentContext().InputTextState
|
||||
assert(st:HasSelection(), "selection lost after refocus")
|
||||
assert(#proxy.Text > 5 and proxy.Text == G.ImGui._ByteArrayToString(st.TextA.Data, 1, st.TextLen + 1), "mirror after refocus: " .. proxy.Text)
|
||||
print("REFOCUS OK")
|
||||
proxy.Text = proxy.Text:sub(1, -2) -- Roblox Backspace without a real TextBox selection: removes 1 char
|
||||
end
|
||||
if f == 37 then
|
||||
local st = ImGui.GetCurrentContext().InputTextState
|
||||
assert(st.TextLen == 0, "backspace should delete imgui's whole selection, len " .. st.TextLen)
|
||||
print("BACKSPACE OK")
|
||||
end
|
||||
if f == 27 then
|
||||
local st = ImGui.GetCurrentContext().InputTextState
|
||||
local got = G.ImGui._ByteArrayToString(st.TextA.Data, 1, st.TextLen + 1)
|
||||
assert(got:find("z"), "key fallback while proxy focused: " .. got)
|
||||
print("PROXY OK")
|
||||
end
|
||||
end)
|
||||
Reference in New Issue
Block a user