Files
imgui-luau/imgui_draw.lua
T

7141 lines
311 KiB
Lua

--- 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 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)
ImFontAtlasBuildUpdateTexDataBasic(atlas)
ImFontAtlasBuildUpdateTexDataLines(atlas)
-- [Port] TODO baked round corners: ImFontAtlasBuildUpdateTexDataCorners(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 + IM_DRAWLIST_TEX_LINES_DETAILED_WIDTH_COUNT) -- ImVec2i
builder.PackIdLinesTexData = atlas:AddCustomRect(pack_size.x, pack_size.y, r)
IM_ASSERT(builder.PackIdLinesTexData ~= ImFontAtlasRectId_Invalid)
end
do
-- FIXME: Lines 0 to IM_DRAWLIST_TEX_LINES_DETAILED_WIDTH(4) are currently never used and could be ditched, saving DETAILED_WIDTH*WIDTH_MAX
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
local pad_left = math.floor((r.w - line_width) / 2)
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_x, uv0_y = (r.x + pad_left - 0.5) * atlas.TexUvScale.x, (r.y + y) * atlas.TexUvScale.y
local uv1_x, uv1_y = (r.x + pad_left + line_width + 0.5) * atlas.TexUvScale.x, (r.y + y + 1) * atlas.TexUvScale.y
local half_v = (uv0_y + uv1_y) * 0.5 -- Calculate a constant V in the middle of the row to avoid sampling artifacts
-- Store inverse of the line-width (thickness) to the w component so that we can avoid division when selecting texture.
local inv_line_width = (line_width > 0) and (1.0 / line_width) or 0.0
builder.TexUvLines[n] = ImVec4(uv0_x, uv1_x, half_v, inv_line_width)
end
end
do
-- Calculate super sampled line textures for smaller line widths to make transition between sizes smoother.
-- Calculate ramp used for all the textures.
local ramp = {} -- ImU8[IM_DRAWLIST_TEX_LINES_SAMPLE_COUNT], 0-based
for n = 0, IM_DRAWLIST_TEX_LINES_SAMPLE_COUNT - 1 do
ramp[n] = math.floor((n / IM_DRAWLIST_TEX_LINES_SAMPLE_COUNT) * 255.0)
end
for n = 0, IM_DRAWLIST_TEX_LINES_DETAILED_WIDTH_COUNT - 1 do
local y = IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1 + n
-- For integer thickness lines the one pixel line texture looks like this:
-- [ 0 | 1 | 0 ]
-- :...........:
-- Since the texture samples are at the center of the texel, we only use range above.
-- The result after bilinear filtering is a triangle across the used uv range.
--
-- To super 2x sample that signal, we end up with following texture:
-- [ 0 | .5 | 1 | .5 | 0 ]
-- :.......................:
-- One might think that there should be 2 opaque pixels in the middle section, but no since we're super sampling the triangle signal.
local line_width = 1 + n
IM_ASSERT(IM_DRAWLIST_TEX_LINES_SAMPLE_COUNT + line_width + IM_DRAWLIST_TEX_LINES_SAMPLE_COUNT <= 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)
local idx = 0
for i = 0, IM_DRAWLIST_TEX_LINES_SAMPLE_COUNT - 1 do
write_ptr[write_ptr_base + idx] = ramp[i]; idx = idx + 1
end
for _ = 0, line_width - 1 do
write_ptr[write_ptr_base + idx] = 0xFF; idx = idx + 1
end
for i = IM_DRAWLIST_TEX_LINES_SAMPLE_COUNT - 1, 0, -1 do
write_ptr[write_ptr_base + idx] = ramp[i]; idx = idx + 1
end
while idx < r.w do
write_ptr[write_ptr_base + idx] = 0x0; idx = idx + 1
end
elseif add_and_draw and tex.Format == ImTextureFormat.RGBA32 then
local write_ptr, write_ptr_base = tex:GetPixelsAt(r.x, r.y + y)
local idx = 0
for i = 0, IM_DRAWLIST_TEX_LINES_SAMPLE_COUNT - 1 do
write_u32(write_ptr, write_ptr_base + idx * 4, IM_COL32(255, 255, 255, ramp[i])); idx = idx + 1
end
for _ = 0, line_width - 1 do
write_u32(write_ptr, write_ptr_base + idx * 4, IM_COL32(255, 255, 255, 255)); idx = idx + 1
end
for i = IM_DRAWLIST_TEX_LINES_SAMPLE_COUNT - 1, 0, -1 do
write_u32(write_ptr, write_ptr_base + idx * 4, IM_COL32(255, 255, 255, ramp[i])); idx = idx + 1
end
while idx < r.w do
write_u32(write_ptr, write_ptr_base + idx * 4, IM_COL32(255, 255, 255, 0)); idx = idx + 1
end
end
-- Refresh UV coordinates
local uv0_x, uv0_y = (r.x + 0.5) * atlas.TexUvScale.x, (r.y + y) * atlas.TexUvScale.y
local uv1_x, uv1_y = (r.x - 0.5 + IM_DRAWLIST_TEX_LINES_SAMPLE_COUNT * 2 + line_width) * atlas.TexUvScale.x, (r.y + y + 1) * atlas.TexUvScale.y
local half_v = (uv0_y + uv1_y) * 0.5 -- Calculate a constant V in the middle of the row to avoid sampling artifacts
-- Store inverse of the line-width (thickness) to the w component so that we can avoid division when selecting texture.
local inv_line_width = 1.0 / (1.0 + n / IM_DRAWLIST_TEX_LINES_SAMPLE_COUNT)
builder.TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1 + n] = ImVec4(uv0_x, uv1_x, half_v, inv_line_width)
end
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.Builder.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 = math.floor(size + 0.5) -- [Luau] inline ImGui.GetRoundedFontSize() == IM_ROUND()
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
local ImFontCalcWordWrapPositionEx_Uncached
local WRAP_MEMO_MAX = 4096
function ImFontCalcWordWrapPositionEx(font, size, text, pos, text_end, wrap_width, flags)
local baked = font:GetFontBaked(size)
if type(text) ~= "string" then
return ImFontCalcWordWrapPositionEx_Uncached(baked, size, text, pos, text_end, wrap_width, flags)
end
-- [Luau] memo per baked font and string: CalcTextSize() and RenderText() of wrapped text ask for the same wrap points
-- (same text/pos/width) every frame. The result only depends on these inputs and the (immutable once loaded) glyph advances.
-- ponytail: whole-memo reset at WRAP_MEMO_MAX strings, LRU if dynamic text churns it
local memo = baked._WrapMemo
if memo == nil or baked._WrapMemoN >= WRAP_MEMO_MAX then
memo = {}; baked._WrapMemo = memo; baked._WrapMemoN = 0
end
local e = memo[text]
if e == nil or e.ww ~= wrap_width or e.size ~= size or e.te ~= text_end or e.fl ~= flags then
if e == nil then baked._WrapMemoN = baked._WrapMemoN + 1 end
e = { ww = wrap_width, size = size, te = text_end, fl = flags }
memo[text] = e
else
local r = e[pos]
if r then return r end
end
local r = ImFontCalcWordWrapPositionEx_Uncached(baked, size, text, pos, text_end, wrap_width, flags)
e[pos] = r
return r
end
@native
function ImFontCalcWordWrapPositionEx_Uncached(baked, 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"
-- [Luau] baked = font:GetFontBaked(size) is done by the caller
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)
local adv_data, adv_n = baked.IndexAdvanceX.Data, baked.IndexAdvanceX.Size -- [Luau] hoisted, refreshed after a glyph load
local CLASS_BLANK, CLASS_PUNCT, CLASS_OTHER = ImWcharClass.Blank, ImWcharClass.Punct, ImWcharClass.Other
local classifier_0000_007f = g_CharClassifierIsSeparator_0000_007f
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 < adv_n) and adv_data[c + 1] or -1.0
if char_width < 0.0 then
char_width = BuildLoadGlyphGetAdvanceOrFallback(baked, c)
adv_data, adv_n = baked.IndexAdvanceX.Data, baked.IndexAdvanceX.Size
end
-- Classify current character
local curr_type
if c < 128 then -- [Luau] ImTextClassifierGet() inlined
curr_type = bit32.band(bit32.rshift(classifier_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 = CLASS_OTHER
end
if curr_type == CLASS_BLANK then
-- End span: 'A ' or '. '
if (prev_type ~= CLASS_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 == CLASS_PUNCT and curr_type ~= CLASS_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 == CLASS_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
local text_len1 = text_is_str and #text + 1 or -1
local cacheable = text_is_str and text_begin == 1 and out_offset == nil and max_width >= FLT_MAX
and (text_end == nil or text_end == text_len1) and (text_end_display == nil or text_end_display == text_len1) and (flags == nil or bit32.band(flags, ImDrawTextFlags.StopOnNewLine) == 0)
if cacheable and wrap_width <= 0.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_len1
end
cache_key = text
elseif cacheable and wrap_width > 0.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
local adv_data, adv_n = baked.IndexAdvanceX.Data, baked.IndexAdvanceX.Size -- [Luau] hoisted, refreshed after a glyph load
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)
adv_data, adv_n = baked.IndexAdvanceX.Data, baked.IndexAdvanceX.Size -- [Luau] may have loaded glyphs
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 < adv_n) and adv_data[c + 1] or -1.0
if (char_width < 0.0) then
char_width = BuildLoadGlyphGetAdvanceOrFallback(baked, c)
adv_data, adv_n = baked.IndexAdvanceX.Data, baked.IndexAdvanceX.Size
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
local RenderChar_a, RenderChar_c, RenderChar_uv_a, RenderChar_uv_c = ImVec2(), ImVec2(), ImVec2(), ImVec2()
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, ImDrawFlags.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)
-- [Luau] scratch vectors, PrimRectUV() only reads them
local a, c, uv_a, uv_c = RenderChar_a, RenderChar_c, RenderChar_uv_a, RenderChar_uv_c
a.x = x1; a.y = y1; c.x = x2; c.y = y2; uv_a.x = u1; uv_a.y = v1; uv_c.x = u2; uv_c.y = v2
draw_list:PrimRectUV(a, c, uv_a, uv_c, 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
-- - The miter limit describes how much the miter cross section can be scaled before we turn it into a bevel instead.
-- The minimum value of miter limit is 1.0, which translates to 180 (all corners are bevels), the higher the number,
-- the closer to zero the angle is (never bevel a corner). The miter limit angle can be calculated as:
-- miter_limit_angle = 2 * acos(1 / miter_limit)
-- - We can use the miter limit angle to figure maximum number of beveled corners in a convex polygon,
-- to prevent having allocate much extra memory (for a reasonable miter limit).
-- (n - 2) * 360 > k * miter_limit_angle + (n - k) * 360
-- Where, n = number of corners in a polygon, k = beveled corners. Solved for k, simplifies to:
-- k < 360 / (180 - miter_limit_angle)
-- To find value strictly less than:
-- max_bevels = k = floor(360/(180-miter_limit_angle))
local IM_POLYLINE_MITER_ANGLE_LIMIT = -0.9999619 -- Safeguard for miter corner calculation to avoid div by zero. cos(179.5)
local IM_POLYLINE_MITER_LIMIT = 4.0 -- (~29 deg corner)
local IM_POLYLINE_CONVEX_POLY_MAX_BEVELS = 2 -- For miter value 4.0
--- @param col ImU32
--- @param alpha_mul float
local function ImAlphaMultiply(col, alpha_mul)
IM_ASSERT_PARANOID(alpha_mul >= 0.0 and alpha_mul < 1.0) -- We don't clamp!
local a = bit32.rshift(bit32.band(col, IM_COL32_A_MASK), IM_COL32_A_SHIFT)
a = math.floor(a * alpha_mul) -- We don't need to clamp 0..255 because alpha is in 0..1 range.
return bit32.bor(bit32.band(col, bit32.bnot(IM_COL32_A_MASK)), bit32.lshift(a, IM_COL32_A_SHIFT))
end
-- #define _GetStrokePos(_FLAGS, _DEFAULT_STROKE_POS)
local function _GetStrokePos(flags, default_stroke_pos)
local stroke_pos = bit32.band(flags, ImDrawFlags.StrokeMask_)
if stroke_pos ~= 0 then return stroke_pos end
return default_stroke_pos
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, ImDrawFlags.UseVtxOffset) == 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
-- Calculates arc step and step count for 90 degree arc. The calculated arc step will complete a 90 degree arc when stepped arc_step_count times.
-- [Luau] returns arc_step, arc_step_count (C++ out-references)
local function CalcCornerArcStepAndCount(segment_count)
local quarter = IM_DRAWLIST_ARCFAST_TABLE_SIZE / 4
local arc_step = ImClamp(math.floor(IM_DRAWLIST_ARCFAST_SAMPLE_MAX / segment_count), 1, quarter)
local arc_step_count = math.floor(quarter / arc_step)
if (arc_step * arc_step_count) ~= quarter then -- Make sure that the arc step can iterate 90 degree arc.
arc_step = math.floor(quarter / arc_step_count)
end
return arc_step, arc_step_count
end
local function ApproxApothem(num_segments)
-- Calculates approximation (within 0.001%) distance from the center of the circumscribed circle to the edge of the polygon, cos(pi / n)
IM_ASSERT_PARANOID(num_segments >= 3)
local u = 1.0 / num_segments
return ((4.058712 * u * u) - 4.934802) * u * u + 1.0
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.InitialDrawFlags
IM_ASSERT(bit32.band(self.Flags, bit32.bnot(ImDrawFlags.AllowInFrameScope_)) == 0)
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._DrawFlagsStack.Size = 0 -- _DrawFlagsStack.clear() (ImSmallStack_)
self._Path:resize(0)
self.CmdBuffer:push_back(ImDrawCmd())
-- Caller needs to bet _SetPixelDensity() as well.
end
function MT.ImDrawList:_ClearFreeMemory()
self.CmdBuffer:clear()
self.IdxBuffer:clear()
self.VtxBuffer:clear()
self.Flags = ImDrawFlags.None
self._VtxCurrentIdx = 1
self._VtxWritePtr = 1
self._IdxWritePtr = 1
self._ClipRectStack:clear()
self._TextureStack:clear()
-- self._CallbacksDataBuf:clear()
self._DrawFlagsStack.Size = 0 -- _DrawFlagsStack.clear() (ImSmallStack_)
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)
-- [Luau] inline self.CmdBuffer:push_back(draw_cmd)
local cmd_buffer = self.CmdBuffer
local n = cmd_buffer.Size + 1
if n > cmd_buffer.Capacity then cmd_buffer:push_back(draw_cmd) else cmd_buffer.Data[n] = draw_cmd; cmd_buffer.Size = n end
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)
local lcr, rcr = CMD_LHS.ClipRect, CMD_RHS.ClipRect -- [Luau] cache
if lcr.x ~= rcr.x or
lcr.y ~= rcr.y or
lcr.z ~= rcr.z or
lcr.w ~= rcr.w then
return false
end
local ltex, rtex = CMD_LHS.TexRef, CMD_RHS.TexRef
if ltex ~= rtex and (ltex._TexData ~= rtex._TexData or
ltex._TexID ~= rtex._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 then
-- [Luau] inline IM_VEC4.__eq
local hcr, ccr = self._CmdHeader.ClipRect, curr_cmd.ClipRect
if hcr.x ~= ccr.x or hcr.y ~= ccr.y or hcr.z ~= ccr.z or hcr.w ~= ccr.w then
self:AddDrawCmd()
return
end
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.Size = 0 -- [Luau] inline resize(0)
channels[i]._IdxBuffer.Size = 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]
-- [Luau] skip empty buffers: their .Data may be nil (reading it goes through IM_VECTOR.__index)
if ch._CmdBuffer.Size > 0 then
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
end
if ch._IdxBuffer.Size > 0 then
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
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
@native
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]
local cmd_buffer = ch._CmdBuffer
local idx_buffer = ch._IdxBuffer
draw_list.CmdBuffer = cmd_buffer
draw_list.IdxBuffer = idx_buffer
draw_list._IdxWritePtr = idx_buffer.Size + 1
-- If current command is used with different settings we need to add a new command
-- [Luau] ImDrawCmd_HeaderCopyRef() / ImDrawCmd_HeaderCompare() inlined
local cmd_count = cmd_buffer.Size
if cmd_count == 0 then
draw_list:AddDrawCmd()
return
end
local curr_cmd = cmd_buffer.Data[cmd_count]
local header = draw_list._CmdHeader
local hcr, ccr = header.ClipRect, curr_cmd.ClipRect
if curr_cmd.ElemCount == 0 then
ccr.x = hcr.x; ccr.y = hcr.y; ccr.z = hcr.z; ccr.w = hcr.w -- Copy ClipRect, TexRef, VtxOffset
curr_cmd.TexRef = header.TexRef
curr_cmd.VtxOffset = header.VtxOffset
elseif ccr.x ~= hcr.x or ccr.y ~= hcr.y or ccr.z ~= hcr.z or ccr.w ~= hcr.w
or curr_cmd.TexRef._TexData ~= header.TexRef._TexData or curr_cmd.TexRef._TexID ~= header.TexRef._TexID
or curr_cmd.VtxOffset ~= header.VtxOffset 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
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
-- IMPORTANT: currently limited to 3 deep
--- @param flags ImDrawFlags
--- @param enabled bool
function MT.ImDrawList:PushDrawFlag(flags, enabled)
IM_ASSERT(bit32.band(flags, bit32.bnot(ImDrawFlags.AllowInPushScope_)) == 0)
IM_ASSERT(bit32.band(flags, ImDrawFlags.StrokeMask_) == 0 or bit32.band(flags, ImDrawFlags.StrokeMask_) == ImDrawFlags.StrokeLegacy)
IM_ASSERT_USER_ERROR_RET(self._DrawFlagsStack.Size < 3, "PushDrawFlag() has a depth limit. Contact us.") -- _DrawFlagsStack.capacity() == 3
self._DrawFlagsStack:push_back(self.Flags)
if enabled then
self.Flags = bit32.bor(self.Flags, flags)
else
self.Flags = bit32.band(self.Flags, bit32.bnot(flags))
end
end
function MT.ImDrawList:PopDrawFlag()
IM_ASSERT_USER_ERROR_RET(self._DrawFlagsStack.Size > 0, "Calling PopDrawFlag() too many times!")
self.Flags = self._DrawFlagsStack:back()
self._DrawFlagsStack:pop_back()
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
--- @param pixel_density float
function MT.ImDrawList:_SetPixelDensity(pixel_density)
IM_ASSERT(pixel_density > 0.0)
self._FringeScale = 1.0 / pixel_density
self._InvFringeScale = pixel_density
self._FringeScaleIsInteger = ImIsTruncated(self._InvFringeScale)
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.
-- [Luau] local function (also MT.ImDrawList.PrimReserve): hot callers in this file skip the method lookup
@native
local function PrimReserve(self, 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, ImDrawFlags.UseVtxOffset) ~= 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
-- [Luau] inline resize() when no growth is needed (IdxBuffer:resize goes through IM_VECTOR.__index)
local vtx_buffer = self.VtxBuffer
local vtx_buffer_old_size = vtx_buffer.Size
local vtx_new_size = vtx_buffer_old_size + vtx_count
if vtx_new_size > vtx_buffer.Capacity then vtx_buffer:resize(vtx_new_size) else vtx_buffer.Size = vtx_new_size end
self._VtxWritePtr = vtx_buffer_old_size + 1
local idx_buffer = self.IdxBuffer
local idx_buffer_old_size = idx_buffer.Size
local idx_new_size = idx_buffer_old_size + idx_count
if idx_new_size > idx_buffer.Capacity then idx_buffer:resize(idx_new_size) else idx_buffer.Size = idx_new_size end
self._IdxWritePtr = idx_buffer_old_size + 1
end
MT.ImDrawList.PrimReserve = PrimReserve
-- 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
-- [Luau] scalar PrimRect (a = ax,ay; c = cx,cy): no ImVec2 needed by callers, vertex writes inlined
@native
local function PrimRectXY(self, ax, ay, cx, cy, col)
local uv = self._Data.TexUvWhitePixel
local idx = self._VtxCurrentIdx
local idx_data = self.IdxBuffer.Data; local b = 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 u, v = uv.x, uv.y
local o = (vtx_write_ptr - 1) * 36
writef64(b, o, ax); writef64(b, o + 8, ay); writef64(b, o + 16, u); writef64(b, o + 24, v); writeu32(b, o + 32, col)
writef64(b, o + 36, cx); writef64(b, o + 44, ay); writef64(b, o + 52, u); writef64(b, o + 60, v); writeu32(b, o + 68, col)
writef64(b, o + 72, cx); writef64(b, o + 80, cy); writef64(b, o + 88, u); writef64(b, o + 96, v); writeu32(b, o + 104, col)
writef64(b, o + 108, ax); writef64(b, o + 116, cy); writef64(b, o + 124, u); writef64(b, o + 132, v); writeu32(b, o + 140, col)
self._VtxWritePtr = vtx_write_ptr + 4
self._VtxCurrentIdx = idx + 4
self._IdxWritePtr = idx_write_ptr + 6
end
function MT.ImDrawList:PrimRect(a, c, col)
PrimRectXY(self, a.x, a.y, c.x, c.y, col)
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 thickness float
--- @return float
-- [Luau] local function (also MT.ImDrawList._CalculateCenterBiasedOffset)
local function _CalculateCenterBiasedOffset(self, thickness)
-- Old jumpy one.
-- return IM_TRUNC(thickness * 0.5f / _FringeScale) * _FringeScale;
-- Calculate outside offset for stroke position ImDrawFlags_StrokeCenterBiased
-- so that one side the of the line is always at pixel boundary.
-- On integer thickness both sides of the line are on pixel boundary.
local screen_thickness = thickness * self._InvFringeScale
local s_thickness = math.floor(screen_thickness) -- (int) of a positive value
return (math.floor(s_thickness / 2) + (s_thickness % 2) * (screen_thickness - s_thickness)) * self._FringeScale
end
MT.ImDrawList._CalculateCenterBiasedOffset = _CalculateCenterBiasedOffset
-- Check if R is considered to be worthy rounding. Can be only used within ImDrawList due to use of _FringeScale.
-- #define IM_HAS_ROUNDING(RAD) ((RAD) >= (0.25f * _FringeScale)) -- [Luau] written inline at each use
-- Select texture and adjust fringe size for specified line thickness.
-- [Luau] returns uv0.x, uv0.y, uv1.x, uv1.y, fringe instead of writing to out parameters
--- @param screen_thickness float
--- @param flags ImDrawFlags
local function _SelectLineTexture(self, screen_thickness, flags)
-- We assume that the screen_thickness has been clamped to 1.0 by the caller.
IM_ASSERT_PARANOID(screen_thickness >= 1.0)
if bit32.band(flags, ImDrawFlags.AALines) ~= 0 then
-- Handle the thickness between 1.0 - IM_DRAWLIST_TEX_LINES_DETAILED_WIDTH].
-- We use super sampled textures in this range to make texture changes less noticeable.
-- There are IM_DRAWLIST_TEX_LINES_DETAILED_WIDTH_COUNT textures, where 0 maps to 1.0 and IM_DRAWLIST_TEX_LINES_DETAILED_WIDTH_COUNT-1 maps to IM_DRAWLIST_TEX_LINES_DETAILED_WIDTH (4.0).
local texture_idx
if screen_thickness <= IM_DRAWLIST_TEX_LINES_DETAILED_WIDTH then
texture_idx = math.floor((screen_thickness - 1.0) * IM_DRAWLIST_TEX_LINES_SAMPLE_COUNT + 0.5) + (IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1)
elseif screen_thickness < IM_DRAWLIST_TEX_LINES_WIDTH_MAX - 1.5 then
texture_idx = math.floor(screen_thickness + 0.5)
else
texture_idx = IM_DRAWLIST_TEX_LINES_WIDTH_MAX - 1 -- Catches large values of screen_thickness (and +inf/NaN) that a ImMin() wouldn't.
end
-- Scale the fringe so that the texture matches the line thickness.
local tex_uvs = self._Data.TexUvLines[texture_idx] -- 0-based table
return tex_uvs.x, tex_uvs.z, tex_uvs.y, tex_uvs.z, self._FringeScale * screen_thickness * tex_uvs.w -- tex_uvs.w = 1 / thickness
else
-- For no anti-alias we use fully opaque texture, and no fringe expansion.
local uv = self._Data.TexUvWhitePixel
return uv.x, uv.y, uv.x, uv.y, 0.0
end
end
MT.ImDrawList._SelectLineTexture = _SelectLineTexture
-- [Luau] scratch arrays (normals x/y and squared segment lengths) used instead of _Data->TempBuffer: no ImVec2 per point
local poly_normals_x, poly_normals_y, poly_sqr_lengths = {}, {}, {}
--- @param points ImVec2[] # 1-based
--- @param points_count int
--- @param col ImU32
--- @param thickness float
--- @param flags ImDrawFlags
--- @param max_inner_offset float
-- [Luau] IM_APPEND_VTX()/IM_APPEND_TRI() are expanded inline: vertex/index write pointers and _VtxCurrentIdx kept in locals, stored back at the end
@native
function MT.ImDrawList:_AddPolyline(points, points_count, col, thickness, flags, max_inner_offset)
if points_count < 2 or bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
local screen_thickness = thickness * self._InvFringeScale
if screen_thickness < 1.0 then
if screen_thickness < 1.0 / 255.0 then
return
end
col = ImAlphaMultiply(col, screen_thickness)
screen_thickness = 1.0
thickness = self._FringeScale
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.
-- Read more details near AddRect() + see "API BREAKING CHANGES" section for 1.82, 1.90 and 1.92.8.
flags = bit32.bor(flags, self.Flags)
IM_ASSERT_USER_ERROR_RET(bit32.band(flags, ImDrawFlags.InvalidMask_) == 0, "Incorrect parameter. Did you swap 'thickness' and 'flags'?")
-- Allocate data for temp buffers
local normals_x, normals_y, sqr_lengths = poly_normals_x, poly_normals_y, poly_sqr_lengths
-- Calculate normals for each line segment
for i = 1, points_count - 1 do
local p0 = points[i]
local p1 = points[i + 1]
local dx = p1.x - p0.x
local dy = p1.y - p0.y
local d2 = dx * dx + dy * dy
local inv_len = 0.0
if d2 > 0.0 then inv_len = 1.0 / math.sqrt(d2) end -- ImRsqrtPrecise()
normals_x[i] = -dy * inv_len
normals_y[i] = dx * inv_len
sqr_lengths[i] = d2
end
if bit32.band(flags, ImDrawFlags.Closed) ~= 0 then
local p0 = points[points_count]
local p1 = points[1]
local dx = p1.x - p0.x
local dy = p1.y - p0.y
local d2 = dx * dx + dy * dy
local inv_len = 0.0
if d2 > 0.0 then inv_len = 1.0 / math.sqrt(d2) end -- ImRsqrtPrecise()
normals_x[points_count] = -dy * inv_len
normals_y[points_count] = dx * inv_len
sqr_lengths[points_count] = d2
else
normals_x[points_count] = normals_x[points_count - 1]
normals_y[points_count] = normals_y[points_count - 1]
sqr_lengths[points_count] = 0.0
end
local u0, v0, u1, v1, fringe = _SelectLineTexture(self, screen_thickness, flags)
local col_trans = bit32.band(col, bit32.bnot(IM_COL32_A_MASK))
local stroke_pos = _GetStrokePos(flags, ImDrawFlags.StrokeCenter)
if stroke_pos == ImDrawFlags.StrokeLegacy then
flags = bit32.band(bit32.bor(flags, ImDrawFlags.JoinMiter), bit32.bnot(ImDrawFlags.AALineEnds))
end
local closed = bit32.band(flags, ImDrawFlags.Closed) ~= 0
local miters_only = bit32.band(flags, ImDrawFlags.JoinMiter) ~= 0
local miter_distance_limit_sqr = IM_POLYLINE_MITER_LIMIT * IM_POLYLINE_MITER_LIMIT
local thickness0 = (thickness + fringe) * 0.5 -- Center (or legacy)
if stroke_pos == ImDrawFlags.StrokeOutside then
thickness0 = thickness + fringe * 0.5
elseif stroke_pos == ImDrawFlags.StrokeCenterBiased then
thickness0 = _CalculateCenterBiasedOffset(self, thickness) + fringe * 0.5
elseif stroke_pos == ImDrawFlags.StrokeInside then
thickness0 = fringe * 0.5
end
local thickness1 = (thickness + fringe) - thickness0
local idx_count = 0
local vtx_count = 0
local max_verts_per_point = miters_only and 2 or 4
local max_tris_per_point = miters_only and 2 or 5
if closed then
vtx_count = --[[body]] points_count * max_verts_per_point + --[[closing]] 2
idx_count = (--[[body]] points_count * max_tris_per_point + --[[closing]] 2) * 3
else
vtx_count = --[[body]] (points_count - 2) * max_verts_per_point + --[[caps]] (4 * 2)
idx_count = (--[[body]] (points_count - 2) * max_tris_per_point + --[[last seg]] 2 + --[[caps]] (2 * 2)) * 3
end
PrimReserve(self, idx_count, vtx_count)
local ratio = thickness0 / (thickness0 + thickness1) -- The points that use uv2 are placed directly on the path (offset = 0), calculate the texture position from stroke offsets.
local u2, v2 = u0 + (u1 - u0) * ratio, v0
-- Clamp inner offset and adjust texture coordinates to match the change.
-- This is used by some of the convex shape rendering to avoid rendering artifacts when thickness is about to fill the whole shape.
if thickness1 > max_inner_offset then
thickness1 = max_inner_offset
local s = (thickness0 + thickness1) / (thickness + fringe)
local du = u1 - u0
u1 = u0 + du * s
end
local half_aa = self._FringeScale * 0.5 -- Used for end caps, does not use "fringe" since end cap AA is not using the texture.
local use_aa_ends = bit32.band(flags, bit32.bor(ImDrawFlags.AALines, ImDrawFlags.AALineEnds)) == bit32.bor(ImDrawFlags.AALines, ImDrawFlags.AALineEnds)
local vb = self.VtxBuffer.Buf
local ib = self.IdxBuffer.Data
local vw = self._VtxWritePtr -- 1-based write position in VtxBuffer
local iw = self._IdxWritePtr -- 1-based write position in IdxBuffer
local vtx_idx = self._VtxCurrentIdx
local first_vtx_offset = vw
local first_vtx_idx = vtx_idx -- [Luau] VtxBuffer position of index 'n' is first_vtx_offset + (n - first_vtx_idx)
local base_idx = vtx_idx
local p1x, p1y
local n1x, n1y
local len_sqr1
local point_idx = 1 -- [Luau] 1-based, iterate point_idx..point_last inclusive
local point_last = points_count
if not closed then
-- Start cap
point_idx = point_idx + 1
point_last = point_last - 1
local p = points[1]
p1x, p1y = p.x, p.y
n1x, n1y = normals_x[1], normals_y[1]
len_sqr1 = sqr_lengths[1]
local dir_x = n1y
local dir_y = -n1x
if bit32.band(flags, ImDrawFlags.CapSquare) ~= 0 then
p1x = p1x - dir_x * thickness * 0.5
p1y = p1y - dir_y * thickness * 0.5
end
if not use_aa_ends then
base_idx = vtx_idx
WV(vb, vw + 0, p1x - n1x * thickness0, p1y - n1y * thickness0, u0, v0, col)
WV(vb, vw + 1, p1x + n1x * thickness1, p1y + n1y * thickness1, u1, v1, col)
vw = vw + 2; vtx_idx = vtx_idx + 2
else
local pa_x = p1x - dir_x * half_aa
local pa_y = p1y - dir_y * half_aa
local pb_x = p1x + dir_x * half_aa
local pb_y = p1y + dir_y * half_aa
base_idx = vtx_idx
WV(vb, vw + 0, pa_x - n1x * thickness0, pa_y - n1y * thickness0, u0, v0, col_trans)
WV(vb, vw + 1, pa_x + n1x * thickness1, pa_y + n1y * thickness1, u1, v1, col_trans)
local next_base_idx = vtx_idx + 2
WV(vb, vw + 2, pb_x - n1x * thickness0, pb_y - n1y * thickness0, u0, v0, col)
WV(vb, vw + 3, pb_x + n1x * thickness1, pb_y + n1y * thickness1, u1, v1, col)
vw = vw + 4; vtx_idx = vtx_idx + 4
-- AA cap
ib[iw + 0] = base_idx + 0; ib[iw + 1] = base_idx + 2; ib[iw + 2] = base_idx + 3
ib[iw + 3] = base_idx + 0; ib[iw + 4] = base_idx + 3; ib[iw + 5] = base_idx + 1
iw = iw + 6
base_idx = next_base_idx
end
else
-- Wrap around segment
local p = points[points_count]
p1x, p1y = p.x, p.y
n1x, n1y = normals_x[points_count], normals_y[points_count]
len_sqr1 = sqr_lengths[points_count]
-- This will be filled later, allocate space.
base_idx = vtx_idx
WV(vb, vw + 0, 0, 0, u0, v0, col)
WV(vb, vw + 1, 0, 0, u1, v1, col)
vw = vw + 2; vtx_idx = vtx_idx + 2
end
if miters_only then
for pi = point_idx, point_last do
local n0x, n0y = n1x, n1y
local p = points[pi]
p1x, p1y = p.x, p.y
n1x, n1y = normals_x[pi], normals_y[pi]
-- theta is the angle between two segments
local cos_theta = n0x * n1x + n0y * n1y
-- miter offset formula is derived here: https://www.angusj.com/clipper2/Docs/Trigonometry.htm
local cos_theta_clamped = (IM_POLYLINE_MITER_ANGLE_LIMIT >= cos_theta) and IM_POLYLINE_MITER_ANGLE_LIMIT or cos_theta -- Avoid div by 0.
local miter_scale_factor = 1.0 / (1.0 + cos_theta_clamped)
if miter_scale_factor > 1000.0 then miter_scale_factor = 1000.0 end
local miter_offset_x = (n0x + n1x) * miter_scale_factor
local miter_offset_y = (n0y + n1y) * miter_scale_factor
-- Miter
local next_base_idx = vtx_idx
WV(vb, vw + 0, p1x - miter_offset_x * thickness0, p1y - miter_offset_y * thickness0, u0, v0, col) -- 2
WV(vb, vw + 1, p1x + miter_offset_x * thickness1, p1y + miter_offset_y * thickness1, u1, v1, col) -- 3
vw = vw + 2; vtx_idx = vtx_idx + 2
-- Connect prev to next
ib[iw + 0] = base_idx + 0; ib[iw + 1] = base_idx + 2; ib[iw + 2] = base_idx + 3
ib[iw + 3] = base_idx + 0; ib[iw + 4] = base_idx + 3; ib[iw + 5] = base_idx + 1
iw = iw + 6
base_idx = next_base_idx
end
else
for pi = point_idx, point_last do
local n0x, n0y = n1x, n1y
local len_sqr0 = len_sqr1
local p = points[pi]
p1x, p1y = p.x, p.y
n1x, n1y = normals_x[pi], normals_y[pi]
len_sqr1 = sqr_lengths[pi]
-- theta is the angle between two segments
local cos_theta = n0x * n1x + n0y * n1y
local sin_theta = n0y * n1x - n0x * n1y
-- miter offset formula is derived here: https://www.angusj.com/clipper2/Docs/Trigonometry.htm
local cos_theta_clamped = (IM_POLYLINE_MITER_ANGLE_LIMIT >= cos_theta) and IM_POLYLINE_MITER_ANGLE_LIMIT or cos_theta -- Avoid div by 0.
local miter_scale_factor = 1.0 / (1.0 + cos_theta_clamped)
if miter_scale_factor > 1000.0 then miter_scale_factor = 1000.0 end
local miter_offset_x = (n0x + n1x) * miter_scale_factor
local miter_offset_y = (n0y + n1y) * miter_scale_factor
local miter_distance_sqr = miter_offset_x * miter_offset_x + miter_offset_y * miter_offset_y
if miter_distance_sqr > miter_distance_limit_sqr then
-- Clipped bevel
-- Limit the inner miter offset to avoid overshooting.
local ref_thickness = (sin_theta < 0.0) and thickness1 or thickness0
local ref_thickness_sqr = ref_thickness * ref_thickness
local limit_sqr = ImMax(ImMax(len_sqr0, len_sqr1), ref_thickness_sqr)
local ref_miter_dist_sqr = miter_distance_sqr * ref_thickness_sqr
if ref_miter_dist_sqr > limit_sqr then
local scale = ImSqrt(limit_sqr / ref_miter_dist_sqr)
miter_offset_x = miter_offset_x * scale
miter_offset_y = miter_offset_y * scale
end
local bn_x, bn_y = IM_NORMALIZE2F_OVER_ZERO(n0x + n1x, n0y + n1y)
local side_offset = ((n0x * bn_x + n0y * bn_y) - 1.0) / (n0y * bn_x - n0x * bn_y)
local sd_x = bn_y * side_offset
local sd_y = -bn_x * side_offset
-- Bevel
local next_base_idx = vtx_idx + 2
if sin_theta < 0.0 then
WV(vb, vw + 0, p1x - (bn_x + sd_x) * thickness0, p1y - (bn_y + sd_y) * thickness0, u0, v0, col) -- 2
WV(vb, vw + 1, p1x, p1y, u2, v2, col) -- 3
WV(vb, vw + 2, p1x - (bn_x - sd_x) * thickness0, p1y - (bn_y - sd_y) * thickness0, u0, v0, col) -- 4
WV(vb, vw + 3, p1x + miter_offset_x * thickness1, p1y + miter_offset_y * thickness1, u1, v1, col) -- 5
-- Connect prev to next
ib[iw + 0] = base_idx + 0; ib[iw + 1] = base_idx + 2; ib[iw + 2] = base_idx + 5
ib[iw + 3] = base_idx + 0; ib[iw + 4] = base_idx + 5; ib[iw + 5] = base_idx + 1
-- Bevel
ib[iw + 6] = base_idx + 3; ib[iw + 7] = base_idx + 5; ib[iw + 8] = base_idx + 2
ib[iw + 9] = base_idx + 3; ib[iw + 10] = base_idx + 2; ib[iw + 11] = base_idx + 4
ib[iw + 12] = base_idx + 3; ib[iw + 13] = base_idx + 4; ib[iw + 14] = base_idx + 5
else
WV(vb, vw + 0, p1x + (bn_x + sd_x) * thickness1, p1y + (bn_y + sd_y) * thickness1, u1, v1, col) -- 2
WV(vb, vw + 1, p1x, p1y, u2, v2, col) -- 3
WV(vb, vw + 2, p1x - miter_offset_x * thickness0, p1y - miter_offset_y * thickness0, u0, v0, col) -- 4
WV(vb, vw + 3, p1x + (bn_x - sd_x) * thickness1, p1y + (bn_y - sd_y) * thickness1, u1, v1, col) -- 5
-- Connect prev to next
ib[iw + 0] = base_idx + 0; ib[iw + 1] = base_idx + 4; ib[iw + 2] = base_idx + 2
ib[iw + 3] = base_idx + 0; ib[iw + 4] = base_idx + 2; ib[iw + 5] = base_idx + 1
-- Bevel
ib[iw + 6] = base_idx + 3; ib[iw + 7] = base_idx + 2; ib[iw + 8] = base_idx + 4
ib[iw + 9] = base_idx + 3; ib[iw + 10] = base_idx + 4; ib[iw + 11] = base_idx + 5
ib[iw + 12] = base_idx + 3; ib[iw + 13] = base_idx + 5; ib[iw + 14] = base_idx + 2
end
vw = vw + 4; vtx_idx = vtx_idx + 4
iw = iw + 15
base_idx = next_base_idx
else
-- Miter
local next_base_idx = vtx_idx
WV(vb, vw + 0, p1x - miter_offset_x * thickness0, p1y - miter_offset_y * thickness0, u0, v0, col) -- 2
WV(vb, vw + 1, p1x + miter_offset_x * thickness1, p1y + miter_offset_y * thickness1, u1, v1, col) -- 3
vw = vw + 2; vtx_idx = vtx_idx + 2
-- Connect prev to next
ib[iw + 0] = base_idx + 0; ib[iw + 1] = base_idx + 2; ib[iw + 2] = base_idx + 3
ib[iw + 3] = base_idx + 0; ib[iw + 4] = base_idx + 3; ib[iw + 5] = base_idx + 1
iw = iw + 6
base_idx = next_base_idx
end
end
end
-- End cap
if not closed then
local p = points[points_count]
p1x, p1y = p.x, p.y
n1x, n1y = normals_x[points_count], normals_y[points_count]
-- End cap
local dir_x = n1y
local dir_y = -n1x
if bit32.band(flags, ImDrawFlags.CapSquare) ~= 0 then
p1x = p1x + dir_x * thickness * 0.5
p1y = p1y + dir_y * thickness * 0.5
end
if not use_aa_ends then
WV(vb, vw + 0, p1x - n1x * thickness0, p1y - n1y * thickness0, u0, v0, col)
WV(vb, vw + 1, p1x + n1x * thickness1, p1y + n1y * thickness1, u1, v1, col)
vw = vw + 2; vtx_idx = vtx_idx + 2
-- Connect
ib[iw + 0] = base_idx + 0; ib[iw + 1] = base_idx + 2; ib[iw + 2] = base_idx + 3
ib[iw + 3] = base_idx + 0; ib[iw + 4] = base_idx + 3; ib[iw + 5] = base_idx + 1
iw = iw + 6
else
local pa_x = p1x - dir_x * half_aa
local pa_y = p1y - dir_y * half_aa
local pb_x = p1x + dir_x * half_aa
local pb_y = p1y + dir_y * half_aa
local next_base_idx = vtx_idx
WV(vb, vw + 0, pa_x - n1x * thickness0, pa_y - n1y * thickness0, u0, v0, col)
WV(vb, vw + 1, pa_x + n1x * thickness1, pa_y + n1y * thickness1, u1, v1, col)
WV(vb, vw + 2, pb_x - n1x * thickness0, pb_y - n1y * thickness0, u0, v0, col_trans)
WV(vb, vw + 3, pb_x + n1x * thickness1, pb_y + n1y * thickness1, u1, v1, col_trans)
vw = vw + 4; vtx_idx = vtx_idx + 4
-- Connect
ib[iw + 0] = base_idx + 0; ib[iw + 1] = base_idx + 2; ib[iw + 2] = base_idx + 3
ib[iw + 3] = base_idx + 0; ib[iw + 4] = base_idx + 3; ib[iw + 5] = base_idx + 1
base_idx = next_base_idx
-- AA cap
ib[iw + 6] = base_idx + 0; ib[iw + 7] = base_idx + 2; ib[iw + 8] = base_idx + 3
ib[iw + 9] = base_idx + 0; ib[iw + 10] = base_idx + 3; ib[iw + 11] = base_idx + 1
iw = iw + 12
end
else
-- Connect the path.
-- [Luau] VtxBuffer.Data[first_vtx_offset + 0..1] = VtxBuffer.Data[_CmdHeader.VtxOffset + base_idx + 0..1] (2 vertices of 36 bytes)
buffer.copy(vb, (first_vtx_offset - 1) * 36, vb, (first_vtx_offset + (base_idx - first_vtx_idx) - 1) * 36, 2 * 36)
end
self._VtxWritePtr = vw
self._IdxWritePtr = iw
self._VtxCurrentIdx = vtx_idx
-- Restore unused memory
local remaining_idx_count = self.IdxBuffer.Size - (iw - 1)
local remaining_vtx_count = self.VtxBuffer.Size - (vw - 1)
IM_ASSERT_PARANOID(remaining_idx_count >= 0 and remaining_vtx_count >= 0)
if remaining_idx_count > 0 or remaining_vtx_count > 0 then -- [Luau] skip the call when nothing is left
self:PrimUnreserve(remaining_idx_count, remaining_vtx_count)
end
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[] # 1-based
--- @param points_count int
--- @param col ImU32
--- @param flags? ImDrawFlags
@native
function MT.ImDrawList:AddConvexPolyFilled(points, points_count, col, flags)
if points_count < 3 or bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
if flags == nil then flags = 0 end
local uv = self._Data.TexUvWhitePixel
local uv_x, uv_y = uv.x, uv.y
flags = bit32.bor(flags, self.Flags)
if bit32.band(flags, ImDrawFlags.AAFill) ~= 0 then
local half_aa = self._FringeScale * 0.5
local col_trans = bit32.band(col, bit32.bnot(IM_COL32_A_MASK))
local miters_only = bit32.band(flags, ImDrawFlags.JoinMiter) ~= 0
local miter_distance_limit_sqr = IM_POLYLINE_MITER_LIMIT * IM_POLYLINE_MITER_LIMIT
local idx_count = ((points_count - 2) + points_count * 2 + IM_POLYLINE_CONVEX_POLY_MAX_BEVELS) * 3
local vtx_count = points_count * 2 + IM_POLYLINE_CONVEX_POLY_MAX_BEVELS
PrimReserve(self, idx_count, vtx_count)
local vb = self.VtxBuffer.Buf
local ib = self.IdxBuffer.Data
local start_vtx_ptr = self._VtxWritePtr
local start_idx_ptr = self._IdxWritePtr
local iw = start_idx_ptr
-- Compute normals and segment lengths
local temp_normals_x, temp_normals_y, temp_sqr_lengths = poly_normals_x, poly_normals_y, poly_sqr_lengths
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
local d2 = dx * dx + dy * dy
if d2 > 0.0 then
local inv_len = 1.0 / math.sqrt(d2) -- ImRsqrtPrecise()
dx = dx * inv_len
dy = dy * inv_len
end
temp_normals_x[i0] = dy
temp_normals_y[i0] = -dx
temp_sqr_lengths[i0] = d2
i0 = i1
end
local vtx_inner_idx = self._VtxCurrentIdx
local inner_vtx_ptr = start_vtx_ptr
local vw = start_vtx_ptr + points_count
local vtx_idx = vtx_inner_idx + points_count
local prev_outer_idx = 0 -- This should be the index of the last vertex, but we don't know it yet. Will need to patch once we're done.
if miters_only then
i0 = points_count
for i1 = 1, points_count do
local p1 = points[i1]
local p1x, p1y = p1.x, p1.y
local n0x, n0y = temp_normals_x[i0], temp_normals_y[i0]
local n1x, n1y = temp_normals_x[i1], temp_normals_y[i1]
-- theta is the angle between two segments
local cos_theta = n0x * n1x + n0y * n1y
-- miter offset formula is derived here: https://www.angusj.com/clipper2/Docs/Trigonometry.htm
local cos_theta_clamped = (IM_POLYLINE_MITER_ANGLE_LIMIT >= cos_theta) and IM_POLYLINE_MITER_ANGLE_LIMIT or cos_theta -- Avoid div by 0.
local miter_scale_factor = 1.0 / (1.0 + cos_theta_clamped)
if miter_scale_factor > 1000.0 then miter_scale_factor = 1000.0 end
local miter_offset_x = (n0x + n1x) * miter_scale_factor * half_aa
local miter_offset_y = (n0y + n1y) * miter_scale_factor * half_aa
-- Inner
WV(vb, inner_vtx_ptr, p1x - miter_offset_x, p1y - miter_offset_y, uv_x, uv_y, col)
inner_vtx_ptr = inner_vtx_ptr + 1
local prev_inner_idx = vtx_inner_idx + (i0 - 1)
local inner_idx = vtx_inner_idx + (i1 - 1)
local outer_idx = vtx_idx
-- Outer
WV(vb, vw, p1x + miter_offset_x, p1y + miter_offset_y, uv_x, uv_y, col_trans)
vw = vw + 1; vtx_idx = vtx_idx + 1
-- Connect with previous
ib[iw + 0] = prev_outer_idx; ib[iw + 1] = outer_idx; ib[iw + 2] = inner_idx
ib[iw + 3] = prev_outer_idx; ib[iw + 4] = inner_idx; ib[iw + 5] = prev_inner_idx
iw = iw + 6
prev_outer_idx = outer_idx
i0 = i1
end
else
local bevel_count = 0
i0 = points_count
for i1 = 1, points_count do
local p1 = points[i1]
local p1x, p1y = p1.x, p1.y
local n0x, n0y = temp_normals_x[i0], temp_normals_y[i0]
local n1x, n1y = temp_normals_x[i1], temp_normals_y[i1]
-- theta is the angle between two segments
local cos_theta = n0x * n1x + n0y * n1y
-- miter offset formula is derived here: https://www.angusj.com/clipper2/Docs/Trigonometry.htm
local cos_theta_clamped = (IM_POLYLINE_MITER_ANGLE_LIMIT >= cos_theta) and IM_POLYLINE_MITER_ANGLE_LIMIT or cos_theta -- Avoid div by 0.
local miter_scale_factor = 1.0 / (1.0 + cos_theta_clamped)
if miter_scale_factor > 1000.0 then miter_scale_factor = 1000.0 end
local miter_offset_x = (n0x + n1x) * miter_scale_factor
local miter_offset_y = (n0y + n1y) * miter_scale_factor
local miter_distance_sqr = miter_offset_x * miter_offset_x + miter_offset_y * miter_offset_y
local bevel = miter_distance_sqr > miter_distance_limit_sqr
if bevel then
-- Limit inner bevel so that it is does not shoot out outside the polygon.
-- (using ImMax is intentional: using min limited too much in Mikko's tests when you have short edge next to long one.
-- The idea of that check is to keep that worst case overshoot no bigger than the polygon)
local ref_thickness_sqr = half_aa * half_aa
local limit_sqr = ImMax(temp_sqr_lengths[i0], temp_sqr_lengths[i1])
local ref_miter_dist_sqr = miter_distance_sqr * ref_thickness_sqr
if ref_miter_dist_sqr > limit_sqr then
local scale = ImSqrt(limit_sqr / ref_miter_dist_sqr)
miter_offset_x = miter_offset_x * scale
miter_offset_y = miter_offset_y * scale
end
end
miter_offset_x = miter_offset_x * half_aa
miter_offset_y = miter_offset_y * half_aa
-- Inner
WV(vb, inner_vtx_ptr, p1x - miter_offset_x, p1y - miter_offset_y, uv_x, uv_y, col)
inner_vtx_ptr = inner_vtx_ptr + 1
local prev_inner_idx = vtx_inner_idx + (i0 - 1)
local inner_idx = vtx_inner_idx + (i1 - 1)
local outer_idx = vtx_idx
-- Outer
if bevel and bevel_count < IM_POLYLINE_CONVEX_POLY_MAX_BEVELS then
-- Because the polygon is convex, we know the maximum number of bevel corners we can hit (which is very small number).
-- We keep track of them just in case the calculations disagree.
bevel_count = bevel_count + 1
WV(vb, vw + 0, p1x + n0x * half_aa, p1y + n0y * half_aa, uv_x, uv_y, col_trans)
WV(vb, vw + 1, p1x + n1x * half_aa, p1y + n1y * half_aa, uv_x, uv_y, col_trans)
vw = vw + 2; vtx_idx = vtx_idx + 2
-- Connect with previous
ib[iw + 0] = prev_outer_idx; ib[iw + 1] = outer_idx; ib[iw + 2] = inner_idx
ib[iw + 3] = prev_outer_idx; ib[iw + 4] = inner_idx; ib[iw + 5] = prev_inner_idx
-- Fill bevel
ib[iw + 6] = outer_idx; ib[iw + 7] = outer_idx + 1; ib[iw + 8] = inner_idx
iw = iw + 9
outer_idx = outer_idx + 1
else
WV(vb, vw, p1x + miter_offset_x, p1y + miter_offset_y, uv_x, uv_y, col_trans)
vw = vw + 1; vtx_idx = vtx_idx + 1
-- Connect with previous
ib[iw + 0] = prev_outer_idx; ib[iw + 1] = outer_idx; ib[iw + 2] = inner_idx
ib[iw + 3] = prev_outer_idx; ib[iw + 4] = inner_idx; ib[iw + 5] = prev_inner_idx
iw = iw + 6
end
prev_outer_idx = outer_idx
i0 = i1
end
end
-- Patch first segment to wrap around
ib[start_idx_ptr + 0] = prev_outer_idx
ib[start_idx_ptr + 3] = prev_outer_idx
-- Add indices for fill
for i = 2, points_count - 1 do
ib[iw + 0] = vtx_inner_idx; ib[iw + 1] = vtx_inner_idx + i - 1; ib[iw + 2] = vtx_inner_idx + i
iw = iw + 3
end
self._VtxWritePtr = vw
self._IdxWritePtr = iw
self._VtxCurrentIdx = vtx_idx
local idx_used = iw - start_idx_ptr
local vtx_used = vw - start_vtx_ptr
IM_ASSERT(idx_used <= idx_count and vtx_used <= vtx_count)
if idx_used < idx_count or vtx_used < vtx_count then
self:PrimUnreserve(idx_count - idx_used, vtx_count - vtx_used)
end
else
-- Non Anti-aliased Fill
local idx_count = (points_count - 2) * 3
local vtx_count = points_count
PrimReserve(self, idx_count, vtx_count)
local vb = self.VtxBuffer.Buf
local ib = self.IdxBuffer.Data
local vw = self._VtxWritePtr
local iw = self._IdxWritePtr
local base_idx = self._VtxCurrentIdx
for i = 1, vtx_count do
local p = points[i]
WV(vb, vw, p.x, p.y, uv_x, uv_y, col)
vw = vw + 1
end
for i = 2, points_count - 1 do
ib[iw + 0] = base_idx; ib[iw + 1] = base_idx + i - 1; ib[iw + 2] = base_idx + i
iw = iw + 3
end
self._VtxWritePtr = vw
self._IdxWritePtr = iw
self._VtxCurrentIdx = base_idx + vtx_count
end
end
-- [Luau] scalar PathLineTo(): no temporary ImVec2 (the path slot is written in place)
local function PathLineToXY(self, x, y)
local path = self._Path
local n = path.Size + 1
if n > path.Capacity then path:push_back(ImVec2(x, y)) return end
local dest = path.Data[n]
dest.x = x; dest.y = y
path.Size = n
end
local _PathArcToFastExXY -- [Luau] forward declaration, defined below with _PathArcToFastEx()
-- [Luau] scalar PathRect (a = ax,ay; b = bx,by); PathRect() below wraps it
local function PathRectXY(self, ax, ay, bx, by, rounding, flags)
if not rounding then rounding = 0.0 end
if not flags then flags = 0 end
local has_rounding = rounding >= 0.25 * self._FringeScale -- IM_HAS_ROUNDING(rounding)
if has_rounding then
if bit32.band(flags, ImDrawFlags.RoundMask_) == 0 then
flags = bit32.bor(flags, ImDrawFlags.RoundAll)
end
rounding = ImMin(rounding, ImAbs(bx - ax) * (((bit32.band(flags, ImDrawFlags.RoundTop) == ImDrawFlags.RoundTop) or (bit32.band(flags, ImDrawFlags.RoundBottom) == ImDrawFlags.RoundBottom)) and 0.5 or 1.0) - 1.0)
rounding = ImMin(rounding, ImAbs(by - ay) * (((bit32.band(flags, ImDrawFlags.RoundLeft) == ImDrawFlags.RoundLeft) or (bit32.band(flags, ImDrawFlags.RoundRight) == ImDrawFlags.RoundRight)) and 0.5 or 1.0) - 1.0)
end
if not has_rounding or (bit32.band(flags, ImDrawFlags.RoundMask_) == ImDrawFlags.RoundNone) then
PathLineToXY(self, ax, ay)
PathLineToXY(self, bx, ay)
PathLineToXY(self, bx, by)
PathLineToXY(self, ax, by)
else
local rounding_tl = (bit32.band(flags, ImDrawFlags.RoundTopLeft) ~= 0) and rounding or 0.0
local rounding_tr = (bit32.band(flags, ImDrawFlags.RoundTopRight) ~= 0) and rounding or 0.0
local rounding_br = (bit32.band(flags, ImDrawFlags.RoundBottomRight) ~= 0) and rounding or 0.0
local rounding_bl = (bit32.band(flags, ImDrawFlags.RoundBottomLeft) ~= 0) and rounding or 0.0
-- [Luau] inline PathArcToFast() (small radius handled by _PathArcToFastExXY())
_PathArcToFastExXY(self, ax + rounding_tl, ay + rounding_tl, rounding_tl, 6 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 9 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 0)
_PathArcToFastExXY(self, bx - rounding_tr, ay + rounding_tr, rounding_tr, 9 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 12 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 0)
_PathArcToFastExXY(self, bx - rounding_br, by - rounding_br, rounding_br, 0 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 3 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 0)
_PathArcToFastExXY(self, ax + rounding_bl, by - rounding_bl, rounding_bl, 3 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 6 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 0)
end
end
function MT.ImDrawList:PathRect(a, b, rounding, flags)
PathRectXY(self, a.x, a.y, b.x, b.y, rounding, flags)
end
-- [Luau] scalar _AddLine (p1 = p1x,p1y; p2 = p2x,p2y); _AddLine() below wraps it
@native
local function _AddLineXY(self, p1x, p1y, p2x, p2y, col, thickness, flags)
if bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
local screen_thickness = thickness * self._InvFringeScale
if screen_thickness < 1.0 then
if screen_thickness < 1.0 / 255.0 then
return
end
col = ImAlphaMultiply(col, screen_thickness)
screen_thickness = 1.0
thickness = self._FringeScale
end
flags = bit32.bor(flags, self.Flags)
local u0, v0, u1, v1, fringe = _SelectLineTexture(self, screen_thickness, flags)
local dir_x = p2x - p1x
local dir_y = p2y - p1y
local d2 = dir_x * dir_x + dir_y * dir_y
local inv_len = 0.0
if d2 > 0.0 then inv_len = 1.0 / math.sqrt(d2) end -- ImRsqrtPrecise()
dir_x = dir_x * inv_len
dir_y = dir_y * inv_len
local norm_x = -dir_y
local norm_y = dir_x
local col_trans = bit32.band(col, bit32.bnot(IM_COL32_A_MASK))
local stroke_pos = _GetStrokePos(flags, ImDrawFlags.StrokeCenter)
if stroke_pos == ImDrawFlags.StrokeLegacy then
flags = bit32.band(bit32.bor(flags, ImDrawFlags.JoinMiter), bit32.bnot(ImDrawFlags.AALineEnds))
end
local thickness0 = (thickness + fringe) * 0.5 -- Center (or legacy)
if stroke_pos == ImDrawFlags.StrokeOutside then
thickness0 = thickness + fringe * 0.5
elseif stroke_pos == ImDrawFlags.StrokeCenterBiased then
thickness0 = _CalculateCenterBiasedOffset(self, thickness) + fringe * 0.5
elseif stroke_pos == ImDrawFlags.StrokeInside then
thickness0 = fringe * 0.5
end
local thickness1 = (thickness + fringe) - thickness0
-- [Luau] uv2 (ratio) is unused by _AddLine(): not computed
local half_aa = self._FringeScale * 0.5 -- Used for end caps, does not use "fringe" since end cap AA is not using the texture.
local use_aa_ends = bit32.band(flags, bit32.bor(ImDrawFlags.AALines, ImDrawFlags.AALineEnds)) == bit32.bor(ImDrawFlags.AALines, ImDrawFlags.AALineEnds)
if not use_aa_ends then
PrimReserve(self, 2 * 3, 4)
else
PrimReserve(self, 6 * 3, 8)
end
local vb = self.VtxBuffer.Buf
local ib = self.IdxBuffer.Data
local vw = self._VtxWritePtr
local iw = self._IdxWritePtr
local base_idx = self._VtxCurrentIdx
if bit32.band(flags, ImDrawFlags.CapSquare) ~= 0 then
local half_thickness = thickness * 0.5
p1x = p1x - dir_x * half_thickness
p1y = p1y - dir_y * half_thickness
p2x = p2x + dir_x * half_thickness
p2y = p2y + dir_y * half_thickness
end
-- Start cap
if not use_aa_ends then
WV(vb, vw + 0, p1x - norm_x * thickness0, p1y - norm_y * thickness0, u0, v0, col)
WV(vb, vw + 1, p1x + norm_x * thickness1, p1y + norm_y * thickness1, u1, v1, col)
WV(vb, vw + 2, p2x - norm_x * thickness0, p2y - norm_y * thickness0, u0, v0, col)
WV(vb, vw + 3, p2x + norm_x * thickness1, p2y + norm_y * thickness1, u1, v1, col)
-- Connect
ib[iw + 0] = base_idx + 0; ib[iw + 1] = base_idx + 2; ib[iw + 2] = base_idx + 3
ib[iw + 3] = base_idx + 0; ib[iw + 4] = base_idx + 3; ib[iw + 5] = base_idx + 1
self._VtxWritePtr = vw + 4
self._IdxWritePtr = iw + 6
self._VtxCurrentIdx = base_idx + 4
else
-- Start
local pa_x = p1x - dir_x * half_aa
local pa_y = p1y - dir_y * half_aa
local pb_x = p1x + dir_x * half_aa
local pb_y = p1y + dir_y * half_aa
WV(vb, vw + 0, pa_x - norm_x * thickness0, pa_y - norm_y * thickness0, u0, v0, col_trans)
WV(vb, vw + 1, pa_x + norm_x * thickness1, pa_y + norm_y * thickness1, u1, v1, col_trans)
WV(vb, vw + 2, pb_x - norm_x * thickness0, pb_y - norm_y * thickness0, u0, v0, col)
WV(vb, vw + 3, pb_x + norm_x * thickness1, pb_y + norm_y * thickness1, u1, v1, col)
-- AA cap
ib[iw + 0] = base_idx + 0; ib[iw + 1] = base_idx + 2; ib[iw + 2] = base_idx + 3
ib[iw + 3] = base_idx + 0; ib[iw + 4] = base_idx + 3; ib[iw + 5] = base_idx + 1
base_idx = base_idx + 2
-- End
pa_x = p2x - dir_x * half_aa
pa_y = p2y - dir_y * half_aa
pb_x = p2x + dir_x * half_aa
pb_y = p2y + dir_y * half_aa
WV(vb, vw + 4, pa_x - norm_x * thickness0, pa_y - norm_y * thickness0, u0, v0, col)
WV(vb, vw + 5, pa_x + norm_x * thickness1, pa_y + norm_y * thickness1, u1, v1, col)
WV(vb, vw + 6, pb_x - norm_x * thickness0, pb_y - norm_y * thickness0, u0, v0, col_trans)
WV(vb, vw + 7, pb_x + norm_x * thickness1, pb_y + norm_y * thickness1, u1, v1, col_trans)
-- Connect
ib[iw + 6] = base_idx + 0; ib[iw + 7] = base_idx + 2; ib[iw + 8] = base_idx + 3
ib[iw + 9] = base_idx + 0; ib[iw + 10] = base_idx + 3; ib[iw + 11] = base_idx + 1
base_idx = base_idx + 2
-- AA cap
ib[iw + 12] = base_idx + 0; ib[iw + 13] = base_idx + 2; ib[iw + 14] = base_idx + 3
ib[iw + 15] = base_idx + 0; ib[iw + 16] = base_idx + 3; ib[iw + 17] = base_idx + 1
self._VtxWritePtr = vw + 8
self._IdxWritePtr = iw + 18
self._VtxCurrentIdx = base_idx + 4
end
end
function MT.ImDrawList:_AddLine(p1, p2, col, thickness, flags)
_AddLineXY(self, p1.x, p1.y, p2.x, p2.y, col, thickness, flags)
end
--- @param p1 ImVec2
--- @param p2 ImVec2
--- @param col ImU32
--- @param thickness? float
--- @param flags? ImDrawFlags
function MT.ImDrawList:AddLine(p1, p2, col, thickness, flags)
if thickness == nil then thickness = 1.0 end
if flags == nil then flags = 0 end
if bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
flags = bit32.bor(flags, self.Flags)
local stroke_pos = _GetStrokePos(flags, ImDrawFlags.StrokeCenter)
if stroke_pos == ImDrawFlags.StrokeLegacy then
_AddLineXY(self, p1.x + 0.5, p1.y + 0.5, p2.x + 0.5, p2.y + 0.5, col, thickness, flags)
return
end
_AddLineXY(self, p1.x, p1.y, p2.x, p2.y, col, thickness, flags)
end
-- AddLineH(10, 20, 5, col, 1.0f) == AddLine( {10,5.5}, {20,5.5}, col, 1.0f)
-- == AddLine( {10,5}, {20,5}, col, 1.0f, ImDrawFlags_StrokeLegacy); // or before 1.93.0
-- Thickness extend inside/bottom by default. Faster than equivalent AddLine() call.
--- @param min_x float
--- @param max_x float
--- @param y float
--- @param col ImU32
--- @param thickness? float
--- @param flags? ImDrawFlags
@native
function MT.ImDrawList:AddLineH(min_x, max_x, y, col, thickness, flags)
if thickness == nil then thickness = 1.0 end
if flags == nil then flags = 0 end
if bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
flags = bit32.bor(flags, self.Flags)
local stroke_pos = _GetStrokePos(flags, ImDrawFlags.StrokeInside)
if stroke_pos == ImDrawFlags.StrokeLegacy then
_AddLineXY(self, min_x + 0.5, y + 0.5, max_x + 0.5, y + 0.5, col, thickness, flags)
return
end
local top_y = y -- Inside
if stroke_pos == ImDrawFlags.StrokeCenter then
top_y = top_y - thickness * 0.5
elseif stroke_pos == ImDrawFlags.StrokeCenterBiased then
top_y = top_y - _CalculateCenterBiasedOffset(self, thickness)
elseif stroke_pos == ImDrawFlags.StrokeOutside then
top_y = top_y - thickness
end
local is_truncated = min_x % 1 == 0 and max_x % 1 == 0 and top_y % 1 == 0 and thickness % 1 == 0 -- IM_IS_TRUNCATED4()
if self._FringeScaleIsInteger and is_truncated and bit32.band(flags, ImDrawFlags.CapSquare) == 0 then
if thickness < self._FringeScale then
if thickness < (1.0 / 255.0) * self._FringeScale then
return
end
col = ImAlphaMultiply(col, thickness * self._InvFringeScale)
thickness = self._FringeScale
end
-- For pixel aligned case, use simple rectangle.
PrimReserve(self, 6, 4)
PrimRectXY(self, min_x, top_y, max_x, top_y + thickness, col)
else
-- Replace stroke pos with the already calculated one, as AddPolyline() default is different.
flags = bit32.bor(bit32.band(flags, bit32.bnot(ImDrawFlags.StrokeMask_)), stroke_pos)
-- For generic case use line, since it needs less triangles than AA rectangle.
_AddLineXY(self, min_x, y, max_x, y, col, thickness, flags)
end
end
-- AddLineV(5, 10, 20, col, 1.0f) == AddLine( {5.5,10}, {5.5,20}, col, 1.0f)
-- == AddLine( {5,10}, {5,20}, col, 1.0f, ImDrawFlags_StrokeLegacy); // or before 1.93.0
-- Thickness extend inside/right by default. Faster than equivalent AddLine() call.
--- @param x float
--- @param min_y float
--- @param max_y float
--- @param col ImU32
--- @param thickness? float
--- @param flags? ImDrawFlags
@native
function MT.ImDrawList:AddLineV(x, min_y, max_y, col, thickness, flags)
if thickness == nil then thickness = 1.0 end
if flags == nil then flags = 0 end
if bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
flags = bit32.bor(flags, self.Flags)
local stroke_pos = _GetStrokePos(flags, ImDrawFlags.StrokeInside)
if stroke_pos == ImDrawFlags.StrokeLegacy then
_AddLineXY(self, x + 0.5, min_y + 0.5, x + 0.5, max_y + 0.5, col, thickness, flags)
return
end
local left_x = x -- Inside
if stroke_pos == ImDrawFlags.StrokeCenter then
left_x = left_x - thickness * 0.5
elseif stroke_pos == ImDrawFlags.StrokeCenterBiased then
left_x = left_x - _CalculateCenterBiasedOffset(self, thickness)
elseif stroke_pos == ImDrawFlags.StrokeOutside then
left_x = left_x - thickness
end
local is_truncated = left_x % 1 == 0 and min_y % 1 == 0 and max_y % 1 == 0 and thickness % 1 == 0 -- IM_IS_TRUNCATED4()
if self._FringeScaleIsInteger and is_truncated and bit32.band(flags, ImDrawFlags.CapSquare) == 0 then
if thickness < self._FringeScale then
if thickness < (1.0 / 255.0) * self._FringeScale then
return
end
col = ImAlphaMultiply(col, thickness * self._InvFringeScale)
thickness = self._FringeScale
end
-- For pixel aligned case, use simple rectangle.
PrimReserve(self, 6, 4)
PrimRectXY(self, left_x, min_y, left_x + thickness, max_y, col)
else
-- Replace stroke pos with the already calculated one, as AddPolyline() default is different.
flags = bit32.bor(bit32.band(flags, bit32.bnot(ImDrawFlags.StrokeMask_)), stroke_pos)
-- For generic case use line, since it needs less triangles than AA rectangle.
-- Drawing for bottom to top, so that the inside stroke pos expands to right.
_AddLineXY(self, x, max_y, x, min_y, col, thickness, flags)
end
end
-- [Port] TODO baked round corners: _AddRectBaked() (UseTexForRoundCorners) not ported yet, AddRect() falls back to the geometry path.
local corner_offset_x = { 1, -1, -1, 1 } -- static const ImVec2 corner_offset[4] = { {1,1}, {-1,1}, {-1,-1}, {1,-1} };
local corner_offset_y = { 1, 1, -1, -1 }
local corner_flags = { ImDrawFlags.RoundTopLeft, ImDrawFlags.RoundTopRight, ImDrawFlags.RoundBottomRight, ImDrawFlags.RoundBottomLeft }
local corner_pos_x, corner_pos_y = { 0, 0, 0, 0 }, { 0, 0, 0, 0 } -- [Luau] scratch
-- Draws rounded rectangle where thickness > rounding. If rendered using the regular polyline, the stroke will fold and leave artifacts on the corners if rendered with transparency.
-- The stroke is positioned inside the rectangle.
--- @param p_min ImVec2
--- @param p_max ImVec2
--- @param col ImU32
--- @param rounding float
--- @param thickness float
--- @param flags ImDrawFlags
function MT.ImDrawList:_AddRectTinyRounding(p_min, p_max, col, rounding, thickness, flags)
local screen_thickness = thickness * self._InvFringeScale
if screen_thickness < 1.0 then
if screen_thickness < 1.0 / 255.0 then
return
end
col = ImAlphaMultiply(col, screen_thickness)
screen_thickness = 1.0
thickness = self._FringeScale
end
flags = bit32.bor(flags, self.Flags)
local outer_u, outer_v, inner_u, inner_v, fringe = _SelectLineTexture(self, screen_thickness, flags)
local auto_seg_count = self:_CalcCircleAutoSegmentCount(rounding)
local arc_step, arc_step_count = CalcCornerArcStepAndCount(auto_seg_count)
local arc_point_count = arc_step_count + 1
local vtx_count = (2 + arc_point_count) * 4 + 3
local idx_count = (4 + arc_point_count - 1) * 4 * 3
PrimReserve(self, idx_count, vtx_count)
local vb = self.VtxBuffer.Buf
local ib = self.IdxBuffer.Data
local start_vtx_ptr = self._VtxWritePtr
local start_idx_ptr = self._IdxWritePtr
local vw, iw = start_vtx_ptr, start_idx_ptr
local vtx_idx = self._VtxCurrentIdx
local first_vtx_idx = vtx_idx -- [Luau] VtxBuffer position of index 'n' is start_vtx_ptr + (n - first_vtx_idx)
local half_fringe = fringe * 0.5
corner_pos_x[1] = p_min.x - half_fringe; corner_pos_y[1] = p_min.y - half_fringe
corner_pos_x[2] = p_max.x + half_fringe; corner_pos_y[2] = p_min.y - half_fringe
corner_pos_x[3] = p_max.x + half_fringe; corner_pos_y[3] = p_max.y + half_fringe
corner_pos_x[4] = p_min.x - half_fringe; corner_pos_y[4] = p_max.y + half_fringe
thickness = thickness + fringe
rounding = rounding + half_fringe
local stem_offset = ImMin(rounding, thickness) -- The function might get called with rounding slightly bigger than thickness, which is ok, but we should keep the stem inside the shape.
local ratio = stem_offset / thickness
local stem_u, stem_v = outer_u + (inner_u - outer_u) * ratio, outer_v
-- Previous vertices, copied from the last vertices.
local base_idx = vtx_idx
WV(vb, vw + 0, 0, 0, inner_u, inner_v, col)
WV(vb, vw + 1, 0, 0, stem_u, stem_v, col)
WV(vb, vw + 2, 0, 0, outer_u, outer_v, col)
vw = vw + 3; vtx_idx = vtx_idx + 3
local arc_fast_vtx = self._Data.ArcFastVtx -- 1-based
for corner = 1, 4 do
local outer_x = corner_pos_x[corner]
local outer_y = corner_pos_y[corner]
local cox, coy = corner_offset_x[corner], corner_offset_y[corner]
local stem_x = outer_x + cox * stem_offset
local stem_y = outer_y + coy * stem_offset
local inner_x = outer_x + cox * thickness
local inner_y = outer_y + coy * thickness
local is_rounded = bit32.band(flags, corner_flags[corner]) ~= 0
local arc_idx = vtx_idx
if is_rounded then
local center_x = outer_x + cox * rounding
local center_y = outer_y + coy * rounding
local a = (IM_DRAWLIST_ARCFAST_TABLE_SIZE / 4) * (corner - 1)
for _ = 0, arc_point_count - 1 do
local s = arc_fast_vtx[a + 1]
WV(vb, vw, center_x - s.x * rounding, center_y - s.y * rounding, outer_u, outer_v, col)
vw = vw + 1; vtx_idx = vtx_idx + 1
a = (a + arc_step) % IM_DRAWLIST_ARCFAST_TABLE_SIZE
end
else
WV(vb, vw, outer_x, outer_y, outer_u, outer_v, col)
vw = vw + 1; vtx_idx = vtx_idx + 1
end
local stem_idx = vtx_idx
WV(vb, vw, stem_x, stem_y, stem_u, stem_v, col)
local inner_idx = vtx_idx + 1
WV(vb, vw + 1, inner_x, inner_y, inner_u, inner_v, col)
vw = vw + 2; vtx_idx = vtx_idx + 2
-- Arc
if is_rounded then
for i = 0, arc_point_count - 2 do
ib[iw + 0] = stem_idx; ib[iw + 1] = arc_idx + i; ib[iw + 2] = arc_idx + i + 1
iw = iw + 3
end
end
-- Connect with previous
ib[iw + 0] = base_idx + 0; ib[iw + 1] = arc_idx; ib[iw + 2] = stem_idx
ib[iw + 3] = base_idx + 0; ib[iw + 4] = stem_idx; ib[iw + 5] = base_idx + 1
ib[iw + 6] = base_idx + 1; ib[iw + 7] = stem_idx; ib[iw + 8] = inner_idx
ib[iw + 9] = base_idx + 1; ib[iw + 10] = inner_idx; ib[iw + 11] = base_idx + 2
iw = iw + 12
base_idx = stem_idx - 1
end
-- Close
-- [Luau] start_vtx_ptr[0..2] = VtxBuffer.Data[_CmdHeader.VtxOffset + base_idx + 0..2] (3 vertices of 36 bytes)
buffer.copy(vb, (start_vtx_ptr - 1) * 36, vb, (start_vtx_ptr + (base_idx - first_vtx_idx) - 1) * 36, 3 * 36)
self._VtxWritePtr = vw
self._IdxWritePtr = iw
self._VtxCurrentIdx = vtx_idx
local idx_used = iw - start_idx_ptr
local vtx_used = vw - start_vtx_ptr
IM_ASSERT_PARANOID(idx_used <= idx_count and vtx_used <= vtx_count)
if idx_used < idx_count or vtx_used < vtx_count then
self:PrimUnreserve(idx_count - idx_used, vtx_count - vtx_used)
end
end
local prim_points = { ImVec2(), ImVec2(), ImVec2(), ImVec2() } -- [Luau] scratch for C++ 'const ImVec2 points[N]' locals (callees only read them)
-- 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 rounding == nil then rounding = 0.0 end
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
flags = bit32.bor(flags, self.Flags)
local stroke_pos = _GetStrokePos(flags, ImDrawFlags.StrokeInside)
if stroke_pos == ImDrawFlags.StrokeLegacy then
if bit32.band(flags, ImDrawFlags.AALines) ~= 0 then
PathRectXY(self, p_min.x + 0.50, p_min.y + 0.50, p_max.x - 0.50, p_max.y - 0.50, rounding, flags) -- [Luau] scalar math, no temporary ImVec2
else
PathRectXY(self, p_min.x + 0.50, p_min.y + 0.50, p_max.x - 0.49, p_max.y - 0.49, rounding, flags) -- Better looking lower-right corner and rounded non-AA shapes.
end
self:PathStroke(col, thickness, bit32.bor(ImDrawFlags.Closed, ImDrawFlags.JoinMiter, ImDrawFlags.StrokeCenter, bit32.band(flags, bit32.bnot(ImDrawFlags.StrokeMask_))))
return
end
local fringe_scale = self._FringeScale
local has_rounding = rounding >= 0.25 * fringe_scale -- IM_HAS_ROUNDING(rounding)
-- Calculate the outer boundary of the rectangle based on the stroke position.
local offset = 0.0 -- Inside
if stroke_pos == ImDrawFlags.StrokeCenter then
offset = thickness * 0.5
elseif stroke_pos == ImDrawFlags.StrokeCenterBiased then
offset = _CalculateCenterBiasedOffset(self, thickness)
elseif stroke_pos == ImDrawFlags.StrokeOutside then
offset = thickness
end
local outer_min_x = p_min.x - offset
local outer_min_y = p_min.y - offset
local outer_max_x = p_max.x + offset
local outer_max_y = p_max.y + offset
local outer_rounding = rounding + (has_rounding and offset or 0.0)
local width = outer_max_x - outer_min_x
local height = outer_max_y - outer_min_y
local half_min_dim = ImMin(width, height) * 0.5
if half_min_dim <= thickness then
-- Rectangle has collapsed into filled rectangle.
self:AddRectFilled(ImVec2(outer_min_x, outer_min_y), ImVec2(outer_max_x, outer_max_y), col, outer_rounding, flags)
return
end
if bit32.band(flags, ImDrawFlags.RoundMask_) == 0 then
flags = bit32.bor(flags, ImDrawFlags.RoundAll)
end
if has_rounding then
-- Constrain rounding to rect dimensions
outer_rounding = ImMin(outer_rounding, ImAbs(width) * (((bit32.band(flags, ImDrawFlags.RoundTop) == ImDrawFlags.RoundTop) or (bit32.band(flags, ImDrawFlags.RoundBottom) == ImDrawFlags.RoundBottom)) and 0.5 or 1.0) - 1.0)
outer_rounding = ImMin(outer_rounding, ImAbs(height) * (((bit32.band(flags, ImDrawFlags.RoundLeft) == ImDrawFlags.RoundLeft) or (bit32.band(flags, ImDrawFlags.RoundRight) == ImDrawFlags.RoundRight)) and 0.5 or 1.0) - 1.0)
end
local is_truncated = outer_min_x % 1 == 0 and outer_min_y % 1 == 0 and outer_max_x % 1 == 0 and outer_max_y % 1 == 0 and outer_rounding % 1 == 0 and thickness % 1 == 0 -- IM_IS_TRUNCATED4() x2
if self._FringeScaleIsInteger and is_truncated then
local s_rounding = ImTrunc64(outer_rounding * self._InvFringeScale)
local s_thickness = ImTrunc64(thickness * self._InvFringeScale)
if s_thickness <= 0 then
return
end
if s_rounding <= 0 or bit32.band(flags, ImDrawFlags.RoundMask_) == ImDrawFlags.RoundNone then
-- Pixel aligned non-rounded rect.
local opaque_uv = self._Data.TexUvWhitePixel
local u, v = opaque_uv.x, opaque_uv.y
local t = s_thickness * fringe_scale
PrimReserve(self, 8 * 3, 8)
local vb = self.VtxBuffer.Buf
local ib = self.IdxBuffer.Data
local vw = self._VtxWritePtr
local iw = self._IdxWritePtr
local idx = self._VtxCurrentIdx
WV(vb, vw + 0, outer_min_x, outer_min_y, u, v, col)
WV(vb, vw + 1, outer_min_x + t, outer_min_y + t, u, v, col)
WV(vb, vw + 2, outer_max_x, outer_min_y, u, v, col)
WV(vb, vw + 3, outer_max_x - t, outer_min_y + t, u, v, col)
WV(vb, vw + 4, outer_max_x, outer_max_y, u, v, col)
WV(vb, vw + 5, outer_max_x - t, outer_max_y - t, u, v, col)
WV(vb, vw + 6, outer_min_x, outer_max_y, u, v, col)
WV(vb, vw + 7, outer_min_x + t, outer_max_y - t, u, v, col)
ib[iw + 0] = idx + 0; ib[iw + 1] = idx + 3; ib[iw + 2] = idx + 1
ib[iw + 3] = idx + 0; ib[iw + 4] = idx + 2; ib[iw + 5] = idx + 3
ib[iw + 6] = idx + 2; ib[iw + 7] = idx + 5; ib[iw + 8] = idx + 3
ib[iw + 9] = idx + 2; ib[iw + 10] = idx + 4; ib[iw + 11] = idx + 5
ib[iw + 12] = idx + 4; ib[iw + 13] = idx + 7; ib[iw + 14] = idx + 5
ib[iw + 15] = idx + 4; ib[iw + 16] = idx + 6; ib[iw + 17] = idx + 7
ib[iw + 18] = idx + 6; ib[iw + 19] = idx + 1; ib[iw + 20] = idx + 7
ib[iw + 21] = idx + 6; ib[iw + 22] = idx + 0; ib[iw + 23] = idx + 1
self._VtxWritePtr = vw + 8
self._IdxWritePtr = iw + 24
self._VtxCurrentIdx = idx + 8
return
end
-- Textured corners are baked with AA, do not use them if no-AA is requested.
-- [Port] TODO baked round corners: allow_tex_corners -> _AddRectBaked(), falls back to the geometry path below.
end
if not has_rounding or bit32.band(flags, ImDrawFlags.RoundMask_) == ImDrawFlags.RoundNone then
local points = prim_points
points[1].x = outer_min_x; points[1].y = outer_min_y
points[2].x = outer_max_x; points[2].y = outer_min_y
points[3].x = outer_max_x; points[3].y = outer_max_y
points[4].x = outer_min_x; points[4].y = outer_max_y
self:_AddPolyline(points, 4, col, thickness, bit32.bor(ImDrawFlags.Closed, ImDrawFlags.JoinMiter, ImDrawFlags.StrokeInside), half_min_dim)
self._Path.Size = 0
else
-- Switch to specialized rendering a bit early to avoid overdraw visible in transparent shapes.
if thickness + (0.75 * fringe_scale) > outer_rounding then
-- Special case rendering to avoid rendering artifacts at the corners.
-- If rendered using the regular polyline, the stroke will fold and leave artifacts on the corners if rendered with transparency.
self:_AddRectTinyRounding(ImVec2(outer_min_x, outer_min_y), ImVec2(outer_max_x, outer_max_y), col, outer_rounding, thickness, flags)
return
end
local rounding_tl = (bit32.band(flags, ImDrawFlags.RoundTopLeft) ~= 0) and outer_rounding or 0.0
local rounding_tr = (bit32.band(flags, ImDrawFlags.RoundTopRight) ~= 0) and outer_rounding or 0.0
local rounding_br = (bit32.band(flags, ImDrawFlags.RoundBottomRight) ~= 0) and outer_rounding or 0.0
local rounding_bl = (bit32.band(flags, ImDrawFlags.RoundBottomLeft) ~= 0) and outer_rounding or 0.0
-- [Luau] inline PathArcToFast()
_PathArcToFastExXY(self, outer_min_x + rounding_tl, outer_min_y + rounding_tl, rounding_tl, 6 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 9 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 0)
_PathArcToFastExXY(self, outer_max_x - rounding_tr, outer_min_y + rounding_tr, rounding_tr, 9 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 12 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 0)
_PathArcToFastExXY(self, outer_max_x - rounding_br, outer_max_y - rounding_br, rounding_br, 0 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 3 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 0)
_PathArcToFastExXY(self, outer_min_x + rounding_bl, outer_max_y - rounding_bl, rounding_bl, 3 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 6 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 0)
self:_AddPolyline(self._Path.Data, self._Path.Size, col, thickness, bit32.bor(ImDrawFlags.Closed, ImDrawFlags.JoinMiter, ImDrawFlags.StrokeInside, bit32.band(flags, bit32.bnot(ImDrawFlags.StrokeMask_))), half_min_dim)
self._Path.Size = 0
end
end
-- [Port] TODO baked round corners: _AddRectFilledBaked() (UseTexForRoundCorners) not ported yet, AddRectFilled() falls back to the geometry path.
--- @param p_min ImVec2
--- @param p_max ImVec2
--- @param col ImU32
--- @param rounding? float
--- @param flags? ImDrawFlags
@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
local ax, ay, bx, by = p_min.x, p_min.y, p_max.x, p_max.y
local width = bx - ax
local height = by - ay
local min_dim = ImMin(width, height)
flags = bit32.bor(flags, self.Flags)
if bit32.band(flags, ImDrawFlags.StrokeMask_) == ImDrawFlags.StrokeLegacy and rounding < 0.5 then
PrimReserve(self, 6, 4) -- [Luau] local calls
PrimRectXY(self, ax, ay, bx, by, col)
return
end
-- The rect is smaller than pixel in one dimension.
local fringe_scale = self._FringeScale
if min_dim < fringe_scale then
if min_dim < (1.0 / 255.0) * fringe_scale then
return
end
if width < fringe_scale then
col = ImAlphaMultiply(col, width * self._InvFringeScale)
width = fringe_scale
end
if height < fringe_scale then
col = ImAlphaMultiply(col, height * self._InvFringeScale)
height = fringe_scale
end
local points = prim_points
points[1].x = ax; points[1].y = ay
points[2].x = ax + width; points[2].y = ay
points[3].x = ax + width; points[3].y = ay + height
points[4].x = ax; points[4].y = ay + height
self:AddConvexPolyFilled(points, 4, col, ImDrawFlags.JoinMiter)
return
end
-- When rounding non-integer and under threshold, has_rounding may be false but still affect is_truncated. Likely not worth catering for.
local has_rounding = bit32.band(flags, ImDrawFlags.RoundMask_) ~= ImDrawFlags.RoundNone and rounding >= 0.25 * fringe_scale -- IM_HAS_ROUNDING(rounding)
local is_truncated = ax % 1 == 0 and ay % 1 == 0 and bx % 1 == 0 and by % 1 == 0 and rounding % 1 == 0 -- IM_IS_TRUNCATED4() && IM_IS_TRUNCATED()
if self._FringeScaleIsInteger and is_truncated then
-- Fast path for the non-AA non-rounded rect.
if not has_rounding then
PrimReserve(self, 6, 4)
PrimRectXY(self, ax, ay, bx, by, col)
return
end
if bit32.band(flags, ImDrawFlags.RoundMask_) == 0 then
flags = bit32.bor(flags, ImDrawFlags.RoundAll)
end
rounding = ImMin(rounding, ImAbs(width) * (((bit32.band(flags, ImDrawFlags.RoundTop) == ImDrawFlags.RoundTop) or (bit32.band(flags, ImDrawFlags.RoundBottom) == ImDrawFlags.RoundBottom)) and 0.5 or 1.0) - 1.0)
rounding = ImMin(rounding, ImAbs(height) * (((bit32.band(flags, ImDrawFlags.RoundLeft) == ImDrawFlags.RoundLeft) or (bit32.band(flags, ImDrawFlags.RoundRight) == ImDrawFlags.RoundRight)) and 0.5 or 1.0) - 1.0)
local s_rounding = ImTrunc64(rounding * self._InvFringeScale)
if s_rounding <= 0 then
PrimReserve(self, 6, 4)
PrimRectXY(self, ax, ay, bx, by, col)
return
end
-- Textured corners are baked with AA, do not use them if no-AA is requested.
-- [Port] TODO baked round corners: allow_tex_corners -> _AddRectFilledBaked(), falls back to the geometry path below.
end
if not has_rounding then
local points = prim_points
points[1].x = ax; points[1].y = ay
points[2].x = bx; points[2].y = ay
points[3].x = bx; points[3].y = by
points[4].x = ax; points[4].y = by
self:AddConvexPolyFilled(points, 4, col, ImDrawFlags.JoinMiter)
else
PathRectXY(self, ax, ay, bx, by, rounding, flags)
self:PathFillConvex(col, ImDrawFlags.JoinMiter)
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
PrimReserve(self, 6, 4)
-- [Luau] inline PrimWriteIdx()/PrimWriteVtx(): direct writes, no temporary ImVec2
local idx, idx_data, idx_write_ptr = self._VtxCurrentIdx, self.IdxBuffer.Data, self._IdxWritePtr
idx_data[idx_write_ptr] = 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
self._IdxWritePtr = idx_write_ptr + 6
local vtx_data, vtx_write_ptr = self.VtxBuffer.Buf, self._VtxWritePtr
WV(vtx_data, vtx_write_ptr + 0, p_min.x, p_min.y, uv.x, uv.y, col_upr_left)
WV(vtx_data, vtx_write_ptr + 1, p_max.x, p_min.y, uv.x, uv.y, col_upr_right)
WV(vtx_data, vtx_write_ptr + 2, p_max.x, p_max.y, uv.x, uv.y, col_bot_right)
WV(vtx_data, vtx_write_ptr + 3, p_min.x, p_max.y, uv.x, uv.y, col_bot_left)
self._VtxWritePtr = vtx_write_ptr + 4
self._VtxCurrentIdx = idx + 4
end
--- @param thickness? float
--- @param flags? ImDrawFlags
function MT.ImDrawList:AddQuad(p1, p2, p3, p4, col, thickness, flags)
if thickness == nil then thickness = 1.0 end
if flags == nil then flags = 0 end
if bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
--flags |= Flags; // Not needed.
local stroke_pos = _GetStrokePos(flags, ImDrawFlags.StrokeInside)
flags = bit32.bor(bit32.band(flags, bit32.bnot(ImDrawFlags.StrokeMask_)), stroke_pos) -- As AddPolyline() default is different
local points = prim_points
ImVec2_Copy(points[1], p1); ImVec2_Copy(points[2], p2); ImVec2_Copy(points[3], p3); ImVec2_Copy(points[4], p4)
self:_AddPolyline(points, 4, col, thickness, bit32.bor(flags, ImDrawFlags.Closed), FLT_MAX)
end
function MT.ImDrawList:AddQuadFilled(p1, p2, p3, p4, col)
if bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
local points = prim_points
ImVec2_Copy(points[1], p1); ImVec2_Copy(points[2], p2); ImVec2_Copy(points[3], p3); ImVec2_Copy(points[4], p4)
self:AddConvexPolyFilled(points, 4, col)
end
--- @param thickness? float
--- @param flags? ImDrawFlags
function MT.ImDrawList:AddTriangle(p1, p2, p3, col, thickness, flags)
if thickness == nil then thickness = 1.0 end
if flags == nil then flags = 0 end
if bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
--flags |= Flags; // Not needed.
local stroke_pos = _GetStrokePos(flags, ImDrawFlags.StrokeInside)
flags = bit32.bor(bit32.band(flags, bit32.bnot(ImDrawFlags.StrokeMask_)), stroke_pos) -- As AddPolyline() default is different
local points = prim_points
ImVec2_Copy(points[1], p1); ImVec2_Copy(points[2], p2); ImVec2_Copy(points[3], p3)
self:_AddPolyline(points, 3, col, thickness, bit32.bor(flags, ImDrawFlags.Closed), FLT_MAX)
end
function MT.ImDrawList:AddTriangleFilled(p1, p2, p3, col)
if bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
local points = prim_points
ImVec2_Copy(points[1], p1); ImVec2_Copy(points[2], p2); ImVec2_Copy(points[3], p3)
self:AddConvexPolyFilled(points, 3, col)
end
--- @param center ImVec2
--- @param radius float
--- @param col ImU32
--- @param num_segments? int
--- @param thickness? float
--- @param flags? ImDrawFlags
function MT.ImDrawList:AddCircle(center, radius, col, num_segments, thickness, flags)
if num_segments == nil then num_segments = 0 end
if thickness == nil then thickness = 1.0 end
if flags == nil then flags = 0 end
if bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
flags = bit32.bor(flags, self.Flags)
local stroke_pos = _GetStrokePos(flags, ImDrawFlags.StrokeInside)
if stroke_pos == ImDrawFlags.StrokeLegacy then
radius = radius - 0.5
stroke_pos = ImDrawFlags.StrokeCenter
end
radius = ImMax(0.01, radius)
local outer_radius = radius
if stroke_pos == ImDrawFlags.StrokeCenter then
outer_radius = radius + thickness * 0.5
elseif stroke_pos == ImDrawFlags.StrokeCenterBiased then
outer_radius = radius + _CalculateCenterBiasedOffset(self, thickness)
elseif stroke_pos == ImDrawFlags.StrokeInside then
outer_radius = radius
elseif stroke_pos == ImDrawFlags.StrokeOutside then
outer_radius = radius + thickness
end
if outer_radius <= thickness then
-- The circle has collapsed into a filled circle.
self:AddCircleFilled(center, outer_radius, col, num_segments)
return
end
if num_segments <= 0 and outer_radius <= self._Data.ArcFastRadiusCutoff then
-- Use arc with automatic segment count
local a_step = math.floor(IM_DRAWLIST_ARCFAST_SAMPLE_MAX / self:_CalcCircleAutoSegmentCount(outer_radius)) -- Use outer_radius for segment count to be consistent with rounded rect.
_PathArcToFastExXY(self, center.x, center.y, radius, 0, IM_DRAWLIST_ARCFAST_SAMPLE_MAX, a_step)
self._Path.Size = self._Path.Size - 1
else
-- Explicit segment count or above fast arc cutoff (still clamp to avoid drawing insanely tessellated shapes)
if num_segments <= 0 then
num_segments = self:_CalcCircleAutoSegmentCount(outer_radius)
end
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.0) / num_segments
self:PathArcTo(center, radius, 0.0, a_max, num_segments - 1)
end
if self._Path.Size >= 3 then
-- Calculate distance to the edge of the polygon describing the circle.
-- This will be used by _AddPolyline() to clamp the inner stroke expansion, to avoid creating rendering artifacts.
local unit_apothem = ApproxApothem(self._Path.Size)
local edge_dist = radius * unit_apothem
self:_AddPolyline(self._Path.Data, self._Path.Size, col, thickness, bit32.bor(ImDrawFlags.Closed, ImDrawFlags.JoinMiter, stroke_pos), edge_dist)
end
self._Path.Size = 0
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 then
return
end
local diameter = radius * 2.0
if diameter < self._FringeScale then
if diameter < (1.0 / 255.0) * self._FringeScale then
return
end
col = ImAlphaMultiply(col, diameter * self._InvFringeScale)
radius = self._FringeScale * 0.5
end
if num_segments <= 0 and radius <= self._Data.ArcFastRadiusCutoff then
-- Use arc with automatic segment count
_PathArcToFastExXY(self, center.x, center.y, radius, 0, IM_DRAWLIST_ARCFAST_SAMPLE_MAX, 0)
self._Path.Size = self._Path.Size - 1
else
-- Explicit segment count or above cutoff (still clamp to avoid drawing insanely tessellated shapes)
if num_segments <= 0 then
num_segments = self:_CalcCircleAutoSegmentCount(radius)
end
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.0) / num_segments
self:PathArcTo(center, radius, 0.0, a_max, num_segments - 1)
end
self:PathFillConvex(col, ImDrawFlags.JoinMiter)
end
-- Guaranteed to honor 'num_segments'
--- @param thickness? float
--- @param flags? ImDrawFlags
function MT.ImDrawList:AddNgon(center, radius, col, num_segments, thickness, flags)
if thickness == nil then thickness = 1.0 end
if flags == nil then flags = 0 end
if bit32.band(col, IM_COL32_A_MASK) == 0 or num_segments <= 2 then
return
end
flags = bit32.bor(flags, self.Flags)
local stroke_pos = _GetStrokePos(flags, ImDrawFlags.StrokeInside)
if stroke_pos == ImDrawFlags.StrokeLegacy then
radius = radius - 0.5
stroke_pos = ImDrawFlags.StrokeCenter
end
radius = ImMax(0.01, radius)
-- Calculate distance to the edge of the polygon describing the circle.
-- This will be used by _AddPolyline() to clamp the inner stroke expansion, to avoid creating rendering artifacts.
local unit_apothem = ApproxApothem(num_segments)
local edge_dist = radius * unit_apothem
-- Check overestimate first before testing details.
if radius < thickness * 2.0 then
local outer_radius = radius
if stroke_pos == ImDrawFlags.StrokeCenter then
outer_radius = radius + thickness * 0.5
elseif stroke_pos == ImDrawFlags.StrokeCenterBiased then
outer_radius = radius + _CalculateCenterBiasedOffset(self, thickness)
elseif stroke_pos == ImDrawFlags.StrokeInside then
outer_radius = radius
elseif stroke_pos == ImDrawFlags.StrokeOutside then
outer_radius = radius + thickness
end
if outer_radius * unit_apothem <= thickness then
-- The polygon has collapsed into a filled polygon.
self:AddNgonFilled(center, outer_radius, col, num_segments)
return
end
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:_AddPolyline(self._Path.Data, self._Path.Size, col, thickness, bit32.bor(ImDrawFlags.Closed, ImDrawFlags.JoinMiter, stroke_pos), edge_dist)
self._Path.Size = 0
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
local diameter = radius * 2.0
if diameter < self._FringeScale then
if diameter < (1.0 / 255.0) * self._FringeScale then
return
end
col = ImAlphaMultiply(col, diameter * self._InvFringeScale)
radius = self._FringeScale * 0.5
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, ImDrawFlags.JoinMiter)
end
-- Ellipse
--- @param rot? float
--- @param num_segments? int
--- @param thickness? float
--- @param flags? ImDrawFlags
function MT.ImDrawList:AddEllipse(center, radius, col, rot, num_segments, thickness, flags)
if rot == nil then rot = 0.0 end
if num_segments == nil then num_segments = 0 end
if thickness == nil then thickness = 1.0 end
if flags == nil then flags = 0 end
if bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
local stroke_pos = _GetStrokePos(flags, ImDrawFlags.StrokeInside)
if stroke_pos == ImDrawFlags.StrokeLegacy then
stroke_pos = ImDrawFlags.StrokeCenter
end
local rad = ImVec2(radius.x, radius.y)
rad.x = ImMax(0.01, rad.x)
rad.y = ImMax(0.01, rad.y)
if num_segments <= 0 then
num_segments = self:_CalcCircleAutoSegmentCount(ImMax(rad.x, rad.y)) -- A bit pessimistic, maybe there's a better computation to do here.
end
self._Path:reserve(self._Path.Size + (num_segments + 1))
-- 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, rad, rot, 0.0, a_max, num_segments - 1)
-- Prevent the inside part of the stroke to be expanded too much to prevent some rendering artifacts.
local max_inner_offset = ImMin(rad.x, rad.y)
self:_AddPolyline(self._Path.Data, self._Path.Size, col, thickness, bit32.bor(ImDrawFlags.Closed, ImDrawFlags.JoinMiter, stroke_pos), max_inner_offset)
self._Path.Size = 0
end
--- @param rot? float
--- @param num_segments? int
function MT.ImDrawList:AddEllipseFilled(center, radius, col, rot, num_segments)
if rot == nil then rot = 0.0 end
if num_segments == nil then num_segments = 0 end
if bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
local rad = ImVec2(radius.x, radius.y)
local diameter_x = rad.x * 2.0
local diameter_y = rad.y * 2.0
local min_dim = ImMin(diameter_x, diameter_y)
-- The ellipse is smaller than pixel in one dimension.
if min_dim < self._FringeScale then
if min_dim < (1.0 / 255.0) * self._FringeScale then
return
end
if diameter_x < self._FringeScale then
col = ImAlphaMultiply(col, diameter_x * self._InvFringeScale)
rad.x = self._FringeScale * 0.5
end
if diameter_y < self._FringeScale then
col = ImAlphaMultiply(col, diameter_y * self._InvFringeScale)
rad.y = self._FringeScale * 0.5
end
end
if 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
-- 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) -- [Port] upstream passes 'radius' (not 'rad') here, kept as-is
self:PathFillConvex(col)
end
-- Cubic Bezier takes 4 controls points
--- @param num_segments? int
--- @param flags? ImDrawFlags
function MT.ImDrawList:AddBezierCubic(p1, p2, p3, p4, col, thickness, num_segments, flags)
if num_segments == nil then num_segments = 0 end
if flags == nil then flags = 0 end
if bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
self:PathLineTo(p1)
self:PathBezierCubicCurveTo(p2, p3, p4, num_segments)
self:PathStroke(col, thickness, flags)
end
-- Quadratic Bezier takes 3 controls points
--- @param num_segments? int
--- @param flags? ImDrawFlags
function MT.ImDrawList:AddBezierQuadratic(p1, p2, p3, col, thickness, num_segments, flags)
if num_segments == nil then num_segments = 0 end
if flags == nil then flags = 0 end
if bit32.band(col, IM_COL32_A_MASK) == 0 then
return
end
self:PathLineTo(p1)
self:PathBezierQuadraticCurveTo(p2, p3, num_segments)
self:PathStroke(col, thickness, flags)
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
local AddText_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 = AddText_clip_rect -- Don't modify the clip rect! -- [Luau] scratch: ImFont:RenderText() only reads it
local src = self._CmdHeader.ClipRect
clip_rect.x = src.x; clip_rect.y = src.y; clip_rect.z = src.z; clip_rect.w = src.w
if (cpu_fine_clip_rect) then
clip_rect.x = math.max(clip_rect.x, cpu_fine_clip_rect.x)
clip_rect.y = math.max(clip_rect.y, cpu_fine_clip_rect.y)
clip_rect.z = math.min(clip_rect.z, cpu_fine_clip_rect.z)
clip_rect.w = math.min(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
if flags == nil then flags = 0 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.RoundMask_) == 0 then
flags = bit32.bor(flags, ImDrawFlags.RoundAll)
end
if rounding < 0.5 or bit32.band(flags, ImDrawFlags.RoundMask_) == ImDrawFlags.RoundNone 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, ImDrawFlags.JoinMiter)
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 = math.max(cr.x, cr.z)
cr.w = math.max(cr.y, cr.w)
-- [Luau] inline self._ClipRectStack:push_back(cr)
local stack = self._ClipRectStack
local n = stack.Size + 1
if n > stack.Capacity then stack:push_back(cr) else stack.Data[n] = cr; stack.Size = n end
ImVec4_Copy(self._CmdHeader.ClipRect, cr)
self:_OnChangedClipRect()
end
function MT.ImDrawList:PushClipRectFullScreen()
local cr = self._Data.ClipRectFullscreen
self:PushClipRect(ImVec2(cr.x, cr.y), ImVec2(cr.z, cr.w))
end
function MT.ImDrawList:PopClipRect()
local stack = self._ClipRectStack
IM_ASSERT(stack.Size > 0)
stack.Size = stack.Size - 1 -- [Luau] inline pop_back()
if (stack.Size == 0) then -- LUA: No "Ternary Operator"
ImVec4_Copy(self._CmdHeader.ClipRect, self._Data.ClipRectFullscreen)
else
ImVec4_Copy(self._CmdHeader.ClipRect, stack.Data[stack.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
-- [Luau] scalar center (cx, cy); assigned to the forward-declared local, _PathArcToFastEx() wraps it
@native
function _PathArcToFastExXY(self, cx, cy, radius, a_min_sample, a_max_sample, a_step)
if not (radius >= 0.25 * self._FringeScale) then -- !IM_HAS_ROUNDING(radius)
PathLineToXY(self, cx, cy)
return
end
-- Calculate arc auto segment step size
if a_step <= 0 then
-- Calculate step size using the common function. No need to clamp, the step is already in range.
local auto_seg_count = self:_CalcCircleAutoSegmentCount(radius)
a_step = CalcCornerArcStepAndCount(auto_seg_count)
else
-- 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))
end
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 path_data, arc_fast_vtx = self._Path.Data, self._Data.ArcFastVtx -- [Luau] cache
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 = arc_fast_vtx[sample_index + 1]
local d = path_data[out_ptr]
d.x = cx + s.x * radius; d.y = cy + 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 = arc_fast_vtx[sample_index + 1]
local d = path_data[out_ptr]
d.x = cx + s.x * radius; d.y = cy + 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 = arc_fast_vtx[normalized_max_sample + 1]
local d = path_data[out_ptr]
d.x = cx + s.x * radius; d.y = cy + s.y * radius
out_ptr = out_ptr + 1
end
IM_ASSERT_PARANOID(self._Path.Size == out_ptr - 1)
end
function MT.ImDrawList:_PathArcToFastEx(center, radius, a_min_sample, a_max_sample, a_step)
_PathArcToFastExXY(self, center.x, center.y, radius, a_min_sample, a_max_sample, a_step)
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 not (radius >= 0.25 * self._FringeScale) then -- !IM_HAS_ROUNDING(radius)
self._Path:push_back(center)
return
end
_PathArcToFastExXY(self, center.x, center.y, 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 not (radius >= 0.25 * self._FringeScale) then -- !IM_HAS_ROUNDING(radius)
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!
local path = self._Path
path:reserve(path.Size + (num_segments + 1))
-- [Luau] write into the reserved ImVec2 slots: no temporary ImVec2, no push_back() method lookups
local data, size = path.Data, path.Size
for i = 0, num_segments do
local a = a_min + (i / num_segments) * (a_max - a_min)
size = size + 1
local p = data[size]
p.x = center.x + ImCos(a) * radius; p.y = center.y + ImSin(a) * radius
end
path.Size = size
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 not (radius >= 0.25 * self._FringeScale) then -- !IM_HAS_ROUNDING(radius)
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
-- Calculate number of segments per circle.
-- First we calculate the step count as usual, then match to the precomputed directions.
-- This will give us the spacing between the samples (step), and how many segments to use in total for a full circle.
local auto_seg_count = self:_CalcCircleAutoSegmentCount(radius)
local arc_step, arc_step_count = CalcCornerArcStepAndCount(auto_seg_count)
local arc_seg_count = arc_step_count * 4 -- Seg count for full circle.
-- We are going to use precomputed values for mid samples.
-- Determine first and last sample in lookup table that belong to the arc.
-- In first step we use the coarser segment count, and after rounding, convert back to the precomputed array index.
local a_min_sample_f = arc_seg_count * a_min / (IM_PI * 2.0)
local a_max_sample_f = arc_seg_count * 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)) * arc_step
local a_max_sample = (a_is_reverse and ImCeil(a_max_sample_f) or ImFloor(a_max_sample_f)) * arc_step
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 PathLineToXY(self, center.x + ImCos(a_min) * radius, center.y + ImSin(a_min) * radius) end -- [Luau] no temporary ImVec2
if a_mid_samples > 0 then _PathArcToFastExXY(self, center.x, center.y, radius, a_min_sample, a_max_sample, arc_step) end
if a_emit_end then PathLineToXY(self, 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
flags = bit32.bor(flags, draw_list.Flags)
if bit32.band(flags, ImDrawFlags.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)
lookup, lookup_n, glyphs = baked.IndexLookup.Data, baked.IndexLookup.Size, baked.Glyphs.Data -- [Luau] may have loaded glyphs
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
local checkmark_p = ImVec2() -- [Luau] scratch: PathLineTo() copies the point
function ImGui.RenderCheckMark(draw_list, pos, col, sz)
local thickness = math.max(sz / 5.0, 1.0)
sz = sz - thickness * 0.5
local pos_x, pos_y = pos.x + thickness * 0.25, pos.y + thickness * 0.25 -- [Luau] scalar pos
local third = sz / 3.0
local bx = pos_x + third
local by = pos_y + sz - third * 0.5
local p = checkmark_p
p.x = bx - third; p.y = by - third
draw_list:PathLineTo(p)
p.x = bx; p.y = by
draw_list:PathLineTo(p)
p.x = bx + third * 2.0; p.y = by - third * 2.0
draw_list:PathLineTo(p)
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.RoundNone
if round_t and round_l then
flags = bit32.bor(flags, ImDrawFlags.RoundTopLeft)
end
if round_t and round_r then
flags = bit32.bor(flags, ImDrawFlags.RoundTopRight)
end
if round_b and round_l then
flags = bit32.bor(flags, ImDrawFlags.RoundBottomLeft)
end
if round_b and round_r then
flags = bit32.bor(flags, ImDrawFlags.RoundBottomRight)
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.RoundMask_) == 0 then
flags = ImDrawFlags.RoundAll
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.RoundNone -- FIXME: Could use CalcRoundingFlagsForRectInRect()
if y1 <= p_min.y then
if x1 <= p_min.x then cell_flags = bit32.bor(cell_flags, ImDrawFlags.RoundTopLeft) end
if x2 >= p_max.x then cell_flags = bit32.bor(cell_flags, ImDrawFlags.RoundTopRight) end
end
if y2 >= p_max.y then
if x1 <= p_min.x then cell_flags = bit32.bor(cell_flags, ImDrawFlags.RoundBottomLeft) end
if x2 >= p_max.x then cell_flags = bit32.bor(cell_flags, ImDrawFlags.RoundBottomRight) end
end
-- Combine flags
if flags == ImDrawFlags.RoundNone or cell_flags == ImDrawFlags.RoundNone then
cell_flags = ImDrawFlags.RoundNone
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.RoundNone, f(fill_U, D.RoundTopLeft), f(fill_D, D.RoundBottomLeft))) 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.RoundNone, f(fill_U, D.RoundTopRight), f(fill_D, D.RoundBottomRight))) 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.RoundNone, f(fill_L, D.RoundTopLeft), f(fill_R, D.RoundTopRight))) 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.RoundNone, f(fill_L, D.RoundBottomLeft), f(fill_R, D.RoundBottomRight))) 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.RoundTopLeft) 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.RoundTopRight) 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.RoundBottomLeft) 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.RoundBottomRight) end
end
-------------------------------------------------------------------------------
-- Extra path functions (elliptical arc, bezier curves) and concave fill
-------------------------------------------------------------------------------
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
IM_ASSERT(self._Data.CurveTessellationMaxError > 0.0)
local max_error_sqr = (self._Data.CurveTessellationMaxError * self._FringeScale) * (self._Data.CurveTessellationMaxError * self._FringeScale)
PathBezierCubicCurveToCasteljau(self._Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, max_error_sqr, 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
IM_ASSERT(self._Data.CurveTessellationMaxError > 0.0)
local max_error_sqr = (self._Data.CurveTessellationMaxError * self._FringeScale) * (self._Data.CurveTessellationMaxError * self._FringeScale)
PathBezierQuadraticCurveToCasteljau(self._Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, max_error_sqr, 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
-- 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