From 9d54f5d35cb172ce5d014c7f264e2ac0592655e9 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 13 Mar 2026 20:59:13 -0600 Subject: [PATCH 1/8] updates --- examples/siwin_text.nim | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/examples/siwin_text.nim b/examples/siwin_text.nim index 9a70dec..8b71dd5 100644 --- a/examples/siwin_text.nim +++ b/examples/siwin_text.nim @@ -233,7 +233,7 @@ proc makeRenderTree*( if rune == Rune(10): monoLines.inc let monoHeight = monoLines.float32 * monoLineHeight + monoPad * 2 - let invertedLineHeight = uiFont.size * 1.4'f32 + let invertedBoxHeight = uiFont.size * 5.0'f32 let sectionGap = 60.0'f32 proc mirroredInputRect(finalRect: Rect): Rect = @@ -243,16 +243,16 @@ proc makeRenderTree*( innerRect.x, innerRect.y, innerRect.w, - innerRect.h - monoHeight - invertedLineHeight * 2.0'f32 - sectionGap * 3.0'f32, + innerRect.h - monoHeight - invertedBoxHeight * 2.0'f32 - sectionGap * 3.0'f32, ) let invertedTextRect = rect( - innerRect.x, textRect.y + textRect.h + sectionGap, innerRect.w, invertedLineHeight + innerRect.x, textRect.y + textRect.h + sectionGap, innerRect.w, invertedBoxHeight ) let mirroredInvertedTextRect = rect( innerRect.x, invertedTextRect.y + invertedTextRect.h + sectionGap, innerRect.w, - invertedLineHeight, + invertedBoxHeight, ) let monoRect = rect( innerRect.x, @@ -262,7 +262,7 @@ proc makeRenderTree*( ) let (layout, highlightRange) = buildBodyTextLayout(uiFont, textRect, modeLine) - let invertedText = "Inverted text line (NfInvertY) with selection" + let invertedText = "Inverted text line (NfInvertY)\nwith selection" let invertedSelectionRange = findPhraseRange(invertedText, "NfInvertY") let invertedLayout = typeset( rect(0, 0, invertedTextRect.w, invertedTextRect.h), @@ -306,18 +306,6 @@ proc makeRenderTree*( ), ) - let invertedGlyphBounds = rect( - invertedTextRect.x + invertedLayout.bounding.x, - invertedTextRect.y + invertedLayout.bounding.y, - invertedLayout.bounding.w, - invertedLayout.bounding.h, - ) - let mirroredInvertedGlyphBounds = rect( - mirroredInvertedTextRect.x + invertedLayout.bounding.x, - mirroredInvertedTextRect.y + invertedLayout.bounding.y, - invertedLayout.bounding.w, - invertedLayout.bounding.h, - ) discard result.addChild( z, cardIdx, @@ -325,7 +313,7 @@ proc makeRenderTree*( kind: nkRectangle, childCount: 0, zlevel: z, - screenBox: invertedGlyphBounds, + screenBox: invertedTextRect, fill: clearColor, stroke: RenderStroke(weight: 1.5, fill: rgba(38, 38, 38, 155).color), corners: [4.0'f32, 4.0, 4.0, 4.0], @@ -354,7 +342,7 @@ proc makeRenderTree*( kind: nkRectangle, childCount: 0, zlevel: z, - screenBox: mirroredInvertedGlyphBounds, + screenBox: mirroredInvertedTextRect, fill: clearColor, stroke: RenderStroke(weight: 1.5, fill: rgba(42, 96, 168, 170).color), corners: [4.0'f32, 4.0, 4.0, 4.0], From 4b1ba47b71b948c2f92c34c904f2a74f4d4b771e Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 13 Mar 2026 21:09:06 -0600 Subject: [PATCH 2/8] trying to improve inverted glyphs --- src/figdraw/figrender.nim | 47 +++++++++++++++++++++++++++++------ tests/trender_text_invert.nim | 6 ++--- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/figdraw/figrender.nim b/src/figdraw/figrender.nim index 24f31ab..706b268 100644 --- a/src/figdraw/figrender.nim +++ b/src/figdraw/figrender.nim @@ -279,6 +279,20 @@ proc glyphScreenPos*( scaled(glyphPos.y - glyphDescent) + nodeBox.y.scaled(), ) +proc glyphScreenPosInverted*( + nodeBox: Rect, + layoutBounds: Rect, + glyphX: float32, + glyphRect: Rect, +): Vec2 {.inline.} = + ## Converts a local glyph position into screen-space coordinates with Y-inverted + ## text layout (line order + glyph placement), mirrored around content bounds. + let invertedTop = layoutBounds.y + layoutBounds.h - (glyphRect.y + glyphRect.h) + vec2( + glyphX.scaled() + nodeBox.x.scaled(), + scaled(invertedTop) + nodeBox.y.scaled(), + ) + proc selectionScreenRect*(nodeBox: Rect, selectionRect: Rect): Rect {.inline.} = ## Converts a local text selection rectangle into screen-space coordinates. @@ -290,6 +304,15 @@ proc selectionScreenRect*(nodeBox: Rect, selectionRect: Rect): Rect {.inline.} = ) .scaled() +proc selectionLocalRectInverted*(layoutBounds: Rect, selectionRect: Rect): Rect {.inline.} = + ## Mirrors a local text selection rectangle along the content bounds' Y axis. + rect( + selectionRect.x, + layoutBounds.y + layoutBounds.h - (selectionRect.y + selectionRect.h), + selectionRect.w, + selectionRect.h, + ) + proc renderText(ctx: BackendContext, node: Fig) {.forbids: [AppMainThreadEff].} = ## Draw characters (glyphs) let @@ -298,6 +321,11 @@ proc renderText(ctx: BackendContext, node: Fig) {.forbids: [AppMainThreadEff].} glyphVariantSubpixelPositioning = subpixelPositioning and ctx.textSubpixelGlyphVariantsEnabled() invertText = NfInvertY in node.flags + layoutBounds = + if node.textLayout.bounding.h > 0.0'f32: + node.textLayout.bounding + else: + rect(0.0'f32, 0.0'f32, node.screenBox.w, node.screenBox.h) if NfSelectText in node.flags and fillAlphaMax(node.fill) > 0'u8: let rects = node.textLayout.selectionRects @@ -307,9 +335,10 @@ proc renderText(ctx: BackendContext, node: Fig) {.forbids: [AppMainThreadEff].} let selectionGradient = node.fill.gradientColors() let zeroRadii = [0.0'f32, 0.0'f32, 0.0'f32, 0.0'f32] for idx in startIdx .. endIdx: - var rect = selectionScreenRect(node.screenBox, rects[idx]) + var selectionRect = rects[idx] if invertText: - rect = rect + selectionRect = selectionLocalRectInverted(layoutBounds, selectionRect) + let rect = selectionScreenRect(node.screenBox, selectionRect) if rect.w > 0 and rect.h > 0: ctx.drawRoundedRectSdf( rect = rect, @@ -326,7 +355,13 @@ proc renderText(ctx: BackendContext, node: Fig) {.forbids: [AppMainThreadEff].} continue var - glyphPos = glyphScreenPos(node.screenBox, glyph.pos, glyph.descent) + glyphPos = + if invertText: + glyphScreenPosInverted( + node.screenBox, layoutBounds, glyph.pos.x, glyph.rect + ) + else: + glyphScreenPos(node.screenBox, glyph.pos, glyph.descent) subpixelShift = 0.0'f32 subpixelVariant = 0 if subpixelPositioning: @@ -355,11 +390,7 @@ proc renderText(ctx: BackendContext, node: Fig) {.forbids: [AppMainThreadEff].} ctx.setTextSubpixelShift(0.0'f32) continue - var drawPos = glyphPos - if invertText: - drawPos.y = drawPos.y - (glyph.lineHeight.scaled() + glyph.descent) * 0.5'f32 - - ctx.drawImage(glyphId, drawPos, glyph.fill.gradientColors(), invertText) + ctx.drawImage(glyphId, glyphPos, glyph.fill.gradientColors(), invertText) if subpixelPositioning: ctx.setTextSubpixelShift(0.0'f32) diff --git a/tests/trender_text_invert.nim b/tests/trender_text_invert.nim index d972e93..975931d 100644 --- a/tests/trender_text_invert.nim +++ b/tests/trender_text_invert.nim @@ -96,7 +96,7 @@ proc profileDiffFlipped(a, b: seq[int]): int = total suite "siwin text invert render": - test "NfInvertY under mirrored parent shifts output downward": + test "NfInvertY under mirrored parent stays upright but shifts downward": setFigUiScale(1.0'f32) setFigDataDir(getCurrentDir() / "data") @@ -205,7 +205,7 @@ suite "siwin text invert render": check leftHighlight.found check rightHighlight.found - check inkHeight(rightBounds) - inkHeight(leftBounds) >= 30 + check abs(inkHeight(leftBounds) - inkHeight(rightBounds)) <= 4 check rightBounds.y0 - leftBounds.y0 >= 40 check abs(inkHeight(leftHighlight) - inkHeight(rightHighlight)) <= 2 @@ -220,4 +220,4 @@ suite "siwin text invert render": let directDiff = profileDiff(leftProfile, rightProfile) flippedDiff = profileDiffFlipped(leftProfile, rightProfile) - check directDiff == flippedDiff + check directDiff <= flippedDiff From f65114112f0b144dddaecf10d16992db9c8904c3 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 13 Mar 2026 21:23:02 -0600 Subject: [PATCH 3/8] trying to improve inverted glyphs --- examples/siwin_graph_bottom_left.nim | 138 +++++++++++++++++++++------ 1 file changed, 108 insertions(+), 30 deletions(-) diff --git a/examples/siwin_graph_bottom_left.nim b/examples/siwin_graph_bottom_left.nim index 0823ced..bd689c0 100644 --- a/examples/siwin_graph_bottom_left.nim +++ b/examples/siwin_graph_bottom_left.nim @@ -12,6 +12,7 @@ import figdraw/fignodes import figdraw/figrender const RunOnce {.booldefine: "figdraw.runOnce".}: bool = false +const FontName {.strdefine: "figdraw.defaultfont".}: string = "Ubuntu.ttf" proc addRectNode( renders: var Renders, @@ -34,7 +35,7 @@ proc addRectNode( ), ) -proc makeRenderTree(windowW, windowH: float32): Renders = +proc makeRenderTree(windowW, windowH: float32, uiFont: FigFont): Renders = result = Renders(layers: initOrderedTable[ZLevel, RenderList]()) let z = 0.ZLevel @@ -49,6 +50,21 @@ proc makeRenderTree(windowW, windowH: float32): Renders = ), ) + let sceneIdx = result.addChild( + z, + rootIdx, + Fig( + kind: nkTransform, + childCount: 0, + zlevel: z, + transform: TransformStyle( + translation: vec2(0.0'f32, windowH), + matrix: scale(vec3(1.0'f32, -1.0'f32, 1.0'f32)), + useMatrix: true, + ), + ), + ) + let margin = max(36.0'f32, min(windowW, windowH) * 0.08'f32) let plotRect = rect( margin, @@ -59,46 +75,43 @@ proc makeRenderTree(windowW, windowH: float32): Renders = result.addRectNode( z, - rootIdx, + sceneIdx, plotRect, rgba(255, 255, 255, 255), corners = [10.0'f32, 10.0, 10.0, 10.0], ) - let graphIdx = result.addChild( - z, - rootIdx, - Fig( - kind: nkTransform, - childCount: 0, - zlevel: z, - transform: TransformStyle( - # Convert the graph to bottom-left origin coordinates. - translation: vec2(plotRect.x, plotRect.y + plotRect.h), - matrix: scale(vec3(1.0'f32, -1.0'f32, 1.0'f32)), - useMatrix: true, - ), - ), - ) - let gridLines = 10 for i in 0 .. gridLines: let t = i.float32 / gridLines.float32 - let gx = t * plotRect.w - let gy = t * plotRect.h + let + gx = plotRect.x + t * plotRect.w + gy = plotRect.y + t * plotRect.h result.addRectNode( - z, graphIdx, rect(gx, 0, 1.0'f32, plotRect.h), rgba(225, 229, 238, 255) + z, + sceneIdx, + rect(gx, plotRect.y, 1.0'f32, plotRect.h), + rgba(225, 229, 238, 255), ) result.addRectNode( - z, graphIdx, rect(0, gy, plotRect.w, 1.0'f32), rgba(225, 229, 238, 255) + z, + sceneIdx, + rect(plotRect.x, gy, plotRect.w, 1.0'f32), + rgba(225, 229, 238, 255), ) result.addRectNode( - z, graphIdx, rect(0, 0, plotRect.w, 2.0'f32), rgba(60, 65, 80, 255) + z, + sceneIdx, + rect(plotRect.x, plotRect.y, plotRect.w, 2.0'f32), + rgba(60, 65, 80, 255), ) result.addRectNode( - z, graphIdx, rect(0, 0, 2.0'f32, plotRect.h), rgba(60, 65, 80, 255) + z, + sceneIdx, + rect(plotRect.x, plotRect.y, 2.0'f32, plotRect.h), + rgba(60, 65, 80, 255), ) let samples = max(120, plotRect.w.int) @@ -106,19 +119,80 @@ proc makeRenderTree(windowW, windowH: float32): Renders = for i in 0 .. samples: let t = i.float32 / samples.float32 - let x = t * plotRect.w + let x = plotRect.x + t * plotRect.w let yNorm = clamp(0.5'f32 + 0.35'f32 * sin(t * cycles), 0.0'f32, 1.0'f32) - let y = yNorm * plotRect.h + let y = plotRect.y + yNorm * plotRect.h result.addRectNode( z, - graphIdx, + sceneIdx, rect(x - 1.5'f32, y - 1.5'f32, 3.0'f32, 3.0'f32), rgba(230, 63, 63, 255), ) - # Origin marker at (0, 0) in graph space. + # Origin marker at (0, 0) in plot-local graph space. result.addRectNode( - z, graphIdx, rect(-3.0'f32, -3.0'f32, 6.0'f32, 6.0'f32), rgba(39, 169, 110, 255) + z, + sceneIdx, + rect(plotRect.x - 3.0'f32, plotRect.y - 3.0'f32, 6.0'f32, 6.0'f32), + rgba(39, 169, 110, 255), + ) + + let legendPadding = 12.0'f32 + let legendRect = rect( + plotRect.x + plotRect.w - 300.0'f32, + plotRect.y + plotRect.h - 20.0'f32 - 124.0'f32, + 280.0'f32, + 124.0'f32, + ) + + discard result.addChild( + z, + sceneIdx, + Fig( + kind: nkRectangle, + childCount: 0, + zlevel: z, + screenBox: legendRect, + fill: rgba(255, 255, 255, 230), + stroke: RenderStroke(weight: 1.2'f32, fill: rgba(120, 130, 150, 180).color), + corners: [8.0'f32, 8.0'f32, 8.0'f32, 8.0'f32], + ), + ) + + let legendText = + "Legend\n" & + "Red points: y = 0.5 + 0.35*sin(2πx)\n" & + "Green point: origin (0, 0)\n" & + "Axes: bottom-left coordinates" + let legendSelectionRange = 0'i16 .. 5'i16 + let legendTextRect = rect( + legendRect.x + legendPadding, + legendRect.y + legendPadding, + legendRect.w - legendPadding * 2.0'f32, + legendRect.h - legendPadding * 2.0'f32, + ) + let legendLayout = typeset( + rect(0, 0, legendTextRect.w, legendTextRect.h), + [span(uiFont, rgba(35, 40, 52, 255), legendText)], + hAlign = Left, + vAlign = Top, + minContent = false, + wrap = true, + ) + + discard result.addChild( + z, + sceneIdx, + Fig( + kind: nkText, + childCount: 0, + zlevel: z, + flags: {NfInvertY, NfSelectText}, + screenBox: legendTextRect, + fill: rgba(255, 221, 122, 220), + selectionRange: legendSelectionRange, + textLayout: legendLayout, + ), ) when isMainModule: @@ -143,6 +217,10 @@ when isMainModule: renderer.setupBackend(appWindow) appWindow.title = siwinWindowTitle(renderer, appWindow, "Siwin Bottom-Left Graph") + registerStaticTypeface("Ubuntu.ttf", "../data/Ubuntu.ttf") + let typefaceId = loadTypeface(FontName, @["Ubuntu.ttf"]) + let uiFont = FigFont(typefaceId: typefaceId, size: 16.0'f32) + var frames = 0 fpsFrames = 0 @@ -151,7 +229,7 @@ when isMainModule: proc redraw() = renderer.beginFrame() let logicalSize = appWindow.logicalSize() - var renders = makeRenderTree(logicalSize.x, logicalSize.y) + var renders = makeRenderTree(logicalSize.x, logicalSize.y, uiFont) renderer.renderFrame(renders, logicalSize) renderer.endFrame() From 25125e6f1323e249b6e7ab66ff9805fd51f5d492 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 13 Mar 2026 21:33:48 -0600 Subject: [PATCH 4/8] trying to improve inverted glyphs --- examples/siwin_graph_bottom_left.nim | 5 +- src/figdraw/figrender.nim | 151 ++++++++++++++------------- 2 files changed, 81 insertions(+), 75 deletions(-) diff --git a/examples/siwin_graph_bottom_left.nim b/examples/siwin_graph_bottom_left.nim index bd689c0..046a7c1 100644 --- a/examples/siwin_graph_bottom_left.nim +++ b/examples/siwin_graph_bottom_left.nim @@ -137,7 +137,8 @@ proc makeRenderTree(windowW, windowH: float32, uiFont: FigFont): Renders = rgba(39, 169, 110, 255), ) - let legendPadding = 12.0'f32 + # let legendPadding = 12.0'f32 + let legendPadding = 0.0'f32 let legendRect = rect( plotRect.x + plotRect.w - 300.0'f32, plotRect.y + plotRect.h - 20.0'f32 - 124.0'f32, @@ -175,7 +176,7 @@ proc makeRenderTree(windowW, windowH: float32, uiFont: FigFont): Renders = rect(0, 0, legendTextRect.w, legendTextRect.h), [span(uiFont, rgba(35, 40, 52, 255), legendText)], hAlign = Left, - vAlign = Top, + vAlign = Bottom, minContent = false, wrap = true, ) diff --git a/src/figdraw/figrender.nim b/src/figdraw/figrender.nim index 706b268..a7c6446 100644 --- a/src/figdraw/figrender.nim +++ b/src/figdraw/figrender.nim @@ -280,18 +280,12 @@ proc glyphScreenPos*( ) proc glyphScreenPosInverted*( - nodeBox: Rect, - layoutBounds: Rect, - glyphX: float32, - glyphRect: Rect, + nodeBox: Rect, layoutBounds: Rect, glyphX: float32, glyphRect: Rect ): Vec2 {.inline.} = ## Converts a local glyph position into screen-space coordinates with Y-inverted ## text layout (line order + glyph placement), mirrored around content bounds. let invertedTop = layoutBounds.y + layoutBounds.h - (glyphRect.y + glyphRect.h) - vec2( - glyphX.scaled() + nodeBox.x.scaled(), - scaled(invertedTop) + nodeBox.y.scaled(), - ) + vec2(glyphX.scaled() + nodeBox.x.scaled(), scaled(invertedTop) + nodeBox.y.scaled()) proc selectionScreenRect*(nodeBox: Rect, selectionRect: Rect): Rect {.inline.} = ## Converts a local text selection rectangle into screen-space coordinates. @@ -304,7 +298,9 @@ proc selectionScreenRect*(nodeBox: Rect, selectionRect: Rect): Rect {.inline.} = ) .scaled() -proc selectionLocalRectInverted*(layoutBounds: Rect, selectionRect: Rect): Rect {.inline.} = +proc selectionLocalRectInverted*( + layoutBounds: Rect, selectionRect: Rect +): Rect {.inline.} = ## Mirrors a local text selection rectangle along the content bounds' Y axis. rect( selectionRect.x, @@ -313,6 +309,10 @@ proc selectionLocalRectInverted*(layoutBounds: Rect, selectionRect: Rect): Rect selectionRect.h, ) +proc glyphLocalPos*(glyphPos: Vec2, glyphDescent: float32): Vec2 {.inline.} = + ## Converts a local glyph baseline position into local glyph top-left coordinates. + vec2(glyphPos.x.scaled(), scaled(glyphPos.y - glyphDescent)) + proc renderText(ctx: BackendContext, node: Fig) {.forbids: [AppMainThreadEff].} = ## Draw characters (glyphs) let @@ -327,72 +327,77 @@ proc renderText(ctx: BackendContext, node: Fig) {.forbids: [AppMainThreadEff].} else: rect(0.0'f32, 0.0'f32, node.screenBox.w, node.screenBox.h) - if NfSelectText in node.flags and fillAlphaMax(node.fill) > 0'u8: - let rects = node.textLayout.selectionRects - if rects.len > 0 and node.selectionRange.a <= node.selectionRange.b: - let startIdx = max(node.selectionRange.a, 0) - let endIdx = min(node.selectionRange.b, rects.len - 1) - let selectionGradient = node.fill.gradientColors() - let zeroRadii = [0.0'f32, 0.0'f32, 0.0'f32, 0.0'f32] - for idx in startIdx .. endIdx: - var selectionRect = rects[idx] - if invertText: - selectionRect = selectionLocalRectInverted(layoutBounds, selectionRect) - let rect = selectionScreenRect(node.screenBox, selectionRect) - if rect.w > 0 and rect.h > 0: - ctx.drawRoundedRectSdf( - rect = rect, - colors = selectionGradient, - radii = zeroRadii, - mode = figbackend.SdfMode.sdfModeClipAA, - factor = 4.0'f32, - spread = 0.0'f32, - shapeSize = vec2(0.0'f32, 0.0'f32), - ) + ctx.saveTransform() + try: + ctx.translate(node.screenBox.xy.scaled()) + if invertText: + # Typeset layout coordinates are top-left based; invert the full local text + # layout around its content bounds so line order + selection + glyphs match. + let invertPivotY = scaled(layoutBounds.y + layoutBounds.h) + ctx.translate(vec2(0.0'f32, invertPivotY)) + ctx.scale(vec2(1.0'f32, -1.0'f32)) + + if NfSelectText in node.flags and fillAlphaMax(node.fill) > 0'u8: + let rects = node.textLayout.selectionRects + if rects.len > 0 and node.selectionRange.a <= node.selectionRange.b: + let startIdx = max(node.selectionRange.a, 0) + let endIdx = min(node.selectionRange.b, rects.len - 1) + let selectionGradient = node.fill.gradientColors() + let zeroRadii = [0.0'f32, 0.0'f32, 0.0'f32, 0.0'f32] + for idx in startIdx .. endIdx: + let rect = rects[idx].scaled() + if rect.w > 0 and rect.h > 0: + ctx.drawRoundedRectSdf( + rect = rect, + colors = selectionGradient, + radii = zeroRadii, + mode = figbackend.SdfMode.sdfModeClipAA, + factor = 4.0'f32, + spread = 0.0'f32, + shapeSize = vec2(0.0'f32, 0.0'f32), + ) - for glyph in node.textLayout.glyphs(): - if unicode.isWhiteSpace(glyph.rune): - continue + for glyph in node.textLayout.glyphs(): + if unicode.isWhiteSpace(glyph.rune): + continue - var - glyphPos = - if invertText: - glyphScreenPosInverted( - node.screenBox, layoutBounds, glyph.pos.x, glyph.rect - ) + var + glyphPos = glyphLocalPos(glyph.pos, glyph.descent) + subpixelShift = 0.0'f32 + subpixelVariant = 0 + if subpixelPositioning: + let snappedX = floor(glyphPos.x) + let fractionalX = max(0.0'f32, min(glyphPos.x - snappedX, 0.999'f32)) + glyphPos.x = snappedX + if glyphVariantSubpixelPositioning: + subpixelVariant = toGlyphVariantSubpixelStep(fractionalX) else: - glyphScreenPos(node.screenBox, glyph.pos, glyph.descent) - subpixelShift = 0.0'f32 - subpixelVariant = 0 - if subpixelPositioning: - let snappedX = floor(glyphPos.x) - let fractionalX = max(0.0'f32, min(glyphPos.x - snappedX, 0.999'f32)) - glyphPos.x = snappedX - if glyphVariantSubpixelPositioning: - subpixelVariant = toGlyphVariantSubpixelStep(fractionalX) - else: - subpixelShift = fractionalX - - let glyphId = - glyph.hash(lcdFiltering = lcdFiltering, subpixelVariant = subpixelVariant) - - ctx.setTextSubpixelShift(subpixelShift) - if glyphId notin ctx.entries: - let img = glyph.generateGlyph(lcdFiltering = lcdFiltering, - subpixelVariant = subpixelVariant, - force = true, - upload = false, - ) - ctx.putImage(glyphId, img) - if glyphId in ctx.entries: - debug "missing glyph image in context", - glyphId = glyphId, glyphRune = $glyph.rune, glyphRuneRepr = repr(glyph.rune) + subpixelShift = fractionalX + + let glyphId = + glyph.hash(lcdFiltering = lcdFiltering, subpixelVariant = subpixelVariant) + + ctx.setTextSubpixelShift(subpixelShift) + if glyphId notin ctx.entries: + let img = glyph.generateGlyph( + lcdFiltering = lcdFiltering, + subpixelVariant = subpixelVariant, + force = true, + upload = false, + ) + ctx.putImage(glyphId, img) + if glyphId in ctx.entries: + debug "missing glyph image in context", + glyphId = glyphId, glyphRune = $glyph.rune, glyphRuneRepr = repr(glyph.rune) + ctx.setTextSubpixelShift(0.0'f32) + continue + + ctx.drawImage(glyphId, glyphPos, glyph.fill.gradientColors(), false) + if subpixelPositioning: ctx.setTextSubpixelShift(0.0'f32) - continue - - ctx.drawImage(glyphId, glyphPos, glyph.fill.gradientColors(), invertText) - if subpixelPositioning: - ctx.setTextSubpixelShift(0.0'f32) + finally: + ctx.setTextSubpixelShift(0.0'f32) + ctx.restoreTransform() import macros except `$` @@ -832,7 +837,7 @@ proc renderImage(ctx: BackendContext, node: Fig) = pos = box.xy, color = fillCenterColor(node.image.fill), size = size, - flipY = NfInvertY in node.flags + flipY = NfInvertY in node.flags, ) proc renderMsdfImage(ctx: BackendContext, node: Fig) = @@ -880,7 +885,7 @@ proc renderMtsdfImage(ctx: BackendContext, node: Fig) = pxRange = pxRange, sdThreshold = sdThreshold, strokeWeight = strokeWeight, - flipY = NfInvertY in node.flags + flipY = NfInvertY in node.flags, ) proc renderBackdropBlur(ctx: BackendContext, node: Fig) = From 020d78fc9467bb34b99677f73117bcc6736e107e Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 13 Mar 2026 21:42:35 -0600 Subject: [PATCH 5/8] trying to improve inverted glyphs --- examples/siwin_graph_bottom_left.nim | 5 ++--- src/figdraw/figrender.nim | 11 +++-------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/examples/siwin_graph_bottom_left.nim b/examples/siwin_graph_bottom_left.nim index 046a7c1..bd689c0 100644 --- a/examples/siwin_graph_bottom_left.nim +++ b/examples/siwin_graph_bottom_left.nim @@ -137,8 +137,7 @@ proc makeRenderTree(windowW, windowH: float32, uiFont: FigFont): Renders = rgba(39, 169, 110, 255), ) - # let legendPadding = 12.0'f32 - let legendPadding = 0.0'f32 + let legendPadding = 12.0'f32 let legendRect = rect( plotRect.x + plotRect.w - 300.0'f32, plotRect.y + plotRect.h - 20.0'f32 - 124.0'f32, @@ -176,7 +175,7 @@ proc makeRenderTree(windowW, windowH: float32, uiFont: FigFont): Renders = rect(0, 0, legendTextRect.w, legendTextRect.h), [span(uiFont, rgba(35, 40, 52, 255), legendText)], hAlign = Left, - vAlign = Bottom, + vAlign = Top, minContent = false, wrap = true, ) diff --git a/src/figdraw/figrender.nim b/src/figdraw/figrender.nim index a7c6446..ac38b1a 100644 --- a/src/figdraw/figrender.nim +++ b/src/figdraw/figrender.nim @@ -321,19 +321,14 @@ proc renderText(ctx: BackendContext, node: Fig) {.forbids: [AppMainThreadEff].} glyphVariantSubpixelPositioning = subpixelPositioning and ctx.textSubpixelGlyphVariantsEnabled() invertText = NfInvertY in node.flags - layoutBounds = - if node.textLayout.bounding.h > 0.0'f32: - node.textLayout.bounding - else: - rect(0.0'f32, 0.0'f32, node.screenBox.w, node.screenBox.h) ctx.saveTransform() try: ctx.translate(node.screenBox.xy.scaled()) if invertText: - # Typeset layout coordinates are top-left based; invert the full local text - # layout around its content bounds so line order + selection + glyphs match. - let invertPivotY = scaled(layoutBounds.y + layoutBounds.h) + # Mirror in local text-box coordinates so first-line top offset/padding is + # preserved instead of swapping Top/Bottom alignment. + let invertPivotY = scaled(node.screenBox.h) ctx.translate(vec2(0.0'f32, invertPivotY)) ctx.scale(vec2(1.0'f32, -1.0'f32)) From ae4f1bbd1e5bead83fe236119c661ba0b68ada94 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 13 Mar 2026 21:46:12 -0600 Subject: [PATCH 6/8] trying to improve inverted glyphs --- src/figdraw/figrender.nim | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/figdraw/figrender.nim b/src/figdraw/figrender.nim index ac38b1a..3b114ec 100644 --- a/src/figdraw/figrender.nim +++ b/src/figdraw/figrender.nim @@ -261,7 +261,7 @@ func fillCenterColor(fill: Fill): Color func gradientColors(fill: Fill): array[4, ColorRGBA] proc renderDrawable*(ctx: BackendContext, node: Fig) = - ## TODO: draw non-node stuff? + ## TODO: render non-node stuff? let box = node.screenBox.scaled() let color = fillCenterColor(node.fill) for point in node.points: @@ -314,18 +314,17 @@ proc glyphLocalPos*(glyphPos: Vec2, glyphDescent: float32): Vec2 {.inline.} = vec2(glyphPos.x.scaled(), scaled(glyphPos.y - glyphDescent)) proc renderText(ctx: BackendContext, node: Fig) {.forbids: [AppMainThreadEff].} = - ## Draw characters (glyphs) + ## Render characters (glyphs) let lcdFiltering = ctx.textLcdFilteringEnabled() subpixelPositioning = ctx.textSubpixelPositioningEnabled() glyphVariantSubpixelPositioning = subpixelPositioning and ctx.textSubpixelGlyphVariantsEnabled() - invertText = NfInvertY in node.flags ctx.saveTransform() - try: + block: ctx.translate(node.screenBox.xy.scaled()) - if invertText: + if NfInvertY in node.flags: # Mirror in local text-box coordinates so first-line top offset/padding is # preserved instead of swapping Top/Bottom alignment. let invertPivotY = scaled(node.screenBox.h) @@ -390,9 +389,8 @@ proc renderText(ctx: BackendContext, node: Fig) {.forbids: [AppMainThreadEff].} ctx.drawImage(glyphId, glyphPos, glyph.fill.gradientColors(), false) if subpixelPositioning: ctx.setTextSubpixelShift(0.0'f32) - finally: - ctx.setTextSubpixelShift(0.0'f32) - ctx.restoreTransform() + ctx.setTextSubpixelShift(0.0'f32) + ctx.restoreTransform() import macros except `$` From 3f91500864b53edb225440c0755e31b096c30947 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 13 Mar 2026 21:46:53 -0600 Subject: [PATCH 7/8] v0.22.4 - finally proper inverted text --- figdraw.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/figdraw.nimble b/figdraw.nimble index db8f2b1..8070654 100644 --- a/figdraw.nimble +++ b/figdraw.nimble @@ -1,4 +1,4 @@ -version = "0.22.3" +version = "0.22.4" author = "Jaremy Creechley" description = "UI Engine for Nim" license = "MIT" From df22a87c857a2bd9931bca30551acd545ebe4d08 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 13 Mar 2026 21:49:36 -0600 Subject: [PATCH 8/8] v0.22.4 - finally proper inverted text --- tests/trender_text_invert.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/trender_text_invert.nim b/tests/trender_text_invert.nim index 975931d..c81c380 100644 --- a/tests/trender_text_invert.nim +++ b/tests/trender_text_invert.nim @@ -96,7 +96,7 @@ proc profileDiffFlipped(a, b: seq[int]): int = total suite "siwin text invert render": - test "NfInvertY under mirrored parent stays upright but shifts downward": + test "NfInvertY under mirrored parent stays upright and vertically aligned": setFigUiScale(1.0'f32) setFigDataDir(getCurrentDir() / "data") @@ -206,10 +206,10 @@ suite "siwin text invert render": check rightHighlight.found check abs(inkHeight(leftBounds) - inkHeight(rightBounds)) <= 4 - check rightBounds.y0 - leftBounds.y0 >= 40 + check abs(rightBounds.y0 - leftBounds.y0) <= 4 check abs(inkHeight(leftHighlight) - inkHeight(rightHighlight)) <= 2 - check rightHighlight.y0 - leftHighlight.y0 >= 40 + check abs(rightHighlight.y0 - leftHighlight.y0) <= 2 let leftProfile = rowInkProfile(img, leftBounds)