diff --git a/vendor/fontstash/fontstash.odin b/vendor/fontstash/fontstash.odin new file mode 100644 index 000000000..ba19d068a --- /dev/null +++ b/vendor/fontstash/fontstash.odin @@ -0,0 +1,1227 @@ +package fontstash + +import "core:runtime" +import "core:fmt" +import "core:log" +import "core:os" +import "core:mem" +import "core:math" +import "core:unicode" +import "core:strings" +import stbtt "vendor:stb/truetype" + +// This is a port from Fontstash into odin - specialized for nanovg + +// Notable features of Fontstash: +// Contains a *single* channel texture atlas for multiple fonts +// Manages a lookup table for frequent glyphs +// Allows blurred font glyphs +// Atlas can resize + +// Changes from the original: +// stb truetype only +// no scratch allocation -> parts use odins dynamic arrays +// leaves GPU vertex creation & texture management up to the user +// texture atlas expands by default + +INVALID :: -1 +MAX_STATES :: 20 +HASH_LUT_SIZE :: 256 +INIT_GLYPHS :: 256 +INIT_ATLAS_NODES :: 256 +MAX_FALLBACKS :: 20 +Glyph_Index :: i32 // in case you want to change the handle for glyph indices + +AlignHorizontal :: enum { + LEFT, + CENTER, + RIGHT, +} + +AlignVertical :: enum { + TOP, + MIDDLE, + BOTTOM, + BASELINE, +} + +Font :: struct { + name: string, // allocated + + info: stbtt.fontinfo, + loadedData: []byte, + + ascender: f32, + descender: f32, + lineHeight: f32, + + glyphs: [dynamic]Glyph, + lut: [HASH_LUT_SIZE]int, + + fallbacks: [MAX_FALLBACKS]int, + nfallbacks: int, +} + +Glyph :: struct { + codepoint: rune, + index: Glyph_Index, + next: int, + isize: i16, + blurSize: i16, + x0, y0, x1, y1: i16, + xoff, yoff: i16, + xadvance: i16, +} + +AtlasNode :: struct { + x, y, width: i16, +} + +Vertex :: struct #packed { + x, y: f32, + u, v: f32, + color: [4]u8, +} + +QuadLocation :: enum { + TOPLEFT, + BOTTOMLEFT, +} + +FontContext :: struct { + fonts: [dynamic]Font, // allocated using context.allocator + + // always assuming user wants to resize + nodes: [dynamic]AtlasNode, + + // actual pixels + textureData: []byte, // allocated using context.allocator + width, height: int, + // 1 / texture_atlas_width, 1 / texture_atlas_height + itw, ith: f32, + + // state + states: []State, + state_count: int, // used states + + location: QuadLocation, + + // dirty rectangle of the texture region that was updated + dirtyRect: [4]f32, + + // callbacks with userData passed + userData: rawptr, // by default set to the context + + // called when a texture is expanded and needs handling + callbackResize: proc(data: rawptr, w, h: int), + // called in state_end to update the texture region that changed + callbackUpdate: proc(data: rawptr, dirtyRect: [4]f32, textureData: rawptr), +} + +Init :: proc(using ctx: ^FontContext, w, h: int, loc: QuadLocation) { + userData = ctx + location = loc + fonts = make([dynamic]Font, 0, 8) + + itw = f32(1) / f32(w) + ith = f32(1) / f32(h) + textureData = make([]byte, w * h) + + width = w + height = h + nodes = make([dynamic]AtlasNode, 0, INIT_ATLAS_NODES) + __dirtyRectReset(ctx) + + states = make([]State, MAX_STATES) + + // NOTE NECESSARY + append(&nodes, AtlasNode { + width = i16(w), + }) + + __AtlasAddWhiteRect(ctx, 2, 2) + + PushState(ctx) + ClearState(ctx) +} + +Destroy :: proc(using ctx: ^FontContext) { + for font in &fonts { + delete(font.loadedData) + delete(font.glyphs) + } + + delete(states) + delete(textureData) + delete(fonts) + delete(nodes) +} + +Reset :: proc(using ctx: ^FontContext) { + __atlasReset(ctx, width, height) + __dirtyRectReset(ctx) + mem.zero_slice(textureData) + + for font in &fonts { + __lutReset(&font) + } + + __AtlasAddWhiteRect(ctx, 2, 2) + PushState(ctx) + ClearState(ctx) +} + +__atlasInsertNode :: proc(using ctx: ^FontContext, idx, x, y, w: int) { + // resize is alright here + resize(&nodes, len(nodes) + 1) + + // shift nodes up once to leave space at idx + for i := len(nodes) - 1; i > idx; i -= 1 { + nodes[i] = nodes[i - 1] + } + + // set new inserted one to properties + nodes[idx].x = i16(x) + nodes[idx].y = i16(y) + nodes[idx].width = i16(w) +} + +__atlasRemoveNode :: proc(using ctx: ^FontContext, idx: int) { + if len(nodes) == 0 { + return + } + + // remove node at index, shift elements down + for i in idx.. width { + __atlasInsertNode(ctx, len(nodes), width, 0, w - width) + } + + width = w + height = h +} + +__atlasReset :: proc(using ctx: ^FontContext, w, h: int) { + width = w + height = h + clear(&nodes) + + // init root node + append(&nodes, AtlasNode { + width = i16(w), + }) +} + +__AtlasAddSkylineLevel :: proc(using ctx: ^FontContext, idx, x, y, w, h: int) { + // insert new node + __atlasInsertNode(ctx, idx, x, y + h, w) + + // Delete skyline segments that fall under the shadow of the new segment. + for i := idx + 1; i < len(nodes); i += 1 { + if nodes[i].x < nodes[i - 1].x + nodes[i - 1].width { + shrink := nodes[i-1].x + nodes[i-1].width - nodes[i].x + nodes[i].x += i16(shrink) + nodes[i].width -= i16(shrink) + + if nodes[i].width <= 0 { + __atlasRemoveNode(ctx, i) + i -= 1 + } else { + break + } + } else { + break + } + } + + // Merge same height skyline segments that are next to each other. + for i := 0; i < len(nodes) - 1; i += 1 { + if nodes[i].y == nodes[i + 1].y { + nodes[i].width += nodes[i + 1].width + __atlasRemoveNode(ctx, i + 1) + i -= 1 + } + } +} + +__AtlasRectFits :: proc(using ctx: ^FontContext, i, w, h: int) -> int { + // Checks if there is enough space at the location of skyline span 'i', + // and return the max height of all skyline spans under that at that location, + // (think tetris block being dropped at that position). Or -1 if no space found. + x := int(nodes[i].x) + y := int(nodes[i].y) + + if x + w > width { + return -1 + } + + i := i + space_left := w + for space_left > 0 { + if i == len(nodes) { + return -1 + } + + y = max(y, int(nodes[i].y)) + if y + h > height { + return -1 + } + + space_left -= int(nodes[i].width) + i += 1 + } + + return y +} + +__AtlasAddRect :: proc(using ctx: ^FontContext, rw, rh: int) -> (rx, ry: int, ok: bool) { + besth := height + bestw := width + besti, bestx, besty := -1, -1, -1 + + // Bottom left fit heuristic. + for i in 0.. int { + data, ok := os.read_entire_file(path) + + if !ok { + log.panicf("FONT: failed to read font at %s", path) + } + + return AddFontMem(ctx, name, data) +} + +// push a font to the font stack +// optionally init with ascii characters at a wanted size +AddFontMem :: proc( + ctx: ^FontContext, + name: string, + data: []u8, +) -> int { + append(&ctx.fonts, Font {}) + res := &ctx.fonts[len(ctx.fonts) - 1] + res.loadedData = data + res.name = strings.clone(name) + + stbtt.InitFont(&res.info, &res.loadedData[0], 0) + ascent, descent, line_gap: i32 + stbtt.GetFontVMetrics(&res.info, &ascent, &descent, &line_gap) + fh := f32(ascent - descent) + res.ascender = f32(ascent) / fh + res.descender = f32(descent) / fh + res.lineHeight = (fh + f32(line_gap)) / fh + res.glyphs = make([dynamic]Glyph, 0, INIT_GLYPHS) + + __lutReset(res) + return len(ctx.fonts) - 1 +} + +AddFont :: proc { AddFontPath, AddFontMem } + +AddFallbackFont :: proc(ctx: ^FontContext, base, fallback: int) -> bool { + base_font := __getFont(ctx, base) + + if base_font.nfallbacks < MAX_FALLBACKS { + base_font.fallbacks[base_font.nfallbacks] = fallback + base_font.nfallbacks += 1 + return true + } + + return false +} + +ResetFallbackFont :: proc(ctx: ^FontContext, base: int) { + base_font := __getFont(ctx, base) + base_font.nfallbacks = 0 + clear(&base_font.glyphs) + __lutReset(base_font) +} + +// find font by name +GetFontByName :: proc(ctx: ^FontContext, name: string) -> int { + for font, i in ctx.fonts { + if font.name == name { + return i + } + } + + return INVALID +} + +__lutReset :: proc(font: ^Font) { + // set lookup table + for i in 0.. u32 { + a := a + a += ~(a << 15) + a ~= (a >> 10) + a += (a << 3) + a ~= (a >> 6) + a += (a << 11) + a ~= (a >> 16) + return a +} + +__renderGlyphBitmap :: proc( + font: ^Font, + output: []u8, + outWidth: i32, + outHeight: i32, + outStride: i32, + scaleX: f32, + scaleY: f32, + glyphIndex: Glyph_Index, +) { + stbtt.MakeGlyphBitmap(&font.info, raw_data(output), outWidth, outHeight, outStride, scaleX, scaleY, glyphIndex) +} + +__buildGlyphBitmap :: proc( + font: ^Font, + glyphIndex: Glyph_Index, + pixelSize: f32, + scale: f32, +) -> (advance, lsb, x0, y0, x1, y1: i32) { + stbtt.GetGlyphHMetrics(&font.info, glyphIndex, &advance, &lsb) + stbtt.GetGlyphBitmapBox(&font.info, glyphIndex, scale, scale, &x0, &y0, &x1, &y1) + return +} + +// get glyph and push to atlas if not exists +__getGlyph :: proc( + ctx: ^FontContext, + font: ^Font, + codepoint: rune, + isize: i16, + blurSize: i16 = 0, +) -> (res: ^Glyph) #no_bounds_check { + if isize < 2 { + return + } + + // find code point and size + h := __hashint(u32(codepoint)) & (HASH_LUT_SIZE - 1) + i := font.lut[h] + for i != -1 { + glyph := &font.glyphs[i] + + if + glyph.codepoint == codepoint && + glyph.isize == isize && + glyph.blurSize == blurSize + { + res = glyph + return + } + + i = glyph.next + } + + // could not find glyph, create it. + render_font := font // font used to render + glyph_index := __getGlyph_index(font, codepoint) + if glyph_index == 0 { + // lookout for possible fallbacks + for i in 0.. 0 { + __blur(dst, int(gw), int(gh), ctx.width, blurSize) + } + + ctx.dirtyRect[0] = cast(f32) min(int(ctx.dirtyRect[0]), int(res.x0)) + ctx.dirtyRect[1] = cast(f32) min(int(ctx.dirtyRect[1]), int(res.y0)) + ctx.dirtyRect[2] = cast(f32) max(int(ctx.dirtyRect[2]), int(res.x1)) + ctx.dirtyRect[3] = cast(f32) max(int(ctx.dirtyRect[3]), int(res.y1)) + + return +} + +///////////////////////////////// +// blur +///////////////////////////////// + +// Based on Exponential blur, Jani Huhtanen, 2006 + +BLUR_APREC :: 16 +BLUR_ZPREC :: 7 + +__blurCols :: proc(dst: []u8, w, h, dstStride, alpha: int) { + dst := dst + + for y in 0..> BLUR_APREC + dst[x] = u8(z >> BLUR_ZPREC) + } + + dst[w - 1] = 0 // force zero border + z = 0 + + for x := w - 2; x >= 0; x -= 1 { + z += (alpha * ((int(dst[x]) << BLUR_ZPREC) - z)) >> BLUR_APREC + dst[x] = u8(z >> BLUR_ZPREC) + } + + dst[0] = 0 // force zero border + dst = dst[dstStride:] // advance slice + } +} + +__blurRows :: proc(dst: []u8, w, h, dstStride, alpha: int) { + dst := dst + + for x in 0..> BLUR_APREC + dst[y] = u8(z >> BLUR_ZPREC) + } + + dst[(h - 1) * dstStride] = 0 // force zero border + z = 0 + + for y := (h - 2) * dstStride; y >= 0; y -= dstStride { + z += (alpha * ((int(dst[y]) << BLUR_ZPREC) - z)) >> BLUR_APREC + dst[y] = u8(z >> BLUR_ZPREC) + } + + dst[0] = 0 // force zero border + dst = dst[1:] // advance + } +} + +__blur :: proc(dst: []u8, w, h, dstStride: int, blurSize: i16) { + assert(blurSize != 0) + + // Calculate the alpha such that 90% of the kernel is within the radius. (Kernel extends to infinity) + sigma := f32(blurSize) * 0.57735 // 1 / sqrt(3) + alpha := int((1 << BLUR_APREC) * (1 - math.exp(-2.3 / (sigma + 1)))) + __blurRows(dst, w, h, dstStride, alpha) + __blurCols(dst, w, h, dstStride, alpha) + __blurRows(dst, w, h, dstStride, alpha) + __blurCols(dst, w, h, dstStride, alpha) +} + +///////////////////////////////// +// Texture expansion +///////////////////////////////// + +ExpandAtlas :: proc(ctx: ^FontContext, width, height: int, allocator := context.allocator) -> bool { + width := max(ctx.width, width) + height := max(ctx.height, height) + + if width == ctx.width && height == ctx.height { + return true + } + + if ctx.callbackResize != nil { + ctx.callbackResize(ctx.userData, width, height) + } + + data := make([]byte, width * height, allocator) + + for i in 0.. ctx.width { + mem.set(&data[i * width + ctx.width], 0, width - ctx.width) + } + } + + if height > ctx.height { + mem.set(&data[ctx.height * width], 0, (height - ctx.height) * width) + } + + delete(ctx.textureData) + ctx.textureData = data + + // increase atlas size + __atlasExpand(ctx, width, height) + + // add existing data as dirty + maxy := i16(0) + for node in ctx.nodes { + maxy = max(maxy, node.y) + } + ctx.dirtyRect[0] = 0 + ctx.dirtyRect[1] = 0 + ctx.dirtyRect[2] = f32(ctx.width) + ctx.dirtyRect[3] = f32(maxy) + + ctx.width = width + ctx.height = height + ctx.itw = 1.0 / f32(width) + ctx.ith = 1.0 / f32(height) + + return true +} + +ResetAtlas :: proc(ctx: ^FontContext, width, height: int, allocator := context.allocator) -> bool { + if width == ctx.width && height == ctx.height { + // just clear + mem.zero_slice(ctx.textureData) + } else { + // realloc + ctx.textureData = make([]byte, width * height, allocator) + } + + ctx.dirtyRect[0] = f32(width) + ctx.dirtyRect[1] = f32(height) + ctx.dirtyRect[2] = 0 + ctx.dirtyRect[3] = 0 + + // reset fonts + for font in &ctx.fonts { + clear(&font.glyphs) + __lutReset(&font) + } + + ctx.width = width + ctx.height = height + ctx.itw = 1.0 / f32(width) + ctx.ith = 1.0 / f32(height) + + __AtlasAddWhiteRect(ctx, 2, 2) + return true +} + +__getGlyph_index :: proc(font: ^Font, codepoint: rune) -> Glyph_Index { + return stbtt.FindGlyphIndex(&font.info, codepoint) +} + +__getPixelHeightScale :: proc(font: ^Font, pixel_height: f32) -> f32 { + return stbtt.ScaleForPixelHeight(&font.info, pixel_height) +} + +__getGlyphKernAdvance :: proc(font: ^Font, glyph1, glyph2: Glyph_Index) -> i32 { + return stbtt.GetGlyphKernAdvance(&font.info, glyph1, glyph2) +} + +// get a font with bounds checking +__getFont :: proc(ctx: ^FontContext, index: int, loc := #caller_location) -> ^Font #no_bounds_check { + runtime.bounds_check_error_loc(loc, index, len(ctx.fonts)) + return &ctx.fonts[index] +} + +// only useful for single glyphs where you quickly want the width +CodepointWidth :: proc( + font: ^Font, + codepoint: rune, + scale: f32, +) -> f32 { + glyph_index := __getGlyph_index(font, codepoint) + xadvance, lsb: i32 + stbtt.GetGlyphHMetrics(&font.info, glyph_index, &xadvance, &lsb) + return f32(xadvance) * scale +} + +// get top and bottom line boundary +LineBounds :: proc(ctx: ^FontContext, y: f32) -> (miny, maxy: f32) { + state := __getState(ctx) + font := __getFont(ctx, state.font) + isize := i16(state.size * 10.0) + y := y + y += __getVerticalAlign(ctx, font, state.av, isize) + + if ctx.location == .TOPLEFT { + miny = y - font.ascender * f32(isize) / 10 + maxy = miny + font.lineHeight * f32(isize / 10) + } else if ctx.location == .BOTTOMLEFT { + miny = y + font.ascender * f32(isize) / 10 + maxy = miny - font.lineHeight * f32(isize / 10) + } + + return +} + +// reset dirty rect +__dirtyRectReset :: proc(using ctx: ^FontContext) { + dirtyRect[0] = f32(width) + dirtyRect[1] = f32(height) + dirtyRect[2] = 0 + dirtyRect[3] = 0 +} + +// true when the dirty rectangle is valid and needs a texture update on the gpu +ValidateTexture :: proc(using ctx: ^FontContext, dirty: ^[4]f32) -> bool { + if dirtyRect[0] < dirtyRect[2] && dirtyRect[1] < dirtyRect[3] { + dirty[0] = dirtyRect[0] + dirty[1] = dirtyRect[1] + dirty[2] = dirtyRect[2] + dirty[3] = dirtyRect[3] + __dirtyRectReset(ctx) + return true + } + + return false +} + +// get alignment based on font +__getVerticalAlign :: proc( + ctx: ^FontContext, + font: ^Font, + av: AlignVertical, + pixelSize: i16, +) -> (res: f32) { + switch ctx.location { + case .TOPLEFT: { + switch av { + case .TOP: res = font.ascender * f32(pixelSize) / 10 + case .MIDDLE: res = (font.ascender + font.descender) / 2 * f32(pixelSize) / 10 + case .BASELINE: res = 0 + case .BOTTOM: res = font.descender * f32(pixelSize) / 10 + } + } + + case .BOTTOMLEFT: { + switch av { + case .TOP: res = -font.ascender * f32(pixelSize) / 10 + case .MIDDLE: res = -(font.ascender + font.descender) / 2 * f32(pixelSize) / 10 + case .BASELINE: res = 0 + case .BOTTOM: res = -font.descender * f32(pixelSize) / 10 + } + } + } + + return +} + +@(private) +UTF8_ACCEPT :: 0 + +@(private) +UTF8_REJECT :: 1 + +@(private) +utf8d := [400]u8 { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf + 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df + 0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef + 0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff + 0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0 + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2 + 1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4 + 1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6 + 1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8 +} + +// decode codepoints from a state +@(private) +__decutf8 :: #force_inline proc(state: ^rune, codep: ^rune, b: byte) -> bool { + b := rune(b) + type := utf8d[b] + codep^ = (state^ != UTF8_ACCEPT) ? ((b & 0x3f) | (codep^ << 6)) : ((0xff >> type) & (b)) + state^ = rune(utf8d[256 + state^ * 16 + rune(type)]) + return state^ == UTF8_ACCEPT +} + +// state used to share font options +State :: struct { + font: int, + size: f32, + color: [4]u8, + spacing: f32, + blur: f32, + + ah: AlignHorizontal, + av: AlignVertical, +} + +// quad that should be used to draw from the texture atlas +Quad :: struct { + x0, y0, s0, t0: f32, + x1, y1, s1, t1: f32, +} + +// text iteration with custom settings +TextIter :: struct { + x, y, nextx, nexty, scale, spacing: f32, + isize, iblur: i16, + + font: ^Font, + previousGlyphIndex: Glyph_Index, + + // unicode iteration + utf8state: rune, // utf8 + codepoint: rune, + text: string, + codepointCount: int, + + // byte indices + str: int, + next: int, + end: int, +} + +// push a state, copies the current one over to the next one +PushState :: proc(using ctx: ^FontContext, loc := #caller_location) #no_bounds_check { + runtime.bounds_check_error_loc(loc, state_count, MAX_STATES) + + if state_count > 0 { + states[state_count] = states[state_count - 1] + } + + state_count += 1 +} + +// pop a state +PopState :: proc(using ctx: ^FontContext) { + if state_count <= 1 { + log.error("FONTSTASH: state underflow! to many pops were called") + } else { + state_count -= 1 + } +} + +// clear current state +ClearState :: proc(ctx: ^FontContext) { + state := __getState(ctx) + state.size = 12 + state.color = 255 + state.blur = 0 + state.spacing = 0 + state.font = 0 + state.ah = .LEFT + state.av = .BASELINE +} + +__getState :: #force_inline proc(ctx: ^FontContext) -> ^State #no_bounds_check { + return &ctx.states[ctx.state_count - 1] +} + +SetSize :: proc(ctx: ^FontContext, size: f32) { + __getState(ctx).size = size +} + +SetColor :: proc(ctx: ^FontContext, color: [4]u8) { + __getState(ctx).color = color +} + +SetSpacing :: proc(ctx: ^FontContext, spacing: f32) { + __getState(ctx).spacing = spacing +} + +SetBlur :: proc(ctx: ^FontContext, blur: f32) { + __getState(ctx).blur = blur +} + +SetFont :: proc(ctx: ^FontContext, font: int) { + __getState(ctx).font = font +} + +SetAH :: SetAlignHorizontal +SetAV :: SetAlignVertical + +SetAlignHorizontal :: proc(ctx: ^FontContext, ah: AlignHorizontal) { + __getState(ctx).ah = ah +} + +SetAlignVertical :: proc(ctx: ^FontContext, av: AlignVertical) { + __getState(ctx).av = av +} + +__getQuad :: proc( + ctx: ^FontContext, + font: ^Font, + + previousGlyphIndex: i32, + glyph: ^Glyph, + + scale: f32, + spacing: f32, + + x, y: ^f32, + quad: ^Quad, +) { + if previousGlyphIndex != -1 { + adv := f32(__getGlyphKernAdvance(font, previousGlyphIndex, glyph.index)) * scale + x^ += f32(int(adv + spacing + 0.5)) + } + + // fill props right + rx, ry, x0, y0, x1, y1, xoff, yoff, glyph_width, glyph_height: f32 + xoff = f32(glyph.xoff + 1) + yoff = f32(glyph.yoff + 1) + x0 = f32(glyph.x0 + 1) + y0 = f32(glyph.y0 + 1) + x1 = f32(glyph.x1 - 1) + y1 = f32(glyph.y1 - 1) + + switch ctx.location { + case .TOPLEFT: { + rx = math.floor(x^ + xoff) + ry = math.floor(y^ + yoff) + + quad.x0 = rx + quad.y0 = ry + quad.x1 = rx + x1 - x0 + quad.y1 = ry + y1 - y0 + + quad.s0 = x0 * ctx.itw + quad.t0 = y0 * ctx.ith + quad.s1 = x1 * ctx.itw + quad.t1 = y1 * ctx.ith + } + + case .BOTTOMLEFT: { + rx = math.floor(x^ + xoff) + ry = math.floor(y^ - yoff) + + quad.x0 = rx + quad.y0 = ry + quad.x1 = rx + x1 - x0 + quad.y1 = ry - y1 + y0 + + quad.s0 = x0 * ctx.itw + quad.t0 = y0 * ctx.ith + quad.s1 = x1 * ctx.itw + quad.t1 = y1 * ctx.ith + } + } + + x^ += f32(int(f32(glyph.xadvance) / 10 + 0.5)) +} + +// init text iter struct with settings +TextIterInit :: proc( + ctx: ^FontContext, + x: f32, + y: f32, + text: string, +) -> (res: TextIter) { + state := __getState(ctx) + res.font = __getFont(ctx, state.font) + res.isize = i16(f32(state.size) * 10) + res.iblur = i16(state.blur) + res.scale = __getPixelHeightScale(res.font, f32(res.isize) / 10) + + // align horizontally + x := x + y := y + switch state.ah { + case .LEFT: {} + case .CENTER: { + width := TextBounds(ctx, text, x, y, nil) + x = math.round(x - width * 0.5) + } + case .RIGHT: { + width := TextBounds(ctx, text, x, y, nil) + x -= width + } + } + + // align vertically + y = math.round(y + __getVerticalAlign(ctx, res.font, state.av, res.isize)) + + // set positions + res.x = x + res.nextx = x + res.y = y + res.nexty = y + res.previousGlyphIndex = -1 + res.spacing = state.spacing + res.text = text + + res.str = 0 + res.next = 0 + res.end = len(text) + + return +} + +// step through each codepoint +TextIterNext :: proc( + ctx: ^FontContext, + iter: ^TextIter, + quad: ^Quad, +) -> (ok: bool) { + str := iter.next + iter.str = iter.next + + for str < iter.end { + defer str += 1 + + if __decutf8(&iter.utf8state, &iter.codepoint, iter.text[str]) { + iter.x = iter.nextx + iter.y = iter.nexty + iter.codepointCount += 1 + glyph := __getGlyph(ctx, iter.font, iter.codepoint, iter.isize, iter.iblur) + + if glyph != nil { + __getQuad(ctx, iter.font, iter.previousGlyphIndex, glyph, iter.scale, iter.spacing, &iter.nextx, &iter.nexty, quad) + } + + iter.previousGlyphIndex = glyph == nil ? -1 : glyph.index + ok = true + break + } + } + + iter.next = str + return +} + +// width of a text line, optionally the full rect +TextBounds :: proc( + ctx: ^FontContext, + text: string, + x: f32 = 0, + y: f32 = 0, + bounds: ^[4]f32 = nil, +) -> f32 { + state := __getState(ctx) + isize := i16(state.size * 10) + iblur := i16(state.blur) + font := __getFont(ctx, state.font) + + // bunch of state + x := x + y := y + minx := x + maxx := x + miny := y + maxy := y + start_x := x + + // iterate + scale := __getPixelHeightScale(font, f32(isize) / 10) + previousGlyphIndex: Glyph_Index = -1 + quad: Quad + utf8state: rune + codepoint: rune + for byte_offset in 0.. maxx { + maxx = quad.x1 + } + + if ctx.location == .TOPLEFT { + if quad.y0 < miny { + miny = quad.y0 + } + if quad.y1 > maxy { + maxy = quad.y1 + } + } else if ctx.location == .BOTTOMLEFT { + if quad.y1 < miny { + miny = quad.y1 + } + if quad.y0 > maxy { + maxy = quad.y0 + } + } + } + + previousGlyphIndex = glyph == nil ? -1 : glyph.index + } + } + + // horizontal alignment + advance := x - start_x + switch state.ah { + case .LEFT: {} + case .CENTER: { + minx -= advance * 0.5 + maxx -= advance * 0.5 + } + case .RIGHT: { + minx -= advance + maxx -= advance + } + } + + if bounds != nil { + bounds^ = { minx, miny, maxx, maxy } + } + + return advance +} + +VerticalMetrics :: proc( + ctx: ^FontContext, +) -> (ascender, descender, lineHeight: f32) { + state := __getState(ctx) + isize := i16(state.size * 10.0) + font := __getFont(ctx, state.font) + ascender = font.ascender * f32(isize / 10) + descender = font.descender * f32(isize / 10) + lineHeight = font.lineHeight * f32(isize / 10) + return +} + +// reset to single state +BeginState :: proc(using ctx: ^FontContext) { + state_count = 0 + PushState(ctx) + ClearState(ctx) +} + +// checks for texture updates after potential __getGlyph calls +EndState :: proc(using ctx: ^FontContext) { + // check for texture update + if dirtyRect[0] < dirtyRect[2] && dirtyRect[1] < dirtyRect[3] { + if callbackUpdate != nil { + callbackUpdate(userData, dirtyRect, raw_data(textureData)) + } + + __dirtyRectReset(ctx) + } +} \ No newline at end of file diff --git a/vendor/nanovg/gl/frag.glsl b/vendor/nanovg/gl/frag.glsl new file mode 100644 index 000000000..423214c9b --- /dev/null +++ b/vendor/nanovg/gl/frag.glsl @@ -0,0 +1,123 @@ +#ifdef GL_ES +#if defined(GL_FRAGMENT_PRECISION_HIGH) || defined(NANOVG_GL3) + precision highp float; +#else + precision mediump float; +#endif +#endif +#ifdef NANOVG_GL3 +#ifdef USE_UNIFORMBUFFER + layout(std140) uniform frag { + mat3 scissorMat; + mat3 paintMat; + vec4 innerCol; + vec4 outerCol; + vec2 scissorExt; + vec2 scissorScale; + vec2 extent; + float radius; + float feather; + float strokeMult; + float strokeThr; + int texType; + int type; + }; +#else // NANOVG_GL3 && !USE_UNIFORMBUFFER + uniform vec4 frag[UNIFORMARRAY_SIZE]; +#endif + uniform sampler2D tex; + in vec2 ftcoord; + in vec2 fpos; + out vec4 outColor; +#else // !NANOVG_GL3 + uniform vec4 frag[UNIFORMARRAY_SIZE]; + uniform sampler2D tex; + varying vec2 ftcoord; + varying vec2 fpos; +#endif +#ifndef USE_UNIFORMBUFFER + #define scissorMat mat3(frag[0].xyz, frag[1].xyz, frag[2].xyz) + #define paintMat mat3(frag[3].xyz, frag[4].xyz, frag[5].xyz) + #define innerCol frag[6] + #define outerCol frag[7] + #define scissorExt frag[8].xy + #define scissorScale frag[8].zw + #define extent frag[9].xy + #define radius frag[9].z + #define feather frag[9].w + #define strokeMult frag[10].x + #define strokeThr frag[10].y + #define texType int(frag[10].z) + #define type int(frag[10].w) +#endif + +float sdroundrect(vec2 pt, vec2 ext, float rad) { + vec2 ext2 = ext - vec2(rad,rad); + vec2 d = abs(pt) - ext2; + return min(max(d.x,d.y),0.0) + length(max(d,0.0)) - rad; +} + +// Scissoring +float scissorMask(vec2 p) { + vec2 sc = (abs((scissorMat * vec3(p,1.0)).xy) - scissorExt); + sc = vec2(0.5,0.5) - sc * scissorScale; + return clamp(sc.x,0.0,1.0) * clamp(sc.y,0.0,1.0); +} +#ifdef EDGE_AA +// Stroke - from [0..1] to clipped pyramid, where the slope is 1px. +float strokeMask() { + return min(1.0, (1.0-abs(ftcoord.x*2.0-1.0))*strokeMult) * min(1.0, ftcoord.y); +} +#endif + +void main(void) { + vec4 result; + float scissor = scissorMask(fpos); +#ifdef EDGE_AA + float strokeAlpha = strokeMask(); + if (strokeAlpha < strokeThr) discard; +#else + float strokeAlpha = 1.0; +#endif + if (type == 0) { // Gradient + // Calculate gradient color using box gradient + vec2 pt = (paintMat * vec3(fpos,1.0)).xy; + float d = clamp((sdroundrect(pt, extent, radius) + feather*0.5) / feather, 0.0, 1.0); + vec4 color = mix(innerCol,outerCol,d); + // Combine alpha + color *= strokeAlpha * scissor; + result = color; + } else if (type == 1) { // Image + // Calculate color fron texture + vec2 pt = (paintMat * vec3(fpos,1.0)).xy / extent; +#ifdef NANOVG_GL3 + vec4 color = texture(tex, pt); +#else + vec4 color = texture2D(tex, pt); +#endif + if (texType == 1) color = vec4(color.xyz*color.w,color.w); + if (texType == 2) color = vec4(color.x); + // Apply color tint and alpha. + color *= innerCol; + // Combine alpha + color *= strokeAlpha * scissor; + result = color; + } else if (type == 2) { // Stencil fill + result = vec4(1,1,1,1); + } else if (type == 3) { // Textured tris +#ifdef NANOVG_GL3 + vec4 color = texture(tex, ftcoord); +#else + vec4 color = texture2D(tex, ftcoord); +#endif + if (texType == 1) color = vec4(color.xyz*color.w,color.w); + if (texType == 2) color = vec4(color.x); + color *= scissor; + result = color * innerCol; + } +#ifdef NANOVG_GL3 + outColor = result; +#else + gl_FragColor = result; +#endif +} \ No newline at end of file diff --git a/vendor/nanovg/gl/gl.odin b/vendor/nanovg/gl/gl.odin new file mode 100644 index 000000000..1f4d8ceb0 --- /dev/null +++ b/vendor/nanovg/gl/gl.odin @@ -0,0 +1,1452 @@ +package nanovg_gl + +import "core:log" +import "core:strings" +import "core:mem" +import "core:math" +import "core:fmt" +import gl "vendor:OpenGL" +import nvg "../../nanovg" + +Color :: nvg.Color +Vertex :: nvg.Vertex +ImageFlags :: nvg.ImageFlags +TextureType :: nvg.Texture +Paint :: nvg.Paint +ScissorT :: nvg.ScissorT + +CreateFlag :: enum { + // Flag indicating if geometry based anti-aliasing is used (may not be needed when using MSAA). + ANTI_ALIAS, + // Flag indicating if strokes should be drawn using stencil buffer. The rendering will be a little + // slower, but path overlaps (i.e. self-intersecting or sharp turns) will be drawn just once. + STENCIL_STROKES, + // additional debug checks + DEBUG, +} +CreateFlags :: bit_set[CreateFlag] + +FRAG_BINDING :: 0 +USE_STATE_FILTER :: true +GL_UNIFORMARRAY_SIZE :: 11 + +UniformLoc :: enum { + VIEW_SIZE, + TEX, + FRAG, +} + +ShaderType :: enum i32 { + FILL_GRAD, + FILL_IMG, + SIMPLE, + IMG, +} + +Shader :: struct { + prog: u32, + frag: u32, + vert: u32, + loc: [UniformLoc]i32, +} + +Texture :: struct { + id: int, + tex: u32, + width, height: int, + type: TextureType, + flags: ImageFlags, +} + +Blend :: struct { + src_RGB: u32, + dst_RGB: u32, + src_alpha: u32, + dst_alpha: u32, +} + +CallType :: enum { + NONE, + FILL, + CONVEX_FILL, + STROKE, + TRIANGLES, +} + +Call :: struct { + type: CallType, + image: int, + pathOffset: int, + pathCount: int, + triangleOffset: int, + triangleCount: int, + uniformOffset: int, + blendFunc: Blend, +} + +Path :: struct { + fillOffset: int, + fillCount: int, + strokeOffset: int, + strokeCount: int, +} + +when GL2_IMPLEMENTATION { + FragUniforms :: struct #raw_union { + using _: struct { + scissorMat: [12]f32, // matrices are actually 3 vec4s + paintMat: [12]f32, + innerColor: Color, + outerColor: Color, + scissorExt: [2]f32, + scissorScale: [2]f32, + extent: [2]f32, + radius: f32, + feather: f32, + strokeMult: f32, + strokeThr: f32, + texType: i32, + type: ShaderType, + }, + uniform_array: [GL_UNIFORMARRAY_SIZE][4]f32, + } +} else { + FragUniforms :: struct #packed { + scissorMat: [12]f32, // matrices are actually 3 vec4s + paintMat: [12]f32, + innerColor: Color, + outerColor: Color, + scissorExt: [2]f32, + scissorScale: [2]f32, + extent: [2]f32, + radius: f32, + feather: f32, + strokeMult: f32, + strokeThr: f32, + texType: i32, + type: ShaderType, + } +} + +GL2_IMPLEMENTATION :: false +GL3_IMPLEMENTATION :: true +GLES2_IMPLEMENTATION :: false +GLES3_IMPLEMENTATION :: false + +when GL2_IMPLEMENTATION { + GL2 :: true + GL3 :: false + GLES2 :: false + GLES3 :: false + GL_IMPLEMENTATION :: true + GL_USE_UNIFORMBUFFER :: false +} else when GL3_IMPLEMENTATION { + GL2 :: false + GL3 :: true + GLES2 :: false + GLES3 :: false + GL_IMPLEMENTATION :: true + GL_USE_UNIFORMBUFFER :: true +} else when GLES2_IMPLEMENTATION { + GL2 :: false + GL3 :: false + GLES2 :: true + GLES3 :: false + GL_IMPLEMENTATION :: true + GL_USE_UNIFORMBUFFER :: false +} else when GLES3_IMPLEMENTATION { + GL2 :: false + GL3 :: false + GLES2 :: false + GLES3 :: true + GL_IMPLEMENTATION :: true + GL_USE_UNIFORMBUFFER :: false +} + +Context :: struct { + shader: Shader, + textures: [dynamic]Texture, + view: [2]f32, + textureId: int, + + vertBuf: u32, + vertArr: u32, // GL3 + fragBuf: u32, // USE_UNIFORMBUFFER + fragSize: int, + flags: CreateFlags, + + // Per frame buffers + calls: [dynamic]Call, + paths: [dynamic]Path, + verts: [dynamic]Vertex, + uniforms: [dynamic]byte, + + // cached state used for state filter + boundTexture: u32, + stencilMask: u32, + stencilFunc: u32, + stencilFuncRef: i32, + stencilFuncMask: u32, + blendFunc: Blend, + + dummyTex: int, +} + +__nearestPow2 :: proc(num: uint) -> uint { + n := num > 0 ? num - 1 : 0 + n |= n >> 1 + n |= n >> 2 + n |= n >> 4 + n |= n >> 8 + n |= n >> 16 + n += 1 + return n +} + +__bindTexture :: proc(ctx: ^Context, tex: u32) { + when USE_STATE_FILTER { + if ctx.boundTexture != tex { + ctx.boundTexture = tex + gl.BindTexture(gl.TEXTURE_2D, tex) + } + } else { + gl.BindTexture(gl.TEXTURE_2D, tex) + } +} + +__stencilMask :: proc(ctx: ^Context, mask: u32) { + when USE_STATE_FILTER { + if ctx.stencilMask != mask { + ctx.stencilMask = mask + gl.StencilMask(mask) + } + } else { + gl.StencilMask(mask) + } +} + +__stencilFunc :: proc(ctx: ^Context, func: u32, ref: i32, mask: u32) { + when USE_STATE_FILTER { + if ctx.stencilFunc != func || + ctx.stencilFuncRef != ref || + ctx.stencilFuncMask != mask { + ctx.stencilFunc = func + ctx.stencilFuncRef = ref + ctx.stencilFuncMask = mask + gl.StencilFunc(func, ref, mask) + } + } else { + gl.StencilFunc(func, ref, mask) + } +} + +__blendFuncSeparate :: proc(ctx: ^Context, blend: ^Blend) { + when USE_STATE_FILTER { + if ctx.blendFunc != blend^ { + ctx.blendFunc = blend^ + gl.BlendFuncSeparate(blend.src_RGB, blend.dst_RGB, blend.src_alpha, blend.dst_alpha) + } + } else { + gl.BlendFuncSeparate(blend.src_RGB, blend.dst_RGB, blend.src_alpha, blend.dst_alpha) + } +} + +__allocTexture :: proc(ctx: ^Context) -> (tex: ^Texture) { + for texture in &ctx.textures { + if texture.id == 0 { + tex = &texture + break + } + } + + if tex == nil { + append(&ctx.textures, Texture {}) + tex = &ctx.textures[len(ctx.textures) - 1] + } + + tex^ = {} + ctx.textureId += 1 + tex.id = ctx.textureId + + return +} + +__findTexture :: proc(ctx: ^Context, id: int) -> ^Texture { + for texture in &ctx.textures { + if texture.id == id { + return &texture + } + } + + return nil +} + +__deleteTexture :: proc(ctx: ^Context, id: int) -> bool { + for texture, i in &ctx.textures { + if texture.id == id { + if texture.tex != 0 && (.NO_DELETE not_in texture.flags) { + gl.DeleteTextures(1, &texture.tex) + } + + ctx.textures[i] = {} + return true + } + } + + return false +} + +__deleteShader :: proc(shader: ^Shader) { + if shader.prog != 0 { + gl.DeleteProgram(shader.prog) + } + + if shader.vert != 0 { + gl.DeleteShader(shader.vert) + } + + if shader.frag != 0 { + gl.DeleteShader(shader.frag) + } +} + +__getUniforms :: proc(shader: ^Shader) { + shader.loc[.VIEW_SIZE] = gl.GetUniformLocation(shader.prog, "viewSize") + shader.loc[.TEX] = gl.GetUniformLocation(shader.prog, "tex") + + when GL_USE_UNIFORMBUFFER { + shader.loc[.FRAG] = i32(gl.GetUniformBlockIndex(shader.prog, "frag")) + } else { + shader.loc[.FRAG] = gl.GetUniformLocation(shader.prog, "frag") + } +} + +vert_shader := #load("vert.glsl") +frag_shader := #load("frag.glsl") + +__renderCreate :: proc(uptr: rawptr) -> bool { + ctx := cast(^Context) uptr + + // just build the string at runtime + builder := strings.builder_make(0, 512, context.temp_allocator) + + when GL2 { + strings.write_string(&builder, "#define NANOVG_GL2 1\n") + } else when GL3 { + strings.write_string(&builder, "#version 150 core\n#define NANOVG_GL3 1\n") + } else when GLES2 { + strings.write_string(&builder, "#version 100\n#define NANOVG_GL2 1\n") + } else when GLES3 { + strings.write_string(&builder, "#version 300 es\n#define NANOVG_GL3 1\n") + } + + when GL_USE_UNIFORMBUFFER { + strings.write_string(&builder, "#define USE_UNIFORMBUFFER 1\n") + } else { + strings.write_string(&builder, "#define UNIFORMARRAY_SIZE 11\n") + } + + __checkError(ctx, "init") + + shader_header := strings.to_string(builder) + anti: string = .ANTI_ALIAS in ctx.flags ? "#define EDGE_AA 1\n" : " " + if !__createShader( + &ctx.shader, + shader_header, + anti, + string(vert_shader), + string(frag_shader), + ) { + return false + } + + __checkError(ctx, "uniform locations") + __getUniforms(&ctx.shader) + + when GL3 { + gl.GenVertexArrays(1, &ctx.vertArr) + } + + gl.GenBuffers(1, &ctx.vertBuf) + align := i32(4) + + when GL_USE_UNIFORMBUFFER { + // Create UBOs + gl.UniformBlockBinding(ctx.shader.prog, u32(ctx.shader.loc[.FRAG]), FRAG_BINDING) + gl.GenBuffers(1, &ctx.fragBuf) + gl.GetIntegerv(gl.UNIFORM_BUFFER_OFFSET_ALIGNMENT, &align) + } + + ctx.fragSize = int(size_of(FragUniforms) + align - size_of(FragUniforms) % align) + // ctx.fragSize = size_of(FragUniforms) + ctx.dummyTex = __renderCreateTexture(ctx, .Alpha, 1, 1, {}, nil) + + __checkError(ctx, "create done") + + gl.Finish() + + return true +} + +__renderCreateTexture :: proc( + uptr: rawptr, + type: TextureType, + w, h: int, + imageFlags: ImageFlags, + data: []byte, +) -> int { + ctx := cast(^Context) uptr + tex := __allocTexture(ctx) + imageFlags := imageFlags + + if tex == nil { + return 0 + } + + when GLES2 { + if __nearestPow2(uint(w)) != uint(w) || __nearestPow2(uint(h)) != uint(h) { + // No repeat + if (.REPEAT_X in imageFlags) || (.REPEAT_Y in imageFlags) { + log.errorf("Repeat X/Y is not supported for non power-of-two textures (%d x %d)\n", w, h) + excl(&imageFlags, ImageFlags { .REPEAT_X, .REPEAT_Y }) + } + + // No mips. + if .GENERATE_MIPMAPS in imageFlags { + log.errorf("Mip-maps is not support for non power-of-two textures (%d x %d)\n", w, h); + excl(&imageFlags, nvg.Image_Flag.GENERATE_MIPMAPS) + } + } + } + + gl.GenTextures(1, &tex.tex) + tex.width = w + tex.height = h + tex.type = type + tex.flags = imageFlags + __bindTexture(ctx, tex.tex) + + gl.PixelStorei(gl.UNPACK_ALIGNMENT,1) + + when GLES2 { + gl.PixelStorei(gl.UNPACK_ROW_LENGTH, i32(tex.width)) + gl.PixelStorei(gl.UNPACK_SKIP_PIXELS, 0) + gl.PixelStorei(gl.UNPACK_SKIP_ROWS, 0) + } + + when GL2 { + if .GENERATE_MIPMAPS in imageFlags { + gl.TexParameteri(gl.TEXTURE_2D, GENERATE_MIPMAP, 1) + } + } + + if type == .RGBA { + gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, i32(w), i32(h), 0, gl.RGBA, gl.UNSIGNED_BYTE, raw_data(data)) + } else { + when GLES2 || GL2 { + // TODO missing in odin + gl.TexImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w, h, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data) + } else when GLES3 { + gl.TexImage2D(gl.TEXTURE_2D, 0, gl.R8, i32(w), i32(h), 0, gl.RED, gl.UNSIGNED_BYTE, raw_data(data)) + } else { + gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, i32(w), i32(h), 0, gl.RED, gl.UNSIGNED_BYTE, raw_data(data)) + } + } + + if .GENERATE_MIPMAPS in imageFlags { + if .NEAREST in imageFlags { + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST) + } else { + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR) + } + } else { + if .NEAREST in imageFlags { + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) + } else { + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + } + } + + if .NEAREST in imageFlags { + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) + } else { + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + } + + if .REPEAT_X in imageFlags { + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT) + } else { + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + } + + if .REPEAT_Y in imageFlags { + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT) + } else { + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + } + + gl.PixelStorei(gl.UNPACK_ALIGNMENT, 4) + + when GLES2 { + gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0) + gl.PixelStorei(gl.UNPACK_SKIP_PIXELS, 0) + gl.PixelStorei(gl.UNPACK_SKIP_ROWS, 0) + } + + // The new way to build mipmaps on GLES and GL3 + when !GL2 { + if .GENERATE_MIPMAPS in imageFlags { + gl.GenerateMipmap(gl.TEXTURE_2D) + } + } + + __checkError(ctx, "create tex") + __bindTexture(ctx, 0) + + return tex.id +} + +__checkError :: proc(ctx: ^Context, str: string) { + if .DEBUG in ctx.flags { + err := gl.GetError() + + if err != gl.NO_ERROR { + log.errorf("FOUND ERROR %08x:\n\t%s\n", err, str) + } + } +} + +__checkProgramError :: proc(prog: u32) { + status: i32 + gl.GetProgramiv(prog, gl.LINK_STATUS, &status) + length: i32 + gl.GetProgramiv(prog, gl.INFO_LOG_LENGTH, &length) + + if status == 0 { + temp := make([]byte, length) + defer delete(temp) + + gl.GetProgramInfoLog(prog, length, nil, raw_data(temp)) + log.errorf("Program Error:\n%s\n", string(temp[:length])) + } +} + +__checkShaderError :: proc(shader: u32, type: string) { + status: i32 + gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status) + length: i32 + gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &length) + + if status == 0 { + temp := make([]byte, length) + defer delete(temp) + + gl.GetShaderInfoLog(shader, length, nil, raw_data(temp)) + log.errorf("Shader error:\n%s\n", string(temp[:length])) + } +} + +// TODO good case for or_return +__createShader :: proc( + shader: ^Shader, + header: string, + opts: string, + vshader: string, + fshader: string, +) -> bool { + shader^ = {} + str: [3]cstring + lengths: [3]i32 + str[0] = cstring(raw_data(header)) + str[1] = cstring(raw_data(opts)) + + lengths[0] = i32(len(header)) + lengths[1] = i32(len(opts)) + + prog := gl.CreateProgram() + vert := gl.CreateShader(gl.VERTEX_SHADER) + frag := gl.CreateShader(gl.FRAGMENT_SHADER) + + // vert shader + str[2] = cstring(raw_data(vshader)) + lengths[2] = i32(len(vshader)) + gl.ShaderSource(vert, 3, &str[0], &lengths[0]) + gl.CompileShader(vert) + __checkShaderError(vert, "vert") + + // fragment shader + str[2] = cstring(raw_data(fshader)) + lengths[2] = i32(len(fshader)) + gl.ShaderSource(frag, 3, &str[0], &lengths[0]) + gl.CompileShader(frag) + __checkShaderError(frag, "frag") + + gl.AttachShader(prog, vert) + gl.AttachShader(prog, frag) + + gl.BindAttribLocation(prog, 0, "vertex") + gl.BindAttribLocation(prog, 1, "tcoord") + + gl.LinkProgram(prog) + __checkProgramError(prog) + + shader.prog = prog + shader.vert = vert + shader.frag = frag + return true +} + +__renderDeleteTexture :: proc(uptr: rawptr, image: int) -> bool { + ctx := cast(^Context) uptr + return __deleteTexture(ctx, image) +} + +__renderUpdateTexture :: proc( + uptr: rawptr, + image: int, + x, y: int, + w, h: int, + data: []byte, +) -> bool { + ctx := cast(^Context) uptr + tex := __findTexture(ctx, image) + + if tex == nil { + return false + } + + __bindTexture(ctx, tex.tex) + + gl.PixelStorei(gl.UNPACK_ALIGNMENT,1) + + x := x + w := w + data := data + + when GLES2 { + gl.PixelStorei(gl.UNPACK_ROW_LENGTH, i32(tex.width)) + gl.PixelStorei(gl.UNPACK_SKIP_PIXELS, i32(x)) + gl.PixelStorei(gl.UNPACK_SKIP_ROWS, i32(y)) + } else { + // No support for all of skip, need to update a whole row at a time. + if tex.type == .RGBA { + data = data[y * tex.width * 4:] + } else { + data = data[y * tex.width:] + } + + x = 0 + w = tex.width + } + + if tex.type == .RGBA { + gl.TexSubImage2D(gl.TEXTURE_2D, 0, i32(x), i32(y), i32(w), i32(h), gl.RGBA, gl.UNSIGNED_BYTE, raw_data(data)) + } else { + when GLES2 || GL2 { + // TODO is missing in odin + // gl.TexSubImage2D(gl.TEXTURE_2D, 0, i32(x), i32(y), i32(w), i32(h), gl.LUMINANCE, gl.UNSIGNED_BYTE, raw_data(data)) + } else { + gl.TexSubImage2D(gl.TEXTURE_2D, 0, i32(x), i32(y), i32(w), i32(h), gl.RED, gl.UNSIGNED_BYTE, raw_data(data)) + } + } + + gl.PixelStorei(gl.UNPACK_ALIGNMENT, 4) + + when GLES2 { + gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0) + gl.PixelStorei(gl.UNPACK_SKIP_PIXELS, 0) + gl.PixelStorei(gl.UNPACK_SKIP_ROWS, 0) + } + + __bindTexture(ctx, 0) + + return true +} + +__renderGetTextureSize :: proc(uptr: rawptr, image: int, w, h: ^int) -> bool { + ctx := cast(^Context) uptr + tex := __findTexture(ctx, image) + + if tex == nil { + return false + } + + w^ = tex.width + h^ = tex.height + return true +} + +__xformToMat3x4 :: proc(m3: ^[12]f32, t: [6]f32) { + m3[0] = t[0] + m3[1] = t[1] + m3[2] = 0 + m3[3] = 0 + m3[4] = t[2] + m3[5] = t[3] + m3[6] = 0 + m3[7] = 0 + m3[8] = t[4] + m3[9] = t[5] + m3[10] = 1 + m3[11] = 0 +} + +__premulColor :: proc(c: Color) -> (res: Color) { + res = c + res.r *= c.a + res.g *= c.a + res.b *= c.a + return +} + +__convertPaint :: proc( + ctx: ^Context, + frag: ^FragUniforms, + paint: ^Paint, + scissor: ^ScissorT, + width: f32, + fringe: f32, + strokeThr: f32, +) -> bool { + invxform: [6]f32 + frag^ = {} + frag.innerColor = __premulColor(paint.innerColor) + frag.outerColor = __premulColor(paint.outerColor) + + if scissor.extent[0] < -0.5 || scissor.extent[1] < -0.5 { + frag.scissorMat = {} + frag.scissorExt[0] = 1.0 + frag.scissorExt[1] = 1.0 + frag.scissorScale[0] = 1.0 + frag.scissorScale[1] = 1.0 + } else { + nvg.TransformInverse(&invxform, scissor.xform) + __xformToMat3x4(&frag.scissorMat, invxform) + frag.scissorExt[0] = scissor.extent[0] + frag.scissorExt[1] = scissor.extent[1] + frag.scissorScale[0] = math.sqrt(scissor.xform[0]*scissor.xform[0] + scissor.xform[2]*scissor.xform[2]) / fringe + frag.scissorScale[1] = math.sqrt(scissor.xform[1]*scissor.xform[1] + scissor.xform[3]*scissor.xform[3]) / fringe + } + + frag.extent = paint.extent + frag.strokeMult = (width * 0.5 + fringe * 0.5) / fringe + frag.strokeThr = strokeThr + + if paint.image != 0 { + tex := __findTexture(ctx, paint.image) + + if tex == nil { + return false + } + + // TODO maybe inversed? + if .FLIP_Y in tex.flags { + m1: [6]f32 + m2: [6]f32 + nvg.TransformTranslate(&m1, 0.0, frag.extent[1] * 0.5) + nvg.TransformMultiply(&m1, paint.xform) + nvg.TransformScale(&m2, 1.0, -1.0) + nvg.TransformMultiply(&m2, m1) + nvg.TransformTranslate(&m1, 0.0, -frag.extent[1] * 0.5) + nvg.TransformMultiply(&m1, m2) + nvg.TransformInverse(&invxform, m1) + } else { + nvg.TransformInverse(&invxform, paint.xform) + } + + frag.type = .FILL_IMG + + when GL_USE_UNIFORMBUFFER { + if tex.type == .RGBA { + frag.texType = (.PREMULTIPLIED in tex.flags) ? 0 : 1 + } else { + frag.texType = 2 + } + } else { + if tex.type == .RGBA { + frag.texType = (.PREMULTIPLIED in tex.flags) ? 0.0 : 1.0 + } else { + frag.texType = 2.0 + } + } + } else { + frag.type = .FILL_GRAD + frag.radius = paint.radius + frag.feather = paint.feather + nvg.TransformInverse(&invxform, paint.xform) + } + + __xformToMat3x4(&frag.paintMat, invxform) + + return true +} + +__setUniforms :: proc(ctx: ^Context, uniformOffset: int, image: int) { + when GL_USE_UNIFORMBUFFER { + gl.BindBufferRange(gl.UNIFORM_BUFFER, FRAG_BINDING, ctx.fragBuf, uniformOffset, size_of(FragUniforms)) + } else { + frag := __fragUniformPtr(ctx, uniformOffset) + gl.Uniform4fv(ctx.shader.loc[.FRAG], GL_UNIFORMARRAY_SIZE, cast(^f32) frag) + } + + __checkError(ctx, "uniform4") + + tex: ^Texture + if image != 0 { + tex = __findTexture(ctx, image) + } + + // If no image is set, use empty texture + if tex == nil { + tex = __findTexture(ctx, ctx.dummyTex) + } + + __bindTexture(ctx, tex != nil ? tex.tex : 0) + __checkError(ctx, "tex paint tex") +} + +__renderViewport :: proc(uptr: rawptr, width, height, devicePixelRatio: f32) { + ctx := cast(^Context) uptr + ctx.view[0] = width + ctx.view[1] = height +} + +__fill :: proc(ctx: ^Context, call: ^Call) { + paths := ctx.paths[call.pathOffset:] + + // Draw shapes + gl.Enable(gl.STENCIL_TEST) + __stencilMask(ctx, 0xff) + __stencilFunc(ctx, gl.ALWAYS, 0, 0xff) + gl.ColorMask(gl.FALSE, gl.FALSE, gl.FALSE, gl.FALSE) + + // set bindpoint for solid loc + __setUniforms(ctx, call.uniformOffset, 0) + __checkError(ctx, "fill simple") + + gl.StencilOpSeparate(gl.FRONT, gl.KEEP, gl.KEEP, gl.INCR_WRAP) + gl.StencilOpSeparate(gl.BACK, gl.KEEP, gl.KEEP, gl.DECR_WRAP) + gl.Disable(gl.CULL_FACE) + for i in 0.. 0 { + gl.DrawArrays(gl.TRIANGLE_STRIP, i32(paths[i].strokeOffset), i32(paths[i].strokeCount)) + } + } +} + +__stroke :: proc(ctx: ^Context, call: ^Call) { + paths := ctx.paths[call.pathOffset:] + + if .STENCIL_STROKES in ctx.flags { + gl.Enable(gl.STENCIL_TEST) + __stencilMask(ctx, 0xff) + + // Fill the stroke base without overlap + __stencilFunc(ctx, gl.EQUAL, 0x0, 0xff) + gl.StencilOp(gl.KEEP, gl.KEEP, gl.INCR) + __setUniforms(ctx, call.uniformOffset + ctx.fragSize, call.image) + __checkError(ctx, "stroke fill 0") + + for i in 0.. Blend { + table := BLEND_FACTOR_TABLE + blend := Blend { + table[op.srcRGB], + table[op.dstRGB], + table[op.srcAlpha], + table[op.dstAlpha], + } + return blend +} + +__renderFlush :: proc(uptr: rawptr) { + ctx := cast(^Context) uptr + + if len(ctx.calls) > 0 { + // Setup require GL state. + gl.UseProgram(ctx.shader.prog) + + gl.Enable(gl.CULL_FACE) + gl.CullFace(gl.BACK) + gl.FrontFace(gl.CCW) + gl.Enable(gl.BLEND) + gl.Disable(gl.DEPTH_TEST) + gl.Disable(gl.SCISSOR_TEST) + gl.ColorMask(gl.TRUE, gl.TRUE, gl.TRUE, gl.TRUE) + gl.StencilMask(0xffffffff) + gl.StencilOp(gl.KEEP, gl.KEEP, gl.KEEP) + gl.StencilFunc(gl.ALWAYS, 0, 0xffffffff) + gl.ActiveTexture(gl.TEXTURE0) + gl.BindTexture(gl.TEXTURE_2D, 0) + + when USE_STATE_FILTER { + ctx.boundTexture = 0 + ctx.stencilMask = 0xffffffff + ctx.stencilFunc = gl.ALWAYS + ctx.stencilFuncRef = 0 + ctx.stencilFuncMask = 0xffffffff + ctx.blendFunc.src_RGB = gl.INVALID_ENUM + ctx.blendFunc.src_alpha = gl.INVALID_ENUM + ctx.blendFunc.dst_RGB = gl.INVALID_ENUM + ctx.blendFunc.dst_alpha = gl.INVALID_ENUM + } + + when GL_USE_UNIFORMBUFFER { + // Upload ubo for frag shaders + gl.BindBuffer(gl.UNIFORM_BUFFER, ctx.fragBuf) + gl.BufferData(gl.UNIFORM_BUFFER, len(ctx.uniforms), raw_data(ctx.uniforms), gl.STREAM_DRAW) + } + + // Upload vertex data + when GL3 { + gl.BindVertexArray(ctx.vertArr) + } + + gl.BindBuffer(gl.ARRAY_BUFFER, ctx.vertBuf) + gl.BufferData(gl.ARRAY_BUFFER, len(ctx.verts) * size_of(Vertex), raw_data(ctx.verts), gl.STREAM_DRAW) + gl.EnableVertexAttribArray(0) + gl.EnableVertexAttribArray(1) + gl.VertexAttribPointer(0, 2, gl.FLOAT, gl.FALSE, size_of(Vertex), 0) + gl.VertexAttribPointer(1, 2, gl.FLOAT, gl.FALSE, size_of(Vertex), 2 * size_of(f32)) + + // Set view and texture just once per frame. + gl.Uniform1i(ctx.shader.loc[.TEX], 0) + gl.Uniform2fv(ctx.shader.loc[.VIEW_SIZE], 1, &ctx.view[0]) + + when GL_USE_UNIFORMBUFFER { + gl.BindBuffer(gl.UNIFORM_BUFFER, ctx.fragBuf) + } + + for i in 0.. (count: int) { + for i in 0.. ^Call { + append(&ctx.calls, Call {}) + return &ctx.calls[len(ctx.calls) - 1] +} + +// alloc paths and return the original start position +__allocPaths :: proc(ctx: ^Context, count: int) -> int { + old := len(ctx.paths) + resize(&ctx.paths, len(ctx.paths) + count) + return old +} + +// alloc verts and return the original start position +__allocVerts :: proc(ctx: ^Context, count: int) -> int { + old := len(ctx.verts) + resize(&ctx.verts, len(ctx.verts) + count) + return old +} + +// alloc uniforms and return the original start position +__allocFragUniforms :: proc(ctx: ^Context, count: int) -> int { + ret := len(ctx.uniforms) + resize(&ctx.uniforms, len(ctx.uniforms) + count * ctx.fragSize) + return ret +} + +// get frag uniforms from byte slice offset +__fragUniformPtr :: proc(ctx: ^Context, offset: int) -> ^FragUniforms { + return cast(^FragUniforms) &ctx.uniforms[offset] +} + +/////////////////////////////////////////////////////////// +// CALLBACKS +/////////////////////////////////////////////////////////// + +__renderFill :: proc( + uptr: rawptr, + paint: ^nvg.Paint, + compositeOperation: nvg.CompositeOperationState, + scissor: ^ScissorT, + fringe: f32, + bounds: [4]f32, + paths: []nvg.Path, +) { + ctx := cast(^Context) uptr + call := __allocCall(ctx) + + call.type = .FILL + call.triangleCount = 4 + call.pathOffset = __allocPaths(ctx, len(paths)) + call.pathCount = len(paths) + call.image = paint.image + call.blendFunc = __blendCompositeOperation(compositeOperation) + + if len(paths) == 1 && paths[0].convex { + call.type = .CONVEX_FILL + call.triangleCount = 0 + } + + // allocate vertices for all the paths + maxverts := __maxVertCount(paths) + call.triangleCount + offset := __allocVerts(ctx, maxverts) + + for i in 0.. 0 { + copy.fillOffset = offset + copy.fillCount = len(path.fill) + mem.copy(&ctx.verts[offset], &path.fill[0], size_of(Vertex) * len(path.fill)) + offset += len(path.fill) + } + + if len(path.stroke) > 0 { + copy.strokeOffset = offset + copy.strokeCount = len(path.stroke) + mem.copy(&ctx.verts[offset], &path.stroke[0], size_of(Vertex) * len(path.stroke)) + offset += len(path.stroke) + } + } + + // setup uniforms for draw calls + if call.type == .FILL { + // quad + call.triangleOffset = offset + quad := ctx.verts[call.triangleOffset:call.triangleOffset+4] + quad[0] = { bounds[2], bounds[3], 0.5, 1 } + quad[1] = { bounds[2], bounds[1], 0.5, 1 } + quad[2] = { bounds[0], bounds[3], 0.5, 1 } + quad[3] = { bounds[0], bounds[1], 0.5, 1 } + + // simple shader for stencil + call.uniformOffset = __allocFragUniforms(ctx, 2) + frag := __fragUniformPtr(ctx, call.uniformOffset) + frag^ = {} + frag.strokeThr = -1 + frag.type = .SIMPLE + + // fill shader + __convertPaint( + ctx, + __fragUniformPtr(ctx, call.uniformOffset + ctx.fragSize), + paint, + scissor, + fringe, + fringe, + -1, + ) + } else { + call.uniformOffset = __allocFragUniforms(ctx, 1) + // fill shader + __convertPaint( + ctx, + __fragUniformPtr(ctx, call.uniformOffset), + paint, + scissor, + fringe, + fringe, + -1, + ) + } +} + +__renderStroke :: proc( + uptr: rawptr, + paint: ^Paint, + compositeOperation: nvg.CompositeOperationState, + scissor: ^ScissorT, + fringe: f32, + strokeWidth: f32, + paths: []nvg.Path, +) { + ctx := cast(^Context) uptr + call := __allocCall(ctx) + + call.type = .STROKE + call.pathOffset = __allocPaths(ctx, len(paths)) + call.pathCount = len(paths) + call.image = paint.image + call.blendFunc = __blendCompositeOperation(compositeOperation) + + // allocate vertices for all the paths + maxverts := __maxVertCount(paths) + offset := __allocVerts(ctx, maxverts) + + for i in 0.. ^nvg.Context { + ctx := new(Context) + params: nvg.Params + params.renderCreate = __renderCreate + params.renderCreateTexture = __renderCreateTexture + params.renderDeleteTexture = __renderDeleteTexture + params.renderUpdateTexture = __renderUpdateTexture + params.renderGetTextureSize = __renderGetTextureSize + params.renderViewport = __renderViewport + params.renderCancel = __renderCancel + params.renderFlush = __renderFlush + params.renderFill = __renderFill + params.renderStroke = __renderStroke + params.renderTriangles = __renderTriangles + params.renderDelete = __renderDelete + params.userPtr = ctx + params.edgeAntiAlias = (.ANTI_ALIAS in flags) + ctx.flags = flags + return nvg.CreateInternal(params) +} + +Destroy :: proc(ctx: ^nvg.Context) { + nvg.DeleteInternal(ctx) +} + +CreateImageFromHandle :: proc(ctx: ^nvg.Context, textureId: u32, w, h: int, imageFlags: ImageFlags) -> int { + gctx := cast(^Context) ctx.params.userPtr + tex := __allocTexture(gctx) + tex.type = .RGBA + tex.tex = textureId + tex.flags = imageFlags + tex.width = w + tex.height = h + return tex.id +} + +ImageHandle :: proc(ctx: ^nvg.Context, textureId: int) -> u32 { + gctx := cast(^Context) ctx.params.userPtr + tex := __findTexture(gctx, textureId) + return tex.tex +} + +// framebuffer additional + +framebuffer :: struct { + ctx: ^nvg.Context, + fbo: u32, + rbo: u32, + texture: u32, + image: int, +} + +DEFAULT_FBO :: 100_000 +defaultFBO := i32(DEFAULT_FBO) + +// helper function to create GL frame buffer to render to +BindFramebuffer :: proc(fb: ^framebuffer) { + if defaultFBO == DEFAULT_FBO { + gl.GetIntegerv(gl.FRAMEBUFFER_BINDING, &defaultFBO) + } + gl.BindFramebuffer(gl.FRAMEBUFFER, fb != nil ? fb.fbo : u32(defaultFBO)) +} + +CreateFramebuffer :: proc(ctx: ^nvg.Context, w, h: int, imageFlags: ImageFlags) -> (fb: framebuffer) { + defaultFBO: i32 + defaultRBO: i32 + gl.GetIntegerv(gl.FRAMEBUFFER_BINDING, &defaultFBO) + gl.GetIntegerv(gl.RENDERBUFFER_BINDING, &defaultRBO) + + imageFlags := imageFlags + incl(&imageFlags, ImageFlags { .FLIP_Y, .PREMULTIPLIED }) + fb.image = nvg.CreateImageRGBA(ctx, w, h, imageFlags, nil) + fb.texture = ImageHandle(ctx, fb.image) + fb.ctx = ctx + + // frame buffer object + gl.GenFramebuffers(1, &fb.fbo) + gl.BindFramebuffer(gl.FRAMEBUFFER, fb.fbo) + + // render buffer object + gl.GenRenderbuffers(1, &fb.rbo) + gl.BindRenderbuffer(gl.RENDERBUFFER, fb.rbo) + gl.RenderbufferStorage(gl.RENDERBUFFER, gl.STENCIL_INDEX8, i32(w), i32(h)) + + // combine all + gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, fb.texture, 0) + gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, fb.rbo) + + if gl.CheckFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE { +// #ifdef gl.DEPTH24_STENCIL8 + // If gl.STENCIL_INDEX8 is not supported, try gl.DEPTH24_STENCIL8 as a fallback. + // Some graphics cards require a depth buffer along with a stencil. + gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, i32(w), i32(h)) + gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, fb.texture, 0) + gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, fb.rbo) + + if gl.CheckFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE { + fmt.eprintln("ERROR") + } +// #endif // gl.DEPTH24_STENCIL8 +// goto error + } + + gl.BindFramebuffer(gl.FRAMEBUFFER, u32(defaultFBO)) + gl.BindRenderbuffer(gl.RENDERBUFFER, u32(defaultRBO)) + return +} + +DeleteFramebuffer :: proc(fb: ^framebuffer) { + if fb == nil { + return + } + + if fb.fbo != 0 { + gl.DeleteFramebuffers(1, &fb.fbo) + } + + if fb.rbo != 0 { + gl.DeleteRenderbuffers(1, &fb.rbo) + } + + if fb.image >= 0 { + nvg.DeleteImage(fb.ctx, fb.image) + } + + fb.ctx = nil + fb.fbo = 0 + fb.rbo = 0 + fb.texture = 0 + fb.image = -1 +} \ No newline at end of file diff --git a/vendor/nanovg/gl/vert.glsl b/vendor/nanovg/gl/vert.glsl new file mode 100644 index 000000000..f937da09a --- /dev/null +++ b/vendor/nanovg/gl/vert.glsl @@ -0,0 +1,19 @@ +#ifdef NANOVG_GL3 + uniform vec2 viewSize; + in vec2 vertex; + in vec2 tcoord; + out vec2 ftcoord; + out vec2 fpos; +#else + uniform vec2 viewSize; + attribute vec2 vertex; + attribute vec2 tcoord; + varying vec2 ftcoord; + varying vec2 fpos; +#endif + +void main(void) { + ftcoord = tcoord; + fpos = vertex; + gl_Position = vec4(2.0*vertex.x/viewSize.x - 1.0, 1.0 - 2.0*vertex.y/viewSize.y, 0, 1); +} \ No newline at end of file diff --git a/vendor/nanovg/nanovg.odin b/vendor/nanovg/nanovg.odin new file mode 100644 index 000000000..db99ef5a4 --- /dev/null +++ b/vendor/nanovg/nanovg.odin @@ -0,0 +1,3503 @@ +package nanovg + +// TODO rename structs to old nanovg style! +// TODO rename enums to old nanovg style! + +import "core:mem" +import "core:runtime" +import "core:math" +import "core:fmt" +import "../fontstash" +import stbi "vendor:stb/image" + +AlignVertical :: fontstash.AlignVertical +AlignHorizontal :: fontstash.AlignHorizontal + +INIT_FONTIMAGE_SIZE :: 512 +MAX_FONTIMAGE_SIZE :: 2048 +MAX_FONTIMAGES :: 4 + +MAX_STATES :: 32 +INIT_COMMANDS_SIZE :: 256 +INIT_POINTS_SIZE :: 128 +INIT_PATH_SIZE :: 16 +INIT_VERTS_SIZE :: 26 +KAPPA :: 0.5522847493 + +Color :: [4]f32 +Matrix :: [6]f32 +Vertex :: [4]f32 // x,y,u,v + +ImageFlag :: enum { + GENERATE_MIPMAPS, + REPEAT_X, + REPEAT_Y, + FLIP_Y, + PREMULTIPLIED, + NEAREST, + NO_DELETE, +} +ImageFlags :: bit_set[ImageFlag] + +Paint :: struct { + xform: Matrix, + extent: [2]f32, + radius: f32, + feather: f32, + innerColor: Color, + outerColor: Color, + image: int, +} + +Winding :: enum { + CCW = 1, + CW, +} + +Solidity :: enum { + SOLID = 1, // CCW + HOLE, // CW +} + +LineCapType :: enum { + BUTT, + ROUND, + SQUARE, + BEVEL, + MITER, +} + +BlendFactor :: enum { + ZERO, + ONE, + SRC_COLOR, + ONE_MINUS_SRC_COLOR, + DST_COLOR, + ONE_MINUS_DST_COLOR, + SRC_ALPHA, + ONE_MINUS_SRC_ALPHA, + DST_ALPHA, + ONE_MINUS_DST_ALPHA, + SRC_ALPHA_SATURATE, +} + +CompositeOperation :: enum { + SOURCE_OVER, + SOURCE_IN, + SOURCE_OUT, + ATOP, + DESTINATION_OVER, + DESTINATION_IN, + DESTINATION_OUT, + DESTINATION_ATOP, + LIGHTER, + COPY, + XOR, +} + +CompositeOperationState :: struct { + srcRGB: BlendFactor, + dstRGB: BlendFactor, + srcAlpha: BlendFactor, + dstAlpha: BlendFactor, +} + +// render data structures + +Texture :: enum { + Alpha, + RGBA, +} + +ScissorT :: struct { + xform: Matrix, + extent: [2]f32, +} + +Commands :: enum { + MOVE_TO, + LINE_TO, + BEZIER_TO, + CLOSE, + WINDING, +} + +PointFlag :: enum { + CORNER, + LEFT, + BEVEL, + INNER_BEVEL, +} +PointFlags :: bit_set[PointFlag] + +Point :: struct { + x, y: f32, + dx, dy: f32, + len: f32, + dmx, dmy: f32, + flags: PointFlags, +} + +PathCache :: struct { + points: [dynamic]Point, + paths: [dynamic]Path, + verts: [dynamic]Vertex, + bounds: [4]f32, +} + +Path :: struct { + first: int, + count: int, + closed: bool, + nbevel: int, + fill: []Vertex, + stroke: []Vertex, + winding: Winding, + convex: bool, +} + +State :: struct { + compositeOperation: CompositeOperationState, + shapeAntiAlias: bool, + fill: Paint, + stroke: Paint, + strokeWidth: f32, + miterLimit: f32, + lineJoin: LineCapType, + lineCap: LineCapType, + alpha: f32, + xform: Matrix, + scissor: ScissorT, + + // font state + fontSize: f32, + letterSpacing: f32, + lineHeight: f32, + fontBlur: f32, + alignHorizontal: AlignHorizontal, + alignVertical: AlignVertical, + fontId: int, +} + +Context :: struct { + params: Params, + commands: [dynamic]f32, + commandx, commandy: f32, + states: [MAX_STATES]State, + nstates: int, + cache: PathCache, + tessTol: f32, + distTol: f32, + fringeWidth: f32, + devicePxRatio: f32, + + // font + fs: fontstash.FontContext, + fontImages: [MAX_FONTIMAGES]int, + fontImageIdx: int, + + // stats + drawCallCount: int, + fillTriCount: int, + strokeTriCount: int, + textTriCount: int, + + // flush texture + textureDirty: bool, +} + +Params :: struct { + userPtr: rawptr, + edgeAntiAlias: bool, + + // callbacks to fill out + renderCreate: proc(uptr: rawptr) -> bool, + renderDelete: proc(uptr: rawptr), + + // textures calls + renderCreateTexture: proc( + uptr: rawptr, + type: Texture, + w, h: int, + imageFlags: ImageFlags, + data: []byte, + ) -> int, + renderDeleteTexture: proc(uptr: rawptr, image: int) -> bool, + renderUpdateTexture: proc( + uptr: rawptr, + image: int, + x, y: int, + w, h: int, + data: []byte, + ) -> bool, + renderGetTextureSize: proc(uptr: rawptr, image: int, w, h: ^int) -> bool, + + // rendering calls + renderViewport: proc(uptr: rawptr, width, height, devicePixelRatio: f32), + renderCancel: proc(uptr: rawptr), + renderFlush: proc(uptr: rawptr), + renderFill: proc( + uptr: rawptr, + paint: ^Paint, + compositeOperation: CompositeOperationState, + scissor: ^ScissorT, + fringe: f32, + bounds: [4]f32, + paths: []Path, + ), + renderStroke: proc( + uptr: rawptr, + paint: ^Paint, + compositeOperation: CompositeOperationState, + scissor: ^ScissorT, + fringe: f32, + strokeWidth: f32, + paths: []Path, + ), + renderTriangles: proc( + uptr: rawptr, + paint: ^Paint, + compositeOperation: CompositeOperationState, + scissor: ^ScissorT, + verts: []Vertex, + fringe: f32, + ), +} + +__allocPathCache :: proc(c: ^PathCache) { + c.points = make([dynamic]Point, 0, INIT_POINTS_SIZE) + c.paths = make([dynamic]Path, 0, INIT_PATH_SIZE) + c.verts = make([dynamic]Vertex, 0, INIT_VERTS_SIZE) +} + +__deletePathCache :: proc(c: PathCache) { + delete(c.points) + delete(c.paths) + delete(c.verts) +} + +__setDevicePxRatio :: proc(ctx: ^Context, ratio: f32) { + ctx.tessTol = 0.25 / ratio + ctx.distTol = 0.01 / ratio + ctx.fringeWidth = 1.0 / ratio + ctx.devicePxRatio = ratio +} + +__getState :: #force_inline proc(ctx: ^Context) -> ^State #no_bounds_check { + return &ctx.states[ctx.nstates - 1] +} + +CreateInternal :: proc(params: Params) -> (ctx: ^Context) { + ctx = new(Context) + ctx.params = params + ctx.commands = make([dynamic]f32, 0, INIT_COMMANDS_SIZE) + __allocPathCache(&ctx.cache) + + Save(ctx) + Reset(ctx) + __setDevicePxRatio(ctx, 1) + + assert(ctx.params.renderCreate != nil) + if !ctx.params.renderCreate(ctx.params.userPtr) { + DeleteInternal(ctx) + panic("Nanovg - CreateInternal failed") + } + + w := INIT_FONTIMAGE_SIZE + h := INIT_FONTIMAGE_SIZE + fontstash.Init(&ctx.fs, w, h, .TOPLEFT) + assert(ctx.params.renderCreateTexture != nil) + ctx.fs.userData = ctx + + // handle to the image needs to be set to the new generated texture + ctx.fs.callbackResize = proc(data: rawptr, w, h: int) { + ctx := cast(^Context) data + ctx.fontImages[0] = ctx.params.renderCreateTexture(ctx.params.userPtr, .Alpha, w, h, {}, ctx.fs.textureData) + } + + // texture atlas + ctx.fontImages[0] = ctx.params.renderCreateTexture(ctx.params.userPtr, .Alpha, w, h, {}, nil) + ctx.fontImageIdx = 0 + + return +} + +DeleteInternal :: proc(ctx: ^Context) { + __deletePathCache(ctx.cache) + fontstash.Destroy(&ctx.fs) + + for image in &ctx.fontImages { + if image != 0 { + DeleteImage(ctx, image) + } + } + + if ctx.params.renderDelete != nil { + ctx.params.renderDelete(ctx.params.userPtr) + } + + free(ctx) +} + +/* + Begin drawing a new frame + Calls to nanovg drawing API should be wrapped in nvgBeginFrame() & nvgEndFrame() + nvgBeginFrame() defines the size of the window to render to in relation currently + set viewport (i.e. glViewport on GL backends). Device pixel ration allows to + control the rendering on Hi-DPI devices. + For example, GLFW returns two dimension for an opened window: window size and + frame buffer size. In that case you would set windowWidth/Height to the window size + devicePixelRatio to: frameBufferWidth / windowWidth. +*/ +BeginFrame :: proc( + ctx: ^Context, + windowWidth: f32, + windowHeight: f32, + devicePixelRatio: f32, +) { + ctx.nstates = 0 + Save(ctx) + Reset(ctx) + __setDevicePxRatio(ctx, devicePixelRatio) + + assert(ctx.params.renderViewport != nil) + ctx.params.renderViewport(ctx.params.userPtr, windowWidth, windowHeight, devicePixelRatio) + + ctx.drawCallCount = 0 + ctx.fillTriCount = 0 + ctx.strokeTriCount = 0 + ctx.textTriCount = 0 +} + +@(deferred_out=EndFrame) +FrameScoped :: proc( + ctx: ^Context, + windowWidth: f32, + windowHeight: f32, + devicePixelRatio: f32, +) -> ^Context { + BeginFrame(ctx, windowWidth, windowHeight, devicePixelRatio) + return ctx +} + +// Cancels drawing the current frame. +CancelFrame :: proc(ctx: ^Context) { + assert(ctx.params.renderCancel != nil) + ctx.params.renderCancel(ctx.params.userPtr) +} + +// Ends drawing flushing remaining render state. +EndFrame :: proc(ctx: ^Context) { + // flush texture only once + if ctx.textureDirty { + __flushTextTexture(ctx) + ctx.textureDirty = false + } + + assert(ctx.params.renderFlush != nil) + ctx.params.renderFlush(ctx.params.userPtr) + + // delete textures with invalid size + if ctx.fontImageIdx != 0 { + font_image := ctx.fontImages[ctx.fontImageIdx] + ctx.fontImages[ctx.fontImageIdx] = 0 + + if font_image == 0 { + return + } + + iw, ih := ImageSize(ctx, font_image) + j: int + for i in 0.. Color { + return RGBA(r, g, b, 255) +} + +// Returns a color value from red, green, blue and alpha values. +RGBA :: proc(r, g, b, a: u8) -> (res: Color) { + res.r = f32(r) / f32(255) + res.g = f32(g) / f32(255) + res.b = f32(b) / f32(255) + res.a = f32(a) / f32(255) + return +} + +// Linearly interpolates from color c0 to c1, and returns resulting color value. +LerpRGBA :: proc(c0, c1: Color, u: f32) -> (cint: Color) { + u := clamp(u, 0.0, 1.0) + oneminu := 1.0 - u + for i in 0..<4 { + cint[i] = c0[i] * oneminu + c1[i] * u + } + + return +} + +// Returns color value specified by hue, saturation and lightness. +// HSL values are all in range [0..1], alpha will be set to 255. +HSL :: proc(h, s, l: f32) -> Color { + return HSLA(h,s,l,255) +} + +// Returns color value specified by hue, saturation and lightness and alpha. +// HSL values are all in range [0..1], alpha in range [0..255] +HSLA :: proc(h, s, l: f32, a: u8) -> (col: Color) { + hue :: proc(h, m1, m2: f32) -> f32 { + h := h + + if h < 0 { + h += 1 + } + + if h > 1 { + h -= 1 + } + + if h < 1.0 / 6.0 { + return m1 + (m2 - m1) * h * 6.0 + } else if h < 3.0 / 6.0 { + return m2 + } else if h < 4.0 / 6.0 { + return m1 + (m2 - m1) * (2.0 / 3.0 - h) * 6.0 + } + + return m1 + } + + h := math.mod(h, 1.0) + if h < 0.0 { + h += 1.0 + } + s := clamp(s, 0.0, 1.0) + l := clamp(l, 0.0, 1.0) + m2 := l <= 0.5 ? (l * (1 + s)) : (l + s - l * s) + m1 := 2 * l - m2 + col.r = clamp(hue(h + 1.0/3.0, m1, m2), 0.0, 1.0) + col.g = clamp(hue(h, m1, m2), 0.0, 1.0) + col.b = clamp(hue(h - 1.0/3.0, m1, m2), 0.0, 1.0) + col.a = f32(a) / 255.0 + return +} + +// hex to 0xAARRGGBB color +ColorHex :: proc(color: u32) -> (res: Color) { + color := color + res.b = f32(0x000000FF & color) / 255 + color = color >> 8 + res.g = f32(0x000000FF & color) / 255 + color = color >> 8 + res.r = f32(0x000000FF & color) / 255 + color = color >> 8 + res.a = f32(0x000000FF & color) / 255 + return +} + +/////////////////////////////////////////////////////////// +// TRANSFORMS +// +// The following functions can be used to make calculations on 2x3 transformation matrices. +// A 2x3 matrix is represented as float[6]. +/////////////////////////////////////////////////////////// + +// Sets the transform to identity matrix. +TransformIdentity :: proc(t: ^Matrix) { + t[0] = 1 + t[1] = 0 + t[2] = 0 + t[3] = 1 + t[4] = 0 + t[5] = 0 +} + +// Sets the transform to translation matrix matrix. +TransformTranslate :: proc(t: ^Matrix, tx, ty: f32) { + t[0] = 1 + t[1] = 0 + t[2] = 0 + t[3] = 1 + t[4] = tx + t[5] = ty +} + +// Sets the transform to scale matrix. +TransformScale :: proc(t: ^Matrix, sx, sy: f32) { + t[0] = sx + t[1] = 0 + t[2] = 0 + t[3] = sy + t[4] = 0 + t[5] = 0 +} + +// Sets the transform to rotate matrix. Angle is specified in radians. +TransformRotate :: proc(t: ^Matrix, a: f32) { + cs := math.cos(a) + sn := math.sin(a) + t[0] = cs + t[1] = sn + t[2] = -sn + t[3] = cs + t[4] = 0 + t[5] = 0 +} + +// Sets the transform to skew-x matrix. Angle is specified in radians. +TransformSkewX :: proc(t: ^Matrix, a: f32) { + t[0] = 1 + t[1] = 0 + t[2] = math.tan(a) + t[3] = 1 + t[4] = 0 + t[5] = 0 +} + +// Sets the transform to skew-y matrix. Angle is specified in radians. +TransformSkewY :: proc(t: ^Matrix, a: f32) { + t[0] = 1 + t[1] = math.tan(a) + t[2] = 0 + t[3] = 1 + t[4] = 0 + t[5] = 0 +} + +// Sets the transform to the result of multiplication of two transforms, of A = A*B. +TransformMultiply :: proc(t: ^Matrix, s: Matrix) { + t0 := t[0] * s[0] + t[1] * s[2] + t2 := t[2] * s[0] + t[3] * s[2] + t4 := t[4] * s[0] + t[5] * s[2] + s[4] + t[1] = t[0] * s[1] + t[1] * s[3] + t[3] = t[2] * s[1] + t[3] * s[3] + t[5] = t[4] * s[1] + t[5] * s[3] + s[5] + t[0] = t0 + t[2] = t2 + t[4] = t4 +} + +// Sets the transform to the result of multiplication of two transforms, of A = B*A. +TransformPremultiply :: proc(t: ^Matrix, s: Matrix) { + temp := s + TransformMultiply(&temp, t^) + t^ = temp +} + +// Sets the destination to inverse of specified transform. +// Returns true if the inverse could be calculated, else false. +TransformInverse :: proc(inv: ^Matrix, t: Matrix) -> bool { + // TODO could be bad math? due to types + det := f64(t[0]) * f64(t[3]) - f64(t[2]) * f64(t[1]) + + if det > -1e-6 && det < 1e-6 { + TransformIdentity(inv) + return false + } + + invdet := 1.0 / det + inv[0] = f32(f64(t[3]) * invdet) + inv[2] = f32(f64(-t[2]) * invdet) + inv[4] = f32((f64(t[2]) * f64(t[5]) - f64(t[3]) * f64(t[4])) * invdet) + inv[1] = f32(f64(-t[1]) * invdet) + inv[3] = f32(f64(t[0]) * invdet) + inv[5] = f32((f64(t[1]) * f64(t[4]) - f64(t[0]) * f64(t[5])) * invdet) + return true +} + +// Transform a point by given transform. +TransformPoint :: proc( + dx: ^f32, + dy: ^f32, + t: Matrix, + sx: f32, + sy: f32, +) { + dx^ = sx * t[0] + sy * t[2] + t[4] + dy^ = sx * t[1] + sy * t[3] + t[5] +} + +DegToRad :: proc(deg: f32) -> f32 { + return deg / 180.0 * math.PI +} + +RadToDeg :: proc(rad: f32) -> f32 { + return rad / math.PI * 180.0 +} + +/////////////////////////////////////////////////////////// +// STATE MANAGEMENT +// +// NanoVG contains state which represents how paths will be rendered. +// The state contains transform, fill and stroke styles, text and font styles, +// and scissor clipping. +/////////////////////////////////////////////////////////// + +// Pushes and saves the current render state into a state stack. +// A matching nvgRestore() must be used to restore the state. +Save :: proc(ctx: ^Context) { + if ctx.nstates >= MAX_STATES { + return + } + + // copy prior + if ctx.nstates > 0 { + ctx.states[ctx.nstates] = ctx.states[ctx.nstates - 1] + } + + ctx.nstates += 1 +} + +// Pops and restores current render state. +Restore :: proc(ctx: ^Context) { + if ctx.nstates <= 1 { + return + } + + ctx.nstates -= 1 +} + +// NOTE useful helper +@(deferred_in=Restore) +SaveScoped :: #force_inline proc(ctx: ^Context) { + Save(ctx) +} + +__setPaintColor :: proc(p: ^Paint, color: Color) { + p^ = {} + TransformIdentity(&p.xform) + p.radius = 0 + p.feather = 1 + p.innerColor = color + p.outerColor = color +} + +// Resets current render state to default values. Does not affect the render state stack. +Reset :: proc(ctx: ^Context) { + state := __getState(ctx) + state^ = {} + + __setPaintColor(&state.fill, RGBA(255, 255, 255, 255)) + __setPaintColor(&state.stroke, RGBA(0, 0, 0, 255)) + + state.compositeOperation = __compositeOperationState(.SOURCE_OVER) + state.shapeAntiAlias = true + state.strokeWidth = 1 + state.miterLimit = 10 + state.lineCap = .BUTT + state.lineJoin = .MITER + state.alpha = 1 + TransformIdentity(&state.xform) + + state.scissor.extent[0] = -1 + state.scissor.extent[1] = -1 + + // font settings + state.fontSize = 16 + state.letterSpacing = 0 + state.lineHeight = 1 + state.fontBlur = 0 + state.alignHorizontal = .LEFT + state.alignVertical = .BASELINE + state.fontId = 0 +} + +/////////////////////////////////////////////////////////// +// STATE SETTING +/////////////////////////////////////////////////////////// + +// Sets whether to draw antialias for nvgStroke() and nvgFill(). It's enabled by default. +ShapeAntiAlias :: proc(ctx: ^Context, enabled: bool) { + state := __getState(ctx) + state.shapeAntiAlias = enabled +} + +// Sets the stroke width of the stroke style. +StrokeWidth :: proc(ctx: ^Context, width: f32) { + state := __getState(ctx) + state.strokeWidth = width +} + +// Sets the miter limit of the stroke style. +// Miter limit controls when a sharp corner is beveled. +MiterLimit :: proc(ctx: ^Context, limit: f32) { + state := __getState(ctx) + state.miterLimit = limit +} + +// Sets how the end of the line (cap) is drawn, +// Can be one of: NVG_BUTT (default), NVG_ROUND, NVG_SQUARE. +LineCap :: proc(ctx: ^Context, cap: LineCapType) { + state := __getState(ctx) + state.lineCap = cap +} + +// Sets how sharp path corners are drawn. +// Can be one of NVG_MITER (default), NVG_ROUND, NVG_BEVEL. +LineJoin :: proc(ctx: ^Context, join: LineCapType) { + state := __getState(ctx) + state.lineJoin = join +} + +// Sets the transparency applied to all rendered shapes. +// Already transparent paths will get proportionally more transparent as well. +GlobalAlpha :: proc(ctx: ^Context, alpha: f32) { + state := __getState(ctx) + state.alpha = alpha +} + +// Sets current stroke style to a solid color. +StrokeColor :: proc(ctx: ^Context, color: Color) { + state := __getState(ctx) + __setPaintColor(&state.stroke, color) +} + +// Sets current stroke style to a paint, which can be a one of the gradients or a pattern. +StrokePaint :: proc(ctx: ^Context, paint: Paint) { + state := __getState(ctx) + state.stroke = paint + TransformMultiply(&state.stroke.xform, state.xform) +} + +// Sets current fill style to a solid color. +FillColor :: proc(ctx: ^Context, color: Color) { + state := __getState(ctx) + __setPaintColor(&state.fill, color) +} + +// Sets current fill style to a paint, which can be a one of the gradients or a pattern. +FillPaint :: proc(ctx: ^Context, paint: Paint) { + state := __getState(ctx) + state.fill = paint + TransformMultiply(&state.fill.xform, state.xform) +} + +/////////////////////////////////////////////////////////// +// STATE TRANSFORMS +// +// The paths, gradients, patterns and scissor region are transformed by an transformation +// matrix at the time when they are passed to the API. +// The current transformation matrix is a affine matrix: +// [sx kx tx] +// [ky sy ty] +// [ 0 0 1] +// Where: sx,sy define scaling, kx,ky skewing, and tx,ty translation. +// The last row is assumed to be 0,0,1 and is not stored. +// +// Apart from nvgResetTransform(), each transformation function first creates +// specific transformation matrix and pre-multiplies the current transformation by it. +// +// Current coordinate system (transformation) can be saved and restored using nvgSave() and nvgRestore(). +/////////////////////////////////////////////////////////// + +Transform :: proc(ctx: ^Context, a, b, c, d, e, f: f32) { + state := __getState(ctx) + TransformPremultiply(&state.xform, { a, b, c, d, e, f }) +} + +// Resets current transform to a identity matrix. +ResetTransform :: proc(ctx: ^Context) { + state := __getState(ctx) + TransformIdentity(&state.xform) +} + +// Translates current coordinate system. +Translate :: proc(ctx: ^Context, x, y: f32) { + state := __getState(ctx) + temp: Matrix + TransformTranslate(&temp, x, y) + TransformPremultiply(&state.xform, temp) +} + +// Rotates current coordinate system. Angle is specified in radians. +Rotate :: proc(ctx: ^Context, angle: f32) { + state := __getState(ctx) + temp: Matrix + TransformRotate(&temp, angle) + TransformPremultiply(&state.xform, temp) +} + +// Skews the current coordinate system along X axis. Angle is specified in radians. +SkewX :: proc(ctx: ^Context, angle: f32) { + state := __getState(ctx) + temp: Matrix + TransformSkewX(&temp, angle) + TransformPremultiply(&state.xform, temp) +} + +// Skews the current coordinate system along Y axis. Angle is specified in radians. +SkewY :: proc(ctx: ^Context, angle: f32) { + state := __getState(ctx) + temp: Matrix + TransformSkewY(&temp, angle) + TransformPremultiply(&state.xform, temp) +} + +// Scales the current coordinate system. +Scale :: proc(ctx: ^Context, x, y: f32) { + state := __getState(ctx) + temp: Matrix + TransformScale(&temp, x, y) + TransformPremultiply(&state.xform, temp) +} + +/* + Stores the top part (a-f) of the current transformation matrix in to the specified buffer. + [a c e] + [b d f] + [0 0 1] + There should be space for 6 floats in the return buffer for the values a-f. +*/ +CurrentTransform :: proc(ctx: ^Context, xform: ^Matrix) { + state := __getState(ctx) + if xform == nil { + return + } + xform^ = state.xform +} + +/////////////////////////////////////////////////////////// +// IMAGE HANDLING +// +// NanoVG allows you to load jpg, png, psd, tga, pic and gif files to be used for rendering. +// In addition you can upload your own image. The image loading is provided by stb_image. +// The parameter imageFlags is a combination of flags defined in NVGimageFlags. +/////////////////////////////////////////////////////////// + +// Creates image by loading it from the disk from specified file name. +// Returns handle to the image. +CreateImagePath :: proc(ctx: ^Context, filename: cstring, imageFlags: ImageFlags) -> int { + stbi.set_unpremultiply_on_load(1) + stbi.convert_iphone_png_to_rgb(1) + w, h, n: i32 + img := stbi.load(filename, &w, &h, &n, 4) + + if img == nil { + return 0 + } + + data := mem.slice_ptr(img, int(w) * int(h) * int(n)) + image := CreateImageRGBA(ctx, int(w), int(h), imageFlags, data) + stbi.image_free(img) + return image +} + +// Creates image by loading it from the specified chunk of memory. +// Returns handle to the image. +CreateImageMem :: proc(ctx: ^Context, data: []byte, imageFlags: ImageFlags) -> int { + stbi.set_unpremultiply_on_load(1) + stbi.convert_iphone_png_to_rgb(1) + w, h, n: i32 + img := stbi.load_from_memory(raw_data(data), i32(len(data)), &w, &h, &n, 4) + + if img == nil { + return 0 + } + + data := mem.slice_ptr(img, int(w) * int(h) * int(n)) + image := CreateImageRGBA(ctx, int(w), int(h), imageFlags, data) + stbi.image_free(img) + return image +} + +CreateImage :: proc { CreateImagePath, CreateImageMem } + +// Creates image from specified image data. +// Returns handle to the image. +CreateImageRGBA :: proc(ctx: ^Context, w, h: int, imageFlags: ImageFlags, data: []byte) -> int { + assert(ctx.params.renderCreateTexture != nil) + return ctx.params.renderCreateTexture( + ctx.params.userPtr, + .RGBA, + w, h, + imageFlags, + data, + ) +} + +// Updates image data specified by image handle. +UpdateImage :: proc(ctx: ^Context, image: int, data: []byte) { + assert(ctx.params.renderGetTextureSize != nil) + assert(ctx.params.renderUpdateTexture != nil) + + w, h: int + found := ctx.params.renderGetTextureSize(ctx.params.userPtr, image, &w, &h) + if found { + ctx.params.renderUpdateTexture(ctx.params.userPtr, image, 0, 0, w, h, data) + } +} + +// Returns the dimensions of a created image. +ImageSize :: proc(ctx: ^Context, image: int) -> (w, h: int) { + assert(ctx.params.renderGetTextureSize != nil) + ctx.params.renderGetTextureSize(ctx.params.userPtr, image, &w, &h) + return +} + +// Deletes created image. +DeleteImage :: proc(ctx: ^Context, image: int) { + assert(ctx.params.renderDeleteTexture != nil) + ctx.params.renderDeleteTexture(ctx.params.userPtr, image) +} + +/////////////////////////////////////////////////////////// +// PAINT gradients / image +// +// NanoVG supports four types of paints: linear gradient, box gradient, radial gradient and image pattern. +// These can be used as paints for strokes and fills. +/////////////////////////////////////////////////////////// + +/* + Creates and returns a linear gradient. Parameters (sx,sy)-(ex,ey) specify the start and end coordinates + of the linear gradient, icol specifies the start color and ocol the end color. + The gradient is transformed by the current transform when it is passed to nvgFillPaint() or nvgStrokePaint(). +*/ +LinearGradient :: proc( + sx, sy: f32, + ex, ey: f32, + icol: Color, + ocol: Color, +) -> (p: Paint) { + LARGE :: f32(1e5) + + // Calculate transform aligned to the line + dx := ex - sx + dy := ey - sy + d := math.sqrt(dx*dx + dy*dy) + if d > 0.0001 { + dx /= d + dy /= d + } else { + dx = 0 + dy = 1 + } + + p.xform[0] = dy + p.xform[1] = -dx + p.xform[2] = dx + p.xform[3] = dy + p.xform[4] = sx - dx*LARGE + p.xform[5] = sy - dy*LARGE + + p.extent[0] = LARGE + p.extent[1] = LARGE + d*0.5 + + p.feather = max(1.0, d) + + p.innerColor = icol + p.outerColor = ocol + + return +} + +/* + Creates and returns a box gradient. Box gradient is a feathered rounded rectangle, it is useful for rendering + drop shadows or highlights for boxes. Parameters (x,y) define the top-left corner of the rectangle, + (w,h) define the size of the rectangle, r defines the corner radius, and f feather. Feather defines how blurry + the border of the rectangle is. Parameter icol specifies the inner color and ocol the outer color of the gradient. + The gradient is transformed by the current transform when it is passed to nvgFillPaint() or nvgStrokePaint(). +*/ +RadialGradient :: proc( + cx, cy: f32, + inr: f32, + outr: f32, + icol: Color, + ocol: Color, +) -> (p: Paint) { + r := (inr+outr)*0.5 + f := (outr-inr) + + TransformIdentity(&p.xform) + p.xform[4] = cx + p.xform[5] = cy + + p.extent[0] = r + p.extent[1] = r + + p.radius = r + p.feather = max(1.0, f) + + p.innerColor = icol + p.outerColor = ocol + + return +} + +/* + Creates and returns a radial gradient. Parameters (cx,cy) specify the center, inr and outr specify + the inner and outer radius of the gradient, icol specifies the start color and ocol the end color. + The gradient is transformed by the current transform when it is passed to nvgFillPaint() or nvgStrokePaint(). +*/ +BoxGradient :: proc( + x, y: f32, + w, h: f32, + r: f32, + f: f32, + icol: Color, + ocol: Color, +) -> (p: Paint) { + TransformIdentity(&p.xform) + p.xform[4] = x+w*0.5 + p.xform[5] = y+h*0.5 + + p.extent[0] = w*0.5 + p.extent[1] = h*0.5 + + p.radius = r + p.feather = max(1.0, f) + + p.innerColor = icol + p.outerColor = ocol + + return +} + +/* + Creates and returns an image pattern. Parameters (ox,oy) specify the left-top location of the image pattern, + (ex,ey) the size of one image, angle rotation around the top-left corner, image is handle to the image to render. + The gradient is transformed by the current transform when it is passed to nvgFillPaint() or nvgStrokePaint(). +*/ +ImagePattern :: proc( + cx, cy: f32, + w, h: f32, + angle: f32, + image: int, + alpha: f32, +) -> (p: Paint) { + TransformRotate(&p.xform, angle) + p.xform[4] = cx + p.xform[5] = cy + + p.extent[0] = w + p.extent[1] = h + + p.image = image + p.innerColor = { 1,1,1,alpha } + p.outerColor = p.innerColor + + return +} + +/////////////////////////////////////////////////////////// +// SCISSOR +// +// Scissoring allows you to clip the rendering into a rectangle. This is useful for various +// user interface cases like rendering a text edit or a timeline. +/////////////////////////////////////////////////////////// + +// Sets the current scissor rectangle. +// The scissor rectangle is transformed by the current transform. +Scissor :: proc( + ctx: ^Context, + x, y: f32, + w, h: f32, +) { + state := __getState(ctx) + w := max(w, 0) + h := max(h, 0) + + TransformIdentity(&state.scissor.xform) + state.scissor.xform[4] = x + w * 0.5 + state.scissor.xform[5] = y + h * 0.5 + TransformMultiply(&state.scissor.xform, state.xform) + + state.scissor.extent[0] = w * 0.5 + state.scissor.extent[1] = h * 0.5 +} + +/* + Intersects current scissor rectangle with the specified rectangle. + The scissor rectangle is transformed by the current transform. + Note: in case the rotation of previous scissor rect differs from + the current one, the intersection will be done between the specified + rectangle and the previous scissor rectangle transformed in the current + transform space. The resulting shape is always rectangle. +*/ +IntersectScissor :: proc( + ctx: ^Context, + x, y, w, h: f32, +) { + isect_rects :: proc( + dst: ^[4]f32, + ax, ay, aw, ah: f32, + bx, by, bw, bh: f32, + ) { + minx := max(ax, bx) + miny := max(ay, by) + maxx := min(ax + aw, bx + bw) + maxy := min(ay + ah, by + bh) + dst[0] = minx + dst[1] = miny + dst[2] = max(0.0, maxx - minx) + dst[3] = max(0.0, maxy - miny) + } + + state := __getState(ctx) + pxform: Matrix + invxorm: Matrix + + // If no previous scissor has been set, set the scissor as current scissor. + if state.scissor.extent[0] < 0 { + Scissor(ctx, x, y, w, h) + return + } + + pxform = state.scissor.xform + ex := state.scissor.extent[0] + ey := state.scissor.extent[1] + TransformInverse(&invxorm, state.xform) + TransformMultiply(&pxform, invxorm) + tex := ex * abs(pxform[0]) + ey * abs(pxform[2]) + tey := ex * abs(pxform[1]) + ey * abs(pxform[3]) + + rect: [4]f32 + isect_rects(&rect, pxform[4] - tex, pxform[5] - tey, tex * 2, tey * 2, x,y,w,h) + Scissor(ctx, rect.x, rect.y, rect.z, rect.w) +} + +// Reset and disables scissoring. +ResetScissor :: proc(ctx: ^Context) { + state := __getState(ctx) + state.scissor.xform = 0 + state.scissor.extent[0] = -1 + state.scissor.extent[1] = -1 +} + +/////////////////////////////////////////////////////////// +// Global composite operation +// +// The composite operations in NanoVG are modeled after HTML Canvas API, and +// the blend func is based on OpenGL (see corresponding manuals for more info). +// The colors in the blending state have premultiplied alpha. +/////////////////////////////////////////////////////////// + +// state table instead of if else chains +OP_STATE_TABLE :: [CompositeOperation][2]BlendFactor { + .SOURCE_OVER = { .ONE, .ONE_MINUS_SRC_ALPHA }, + .SOURCE_IN = { .DST_ALPHA, .ZERO }, + .SOURCE_OUT = { .ONE_MINUS_DST_ALPHA, .ZERO }, + .ATOP = { .DST_ALPHA, .ONE_MINUS_SRC_ALPHA }, + + .DESTINATION_OVER = { .ONE_MINUS_DST_ALPHA, .ONE }, + .DESTINATION_IN = { .ZERO, .SRC_ALPHA }, + .DESTINATION_OUT = { .ZERO, .ONE_MINUS_SRC_ALPHA }, + .DESTINATION_ATOP = { .ONE_MINUS_DST_ALPHA, .SRC_ALPHA }, + + .LIGHTER = { .ONE, .ONE }, + .COPY = { .ONE, .ZERO }, + .XOR = { .ONE_MINUS_DST_ALPHA, .ONE_MINUS_SRC_ALPHA }, +} + +__compositeOperationState :: proc(op: CompositeOperation) -> (res: CompositeOperationState) { + table := OP_STATE_TABLE + factors := table[op] + res.srcRGB = factors.x + res.dstRGB = factors.y + res.srcAlpha = factors.x + res.dstAlpha = factors.y + return +} + +// Sets the composite operation. The op parameter should be one of NVGcompositeOperation. +GlobalCompositeOperation :: proc(ctx: ^Context, op: CompositeOperation) { + state := __getState(ctx) + state.compositeOperation = __compositeOperationState(op) +} + +// Sets the composite operation with custom pixel arithmetic. The parameters should be one of NVGblendFactor. +GlobalCompositeBlendFunc :: proc(ctx: ^Context, sfactor, dfactor: BlendFactor) { + GlobalCompositeBlendFuncSeparate(ctx, sfactor, dfactor, sfactor, dfactor) +} + +// Sets the composite operation with custom pixel arithmetic for RGB and alpha components separately. The parameters should be one of NVGblendFactor. +GlobalCompositeBlendFuncSeparate :: proc( + ctx: ^Context, + srcRGB: BlendFactor, + dstRGB: BlendFactor, + srcAlpha: BlendFactor, + dstAlpha: BlendFactor, +) { + op := CompositeOperationState { + srcRGB, + dstRGB, + srcAlpha, + dstAlpha, + } + state := __getState(ctx) + state.compositeOperation = op +} + +/////////////////////////////////////////////////////////// +// Points / Path handling +/////////////////////////////////////////////////////////// + +__cross :: proc(dx0, dy0, dx1, dy1: f32) -> f32 { + return dx1*dy0 - dx0*dy1 +} + +__ptEquals :: proc(x1, y1, x2, y2, tol: f32) -> bool { + dx := x2 - x1 + dy := y2 - y1 + return dx * dx + dy * dy < tol * tol +} + +__distPtSeg :: proc(x, y, px, py, qx, qy: f32) -> f32 { + pqx := qx - px + pqy := qy - py + dx := x - px + dy := y - py + d := pqx * pqx + pqy * pqy + t := pqx * dx + pqy * dy + + if d > 0 { + t /= d + } + + if t < 0 { + t = 0 + } else if t > 1 { + t = 1 + } + + dx = px + t * pqx - x + dy = py + t * pqy - y + return dx * dx + dy * dy +} + +__appendCommands :: proc(ctx: ^Context, values: []f32) { + state := __getState(ctx) + + if Commands(values[0]) != .CLOSE && Commands(values[0]) != .WINDING { + ctx.commandx = values[len(values) - 2] + ctx.commandy = values[len(values) - 1] + } + + i := 0 + for i < len(values) { + cmd := Commands(values[i]) + + switch cmd { + case .MOVE_TO, .LINE_TO: { + TransformPoint(&values[i + 1], &values[i + 2], state.xform, values[i + 1], values[i + 2]) + i += 3 + } + + case .BEZIER_TO: { + TransformPoint(&values[i + 1], &values[i + 2], state.xform, values[i + 1], values[i + 2]) + TransformPoint(&values[i + 3], &values[i + 4], state.xform, values[i + 3], values[i + 4]) + TransformPoint(&values[i + 5], &values[i + 6], state.xform, values[i + 5], values[i + 6]) + i += 7 + } + + case .CLOSE: { + i += 1 + } + + case .WINDING: { + i += 2 + } + + // default + case: { + i += 1 + } + } + } + + // append values + append(&ctx.commands, ..values) +} + +__clearPathCache :: proc(ctx: ^Context) { + clear(&ctx.cache.points) + clear(&ctx.cache.paths) +} + +__lastPath :: proc(ctx: ^Context) -> ^Path { + if len(ctx.cache.paths) > 0 { + return &ctx.cache.paths[len(ctx.cache.paths) - 1] + } + + return nil +} + +__addPath :: proc(ctx: ^Context) { + append(&ctx.cache.paths, Path { + first = len(ctx.cache.points), + winding = .CCW, + }) +} + +__lastPoint :: proc(ctx: ^Context) -> ^Point { + if len(ctx.cache.paths) > 0 { + return &ctx.cache.points[len(ctx.cache.points) - 1] + } + + return nil +} + +__addPoint :: proc(ctx: ^Context, x, y: f32, flags: PointFlags) { + path := __lastPath(ctx) + + if path == nil { + return + } + + if path.count > 0 && len(ctx.cache.points) > 0 { + pt := __lastPoint(ctx) + + if __ptEquals(pt.x, pt.y, x, y, ctx.distTol) { + pt.flags |= flags + return + } + } + + append(&ctx.cache.points, Point { + x = x, + y = y, + flags = flags, + }) + path.count += 1 +} + +__closePath :: proc(ctx: ^Context) { + path := __lastPath(ctx) + if path == nil { + return + } + path.closed = true +} + +__pathWinding :: proc(ctx: ^Context, winding: Winding) { + path := __lastPath(ctx) + if path == nil { + return + } + path.winding = winding +} + +__getAverageScale :: proc(t: []f32) -> f32 { + assert(len(t) > 4) + sx := math.sqrt(f64(t[0]) * f64(t[0]) + f64(t[2]) * f64(t[2])) + sy := math.sqrt(f64(t[1]) * f64(t[1]) + f64(t[3]) * f64(t[3])) + return f32((sx + sy) * 0.5) + // sx := math.sqrt(t[0] * t[0] + t[2] * t[2]) + // sy := math.sqrt(t[1] * t[1] + t[3] * t[3]) + // return (sx + sy) * 0.5 +} + +__triarea2 :: proc(ax, ay, bx, by, cx, cy: f32) -> f32 { + abx := bx - ax + aby := by - ay + acx := cx - ax + acy := cy - ay + return acx * aby - abx * acy +} + +__polyArea :: proc(points: []Point) -> f32 { + area := f32(0) + + for i := 2; i < len(points); i += 1 { + a := &points[0] + b := &points[i - 1] + c := &points[i] + area += __triarea2(a.x, a.y, b.x, b.y, c.x, c.y) + } + + return area * 0.5 +} + +__polyReverse :: proc(points: []Point) { + tmp: Point + i := 0 + j := len(points) - 1 + + for i < j { + tmp = points[i] + points[i] = points[j] + points[j] = tmp + i += 1 + j -= 1 + } +} + +__normalize :: proc(x, y: ^f32) -> f32 { + d := math.sqrt(x^ * x^ + y^ * y^) + if d > 1e-6 { + id := 1.0 / d + x^ *= id + y^ *= id + } + return d +} + +__tesselateBezier :: proc( + ctx: ^Context, + x1, y1: f32, + x2, y2: f32, + x3, y3: f32, + x4, y4: f32, + level: int, + flags: PointFlags, +) { + if level > 10 { + return + } + + x12 := (x1 + x2) * 0.5 + y12 := (y1 + y2) * 0.5 + x23 := (x2 + x3) * 0.5 + y23 := (y2 + y3) * 0.5 + x34 := (x3 + x4) * 0.5 + y34 := (y3 + y4) * 0.5 + x123 := (x12 + x23) * 0.5 + y123 := (y12 + y23) * 0.5 + + dx := x4 - x1 + dy := y4 - y1 + d2 := abs(((x2 - x4) * dy - (y2 - y4) * dx)) + d3 := abs(((x3 - x4) * dy - (y3 - y4) * dx)) + + if (d2 + d3)*(d2 + d3) < ctx.tessTol * (dx*dx + dy*dy) { + __addPoint(ctx, x4, y4, flags) + return + } + + x234 := (x23 + x34) * 0.5 + y234 := (y23 + y34) * 0.5 + x1234 := (x123 + x234) * 0.5 + y1234 := (y123 + y234) * 0.5 + + __tesselateBezier(ctx, x1,y1, x12,y12, x123,y123, x1234,y1234, level+1, {}) + __tesselateBezier(ctx, x1234,y1234, x234,y234, x34,y34, x4,y4, level+1, flags) +} + +__flattenPaths :: proc(ctx: ^Context) { + cache := &ctx.cache + + if len(cache.paths) > 0 { + return + } + + // flatten + i := 0 + for i < len(ctx.commands) { + cmd := Commands(ctx.commands[i]) + + switch cmd { + case .MOVE_TO: { + __addPath(ctx) + p := ctx.commands[i + 1:] + __addPoint(ctx, p[0], p[1], { .CORNER }) + i += 3 + } + + case .LINE_TO: { + p := ctx.commands[i + 1:] + __addPoint(ctx, p[0], p[1], { .CORNER }) + i += 3 + } + + case .BEZIER_TO: { + last := __lastPoint(ctx) + + if last != nil { + cp1 := ctx.commands[i + 1:] + cp2 := ctx.commands[i + 3:] + p := ctx.commands[i + 5:] + __tesselateBezier(ctx, last.x,last.y, cp1[0],cp1[1], cp2[0],cp2[1], p[0],p[1], 0, { .CORNER }) + } + + i += 7 + } + + case .CLOSE: { + __closePath(ctx) + i += 1 + } + + case .WINDING: { + __pathWinding(ctx, Winding(ctx.commands[i + 1])) + i += 2 + } + + case: { + i += 1 + } + } + } + + cache.bounds[0] = 1e6 + cache.bounds[1] = 1e6 + cache.bounds[2] = -1e6 + cache.bounds[3] = -1e6 + + // Calculate the direction and length of line segments. + for j in 0.. 2 { + area := __polyArea(pts[:path.count]) + + if path.winding == .CCW && area < 0 { + __polyReverse(pts[:path.count]) + } + + if path.winding == .CW && area > 0 { + __polyReverse(pts[:path.count]) + } + } + + for k in 0.. f32 { + da := math.acos(r / (r + tol)) * 2 + return max(2, math.ceil(arc / da)) +} + +__chooseBevel :: proc( + bevel: bool, + p0: ^Point, + p1: ^Point, + w: f32, + x0, y0, x1, y1: ^f32, +) { + if bevel { + x0^ = p1.x + p0.dy * w + y0^ = p1.y - p0.dx * w + x1^ = p1.x + p1.dy * w + y1^ = p1.y - p1.dx * w + } else { + x0^ = p1.x + p1.dmx * w + y0^ = p1.y + p1.dmy * w + x1^ = p1.x + p1.dmx * w + y1^ = p1.y + p1.dmy * w + } +} + +/////////////////////////////////////////////////////////// +// Vertice Setting +/////////////////////////////////////////////////////////// + +// set vertex & increase slice position (decreases length) +__vset :: proc(dst: ^[]Vertex, x, y, u, v: f32, loc := #caller_location) { + dst[0] = { x, y, u, v } + dst^ = dst[1:] +} + +__roundJoin :: proc( + dst: ^[]Vertex, + p0: ^Point, + p1: ^Point, + lw: f32, + rw: f32, + lu: f32, + ru: f32, + ncap: int, +) { + dlx0 := p0.dy + dly0 := -p0.dx + dlx1 := p1.dy + dly1 := -p1.dx + + if .LEFT in p1.flags { + lx0,ly0,lx1,ly1: f32 + __chooseBevel(.INNER_BEVEL in p1.flags, p0, p1, lw, &lx0,&ly0, &lx1,&ly1) + a0 := math.atan2(-dly0, -dlx0) + a1 := math.atan2(-dly1, -dlx1) + + if a1 > a0 { + a1 -= math.PI * 2 + } + + __vset(dst, lx0, ly0, lu, 1) + __vset(dst, p1.x - dlx0 * rw, p1.y - dly0 * rw, ru, 1) + + temp := int(math.ceil((a0 - a1) / math.PI * f32(ncap))) + n := clamp(temp, 2, ncap) + + for i := 0; i < n; i += 1 { + u := f32(i) / f32(n - 1) + a := a0 + u * (a1 - a0) + rx := p1.x + math.cos(a) * rw + ry := p1.y + math.sin(a) * rw + __vset(dst, p1.x, p1.y, 0.5, 1) + __vset(dst, rx, ry, ru,1) + } + + __vset(dst, lx1, ly1, lu,1) + __vset(dst, p1.x - dlx1*rw, p1.y - dly1*rw, ru,1) + } else { + rx0,ry0,rx1,ry1: f32 + __chooseBevel(.INNER_BEVEL in p1.flags, p0, p1, -rw, &rx0, &ry0, &rx1, &ry1) + a0 := math.atan2(dly0, dlx0) + a1 := math.atan2(dly1, dlx1) + if a1 < a0 { + a1 += math.PI * 2 + } + + __vset(dst, p1.x + dlx0*rw, p1.y + dly0*rw, lu,1) + __vset(dst, rx0, ry0, ru,1) + + temp := int(math.ceil((a1 - a0) / math.PI * f32(ncap))) + n := clamp(temp, 2, ncap) + + for i := 0; i < n; i += 1 { + u := f32(i) / f32(n - 1) + a := a0 + u*(a1-a0) + lx := p1.x + math.cos(a) * lw + ly := p1.y + math.sin(a) * lw + __vset(dst, lx, ly, lu, 1) + __vset(dst, p1.x, p1.y, 0.5, 1) + } + + __vset(dst, p1.x + dlx1*rw, p1.y + dly1*rw, lu,1) + __vset(dst, rx1, ry1, ru,1) + } +} + +__bevelJoin :: proc( + dst: ^[]Vertex, + p0: ^Point, + p1: ^Point, + lw: f32, + rw: f32, + lu: f32, + ru: f32, +) { + dlx0 := p0.dy + dly0 := -p0.dx + dlx1 := p1.dy + dly1 := -p1.dx + + rx0, ry0, rx1, ry1: f32 + lx0, ly0, lx1, ly1: f32 + + if .LEFT in p1.flags { + __chooseBevel(.INNER_BEVEL in p1.flags, p0, p1, lw, &lx0,&ly0, &lx1,&ly1) + + __vset(dst, lx0, ly0, lu,1) + __vset(dst, p1.x - dlx0*rw, p1.y - dly0*rw, ru,1) + + if .BEVEL in p1.flags { + __vset(dst, lx0, ly0, lu,1) + __vset(dst, p1.x - dlx0*rw, p1.y - dly0*rw, ru,1) + + __vset(dst, lx1, ly1, lu,1) + __vset(dst, p1.x - dlx1*rw, p1.y - dly1*rw, ru,1) + } else { + rx0 = p1.x - p1.dmx * rw + ry0 = p1.y - p1.dmy * rw + + __vset(dst, p1.x, p1.y, 0.5,1) + __vset(dst, p1.x - dlx0*rw, p1.y - dly0*rw, ru,1) + + __vset(dst, rx0, ry0, ru,1) + __vset(dst, rx0, ry0, ru,1) + + __vset(dst, p1.x, p1.y, 0.5,1) + __vset(dst, p1.x - dlx1*rw, p1.y - dly1*rw, ru,1) + } + + __vset(dst, lx1, ly1, lu,1) + __vset(dst, p1.x - dlx1*rw, p1.y - dly1*rw, ru,1) + } else { + __chooseBevel(.INNER_BEVEL in p1.flags, p0, p1, -rw, &rx0,&ry0, &rx1,&ry1) + + __vset(dst, p1.x + dlx0*lw, p1.y + dly0*lw, lu,1) + __vset(dst, rx0, ry0, ru,1) + + if .BEVEL in p1.flags { + __vset(dst, p1.x + dlx0*lw, p1.y + dly0*lw, lu,1) + __vset(dst, rx0, ry0, ru,1) + + __vset(dst, p1.x + dlx1*lw, p1.y + dly1*lw, lu,1) + __vset(dst, rx1, ry1, ru,1) + } else { + lx0 = p1.x + p1.dmx * lw + ly0 = p1.y + p1.dmy * lw + + __vset(dst, p1.x + dlx0*lw, p1.y + dly0*lw, lu,1) + __vset(dst, p1.x, p1.y, 0.5,1) + + __vset(dst, lx0, ly0, lu,1) + __vset(dst, lx0, ly0, lu,1) + + __vset(dst, p1.x + dlx1*lw, p1.y + dly1*lw, lu,1) + __vset(dst, p1.x, p1.y, 0.5,1) + } + + __vset(dst, p1.x + dlx1*lw, p1.y + dly1*lw, lu,1) + __vset(dst, rx1, ry1, ru,1) + } +} + +__buttCapStart :: proc( + dst: ^[]Vertex, + p: ^Point, + dx, dy: f32, + w: f32, + d: f32, + aa: f32, + u0: f32, + u1: f32, +) { + px := p.x - dx * d + py := p.y - dy * d + dlx := dy + dly := -dx + __vset(dst, px + dlx*w - dx*aa, py + dly*w - dy*aa, u0,0) + __vset(dst, px - dlx*w - dx*aa, py - dly*w - dy*aa, u1,0) + __vset(dst, px + dlx*w, py + dly*w, u0,1) + __vset(dst, px - dlx*w, py - dly*w, u1,1) +} + +__buttCapEnd :: proc( + dst: ^[]Vertex, + p: ^Point, + dx, dy: f32, + w: f32, + d: f32, + aa: f32, + u0: f32, + u1: f32, +) { + px := p.x + dx * d + py := p.y + dy * d + dlx := dy + dly := -dx + __vset(dst, px + dlx*w, py + dly*w, u0,1) + __vset(dst, px - dlx*w, py - dly*w, u1,1) + __vset(dst, px + dlx*w + dx*aa, py + dly*w + dy*aa, u0,0) + __vset(dst, px - dlx*w + dx*aa, py - dly*w + dy*aa, u1,0) +} + +__roundCapStart :: proc( + dst: ^[]Vertex, + p: ^Point, + dx, dy: f32, + w: f32, + ncap: int, + u0: f32, + u1: f32, +) { + px := p.x + py := p.y + dlx := dy + dly := -dx + + for i in 0.. 0 { + iw = 1.0 / w + } + + // Calculate which joins needs extra vertices to append, and gather vertex count. + for path, i in &cache.paths { + pts := cache.points[path.first:] + p0 := &pts[path.count-1] + p1 := &pts[0] + nleft := 0 + path.nbevel = 0 + + for j in 0.. 0.000001) { + scale := 1.0 / dmr2 + if (scale > 600.0) { + scale = 600.0 + } + p1.dmx *= scale + p1.dmy *= scale + } + + // Clear flags, but keep the corner. + p1.flags = (.CORNER in p1.flags) ? { .CORNER } : {} + + // Keep track of left turns. + __cross = p1.dx * p0.dy - p0.dx * p1.dy + if __cross > 0.0 { + nleft += 1 + incl(&p1.flags, PointFlag.LEFT) + } + + // Calculate if we should use bevel or miter for inner join. + limit = max(1.01, min(p0.len, p1.len) * iw) + if (dmr2 * limit * limit) < 1.0 { + incl(&p1.flags, PointFlag.INNER_BEVEL) + } + + // Check to see if the corner needs to be beveled. + if .CORNER in p1.flags { + if (dmr2 * miterLimit*miterLimit) < 1.0 || lineJoin == .BEVEL || lineJoin == .ROUND { + incl(&p1.flags, PointFlag.BEVEL) + } + } + + if (.BEVEL in p1.flags) || (.INNER_BEVEL in p1.flags) { + path.nbevel += 1 + } + + p0 = p1 + p1 = mem.ptr_offset(p1, 1) + } + + path.convex = nleft == path.count + } +} + +// TODO could be done better? or not need dynamic +__allocTempVerts :: proc(ctx: ^Context, nverts: int) -> []Vertex { + resize(&ctx.cache.verts, nverts) + return ctx.cache.verts[:] +} + +__expandStroke :: proc( + ctx: ^Context, + w: f32, + fringe: f32, + lineCap: LineCapType, + lineJoin: LineCapType, + miterLimit: f32, +) -> bool { + cache := &ctx.cache + aa := fringe + u0 := f32(0.0) + u1 := f32(1.0) + ncap := __curveDivs(w, math.PI, ctx.tessTol) // Calculate divisions per half circle. + + w := w + w += aa * 0.5 + + // Disable the gradient used for antialiasing when antialiasing is not used. + if aa == 0.0 { + u0 = 0.5 + u1 = 0.5 + } + + __calculateJoins(ctx, w, lineJoin, miterLimit) + + // Calculate max vertex usage. + cverts := 0 + for path in &cache.paths { + loop := path.closed + + // TODO check if f32 calculation necessary? + if lineJoin == .ROUND { + cverts += (path.count + path.nbevel * int(ncap + 2) + 1) * 2 // plus one for loop + } else { + cverts += (path.count + path.nbevel*5 + 1) * 2 // plus one for loop + } + + if !loop { + // space for caps + if lineCap == .ROUND { + cverts += int(ncap*2 + 2)*2 + } else { + cverts += (3 + 3)*2 + } + } + } + + verts := __allocTempVerts(ctx, cverts) + dst_index: int + + for i in 0.. bool { + cache := &ctx.cache + aa := ctx.fringeWidth + fringe := w > 0.0 + __calculateJoins(ctx, w, lineJoin, miterLimit) + + // Calculate max vertex usage. + cverts := 0 + for path in &cache.paths { + cverts += path.count + path.nbevel + 1 + + if fringe { + cverts += (path.count + path.nbevel*5 + 1) * 2 // plus one for loop + } + } + + convex := len(cache.paths) == 1 && cache.paths[0].convex + verts := __allocTempVerts(ctx, cverts) + dst_index: int + + for path in &cache.paths { + pts := cache.points[path.first:] + p0, p1: ^Point + rw, lw, woff: f32 + ru, lu: f32 + + // Calculate shape vertices. + woff = 0.5*aa + dst := verts[dst_index:] + dst_start_length := len(dst) + + if fringe { + // Looping + p0 = &pts[path.count-1] + p1 = &pts[0] + + for j in 0.. f32 { + return f32(cmd) +} + +// Clears the current path and sub-paths. +BeginPath :: proc(ctx: ^Context) { + clear(&ctx.commands) + __clearPathCache(ctx) +} + +@(deferred_in=Fill) +FillScoped :: proc(ctx: ^Context) { + BeginPath(ctx) +} + +@(deferred_in=Stroke) +StrokeScoped :: proc(ctx: ^Context) { + BeginPath(ctx) +} + +@(deferred_in=Stroke) +FillStrokeScoped :: proc(ctx: ^Context) { + BeginPath(ctx) +} + +// Starts new sub-path with specified point as first point. +MoveTo :: proc(ctx: ^Context, x, y: f32) { + values := [3]f32 { __cmdf(.MOVE_TO), x, y } + __appendCommands(ctx, values[:]) +} + +// Adds line segment from the last point in the path to the specified point. +LineTo :: proc(ctx: ^Context, x, y: f32) { + values := [3]f32 { __cmdf(.LINE_TO), x, y } + __appendCommands(ctx, values[:]) +} + +// Adds cubic bezier segment from last point in the path via two control points to the specified point. +BezierTo :: proc( + ctx: ^Context, + c1x, c1y: f32, + c2x, c2y: f32, + x, y: f32, +) { + values := [?]f32 { __cmdf(.BEZIER_TO), c1x, c1y, c2x, c2y, x, y } + __appendCommands(ctx, values[:]) +} + +// Adds quadratic bezier segment from last point in the path via a control point to the specified point. +QuadTo :: proc(ctx: ^Context, cx, cy, x, y: f32) { + x0 := ctx.commandx + y0 := ctx.commandy + values := [?]f32 { + __cmdf(.BEZIER_TO), + x0 + 2 / 3 * (cx - x0), + y0 + 2 / 3 * (cy - y0), + x + 2 / 3 * (cx - x), + y + 2 / 3 * (cy - y), + x, + y, + } + __appendCommands(ctx, values[:]) +} + +// Adds an arc segment at the corner defined by the last path point, and two specified points. +ArcTo :: proc( + ctx: ^Context, + x1, y1: f32, + x2, y2: f32, + radius: f32, +) { + if len(ctx.commands) == 0 { + return + } + + x0 := ctx.commandx + y0 := ctx.commandy + // Handle degenerate cases. + if __ptEquals(x0,y0, x1,y1, ctx.distTol) || + __ptEquals(x1,y1, x2,y2, ctx.distTol) || + __distPtSeg(x1,y1, x0,y0, x2,y2) < ctx.distTol*ctx.distTol || + radius < ctx.distTol { + LineTo(ctx, x1, y1) + return + } + + // Calculate tangential circle to lines (x0,y0)-(x1,y1) and (x1,y1)-(x2,y2). + dx0 := x0-x1 + dy0 := y0-y1 + dx1 := x2-x1 + dy1 := y2-y1 + __normalize(&dx0,&dy0) + __normalize(&dx1,&dy1) + a := math.acos(dx0*dx1 + dy0*dy1) + d := radius / math.tan(a / 2.0) + + if d > 10000 { + LineTo(ctx, x1, y1) + return + } + + a0, a1, cx, cy: f32 + direction: Winding + + if __cross(dx0,dy0, dx1,dy1) > 0.0 { + cx = x1 + dx0*d + dy0*radius + cy = y1 + dy0*d + -dx0*radius + a0 = math.atan2(dx0, -dy0) + a1 = math.atan2(-dx1, dy1) + direction = .CW + } else { + cx = x1 + dx0*d + -dy0*radius + cy = y1 + dy0*d + dx0*radius + a0 = math.atan2(-dx0, dy0) + a1 = math.atan2(dx1, -dy1) + direction = .CCW + } + + Arc(ctx, cx, cy, radius, a0, a1, direction) +} + +// Creates new circle arc shaped sub-path. The arc center is at cx,cy, the arc radius is r, +// and the arc is drawn from angle a0 to a1, and swept in direction dir (NVG_CCW, or NVG_CW). +// Angles are specified in radians. +Arc :: proc(ctx: ^Context, cx, cy, r, a0, a1: f32, dir: Winding) { + move: Commands = .LINE_TO if len(ctx.commands) > 0 else .MOVE_TO + + // Clamp angles + da := a1 - a0 + if dir == .CW { + if abs(da) >= math.PI*2 { + da = math.PI*2 + } else { + for da < 0.0 { + da += math.PI*2 + } + } + } else { + if abs(da) >= math.PI*2 { + da = -math.PI*2 + } else { + for da > 0.0 { + da -= math.PI*2 + } + } + } + + // Split arc into max 90 degree segments. + ndivs := max(1, min((int)(abs(da) / (math.PI*0.5) + 0.5), 5)) + hda := (da / f32(ndivs)) / 2.0 + kappa := abs(4.0 / 3.0 * (1.0 - math.cos(hda)) / math.sin(hda)) + + if dir == .CCW { + kappa = -kappa + } + + values: [3 + 5 * 7 + 100]f32 + nvals := 0 + + px, py, ptanx, ptany: f32 + for i in 0..=ndivs { + a := a0 + da * f32(i) / f32(ndivs) + dx := math.cos(a) + dy := math.sin(a) + x := cx + dx*r + y := cy + dy*r + tanx := -dy*r*kappa + tany := dx*r*kappa + + if i == 0 { + values[nvals] = __cmdf(move); nvals += 1 + values[nvals] = x; nvals += 1 + values[nvals] = y; nvals += 1 + } else { + values[nvals] = __cmdf(.BEZIER_TO); nvals += 1 + values[nvals] = px + ptanx; nvals += 1 + values[nvals] = py + ptany; nvals += 1 + values[nvals] = x-tanx; nvals += 1 + values[nvals] = y-tany; nvals += 1 + values[nvals] = x; nvals += 1 + values[nvals] = y; nvals += 1 + } + px = x + py = y + ptanx = tanx + ptany = tany + } + + // stored internally + __appendCommands(ctx, values[:nvals]) +} + +// Closes current sub-path with a line segment. +ClosePath :: proc(ctx: ^Context) { + values := [1]f32 { __cmdf(.CLOSE) } + __appendCommands(ctx, values[:]) +} + +// Sets the current sub-path winding, see NVGwinding and NVGsolidity. +PathWinding :: proc(ctx: ^Context, direction: Winding) { + values := [2]f32 { __cmdf(.WINDING), f32(direction) } + __appendCommands(ctx, values[:]) +} + +// same as path_winding but with different enum +PathSolidity :: proc(ctx: ^Context, solidity: Solidity) { + values := [2]f32 { __cmdf(.WINDING), f32(solidity) } + __appendCommands(ctx, values[:]) +} + +// Creates new rectangle shaped sub-path. +Rect :: proc(ctx: ^Context, x, y, w, h: f32) { + values := [?]f32 { + __cmdf(.MOVE_TO), x, y, + __cmdf(.LINE_TO), x, y + h, + __cmdf(.LINE_TO), x + w, y + h, + __cmdf(.LINE_TO), x + w, y, + __cmdf(.CLOSE), + } + __appendCommands(ctx, values[:]) +} + +// Creates new rounded rectangle shaped sub-path. +RoundedRect :: proc(ctx: ^Context, x, y, w, h, radius: f32) { + RoundedRectVarying(ctx, x, y, w, h, radius, radius, radius, radius) +} + +// Creates new rounded rectangle shaped sub-path with varying radii for each corner. +RoundedRectVarying :: proc( + ctx: ^Context, + x, y: f32, + w, h: f32, + radius_top_left: f32, + radius_top_right: f32, + radius_bottom_right: f32, + radius_bottom_left: f32, +) { + if radius_top_left < 0.1 && radius_top_right < 0.1 && radius_bottom_right < 0.1 && radius_bottom_left < 0.1 { + Rect(ctx, x, y, w, h) + } else { + halfw := abs(w) * 0.5 + halfh := abs(h) * 0.5 + rxBL := min(radius_bottom_left, halfw) * math.sign(w) + ryBL := min(radius_bottom_left, halfh) * math.sign(h) + rxBR := min(radius_bottom_right, halfw) * math.sign(w) + ryBR := min(radius_bottom_right, halfh) * math.sign(h) + rxTR := min(radius_top_right, halfw) * math.sign(w) + ryTR := min(radius_top_right, halfh) * math.sign(h) + rxTL := min(radius_top_left, halfw) * math.sign(w) + ryTL := min(radius_top_left, halfh) * math.sign(h) + values := [?]f32 { + __cmdf(.MOVE_TO), x, y + ryTL, + __cmdf(.LINE_TO), x, y + h - ryBL, + __cmdf(.BEZIER_TO), x, y + h - ryBL*(1 - KAPPA), x + rxBL*(1 - KAPPA), y + h, x + rxBL, y + h, + __cmdf(.LINE_TO), x + w - rxBR, y + h, + __cmdf(.BEZIER_TO), x + w - rxBR*(1 - KAPPA), y + h, x + w, y + h - ryBR*(1 - KAPPA), x + w, y + h - ryBR, + __cmdf(.LINE_TO), x + w, y + ryTR, + __cmdf(.BEZIER_TO), x + w, y + ryTR*(1 - KAPPA), x + w - rxTR*(1 - KAPPA), y, x + w - rxTR, y, + __cmdf(.LINE_TO), x + rxTL, y, + __cmdf(.BEZIER_TO), x + rxTL*(1 - KAPPA), y, x, y + ryTL*(1 - KAPPA), x, y + ryTL, + __cmdf(.CLOSE), + } + __appendCommands(ctx, values[:]) + } +} + +// Creates new ellipse shaped sub-path. +Ellipse :: proc(ctx: ^Context, cx, cy, rx, ry: f32) { + values := [?]f32 { + __cmdf(.MOVE_TO), cx-rx, cy, + __cmdf(.BEZIER_TO), cx-rx, cy+ry*KAPPA, cx-rx*KAPPA, cy+ry, cx, cy+ry, + __cmdf(.BEZIER_TO), cx+rx*KAPPA, cy+ry, cx+rx, cy+ry*KAPPA, cx+rx, cy, + __cmdf(.BEZIER_TO), cx+rx, cy-ry*KAPPA, cx+rx*KAPPA, cy-ry, cx, cy-ry, + __cmdf(.BEZIER_TO), cx-rx*KAPPA, cy-ry, cx-rx, cy-ry*KAPPA, cx-rx, cy, + __cmdf(.CLOSE) + } + __appendCommands(ctx, values[:]) +} + +// Creates new circle shaped sub-path. +Circle :: #force_inline proc(ctx: ^Context, cx, cy: f32, radius: f32) { + Ellipse(ctx, cx, cy, radius, radius) +} + +// Fills the current path with current fill style. +Fill :: proc(ctx: ^Context) { + state := __getState(ctx) + fill_paint := state.fill + + __flattenPaths(ctx) + + if ctx.params.edgeAntiAlias && state.shapeAntiAlias { + __expandFill(ctx, ctx.fringeWidth, .MITER, 2.4) + } else { + __expandFill(ctx, 0, .MITER, 2.4) + } + + // apply global alpha + fill_paint.innerColor.a *= state.alpha + fill_paint.outerColor.a *= state.alpha + + assert(ctx.params.renderFill != nil) + ctx.params.renderFill( + ctx.params.userPtr, + &fill_paint, + state.compositeOperation, + &state.scissor, + ctx.fringeWidth, + ctx.cache.bounds, + ctx.cache.paths[:], + ) + + for path in &ctx.cache.paths { + ctx.fillTriCount += len(path.fill) - 2 + ctx.fillTriCount += len(path.stroke) - 2 + ctx.drawCallCount += 2 + } +} + +// Fills the current path with current stroke style. +Stroke :: proc(ctx: ^Context) { + state := __getState(ctx) + scale := __getAverageScale(state.xform[:]) + strokeWidth := clamp(state.strokeWidth * scale, 0, 200) + stroke_paint := state.stroke + + if strokeWidth < ctx.fringeWidth { + // If the stroke width is less than pixel size, use alpha to emulate coverage. + // Since coverage is area, scale by alpha*alpha. + alpha := clamp(strokeWidth / ctx.fringeWidth, 0, 1) + stroke_paint.innerColor.a *= alpha * alpha + stroke_paint.outerColor.a *= alpha * alpha + strokeWidth = ctx.fringeWidth + } + + // apply global alpha + stroke_paint.innerColor.a *= state.alpha + stroke_paint.outerColor.a *= state.alpha + + __flattenPaths(ctx) + + if ctx.params.edgeAntiAlias && state.shapeAntiAlias { + __expandStroke(ctx, strokeWidth * 0.5, ctx.fringeWidth, state.lineCap, state.lineJoin, state.miterLimit) + } else { + __expandStroke(ctx, strokeWidth * 0.5, 0, state.lineCap, state.lineJoin, state.miterLimit) + } + + assert(ctx.params.renderStroke != nil) + ctx.params.renderStroke( + ctx.params.userPtr, + &stroke_paint, + state.compositeOperation, + &state.scissor, + ctx.fringeWidth, + strokeWidth, + ctx.cache.paths[:], + ) + + for path in &ctx.cache.paths { + ctx.strokeTriCount += len(path.stroke) - 2 + ctx.drawCallCount += 1 + } +} + +DebugDumpPathCache :: proc(ctx: ^Context) { + fmt.printf("~~~~~~~~~~~~~Dumping %d cached paths\n", len(ctx.cache.paths)) + + for path, i in &ctx.cache.paths { + fmt.printf(" - Path %d\n", i) + + if len(path.fill) != 0 { + fmt.printf(" - fill: %d\n", len(path.fill)) + + for j in 0.. int { + return fontstash.AddFontPath(&ctx.fs, name, filename) +} + +// Creates font by loading it from the specified memory chunk. +// Returns handle to the font. +CreateFontMem :: proc(ctx: ^Context, name: string, slice: []byte) -> int { + return fontstash.AddFontMem(&ctx.fs, name, slice) +} + +// Finds a loaded font of specified name, and returns handle to it, or -1 if the font is not found. +FindFont :: proc(ctx: ^Context, name: string) -> int { + if name == "" { + return -1 + } + + return fontstash.GetFontByName(&ctx.fs, name) +} + +// Adds a fallback font by handle. +AddFallbackFontId :: proc(ctx: ^Context, base_font, fallback_font: int) -> bool { + if base_font == -1 || fallback_font == -1 { + return false + } + + return fontstash.AddFallbackFont(&ctx.fs, base_font, fallback_font) +} + +// Adds a fallback font by name. +AddFallbackFont :: proc(ctx: ^Context, base_font: string, fallback_font: string) -> bool { + return AddFallbackFontId( + ctx, + FindFont(ctx, base_font), + FindFont(ctx, fallback_font), + ) +} + +// Resets fallback fonts by handle. +ResetFallbackFontsId :: proc(ctx: ^Context, base_font: int) { + fontstash.ResetFallbackFont(&ctx.fs, base_font) +} + +// Resets fallback fonts by name. +ResetFallbackFonts :: proc(ctx: ^Context, base_font: string) { + fontstash.ResetFallbackFont(&ctx.fs, FindFont(ctx, base_font)) +} + +// Sets the font size of current text style. +FontSize :: proc(ctx: ^Context, size: f32) { + state := __getState(ctx) + state.fontSize = size +} + +// Sets the blur of current text style. +FontBlur :: proc(ctx: ^Context, blur: f32) { + state := __getState(ctx) + state.fontBlur = blur +} + +// Sets the letter spacing of current text style. +TextLetterSpacing :: proc(ctx: ^Context, spacing: f32) { + state := __getState(ctx) + state.letterSpacing = spacing +} + +// Sets the proportional line height of current text style. The line height is specified as multiple of font size. +TextLineHeight :: proc(ctx: ^Context, lineHeight: f32) { + state := __getState(ctx) + state.lineHeight = lineHeight +} + +// Sets the horizontal text align of current text style +TextAlignHorizontal :: proc(ctx: ^Context, align: AlignHorizontal) { + state := __getState(ctx) + state.alignHorizontal = align +} + +// Sets the vertical text align of current text style +TextAlignVertical :: proc(ctx: ^Context, align: AlignVertical) { + state := __getState(ctx) + state.alignVertical = align +} + +// Sets the text align of current text style, see NVGalign for options. +TextAlign :: proc(ctx: ^Context, ah: AlignHorizontal, av: AlignVertical) { + state := __getState(ctx) + state.alignHorizontal = ah + state.alignVertical = av +} + +// Sets the font face based on specified name of current text style. +FontFaceId :: proc(ctx: ^Context, font: int) { + state := __getState(ctx) + state.fontId = font +} + +// Sets the font face based on specified name of current text style. +FontFace :: proc(ctx: ^Context, font: string) { + state := __getState(ctx) + state.fontId = fontstash.GetFontByName(&ctx.fs, font) +} + +__quantize :: proc(a, d: f32) -> f32 { + return f32(int(a / d + 0.5)) * d +} + +__getFontScale :: proc(state: ^State) -> f32 { + return min(__quantize(__getAverageScale(state.xform[:]), 0.01), 4.0) +} + +__flushTextTexture :: proc(ctx: ^Context) { + dirty: [4]f32 + assert(ctx.params.renderUpdateTexture != nil) + + + if fontstash.ValidateTexture(&ctx.fs, &dirty) { + font_image := ctx.fontImages[ctx.fontImageIdx] + + // Update texture + if font_image != 0 { + data := ctx.fs.textureData + x := dirty[0] + y := dirty[1] + w := dirty[2] - dirty[0] + h := dirty[3] - dirty[1] + ctx.params.renderUpdateTexture(ctx.params.userPtr, font_image, int(x), int(y), int(w), int(h), data) + } + } +} + +__allocTextAtlas :: proc(ctx: ^Context) -> bool { + __flushTextTexture(ctx) + + if ctx.fontImageIdx >= MAX_FONTIMAGES - 1 { + return false + } + + // if next fontImage already have a texture + iw, ih: int + if ctx.fontImages[ctx.fontImageIdx+1] != 0 { + iw, ih = ImageSize(ctx, ctx.fontImages[ctx.fontImageIdx+1]) + } else { // calculate the new font image size and create it. + iw, ih = ImageSize(ctx, ctx.fontImages[ctx.fontImageIdx]) + + if iw > ih { + ih *= 2 + } else { + iw *= 2 + } + + if iw > MAX_FONTIMAGE_SIZE || ih > MAX_FONTIMAGE_SIZE { + iw = MAX_FONTIMAGE_SIZE + ih = MAX_FONTIMAGE_SIZE + } + + ctx.fontImages[ctx.fontImageIdx + 1] = ctx.params.renderCreateTexture(ctx.params.userPtr, .Alpha, iw, ih, {}, nil) + } + + ctx.fontImageIdx += 1 + fontstash.ResetAtlas(&ctx.fs, iw, ih) + + return true +} + +__renderText :: proc(ctx: ^Context, verts: []Vertex) { + // disallow 0 + if len(verts) == 0 { + return + } + + state := __getState(ctx) + paint := state.fill + + // Render triangles. + paint.image = ctx.fontImages[ctx.fontImageIdx] + + // Apply global alpha + paint.innerColor.a *= state.alpha + paint.outerColor.a *= state.alpha + + ctx.params.renderTriangles(ctx.params.userPtr, &paint, state.compositeOperation, &state.scissor, verts, ctx.fringeWidth) + + ctx.drawCallCount += 1 + ctx.textTriCount += len(verts) / 3 +} + +__isTransformFlipped :: proc(xform: []f32) -> bool { + det := xform[0] * xform[3] - xform[2] * xform[1] + return det < 0 +} + +// draw a single codepoint, useful for icons +TextIcon :: proc(ctx: ^Context, x, y: f32, codepoint: rune) -> f32 { + state := __getState(ctx) + scale := __getFontScale(state) * ctx.devicePxRatio + invscale := f32(1.0) / scale + is_flipped := __isTransformFlipped(state.xform[:]) + + if state.fontId == -1 { + return x + } + + fs := &ctx.fs + fontstash.SetSize(fs, state.fontSize * scale) + fontstash.SetSpacing(fs, state.letterSpacing * scale) + fontstash.SetBlur(fs, state.fontBlur * scale) + fontstash.SetAlignHorizontal(fs, state.alignHorizontal) + fontstash.SetAlignVertical(fs, state.alignVertical) + fontstash.SetFont(fs, state.fontId) + + // fontstash internals + fstate := fontstash.__getState(fs) + font := fontstash.__getFont(fs, state.fontId) + isize := i16(fstate.size * 10) + iblur := i16(fstate.blur) + glyph := fontstash.__getGlyph(fs, font, codepoint, isize, iblur) + fscale := fontstash.__getPixelHeightScale(font, f32(isize) / 10) + + // transform x / y + x := x * scale + y := y * scale + switch fstate.ah { + case .LEFT: {} + case .CENTER: { + width := fontstash.CodepointWidth(font, codepoint, fscale) + x = math.round(x - width * 0.5) + } + case .RIGHT: { + width := fontstash.CodepointWidth(font, codepoint, fscale) + x -= width + } + } + + // align vertically + y = math.round(y + fontstash.__getVerticalAlign(fs, font, fstate.av, isize)) + nextx := f32(x) + nexty := f32(y) + + if glyph != nil { + q: fontstash.Quad + fontstash.__getQuad(fs, font, -1, glyph, fscale, fstate.spacing, &nextx, &nexty, &q) + + if is_flipped { + q.y0, q.y1 = q.y1, q.y0 + q.t0, q.t1 = q.t1, q.t0 + } + + // single glyph only + verts := __allocTempVerts(ctx, 6) + c: [4 * 2]f32 + + // Transform corners. + TransformPoint(&c[0], &c[1], state.xform, q.x0 * invscale, q.y0 * invscale) + TransformPoint(&c[2], &c[3], state.xform, q.x1 * invscale, q.y0 * invscale) + TransformPoint(&c[4], &c[5], state.xform, q.x1 * invscale, q.y1 * invscale) + TransformPoint(&c[6], &c[7], state.xform, q.x0 * invscale, q.y1 * invscale) + + // Create triangles + verts[0] = { c[0], c[1], q.s0, q.t0 } + verts[1] = { c[4], c[5], q.s1, q.t1 } + verts[2] = { c[2], c[3], q.s1, q.t0 } + verts[3] = { c[0], c[1], q.s0, q.t0 } + verts[4] = { c[6], c[7], q.s0, q.t1 } + verts[5] = { c[4], c[5], q.s1, q.t1 } + + ctx.textureDirty = true + __renderText(ctx, verts[:]) + } + + return nextx / scale +} + +// Draws text string at specified location. If end is specified only the sub-string up to the end is drawn. +Text :: proc(ctx: ^Context, x, y: f32, text: string) -> f32 { + state := __getState(ctx) + scale := __getFontScale(state) * ctx.devicePxRatio + invscale := f32(1.0) / scale + is_flipped := __isTransformFlipped(state.xform[:]) + + if state.fontId == -1 { + return x + } + + fs := &ctx.fs + fontstash.SetSize(fs, state.fontSize * scale) + fontstash.SetSpacing(fs, state.letterSpacing * scale) + fontstash.SetBlur(fs, state.fontBlur * scale) + fontstash.SetAlignHorizontal(fs, state.alignHorizontal) + fontstash.SetAlignVertical(fs, state.alignVertical) + fontstash.SetFont(fs, state.fontId) + + cverts := max(2, len(text)) * 6 // conservative estimate. + verts := __allocTempVerts(ctx, cverts) + nverts: int + + iter := fontstash.TextIterInit(fs, x * scale, y * scale, text) + prev_iter := iter + q: fontstash.Quad + for fontstash.TextIterNext(&ctx.fs, &iter, &q) { + c: [4 * 2]f32 + + if iter.previousGlyphIndex == -1 { // can not retrieve glyph? + if nverts != 0 { + __renderText(ctx, verts[:]) + nverts = 0 + } + + if !__allocTextAtlas(ctx) { + break // no memory :( + } + + iter = prev_iter + fontstash.TextIterNext(fs, &iter, &q) // try again + + if iter.previousGlyphIndex == -1 { + // still can not find glyph? + break + } + } + + prev_iter = iter + if is_flipped { + q.y0, q.y1 = q.y1, q.y0 + q.t0, q.t1 = q.t1, q.t0 + } + + // Transform corners. + TransformPoint(&c[0], &c[1], state.xform, q.x0 * invscale, q.y0 * invscale) + TransformPoint(&c[2], &c[3], state.xform, q.x1 * invscale, q.y0 * invscale) + TransformPoint(&c[4], &c[5], state.xform, q.x1 * invscale, q.y1 * invscale) + TransformPoint(&c[6], &c[7], state.xform, q.x0 * invscale, q.y1 * invscale) + + // Create triangles + if nverts + 6 <= cverts { + verts[nverts] = { c[0], c[1], q.s0, q.t0 } + verts[nverts + 1] = { c[4], c[5], q.s1, q.t1 } + verts[nverts + 2] = { c[2], c[3], q.s1, q.t0 } + verts[nverts + 3] = { c[0], c[1], q.s0, q.t0 } + verts[nverts + 4] = { c[6], c[7], q.s0, q.t1 } + verts[nverts + 5] = { c[4], c[5], q.s1, q.t1 } + nverts += 6 + } + } + + ctx.textureDirty = true + __renderText(ctx, verts[:nverts]) + + return iter.nextx / scale +} + +// Returns the vertical metrics based on the current text style. +// Measured values are returned in local coordinate space. +TextMetrics :: proc(ctx: ^Context) -> (ascender, descender, lineHeight: f32) { + state := __getState(ctx) + scale := __getFontScale(state) * ctx.devicePxRatio + invscale := f32(1.0) / scale + + if state.fontId == -1 { + return + } + + fs := &ctx.fs + fontstash.SetSize(fs, state.fontSize*scale) + fontstash.SetSpacing(fs, state.letterSpacing*scale) + fontstash.SetBlur(fs, state.fontBlur*scale) + fontstash.SetAlignHorizontal(fs, state.alignHorizontal) + fontstash.SetAlignVertical(fs, state.alignVertical) + fontstash.SetFont(fs, state.fontId) + + ascender, descender, lineHeight = fontstash.VerticalMetrics(fs) + ascender *= invscale + descender *= invscale + lineHeight *= invscale + return +} + +// Measures the specified text string. Parameter bounds should be a pointer to float[4], +// if the bounding box of the text should be returned. The bounds value are [xmin,ymin, xmax,ymax] +// Returns the horizontal advance of the measured text (i.e. where the next character should drawn). +// Measured values are returned in local coordinate space. +TextBounds :: proc( + ctx: ^Context, + x, y: f32, + input: string, + bounds: ^[4]f32 = nil, +) -> (advance: f32) { + state := __getState(ctx) + scale := __getFontScale(state) * ctx.devicePxRatio + invscale := f32(1.0) / scale + + if state.fontId == -1 { + return {} + } + + fs := &ctx.fs + fontstash.SetSize(fs, state.fontSize*scale) + fontstash.SetSpacing(fs, state.letterSpacing*scale) + fontstash.SetBlur(fs, state.fontBlur*scale) + fontstash.SetAlignHorizontal(fs, state.alignHorizontal) + fontstash.SetAlignVertical(fs, state.alignVertical) + fontstash.SetFont(fs, state.fontId) + + width := fontstash.TextBounds(fs, input, x * scale, y * scale, bounds) + + // Use line bounds for height. + one, two := fontstash.LineBounds(fs, y * scale) + + if bounds != nil { + bounds[1] = one + bounds[3] = two + bounds[0] *= invscale + bounds[1] *= invscale + bounds[2] *= invscale + bounds[3] *= invscale + } + + return width * invscale +} + +// text row with relative byte offsets into a string +Text_Row :: struct { + start: int, + end: int, + next: int, + width: f32, + minx, maxx: f32, +} + +Codepoint_Type :: enum { + Space, + Newline, + Char, + CJK, +} + +// Draws multi-line text string at specified location wrapped at the specified width. If end is specified only the sub-string up to the end is drawn. +// White space is stripped at the beginning of the rows, the text is split at word boundaries or when new-line characters are encountered. +// Words longer than the max width are slit at nearest character (i.e. no hyphenation). +TextBox :: proc( + ctx: ^Context, + x, y: f32, + break_row_width: f32, + input: string, +) { + state := __getState(ctx) + rows: [2]Text_Row + + if state.fontId == -1 { + return + } + + _, _, lineHeight := TextMetrics(ctx) + old_align := state.alignHorizontal + defer state.alignHorizontal = old_align + state.alignHorizontal = .LEFT + rows_mod := rows[:] + + y := y + input := input + for nrows, input_last in TextBreakLines(ctx, &input, break_row_width, &rows_mod) { + for i in 0.. (nrows: int, last: string, ok: bool) { + state := __getState(ctx) + scale := __getFontScale(state) * ctx.devicePxRatio + invscale := 1.0 / scale + + row_start_x, row_width, row_min_x, row_max_x: f32 + max_rows := len(rows) + + row_start: int = -1 + row_end: int = -1 + word_start: int = -1 + break_end: int = -1 + word_start_x, word_min_x: f32 + + break_width, break_max_x: f32 + type := Codepoint_Type.Space + ptype := Codepoint_Type.Space + pcodepoint: rune + + if max_rows == 0 || state.fontId == -1 || len(text) == 0 { + return + } + + fs := &ctx.fs + fontstash.SetSize(fs, state.fontSize * scale) + fontstash.SetSpacing(fs, state.letterSpacing * scale) + fontstash.SetBlur(fs, state.fontBlur * scale) + fontstash.SetAlignHorizontal(fs, state.alignHorizontal) + fontstash.SetAlignVertical(fs, state.alignVertical) + fontstash.SetFont(fs, state.fontId) + + break_row_width := break_row_width * scale + iter := fontstash.TextIterInit(fs, 0, 0, text^) + prev_iter := iter + q: fontstash.Quad + stopped_early: bool + + for fontstash.TextIterNext(fs, &iter, &q) { + if iter.previousGlyphIndex < 0 && __allocTextAtlas(ctx) { // can not retrieve glyph? + iter = prev_iter + fontstash.TextIterNext(fs, &iter, &q) // try again + } + prev_iter = iter + + switch iter.codepoint { + case '\t', '\v', '\f', ' ', 0x00a0: { + // NBSP + type = .Space + } + + case '\n': { + type = pcodepoint == 13 ? .Space : .Newline + } + + case '\r': { + type = pcodepoint == 10 ? .Space : .Newline + } + + case 0x0085: { + // NEL + type = .Newline + } + + case: { + if (iter.codepoint >= 0x4E00 && iter.codepoint <= 0x9FFF) || + (iter.codepoint >= 0x3000 && iter.codepoint <= 0x30FF) || + (iter.codepoint >= 0xFF00 && iter.codepoint <= 0xFFEF) || + (iter.codepoint >= 0x1100 && iter.codepoint <= 0x11FF) || + (iter.codepoint >= 0x3130 && iter.codepoint <= 0x318F) || + (iter.codepoint >= 0xAC00 && iter.codepoint <= 0xD7AF) { + type = .CJK + } else { + type = .Char + } + } + } + + if type == .Newline { + // Always handle new lines. + rows[nrows].start = row_start != -1 ? row_start : iter.str + rows[nrows].end = row_end != -1 ? row_end : iter.str + rows[nrows].width = row_width * invscale + rows[nrows].minx = row_min_x * invscale + rows[nrows].maxx = row_max_x * invscale + rows[nrows].next = iter.next + nrows += 1 + + if nrows >= max_rows { + stopped_early = true + break + } + + // Set nil break point + break_end = row_start + break_width = 0.0 + break_max_x = 0.0 + // Indicate to skip the white space at the beginning of the row. + row_start = -1 + row_end = -1 + row_width = 0 + row_min_x = 0 + row_max_x = 0 + } else { + if row_start == -1 { + // Skip white space until the beginning of the line + if type == .Char || type == .CJK { + // The current char is the row so far + row_start_x = iter.x + row_start = iter.str + row_end = iter.next + row_width = iter.nextx - row_start_x + row_min_x = q.x0 - row_start_x + row_max_x = q.x1 - row_start_x + word_start = iter.str + word_start_x = iter.x + word_min_x = q.x0 - row_start_x + // Set nil break point + break_end = row_start + break_width = 0.0 + break_max_x = 0.0 + } + } else { + next_width := iter.nextx - row_start_x + + // track last non-white space character + if type == .Char || type == .CJK { + row_end = iter.next + row_width = iter.nextx - row_start_x + row_max_x = q.x1 - row_start_x + } + // track last end of a word + if ((ptype == .Char || ptype == .CJK) && type == .Space) || type == .CJK { + break_end = iter.str + break_width = row_width + break_max_x = row_max_x + } + // track last beginning of a word + if ((ptype == .Space && (type == .Char || type == .CJK)) || type == .CJK) { + word_start = iter.str + word_start_x = iter.x + word_min_x = q.x0 + } + + // Break to new line when a character is beyond break width. + if (type == .Char || type == .CJK) && next_width > break_row_width { + // The run length is too long, need to break to new line. + if (break_end == row_start) { + // The current word is longer than the row length, just break it from here. + rows[nrows].start = row_start + rows[nrows].end = iter.str + rows[nrows].width = row_width * invscale + rows[nrows].minx = row_min_x * invscale + rows[nrows].maxx = row_max_x * invscale + rows[nrows].next = iter.str + nrows += 1 + + if nrows >= max_rows { + stopped_early = true + break + } + + row_start_x = iter.x + row_start = iter.str + row_end = iter.next + row_width = iter.nextx - row_start_x + row_min_x = q.x0 - row_start_x + row_max_x = q.x1 - row_start_x + word_start = iter.str + word_start_x = iter.x + word_min_x = q.x0 - row_start_x + } else { + // Break the line from the end of the last word, and start new line from the beginning of the new. + rows[nrows].start = row_start + rows[nrows].end = break_end + rows[nrows].width = break_width * invscale + rows[nrows].minx = row_min_x * invscale + rows[nrows].maxx = break_max_x * invscale + rows[nrows].next = word_start + nrows += 1 + if nrows >= max_rows { + stopped_early = true + break + } + // Update row + row_start_x = word_start_x + row_start = word_start + row_end = iter.next + row_width = iter.nextx - row_start_x + row_min_x = word_min_x - row_start_x + row_max_x = q.x1 - row_start_x + } + // Set nil break point + break_end = row_start + break_width = 0.0 + break_max_x = 0.0 + } + } + } + + pcodepoint = iter.codepoint + ptype = type + } + + // Break the line from the end of the last word, and start new line from the beginning of the new. + if !stopped_early && row_start != -1 { + rows[nrows].start = row_start + rows[nrows].end = row_end + rows[nrows].width = row_width * invscale + rows[nrows].minx = row_min_x * invscale + rows[nrows].maxx = row_max_x * invscale + rows[nrows].next = iter.end + nrows += 1 + } + + // NOTE a bit hacky, row.start / row.end need to work with last string range + last = text^ + // advance early + next := rows[nrows - 1].next + text^ = text[next:] + // terminate the for loop on non ok + ok = nrows != 0 + + return +} + +// Measures the specified multi-text string. Parameter bounds should be a pointer to float[4], +// if the bounding box of the text should be returned. The bounds value are [xmin,ymin, xmax,ymax] +// Measured values are returned in local coordinate space. +TextBoxBounds :: proc( + ctx: ^Context, + x, y: f32, + breakRowWidth: f32, + input: string, + bounds: ^[4]f32, +) { + state := __getState(ctx) + rows: [2]Text_Row + scale := __getFontScale(state) * ctx.devicePxRatio + invscale := f32(1.0) / scale + + if state.fontId == -1 { + if bounds != nil { + bounds^ = {} + } + + return + } + + // alignment + halign := state.alignHorizontal + valign := state.alignVertical + old_align := state.alignHorizontal + defer state.alignHorizontal = old_align + state.alignHorizontal = .LEFT + + _, _, lineh := TextMetrics(ctx) + minx, maxx := x, x + miny, maxy := y, y + + fs := &ctx.fs + fontstash.SetSize(fs, state.fontSize * scale) + fontstash.SetSpacing(fs, state.letterSpacing * scale) + fontstash.SetBlur(fs, state.fontBlur * scale) + fontstash.SetAlignHorizontal(fs, state.alignHorizontal) + fontstash.SetAlignVertical(fs, state.alignVertical) + fontstash.SetFont(fs, state.fontId) + rminy, rmaxy := fontstash.LineBounds(fs, 0) + rminy *= invscale + rmaxy *= invscale + + input := input + rows_mod := rows[:] + y := y + + for nrows, input_last in TextBreakLines(ctx, &input, breakRowWidth, &rows_mod) { + for i in 0.. int { + state := __getState(ctx) + scale := __getFontScale(state) * ctx.devicePxRatio + + if state.fontId == -1 || len(text) == 0 { + return 0 + } + + fs := &ctx.fs + fontstash.SetSize(fs, state.fontSize*scale) + fontstash.SetSpacing(fs, state.letterSpacing*scale) + fontstash.SetBlur(fs, state.fontBlur*scale) + fontstash.SetAlignHorizontal(fs, state.alignHorizontal) + fontstash.SetAlignVertical(fs, state.alignVertical) + fontstash.SetFont(fs, state.fontId) + + iter := fontstash.TextIterInit(fs, 0, 0, text) + prev_iter := iter + q: fontstash.Quad + npos: int + for fontstash.TextIterNext(fs, &iter, &q) { + if iter.previousGlyphIndex < 0 && __allocTextAtlas(ctx) { // can not retrieve glyph? + iter = prev_iter + fontstash.TextIterNext(fs, &iter, &q) // try again + } + + prev_iter = iter + positions[npos].str = iter.str + positions[npos].x = iter.x + x + positions[npos].minx = min(iter.x, q.x0) + x + positions[npos].maxx = max(iter.nextx, q.x1) + x + npos += 1 + + if npos >= len(positions) { + break + } + } + + return npos +} \ No newline at end of file