From efe71fbe24ac04ce7df205a64f6d48ce8cd34fac Mon Sep 17 00:00:00 2001
From: Kawe Mazidjatari <48657826+Mauler125@users.noreply.github.com>
Date: Sun, 26 Jun 2022 16:47:00 +0200
Subject: [PATCH] ImGui console UX improvements

* Fixed click-through parent window.
* Keep selection position when lines get cleared.
* Keep cursor position when lines get cleared.
* Ensure cursor visibility when dragging cursor outside rect.
* Falter free scrolling in autocomplete window.
* Falter free scrolling in logging window (window no longer stutters when lines get cleared).
---
 r5dev/gameui/IConsole.cpp                     |  45 +++---
 r5dev/gameui/IConsole.h                       |   9 +-
 r5dev/thirdparty/imgui/include/imgui.h        |   2 +
 r5dev/thirdparty/imgui/include/imgui_logger.h |  11 +-
 r5dev/thirdparty/imgui/src/imgui.cpp          |  20 +++
 r5dev/thirdparty/imgui/src/imgui_logger.cpp   | 143 ++++++++++++++----
 6 files changed, 178 insertions(+), 52 deletions(-)

diff --git a/r5dev/gameui/IConsole.cpp b/r5dev/gameui/IConsole.cpp
index dfcaa8a5..34b16c28 100644
--- a/r5dev/gameui/IConsole.cpp
+++ b/r5dev/gameui/IConsole.cpp
@@ -32,13 +32,14 @@ CConsole::CConsole(void)
 
     m_nHistoryPos     = -1;
     m_bInitialized    = false;
-    m_pszConsoleTitle = "Console";
+    m_pszConsoleLabel = "Console";
+    m_pszLoggingLabel = "LoggingRegion";
 
     m_vCommands.push_back("CLEAR");
     m_vCommands.push_back("HELP");
     m_vCommands.push_back("HISTORY");
 
-    snprintf(m_szSummary, 256, "%zu history items", m_vHistory.size());
+    snprintf(m_szSummary, sizeof(m_szSummary), "%zu history items", m_vHistory.size());
 
     std::thread think(&CConsole::Think, this);
     think.detach();
@@ -119,6 +120,11 @@ void CConsole::Draw(void)
 
             ImGui::SetNextWindowPos(m_ivSuggestWindowPos);
             ImGui::SetNextWindowSize(m_ivSuggestWindowSize);
+            if (m_bSuggestUpdate)
+            {
+                ImGui::SetNextWindowScroll(ImVec2(0.f, 0.f));
+                m_bSuggestUpdate = false;
+            }
 
             ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(500, 37)); nVars++;
             ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);         nVars++;
@@ -140,11 +146,15 @@ void CConsole::Think(void)
     {
         if (m_Logger.GetTotalLines() > con_max_size_logvector->GetInt())
         {
-            while (m_Logger.GetTotalLines() > con_max_size_logvector->GetInt() / 4 * 3)
+            while (m_Logger.GetTotalLines() > con_max_size_logvector->GetInt())
             {
                 m_Logger.RemoveLine(0);
                 m_nScrollBack++;
+                m_nSelectBack++;
             }
+            m_Logger.MoveSelection(m_nSelectBack, false);
+            m_Logger.MoveCursor(m_nSelectBack, false);
+            m_nSelectBack = 0;
         }
 
         while (m_vHistory.size() > 512)
@@ -174,7 +184,7 @@ void CConsole::Think(void)
 //-----------------------------------------------------------------------------
 void CConsole::BasePanel(void)
 {
-    if (!ImGui::Begin(m_pszConsoleTitle, &m_bActivate))
+    if (!ImGui::Begin(m_pszConsoleLabel, &m_bActivate))
     {
         ImGui::End();
         return;
@@ -205,7 +215,18 @@ void CConsole::BasePanel(void)
     ImGui::Separator();
 
     ///////////////////////////////////////////////////////////////////////
-    ImGui::BeginChild("ScrollingRegion", ImVec2(0, -flFooterHeightReserve), true, m_nLoggingFlags);
+    if (!m_Logger.m_bScrolledToMax && m_nScrollBack > 0)
+    {
+        ImGuiWindow* pWindow = ImGui::GetCurrentWindow();
+        ImGuiID nID = pWindow->GetID(m_pszLoggingLabel);
+
+        snprintf(m_szWindowLabel, sizeof(m_szWindowLabel), "%s/%s_%08X", m_pszConsoleLabel, m_pszLoggingLabel, nID);
+        ImGui::SetWindowScrollY(m_szWindowLabel, m_flScrollY - m_nScrollBack * fontSize.y);
+    }
+    m_nScrollBack = 0;
+
+    ///////////////////////////////////////////////////////////////////////
+    ImGui::BeginChild(m_pszLoggingLabel, ImVec2(0, -flFooterHeightReserve), true, m_nLoggingFlags);
     m_Logger.Render();
 
     if (m_bCopyToClipBoard)
@@ -214,12 +235,8 @@ void CConsole::BasePanel(void)
         m_bCopyToClipBoard = false;
     }
 
-    if (!m_Logger.m_bScrolledToMax && m_nScrollBack > 0)
-    {
-        ImGui::SetScrollY(ImGui::GetScrollY() - m_nScrollBack * fontSize.y);
-        m_nScrollBack = 0;
-    }
-    m_nScrollBack = 0;
+    m_flScrollX = ImGui::GetScrollX();
+    m_flScrollY = ImGui::GetScrollY();
 
     ///////////////////////////////////////////////////////////////////////
     ImGui::EndChild();
@@ -373,12 +390,6 @@ void CConsole::SuggestPanel(void)
             ImGui::ScrollToRect(pWindow, imRect);
             m_bSuggestMoved = false;
         }
-
-        if (m_bSuggestUpdate)
-        {
-            ImGui::SetScrollHereY(0.f);
-            m_bSuggestUpdate = false;
-        }
     }
 
     ImGui::PopAllowKeyboardFocus();
