diff --git a/FlexLove.lua b/FlexLove.lua index da20b17..f0f0d32 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -132,7 +132,7 @@ function Units.parse(value) unit = "px" end - local validUnits = { px = true, ["%"] = true, vw = true, vh = true } + local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true } if not validUnits[unit] then return num, "px" end @@ -462,6 +462,7 @@ end ---@field justifySelf JustifySelf -- Alignment of the item itself along main axis (default: AUTO) ---@field alignSelf AlignSelf -- Alignment of the item itself along cross axis (default: AUTO) ---@field textSize number? -- Font size for text content +---@field autoScaleText boolean -- Whether text should auto-scale with window size (default: true) ---@field transform TransformProps -- Transform properties for animations and styling ---@field transition TransitionProps -- Transition settings for animations ---@field callback function? -- Callback function for click events @@ -492,7 +493,8 @@ Element.__index = Element ---@field titleColor Color? -- Color of the text content (default: black) ---@field textAlign TextAlign? -- Alignment of the text content (default: START) ---@field textColor Color? -- Color of the text content (default: black) ----@field textSize number|string? -- Font size for text content (default: nil) +---@field textSize number|string? -- Font size for text content (default: auto-scaled) +---@field autoScaleText boolean? -- Whether text should auto-scale with window size (default: true) ---@field positioning Positioning? -- Layout positioning mode (default: ABSOLUTE) ---@field flexDirection FlexDirection? -- Direction of flex layout (default: HORIZONTAL) ---@field justifyContent JustifyContent? -- Alignment of items along main axis (default: FLEX_START) @@ -629,17 +631,61 @@ function Element.new(props) self.padding = Units.resolveSpacing(props.padding, self.width, self.height) self.margin = Units.resolveSpacing(props.margin, self.width, self.height) - -- Store original textSize units + -- Store original textSize units and constraints + self.minTextSize = props.minTextSize + self.maxTextSize = props.maxTextSize + + -- Auto-scale text by default (can be disabled with autoScaleText = false) + if props.autoScaleText == nil then + self.autoScaleText = true + else + self.autoScaleText = props.autoScaleText + end + if props.textSize then if type(props.textSize) == "string" then local value, unit = Units.parse(props.textSize) self.units.textSize = { value = value, unit = unit } - self.textSize = Units.resolve(value, unit, viewportWidth, viewportHeight, nil) + + -- Resolve textSize based on unit type + if unit == "%" or unit == "vh" then + -- Percentage and vh are relative to viewport height + self.textSize = Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight) + elseif unit == "vw" then + -- vw is relative to viewport width + self.textSize = Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth) + elseif unit == "ew" then + -- Element width relative (will be resolved after width is set) + self.textSize = (value / 100) * self.width + elseif unit == "eh" then + -- Element height relative (will be resolved after height is set) + self.textSize = (value / 100) * self.height + else + self.textSize = Units.resolve(value, unit, viewportWidth, viewportHeight, nil) + end else + self.textSize = props.textSize self.units.textSize = { value = props.textSize, unit = "px" } end else - self.units.textSize = { value = 12, unit = "px" } + -- No textSize specified - use auto-scaling default + if self.autoScaleText then + -- Default to 1.5vh (1.5% of viewport height) for auto-scaling + self.textSize = (1.5 / 100) * viewportHeight + self.units.textSize = { value = 1.5, unit = "vh" } + else + -- Fixed 12px when auto-scaling is disabled + self.textSize = 12 + self.units.textSize = { value = nil, unit = "px" } + end + end + + -- Apply min/max constraints + if self.minTextSize and self.textSize < self.minTextSize then + self.textSize = self.minTextSize + end + if self.maxTextSize and self.textSize > self.maxTextSize then + self.textSize = self.maxTextSize end -- Store original spacing values for proper resize handling @@ -1369,10 +1415,10 @@ function Element:draw() love.graphics.setColor(textColorWithOpacity:toRGBA()) local origFont = love.graphics.getFont() - local tempFont if self.textSize then - tempFont = love.graphics.newFont(self.textSize) - love.graphics.setFont(tempFont) + -- Use cached font instead of creating new one every frame + local font = FONT_CACHE.get(self.textSize) + love.graphics.setFont(font) end local font = love.graphics.getFont() local textWidth = font:getWidth(self.text) @@ -1520,10 +1566,34 @@ function Element:recalculateUnits(newViewportWidth, newViewportHeight) end end - -- Recalculate textSize if using viewport units - if self.units.textSize.unit ~= "px" then - self.textSize = - Units.resolve(self.units.textSize.value, self.units.textSize.unit, newViewportWidth, newViewportHeight, nil) + -- Recalculate textSize if auto-scaling is enabled or using viewport/element-relative units + if self.autoScaleText and self.units.textSize.value and self.units.textSize.unit ~= "px" then + local unit = self.units.textSize.unit + local value = self.units.textSize.value + + if unit == "%" or unit == "vh" then + -- Percentage and vh are relative to viewport height + self.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, newViewportHeight) + elseif unit == "vw" then + -- vw is relative to viewport width + self.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, newViewportWidth) + elseif unit == "ew" then + -- Element width relative + self.textSize = (value / 100) * self.width + elseif unit == "eh" then + -- Element height relative + self.textSize = (value / 100) * self.height + else + self.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, nil) + end + + -- Apply min/max constraints + if self.minTextSize and self.textSize < self.minTextSize then + self.textSize = self.minTextSize + end + if self.maxTextSize and self.textSize > self.maxTextSize then + self.textSize = self.maxTextSize + end end -- Recalculate gap if using viewport or percentage units diff --git a/examples/TextAutoScaling.lua b/examples/TextAutoScaling.lua new file mode 100644 index 0000000..345379b --- /dev/null +++ b/examples/TextAutoScaling.lua @@ -0,0 +1,131 @@ +-- Example demonstrating text auto-scaling feature +-- Text automatically scales proportionally with window size by default + +package.path = package.path .. ";?.lua" +require("testing/loveStub") +local FlexLove = require("FlexLove") +local Gui = FlexLove.GUI +local Color = FlexLove.Color + +print("=== Text Auto-Scaling Examples ===\n") + +-- Example 1: Default auto-scaling (enabled by default) +print("1. Default Auto-Scaling (no textSize specified)") +print(" Text will scale proportionally with window size") +local button1 = Gui.new({ + x = 10, + y = 10, + padding = { horizontal = 16, vertical = 8 }, + text = "Auto-Scaled Button", + textAlign = "center", + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(1, 1, 1), + textColor = Color.new(1, 1, 1), +}) +print(" Initial size (800x600): textSize = " .. button1.textSize .. "px") +button1:resize(1600, 1200) +print(" After resize (1600x1200): textSize = " .. button1.textSize .. "px") +print(" Scaling factor: " .. (button1.textSize / 9.0) .. "x\n") + +-- Example 2: Disable auto-scaling for fixed text size +print("2. Auto-Scaling Disabled (autoScaleText = false)") +print(" Text remains fixed at 12px regardless of window size") +Gui.destroy() +local button2 = Gui.new({ + x = 10, + y = 60, + padding = { horizontal = 16, vertical = 8 }, + text = "Fixed Size Button", + textAlign = "center", + autoScaleText = false, + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(1, 1, 1), + textColor = Color.new(1, 1, 1), +}) +print(" Initial size (800x600): textSize = " .. button2.textSize .. "px") +button2:resize(1600, 1200) +print(" After resize (1600x1200): textSize = " .. button2.textSize .. "px") +print(" No scaling applied\n") + +-- Example 3: Custom auto-scaling with viewport units +print("3. Custom Auto-Scaling (textSize = '2vh')") +print(" Text scales at 2% of viewport height") +Gui.destroy() +local title = Gui.new({ + x = 10, + y = 110, + text = "Large Title", + textSize = "2vh", + textColor = Color.new(1, 1, 1), +}) +print(" Initial size (800x600): textSize = " .. title.textSize .. "px") +title:resize(1600, 1200) +print(" After resize (1600x1200): textSize = " .. title.textSize .. "px") +print(" Scaling factor: " .. (title.textSize / 12.0) .. "x\n") + +-- Example 4: Fixed pixel size (still auto-scales if using viewport units) +print("4. Fixed Pixel Size (textSize = 20)") +print(" Explicit pixel values don't scale") +Gui.destroy() +local button3 = Gui.new({ + x = 10, + y = 160, + padding = { horizontal = 16, vertical = 8 }, + text = "20px Button", + textSize = 20, + textAlign = "center", + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(1, 1, 1), + textColor = Color.new(1, 1, 1), +}) +print(" Initial size (800x600): textSize = " .. button3.textSize .. "px") +button3:resize(1600, 1200) +print(" After resize (1600x1200): textSize = " .. button3.textSize .. "px") +print(" Fixed at 20px\n") + +-- Example 5: Element-relative scaling +print("5. Element-Relative Scaling (textSize = '10ew')") +print(" Text scales at 10% of element width") +Gui.destroy() +local box = Gui.new({ + x = 10, + y = 210, + width = 200, + height = 100, + text = "Responsive Box", + textSize = "10ew", + textAlign = "center", + background = Color.new(0.2, 0.2, 0.2), + textColor = Color.new(1, 1, 1), +}) +print(" Initial (width=200): textSize = " .. box.textSize .. "px") +box.width = 400 +box:resize(800, 600) +print(" After width change (width=400): textSize = " .. box.textSize .. "px") +print(" Scales with element size\n") + +-- Example 6: Combining auto-scaling with min/max constraints +print("6. Auto-Scaling with Constraints") +print(" Text scales between 10px and 24px") +Gui.destroy() +local constrained = Gui.new({ + x = 10, + y = 260, + text = "Constrained Text", + textSize = "3vh", + minTextSize = 10, + maxTextSize = 24, + textColor = Color.new(1, 1, 1), +}) +print(" Initial (3vh of 600): textSize = " .. constrained.textSize .. "px") +constrained:resize(1600, 1200) +print(" After resize (3vh of 1200 = 36px, clamped): textSize = " .. constrained.textSize .. "px") +print(" Clamped to maxTextSize = 24px\n") + +print("=== Summary ===") +print("• Auto-scaling is ENABLED by default") +print("• Default scaling: 1.5vh (1.5% of viewport height)") +print("• Disable with: autoScaleText = false") +print("• Custom scaling: use vh, vw, %, ew, or eh units") +print("• Fixed sizes: use pixel values (e.g., textSize = 16)") +print("• Constraints: use minTextSize and maxTextSize") diff --git a/testing/__tests__/14_text_scaling_basic_tests.lua b/testing/__tests__/14_text_scaling_basic_tests.lua index dc69127..e0e4717 100644 --- a/testing/__tests__/14_text_scaling_basic_tests.lua +++ b/testing/__tests__/14_text_scaling_basic_tests.lua @@ -107,7 +107,7 @@ function TestTextScaling.testVhTextSize() end function TestTextScaling.testNoTextSize() - -- Create an element without textSize specified + -- Create an element without textSize specified (auto-scaling enabled by default) local element = Gui.new({ id = "testElement", width = 100, @@ -115,13 +115,32 @@ function TestTextScaling.testNoTextSize() text = "Hello World", }) - -- Check initial state - should default to some value - luaunit.assertEquals(element.units.textSize.value, nil) - luaunit.assertEquals(element.textSize, 12) -- Default fallback + -- Check initial state - should auto-scale by default (1.5vh) + luaunit.assertEquals(element.autoScaleText, true) + luaunit.assertEquals(element.units.textSize.value, 1.5) + luaunit.assertEquals(element.units.textSize.unit, "vh") + luaunit.assertEquals(element.textSize, 9.0) -- 1.5% of 600px - -- Resize should not affect default textSize + -- Resize should scale the text element:resize(1600, 1200) - luaunit.assertEquals(element.textSize, 12) + luaunit.assertEquals(element.textSize, 18.0) -- 1.5% of 1200px + + -- Test with auto-scaling disabled + local elementNoScale = Gui.new({ + id = "testElementNoScale", + width = 100, + height = 50, + text = "Hello World", + autoScaleText = false, + }) + + luaunit.assertEquals(elementNoScale.autoScaleText, false) + luaunit.assertEquals(elementNoScale.units.textSize.value, nil) + luaunit.assertEquals(elementNoScale.textSize, 12) -- Fixed 12px + + -- Resize should not affect textSize when auto-scaling is disabled + elementNoScale:resize(1600, 1200) + luaunit.assertEquals(elementNoScale.textSize, 12) end -- Edge case tests diff --git a/testing/loveStub.lua b/testing/loveStub.lua index d608314..c9f420b 100644 --- a/testing/loveStub.lua +++ b/testing/loveStub.lua @@ -17,20 +17,22 @@ function love_helper.graphics.getDimensions() end function love_helper.graphics.newFont(size) + -- Ensure size is a number + local fontSize = tonumber(size) or 12 -- Return a mock font object with basic methods return { getWidth = function(self, text) -- Handle both colon and dot syntax if type(self) == "string" then -- Called with dot syntax: font.getWidth(text) - return #self * size / 2 + return #self * fontSize / 2 else -- Called with colon syntax: font:getWidth(text) - return #text * size / 2 + return #text * fontSize / 2 end end, getHeight = function() - return size + return fontSize end, } end