Beispiel #1
0
        private unsafe void DrawResourceMap(ResourceCategory category, uint ext, StdMap <uint, Pointer <ResourceHandle> > *map)
        {
            if (map == null)
            {
                return;
            }

            var label = GetNodeLabel(( uint )category, ext, map->Count);

            using var tree = ImRaii.TreeNode(label);
            if (!tree || map->Count == 0)
            {
                return;
            }

            using var table = ImRaii.Table("##table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
            if (!table)
            {
                return;
            }

            ImGui.TableSetupColumn("Hash", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth);
            ImGui.TableSetupColumn("Ptr", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth);
            ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthFixed, _pathColumnWidth);
            ImGui.TableSetupColumn("Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth);
            ImGui.TableHeadersRow();

            ResourceLoader.IterateResourceMap(map, (hash, r) =>
            {
                // Filter unwanted names.
                if (_resourceManagerFilter.Length != 0 &&
                    !r->FileName.ToString().Contains(_resourceManagerFilter, StringComparison.InvariantCultureIgnoreCase))
                {
                    return;
                }

                var address = $"0x{( ulong )r:X}";
                ImGuiUtil.TextNextColumn($"0x{hash:X8}");
                ImGui.TableNextColumn();
                ImGuiUtil.CopyOnClickSelectable(address);

                var resource = (Interop.Structs.ResourceHandle *)r;
                ImGui.TableNextColumn();
                Text(resource);
                if (ImGui.IsItemClicked())
                {
                    var data = Interop.Structs.ResourceHandle.GetData(resource);
                    if (data != null)
                    {
                        var length = ( int )Interop.Structs.ResourceHandle.GetLength(resource);
                        ImGui.SetClipboardText(string.Join(" ",
                                                           new ReadOnlySpan <byte>(data, length).ToArray().Select(b => b.ToString("X2"))));
                    }
                }

                ImGuiUtil.HoverTooltip("Click to copy byte-wise file data to clipboard, if any.");

                ImGuiUtil.TextNextColumn(r->RefCount.ToString());
            });
        }
        // Draw either a website button if the source is a valid website address,
        // or a source text if it is not.
        private void DrawWebsite()
        {
            if (_websiteValid)
            {
                if (ImGui.SmallButton(_modWebsiteButton))
                {
                    try
                    {
                        var process = new ProcessStartInfo(_modWebsite)
                        {
                            UseShellExecute = true,
                        };
                        Process.Start(process);
                    }
                    catch
                    {
                        // ignored
                    }
                }

                ImGuiUtil.HoverTooltip(_modWebsite);
            }
            else
            {
                using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
                ImGuiUtil.TextColored(Colors.MetaInfoText, "from ");
                ImGui.SameLine();
                style.Pop();
                ImGui.TextUnformatted(_mod.Website);
            }
        }
Beispiel #3
0
    private void PathInputBox(string label, string hint, string tooltip, int which)
    {
        var tmp = which == 0 ? _pathLeft : _pathRight;

        using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * ImGuiHelpers.GlobalScale, 0));
        ImGui.SetNextItemWidth(-ImGui.GetFrameHeight() - 3 * ImGuiHelpers.GlobalScale);
        ImGui.InputTextWithHint(label, hint, ref tmp, Utf8GamePath.MaxGamePathLength);
        if (ImGui.IsItemDeactivatedAfterEdit())
        {
            UpdateImage(tmp, which);
        }

        ImGuiUtil.HoverTooltip(tooltip);
        ImGui.SameLine();
        if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), new Vector2(ImGui.GetFrameHeight()), string.Empty, false,
                                         true))
        {
            var startPath = Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath : _mod?.ModPath.FullName;

            void UpdatePath(bool success, List <string> paths)
            {
                if (success && paths.Count > 0)
                {
                    UpdateImage(paths[0], which);
                }
            }

            _dialogManager.OpenFileDialog("Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath);
        }
    }
        // Different supported sort modes as a combo.
        private void DrawFolderSortType()
        {
            var sortMode = Penumbra.Config.SortMode;

            ImGui.SetNextItemWidth(_window._inputTextWidth.X);
            using var combo = ImRaii.Combo("##sortMode", sortMode.Data().Name);
            if (combo)
            {
                foreach (var val in Enum.GetValues <SortMode>())
                {
                    var(name, desc) = val.Data();
                    if (ImGui.Selectable(name, val == sortMode) && val != sortMode)
                    {
                        Penumbra.Config.SortMode = val;
                        _window._selector.SetFilterDirty();
                        Penumbra.Config.Save();
                    }

                    ImGuiUtil.HoverTooltip(desc);
                }
            }

            combo.Dispose();
            ImGuiUtil.LabeledHelpMarker("Sort Mode", "Choose the sort mode for the mod selector in the mods tab.");
        }
        private static void DrawReloadResourceButton()
        {
            if (ImGui.Button("Reload Resident Resources"))
            {
                Penumbra.ResidentResources.Reload();
            }

            ImGuiUtil.HoverTooltip("Reload some specific files that the game keeps in memory at all times.\n"
                                   + "You usually should not need to do this.");
        }
        // Draw information about the character utility class from SE,
        // displaying all files, their sizes, the default files and the default sizes.
        public unsafe void DrawDebugCharacterUtility()
        {
            if (!ImGui.CollapsingHeader("Character Utility"))
            {
                return;
            }

            using var table = ImRaii.Table("##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit,
                                           -Vector2.UnitX);
            if (!table)
            {
                return;
            }

            for (var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i)
            {
                var idx      = CharacterUtility.RelevantIndices[i];
                var resource = ( ResourceHandle * )Penumbra.CharacterUtility.Address->Resources[idx];
                ImGui.TableNextColumn();
                ImGui.TextUnformatted($"0x{( ulong )resource:X}");
                ImGui.TableNextColumn();
                Text(resource);
                ImGui.TableNextColumn();
                ImGui.Selectable($"0x{resource->GetData().Data:X}");
                if (ImGui.IsItemClicked())
                {
                    var(data, length) = resource->GetData();
                    if (data != IntPtr.Zero && length > 0)
                    {
                        ImGui.SetClipboardText(string.Join("\n",
                                                           new ReadOnlySpan <byte>(( byte * )data, length).ToArray().Select(b => b.ToString("X2"))));
                    }
                }

                ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard.");

                ImGui.TableNextColumn();
                ImGui.TextUnformatted($"{resource->GetData().Length}");
                ImGui.TableNextColumn();
                ImGui.Selectable($"0x{Penumbra.CharacterUtility.DefaultResources[ i ].Address:X}");
                if (ImGui.IsItemClicked())
                {
                    ImGui.SetClipboardText(string.Join("\n",
                                                       new ReadOnlySpan <byte>(( byte * )Penumbra.CharacterUtility.DefaultResources[i].Address,
                                                                               Penumbra.CharacterUtility.DefaultResources[i].Size).ToArray().Select(b => b.ToString("X2"))));
                }

                ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard.");

                ImGui.TableNextColumn();
                ImGui.TextUnformatted($"{Penumbra.CharacterUtility.DefaultResources[ i ].Size}");
            }
        }