diff --git a/r5dev/gameui/IConsole.h b/r5dev/gameui/IConsole.h
index c31d3aa0..aded4f0c 100644
--- a/r5dev/gameui/IConsole.h
+++ b/r5dev/gameui/IConsole.h
@@ -28,13 +28,18 @@ class CConsole
 private:
     ///////////////////////////////////////////////////////////////////////////
     char                           m_szInputBuf[512]     = { '\0' };
-    char                           m_szSummary[256]      = { '\0' };
-    const char*                    m_pszConsoleTitle     = nullptr;
+    char                           m_szSummary[512]      = { '\0' };
+    char                           m_szWindowLabel[512]  = { '\0' };
+    const char*                    m_pszConsoleLabel     = nullptr;
+    const char*                    m_pszLoggingLabel     = nullptr;
 
     vector<string>                 m_vCommands;
     vector<string>                 m_vHistory;
     ssize_t                        m_nHistoryPos      = -1;
     int                            m_nScrollBack      = 0;
+    int                            m_nSelectBack      = 0;
+    float                          m_flScrollX        = 0.f;
+    float                          m_flScrollY        = 0.f;
     float                          m_flFadeAlpha      = 0.f;
     bool                           m_bInitialized     = false;
     bool                           m_bModernTheme     = false;
diff --git a/r5dev/thirdparty/imgui/include/imgui.h b/r5dev/thirdparty/imgui/include/imgui.h
index b38777ca..27fad4d0 100644
--- a/r5dev/thirdparty/imgui/include/imgui.h
+++ b/r5dev/thirdparty/imgui/include/imgui.h
@@ -365,6 +365,8 @@ namespace ImGui
     IMGUI_API void          SetWindowFontScale(float scale);                                            // [OBSOLETE] set font scale. Adjust IO.FontGlobalScale if you want to scale all windows. This is an old API! For correct scaling, prefer to reload font + rebuild ImFontAtlas + call style.ScaleAllSizes().
     IMGUI_API void          SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond = 0);      // set named window position.
     IMGUI_API void          SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond = 0);    // set named window size. set axis to 0.0f to force an auto-fit on this axis.
+    IMGUI_API void          SetWindowScrollX(const char* name, float scroll_x);                         // set named window scroll x position.
+    IMGUI_API void          SetWindowScrollY(const char* name, float scroll_y);                         // set named window scroll y position.
     IMGUI_API void          SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond = 0);   // set named window collapsed state
     IMGUI_API void          SetWindowFocus(const char* name);                                           // set named window to be focused / top-most. use NULL to remove focus.
 
