From 3b973c2c457b86d703cc773ba21208d171eb52eb Mon Sep 17 00:00:00 2001 From: dawsh Date: Fri, 27 Jun 2025 15:26:33 -0500 Subject: [PATCH] Added input-history feature to TextInputForm. Control via up/down arrows. --- include/JUI/Widgets/TextInputForm.hpp | 63 +++++++++++-- src/JUI/Widgets/TextInputForm.cpp | 128 ++++++++++++++++++++++---- 2 files changed, 163 insertions(+), 28 deletions(-) diff --git a/include/JUI/Widgets/TextInputForm.hpp b/include/JUI/Widgets/TextInputForm.hpp index f6645de..e592536 100644 --- a/include/JUI/Widgets/TextInputForm.hpp +++ b/include/JUI/Widgets/TextInputForm.hpp @@ -27,18 +27,32 @@ namespace JUI { class TextInputForm : public TextRect, public Clickable { public: +#pragma region Events Event<> OnSelect; Event<> OnDeselect; Event OnReturn; +#pragma endregion + public: +#pragma region Constructors TextInputForm(); explicit TextInputForm(Widget* parent); +#pragma endregion + public: +#pragma region Methods void Update(float elapsed) override; void InnerDraw() override; void Draw() override; + /// @return The number of characters the cursor is actually moved to the left by. + int MoveCursorLeft(int chars = 1); + /// @note The amount of + /// @return The number of characters the cursor is actually moved to the right by. + int MoveCursorRight(int chars = 1); +#pragma region Getters - void MoveCursorLeft(); +#pragma endregion +#pragma region Setters - void MoveCursorRight(); +#pragma endregion /// Returns the maximum position of the cursor, which is determined by the input buffer's length. [[nodiscard]] unsigned int CursorMaxPosition() const; @@ -64,6 +78,11 @@ namespace JUI { void PushKeyToCurrentPlaceInInputBuffer(const Key &key); bool ObserveKeyInput(Key key, bool pressed) override; + + void ShowNextHistory(); + + void ShowPrevHistory(); + bool ObserveMouseInput(MouseButton btn, bool pressed) override; bool ObserveMouseMovement(const Vector2 &latest_known_pos) override; [[nodiscard]] std::string GetAutocompleteText() const; @@ -103,22 +122,50 @@ namespace JUI { void SetSelectionColor(const Color4& color); bool HasSelection() const; std::string GetSelectedText() const; + + [[nodiscard]] bool KeepInputHistory() const; + + void KeepInputHistory(bool value); + + std::string InputHistory(int index) const; + +#pragma endregion protected: +#pragma region Properties + bool keep_input_history = false; bool clear_text_on_return = true; bool drop_focus_on_return = false; bool focused = false; - bool selection_enabled; - bool selection_active; - int selection_start_index; - int selection_end_index; - unsigned int cursor_position = 0; - std::string input_buffer; + float cursor_blink_time = 0.f; Color4 autocomplete_color = Style::InputForm::AutocompleteTextColor; std::string autocomplete_text = "Hello World"; bool hide_autocomplete_on_select = true; bool autocomplete_text_enabled = true; std::set blacklist; + bool selection_enabled; +#pragma endregion +#pragma region Working Variables + std::vector history; + int history_index = -1; + bool selection_active; + int selection_start_index; + int selection_end_index; + unsigned int cursor_position = 0; + std::string input_buffer; + std::string saved_input_buffer; + + + /// Tracks the time (in seconds) since the TextInputForm was last opened. + /// @note This is used to circumvent a behavioral bug caused by how the input code is structured: + /// 1. User clicks a button that opens an InputForm. + /// 2. Grab InputForm Focus. + /// 3. User releases the button. + /// 4. The input form interprets this as "I am focused, but something else was just clicked". + /// 5. The input form drops its focus instantly, and it appears as if it was never focused. + float time_focused = 0.f; +#pragma endregion + private: }; } \ No newline at end of file diff --git a/src/JUI/Widgets/TextInputForm.cpp b/src/JUI/Widgets/TextInputForm.cpp index de39a61..87b61b3 100644 --- a/src/JUI/Widgets/TextInputForm.cpp +++ b/src/JUI/Widgets/TextInputForm.cpp @@ -31,20 +31,18 @@ namespace JUI { bool TextInputForm::ObserveMouseInput(MouseButton btn, bool pressed) { - if (pressed && btn == MouseButton::Left) { + if (!IsMouseInside() && focused && time_focused > 1.f) { + OnDeselect.Invoke(); + focused = false; + } - if (IsMouseInside()) { - if (!focused) { - OnSelect.Invoke(); - } - focused = true; - return true; - } else { - if (focused) - OnDeselect.Invoke(); - focused = false; + + if (IsMouseInside() && pressed && btn == MouseButton::Left) { + if (!focused) { + OnSelect.Invoke(); } - + focused = true; + return true; } return Widget::ObserveMouseInput(btn, pressed); } @@ -87,6 +85,7 @@ namespace JUI { // TODO: Make cursor actually blink if (focused) { + time_focused += elapsed; cursor_blink_time += elapsed; if (cursor_blink_time > 1.f) @@ -115,12 +114,27 @@ namespace JUI { return s; } - void TextInputForm::MoveCursorLeft() { - if (cursor_position > 0) - cursor_position--; + int TextInputForm::MoveCursorLeft(int chars) { + if (cursor_position <= 0) return 0; // TODO: This may be redundant? + + int available_moves = Math::Min(chars, cursor_position); + + cursor_position -= available_moves; + + return available_moves; } - void TextInputForm::MoveCursorRight() { + int TextInputForm::MoveCursorRight(int chars) { + if (cursor_position >= CursorMaxPosition()) return 0; + + int chars_until_end = CursorMaxPosition() - cursor_position; + + int available_moves = Math::Min(chars, chars_until_end); + + cursor_position += available_moves; + + return available_moves; + if (cursor_position < CursorMaxPosition()-1) cursor_position++; } @@ -137,6 +151,15 @@ namespace JUI { void TextInputForm::SendInput(bool clear_input) { OnReturn.Invoke(input_buffer); + if (keep_input_history) { + if (history.size() == 0 || history[0] != input_buffer) { + history.insert(history.begin(), input_buffer); + } + } + + history_index = -1; + saved_input_buffer = ""; + if (clear_input) { input_buffer = ""; cursor_position = 0; @@ -159,11 +182,15 @@ namespace JUI { if (cursor_position > 0) { input_buffer = input_buffer.erase(cursor_position-1, 1); cursor_position--; + + saved_input_buffer = input_buffer; } } void TextInputForm::PushStringToCurrentPlaceInInputBuffer(const std::string& snippet) { input_buffer = input_buffer.insert(cursor_position, snippet); cursor_position += snippet.length(); + + saved_input_buffer = input_buffer; } void TextInputForm::PushKeyToCurrentPlaceInInputBuffer(const Key& key) { std::string insertion = lowercase(key.Mnemonic); @@ -194,12 +221,30 @@ namespace JUI { } if (key == Keys::LeftArrow) { - MoveCursorLeft(); + int qty = 1; + // TODO: Move Left until next space. + if (InputService::IsKeyDown(Keys::LeftShift)) qty = 5; + MoveCursorLeft(qty); return true; } if (key == Keys::RightArrow) { - MoveCursorRight(); + int qty = 1; + // TODO: Move Right until next space. + if (InputService::IsKeyDown(Keys::LeftShift)) qty = 5; + MoveCursorRight(qty); + return true; + } + + if (key == Keys::DownArrow) { + if (keep_input_history) + ShowNextHistory(); + return true; + } + + if (key == Keys::UpArrow) { + if (keep_input_history) + ShowPrevHistory(); return true; } @@ -218,11 +263,45 @@ namespace JUI { return true; } + void TextInputForm::ShowNextHistory() { + if (keep_input_history) { + + if (history_index > -1) + history_index--; + + std::string result = ""; + if (history_index == -1) + SetInputBuffer(saved_input_buffer); + else + SetInputBuffer(history[history_index]); + + } + } + + void TextInputForm::ShowPrevHistory() { + if (keep_input_history) { + if (history_index == -1 && !history.empty() || history.size()-1 > history_index) { + history_index++; + } + + std::string result = ""; + if (history_index == -1) + SetInputBuffer(saved_input_buffer); + else + SetInputBuffer(history[history_index]); + } + } + bool TextInputForm::HasFocus() const { return focused; } - void TextInputForm::SetFocused(bool focused) { this->focused = focused;} + void TextInputForm::SetFocused(bool focused) { + this->focused = focused; + if (focused) time_focused = 0; + } - void TextInputForm::GrabFocus() { SetFocused(true); } + void TextInputForm::GrabFocus() { + SetFocused(true); + } void TextInputForm::DropFocus() { SetFocused(false); } @@ -250,4 +329,13 @@ namespace JUI { blacklist.insert(value); } + bool TextInputForm::KeepInputHistory() const { return keep_input_history; } + + void TextInputForm::KeepInputHistory(bool value) { + keep_input_history = value; + } + + std::string TextInputForm::InputHistory(int index) const { + return history[index]; + } }