Beispiel #7
0
        public static void DrawNew(Mod.Editor editor, Vector2 iconSize)
        {
            ImGui.TableNextColumn();
            CopyToClipboardButton("Copy all current EQP manipulations to clipboard.", iconSize,
                                  editor.Meta.Eqp.Select(m => (MetaManipulation) m));
            ImGui.TableNextColumn();
            var canAdd       = editor.Meta.CanAdd(_new);
            var tt           = canAdd ? "Stage this edit." : "This entry is already edited.";
            var defaultEntry = ExpandedEqpFile.GetDefault(_new.SetId);

            if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
            {
                editor.Meta.Add(_new with {
                    Entry = defaultEntry
                });
            }

            // Identifier
            ImGui.TableNextColumn();
            if (IdInput("##eqpId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1))
            {
                _new = _new with {
                    SetId = setId
                };
            }

            ImGuiUtil.HoverTooltip("Model Set ID");

            ImGui.TableNextColumn();
            if (EqpEquipSlotCombo("##eqpSlot", _new.Slot, out var slot))
            {
                _new = _new with {
                    Slot = slot
                };
            }

            ImGuiUtil.HoverTooltip("Equip Slot");

            // Values
            ImGui.TableNextColumn();
            using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
                                               new Vector2(3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y));
            foreach (var flag in Eqp.EqpAttributes[_new.Slot])
            {
                var value = defaultEntry.HasFlag(flag);
                Checkmark("##eqp", flag.ToLocalName(), value, value, out _);
                ImGui.SameLine();
            }

            ImGui.NewLine();
        }
Beispiel #8
0
        private static void DrawOpenDirectoryButton(int id, DirectoryInfo directory, bool condition)
        {
            using var _ = ImRaii.PushId(id);
            var ret = ImGui.Button("Open Directory");

            ImGuiUtil.HoverTooltip("Open this directory in your configured file explorer.");
            if (ret && condition && Directory.Exists(directory.FullName))
            {
                Process.Start(new ProcessStartInfo(directory.FullName)
                {
                    UseShellExecute = true,
                });
            }
        }
        // Draw resources with unusual reference count.
        private static unsafe void DrawResourceProblems()
        {
            var header = ImGui.CollapsingHeader("Resource Problems");

            ImGuiUtil.HoverTooltip("Draw resources with unusually high reference count to detect overflows.");
            if (!header)
            {
                return;
            }

            using var table = ImRaii.Table("##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
            if (!table)
            {
                return;
            }

            ResourceLoader.IterateResources((_, r) =>
            {
                if (r->RefCount < 10000)
                {
                    return;
                }

                ImGui.TableNextColumn();
                ImGui.TextUnformatted(r->Category.ToString());
                ImGui.TableNextColumn();
                ImGui.TextUnformatted(r->FileType.ToString("X"));
                ImGui.TableNextColumn();
                ImGui.TextUnformatted(r->Id.ToString("X"));
                ImGui.TableNextColumn();
                ImGui.TextUnformatted((( ulong )r).ToString("X"));
                ImGui.TableNextColumn();
                ImGui.TextUnformatted(r->RefCount.ToString());
                ImGui.TableNextColumn();
                ref var name = ref r->FileName;
                if (name.Capacity > 15)
                {
                    ImGuiNative.igTextUnformatted(name.BufferPtr, name.BufferPtr + name.Length);
                }
                else
                {
                    fixed(byte *ptr = name.Buffer)
                    {
                        ImGuiNative.igTextUnformatted(ptr, ptr + name.Length);
                    }
                }
            });
Beispiel #10
0
        // Draw a big red bar if the current setting is inherited.
        private void DrawInheritedWarning()
        {
            if (!_inherited)
            {
                return;
            }

            using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg);
            var width = new Vector2(ImGui.GetContentRegionAvail().X, 0);

            if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width))
            {
                Penumbra.CollectionManager.Current.SetModInheritance(_mod.Index, false);
            }

            ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n"
                                   + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection.");
        }