diff --git a/r5dev/thirdparty/imgui/include/imgui_logger.h b/r5dev/thirdparty/imgui/include/imgui_logger.h
index c61e2061..5effd6c9 100644
--- a/r5dev/thirdparty/imgui/include/imgui_logger.h
+++ b/r5dev/thirdparty/imgui/include/imgui_logger.h
@@ -131,8 +131,8 @@ public:
 
 	Coordinates GetCursorPosition() const { return GetActualCursorCoordinates(); }
 
+	void MoveCursor(int aLines, bool aForward = true);
 	void SetCursorPosition(const Coordinates& aPosition);
-	bool IsCursorPositionChanged() const { return m_bCursorPositionChanged; }
 
 	inline void SetHandleMouseInputs    (bool aValue){ m_bHandleMouseInputs    = aValue;}
 	inline bool IsHandleMouseInputsEnabled() const { return m_bHandleKeyboardInputs; }
@@ -163,12 +163,13 @@ public:
 	void SelectWordUnderCursor();
 	void SelectAll();
 	bool HasSelection() const;
+	void MoveSelection(int aLines, bool aForward = true);
 
 	void RemoveLine(int aStart, int aEnd, bool aInternal = false);
 	void RemoveLine(int aIndex, bool aInternal = false);
 
 private:
-	struct EditorState
+	struct LoggerState_t
 	{
 		Coordinates m_SelectionStart;
 		Coordinates m_SelectionEnd;
@@ -205,13 +206,13 @@ private:
 public:
 	bool m_bAutoScroll;
 	bool m_bScrollToBottom;
+	bool m_bScrollToCursor;
 	bool m_bScrolledToMax;
 private:
 	bool m_bHandleKeyboardInputs;
 	bool m_bHandleMouseInputs;
+	bool m_bWithinLoggingRect;
 	bool m_bShowWhiteSpaces;
-	bool m_bScrollToCursor;
-	bool m_bCursorPositionChanged;
 	float m_flTextStart;                   // position (in pixels) where a code line starts relative to the left of the TextLogger.
 	float m_flLineSpacing;
 	double m_flLastClick;
@@ -227,7 +228,7 @@ private:
 	ImVec2 m_CharAdvance;
 
 	Lines m_Lines;
-	EditorState m_State;
+	LoggerState_t m_State;
 	std::mutex m_Mutex;
 	std::string m_svLineBuffer;
 public:
diff --git a/r5dev/thirdparty/imgui/src/imgui.cpp b/r5dev/thirdparty/imgui/src/imgui.cpp
index c866de9b..58f6a692 100644
--- a/r5dev/thirdparty/imgui/src/imgui.cpp
+++ b/r5dev/thirdparty/imgui/src/imgui.cpp
@@ -7148,6 +7148,26 @@ void ImGui::SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond)
         SetWindowSize(window, size, cond);
 }
 
