--- Dear ImGui WIP -- (drawing and font code) -- -- Index of this file: -- -- [SECTION] STB libraries implementation -- [SECTION] Style functions -- [SECTION] ImDrawList -- [SECTION] ImTriangulator, ImDrawList concave polygon fill -- [SECTION] ImDrawListSplitter -- [SECTION] ImDrawData -- [SECTION] Helpers ShadeVertsXXX functions -- [SECTION] ImFontConfig -- [SECTION] ImFontAtlas, ImFontAtlasBuilder -- [SECTION] ImFontAtlas: backend for stb_truetype -- [SECTION] ImFontAtlas: glyph ranges helpers -- [SECTION] ImFontGlyphRangesBuilder -- [SECTION] ImFontBaked, ImFont -- [SECTION] ImGui Internal Render Helpers -- [SECTION] Decompression code -- [SECTION] Default font data (ProggyClean.ttf) -- [SECTION] Default font data (ProggyForever.ttf) -- FONT_ATLAS_DEFAULT_TEX_DATA_W = 122 -- Actual texture will be 2 times that + 1 spacing. FONT_ATLAS_DEFAULT_TEX_DATA_H = 27 local IM_DRAWLIST_TEX_LINES_WIDTH_MAX = 32 -- The maximum line width to bake anti-aliased textures for. local writef64, writeu32, readf64, readu32 = buffer.writef64, buffer.writeu32, buffer.readf64, buffer.readu32 -- Write vertex i (1-based) of a VtxBuffer.Buf local function WV(b, i, x, y, u, v, col) local o = (i - 1) * 36 writef64(b, o, x); writef64(b, o + 8, y); writef64(b, o + 16, u); writef64(b, o + 24, v); writeu32(b, o + 32, col) end local function WVP(b, i, p, uv, col) WV(b, i, p.x, p.y, uv.x, uv.y, col) end local IM_FONTGLYPH_INDEX_UNUSED = 0xFFFF -- ((ImU16)-1) local IM_FONTGLYPH_INDEX_NOT_FOUND = 0xFFFE -- ((ImU16)-2) --- @param str string --- @return table --- @nodiscard --- @package local function str_to_table(str) local t = {} for i = 1, #str do t[i] = string.sub(str, i, i) end return t end --- @param buf char[] # 1-based --- @param base int # 1-based start index --- @param u32 ImU32 local function write_u32(buf, base, u32) buf[base + 0] = bit32.band(u32, 0xFF) buf[base + 1] = bit32.band(bit32.rshift(u32, 8), 0xFF) buf[base + 2] = bit32.band(bit32.rshift(u32, 16), 0xFF) buf[base + 3] = bit32.band(bit32.rshift(u32, 24), 0xFF) end -- A work of art lies ahead! (. = white layer, X = black layer, others are blank) -- The 2x2 white texels on the top left are the ones we'll use everywhere in Dear ImGui to render filled shapes. -- (This is used when io.MouseDrawCursor = true) local FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS = str_to_table( "..- -XXXXXXX- X - X -XXXXXXX - XXXXXXX- XX - XX XX " .. "..- -X.....X- X.X - X.X -X.....X - X.....X- X..X -X..X X..X" .. "--- -XXX.XXX- X...X - X...X -X....X - X....X- X..X -X...X X...X" .. "X - X.X - X.....X - X.....X -X...X - X...X- X..X - X...X X...X " .. "XX - X.X -X.......X- X.......X -X..X.X - X.X..X- X..X - X...X...X " .. "X.X - X.X -XXXX.XXXX- XXXX.XXXX -X.X X.X - X.X X.X- X..XXX - X.....X " .. "X..X - X.X - X.X - X.X -XX X.X - X.X XX- X..X..XXX - X...X " .. "X...X - X.X - X.X - XX X.X XX - X.X - X.X - X..X..X..XX - X.X " .. "X....X - X.X - X.X - X.X X.X X.X - X.X - X.X - X..X..X..X.X - X...X " .. "X.....X - X.X - X.X - X..X X.X X..X - X.X - X.X -XXX X..X..X..X..X- X.....X " .. "X......X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X XX-XX X.X -X..XX........X..X- X...X...X " .. "X.......X - X.X - X.X -X.....................X- X.X X.X-X.X X.X -X...X...........X- X...X X...X " .. "X........X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X..X-X..X.X - X..............X-X...X X...X" .. "X.........X -XXX.XXX- X.X - X..X X.X X..X - X...X-X...X - X.............X-X..X X..X" .. "X..........X-X.....X- X.X - X.X X.X X.X - X....X-X....X - X.............X- XX XX " .. "X......XXXXX-XXXXXXX- X.X - XX X.X XX - X.....X-X.....X - X............X--------------" .. "X...X..X --------- X.X - X.X - XXXXXXX-XXXXXXX - X...........X - " .. "X..X X..X - -XXXX.XXXX- XXXX.XXXX ------------------------------------- X..........X - " .. "X.X X..X - -X.......X- X.......X - XX XX - - X..........X - " .. "XX X..X - - X.....X - X.....X - X.X X.X - - X........X - " .. " X..X - - X...X - X...X - X..X X..X - - X........X - " .. " XX - - X.X - X.X - X...XXXXXXXXXXXXX...X - - XXXXXXXXXX - " .. "------------- - X - X -X.....................X- ------------------- " .. " ----------------------------------- X...XXXXXXXXXXXXX...X - " .. " - X..X X..X - " .. " - X.X X.X - " .. " - XX XX - " ) local FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA = { -- Pos .......... Size ......... Offset ....... { ImVec2( 0, 3), ImVec2(12, 19), ImVec2( 0, 0) }, -- ImGuiMouseCursor.Arrow { ImVec2( 13, 0), ImVec2( 7, 16), ImVec2( 1, 8) }, -- ImGuiMouseCursor.TextInput { ImVec2( 31, 0), ImVec2(23, 23), ImVec2(11, 11) }, -- ImGuiMouseCursor.ResizeAll { ImVec2( 21, 0), ImVec2( 9, 23), ImVec2( 4, 11) }, -- ImGuiMouseCursor.ResizeNS { ImVec2( 55, 18), ImVec2(23, 9), ImVec2(11, 4) }, -- ImGuiMouseCursor.ResizeEW { ImVec2( 73, 0), ImVec2(17, 17), ImVec2( 8, 8) }, -- ImGuiMouseCursor.ResizeNESW { ImVec2( 55, 0), ImVec2(17, 17), ImVec2( 8, 8) }, -- ImGuiMouseCursor.ResizeNWSE { ImVec2( 91, 0), ImVec2(17, 22), ImVec2( 5, 0) }, -- ImGuiMouseCursor.Hand { ImVec2( 0, 3), ImVec2(12, 19), ImVec2( 0, 0) }, -- ImGuiMouseCursor.Wait -- Arrow + custom code in ImGui.RenderMouseCursor() { ImVec2( 0, 3), ImVec2(12, 19), ImVec2( 0, 0) }, -- ImGuiMouseCursor.Progress -- Arrow + custom code in ImGui.RenderMouseCursor() { ImVec2(109, 0), ImVec2(13, 15), ImVec2( 6, 7) }, -- ImGuiMouseCursor.NotAllowed } local MT = ImGui.GetMetatables() ------------------------------------------------------------------------------- -- [SECTION] STB libraries implementation (for stb_truetype and stb_rect_pack) ------------------------------------------------------------------------------- --- @module "imstb_rectpack" local stbrp = IM_INCLUDE("imstb_rectpack.lua") --- @module "imstb_truetype" local stbtt = IM_INCLUDE("imstb_truetype.lua") ------------------------------------------------------------------------------- -- [SECTION] Style functions ------------------------------------------------------------------------------- --- @param a ImVec4 --- @param b ImVec4 --- @param t float local function ImLerpV4V4F(a, b, t) return ImVec4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t) end --- @param dst? ImGuiStyle function ImGui.StyleColorsDark(dst) local style = dst and dst or ImGui.GetStyle() local colors = style.Colors colors[ImGuiCol.Text] = ImVec4(1.00, 1.00, 1.00, 1.00) colors[ImGuiCol.TextDisabled] = ImVec4(0.50, 0.50, 0.50, 1.00) colors[ImGuiCol.WindowBg] = ImVec4(0.06, 0.06, 0.06, 0.94) colors[ImGuiCol.ChildBg] = ImVec4(0.00, 0.00, 0.00, 0.00) colors[ImGuiCol.PopupBg] = ImVec4(0.08, 0.08, 0.08, 0.94) colors[ImGuiCol.Border] = ImVec4(0.43, 0.43, 0.50, 0.50) colors[ImGuiCol.BorderShadow] = ImVec4(0.00, 0.00, 0.00, 0.00) colors[ImGuiCol.FrameBg] = ImVec4(0.16, 0.29, 0.48, 0.54) colors[ImGuiCol.FrameBgHovered] = ImVec4(0.26, 0.59, 0.98, 0.40) colors[ImGuiCol.FrameBgActive] = ImVec4(0.26, 0.59, 0.98, 0.67) colors[ImGuiCol.TitleBg] = ImVec4(0.04, 0.04, 0.04, 1.00) colors[ImGuiCol.TitleBgActive] = ImVec4(0.16, 0.29, 0.48, 1.00) colors[ImGuiCol.TitleBgCollapsed] = ImVec4(0.00, 0.00, 0.00, 0.51) colors[ImGuiCol.MenuBarBg] = ImVec4(0.14, 0.14, 0.14, 1.00) colors[ImGuiCol.ScrollbarBg] = ImVec4(0.02, 0.02, 0.02, 0.53) colors[ImGuiCol.ScrollbarGrab] = ImVec4(0.31, 0.31, 0.31, 1.00) colors[ImGuiCol.ScrollbarGrabHovered] = ImVec4(0.41, 0.41, 0.41, 1.00) colors[ImGuiCol.ScrollbarGrabActive] = ImVec4(0.51, 0.51, 0.51, 1.00) colors[ImGuiCol.CheckMark] = ImVec4(0.26, 0.59, 0.98, 1.00) colors[ImGuiCol.CheckboxSelectedBg] = ImLerpV4V4F(colors[ImGuiCol.FrameBg], colors[ImGuiCol.FrameBgHovered], 0.65) colors[ImGuiCol.SliderGrab] = ImVec4(0.24, 0.52, 0.88, 1.00) colors[ImGuiCol.SliderGrabActive] = ImVec4(0.26, 0.59, 0.98, 1.00) colors[ImGuiCol.Button] = ImVec4(0.26, 0.59, 0.98, 0.40) colors[ImGuiCol.ButtonHovered] = ImVec4(0.26, 0.59, 0.98, 1.00) colors[ImGuiCol.ButtonActive] = ImVec4(0.06, 0.53, 0.98, 1.00) colors[ImGuiCol.Header] = ImVec4(0.26, 0.59, 0.98, 0.31) colors[ImGuiCol.HeaderHovered] = ImVec4(0.26, 0.59, 0.98, 0.80) colors[ImGuiCol.HeaderActive] = ImVec4(0.26, 0.59, 0.98, 1.00) colors[ImGuiCol.Separator] = colors[ImGuiCol.Border] colors[ImGuiCol.SeparatorHovered] = ImVec4(0.10, 0.40, 0.75, 0.78) colors[ImGuiCol.SeparatorActive] = ImVec4(0.10, 0.40, 0.75, 1.00) colors[ImGuiCol.ResizeGrip] = ImVec4(0.26, 0.59, 0.98, 0.20) colors[ImGuiCol.ResizeGripHovered] = ImVec4(0.26, 0.59, 0.98, 0.67) colors[ImGuiCol.ResizeGripActive] = ImVec4(0.26, 0.59, 0.98, 0.95) colors[ImGuiCol.InputTextCursor] = colors[ImGuiCol.Text] colors[ImGuiCol.TabHovered] = colors[ImGuiCol.HeaderHovered] colors[ImGuiCol.Tab] = ImLerpV4V4F(colors[ImGuiCol.Header], colors[ImGuiCol.TitleBgActive], 0.80) colors[ImGuiCol.TabSelected] = ImLerpV4V4F(colors[ImGuiCol.HeaderActive], colors[ImGuiCol.TitleBgActive], 0.60) colors[ImGuiCol.TabSelectedOverline] = colors[ImGuiCol.HeaderActive] colors[ImGuiCol.TabDimmed] = ImLerpV4V4F(colors[ImGuiCol.Tab], colors[ImGuiCol.TitleBg], 0.80) colors[ImGuiCol.TabDimmedSelected] = ImLerpV4V4F(colors[ImGuiCol.TabSelected], colors[ImGuiCol.TitleBg], 0.40) colors[ImGuiCol.TabDimmedSelectedOverline] = ImVec4(0.50, 0.50, 0.50, 0.00) colors[ImGuiCol.DockingPreview] = ImVec4(colors[ImGuiCol.HeaderActive].x, colors[ImGuiCol.HeaderActive].y, colors[ImGuiCol.HeaderActive].z, colors[ImGuiCol.HeaderActive].w * 0.7) colors[ImGuiCol.DockingEmptyBg] = ImVec4(0.20, 0.20, 0.20, 1.00) colors[ImGuiCol.PlotLines] = ImVec4(0.61, 0.61, 0.61, 1.00) colors[ImGuiCol.PlotLinesHovered] = ImVec4(1.00, 0.43, 0.35, 1.00) colors[ImGuiCol.PlotHistogram] = ImVec4(0.90, 0.70, 0.00, 1.00) colors[ImGuiCol.PlotHistogramHovered] = ImVec4(1.00, 0.60, 0.00, 1.00) colors[ImGuiCol.TableHeaderBg] = ImVec4(0.19, 0.19, 0.20, 1.00) colors[ImGuiCol.TableBorderStrong] = ImVec4(0.31, 0.31, 0.35, 1.00) -- Prefer using Alpha=1.0 here colors[ImGuiCol.TableBorderLight] = ImVec4(0.23, 0.23, 0.25, 1.00) -- Prefer using Alpha=1.0 here colors[ImGuiCol.TableRowBg] = ImVec4(0.00, 0.00, 0.00, 0.00) colors[ImGuiCol.TableRowBgAlt] = ImVec4(1.00, 1.00, 1.00, 0.06) colors[ImGuiCol.TextLink] = colors[ImGuiCol.HeaderActive] colors[ImGuiCol.TextSelectedBg] = ImVec4(0.26, 0.59, 0.98, 0.35) colors[ImGuiCol.TreeLines] = colors[ImGuiCol.Border] colors[ImGuiCol.DragDropTarget] = ImVec4(1.00, 1.00, 0.00, 0.90) colors[ImGuiCol.DragDropTargetBg] = ImVec4(0.00, 0.00, 0.00, 0.00) colors[ImGuiCol.UnsavedMarker] = ImVec4(1.00, 1.00, 1.00, 1.00) colors[ImGuiCol.NavCursor] = ImVec4(0.26, 0.59, 0.98, 1.00) colors[ImGuiCol.NavWindowingHighlight] = ImVec4(1.00, 1.00, 1.00, 0.70) colors[ImGuiCol.NavWindowingDimBg] = ImVec4(0.80, 0.80, 0.80, 0.20) colors[ImGuiCol.ModalWindowDimBg] = ImVec4(0.80, 0.80, 0.80, 0.35) end --- @param dst? ImGuiStyle function ImGui.StyleColorsClassic(dst) local style = dst and dst or ImGui.GetStyle() local colors = style.Colors colors[ImGuiCol.Text] = ImVec4(0.90, 0.90, 0.90, 1.00) colors[ImGuiCol.TextDisabled] = ImVec4(0.60, 0.60, 0.60, 1.00) colors[ImGuiCol.WindowBg] = ImVec4(0.00, 0.00, 0.00, 0.85) colors[ImGuiCol.ChildBg] = ImVec4(0.00, 0.00, 0.00, 0.00) colors[ImGuiCol.PopupBg] = ImVec4(0.11, 0.11, 0.14, 0.92) colors[ImGuiCol.Border] = ImVec4(0.50, 0.50, 0.50, 0.50) colors[ImGuiCol.BorderShadow] = ImVec4(0.00, 0.00, 0.00, 0.00) colors[ImGuiCol.FrameBg] = ImVec4(0.43, 0.43, 0.43, 0.39) colors[ImGuiCol.FrameBgHovered] = ImVec4(0.47, 0.47, 0.69, 0.40) colors[ImGuiCol.FrameBgActive] = ImVec4(0.42, 0.41, 0.64, 0.69) colors[ImGuiCol.TitleBg] = ImVec4(0.27, 0.27, 0.54, 0.83) colors[ImGuiCol.TitleBgActive] = ImVec4(0.32, 0.32, 0.63, 0.87) colors[ImGuiCol.TitleBgCollapsed] = ImVec4(0.40, 0.40, 0.80, 0.20) colors[ImGuiCol.MenuBarBg] = ImVec4(0.40, 0.40, 0.55, 0.80) colors[ImGuiCol.ScrollbarBg] = ImVec4(0.20, 0.25, 0.30, 0.60) colors[ImGuiCol.ScrollbarGrab] = ImVec4(0.40, 0.40, 0.80, 0.30) colors[ImGuiCol.ScrollbarGrabHovered] = ImVec4(0.40, 0.40, 0.80, 0.40) colors[ImGuiCol.ScrollbarGrabActive] = ImVec4(0.41, 0.39, 0.80, 0.60) colors[ImGuiCol.CheckMark] = ImVec4(0.90, 0.90, 0.90, 0.50) colors[ImGuiCol.CheckboxSelectedBg] = ImLerpV4V4F(colors[ImGuiCol.FrameBg], colors[ImGuiCol.FrameBgActive], 0.65) colors[ImGuiCol.SliderGrab] = ImVec4(1.00, 1.00, 1.00, 0.30) colors[ImGuiCol.SliderGrabActive] = ImVec4(0.41, 0.39, 0.80, 0.60) colors[ImGuiCol.Button] = ImVec4(0.35, 0.40, 0.61, 0.62) colors[ImGuiCol.ButtonHovered] = ImVec4(0.40, 0.48, 0.71, 0.79) colors[ImGuiCol.ButtonActive] = ImVec4(0.46, 0.54, 0.80, 1.00) colors[ImGuiCol.Header] = ImVec4(0.40, 0.40, 0.90, 0.45) colors[ImGuiCol.HeaderHovered] = ImVec4(0.45, 0.45, 0.90, 0.80) colors[ImGuiCol.HeaderActive] = ImVec4(0.53, 0.53, 0.87, 0.80) colors[ImGuiCol.Separator] = ImVec4(0.50, 0.50, 0.50, 0.60) colors[ImGuiCol.SeparatorHovered] = ImVec4(0.60, 0.60, 0.70, 1.00) colors[ImGuiCol.SeparatorActive] = ImVec4(0.70, 0.70, 0.90, 1.00) colors[ImGuiCol.ResizeGrip] = ImVec4(1.00, 1.00, 1.00, 0.10) colors[ImGuiCol.ResizeGripHovered] = ImVec4(0.78, 0.82, 1.00, 0.60) colors[ImGuiCol.ResizeGripActive] = ImVec4(0.78, 0.82, 1.00, 0.90) colors[ImGuiCol.InputTextCursor] = colors[ImGuiCol.Text] colors[ImGuiCol.TabHovered] = colors[ImGuiCol.HeaderHovered] colors[ImGuiCol.Tab] = ImLerpV4V4F(colors[ImGuiCol.Header], colors[ImGuiCol.TitleBgActive], 0.80) colors[ImGuiCol.TabSelected] = ImLerpV4V4F(colors[ImGuiCol.HeaderActive], colors[ImGuiCol.TitleBgActive], 0.60) colors[ImGuiCol.TabSelectedOverline] = colors[ImGuiCol.HeaderActive] colors[ImGuiCol.TabDimmed] = ImLerpV4V4F(colors[ImGuiCol.Tab], colors[ImGuiCol.TitleBg], 0.80) colors[ImGuiCol.TabDimmedSelected] = ImLerpV4V4F(colors[ImGuiCol.TabSelected], colors[ImGuiCol.TitleBg], 0.40) colors[ImGuiCol.TabDimmedSelectedOverline] = ImVec4(0.53, 0.53, 0.87, 0.00) colors[ImGuiCol.DockingPreview] = ImVec4(colors[ImGuiCol.Header].x, colors[ImGuiCol.Header].y, colors[ImGuiCol.Header].z, colors[ImGuiCol.Header].w * 0.7) colors[ImGuiCol.DockingEmptyBg] = ImVec4(0.20, 0.20, 0.20, 1.00) colors[ImGuiCol.PlotLines] = ImVec4(1.00, 1.00, 1.00, 1.00) colors[ImGuiCol.PlotLinesHovered] = ImVec4(0.90, 0.70, 0.00, 1.00) colors[ImGuiCol.PlotHistogram] = ImVec4(0.90, 0.70, 0.00, 1.00) colors[ImGuiCol.PlotHistogramHovered] = ImVec4(1.00, 0.60, 0.00, 1.00) colors[ImGuiCol.TableHeaderBg] = ImVec4(0.27, 0.27, 0.38, 1.00) colors[ImGuiCol.TableBorderStrong] = ImVec4(0.31, 0.31, 0.45, 1.00) -- Prefer using Alpha=1.0 here colors[ImGuiCol.TableBorderLight] = ImVec4(0.26, 0.26, 0.28, 1.00) -- Prefer using Alpha=1.0 here colors[ImGuiCol.TableRowBg] = ImVec4(0.00, 0.00, 0.00, 0.00) colors[ImGuiCol.TableRowBgAlt] = ImVec4(1.00, 1.00, 1.00, 0.07) colors[ImGuiCol.TextLink] = colors[ImGuiCol.HeaderActive] colors[ImGuiCol.TextSelectedBg] = ImVec4(0.00, 0.00, 1.00, 0.35) colors[ImGuiCol.TreeLines] = colors[ImGuiCol.Border] colors[ImGuiCol.DragDropTarget] = ImVec4(1.00, 1.00, 0.00, 0.90) colors[ImGuiCol.DragDropTargetBg] = ImVec4(0.00, 0.00, 0.00, 0.00) colors[ImGuiCol.UnsavedMarker] = ImVec4(0.90, 0.90, 0.90, 1.00) colors[ImGuiCol.NavCursor] = colors[ImGuiCol.HeaderHovered] colors[ImGuiCol.NavWindowingHighlight] = ImVec4(1.00, 1.00, 1.00, 0.70) colors[ImGuiCol.NavWindowingDimBg] = ImVec4(0.80, 0.80, 0.80, 0.20) colors[ImGuiCol.ModalWindowDimBg] = ImVec4(0.20, 0.20, 0.20, 0.35) end -- Those light colors are better suited with a thicker font than the default one + FrameBorder --- @param dst? ImGuiStyle function ImGui.StyleColorsLight(dst) local style = dst and dst or ImGui.GetStyle() local colors = style.Colors colors[ImGuiCol.Text] = ImVec4(0.00, 0.00, 0.00, 1.00) colors[ImGuiCol.TextDisabled] = ImVec4(0.60, 0.60, 0.60, 1.00) colors[ImGuiCol.WindowBg] = ImVec4(0.94, 0.94, 0.94, 1.00) colors[ImGuiCol.ChildBg] = ImVec4(0.00, 0.00, 0.00, 0.00) colors[ImGuiCol.PopupBg] = ImVec4(1.00, 1.00, 1.00, 0.98) colors[ImGuiCol.Border] = ImVec4(0.00, 0.00, 0.00, 0.30) colors[ImGuiCol.BorderShadow] = ImVec4(0.00, 0.00, 0.00, 0.00) colors[ImGuiCol.FrameBg] = ImVec4(1.00, 1.00, 1.00, 1.00) colors[ImGuiCol.FrameBgHovered] = ImVec4(0.26, 0.59, 0.98, 0.40) colors[ImGuiCol.FrameBgActive] = ImVec4(0.26, 0.59, 0.98, 0.67) colors[ImGuiCol.TitleBg] = ImVec4(0.96, 0.96, 0.96, 1.00) colors[ImGuiCol.TitleBgActive] = ImVec4(0.82, 0.82, 0.82, 1.00) colors[ImGuiCol.TitleBgCollapsed] = ImVec4(1.00, 1.00, 1.00, 0.51) colors[ImGuiCol.MenuBarBg] = ImVec4(0.86, 0.86, 0.86, 1.00) colors[ImGuiCol.ScrollbarBg] = ImVec4(0.98, 0.98, 0.98, 0.53) colors[ImGuiCol.ScrollbarGrab] = ImVec4(0.69, 0.69, 0.69, 0.80) colors[ImGuiCol.ScrollbarGrabHovered] = ImVec4(0.49, 0.49, 0.49, 0.80) colors[ImGuiCol.ScrollbarGrabActive] = ImVec4(0.49, 0.49, 0.49, 1.00) colors[ImGuiCol.CheckMark] = ImVec4(0.26, 0.59, 0.98, 1.00) colors[ImGuiCol.CheckboxSelectedBg] = ImVec4(0.95, 0.97, 1.00, 1.00) colors[ImGuiCol.SliderGrab] = ImVec4(0.26, 0.59, 0.98, 0.78) colors[ImGuiCol.SliderGrabActive] = ImVec4(0.46, 0.54, 0.80, 0.60) colors[ImGuiCol.Button] = ImVec4(0.26, 0.59, 0.98, 0.40) colors[ImGuiCol.ButtonHovered] = ImVec4(0.26, 0.59, 0.98, 1.00) colors[ImGuiCol.ButtonActive] = ImVec4(0.06, 0.53, 0.98, 1.00) colors[ImGuiCol.Header] = ImVec4(0.26, 0.59, 0.98, 0.31) colors[ImGuiCol.HeaderHovered] = ImVec4(0.26, 0.59, 0.98, 0.80) colors[ImGuiCol.HeaderActive] = ImVec4(0.26, 0.59, 0.98, 1.00) colors[ImGuiCol.Separator] = ImVec4(0.39, 0.39, 0.39, 0.62) colors[ImGuiCol.SeparatorHovered] = ImVec4(0.14, 0.44, 0.80, 0.78) colors[ImGuiCol.SeparatorActive] = ImVec4(0.14, 0.44, 0.80, 1.00) colors[ImGuiCol.ResizeGrip] = ImVec4(0.35, 0.35, 0.35, 0.17) colors[ImGuiCol.ResizeGripHovered] = ImVec4(0.26, 0.59, 0.98, 0.67) colors[ImGuiCol.ResizeGripActive] = ImVec4(0.26, 0.59, 0.98, 0.95) colors[ImGuiCol.InputTextCursor] = colors[ImGuiCol.Text] colors[ImGuiCol.TabHovered] = colors[ImGuiCol.HeaderHovered] colors[ImGuiCol.Tab] = ImLerpV4V4F(colors[ImGuiCol.Header], colors[ImGuiCol.TitleBgActive], 0.90) colors[ImGuiCol.TabSelected] = ImLerpV4V4F(colors[ImGuiCol.HeaderActive], colors[ImGuiCol.TitleBgActive], 0.60) colors[ImGuiCol.TabSelectedOverline] = colors[ImGuiCol.HeaderActive] colors[ImGuiCol.TabDimmed] = ImLerpV4V4F(colors[ImGuiCol.Tab], colors[ImGuiCol.TitleBg], 0.80) colors[ImGuiCol.TabDimmedSelected] = ImLerpV4V4F(colors[ImGuiCol.TabSelected], colors[ImGuiCol.TitleBg], 0.40) colors[ImGuiCol.TabDimmedSelectedOverline] = ImVec4(0.26, 0.59, 1.00, 0.00) colors[ImGuiCol.DockingPreview] = ImVec4(colors[ImGuiCol.Header].x, colors[ImGuiCol.Header].y, colors[ImGuiCol.Header].z, colors[ImGuiCol.Header].w * 0.7) colors[ImGuiCol.DockingEmptyBg] = ImVec4(0.68, 0.69, 0.73, 1.00) colors[ImGuiCol.PlotLines] = ImVec4(0.39, 0.39, 0.39, 1.00) colors[ImGuiCol.PlotLinesHovered] = ImVec4(1.00, 0.43, 0.35, 1.00) colors[ImGuiCol.PlotHistogram] = ImVec4(0.90, 0.70, 0.00, 1.00) colors[ImGuiCol.PlotHistogramHovered] = ImVec4(1.00, 0.45, 0.00, 1.00) colors[ImGuiCol.TableHeaderBg] = ImVec4(0.78, 0.87, 0.98, 1.00) colors[ImGuiCol.TableBorderStrong] = ImVec4(0.57, 0.57, 0.64, 1.00) -- Prefer using Alpha=1.0 here colors[ImGuiCol.TableBorderLight] = ImVec4(0.68, 0.68, 0.74, 1.00) -- Prefer using Alpha=1.0 here colors[ImGuiCol.TableRowBg] = ImVec4(0.00, 0.00, 0.00, 0.00) colors[ImGuiCol.TableRowBgAlt] = ImVec4(0.30, 0.30, 0.30, 0.09) colors[ImGuiCol.TextLink] = colors[ImGuiCol.HeaderActive] colors[ImGuiCol.TextSelectedBg] = ImVec4(0.26, 0.59, 0.98, 0.35) colors[ImGuiCol.TreeLines] = colors[ImGuiCol.Border] colors[ImGuiCol.DragDropTarget] = ImVec4(0.26, 0.59, 0.98, 0.95) colors[ImGuiCol.DragDropTargetBg] = ImVec4(0.00, 0.00, 0.00, 0.00) colors[ImGuiCol.UnsavedMarker] = ImVec4(0.00, 0.00, 0.00, 1.00) colors[ImGuiCol.NavCursor] = colors[ImGuiCol.HeaderHovered] colors[ImGuiCol.NavWindowingHighlight] = ImVec4(0.70, 0.70, 0.70, 0.70) colors[ImGuiCol.NavWindowingDimBg] = ImVec4(0.20, 0.20, 0.20, 0.20) colors[ImGuiCol.ModalWindowDimBg] = ImVec4(0.20, 0.20, 0.20, 0.35) end ------------------------------------------------------------------------------- -- [SECTION] Helpers ShadeVertsXXX functions ------------------------------------------------------------------------------- -- Generic linear color gradient, write to RGB fields, leave A untouched. --- @param draw_list ImDrawList --- @param vert_start_idx int --- @param vert_end_idx int --- @param gradient_p0 ImVec2 --- @param gradient_p1 ImVec2 --- @param col0 ImU32 --- @param col1 ImU32 function ImGui.ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col0, col1) local gradient_extent = gradient_p1 - gradient_p0 local gradient_inv_length2 = 1.0 / ImLengthSqr(gradient_extent) local col0_r = bit32.band(bit32.rshift(col0, IM_COL32_R_SHIFT), 0xFF) local col0_g = bit32.band(bit32.rshift(col0, IM_COL32_G_SHIFT), 0xFF) local col0_b = bit32.band(bit32.rshift(col0, IM_COL32_B_SHIFT), 0xFF) local col_delta_r = bit32.band(bit32.rshift(col1, IM_COL32_R_SHIFT), 0xFF) - col0_r local col_delta_g = bit32.band(bit32.rshift(col1, IM_COL32_G_SHIFT), 0xFF) - col0_g local col_delta_b = bit32.band(bit32.rshift(col1, IM_COL32_B_SHIFT), 0xFF) - col0_b for vert_idx = vert_start_idx, vert_end_idx - 1 do local vb = draw_list.VtxBuffer.Buf local o = (vert_idx - 1) * 36 local d = (readf64(vb, o) - gradient_p0.x) * gradient_extent.x + (readf64(vb, o + 8) - gradient_p0.y) * gradient_extent.y local t = ImClamp(d * gradient_inv_length2, 0.0, 1.0) local r = math.floor(col0_r + col_delta_r * t) local g = math.floor(col0_g + col_delta_g * t) local b = math.floor(col0_b + col_delta_b * t) writeu32(vb, o + 32, bit32.bor(bit32.lshift(r, IM_COL32_R_SHIFT), bit32.lshift(g, IM_COL32_G_SHIFT), bit32.lshift(b, IM_COL32_B_SHIFT), bit32.band(readu32(vb, o + 32), IM_COL32_A_MASK))) end end -- Rotate and translate vertices [vert_start_idx, vert_end_idx) (1-based, like _VtxCurrentIdx) function ImGui.ShadeVertsTransformPos(draw_list, vert_start_idx, vert_end_idx, pivot_in, cos_a, sin_a, pivot_out) local data = draw_list.VtxBuffer.Buf local pix, piy, pox, poy = pivot_in.x, pivot_in.y, pivot_out.x, pivot_out.y for i = vert_start_idx, vert_end_idx - 1 do local o = (i - 1) * 36 local x, y = readf64(data, o) - pix, readf64(data, o + 8) - piy writef64(data, o, x * cos_a - y * sin_a + pox) writef64(data, o + 8, x * sin_a + y * cos_a + poy) end end -- Distribute UV over (a, b) rectangle --- @param draw_list ImDrawList --- @param vert_start_idx int --- @param vert_end_idx int --- @param a ImVec2 --- @param b ImVec2 --- @param uv_a ImVec2 --- @param uv_b ImVec2 --- @param clamp bool function ImGui.ShadeVertsLinearUV(draw_list, vert_start_idx, vert_end_idx, a, b, uv_a, uv_b, clamp) local size = b - a local uv_size = uv_b - uv_a local scale = ImVec2( (size.x ~= 0.0) and (uv_size.x / size.x) or 0.0, (size.y ~= 0.0) and (uv_size.y / size.y) or 0.0 ) local vertex local verts = draw_list.VtxBuffer.Buf if clamp then local min = ImMin(uv_a, uv_b) local max = ImMax(uv_a, uv_b) for vert_idx = vert_start_idx, vert_end_idx - 1 do local o = (vert_idx - 1) * 36 local uv = ImClamp(uv_a + ImMul(ImVec2(readf64(verts, o), readf64(verts, o + 8)) - a, scale), min, max) writef64(verts, o + 16, uv.x); writef64(verts, o + 24, uv.y) end else for vert_idx = vert_start_idx, vert_end_idx - 1 do local o = (vert_idx - 1) * 36 local uv = uv_a + ImMul(ImVec2(readf64(verts, o), readf64(verts, o + 8)) - a, scale) writef64(verts, o + 16, uv.x); writef64(verts, o + 24, uv.y) end end end --- Forward Declarations local ImFontAtlasTextureAdd local ImFontAtlasTextureGrow local ImFontAtlasTextureGetSizeEstimate local ImFontAtlasTextureBlockCopy local ImFontAtlasTextureBlockConvert local ImFontAtlasTextureBlockPostProcess local ImFontAtlasTextureBlockPostProcessMultiply local ImFontAtlasTextureBlockQueueUpload local ImFontAtlasTextureCompact local ImFontAtlasPackInit local ImFontAtlasPackAddRect local ImFontAtlasPackGetRect local ImFontAtlasPackGetRectSafe local ImFontAtlasBakedAdd local ImFontAtlasBakedAddFontGlyph local ImFontAtlasBakedAddFontGlyphAdvancedX local ImFontAtlasBakedDiscard local ImFontAtlasBakedGetOrAdd local ImFontAtlasBakedGetId local ImFontAtlasBakedGetClosestMatch local ImFontAtlasBuildInit local ImFontAtlasBuildSetTexture local ImFontAtlasBuildUpdateTexDataLines local ImFontAtlasBuildUpdateTexDataBasic local ImFontAtlasBuildSetupFontLoader local ImFontAtlasBuildSetupFontSpecialGlyphs local ImFontAtlasBuildSetupFontBakedBlanks local ImFontAtlasBuildSetupFontBakedEllipsis local ImFontAtlasBuildSetupFontBakedFallback local ImFontAtlasBuildDestroy local ImFontAtlasBuildAcceptCodepointForSource local ImFontAtlasBuildNotifySetFont local ImFontAtlasBuildRenderBitmapFromString local ImFontAtlasFontSourceInit local ImFontAtlasFontSourceAddToFont local ImFontAtlasFontDestroySourceData local ImFontAtlasFontDestroyOutput local ImFontBaked_BuildGrowIndex local ImFontBaked_BuildLoadGlyph local ImFontBaked_BuildLoadGlyphAdvanceX local ImTextInitClassifiers local ImTextClassifierGet local ImTextClassifierClear local ImTextClassifierSetCharClass local ImTextClassifierSetCharClassFromStr local ImTextureDataGetFormatBytesPerPixel ------------------------------------------------------------------------------- -- [SECTION] ImFontAtlas, ImFontAtlasBuilder ------------------------------------------------------------------------------- -- - Default texture data encoded in ASCII -- - ImFontAtlas() -- - ImFontAtlas:Clear() -- - ImFontAtlas:ClearFonts() -- - ImFontAtlas:ClearInputData() -- - ImFontAtlas:ClearTexData() -- - ImFontAtlas:CompactCache() -- - ImFontAtlas:SetFontLoader() ------------------------------------------------------------------------------- -- - ImFontAtlasUpdateNewFrame() -- - ImFontAtlasTextureBlockConvert() -- - ImFontAtlasTextureBlockPostProcess() -- - ImFontAtlasTextureBlockPostProcessMultiply() -- - ImFontAtlasTextureBlockFill() -- - ImFontAtlasTextureBlockCopy() -- - ImFontAtlasTextureBlockQueueUpload() ------------------------------------------------------------------------------- -- - ImFontAtlas:GetTexDataAsAlpha8() [legacy] -- - ImFontAtlas:GetTexDataAsRGBA32() [legacy] -- - ImFontAtlas:Build() [legacy] ------------------------------------------------------------------------------- -- - ImFontAtlas:AddFont() -- - ImFontAtlas:AddFontDefault() -- - ImFontAtlas:AddFontDefaultBitmap() -- - ImFontAtlas:AddFontDefaultVector() -- - ImFontAtlas:AddFontFromFileTTF() -- - ImFontAtlas:AddFontFromMemoryTTF() -- - ImFontAtlas:AddFontFromMemoryCompressedTTF() -- - ImFontAtlas:AddFontFromMemoryCompressedBase85TTF() -- - ImFontAtlas:RemoveFont() -- - ImFontAtlasBuildNotifySetFont() ------------------------------------------------------------------------------- -- - ImFontAtlas:AddCustomRect() -- - ImFontAtlas:RemoveCustomRect() -- - ImFontAtlas:GetCustomRect() -- - ImFontAtlas:AddCustomRectFontGlyph() [legacy] -- - ImFontAtlas:AddCustomRectFontGlyphForSize() [legacy] -- - ImFontAtlasGetMouseCursorTexData() ------------------------------------------------------------------------------- -- - ImFontAtlasBuildMain() -- - ImFontAtlasBuildSetupFontLoader() -- - ImFontAtlasBuildPreloadAllGlyphRanges() -- - ImFontAtlasBuildUpdatePointers() -- - ImFontAtlasBuildRenderBitmapFromString() -- - ImFontAtlasBuildAddFont() -- - ImFontAtlasBuildSetupFontBakedEllipsis() -- - ImFontAtlasBuildSetupFontBakedBlanks() -- - ImFontAtlasBuildSetupFontBakedFallback() -- - ImFontAtlasBuildSetupFontSpecialGlyphs() -- - ImFontAtlasBuildDiscardBakes() -- - ImFontAtlasBuildDiscardFontBakedGlyph() -- - ImFontAtlasBuildDiscardFontBaked() -- - ImFontAtlasBuildDiscardFontBakes() ------------------------------------------------------------------------------- -- - ImFontAtlasAddDrawListSharedData() -- - ImFontAtlasRemoveDrawListSharedData() -- - ImFontAtlasUpdateDrawListsTextures() -- - ImFontAtlasUpdateDrawListsSharedData() ------------------------------------------------------------------------------- -- - ImFontAtlasBuildSetTexture() -- - ImFontAtlasBuildUpdateTexData() -- - ImFontAtlasTextureAdd() -- - ImFontAtlasTextureRepack() -- - ImFontAtlasTextureGrow() -- - ImFontAtlasTextureMakeSpace() -- - ImFontAtlasTextureGetSizeEstimate() -- - ImFontAtlasBuildClear() -- - ImFontAtlasTextureCompact() -- - ImFontAtlasBuildInit() -- - ImFontAtlasBuildDestroy() ------------------------------------------------------------------------------- -- - ImFontAtlasPackInit() -- - ImFontAtlasPackAllocRectEntry() -- - ImFontAtlasPackReuseRectEntry() -- - ImFontAtlasPackDiscardRect() -- - ImFontAtlasPackAddRect() -- - ImFontAtlasPackGetRect() ------------------------------------------------------------------------------- -- - ImFontBaked_BuildGrowIndex() -- - ImFontBaked_BuildLoadGlyph() -- - ImFontBaked_BuildLoadGlyphAdvanceX() -- - ImFontAtlasDebugLogTextureRequests() ------------------------------------------------------------------------------- -- - ImFontAtlasGetFontLoaderForStbTruetype() ------------------------------------------------------------------------------- -- You probably should not call this directly. It is not well specified. -- If you want to replace all your fonts mid-frame, most likely you should instead call ClearFonts() then load the new fonts. -- Calling this mid-frame will discard the CPU-side copy of the texture data which is generally unreliable as you may have textures queued for creation or updates. function MT.ImFontAtlas:Clear() IMGUI_DEBUG_LOG_FONT("[font] ImFontAtlas:Clear()") local backup_renderer_has_textures = self.RendererHasTextures self.RendererHasTextures = false -- Full Clear() is supported, but ClearTexData() only isn't. self:ClearFonts() self:ClearTexData() self.RendererHasTextures = backup_renderer_has_textures end function MT.ImFontAtlas:SetFontLoader(font_loader) ImFontAtlasBuildSetupFontLoader(self, font_loader) end function MT.ImFontAtlas:ClearInputData() IM_ASSERT(not self.Locked, "Cannot modify a locked ImFontAtlas!") for _, font in self.Fonts:iter() do ImFontAtlasFontDestroyOutput(self, font) end for _, font_cfg in self.Sources:iter() do ImFontAtlasFontDestroySourceData(self, font_cfg) end for _, font in self.Fonts:iter() do -- When clearing this we lose access to the font name and other information used to build the font. font.Sources:clear() font.Flags = bit32.bor(font.Flags, ImFontFlags.NoLoadGlyphs) end self.Sources:clear() end -- Clear CPU-side copy of the texture data. function MT.ImFontAtlas:ClearTexData() IM_ASSERT(not self.Locked, "Cannot modify a locked ImFontAtlas!") IM_ASSERT(self.RendererHasTextures == false, "Not supported for dynamic atlases, but you may call Clear().") for _, tex in self.TexList:iter() do tex:DestroyPixels() end --self.Locked = true -- Hoped to be able to lock this down but some reload patterns may not be happy with it. end function MT.ImFontAtlas:CompactCache() ImFontAtlasTextureCompact(self) end function MT.ImFontAtlas:ClearFonts() -- FIXME-NEWATLAS: Illegal to remove currently bound font. IMGUI_DEBUG_LOG_FONT("[font] ImFontAtlas:ClearFonts()") IM_ASSERT(not self.Locked, "Cannot modify a locked ImFontAtlas!") for _, font in self.Fonts:iter() do ImFontAtlasBuildNotifySetFont(self, font, nil) end ImFontAtlasBuildDestroy(self) self:ClearInputData() self.Fonts:clear_delete() self.TexIsBuilt = false for _, shared_data in self.DrawListSharedDatas:iter() do if shared_data.FontAtlas == self then shared_data.Font = nil shared_data.FontSize = 0.0 shared_data.FontScale = 0.0 end end end --- @param tex ImTextureData function ImTextureDataUpdateNewFrame(tex) local remove_from_list = false if tex.Status == ImTextureStatus.OK then tex.Updates:resize(0) tex.UpdateRect.x = 65535 tex.UpdateRect.y = 65535 tex.UpdateRect.w = 0 tex.UpdateRect.h = 0 end -- Request destroy -- - Keep bool to true in order to differentiate a planned destroy vs a destroy decided by the backend. -- - We don't destroy pixels right away, as backend may have an in-flight copy from RAM. if (tex.WantDestroyNextFrame and tex.Status ~= ImTextureStatus.Destroyed and tex.Status ~= ImTextureStatus.WantDestroy) then IM_ASSERT(tex.Status == ImTextureStatus.OK or tex.Status == ImTextureStatus.WantCreate or tex.Status == ImTextureStatus.WantUpdates) tex.Status = ImTextureStatus.WantDestroy end -- If a texture has never reached the backend, they don't need to know about it. -- (note: backends between 1.92.0 and 1.92.4 could set an already destroyed texture to ImTextureStatus.WantDestroy -- when invalidating graphics objects twice, which would previously remove it from the list and crash.) if (tex.Status == ImTextureStatus.WantDestroy and tex.TexID == ImTextureID_Invalid and tex.BackendUserData == nil and tex.QueueUserData == nil) then tex.Status = ImTextureStatus.Destroyed end -- Process texture being destroyed if (tex.Status == ImTextureStatus.Destroyed) then IM_ASSERT(tex.TexID == ImTextureID_Invalid and tex.BackendUserData == nil, "Backend set texture Status to Destroyed but did not clear TexID/BackendUserData!") IM_ASSERT(tex.QueueUserData == nil, "Texture queue set Status to Destroyed but did not clear QueueUserData!") if (tex.WantDestroyNextFrame) then remove_from_list = true -- Destroy was scheduled by us else tex.Status = ImTextureStatus.WantCreate -- Destroy was done was backend: recreate it (e.g. freed resources mid-run) end end -- The backend may need defer destroying by a few frames, to handle texture used by previous in-flight rendering. -- We allow the texture staying in _WantDestroy state and increment a counter which the backend can use to take its decision. if (tex.Status == ImTextureStatus.WantDestroy) then tex.UnusedFrames = tex.UnusedFrames + 1 end return remove_from_list end -- Called by NewFrame() for atlases owned by a context. -- If you manually manage font atlases, you'll need to call this yourself. -- - 'frame_count' needs to be provided because we can gc/prioritize baked fonts based on their age. -- - 'frame_count' may not match those of all imgui contexts using this atlas, as contexts may be updated as different frequencies. But generally you can use ImGui.GetFrameCount() on one of your context. --- @param atlas ImFontAtlas --- @param frame_count int --- @param renderer_has_textures any function ImFontAtlasUpdateNewFrame(atlas, frame_count, renderer_has_textures) IM_ASSERT(atlas.Builder == nil or atlas.Builder.FrameCount < frame_count) -- Protection against being called twice. atlas.RendererHasTextures = renderer_has_textures -- Update texture status and discard old textures. -- (we do this first thing to handle an edge case: if user mistakenly calls ClearFonts()+SetStatus(OK) during -- rendering, it would ImFontAtlasBuildMain() rebuilding before tex.Updates[] gets a chance to be cleared) -- (if somehow we need to move this back lower in the function, we could manually call the code to clear Updates[]). local tex_n = 1 while tex_n <= atlas.TexList.Size do -- Update and remove if requested local tex = atlas.TexList.Data[tex_n] if (tex.Status == ImTextureStatus.WantCreate and atlas.RendererHasTextures) then IM_ASSERT(tex.TexID == ImTextureID_Invalid and tex.BackendUserData == nil, "Backend set texture's TexID/BackendUserData but did not update Status to OK.") end local remove_from_list = ImTextureDataUpdateNewFrame(tex) if remove_from_list then IM_ASSERT(atlas.TexData ~= tex) tex:DestroyPixels() atlas.TexList:erase(tex_n) tex_n = tex_n - 1 end tex_n = tex_n + 1 end -- Check that font atlas was built or backend support texture reload in which case we can build now if (atlas.RendererHasTextures) then atlas.TexIsBuilt = true if (atlas.Builder == nil) then -- This will only happen if fonts were not already loaded. ImFontAtlasBuildMain(atlas) end end -- Legacy backend if (not atlas.RendererHasTextures) then IM_ASSERT_USER_ERROR(atlas.TexIsBuilt, "Backend does not support ImGuiBackendFlags.RendererHasTextures, and font atlas is not built! Update backend OR make sure you called ImGui_ImplXXXX_NewFrame() function for renderer backend, which should call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8().") end if (atlas.TexIsBuilt and atlas.Builder.PreloadedAllGlyphsRanges) then IM_ASSERT_USER_ERROR(atlas.RendererHasTextures == false, "Called ImFontAtlas::Build() before ImGuiBackendFlags.RendererHasTextures got set! With new backends: you don't need to call Build().") end -- Clear BakedCurrent cache, this is important because it ensure the uncached path gets taken once. -- We also rely on ImFontBaked pointers never crossing frames. local builder = atlas.Builder builder.FrameCount = frame_count for _, font in atlas.Fonts:iter() do font.LastBaked = nil end -- Garbage collect BakedPool if builder.BakedDiscardedCount > 0 then local dst_n = 1 for src_n = 1, builder.BakedPool.Size do repeat local p_src = builder.BakedPool.Data[src_n] if p_src.WantDestroy then break end local p_dst = builder.BakedPool.Data[dst_n] dst_n = dst_n + 1 if p_dst == p_src then break end builder.BakedPool.Data[dst_n - 1] = p_src builder.BakedMap[p_src.BakedId] = p_src until true end IM_ASSERT(dst_n - 1 + builder.BakedDiscardedCount == builder.BakedPool.Size) builder.BakedPool.Size = builder.BakedPool.Size - builder.BakedDiscardedCount builder.BakedDiscardedCount = 0 end end --- @param src_pixels unsigned_char[] --- @param src_fmt ImTextureFormat --- @param src_pitch int --- @param dst_pixels unsigned_char[] --- @param dst_pixels_base int --- @param dst_fmt ImTextureFormat --- @param dst_pitch int --- @param w int --- @param h int function ImFontAtlasTextureBlockConvert(src_pixels, src_fmt, src_pitch, dst_pixels, dst_pixels_base, dst_fmt, dst_pitch, w, h) IM_ASSERT(src_pixels ~= nil and dst_pixels ~= nil) local src_pixels_base = 1 if src_fmt == dst_fmt then local line_sz = w * ImTextureDataGetFormatBytesPerPixel(src_fmt) for ny = h, 1, -1 do ImStd.memmove(dst_pixels, dst_pixels_base, src_pixels, src_pixels_base, line_sz) src_pixels_base = src_pixels_base + src_pitch dst_pixels_base = dst_pixels_base + dst_pitch end elseif src_fmt == ImTextureFormat.Alpha8 and dst_fmt == ImTextureFormat.RGBA32 then for ny = h, 1, -1 do for nx = w, 1, -1 do local alpha = src_pixels[src_pixels_base + 0] src_pixels_base = src_pixels_base + 1 write_u32(dst_pixels, dst_pixels_base, IM_COL32(255, 255, 255, alpha)) dst_pixels_base = dst_pixels_base + 4 end src_pixels_base = src_pixels_base + (src_pitch - w) dst_pixels_base = dst_pixels_base + (dst_pitch - w * 4) end elseif src_fmt == ImTextureFormat.RGBA32 and dst_fmt == ImTextureFormat.Alpha8 then for ny = h, 1, -1 do for nx = w, 1, -1 do local alpha = src_pixels[src_pixels_base + 3] src_pixels_base = src_pixels_base + 4 dst_pixels[dst_pixels_base + 0] = alpha dst_pixels_base = dst_pixels_base + 1 end src_pixels_base = src_pixels_base + (src_pitch - w * 4) dst_pixels_base = dst_pixels_base + (dst_pitch - w) end else IM_ASSERT(false) end end -- Source buffer may be written to (used for in-place mods). -- Post-process hooks may eventually be added here. --- @param data ImFontAtlasPostProcessData function ImFontAtlasTextureBlockPostProcess(data) -- Multiply operator (legacy) if data.FontSrc.RasterizerMultiply ~= 1.0 then ImFontAtlasTextureBlockPostProcessMultiply(data, data.FontSrc.RasterizerMultiply) end end --- @param data ImFontAtlasPostProcessData --- @param multiply_factor float function ImFontAtlasTextureBlockPostProcessMultiply(data, multiply_factor) local pixels, pixels_base = data.Pixels, data._PixelsBase local pitch = data.Pitch if data.Format == ImTextureFormat.Alpha8 then for ny = data.Height, 1, -1 do local p = pixels_base for nx = data.Width, 1, -1 do local v = ImMin(ImFloor(pixels[p + 0] * multiply_factor), 255) pixels[p + 0] = v p = p + 1 end pixels_base = pixels_base + pitch end elseif data.Format == ImTextureFormat.RGBA32 then for ny = data.Height, 1, -1 do local p = pixels_base for nx = data.Width, 1, -1 do local a = ImMin(ImFloor(pixels[p + 3] * multiply_factor), 255) write_u32(pixels, p, IM_COL32(pixels[p + 0], pixels[p + 1], pixels[p + 2], a)) p = p + 4 end pixels_base = pixels_base + pitch end else IM_ASSERT(false) end end -- Copy block from one texture to another --- @param src_tex ImTextureData --- @param src_x int --- @param src_y int --- @param dst_tex ImTextureData --- @param dst_x int --- @param dst_y int --- @param w int --- @param h int function ImFontAtlasTextureBlockCopy(src_tex, src_x, src_y, dst_tex, dst_x, dst_y, w, h) IM_ASSERT(src_tex.Pixels ~= nil and dst_tex.Pixels ~= nil) IM_ASSERT(src_tex.Format == dst_tex.Format) IM_ASSERT(src_x >= 0 and src_x + w <= src_tex.Width) IM_ASSERT(src_y >= 0 and src_y + h <= src_tex.Height) IM_ASSERT(dst_x >= 0 and dst_x + w <= dst_tex.Width) IM_ASSERT(dst_y >= 0 and dst_y + h <= dst_tex.Height) for y = 0, h - 1 do local dst, dst_base = dst_tex:GetPixelsAt(dst_x, dst_y + y) local src, src_base = src_tex:GetPixelsAt(src_x, src_y + y) ImStd.memmove(dst, dst_base, src, src_base, w * dst_tex.BytesPerPixel) end end -- Queue texture block update for renderer backend --- @param tex ImTextureData --- @param x int --- @param y int --- @param w int --- @param h int local function ImTextureDataQueueUpload(tex, x, y, w, h) IM_ASSERT(tex.Status ~= ImTextureStatus.WantDestroy and tex.Status ~= ImTextureStatus.Destroyed) IM_ASSERT(x >= 0 and x <= 0xFFFF and y >= 0 and y <= 0xFFFF and w >= 0 and x + w <= 0x10000 and h >= 0 and y + h <= 0x10000) local req = ImTextureRect(x, y, w, h) -- (unsigned short) local new_x1 = ImMax(tex.UpdateRect.w == 0 and 0 or tex.UpdateRect.x + tex.UpdateRect.w, req.x + req.w) local new_y1 = ImMax(tex.UpdateRect.h == 0 and 0 or tex.UpdateRect.y + tex.UpdateRect.h, req.y + req.h) tex.UpdateRect.x = ImMin(tex.UpdateRect.x, req.x) tex.UpdateRect.y = ImMin(tex.UpdateRect.y, req.y) tex.UpdateRect.w = (new_x1 - tex.UpdateRect.x) -- (unsigned short) tex.UpdateRect.h = (new_y1 - tex.UpdateRect.y) -- (unsigned short) tex.UsedRect.x = ImMin(tex.UsedRect.x, req.x) tex.UsedRect.y = ImMin(tex.UsedRect.y, req.y) tex.UsedRect.w = (ImMax(tex.UsedRect.x + tex.UsedRect.w, req.x + req.w) - tex.UsedRect.x) -- (unsigned short) tex.UsedRect.h = (ImMax(tex.UsedRect.y + tex.UsedRect.h, req.y + req.h) - tex.UsedRect.y) -- (unsigned short) -- No need to queue if status is == ImTextureStatus.WantCreate if (tex.Status == ImTextureStatus.OK or tex.Status == ImTextureStatus.WantUpdates) then tex.Status = ImTextureStatus.WantUpdates tex.Updates:push_back(req) end end --- @param atlas ImFontAtlas --- @param tex ImTextureData --- @param x int --- @param y int --- @param w int --- @param h int function ImFontAtlasTextureBlockQueueUpload(atlas, tex, x, y, w, h) ImTextureDataQueueUpload(tex, x, y, w, h) atlas.TexIsBuilt = false end -- Copy to texture, post-process and queue update for backend --- @param atlas ImFontAtlas --- @param baked ImFontBaked --- @param src ImFontConfig --- @param glyph ImFontGlyph --- @param r ImTextureRect --- @param src_pixels unsigned_char[] --- @param src_fmt ImTextureFormat --- @param src_pitch int local function ImFontAtlasBakedSetFontGlyphBitmap(atlas, baked, src, glyph, r, src_pixels, src_fmt, src_pitch) local tex = atlas.TexData IM_ASSERT(r.x + r.w <= tex.Width and r.y + r.h <= tex.Height) local out_p, out_p_base = tex:GetPixelsAt(r.x, r.y) IM_ASSERT(out_p ~= nil) ImFontAtlasTextureBlockConvert(src_pixels, src_fmt, src_pitch, out_p, out_p_base, tex.Format, tex:GetPitch(), r.w, r.h) local pp_data = ImFontAtlasPostProcessData(atlas, baked.OwnerFont, src, baked, glyph, out_p, out_p_base, tex.Format, tex:GetPitch(), r.w, r.h) ImFontAtlasTextureBlockPostProcess(pp_data) ImFontAtlasTextureBlockQueueUpload(atlas, tex, r.x, r.y, r.w, r.h) end local function ImFontAtlasBuildGetOversampleFactors(src, baked) -- (Only used by stb_truetype builder) -- Automatically disable horizontal oversampling over size 36 local raster_size = baked.Size * baked.RasterizerDensity * src.RasterizerDensity local out_oversample_h if src.OversampleH ~= 0 then out_oversample_h = src.OversampleH elseif raster_size > 36.0 or src.PixelSnapH then out_oversample_h = 1 else out_oversample_h = 2 end local out_oversample_v = (src.OversampleV ~= 0) and src.OversampleV or 1 return out_oversample_h, out_oversample_v end -- use unused_frames==0 to discard everything. local function ImFontAtlasBuildDiscardBakes(atlas, unused_frames) local builder = atlas.Builder for baked_n = 1, builder.BakedPool.Size do repeat local baked = builder.BakedPool[baked_n] if (baked.LastUsedFrame + unused_frames > atlas.Builder.FrameCount) then break end if (baked.WantDestroy or (bit32.band(baked.OwnerFont.Flags, ImFontFlags.LockBakedSizes) ~= 0)) then break end ImFontAtlasBakedDiscard(atlas, baked.OwnerFont, baked) until true end end -- Those functions are designed to facilitate changing the underlying structures for ImFontAtlas to store an array of ImDrawListSharedData --- @param atlas ImFontAtlas --- @param data ImDrawListSharedData function ImFontAtlasAddDrawListSharedData(atlas, data) IM_ASSERT(not atlas.DrawListSharedDatas:contains(data)) atlas.DrawListSharedDatas:push_back(data) end --- @param atlas ImFontAtlas --- @param data ImDrawListSharedData function ImFontAtlasRemoveDrawListSharedData(atlas, data) IM_ASSERT(atlas.DrawListSharedDatas:contains(data)) atlas.DrawListSharedDatas:find_erase(data) end --- @param atlas ImFontAtlas --- @param w int --- @param h int local function ImFontAtlasTextureRepack(atlas, w, h) local builder = atlas.Builder builder.LockDisableResize = true local old_tex = atlas.TexData local new_tex = ImFontAtlasTextureAdd(atlas, w, h) new_tex.UseColors = old_tex.UseColors IMGUI_DEBUG_LOG_FONT("[font] Texture #%03d: resize+repack %dx%d => Texture #%03d: %dx%d", old_tex.UniqueID, old_tex.Width, old_tex.Height, new_tex.UniqueID, new_tex.Width, new_tex.Height) --for baked_n = 1, builder.BakedPool.Size do -- IMGUI_DEBUG_LOG_FONT("[font] - Baked %.2fpx, %d glyphs, want_destroy=%d", builder.BakedPool[baked_n].FontSize, builder.BakedPool[baked_n].Glyphs.Size, builder.BakedPool[baked_n].WantDestroy) --end --IMGUI_DEBUG_LOG_FONT("[font] - Old packed rects: %d, area %d px", builder.RectsPackedCount, builder.RectsPackedSurface) --ImFontAtlasDebugWriteTexToDisk(old_tex, "Before Pack") -- Repack, lose discarded rectangle, copy pixels -- FIXME-NEWATLAS: This is unstable because packing order is based on RectsIndex -- FIXME-NEWATLAS-V2: Repacking in batch would be beneficial to packing heuristic, and fix stability. -- FIXME-NEWATLAS-TESTS: Test calling RepackTexture with size too small to fits existing rects. ImFontAtlasPackInit(atlas) local old_rects = ImVector() local old_index = builder.RectsIndex:copy() old_rects:swap(builder.Rects) for _, index_entry in builder.RectsIndex:iter() do repeat if index_entry.IsUsed == false then break end local old_r = old_rects[index_entry.TargetIndex + 1] if (old_r.w == 0 and old_r.h == 0) then break end local new_r_id = ImFontAtlasPackAddRect(atlas, old_r.w, old_r.h, index_entry) if new_r_id == ImFontAtlasRectId_Invalid then -- Undo, grow texture and try repacking again. -- FIXME-NEWATLAS-TESTS: This is a very rarely exercised path! It needs to be automatically tested properly. IMGUI_DEBUG_LOG_FONT("[font] Texture #%03d: resize failed. Will grow.", new_tex.UniqueID) new_tex.WantDestroyNextFrame = true builder.Rects:swap(old_rects) builder.RectsIndex = old_index ImFontAtlasBuildSetTexture(atlas, old_tex) ImFontAtlasTextureGrow(atlas, w, h) -- Recurse return end IM_ASSERT(ImFontAtlasRectId_GetIndex(new_r_id) == builder.RectsIndex:index_from_ptr(index_entry)) local new_r = ImFontAtlasPackGetRect(atlas, new_r_id) ImFontAtlasTextureBlockCopy(old_tex, old_r.x, old_r.y, new_tex, new_r.x, new_r.y, new_r.w, new_r.h) until true end IM_ASSERT(old_rects.Size == builder.Rects.Size + builder.RectsDiscardedCount) builder.RectsDiscardedCount = 0 builder.RectsDiscardedSurface = 0 -- Patch glyphs UV for baked_n = 1, builder.BakedPool.Size do for _, glyph in builder.BakedPool[baked_n].Glyphs:iter() do if (glyph.PackId ~= ImFontAtlasRectId_Invalid) then local r = ImFontAtlasPackGetRect(atlas, glyph.PackId) glyph.U0 = (r.x) * atlas.TexUvScale.x glyph.V0 = (r.y) * atlas.TexUvScale.y glyph.U1 = (r.x + r.w) * atlas.TexUvScale.x glyph.V1 = (r.y + r.h) * atlas.TexUvScale.y end end end -- Update other cached UV ImFontAtlasBuildUpdateTexData(atlas) builder.LockDisableResize = false ImFontAtlasUpdateDrawListsSharedData(atlas) --ImFontAtlasDebugWriteTexToDisk(new_tex, "After Pack") end function ImFontAtlasTextureGrow(atlas, old_tex_w, old_tex_h) --ImFontAtlasDebugWriteTexToDisk(atlas.TexData, "Before Grow") local builder = atlas.Builder old_tex_w = old_tex_w or -1 old_tex_h = old_tex_h or -1 if (old_tex_w == -1) then old_tex_w = atlas.TexData.Width end if (old_tex_h == -1) then old_tex_h = atlas.TexData.Height end -- FIXME-NEWATLAS-V2: What to do when reaching limits exposed by backend? -- FIXME-NEWATLAS-V2: Does ImFontAtlasFlags.NoPowerOfTwoHeight makes sense now? Allow 'lock' and 'compact' operations? IM_ASSERT(ImIsPowerOfTwo(old_tex_w) and ImIsPowerOfTwo(old_tex_h)) IM_ASSERT(ImIsPowerOfTwo(atlas.TexMinWidth) and ImIsPowerOfTwo(atlas.TexMaxWidth) and ImIsPowerOfTwo(atlas.TexMinHeight) and ImIsPowerOfTwo(atlas.TexMaxHeight)) -- Grow texture so it follows roughly a square. -- - Grow height before width, as width imply more packing nodes. -- - Caller should be taking account of RectsDiscardedSurface and may not need to grow. local new_tex_w = (old_tex_h <= old_tex_w) and old_tex_w or old_tex_w * 2 local new_tex_h = (old_tex_h <= old_tex_w) and old_tex_h * 2 or old_tex_h -- Handle minimum size first (for pathologically large packed rects) local pack_padding = atlas.TexGlyphPadding new_tex_w = ImMax(new_tex_w, ImUpperPowerOfTwo(builder.MaxRectSize.x + pack_padding)) new_tex_h = ImMax(new_tex_h, ImUpperPowerOfTwo(builder.MaxRectSize.y + pack_padding)) new_tex_w = ImClamp(new_tex_w, atlas.TexMinWidth, atlas.TexMaxWidth) new_tex_h = ImClamp(new_tex_h, atlas.TexMinHeight, atlas.TexMaxHeight) if (new_tex_w == old_tex_w and new_tex_h == old_tex_h) then return end ImFontAtlasTextureRepack(atlas, new_tex_w, new_tex_h) end --- @param atlas ImFontAtlas local function ImFontAtlasTextureMakeSpace(atlas) -- Can some baked contents be ditched? --IMGUI_DEBUG_LOG_FONT("[font] ImFontAtlasBuildMakeSpace()") local builder = atlas.Builder ImFontAtlasBuildDiscardBakes(atlas, 2) -- Currently using a heuristic for repack without growing. if (builder.RectsDiscardedSurface < builder.RectsPackedSurface * 0.20) then ImFontAtlasTextureGrow(atlas) else ImFontAtlasTextureRepack(atlas, atlas.TexData.Width, atlas.TexData.Height) end end -- This is essentially a free-list pattern, it may be nice to wrap it into a dedicated type. --- @param atlas ImFontAtlas --- @param rect_idx ImFontAtlasRectId local function ImFontAtlasPackAllocRectEntry(atlas, rect_idx) local builder = atlas.Builder local index_idx, index_entry if builder.RectsIndexFreeListStart < 0 then builder.RectsIndex:resize(builder.RectsIndex.Size + 1) index_idx = builder.RectsIndex.Size - 1 index_entry = ImFontAtlasRectEntry() builder.RectsIndex.Data[builder.RectsIndex.Size] = index_entry else index_idx = builder.RectsIndexFreeListStart index_entry = builder.RectsIndex[index_idx + 1] IM_ASSERT(index_entry.IsUsed == false and index_entry.Generation > 0) -- Generation is incremented during DiscardRect builder.RectsIndexFreeListStart = index_entry.TargetIndex end index_entry.TargetIndex = rect_idx index_entry.IsUsed = true return ImFontAtlasRectId_Make(index_idx, index_entry.Generation) end -- Overwrite existing entry --- @param atlas ImFontAtlas --- @param index_entry ImFontAtlasRectEntry local function ImFontAtlasPackReuseRectEntry(atlas, index_entry) IM_ASSERT(index_entry.IsUsed) index_entry.TargetIndex = atlas.Builder.Rects.Size - 1 local index_idx = atlas.Builder.RectsIndex:index_from_ptr(index_entry) return ImFontAtlasRectId_Make(index_idx, index_entry.Generation) end -- This is expected to be called in batches and followed by a repack --- @param atlas ImFontAtlas --- @param id ImFontAtlasRectId function ImFontAtlasPackDiscardRect(atlas, id) IM_ASSERT(id ~= ImFontAtlasRectId_Invalid) local rect = ImFontAtlasPackGetRect(atlas, id) if (rect == nil) then return end local builder = atlas.Builder local index_idx = ImFontAtlasRectId_GetIndex(id) local index_entry = builder.RectsIndex.Data[index_idx + 1] IM_ASSERT(index_entry.IsUsed and index_entry.TargetIndex >= 0) index_entry.IsUsed = false index_entry.TargetIndex = builder.RectsIndexFreeListStart index_entry.Generation = index_entry.Generation + 1 if (index_entry.Generation == 0) then index_entry.Generation = index_entry.Generation + 1 -- Keep non-zero on overflow end local pack_padding = atlas.TexGlyphPadding builder.RectsIndexFreeListStart = index_idx builder.RectsDiscardedCount = builder.RectsDiscardedCount + 1 builder.RectsDiscardedSurface = builder.RectsDiscardedSurface + ((rect.w + pack_padding) * (rect.h + pack_padding)) rect.w = 0 rect.h = 0 -- Clear rectangle so it won't be packed again end -- Important: Calling this may recreate a new texture and therefore change atlas.TexData -- FIXME-NEWFONTS: Expose other glyph padding settings for custom alteration (e.g. drop shadows). See #7962 function ImFontAtlasPackAddRect(atlas, w, h, overwrite_entry) IM_ASSERT(w > 0 and w <= 0xFFFF) IM_ASSERT(h > 0 and h <= 0xFFFF) local builder = atlas.Builder local pack_padding = atlas.TexGlyphPadding builder.MaxRectSize.x = ImMax(builder.MaxRectSize.x, w) builder.MaxRectSize.y = ImMax(builder.MaxRectSize.y, h) -- Pack local r = ImTextureRect(0, 0, w, h) for attempts_remaining = 3, 0, -1 do -- Try packing local pack_r = stbrp.rect() pack_r.w = w + pack_padding pack_r.h = h + pack_padding stbrp.pack_rects(builder.PackContext, {pack_r}, 1) r.x = pack_r.x -- (unsigned short) r.y = pack_r.y -- (unsigned short) if pack_r.was_packed == 1 then break end -- If we ran out of attempts, return fallback if (attempts_remaining == 0 or builder.LockDisableResize) then IMGUI_DEBUG_LOG_FONT("[font] Failed packing %dx%d rectangle. Returning fallback.", w, h) return ImFontAtlasRectId_Invalid end -- Resize or repack atlas! (this should be a rare event) ImFontAtlasTextureMakeSpace(atlas) end builder.MaxRectBounds.x = ImMax(builder.MaxRectBounds.x, r.x + r.w + pack_padding) builder.MaxRectBounds.y = ImMax(builder.MaxRectBounds.y, r.y + r.h + pack_padding) builder.RectsPackedCount = builder.RectsPackedCount + 1 builder.RectsPackedSurface = builder.RectsPackedSurface + (w + pack_padding) * (h + pack_padding) builder.Rects:push_back(r) if (overwrite_entry ~= nil) then return ImFontAtlasPackReuseRectEntry(atlas, overwrite_entry) -- Write into an existing entry instead of adding one (used during repack) else return ImFontAtlasPackAllocRectEntry(atlas, builder.Rects.Size - 1) end end -- Generally for non-user facing functions: assert on invalid ID. --- @param atlas ImFontAtlas --- @param id ImFontAtlasRectId --- @return ImTextureRect function ImFontAtlasPackGetRect(atlas, id) IM_ASSERT(id ~= ImFontAtlasRectId_Invalid) local index_idx = ImFontAtlasRectId_GetIndex(id) local builder = atlas.Builder local index_entry = builder.RectsIndex.Data[index_idx + 1] IM_ASSERT(index_entry.Generation == ImFontAtlasRectId_GetGeneration(id)) IM_ASSERT(index_entry.IsUsed) --- @type ImTextureRect return builder.Rects.Data[index_entry.TargetIndex + 1] end -- For user-facing functions: return nil on invalid ID. -- Important: return value is valid until next call to AddRect(), e.g. FindGlyph(), CalcTextSize() can all potentially invalidate previous rects. --- @param atlas ImFontAtlas --- @param id ImFontAtlasRectId function ImFontAtlasPackGetRectSafe(atlas, id) if id == ImFontAtlasRectId_Invalid then return nil end local index_idx = ImFontAtlasRectId_GetIndex(id) if atlas.Builder == nil then ImFontAtlasBuildInit(atlas) end local builder = atlas.Builder if index_idx >= builder.RectsIndex.Size then return nil end local index_entry = builder.RectsIndex.Data[index_idx + 1] if (index_entry.Generation ~= ImFontAtlasRectId_GetGeneration(id) or not index_entry.IsUsed) then return nil end return builder.Rects.Data[index_entry.TargetIndex + 1] end -- Important! This assume by ImFontConfig.GlyphExcludeRanges[] is a SMALL ARRAY (e.g. <10 entries) -- Use "Input Glyphs Overlap Detection Tool" to display a list of glyphs provided by multiple sources in order to set this array up. --- @param src ImFontConfig --- @param codepoint ImWchar --- @return bool function ImFontAtlasBuildAcceptCodepointForSource(src, codepoint) local exclude_list = src.GlyphExcludeRanges if exclude_list then local i = 1 while exclude_list[i] ~= 0 do if codepoint >= exclude_list[i] and codepoint <= exclude_list[i + 1] then return false end i = i + 2 end end return true end --- @param baked ImFontBaked --- @param new_size int function ImFontBaked_BuildGrowIndex(baked, new_size) IM_ASSERT(baked.IndexAdvanceX.Size == baked.IndexLookup.Size) if (new_size <= baked.IndexLookup.Size) then return end baked.IndexAdvanceX:resize(new_size, -1.0) baked.IndexLookup:resize(new_size, IM_FONTGLYPH_INDEX_UNUSED) end ------------------------------------------------------------------------------- -- [SECTION] ImFontAtlas: backend for stb_truetype ------------------------------------------------------------------------------- -- (imstb_truetype.lua in included near the top of this file) ------------------------------------------------------------------------------- -- One for each ConfigData --- @class ImGui_ImplStbTrueType_FontSrcData --- @field FontInfo stbtt_fontinfo --- @field ScaleFactor float --- @return ImGui_ImplStbTrueType_FontSrcData --- @nodiscard local function ImGui_ImplStbTrueType_FontSrcData() return { FontInfo = stbtt.fontinfo(), ScaleFactor = nil } end -- Need this in Lua since I don't want to include 'stbrp' just for context constructor -- in ImFontAtlasBuilder def code. The cpp code has 'stbrp_context_opaque' which I don't/can't have here --- @param atlas ImFontAtlas local function ImGui_ImplStbTrueType_LoaderInit(atlas) atlas.Builder.PackContext = atlas.Builder.PackContext or stbrp.context() end local function ImGui_ImplStbTrueType_FontSrcInit(atlas, src) -- IM_UNUSED(atlas) --- @type ImGui_ImplStbTrueType_FontSrcData? local bd_font_data = ImGui_ImplStbTrueType_FontSrcData() IM_ASSERT(src.FontLoaderData == nil) -- Initialize helper structure for font loading and verify that the TTF/OTF data is correct local font_offset = stbtt.GetFontOffsetForIndex(src.FontData, src.FontNo) if font_offset < 0 then bd_font_data = nil IM_ASSERT_USER_ERROR(0, "stbtt_GetFontOffsetForIndex(): FontData is incorrect, or FontNo cannot be found.") return false end if (not stbtt.InitFont(bd_font_data.FontInfo, src.FontData, font_offset)) then bd_font_data = nil IM_ASSERT_USER_ERROR(0, "stbtt_InitFont(): failed to parse FontData. It is correct and complete? Check FontDataSize.") return false end src.FontLoaderData = bd_font_data local ref_size = src.DstFont.Sources[1].SizePixels if (src.MergeMode and src.SizePixels == 0.0) then src.SizePixels = ref_size end bd_font_data.ScaleFactor = stbtt.ScaleForPixelHeight(bd_font_data.FontInfo, 1.0) if (src.MergeMode and src.SizePixels ~= 0.0 and ref_size ~= 0.0) then bd_font_data.ScaleFactor = bd_font_data.ScaleFactor * (src.SizePixels / ref_size) -- FIXME-NEWATLAS: Should tidy up that a bit end bd_font_data.ScaleFactor = bd_font_data.ScaleFactor * src.ExtraSizeScale return true end local function ImGui_ImplStbTrueType_FontSrcDestroy(atlas, src) -- IM_UNUSED(atlas) src.FontLoaderData = nil end local function ImGui_ImplStbTrueType_FontSrcContainsGlyph(atlas, src, codepoint) -- IM_UNUSED(atlas) local bd_font_data = src.FontLoaderData IM_ASSERT(bd_font_data ~= nil) local glyph_index = stbtt.FindGlyphIndex(bd_font_data.FontInfo, codepoint) return (glyph_index ~= 0) end local function ImGui_ImplStbTrueType_FontBakedInit(atlas, src, baked) -- IM_UNUSED(atlas) local bd_font_data = src.FontLoaderData if (src.MergeMode == false) then -- FIXME-NEWFONTS: reevaluate how to use sizing metrics -- FIXME-NEWFONTS: make use of line gap value local scale_for_layout = bd_font_data.ScaleFactor * baked.Size / src.ExtraSizeScale local unscaled_ascent, unscaled_descent, unscaled_line_gap = stbtt.GetFontVMetrics(bd_font_data.FontInfo) baked.Ascent = ImCeil(unscaled_ascent * scale_for_layout) baked.Descent = ImFloor(unscaled_descent * scale_for_layout) -- FIXME: round to rasterizer density? (#9348) end return true end --- @param atlas ImFontAtlas --- @param src ImFontConfig --- @param baked ImFontBaked --- @param _ any --- @param codepoint ImWchar --- @param out_glyph ImFontGlyph --- @param advance_x? float --- @return bool --- @return float? out_advance_x # Updated `advance_x` local function ImGui_ImplStbTrueType_FontBakedLoadGlyph(atlas, src, baked, _, codepoint, out_glyph, advance_x) -- Search for first font which has the glyph local bd_font_data = src.FontLoaderData IM_ASSERT(bd_font_data ~= nil) local glyph_index = stbtt.FindGlyphIndex(bd_font_data.FontInfo, codepoint) if (glyph_index == 0) then return false, advance_x end -- Fonts unit to pixels local oversample_h, oversample_v = ImFontAtlasBuildGetOversampleFactors(src, baked) local scale_for_layout = bd_font_data.ScaleFactor * baked.Size local rasterizer_density = src.RasterizerDensity * baked.RasterizerDensity local scale_for_raster_x = bd_font_data.ScaleFactor * baked.Size * rasterizer_density * oversample_h local scale_for_raster_y = bd_font_data.ScaleFactor * baked.Size * rasterizer_density * oversample_v -- Obtain size and advance local x0, y0, x1, y1 = stbtt.GetGlyphBitmapBoxSubpixel(bd_font_data.FontInfo, glyph_index, scale_for_raster_x, scale_for_raster_y, 0, 0) local advance, lsb = stbtt.GetGlyphHMetrics(bd_font_data.FontInfo, glyph_index) -- Load metrics only mode if (advance_x ~= nil) then IM_ASSERT(out_glyph == nil) advance_x = advance * scale_for_layout return true, advance_x end -- Prepare glyph out_glyph.Codepoint = codepoint out_glyph.AdvanceX = advance * scale_for_layout -- Pack and retrieve position inside texture atlas -- (generally based on stbtt_PackFontRangesRenderIntoRects) local is_visible = (x0 ~= x1 and y0 ~= y1) if is_visible then local w = (x1 - x0 + oversample_h - 1) local h = (y1 - y0 + oversample_v - 1) local pack_id = ImFontAtlasPackAddRect(atlas, w, h) if (pack_id == ImFontAtlasRectId_Invalid) then -- Pathological out of memory case (TexMaxWidth/TexMaxHeight set too small?) IM_ASSERT(pack_id ~= ImFontAtlasRectId_Invalid, "Out of texture memory.") return false, advance_x end local r = ImFontAtlasPackGetRect(atlas, pack_id) -- Render x0, y0, x1, y1 = stbtt.GetGlyphBitmapBox(bd_font_data.FontInfo, glyph_index, scale_for_raster_x, scale_for_raster_y) local builder = atlas.Builder builder.TempBuffer:resize(w * h * 1) local bitmap_pixels = builder.TempBuffer.Data ImStd.memset(bitmap_pixels, 0, w * h * 1) -- Render with oversampling -- (those functions conveniently assert if pixels are not cleared, which is another safety layer) local sub_x, sub_y = stbtt.MakeGlyphBitmapSubpixelPrefilter(bd_font_data.FontInfo, bitmap_pixels, w, h, w, scale_for_raster_x, scale_for_raster_y, 0, 0, oversample_h, oversample_v, glyph_index) local ref_size = baked.OwnerFont.Sources[1].SizePixels local offsets_scale = (ref_size ~= 0.0) and (baked.Size / ref_size) or 1.0 local font_off_x = ImFloor(src.GlyphOffset.x * offsets_scale + 0.5) -- Snap scaled offset. local font_off_y = ImFloor(src.GlyphOffset.y * offsets_scale + 0.5) font_off_x = font_off_x + sub_x font_off_y = font_off_y + (sub_y + IM_ROUND(baked.Ascent)) local recip_h = 1.0 / (oversample_h * rasterizer_density) local recip_v = 1.0 / (oversample_v * rasterizer_density) -- Register glyph -- r.x r.y are coordinates inside texture (in pixels) -- glyph.X0, glyph.Y0 are drawing coordinates from base text position, and accounting for oversampling. out_glyph.X0 = x0 * recip_h + font_off_x out_glyph.Y0 = y0 * recip_v + font_off_y out_glyph.X1 = (x0 + r.w) * recip_h + font_off_x out_glyph.Y1 = (y0 + r.h) * recip_v + font_off_y out_glyph.Visible = true out_glyph.PackId = pack_id ImFontAtlasBakedSetFontGlyphBitmap(atlas, baked, src, out_glyph, r, bitmap_pixels, ImTextureFormat.Alpha8, w) end return true, advance_x end local function ImFontAtlasGetFontLoaderForStbTruetype() local loader = ImFontLoader() loader.Name = "stb_truetype" loader.LoaderInit = ImGui_ImplStbTrueType_LoaderInit loader.FontSrcInit = ImGui_ImplStbTrueType_FontSrcInit loader.FontSrcDestroy = ImGui_ImplStbTrueType_FontSrcDestroy loader.FontSrcContainsGlyph = ImGui_ImplStbTrueType_FontSrcContainsGlyph loader.FontBakedInit = ImGui_ImplStbTrueType_FontBakedInit loader.FontBakedDestroy = nil loader.FontBakedLoadGlyph = ImGui_ImplStbTrueType_FontBakedLoadGlyph return loader end --- @param atlas ImFontAtlas --- @param baked ImFontBaked function ImFontAtlasBuildSetupFontBakedBlanks(atlas, baked) -- Mark space as always hidden (not strictly correct/necessary. but some e.g. icons fonts don't have a space. it tends to look neater in previews) local space_glyph = baked:FindGlyphNoFallback(32) if space_glyph ~= nil then space_glyph.Visible = false end -- Setup Tab character. -- FIXME: Needs proper TAB handling but it needs to be contextualized (or we could arbitrary say that each string starts at "column 0" ?) if baked:FindGlyphNoFallback(9) == nil and space_glyph ~= nil then local tab_glyph = ImFontGlyph() tab_glyph.Codepoint = 9 tab_glyph.AdvanceX = space_glyph.AdvanceX * IM_TABSIZE ImFontAtlasBakedAddFontGlyph(atlas, baked, nil, tab_glyph) end end --- @param atlas ImFontAtlas --- @param font ImFont --- @param font_size float --- @param font_rasterizer_density float --- @param baked_id ImGuiID --- @return ImFontBaked? function ImFontAtlasBakedAdd(atlas, font, font_size, font_rasterizer_density, baked_id) IMGUI_DEBUG_LOG_FONT("[font] Created baked %.2fpx", font_size) local baked = atlas.Builder.BakedPool:push_back(ImFontBaked()) baked.Size = font_size baked.RasterizerDensity = font_rasterizer_density baked.BakedId = baked_id baked.OwnerFont = font baked.LastUsedFrame = atlas.Builder.FrameCount -- Initialize backend data local loader_data_size = 0 for _, src in font.Sources:iter() do -- Cannot easily be cached as we allow changing backend local loader = src.FontLoader and src.FontLoader or atlas.FontLoader loader_data_size = loader_data_size + loader.FontBakedSrcLoaderDataSize end -- baked.FontLoaderDatas = (loader_data_size > 0) and IM_ALLOC(loader_data_size) or nil -- local loader_data_p = baked->FontLoaderDatas for _, src in font.Sources:iter() do local loader = src.FontLoader and src.FontLoader or atlas.FontLoader if (loader.FontBakedInit) then loader.FontBakedInit(atlas, src, baked--[[, loader_data_p--]]) end -- loader_data_p += loader->FontBakedSrcLoaderDataSize end ImFontAtlasBuildSetupFontBakedBlanks(atlas, baked) return baked end -- FIXME-OPT: This is not a fast query. Adding a BakedCount field in Font might allow to take a shortcut for the most common case. --- @param atlas ImFontAtlas --- @param font ImFont --- @param font_size float --- @param font_rasterizer_density float --- @return ImFontBaked? function ImFontAtlasBakedGetClosestMatch(atlas, font, font_size, font_rasterizer_density) local builder = atlas.Builder for step_n = 0, 1 do local closest_larger_match local closest_smaller_match for baked_n = 1, builder.BakedPool.Size do repeat local baked = builder.BakedPool[baked_n] if (baked.OwnerFont ~= font or baked.WantDestroy) then break end if (step_n == 0 and baked.RasterizerDensity ~= font_rasterizer_density) then -- First try with same density break end if (baked.Size > font_size and (closest_larger_match == nil or baked.Size < closest_larger_match.Size)) then closest_larger_match = baked end if (baked.Size < font_size and (closest_smaller_match == nil or baked.Size > closest_smaller_match.Size)) then closest_smaller_match = baked end until true end if (closest_larger_match) then if (closest_smaller_match == nil or (closest_larger_match.Size >= font_size * 2.0 and closest_smaller_match.Size > font_size * 0.5)) then return closest_larger_match end end if (closest_smaller_match) then return closest_smaller_match end end return nil end --- @param atlas ImFontAtlas --- @param font ImFont --- @param baked ImFontBaked function ImFontAtlasBakedDiscard(atlas, font, baked) local builder = atlas.Builder for _, glyph in baked.Glyphs:iter() do if glyph.PackId ~= ImFontAtlasRectId_Invalid then ImFontAtlasPackDiscardRect(atlas, glyph.PackId) end end -- char* loader_data_p = (char*)baked->FontLoaderDatas for _, src in font.Sources:iter() do local loader = src.FontLoader and src.FontLoader or atlas.FontLoader if loader.FontBakedDestroy then loader.FontBakedDestroy(atlas, src, baked) end end if baked.FontLoaderDatas then baked.FontLoaderDatas = nil end builder.BakedMap[baked.BakedId] = nil builder.BakedDiscardedCount = builder.BakedDiscardedCount + 1 baked:ClearOutputData() baked.WantDestroy = true font.LastBaked = nil end -- use unused_frames==0 to discard everything. function ImFontAtlasFontDiscardBakes(atlas, font, unused_frames) local builder = atlas.Builder if builder then -- This can be called from font destructor for baked_n = 1, builder.BakedPool.Size do repeat local baked = builder.BakedPool[baked_n] if baked.LastUsedFrame + unused_frames > atlas.Builder.FrameCount then break end if (baked.OwnerFont ~= font) or baked.WantDestroy then break end ImFontAtlasBakedDiscard(atlas, font, baked) until true end end end -- Was tempted to lazily init FontSrc but wouldn't save much + makes it more complicated to detect invalid data at AddFont() function ImFontAtlasFontInitOutput(atlas, font) local ret = true for _, src in font.Sources:iter() do if not ImFontAtlasFontSourceInit(atlas, src) then ret = false end end IM_ASSERT(ret) -- Unclear how to react to this meaningfully. Assume that result will be same as initial AddFont() call. return ret end -- Keep source/input FontData function ImFontAtlasFontDestroyOutput(atlas, font) font:ClearOutputData() for _, src in font.Sources:iter() do local loader = src.FontLoader and src.FontLoader or atlas.FontLoader if loader and loader.FontSrcDestroy ~= nil then loader.FontSrcDestroy(atlas, src) end end end --- @param atlas ImFontAtlas --- @param src ImFontConfig --- @return boolean function ImFontAtlasFontSourceInit(atlas, src) local loader = (src.FontLoader) and src.FontLoader or atlas.FontLoader if (loader.FontSrcInit ~= nil and not loader.FontSrcInit(atlas, src)) then return false end return true end --- @param atlas ImFontAtlas --- @param font ImFont --- @param src ImFontConfig function ImFontAtlasFontSourceAddToFont(atlas, font, src) if src.MergeMode == false then font:ClearOutputData() --font.FontSize = src.SizePixels font.OwnerAtlas = atlas IM_ASSERT(font.Sources[1] == src) end atlas.TexIsBuilt = false -- For legacy backends ImFontAtlasBuildSetupFontSpecialGlyphs(atlas, font, src) end --- @param atlas ImFontAtlas --- @param src ImFontConfig function ImFontAtlasFontDestroySourceData(atlas, src) -- IM_UNUSED(atlas) -- if src.FontDataOwnedByAtlas then -- IM_FREE(src.FontData) -- end src.FontData = nil -- if src.GlyphExcludeRanges then -- IM_FREE(src.GlyphExcludeRanges) -- end src.GlyphExcludeRanges = nil end -- Load/identify special glyphs -- (note that this is called again for fonts with MergeMode) --- @param atlas ImFontAtlas --- @param font ImFont --- @param src ImFontConfig function ImFontAtlasBuildSetupFontSpecialGlyphs(atlas, font, src) -- IM_UNUSED(atlas) IM_ASSERT(font.Sources:contains(src)) -- Find Fallback character. Actual glyph loaded in GetFontBaked(). --- @type ImWchar[] local fallback_chars = {font.FallbackChar, IM_UNICODE_CODEPOINT_INVALID, 63, 32} if font.FallbackChar == 0 then for _, candidate_char in ipairs(fallback_chars) do if candidate_char ~= 0 and font:IsGlyphInFont(candidate_char) then font.FallbackChar = candidate_char break end end end -- Setup Ellipsis character. It is required for rendering elided text. We prefer using U+2026 (horizontal ellipsis). -- However some old fonts may contain ellipsis at U+0085. Here we auto-detect most suitable ellipsis character. -- FIXME: Note that 0x2026 is rarely included in our font ranges. Because of this we are more likely to use three individual dots. --- @type ImWchar[] local ellipsis_chars = {src.EllipsisChar, 0x2026, 0x0085} if font.EllipsisChar == 0 then for _, candidate_char in ipairs(ellipsis_chars) do if candidate_char ~= 0 and font:IsGlyphInFont(candidate_char) then font.EllipsisChar = candidate_char break end end end if font.EllipsisChar == 0 then font.EllipsisChar = 0x0085 font.EllipsisAutoBake = true end end -- Create a compact, baked "..." if it doesn't exist, by using the ".". -- This may seem overly complicated right now but the point is to exercise and improve a technique which should be increasingly used. -- FIXME-NEWATLAS: This borrows too much from FontLoader's FontLoadGlyph() handlers and suggest that we should add further helpers. --- @param atlas ImFontAtlas --- @param baked ImFontBaked --- @return ImFontGlyph? function ImFontAtlasBuildSetupFontBakedEllipsis(atlas, baked) local font = baked.OwnerFont IM_ASSERT(font.EllipsisChar ~= 0) local dot_glyph = baked:FindGlyphNoFallback(46) -- '.' if (dot_glyph == nil) then dot_glyph = baked:FindGlyphNoFallback(0xFF0E) end if (dot_glyph == nil) then return nil end local dot_r_id = dot_glyph.PackId -- Deep copy to avoid invalidation of glyphs and rect pointers local dot_r = ImFontAtlasPackGetRect(atlas, dot_r_id) local dot_spacing = 1 local dot_step = (dot_glyph.X1 - dot_glyph.X0) + dot_spacing local pack_id = ImFontAtlasPackAddRect(atlas, (dot_r.w * 3 + dot_spacing * 2), dot_r.h) local r = ImFontAtlasPackGetRect(atlas, pack_id) local glyph = ImFontGlyph() glyph.Codepoint = font.EllipsisChar glyph.AdvanceX = ImMax(dot_glyph.AdvanceX, dot_glyph.X0 + dot_step * 3.0 - dot_spacing) -- FIXME: Slightly odd for normally mono-space fonts but since this is used for trailing contents. glyph.X0 = dot_glyph.X0 glyph.Y0 = dot_glyph.Y0 glyph.X1 = dot_glyph.X0 + dot_step * 3 - dot_spacing glyph.Y1 = dot_glyph.Y1 glyph.Visible = true glyph.PackId = pack_id glyph = ImFontAtlasBakedAddFontGlyph(atlas, baked, nil, glyph) dot_glyph = nil dot_r = ImFontAtlasPackGetRect(atlas, dot_r_id) local tex = atlas.TexData for n = 0, 2 do ImFontAtlasTextureBlockCopy(tex, dot_r.x, dot_r.y, tex, r.x + (dot_r.w + dot_spacing) * n, r.y, dot_r.w, dot_r.h) end ImFontAtlasTextureBlockQueueUpload(atlas, tex, r.x, r.y, r.w, r.h) return glyph end -- Load fallback in order to obtain its index -- (this is called from in hot-path so we avoid extraneous parameters to minimize impact on code size) --- @param baked ImFontBaked function ImFontAtlasBuildSetupFontBakedFallback(baked) IM_ASSERT(baked.FallbackGlyphIndex == -1) IM_ASSERT(baked.FallbackAdvanceX == 0.0) local font = baked.OwnerFont local fallback_glyph if (font.FallbackChar ~= 0) then fallback_glyph = baked:FindGlyphNoFallback(font.FallbackChar) end if (fallback_glyph == nil) then local space_glyph = baked:FindGlyphNoFallback(32) -- ' ' local glyph = ImFontGlyph() glyph.Codepoint = 0 glyph.AdvanceX = space_glyph and space_glyph.AdvanceX or IM_ROUND(baked.Size * 0.40) fallback_glyph = ImFontAtlasBakedAddFontGlyph(font.OwnerAtlas, baked, nil, glyph) end baked.FallbackGlyphIndex = baked.Glyphs:index_from_ptr(fallback_glyph) + 1 -- Storing index avoid need to update pointer on growth and simplify inner loop code baked.FallbackAdvanceX = fallback_glyph.AdvanceX end -- Setup main font loader for the atlas -- Every font source (ImFontConfig) will use this unless ImFontConfig.FontLoader specify a custom loader. function ImFontAtlasBuildSetupFontLoader(atlas, font_loader) if atlas.FontLoader == font_loader then return end IM_ASSERT(not atlas.Locked, "Cannot modify a locked ImFontAtlas!") for _, font in atlas.Fonts:iter() do ImFontAtlasFontDestroyOutput(atlas, font) end if atlas.Builder and atlas.FontLoader and atlas.FontLoader.LoaderShutdown then atlas.FontLoader.LoaderShutdown(atlas) end atlas.FontLoader = font_loader atlas.FontLoaderName = font_loader and font_loader.Name or "NULL" IM_ASSERT(atlas.FontLoaderData == nil) if atlas.Builder and atlas.FontLoader and atlas.FontLoader.LoaderInit then atlas.FontLoader.LoaderInit(atlas) end for _, font in atlas.Fonts:iter() do ImFontAtlasFontInitOutput(atlas, font) end for _, font in atlas.Fonts:iter() do for _, src in font.Sources:iter() do ImFontAtlasFontSourceAddToFont(atlas, font, src) end end end local function ImFontAtlasBuildUpdateRendererHasTexturesFromContext(atlas) -- [LEGACY] Copy back the ImGuiBackendFlags.RendererHasTextures flag from ImGui context. -- - This is the 1% exceptional case where that dependency if useful, to bypass an issue where otherwise at the -- time of an early call to Build(), it would be impossible for us to tell if the backend supports texture update. -- - Without this hack, we would have quite a pitfall as many legacy codebases have an early call to Build(). -- Whereas conversely, the portion of people using ImDrawList without ImGui is expected to be pathologically rare. for _, shared_data in atlas.DrawListSharedDatas:iter() do local imgui_ctx = shared_data.Context if (imgui_ctx) then atlas.RendererHasTextures = bit32.band(imgui_ctx.IO.BackendFlags, ImGuiBackendFlags.RendererHasTextures) ~= 0 break end end end local function ImFontAtlasBuildUpdatePointers(atlas) -- for _, font in atlas.Fonts:iter() do -- font.Sources:resize(0) -- end -- for _, src in atlas.Sources:iter() do -- src.DstFont.Sources:push_back(src) -- end end -- Update texture identifier in all active draw lists --- @param atlas ImFontAtlas --- @param old_tex ImTextureRef --- @param new_tex ImTextureRef local function ImFontAtlasUpdateDrawListsTextures(atlas, old_tex, new_tex) for _, shared_data in atlas.DrawListSharedDatas:iter() do repeat -- If Context 2 uses font owned by Context 1 which already called EndFrame()/Render(), we don't want to mess with draw commands for Context 1 if (shared_data.Context and not shared_data.Context.WithinFrameScope) then break end for _, draw_list in shared_data.DrawLists:iter() do -- Replace in command-buffer -- (there is not need to replace in ImDrawListSplitter: current channel is in ImDrawList's CmdBuffer[], -- other channels will be on SetCurrentChannel() which already needs to compare CmdHeader anyhow) if (draw_list.CmdBuffer.Size > 0 and draw_list._CmdHeader.TexRef == old_tex) then draw_list:_SetTexture(new_tex) end -- Replace in stack for i, stacked_tex in draw_list._TextureStack:iter() do if (stacked_tex == old_tex) then draw_list._TextureStack.Data[i] = new_tex end end end until true end end -- Set current texture. This is mostly called from AddTexture() + to handle a failed resize. function ImFontAtlasBuildSetTexture(atlas, tex) local old_tex_ref = atlas.TexRef atlas.TexData = tex atlas.TexUvScale = ImVec2(1.0 / tex.Width, 1.0 / tex.Height) atlas.TexRef._TexData = tex --atlas.TexRef._TexID = tex.TexID -- <-- We intentionally don't do that. It would be misleading and betray promise that both fields aren't set. ImFontAtlasUpdateDrawListsTextures(atlas, old_tex_ref, atlas.TexRef) end --- @param atlas ImFontAtlas function ImFontAtlasBuildUpdateTexData(atlas) ImFontAtlasBuildUpdateTexDataLines(atlas) ImFontAtlasBuildUpdateTexDataBasic(atlas) end -- Create a new texture, discard previous one function ImFontAtlasTextureAdd(atlas, w, h) local old_tex = atlas.TexData local new_tex -- FIXME: Cannot reuse texture because old UV may have been used already (unless we remap UV). --if (old_tex ~= nil and old_tex.Status == ImTextureStatus.WantCreate) then -- -- Reuse texture not yet used by backend. -- IM_ASSERT(old_tex.TexID == ImTextureID_Invalid and old_tex.BackendUserData == nil) -- old_tex:DestroyPixels() -- old_tex.Updates:clear() -- new_tex = old_tex -- old_tex = nil --else -- Add new new_tex = ImTextureData() new_tex.UniqueID = atlas.TexNextUniqueID atlas.TexNextUniqueID = atlas.TexNextUniqueID + 1 atlas.TexList:push_back(new_tex) if old_tex ~= nil then -- Queue old as to destroy next frame old_tex.WantDestroyNextFrame = true IM_ASSERT(old_tex.Status == ImTextureStatus.OK or old_tex.Status == ImTextureStatus.WantCreate or old_tex.Status == ImTextureStatus.WantUpdates) end new_tex:Create(atlas.TexDesiredFormat, w, h) atlas.TexIsBuilt = false ImFontAtlasBuildSetTexture(atlas, new_tex) return new_tex end --- @param atlas ImFontAtlas --- @return ImVec2 function ImFontAtlasTextureGetSizeEstimate(atlas) local min_w = ImUpperPowerOfTwo(atlas.TexMinWidth) local min_h = ImUpperPowerOfTwo(atlas.TexMinHeight) if (atlas.Builder == nil or atlas.TexData == nil or atlas.TexData.Status == ImTextureStatus.WantDestroy) then return ImVec2(min_w, min_h) -- ImVec2i end local builder = atlas.Builder min_w = ImMax(ImUpperPowerOfTwo(builder.MaxRectSize.x), min_w) min_h = ImMax(ImUpperPowerOfTwo(builder.MaxRectSize.y), min_h) local surface_approx = builder.RectsPackedSurface - builder.RectsDiscardedSurface -- Expected surface after repack local surface_sqrt = ImFloor(ImSqrt(surface_approx)) local new_tex_w local new_tex_h if (min_w >= min_h) then new_tex_w = ImMax(min_w, ImUpperPowerOfTwo(surface_sqrt)) new_tex_h = ImMax(min_h, ImFloor((surface_approx + new_tex_w - 1) / new_tex_w)) if (bit32.band(atlas.Flags, ImFontAtlasFlags.NoPowerOfTwoHeight) == 0) then new_tex_h = ImUpperPowerOfTwo(new_tex_h) end else new_tex_h = ImMax(min_h, ImUpperPowerOfTwo(surface_sqrt)) if (bit32.band(atlas.Flags, ImFontAtlasFlags.NoPowerOfTwoHeight) == 0) then new_tex_h = ImUpperPowerOfTwo(new_tex_h) end new_tex_w = ImMax(min_w, ImFloor((surface_approx + new_tex_h - 1) / new_tex_h)) end IM_ASSERT(ImIsPowerOfTwo(new_tex_w) and ImIsPowerOfTwo(new_tex_h)) return ImVec2(new_tex_w, new_tex_h) -- ImVec2i end -- Clear all output. Invalidates all AddCustomRect() return values! --- @param atlas ImFontAtlas function ImFontAtlasBuildClear(atlas) local new_tex_size = ImFontAtlasTextureGetSizeEstimate(atlas) -- ImVec2i ImFontAtlasBuildDestroy(atlas) ImFontAtlasTextureAdd(atlas, new_tex_size.x, new_tex_size.y) ImFontAtlasBuildInit(atlas) for _, src in atlas.Sources:iter() do ImFontAtlasFontSourceInit(atlas, src) end for _, font in atlas.Fonts:iter() do for _, src in font.Sources:iter() do ImFontAtlasFontSourceAddToFont(atlas, font, src) end end end -- You should not need to call this manually! -- If you think you do, let us know and we can advise about policies auto-compact. --- @param atlas ImFontAtlas function ImFontAtlasTextureCompact(atlas) local builder = atlas.Builder ImFontAtlasBuildDiscardBakes(atlas, 1) local old_tex = atlas.TexData local old_tex_size = ImVec2(old_tex.Width, old_tex.Height) local new_tex_size = ImFontAtlasTextureGetSizeEstimate(atlas) if (builder.RectsDiscardedCount == 0 and new_tex_size.x == old_tex_size.x and new_tex_size.y == old_tex_size.y) then return end ImFontAtlasTextureRepack(atlas, new_tex_size.x, new_tex_size.y) end --- @param atlas ImFontAtlas function ImFontAtlasPackInit(atlas) local tex = atlas.TexData local builder = atlas.Builder -- In theory we could decide to reduce the number of nodes, e.g. halve them, and waste a little texture space, but it doesn't seem worth it. local pack_node_count = ImFloor(tex.Width / 2) builder.PackNodes:resize(pack_node_count) for i = 1, builder.PackNodes.Size do builder.PackNodes.Data[i] = stbrp.node() end stbrp.init_target(builder.PackContext, tex.Width, tex.Height, builder.PackNodes.Data, builder.PackNodes.Size) builder.RectsPackedCount = 0 builder.RectsPackedSurface = 0 builder.MaxRectSize = ImVec2(0, 0) -- ImVec2i builder.MaxRectBounds = ImVec2(0, 0) -- ImVec2i end --- @param atlas ImFontAtlas function ImFontAtlasBuildUpdateTexDataLines(atlas) if bit32.band(atlas.Flags, ImFontAtlasFlags.NoBakedLines) ~= 0 then return end -- Pack and store identifier so we can refresh UV coordinates on texture resize. local tex = atlas.TexData local builder = atlas.Builder local r = ImFontAtlasRect() local add_and_draw = (atlas:GetCustomRect(builder.PackIdLinesTexData, r) == false) if add_and_draw then -- Register texture region for thick lines -- The +2 here is to give space for the end caps, whilst height +1 is to accommodate the fact we have a zero-width row local pack_size = ImVec2(IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 2, IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1) -- ImVec2i builder.PackIdLinesTexData = atlas:AddCustomRect(pack_size.x, pack_size.y, r) IM_ASSERT(builder.PackIdLinesTexData ~= ImFontAtlasRectId_Invalid) end for n = 0, IM_DRAWLIST_TEX_LINES_WIDTH_MAX do -- +1 because of the zero-width row -- Each line consists of at least two empty pixels at the ends, with a line of solid pixels in the middle local y = n local line_width = n --- @type int # If not floored, will cause black and purple horizontal stripes appearing on the right of the arrow(triangle) in the upper-left most area of the atlas local pad_left = math.floor((r.w - line_width) / 2) --- @type int local pad_right = r.w - (pad_left + line_width) IM_ASSERT(pad_left + line_width + pad_right == r.w and y < r.h) -- Make sure we're inside the texture bounds before we start writing pixels -- Write each slice if (add_and_draw and tex.Format == ImTextureFormat.Alpha8) then local write_ptr, write_ptr_base = tex:GetPixelsAt(r.x, r.y + y) for i = 0, pad_left - 1 do write_ptr[write_ptr_base + i] = 0x00 end for i = 0, line_width - 1 do write_ptr[write_ptr_base + pad_left + i] = 0xFF end for i = 0, pad_right - 1 do write_ptr[write_ptr_base + pad_left + line_width + i] = 0x00 end elseif (add_and_draw and tex.Format == ImTextureFormat.RGBA32) then local write_ptr, write_ptr_base = tex:GetPixelsAt(r.x, r.y + y) for i = 0, pad_left - 1 do write_u32(write_ptr, write_ptr_base + i * 4, IM_COL32(255, 255, 255, 0)) end for i = 0, line_width - 1 do write_u32(write_ptr, write_ptr_base + (pad_left + i) * 4, IM_COL32_WHITE) end for i = 0, pad_right - 1 do write_u32(write_ptr, write_ptr_base + (pad_left + line_width + i) * 4, IM_COL32(255, 255, 255, 0)) end end -- Refresh UV coordinates local uv0 = ImVec2(r.x + pad_left - 1, r.y + y) * atlas.TexUvScale local uv1 = ImVec2(r.x + pad_left + line_width + 1, r.y + y + 1) * atlas.TexUvScale local half_v = (uv0.y + uv1.y) * 0.5 -- Calculate a constant V in the middle of the row to avoid sampling artifacts atlas.TexUvLines[n] = ImVec4(uv0.x, half_v, uv1.x, half_v) end end -- Render a white-colored bitmap encoded in a string --- @param atlas ImFontAtlas --- @param x int --- @param y int --- @param w int --- @param h int --- @param in_str string[] --- @param in_marker_char string function ImFontAtlasBuildRenderBitmapFromString(atlas, x, y, w, h, in_str, in_marker_char) local tex = atlas.TexData IM_ASSERT(x >= 0 and x + w <= tex.Width) IM_ASSERT(y >= 0 and y + h <= tex.Height) if tex.Format == ImTextureFormat.Alpha8 then local out_p, out_p_base = tex:GetPixelsAt(x, y) local str_base = 1 for off_y = 0, h - 1 do for off_x = 0, w - 1 do out_p[out_p_base + off_x] = (in_str[str_base + off_x] == in_marker_char) and 0xFF or 0x00 end out_p_base = out_p_base + tex.Width str_base = str_base + w end elseif tex.Format == ImTextureFormat.RGBA32 then local out_p, out_p_base = tex:GetPixelsAt(x, y) local str_base = 1 for off_y = 0, h - 1 do for off_x = 0, w - 1 do write_u32(out_p, out_p_base + off_x * 4, (in_str[str_base + off_x] == in_marker_char) and IM_COL32_WHITE or IM_COL32_BLACK_TRANS) end out_p_base = out_p_base + tex.Width * 4 str_base = str_base + w end end end --- @param atlas ImFontAtlas function ImFontAtlasBuildUpdateTexDataBasic(atlas) -- Pack and store identifier so we can refresh UV coordinates on texture resize. -- FIXME-NEWATLAS: User/custom rects where user code wants to store UV coordinates will need to do the same thing. local builder = atlas.Builder local pack_size = (bit32.band(atlas.Flags, ImFontAtlasFlags.NoMouseCursors) ~= 0) and ImVec2(2, 2) or ImVec2(FONT_ATLAS_DEFAULT_TEX_DATA_W * 2 + 1, FONT_ATLAS_DEFAULT_TEX_DATA_H) local r = ImFontAtlasRect() local add_and_draw = (atlas:GetCustomRect(builder.PackIdMouseCursors, r) == false) if (add_and_draw) then builder.PackIdMouseCursors = atlas:AddCustomRect(pack_size.x, pack_size.y, r) IM_ASSERT(builder.PackIdMouseCursors ~= ImFontAtlasRectId_Invalid) -- Draw to texture if bit32.band(atlas.Flags, ImFontAtlasFlags.NoMouseCursors) ~= 0 then -- 2x2 white pixels ImFontAtlasBuildRenderBitmapFromString(atlas, r.x, r.y, 2, 2, {"X", "X", "X", "X"}, "X") else -- 2x2 white pixels + mouse cursors local x_for_white = r.x local x_for_black = r.x + FONT_ATLAS_DEFAULT_TEX_DATA_W + 1 ImFontAtlasBuildRenderBitmapFromString(atlas, x_for_white, r.y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, ".") ImFontAtlasBuildRenderBitmapFromString(atlas, x_for_black, r.y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, "X") end end -- Refresh UV coordinates atlas.TexUvWhitePixel = ImVec2((r.x + 0.5) * atlas.TexUvScale.x, (r.y + 0.5) * atlas.TexUvScale.y) end -- Update texture coordinates in all draw list shared context -- FIXME-NEWATLAS FIXME-OPT: Doesn't seem necessary to update for all, only one bound to current context? function ImFontAtlasUpdateDrawListsSharedData(atlas) for _, shared_data in atlas.DrawListSharedDatas:iter() do if (shared_data.FontAtlas == atlas) then shared_data.TexUvWhitePixel = atlas.TexUvWhitePixel shared_data.TexUvLines = atlas.TexUvLines end end end -- Start packing over current empty texture function ImFontAtlasBuildInit(atlas) -- Select Backend -- - Note that we do not reassign to atlas.FontLoader, since it is likely to point to static data which -- may mess with some hot-reloading schemes. If you need to assign to this (for dynamic selection) AND are -- using a hot-reloading scheme that messes up static data, store your own instance of FontLoader somewhere -- and point to it instead of pointing directly to return value of the GetFontLoaderXXX functions. if atlas.FontLoader == nil then -- IMGUI_ENABLE_STB_TRUETYPE atlas:SetFontLoader(ImFontAtlasGetFontLoaderForStbTruetype()) end -- Create initial texture size if atlas.TexData == nil or atlas.TexData.Pixels == nil then ImFontAtlasTextureAdd(atlas, ImUpperPowerOfTwo(atlas.TexMinWidth), ImUpperPowerOfTwo(atlas.TexMinHeight)) end atlas.Builder = ImFontAtlasBuilder() if atlas.FontLoader.LoaderInit then atlas.FontLoader.LoaderInit(atlas) end ImFontAtlasBuildUpdateRendererHasTexturesFromContext(atlas) ImFontAtlasPackInit(atlas) -- Add required texture data ImFontAtlasBuildUpdateTexData(atlas) -- Register fonts ImFontAtlasBuildUpdatePointers(atlas) -- Update UV coordinates etc. stored in bound ImDrawListSharedData instance ImFontAtlasUpdateDrawListsSharedData(atlas) --atlas.TexIsBuilt = true -- Lazily initialize char/text classifier -- FIXME: This could be practically anywhere, and should eventually be parameters to CalcTextSize/word-wrapping code, but there's no obvious spot now. ImTextInitClassifiers() end -- Destroy builder and all cached glyphs. Do not destroy actual fonts. --- @param atlas ImFontAtlas function ImFontAtlasBuildDestroy(atlas) for _, font in atlas.Fonts:iter() do ImFontAtlasFontDestroyOutput(atlas, font) end if atlas.Builder and atlas.FontLoader and atlas.FontLoader.LoaderShutdown then atlas.FontLoader.LoaderShutdown(atlas) IM_ASSERT(atlas.FontLoaderData == nil) end atlas.Builder = nil end -- On font removal we need to remove references (otherwise we could queue removal?) -- We allow old_font == new_font which forces updating all values (e.g. sizes) --- @param atlas ImFontAtlas --- @param old_font ImFont? --- @param new_font ImFont? function ImFontAtlasBuildNotifySetFont(atlas, old_font, new_font) for _, shared_data in atlas.DrawListSharedDatas:iter() do if shared_data.Font == old_font then shared_data.Font = new_font end local ctx = shared_data.Context if ctx then repeat -- While this should work either way, we save ourselves the bother / debugging confusion of running ImGui code so early when it is not needed. -- Also fixes erroneously rewriting style.FontSizeBase during init if adding default fonts. if old_font == nil and ctx.Font == nil and ctx.FontSizeBase == 0.0 then break end if ctx.IO.FontDefault == old_font then ctx.IO.FontDefault = new_font end if ctx.Font == old_font then local curr_ctx = ImGui.GetCurrentContext() local need_bind_ctx = ctx ~= curr_ctx if need_bind_ctx then ImGui.SetCurrentContext(ctx) end ImGui.SetCurrentFont(new_font, ctx.FontSizeBase, ctx.FontSize) if need_bind_ctx then ImGui.SetCurrentContext(curr_ctx) end end for _, font_stack_data in ctx.FontStack:iter() do if font_stack_data.Font == old_font then font_stack_data.Font = new_font end end until true end end end -- When atlas.RendererHasTextures = true, this is only called if no font were loaded. --- @param atlas ImFontAtlas function ImFontAtlasBuildMain(atlas) IM_ASSERT(not atlas.Locked, "Cannot modify a locked ImFontAtlas!") if (atlas.TexData and atlas.TexData.Format ~= atlas.TexDesiredFormat) then ImFontAtlasBuildClear(atlas) end if atlas.Builder == nil then ImFontAtlasBuildInit(atlas) end -- Default font is none are specified if atlas.Sources.Size == 0 then atlas:AddFontDefault() end -- [LEGACY] For backends not supporting RendererHasTextures: preload all glyphs -- ImFontAtlasBuildUpdateRendererHasTexturesFromContext(atlas); -- if atlas.RendererHasTextures == false then -- ~ImGuiBackendFlags.RendererHasTextures -- ImFontAtlasBuildLegacyPreloadAllGlyphRanges(atlas) -- end atlas.TexIsBuilt = true end ------------------------------------------------------------------------------- -- [SECTION] ImTextureData ------------------------------------------------------------------------------- -- - ImTextureData:Create() -- - ImTextureData:DestroyPixels() ------------------------------------------------------------------------------- function MT.ImTextureData:DestroyPixels() self.Pixels = nil self.UseColors = false end function ImTextureDataGetFormatBytesPerPixel(format) if format == ImTextureFormat.Alpha8 then return 1 elseif format == ImTextureFormat.RGBA32 then return 4 end IM_ASSERT(false) return 0 end function MT.ImTextureData:Create(format, w, h) IM_ASSERT(self.Status == ImTextureStatus.Destroyed) self:DestroyPixels() self.Format = format self.Status = ImTextureStatus.WantCreate self.Width = w self.Height = h self.BytesPerPixel = ImTextureDataGetFormatBytesPerPixel(format) self.UseColors = false self.Pixels = {} ImStd.memset(self.Pixels, 0, self.Width * self.Height * self.BytesPerPixel) self.UsedRect.x = 0 self.UsedRect.y = 0 self.UsedRect.w = 0 self.UsedRect.h = 0 self.UpdateRect.x = 65535 self.UpdateRect.y = 65535 self.UpdateRect.w = 0 self.UpdateRect.h = 0 end ------------------------------------------------------------------------------- -- [SECTION] ImFontBaked, ImFont ------------------------------------------------------------------------------- function MT.ImFontBaked:ClearOutputData() self.FallbackAdvanceX = 0.0 self.Glyphs:clear() self.IndexAdvanceX:clear() self.IndexLookup:clear() self.FallbackGlyphIndex = -1 self.Ascent = 0.0 self.Descent = 0.0 self.MetricsTotalSurface = 0 end -- Find glyph, load if necessary, return fallback if missing --- @param c ImWchar --- @return ImFontGlyph? function MT.ImFontBaked:FindGlyph(c) if c < self.IndexLookup.Size then local i = self.IndexLookup.Data[c + 1] if i == IM_FONTGLYPH_INDEX_NOT_FOUND then return self.Glyphs.Data[self.FallbackGlyphIndex] end if i ~= IM_FONTGLYPH_INDEX_UNUSED then return self.Glyphs.Data[i] end end local glyph = ImFontBaked_BuildLoadGlyph(self, c, nil) return (glyph) and glyph or self.Glyphs.Data[self.FallbackGlyphIndex] end function MT.ImFontBaked:IsGlyphLoaded(c) if c >= self.IndexLookup.Size then return false end return self.IndexLookup.Data[c + 1] ~= IM_FONTGLYPH_INDEX_UNUSED end -- Attempt to load but when missing, return nil instead of FallbackGlyph --- @param c ImWchar --- @return ImFontGlyph? function MT.ImFontBaked:FindGlyphNoFallback(c) if c < self.IndexLookup.Size then -- IM_LIKELY local i = self.IndexLookup.Data[c + 1] if i == IM_FONTGLYPH_INDEX_NOT_FOUND then return nil end if i ~= IM_FONTGLYPH_INDEX_UNUSED then return self.Glyphs.Data[i] end end self.LoadNoFallback = true -- This is actually a rare call, not done in hot-loop, so we prioritize not adding extra cruft to ImFontBaked_BuildLoadGlyph() call sites. local glyph = ImFontBaked_BuildLoadGlyph(self, c, nil) self.LoadNoFallback = false return glyph end function MT.ImFont:ClearOutputData() local atlas = self.OwnerAtlas if atlas ~= nil then ImFontAtlasFontDiscardBakes(atlas, self, 0) end --self.FallbackChar = 0 self.EllipsisChar = 0 for i = 1, (IM_UNICODE_CODEPOINT_MAX + 1) / 8192 / 8 do self.Used8kPagesMap[i] = 0 end self.LastBaked = nil end --- @param font_id ImGuiID --- @param baked_size float --- @param rasterizer_density float --- @return ImGuiID function ImFontAtlasBakedGetId(font_id, baked_size, rasterizer_density) local hashed_data = {font_id, baked_size, rasterizer_density} return ImHashData(hashed_data) end --- @param atlas ImFontAtlas --- @param font ImFont --- @param font_size float --- @param font_rasterizer_density float --- @return ImFontBaked? function ImFontAtlasBakedGetOrAdd(atlas, font, font_size, font_rasterizer_density) -- FIXME-NEWATLAS: Design for picking a nearest size based on some criteria? -- FIXME-NEWATLAS: Altering font density won't work right away. IM_ASSERT(font_size > 0.0 and font_rasterizer_density > 0.0) local baked_id = ImFontAtlasBakedGetId(font.FontId, font_size, font_rasterizer_density) local builder = atlas.Builder local baked = builder.BakedMap[baked_id] if baked ~= nil then IM_ASSERT(baked.Size == font_size and baked.OwnerFont == font and baked.BakedId == baked_id) return baked end -- If atlas is locked, find closest match -- FIXME-OPT: This is not an optimal query. if (bit32.band(font.Flags, ImFontFlags.LockBakedSizes) ~= 0 or atlas.Locked) then baked = ImFontAtlasBakedGetClosestMatch(atlas, font, font_size, font_rasterizer_density) if baked ~= nil then return baked end if atlas.Locked then IM_ASSERT(not atlas.Locked, "Cannot use dynamic font size with a locked ImFontAtlas!") -- Locked because rendering backend does not support ImGuiBackendFlags.RendererHasTextures! return nil end end -- Create new baked = ImFontAtlasBakedAdd(atlas, font, font_size, font_rasterizer_density, baked_id) builder.BakedMap[baked_id] = baked return baked end -- x0/y0/x1/y1 are offset from the character upper-left layout position, in pixels. Therefore x0/y0 are often fairly close to zero. -- Not to be mistaken with texture coordinates, which are held by u0/v0/u1/v1 in normalized format (0.0..1.0 on each texture axis). -- - 'src' is not necessarily == 'self.Sources' because multiple source fonts+configs can be used to build one target font. --- @param atlas ImFontAtlas --- @param baked ImFontBaked --- @param src? ImFontConfig --- @param in_glyph ImFontGlyph --- @return ImFontGlyph function ImFontAtlasBakedAddFontGlyph(atlas, baked, src, in_glyph) local glyph_idx = baked.Glyphs.Size + 1 baked.Glyphs:push_back(in_glyph) local glyph = baked.Glyphs.Data[glyph_idx] IM_ASSERT(baked.Glyphs.Size < 0xFFFE) -- IndexLookup[] hold 16-bit values and -1/-2 are reserved. -- Set UV from packed rectangle if (glyph.PackId ~= ImFontAtlasRectId_Invalid) then local r = ImFontAtlasPackGetRect(atlas, glyph.PackId) IM_ASSERT(glyph.U0 == 0.0 and glyph.V0 == 0.0 and glyph.U1 == 0.0 and glyph.V1 == 0.0) glyph.U0 = (r.x) * atlas.TexUvScale.x glyph.V0 = (r.y) * atlas.TexUvScale.y glyph.U1 = (r.x + r.w) * atlas.TexUvScale.x glyph.V1 = (r.y + r.h) * atlas.TexUvScale.y baked.MetricsTotalSurface = baked.MetricsTotalSurface + r.w * r.h end if (src ~= nil) then -- Clamp & recenter if needed local ref_size = baked.OwnerFont.Sources[1].SizePixels local offsets_scale = (ref_size ~= 0.0) and (baked.Size / ref_size) or 1.0 local advance_x = ImClamp(glyph.AdvanceX, src.GlyphMinAdvanceX * offsets_scale, src.GlyphMaxAdvanceX * offsets_scale) if (advance_x ~= glyph.AdvanceX) then local char_off_x = src.PixelSnapH and ImTrunc((advance_x - glyph.AdvanceX) * 0.5) or (advance_x - glyph.AdvanceX) * 0.5 glyph.X0 = glyph.X0 + char_off_x glyph.X1 = glyph.X1 + char_off_x end -- Snap to pixel if (src.PixelSnapH) then advance_x = IM_ROUND(advance_x) end -- Bake spacing glyph.AdvanceX = advance_x + src.GlyphExtraAdvanceX end if (glyph.Colored) then atlas.TexData.UseColors = true atlas.TexPixelsUseColors = true end -- Update lookup tables local codepoint = glyph.Codepoint ImFontBaked_BuildGrowIndex(baked, codepoint + 1) baked.IndexAdvanceX.Data[codepoint + 1] = glyph.AdvanceX baked.IndexLookup.Data[codepoint + 1] = glyph_idx -- (ImU16) local page_n = math.floor(codepoint / 8192) baked.OwnerFont.Used8kPagesMap[bit32.rshift(page_n, 3) + 1] = bit32.bor(baked.OwnerFont.Used8kPagesMap[bit32.rshift(page_n, 3) + 1], bit32.lshift(1, bit32.band(page_n, 7))) return glyph end -- FIXME: Code is duplicated with code above. --- @param atlas ImFontAtlas --- @param baked ImFontBaked --- @param src? ImFontConfig --- @param codepoint ImWchar --- @param advance_x float function ImFontAtlasBakedAddFontGlyphAdvancedX(atlas, baked, src, codepoint, advance_x) -- IM_UNUSED(atlas) if (src ~= nil) then -- Clamp & recenter if needed local ref_size = baked.OwnerFont.Sources[1].SizePixels local offsets_scale = (ref_size ~= 0.0) and (baked.Size / ref_size) or 1.0 advance_x = ImClamp(advance_x, src.GlyphMinAdvanceX * offsets_scale, src.GlyphMaxAdvanceX * offsets_scale) -- Snap to pixel if (src.PixelSnapH) then advance_x = IM_ROUND(advance_x) end -- Bake spacing advance_x = advance_x + src.GlyphExtraAdvanceX end ImFontBaked_BuildGrowIndex(baked, codepoint + 1) baked.IndexAdvanceX.Data[codepoint + 1] = advance_x end -- ImFontBaked are valid for the entire frame but shall never be kept between frames. --- @param size float --- @param density float? --- @return ImFontBaked? function MT.ImFont:GetFontBaked(size, density) if not density then density = -1.0 end --- @type ImFontBaked? local baked = self.LastBaked -- Round font size -- - ImGui.PushFont() will already round, but other paths calling GetFontBaked() directly also needs it (e.g. ImFontAtlasBuildPreloadAllGlyphRanges) size = ImGui.GetRoundedFontSize(size) if density < 0.0 then density = self.CurrentRasterizerDensity end if baked and baked.Size == size and baked.RasterizerDensity == density then return baked end local atlas = self.OwnerAtlas local builder = atlas.Builder baked = ImFontAtlasBakedGetOrAdd(atlas, self, size, density) if baked == nil then return nil end baked.LastUsedFrame = builder.FrameCount self.LastBaked = baked return baked end --- @param atlas ImFontAtlas --- @param font ImFont --- @param c ImWchar --- @return ImWchar --- @nodiscard local function ImFontAtlas_FontHookRemapCodepoint(atlas, font, c) -- IM_UNUSED(atlas) if #font.RemapPairs > 0 then local ret = font.RemapPairs[c] return (ret ~= nil) and ret or c end return c end --- @param baked ImFontBaked --- @param codepoint ImWchar --- @param only_load_advance_x? float --- @return ImFontGlyph? --- @return float? # Updated `only_load_advance_x` function ImFontBaked_BuildLoadGlyph(baked, codepoint, only_load_advance_x) local font = baked.OwnerFont local atlas = font.OwnerAtlas if atlas.Locked or bit32.band(font.Flags, ImFontFlags.NoLoadGlyphs) ~= 0 then -- Lazily load fallback glyph if baked.FallbackGlyphIndex == -1 and baked.LoadNoFallback == false then ImFontAtlasBuildSetupFontBakedFallback(baked) end return nil, only_load_advance_x end -- User remapping hooks local src_codepoint = codepoint codepoint = ImFontAtlas_FontHookRemapCodepoint(atlas, font, codepoint) --IMGUI_DEBUG_LOG("[font] BuildLoadGlyph U+%04X (%s)", codepoint, ImTextCharToUtf8(codepoint)) -- Special hook -- FIXME-NEWATLAS: it would be nicer if this used a more standardized way of hooking if (codepoint == font.EllipsisChar and font.EllipsisAutoBake) then local glyph = ImFontAtlasBuildSetupFontBakedEllipsis(atlas, baked) if (glyph) then return glyph, only_load_advance_x end end -- Call backend local loader_user_data_p = baked.FontLoaderDatas local src_n = 1 for _, src in font.Sources:iter() do local loader = src.FontLoader and src.FontLoader or atlas.FontLoader if (not src.GlyphExcludeRanges or ImFontAtlasBuildAcceptCodepointForSource(src, codepoint)) then if only_load_advance_x == nil then local glyph_buf = ImFontGlyph() if loader.FontBakedLoadGlyph(atlas, src, baked, loader_user_data_p, codepoint, glyph_buf, nil) then -- FIXME: Add hooks for e.g. #7962 glyph_buf.Codepoint = src_codepoint glyph_buf.SourceIdx = src_n return ImFontAtlasBakedAddFontGlyph(atlas, baked, src, glyph_buf) end else -- Special mode but only loading glyphs metrics. Will rasterize and pack later. local ret --[[@as bool]] ret, only_load_advance_x = loader.FontBakedLoadGlyph(atlas, src, baked, loader_user_data_p, codepoint, nil, only_load_advance_x) if ret then --- @cast only_load_advance_x float ImFontAtlasBakedAddFontGlyphAdvancedX(atlas, baked, src, codepoint, only_load_advance_x) return nil, only_load_advance_x end end end -- loader_user_data_p = loader_user_data_p + loader.FontBakedSrcLoaderDataSize src_n = src_n + 1 end -- Lazily load fallback glyph if (baked.LoadNoFallback) then return nil, only_load_advance_x end if (baked.FallbackGlyphIndex == -1) then ImFontAtlasBuildSetupFontBakedFallback(baked) end -- Mark index as not found, so we don't attempt the search twice ImFontBaked_BuildGrowIndex(baked, codepoint + 1); baked.IndexAdvanceX.Data[codepoint + 1] = baked.FallbackAdvanceX baked.IndexLookup.Data[codepoint + 1] = IM_FONTGLYPH_INDEX_NOT_FOUND return nil, only_load_advance_x end --- @param baked ImFontBaked --- @param codepoint ImWchar --- @return float function ImFontBaked_BuildLoadGlyphAdvanceX(baked, codepoint) if (baked.Size >= IMGUI_FONT_SIZE_THRESHOLD_FOR_LOADADVANCEXONLYMODE) or baked.LoadNoRenderOnLayout then -- First load AdvanceX value used by CalcTextSize() API then load the rest when loaded by drawing API. local only_advance_x local glyph glyph, only_advance_x = ImFontBaked_BuildLoadGlyph(baked, codepoint, only_advance_x) --- @cast only_advance_x float return glyph and glyph.AdvanceX or only_advance_x else local glyph = ImFontBaked_BuildLoadGlyph(baked, codepoint, nil) return glyph and glyph.AdvanceX or baked.FallbackAdvanceX end end -- This is not fast query --- @param c ImWchar --- @return bool function MT.ImFont:IsGlyphInFont(c) local atlas = self.OwnerAtlas c = ImFontAtlas_FontHookRemapCodepoint(atlas, self, c) for _, src in self.Sources:iter() do local loader = src.FontLoader and src.FontLoader or atlas.FontLoader if loader.FontSrcContainsGlyph ~= nil and loader.FontSrcContainsGlyph(atlas, src, c) then return true end end return false end -- This is manually inlined in CalcTextSizeA() and CalcWordWrapPosition(), with a non-inline call to BuildLoadGlyphGetAdvanceOrFallback(). --- @param c ImWchar --- @return float function MT.ImFontBaked:GetCharAdvance(c) if c < self.IndexAdvanceX.Size then -- Missing glyphs fitting inside index will have stored FallbackAdvanceX already. local x = self.IndexAdvanceX.Data[c + 1] if x >= 0.0 then return x end end return ImFontBaked_BuildLoadGlyphAdvanceX(self, c) end -- The point of this indirection is to not be inlined in debug mode in order to not bloat inner loop.b function BuildLoadGlyphGetAdvanceOrFallback(baked, codepoint) return ImFontBaked_BuildLoadGlyphAdvanceX(baked, codepoint) end -- Trim trailing space and find beginning of next line --- @param text ImString --- @param text_begin int --- @param text_end int --- @param flags ImDrawTextFlags --- @return int local function ImTextCalcWordWrapNextLineStart(text, text_begin, text_end, flags) local pos = text_begin if bit32.band(flags, ImDrawTextFlags.WrapKeepBlanks) == 0 then while pos < text_end and ImCharIsBlankA(ImStrByte(text, pos)) do pos = pos + 1 end end if pos < text_end and ImStrByte(text, pos) == 10 then -- '\n' pos = pos + 1 end return pos end --- @param bits ImU32[] --- @param codepoint_min unsigned_int --- @param codepoint_end unsigned_int --- @param char_class ImWcharClass function ImTextClassifierClear(bits, codepoint_min, codepoint_end, char_class) for c = codepoint_min, codepoint_end - 1 do ImTextClassifierSetCharClass(bits, codepoint_min, codepoint_end, char_class, c) end end --- @param bits ImU32[] --- @param codepoint_min unsigned_int --- @param codepoint_end unsigned_int --- @param char_class ImWcharClass --- @param c unsigned_int function ImTextClassifierSetCharClass(bits, codepoint_min, codepoint_end, char_class, c) IM_ASSERT(c >= codepoint_min and c < codepoint_end) -- IM_UNUSED(codepoint_end) c = c - codepoint_min local shift = bit32.lshift(bit32.band(c, 15), 1) bits[bit32.rshift(c, 4) + 1] = bit32.bor(bit32.band(bits[bit32.rshift(c, 4) + 1], bit32.bnot(bit32.lshift(0x03, shift))), bit32.lshift(char_class, shift)) end function ImTextClassifierSetCharClassFromStr(bits, codepoint_min, codepoint_end, char_class, s) local s_end = #s + 1 local pos = 1 while (pos < s_end) do local wanted, c = ImStd.ImTextCharFromUtf8(s, pos, s_end) pos = pos + wanted ImTextClassifierSetCharClass(bits, codepoint_min, codepoint_end, char_class, c) end end function ImTextClassifierGet(_BITS, _CHAR_OFFSET) return bit32.band(bit32.rshift(_BITS[bit32.rshift(_CHAR_OFFSET, 4) + 1], bit32.lshift(bit32.band(_CHAR_OFFSET, 15), 1)), 0x03) end -- 2-bit per character local g_CharClassifierIsSeparator_0000_007f = {} for i = 1, 128 / 16 do g_CharClassifierIsSeparator_0000_007f[i] = 0 end local g_CharClassifierIsSeparator_3000_300f = {} for i = 1, 16 / 16 do g_CharClassifierIsSeparator_3000_300f[i] = 0 end function ImTextInitClassifiers() if (ImTextClassifierGet(g_CharClassifierIsSeparator_0000_007f, 44) ~= 0) then -- ',' return end -- List of hardcoded separators: .,;!?'" -- Making this dynamic given known ranges is trivial BUT requires us to standardize where you pass them as parameters. (#3002, #8503) ImTextClassifierClear(g_CharClassifierIsSeparator_0000_007f, 0, 128, ImWcharClass.Other) ImTextClassifierSetCharClassFromStr(g_CharClassifierIsSeparator_0000_007f, 0, 128, ImWcharClass.Blank, " \t") ImTextClassifierSetCharClassFromStr(g_CharClassifierIsSeparator_0000_007f, 0, 128, ImWcharClass.Punct, ".,;!?\"") ImTextClassifierClear(g_CharClassifierIsSeparator_3000_300f, 0x3000, 0x300F, ImWcharClass.Other) ImTextClassifierSetCharClass(g_CharClassifierIsSeparator_3000_300f, 0x3000, 0x300F, ImWcharClass.Blank, 0x3000) ImTextClassifierSetCharClass(g_CharClassifierIsSeparator_3000_300f, 0x3000, 0x300F, ImWcharClass.Punct, 0x3001) ImTextClassifierSetCharClass(g_CharClassifierIsSeparator_3000_300f, 0x3000, 0x300F, ImWcharClass.Punct, 0x3002) end -- Simple word-wrapping for English, not full-featured. Please submit failing cases! -- This will return the next location to wrap from. If no wrapping if necessary, this will fast-forward to e.g. text_end. -- Refer to imgui_test_suite's "drawlist_text_wordwrap_1" for tests. --- @param font ImFont --- @param size float --- @param text ImString --- @param pos int --- @param text_end int --- @param wrap_width float --- @param flags ImDrawTextFlags --- @return int @native function ImFontCalcWordWrapPositionEx(font, size, text, pos, text_end, wrap_width, flags) -- For references, possible wrap point marked with ^ -- "aaa bbb, ccc,ddd. eee fff. ggg!" -- ^ ^ ^ ^ ^__ ^ ^ -- Skip extra blanks after a line returns (that includes not counting them in width computation) -- e.g. "Hello world" --> "Hello" "World" -- Cut words that cannot possibly fit within one line. -- e.g.: "The tropical fish" with ~5 characters worth of width --> "The tr" "opical" "fish" local baked = font:GetFontBaked(size) local scale = size / baked.Size local line_width = 0.0 local blank_width = 0.0 wrap_width = wrap_width / scale -- We work with unscaled widths to avoid scaling every characters local s = pos IM_ASSERT(text_end ~= nil) local prev_type = ImWcharClass.Other local keep_blanks = bit32.band(flags, ImDrawTextFlags.WrapKeepBlanks) ~= 0 -- Find next wrapping point --local span_begin = s local span_end = s local span_width = 0.0 local stop = false local text_is_str = type(text) == "string" -- [Luau] ImStrByte() inlined (once per character) while s < text_end do repeat --- @type unsigned_int local c if text_is_str then c = string.byte(text, s) else c = text[s] end local next_s if c < 0x80 then next_s = s + 1 else local wanted, out_char = ImStd.ImTextCharFromUtf8(text, s, text_end) c = out_char next_s = s + wanted end if c < 32 then if c == 10 then -- '\n' return s -- Direct return, skip "Wrap_width is too small to fit anything" path. end if c == 13 then -- '\r' s = next_s -- Fast-skip break end end -- Optimized inline version of 'float char_width = GetCharAdvance((ImWchar)c);' local char_width = (c < baked.IndexAdvanceX.Size) and baked.IndexAdvanceX.Data[c + 1] or -1.0 if char_width < 0.0 then char_width = BuildLoadGlyphGetAdvanceOrFallback(baked, c) end -- Classify current character local curr_type if c < 128 then -- [Luau] ImTextClassifierGet() inlined curr_type = bit32.band(bit32.rshift(g_CharClassifierIsSeparator_0000_007f[bit32.rshift(c, 4) + 1], bit32.lshift(bit32.band(c, 15), 1)), 0x03) elseif (c >= 0x3000 and c < 0x3010) then curr_type = ImTextClassifierGet(g_CharClassifierIsSeparator_3000_300f, bit32.band(c, 15)) else curr_type = ImWcharClass.Other end if curr_type == ImWcharClass.Blank then -- End span: 'A ' or '. ' if (prev_type ~= ImWcharClass.Blank and not keep_blanks) then span_end = s line_width = line_width + span_width span_width = 0.0 end blank_width = blank_width + char_width else -- End span: '.X' unless X is a digit if (prev_type == ImWcharClass.Punct and curr_type ~= ImWcharClass.Punct and not (c >= 48 and c <= 57)) then -- FIXME: Digit checks might be removed if allow custom separators (#8503) span_end = s line_width = line_width + (span_width + blank_width) blank_width = 0.0 span_width = 0.0 -- End span: 'A ' or '. ' elseif (prev_type == ImWcharClass.Blank and keep_blanks) then span_end = s line_width = line_width + (span_width + blank_width) blank_width = 0.0 span_width = 0.0 end span_width = span_width + char_width end if (span_width + blank_width + line_width > wrap_width) then if span_width + blank_width > wrap_width then stop = true; break end -- FIXME: Narrow wrapping e.g. "A quick brown" -> "Quic|k br|own", would require knowing if span is going to be longer than wrap_width. --if (span_width > wrap_width and not is_blank and not was_blank) then -- return s --end return span_end end prev_type = curr_type s = next_s until true if stop then break end end -- Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. -- +1 may not be a character start point in UTF-8 but it's ok because caller loops use (text >= word_wrap_eol). if s == pos and s < text_end then local bytes = ImStd.ImTextCountUtf8BytesFromChar(text, s, text_end) return s + bytes end return s end --- @param font ImFont --- @param size float --- @param max_width float --- @param wrap_width float --- @param text ImString --- @param text_begin int --- @param text_end_display? int --- @param text_end? int --- @param out_offset? ImVec2 --- @param flags ImDrawTextFlags --- @return ImVec2 text_size --- @return int remaining local TEXT_SIZE_CACHE_MAX = 4096 @native function ImFontCalcTextSizeEx(font, size, max_width, wrap_width, text, text_begin, text_end_display, text_end, out_offset, flags) local text_is_str = type(text) == "string" -- [Luau] cache for the common case (whole string, single pass, no wrap/limit): per baked font, text -> unscaled width, line count. -- Advances of a baked font never change once loaded, so entries stay valid. ponytail: whole-cache reset at TEXT_SIZE_CACHE_MAX entries, LRU if dynamic text churns it local cache_key = nil local wrap_cache = nil if text_begin == 1 and out_offset == nil and wrap_width <= 0.0 and max_width >= FLT_MAX and type(text) == "string" and (text_end == nil or text_end == #text + 1) and (text_end_display == nil or text_end_display == #text + 1) and (flags == nil or bit32.band(flags, ImDrawTextFlags.StopOnNewLine) == 0) then local baked = font:GetFontBaked(size) local cache = baked._TextSizeCache if cache == nil or baked._TextSizeCacheN >= TEXT_SIZE_CACHE_MAX then cache = {}; baked._TextSizeCache = cache; baked._TextSizeCacheN = 0 end local e = cache[text] if e then return ImVec2(e[1] * (size / baked.Size), e[2] * size), #text + 1 end cache_key = text elseif text_begin == 1 and out_offset == nil and wrap_width > 0.0 and max_width >= FLT_MAX and text_is_str and (text_end == nil or text_end == #text + 1) and (text_end_display == nil or text_end_display == #text + 1) and (flags == nil or bit32.band(flags, ImDrawTextFlags.StopOnNewLine) == 0) then -- [Luau] same for wrapped text (TextWrapped() measures every line every frame): exact results per (size, wrap width, text) local baked = font:GetFontBaked(size) local cache = baked._TextSizeCache if cache == nil or baked._TextSizeCacheN >= TEXT_SIZE_CACHE_MAX then cache = {}; baked._TextSizeCache = cache; baked._TextSizeCacheN = 0 end local by_size = cache[size] if by_size == nil then by_size = {}; cache[size] = by_size end local by_wrap = by_size[wrap_width] if by_wrap == nil then by_wrap = {}; by_size[wrap_width] = by_wrap end local e = by_wrap[text] if e then return ImVec2(e[1], e[2]), e[3] end wrap_cache = by_wrap end if not text_end then text_end = #text + 1 -- FIXME-OPT: Need to avoid this. end if not text_end_display then text_end_display = text_end end local baked = font:GetFontBaked(size) local line_height = size local scale = line_height / baked.Size local text_size = ImVec2() local line_width = 0.0 local word_wrap_enabled = (wrap_width > 0.0) local word_wrap_eol local s = text_begin local stop = false while s < text_end_display do repeat -- Word-wrapping if word_wrap_enabled then -- Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. if not word_wrap_eol then word_wrap_eol = ImFontCalcWordWrapPositionEx(font, size, text, s, text_end, wrap_width - line_width, flags) end if s >= word_wrap_eol then if text_size.x < line_width then text_size.x = line_width end text_size.y = text_size.y + line_height line_width = 0.0 s = ImTextCalcWordWrapNextLineStart(text, s, text_end, flags) -- Wrapping skips upcoming blanks if bit32.band(flags, ImDrawTextFlags.StopOnNewLine) ~= 0 then stop = true; break end word_wrap_eol = nil break end end -- Decode and advance source local prev_s = s local c if text_is_str then c = string.byte(text, s) else c = text[s] end -- [Luau] ImStrByte() inlined if c < 0x80 then s = s + 1 else local wanted, out_char = ImStd.ImTextCharFromUtf8(text, s, text_end) c = out_char s = s + wanted end if c == 10 then -- '\n' text_size.x = ImMax(text_size.x, line_width) text_size.y = text_size.y + line_height line_width = 0.0 if bit32.band(flags, ImDrawTextFlags.StopOnNewLine) ~= 0 then stop = true; break end break end if c == 13 then -- '\r' break end -- Optimized inline version of 'float char_width = GetCharAdvance((ImWchar)c);' local char_width = (c < baked.IndexAdvanceX.Size) and baked.IndexAdvanceX.Data[c + 1] or -1.0 if (char_width < 0.0) then char_width = BuildLoadGlyphGetAdvanceOrFallback(baked, c) end char_width = char_width * scale if (line_width + char_width >= max_width) then s = prev_s stop = true; break end line_width = line_width + char_width until true if stop then break end end if (text_size.x < line_width) then text_size.x = line_width end if out_offset then -- offset allow for the possibility of sitting after a trailing \n out_offset.x = line_width out_offset.y = text_size.y + line_height end if (line_width > 0 or text_size.y == 0.0) then -- whereas size.y will ignore the trailing \n text_size.y = text_size.y + line_height end local out_remaining = s if cache_key then baked._TextSizeCache[cache_key] = { text_size.x / scale, text_size.y / line_height } baked._TextSizeCacheN = baked._TextSizeCacheN + 1 elseif wrap_cache then wrap_cache[text] = { text_size.x, text_size.y, out_remaining } baked._TextSizeCacheN = baked._TextSizeCacheN + 1 end return text_size, out_remaining end --- @param size float --- @param max_width float --- @param wrap_width float --- @param text ImString --- @param text_begin int --- @param text_end? int function MT.ImFont:CalcTextSizeA(size, max_width, wrap_width, text, text_begin, text_end) return ImFontCalcTextSizeEx(self, size, max_width, wrap_width, text, text_begin, text_end, text_end, nil, ImDrawTextFlags.None) end --- Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. --- @param draw_list ImDrawList --- @param size float --- @param pos ImVec2 --- @param col ImU32 --- @param c ImWchar --- @param cpu_fine_clip ImVec4 function MT.ImFont:RenderChar(draw_list, size, pos, col, c, cpu_fine_clip) local baked = self:GetFontBaked(size) local glyph = baked:FindGlyph(c) if not glyph or not glyph.Visible then return end if glyph.Colored then col = bit32.bor(col, bit32.bnot(IM_COL32_A_MASK)) end local scale if size >= 0.0 then scale = size / baked.Size else scale = 1.0 end local x = pos.x local y = pos.y if bit32.band(draw_list.Flags, ImDrawListFlags.TextNoPixelSnap) == 0 then x = IM_TRUNC(x) y = IM_TRUNC(y) end local x1 = x + glyph.X0 * scale local x2 = x + glyph.X1 * scale if cpu_fine_clip and (x1 > cpu_fine_clip.z or x2 < cpu_fine_clip.x) then return end local y1 = y + glyph.Y0 * scale local y2 = y + glyph.Y1 * scale local u1 = glyph.U0 local v1 = glyph.V0 local u2 = glyph.U1 local v2 = glyph.V1 -- Always CPU fine clip. Code extracted from RenderText(). -- CPU side clipping used to fit text in their frame when the frame is too small. Only does clipping for axis aligned quads. if cpu_fine_clip then if x1 < cpu_fine_clip.x then u1 = u1 + (1.0 - (x2 - cpu_fine_clip.x) / (x2 - x1)) * (u2 - u1); x1 = cpu_fine_clip.x end if y1 < cpu_fine_clip.y then v1 = v1 + (1.0 - (y2 - cpu_fine_clip.y) / (y2 - y1)) * (v2 - v1); y1 = cpu_fine_clip.y end if x2 > cpu_fine_clip.z then u2 = u1 + ((cpu_fine_clip.z - x1) / (x2 - x1)) * (u2 - u1); x2 = cpu_fine_clip.z end if y2 > cpu_fine_clip.w then v2 = v1 + ((cpu_fine_clip.w - y1) / (y2 - y1)) * (v2 - v1); y2 = cpu_fine_clip.w end if y1 >= y2 then return end end draw_list:PrimReserve(6, 4) draw_list:PrimRectUV(ImVec2(x1, y1), ImVec2(x2, y2), ImVec2(u1, v1), ImVec2(u2, v2), col) end function MT.ImFontAtlas:AddFont(font_cfg_in) -- Sanity Checks IM_ASSERT(not self.Locked, "Cannot modify a locked ImFontAtlas!") IM_ASSERT((font_cfg_in.FontData ~= nil and font_cfg_in.FontDataSize > 0) or (font_cfg_in.FontLoader ~= nil)) IM_ASSERT(font_cfg_in.SizePixels > 0.0, "Is ImFontConfig struct correctly initialized?") IM_ASSERT(font_cfg_in.RasterizerDensity > 0.0, "Is ImFontConfig struct correctly initialized?") if font_cfg_in.GlyphOffset.x ~= 0.0 or font_cfg_in.GlyphOffset.y ~= 0.0 or font_cfg_in.GlyphMinAdvanceX ~= 0.0 or font_cfg_in.GlyphMaxAdvanceX ~= FLT_MAX then IM_ASSERT(font_cfg_in.SizePixels ~= 0.0, "Specifying glyph offset/advances requires a reference size to base it on.") end -- Lazily create builder on the first call to AddFont if self.Builder == nil then ImFontAtlasBuildInit(self) end -- Create new font local is_first_font = (self.Fonts.Size == 0) local font if not font_cfg_in.MergeMode then font = ImFont() font.FontId = self.FontNextUniqueID self.FontNextUniqueID = self.FontNextUniqueID + 1 font.Flags = font_cfg_in.Flags font.LegacySize = font_cfg_in.SizePixels font.CurrentRasterizerDensity = font_cfg_in.RasterizerDensity self.Fonts:push_back(font) else IM_ASSERT(self.Fonts.Size > 0, "Cannot use MergeMode for the first font!") -- When using MergeMode make sure that a font has already been added before. font = (font_cfg_in.DstFont ~= nil) and font_cfg_in.DstFont or self.Fonts:back() ImFontAtlasFontDiscardBakes(self, font, 0) -- Need to discard bakes if the font was already used, because baked->FontLoaderDatas[] will change size. (#9162) end -- Add to list self.Sources:push_back(font_cfg_in) local font_cfg = self.Sources:back() if (font_cfg.DstFont == nil) then font_cfg.DstFont = font end font.Sources:push_back(font_cfg) ImFontAtlasBuildUpdatePointers(self) -- Sanity check -- We don't round cfg.SizePixels yet as relative size of merged fonts are used afterwards. if font_cfg.GlyphExcludeRanges ~= nil then local size = 0 for _, v in ipairs(font_cfg.GlyphExcludeRanges) do if v == 0 then break end size = size + 1 end IM_ASSERT(bit32.band(size, 1) == 0, "GlyphExcludeRanges[] size must be multiple of two!") IM_ASSERT(size <= 64, "GlyphExcludeRanges[] size must be small!") end if font_cfg.FontLoader ~= nil then IM_ASSERT(font_cfg.FontLoader.FontBakedLoadGlyph ~= nil) IM_ASSERT(font_cfg.FontLoader.LoaderInit == nil and font_cfg.FontLoader.LoaderShutdown == nil) -- FIXME-NEWATLAS: Unsupported yet. end -- | Target w/ Implicit RefSize | Target w/ Explicit RefSize | -- Adding w/ Implicit RefSize: | OK (same scale) | OK (same scale) | -- Adding w/ Explicit RefSize: | KO | OK (custom scale) | if font_cfg_in.MergeMode and font_cfg_in.SizePixels > 0 then IM_ASSERT(bit32.band(font.Flags, ImFontFlags.ImplicitRefSize) == 0, "Cannot use MergeMode with an explicit reference size when the destination font used an implicit reference size!") end IM_ASSERT(font_cfg.FontLoaderData == nil) if not ImFontAtlasFontSourceInit(self, font_cfg) then -- Rollback (this is a fragile/rarely exercised code-path. TestSuite's "misc_atlas_add_invalid_font" aim to test this) ImFontAtlasFontDestroySourceData(self, font_cfg) self.Sources:pop_back() font.Sources:pop_back() if not font_cfg.MergeMode then font = nil self.Fonts:pop_back() end return nil end ImFontAtlasFontSourceAddToFont(self, font, font_cfg) if (is_first_font) then ImFontAtlasBuildNotifySetFont(self, nil, font) end return font end -- This duplicates some of the logic in UpdateFontsNewFrame() which is a bit chicken-and-eggy/tricky to extract due to variety of codepaths and possible initialization ordering. --- @param ctx ImGuiContext --- @return float local function GetExpectedContextFontSize(ctx) return ((ctx.Style.FontSizeBase > 0.0) and ctx.Style.FontSizeBase or 13.0) * ctx.Style.FontScaleMain * ctx.Style.FontScaleDpi end -- Legacy function with heuristic to select Pixel or Vector font. -- The selection is based on (style.FontSizeBase * style.FontScaleMain * style.FontScaleDpi) reaching a small threshold at the time of adding the default font. -- Prefer calling AddFontDefaultVector() or AddFontDefaultBitmap() based on your own logic. --- @param font_cfg ImFontConfig function MT.ImFontAtlas:AddFontDefault(font_cfg) if self.OwnerContext == nil or GetExpectedContextFontSize(self.OwnerContext) >= 15.0 then return self:AddFontDefaultVector(font_cfg) else return self:AddFontDefaultBitmap(font_cfg) end end function MT.ImFontAtlas:AddFontFromMemoryTTF(font_data, font_data_size, size_pixels, font_cfg_template, glyph_ranges) IM_ASSERT(not self.Locked, "Cannot modify a locked ImFontAtlas!") local font_cfg = font_cfg_template and font_cfg_template or ImFontConfig() IM_ASSERT(font_cfg.FontData == nil) IM_ASSERT(font_data_size > 100, "Incorrect value for font_data_size!") -- Heuristic to prevent accidentally passing a wrong value to font_data_size. font_cfg.FontData = font_data font_cfg.FontDataSize = font_data_size font_cfg.SizePixels = (size_pixels > 0.0) and size_pixels or font_cfg.SizePixels if glyph_ranges then font_cfg.GlyphRanges = glyph_ranges end return self:AddFont(font_cfg) end -- TODO: GetDefaultCompressedFontDataProggyClean, GetDefaultCompressedFontDataProggyForever function MT.ImFontAtlas:AddFontFromMemoryCompressedTTF() -- TODO: end function MT.ImFontAtlas:AddFontFromFileTTF(filename, size_pixels, font_cfg_template, glyph_ranges) IM_ASSERT(not self.Locked, "Cannot modify a locked ImFontAtlas!") local data, data_size = ImStd.ImFileLoadToMemory(filename, "rb") if not data then if (font_cfg_template == nil or (bit32.band(font_cfg_template.Flags, ImFontFlags.NoLoadError) == 0)) then -- IMGUI_DEBUG_LOG("While loading '%s'\n", filename) IM_ASSERT_USER_ERROR(0, "Could not load font file!") end return nil end local font_cfg = font_cfg_template and font_cfg_template or ImFontConfig() if not font_cfg.Name or font_cfg.Name == "" then -- Store a short copy of filename into the font name for convenience for i = #filename, 1, -1 do local c = string.byte(filename, i, i) if (c == 47 or c == 92) then -- '/' or '\\' font_cfg.Name = string.sub(filename, i + 1) break end end end return self:AddFontFromMemoryTTF(data, data_size, size_pixels, font_cfg, glyph_ranges) end function GetDefaultFontDataProggyClean() return ImStd.ImFileLoadToMemory("fonts/ProggyClean.ttf", "rb") end function GetDefaultFontDataProggyForever() return ImStd.ImFileLoadToMemory("fonts/ProggyForever.ttf", "rb") end -- TODO: IMGUI_DISABLE_DEFAULT_FONT, IMGUI_DISABLE_DEFAULT_FONT_BITMAP -- Load ProggyClean.ttf. Default size 13, disable oversampling. -- If you want a similar font which may be better scaled, consider using AddFontDefaultVector(). --- @param font_cfg_template ImFontConfig function MT.ImFontAtlas:AddFontDefaultBitmap(font_cfg_template) local font_cfg = font_cfg_template and font_cfg_template or ImFontConfig() if not font_cfg_template then font_cfg.PixelSnapH = true -- Prevents sub-integer scaling factors at lower-level layers. end if font_cfg.SizePixels <= 0.0 then font_cfg.SizePixels = 13.0 -- This only serves (1) as a reference for GlyphOffset.y setting and (2) as a default for pre-1.92 backend. font_cfg.Flags = bit32.bor(font_cfg.Flags, ImFontFlags.ImplicitRefSize) end if not font_cfg.Name or font_cfg.Name == "" then font_cfg.Name = "ProggyClean.ttf" end font_cfg.EllipsisChar = 0x0085 font_cfg.GlyphOffset.y = font_cfg.GlyphOffset.y + 1.0 * (font_cfg.SizePixels / 13.0) -- Add +1 offset per 13 units local ttf_data, ttf_data_size = GetDefaultFontDataProggyClean() return self:AddFontFromMemoryTTF(ttf_data, ttf_data_size, font_cfg.SizePixels, font_cfg) end -- Load a minimal version of ProggyForever, designed to match our good old ProggyClean, but nicely scalable. -- (See build script in https://github.com/ocornut/proggyforever for details) --- @param font_cfg_template ImFontConfig function MT.ImFontAtlas:AddFontDefaultVector(font_cfg_template) local font_cfg = font_cfg_template and font_cfg_template or ImFontConfig() if not font_cfg_template then font_cfg.PixelSnapH = true -- Precisely match ProggyClean, but prevents sub-integer scaling factors at lower-level layers. end if font_cfg.SizePixels <= 0.0 then font_cfg.SizePixels = 13.0 font_cfg.Flags = bit32.bor(font_cfg.Flags, ImFontFlags.ImplicitRefSize) end if not font_cfg.Name or font_cfg.Name == "" then font_cfg.Name = "ProggyForever.ttf" end font_cfg.ExtraSizeScale = font_cfg.ExtraSizeScale * 1.015 -- Match ProggyClean font_cfg.GlyphOffset.y = font_cfg.GlyphOffset.y + 0.5 * (font_cfg.SizePixels / 16.0) -- Closer match ProggyClean + avoid descenders going too high (with current code). local ttf_data, ttf_data_size = GetDefaultFontDataProggyForever() return self:AddFontFromMemoryTTF(ttf_data, ttf_data_size, font_cfg.SizePixels, font_cfg) end function MT.ImFontAtlas:GetCustomRect(id, out_r) local r = ImFontAtlasPackGetRectSafe(self, id) if r == nil then return false end IM_ASSERT(self.TexData.Width > 0 and self.TexData.Height > 0) -- Font atlas needs to be built before we can calculate UV coordinates if out_r == nil then return true end out_r.x = r.x out_r.y = r.y out_r.w = r.w out_r.h = r.h ImVec2_Copy(out_r.uv0, ImVec2((r.x), (r.y)) * self.TexUvScale) ImVec2_Copy(out_r.uv1, ImVec2((r.x + r.w), (r.y + r.h)) * self.TexUvScale) return true end --- @param atlas ImFontAtlas --- @param cursor_type ImGuiMouseCursor --- @param out_offset ImVec2 --- @param out_size ImVec2 --- @param out_uv_border ImVec2[] --- @param out_uv_fill ImVec2[] function ImFontAtlasGetMouseCursorTexData(atlas, cursor_type, out_offset, out_size, out_uv_border, out_uv_fill) if cursor_type <= ImGuiMouseCursor.None or cursor_type >= ImGuiMouseCursor.COUNT then return false end if bit32.band(atlas.Flags, ImFontAtlasFlags.NoMouseCursors) ~= 0 then return false end local r = ImFontAtlasPackGetRect(atlas, atlas.Builder.PackIdMouseCursors) local pos = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type + 1][1] + ImVec2(r.x, r.y) local size = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type + 1][2] ImVec2_Copy(out_size, size) ImVec2_Copy(out_offset, FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type + 1][3]) ImVec2_Copy(out_uv_border[1], (pos) * atlas.TexUvScale) ImVec2_Copy(out_uv_border[2], (pos + size) * atlas.TexUvScale) pos.x = pos.x + (FONT_ATLAS_DEFAULT_TEX_DATA_W + 1) ImVec2_Copy(out_uv_fill[1], (pos) * atlas.TexUvScale) ImVec2_Copy(out_uv_fill[2], (pos + size) * atlas.TexUvScale) return true end -- At it is common to do an AddCustomRect() followed by a GetCustomRect(), we provide an optional 'out_r' argument to retrieve the info straight away. function MT.ImFontAtlas:AddCustomRect(width, height, out_r) IM_ASSERT(width > 0 and width <= 0xFFFF) IM_ASSERT(height > 0 and height <= 0xFFFF) if (self.Builder == nil) then ImFontAtlasBuildInit(self) end local r_id = ImFontAtlasPackAddRect(self, width, height) if (r_id == ImFontAtlasRectId_Invalid) then return ImFontAtlasRectId_Invalid end if (out_r ~= nil) then self:GetCustomRect(r_id, out_r) end if (self.RendererHasTextures) then local r = ImFontAtlasPackGetRect(self, r_id) ImFontAtlasTextureBlockQueueUpload(self, self.TexData, r.x, r.y, r.w, r.h) end return r_id end local function IM_NORMALIZE2F_OVER_ZERO(VX, VY) local d2 = VX * VX + VY * VY if d2 > 0.0 then local inv_len = ImRsqrt(d2) VX = VX * inv_len VY = VY * inv_len end return VX, VY end local function IM_FIXNORMAL2F(VX, VY) local IM_FIXNORMAL2F_MAX_INVLEN2 = 100 -- 500.0f (see #4053, #3366) local d2 = VX * VX + VY * VY if d2 > 0.000001 then local inv_len2 = 1.0 / d2 if inv_len2 > IM_FIXNORMAL2F_MAX_INVLEN2 then inv_len2 = IM_FIXNORMAL2F_MAX_INVLEN2 end VX = VX * inv_len2 VY = VY * inv_len2 end return VX, VY end ------------------------------------------------------------------------------- -- [SECTION] ImDrawData ------------------------------------------------------------------------------- function MT.ImDrawData:Clear() self.Valid = false self.FrameCount = 0; self.TotalIdxCount = 0; self.TotalVtxCount = 0 self.CmdLists:resize(0) -- The ImDrawList are NOT owned by ImDrawData but e.g. by ImGuiContext, so we don't clear them. self.DisplayPos = ImVec2(0.0, 0.0) self.DisplaySize = ImVec2(0.0, 0.0) self.FramebufferScale = ImVec2(0.0, 0.0) self.OwnerViewport = nil self.Textures = nil end -- Important: 'out_list' is generally going to be draw_data.CmdLists, but may be another temporary list -- as long at it is expected that the result will be later merged into draw_data.CmdLists[]. function ImGui.AddDrawListToDrawDataEx(draw_data, out_list, draw_list) if draw_list.CmdBuffer.Size == 0 then return end if draw_list.CmdBuffer.Size == 1 and draw_list.CmdBuffer.Data[1].ElemCount == 0 and draw_list.CmdBuffer.Data[1].UserCallback == nil then return end -- Draw list sanity check. Detect mismatch between PrimReserve() calls and incrementing _VtxCurrentIdx, _VtxWritePtr etc. -- May trigger for you if you are using PrimXXX functions incorrectly. IM_ASSERT(draw_list.VtxBuffer.Size == 0 or draw_list._VtxWritePtr == draw_list.VtxBuffer.Size + 1) IM_ASSERT(draw_list.IdxBuffer.Size == 0 or draw_list._IdxWritePtr == draw_list.IdxBuffer.Size + 1) if (bit32.band(draw_list.Flags, ImDrawListFlags.AllowVtxOffset) == 0) then IM_ASSERT(draw_list._VtxCurrentIdx == draw_list.VtxBuffer.Size + 1) end -- Add to output list + records state in ImDrawData out_list:push_back(draw_list) draw_data.TotalVtxCount = draw_data.TotalVtxCount + draw_list.VtxBuffer.Size draw_data.TotalIdxCount = draw_data.TotalIdxCount + draw_list.IdxBuffer.Size end function MT.ImDrawData:AddDrawList(draw_list) draw_list:_PopUnusedDrawCmd() ImGui.AddDrawListToDrawDataEx(self, self.CmdLists, draw_list) end local IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN = 4 local IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX = 512 local function IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(_RAD, _MAXERROR) return ImClamp(IM_ROUNDUP_TO_EVEN(ImCeil(IM_PI / ImAcos(1 - ImMin(_MAXERROR, _RAD) / _RAD))), IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX) end local function IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(_N, _MAXERROR) return (_MAXERROR / (1 - ImCos(IM_PI / ImMax(_N, IM_PI)))) end local function IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_ERROR(_N, _RAD) return ((1 - ImCos(IM_PI / ImMax(_N, IM_PI))) / _RAD) end ------------------------------------------------------------------------------- -- [SECTION] ImDrawList ------------------------------------------------------------------------------- function MT.ImDrawListSharedData:SetCircleTessellationMaxError(max_error) if self.CircleTessellationMaxError == max_error then return end IM_ASSERT(max_error > 0) self.CircleTessellationMaxError = max_error for i = 1, 64 do -- IM_COUNTOF(CircleSegmentCounts) local radius = (i - 1) self.CircleSegmentCounts[i] = (i > 1) and IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, self.CircleTessellationMaxError) or IM_DRAWLIST_ARCFAST_SAMPLE_MAX end self.ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, self.CircleTessellationMaxError) end --- @param data ImDrawListSharedData function MT.ImDrawList:_SetDrawListSharedData(data) if self._Data ~= nil then self._Data.DrawLists:find_erase_unsorted(self) end self._Data = data if self._Data ~= nil then self._Data.DrawLists:push_back(self) end end -- Initialize before use in a new frame. We always have a command ready in the buffer. -- In the majority of cases, you would want to call PushClipRect() and PushTexture() after this. -- TODO: function MT.ImDrawList:_ResetForNewFrame() self._ContentClipX0 = nil -- set again by Begin() (Roblox backend scroll reuse) if self._Splitter._Count > 1 then self._Splitter:Merge(self) end self._Splitter._Current = 0 -- [Lua] recover from an error thrown mid-split self._Splitter._Count = 1 self.CmdBuffer:resize(0) self.IdxBuffer:resize(0) self.VtxBuffer:resize(0) self.Flags = self._Data.InitialFlags self._CmdHeader = ImDrawCmdHeader() -- memset(&_CmdHeader, 0, sizeof(_CmdHeader)) self._VtxCurrentIdx = 1 self._VtxWritePtr = 1 self._IdxWritePtr = 1 self._ClipRectStack:resize(0) self._TextureStack:resize(0) self._Path:resize(0) self.CmdBuffer:push_back(ImDrawCmd()) self._FringeScale = self._Data.InitialFringeScale end function MT.ImDrawList:_ClearFreeMemory() self.CmdBuffer:clear() self.IdxBuffer:clear() self.VtxBuffer:clear() self.Flags = ImDrawListFlags.None self._VtxCurrentIdx = 1 self._VtxWritePtr = 1 self._IdxWritePtr = 1 self._ClipRectStack:clear() self._TextureStack:clear() -- self._CallbacksDataBuf:clear() self._Path:clear() self._Splitter:ClearFreeMemory() end function MT.ImDrawList:AddDrawCmd() local draw_cmd = ImDrawCmd() ImVec4_Copy(draw_cmd.ClipRect, self._CmdHeader.ClipRect) -- Same as calling ImDrawCmd_HeaderCopy() draw_cmd.TexRef = self._CmdHeader.TexRef draw_cmd.VtxOffset = self._CmdHeader.VtxOffset draw_cmd.IdxOffset = self.IdxBuffer.Size IM_ASSERT(draw_cmd.ClipRect.x <= draw_cmd.ClipRect.z and draw_cmd.ClipRect.y <= draw_cmd.ClipRect.w) self.CmdBuffer:push_back(draw_cmd) end -- Pop trailing draw command (used before merging or presenting to user) -- Note that this leaves the ImDrawList in a state unfit for further commands, as most code assume that CmdBuffer.Size > 0 && CmdBuffer.back().UserCallback == nil function MT.ImDrawList:_PopUnusedDrawCmd() while self.CmdBuffer.Size > 0 do local curr_cmd = self.CmdBuffer.Data[self.CmdBuffer.Size] if curr_cmd.ElemCount ~= 0 or curr_cmd.UserCallback ~= nil then break -- return;// break; end self.CmdBuffer:pop_back() end end --- @param callback ImDrawCallback --- @param userdata any --- @param userdata_size? size_t function MT.ImDrawList:AddCallback(callback, userdata, userdata_size) if userdata_size == nil then userdata_size = 0 end IM_ASSERT(callback ~= nil) IM_ASSERT_PARANOID(self.CmdBuffer.Size > 0) local curr_cmd = self.CmdBuffer.Data[self.CmdBuffer.Size] IM_ASSERT(curr_cmd.UserCallback == nil) if curr_cmd.ElemCount ~= 0 then self:AddDrawCmd() curr_cmd = self.CmdBuffer.Data[self.CmdBuffer.Size] end curr_cmd.UserCallback = callback -- TODO: _CallbacksDataBuf? if userdata_size == 0 then -- Store user data directly in command (no indirection) curr_cmd.UserCallbackData = userdata else IM_ASSERT(userdata ~= nil) IM_ASSERT(userdata_size < bit32.lshift(1, 31)) end self:AddDrawCmd() -- Force a new command after us (see comment below) end --- Compare ClipRect, TexRef, VtxOffset --- @param CMD_LHS ImDrawCmd|ImDrawCmdHeader --- @param CMD_RHS ImDrawCmd|ImDrawCmdHeader --- @return bool local function ImDrawCmd_HeaderCompare(CMD_LHS, CMD_RHS) if CMD_LHS.ClipRect.x ~= CMD_RHS.ClipRect.x or CMD_LHS.ClipRect.y ~= CMD_RHS.ClipRect.y or CMD_LHS.ClipRect.z ~= CMD_RHS.ClipRect.z or CMD_LHS.ClipRect.w ~= CMD_RHS.ClipRect.w then return false end if CMD_LHS.TexRef._TexData ~= CMD_RHS.TexRef._TexData or CMD_LHS.TexRef._TexID ~= CMD_RHS.TexRef._TexID then return false end if CMD_LHS.VtxOffset ~= CMD_RHS.VtxOffset then return false end return true end --- Copy ClipRect, TexRef, VtxOffset --- @param CMD_DST ImDrawCmd|ImDrawCmdHeader --- @param CMD_SRC ImDrawCmd|ImDrawCmdHeader local function ImDrawCmd_HeaderCopy(CMD_DST, CMD_SRC) CMD_DST.ClipRect.x = CMD_SRC.ClipRect.x CMD_DST.ClipRect.y = CMD_SRC.ClipRect.y CMD_DST.ClipRect.z = CMD_SRC.ClipRect.z CMD_DST.ClipRect.w = CMD_SRC.ClipRect.w CMD_DST.TexRef._TexData = CMD_SRC.TexRef._TexData CMD_DST.TexRef._TexID = CMD_SRC.TexRef._TexID CMD_DST.VtxOffset = CMD_SRC.VtxOffset end --- @param CMD_0 ImDrawCmd --- @param CMD_1 ImDrawCmd local function ImDrawCmd_AreSequentialIdxOffset(CMD_0, CMD_1) return CMD_0.IdxOffset + CMD_0.ElemCount == CMD_1.IdxOffset end -- Try to merge two last draw commands function MT.ImDrawList:_TryMergeDrawCmds() IM_ASSERT_PARANOID(self.CmdBuffer.Size > 0) local curr_cmd = self.CmdBuffer.Data[self.CmdBuffer.Size] local prev_cmd = self.CmdBuffer.Data[self.CmdBuffer.Size - 1] if ImDrawCmd_HeaderCompare(curr_cmd, prev_cmd) and ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) and curr_cmd.UserCallback == nil and prev_cmd.UserCallback == nil then prev_cmd.ElemCount = prev_cmd.ElemCount + curr_cmd.ElemCount self.CmdBuffer:pop_back() end end -- Our scheme may appears a bit unusual, basically we want the most-common calls AddLine AddRect etc. to not have to perform any check so we always have a command ready in the stack. -- The cost of figuring out if a new command has to be added or if we can merge is paid in those Update** functions only. function MT.ImDrawList:_OnChangedClipRect() -- If current command is used with different settings we need to add a new command IM_ASSERT_PARANOID(self.CmdBuffer.Size > 0) local curr_cmd = self.CmdBuffer.Data[self.CmdBuffer.Size] if (curr_cmd.ElemCount ~= 0 and self._CmdHeader.ClipRect ~= curr_cmd.ClipRect) then self:AddDrawCmd() return end IM_ASSERT(curr_cmd.UserCallback == nil) -- Try to merge with previous command if it matches, else use current command local prev_cmd = self.CmdBuffer.Data[self.CmdBuffer.Size - 1] if (curr_cmd.ElemCount == 0 and self.CmdBuffer.Size > 1 and ImDrawCmd_HeaderCompare(self._CmdHeader, prev_cmd) and ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) and prev_cmd.UserCallback == nil) then self.CmdBuffer:pop_back() return end ImVec4_Copy(curr_cmd.ClipRect, self._CmdHeader.ClipRect) end function MT.ImDrawList:_OnChangedTexture() -- If current command is used with different settings we need to add a new command IM_ASSERT_PARANOID(self.CmdBuffer.Size > 0) local curr_cmd = self.CmdBuffer.Data[self.CmdBuffer.Size] if curr_cmd.ElemCount ~= 0 and curr_cmd.TexRef ~= self._CmdHeader.TexRef then self:AddDrawCmd() return end -- Unlike other _OnChangedXXX functions this may be called by ImFontAtlasUpdateDrawListsTextures() in more locations so we need to handle this case. if curr_cmd.UserCallback ~= nil then return end -- Try to merge with previous command if it matches, else use current command local prev_cmd = self.CmdBuffer.Data[self.CmdBuffer.Size - 1] if curr_cmd.ElemCount == 0 and self.CmdBuffer.Size > 1 and ImDrawCmd_HeaderCompare(self._CmdHeader, prev_cmd) and ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) and prev_cmd.UserCallback == nil then self.CmdBuffer:pop_back() return end curr_cmd.TexRef = self._CmdHeader.TexRef end function MT.ImDrawList:_OnChangedVtxOffset() -- We don't need to compare curr_cmd.VtxOffset ~= _CmdHeader.VtxOffset because we know it'll be different at the time we call this. self._VtxCurrentIdx = 1 IM_ASSERT_PARANOID(self.CmdBuffer.Size > 0) local curr_cmd = self.CmdBuffer.Data[self.CmdBuffer.Size] --IM_ASSERT(curr_cmd.VtxOffset ~= self._CmdHeader.VtxOffset) -- See #3349 if curr_cmd.ElemCount ~= 0 then self:AddDrawCmd() return end IM_ASSERT(curr_cmd.UserCallback == nil) curr_cmd.VtxOffset = self._CmdHeader.VtxOffset end ------------------------------------------------------------------------------- -- [SECTION] ImDrawListSplitter ------------------------------------------------------------------------------- -- FIXME: This may be a little confusing, trying to be a little too low-level/optimal instead of just doing vector swap.. ------------------------------------------------------------------------------- -- Lua: channels swap ImVector objects with the draw list (C++ memcpy of the vector headers). -- Channel 0 holds nothing while it is the current channel (its buffers live in the draw list). -- Copy ClipRect, TexRef, VtxOffset (TexRef is assigned by reference, never mutated) local function ImDrawCmd_HeaderCopyRef(dst, src) ImVec4_Copy(dst.ClipRect, src.ClipRect) dst.TexRef = src.TexRef dst.VtxOffset = src.VtxOffset end function MT.ImDrawListSplitter:Clear() self._Current = 0 self._Count = 1 end function MT.ImDrawListSplitter:ClearFreeMemory() for i = 1, self._Channels.Size do local ch = self._Channels.Data[i] if i - 1 == self._Current then -- Current channel is a copy of CmdBuffer/IdxBuffer, don't destruct again self._Channels.Data[i] = ImDrawChannel() else ch._CmdBuffer:clear() ch._IdxBuffer:clear() end end self._Current = 0 self._Count = 1 self._Channels:clear() end --- @param draw_list ImDrawList --- @param channels_count int function MT.ImDrawListSplitter:Split(draw_list, channels_count) IM_ASSERT(self._Current == 0 and self._Count <= 1, "Nested channel splitting is not supported. Please use separate instances of ImDrawListSplitter.") local old_channels_count = self._Channels.Size if old_channels_count < channels_count then self._Channels:reserve(channels_count) -- Avoid over reserving since this is likely to stay stable self._Channels:resize(channels_count) end self._Count = channels_count -- Channels[] hold storage that we'll swap with draw_list._CmdBuffer/_IdxBuffer -- The content of Channels[0] at this point doesn't matter. We clear it to make state tidy in a debugger but we don't strictly need to. -- When we switch to the next channel, we'll copy draw_list._CmdBuffer/_IdxBuffer into Channels[0] and then Channels[1] into draw_list.CmdBuffer/_IdxBuffer -- Channel 0 is the draw list itself (memset to 0 in C++) local channels = self._Channels.Data channels[1] = { _CmdBuffer = nil, _IdxBuffer = nil } for i = 2, channels_count do if i > old_channels_count or channels[i] == nil then channels[i] = ImDrawChannel() else channels[i]._CmdBuffer:resize(0) channels[i]._IdxBuffer:resize(0) end end end --- @param draw_list ImDrawList function MT.ImDrawListSplitter:Merge(draw_list) -- Note that we never use or rely on _Channels.Size because it is merely a buffer that we never shrink back to 0 to keep all sub-buffers ready for use. if self._Count <= 1 then return end self:SetCurrentChannel(draw_list, 0) draw_list:_PopUnusedDrawCmd() -- Calculate our final buffer sizes. Also fix the incorrect IdxOffset values in each command. local new_cmd_buffer_count = 0 local new_idx_buffer_count = 0 local last_cmd = (self._Count > 0 and draw_list.CmdBuffer.Size > 0) and draw_list.CmdBuffer.Data[draw_list.CmdBuffer.Size] or nil local idx_offset = last_cmd and (last_cmd.IdxOffset + last_cmd.ElemCount) or 0 local channels = self._Channels.Data for i = 2, self._Count do local ch = channels[i] local cmds = ch._CmdBuffer if cmds.Size > 0 and cmds.Data[cmds.Size].ElemCount == 0 and cmds.Data[cmds.Size].UserCallback == nil then -- Equivalent of PopUnusedDrawCmd() cmds:pop_back() end if cmds.Size > 0 and last_cmd ~= nil then -- Do not include ImDrawCmd_AreSequentialIdxOffset() in the compare as we rebuild IdxOffset values ourselves. -- Manipulating IdxOffset (e.g. by reordering draw commands like done by RenderDimmedBackgroundBehindWindow()) is not supported within a splitter. local next_cmd = cmds.Data[1] if ImDrawCmd_HeaderCompare(last_cmd, next_cmd) and last_cmd.UserCallback == nil and next_cmd.UserCallback == nil then -- Merge previous channel last draw command with current channel first draw command if matching. last_cmd.ElemCount = last_cmd.ElemCount + next_cmd.ElemCount idx_offset = idx_offset + next_cmd.ElemCount cmds:erase(1) -- FIXME-OPT: Improve for multiple merges. end end if cmds.Size > 0 then last_cmd = cmds.Data[cmds.Size] end new_cmd_buffer_count = new_cmd_buffer_count + cmds.Size new_idx_buffer_count = new_idx_buffer_count + ch._IdxBuffer.Size for cmd_n = 1, cmds.Size do cmds.Data[cmd_n].IdxOffset = idx_offset idx_offset = idx_offset + cmds.Data[cmd_n].ElemCount end end local cmd_write = draw_list.CmdBuffer.Size + 1 local idx_write = draw_list.IdxBuffer.Size + 1 draw_list.CmdBuffer:resize(draw_list.CmdBuffer.Size + new_cmd_buffer_count) draw_list.IdxBuffer:resize(draw_list.IdxBuffer.Size + new_idx_buffer_count) -- Write commands and indices in order (they are fairly small structures, we don't copy vertices only indices) local dst_cmds = draw_list.CmdBuffer.Data local dst_idx = draw_list.IdxBuffer.Data for i = 2, self._Count do local ch = channels[i] local src_cmds = ch._CmdBuffer.Data for n = 1, ch._CmdBuffer.Size do dst_cmds[cmd_write] = src_cmds[n] src_cmds[n] = nil -- ownership moved to the draw list cmd_write = cmd_write + 1 end local src_idx = ch._IdxBuffer.Data for n = 1, ch._IdxBuffer.Size do dst_idx[idx_write] = src_idx[n] idx_write = idx_write + 1 end end draw_list._IdxWritePtr = idx_write -- Ensure there's always a non-callback draw command trailing the command-buffer if draw_list.CmdBuffer.Size == 0 or draw_list.CmdBuffer.Data[draw_list.CmdBuffer.Size].UserCallback ~= nil then draw_list:AddDrawCmd() end -- If current command is used with different settings we need to add a new command local curr_cmd = draw_list.CmdBuffer.Data[draw_list.CmdBuffer.Size] if curr_cmd.ElemCount == 0 then ImDrawCmd_HeaderCopyRef(curr_cmd, draw_list._CmdHeader) -- Copy ClipRect, TexRef, VtxOffset elseif not ImDrawCmd_HeaderCompare(curr_cmd, draw_list._CmdHeader) then draw_list:AddDrawCmd() end self._Count = 1 end --- @param draw_list ImDrawList --- @param idx int # 0-based channel index function MT.ImDrawListSplitter:SetCurrentChannel(draw_list, idx) IM_ASSERT(idx >= 0 and idx < self._Count) if self._Current == idx then return end -- Overwrite ImVector (12/16 bytes), four times. This is merely a silly optimization instead of doing .swap() local channels = self._Channels.Data local cur = channels[self._Current + 1] cur._CmdBuffer = draw_list.CmdBuffer cur._IdxBuffer = draw_list.IdxBuffer self._Current = idx local ch = channels[idx + 1] draw_list.CmdBuffer = ch._CmdBuffer draw_list.IdxBuffer = ch._IdxBuffer draw_list._IdxWritePtr = draw_list.IdxBuffer.Size + 1 -- If current command is used with different settings we need to add a new command local curr_cmd = nil if draw_list.CmdBuffer.Size > 0 then curr_cmd = draw_list.CmdBuffer.Data[draw_list.CmdBuffer.Size] end if curr_cmd == nil then draw_list:AddDrawCmd() elseif curr_cmd.ElemCount == 0 then ImDrawCmd_HeaderCopyRef(curr_cmd, draw_list._CmdHeader) -- Copy ClipRect, TexRef, VtxOffset elseif not ImDrawCmd_HeaderCompare(curr_cmd, draw_list._CmdHeader) then draw_list:AddDrawCmd() end end --- @param count int function MT.ImDrawList:ChannelsSplit(count) self._Splitter:Split(self, count) end function MT.ImDrawList:ChannelsMerge() self._Splitter:Merge(self) end --- @param n int # 0-based function MT.ImDrawList:ChannelsSetCurrent(n) self._Splitter:SetCurrentChannel(self, n) end -- - We intentionally avoid using ImVec2 and its math operators here to reduce cost to a minimum for debug/non-inlined builds. -- - Filled shapes must always use clockwise winding order. The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. --- @param points ImVec2[] --- @param points_count int --- @param col ImU32 @native function MT.ImDrawList:AddConvexPolyFilled(points, points_count, col) if points_count < 3 or bit32.band(col, IM_COL32_A_MASK) == 0 then return end local uv = self._Data.TexUvWhitePixel if bit32.band(self.Flags, ImDrawListFlags.AntiAliasedFill) ~= 0 then local AA_SIZE = self._FringeScale local col_trans = bit32.band(col, bit32.bnot(IM_COL32_A_MASK)) local idx_count = (points_count - 2) * 3 + points_count * 6 local vtx_count = points_count * 2 self:PrimReserve(idx_count, vtx_count) local vtx_inner_idx = self._VtxCurrentIdx local vtx_outer_idx = self._VtxCurrentIdx + 1 local idx_data = self.IdxBuffer.Data for i = 2, points_count - 1 do local idx_write_ptr = self._IdxWritePtr idx_data[idx_write_ptr + 0] = vtx_inner_idx; idx_data[idx_write_ptr + 1] = vtx_inner_idx + ((i - 1) * 2); idx_data[idx_write_ptr + 2] = vtx_inner_idx + (i * 2) self._IdxWritePtr = idx_write_ptr + 3 end -- Compute normals self._Data.TempBuffer:reserve_discard(points_count) local temp_normals = self._Data.TempBuffer.Data local i0 = points_count for i1 = 1, points_count do local p0 = points[i0] local p1 = points[i1] local dx = p1.x - p0.x local dy = p1.y - p0.y dx, dy = IM_NORMALIZE2F_OVER_ZERO(dx, dy) temp_normals[i0].x = dy temp_normals[i0].y = -dx i0 = i1 end local vtx_data = self.VtxBuffer.Buf i0 = points_count for i1 = 1, points_count do local n0 = temp_normals[i0] local n1 = temp_normals[i1] local dm_x = (n0.x + n1.x) * 0.5 local dm_y = (n0.y + n1.y) * 0.5 dm_x, dm_y = IM_FIXNORMAL2F(dm_x, dm_y) dm_x = dm_x * AA_SIZE * 0.5 dm_y = dm_y * AA_SIZE * 0.5 local vtx_write_ptr = self._VtxWritePtr WV(vtx_data, vtx_write_ptr + 0, points[i1].x - dm_x, points[i1].y - dm_y, uv.x, uv.y, col) WV(vtx_data, vtx_write_ptr + 1, points[i1].x + dm_x, points[i1].y + dm_y, uv.x, uv.y, col_trans) self._VtxWritePtr = vtx_write_ptr + 2 local idx_write_ptr = self._IdxWritePtr idx_data[idx_write_ptr + 0] = vtx_inner_idx + ((i1 - 1) * 2); idx_data[idx_write_ptr + 1] = vtx_inner_idx + ((i0 - 1) * 2); idx_data[idx_write_ptr + 2] = vtx_outer_idx + ((i0 - 1) * 2) idx_data[idx_write_ptr + 3] = vtx_outer_idx + ((i0 - 1) * 2); idx_data[idx_write_ptr + 4] = vtx_outer_idx + ((i1 - 1) * 2); idx_data[idx_write_ptr + 5] = vtx_inner_idx + ((i1 - 1) * 2) self._IdxWritePtr = idx_write_ptr + 6 i0 = i1 end self._VtxCurrentIdx = self._VtxCurrentIdx + vtx_count else local idx_count = (points_count - 2) * 3 local vtx_count = points_count self:PrimReserve(idx_count, vtx_count) local idx_data = self.IdxBuffer.Data; local vtx_data = self.VtxBuffer.Buf for i = 1, vtx_count do local vtx_write_ptr = self._VtxWritePtr WVP(vtx_data, vtx_write_ptr + 0, points[i], uv, col) self._VtxWritePtr = vtx_write_ptr + 1 end for i = 3, points_count do local idx_write_ptr = self._IdxWritePtr idx_data[idx_write_ptr + 0] = self._VtxCurrentIdx; idx_data[idx_write_ptr + 1] = self._VtxCurrentIdx + i - 2; idx_data[idx_write_ptr + 2] = self._VtxCurrentIdx + i - 1 self._IdxWritePtr = idx_write_ptr + 3 end self._VtxCurrentIdx = self._VtxCurrentIdx + vtx_count end end function MT.ImDrawList:PushTexture(tex_ref) self._TextureStack:push_back(tex_ref) self._CmdHeader.TexRef = tex_ref if (tex_ref._TexData ~= nil) then IM_ASSERT(tex_ref._TexData.WantDestroyNextFrame == false) end self:_OnChangedTexture() end function MT.ImDrawList:PopTexture() self._TextureStack:pop_back() self._CmdHeader.TexRef = (self._TextureStack.Size == 0) and ImTextureRef() or self._TextureStack.Data[self._TextureStack.Size] self:_OnChangedTexture() end -- This is used by ImGui.PushFont()/PopFont(). It works because we never use _TextureIdStack[] elsewhere than in PushTexture()/PopTexture(). function MT.ImDrawList:_SetTexture(tex_ref) if (self._CmdHeader.TexRef == tex_ref) then return end self._CmdHeader.TexRef = tex_ref self._TextureStack.Data[self._TextureStack.Size] = tex_ref self:_OnChangedTexture() end -- Reserve space for a number of vertices and indices. -- You must finish filling your reserved data before calling PrimReserve() again, as it may reallocate or -- submit the intermediate results. PrimUnreserve() can be used to release unused allocations. @native function MT.ImDrawList:PrimReserve(idx_count, vtx_count) -- Large mesh support (when enabled) IM_ASSERT_PARANOID(idx_count >= 0 and vtx_count >= 0) if (self._VtxCurrentIdx + vtx_count >= bit32.lshift(1, 16)) and (bit32.band(self.Flags, ImDrawListFlags.AllowVtxOffset) ~= 0) then -- FIXME: In theory we should be testing that vtx_count <64k here. -- In practice, RenderText() relies on reserving ahead for a worst case scenario so it is currently useful for us -- to not make that check until we rework the text functions to handle clipping and large horizontal lines better. self._CmdHeader.VtxOffset = self.VtxBuffer.Size + 1 self:_OnChangedVtxOffset() end local draw_cmd = self.CmdBuffer.Data[self.CmdBuffer.Size] draw_cmd.ElemCount = draw_cmd.ElemCount + idx_count local vtx_buffer_old_size = self.VtxBuffer.Size self.VtxBuffer:resize(vtx_buffer_old_size + vtx_count) self._VtxWritePtr = vtx_buffer_old_size + 1 local idx_buffer_old_size = self.IdxBuffer.Size self.IdxBuffer:resize(idx_buffer_old_size + idx_count) self._IdxWritePtr = idx_buffer_old_size + 1 end -- Release the number of reserved vertices/indices from the end of the last reservation made with PrimReserve(). function MT.ImDrawList:PrimUnreserve(idx_count, vtx_count) IM_ASSERT_PARANOID(idx_count >= 0 and vtx_count >= 0) local draw_cmd = self.CmdBuffer.Data[self.CmdBuffer.Size] draw_cmd.ElemCount = draw_cmd.ElemCount - idx_count self.VtxBuffer:shrink(self.VtxBuffer.Size - vtx_count) self.IdxBuffer:shrink(self.IdxBuffer.Size - idx_count) self._VtxWritePtr = self.VtxBuffer.Size + 1 self._IdxWritePtr = self.IdxBuffer.Size + 1 end -- Fully unrolled with inline call to keep our debug builds decently fast. --- @param a ImVec2 --- @param c ImVec2 --- @param col ImU32 @native function MT.ImDrawList:PrimRect(a, c, col) local uv = self._Data.TexUvWhitePixel local idx = self._VtxCurrentIdx local idx_data = self.IdxBuffer.Data; local vtx_data = self.VtxBuffer.Buf local idx_write_ptr = self._IdxWritePtr idx_data[idx_write_ptr + 0] = idx idx_data[idx_write_ptr + 1] = idx + 1 idx_data[idx_write_ptr + 2] = idx + 2 idx_data[idx_write_ptr + 3] = idx idx_data[idx_write_ptr + 4] = idx + 2 idx_data[idx_write_ptr + 5] = idx + 3 local vtx_write_ptr = self._VtxWritePtr local ax, ay, cx, cy, u, v = a.x, a.y, c.x, c.y, uv.x, uv.y WV(vtx_data, vtx_write_ptr + 0, ax, ay, u, v, col) WV(vtx_data, vtx_write_ptr + 1, cx, ay, u, v, col) WV(vtx_data, vtx_write_ptr + 2, cx, cy, u, v, col) WV(vtx_data, vtx_write_ptr + 3, ax, cy, u, v, col) self._VtxWritePtr = vtx_write_ptr + 4 self._VtxCurrentIdx = idx + 4 self._IdxWritePtr = idx_write_ptr + 6 end --- @param a ImVec2 --- @param c ImVec2 --- @param uv_a ImVec2 --- @param uv_c ImVec2 --- @param col any function MT.ImDrawList:PrimRectUV(a, c, uv_a, uv_c, col) local idx = self._VtxCurrentIdx local idx_data = self.IdxBuffer.Data; local vtx_data = self.VtxBuffer.Buf local idx_write_ptr = self._IdxWritePtr idx_data[idx_write_ptr + 0] = idx idx_data[idx_write_ptr + 1] = idx + 1 idx_data[idx_write_ptr + 2] = idx + 2 idx_data[idx_write_ptr + 3] = idx idx_data[idx_write_ptr + 4] = idx + 2 idx_data[idx_write_ptr + 5] = idx + 3 local vtx_write_ptr = self._VtxWritePtr WV(vtx_data, vtx_write_ptr + 0, a.x, a.y, uv_a.x, uv_a.y, col) WV(vtx_data, vtx_write_ptr + 1, c.x, a.y, uv_c.x, uv_a.y, col) WV(vtx_data, vtx_write_ptr + 2, c.x, c.y, uv_c.x, uv_c.y, col) WV(vtx_data, vtx_write_ptr + 3, a.x, c.y, uv_a.x, uv_c.y, col) self._VtxWritePtr = vtx_write_ptr + 4 self._VtxCurrentIdx = idx + 4 self._IdxWritePtr = idx_write_ptr + 6 end --- @param points ImVec2[] --- @param points_count int --- @param col ImU32 --- @param thickness float --- @param flags? ImDrawFlags @native function MT.ImDrawList:AddPolyline(points, points_count, col, thickness, flags) if flags == nil then flags = 0 end if points_count < 2 or bit32.band(col, IM_COL32_A_MASK) == 0 then return end local closed = bit32.band(flags, ImDrawFlags.Closed) ~= 0 local opaque_uv = self._Data.TexUvWhitePixel local count = closed and points_count or points_count - 1 -- Number of line segments local thick_line = thickness > self._FringeScale -- If this assert triggers on legacy code: -- - 1.92.8 (2025/05): swapped two last parameters order: flags, thickness --> thickness, flags. This should normally be caught by compile-time type-checking. -- - 1.92.8 (2025/05): changed value of ImDrawList_Closed which was previously guaranteed to be == 1. Hardcoded use of 1 or true should be replaced. -- Read more details near AddRect() + see "API BREAKING CHANGES" section for 1.82, 1.90 and 1.92.8. IM_ASSERT_USER_ERROR_RET(bit32.band(flags, ImDrawFlags.InvalidMask_) == 0, "Incorrect parameter. Did you swap 'thickness' and 'flags'?") if bit32.band(self.Flags, ImDrawListFlags.AntiAliasedLines) ~= 0 then -- Anti-aliased stroke local AA_SIZE = self._FringeScale local col_trans = bit32.band(col, bit32.bnot(IM_COL32_A_MASK)) -- Thicknesses <1.0 should behave like thickness 1.0 thickness = ImMax(thickness, 1.0) local integer_thickness = ImFloor(thickness) local fractional_thickness = thickness - integer_thickness -- Do we want to draw this line using a texture? local use_texture = bit32.band(self.Flags, ImDrawListFlags.AntiAliasedLinesUseTex) ~= 0 and integer_thickness < IM_DRAWLIST_TEX_LINES_WIDTH_MAX and fractional_thickness <= 0.00001 and AA_SIZE == 1.0 IM_ASSERT_PARANOID((not use_texture) or bit32.band(self._Data.Font.OwnerAtlas.Flags, ImFontAtlasFlags.NoBakedLines) == 0) local idx_count = use_texture and (count * 6) or (thick_line and count * 18 or count * 12) local vtx_count = use_texture and (points_count * 2) or (thick_line and points_count * 4 or points_count * 3) self:PrimReserve(idx_count, vtx_count) -- Temporary buffer local temp_buffer_size = points_count * ((use_texture or not thick_line) and 3 or 5) self._Data.TempBuffer:reserve_discard(temp_buffer_size) local temp_normals = self._Data.TempBuffer.Data local temp_points = self._Data.TempBuffer.Data; local temp_points_start = points_count + 1 -- Calculate normals for each line segment for i1 = 1, count do local i2 = (i1 == points_count) and 1 or i1 + 1 local p1 = points[i1] local p2 = points[i2] local dx = p2.x - p1.x local dy = p2.y - p1.y dx, dy = IM_NORMALIZE2F_OVER_ZERO(dx, dy) temp_normals[i1].x = dy temp_normals[i1].y = -dx end if not closed then ImVec2_Copy(temp_normals[points_count], temp_normals[points_count - 1]) end -- If we are drawing a one-pixel-wide line without a texture, or a textured line of any width if use_texture or not thick_line then -- [PATH 1] Texture-based lines (thick or non-thick) -- [PATH 2] Non texture-based lines (non-thick) local half_draw_size = use_texture and (thickness * 0.5 + 1) or AA_SIZE -- If line is not closed, the first and last points need to be generated differently if not closed then ImVec2_CopyV(temp_points[temp_points_start + 0], ImVec2_AddVA(points[1], ImVec2_MulVX(temp_normals[1], half_draw_size))) ImVec2_CopyV(temp_points[temp_points_start + 1], ImVec2_SubVA(points[1], ImVec2_MulVX(temp_normals[1], half_draw_size))) ImVec2_CopyV(temp_points[temp_points_start + (points_count - 1) * 2 + 0], ImVec2_AddVA(points[points_count], ImVec2_MulVX(temp_normals[points_count], half_draw_size))) ImVec2_CopyV(temp_points[temp_points_start + (points_count - 1) * 2 + 1], ImVec2_SubVA(points[points_count], ImVec2_MulVX(temp_normals[points_count], half_draw_size))) end -- Generate indices and vertices local idx1 = self._VtxCurrentIdx local idx_data = self.IdxBuffer.Data; local vtx_data = self.VtxBuffer.Buf for i1 = 1, count do local i2 = (i1 == points_count) and 1 or i1 + 1 local idx2 = (i1 == points_count) and self._VtxCurrentIdx or (idx1 + (use_texture and 2 or 3)) -- Average normals local dm_x = (temp_normals[i1].x + temp_normals[i2].x) * 0.5 local dm_y = (temp_normals[i1].y + temp_normals[i2].y) * 0.5 dm_x, dm_y = IM_FIXNORMAL2F(dm_x, dm_y) dm_x = dm_x * half_draw_size dm_y = dm_y * half_draw_size -- Add temporary vertices for the outer edges local out_off = (i2 - 1) * 2 temp_points[temp_points_start + out_off + 0].x = points[i2].x + dm_x temp_points[temp_points_start + out_off + 0].y = points[i2].y + dm_y temp_points[temp_points_start + out_off + 1].x = points[i2].x - dm_x temp_points[temp_points_start + out_off + 1].y = points[i2].y - dm_y if use_texture then -- Add indices for two triangles local idx_write_ptr = self._IdxWritePtr idx_data[idx_write_ptr + 0] = idx2 + 0; idx_data[idx_write_ptr + 1] = idx1 + 0; idx_data[idx_write_ptr + 2] = idx1 + 1 idx_data[idx_write_ptr + 3] = idx2 + 1; idx_data[idx_write_ptr + 4] = idx1 + 1; idx_data[idx_write_ptr + 5] = idx2 + 0 self._IdxWritePtr = idx_write_ptr + 6 else -- Add indices for four triangles local idx_write_ptr = self._IdxWritePtr idx_data[idx_write_ptr + 0] = idx2 + 0; idx_data[idx_write_ptr + 1] = idx1 + 0; idx_data[idx_write_ptr + 2] = idx1 + 2 idx_data[idx_write_ptr + 3] = idx1 + 2; idx_data[idx_write_ptr + 4] = idx2 + 2; idx_data[idx_write_ptr + 5] = idx2 + 0 idx_data[idx_write_ptr + 6] = idx2 + 1; idx_data[idx_write_ptr + 7] = idx1 + 1; idx_data[idx_write_ptr + 8] = idx1 + 0 idx_data[idx_write_ptr + 9] = idx1 + 0; idx_data[idx_write_ptr + 10] = idx2 + 0; idx_data[idx_write_ptr + 11] = idx2 + 1 self._IdxWritePtr = idx_write_ptr + 12 end idx1 = idx2 end -- Add vertices if use_texture then -- Texture-based: need to implement TexUvLines lookup local tex_uvs = self._Data.TexUvLines[integer_thickness] local tex_uv0 = ImVec2(tex_uvs.x, tex_uvs.y) local tex_uv1 = ImVec2(tex_uvs.z, tex_uvs.w) for i = 0, points_count - 1 do local vtx_write_ptr = self._VtxWritePtr WVP(vtx_data, vtx_write_ptr + 0, temp_points[temp_points_start + i * 2 + 0], tex_uv0, col) WVP(vtx_data, vtx_write_ptr + 1, temp_points[temp_points_start + i * 2 + 1], tex_uv1, col) self._VtxWritePtr = vtx_write_ptr + 2 end else -- If we're not using a texture, we need the center vertex as well for i = 0, points_count - 1 do local vtx_write_ptr = self._VtxWritePtr WVP(vtx_data, vtx_write_ptr + 0, points[i + 1], opaque_uv, col) WVP(vtx_data, vtx_write_ptr + 1, temp_points[temp_points_start + i * 2 + 0], opaque_uv, col_trans) WVP(vtx_data, vtx_write_ptr + 2, temp_points[temp_points_start + i * 2 + 1], opaque_uv, col_trans) self._VtxWritePtr = vtx_write_ptr + 3 end end else -- [PATH 3] Non texture-based lines (thick) local half_inner_thickness = (thickness - AA_SIZE) * 0.5 -- If line is not closed, handle first and last points if not closed then local points_last = points_count - 1 ImVec2_CopyV(temp_points[temp_points_start + 0], ImVec2_AddVA(points[1], ImVec2_MulVX(temp_normals[1], (half_inner_thickness + AA_SIZE)))) ImVec2_CopyV(temp_points[temp_points_start + 1], ImVec2_AddVA(points[1], ImVec2_MulVX(temp_normals[1], half_inner_thickness))) ImVec2_CopyV(temp_points[temp_points_start + 2], ImVec2_SubVA(points[1], ImVec2_MulVX(temp_normals[1], half_inner_thickness))) ImVec2_CopyV(temp_points[temp_points_start + 3], ImVec2_SubVA(points[1], ImVec2_MulVX(temp_normals[1], (half_inner_thickness + AA_SIZE)))) ImVec2_CopyV(temp_points[temp_points_start + points_last * 4 + 0], ImVec2_AddVA(points[points_count], ImVec2_MulVX(temp_normals[points_last + 1], (half_inner_thickness + AA_SIZE)))) ImVec2_CopyV(temp_points[temp_points_start + points_last * 4 + 1], ImVec2_AddVA(points[points_count], ImVec2_MulVX(temp_normals[points_last + 1], half_inner_thickness))) ImVec2_CopyV(temp_points[temp_points_start + points_last * 4 + 2], ImVec2_SubVA(points[points_count], ImVec2_MulVX(temp_normals[points_last + 1], half_inner_thickness))) ImVec2_CopyV(temp_points[temp_points_start + points_last * 4 + 3], ImVec2_SubVA(points[points_count], ImVec2_MulVX(temp_normals[points_last + 1], (half_inner_thickness + AA_SIZE)))) end -- Generate indices and vertices local idx1 = self._VtxCurrentIdx local idx_data = self.IdxBuffer.Data; local vtx_data = self.VtxBuffer.Buf for i1 = 1, count do local i2 = (i1 == points_count) and 1 or i1 + 1 local idx2 = (i1 == points_count) and self._VtxCurrentIdx or (idx1 + 4) -- Average normals local dm_x = (temp_normals[i1].x + temp_normals[i2].x) * 0.5 local dm_y = (temp_normals[i1].y + temp_normals[i2].y) * 0.5 dm_x, dm_y = IM_FIXNORMAL2F(dm_x, dm_y) local dm_out_x = dm_x * (half_inner_thickness + AA_SIZE) local dm_out_y = dm_y * (half_inner_thickness + AA_SIZE) local dm_in_x = dm_x * half_inner_thickness local dm_in_y = dm_y * half_inner_thickness -- Add temporary vertices local out_off = (i2 - 1) * 4 temp_points[temp_points_start + out_off + 0].x = points[i2].x + dm_out_x temp_points[temp_points_start + out_off + 0].y = points[i2].y + dm_out_y temp_points[temp_points_start + out_off + 1].x = points[i2].x + dm_in_x temp_points[temp_points_start + out_off + 1].y = points[i2].y + dm_in_y temp_points[temp_points_start + out_off + 2].x = points[i2].x - dm_in_x temp_points[temp_points_start + out_off + 2].y = points[i2].y - dm_in_y temp_points[temp_points_start + out_off + 3].x = points[i2].x - dm_out_x temp_points[temp_points_start + out_off + 3].y = points[i2].y - dm_out_y -- Add indices local idx_write_ptr = self._IdxWritePtr idx_data[idx_write_ptr + 0] = idx2 + 1; idx_data[idx_write_ptr + 1] = idx1 + 1; idx_data[idx_write_ptr + 2] = idx1 + 2 idx_data[idx_write_ptr + 3] = idx1 + 2; idx_data[idx_write_ptr + 4] = idx2 + 2; idx_data[idx_write_ptr + 5] = idx2 + 1 idx_data[idx_write_ptr + 6] = idx2 + 1; idx_data[idx_write_ptr + 7] = idx1 + 1; idx_data[idx_write_ptr + 8] = idx1 + 0 idx_data[idx_write_ptr + 9] = idx1 + 0; idx_data[idx_write_ptr + 10] = idx2 + 0; idx_data[idx_write_ptr + 11] = idx2 + 1 idx_data[idx_write_ptr + 12] = idx2 + 2; idx_data[idx_write_ptr + 13] = idx1 + 2; idx_data[idx_write_ptr + 14] = idx1 + 3 idx_data[idx_write_ptr + 15] = idx1 + 3; idx_data[idx_write_ptr + 16] = idx2 + 3; idx_data[idx_write_ptr + 17] = idx2 + 2 self._IdxWritePtr = idx_write_ptr + 18 idx1 = idx2 end -- Add vertices for i = 0, points_count - 1 do local vtx_write_ptr = self._VtxWritePtr WVP(vtx_data, vtx_write_ptr + 0, temp_points[temp_points_start + i * 4 + 0], opaque_uv, col_trans) WVP(vtx_data, vtx_write_ptr + 1, temp_points[temp_points_start + i * 4 + 1], opaque_uv, col) WVP(vtx_data, vtx_write_ptr + 2, temp_points[temp_points_start + i * 4 + 2], opaque_uv, col) WVP(vtx_data, vtx_write_ptr + 3, temp_points[temp_points_start + i * 4 + 3], opaque_uv, col_trans) self._VtxWritePtr = vtx_write_ptr + 4 end end self._VtxCurrentIdx = self._VtxCurrentIdx + vtx_count else -- [PATH 4] Non texture-based, Non anti-aliased lines local idx_count = count * 6 local vtx_count = count * 4 self:PrimReserve(idx_count, vtx_count) local idx_data = self.IdxBuffer.Data; local vtx_data = self.VtxBuffer.Buf for i1 = 1, count do local i2 = (i1 == points_count) and 1 or i1 + 1 local p1 = points[i1] local p2 = points[i2] local dx = p2.x - p1.x local dy = p2.y - p1.y dx, dy = IM_NORMALIZE2F_OVER_ZERO(dx, dy) dx = dx * (thickness * 0.5) dy = dy * (thickness * 0.5) local vtx_write_ptr = self._VtxWritePtr WV(vtx_data, vtx_write_ptr + 0, p1.x + dy, p1.y - dx, opaque_uv.x, opaque_uv.y, col) WV(vtx_data, vtx_write_ptr + 1, p2.x + dy, p2.y - dx, opaque_uv.x, opaque_uv.y, col) WV(vtx_data, vtx_write_ptr + 2, p2.x - dy, p2.y + dx, opaque_uv.x, opaque_uv.y, col) WV(vtx_data, vtx_write_ptr + 3, p1.x - dy, p1.y + dx, opaque_uv.x, opaque_uv.y, col) self._VtxWritePtr = vtx_write_ptr + 4 local idx_write_ptr = self._IdxWritePtr idx_data[idx_write_ptr + 0] = self._VtxCurrentIdx + 0; idx_data[idx_write_ptr + 1] = self._VtxCurrentIdx + 1; idx_data[idx_write_ptr + 2] = self._VtxCurrentIdx + 2 idx_data[idx_write_ptr + 3] = self._VtxCurrentIdx + 0; idx_data[idx_write_ptr + 4] = self._VtxCurrentIdx + 2; idx_data[idx_write_ptr + 5] = self._VtxCurrentIdx + 3 self._IdxWritePtr = idx_write_ptr + 6 self._VtxCurrentIdx = self._VtxCurrentIdx + 4 end end end function MT.ImDrawList:PathRect(a, b, rounding, flags) if not rounding then rounding = 0.0 end if not flags then flags = 0 end if rounding >= 0.5 then IM_ASSERT(bit32.band(flags, 0x0F) == 0, "Misuse of legacy hardcoded ImDrawCornerFlags values!") if bit32.band(flags, ImDrawFlags.RoundCornersMask_) == 0 then flags = bit32.bor(flags, ImDrawFlags.RoundCornersAll) end rounding = ImMin(rounding, ImAbs(b.x - a.x) * (((bit32.band(flags, ImDrawFlags.RoundCornersTop) == ImDrawFlags.RoundCornersTop) or (bit32.band(flags, ImDrawFlags.RoundCornersBottom) == ImDrawFlags.RoundCornersBottom)) and 0.5 or 1.0) - 1.0) rounding = ImMin(rounding, ImAbs(b.y - a.y) * (((bit32.band(flags, ImDrawFlags.RoundCornersLeft) == ImDrawFlags.RoundCornersLeft) or (bit32.band(flags, ImDrawFlags.RoundCornersRight) == ImDrawFlags.RoundCornersRight)) and 0.5 or 1.0) - 1.0) end if rounding < 0.5 or (bit32.band(flags, ImDrawFlags.RoundCornersMask_) == ImDrawFlags.RoundCornersNone) then self:PathLineTo(a) self:PathLineTo(ImVec2(b.x, a.y)) self:PathLineTo(b) self:PathLineTo(ImVec2(a.x, b.y)) else local rounding_tl = (bit32.band(flags, ImDrawFlags.RoundCornersTopLeft) ~= 0) and rounding or 0.0 local rounding_tr = (bit32.band(flags, ImDrawFlags.RoundCornersTopRight) ~= 0) and rounding or 0.0 local rounding_br = (bit32.band(flags, ImDrawFlags.RoundCornersBottomRight) ~= 0) and rounding or 0.0 local rounding_bl = (bit32.band(flags, ImDrawFlags.RoundCornersBottomLeft) ~= 0) and rounding or 0.0 self:PathArcToFast(ImVec2(a.x + rounding_tl, a.y + rounding_tl), rounding_tl, 6, 9) self:PathArcToFast(ImVec2(b.x - rounding_tr, a.y + rounding_tr), rounding_tr, 9, 12) self:PathArcToFast(ImVec2(b.x - rounding_br, b.y - rounding_br), rounding_br, 0, 3) self:PathArcToFast(ImVec2(a.x + rounding_bl, b.y - rounding_bl), rounding_bl, 3, 6) end end @native function MT.ImDrawList:AddRectFilled(p_min, p_max, col, rounding, flags) if not rounding then rounding = 0.0 end if not flags then flags = 0 end if bit32.band(col, IM_COL32_A_MASK) == 0 then return end if rounding < 0.5 or (bit32.band(flags, ImDrawFlags.RoundCornersMask_) == ImDrawFlags.RoundCornersNone) then self:PrimReserve(6, 4) self:PrimRect(p_min, p_max, col) else self:PathRect(p_min, p_max, rounding, flags) self:PathFillConvex(col) end end -- p_min = upper-left, p_max = lower-right -- Note: this does not support anti-aliasing when provided non-integer vertices. --- @param p_min ImVec2 --- @param p_max ImVec2 --- @param col_upr_left ImU32 --- @param col_upr_right ImU32 --- @param col_bot_right ImU32 --- @param col_bot_left ImU32 function MT.ImDrawList:AddRectFilledMultiColor(p_min, p_max, col_upr_left, col_upr_right, col_bot_right, col_bot_left) if bit32.band(bit32.bor(col_upr_left, col_upr_right, col_bot_right, col_bot_left), IM_COL32_A_MASK) == 0 then return end local uv = self._Data.TexUvWhitePixel self:PrimReserve(6, 4) self:PrimWriteIdx(self._VtxCurrentIdx); self:PrimWriteIdx(self._VtxCurrentIdx + 1); self:PrimWriteIdx(self._VtxCurrentIdx + 2) self:PrimWriteIdx(self._VtxCurrentIdx); self:PrimWriteIdx(self._VtxCurrentIdx + 2); self:PrimWriteIdx(self._VtxCurrentIdx + 3) self:PrimWriteVtx(p_min, uv, col_upr_left) self:PrimWriteVtx(ImVec2(p_max.x, p_min.y), uv, col_upr_right) self:PrimWriteVtx(p_max, uv, col_bot_right) self:PrimWriteVtx(ImVec2(p_min.x, p_max.y), uv, col_bot_left) end -- p_min = upper-left, p_max = lower-right --- @param p_min ImVec2 --- @param p_max ImVec2 --- @param col ImU32 --- @param rounding float --- @param thickness? float --- @param flags? ImDrawFlags @native function MT.ImDrawList:AddRect(p_min, p_max, col, rounding, thickness, flags) if thickness == nil then thickness = 1.0 end if flags == nil then flags = 0 end -- If this assert triggers on legacy code: -- - 1.92.8 (2025/05): swapped two last parameters order: flags, thickness --> thickness, flags. This should normally be caught by compile-time type-checking. -- - 1.92.8 (2025/05): changed value of ImDrawList_Closed which was previously guaranteed to be == 1. Hardcoded use of 1 or true should be replaced. -- - 1.82.0 (2021/03): changed ImDrawCornerFlags to ImDrawFlags_RoundXXX values. -- If you used hard-coded 1 to 15 or ~0 in flags to configure corner rounding use the new flags! -- - Hard coded support for ~0 == ImDrawFlags_RoundAll. -- - Hard coded support for values 0x01 to 0x0F (matching 15 out of 16 old flags combinations) --> see FixRectCornerFlags() in <1.90 code. -- - Hard coded 0x00 with 'float rounding > 0.0f' --> replace with ImDrawFlags_RoundNone or use 'float rounding = 0.0f'. -- See "API BREAKING CHANGES" section for 1.82, 1.90 and 1.92.8. IM_ASSERT_USER_ERROR_RET(bit32.band(flags, ImDrawFlags.InvalidMask_) == 0, "Incorrect parameter. Did you swap 'thickness' and 'flags'?") -- Or misuse of legacy hard-coded ImDrawCornerFlags values if bit32.band(col, IM_COL32_A_MASK) == 0 then return end -- [Luau] square corners + whole-pixel thickness: 4 filled rects instead of a stroked path (same pixels, far cheaper here) if (rounding == nil or rounding < 0.5 or bit32.band(flags, ImDrawFlags.RoundCornersMask_) == ImDrawFlags.RoundCornersNone) and thickness >= 1.0 and thickness % 1 == 0 and p_max.x - p_min.x > 2 * thickness and p_max.y - p_min.y > 2 * thickness then local x0, y0, x1, y1, t = p_min.x, p_min.y, p_max.x, p_max.y, thickness self:PrimReserve(24, 16) self:PrimRect(ImVec2(x0, y0), ImVec2(x1, y0 + t), col) self:PrimRect(ImVec2(x0, y1 - t), ImVec2(x1, y1), col) self:PrimRect(ImVec2(x0, y0 + t), ImVec2(x0 + t, y1 - t), col) self:PrimRect(ImVec2(x1 - t, y0 + t), ImVec2(x1, y1 - t), col) return end if bit32.band(self.Flags, ImDrawListFlags.AntiAliasedLines) ~= 0 then self:PathRect(p_min + ImVec2(0.50, 0.50), p_max - ImVec2(0.50, 0.50), rounding, flags) else self:PathRect(p_min + ImVec2(0.50, 0.50), p_max - ImVec2(0.49, 0.49), rounding, flags) -- Better looking lower-right corner and rounded non-AA shapes. end self:PathStroke(col, thickness, ImDrawFlags.Closed) end function MT.ImDrawList:AddLine(p1, p2, col, thickness) if bit32.band(col, IM_COL32_A_MASK) == 0 then return end self:PathLineTo(p1 + ImVec2(0.5, 0.5)) self:PathLineTo(p2 + ImVec2(0.5, 0.5)) self:PathStroke(col, thickness) end --- @param min_x float --- @param max_x float --- @param y float --- @param col ImU32 --- @param thickness? float function MT.ImDrawList:AddLineH(min_x, max_x, y, col, thickness) if thickness == nil then thickness = 1.0 end if bit32.band(col, IM_COL32_A_MASK) == 0 then return end self:PathLineTo(ImVec2(min_x + 0.5, y + 0.5)) self:PathLineTo(ImVec2(max_x + 0.5, y + 0.5)) self:PathStroke(col, thickness) end --- @param x float --- @param min_y float --- @param max_y float --- @param col ImU32 --- @param thickness? float function MT.ImDrawList:AddLineV(x, min_y, max_y, col, thickness) if thickness == nil then thickness = 1.0 end if bit32.band(col, IM_COL32_A_MASK) == 0 then return end self:PathLineTo(ImVec2(x + 0.5, min_y + 0.5)) self:PathLineTo(ImVec2(x + 0.5, max_y + 0.5)) self:PathStroke(col, thickness) end function MT.ImDrawList:AddTriangle(p1, p2, p3, col, thickness) if bit32.band(col, IM_COL32_A_MASK) == 0 then return end self:PathLineTo(p1) self:PathLineTo(p2) self:PathLineTo(p3) self:PathStroke(col, thickness, ImDrawFlags.Closed) end function MT.ImDrawList:AddQuad(p1, p2, p3, p4, col, thickness) if thickness == nil then thickness = 1.0 end if bit32.band(col, IM_COL32_A_MASK) == 0 then return end self:PathLineTo(p1) self:PathLineTo(p2) self:PathLineTo(p3) self:PathLineTo(p4) self:PathStroke(col, thickness, ImDrawFlags.Closed) end function MT.ImDrawList:AddQuadFilled(p1, p2, p3, p4, col) if bit32.band(col, IM_COL32_A_MASK) == 0 then return end self:PathLineTo(p1) self:PathLineTo(p2) self:PathLineTo(p3) self:PathLineTo(p4) self:PathFillConvex(col) end function MT.ImDrawList:AddTriangleFilled(p1, p2, p3, col) if bit32.band(col, IM_COL32_A_MASK) == 0 then return end self:PathLineTo(p1) self:PathLineTo(p2) self:PathLineTo(p3) self:PathFillConvex(col) end --- @param center ImVec2 --- @param radius float --- @param col ImU32 --- @param num_segments int --- @param thickness float function MT.ImDrawList:AddCircle(center, radius, col, num_segments, thickness) if num_segments == nil then num_segments = 0 end if thickness == nil then thickness = 1.0 end if bit32.band(col, IM_COL32_A_MASK) == 0 or radius < 0.5 then return end if num_segments <= 0 then -- Use arc with automatic segment count self:_PathArcToFastEx(center, radius - 0.5, 0, IM_DRAWLIST_ARCFAST_SAMPLE_MAX, 0) self._Path.Size = self._Path.Size - 1 else -- Explicit segment count (still clamp to avoid drawing insanely tessellated shapes) num_segments = ImClamp(num_segments, 3, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX) -- Because we are filling a closed shape we remove 1 from the count of segments/points local a_max = (IM_PI * 2.0) * (num_segments - 1) / num_segments self:PathArcTo(center, radius - 0.5, 0.0, a_max, num_segments - 1) end self:PathStroke(col, thickness, ImDrawFlags.Closed) end --- @param center ImVec2 --- @param radius float --- @param col ImU32 --- @param num_segments int function MT.ImDrawList:AddCircleFilled(center, radius, col, num_segments) if num_segments == nil then num_segments = 0 end if bit32.band(col, IM_COL32_A_MASK) == 0 or radius < 0.5 then return end if num_segments <= 0 then -- Use arc with automatic segment count self:_PathArcToFastEx(center, radius, 0, IM_DRAWLIST_ARCFAST_SAMPLE_MAX, 0) self._Path.Size = self._Path.Size - 1 else -- Explicit segment count (still clamp to avoid drawing insanely tessellated shapes) num_segments = ImClamp(num_segments, 3, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX) -- Because we are filling a closed shape we remove 1 from the count of segments/points local a_max = (IM_PI * 2.0) * (num_segments - 1) / num_segments self:PathArcTo(center, radius, 0.0, a_max, num_segments - 1) end self:PathFillConvex(col) end --- @param font ImFont --- @param font_size float --- @param pos ImVec2 --- @param col ImU32 --- @param text string --- @param text_begin? int # Defaults to 1 --- @param text_end int --- @param wrap_width float --- @param cpu_fine_clip_rect? ImVec4 @native function MT.ImDrawList:AddText(font, font_size, pos, col, text, text_begin, text_end, wrap_width, cpu_fine_clip_rect) -- Overload: AddText(pos, col, text, text_end?) if type(font) == "table" and type(font[1]) == "number" then font, font_size, pos, col, text, text_begin, text_end, wrap_width, cpu_fine_clip_rect = nil, 0.0, font, font_size, pos, 1, col, 0.0, nil if text_end == nil then text_end = #text + 1 end end if text_begin == nil then text_begin = 1 end if bit32.band(col, IM_COL32_A_MASK) == 0 then return end -- Accept null ranges if text_begin == text_end then return end -- No need to strlen() here: font:RenderText() will do it and may early out. -- Pull default font/size from the shared ImDrawListSharedData instance if font == nil then font = self._Data.Font end if font_size == 0.0 then font_size = self._Data.FontSize end local clip_rect = ImVec4() -- Don't modify the clip rect! ImVec4_Copy(clip_rect, self._CmdHeader.ClipRect) if (cpu_fine_clip_rect) then clip_rect.x = ImMax(clip_rect.x, cpu_fine_clip_rect.x) clip_rect.y = ImMax(clip_rect.y, cpu_fine_clip_rect.y) clip_rect.z = ImMin(clip_rect.z, cpu_fine_clip_rect.z) clip_rect.w = ImMin(clip_rect.w, cpu_fine_clip_rect.w) end font:RenderText(self, font_size, pos, col, clip_rect, text, text_begin, text_end, wrap_width, (cpu_fine_clip_rect ~= nil) and ImDrawTextFlags.CpuFineClip or ImDrawTextFlags.None) end --- @param tex_ref ImTextureRef --- @param p_min ImVec2 --- @param p_max ImVec2 --- @param uv_min? ImVec2 --- @param uv_max? ImVec2 --- @param col? ImU32 function MT.ImDrawList:AddImage(tex_ref, p_min, p_max, uv_min, uv_max, col) if uv_min == nil then uv_min = ImVec2(0, 0) end if uv_max == nil then uv_max = ImVec2(1, 1) end if col == nil then col = IM_COL32_WHITE end if bit32.band(col, IM_COL32_A_MASK) == 0 then return end local push_texture_id = tex_ref ~= self._CmdHeader.TexRef if push_texture_id then self:PushTexture(tex_ref) end self:PrimReserve(6, 4) self:PrimRectUV(p_min, p_max, uv_min, uv_max, col) if push_texture_id then self:PopTexture() end end --- @param tex_ref ImTextureRef --- @param p_min ImVec2 --- @param p_max ImVec2 --- @param uv_min ImVec2 --- @param uv_max ImVec2 --- @param col ImU32 --- @param rounding float --- @param flags ImDrawFlags function MT.ImDrawList:AddImageRounded(tex_ref, p_min, p_max, uv_min, uv_max, col, rounding, flags) if bit32.band(col, IM_COL32_A_MASK) == 0 then return end IM_ASSERT(bit32.band(flags, 0x0F) == 0, "Misuse of legacy hardcoded ImDrawCornerFlags values!") -- If this assert triggers on legacy code: see comments in ImDrawList:PathRect(). if bit32.band(flags, ImDrawFlags.RoundCornersMask_) == 0 then flags = bit32.bor(flags, ImDrawFlags.RoundCornersAll) end if rounding < 0.5 or bit32.band(flags, ImDrawFlags.RoundCornersMask_) == ImDrawFlags.RoundCornersNone then self:AddImage(tex_ref, p_min, p_max, uv_min, uv_max, col) return end local push_texture_id = (tex_ref ~= self._CmdHeader.TexRef) if push_texture_id then self:PushTexture(tex_ref) end local vert_start_idx = self.VtxBuffer.Size + 1 self:PathRect(p_min, p_max, rounding, flags) self:PathFillConvex(col) local vert_end_idx = self.VtxBuffer.Size + 1 ImGui.ShadeVertsLinearUV(self, vert_start_idx, vert_end_idx, p_min, p_max, uv_min, uv_max, true) if push_texture_id then self:PopTexture() end end --- @param radius float function MT.ImDrawList:_CalcCircleAutoSegmentCount(radius) -- Automatic segment count local radius_idx = ImFloor(radius + 0.999) + 1 -- ceil to never reduce accuracy if radius_idx >= 1 and radius_idx <= 64 then -- IM_COUNTOF(_Data->CircleSegmentCounts) return self._Data.CircleSegmentCounts[radius_idx] -- Use cached value else return IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, self._Data.CircleTessellationMaxError) end end -- Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui.PushClipRect() to affect logic (hit-testing and widget culling) function MT.ImDrawList:PushClipRect(cr_min, cr_max, intersect_with_current_clip_rect) local cr = ImVec4(cr_min.x, cr_min.y, cr_max.x, cr_max.y) if intersect_with_current_clip_rect then local current = self._CmdHeader.ClipRect if cr.x < current.x then cr.x = current.x end -- = ClipWith(). Note that passing inverted range wouldn't be fixed here. if cr.y < current.y then cr.y = current.y end if cr.z > current.z then cr.z = current.z end if cr.w > current.w then cr.w = current.w end end cr.z = ImMax(cr.x, cr.z) cr.w = ImMax(cr.y, cr.w) self._ClipRectStack:push_back(cr) ImVec4_Copy(self._CmdHeader.ClipRect, cr) self:_OnChangedClipRect() end function MT.ImDrawList:PopClipRect() self._ClipRectStack:pop_back() if (self._ClipRectStack.Size == 0) then -- LUA: No "Ternary Operator" ImVec4_Copy(self._CmdHeader.ClipRect, self._Data.ClipRectFullscreen) else ImVec4_Copy(self._CmdHeader.ClipRect, self._ClipRectStack.Data[self._ClipRectStack.Size]) end self:_OnChangedClipRect() end --- @param center ImVec2 --- @param radius float --- @param a_min_sample int --- @param a_max_sample int --- @param a_step int @native function MT.ImDrawList:_PathArcToFastEx(center, radius, a_min_sample, a_max_sample, a_step) if radius < 0.5 then self._Path:push_back(center) return end -- Calculate arc auto segment step size if a_step <= 0 then a_step = math.floor(IM_DRAWLIST_ARCFAST_SAMPLE_MAX / self:_CalcCircleAutoSegmentCount(radius)) end -- Make sure we never do steps larger than one quarter of the circle a_step = ImClamp(a_step, 1, math.floor(IM_DRAWLIST_ARCFAST_TABLE_SIZE / 4)) local sample_range = ImAbs(a_max_sample - a_min_sample) local a_next_step = a_step local samples = sample_range + 1 local extra_max_sample = false if a_step > 1 then samples = math.floor(sample_range / a_step) + 1 local overstep = sample_range % a_step if overstep > 0 then extra_max_sample = true samples = samples + 1 -- When we have overstep to avoid awkwardly looking one long line and one tiny one at the end, -- distribute first step range evenly between them by reducing first step size. if sample_range > 0 then a_step = a_step - math.floor((a_step - overstep) / 2) end end end self._Path:resize(self._Path.Size + samples) local out_ptr = self._Path.Size - samples + 1 local sample_index = a_min_sample if sample_index < 0 or sample_index >= IM_DRAWLIST_ARCFAST_SAMPLE_MAX then sample_index = sample_index % IM_DRAWLIST_ARCFAST_SAMPLE_MAX if sample_index < 0 then sample_index = sample_index + IM_DRAWLIST_ARCFAST_SAMPLE_MAX end end if a_max_sample >= a_min_sample then local a = a_min_sample while a <= a_max_sample do -- a_step is clamped to IM_DRAWLIST_ARCFAST_SAMPLE_MAX, so we have guaranteed that it will not wrap over range twice or more if sample_index >= IM_DRAWLIST_ARCFAST_SAMPLE_MAX then sample_index = sample_index - IM_DRAWLIST_ARCFAST_SAMPLE_MAX end local s = self._Data.ArcFastVtx[sample_index + 1] ImVec2_CopyV(self._Path.Data[out_ptr], center.x + s.x * radius, center.y + s.y * radius) out_ptr = out_ptr + 1 a = a + a_step sample_index = sample_index + a_step a_step = a_next_step end else local a = a_min_sample while a >= a_max_sample do -- a_step is clamped to IM_DRAWLIST_ARCFAST_SAMPLE_MAX, so we have guaranteed that it will not wrap over range twice or more if sample_index < 0 then sample_index = sample_index + IM_DRAWLIST_ARCFAST_SAMPLE_MAX end local s = self._Data.ArcFastVtx[sample_index + 1] ImVec2_CopyV(self._Path.Data[out_ptr], center.x + s.x * radius, center.y + s.y * radius) out_ptr = out_ptr + 1 a = a - a_step sample_index = sample_index - a_step a_step = a_next_step end end if extra_max_sample then local normalized_max_sample = a_max_sample % IM_DRAWLIST_ARCFAST_SAMPLE_MAX if normalized_max_sample < 0 then normalized_max_sample = normalized_max_sample + IM_DRAWLIST_ARCFAST_SAMPLE_MAX end local s = self._Data.ArcFastVtx[normalized_max_sample + 1] ImVec2_CopyV(self._Path.Data[out_ptr], center.x + s.x * radius, center.y + s.y * radius) out_ptr = out_ptr + 1 end IM_ASSERT_PARANOID(self._Path.Size == out_ptr - 1) end -- 0: East, 3: South, 6: West, 9: North, 12: East @native function MT.ImDrawList:PathArcToFast(center, radius, a_min_of_12, a_max_of_12) if radius < 0.5 then self._Path:push_back(center) return end self:_PathArcToFastEx(center, radius, a_min_of_12 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, a_max_of_12 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 0) end --- @param center ImVec2 --- @param radius float --- @param a_min float --- @param a_max float --- @param num_segments int function MT.ImDrawList:_PathArcToN(center, radius, a_min, a_max, num_segments) if radius < 0.5 then self._Path:push_back(center) return end -- Note that we are adding a point at both a_min and a_max. -- If you are trying to draw a full closed circle you don't want the overlapping points! self._Path:reserve(self._Path.Size + (num_segments + 1)) for i = 0, num_segments do local a = a_min + (i / num_segments) * (a_max - a_min) self._Path:push_back(ImVec2(center.x + ImCos(a) * radius, center.y + ImSin(a) * radius)) end end --- @param center ImVec2 --- @param radius float --- @param a_min float --- @param a_max float --- @param num_segments? int function MT.ImDrawList:PathArcTo(center, radius, a_min, a_max, num_segments) if num_segments == nil then num_segments = 0 end if radius < 0.5 then self._Path:push_back(center) return end if num_segments > 0 then self:_PathArcToN(center, radius, a_min, a_max, num_segments) return end -- Automatic segment count if radius <= self._Data.ArcFastRadiusCutoff then local a_is_reverse = a_max < a_min -- We are going to use precomputed values for mid samples. -- Determine first and last sample in lookup table that belong to the arc. local a_min_sample_f = IM_DRAWLIST_ARCFAST_SAMPLE_MAX * a_min / (IM_PI * 2.0) local a_max_sample_f = IM_DRAWLIST_ARCFAST_SAMPLE_MAX * a_max / (IM_PI * 2.0) local a_min_sample = a_is_reverse and ImFloor(a_min_sample_f) or ImCeil(a_min_sample_f) local a_max_sample = a_is_reverse and ImCeil(a_max_sample_f) or ImFloor(a_max_sample_f) local a_mid_samples = a_is_reverse and ImMax(a_min_sample - a_max_sample, 0) or ImMax(a_max_sample - a_min_sample, 0) local a_min_segment_angle = a_min_sample * IM_PI * 2.0 / IM_DRAWLIST_ARCFAST_SAMPLE_MAX local a_max_segment_angle = a_max_sample * IM_PI * 2.0 / IM_DRAWLIST_ARCFAST_SAMPLE_MAX local a_emit_start = ImAbs(a_min_segment_angle - a_min) >= 1e-5 local a_emit_end = ImAbs(a_max - a_max_segment_angle) >= 1e-5 self._Path:reserve(self._Path.Size + (a_mid_samples + 1 + (a_emit_start and 1 or 0) + (a_emit_end and 1 or 0))) if a_emit_start then self._Path:push_back(ImVec2(center.x + ImCos(a_min) * radius, center.y + ImSin(a_min) * radius)) end if a_mid_samples > 0 then self:_PathArcToFastEx(center, radius, a_min_sample, a_max_sample, 0) end if a_emit_end then self._Path:push_back(ImVec2(center.x + ImCos(a_max) * radius, center.y + ImSin(a_max) * radius)) end else local arc_length = ImAbs(a_max - a_min) local circle_segment_count = self:_CalcCircleAutoSegmentCount(radius) local arc_segment_count = ImMax(ImCeil(circle_segment_count * arc_length / (IM_PI * 2.0)), 1) self:_PathArcToN(center, radius, a_min, a_max, arc_segment_count) end end -- Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. -- DO NOT CALL DIRECTLY THIS WILL CHANGE WILDLY IN 2026. Use ImDrawList:AddText(). --- @param draw_list ImDrawList --- @param size float --- @param pos ImVec2 --- @param col ImU32 --- @param clip_rect ImVec4 --- @param text ImString --- @param text_begin int --- @param text_end int --- @param wrap_width? float --- @param flags? ImDrawTextFlags @native function MT.ImFont:RenderText(draw_list, size, pos, col, clip_rect, text, text_begin, text_end, wrap_width, flags) if wrap_width == nil then wrap_width = .0 end if flags == nil then flags = 0 end -- Align to be pixel perfect local x = pos.x local y = pos.y if y > clip_rect.w then return end if bit32.band(draw_list.Flags, ImDrawListFlags.TextNoPixelSnap) == 0 then x = IM_TRUNC(x) y = IM_TRUNC(y) end -- if not text_end then -- end local line_height = size local baked = self:GetFontBaked(size) local scale = size / baked.Size local origin_x = x local word_wrap_enabled = (wrap_width > 0.0) -- Fast-forward to first visible line local s = text_begin if y + line_height < clip_rect.y then while y + line_height < clip_rect.y and s < text_end do local line_end = ImMemchr(text, '\n', s) if word_wrap_enabled then -- FIXME-OPT: This is not optimal as do first do a search for \n before calling CalcWordWrapPosition(). -- If the specs for CalcWordWrapPosition() were reworked to optionally return on \n we could combine both. -- However it is still better than nothing performing the fast-forward! s = ImFontCalcWordWrapPositionEx(self, size, text, s, (line_end ~= nil) and line_end or text_end, wrap_width, flags) s = ImTextCalcWordWrapNextLineStart(text, s, text_end, flags) else s = (line_end ~= nil) and (line_end + 1) or text_end end y = y + line_height end end -- For large text, scan for the last visible line in order to avoid over-reserving in the call to PrimReserve() -- Note that very large horizontal line will still be affected by the issue (e.g. a one megabyte string buffer without a newline will likely crash atm) if text_end - s > 1e4 and not word_wrap_enabled then local s_end = s local y_end = y while (y_end < clip_rect.w and s_end < text_end) do local _end = ImMemchr(text, '\n', s_end) s_end = (_end ~= nil) and (_end + 1) or text_end y_end = y_end + line_height end text_end = s_end end if s == text_end then return end -- Reserve vertices for remaining worse case (over-reserving is useful and easily amortized) local vtx_count_max = (text_end - s) * 4 local idx_count_max = (text_end - s) * 6 local idx_expected_size = draw_list.IdxBuffer.Size + idx_count_max draw_list:PrimReserve(idx_count_max, vtx_count_max) local vtx_write = draw_list._VtxWritePtr local idx_write = draw_list._IdxWritePtr local vtx_index = draw_list._VtxCurrentIdx local cmd_count = draw_list.CmdBuffer.Size local cpu_fine_clip = bit32.band(flags, ImDrawTextFlags.CpuFineClip) ~= 0 local idx_data = draw_list.IdxBuffer.Data; local vtx_data = draw_list.VtxBuffer.Buf local color_untinted = bit32.bor(col, bit32.bnot(IM_COL32_A_MASK)) local word_wrap_eol local stop = false local text_is_str = type(text) == "string" local lookup, lookup_n, glyphs = baked.IndexLookup.Data, baked.IndexLookup.Size, baked.Glyphs.Data while s < text_end do repeat if word_wrap_enabled then -- Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. if not word_wrap_eol then word_wrap_eol = ImFontCalcWordWrapPositionEx(self, size, text, s, text_end, wrap_width - (x - origin_x), flags) end if s >= word_wrap_eol then x = origin_x y = y + line_height if y > clip_rect.w then stop = true; break -- break out of main loop end word_wrap_eol = nil s = ImTextCalcWordWrapNextLineStart(text, s, text_end, flags) -- Wrapping skips upcoming blanks break end end -- Decode and advance source local c if text_is_str then c = string.byte(text, s) else c = text[s] end if c < 0x80 then s = s + 1 else local wanted, out_char = ImStd.ImTextCharFromUtf8(text, s, text_end) c = out_char s = s + wanted end if c < 32 then if c == 10 then -- '\n' x = origin_x y = y + line_height if y > clip_rect.w then stop = true; break -- break out of main loop end break end if c == 13 then -- '\r' break end end local glyph local gi = (c < lookup_n) and lookup[c + 1] or nil if gi ~= nil and gi ~= IM_FONTGLYPH_INDEX_NOT_FOUND and gi ~= IM_FONTGLYPH_INDEX_UNUSED then glyph = glyphs[gi] else glyph = baked:FindGlyph(c) -- may load the glyph and grow the tables lookup, lookup_n, glyphs = baked.IndexLookup.Data, baked.IndexLookup.Size, baked.Glyphs.Data end local char_width = glyph.AdvanceX * scale if glyph.Visible then -- We don't do a second finer clipping test on the Y axis as we've already skipped anything before clip_rect.y and exit once we pass clip_rect.w local x1 = x + glyph.X0 * scale local x2 = x + glyph.X1 * scale local y1 = y + glyph.Y0 * scale local y2 = y + glyph.Y1 * scale if x1 <= clip_rect.z and x2 >= clip_rect.x then -- Render a character local u1 = glyph.U0 local v1 = glyph.V0 local u2 = glyph.U1 local v2 = glyph.V1 -- CPU side clipping used to fit text in their frame when the frame is too small. Only does clipping for axis aligned quads. if (cpu_fine_clip) then if (x1 < clip_rect.x) then u1 = u1 + (1.0 - (x2 - clip_rect.x) / (x2 - x1)) * (u2 - u1) x1 = clip_rect.x end if (y1 < clip_rect.y) then v1 = v1 + (1.0 - (y2 - clip_rect.y) / (y2 - y1)) * (v2 - v1) y1 = clip_rect.y end if (x2 > clip_rect.z) then u2 = u1 + ((clip_rect.z - x1) / (x2 - x1)) * (u2 - u1) x2 = clip_rect.z end if (y2 > clip_rect.w) then v2 = v1 + ((clip_rect.w - y1) / (y2 - y1)) * (v2 - v1) y2 = clip_rect.w end if (y1 >= y2) then x = x + char_width break end end -- Support for untinted glyphs local glyph_col = glyph.Colored and color_untinted or col -- We are NOT calling PrimRectUV() here because non-inlined causes too much overhead in a debug builds. Inlined here: do local o = (vtx_write - 1) * 36 writef64(vtx_data, o, x1); writef64(vtx_data, o + 8, y1); writef64(vtx_data, o + 16, u1); writef64(vtx_data, o + 24, v1); writeu32(vtx_data, o + 32, glyph_col) writef64(vtx_data, o + 36, x2); writef64(vtx_data, o + 44, y1); writef64(vtx_data, o + 52, u2); writef64(vtx_data, o + 60, v1); writeu32(vtx_data, o + 68, glyph_col) writef64(vtx_data, o + 72, x2); writef64(vtx_data, o + 80, y2); writef64(vtx_data, o + 88, u2); writef64(vtx_data, o + 96, v2); writeu32(vtx_data, o + 104, glyph_col) writef64(vtx_data, o + 108, x1); writef64(vtx_data, o + 116, y2); writef64(vtx_data, o + 124, u1); writef64(vtx_data, o + 132, v2); writeu32(vtx_data, o + 140, glyph_col) idx_data[idx_write + 0] = vtx_index; idx_data[idx_write + 1] = vtx_index + 1; idx_data[idx_write + 2] = vtx_index + 2; idx_data[idx_write + 3] = vtx_index; idx_data[idx_write + 4] = vtx_index + 2; idx_data[idx_write + 5] = vtx_index + 3; vtx_write = vtx_write + 4 vtx_index = vtx_index + 4 idx_write = idx_write + 6 end end end x = x + char_width until true if stop then break end end -- Edge case: calling RenderText() with unloaded glyphs triggering texture change. It doesn't happen via ImGui.* calls because CalcTextSize() is always used. if cmd_count ~= draw_list.CmdBuffer.Size then IM_ASSERT(draw_list.CmdBuffer.Data[draw_list.CmdBuffer.Size].ElemCount == 0) draw_list.CmdBuffer:pop_back() draw_list:PrimUnreserve(idx_count_max, vtx_count_max) draw_list:AddDrawCmd() return self:RenderText(draw_list, size, pos, col, clip_rect, text, text_begin, text_end, wrap_width, flags) -- was `goto begin` end -- Give back unused vertices (clipped ones, blanks) ~ this is essentially a PrimUnreserve() action. draw_list.VtxBuffer.Size = vtx_write - 1 -- Same as calling shrink() draw_list.IdxBuffer.Size = idx_write - 1 draw_list.CmdBuffer.Data[draw_list.CmdBuffer.Size].ElemCount = draw_list.CmdBuffer.Data[draw_list.CmdBuffer.Size].ElemCount - (idx_expected_size - draw_list.IdxBuffer.Size) draw_list._VtxWritePtr = vtx_write draw_list._IdxWritePtr = idx_write draw_list._VtxCurrentIdx = vtx_index end ------------------------------------------------------------------------------- -- [SECTION] ImGui Internal Render Helpers ------------------------------------------------------------------------------- -- Vaguely redesigned to stop accessing ImGui global state: -- - RenderArrow() -- - RenderBullet() -- - RenderCheckMark() -- - RenderArrowPointingAt() -- - RenderRectFilledInRangeH() -- - RenderRectFilledWithHole() ------------------------------------------------------------------------------- -- Function in need of a redesign (legacy mess) -- - RenderColorRectWithAlphaCheckerboard() ------------------------------------------------------------------------------- -- Render an arrow aimed to be aligned with text (p_min is a position in the same space text would be positioned). To e.g. denote expanded/collapsed state --- @param draw_list ImDrawList --- @param pos ImVec2 --- @param color ImU32 --- @param dir ImGuiDir --- @param scale? float local arrow_a, arrow_b, arrow_c = ImVec2(), ImVec2(), ImVec2() -- scratch: the path is consumed by AddTriangleFilled before returning function ImGui.RenderArrow(draw_list, pos, color, dir, scale) if scale == nil then scale = 1.0 end local h = draw_list._Data.FontSize * 1.00 local r = h * 0.40 * scale local cx, cy = pos.x + h * 0.50, pos.y + h * 0.50 * scale local ax, ay, bx, by, qx, qy if dir == ImGuiDir.Up or dir == ImGuiDir.Down then if dir == ImGuiDir.Up then r = -r end ax, ay = 0.000 * r, 0.750 * r bx, by = -0.866 * r, -0.750 * r qx, qy = 0.866 * r, -0.750 * r elseif dir == ImGuiDir.Left or dir == ImGuiDir.Right then if dir == ImGuiDir.Left then r = -r end ax, ay = 0.750 * r, 0.000 * r bx, by = -0.750 * r, 0.866 * r qx, qy = -0.750 * r, -0.866 * r else IM_ASSERT(false) end arrow_a.x, arrow_a.y = cx + ax, cy + ay arrow_b.x, arrow_b.y = cx + bx, cy + by arrow_c.x, arrow_c.y = cx + qx, cy + qy draw_list:AddTriangleFilled(arrow_a, arrow_b, arrow_c, color) end --- @param draw_list ImDrawList --- @param pos ImVec2 --- @param col ImU32 function ImGui.RenderBullet(draw_list, pos, col) -- FIXME-OPT: This should be baked in font now that it's easier. local font_size = draw_list._Data.FontSize draw_list:AddCircleFilled(pos, font_size * 0.20, col, (font_size < 22) and 8 or ((font_size < 40) and 12 or 0)) -- Hardcode optimal/nice tessellation threshold end --- @param draw_list ImDrawList --- @param pos ImVec2 --- @param col ImU32 --- @param sz float function ImGui.RenderCheckMark(draw_list, pos, col, sz) local thickness = ImMax(sz / 5.0, 1.0) sz = sz - thickness * 0.5 pos = pos + ImVec2(thickness * 0.25, thickness * 0.25) local third = sz / 3.0 local bx = pos.x + third local by = pos.y + sz - third * 0.5 draw_list:PathLineTo(ImVec2(bx - third, by - third)) draw_list:PathLineTo(ImVec2(bx, by)) draw_list:PathLineTo(ImVec2(bx + third * 2.0, by - third * 2.0)) draw_list:PathStroke(col, thickness) end -- Render an arrow. 'pos' is position of the arrow tip. half_sz.x is length from base to tip. half_sz.y is length on each side. --- @param draw_list ImDrawList --- @param pos ImVec2 --- @param half_sz ImVec2 --- @param direction ImGuiDir --- @param col ImU32 function ImGui.RenderArrowPointingAt(draw_list, pos, half_sz, direction, col) if direction == ImGuiDir.Left then draw_list:AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), pos, col) elseif direction == ImGuiDir.Right then draw_list:AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), pos, col) elseif direction == ImGuiDir.Up then draw_list:AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), pos, col) elseif direction == ImGuiDir.Down then draw_list:AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), pos, col) elseif direction == ImGuiDir.None or direction == ImGuiDir.COUNT then -- Fix warnings end end local function ImAcos01(x) if x <= 0.0 then return IM_PI * 0.5 end if x >= 1.0 then return 0.0 end return ImAcos(x) --return (-0.69813170079773212 * x * x - 0.87266462599716477) * x + 1.5707963267948966 -- Cheap approximation, may be enough for what we do. end -- FIXME: Cleanup and move code to ImDrawList. -- - Before 2025-12-04: RenderRectFilledRangeH() with 'float x_start_norm, float x_end_norm` <- normalized -- - After 2025-12-04: RenderRectFilledInRangeH() with 'float x1, float x2' <- absolute coords!! --- @param draw_list ImDrawList --- @param rect ImRect --- @param col ImU32 --- @param fill_x0 float --- @param fill_x1 float --- @param rounding float function ImGui.RenderRectFilledInRangeH(draw_list, rect, col, fill_x0, fill_x1, rounding) if fill_x0 > fill_x1 then return end local p0 = ImVec2(fill_x0, rect.Min.y) local p1 = ImVec2(fill_x1, rect.Max.y) if rounding == 0.0 then draw_list:AddRectFilled(p0, p1, col, 0.0) return end rounding = ImClamp(ImMin((rect.Max.x - rect.Min.x) * 0.5, (rect.Max.y - rect.Min.y) * 0.5) - 1.0, 0.0, rounding) local inv_rounding = 1.0 / rounding local arc0_b = ImAcos01(1.0 - (p0.x - rect.Min.x) * inv_rounding) local arc0_e = ImAcos01(1.0 - (p1.x - rect.Min.x) * inv_rounding) local half_pi = IM_PI * 0.5 -- We will == compare to this because we know this is the exact value ImAcos01 can return. local x0 = ImMax(p0.x, rect.Min.x + rounding) if arc0_b == arc0_e then draw_list:PathLineTo(ImVec2(x0, p1.y)) draw_list:PathLineTo(ImVec2(x0, p0.y)) elseif arc0_b == 0.0 and arc0_e == half_pi then draw_list:PathArcToFast(ImVec2(x0, p1.y - rounding), rounding, 3, 6) -- BL draw_list:PathArcToFast(ImVec2(x0, p0.y + rounding), rounding, 6, 9) -- TR else draw_list:PathArcTo(ImVec2(x0, p1.y - rounding), rounding, math.pi - arc0_e, math.pi - arc0_b) -- BL draw_list:PathArcTo(ImVec2(x0, p0.y + rounding), rounding, math.pi + arc0_b, math.pi + arc0_e) -- TR end if p1.x > rect.Min.x + rounding then local arc1_b = ImAcos01(1.0 - (rect.Max.x - p1.x) * inv_rounding) local arc1_e = ImAcos01(1.0 - (rect.Max.x - p0.x) * inv_rounding) local x1 = ImMin(p1.x, rect.Max.x - rounding) if arc1_b == arc1_e then draw_list:PathLineTo(ImVec2(x1, p0.y)) draw_list:PathLineTo(ImVec2(x1, p1.y)) elseif arc1_b == 0.0 and arc1_e == half_pi then draw_list:PathArcToFast(ImVec2(x1, p0.y + rounding), rounding, 9, 12) -- TR draw_list:PathArcToFast(ImVec2(x1, p1.y - rounding), rounding, 0, 3) -- BR else draw_list:PathArcTo(ImVec2(x1, p0.y + rounding), rounding, -arc1_e, -arc1_b) -- TR draw_list:PathArcTo(ImVec2(x1, p1.y - rounding), rounding, arc1_b, arc1_e) -- BR end end draw_list:PathFillConvex(col) end --- @param r_in ImRect --- @param r_outer ImRect --- @param threshold float --- @return ImDrawFlags function ImGui.CalcRoundingFlagsForRectInRect(r_in, r_outer, threshold) local round_l = r_in.Min.x <= r_outer.Min.x + threshold local round_r = r_in.Max.x >= r_outer.Max.x - threshold local round_t = r_in.Min.y <= r_outer.Min.y + threshold local round_b = r_in.Max.y >= r_outer.Max.y - threshold local flags = ImDrawFlags.RoundCornersNone if round_t and round_l then flags = bit32.bor(flags, ImDrawFlags.RoundCornersTopLeft) end if round_t and round_r then flags = bit32.bor(flags, ImDrawFlags.RoundCornersTopRight) end if round_b and round_l then flags = bit32.bor(flags, ImDrawFlags.RoundCornersBottomLeft) end if round_b and round_r then flags = bit32.bor(flags, ImDrawFlags.RoundCornersBottomRight) end return flags end -- Helper for ColorPicker4() -- NB: This is rather brittle and will show artifact when rounding this enabled if rounded corners overlap multiple cells. Caller currently responsible for avoiding that. -- Spent a non reasonable amount of time trying to getting this right for ColorButton with rounding+anti-aliasing+ImGuiColorEditFlags_HalfAlphaPreview flag + various grid sizes and offsets, and eventually gave up... probably more reasonable to disable rounding altogether. -- FIXME: uses ImGui.GetColorU32 --- @param draw_list ImDrawList --- @param p_min ImVec2 --- @param p_max ImVec2 --- @param col ImU32 --- @param grid_step float --- @param grid_off ImVec2 --- @param rounding? float --- @param flags? ImDrawFlags function ImGui.RenderColorRectWithAlphaCheckerboard(draw_list, p_min, p_max, col, grid_step, grid_off, rounding, flags) if rounding == nil then rounding = 0.0 end if flags == nil then flags = 0 end if bit32.band(flags, ImDrawFlags.RoundCornersMask_) == 0 then flags = ImDrawFlags.RoundCornersDefault_ end if bit32.rshift(bit32.band(col, IM_COL32_A_MASK), IM_COL32_A_SHIFT) < 0xFF then local col_bg1 = ImGui.GetColorU32(ImStd.ImAlphaBlendColors(IM_COL32(128, 128, 128, 255), col), nil, true) local col_bg2 = ImGui.GetColorU32(ImStd.ImAlphaBlendColors(IM_COL32(204, 204, 204, 255), col), nil, true) draw_list:AddRectFilled(p_min, p_max, col_bg1, rounding, flags) local yi = 0 local y = p_min.y + grid_off.y local x_start local x local cell_flags while y < p_max.y do repeat local y1 = ImClamp(y, p_min.y, p_max.y) local y2 = ImMin(y + grid_step, p_max.y) if y2 <= y1 then break end x_start = p_min.x + grid_off.x + ((yi + 1) % 2) * grid_step x = x_start while x < p_max.x do repeat local x1 = ImClamp(x, p_min.x, p_max.x) local x2 = ImMin(x + grid_step, p_max.x) if x2 <= x1 then break end cell_flags = ImDrawFlags.RoundCornersNone -- FIXME: Could use CalcRoundingFlagsForRectInRect() if y1 <= p_min.y then if x1 <= p_min.x then cell_flags = bit32.bor(cell_flags, ImDrawFlags.RoundCornersTopLeft) end if x2 >= p_max.x then cell_flags = bit32.bor(cell_flags, ImDrawFlags.RoundCornersTopRight) end end if y2 >= p_max.y then if x1 <= p_min.x then cell_flags = bit32.bor(cell_flags, ImDrawFlags.RoundCornersBottomLeft) end if x2 >= p_max.x then cell_flags = bit32.bor(cell_flags, ImDrawFlags.RoundCornersBottomRight) end end -- Combine flags if flags == ImDrawFlags.RoundCornersNone or cell_flags == ImDrawFlags.RoundCornersNone then cell_flags = ImDrawFlags.RoundCornersNone else cell_flags = bit32.band(cell_flags, flags) end draw_list:AddRectFilled(ImVec2(x1, y1), ImVec2(x2, y2), col_bg2, rounding, cell_flags) until true x = x + grid_step * 2.0 end until true y = y + grid_step yi = yi + 1 end else draw_list:AddRectFilled(p_min, p_max, col, rounding, flags) end end function ImGui.RenderRectFilledWithHole(draw_list, outer, inner, col, rounding) local fill_L = inner.Min.x > outer.Min.x local fill_R = inner.Max.x < outer.Max.x local fill_U = inner.Min.y > outer.Min.y local fill_D = inner.Max.y < outer.Max.y local D = ImDrawFlags local bor = bit32.bor local function f(c, v) return c and 0 or v end if fill_L then draw_list:AddRectFilled(ImVec2(outer.Min.x, inner.Min.y), ImVec2(inner.Min.x, inner.Max.y), col, rounding, bor(D.RoundCornersNone, f(fill_U, D.RoundCornersTopLeft), f(fill_D, D.RoundCornersBottomLeft))) end if fill_R then draw_list:AddRectFilled(ImVec2(inner.Max.x, inner.Min.y), ImVec2(outer.Max.x, inner.Max.y), col, rounding, bor(D.RoundCornersNone, f(fill_U, D.RoundCornersTopRight), f(fill_D, D.RoundCornersBottomRight))) end if fill_U then draw_list:AddRectFilled(ImVec2(inner.Min.x, outer.Min.y), ImVec2(inner.Max.x, inner.Min.y), col, rounding, bor(D.RoundCornersNone, f(fill_L, D.RoundCornersTopLeft), f(fill_R, D.RoundCornersTopRight))) end if fill_D then draw_list:AddRectFilled(ImVec2(inner.Min.x, inner.Max.y), ImVec2(inner.Max.x, outer.Max.y), col, rounding, bor(D.RoundCornersNone, f(fill_L, D.RoundCornersBottomLeft), f(fill_R, D.RoundCornersBottomRight))) end if fill_L and fill_U then draw_list:AddRectFilled(ImVec2(outer.Min.x, outer.Min.y), ImVec2(inner.Min.x, inner.Min.y), col, rounding, D.RoundCornersTopLeft) end if fill_R and fill_U then draw_list:AddRectFilled(ImVec2(inner.Max.x, outer.Min.y), ImVec2(outer.Max.x, inner.Min.y), col, rounding, D.RoundCornersTopRight) end if fill_L and fill_D then draw_list:AddRectFilled(ImVec2(outer.Min.x, inner.Max.y), ImVec2(inner.Min.x, outer.Max.y), col, rounding, D.RoundCornersBottomLeft) end if fill_R and fill_D then draw_list:AddRectFilled(ImVec2(inner.Max.x, inner.Max.y), ImVec2(outer.Max.x, outer.Max.y), col, rounding, D.RoundCornersBottomRight) end end ------------------------------------------------------------------------------- -- Extra primitives (Ngon, Ellipse, Bezier, concave fill) ------------------------------------------------------------------------------- -- [port] These follow the 1.92 stroker this port uses (no ImDrawFlags_StrokeXXX placement flags). function MT.ImDrawList:PathEllipticalArcTo(center, radius, rot, a_min, a_max, num_segments) if num_segments == nil or num_segments <= 0 then num_segments = self:_CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)) -- A bit pessimistic, maybe there's a better computation to do here. end local cos_rot, sin_rot = ImCos(rot), ImSin(rot) for i = 0, num_segments do local a = a_min + (i / num_segments) * (a_max - a_min) local px, py = ImCos(a) * radius.x, ImSin(a) * radius.y self._Path:push_back(ImVec2(px * cos_rot - py * sin_rot + center.x, px * sin_rot + py * cos_rot + center.y)) end end function ImBezierCubicCalc(p1, p2, p3, p4, t) local u = 1.0 - t local w1, w2, w3, w4 = u * u * u, 3 * u * u * t, 3 * u * t * t, t * t * t return ImVec2(w1 * p1.x + w2 * p2.x + w3 * p3.x + w4 * p4.x, w1 * p1.y + w2 * p2.y + w3 * p3.y + w4 * p4.y) end function ImBezierQuadraticCalc(p1, p2, p3, t) local u = 1.0 - t local w1, w2, w3 = u * u, 2 * u * t, t * t return ImVec2(w1 * p1.x + w2 * p2.x + w3 * p3.x, w1 * p1.y + w2 * p2.y + w3 * p3.y) end -- Closely mimics ImBezierCubicClosestPointCasteljau() in imgui.cpp local function PathBezierCubicCurveToCasteljau(path, x1, y1, x2, y2, x3, y3, x4, y4, tess_tol, level) local dx, dy = x4 - x1, y4 - y1 local d2 = math.abs((x2 - x4) * dy - (y2 - y4) * dx) local d3 = math.abs((x3 - x4) * dy - (y3 - y4) * dx) if (d2 + d3) * (d2 + d3) < tess_tol * (dx * dx + dy * dy) then path:push_back(ImVec2(x4, y4)) elseif level < 10 then local x12, y12 = (x1 + x2) * 0.5, (y1 + y2) * 0.5 local x23, y23 = (x2 + x3) * 0.5, (y2 + y3) * 0.5 local x34, y34 = (x3 + x4) * 0.5, (y3 + y4) * 0.5 local x123, y123 = (x12 + x23) * 0.5, (y12 + y23) * 0.5 local x234, y234 = (x23 + x34) * 0.5, (y23 + y34) * 0.5 local x1234, y1234 = (x123 + x234) * 0.5, (y123 + y234) * 0.5 PathBezierCubicCurveToCasteljau(path, x1, y1, x12, y12, x123, y123, x1234, y1234, tess_tol, level + 1) PathBezierCubicCurveToCasteljau(path, x1234, y1234, x234, y234, x34, y34, x4, y4, tess_tol, level + 1) end end local function PathBezierQuadraticCurveToCasteljau(path, x1, y1, x2, y2, x3, y3, tess_tol, level) local dx, dy = x3 - x1, y3 - y1 local det = (x2 - x3) * dy - (y2 - y3) * dx if det * det * 4.0 < tess_tol * (dx * dx + dy * dy) then path:push_back(ImVec2(x3, y3)) elseif level < 10 then local x12, y12 = (x1 + x2) * 0.5, (y1 + y2) * 0.5 local x23, y23 = (x2 + x3) * 0.5, (y2 + y3) * 0.5 local x123, y123 = (x12 + x23) * 0.5, (y12 + y23) * 0.5 PathBezierQuadraticCurveToCasteljau(path, x1, y1, x12, y12, x123, y123, tess_tol, level + 1) PathBezierQuadraticCurveToCasteljau(path, x123, y123, x23, y23, x3, y3, tess_tol, level + 1) end end function MT.ImDrawList:PathBezierCubicCurveTo(p2, p3, p4, num_segments) local p1 = self._Path:back() if num_segments == nil or num_segments == 0 then PathBezierCubicCurveToCasteljau(self._Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, self._Data.CurveTessellationTol, 0) -- Auto-tessellated else local t_step = 1.0 / num_segments for i_step = 1, num_segments do self._Path:push_back(ImBezierCubicCalc(p1, p2, p3, p4, t_step * i_step)) end end end function MT.ImDrawList:PathBezierQuadraticCurveTo(p2, p3, num_segments) local p1 = self._Path:back() if num_segments == nil or num_segments == 0 then PathBezierQuadraticCurveToCasteljau(self._Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, self._Data.CurveTessellationTol, 0) -- Auto-tessellated else local t_step = 1.0 / num_segments for i_step = 1, num_segments do self._Path:push_back(ImBezierQuadraticCalc(p1, p2, p3, t_step * i_step)) end end end -- Guaranteed to honor 'num_segments' function MT.ImDrawList:AddNgon(center, radius, col, num_segments, thickness) if thickness == nil then thickness = 1.0 end if bit32.band(col, IM_COL32_A_MASK) == 0 or num_segments <= 2 then return end -- Because we are filling a closed shape we remove 1 from the count of segments/points local a_max = (IM_PI * 2.0) * (num_segments - 1.0) / num_segments self:PathArcTo(center, radius - 0.5, 0.0, a_max, num_segments - 1) self:PathStroke(col, thickness, ImDrawFlags.Closed) end -- Guaranteed to honor 'num_segments' function MT.ImDrawList:AddNgonFilled(center, radius, col, num_segments) if bit32.band(col, IM_COL32_A_MASK) == 0 or num_segments <= 2 then return end -- Because we are filling a closed shape we remove 1 from the count of segments/points local a_max = (IM_PI * 2.0) * (num_segments - 1.0) / num_segments self:PathArcTo(center, radius, 0.0, a_max, num_segments - 1) self:PathFillConvex(col) end -- Ellipse function MT.ImDrawList:AddEllipse(center, radius, col, rot, num_segments, thickness) if rot == nil then rot = 0.0 end if thickness == nil then thickness = 1.0 end if bit32.band(col, IM_COL32_A_MASK) == 0 then return end if num_segments == nil or num_segments <= 0 then num_segments = self:_CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)) end -- A bit pessimistic, maybe there's a better computation to do here. -- Because we are filling a closed shape we remove 1 from the count of segments/points local a_max = IM_PI * 2.0 * (num_segments - 1.0) / num_segments self:PathEllipticalArcTo(center, radius, rot, 0.0, a_max, num_segments - 1) self:PathStroke(col, thickness, ImDrawFlags.Closed) end function MT.ImDrawList:AddEllipseFilled(center, radius, col, rot, num_segments) if rot == nil then rot = 0.0 end if bit32.band(col, IM_COL32_A_MASK) == 0 then return end if num_segments == nil or num_segments <= 0 then num_segments = self:_CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)) end -- A bit pessimistic, maybe there's a better computation to do here. -- Because we are filling a closed shape we remove 1 from the count of segments/points local a_max = IM_PI * 2.0 * (num_segments - 1.0) / num_segments self:PathEllipticalArcTo(center, radius, rot, 0.0, a_max, num_segments - 1) self:PathFillConvex(col) end -- Cubic Bezier takes 4 controls points function MT.ImDrawList:AddBezierCubic(p1, p2, p3, p4, col, thickness, num_segments) if bit32.band(col, IM_COL32_A_MASK) == 0 then return end self:PathLineTo(p1) self:PathBezierCubicCurveTo(p2, p3, p4, num_segments or 0) self:PathStroke(col, thickness or 1.0) end -- Quadratic Bezier takes 3 controls points function MT.ImDrawList:AddBezierQuadratic(p1, p2, p3, col, thickness, num_segments) if bit32.band(col, IM_COL32_A_MASK) == 0 then return end self:PathLineTo(p1) self:PathBezierQuadraticCurveTo(p2, p3, num_segments or 0) self:PathStroke(col, thickness or 1.0) end -- ponytail: ear-clipping, no anti-aliased fringe (upstream's AA triangulator not ported). O(n^2) is fine for UI polygons. --- @param points ImVec2[] # 1-based function MT.ImDrawList:AddConcavePolyFilled(points, points_count, col) if points_count < 3 or bit32.band(col, IM_COL32_A_MASK) == 0 then return end local idx = {} local area = 0.0 for i = 1, points_count do idx[i] = i local a, b = points[i], points[i % points_count + 1] area = area + (a.x * b.y - b.x * a.y) end local sign = (area >= 0) and 1 or -1 local function cross(o, a, b) return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x) end local guard = 0 while #idx > 3 and guard < points_count * points_count do guard = guard + 1 local n = #idx local clipped = false for i = 1, n do local ia, ib, ic = idx[(i - 2) % n + 1], idx[i], idx[i % n + 1] local a, b, c = points[ia], points[ib], points[ic] if cross(a, b, c) * sign > 0 then local inside = false for j = 1, n do local p = idx[j] if p ~= ia and p ~= ib and p ~= ic then local q = points[p] if cross(a, b, q) * sign >= 0 and cross(b, c, q) * sign >= 0 and cross(c, a, q) * sign >= 0 then inside = true; break end end end if not inside then self:AddTriangleFilled(a, b, c, col) table.remove(idx, i) clipped = true break end end end if not clipped then break end end if #idx == 3 then self:AddTriangleFilled(points[idx[1]], points[idx[2]], points[idx[3]], col) end end function MT.ImDrawList:PathFillConcave(col) self:AddConcavePolyFilled(self._Path.Data, self._Path.Size, col) self._Path.Size = 0 end return true -- [Roblox] ModuleScripts must return exactly one value