Implemented console items window using ImGuiTextSelect after it was implemented into imguin.

This commit is contained in:
Jakob Friedl
2025-09-14 22:55:44 +02:00
parent c6bbef8520
commit ce417db941
4 changed files with 114 additions and 115 deletions

View File

@@ -28,5 +28,5 @@ requires "tiny_sqlite >= 0.2.0"
requires "prologue >= 0.6.6"
requires "winim >= 3.9.4"
requires "ptr_math >= 0.3.0"
requires "imguin >= 1.92.2.0"
requires "imguin >= 1.92.2.1"
requires "zippy >= 0.10.16"

View File

@@ -1,6 +1,6 @@
switch "o", "bin/client"
switch "d", "ImColorTextEdit"
switch "d", "ImGuiTextSelect"
# Select compiler
var TC = "gcc"

View File

@@ -1,41 +1,41 @@
[Window][Sessions [Table View]]
Pos=10,43
Size=1477,421
Size=2117,381
Collapsed=0
DockId=0x00000003,0
[Window][Listeners]
Pos=10,466
Size=1888,523
Pos=10,426
Size=2528,971
Collapsed=0
DockId=0x00000002,0
[Window][Eventlog]
Pos=1489,43
Size=409,421
Pos=2129,43
Size=409,381
Collapsed=0
DockId=0x00000004,0
[Window][Dear ImGui Demo]
Pos=1489,43
Size=409,421
Pos=2129,43
Size=409,381
Collapsed=0
DockId=0x00000004,1
[Window][Dockspace]
Pos=0,0
Size=1908,999
Size=2548,1407
Collapsed=0
[Window][[FACEDEAD] bob@LAPTOP-02]
Pos=10,466
Size=1888,523
Pos=10,426
Size=2528,971
Collapsed=0
DockId=0x00000002,3
[Window][[C9D8E7F6] charlie@SERVER-03]
Pos=10,466
Size=1888,523
Pos=10,426
Size=2528,971
Collapsed=0
DockId=0x00000002,2
@@ -45,8 +45,8 @@ Size=400,400
Collapsed=0
[Window][[G1H2I3J5] diana@WORKSTATION-04]
Pos=10,466
Size=1888,523
Pos=10,426
Size=2528,971
Collapsed=0
DockId=0x00000002,1
@@ -78,23 +78,23 @@ Size=1717,576
Collapsed=0
[Table][0x32886A44,8]
Column 0 Weight=0.6522
Column 1 Weight=0.9755
Column 2 Weight=0.5541
Column 3 Weight=1.0620
Column 4 Weight=1.7316
Column 5 Weight=1.1486
Column 6 Weight=0.3290
Column 7 Weight=1.5469
Column 0 Weight=0.6513
Column 1 Weight=0.9753
Column 2 Weight=0.5524
Column 3 Weight=1.0605
Column 4 Weight=1.7323
Column 5 Weight=1.1492
Column 6 Weight=0.4263
Column 7 Weight=1.4527
[Table][0xB6880529,2]
RefScale=27
Column 0 Sort=0v
[Docking][Data]
DockSpace ID=0x85940918 Window=0x260A4489 Pos=10,43 Size=1888,946 Split=Y
DockNode ID=0x00000001 Parent=0x85940918 SizeRef=1024,421 Split=X
DockSpace ID=0x85940918 Window=0x260A4489 Pos=10,43 Size=2528,1354 Split=Y
DockNode ID=0x00000001 Parent=0x85940918 SizeRef=1024,381 Split=X
DockNode ID=0x00000003 Parent=0x00000001 SizeRef=613,159 CentralNode=1 Selected=0x61E02D75
DockNode ID=0x00000004 Parent=0x00000001 SizeRef=409,159 Selected=0x5E5F7166
DockNode ID=0x00000002 Parent=0x85940918 SizeRef=1024,523 Selected=0x8D780333
DockNode ID=0x00000002 Parent=0x85940918 SizeRef=1024,971 Selected=0x8D780333

View File

