Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,18 @@ jobs:
if: ${{ !matrix.emscripten }}
run: |
nim r tests/test_textboxes.nim
nim r ${{ matrix.build_flags }} tests/test_richtextboxes.nim
nim r -d:silkyTesting -d:nimTypeNames tests/test_inputs.nim
nim r -d:silkyTesting tests/test_buttons.nim
nim r -d:silkyTesting tests/test_focus.nim
nim r -d:silkyTesting tests/test_subwindow.nim
nim r tests/test_atlas_png.nim
nim r tests/test_shader_codegen.nim
nim r ${{ matrix.build_flags }} tests/test_density.nim
nim r ${{ matrix.build_flags }} tests/test_decorations.nim
nim r ${{ matrix.build_flags }} tests/test_rules.nim
nim r ${{ matrix.build_flags }} tests/test_quotes.nim
nim r ${{ matrix.build_flags }} tests/test_button_vertices.nim
nim r -d:silkyTesting tests/test_layout.nim
nim r -d:silkyTesting tests/test_layout_regressions.nim
nim r ${{ matrix.build_flags }} tests/test_layout_clips.nim
Expand Down
2 changes: 1 addition & 1 deletion nimby.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ nimsimd 1.3.2 https://github.com/guzba/nimsimd 3f6b2668ffb0867d0bf786a658b817763
bumpy 1.1.3 https://github.com/treeform/bumpy dc9a3d6d15680ec7f603959d65ba8ee4f1ddbc72
crunchy 0.1.11 https://github.com/guzba/crunchy 98eb6526982bb8aae8eec6e8781f4539fa19e049
bitty 0.1.4 https://github.com/treeform/bitty/ c40e81277c04befa37234cba5052f58648bef895
windy 0.5.0 https://github.com/treeform/windy d3d92f5decf22f98c61d198d6285427c83f67b60
windy 0.5.0 https://github.com/treeform/windy 8e1e2499a050bcbb1546f239b8c3a8b1b16748c0
opengl 1.2.9 https://github.com/nim-lang/opengl 8e2e098f82dc5eefd874488c37b5830233cd18f4
urlly 1.1.1 https://github.com/treeform/urlly 99784779f05649df25fd9c33003d8ef6de027345
ws 0.5.0 https://github.com/treeform/ws cbb8f763b436669392d10baec2a45778395395cc
Expand Down
11 changes: 7 additions & 4 deletions src/silky.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ from silky/common import SilkyError
export SilkyError

when defined(silkyTesting):
import silky/[semantics, atlas, widgets, textboxes, testing, dsl, menus, profiles]
import silky/[
semantics, atlas, widgets, textboxes, richtextboxes, testing, dsl, menus,
profiles
]
export semantics except textInputs
export atlas, tables, textboxes, testing, dsl, menus, profiles
export atlas, tables, textboxes, richtextboxes, testing, dsl, menus, profiles
export widgets except
button, checkBox, clickableIcon, dropDown, frame, group, h1text, icon,
iconButton, image, listBox, progressBar, radioButton, ribbon, scrubber, text
Expand All @@ -16,14 +19,14 @@ else:
not defined(useMetal4) and
not defined(useCpu):
import opengl
import silky/[contexts, atlas, widgets, textboxes, dsl, menus]
import silky/[contexts, atlas, widgets, textboxes, richtextboxes, dsl, menus]
when not defined(useDirectX) and
not defined(useVulkan) and
not defined(useMetal4) and
not defined(useCpu):
export opengl
export contexts except textInputs
export windy, atlas, tables, textboxes, dsl, menus
export windy, atlas, tables, textboxes, richtextboxes, dsl, menus
export widgets except
button, checkBox, clickableIcon, dropDown, frame, group, h1text, icon,
iconButton, image, listBox, progressBar, radioButton, ribbon, scrubber, text
112 changes: 82 additions & 30 deletions src/silky/atlas.nim
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ type
y*: int
width*: int
height*: int
pixelScale*: int = 1
## The number of texture pixels per logical pixel.

LetterEntry* = object
## The position and size of a letter in the font atlas.
Expand Down Expand Up @@ -53,6 +55,8 @@ type
ascent*: float32
lineGap*: float32
subpixelSteps*: int
pixelScale*: float32 = 1
## The glyph raster density; font metrics remain logical.
entries*: Table[string, seq[LetterEntry]]
lookup*: GlyphLookup