Beispiel #11
0
        // Draw a button to remove the current settings and inherit them instead
        // on the top-right corner of the window/tab.
        private void DrawRemoveSettings()
        {
            const string text = "Inherit Settings";

            if (_inherited || _emptySetting)
            {
                return;
            }

            var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0;

            ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll);
            if (ImGui.Button(text))
            {
                Penumbra.CollectionManager.Current.SetModInheritance(_mod.Index, true);
            }

            ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n"
                                   + "If no inherited collection has settings for this mod, it will be disabled.");
        }
Beispiel #12
0
        public static void Draw(EqpManipulation meta, Mod.Editor editor, Vector2 iconSize)
        {
            DrawMetaButtons(meta, editor, iconSize);

            // Identifier
            ImGui.TableNextColumn();
            ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
            ImGui.TextUnformatted(meta.SetId.ToString());
            ImGuiUtil.HoverTooltip("Model Set ID");
            var defaultEntry = ExpandedEqpFile.GetDefault(meta.SetId);

            ImGui.TableNextColumn();
            ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
            ImGui.TextUnformatted(meta.Slot.ToName());
            ImGuiUtil.HoverTooltip("Equip Slot");

            // Values
            ImGui.TableNextColumn();
            using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
                                               new Vector2(3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y));
            var idx = 0;

            foreach (var flag in Eqp.EqpAttributes[meta.Slot])
            {
                using var id = ImRaii.PushId(idx++);
                var defaultValue = defaultEntry.HasFlag(flag);
                var currentValue = meta.Entry.HasFlag(flag);
                if (Checkmark("##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value))
                {
                    editor.Meta.Change(meta with {
                        Entry = value ? meta.Entry | flag : meta.Entry & ~flag
                    });
                }

                ImGui.SameLine();
            }

            ImGui.NewLine();
        }
Beispiel #13
0
        private void DrawTabBar()
        {
            ImGui.Dummy(_window._defaultSpace);
            using var tabBar = ImRaii.TabBar("##ModTabs");
            if (!tabBar)
            {
                return;
            }

            _availableTabs = Tabs.Settings
                             | (_mod.ChangedItems.Count > 0 ? Tabs.ChangedItems : 0)
                             | (_mod.Description.Length > 0 ? Tabs.Description : 0)
                             | (_conflicts.Count > 0 ? Tabs.Conflicts : 0)
                             | (Penumbra.Config.ShowAdvanced ? Tabs.Edit : 0);

            DrawSettingsTab();
            DrawDescriptionTab();
            DrawChangedItemsTab();
            DrawConflictsTab();
            DrawEditModTab();
            if (Penumbra.Config.ShowAdvanced && ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip))
            {
                _window.ModEditPopup.ChangeMod(_mod);
                _window.ModEditPopup.ChangeOption(-1, 0);
                _window.ModEditPopup.IsOpen = true;
            }

            ImGuiUtil.HoverTooltip(
                "Clicking this will open a new window in which you can\nedit the following things per option for this mod:\n\n"
                + "\t\t- file redirections\n"
                + "\t\t- file swaps\n"
                + "\t\t- metadata manipulations\n"
                + "\t\t- model materials\n"
                + "\t\t- duplicates\n"
                + "\t\t- textures");
        }
