From c19befa29e29ec713be8711a52ac85b34f21b794 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Tue, 11 Nov 2025 10:03:05 -0500 Subject: [PATCH] fix sizing bug --- modules/Element.lua | 102 +++++++++++++++++++++++++++++++------------- 1 file changed, 72 insertions(+), 30 deletions(-) diff --git a/modules/Element.lua b/modules/Element.lua index 776f61e..ce0055a 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -350,7 +350,7 @@ function Element.new(props) -- Scroll state for text overflow self._textScrollX = 0 -- Horizontal scroll offset in pixels - + -- Restore state from StateManager in immediate mode if Gui._immediateMode and self._stateId then local state = StateManager.getState(self._stateId) @@ -360,17 +360,17 @@ function Element.new(props) self._focused = true Gui._focusedElement = self end - + -- Restore text buffer (prefer state over props for immediate mode) if state._textBuffer and state._textBuffer ~= "" then self._textBuffer = state._textBuffer end - + -- Restore cursor position if state._cursorPosition then self._cursorPosition = state._cursorPosition end - + -- Restore selection if state._selectionStart then self._selectionStart = state._selectionStart @@ -436,12 +436,12 @@ function Element.new(props) else self.text = props.text end - + -- Sync self.text with restored _textBuffer for editable elements in immediate mode if self.editable and Gui._immediateMode and self._textBuffer then self.text = self._textBuffer end - + self.textAlign = props.textAlign or TextAlign.START -- Image properties @@ -629,10 +629,19 @@ function Element.new(props) self.width = tempWidth else self.autosizing.width = true - -- Calculate auto-width without padding first - tempWidth = self:calculateAutoWidth() - self.width = tempWidth - self.units.width = { value = nil, unit = "auto" } -- Mark as auto-sized + -- Special case: if textWrap is enabled and parent exists, constrain width to parent + -- Text wrapping requires a width constraint, so use parent's content width + if props.textWrap and self.parent and self.parent.width then + 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 -- 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 -- (CSS: absolutely positioned children don't affect parent auto-sizing) if not child._explicitlyAbsolute then + local sizeChanged = false + if self.autosizing.height then + local oldHeight = self.height local contentHeight = self:calculateAutoHeight() -- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content self._borderBoxHeight = contentHeight + self.padding.top + self.padding.bottom self.height = contentHeight + if oldHeight ~= self.height then + sizeChanged = true + end end if self.autosizing.width then + local oldWidth = self.width local contentWidth = self:calculateAutoWidth() -- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content self._borderBoxWidth = contentWidth + self.padding.left + self.padding.right 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 @@ -3276,7 +3311,7 @@ function Element:update(dt) self.callback(self, pressEvent) end self._pressed[button] = true - + -- Set mouse down position for text selection on left click if button == 1 and self.editable then self._mouseDownPosition = self:_mouseToTextPosition(mx, my) @@ -3387,7 +3422,7 @@ function Element:update(dt) if not self._textDragOccurred then self:_handleTextClick(mx, my, clickCount) end - + -- Reset drag flag after release self._textDragOccurred = false 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 -- 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) @@ -4167,7 +4209,7 @@ function Element:_resetCursorBlink(pauseBlink) end self._cursorBlinkTimer = 0 self._cursorVisible = true - + if pauseBlink then self._cursorBlinkPaused = true -- Pause blinking while typing self._cursorBlinkPauseTimer = 0 -- Reset pause timer @@ -4494,7 +4536,7 @@ function Element:insertText(text, position) self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping self:_updateAutoGrowHeight() -- Then update height based on new content self:_validateCursorPosition() - + -- Reset cursor blink to show cursor and pause blinking while typing self:_resetCursorBlink(true) @@ -4533,7 +4575,7 @@ function Element:deleteText(startPos, endPos) self:_markTextDirty() self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping self:_updateAutoGrowHeight() -- Then update height based on new content - + -- Reset cursor blink to show cursor and pause blinking while deleting self:_resetCursorBlink(true) @@ -4660,7 +4702,7 @@ function Element:_wrapLine(line, maxWidth) 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) @@ -4674,7 +4716,7 @@ function Element:_wrapLine(line, maxWidth) 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 + length = pos - wsStart, }) else -- Collect word (non-whitespace sequence) @@ -4686,13 +4728,13 @@ function Element:_wrapLine(line, maxWidth) 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 + length = pos - wordStart, }) end end -- 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 if token.type == "word" then local testLine = currentLine .. token.text @@ -4987,7 +5029,7 @@ function Element:_getSelectionRects(selStart, selEnd) local selY = 0 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 return rects @@ -5062,7 +5104,7 @@ function Element:_getSelectionRects(selStart, selEnd) local selY = visualLineNum * 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 visualLineNum = visualLineNum + 1 @@ -5090,7 +5132,7 @@ function Element:_getSelectionRects(selStart, selEnd) local selY = visualLineNum * 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 end else @@ -5229,7 +5271,7 @@ function Element:_mouseToTextPosition(mouseX, mouseY) end -- === MULTILINE TEXT HANDLING === - + -- Update text wrapping if dirty self:_updateTextIfDirty() @@ -5243,7 +5285,7 @@ function Element:_mouseToTextPosition(mouseX, mouseY) end local lineHeight = font:getHeight() - + -- Get text area width for wrapping calculations local textAreaWidth = self.width local scaledContentPadding = self:getScaledContentPadding() @@ -5270,32 +5312,32 @@ function Element:_mouseToTextPosition(mouseX, mouseY) -- If text wrapping is enabled, handle wrapped segments if self.textWrap and textAreaWidth > 0 then local wrappedSegments = self:_wrapLine(clickedLine, textAreaWidth) - + -- Determine which wrapped segment was clicked local lineYOffset = (clickedLineNum - 1) * lineHeight local segmentNum = math.floor((relativeY - lineYOffset) / lineHeight) + 1 segmentNum = math.max(1, math.min(segmentNum, #wrappedSegments)) - + local segment = wrappedSegments[segmentNum] - + -- Find closest position within the segment local segmentText = segment.text local segmentLen = utf8.len(segmentText) or 0 local closestPos = segment.startIdx local closestDist = math.huge - + for i = 0, segmentLen do local offset = utf8.offset(segmentText, i + 1) local beforeText = offset and segmentText:sub(1, offset - 1) or segmentText local textWidth = font:getWidth(beforeText) local dist = math.abs(relativeX - textWidth) - + if dist < closestDist then closestDist = dist closestPos = segment.startIdx + i end end - + return charOffset + closestPos end