+void ImGui::SetWindowScrollX(const char* name, float scroll_x)
+{
+    if (ImGuiWindow* window = FindWindowByName(name))
+    {
+        window->DC.CursorMaxPos.x += window->Scroll.x;
+        window->Scroll.x = scroll_x;
+        window->DC.CursorMaxPos.x -= window->Scroll.x;
+    }
+}
+
+void ImGui::SetWindowScrollY(const char* name, float scroll_y)
+{
+    if (ImGuiWindow* window = FindWindowByName(name))
+    {
+        window->DC.CursorMaxPos.y += window->Scroll.y;
+        window->Scroll.y = scroll_y;
+        window->DC.CursorMaxPos.y -= window->Scroll.y;
+    }
+}
+
 void ImGui::SetWindowCollapsed(ImGuiWindow* window, bool collapsed, ImGuiCond cond)
 {
     // Test condition (NB: bit 0 is always true) and clear flags for next time
diff --git a/r5dev/thirdparty/imgui/src/imgui_logger.cpp b/r5dev/thirdparty/imgui/src/imgui_logger.cpp
index 7a358d53..87980f89 100644
--- a/r5dev/thirdparty/imgui/src/imgui_logger.cpp
+++ b/r5dev/thirdparty/imgui/src/imgui_logger.cpp
@@ -23,23 +23,23 @@ bool equals(InputIt1 first1, InputIt1 last1,
 }
 
 CTextLogger::CTextLogger()
-	: m_flLineSpacing(1.0f)
-	, m_nTabSize(4)
-	, m_bAutoScroll(true)
+	: m_bAutoScroll(true)
 	, m_bScrollToBottom(true)
 	, m_bScrollToCursor(false)
 	, m_bScrolledToMax(false)
-	, m_flTextStart(0.0f)
-	, m_nLeftMargin(0)
-	, m_bCursorPositionChanged(false)
-	, m_nColorRangeMin(0)
-	, m_nColorRangeMax(0)
-	, m_SelectionMode(SelectionMode::Normal)
-	, m_flLastClick(-1.0)
 	, m_bHandleKeyboardInputs(true)
 	, m_bHandleMouseInputs(true)
+	, m_bWithinLoggingRect(false)
 	, m_bShowWhiteSpaces(false)
+	, m_flTextStart(0.0f)
+	, m_flLineSpacing(1.0f)
+	, m_flLastClick(-1.0)
+	, m_nTabSize(4)
+	, m_nLeftMargin(0)
+	, m_nColorRangeMin(0)
+	, m_nColorRangeMax(0)
 	, m_nStartTime(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count())
+	, m_SelectionMode(SelectionMode::Normal)
 {
 	m_Lines.push_back(Line());
 }
@@ -609,7 +609,17 @@ void CTextLogger::HandleMouseInputs(bool bHoveredScrollbar, bool bActiveScrollba
 	bool bCtrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl;
 	bool bAlt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt;
 
-	if (!bHoveredScrollbar && !bActiveScrollbar && ImGui::IsWindowFocused())
+
+	if (ImGui::IsMouseClicked(0) && ImGui::IsWindowHovered())
+	{
+		m_bWithinLoggingRect = true;
+	}
+	if (ImGui::IsMouseReleased(0))
+	{
+		m_bWithinLoggingRect = false;
+	}
+
+	if (!bHoveredScrollbar && !bActiveScrollbar && m_bWithinLoggingRect)
 	{
 		if (!bShift && !bAlt)
 		{
@@ -675,6 +685,7 @@ void CTextLogger::HandleMouseInputs(bool bHoveredScrollbar, bool bActiveScrollba
 				io.WantCaptureMouse = true;
 				m_State.m_CursorPosition = m_InteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos());
 				SetSelection(m_InteractiveStart, m_InteractiveEnd, m_SelectionMode);
+				EnsureCursorVisible();
 			}
 		}
 	}
@@ -683,7 +694,6 @@ void CTextLogger::HandleMouseInputs(bool bHoveredScrollbar, bool bActiveScrollba
 void CTextLogger::Render()
 {
 	m_Mutex.lock();
-	m_bCursorPositionChanged = false;
 
 	ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
 
@@ -947,12 +957,41 @@ void CTextLogger::SetTextLines(const std::vector<CConLog>& aLines)
 	}
 }
 
+void CTextLogger::MoveCursor(int aLines, bool aForward)
+{
+	Coordinates newStart;
+
+	if (aForward)
+	{
+		newStart = m_State.m_CursorPosition;
+		newStart.m_nLine += aLines;
+
+		if (newStart.m_nLine >= static_cast<int>(m_Lines.size()))
+		{
+			newStart.m_nLine = static_cast<int>(m_Lines.size()) - 1;
+			newStart.m_nColumn = GetLineMaxColumn(newStart.m_nLine);
+		}
+	}
+	else
+	{
+		newStart = m_State.m_CursorPosition;
+		newStart.m_nLine -= aLines;
+
+		if (newStart.m_nLine < 0)
+		{
+			newStart.m_nLine = 0;
+			newStart.m_nColumn = 0;
+		}
+	}
+
+	m_State.m_CursorPosition = newStart;
+}
+
 void CTextLogger::SetCursorPosition(const Coordinates & aPosition)
 {
 	if (m_State.m_CursorPosition != aPosition)
 	{
 		m_State.m_CursorPosition = aPosition;
-		m_bCursorPositionChanged = true;
 		EnsureCursorVisible();
 	}
 }
@@ -1003,10 +1042,6 @@ void CTextLogger::SetSelection(const Coordinates & aStart, const Coordinates & a
 	default:
 		break;
 	}
-
-	if (m_State.m_SelectionStart != oldSelStart ||
-		m_State.m_SelectionEnd != oldSelEnd)
-		m_bCursorPositionChanged = true;
 }
 
 void CTextLogger::SetTabSize(int aValue)
@@ -1309,6 +1344,60 @@ bool CTextLogger::HasSelection() const
 	return m_State.m_SelectionEnd > m_State.m_SelectionStart;
 }
 