Beispiel #14
0
        public static void DrawDiscordButton(float width)
        {
            const string discord = "Join Discord for Support";
            const string address = @"https://discord.gg/kVva7DHV4r";

            using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.DiscordColor);
            if (ImGui.Button(discord, new Vector2(width, 0)))
            {
                try
                {
                    var process = new ProcessStartInfo(address)
                    {
                        UseShellExecute = true,
                    };
                    Process.Start(process);
                }
                catch
                {
                    // ignored
                }
            }

            ImGuiUtil.HoverTooltip($"Open {address}");
        }
Beispiel #15
0
            // Draw a line for a single option.
            private static void EditOption(ModPanel panel, IModGroup group, int groupIdx, int optionIdx)
            {
                var option = group[optionIdx];

                using var id = ImRaii.PushId(optionIdx);
                ImGui.TableNextColumn();
                ImGui.AlignTextToFramePadding();
                ImGui.Selectable($"Option #{optionIdx + 1}");
                Source(group, groupIdx, optionIdx);
                Target(panel, group, groupIdx, optionIdx);

                ImGui.TableNextColumn();
                if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1))
                {
                    Penumbra.ModManager.RenameOption(panel._mod, groupIdx, optionIdx, newOptionName);
                }

                ImGui.TableNextColumn();
                if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), panel._window._iconButtonSize,
                                                 "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true))
                {
                    panel._delayedActions.Enqueue(() => Penumbra.ModManager.DeleteOption(panel._mod, groupIdx, optionIdx));
                }

                ImGui.TableNextColumn();
                if (group.Type == SelectType.Multi)
                {
                    if (Input.Priority("##Priority", groupIdx, optionIdx, group.OptionPriority(optionIdx), out var priority,
                                       50 * ImGuiHelpers.GlobalScale))
                    {
                        Penumbra.ModManager.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority);
                    }

                    ImGuiUtil.HoverTooltip("Option priority.");
                }
            }