Expand Down Expand Up @@ -291,9 +295,12 @@ proc writePng*(path, json: string, image: Image) =
proc addImage*(
builder: AtlasBuilder,
name: string,
image: Image
image: Image,
pixelScale: int = 1
): bool =
## Pack one image into the atlas. False when the atlas is full.
if pixelScale < 1:
raise newException(SilkyAtlasError, "Image pixel scale must be positive")
let allocation = builder.allocator.allocate(image.width, image.height)
if not allocation.success:
return false
Expand All @@ -306,7 +313,8 @@ proc addImage*(
x: allocation.x,
y: allocation.y,
width: image.width,
height: image.height
height: image.height,
pixelScale: pixelScale
)
true

Expand Down Expand Up @@ -340,8 +348,9 @@ proc newAtlasBuilderFromAtlas*(
for variants in fontAtlas.entries.values:
for letter in variants:
let
w = letter.boundsWidth.ceil.int
h = letter.boundsHeight.ceil.int
density = max(fontAtlas.pixelScale, 0.001'f)
w = (letter.boundsWidth * density).ceil.int
h = (letter.boundsHeight * density).ceil.int
if w > 0 and h > 0:
result.markOccupiedEntry(letter.x, letter.y, w, h)

Expand All @@ -360,7 +369,12 @@ proc growAtlas*(builder: AtlasBuilder, newSize: int) =
builder.allocator = newSkylineAllocator(newSize, builder.margin)
builder.allocator.markRegion(0, 0, oldSize, oldSize)

proc addDir*(builder: AtlasBuilder, path: string, removePrefix: string = "") =
proc addDir*(
builder: AtlasBuilder,
path: string,
removePrefix: string = "",
pixelScale: int = 1
) =
## Add all images in the given directory to the atlas.
for file in walkDir(path):
if file.path.endsWith(".png"):
Expand All @@ -369,19 +383,24 @@ proc addDir*(builder: AtlasBuilder, path: string, removePrefix: string = "") =
if removePrefix.len > 0:
key.removePrefix(removePrefix)
key.removeSuffix(".png")
if not builder.addImage(key, image):
if not builder.addImage(key, image, pixelScale):
raise newException(
ValueError,
"Failed to allocate space for " & file.path & "\n" &
"You need to increase the size of the atlas"
)

proc addDirRecursive*(builder: AtlasBuilder, path: string, removePrefix: string = "") =
proc addDirRecursive*(
builder: AtlasBuilder,
path: string,
removePrefix: string = "",
pixelScale: int = 1
) =
## Add all images in the given directory and its subdirectories to the atlas.
builder.addDir(path, removePrefix)
builder.addDir(path, removePrefix, pixelScale)
for entry in walkDir(path):
if entry.kind == pcDir:
builder.addDirRecursive(entry.path, removePrefix)
builder.addDirRecursive(entry.path, removePrefix, pixelScale)

proc finiteBounds(bounds: Rect): Rect =
## Glyphs with no outline (such as space) compute to infinite/NaN bounds,
Expand All @@ -393,18 +412,33 @@ proc finiteBounds(bounds: Rect): Rect =
else:
rect(0, 0, 0, 0)

proc addFont*(builder: AtlasBuilder, path: string, name: string, size: float32, chars: seq[string] = AsciiGlyphs, subpixelSteps: int = 0) =
## Add a font to the atlas.
let fontAtlas = FontAtlas()
proc addFont*(
builder: AtlasBuilder,
typeface: Typeface,
name: string,
size: float32,
chars: seq[string] = AsciiGlyphs,
subpixelSteps: int = 0,
pixelScale: float32 = 1
) =
## Rasterizes a font at pixelScale with logical layout metrics.
if pixelScale <= 0 or
pixelScale.classify notin {fcNormal, fcSubnormal}:
raise newException(SilkyAtlasError, "Font pixel scale must be positive")
let
density = pixelScale.float32
fontAtlas = FontAtlas()
fontAtlas.size = size
fontAtlas.subpixelSteps = subpixelSteps
let typeface = readTypeface(path)
var fontObj = newFont(typeface)
fontAtlas.pixelScale = pixelScale
let fontObj = newFont(typeface)
fontObj.size = size
fontAtlas.lineHeight = (typeface.ascent - typeface.descent + typeface.lineGap) * fontObj.scale
fontAtlas.ascent = typeface.ascent * fontObj.scale
fontAtlas.descent = typeface.descent * fontObj.scale
fontAtlas.lineGap = typeface.lineGap * fontObj.scale
let logicalScale = fontObj.scale
fontAtlas.lineHeight =
(typeface.ascent - typeface.descent + typeface.lineGap) * logicalScale
fontAtlas.ascent = typeface.ascent * logicalScale
fontAtlas.descent = typeface.descent * logicalScale
fontAtlas.lineGap = typeface.lineGap * logicalScale
# If subpixelSteps > 0, generates multiple versions of each glyph shifted by 1/subpixelSteps pixels.
# For example, subpixelSteps=10 generates 10 versions per glyph at 0.0, 0.1, 0.2, ... 0.9 pixel offsets.
let numVariants = if subpixelSteps > 0: subpixelSteps else: 1
Expand All @@ -413,10 +447,10 @@ proc addFont*(builder: AtlasBuilder, path: string, name: string, size: float32,
let
rune = glyphStr.runeAt(0)
glyphPath = typeface.getGlyphPath(rune)
scale = fontObj.scale
scale = logicalScale * density
scaleMat = scale(vec2(scale))
baseBounds = glyphPath.computeBounds(scaleMat).snapToPixels().finiteBounds()
advance = typeface.getAdvance(rune) * scale
advance = typeface.getAdvance(rune) * logicalScale
fontAtlas.entries[glyphStr] = @[]
for variant in 0 ..< numVariants:
let
Expand All @@ -441,7 +475,7 @@ proc addFont*(builder: AtlasBuilder, path: string, name: string, size: float32,
let allocation = builder.allocator.allocate(glyphImage.width, glyphImage.height)
if not allocation.success:
raise newException(
ValueError,
SilkyAtlasError,
"Failed to allocate space for glyph: " & glyphStr & " variant " & $variant & "\n" &
"You need to increase the size of the atlas"
)
Expand All @@ -453,31 +487,49 @@ proc addFont*(builder: AtlasBuilder, path: string, name: string, size: float32,
fontAtlas.entries[glyphStr].add(LetterEntry(
x: allocation.x,
y: allocation.y,
boundsX: baseBounds.x + subpixelOffset.floor,
boundsY: baseBounds.y,
boundsWidth: offsetBounds.w.ceil,
boundsHeight: offsetBounds.h.ceil,
boundsX: (baseBounds.x + subpixelOffset.floor) / density,
boundsY: baseBounds.y / density,
boundsWidth: offsetBounds.w.ceil / density,
boundsHeight: offsetBounds.h.ceil / density,
advance: advance
))
else:
fontAtlas.entries[glyphStr].add(LetterEntry(
x: 0,
y: 0,
boundsX: baseBounds.x,
boundsY: baseBounds.y,
boundsWidth: baseBounds.w,
boundsHeight: baseBounds.h,
boundsX: baseBounds.x / density,
boundsY: baseBounds.y / density,
boundsWidth: baseBounds.w / density,
boundsHeight: baseBounds.h / density,
advance: advance
))
# Kerning only needs to be stored once per base glyph (variant 0).
for glyphStr2 in chars:
let rune2 = glyphStr2.runeAt(0)
let kerning = typeface.getKerningAdjustment(rune, rune2)
if kerning != 0:
fontAtlas.entries[glyphStr][0].kerning[glyphStr2] = kerning * scale
fontAtlas.entries[glyphStr][0].kerning[glyphStr2] =
kerning * logicalScale
fontAtlas.rebuildGlyphLookups()
builder.atlas.fonts[name] = fontAtlas

proc addFont*(
builder: AtlasBuilder,
path: string,
name: string,
size: float32,
chars: seq[string] = AsciiGlyphs,
subpixelSteps: int = 0,
pixelScale: float32 = 1
) =
## Loads a font file and rasterizes it at the requested pixel density.
try:
builder.addFont(
readTypeface(path), name, size, chars, subpixelSteps, pixelScale
)
except PixieError as error:
raise newException(SilkyAtlasError, "Could not load font: " & error.msg)

proc write*(builder: AtlasBuilder, outputPngPath: string) =
## Write atlas image and JSON metadata into a single PNG.
writePng(outputPngPath, builder.atlas.toJson(), builder.atlasImage)
1 change: 1 addition & 0 deletions src/silky/clips.nim
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type
## Vertex and clip boundaries for a deferred layout on both layers.
marks*: array[2, int]
clipMark*: int
textMark*: int

proc intersectClips(a, b: Rect): Rect =
## Intersects clip bounds while keeping empty intersections empty.
Expand Down
Loading
Loading