diff --git a/build/ImGui.linemap.json b/build/ImGui.linemap.json index c24ca95..f3817bf 100644 --- a/build/ImGui.linemap.json +++ b/build/ImGui.linemap.json @@ -1 +1 @@ -[[13, "imgui.lua"], [12931, "imgui_demo.lua"], [13824, "imgui_demo_2.lua"], [16712, "imgui_docking.lua"], [19839, "imgui_dragdrop.lua"], [20131, "imgui_draw.lua"], [25106, "imgui_h.lua"], [27260, "imgui_impl_roblox.lua"], [28016, "imgui_impl_roblox_fonts.lua"], [28794, "imgui_internal.lua"], [31943, "imgui_tables.lua"], [31990, "imgui_tabs.lua"], [33157, "imgui_widgets.lua"], [41247, "imstb_rectpack.lua"], [41693, "imstb_textedit.lua"], [42934, "imstb_truetype.lua"], [45402, "imstd_minstdio.lua"]] \ No newline at end of file +[[13, "imgui.lua"], [12966, "imgui_demo.lua"], [13859, "imgui_demo_2.lua"], [16747, "imgui_docking.lua"], [19875, "imgui_dragdrop.lua"], [20167, "imgui_draw.lua"], [25162, "imgui_h.lua"], [27316, "imgui_impl_roblox.lua"], [28072, "imgui_impl_roblox_fonts.lua"], [28850, "imgui_internal.lua"], [31999, "imgui_tables.lua"], [32046, "imgui_tabs.lua"], [33213, "imgui_widgets.lua"], [41303, "imstb_rectpack.lua"], [41749, "imstb_textedit.lua"], [42990, "imstb_truetype.lua"], [45458, "imstd_minstdio.lua"]] \ No newline at end of file diff --git a/build/ImGui.luau b/build/ImGui.luau index 54434cb..87298bc 100644 --- a/build/ImGui.luau +++ b/build/ImGui.luau @@ -1010,8 +1010,8 @@ local function ApplyWindowSettings(window, settings) window.AutoFitFramesX = 0; window.AutoFitFramesY = 0 end window.Collapsed = settings.Collapsed - -- window.DockId = settings.DockId - -- window.DockOrder = settings.DockOrder + window.DockId = settings.DockId or 0 + window.DockOrder = settings.DockOrder or -1 SetWindowConditionAllowFlags(window, ImGuiCond.FirstUseEver, false) end @@ -1042,7 +1042,7 @@ local function InitOrLoadWindowSettings(window, settings) window.SetWindowPosAllowFlags = bit32.bor(ImGuiCond.Always, ImGuiCond.Once, ImGuiCond.FirstUseEver, ImGuiCond.Appearing) window.SetWindowSizeAllowFlags = window.SetWindowPosAllowFlags window.SetWindowCollapsedAllowFlags = window.SetWindowPosAllowFlags - -- window.SetWindowDockAllowFlags = window.SetWindowPosAllowFlags + window.SetWindowDockAllowFlags = window.SetWindowPosAllowFlags ApplyWindowSettings(window, settings) if settings ~= nil then -- TODO: settings.LastUsedDate = g.SessionDate @@ -5117,7 +5117,7 @@ local function RenderWindowOuterBorders(window) RenderWindowOuterSingleBorder(window, border_n, border_col_resizing, ImMax(2.0, window.WindowBorderSize)) end - if g.Style.FrameBorderSize > 0 and (bit32.band(window.Flags, ImGuiWindowFlags.NoTitleBar) == 0) then + if g.Style.FrameBorderSize > 0 and (bit32.band(window.Flags, ImGuiWindowFlags.NoTitleBar) == 0) and not window.DockIsActive then local y = window.Pos.y + window.TitleBarHeight - 1 window.DrawList:AddLineH(window.Pos.x + border_size * 0.5, window.Pos.x + window.Size.x - border_size * 0.5, y, border_col, g.Style.FrameBorderSize) end @@ -5162,11 +5162,19 @@ local function RenderWindowDecorations(window, title_bar_rect, title_bar_is_high if bit32.band(bg_col, IM_COL32_A_MASK) ~= 0 then local bg_rect = ImRect(window.Pos + ImVec2(0, window.TitleBarHeight), window.Pos + window.Size) - local bg_rounding_flags = (bit32.band(flags, ImGuiWindowFlags.NoTitleBar) ~= 0) and ImDrawFlags.RoundCornersAll or ImDrawFlags.RoundCornersBottom - local bg_draw_list = window.DrawList + local bg_rounding_flags + if window.DockIsActive then + bg_rounding_flags = ImGui.CalcRoundingFlagsForRectInRect(bg_rect, window.DockNode.HostWindow:Rect(), 0.0) + else + bg_rounding_flags = (bit32.band(flags, ImGuiWindowFlags.NoTitleBar) ~= 0) and ImDrawFlags.RoundCornersAll or ImDrawFlags.RoundCornersBottom + end + local bg_draw_list = window.DockIsActive and window.DockNode.HostWindow.DrawList or window.DrawList + if window.DockIsActive then bg_draw_list:ChannelsSetCurrent(0) end bg_draw_list:AddRectFilled(bg_rect.Min, bg_rect.Max, bg_col, window_rounding, bg_rounding_flags) + if window.DockIsActive then bg_draw_list:ChannelsSetCurrent(1) end end end + if window.DockIsActive then window.DockNode.IsBgDrawnThisFrame = true end -- Title bar if bit32.band(flags, ImGuiWindowFlags.NoTitleBar) == 0 and not window.DockIsActive then @@ -5184,6 +5192,25 @@ local function RenderWindowDecorations(window, title_bar_rect, title_bar_is_high end end + -- Docking: Unhide tab bar (small triangle in the corner), drag from small triangle to quickly undock + local node = window.DockNode + if window.DockIsActive and node:IsHiddenTabBar() and not node:IsNoTabBar() then + local unhide_sz_draw = ImTrunc(g.FontSize * 0.70) + local unhide_sz_hit = ImTrunc(g.FontSize * 0.55) + local p = node.Pos + local r = ImRect(p, p + ImVec2(unhide_sz_hit, unhide_sz_hit)) + local unhide_id = window:GetID("#UNHIDE") + ImGui.KeepAliveID(unhide_id) + local pressed, hovered, held = ImGui.ButtonBehavior(r, unhide_id, ImGuiButtonFlags.FlattenChildren) + if pressed then + node.WantHiddenTabBarToggle = true + elseif held and ImGui.IsMouseDragging(0) then + ImGui.StartMouseMovingWindowOrNode(window, node, true) + end + local col = ImGui.GetColorU32(((held and hovered) or (node.IsFocused and not hovered)) and ImGuiCol.ButtonActive or (hovered and ImGuiCol.ButtonHovered or ImGuiCol.Button)) + window.DrawList:AddTriangleFilled(p, p + ImVec2(unhide_sz_draw, 0.0), p + ImVec2(0.0, unhide_sz_draw), col) + end + if window.ScrollbarX then ImGui.Scrollbar(ImGuiAxis.X) end @@ -5212,13 +5239,14 @@ local function RenderWindowDecorations(window, title_bar_rect, title_bar_is_high end end - if handle_borders_and_resize_grips then + if handle_borders_and_resize_grips and not window.DockNodeAsHost then RenderWindowOuterBorders(window) end end window.DC.NavLayerCurrent = ImGuiNavLayer.Main end +ImGui.RenderWindowOuterBorders = RenderWindowOuterBorders --- @param window ImGuiWindow --- @param title_bar_rect ImRect @@ -6068,6 +6096,11 @@ end --- @param popup_flags ImGuiPopupFlags function ImGui.IsPopupOpen(id, popup_flags) local g = GImGui + if popup_flags == nil then popup_flags = 0 end + if type(id) == "string" then + -- str_id overload: with AnyPopupId the id must be 0, otherwise it is hashed with the current window's ID stack + id = (bit32.band(popup_flags, ImGuiPopupFlags.AnyPopupId) ~= 0) and 0 or g.CurrentWindow:GetID(id) + end if bit32.band(popup_flags, ImGuiPopupFlags.AnyPopupId) ~= 0 then -- Return true if any popup is open at the current BeginPopup() level of the popup stack @@ -7986,7 +8019,9 @@ local function AddWindowToDrawData(window, layer) local g = GImGui local viewport = window.Viewport g.IO.MetricsRenderWindows = g.IO.MetricsRenderWindows + 1 - -- TODO: splitter + if window.DrawList._Splitter._Count > 1 then + window.DrawList:ChannelsMerge() -- Merge if user forgot to merge back. Also required for DockNodeHost windows. + end ImGui.AddDrawListToDrawDataEx(viewport.DrawDataP, viewport.DrawDataBuilder.Layers[layer], window.DrawList) for _, child in window.DC.ChildWindows:iter() do if (ImGui.IsWindowActiveAndVisible(child)) then -- Clipped children may have been marked not active @@ -17305,6 +17340,7 @@ local function FixLargeWindowsWhenUndocking(size, ref_viewport) end function ImGui.DockContextProcessUndockWindow(ctx, window, clear_persistent_docking_ref) + if clear_persistent_docking_ref == nil then clear_persistent_docking_ref = true end -- C++ default argument if window.DockNode then ImGui.DockNodeRemoveWindow(window.DockNode, window, clear_persistent_docking_ref and 0 or window.DockId) else @@ -18291,7 +18327,7 @@ function ImGui.DockNodeCalcSplitRects(pos_old, size_old, pos_new, size_new, dir, local g = ImGui.GetCurrentContext() local dock_spacing = g.Style.ItemInnerSpacing.x local axis = (dir == ImGuiDir.Left or dir == ImGuiDir.Right) and ImGuiAxis.X or ImGuiAxis.Y - local a, o = axis + 1, bit32.bxor(axis, 1) + 1 + local a, o = axis, 3 - axis -- the port's ImGuiAxis is 1-based (X=1,Y=2): it is directly the ImVec2 index pos_new[o] = pos_old[o] size_new[o] = size_old[o] @@ -18435,7 +18471,7 @@ function ImGui.DockNodePreviewDockSetup(host_window, host_node, payload_window, local size_new, size_old = ImVec2(0, 0), ImVec2(data.FutureNode.Size.x, data.FutureNode.Size.y) ImGui.DockNodeCalcSplitRects(pos_old, size_old, pos_new, size_new, split_dir, payload_window.Size) - local split_ratio = ImSaturate(size_new[split_axis + 1] / data.FutureNode.Size[split_axis + 1]) + local split_ratio = ImSaturate(size_new[split_axis] / data.FutureNode.Size[split_axis]) data.FutureNode.Pos = pos_new data.FutureNode.Size = size_new data.SplitRatio = (split_dir == ImGuiDir.Right or split_dir == ImGuiDir.Down) and (1.0 - split_ratio) or split_ratio @@ -18523,7 +18559,7 @@ function ImGui.DockNodePreviewDockRender(host_window, host_node, root_payload, d local cc = draw_r_in:GetCenter() local center = ImVec2(math.floor(cc.x), math.floor(cc.y)) dl:AddRectFilled(draw_r.Min, draw_r.Max, overlay_col, overlay_rounding) - dl:AddRect(draw_r_in.Min, draw_r_in.Max, overlay_col_lines, overlay_rounding, 0, thickness) + dl:AddRect(draw_r_in.Min, draw_r_in.Max, overlay_col_lines, overlay_rounding, thickness) if dir == ImGuiDir.Left or dir == ImGuiDir.Right then dl:AddLineV(center.x, draw_r_in.Min.y, draw_r_in.Max.y, overlay_col_lines, thickness) end @@ -18562,7 +18598,7 @@ function ImGui.DockNodeTreeSplit(ctx, parent_node, split_axis, split_inheritor_c parent_node.AuthorityForPos = ImGuiDataAuthority.DockNode parent_node.AuthorityForSize = ImGuiDataAuthority.DockNode - local a = split_axis + 1 + local a = split_axis local size_avail = parent_node.Size[a] - g.Style.DockingSeparatorSize size_avail = ImMax(size_avail, g.Style.WindowMinSize[a] * 2.0) IM_ASSERT(size_avail > 0.0) @@ -18648,7 +18684,7 @@ function ImGui.DockNodeTreeUpdatePosSize(node, pos, size, only_write_to_single_n if child_0_is_or_will_be_visible and child_1_is_or_will_be_visible then local spacing = g.Style.DockingSeparatorSize - local a = node.SplitAxis + 1 + local a = node.SplitAxis local size_avail = ImMax(size[a] - spacing, 0.0) local size_min_each = ImTrunc(ImMin(size_avail, g.Style.WindowMinSize[a] * 2.0) * 0.5) @@ -18717,7 +18753,7 @@ function ImGui.DockNodeTreeUpdateSplitter(node) if child_0.IsVisible and child_1.IsVisible then local axis = node.SplitAxis IM_ASSERT(axis ~= ImGuiAxis.None) - local a, o = axis + 1, bit32.bxor(axis, 1) + 1 + local a, o = axis, 3 - axis -- the port's ImGuiAxis is 1-based (X=1,Y=2): it is directly the ImVec2 index local bb = ImRect(ImVec2(child_0.Pos.x, child_0.Pos.y), ImVec2(child_1.Pos.x, child_1.Pos.y)) bb.Min[a] = bb.Min[a] + child_0.Size[a] bb.Max[o] = bb.Max[o] + child_1.Size[o] @@ -19808,7 +19844,7 @@ function ImGui.SplitterBehavior(bb, id, axis, size1, size2, min_size1, min_size2 local bb_render = ImRect(bb.Min, bb.Max) if held then local d = g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min - local mouse_delta = d[axis + 1] + local mouse_delta = d[axis] local size_1_maximum_delta = ImMax(0.0, size1 - min_size1) local size_2_maximum_delta = ImMax(0.0, size2 - min_size2) @@ -20104,7 +20140,7 @@ end function ImGui.RenderDragDropTargetRectEx(draw_list, bb, rounding) local g = ImGui.GetCurrentContext() draw_list:AddRectFilled(bb.Min, bb.Max, ImGui.GetColorU32(ImGuiCol.DragDropTargetBg), rounding, 0) - draw_list:AddRect(bb.Min, bb.Max, ImGui.GetColorU32(ImGuiCol.DragDropTarget), rounding, 0, g.Style.DragDropTargetBorderSize) + draw_list:AddRect(bb.Min, bb.Max, ImGui.GetColorU32(ImGuiCol.DragDropTarget), rounding, g.Style.DragDropTargetBorderSize) end --- @return ImGuiPayload? @@ -23622,7 +23658,8 @@ function MT.ImDrawListSplitter:SetCurrentChannel(draw_list, idx) draw_list._IdxWritePtr = draw_list.IdxBuffer.Size + 1 -- If current command is used with different settings we need to add a new command - local curr_cmd = (draw_list.CmdBuffer.Size == 0) and nil or draw_list.CmdBuffer.Data[draw_list.CmdBuffer.Size] + local curr_cmd = nil + if draw_list.CmdBuffer.Size > 0 then curr_cmd = draw_list.CmdBuffer.Data[draw_list.CmdBuffer.Size] end if curr_cmd == nil then draw_list:AddDrawCmd() elseif curr_cmd.ElemCount == 0 then @@ -25099,6 +25136,25 @@ function ImGui.RenderColorRectWithAlphaCheckerboard(draw_list, p_min, p_max, col end + +function ImGui.RenderRectFilledWithHole(draw_list, outer, inner, col, rounding) + local fill_L = inner.Min.x > outer.Min.x + local fill_R = inner.Max.x < outer.Max.x + local fill_U = inner.Min.y > outer.Min.y + local fill_D = inner.Max.y < outer.Max.y + local D = ImDrawFlags + local bor = bit32.bor + local function f(c, v) return c and 0 or v end + if fill_L then draw_list:AddRectFilled(ImVec2(outer.Min.x, inner.Min.y), ImVec2(inner.Min.x, inner.Max.y), col, rounding, bor(D.RoundCornersNone, f(fill_U, D.RoundCornersTopLeft), f(fill_D, D.RoundCornersBottomLeft))) end + if fill_R then draw_list:AddRectFilled(ImVec2(inner.Max.x, inner.Min.y), ImVec2(outer.Max.x, inner.Max.y), col, rounding, bor(D.RoundCornersNone, f(fill_U, D.RoundCornersTopRight), f(fill_D, D.RoundCornersBottomRight))) end + if fill_U then draw_list:AddRectFilled(ImVec2(inner.Min.x, outer.Min.y), ImVec2(inner.Max.x, inner.Min.y), col, rounding, bor(D.RoundCornersNone, f(fill_L, D.RoundCornersTopLeft), f(fill_R, D.RoundCornersTopRight))) end + if fill_D then draw_list:AddRectFilled(ImVec2(inner.Min.x, inner.Max.y), ImVec2(inner.Max.x, outer.Max.y), col, rounding, bor(D.RoundCornersNone, f(fill_L, D.RoundCornersBottomLeft), f(fill_R, D.RoundCornersBottomRight))) end + if fill_L and fill_U then draw_list:AddRectFilled(ImVec2(outer.Min.x, outer.Min.y), ImVec2(inner.Min.x, inner.Min.y), col, rounding, D.RoundCornersTopLeft) end + if fill_R and fill_U then draw_list:AddRectFilled(ImVec2(inner.Max.x, outer.Min.y), ImVec2(outer.Max.x, inner.Min.y), col, rounding, D.RoundCornersTopRight) end + if fill_L and fill_D then draw_list:AddRectFilled(ImVec2(outer.Min.x, inner.Max.y), ImVec2(inner.Min.x, outer.Max.y), col, rounding, D.RoundCornersBottomLeft) end + if fill_R and fill_D then draw_list:AddRectFilled(ImVec2(inner.Max.x, inner.Max.y), ImVec2(outer.Max.x, outer.Max.y), col, rounding, D.RoundCornersBottomRight) end +end + return true -- [Roblox] ModuleScripts must return exactly one value end @@ -32207,7 +32263,7 @@ function ImGui.EndTabBar() end g.CurrentTabBarStack:pop_back() - g.CurrentTabBar = (g.CurrentTabBarStack.Size == 0) and nil or g.CurrentTabBarStack.Data[g.CurrentTabBarStack.Size] + if g.CurrentTabBarStack.Size == 0 then g.CurrentTabBar = nil else g.CurrentTabBar = g.CurrentTabBarStack.Data[g.CurrentTabBarStack.Size] end end function ImGui.TabBarLayout(tab_bar) diff --git a/imgui/imgui.lua b/imgui/imgui.lua index 28c649e..20d8517 100644 --- a/imgui/imgui.lua +++ b/imgui/imgui.lua @@ -998,8 +998,8 @@ local function ApplyWindowSettings(window, settings) window.AutoFitFramesX = 0; window.AutoFitFramesY = 0 end window.Collapsed = settings.Collapsed - -- window.DockId = settings.DockId - -- window.DockOrder = settings.DockOrder + window.DockId = settings.DockId or 0 + window.DockOrder = settings.DockOrder or -1 SetWindowConditionAllowFlags(window, ImGuiCond.FirstUseEver, false) end @@ -1030,7 +1030,7 @@ local function InitOrLoadWindowSettings(window, settings) window.SetWindowPosAllowFlags = bit32.bor(ImGuiCond.Always, ImGuiCond.Once, ImGuiCond.FirstUseEver, ImGuiCond.Appearing) window.SetWindowSizeAllowFlags = window.SetWindowPosAllowFlags window.SetWindowCollapsedAllowFlags = window.SetWindowPosAllowFlags - -- window.SetWindowDockAllowFlags = window.SetWindowPosAllowFlags + window.SetWindowDockAllowFlags = window.SetWindowPosAllowFlags ApplyWindowSettings(window, settings) if settings ~= nil then -- TODO: settings.LastUsedDate = g.SessionDate @@ -5105,7 +5105,7 @@ local function RenderWindowOuterBorders(window) RenderWindowOuterSingleBorder(window, border_n, border_col_resizing, ImMax(2.0, window.WindowBorderSize)) end - if g.Style.FrameBorderSize > 0 and (bit32.band(window.Flags, ImGuiWindowFlags.NoTitleBar) == 0) then + if g.Style.FrameBorderSize > 0 and (bit32.band(window.Flags, ImGuiWindowFlags.NoTitleBar) == 0) and not window.DockIsActive then local y = window.Pos.y + window.TitleBarHeight - 1 window.DrawList:AddLineH(window.Pos.x + border_size * 0.5, window.Pos.x + window.Size.x - border_size * 0.5, y, border_col, g.Style.FrameBorderSize) end @@ -5150,11 +5150,19 @@ local function RenderWindowDecorations(window, title_bar_rect, title_bar_is_high if bit32.band(bg_col, IM_COL32_A_MASK) ~= 0 then local bg_rect = ImRect(window.Pos + ImVec2(0, window.TitleBarHeight), window.Pos + window.Size) - local bg_rounding_flags = (bit32.band(flags, ImGuiWindowFlags.NoTitleBar) ~= 0) and ImDrawFlags.RoundCornersAll or ImDrawFlags.RoundCornersBottom - local bg_draw_list = window.DrawList + local bg_rounding_flags + if window.DockIsActive then + bg_rounding_flags = ImGui.CalcRoundingFlagsForRectInRect(bg_rect, window.DockNode.HostWindow:Rect(), 0.0) + else + bg_rounding_flags = (bit32.band(flags, ImGuiWindowFlags.NoTitleBar) ~= 0) and ImDrawFlags.RoundCornersAll or ImDrawFlags.RoundCornersBottom + end + local bg_draw_list = window.DockIsActive and window.DockNode.HostWindow.DrawList or window.DrawList + if window.DockIsActive then bg_draw_list:ChannelsSetCurrent(0) end bg_draw_list:AddRectFilled(bg_rect.Min, bg_rect.Max, bg_col, window_rounding, bg_rounding_flags) + if window.DockIsActive then bg_draw_list:ChannelsSetCurrent(1) end end end + if window.DockIsActive then window.DockNode.IsBgDrawnThisFrame = true end -- Title bar if bit32.band(flags, ImGuiWindowFlags.NoTitleBar) == 0 and not window.DockIsActive then @@ -5172,6 +5180,25 @@ local function RenderWindowDecorations(window, title_bar_rect, title_bar_is_high end end + -- Docking: Unhide tab bar (small triangle in the corner), drag from small triangle to quickly undock + local node = window.DockNode + if window.DockIsActive and node:IsHiddenTabBar() and not node:IsNoTabBar() then + local unhide_sz_draw = ImTrunc(g.FontSize * 0.70) + local unhide_sz_hit = ImTrunc(g.FontSize * 0.55) + local p = node.Pos + local r = ImRect(p, p + ImVec2(unhide_sz_hit, unhide_sz_hit)) + local unhide_id = window:GetID("#UNHIDE") + ImGui.KeepAliveID(unhide_id) + local pressed, hovered, held = ImGui.ButtonBehavior(r, unhide_id, ImGuiButtonFlags.FlattenChildren) + if pressed then + node.WantHiddenTabBarToggle = true + elseif held and ImGui.IsMouseDragging(0) then + ImGui.StartMouseMovingWindowOrNode(window, node, true) + end + local col = ImGui.GetColorU32(((held and hovered) or (node.IsFocused and not hovered)) and ImGuiCol.ButtonActive or (hovered and ImGuiCol.ButtonHovered or ImGuiCol.Button)) + window.DrawList:AddTriangleFilled(p, p + ImVec2(unhide_sz_draw, 0.0), p + ImVec2(0.0, unhide_sz_draw), col) + end + if window.ScrollbarX then ImGui.Scrollbar(ImGuiAxis.X) end @@ -5200,13 +5227,14 @@ local function RenderWindowDecorations(window, title_bar_rect, title_bar_is_high end end - if handle_borders_and_resize_grips then + if handle_borders_and_resize_grips and not window.DockNodeAsHost then RenderWindowOuterBorders(window) end end window.DC.NavLayerCurrent = ImGuiNavLayer.Main end +ImGui.RenderWindowOuterBorders = RenderWindowOuterBorders --- @param window ImGuiWindow --- @param title_bar_rect ImRect @@ -6056,6 +6084,11 @@ end --- @param popup_flags ImGuiPopupFlags function ImGui.IsPopupOpen(id, popup_flags) local g = GImGui + if popup_flags == nil then popup_flags = 0 end + if type(id) == "string" then + -- str_id overload: with AnyPopupId the id must be 0, otherwise it is hashed with the current window's ID stack + id = (bit32.band(popup_flags, ImGuiPopupFlags.AnyPopupId) ~= 0) and 0 or g.CurrentWindow:GetID(id) + end if bit32.band(popup_flags, ImGuiPopupFlags.AnyPopupId) ~= 0 then -- Return true if any popup is open at the current BeginPopup() level of the popup stack @@ -7974,7 +8007,9 @@ local function AddWindowToDrawData(window, layer) local g = GImGui local viewport = window.Viewport g.IO.MetricsRenderWindows = g.IO.MetricsRenderWindows + 1 - -- TODO: splitter + if window.DrawList._Splitter._Count > 1 then + window.DrawList:ChannelsMerge() -- Merge if user forgot to merge back. Also required for DockNodeHost windows. + end ImGui.AddDrawListToDrawDataEx(viewport.DrawDataP, viewport.DrawDataBuilder.Layers[layer], window.DrawList) for _, child in window.DC.ChildWindows:iter() do if (ImGui.IsWindowActiveAndVisible(child)) then -- Clipped children may have been marked not active diff --git a/imgui/imgui_docking.lua b/imgui/imgui_docking.lua index c0231d4..5fc2568 100644 --- a/imgui/imgui_docking.lua +++ b/imgui/imgui_docking.lua @@ -594,6 +594,7 @@ local function FixLargeWindowsWhenUndocking(size, ref_viewport) end function ImGui.DockContextProcessUndockWindow(ctx, window, clear_persistent_docking_ref) + if clear_persistent_docking_ref == nil then clear_persistent_docking_ref = true end -- C++ default argument if window.DockNode then ImGui.DockNodeRemoveWindow(window.DockNode, window, clear_persistent_docking_ref and 0 or window.DockId) else @@ -1580,7 +1581,7 @@ function ImGui.DockNodeCalcSplitRects(pos_old, size_old, pos_new, size_new, dir, local g = ImGui.GetCurrentContext() local dock_spacing = g.Style.ItemInnerSpacing.x local axis = (dir == ImGuiDir.Left or dir == ImGuiDir.Right) and ImGuiAxis.X or ImGuiAxis.Y - local a, o = axis + 1, bit32.bxor(axis, 1) + 1 + local a, o = axis, 3 - axis -- the port's ImGuiAxis is 1-based (X=1,Y=2): it is directly the ImVec2 index pos_new[o] = pos_old[o] size_new[o] = size_old[o] @@ -1724,7 +1725,7 @@ function ImGui.DockNodePreviewDockSetup(host_window, host_node, payload_window, local size_new, size_old = ImVec2(0, 0), ImVec2(data.FutureNode.Size.x, data.FutureNode.Size.y) ImGui.DockNodeCalcSplitRects(pos_old, size_old, pos_new, size_new, split_dir, payload_window.Size) - local split_ratio = ImSaturate(size_new[split_axis + 1] / data.FutureNode.Size[split_axis + 1]) + local split_ratio = ImSaturate(size_new[split_axis] / data.FutureNode.Size[split_axis]) data.FutureNode.Pos = pos_new data.FutureNode.Size = size_new data.SplitRatio = (split_dir == ImGuiDir.Right or split_dir == ImGuiDir.Down) and (1.0 - split_ratio) or split_ratio @@ -1812,7 +1813,7 @@ function ImGui.DockNodePreviewDockRender(host_window, host_node, root_payload, d local cc = draw_r_in:GetCenter() local center = ImVec2(math.floor(cc.x), math.floor(cc.y)) dl:AddRectFilled(draw_r.Min, draw_r.Max, overlay_col, overlay_rounding) - dl:AddRect(draw_r_in.Min, draw_r_in.Max, overlay_col_lines, overlay_rounding, 0, thickness) + dl:AddRect(draw_r_in.Min, draw_r_in.Max, overlay_col_lines, overlay_rounding, thickness) if dir == ImGuiDir.Left or dir == ImGuiDir.Right then dl:AddLineV(center.x, draw_r_in.Min.y, draw_r_in.Max.y, overlay_col_lines, thickness) end @@ -1851,7 +1852,7 @@ function ImGui.DockNodeTreeSplit(ctx, parent_node, split_axis, split_inheritor_c parent_node.AuthorityForPos = ImGuiDataAuthority.DockNode parent_node.AuthorityForSize = ImGuiDataAuthority.DockNode - local a = split_axis + 1 + local a = split_axis local size_avail = parent_node.Size[a] - g.Style.DockingSeparatorSize size_avail = ImMax(size_avail, g.Style.WindowMinSize[a] * 2.0) IM_ASSERT(size_avail > 0.0) @@ -1937,7 +1938,7 @@ function ImGui.DockNodeTreeUpdatePosSize(node, pos, size, only_write_to_single_n if child_0_is_or_will_be_visible and child_1_is_or_will_be_visible then local spacing = g.Style.DockingSeparatorSize - local a = node.SplitAxis + 1 + local a = node.SplitAxis local size_avail = ImMax(size[a] - spacing, 0.0) local size_min_each = ImTrunc(ImMin(size_avail, g.Style.WindowMinSize[a] * 2.0) * 0.5) @@ -2006,7 +2007,7 @@ function ImGui.DockNodeTreeUpdateSplitter(node) if child_0.IsVisible and child_1.IsVisible then local axis = node.SplitAxis IM_ASSERT(axis ~= ImGuiAxis.None) - local a, o = axis + 1, bit32.bxor(axis, 1) + 1 + local a, o = axis, 3 - axis -- the port's ImGuiAxis is 1-based (X=1,Y=2): it is directly the ImVec2 index local bb = ImRect(ImVec2(child_0.Pos.x, child_0.Pos.y), ImVec2(child_1.Pos.x, child_1.Pos.y)) bb.Min[a] = bb.Min[a] + child_0.Size[a] bb.Max[o] = bb.Max[o] + child_1.Size[o] @@ -3097,7 +3098,7 @@ function ImGui.SplitterBehavior(bb, id, axis, size1, size2, min_size1, min_size2 local bb_render = ImRect(bb.Min, bb.Max) if held then local d = g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min - local mouse_delta = d[axis + 1] + local mouse_delta = d[axis] local size_1_maximum_delta = ImMax(0.0, size1 - min_size1) local size_2_maximum_delta = ImMax(0.0, size2 - min_size2) diff --git a/imgui/imgui_dragdrop.lua b/imgui/imgui_dragdrop.lua index a241a7f..ada8904 100644 --- a/imgui/imgui_dragdrop.lua +++ b/imgui/imgui_dragdrop.lua @@ -266,7 +266,7 @@ end function ImGui.RenderDragDropTargetRectEx(draw_list, bb, rounding) local g = ImGui.GetCurrentContext() draw_list:AddRectFilled(bb.Min, bb.Max, ImGui.GetColorU32(ImGuiCol.DragDropTargetBg), rounding, 0) - draw_list:AddRect(bb.Min, bb.Max, ImGui.GetColorU32(ImGuiCol.DragDropTarget), rounding, 0, g.Style.DragDropTargetBorderSize) + draw_list:AddRect(bb.Min, bb.Max, ImGui.GetColorU32(ImGuiCol.DragDropTarget), rounding, g.Style.DragDropTargetBorderSize) end --- @return ImGuiPayload? diff --git a/imgui/imgui_draw.lua b/imgui/imgui_draw.lua index d7d1bfc..cdef103 100644 --- a/imgui/imgui_draw.lua +++ b/imgui/imgui_draw.lua @@ -3492,7 +3492,8 @@ function MT.ImDrawListSplitter:SetCurrentChannel(draw_list, idx) draw_list._IdxWritePtr = draw_list.IdxBuffer.Size + 1 -- If current command is used with different settings we need to add a new command - local curr_cmd = (draw_list.CmdBuffer.Size == 0) and nil or draw_list.CmdBuffer.Data[draw_list.CmdBuffer.Size] + local curr_cmd = nil + if draw_list.CmdBuffer.Size > 0 then curr_cmd = draw_list.CmdBuffer.Data[draw_list.CmdBuffer.Size] end if curr_cmd == nil then draw_list:AddDrawCmd() elseif curr_cmd.ElemCount == 0 then @@ -4969,4 +4970,23 @@ function ImGui.RenderColorRectWithAlphaCheckerboard(draw_list, p_min, p_max, col end + +function ImGui.RenderRectFilledWithHole(draw_list, outer, inner, col, rounding) + local fill_L = inner.Min.x > outer.Min.x + local fill_R = inner.Max.x < outer.Max.x + local fill_U = inner.Min.y > outer.Min.y + local fill_D = inner.Max.y < outer.Max.y + local D = ImDrawFlags + local bor = bit32.bor + local function f(c, v) return c and 0 or v end + if fill_L then draw_list:AddRectFilled(ImVec2(outer.Min.x, inner.Min.y), ImVec2(inner.Min.x, inner.Max.y), col, rounding, bor(D.RoundCornersNone, f(fill_U, D.RoundCornersTopLeft), f(fill_D, D.RoundCornersBottomLeft))) end + if fill_R then draw_list:AddRectFilled(ImVec2(inner.Max.x, inner.Min.y), ImVec2(outer.Max.x, inner.Max.y), col, rounding, bor(D.RoundCornersNone, f(fill_U, D.RoundCornersTopRight), f(fill_D, D.RoundCornersBottomRight))) end + if fill_U then draw_list:AddRectFilled(ImVec2(inner.Min.x, outer.Min.y), ImVec2(inner.Max.x, inner.Min.y), col, rounding, bor(D.RoundCornersNone, f(fill_L, D.RoundCornersTopLeft), f(fill_R, D.RoundCornersTopRight))) end + if fill_D then draw_list:AddRectFilled(ImVec2(inner.Min.x, inner.Max.y), ImVec2(inner.Max.x, outer.Max.y), col, rounding, bor(D.RoundCornersNone, f(fill_L, D.RoundCornersBottomLeft), f(fill_R, D.RoundCornersBottomRight))) end + if fill_L and fill_U then draw_list:AddRectFilled(ImVec2(outer.Min.x, outer.Min.y), ImVec2(inner.Min.x, inner.Min.y), col, rounding, D.RoundCornersTopLeft) end + if fill_R and fill_U then draw_list:AddRectFilled(ImVec2(inner.Max.x, outer.Min.y), ImVec2(outer.Max.x, inner.Min.y), col, rounding, D.RoundCornersTopRight) end + if fill_L and fill_D then draw_list:AddRectFilled(ImVec2(outer.Min.x, inner.Max.y), ImVec2(inner.Min.x, outer.Max.y), col, rounding, D.RoundCornersBottomLeft) end + if fill_R and fill_D then draw_list:AddRectFilled(ImVec2(inner.Max.x, inner.Max.y), ImVec2(outer.Max.x, outer.Max.y), col, rounding, D.RoundCornersBottomRight) end +end + return true -- [Roblox] ModuleScripts must return exactly one value diff --git a/imgui/imgui_tabs.lua b/imgui/imgui_tabs.lua index 3d0b079..d088d27 100644 --- a/imgui/imgui_tabs.lua +++ b/imgui/imgui_tabs.lua @@ -218,7 +218,7 @@ function ImGui.EndTabBar() end g.CurrentTabBarStack:pop_back() - g.CurrentTabBar = (g.CurrentTabBarStack.Size == 0) and nil or g.CurrentTabBarStack.Data[g.CurrentTabBarStack.Size] + if g.CurrentTabBarStack.Size == 0 then g.CurrentTabBar = nil else g.CurrentTabBar = g.CurrentTabBarStack.Data[g.CurrentTabBarStack.Size] end end function ImGui.TabBarLayout(tab_bar) diff --git a/main.client.luau b/main.client.luau index b115687..805cbeb 100755 --- a/main.client.luau +++ b/main.client.luau @@ -17,7 +17,7 @@ local counter = 0 ImGui.CreateContext() local io = ImGui.GetIO() -io.ConfigFlags = bit32.bor(io.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard) +io.ConfigFlags = bit32.bor(io.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard, ImGuiConfigFlags.DockingEnable) local style = ImGui.GetStyle() style:ScaleAllSizes(main_scale) @@ -29,6 +29,9 @@ RunService.Stepped:Connect(function() ImGui_ImplRoblox.NewFrame() ImGui.NewFrame() + -- Dockspace covering the whole screen; the centre stays transparent so the game shows through + ImGui.DockSpaceOverViewport(0, nil, G.ImGuiDockNodeFlags.PassthruCentralNode) + if show_demo_window then ImGui.ShowDemoWindow() end diff --git a/tools/test/PORTING.md b/tools/test/PORTING.md index 716724e..5df50ed 100644 --- a/tools/test/PORTING.md +++ b/tools/test/PORTING.md @@ -45,10 +45,14 @@ there, but keep this port's existing data structures and conventions. Read the s - `python3 tools/test/run.py` — runs main.client.luau 400 frames with scripted random input (mouse, clicks, keys, typing). Errors print with `imgui/.lua:` mapped stack traces. - `--open` forces every CollapsingHeader/TreeNode open (exercises the whole demo). `--big` makes the demo - window 900x1000. `--png out.png` writes a screenshot (Read the PNG to look at it). `--check` asserts the incremental renderer is pixel identical to a full redraw. `--main file.luau` runs your own test LocalScript (copy main.client.luau as a template: `local G = require(ReplicatedStorage:WaitForChild("ImGui"))`, all library globals are fields of `G`). - `python3 tools/test/missing.py [tables|widgets|imgui.cpp]` lists upstream functions not yet ported. - Several people edit the repo concurrently. Keep every file syntactically valid after each edit. If a run fails because of an error in a file you don't own, wait a minute and retry instead of editing it. + +- **ImGuiAxis is 1-based in this port** (`None=0, X=1, Y=2`; upstream `None=-1, X=0, Y=1`). Use `v[axis]` to index an ImVec2 and `3 - axis` for the other axis. `ImGuiDir` keeps upstream values (`None=-1, Left=0, Right=1, Up=2, Down=3`). +- Never write `cond and nil or x` in Lua: it always evaluates `x`. Use an `if`. +- Port signature quirk: `AddRect(min, max, col, rounding, thickness, flags)` (thickness BEFORE flags; upstream has flags first). Check the port's signature before porting any draw call that takes both.\n +- C++ default arguments (e.g. `bool x = true`) must be reproduced with `if x == nil then x = true end`; a missing Lua argument is nil/falsy. diff --git a/tools/test/roblox_mock.luau b/tools/test/roblox_mock.luau index 8600abb..cb51e4c 100644 --- a/tools/test/roblox_mock.luau +++ b/tools/test/roblox_mock.luau @@ -195,7 +195,7 @@ for i = 0, 255 do hex[i] = string.format("%02x", i) end local total = 0 for frame = 1, NFRAMES do - local r = rnd(100) + local r = ARG_QUIET and 100 or rnd(100) if r < 40 then mouse = v2(math.clamp(mouse.X + rnd(81) - 40, 0, SCREEN_W - 1), math.clamp(mouse.Y + rnd(81) - 40, 0, SCREEN_H - 1)) elseif r < 55 then @@ -221,25 +221,5 @@ for frame = 1, NFRAMES do if frame > 10 then total = total + dt end if frame == 10 then render_time = 0; _G_UPLOADS = 0 end - if DUMP_FRAMES[frame] and ARG_DUMP then - local W, H = SCREEN_W, SCREEN_H - local screen = buffer.create(W * H * 4) - local gui = PlayerGui:FindFirstChild("ImGui") - for _, label in ipairs(gui:GetChildren()) do - if label.Visible then - local img, px, py = label.ImageContent.Object, label.Position.X, label.Position.Y - local tw, th = img.Size.X, img.Size.Y - for y = 0, th - 1 do - if py + y < H then - local n = math.min(tw, W - px) - buffer.copy(screen, ((py + y) * W + px) * 4, img.Pixels, y * tw * 4, n * 4) - end - end - end - end - local parts = {} - for i = 0, W * H * 4 - 1 do parts[#parts + 1] = hex[buffer.readu8(screen, i)] end - print(string.format("IMG %d %d %d %s", frame, W, H, table.concat(parts))) - end end print(string.format("avg frame total %.2f ms, of which render %.2f ms, uploads/frame %.2f", total / (NFRAMES - 10) * 1000, render_time / (NFRAMES - 10) * 1000, _G_UPLOADS / (NFRAMES - 10))) diff --git a/tools/test/run.py b/tools/test/run.py index da9d077..f121434 100644 --- a/tools/test/run.py +++ b/tools/test/run.py @@ -4,12 +4,9 @@ python3 tools/test/run.py # 400 frames of main.client.luau, scripted random input python3 tools/test/run.py --open # force every CollapsingHeader/TreeNode open (exercises whole demo) - python3 tools/test/run.py --big --open --bench # timing, best of 5 - python3 tools/test/run.py --check # incremental vs forced redraw must be pixel identical - python3 tools/test/run.py --png out.png # screenshot of the last frame python3 tools/test/run.py --main my_test.luau # different LocalScript """ -import argparse, os, shutil, subprocess, sys, zlib, struct, tempfile +import argparse, os, shutil, subprocess, sys, tempfile TMP = tempfile.mkdtemp(prefix="imgui_test_") # per process: parallel runs must not share files HERE = os.path.dirname(os.path.abspath(__file__)) @@ -34,7 +31,7 @@ def build_run(args, flags): return path def run(args, extra=""): - flags = "ARG_FRAMES=%d; ARG_BIG=%s; ARG_OPEN=%s; %s" % (args.frames, str(args.big).lower(), str(args.open).lower(), extra) + flags = "ARG_FRAMES=%d; ARG_BIG=%s; ARG_OPEN=%s; ARG_QUIET=%s; %s" % (args.frames, str(args.big).lower(), str(args.open).lower(), str(args.quiet).lower(), extra) try: p = subprocess.run([LUAU, "-O2", "--codegen", build_run(args, flags)], capture_output=True, text=True, timeout=args.timeout) except subprocess.TimeoutExpired as e: @@ -56,57 +53,18 @@ def remap(text): return "%s:%d" % (best[1], n - best[0] + 1) return re.sub(r"\S*run\.luau:(\d+)", sub, text) -def images(out): - return [l.split() for l in out.split("\n") if l.startswith("IMG ")] - -def write_png(img, path, crop=None): - _, fr, w, h, hx = img; w, h = int(w), int(h); px = bytes.fromhex(hx) - W, H = crop or (w, h) - raw = bytearray() - for y in range(H): - raw.append(0) - for x in range(W): - o = (y * w + x) * 4; r, g, b, a = px[o:o + 4]; k = a / 255 - raw += bytes([int(r * k + 90 * (1 - k)), int(g * k + 90 * (1 - k)), int(b * k + 90 * (1 - k))]) - def chunk(t, d): return struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t + d) & 0xffffffff) - open(path, 'wb').write(b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', W, H, 8, 2, 0, 0, 0)) - + chunk(b'IDAT', zlib.compress(bytes(raw))) + chunk(b'IEND', b'')) - def main(): ap = argparse.ArgumentParser() ap.add_argument("--frames", type=int, default=400) ap.add_argument("--main", default=os.path.join(ROOT, "main.client.luau")) ap.add_argument("--big", action="store_true", help="demo window forced to 900x1000") ap.add_argument("--open", action="store_true", help="force all CollapsingHeader/TreeNode open") - ap.add_argument("--bench", action="store_true") - ap.add_argument("--check", action="store_true") - ap.add_argument("--png") - ap.add_argument("--crop", default="1100x1080") ap.add_argument("--timeout", type=int, default=120) + ap.add_argument("--quiet", action="store_true", help="no random input (scripted tests drive io themselves)") args = ap.parse_args() - if args.bench: - for mode in ["", "ARG_FORCE=true"]: - best = None - for _ in range(5): - code, out = run(args, mode) - if code: print(out[-3000:]); sys.exit(1) - line = [l for l in out.split("\n") if l.startswith("avg frame")][-1] - if best is None or float(line.split()[3]) < float(best.split()[3]): best = line - print("%-12s %s" % (mode or "incremental", best)) - return - if args.check: - code, a = run(args, "ARG_DUMP=true") - code2, b = run(args, "ARG_DUMP=true; ARG_FORCE=true") - if code or code2: print((a if code else b)[-3000:]); sys.exit(1) - ok = [x[4] for x in images(a)] == [x[4] for x in images(b)] - print("PIXELS-IDENTICAL" if ok else "PIXELS-DIFFER"); sys.exit(0 if ok else 1) - - code, out = run(args, "ARG_DUMP=true" if args.png else "") - print("\n".join(l if not l.startswith("IMG ") else l[:40] + "..." for l in out.split("\n"))[-4000:]) - if args.png and images(out): - write_png(images(out)[-1], args.png, tuple(map(int, args.crop.split("x")))) - print("wrote", args.png) + code, out = run(args, "") + print(out[-4000:]) sys.exit(code) try: diff --git a/tools/test/scripts/t_dock.luau b/tools/test/scripts/t_dock.luau new file mode 100644 index 0000000..9779726 --- /dev/null +++ b/tools/test/scripts/t_dock.luau @@ -0,0 +1,47 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") +local G = require(ReplicatedStorage:WaitForChild("ImGui")) +local ImGui, ImVec2 = G.ImGui, G.ImVec2 +ImGui.CreateContext() +local io = ImGui.GetIO() +io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.DockingEnable) +G.ImGui_ImplRoblox.Init() +local frame = 0 +local dockspace_id +local winA, winB, winC +RunService.Stepped:Connect(function() + frame += 1 + G.ImGui_ImplRoblox.NewFrame() + ImGui.NewFrame() + dockspace_id = ImGui.DockSpaceOverViewport(0, nil, 0) + if frame == 2 then + ImGui.DockBuilderRemoveNode(dockspace_id) + ImGui.DockBuilderAddNode(dockspace_id, bit32.bor(G.ImGuiDockNodeFlags.DockSpace)) + ImGui.DockBuilderSetNodeSize(dockspace_id, ImGui.GetMainViewport().Size) + local left, right = ImGui.DockBuilderSplitNode(dockspace_id, G.ImGuiDir.Left, 0.3) + ImGui.DockBuilderDockWindow("A", left) + ImGui.DockBuilderDockWindow("B", right) + ImGui.DockBuilderDockWindow("C", right) + ImGui.DockBuilderFinish(dockspace_id) + print("split", left, right) + end + for _, n in ipairs({"A", "B", "C"}) do + if ImGui.Begin(n) then ImGui.Text("window " .. n) end + ImGui.End() + end + if frame == 30 then + local g = ImGui.GetCurrentContext() + local a, b, c = ImGui.FindWindowByName("A"), ImGui.FindWindowByName("B"), ImGui.FindWindowByName("C") + print("A node", a.DockNode and a.DockNode.ID, "B node", b.DockNode and b.DockNode.ID, "C node", c.DockNode and c.DockNode.ID) + assert(a.DockNode ~= nil and b.DockNode ~= nil, "windows must be docked") + assert(a.DockNode ~= b.DockNode, "A and B in different nodes") + assert(b.DockNode == c.DockNode, "B and C share a node") + assert(b.DockNode.TabBar ~= nil and b.DockNode.TabBar.Tabs.Size == 2, "B/C tab bar with 2 tabs") + local ini = ImGui.SaveIniSettingsToMemory() + print(ini) + assert(ini:find("%[Docking%]%[Data%]"), "docking ini section") + print("DOCK OK") + end + ImGui.Render() + G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData()) +end) diff --git a/tools/test/scripts/t_dock_drag.luau b/tools/test/scripts/t_dock_drag.luau new file mode 100644 index 0000000..57ea90b --- /dev/null +++ b/tools/test/scripts/t_dock_drag.luau @@ -0,0 +1,69 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") +local G = require(ReplicatedStorage:WaitForChild("ImGui")) +local ImGui, ImVec2 = G.ImGui, G.ImVec2 +ImGui.CreateContext() +local io = ImGui.GetIO() +io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.DockingEnable) +G.ImGui_ImplRoblox.Init() +local frame = 0 +-- script: {frame, x, y, down} +local function lerp(a, b, t) return a + (b - a) * t end +local function script(f) + if f >= 50 then + -- tab of B inside the docked node: press on the tab, drag far downward to undock + if f < 52 then return 150, 108, false end + if f == 52 then return 150, 108, true end + if f <= 66 then local t = (f - 52) / 14 return lerp(150, 700, t), lerp(108, 600, t), true end + if f <= 70 then return 700, 600, true end + return 700, 600, false + end + if f < 6 then return 600, 108, false end + if f == 6 then return 600, 108, true end + if f <= 20 then local t = (f - 6) / 14 return lerp(600, 250, t), lerp(108, 190, t), true end + if f <= 26 then return 250, 190, true end + if f == 27 then return 250, 190, false end + return 250, 190, false +end +local last_down = false +RunService.Stepped:Connect(function() + frame += 1 + G.ImGui_ImplRoblox.NewFrame() + local x, y, down = script(frame) + io:AddMousePosEvent(x, y) + if down ~= last_down then io:AddMouseButtonEvent(0, down); last_down = down end + ImGui.NewFrame() + ImGui.SetNextWindowPos(ImVec2(100, 100), G.ImGuiCond.FirstUseEver) + ImGui.SetNextWindowSize(ImVec2(300, 200), G.ImGuiCond.FirstUseEver) + if ImGui.Begin("A") then ImGui.Text("window A") end + ImGui.End() + ImGui.SetNextWindowPos(ImVec2(500, 100), G.ImGuiCond.FirstUseEver) + ImGui.SetNextWindowSize(ImVec2(300, 200), G.ImGuiCond.FirstUseEver) + if ImGui.Begin("B") then ImGui.Text("window B") end + ImGui.End() + if frame == 28 or frame == 40 then + local a, b = ImGui.FindWindowByName("A"), ImGui.FindWindowByName("B") + local g = ImGui.GetCurrentContext() + print("frame", frame, "movingwindow", g.MovingWindow and g.MovingWindow.Name, "dragdrop", g.DragDropActive, "A.node", a.DockNode and a.DockNode.ID, "B.node", b.DockNode and b.DockNode.ID, "B.pos", b.Pos.x, b.Pos.y) + end + if frame == 45 then + local a, b = ImGui.FindWindowByName("A"), ImGui.FindWindowByName("B") + assert(a.DockNode ~= nil and a.DockNode == b.DockNode, "B must be docked into A's node") + assert(a.DockNode.Windows.Size == 2, "node has 2 windows") + print("DRAG DOCK OK") + end + if frame >= 60 and frame <= 90 and frame % 3 == 0 then local g = ImGui.GetCurrentContext(); local b = ImGui.FindWindowByName("B"); local tb = b.DockNode and b.DockNode.TabBar; print("f", frame, "Bnode", b.DockNode and b.DockNode.ID, "Bpos", b.Pos.x, b.Pos.y, "active", g.ActiveId, "hovered", g.HoveredId, "hovwin", g.HoveredWindow and g.HoveredWindow.Name, "tabid", b.TabId, "moving", g.MovingWindow and g.MovingWindow.Name, "dd", g.DragDropActive, "mouse", g.IO.MousePos.x, g.IO.MousePos.y, "down", g.IO.MouseDown[0], "drag", ImGui.IsMouseDragging(0)) end + if frame == 52 then + local b = ImGui.FindWindowByName("B") + local tb = b.DockNode.TabBar + for i = 1, tb.Tabs.Size do local t = tb.Tabs.Data[i]; print("tab", i, ImGui.TabBarGetTabName(tb, t), "offset", t.Offset, "width", t.Width, "bar", tb.BarRect.Min.x, tb.BarRect.Min.y, tb.BarRect.Max.x, tb.BarRect.Max.y) end + end + if frame == 90 then + local a, b = ImGui.FindWindowByName("A"), ImGui.FindWindowByName("B") + print("final", "A.node", a.DockNode and a.DockNode.ID, "B.node", b.DockNode and b.DockNode.ID, "B.pos", b.Pos.x, b.Pos.y) + assert(b.DockNode == nil or b.DockNode ~= a.DockNode, "B must be undocked from A") + print("UNDOCK OK") + end + ImGui.Render() + G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData()) +end) diff --git a/tools/test/scripts/t_dock_fuzz2.luau b/tools/test/scripts/t_dock_fuzz2.luau new file mode 100644 index 0000000..c5e0268 --- /dev/null +++ b/tools/test/scripts/t_dock_fuzz2.luau @@ -0,0 +1,80 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") +local G = require(ReplicatedStorage:WaitForChild("ImGui")) +local ImGui, ImVec2 = G.ImGui, G.ImVec2 +ImGui.CreateContext() +local io = ImGui.GetIO() +io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.DockingEnable) +G.ImGui_ImplRoblox.Init() +local frame = 0 +local seed = 7 +local function rnd(n) seed = (seed * 1103515245 + 12345) % 2147483648; return seed % n end +local names = {"A", "B", "C", "D", "E"} +local drag -- {fx,fy,tx,ty,start} +local last_down = false +local docks, undocks = 0, 0 +local function check_tree() + local g = ImGui.GetCurrentContext() + for id, node in pairs(g.DockContext.Nodes) do + assert(node.ID == id) + if node.ParentNode then + assert(node.ParentNode.ChildNodes[0] == node or node.ParentNode.ChildNodes[1] == node, "parent link") + end + for _, w in node.Windows:iter() do assert(w.DockNode == node or w.DockNode == nil, "window->node link") end + if node:IsSplitNode() then assert(node.ChildNodes[1] ~= nil and node.Windows.Size == 0, "split node has two children and no windows") end + end + for _, n in ipairs(names) do + local w = ImGui.FindWindowByName(n) + if w and w.DockNode then assert(w.DockNode.Windows:find_index(w) ~= nil, "node contains window") end + end +end +RunService.Stepped:Connect(function() + frame += 1 + G.ImGui_ImplRoblox.NewFrame() + -- scripted drag + local x, y, down = (drag and drag.x or 5), (drag and drag.y or 5), false + if not drag and frame % 45 == 0 then + local w = ImGui.FindWindowByName(names[rnd(#names) + 1]) + if w and w.Active then + local sx, sy + if w.DockNode and w.DockNode.TabBar and w.DockNode.TabBar.Tabs.Size > 0 and rnd(2) == 0 then + local tb = w.DockNode.TabBar; local t = tb.Tabs.Data[rnd(tb.Tabs.Size) + 1] + sx, sy = tb.BarRect.Min.x + t.Offset + 4, tb.BarRect.Min.y + 8 + else sx, sy = w.Pos.x + 40, w.Pos.y + 8 end + local mode = rnd(3) + local tx, ty + if mode == 0 then local o = ImGui.FindWindowByName(names[rnd(#names) + 1]); tx, ty = o.Pos.x + o.Size.x * (0.1 + rnd(80) / 100), o.Pos.y + o.Size.y * (0.1 + rnd(80) / 100) + else tx, ty = 50 + rnd(1800), 50 + rnd(980) end + drag = { sx = sx, sy = sy, tx = tx, ty = ty, start = frame, x = sx, y = sy } + x, y = sx, sy + end + end + if drag then + local f = frame - drag.start + local t = math.min(f / 14, 1) + drag.x = drag.sx + (drag.tx - drag.sx) * t; drag.y = drag.sy + (drag.ty - drag.sy) * t + x, y = drag.x, drag.y + down = f >= 1 and f <= 24 + if f > 26 then drag = nil end + end + io:AddMousePosEvent(x, y) + if down ~= last_down then io:AddMouseButtonEvent(0, down); last_down = down end + ImGui.NewFrame() + ImGui.DockSpaceOverViewport(0, nil, G.ImGuiDockNodeFlags.PassthruCentralNode) + for i, n in ipairs(names) do + ImGui.SetNextWindowPos(ImVec2(60 + i * 90, 60 + i * 50), G.ImGuiCond.FirstUseEver) + ImGui.SetNextWindowSize(ImVec2(260, 180), G.ImGuiCond.FirstUseEver) + if ImGui.Begin(n) then ImGui.Text("window " .. n) end + ImGui.End() + end + ImGui.Render() + G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData()) + if frame % 50 == 0 then check_tree() end + if frame % 1000 == 0 then + local nodes, docked = 0, 0 + for _ in pairs(ImGui.GetCurrentContext().DockContext.Nodes) do nodes += 1 end + for _, n in ipairs(names) do local w = ImGui.FindWindowByName(n); if w and w.DockNode then docked += 1 end end + print("frame", frame, "nodes", nodes, "docked windows", docked) + local ini = ImGui.SaveIniSettingsToMemory() + end +end) diff --git a/tools/test/scripts/t_dock_ini.luau b/tools/test/scripts/t_dock_ini.luau new file mode 100644 index 0000000..328b192 --- /dev/null +++ b/tools/test/scripts/t_dock_ini.luau @@ -0,0 +1,61 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") +local G = require(ReplicatedStorage:WaitForChild("ImGui")) +local ImGui, ImVec2 = G.ImGui, G.ImVec2 +local function setup() + ImGui.CreateContext() + local io = ImGui.GetIO() + io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.DockingEnable) + G.ImGui_ImplRoblox.Init() +end +setup() +local frame, phase, ini, base = 0, 1, nil, 0 +local function windows() + for _, n in ipairs({"A", "B", "C"}) do + if ImGui.Begin(n) then ImGui.Text("window " .. n) end + ImGui.End() + end +end +RunService.Stepped:Connect(function() + frame += 1 + G.ImGui_ImplRoblox.NewFrame() + ImGui.NewFrame() + local id = ImGui.DockSpaceOverViewport(0, nil, 0) + local f = frame - base + if phase == 1 then + if f == 2 then + ImGui.DockBuilderRemoveNode(id) + ImGui.DockBuilderAddNode(id, G.ImGuiDockNodeFlags.DockSpace) + ImGui.DockBuilderSetNodeSize(id, ImGui.GetMainViewport().Size) + local l, r = ImGui.DockBuilderSplitNode(id, G.ImGuiDir.Left, 0.25) + local rt, rb = ImGui.DockBuilderSplitNode(r, G.ImGuiDir.Up, 0.6) + ImGui.DockBuilderDockWindow("A", l); ImGui.DockBuilderDockWindow("B", rt); ImGui.DockBuilderDockWindow("C", rb) + ImGui.DockBuilderFinish(id) + end + windows() + if f == 20 then + ini = ImGui.SaveIniSettingsToMemory() + print(ini:match("%[Docking%].*")) + end + else + windows() + if f == 20 then + local a, b, c = ImGui.FindWindowByName("A"), ImGui.FindWindowByName("B"), ImGui.FindWindowByName("C") + print("reloaded nodes", a.DockNode and a.DockNode.ID, b.DockNode and b.DockNode.ID, c.DockNode and c.DockNode.ID, "sizes", a.Size.x, b.Size.x, b.Size.y, c.Size.y) + assert(a.DockNode and b.DockNode and c.DockNode, "all docked after reload") + assert(a.DockNode ~= b.DockNode and b.DockNode ~= c.DockNode, "3 distinct nodes") + local ini2 = ImGui.SaveIniSettingsToMemory() + assert(ini2:match("%[Docking%].*") == ini:match("%[Docking%].*"), "docking ini identical after round trip") + print("INI ROUNDTRIP OK") + end + end + ImGui.Render() + G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData()) + if phase == 1 and f == 20 then + G.ImGui_ImplRoblox.Shutdown() + ImGui.DestroyContext() + setup() + ImGui.LoadIniSettingsFromMemory(ini) + phase, base = 2, frame + end +end) diff --git a/tools/test/scripts/t_modal.luau b/tools/test/scripts/t_modal.luau new file mode 100644 index 0000000..b00bc49 --- /dev/null +++ b/tools/test/scripts/t_modal.luau @@ -0,0 +1,41 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") +local G = require(ReplicatedStorage:WaitForChild("ImGui")) +local ImGui, ImVec2 = G.ImGui, G.ImVec2 +ImGui.CreateContext() +G.ImGui_ImplRoblox.Init() +local frame = 0 +local logged = nil +RunService.Stepped:Connect(function() + frame += 1 + G.ImGui_ImplRoblox.NewFrame() + ImGui.NewFrame() + ImGui.SetNextWindowPos(ImVec2(20, 20), G.ImGuiCond.Always) + ImGui.SetNextWindowSize(ImVec2(400, 300), G.ImGuiCond.Always) + ImGui.Begin("Popups") + if frame == 3 then ImGui.OpenPopup("Delete?") end + if frame == 5 then ImGui.LogToClipboard() end + ImGui.Text("Some text to log") + if frame == 5 then ImGui.LogFinish(); logged = ImGui.GetClipboardText() end + ImGui.BeginChild("child", ImVec2(0, 100), G.ImGuiChildFlags.Borders) + for i = 1, 20 do ImGui.Text("child line " .. i) end + ImGui.EndChild() + if ImGui.BeginPopupContextWindow() then ImGui.Text("ctx"); ImGui.EndPopup() end + if ImGui.BeginPopupModal("Delete?", nil, G.ImGuiWindowFlags.AlwaysAutoResize) then + ImGui.Text("All those beautiful files will be deleted.\nThis operation cannot be undone!") + ImGui.Separator() + if ImGui.Button("OK", ImVec2(120, 0)) then ImGui.CloseCurrentPopup() end + ImGui.EndPopup() + end + ImGui.End() + ImGui.Render() + G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData()) + if frame == 6 then + print("LOG:", logged) + local ini = ImGui.SaveIniSettingsToMemory() + print("INI:", ini) + ImGui.LoadIniSettingsFromMemory(ini) + assert(ImGui.SaveIniSettingsToMemory() == ini, "ini round trip") + print("INI round trip ok") + end +end) diff --git a/tools/test/scripts/t_tabs.luau b/tools/test/scripts/t_tabs.luau new file mode 100644 index 0000000..cb18f0c --- /dev/null +++ b/tools/test/scripts/t_tabs.luau @@ -0,0 +1,30 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") +local G = require(ReplicatedStorage:WaitForChild("ImGui")) +local ImGui, ImVec2 = G.ImGui, G.ImVec2 +ImGui.CreateContext() +G.ImGui_ImplRoblox.Init() +local opened = {true, true, true, true, true, true, true, true} +local frame = 0 +RunService.Stepped:Connect(function() + frame += 1 + G.ImGui_ImplRoblox.NewFrame() + ImGui.NewFrame() + ImGui.SetNextWindowPos(ImVec2(0, 0), G.ImGuiCond.Always) + ImGui.SetNextWindowSize(ImVec2(300, 300), G.ImGuiCond.Always) + ImGui.Begin("Tabs") + local flags = bit32.bor(G.ImGuiTabBarFlags.Reorderable, G.ImGuiTabBarFlags.TabListPopupButton, G.ImGuiTabBarFlags.FittingPolicyScroll, G.ImGuiTabBarFlags.AutoSelectNewTabs) + if ImGui.BeginTabBar("bar", flags) then + if ImGui.TabItemButton("+", G.ImGuiTabItemFlags.Trailing) then end + for i = 1, #opened do + local vis + vis, opened[i] = ImGui.BeginTabItem("Tab number " .. i, opened[i], (i == 3) and G.ImGuiTabItemFlags.UnsavedDocument or 0) + if vis then ImGui.Text("Contents of tab " .. i); ImGui.EndTabItem() end + end + ImGui.EndTabBar() + end + if frame == 50 then local tb = ImGui.GetCurrentContext().TabBars:GetByIndex(0); print("tabs", tb.Tabs.Size, "selected", tb.SelectedTabId ~= 0, "scrollbtn", tb.ScrollButtonEnabled) end + ImGui.End() + ImGui.Render() + G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData()) +end)