Beispiel #16
0
        private void EditGroup(int groupIdx)
        {
            var group = _mod.Groups[groupIdx];

            using var id    = ImRaii.PushId(groupIdx);
            using var frame = ImRaii.FramedGroup($"Group #{groupIdx + 1}");

            using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, _cellPadding)
                              .Push(ImGuiStyleVar.ItemSpacing, _itemSpacing);

            if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, _window._inputTextWidth.X))
            {
                Penumbra.ModManager.RenameModGroup(_mod, groupIdx, newGroupName);
            }

            ImGuiUtil.HoverTooltip("Group Name");
            ImGui.SameLine();
            if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize,
                                             "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true))
            {
                _delayedActions.Enqueue(() => Penumbra.ModManager.DeleteModGroup(_mod, groupIdx));
            }

            ImGui.SameLine();

            if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * ImGuiHelpers.GlobalScale))
            {
                Penumbra.ModManager.ChangeGroupPriority(_mod, groupIdx, priority);
            }

            ImGuiUtil.HoverTooltip("Group Priority");

            DrawGroupCombo(group, groupIdx);
            ImGui.SameLine();

            var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}.";

            if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), _window._iconButtonSize,
                                             tt, groupIdx == 0, true))
            {
                _delayedActions.Enqueue(() => Penumbra.ModManager.MoveModGroup(_mod, groupIdx, groupIdx - 1));
            }

            ImGui.SameLine();
            tt = groupIdx == _mod.Groups.Count - 1
                ? "Can not move this group further downwards."
                : $"Move this group down to group {groupIdx + 2}.";
            if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), _window._iconButtonSize,
                                             tt, groupIdx == _mod.Groups.Count - 1, true))
            {
                _delayedActions.Enqueue(() => Penumbra.ModManager.MoveModGroup(_mod, groupIdx, groupIdx + 1));
            }

            ImGui.SameLine();

            if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize,
                                             "Edit group description.", false, true))
            {
                _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx));
            }

            ImGui.SameLine();
            var fileName   = group.FileName(_mod.ModPath, groupIdx);
            var fileExists = File.Exists(fileName);

            tt = fileExists
                ? $"Open the {group.Name} json file in the text editor of your choice."
                : $"The {group.Name} json file does not exist.";
            if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileExport.ToIconString(), _window._iconButtonSize, tt, !fileExists, true))
            {
                Process.Start(new ProcessStartInfo(fileName)
                {
                    UseShellExecute = true
                });
            }

            ImGui.Dummy(_window._defaultSpace);

            OptionTable.Draw(this, groupIdx);
        }
    // Draw a simple clipped table containing all changed items.
    private void DrawChangedItemTab()
    {
        // Functions in here for less pollution.
        bool FilterChangedItem(KeyValuePair <string, (SingleArray <IMod>, object?)> item)
        => (_changedItemFilter.IsEmpty ||
            ChangedItemName(item.Key, item.Value.Item2)
            .Contains(_changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase)) &&
        (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter)));

        void DrawChangedItemColumn(KeyValuePair <string, (SingleArray <IMod>, object?)> item)
        {
            ImGui.TableNextColumn();
            DrawChangedItem(item.Key, item.Value.Item2, false);
            ImGui.TableNextColumn();
            if (item.Value.Item1.Count > 0)
            {
                ImGui.TextUnformatted(item.Value.Item1[0].Name);
                if (item.Value.Item1.Count > 1)
                {
                    ImGuiUtil.HoverTooltip(string.Join("\n", item.Value.Item1.Skip(1).Select(m => m.Name)));
                }
            }

            ImGui.TableNextColumn();
            if (item.Value.Item2 is Item it)
            {
                using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value());
                ImGuiUtil.RightAlign($"({( ( Quad )it.ModelMain ).A})");
            }
        }

        using var tab = ImRaii.TabItem("Changed Items");
        if (!tab)
        {
            return;
        }

        // Draw filters.
        var varWidth = ImGui.GetContentRegionAvail().X
                       - 400 * ImGuiHelpers.GlobalScale
                       - ImGui.GetStyle().ItemSpacing.X;

        ImGui.SetNextItemWidth(400 * ImGuiHelpers.GlobalScale);
        LowerString.InputWithHint("##changedItemsFilter", "Filter Item...", ref _changedItemFilter, 128);
        ImGui.SameLine();
        ImGui.SetNextItemWidth(varWidth);
        LowerString.InputWithHint("##changedItemsModFilter", "Filter Mods...", ref _changedItemModFilter, 128);

        using var child = ImRaii.Child("##changedItemsChild", -Vector2.One);
        if (!child)
        {
            return;
        }

        // Draw table of changed items.
        var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y;
        var skips  = ImGuiClip.GetNecessarySkips(height);

        using var list = ImRaii.Table("##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One);
        if (!list)
        {
            return;
        }

        const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed;

        ImGui.TableSetupColumn("items", flags, 400 * ImGuiHelpers.GlobalScale);
        ImGui.TableSetupColumn("mods", flags, varWidth - 100 * ImGuiHelpers.GlobalScale);
        ImGui.TableSetupColumn("id", flags, 100 * ImGuiHelpers.GlobalScale);

        var items = Penumbra.CollectionManager.Current.ChangedItems;
        var rest  = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty
            ? ImGuiClip.ClippedDraw(items, skips, DrawChangedItemColumn, items.Count)
            : ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn);

        ImGuiClip.DrawEndDummy(rest, height);
    }