continued refactor
This commit is contained in:
@@ -2095,95 +2095,18 @@ end
|
|||||||
--- Calculate text width for button
|
--- Calculate text width for button
|
||||||
---@return number
|
---@return number
|
||||||
function Element:calculateTextWidth()
|
function Element:calculateTextWidth()
|
||||||
if self.text == nil then
|
if not self._layoutEngine then
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
|
return self._layoutEngine:calculateTextWidth()
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return number
|
---@return number
|
||||||
function Element:calculateTextHeight()
|
function Element:calculateTextHeight()
|
||||||
if self.text == nil then
|
if not self._layoutEngine then
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
|
return self._layoutEngine:calculateTextHeight()
|
||||||
-- 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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Element:calculateAutoWidth()
|
function Element:calculateAutoWidth()
|
||||||
@@ -2516,206 +2439,17 @@ end
|
|||||||
---@param line string -- Line to wrap
|
---@param line string -- Line to wrap
|
||||||
---@param maxWidth number -- Maximum width in pixels
|
---@param maxWidth number -- Maximum width in pixels
|
||||||
---@return table -- Array of wrapped line parts
|
---@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)
|
function Element:_wrapLine(line, maxWidth)
|
||||||
if not self.editable then
|
return self._renderer:wrapLine(self, line, maxWidth)
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return love.Font
|
---@return love.Font
|
||||||
function Element:_getFont()
|
function Element:_getFont()
|
||||||
-- Get font path from theme or element
|
return self._renderer:getFont(self)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
|
|||||||
@@ -534,13 +534,89 @@ function LayoutEngine:layoutChildren()
|
|||||||
end
|
end
|
||||||
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
|
--- Calculate auto width based on children
|
||||||
---@return number The calculated width
|
---@return number The calculated width
|
||||||
function LayoutEngine:calculateAutoWidth()
|
function LayoutEngine:calculateAutoWidth()
|
||||||
local element = self.element
|
local element = self.element
|
||||||
|
|
||||||
-- BORDER-BOX MODEL: Calculate content width, caller will add padding to get border-box
|
-- 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
|
if not element.children or #element.children == 0 then
|
||||||
return contentWidth
|
return contentWidth
|
||||||
end
|
end
|
||||||
@@ -580,7 +656,7 @@ end
|
|||||||
function LayoutEngine:calculateAutoHeight()
|
function LayoutEngine:calculateAutoHeight()
|
||||||
local element = self.element
|
local element = self.element
|
||||||
|
|
||||||
local height = element:calculateTextHeight()
|
local height = self:calculateTextHeight()
|
||||||
if not element.children or #element.children == 0 then
|
if not element.children or #element.children == 0 then
|
||||||
return height
|
return height
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -351,6 +351,218 @@ function Renderer:draw(backdropCanvas)
|
|||||||
self:_drawBorders(element.x, element.y, borderBoxWidth, borderBoxHeight)
|
self:_drawBorders(element.x, element.y, borderBoxWidth, borderBoxHeight)
|
||||||
end
|
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)
|
--- Draw text content (includes text, cursor, selection, placeholder, password masking)
|
||||||
---@param element table Reference to the parent Element instance
|
---@param element table Reference to the parent Element instance
|
||||||
function Renderer:drawText(element)
|
function Renderer:drawText(element)
|
||||||
|
|||||||
@@ -325,188 +325,8 @@ function TextEditor:_wrapLine(line, maxWidth)
|
|||||||
return { { text = line, startIdx = 0, endIdx = utf8.len(line) } }
|
return { { text = line, startIdx = 0, endIdx = utf8.len(line) } }
|
||||||
end
|
end
|
||||||
|
|
||||||
local font = self:_getFont()
|
-- Delegate to Renderer
|
||||||
if not font then
|
return self._element._renderer:wrapLine(self._element, line, maxWidth)
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
@@ -1712,19 +1532,8 @@ function TextEditor:_getFont()
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Resolve font path
|
-- Delegate to Renderer
|
||||||
local fontPath = nil
|
return self._element._renderer:getFont(self._element)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---Save state to StateManager (for immediate mode)
|
---Save state to StateManager (for immediate mode)
|
||||||
|
|||||||
Reference in New Issue
Block a user