Skip to content

Commit a9649e3

Browse files
authored
Merge pull request #47 from treeform/heysokam/tooltips
Improve tooltips behavior and configurability
2 parents 0bea01a + b1e34bb commit a9649e3

8 files changed

Lines changed: 295 additions & 25 deletions

File tree

-345 Bytes
Loading
401 Bytes
Loading

src/silky/contexts.nim

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,16 @@ type
101101
mouseConsumed*: bool = false
102102
hover*: bool = false
103103
showTooltip*: bool = false
104+
tooltipActive*: bool = false
105+
tooltipPos*: Vec2
106+
tooltipAnchor*: Rect
107+
tooltipLastAnchor*: Rect
108+
tooltipOffset*: Vec2
109+
tooltipFadeInTime*: float64
110+
tooltipFadeInDuration*: float64 = 0.25
104111
framebufferSize*: IVec2
105112
lastMousePos*: Vec2
106-
tooltipThreshold*: float64 = 0.5
113+
tooltipThreshold*: float64 = 0
107114
atlas*: SilkyAtlas
108115
image*: Image
109116
drawer*: Drawer
@@ -257,6 +264,7 @@ proc beginUiShared*(sk: Silky, window: Window, size: IVec2) =
257264
createDir("tmp")
258265
dumpMeasures("tmp/trace.json")
259266

267+
sk.tooltipActive = sk.showTooltip
260268
sk.showTooltip = false
261269
sk.mouseConsumed = false
262270
sk.framebufferSize = size
@@ -277,7 +285,11 @@ proc beginUiShared*(sk: Silky, window: Window, size: IVec2) =
277285
else:
278286
sk.mouseIdleTime += deltaTime
279287

280-
sk.showTooltip = false
288+
if sk.tooltipActive:
289+
sk.tooltipFadeInTime += deltaTime
290+
else:
291+
sk.tooltipFadeInTime = 0
292+
281293
measurePush("frame")
282294
sk.pushClipRect(rect(0, 0, sk.size.x, sk.size.y))
283295

@@ -410,7 +422,8 @@ proc drawTriangle*(
410422
uv: uvs[i],
411423
color: colors[i],
412424
clipPos: cPos,
413-
clipSize: cSize
425+
clipSize: cSize,
426+
maskUv: vec2(-1, -1)
414427
))
415428

