continued refactor
This commit is contained in:
@@ -2095,95 +2095,18 @@ end
|
||||
--- Calculate text width for button
|
||||
---@return number
|
||||
function Element:calculateTextWidth()
|
||||
if self.text == nil then
|
||||
if not self._layoutEngine then
|
||||
return 0
|
||||
end
|
||||
|
||||
if self.textSize then
|
||||
-- Resolve font path from font family (same logic as in draw)
|
||||
local fontPath = nil
|
||||
if self.fontFamily then
|
||||
local themeToUse = self._themeManager:getTheme()
|
||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then
|
||||
fontPath = themeToUse.fonts[self.fontFamily]
|
||||
else
|
||||
fontPath = self.fontFamily
|
||||
end
|
||||
elseif self.themeComponent then
|
||||
fontPath = self._themeManager:getDefaultFontFamily()
|
||||
end
|
||||
|
||||
local tempFont = FONT_CACHE.get(self.textSize, fontPath)
|
||||
local width = tempFont:getWidth(self.text)
|
||||
-- Apply contentAutoSizingMultiplier if set
|
||||
if self.contentAutoSizingMultiplier and self.contentAutoSizingMultiplier.width then
|
||||
width = width * self.contentAutoSizingMultiplier.width
|
||||
end
|
||||
return width
|
||||
end
|
||||
|
||||
local font = love.graphics.getFont()
|
||||
local width = font:getWidth(self.text)
|
||||
-- Apply contentAutoSizingMultiplier if set
|
||||
if self.contentAutoSizingMultiplier and self.contentAutoSizingMultiplier.width then
|
||||
width = width * self.contentAutoSizingMultiplier.width
|
||||
end
|
||||
return width
|
||||
return self._layoutEngine:calculateTextWidth()
|
||||
end
|
||||
|
||||
---@return number
|
||||
function Element:calculateTextHeight()
|
||||
if self.text == nil then
|
||||
if not self._layoutEngine then
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Get the font
|
||||
local font
|
||||
if self.textSize then
|
||||
-- Resolve font path from font family (same logic as in draw)
|
||||
local fontPath = nil
|
||||
if self.fontFamily then
|
||||
local themeToUse = self._themeManager:getTheme()
|
||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then
|
||||
fontPath = themeToUse.fonts[self.fontFamily]
|
||||
else
|
||||
fontPath = self.fontFamily
|
||||
end
|
||||
elseif self.themeComponent then
|
||||
fontPath = self._themeManager:getDefaultFontFamily()
|
||||
end
|
||||
font = FONT_CACHE.get(self.textSize, fontPath)
|
||||
else
|
||||
font = love.graphics.getFont()
|
||||
end
|
||||
|
||||
local height = font:getHeight()
|
||||
|
||||
-- If text wrapping is enabled, calculate height based on wrapped lines
|
||||
if self.textWrap and (self.textWrap == "word" or self.textWrap == "char" or self.textWrap == true) then
|
||||
-- Calculate available width for wrapping
|
||||
local availableWidth = self.width
|
||||
|
||||
-- If width is not set or is 0, try to use parent's content width
|
||||
if (not availableWidth or availableWidth <= 0) and self.parent then
|
||||
-- Use parent's content width (excluding padding)
|
||||
availableWidth = self.parent.width
|
||||
end
|
||||
|
||||
if availableWidth and availableWidth > 0 then
|
||||
-- Get the wrapped text lines using getWrap (returns width and table of lines)
|
||||
local wrappedWidth, wrappedLines = font:getWrap(self.text, availableWidth)
|
||||
-- Height is line height * number of lines
|
||||
height = height * #wrappedLines
|
||||
end
|
||||
end
|
||||
|
||||
-- Apply contentAutoSizingMultiplier if set
|
||||
if self.contentAutoSizingMultiplier and self.contentAutoSizingMultiplier.height then
|
||||
height = height * self.contentAutoSizingMultiplier.height
|
||||
end
|
||||
|
||||
return height
|
||||
return self._layoutEngine:calculateTextHeight()
|
||||
end
|
||||
|
||||
function Element:calculateAutoWidth()
|
||||
@@ -2516,206 +2439,17 @@ end
|
||||
---@param line string -- Line to wrap
|
||||
---@param maxWidth number -- Maximum width in pixels
|
||||
---@return table -- Array of wrapped line parts
|
||||
--- Wrap a line of text (delegates to Renderer)
|
||||
---@param line string The line of text to wrap
|
||||
---@param maxWidth number Maximum width for wrapping
|
||||
---@return table Array of {text, startIdx, endIdx}
|
||||
function Element:_wrapLine(line, maxWidth)
|
||||
if not self.editable then
|
||||
return { { text = line, startIdx = 0, endIdx = utf8.len(line) } }
|
||||
end
|
||||
|
||||
local font = self:_getFont()
|
||||
local wrappedParts = {}
|
||||
local currentLine = ""
|
||||
local startIdx = 0
|
||||
|
||||
-- Helper function to extract a UTF-8 character by character index
|
||||
local function getUtf8Char(str, charIndex)
|
||||
local byteStart = utf8.offset(str, charIndex)
|
||||
if not byteStart then
|
||||
return ""
|
||||
end
|
||||
local byteEnd = utf8.offset(str, charIndex + 1)
|
||||
if byteEnd then
|
||||
return str:sub(byteStart, byteEnd - 1)
|
||||
else
|
||||
return str:sub(byteStart)
|
||||
end
|
||||
end
|
||||
|
||||
if self.textWrap == "word" then
|
||||
-- Tokenize into words and whitespace, preserving exact spacing
|
||||
local tokens = {}
|
||||
local pos = 1
|
||||
local lineLen = utf8.len(line)
|
||||
|
||||
while pos <= lineLen do
|
||||
-- Check if current position is whitespace
|
||||
local char = getUtf8Char(line, pos)
|
||||
if char:match("%s") then
|
||||
-- Collect whitespace sequence
|
||||
local wsStart = pos
|
||||
while pos <= lineLen and getUtf8Char(line, pos):match("%s") do
|
||||
pos = pos + 1
|
||||
end
|
||||
table.insert(tokens, {
|
||||
type = "space",
|
||||
text = line:sub(utf8.offset(line, wsStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line),
|
||||
startPos = wsStart - 1,
|
||||
length = pos - wsStart,
|
||||
})
|
||||
else
|
||||
-- Collect word (non-whitespace sequence)
|
||||
local wordStart = pos
|
||||
while pos <= lineLen and not getUtf8Char(line, pos):match("%s") do
|
||||
pos = pos + 1
|
||||
end
|
||||
table.insert(tokens, {
|
||||
type = "word",
|
||||
text = line:sub(utf8.offset(line, wordStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line),
|
||||
startPos = wordStart - 1,
|
||||
length = pos - wordStart,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Process tokens and wrap
|
||||
local charPos = 0 -- Track our position in the original line
|
||||
for i, token in ipairs(tokens) do
|
||||
if token.type == "word" then
|
||||
local testLine = currentLine .. token.text
|
||||
local width = font:getWidth(testLine)
|
||||
|
||||
if width > maxWidth and currentLine ~= "" then
|
||||
-- Current line is full, wrap before this word
|
||||
local currentLineLen = utf8.len(currentLine)
|
||||
table.insert(wrappedParts, {
|
||||
text = currentLine,
|
||||
startIdx = startIdx,
|
||||
endIdx = startIdx + currentLineLen,
|
||||
})
|
||||
startIdx = charPos
|
||||
currentLine = token.text
|
||||
charPos = charPos + token.length
|
||||
|
||||
-- Check if the word itself is too long - if so, break it with character wrapping
|
||||
if font:getWidth(token.text) > maxWidth then
|
||||
local wordLen = utf8.len(token.text)
|
||||
local charLine = ""
|
||||
local charStartIdx = startIdx
|
||||
|
||||
for j = 1, wordLen do
|
||||
local char = getUtf8Char(token.text, j)
|
||||
local testCharLine = charLine .. char
|
||||
local charWidth = font:getWidth(testCharLine)
|
||||
|
||||
if charWidth > maxWidth and charLine ~= "" then
|
||||
table.insert(wrappedParts, {
|
||||
text = charLine,
|
||||
startIdx = charStartIdx,
|
||||
endIdx = charStartIdx + utf8.len(charLine),
|
||||
})
|
||||
charStartIdx = charStartIdx + utf8.len(charLine)
|
||||
charLine = char
|
||||
else
|
||||
charLine = testCharLine
|
||||
end
|
||||
end
|
||||
|
||||
currentLine = charLine
|
||||
startIdx = charStartIdx
|
||||
end
|
||||
elseif width > maxWidth and currentLine == "" then
|
||||
-- Word is too long to fit on a line by itself - use character wrapping
|
||||
local wordLen = utf8.len(token.text)
|
||||
local charLine = ""
|
||||
local charStartIdx = startIdx
|
||||
|
||||
for j = 1, wordLen do
|
||||
local char = getUtf8Char(token.text, j)
|
||||
local testCharLine = charLine .. char
|
||||
local charWidth = font:getWidth(testCharLine)
|
||||
|
||||
if charWidth > maxWidth and charLine ~= "" then
|
||||
table.insert(wrappedParts, {
|
||||
text = charLine,
|
||||
startIdx = charStartIdx,
|
||||
endIdx = charStartIdx + utf8.len(charLine),
|
||||
})
|
||||
charStartIdx = charStartIdx + utf8.len(charLine)
|
||||
charLine = char
|
||||
else
|
||||
charLine = testCharLine
|
||||
end
|
||||
end
|
||||
|
||||
currentLine = charLine
|
||||
startIdx = charStartIdx
|
||||
charPos = charPos + token.length
|
||||
else
|
||||
currentLine = testLine
|
||||
charPos = charPos + token.length
|
||||
end
|
||||
else
|
||||
-- It's whitespace - add to current line
|
||||
currentLine = currentLine .. token.text
|
||||
charPos = charPos + token.length
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Character wrapping
|
||||
local lineLength = utf8.len(line)
|
||||
for i = 1, lineLength do
|
||||
local char = getUtf8Char(line, i)
|
||||
local testLine = currentLine .. char
|
||||
local width = font:getWidth(testLine)
|
||||
|
||||
if width > maxWidth and currentLine ~= "" then
|
||||
table.insert(wrappedParts, {
|
||||
text = currentLine,
|
||||
startIdx = startIdx,
|
||||
endIdx = startIdx + utf8.len(currentLine),
|
||||
})
|
||||
currentLine = char
|
||||
startIdx = i - 1
|
||||
else
|
||||
currentLine = testLine
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Add remaining text
|
||||
if currentLine ~= "" then
|
||||
table.insert(wrappedParts, {
|
||||
text = currentLine,
|
||||
startIdx = startIdx,
|
||||
endIdx = startIdx + utf8.len(currentLine),
|
||||
})
|
||||
end
|
||||
|
||||
-- Ensure at least one part
|
||||
if #wrappedParts == 0 then
|
||||
table.insert(wrappedParts, {
|
||||
text = "",
|
||||
startIdx = 0,
|
||||
endIdx = 0,
|
||||
})
|
||||
end
|
||||
|
||||
return wrappedParts
|
||||
return self._renderer:wrapLine(self, line, maxWidth)
|
||||
end
|
||||
|
||||
---@return love.Font
|
||||
function Element:_getFont()
|
||||
-- Get font path from theme or element
|
||||
local fontPath = nil
|
||||
if self.fontFamily then
|
||||
local themeToUse = self._themeManager:getTheme()
|
||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then
|
||||
fontPath = themeToUse.fonts[self.fontFamily]
|
||||
else
|
||||
fontPath = self.fontFamily
|
||||
end
|
||||
end
|
||||
|
||||
return FONT_CACHE.getFont(self.textSize, fontPath)
|
||||
return self._renderer:getFont(self)
|
||||
end
|
||||
|
||||
-- ====================
|
||||
|
||||
@@ -534,13 +534,89 @@ function LayoutEngine:layoutChildren()
|
||||
end
|
||||
end
|
||||
|
||||
--- Calculate text width
|
||||
---@return number The calculated text width
|
||||
function LayoutEngine:calculateTextWidth()
|
||||
local element = self.element
|
||||
|
||||
if element.text == nil then
|
||||
return 0
|
||||
end
|
||||
|
||||
if element.textSize then
|
||||
-- Get font from Renderer (Phase 1 integration)
|
||||
local font = element._renderer:getFont(element)
|
||||
local width = font:getWidth(element.text)
|
||||
-- Apply contentAutoSizingMultiplier if set
|
||||
if element.contentAutoSizingMultiplier and element.contentAutoSizingMultiplier.width then
|
||||
width = width * element.contentAutoSizingMultiplier.width
|
||||
end
|
||||
return width
|
||||
end
|
||||
|
||||
local font = love.graphics.getFont()
|
||||
local width = font:getWidth(element.text)
|
||||
-- Apply contentAutoSizingMultiplier if set
|
||||
if element.contentAutoSizingMultiplier and element.contentAutoSizingMultiplier.width then
|
||||
width = width * element.contentAutoSizingMultiplier.width
|
||||
end
|
||||
return width
|
||||
end
|
||||
|
||||
--- Calculate text height
|
||||
---@return number The calculated text height
|
||||
function LayoutEngine:calculateTextHeight()
|
||||
local element = self.element
|
||||
|
||||
if element.text == nil then
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Get the font
|
||||
local font
|
||||
if element.textSize then
|
||||
-- Get font from Renderer (Phase 1 integration)
|
||||
font = element._renderer:getFont(element)
|
||||
else
|
||||
font = love.graphics.getFont()
|
||||
end
|
||||
|
||||
local height = font:getHeight()
|
||||
|
||||
-- If text wrapping is enabled, calculate height based on wrapped lines
|
||||
if element.textWrap and (element.textWrap == "word" or element.textWrap == "char" or element.textWrap == true) then
|
||||
-- Calculate available width for wrapping
|
||||
local availableWidth = element.width
|
||||
|
||||
-- If width is not set or is 0, try to use parent's content width
|
||||
if (not availableWidth or availableWidth <= 0) and element.parent then
|
||||
-- Use parent's content width (excluding padding)
|
||||
availableWidth = element.parent.width
|
||||
end
|
||||
|
||||
if availableWidth and availableWidth > 0 then
|
||||
-- Get the wrapped text lines using getWrap (returns width and table of lines)
|
||||
local wrappedWidth, wrappedLines = font:getWrap(element.text, availableWidth)
|
||||
-- Height is line height * number of lines
|
||||
height = height * #wrappedLines
|
||||
end
|
||||
end
|
||||
|
||||
-- Apply contentAutoSizingMultiplier if set
|
||||
if element.contentAutoSizingMultiplier and element.contentAutoSizingMultiplier.height then
|
||||
height = height * element.contentAutoSizingMultiplier.height
|
||||
end
|
||||
|
||||
return height
|
||||
end
|
||||
|
||||
--- Calculate auto width based on children
|
||||
---@return number The calculated width
|
||||
function LayoutEngine:calculateAutoWidth()
|
||||
local element = self.element
|
||||
|
||||
-- BORDER-BOX MODEL: Calculate content width, caller will add padding to get border-box
|
||||
local contentWidth = element:calculateTextWidth()
|
||||
local contentWidth = self:calculateTextWidth()
|
||||
if not element.children or #element.children == 0 then
|
||||
return contentWidth
|
||||
end
|
||||
@@ -580,7 +656,7 @@ end
|
||||
function LayoutEngine:calculateAutoHeight()
|
||||
local element = self.element
|
||||
|
||||
local height = element:calculateTextHeight()
|
||||
local height = self:calculateTextHeight()
|
||||
if not element.children or #element.children == 0 then
|
||||
return height
|
||||
end
|
||||
|
||||
@@ -351,6 +351,218 @@ function Renderer:draw(backdropCanvas)
|
||||
self:_drawBorders(element.x, element.y, borderBoxWidth, borderBoxHeight)
|
||||
end
|
||||
|
||||
--- Get font for element (resolves from theme or fontFamily)
|
||||
---@param element table Reference to the parent Element instance
|
||||
---@return love.Font
|
||||
function Renderer:getFont(element)
|
||||
-- Get font path from theme or element
|
||||
local fontPath = nil
|
||||
if element.fontFamily then
|
||||
local themeToUse = element._themeManager:getTheme()
|
||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[element.fontFamily] then
|
||||
fontPath = themeToUse.fonts[element.fontFamily]
|
||||
else
|
||||
fontPath = element.fontFamily
|
||||
end
|
||||
end
|
||||
|
||||
return FONT_CACHE.getFont(element.textSize, fontPath)
|
||||
end
|
||||
|
||||
--- Wrap a line of text based on element's textWrap mode
|
||||
---@param element table Reference to the parent Element instance
|
||||
---@param line string The line of text to wrap
|
||||
---@param maxWidth number Maximum width for wrapping
|
||||
---@return table Array of {text, startIdx, endIdx}
|
||||
function Renderer:wrapLine(element, line, maxWidth)
|
||||
-- UTF-8 support
|
||||
local utf8 = utf8 or require("utf8")
|
||||
|
||||
if not element.editable then
|
||||
return { { text = line, startIdx = 0, endIdx = utf8.len(line) } }
|
||||
end
|
||||
|
||||
local font = self:getFont(element)
|
||||
local wrappedParts = {}
|
||||
local currentLine = ""
|
||||
local startIdx = 0
|
||||
|
||||
-- Helper function to extract a UTF-8 character by character index
|
||||
local function getUtf8Char(str, charIndex)
|
||||
local byteStart = utf8.offset(str, charIndex)
|
||||
if not byteStart then
|
||||
return ""
|
||||
end
|
||||
local byteEnd = utf8.offset(str, charIndex + 1)
|
||||
if byteEnd then
|
||||
return str:sub(byteStart, byteEnd - 1)
|
||||
else
|
||||
return str:sub(byteStart)
|
||||
end
|
||||
end
|
||||
|
||||
if element.textWrap == "word" then
|
||||
-- Tokenize into words and whitespace, preserving exact spacing
|
||||
local tokens = {}
|
||||
local pos = 1
|
||||
local lineLen = utf8.len(line)
|
||||
|
||||
while pos <= lineLen do
|
||||
-- Check if current position is whitespace
|
||||
local char = getUtf8Char(line, pos)
|
||||
if char:match("%s") then
|
||||
-- Collect whitespace sequence
|
||||
local wsStart = pos
|
||||
while pos <= lineLen and getUtf8Char(line, pos):match("%s") do
|
||||
pos = pos + 1
|
||||
end
|
||||
table.insert(tokens, {
|
||||
type = "space",
|
||||
text = line:sub(utf8.offset(line, wsStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line),
|
||||
startPos = wsStart - 1,
|
||||
length = pos - wsStart,
|
||||
})
|
||||
else
|
||||
-- Collect word (non-whitespace sequence)
|
||||
local wordStart = pos
|
||||
while pos <= lineLen and not getUtf8Char(line, pos):match("%s") do
|
||||
pos = pos + 1
|
||||
end
|
||||
table.insert(tokens, {
|
||||
type = "word",
|
||||
text = line:sub(utf8.offset(line, wordStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line),
|
||||
startPos = wordStart - 1,
|
||||
length = pos - wordStart,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Process tokens and wrap
|
||||
local charPos = 0 -- Track our position in the original line
|
||||
for i, token in ipairs(tokens) do
|
||||
if token.type == "word" then
|
||||
local testLine = currentLine .. token.text
|
||||
local width = font:getWidth(testLine)
|
||||
|
||||
if width > maxWidth and currentLine ~= "" then
|
||||
-- Current line is full, wrap before this word
|
||||
local currentLineLen = utf8.len(currentLine)
|
||||
table.insert(wrappedParts, {
|
||||
text = currentLine,
|
||||
startIdx = startIdx,
|
||||
endIdx = startIdx + currentLineLen,
|
||||
})
|
||||
startIdx = charPos
|
||||
currentLine = token.text
|
||||
charPos = charPos + token.length
|
||||
|
||||
-- Check if the word itself is too long - if so, break it with character wrapping
|
||||
if font:getWidth(token.text) > maxWidth then
|
||||
local wordLen = utf8.len(token.text)
|
||||
local charLine = ""
|
||||
local charStartIdx = startIdx
|
||||
|
||||
for j = 1, wordLen do
|
||||
local char = getUtf8Char(token.text, j)
|
||||
local testCharLine = charLine .. char
|
||||
local charWidth = font:getWidth(testCharLine)
|
||||
|
||||
if charWidth > maxWidth and charLine ~= "" then
|
||||
table.insert(wrappedParts, {
|
||||
text = charLine,
|
||||
startIdx = charStartIdx,
|
||||
endIdx = charStartIdx + utf8.len(charLine),
|
||||
})
|
||||
charStartIdx = charStartIdx + utf8.len(charLine)
|
||||
charLine = char
|
||||
else
|
||||
charLine = testCharLine
|
||||
end
|
||||
end
|
||||
|
||||
currentLine = charLine
|
||||
startIdx = charStartIdx
|
||||
end
|
||||
elseif width > maxWidth and currentLine == "" then
|
||||
-- Word is too long to fit on a line by itself - use character wrapping
|
||||
local wordLen = utf8.len(token.text)
|
||||
local charLine = ""
|
||||
local charStartIdx = startIdx
|
||||
|
||||
for j = 1, wordLen do
|
||||
local char = getUtf8Char(token.text, j)
|
||||
local testCharLine = charLine .. char
|
||||
local charWidth = font:getWidth(testCharLine)
|
||||
|
||||
if charWidth > maxWidth and charLine ~= "" then
|
||||
table.insert(wrappedParts, {
|
||||
text = charLine,
|
||||
startIdx = charStartIdx,
|
||||
endIdx = charStartIdx + utf8.len(charLine),
|
||||
})
|
||||
charStartIdx = charStartIdx + utf8.len(charLine)
|
||||
charLine = char
|
||||
else
|
||||
charLine = testCharLine
|
||||
end
|
||||
end
|
||||
|
||||
currentLine = charLine
|
||||
startIdx = charStartIdx
|
||||
charPos = charPos + token.length
|
||||
else
|
||||
currentLine = testLine
|
||||
charPos = charPos + token.length
|
||||
end
|
||||
else
|
||||
-- It's whitespace - add to current line
|
||||
currentLine = currentLine .. token.text
|
||||
charPos = charPos + token.length
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Character wrapping
|
||||
local lineLength = utf8.len(line)
|
||||
for i = 1, lineLength do
|
||||
local char = getUtf8Char(line, i)
|
||||
local testLine = currentLine .. char
|
||||
local width = font:getWidth(testLine)
|
||||
|
||||
if width > maxWidth and currentLine ~= "" then
|
||||
table.insert(wrappedParts, {
|
||||
text = currentLine,
|
||||
startIdx = startIdx,
|
||||
endIdx = startIdx + utf8.len(currentLine),
|
||||
})
|
||||
currentLine = char
|
||||
startIdx = i - 1
|
||||
else
|
||||
currentLine = testLine
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Add remaining text
|
||||
if currentLine ~= "" then
|
||||
table.insert(wrappedParts, {
|
||||
text = currentLine,
|
||||
startIdx = startIdx,
|
||||
endIdx = startIdx + utf8.len(currentLine),
|
||||
})
|
||||
end
|
||||
|
||||
-- Ensure at least one part
|
||||
if #wrappedParts == 0 then
|
||||
table.insert(wrappedParts, {
|
||||
text = "",
|
||||
startIdx = 0,
|
||||
endIdx = 0,
|
||||
})
|
||||
end
|
||||
|
||||
return wrappedParts
|
||||
end
|
||||
|
||||
--- Draw text content (includes text, cursor, selection, placeholder, password masking)
|
||||
---@param element table Reference to the parent Element instance
|
||||
function Renderer:drawText(element)
|
||||
|
||||
@@ -325,188 +325,8 @@ function TextEditor:_wrapLine(line, maxWidth)
|
||||
return { { text = line, startIdx = 0, endIdx = utf8.len(line) } }
|
||||
end
|
||||
|
||||
local font = self:_getFont()
|
||||
if not font then
|
||||
return { { text = line, startIdx = 0, endIdx = utf8.len(line) } }
|
||||
end
|
||||
|
||||
local wrappedParts = {}
|
||||
local currentLine = ""
|
||||
local startIdx = 0
|
||||
|
||||
-- Helper function to extract a UTF-8 character by character index
|
||||
local function getUtf8Char(str, charIndex)
|
||||
local byteStart = utf8.offset(str, charIndex)
|
||||
if not byteStart then
|
||||
return ""
|
||||
end
|
||||
local byteEnd = utf8.offset(str, charIndex + 1)
|
||||
if byteEnd then
|
||||
return str:sub(byteStart, byteEnd - 1)
|
||||
else
|
||||
return str:sub(byteStart)
|
||||
end
|
||||
end
|
||||
|
||||
if self.textWrap == "word" then
|
||||
-- Tokenize into words and whitespace, preserving exact spacing
|
||||
local tokens = {}
|
||||
local pos = 1
|
||||
local lineLen = utf8.len(line)
|
||||
|
||||
while pos <= lineLen do
|
||||
local char = getUtf8Char(line, pos)
|
||||
if char:match("%s") then
|
||||
-- Collect whitespace sequence
|
||||
local wsStart = pos
|
||||
while pos <= lineLen and getUtf8Char(line, pos):match("%s") do
|
||||
pos = pos + 1
|
||||
end
|
||||
table.insert(tokens, {
|
||||
type = "space",
|
||||
text = line:sub(utf8.offset(line, wsStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line),
|
||||
startPos = wsStart - 1,
|
||||
length = pos - wsStart,
|
||||
})
|
||||
else
|
||||
-- Collect word
|
||||
local wordStart = pos
|
||||
while pos <= lineLen and not getUtf8Char(line, pos):match("%s") do
|
||||
pos = pos + 1
|
||||
end
|
||||
table.insert(tokens, {
|
||||
type = "word",
|
||||
text = line:sub(utf8.offset(line, wordStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line),
|
||||
startPos = wordStart - 1,
|
||||
length = pos - wordStart,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Process tokens and wrap
|
||||
local charPos = 0
|
||||
for i, token in ipairs(tokens) do
|
||||
if token.type == "word" then
|
||||
local testLine = currentLine .. token.text
|
||||
local width = font:getWidth(testLine)
|
||||
|
||||
if width > maxWidth and currentLine ~= "" then
|
||||
-- Current line is full, wrap before this word
|
||||
local currentLineLen = utf8.len(currentLine)
|
||||
table.insert(wrappedParts, {
|
||||
text = currentLine,
|
||||
startIdx = startIdx,
|
||||
endIdx = startIdx + currentLineLen,
|
||||
})
|
||||
startIdx = charPos
|
||||
currentLine = token.text
|
||||
charPos = charPos + token.length
|
||||
|
||||
-- Check if the word itself is too long
|
||||
if font:getWidth(token.text) > maxWidth then
|
||||
local wordLen = utf8.len(token.text)
|
||||
local charLine = ""
|
||||
local charStartIdx = startIdx
|
||||
|
||||
for j = 1, wordLen do
|
||||
local char = getUtf8Char(token.text, j)
|
||||
local testCharLine = charLine .. char
|
||||
local charWidth = font:getWidth(testCharLine)
|
||||
|
||||
if charWidth > maxWidth and charLine ~= "" then
|
||||
table.insert(wrappedParts, {
|
||||
text = charLine,
|
||||
startIdx = charStartIdx,
|
||||
endIdx = charStartIdx + utf8.len(charLine),
|
||||
})
|
||||
charStartIdx = charStartIdx + utf8.len(charLine)
|
||||
charLine = char
|
||||
else
|
||||
charLine = testCharLine
|
||||
end
|
||||
end
|
||||
|
||||
currentLine = charLine
|
||||
startIdx = charStartIdx
|
||||
end
|
||||
elseif width > maxWidth and currentLine == "" then
|
||||
-- Word is too long to fit on a line by itself
|
||||
local wordLen = utf8.len(token.text)
|
||||
local charLine = ""
|
||||
local charStartIdx = startIdx
|
||||
|
||||
for j = 1, wordLen do
|
||||
local char = getUtf8Char(token.text, j)
|
||||
local testCharLine = charLine .. char
|
||||
local charWidth = font:getWidth(testCharLine)
|
||||
|
||||
if charWidth > maxWidth and charLine ~= "" then
|
||||
table.insert(wrappedParts, {
|
||||
text = charLine,
|
||||
startIdx = charStartIdx,
|
||||
endIdx = charStartIdx + utf8.len(charLine),
|
||||
})
|
||||
charStartIdx = charStartIdx + utf8.len(charLine)
|
||||
charLine = char
|
||||
else
|
||||
charLine = testCharLine
|
||||
end
|
||||
end
|
||||
|
||||
currentLine = charLine
|
||||
startIdx = charStartIdx
|
||||
charPos = charPos + token.length
|
||||
else
|
||||
currentLine = testLine
|
||||
charPos = charPos + token.length
|
||||
end
|
||||
else
|
||||
-- It's whitespace - add to current line
|
||||
currentLine = currentLine .. token.text
|
||||
charPos = charPos + token.length
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Character wrapping
|
||||
local lineLength = utf8.len(line)
|
||||
for i = 1, lineLength do
|
||||
local char = getUtf8Char(line, i)
|
||||
local testLine = currentLine .. char
|
||||
local width = font:getWidth(testLine)
|
||||
|
||||
if width > maxWidth and currentLine ~= "" then
|
||||
table.insert(wrappedParts, {
|
||||
text = currentLine,
|
||||
startIdx = startIdx,
|
||||
endIdx = startIdx + utf8.len(currentLine),
|
||||
})
|
||||
currentLine = char
|
||||
startIdx = i - 1
|
||||
else
|
||||
currentLine = testLine
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Add remaining text
|
||||
if currentLine ~= "" then
|
||||
table.insert(wrappedParts, {
|
||||
text = currentLine,
|
||||
startIdx = startIdx,
|
||||
endIdx = startIdx + utf8.len(currentLine),
|
||||
})
|
||||
end
|
||||
|
||||
-- Ensure at least one part
|
||||
if #wrappedParts == 0 then
|
||||
table.insert(wrappedParts, {
|
||||
text = "",
|
||||
startIdx = 0,
|
||||
endIdx = 0,
|
||||
})
|
||||
end
|
||||
|
||||
return wrappedParts
|
||||
-- Delegate to Renderer
|
||||
return self._element._renderer:wrapLine(self._element, line, maxWidth)
|
||||
end
|
||||
|
||||
-- ====================
|
||||
@@ -1712,19 +1532,8 @@ function TextEditor:_getFont()
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Resolve font path
|
||||
local fontPath = nil
|
||||
if self._element.fontFamily then
|
||||
local Theme = req("Theme")
|
||||
local themeToUse = self._element.theme and Theme.get(self._element.theme) or Theme.getActive()
|
||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[self._element.fontFamily] then
|
||||
fontPath = themeToUse.fonts[self._element.fontFamily]
|
||||
else
|
||||
fontPath = self._element.fontFamily
|
||||
end
|
||||
end
|
||||
|
||||
return FONT_CACHE.getFont(self._element.textSize, fontPath)
|
||||
-- Delegate to Renderer
|
||||
return self._element._renderer:getFont(self._element)
|
||||
end
|
||||
|
||||
---Save state to StateManager (for immediate mode)
|
||||
|
||||
Reference in New Issue
Block a user