summaryrefslogtreecommitdiff
path: root/internal/display/tooltip.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/display/tooltip.go')
-rw-r--r--internal/display/tooltip.go194
1 files changed, 194 insertions, 0 deletions
diff --git a/internal/display/tooltip.go b/internal/display/tooltip.go
new file mode 100644
index 0000000..42075e0
--- /dev/null
+++ b/internal/display/tooltip.go
@@ -0,0 +1,194 @@
+package display
+
+import (
+ "fmt"
+ "time"
+
+ "codeberg.org/snonux/loadbars/internal/config"
+ "codeberg.org/snonux/loadbars/internal/stats"
+ "github.com/veandco/go-sdl2/sdl"
+)
+
+// mouseIdleTimeout is how long the mouse must be idle before the tooltip
+// and host inversion overlay are hidden.
+const mouseIdleTimeout = 3 * time.Second
+
+const (
+ tooltipScale int32 = 2 // pixel scale for bitmap font
+ tooltipPadX int32 = 6 // horizontal padding inside tooltip box
+ tooltipPadY int32 = 4 // vertical padding inside tooltip box
+ tooltipOffsetX int32 = 12 // offset from cursor to tooltip
+ tooltipOffsetY int32 = 12
+)
+
+// tooltipLines builds the text lines to display for the hovered bar.
+func tooltipLines(bar *barDescriptor, snap map[string]*stats.HostStats, cfg *config.Config, state *runState) []string {
+ h := snap[bar.host]
+ if h == nil {
+ return []string{bar.host}
+ }
+ switch bar.kind {
+ case barCPU:
+ return cpuTooltipLines(bar, h, state)
+ case barMem:
+ return memTooltipLines(bar, h, state)
+ case barNet:
+ return netTooltipLines(bar, cfg, state)
+ }
+ return nil
+}
+
+// cpuTooltipLines returns tooltip text for a CPU bar showing smoothed percentages.
+func cpuTooltipLines(bar *barDescriptor, h *stats.HostStats, state *runState) []string {
+ lines := []string{fmt.Sprintf("%s [%s]", bar.host, bar.cpuName)}
+ key := bar.host + ";" + bar.cpuName
+ s := state.smoothedCPU[key]
+ if s == nil {
+ lines = append(lines, "No data yet")
+ return lines
+ }
+ // Indices match cpuBarTargetPcts: 0=sys 1=usr 2=nice 3=idle 4=io 5=irq 6=softirq 7=guest 8=steal 9=guestnice
+ lines = append(lines,
+ fmt.Sprintf("Sys: %5.1f%%", s[0]),
+ fmt.Sprintf("Usr: %5.1f%%", s[1]),
+ fmt.Sprintf("Nice: %5.1f%%", s[2]),
+ fmt.Sprintf("IO: %5.1f%%", s[4]),
+ fmt.Sprintf("Steal: %5.1f%%", s[8]),
+ fmt.Sprintf("Idle: %5.1f%%", s[3]),
+ )
+ return lines
+}
+
+// memTooltipLines returns tooltip text for a memory bar.
+func memTooltipLines(bar *barDescriptor, h *stats.HostStats, state *runState) []string {
+ lines := []string{fmt.Sprintf("%s [mem]", bar.host)}
+ sm := state.smoothedMem[bar.host]
+ if sm == nil || h.Mem == nil {
+ lines = append(lines, "No data yet")
+ return lines
+ }
+ memTotal := h.Mem["MemTotal"]
+ memFree := h.Mem["MemFree"]
+ swapTotal := h.Mem["SwapTotal"]
+ swapFree := h.Mem["SwapFree"]
+ lines = append(lines,
+ fmt.Sprintf("RAM: %s / %s", formatKB(memTotal-memFree), formatKB(memTotal)),
+ fmt.Sprintf(" %5.1f%%", sm.ramUsed),
+ fmt.Sprintf("Swap: %s / %s", formatKB(swapTotal-swapFree), formatKB(swapTotal)),
+ fmt.Sprintf(" %5.1f%%", sm.swapUsed),
+ )
+ return lines
+}
+
+// netTooltipLines returns tooltip text for a network bar.
+func netTooltipLines(bar *barDescriptor, cfg *config.Config, state *runState) []string {
+ lines := []string{fmt.Sprintf("%s [net]", bar.host)}
+ sm := state.smoothedNet[bar.host]
+ if sm == nil {
+ lines = append(lines, "No data yet")
+ return lines
+ }
+ lines = append(lines,
+ fmt.Sprintf("RX: %5.1f%%", sm.rxPct),
+ fmt.Sprintf("TX: %5.1f%%", sm.txPct),
+ fmt.Sprintf("Link: %s", cfg.NetLink),
+ )
+ return lines
+}
+
+// formatKB formats a value in KB as a human-readable string (KB, MB, or GB).
+func formatKB(kb int64) string {
+ switch {
+ case kb >= 1024*1024:
+ return fmt.Sprintf("%.1fG", float64(kb)/(1024*1024))
+ case kb >= 1024:
+ return fmt.Sprintf("%.1fM", float64(kb)/1024)
+ default:
+ return fmt.Sprintf("%dK", kb)
+ }
+}
+
+// drawTooltip renders the tooltip box with text lines near the cursor position,
+// clamped to stay within the window.
+func drawTooltip(renderer *sdl.Renderer, lines []string, mx, my, winW, winH int32) {
+ if len(lines) == 0 {
+ return
+ }
+ // Compute box dimensions from the longest line
+ lineH := glyphH*tooltipScale + 2 // 2px spacing between lines
+ maxW := int32(0)
+ for _, l := range lines {
+ w := stringWidth(l, tooltipScale)
+ if w > maxW {
+ maxW = w
+ }
+ }
+ boxW := maxW + tooltipPadX*2
+ boxH := int32(len(lines))*lineH + tooltipPadY*2 - 2 // subtract trailing spacing
+
+ // Position: prefer below-right of cursor, clamp to window
+ bx := mx + tooltipOffsetX
+ by := my + tooltipOffsetY
+ if bx+boxW > winW {
+ bx = mx - tooltipOffsetX - boxW
+ }
+ if by+boxH > winH {
+ by = my - tooltipOffsetY - boxH
+ }
+ if bx < 0 {
+ bx = 0
+ }
+ if by < 0 {
+ by = 0
+ }
+
+ // Dark background
+ renderer.SetDrawColor(0x18, 0x18, 0x18, 240)
+ renderer.FillRect(&sdl.Rect{X: bx, Y: by, W: boxW, H: boxH})
+ // Grey border (1px)
+ renderer.SetDrawColor(0x60, 0x60, 0x60, 255)
+ renderer.DrawRect(&sdl.Rect{X: bx, Y: by, W: boxW, H: boxH})
+
+ // Light text
+ renderer.SetDrawColor(0xE0, 0xE0, 0xE0, 255)
+ ty := by + tooltipPadY
+ for _, l := range lines {
+ drawString(renderer, l, bx+tooltipPadX, ty, tooltipScale)
+ ty += lineH
+ }
+}
+
+// invertHostBars draws a color-inversion overlay on all bars belonging to the given host.
+// Uses SDL custom blend mode: src factor = ONE_MINUS_DST_COLOR, so drawing white
+// produces (1 - dst) = inverted colors.
+func invertHostBars(renderer *sdl.Renderer, bars []barDescriptor, host string) {
+ invertBlend := sdl.ComposeCustomBlendMode(
+ sdl.BLENDFACTOR_ONE_MINUS_DST_COLOR, sdl.BLENDFACTOR_ZERO, sdl.BLENDOPERATION_ADD,
+ sdl.BLENDFACTOR_ONE, sdl.BLENDFACTOR_ZERO, sdl.BLENDOPERATION_ADD,
+ )
+ renderer.SetDrawBlendMode(invertBlend)
+ renderer.SetDrawColor(255, 255, 255, 255)
+ for i := range bars {
+ if bars[i].host == host {
+ renderer.FillRect(&bars[i].rect)
+ }
+ }
+ renderer.SetDrawBlendMode(sdl.BLENDMODE_NONE)
+}
+
+// drawOverlay handles the full mouse-over effect: builds the bar map, performs
+// hit testing, inverts the hovered host's bars, and draws the tooltip.
+func drawOverlay(renderer *sdl.Renderer, snap map[string]*stats.HostStats, cfg *config.Config, state *runState) {
+ // Hide tooltip and inversion when mouse has been idle for 3 seconds
+ if state.mouseLastMove.IsZero() || time.Since(state.mouseLastMove) > mouseIdleTimeout {
+ return
+ }
+ bars := buildBarMap(snap, cfg, state)
+ hit := hitTest(bars, state.mouseX, state.mouseY)
+ if hit == nil {
+ return
+ }
+ invertHostBars(renderer, bars, hit.host)
+ lines := tooltipLines(hit, snap, cfg, state)
+ drawTooltip(renderer, lines, state.mouseX, state.mouseY, state.winW, state.winH)
+}