+void CTextLogger::MoveSelection(int aLines, bool aForward)
+{
+	assert(aLines > 0);
+
+	if (aLines < 1)
+		return;
+
+	if (HasSelection())
+	{
+		Coordinates newStart;
+		Coordinates newEnd;
+
+		if (aForward)
+		{
+			newStart = m_State.m_SelectionStart;
+			newStart.m_nLine += aLines;
+			newEnd = m_State.m_SelectionEnd;
+			newEnd.m_nLine += aLines;
+
+			if (newStart.m_nLine >= static_cast<int>(m_Lines.size()))
+			{
+				newStart.m_nLine = static_cast<int>(m_Lines.size()) - 1;
+				newStart.m_nColumn = GetLineMaxColumn(newStart.m_nLine);
+			}
+			if (newEnd.m_nLine >= static_cast<int>(m_Lines.size()))
+			{
+				newEnd.m_nLine = static_cast<int>(m_Lines.size()) - 1;
+				newEnd.m_nColumn = GetLineMaxColumn(newStart.m_nLine);
+			}
+		}
+		else
+		{
+			newStart = m_State.m_SelectionStart;
+			newStart.m_nLine -= aLines;
+			newEnd = m_State.m_SelectionEnd;
+			newEnd.m_nLine -= aLines;
+
+			if (newStart.m_nLine < 0)
+			{
+				newStart.m_nLine = 0;
+				newStart.m_nColumn = 0;
+			}
+			if (newEnd.m_nLine < 0)
+			{
+				newEnd.m_nLine = 0;
+				newEnd.m_nColumn = 0;
+			}
+		}
+
+		SetSelectionStart(newStart);
+		SetSelectionEnd(newEnd);
+	}
+}
+
 std::string CTextLogger::GetText() const
 {
 	return GetText(Coordinates(), Coordinates(static_cast<int>(m_Lines.size()), 0));
@@ -1408,12 +1497,13 @@ float CTextLogger::TextDistanceToLineStart(const Coordinates& aFrom) const
 void CTextLogger::EnsureCursorVisible()
 {
 	m_bScrollToCursor = true;
+	Coordinates pos = GetActualCursorCoordinates();
 
 	float scrollX = ImGui::GetScrollX();
 	float scrollY = ImGui::GetScrollY();
 
-	float height = ImGui::GetWindowHeight();
 	float width = ImGui::GetWindowWidth();
+	float height = ImGui::GetWindowHeight();
 
 	int top = 1 + static_cast<int>(ceil(scrollY / m_CharAdvance.y));
 	int bottom = static_cast<int>(ceil((scrollY + height) / m_CharAdvance.y));
@@ -1421,17 +1511,14 @@ void CTextLogger::EnsureCursorVisible()
 	int left = static_cast<int>(ceil(scrollX / m_CharAdvance.x));
 	int right = static_cast<int>(ceil((scrollX + width) / m_CharAdvance.x));
 
-	Coordinates pos = GetActualCursorCoordinates();
-	float len = TextDistanceToLineStart(pos);
-
+	if (pos.m_nColumn < left)
+		ImGui::SetScrollX(std::max(0.0f, (pos.m_nColumn) * m_CharAdvance.x));
+	if (pos.m_nColumn > right - 3)
+		ImGui::SetScrollX(std::max(0.0f, (pos.m_nColumn + 3) * m_CharAdvance.x - width));
 	if (pos.m_nLine < top)
-		ImGui::SetScrollY(std::max(0.0f, (pos.m_nLine - 1) * m_CharAdvance.y));
-	if (pos.m_nLine > bottom - 4)
-		ImGui::SetScrollY(std::max(0.0f, (pos.m_nLine + 4) * m_CharAdvance.y - height));
-	if (len + m_flTextStart < left + 4)
-		ImGui::SetScrollX(std::max(0.0f, len + m_flTextStart - 4));
-	if (len + m_flTextStart > right - 4)
-		ImGui::SetScrollX(std::max(0.0f, len + m_flTextStart + 4 - width));
+		ImGui::SetScrollY(std::max(0.0f, (pos.m_nLine) * m_CharAdvance.y));
+	if (pos.m_nLine > bottom - 2)
+		ImGui::SetScrollY(std::max(0.0f, (pos.m_nLine + 2) * m_CharAdvance.y - height));
 }
 
 int CTextLogger::GetPageSize() const