@@ -1,118 +1,117 @@
import strformat, strutils
import strformat, strutils, times
import imguin/[cimgui, glfw_opengl, simple]
import ../utils/appImGui
import ../../common/[types]
type
type
ConsoleItem = ref object
timestamp: DateTime
logType: LogType
text: string
ConsoleItems = ref object
items: seq[string]
ConsoleComponent* = ref object of RootObj
agent: Agent
showConsole*: bool
inputBuffer: string
consoleEntries: seq[string]
console: ptr TextEditor
consoleItems: ConsoleItems
textSelect: ptr TextSelect
proc Console*(agent: Agent): ConsoleComponent =
proc getNumLines(data: pointer): csize_t {.cdecl.} =
if data.isNil:
return 0
let consoleItems = cast[ConsoleItems](data)
return consoleItems.items.len().csize_t
proc getLineAtIndex(i: csize_t, data: pointer, outLen: ptr csize_t): cstring {.cdecl.} =
if data.isNil:
return nil
let consoleItems = cast[ConsoleItems](data)
let line = consoleItems.items[i].cstring
if not outLen.isNil:
outLen[] = line.len.csize_t
return line
proc Console*(agent: Agent): ConsoleComponent =
result = new ConsoleComponent
result.agent = agent
result.showConsole = true
result.console = TextEditor_TextEditor()
result.consoleEntries = @[
"a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
]
result.inputBuffer = ""
result.console.TextEditor_SetText(result.consoleEntries.join("\n") & '\0')
proc findLongestLength(text: string): float32 =
var maxWidth = 0.0f
for line in text.splitLines():
let line_cstring = line.cstring
var textSizeOut: ImVec2
igCalcTextSize(addr textSizeOut, line_cstring, nil, false, -1.0f)
if textSizeOut.x > maxWidth:
maxWidth = textSizeOut.x
return maxWidth
proc draw*(component: ConsoleComponent) =
result.consoleItems = new ConsoleItems
result.consoleItems.items = @[]
result.textSelect = textselect_create(getLineAtIndex, getNumLines, cast[pointer](result.consoleItems), 0)
proc draw*(component: ConsoleComponent) =
igBegin(fmt"[{component.agent.agentId}] {component.agent.username}@{component.agent.hostname}", addr component.showConsole, 0)
defer: igEnd()
#[
Console entries/text section
Problems:
# A InputTextMultiline component is placed within a Child Frame to enable both proper text selection and a horizontal scrollbar
# The only thing missing from this implementation is the ability change the text color and auto-scrolling
# https://github.com/ocornut/imgui/issues/383#issuecomment-2080346129
# https://github.com/ocornut/imgui/issues/950
]#
let footerHeight = igGetStyle().ItemSpacing.y + igGetFrameHeightWithSpacing() # * 2
let buffer = component.consoleEntries.join("\n") & '\0'
defer: igEnd()
# Push styles to hide the Child's background and scrollbar background.
igPushStyleColor_Vec4(ImGuiCol_FrameBg.int32, vec4(0.0f, 0.0f, 0.0f, 0.0f))
igPushStyleColor_Vec4(ImGuiCol_ScrollbarBg.int32, vec4(0.0f, 0.0f, 0.0f, 0.0f))
#[
Console items/text section using ImGuiTextSelect in a child window
Supports:
- horizontal+vertical scrolling,
- autoscroll
- colored text
- text selection and copy functionality
if igBeginChild_Str("##Console", vec2(-0.99f, -footerHeight), ImGuiChildFlags_NavFlattened.int32, ImGuiWindowFlags_HorizontalScrollbar.int32):
# Manually handle horizontal scrolling with the mouse wheel/touchpad
let io = igGetIO()
if io.MouseWheelH != 0:
let scroll_delta = io.MouseWheelH * igGetScrollX() * 0.5
igSetScrollX_Float(igGetScrollX() - scroll_delta)
if igGetScrollX() == 0:
igSetScrollX_Float(1.0f) # This is required to prevent the horizontal scrolling from snapping in
# Retrieve the length of the longes console entry
var width = findLongestLength(buffer)
if width <= io.DisplaySize.x:
width = -1.0f
# Set the Text edit background color and make it visible.
igPushStyleColor_Vec4(ImGuiCol_FrameBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
igPushStyleColor_Vec4(ImGuiCol_ScrollbarBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
discard igInputTextMultiline("##ConsoleText", buffer, cast[csize_t](buffer.len()), vec2(width, -1.0f), ImGui_InputTextFlags_ReadOnly.int32 or ImGui_InputTextFlags_AllowTabInput.int32, nil, nil)
Problems I encountered with other approaches (Multi-line Text Input, TextEditor, ...):
- https://github.com/ocornut/imgui/issues/383#issuecomment-2080346129
- https://github.com/ocornut/imgui/issues/950
Huge thanks to @dinau for implementing ImGuiTextSelect into imguin very rapidly after I requested it.
]#
let footerHeight = igGetStyle().ItemSpacing.y + igGetFrameHeightWithSpacing() * 2
if igBeginChild_Str("##Console", vec2(-1.0f, -footerHeight), ImGuiChildFlags_NavFlattened.int32, ImGuiWindowFlags_HorizontalScrollbar.int32):
# Alternative: ImGuiColorTextEdit
# component.console.TextEditor_SetReadOnlyEnabled(true)
# component.console.TextEditor_SetShowLineNumbersEnabled(false)
# component.console.TextEditor_Render("##ConsoleEntries", false, vec2(-1, -1), true)
# Display console items
for entry in component.consoleItems.items:
igTextColored(vec4(0.0f, 1.0f, 1.0f, 1.0f), entry.cstring)
# # Scroll to bottom
# if igGetScrollY() >= igGetScrollMaxY():
# let lineCount = component.console.TextEditor_GetLineCount()
# component.console.TextEditor_SetCursorPosition(lineCount, 0)
igPopStyleColor(2)
igPopStyleColor(2)
component.textSelect.textselect_update()
# Auto-scroll to bottom if we're already at the bottom
if igGetScrollY() >= igGetScrollMaxY():
igSetScrollHereY(1.0f)
igEndChild()
# Buttons for testing the console
if igButton("Add Items", vec2(0.0f, 0.0f)):
for i in 1..10:
component.consoleItems.items.add("Hello world!")
igSameLine(0.0f, 5.0f)
if igButton("Add Long Items", vec2(0.0f, 0.0f)):
for i in 1..3:
component.consoleItems.items.add("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.")
igSameLine(0.0f, 5.0f)
if igButton("Clear", vec2(0.0f, 0.0f)):
component.consoleItems.items.setLen(0)
#[
Input field with prompt indicator
]#
let promptIndicator = fmt"[{component.agent.agentId}]"
var charWidth: ImVec2
igCalcTextSize(addr charWidth, "A", nil, false, -1.0f)
let promptWidth = charWidth.x * float(promptIndicator.len())
let spacing = igGetStyle().ItemSpacing.x
igTextColored(vec4(1.0f, 1.0f, 1.0f, 1.0f), promptIndicator)
igSameLine(0.0f, spacing)
]#
igText(fmt"[{component.agent.agentId}]")
let spacing = igGetStyle().ItemSpacing.x
igSameLine(0.0f, spacing)
# Calculate available width for input
var availableWidth: ImVec2
igGetContentRegionAvail(addr availableWidth)
igSetNextItemWidth(availableWidth.x)
let inputFlags = ImGuiInputTextFlags_EnterReturnsTrue.int32 or ImGuiInputTextFlags_EscapeClearsAll.int32 or ImGuiInputTextFlags_CallbackCompletion.int32 or ImGuiInputTextFlags_CallbackHistory.int32
if igInputText("##Input", component.inputBuffer, 256, inputFlags, nil, nil):
echo component.inputBuffer
#[
Session information (requires footerHeight to be doubled)
]#
if igInputText("##Input", component.inputBuffer, 256, inputFlags, nil, nil):
discard
#[
Session information (optional footer)
]#
# igSeparator()
# igText(fmt"{component.agent.username}@{component.agent.hostname} [{component.agent.ip}]")
# let sessionInfo = fmt"{component.agent.username}@{component.agent.hostname} [{component.agent.ip}]"
# igText(sessionInfo)
igSetItemDefaultFocus()