fix sizing bug

This commit is contained in:
Michael Freno
2025-11-11 10:03:05 -05:00
parent 6d8324f61b
commit c19befa29e

View File

@@ -350,7 +350,7 @@ function Element.new(props)
-- Scroll state for text overflow -- Scroll state for text overflow
self._textScrollX = 0 -- Horizontal scroll offset in pixels self._textScrollX = 0 -- Horizontal scroll offset in pixels
-- Restore state from StateManager in immediate mode -- Restore state from StateManager in immediate mode
if Gui._immediateMode and self._stateId then if Gui._immediateMode and self._stateId then
local state = StateManager.getState(self._stateId) local state = StateManager.getState(self._stateId)
@@ -360,17 +360,17 @@ function Element.new(props)
self._focused = true self._focused = true
Gui._focusedElement = self Gui._focusedElement = self
end end
-- Restore text buffer (prefer state over props for immediate mode) -- Restore text buffer (prefer state over props for immediate mode)
if state._textBuffer and state._textBuffer ~= "" then if state._textBuffer and state._textBuffer ~= "" then
self._textBuffer = state._textBuffer self._textBuffer = state._textBuffer
end end
-- Restore cursor position -- Restore cursor position
if state._cursorPosition then if state._cursorPosition then
self._cursorPosition = state._cursorPosition self._cursorPosition = state._cursorPosition
end end
-- Restore selection -- Restore selection
if state._selectionStart then if state._selectionStart then
self._selectionStart = state._selectionStart self._selectionStart = state._selectionStart
@@ -436,12 +436,12 @@ function Element.new(props)
else else
self.text = props.text self.text = props.text
end end
-- Sync self.text with restored _textBuffer for editable elements in immediate mode -- Sync self.text with restored _textBuffer for editable elements in immediate mode
if self.editable and Gui._immediateMode and self._textBuffer then if self.editable and Gui._immediateMode and self._textBuffer then
self.text = self._textBuffer self.text = self._textBuffer
end end
self.textAlign = props.textAlign or TextAlign.START self.textAlign = props.textAlign or TextAlign.START
-- Image properties -- Image properties
@@ -629,10 +629,19 @@ function Element.new(props)
self.width = tempWidth self.width = tempWidth
else else
self.autosizing.width = true self.autosizing.width = true
-- Calculate auto-width without padding first -- Special case: if textWrap is enabled and parent exists, constrain width to parent
tempWidth = self:calculateAutoWidth() -- Text wrapping requires a width constraint, so use parent's content width
self.width = tempWidth if props.textWrap and self.parent and self.parent.width then
self.units.width = { value = nil, unit = "auto" } -- Mark as auto-sized tempWidth = self.parent.width
self.width = tempWidth
self.units.width = { value = 100, unit = "%" } -- Mark as parent-constrained
self.autosizing.width = false -- Not truly autosizing, constrained by parent
else
-- Calculate auto-width without padding first
tempWidth = self:calculateAutoWidth()
self.width = tempWidth
self.units.width = { value = nil, unit = "auto" } -- Mark as auto-sized
end
end end
-- Handle height (both h and height properties, prefer h if both exist) -- Handle height (both h and height properties, prefer h if both exist)
@@ -1970,17 +1979,43 @@ function Element:addChild(child)
-- Only recalculate auto-sizing if the child participates in layout -- Only recalculate auto-sizing if the child participates in layout
-- (CSS: absolutely positioned children don't affect parent auto-sizing) -- (CSS: absolutely positioned children don't affect parent auto-sizing)
if not child._explicitlyAbsolute then if not child._explicitlyAbsolute then
local sizeChanged = false
if self.autosizing.height then if self.autosizing.height then
local oldHeight = self.height
local contentHeight = self:calculateAutoHeight() local contentHeight = self:calculateAutoHeight()
-- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content -- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
self._borderBoxHeight = contentHeight + self.padding.top + self.padding.bottom self._borderBoxHeight = contentHeight + self.padding.top + self.padding.bottom
self.height = contentHeight self.height = contentHeight
if oldHeight ~= self.height then
sizeChanged = true
end
end end
if self.autosizing.width then if self.autosizing.width then
local oldWidth = self.width
local contentWidth = self:calculateAutoWidth() local contentWidth = self:calculateAutoWidth()
-- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content -- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
self._borderBoxWidth = contentWidth + self.padding.left + self.padding.right self._borderBoxWidth = contentWidth + self.padding.left + self.padding.right
self.width = contentWidth self.width = contentWidth
if oldWidth ~= self.width then
sizeChanged = true
end
end
-- Propagate size change up the tree
if sizeChanged and self.parent and (self.parent.autosizing.width or self.parent.autosizing.height) then
-- Trigger parent to recalculate its size by re-adding this child's contribution
-- This ensures grandparents are notified of size changes
if self.parent.autosizing.height then
local contentHeight = self.parent:calculateAutoHeight()
self.parent._borderBoxHeight = contentHeight + self.parent.padding.top + self.parent.padding.bottom
self.parent.height = contentHeight
end
if self.parent.autosizing.width then
local contentWidth = self.parent:calculateAutoWidth()
self.parent._borderBoxWidth = contentWidth + self.parent.padding.left + self.parent.padding.right
self.parent.width = contentWidth
end
end end
end end
@@ -3276,7 +3311,7 @@ function Element:update(dt)
self.callback(self, pressEvent) self.callback(self, pressEvent)
end end
self._pressed[button] = true self._pressed[button] = true
-- Set mouse down position for text selection on left click -- Set mouse down position for text selection on left click
if button == 1 and self.editable then if button == 1 and self.editable then
self._mouseDownPosition = self:_mouseToTextPosition(mx, my) self._mouseDownPosition = self:_mouseToTextPosition(mx, my)
@@ -3387,7 +3422,7 @@ function Element:update(dt)
if not self._textDragOccurred then if not self._textDragOccurred then
self:_handleTextClick(mx, my, clickCount) self:_handleTextClick(mx, my, clickCount)
end end
-- Reset drag flag after release -- Reset drag flag after release
self._textDragOccurred = false self._textDragOccurred = false
elseif button == 1 then elseif button == 1 then
@@ -3854,6 +3889,13 @@ function Element:calculateTextHeight()
if self.textWrap and (self.textWrap == "word" or self.textWrap == "char" or self.textWrap == true) then if self.textWrap and (self.textWrap == "word" or self.textWrap == "char" or self.textWrap == true) then
-- Calculate available width for wrapping -- Calculate available width for wrapping
local availableWidth = self.width 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 if availableWidth and availableWidth > 0 then
-- Get the wrapped text lines using getWrap (returns width and table of lines) -- Get the wrapped text lines using getWrap (returns width and table of lines)
local wrappedWidth, wrappedLines = font:getWrap(self.text, availableWidth) local wrappedWidth, wrappedLines = font:getWrap(self.text, availableWidth)
@@ -4167,7 +4209,7 @@ function Element:_resetCursorBlink(pauseBlink)
end end
self._cursorBlinkTimer = 0 self._cursorBlinkTimer = 0
self._cursorVisible = true self._cursorVisible = true
if pauseBlink then if pauseBlink then
self._cursorBlinkPaused = true -- Pause blinking while typing self._cursorBlinkPaused = true -- Pause blinking while typing
self._cursorBlinkPauseTimer = 0 -- Reset pause timer self._cursorBlinkPauseTimer = 0 -- Reset pause timer
@@ -4494,7 +4536,7 @@ function Element:insertText(text, position)
self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping
self:_updateAutoGrowHeight() -- Then update height based on new content self:_updateAutoGrowHeight() -- Then update height based on new content
self:_validateCursorPosition() self:_validateCursorPosition()
-- Reset cursor blink to show cursor and pause blinking while typing -- Reset cursor blink to show cursor and pause blinking while typing
self:_resetCursorBlink(true) self:_resetCursorBlink(true)
@@ -4533,7 +4575,7 @@ function Element:deleteText(startPos, endPos)
self:_markTextDirty() self:_markTextDirty()
self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping
self:_updateAutoGrowHeight() -- Then update height based on new content self:_updateAutoGrowHeight() -- Then update height based on new content
-- Reset cursor blink to show cursor and pause blinking while deleting -- Reset cursor blink to show cursor and pause blinking while deleting
self:_resetCursorBlink(true) self:_resetCursorBlink(true)
@@ -4660,7 +4702,7 @@ function Element:_wrapLine(line, maxWidth)
local tokens = {} local tokens = {}
local pos = 1 local pos = 1
local lineLen = utf8.len(line) local lineLen = utf8.len(line)
while pos <= lineLen do while pos <= lineLen do
-- Check if current position is whitespace -- Check if current position is whitespace
local char = getUtf8Char(line, pos) local char = getUtf8Char(line, pos)
@@ -4674,7 +4716,7 @@ function Element:_wrapLine(line, maxWidth)
type = "space", type = "space",
text = line:sub(utf8.offset(line, wsStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line), text = line:sub(utf8.offset(line, wsStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line),
startPos = wsStart - 1, startPos = wsStart - 1,
length = pos - wsStart length = pos - wsStart,
}) })
else else
-- Collect word (non-whitespace sequence) -- Collect word (non-whitespace sequence)
@@ -4686,13 +4728,13 @@ function Element:_wrapLine(line, maxWidth)
type = "word", type = "word",
text = line:sub(utf8.offset(line, wordStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line), text = line:sub(utf8.offset(line, wordStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line),
startPos = wordStart - 1, startPos = wordStart - 1,
length = pos - wordStart length = pos - wordStart,
}) })
end end
end end
-- Process tokens and wrap -- Process tokens and wrap
local charPos = 0 -- Track our position in the original line local charPos = 0 -- Track our position in the original line
for i, token in ipairs(tokens) do for i, token in ipairs(tokens) do
if token.type == "word" then if token.type == "word" then
local testLine = currentLine .. token.text local testLine = currentLine .. token.text
@@ -4987,7 +5029,7 @@ function Element:_getSelectionRects(selStart, selEnd)
local selY = 0 local selY = 0
local selHeight = font:getHeight() local selHeight = font:getHeight()
table.insert(rects, {x = selX, y = selY, width = selWidth, height = selHeight}) table.insert(rects, { x = selX, y = selY, width = selWidth, height = selHeight })
end end
return rects return rects
@@ -5062,7 +5104,7 @@ function Element:_getSelectionRects(selStart, selEnd)
local selY = visualLineNum * lineHeight local selY = visualLineNum * lineHeight
local selHeight = lineHeight local selHeight = lineHeight
table.insert(rects, {x = selX, y = selY, width = selWidth, height = selHeight}) table.insert(rects, { x = selX, y = selY, width = selWidth, height = selHeight })
end end
visualLineNum = visualLineNum + 1 visualLineNum = visualLineNum + 1
@@ -5090,7 +5132,7 @@ function Element:_getSelectionRects(selStart, selEnd)
local selY = visualLineNum * lineHeight local selY = visualLineNum * lineHeight
local selHeight = lineHeight local selHeight = lineHeight
table.insert(rects, {x = selX, y = selY, width = selWidth, height = selHeight}) table.insert(rects, { x = selX, y = selY, width = selWidth, height = selHeight })
visualLineNum = visualLineNum + 1 visualLineNum = visualLineNum + 1
end end
else else
@@ -5229,7 +5271,7 @@ function Element:_mouseToTextPosition(mouseX, mouseY)
end end
-- === MULTILINE TEXT HANDLING === -- === MULTILINE TEXT HANDLING ===
-- Update text wrapping if dirty -- Update text wrapping if dirty
self:_updateTextIfDirty() self:_updateTextIfDirty()
@@ -5243,7 +5285,7 @@ function Element:_mouseToTextPosition(mouseX, mouseY)
end end
local lineHeight = font:getHeight() local lineHeight = font:getHeight()
-- Get text area width for wrapping calculations -- Get text area width for wrapping calculations
local textAreaWidth = self.width local textAreaWidth = self.width
local scaledContentPadding = self:getScaledContentPadding() local scaledContentPadding = self:getScaledContentPadding()
@@ -5270,32 +5312,32 @@ function Element:_mouseToTextPosition(mouseX, mouseY)
-- If text wrapping is enabled, handle wrapped segments -- If text wrapping is enabled, handle wrapped segments
if self.textWrap and textAreaWidth > 0 then if self.textWrap and textAreaWidth > 0 then
local wrappedSegments = self:_wrapLine(clickedLine, textAreaWidth) local wrappedSegments = self:_wrapLine(clickedLine, textAreaWidth)
-- Determine which wrapped segment was clicked -- Determine which wrapped segment was clicked
local lineYOffset = (clickedLineNum - 1) * lineHeight local lineYOffset = (clickedLineNum - 1) * lineHeight
local segmentNum = math.floor((relativeY - lineYOffset) / lineHeight) + 1 local segmentNum = math.floor((relativeY - lineYOffset) / lineHeight) + 1
segmentNum = math.max(1, math.min(segmentNum, #wrappedSegments)) segmentNum = math.max(1, math.min(segmentNum, #wrappedSegments))
local segment = wrappedSegments[segmentNum] local segment = wrappedSegments[segmentNum]
-- Find closest position within the segment -- Find closest position within the segment
local segmentText = segment.text local segmentText = segment.text
local segmentLen = utf8.len(segmentText) or 0 local segmentLen = utf8.len(segmentText) or 0
local closestPos = segment.startIdx local closestPos = segment.startIdx
local closestDist = math.huge local closestDist = math.huge
for i = 0, segmentLen do for i = 0, segmentLen do
local offset = utf8.offset(segmentText, i + 1) local offset = utf8.offset(segmentText, i + 1)
local beforeText = offset and segmentText:sub(1, offset - 1) or segmentText local beforeText = offset and segmentText:sub(1, offset - 1) or segmentText
local textWidth = font:getWidth(beforeText) local textWidth = font:getWidth(beforeText)
local dist = math.abs(relativeX - textWidth) local dist = math.abs(relativeX - textWidth)
if dist < closestDist then if dist < closestDist then
closestDist = dist closestDist = dist
closestPos = segment.startIdx + i closestPos = segment.startIdx + i
end end
end end
return charOffset + closestPos return charOffset + closestPos
end end