416429
proc drawText*(
@@ -602,11 +615,14 @@ proc getTextSize*(sk: Silky, font: string, text: string): Vec2 =
602615
let
603616
fontData = sk.atlas.fonts[font]
604617
runedText = text.toRunes
605-
var currentPos = vec2(0, fontData.lineHeight)
618+
var
619+
currentPos = vec2(0, fontData.lineHeight)
620+
maxWidth = 0.0'f
606621

607622
for i in 0 ..< runedText.len:
608623
let rune = runedText[i]
609624
if rune == Rune(10):
625+
maxWidth = max(maxWidth, currentPos.x)
610626
currentPos.x = 0
611627
currentPos.y += fontData.lineHeight
612628
continue
@@ -626,7 +642,8 @@ proc getTextSize*(sk: Silky, font: string, text: string): Vec2 =
626642
if nextGlyphStr in entry.kerning:
627643
currentPos.x += entry.kerning[nextGlyphStr]
628644

629-
currentPos
645+
maxWidth = max(maxWidth, currentPos.x)
646+
vec2(maxWidth, currentPos.y)
630647

631648
proc newSilky*(
632649
window: Window,

src/silky/semantic.nim

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,13 @@ type
284284
mouseConsumed*: bool = false
285285
hover*: bool = false
286286
showTooltip*: bool = false
287+
tooltipActive*: bool = false
288+
tooltipPos*: Vec2
289+
tooltipAnchor*: Rect
290+
tooltipLastAnchor*: Rect
291+
tooltipOffset*: Vec2
292+
tooltipFadeInTime*: float64
293+
tooltipFadeInDuration*: float64 = 0.25
287294
framebufferSize*: IVec2
288295
lastMousePos*: Vec2
289296
tooltipThreshold*: float64 = 0.5
@@ -447,6 +454,9 @@ proc drawRect*(sk: Silky, pos: Vec2, size: Vec2, color: ColorRGBX) {.inline.} =
447454
## Stub for drawing a solid rectangle.
448455
discard
449456

457+
proc drawTriangle*(sk: Silky, positions: array[3, Vec2], uvs: array[3, Vec2], colors: array[3, ColorRGBX]) {.inline.} =
458+
discard
459+
450460
proc draw9Patch*(sk: Silky, name: string, patch: int, pos: Vec2, size: Vec2, color = rgbx(255, 255, 255, 255)) {.inline.} =
451461
## Stub for drawing a 9-patch image.
452462
discard
@@ -497,6 +507,7 @@ proc newSilky*(window: Window, atlasPngPath: string): Silky =
497507

498508
proc beginUi*(sk: Silky, window: auto, size: IVec2) =
499509
## Begins a new UI frame.
510+
sk.tooltipActive = sk.showTooltip
500511
sk.showTooltip = false
501512
sk.framebufferSize = size
502513
sk.mousePos = window.mousePos.vec2 / sk.uiScale

src/silky/widgets.nim

Lines changed: 93 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -547,9 +547,11 @@ template iconButton*(image: string, body) =
547547
case interaction
548548
of Hovered:
549549
sk.hover = true
550+
sk.tooltipAnchor = buttonRect
550551
patch = "button.hover.9patch"
551552
of Pressed, Held:
552553
sk.hover = true
554+
sk.tooltipAnchor = buttonRect
553555
patch = "button.down.9patch"
554556
of Released:
555557
sk.hover = false
@@ -583,6 +585,8 @@ template clickableIcon*(image: string, on: bool, body) =
583585
offColor
584586

585587
sk.hover = interaction in [Hovered, Pressed, Held]
588+
if sk.hover:
589+
sk.tooltipAnchor = iconRect
586590

587591
if interaction == Pressed:
588592
body
@@ -1144,33 +1148,102 @@ template menuItem*(label: string, body: untyped) =
11441148
sk.menuItemEnd(ctx)
11451149

11461150
template tooltip*(text: string) =
1147-
## Display a tooltip at the mouse cursor.
1148-
## This should be called after a widget when sk.showTooltip is true.
1151+
## Draws a tooltip with fade-in and stem image.
11491152
let tooltipText = text
11501153
sk.pushLayer(PopupsLayer)
11511154
sk.pushRawClipRect(rect(vec2(0, 0), sk.rootSize))
11521155

1153-
let textSize = sk.getTextSize(sk.textStyle, tooltipText)
1154-
let tooltipSize = textSize + vec2(sk.theme.padding.float32 * 2, sk.theme.padding.float32 * 2)
1155-
let mousePos = sk.mousePos
1156-
1157-
# Position tooltip near mouse, offset slightly to avoid cursor.
1158-
var tooltipPos = mousePos + vec2(16, 16)
1159-
1160-
# Keep tooltip on screen.
1161-
let root = sk.rootSize
1162-
if tooltipPos.x + tooltipSize.x > root.x:
1163-
tooltipPos.x = root.x - tooltipSize.x - sk.theme.padding.float32
1156+
let
1157+
textSize = sk.getTextSize(sk.textStyle, tooltipText)
1158+
pad = sk.theme.padding.float32
1159+
tooltipSize = textSize + vec2(pad * 2, pad * 2)
1160+
root = sk.rootSize
1161+
hasAnchor = sk.tooltipAnchor.w > 0 and
1162+
sk.tooltipAnchor.h > 0
1163+
anchorCenter = sk.tooltipAnchor.xy +
1164+
vec2(sk.tooltipAnchor.w, sk.tooltipAnchor.h) * 0.5
1165+
1166+
# Position centered on anchor below, flip above if needed.
1167+
var tooltipPos =
1168+
if hasAnchor:
1169+
vec2(
1170+
anchorCenter.x - tooltipSize.x * 0.5,
1171+
sk.tooltipAnchor.y + sk.tooltipAnchor.h + 8
1172+
)
1173+
else:
1174+
sk.mousePos + vec2(16, 16)
1175+
tooltipPos += sk.tooltipOffset
11641176
if tooltipPos.y + tooltipSize.y > root.y:
1165-
tooltipPos.y = mousePos.y - tooltipSize.y - 4
1177+
tooltipPos.y = sk.tooltipAnchor.y - tooltipSize.y - 8
1178+
if tooltipSize.x <= root.x:
1179+
tooltipPos.x = clamp(
1180+
tooltipPos.x, pad, root.x - tooltipSize.x - pad)
1181+
if tooltipSize.y <= root.y:
1182+
tooltipPos.y = max(tooltipPos.y, pad)
1183+
1184+
# Reset fade when anchor changes.
1185+
if not sk.tooltipActive or
1186+
sk.tooltipAnchor != sk.tooltipLastAnchor:
1187+
sk.tooltipPos = tooltipPos
1188+
sk.tooltipFadeInTime = 0
1189+
sk.tooltipLastAnchor = sk.tooltipAnchor
1190+
sk.showTooltip = true
11661191

1167-
# Ensure tooltip doesn't go off-screen left or top.
1168-
tooltipPos.x = max(tooltipPos.x, sk.theme.padding.float32)
1169-
tooltipPos.y = max(tooltipPos.y, sk.theme.padding.float32)
1192+
let
1193+
fadeAlpha = clamp(
1194+
sk.tooltipFadeInTime / sk.tooltipFadeInDuration,
1195+
0.0, 1.0
1196+
).float32
1197+
fadeByte = (255.0 * fadeAlpha).uint8
1198+
fadeColor = rgbx(fadeByte, fadeByte, fadeByte, fadeByte)
1199+
sk.pushLayout(sk.tooltipPos, tooltipSize)
1200+
sk.draw9Patch(
1201+
"tooltip.9patch", 6, sk.pos, sk.size, fadeColor
1202+
)
11701203

1171-
sk.pushLayout(tooltipPos, tooltipSize)
1172-
sk.draw9Patch("tooltip.9patch", 6, sk.pos, sk.size)
1173-
discard sk.drawText(sk.textStyle, tooltipText, sk.pos + vec2(sk.theme.padding), sk.theme.defaultTextColor)
1204+
# Draw stem on top, pointing toward anchor.
1205+
if hasAnchor and "tooltip.stem" in sk.atlas.entries:
1206+
let
1207+
stem = sk.atlas.entries["tooltip.stem"]
1208+
stemW = stem.width.float32
1209+
stemH = stem.height.float32
1210+
halfH = stemH * 0.5
1211+
uvPos = vec2(stem.x.float32, stem.y.float32)
1212+
uv0 = uvPos
1213+
uv1 = uvPos + vec2(stemW, 0)
1214+
uv2 = uvPos + vec2(stemW, stemH)
1215+
uv3 = uvPos + vec2(0, stemH)
1216+
colors = [fadeColor, fadeColor, fadeColor]
1217+
above = anchorCenter.y < sk.pos.y + sk.size.y * 0.5
1218+
x = clamp(anchorCenter.x,
1219+
sk.pos.x + stemW * 0.5,
1220+
sk.pos.x + sk.size.x - stemW * 0.5)
1221+
cy =
1222+
if above: sk.pos.y + 1.0
1223+
else: sk.pos.y + sk.size.y - 1.0
1224+
p0 = vec2(x - stemW * 0.5, cy - halfH)
1225+
p1 = vec2(x + stemW * 0.5, cy - halfH)
1226+
p2 = vec2(x + stemW * 0.5, cy + halfH)
1227+
p3 = vec2(x - stemW * 0.5, cy + halfH)
1228+
t0 = if above: uv3 else: uv0
1229+
t1 = if above: uv2 else: uv1
1230+
t2 = if above: uv1 else: uv2
1231+
t3 = if above: uv0 else: uv3
1232+
sk.drawTriangle([p0, p1, p2], [t0, t1, t2], colors)
1233+
sk.drawTriangle([p0, p2, p3], [t0, t2, t3], colors)
1234+
1235+
let
1236+
baseColor = sk.theme.defaultTextColor
1237+
textColor = rgbx(
1238+
(baseColor.r.float32 * fadeAlpha).uint8,
1239+
(baseColor.g.float32 * fadeAlpha).uint8,
1240+
(baseColor.b.float32 * fadeAlpha).uint8,
1241+
(baseColor.a.float32 * fadeAlpha).uint8,
1242+
)
1243+
discard sk.drawText(
1244+
sk.textStyle, tooltipText,
1245+
sk.pos + vec2(sk.theme.padding), textColor
1246+
)
11741247
sk.popLayout()
11751248

11761249
sk.popClipRect()

tests/data/tooltip.9patch.png

335 Bytes
Loading

tests/data/tooltip.stem.png

401 Bytes
Loading

0 commit comments

Comments
 (0)