From 3ae0e05b8fb2a8dbbac0b3734ed52b890c79a0ef Mon Sep 17 00:00:00 2001 From: onepiecefreak3 Date: Sun, 25 Aug 2024 18:12:56 +0200 Subject: [PATCH] Rename ComboBox to DropDownBox; Introduce actual ComboBox with text input nad proposals; --- ImGui.Forms/Controls/ComboBox.cs | 159 +++++++++++++++++++++++----- ImGui.Forms/Controls/DropDownBox.cs | 80 ++++++++++++++ ImGui.Forms/ImGui.Forms.nuspec | 2 +- 3 files changed, 214 insertions(+), 27 deletions(-) create mode 100644 ImGui.Forms/Controls/DropDownBox.cs diff --git a/ImGui.Forms/Controls/ComboBox.cs b/ImGui.Forms/Controls/ComboBox.cs index f24cc52..7758673 100644 --- a/ImGui.Forms/Controls/ComboBox.cs +++ b/ImGui.Forms/Controls/ComboBox.cs @@ -1,24 +1,40 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; using ImGui.Forms.Controls.Base; -using ImGui.Forms.Localization; using ImGui.Forms.Models; using ImGui.Forms.Resources; +using ImGuiNET; using Veldrid; +// Initial code: https://github.com/ocornut/imgui/issues/2057 + namespace ImGui.Forms.Controls { public class ComboBox : Component { + private string _input = string.Empty; + public IList> Items { get; } = new List>(); public ComboBoxItem SelectedItem { get; set; } public SizeValue Width { get; set; } = SizeValue.Content; - #region Events + /// + /// Get or set the max count of characters allowed in the input. + /// + public uint MaxCharacters { get; set; } = 256; + /// + /// Get or set the max count of items in the drop down. + /// + public uint MaxShowItems { get; set; } = 10; + + #region Events + public event EventHandler SelectedItemChanged; #endregion @@ -34,47 +50,138 @@ public override Size GetSize() return new Size(width, height); } - protected override void UpdateInternal(Rectangle contentRect) + protected override unsafe void UpdateInternal(Rectangle contentRect) { - ImGuiNET.ImGui.SetNextItemWidth(contentRect.Width); + //Check if both strings matches + uint maxShowItems = MaxShowItems; + if (maxShowItems == 0) + maxShowItems = (uint)Items.Count; + + ImGuiNET.ImGui.PushID(Id); - var selectedName = SelectedItem?.Name ?? string.Empty; - if (ImGuiNET.ImGui.BeginCombo($"##combo{Id}", selectedName)) + bool isFinal = ImGuiNET.ImGui.InputText("##in", ref _input, MaxCharacters, ImGuiInputTextFlags.CallbackAlways | ImGuiInputTextFlags.EnterReturnsTrue, Propose); + if (isFinal) { - for (var i = 0; i < Items.Count; i++) + ComboBoxItem selectedItem = Items.FirstOrDefault(i => i.Name == _input); + if (SelectedItem != selectedItem) { - if (ImGuiNET.ImGui.Selectable(Items[i].Name, SelectedItem == Items[i])) - { - var hasChanged = SelectedItem != Items[i]; - SelectedItem = Items[i]; + SelectedItem = selectedItem; + OnSelectedItemChanged(); + } + } - if (hasChanged) - OnSelectedItemChanged(); + ImGuiNET.ImGui.OpenPopupOnItemClick("combobox"); // Enable right-click + Vector2 pos = ImGuiNET.ImGui.GetItemRectMin(); + Vector2 size = ImGuiNET.ImGui.GetItemRectSize(); + + ImGuiNET.ImGui.SameLine(0, 0); + if (ImGuiNET.ImGui.ArrowButton("##openCombo", ImGuiDir.Down)) + { + ImGuiNET.ImGui.OpenPopup("combobox"); + } + ImGuiNET.ImGui.OpenPopupOnItemClick("combobox"); // Enable right-click + + pos.Y += size.Y; + size.X += ImGuiNET.ImGui.GetItemRectSize().Y; + size.Y += 5 + size.Y * maxShowItems; + ImGuiNET.ImGui.SetNextWindowPos(pos); + ImGuiNET.ImGui.SetNextWindowSize(size); + if (ImGuiNET.ImGui.BeginPopup("combobox", ImGuiWindowFlags.NoMove)) + { + ImGuiNET.ImGui.Text("Select an item"); + ImGuiNET.ImGui.Separator(); + foreach (ComboBoxItem item in Items) + { + if (!ImGuiNET.ImGui.Selectable(item.Name)) + continue; + + _input = item.Name; + + if (SelectedItem != item) + { + SelectedItem = item; + OnSelectedItemChanged(); } } - ImGuiNET.ImGui.EndCombo(); + ImGuiNET.ImGui.EndPopup(); } + + ImGuiNET.ImGui.PopID(); } private void OnSelectedItemChanged() { - SelectedItemChanged?.Invoke(this, new EventArgs()); + SelectedItemChanged?.Invoke(this, EventArgs.Empty); } - } - - public class ComboBoxItem - { - public TItem Content { get; } - public LocalizedString Name { get; } - - public ComboBoxItem(TItem content, LocalizedString name = default) + private bool Identical(string buf, string item) { - Content = content; - Name = name.IsEmpty ? (LocalizedString)content.ToString() : name; + //Check if the item length is shorter or equal --> exclude + if (buf.Length >= item.Length) + return false; + + for (var i = 0; i < buf.Length; ++i) + // set the current pos if matching or return the pos if not + if (buf[i] != item[i]) + return false; + + // Complete match + // and the item size is greater --> include + return true; } - public static implicit operator ComboBoxItem(TItem o) => new(o); + private unsafe int Propose(ImGuiInputTextCallbackData* data) + { + var dataPtr = new ImGuiInputTextCallbackDataPtr(data); + + // We don't want to "preselect" anything + if (dataPtr.BufTextLen == 0) + return 0; + + // We need to give the user a chance to remove wrong input + if (dataPtr.EventKey == ImGuiKey.Backspace) + { + // We delete the last char automatically, since it is what the user wants to delete, but only if there is something (selected/marked/hovered) + // FIXME: This worked fine, when not used as helper function + if (data->SelectionEnd == data->SelectionStart) + return 0; + + if (data->BufTextLen <= 0) + return 0; //...and the buffer isn't empty + + if (data->CursorPos > 0) //...and the cursor not at pos 0 + dataPtr.DeleteChars(data->CursorPos - 1, 1); + + return 0; + } + + if (dataPtr.EventKey == ImGuiKey.Delete) + return 0; + + string bufferString = Marshal.PtrToStringUTF8(dataPtr.Buf); + foreach (ComboBoxItem item in Items) + { + if (!Identical(bufferString, item.Name)) + continue; + + int cursor = data->CursorPos; + + //Insert the first match + dataPtr.DeleteChars(0, data->BufTextLen); + dataPtr.InsertChars(0, item.Name); + + //Reset the cursor position + data->CursorPos = cursor; + + //Select the text, so the user can simply go on writing + data->SelectionStart = cursor; + data->SelectionEnd = data->BufTextLen; + + break; + } + + return 0; + } } } diff --git a/ImGui.Forms/Controls/DropDownBox.cs b/ImGui.Forms/Controls/DropDownBox.cs new file mode 100644 index 0000000..01f57e0 --- /dev/null +++ b/ImGui.Forms/Controls/DropDownBox.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ImGui.Forms.Controls.Base; +using ImGui.Forms.Localization; +using ImGui.Forms.Models; +using ImGui.Forms.Resources; +using Veldrid; + +namespace ImGui.Forms.Controls +{ + public class DropDownBox : Component + { + public IList> Items { get; } = new List>(); + + public ComboBoxItem SelectedItem { get; set; } + + public SizeValue Width { get; set; } = SizeValue.Content; + + #region Events + + public event EventHandler SelectedItemChanged; + + #endregion + + public override Size GetSize() + { + var maxWidth = Items.Select(x => FontResource.GetCurrentLineWidth(x.Name)).DefaultIfEmpty(0).Max() + (int)ImGuiNET.ImGui.GetStyle().ItemInnerSpacing.X * 2; + var arrowWidth = 20; + + SizeValue width = Width.IsContentAligned ? maxWidth + arrowWidth : Width; + var height = FontResource.GetCurrentLineHeight() + (int)ImGuiNET.ImGui.GetStyle().ItemInnerSpacing.Y * 2; + + return new Size(width, height); + } + + protected override void UpdateInternal(Rectangle contentRect) + { + ImGuiNET.ImGui.SetNextItemWidth(contentRect.Width); + + var selectedName = SelectedItem?.Name ?? string.Empty; + if (ImGuiNET.ImGui.BeginCombo($"##combo{Id}", selectedName)) + { + for (var i = 0; i < Items.Count; i++) + { + if (ImGuiNET.ImGui.Selectable(Items[i].Name, SelectedItem == Items[i])) + { + var hasChanged = SelectedItem != Items[i]; + SelectedItem = Items[i]; + + if (hasChanged) + OnSelectedItemChanged(); + } + } + + ImGuiNET.ImGui.EndCombo(); + } + } + + private void OnSelectedItemChanged() + { + SelectedItemChanged?.Invoke(this, new EventArgs()); + } + } + + public class ComboBoxItem + { + public TItem Content { get; } + + public LocalizedString Name { get; } + + public ComboBoxItem(TItem content, LocalizedString name = default) + { + Content = content; + Name = name.IsEmpty ? (LocalizedString)content.ToString() : name; + } + + public static implicit operator ComboBoxItem(TItem o) => new(o); + } +} diff --git a/ImGui.Forms/ImGui.Forms.nuspec b/ImGui.Forms/ImGui.Forms.nuspec index 48b4a08..c14ec3d 100644 --- a/ImGui.Forms/ImGui.Forms.nuspec +++ b/ImGui.Forms/ImGui.Forms.nuspec @@ -2,7 +2,7 @@ Imgui.Forms - 1.0.60 + 1.0.61 A WinForms-inspired object-oriented framework around Dear ImGui (https://github.com/ocornut/imgui) onepiecefreak