diff --git a/backends/imgui_impl_roblox.lua b/backends/imgui_impl_roblox.lua index a8a8010..63d2def 100644 --- a/backends/imgui_impl_roblox.lua +++ b/backends/imgui_impl_roblox.lua @@ -95,7 +95,8 @@ end -- Games can't touch the OS clipboard, so copy/paste only works inside ImGui local clipboard = "" local function ImGui_ImplRoblox_PlatformSetClipboardText(ctx, text) clipboard = text end -local function ImGui_ImplRoblox_PlatformGetClipboardText(ctx) return clipboard end +local proxy_focused = false +local function ImGui_ImplRoblox_PlatformGetClipboardText(ctx) if proxy_focused then return "" end return clipboard end --------------------------------------------------------- -- FILE SYSTEM (in-memory, Roblox has no file access) @@ -1089,17 +1090,21 @@ function ImGui_ImplRoblox_Init(parent, render_scale) if button then io:AddMouseSourceEvent(ImGuiMouseSource.Mouse) io:AddMouseButtonEvent(button, true) - elseif input.UserInputType == Enum.UserInputType.Keyboard and not game_processed then + elseif input.UserInputType == Enum.UserInputType.Keyboard and (not game_processed or proxy_focused) then local ctrl, shift, alt = ImGui_ImplRoblox_UpdateKeyModifiers() local key = KEY_MAP[input.KeyCode] if key then io:AddKeyEvent(key, true) end + if proxy_focused and (key == ImGuiKey.Backspace or key == ImGuiKey.Delete) then ImGui_ImplRoblox_GetBackendData().DeleteKeySeen = true end -- Text: printable ASCII keys, skipped while Ctrl/Alt are held (shortcuts) local v = input.KeyCode.Value if v >= 32 and v < 127 and not (ctrl or alt) then local c = string.char(v) if shift then c = SHIFTED[c] or c:upper() end - io:AddInputCharacter(string.byte(c)) + -- While the proxy may own text input, hold the char: NewFrame uses the proxy's text if it changed + -- (correct layout/unicode), otherwise these. So typing works even if the proxy silently lost focus. + if proxy_focused then table.insert(ImGui_ImplRoblox_GetBackendData().PendingChars, string.byte(c)) + else io:AddInputCharacter(string.byte(c)) end end end end) @@ -1122,6 +1127,34 @@ function ImGui_ImplRoblox_Init(parent, render_scale) end end) + -- Text proxy: a hidden TextBox holds focus while an imgui text field is active, so the OS gives us typing (any + -- layout/unicode), paste (Ctrl+V) and copy/cut (Ctrl+C/X of the proxy's text = imgui's current selection). + local proxy = Instance.new("TextBox") + proxy.Name = "TextProxy" + proxy.BackgroundTransparency = 1 + proxy.TextTransparency = 0 -- kept opaque but parked off screen: a transparent TextBox may not get native paste + proxy.BorderSizePixel = 0 + proxy.Size = UDim2.fromOffset(200, 20) + proxy.Position = UDim2.fromOffset(-10000, -10000) + proxy.MultiLine = true + proxy.ClearTextOnFocus = false + proxy.TextEditable = true + proxy.Text = "" + proxy.Parent = gui + bd.Proxy, bd.ProxyText, bd.PendingChars = proxy, "", {} + -- Clicking elsewhere (or Roblox taking focus) ends the edit, like clicking outside the field in imgui. + -- (Text is polled in NewFrame instead of using Text changed signals: those are deferred in Roblox.) + connect(proxy.FocusLost, function() + if bd.ProxyDebug then print("[ImGui text proxy] FocusLost, mouse over imgui:", ImGui.GetIO().WantCaptureMouse) end + if bd.ProxyReleasing then return end + -- Roblox drops TextBox focus on any click, including clicks inside imgui (e.g. moving the cursor within the + -- same field). Those must not end the edit: just recapture next frame. Only a click outside imgui ends it. + if ImGui.GetIO().WantCaptureMouse then bd.ProxyLostId = nil; return end + bd.ProxyLostId = bd.ProxyActiveId -- don't recapture for the same field + local g = ImGui.GetCurrentContext() + if g and g.ActiveId ~= 0 and g.ActiveId == bd.ProxyActiveId then ImGui.ClearActiveID() end + end) + connect(UserInputService.WindowFocused, function() ImGui.GetIO():AddFocusEvent(true) end) connect(UserInputService.WindowFocusReleased, function() ImGui.GetIO():AddFocusEvent(false) end) end @@ -1164,6 +1197,86 @@ function ImGui_ImplRoblox_NewFrame() io:AddMousePosEvent(m.X * bd.Scale, m.Y * bd.Scale) end + -- Text proxy: focused while an imgui text field is active. proxy_focused only when Roblox really gave it focus, + -- otherwise the KeyCode fallback keeps typing working. + local proxy = bd.Proxy + local g = ImGui.GetCurrentContext() + local focused_box = UserInputService:GetFocusedTextBox() + if io.WantTextInput and g.ActiveId ~= 0 then + if g.ActiveId ~= bd.ProxyActiveId or g.ActiveIdIsJustActivated or g.IO.MouseClicked[0] then bd.ProxyActiveId = g.ActiveId; bd.ProxyLostId = nil end -- (re)clicking a field recaptures + local mouse_up = not (io.MouseDown[0] or io.MouseDown[1] or io.MouseDown[2]) + if focused_box ~= proxy and mouse_up and bd.ProxyLostId ~= g.ActiveId and (focused_box == nil or not focused_box:IsDescendantOf(game)) then + bd.ProxyText = proxy.Text -- what it already holds is not new input (else re-typing it would collapse imgui's selection) + proxy:CaptureFocus() + focused_box = UserInputService:GetFocusedTextBox() + if bd.ProxyDebug then print("[ImGui text proxy] CaptureFocus ->", focused_box == proxy and "focused" or ("FAILED, focused = " .. tostring(focused_box))) end + end + elseif focused_box == proxy then + bd.ProxyReleasing = true + proxy:ReleaseFocus() + bd.ProxyReleasing = false + focused_box = nil + end + local was_focused = proxy_focused + proxy_focused = focused_box == proxy + local used_proxy_text = false + if proxy_focused then + -- 1) whatever the OS put in the proxy since last frame (typing, paste, cut) replaces imgui's selection + local text = proxy.Text + if text ~= bd.ProxyText and bd.ProxyDebug then print("[ImGui text proxy] text", string.format("%q", text), "was", string.format("%q", tostring(bd.ProxyText))) end + if text ~= bd.ProxyText and text ~= bd.ProxyLastRaw then -- (LastRaw: don't re-send if our reset hasn't applied yet) + bd.ProxyLastRaw = text + -- InputText ignores characters while Ctrl is held (upstream, so shortcuts don't type); a paste arrives with + -- Ctrl still down, so release Ctrl around the characters in the event queue. + local ctrl = io.KeyCtrl or UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) or UserInputService:IsKeyDown(Enum.KeyCode.RightControl) + if ctrl then io:AddKeyEvent(ImGuiMod_Ctrl, false) end + local prev = bd.ProxyText or "" + if #text < #prev and prev:sub(1, #text) == text and text:sub(-1) ~= "\n" then + -- The TextBox deleted from its own text (Backspace/Delete; Roblox can't hold a real selection in it, so it + -- may remove just 1 char). Apply the deletion with imgui's own selection instead: one Backspace deletes + -- the whole imgui selection, or one char if none. Skipped if imgui already got the key event. + if not bd.DeleteKeySeen then io:AddKeyEvent(ImGuiKey.Backspace, true); io:AddKeyEvent(ImGuiKey.Backspace, false) end + elseif text ~= "\n" and text ~= "\r" and text ~= "\t" then -- Enter/Tab arrive as key events + local prev = bd.ProxyText or "" + if #text > 0 and (prev == "" or text:sub(1, #prev) ~= prev or #text <= #prev) then + for _, cp in utf8.codes(text) do io:AddInputCharacter(cp) end + elseif #text > #prev then + for _, cp in utf8.codes(text:sub(#prev + 1)) do io:AddInputCharacter(cp) end + end + end + if ctrl then io:AddKeyEvent(ImGuiMod_Ctrl, true) end + used_proxy_text = true + bd.ProxyText = nil + end + -- 2) mirror imgui's selection, fully selected, so the native Ctrl+C / Ctrl+X copy exactly that + local st = g.InputTextState + local sel = "" + if st and st.ID ~= 0 and st.ID == g.ActiveId and st.Stb and st:HasSelection() then + local a, b = math.min(st.Stb.select_start, st.Stb.select_end), math.max(st.Stb.select_start, st.Stb.select_end) + sel = ImGui._ByteArrayToString(st.TextA.Data, a, b) -- stb positions are 1-based in this port + end + if sel ~= bd.ProxyText then + proxy.Text = sel + bd.ProxyLastRaw = nil + bd.ProxyText = sel + if bd.ProxyDebug then print("[ImGui text proxy] mirror selection", string.format("%q", sel)) end + end + -- keep imgui's selection fully selected in the proxy, so Ctrl+C copies it and typing/Ctrl+V replaces it + local want_sel, want_cur = (#sel > 0) and 1 or -1, #sel + 1 + if proxy.SelectionStart ~= want_sel then proxy.SelectionStart = want_sel end + if proxy.CursorPosition ~= want_cur then proxy.CursorPosition = want_cur end + end + + bd.DeleteKeySeen = false + + -- Held KeyCode chars: dropped if the proxy delivered the text, otherwise typed (proxy didn't really get the keys) + local pending = bd.PendingChars + if #pending > 0 then + if not used_proxy_text then for _, c in ipairs(pending) do io:AddInputCharacter(c) end end + table.clear(pending) + end + if bd.ProxyDebug and was_focused ~= proxy_focused then print("[ImGui text proxy] proxy_focused =", proxy_focused) end + -- While the mouse is over imgui, make the tiles Active so Roblox stops passing clicks/wheel to the game (camera zoom etc.) local want = io.WantCaptureMouse if want ~= bd.TilesActive then @@ -1204,6 +1317,7 @@ ImGui_ImplRoblox = { SetRenderScale = ImGui_ImplRoblox_SetRenderScale, SetRenderRate = ImGui_ImplRoblox_SetRenderRate, SetOverlay = ImGui_ImplRoblox_SetOverlay, + SetTextProxyDebug = function(on) ImGui_ImplRoblox_GetBackendData().ProxyDebug = on end, -- prints focus/text events CreateTexture = ImGui_ImplRoblox_CreateTexture, DestroyTexture = ImGui_ImplRoblox_DestroyTexture, GetRenderScale = function() return ImGui_ImplRoblox_GetBackendData().Scale end, diff --git a/build/ImGui.linemap.json b/build/ImGui.linemap.json index d95d75f..6a77b25 100644 --- a/build/ImGui.linemap.json +++ b/build/ImGui.linemap.json @@ -1 +1 @@ -[[12, "imgui.lua"], [18472, "imgui_demo.lua"], [25757, "imgui_draw.lua"], [30995, "imgui_h.lua"], [33193, "imgui_impl_roblox.lua"], [34411, "imgui_impl_roblox_fonts.lua"], [40814, "imgui_internal.lua"], [43986, "imgui_tables.lua"], [48333, "imgui_widgets.lua"], [58088, "imstb_rectpack.lua"], [58534, "imstb_textedit.lua"], [59775, "imstb_truetype.lua"], [62243, "imstd_minstdio.lua"]] \ No newline at end of file +[[12, "imgui.lua"], [18472, "imgui_demo.lua"], [25757, "imgui_draw.lua"], [30995, "imgui_h.lua"], [33193, "imgui_impl_roblox.lua"], [34525, "imgui_impl_roblox_fonts.lua"], [40928, "imgui_internal.lua"], [44100, "imgui_tables.lua"], [48447, "imgui_widgets.lua"], [58202, "imstb_rectpack.lua"], [58648, "imstb_textedit.lua"], [59889, "imstb_truetype.lua"], [62357, "imstd_minstdio.lua"]] \ No newline at end of file diff --git a/build/ImGui.luau b/build/ImGui.luau index 7a13cef..01045e6 100644 --- a/build/ImGui.luau +++ b/build/ImGui.luau @@ -33287,7 +33287,8 @@ end -- Games can't touch the OS clipboard, so copy/paste only works inside ImGui local clipboard = "" local function ImGui_ImplRoblox_PlatformSetClipboardText(ctx, text) clipboard = text end -local function ImGui_ImplRoblox_PlatformGetClipboardText(ctx) return clipboard end +local proxy_focused = false +local function ImGui_ImplRoblox_PlatformGetClipboardText(ctx) if proxy_focused then return "" end return clipboard end --------------------------------------------------------- -- FILE SYSTEM (in-memory, Roblox has no file access) @@ -34281,17 +34282,21 @@ function ImGui_ImplRoblox_Init(parent, render_scale) if button then io:AddMouseSourceEvent(ImGuiMouseSource.Mouse) io:AddMouseButtonEvent(button, true) - elseif input.UserInputType == Enum.UserInputType.Keyboard and not game_processed then + elseif input.UserInputType == Enum.UserInputType.Keyboard and (not game_processed or proxy_focused) then local ctrl, shift, alt = ImGui_ImplRoblox_UpdateKeyModifiers() local key = KEY_MAP[input.KeyCode] if key then io:AddKeyEvent(key, true) end + if proxy_focused and (key == ImGuiKey.Backspace or key == ImGuiKey.Delete) then ImGui_ImplRoblox_GetBackendData().DeleteKeySeen = true end -- Text: printable ASCII keys, skipped while Ctrl/Alt are held (shortcuts) local v = input.KeyCode.Value if v >= 32 and v < 127 and not (ctrl or alt) then local c = string.char(v) if shift then c = SHIFTED[c] or c:upper() end - io:AddInputCharacter(string.byte(c)) + -- While the proxy may own text input, hold the char: NewFrame uses the proxy's text if it changed + -- (correct layout/unicode), otherwise these. So typing works even if the proxy silently lost focus. + if proxy_focused then table.insert(ImGui_ImplRoblox_GetBackendData().PendingChars, string.byte(c)) + else io:AddInputCharacter(string.byte(c)) end end end end) @@ -34314,6 +34319,34 @@ function ImGui_ImplRoblox_Init(parent, render_scale) end end) + -- Text proxy: a hidden TextBox holds focus while an imgui text field is active, so the OS gives us typing (any + -- layout/unicode), paste (Ctrl+V) and copy/cut (Ctrl+C/X of the proxy's text = imgui's current selection). + local proxy = Instance.new("TextBox") + proxy.Name = "TextProxy" + proxy.BackgroundTransparency = 1 + proxy.TextTransparency = 0 -- kept opaque but parked off screen: a transparent TextBox may not get native paste + proxy.BorderSizePixel = 0 + proxy.Size = UDim2.fromOffset(200, 20) + proxy.Position = UDim2.fromOffset(-10000, -10000) + proxy.MultiLine = true + proxy.ClearTextOnFocus = false + proxy.TextEditable = true + proxy.Text = "" + proxy.Parent = gui + bd.Proxy, bd.ProxyText, bd.PendingChars = proxy, "", {} + -- Clicking elsewhere (or Roblox taking focus) ends the edit, like clicking outside the field in imgui. + -- (Text is polled in NewFrame instead of using Text changed signals: those are deferred in Roblox.) + connect(proxy.FocusLost, function() + if bd.ProxyDebug then print("[ImGui text proxy] FocusLost, mouse over imgui:", ImGui.GetIO().WantCaptureMouse) end + if bd.ProxyReleasing then return end + -- Roblox drops TextBox focus on any click, including clicks inside imgui (e.g. moving the cursor within the + -- same field). Those must not end the edit: just recapture next frame. Only a click outside imgui ends it. + if ImGui.GetIO().WantCaptureMouse then bd.ProxyLostId = nil; return end + bd.ProxyLostId = bd.ProxyActiveId -- don't recapture for the same field + local g = ImGui.GetCurrentContext() + if g and g.ActiveId ~= 0 and g.ActiveId == bd.ProxyActiveId then ImGui.ClearActiveID() end + end) + connect(UserInputService.WindowFocused, function() ImGui.GetIO():AddFocusEvent(true) end) connect(UserInputService.WindowFocusReleased, function() ImGui.GetIO():AddFocusEvent(false) end) end @@ -34356,6 +34389,86 @@ function ImGui_ImplRoblox_NewFrame() io:AddMousePosEvent(m.X * bd.Scale, m.Y * bd.Scale) end + -- Text proxy: focused while an imgui text field is active. proxy_focused only when Roblox really gave it focus, + -- otherwise the KeyCode fallback keeps typing working. + local proxy = bd.Proxy + local g = ImGui.GetCurrentContext() + local focused_box = UserInputService:GetFocusedTextBox() + if io.WantTextInput and g.ActiveId ~= 0 then + if g.ActiveId ~= bd.ProxyActiveId or g.ActiveIdIsJustActivated or g.IO.MouseClicked[0] then bd.ProxyActiveId = g.ActiveId; bd.ProxyLostId = nil end -- (re)clicking a field recaptures + local mouse_up = not (io.MouseDown[0] or io.MouseDown[1] or io.MouseDown[2]) + if focused_box ~= proxy and mouse_up and bd.ProxyLostId ~= g.ActiveId and (focused_box == nil or not focused_box:IsDescendantOf(game)) then + bd.ProxyText = proxy.Text -- what it already holds is not new input (else re-typing it would collapse imgui's selection) + proxy:CaptureFocus() + focused_box = UserInputService:GetFocusedTextBox() + if bd.ProxyDebug then print("[ImGui text proxy] CaptureFocus ->", focused_box == proxy and "focused" or ("FAILED, focused = " .. tostring(focused_box))) end + end + elseif focused_box == proxy then + bd.ProxyReleasing = true + proxy:ReleaseFocus() + bd.ProxyReleasing = false + focused_box = nil + end + local was_focused = proxy_focused + proxy_focused = focused_box == proxy + local used_proxy_text = false + if proxy_focused then + -- 1) whatever the OS put in the proxy since last frame (typing, paste, cut) replaces imgui's selection + local text = proxy.Text + if text ~= bd.ProxyText and bd.ProxyDebug then print("[ImGui text proxy] text", string.format("%q", text), "was", string.format("%q", tostring(bd.ProxyText))) end + if text ~= bd.ProxyText and text ~= bd.ProxyLastRaw then -- (LastRaw: don't re-send if our reset hasn't applied yet) + bd.ProxyLastRaw = text + -- InputText ignores characters while Ctrl is held (upstream, so shortcuts don't type); a paste arrives with + -- Ctrl still down, so release Ctrl around the characters in the event queue. + local ctrl = io.KeyCtrl or UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) or UserInputService:IsKeyDown(Enum.KeyCode.RightControl) + if ctrl then io:AddKeyEvent(ImGuiMod_Ctrl, false) end + local prev = bd.ProxyText or "" + if #text < #prev and prev:sub(1, #text) == text and text:sub(-1) ~= "\n" then + -- The TextBox deleted from its own text (Backspace/Delete; Roblox can't hold a real selection in it, so it + -- may remove just 1 char). Apply the deletion with imgui's own selection instead: one Backspace deletes + -- the whole imgui selection, or one char if none. Skipped if imgui already got the key event. + if not bd.DeleteKeySeen then io:AddKeyEvent(ImGuiKey.Backspace, true); io:AddKeyEvent(ImGuiKey.Backspace, false) end + elseif text ~= "\n" and text ~= "\r" and text ~= "\t" then -- Enter/Tab arrive as key events + local prev = bd.ProxyText or "" + if #text > 0 and (prev == "" or text:sub(1, #prev) ~= prev or #text <= #prev) then + for _, cp in utf8.codes(text) do io:AddInputCharacter(cp) end + elseif #text > #prev then + for _, cp in utf8.codes(text:sub(#prev + 1)) do io:AddInputCharacter(cp) end + end + end + if ctrl then io:AddKeyEvent(ImGuiMod_Ctrl, true) end + used_proxy_text = true + bd.ProxyText = nil + end + -- 2) mirror imgui's selection, fully selected, so the native Ctrl+C / Ctrl+X copy exactly that + local st = g.InputTextState + local sel = "" + if st and st.ID ~= 0 and st.ID == g.ActiveId and st.Stb and st:HasSelection() then + local a, b = math.min(st.Stb.select_start, st.Stb.select_end), math.max(st.Stb.select_start, st.Stb.select_end) + sel = ImGui._ByteArrayToString(st.TextA.Data, a, b) -- stb positions are 1-based in this port + end + if sel ~= bd.ProxyText then + proxy.Text = sel + bd.ProxyLastRaw = nil + bd.ProxyText = sel + if bd.ProxyDebug then print("[ImGui text proxy] mirror selection", string.format("%q", sel)) end + end + -- keep imgui's selection fully selected in the proxy, so Ctrl+C copies it and typing/Ctrl+V replaces it + local want_sel, want_cur = (#sel > 0) and 1 or -1, #sel + 1 + if proxy.SelectionStart ~= want_sel then proxy.SelectionStart = want_sel end + if proxy.CursorPosition ~= want_cur then proxy.CursorPosition = want_cur end + end + + bd.DeleteKeySeen = false + + -- Held KeyCode chars: dropped if the proxy delivered the text, otherwise typed (proxy didn't really get the keys) + local pending = bd.PendingChars + if #pending > 0 then + if not used_proxy_text then for _, c in ipairs(pending) do io:AddInputCharacter(c) end end + table.clear(pending) + end + if bd.ProxyDebug and was_focused ~= proxy_focused then print("[ImGui text proxy] proxy_focused =", proxy_focused) end + -- While the mouse is over imgui, make the tiles Active so Roblox stops passing clicks/wheel to the game (camera zoom etc.) local want = io.WantCaptureMouse if want ~= bd.TilesActive then @@ -34396,6 +34509,7 @@ ImGui_ImplRoblox = { SetRenderScale = ImGui_ImplRoblox_SetRenderScale, SetRenderRate = ImGui_ImplRoblox_SetRenderRate, SetOverlay = ImGui_ImplRoblox_SetOverlay, + SetTextProxyDebug = function(on) ImGui_ImplRoblox_GetBackendData().ProxyDebug = on end, -- prints focus/text events CreateTexture = ImGui_ImplRoblox_CreateTexture, DestroyTexture = ImGui_ImplRoblox_DestroyTexture, GetRenderScale = function() return ImGui_ImplRoblox_GetBackendData().Scale end, diff --git a/tools/test/roblox_mock.luau b/tools/test/roblox_mock.luau index 5f748f7..9388299 100644 --- a/tools/test/roblox_mock.luau +++ b/tools/test/roblox_mock.luau @@ -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() } diff --git a/tools/test/scripts/t_textproxy.luau b/tools/test/scripts/t_textproxy.luau new file mode 100644 index 0000000..4d2936d --- /dev/null +++ b/tools/test/scripts/t_textproxy.luau @@ -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)