diff --git a/.gitignore b/.gitignore index 363fc82..40a389f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ themes/metal/ themes/space/ .DS_STORE tasks -testoutput +testoutput* luacov.* docs/doc.json docs/doc.md diff --git a/FlexLove.lua b/FlexLove.lua index 771c31d..0ad7003 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -147,7 +147,8 @@ flexlove._gcState = { -- Deferred callback queue for operations that cannot run while Canvas is active flexlove._deferredCallbacks = {} ---- Initialize FlexLove with configuration options, set refence scale for autoscaling on window resize, immediate mode, and error logging / error file path +--- Set up FlexLove for your application's specific needs - configure responsive scaling, theming, rendering mode, and debugging tools +--- Use this to establish a consistent UI foundation that adapts to different screen sizes and provides performance insights ---@param config {baseScale?: {width?:number, height?:number}, theme?: string|ThemeDefinition, immediateMode?: boolean, stateRetentionFrames?: number, maxStateEntries?: number, autoFrameManagement?: boolean, errorLogFile?: string, enableErrorLogging?: boolean, performanceMonitoring?: boolean, performanceWarnings?: boolean, performanceHudKey?: string, performanceHudPosition?: {x: number, y: number} } function flexlove.init(config) config = config or {} @@ -254,8 +255,8 @@ function flexlove.init(config) end end ---- Queue a callback to be executed after the current frame's canvas operations complete ---- This is necessary for operations that cannot run while a Canvas is active (e.g., love.window.setMode) +--- Safely schedule operations that modify LÖVE's rendering state (like window mode changes) to execute after all canvas operations complete +--- Prevents crashes from attempting canvas-incompatible operations during rendering ---@param callback function The callback to execute function flexlove.deferCallback(callback) if type(callback) ~= "function" then @@ -265,9 +266,8 @@ function flexlove.deferCallback(callback) table.insert(flexlove._deferredCallbacks, callback) end ---- Execute all deferred callbacks and clear the queue ---- IMPORTANT: This MUST be called at the very end of love.draw() after ALL canvases ---- have been released, including any canvases created by the application (not just FlexLove's canvases) +--- Execute deferred operations at the safest point in the render cycle - after all canvas operations are complete +--- Call this at the end of love.draw() to enable window resizing and other state-modifying operations without crashes --- @usage --- function love.draw() --- love.graphics.setCanvas(myCanvas) @@ -293,6 +293,8 @@ function flexlove.executeDeferredCallbacks() end end +--- Recalculate all UI layouts when the window size changes - ensures your interface adapts seamlessly to new dimensions +--- Hook this to love.resize() to maintain proper scaling and positioning across window size changes function flexlove.resize() local newWidth, newHeight = love.window.getMode() @@ -320,7 +322,8 @@ function flexlove.resize() end end ---- Can also be set in init() +--- Switch between immediate mode (React-like, recreates UI each frame) and retained mode (persistent elements) to match your architectural needs +--- Use immediate for simpler state management and declarative UIs, retained for performance-critical applications with complex state ---@param mode "immediate"|"retained" function flexlove.setMode(mode) if mode == "immediate" then @@ -340,13 +343,15 @@ function flexlove.setMode(mode) end end +--- Check which rendering mode is active to conditionally handle state management logic +--- Useful for libraries and reusable components that need to adapt to different rendering strategies ---@return "immediate"|"retained" function flexlove.getMode() return flexlove._immediateMode and "immediate" or "retained" end ---- Begin a new immediate mode frame ---- You do NOT need to call this directly, it will autodetect, but you can if you need more granular control - must then paired with endFrame() +--- Manually start a new frame in immediate mode for precise control over the UI lifecycle +--- Only needed when you want explicit frame boundaries; otherwise FlexLove auto-manages frames function flexlove.beginFrame() if not flexlove._immediateMode then return @@ -364,9 +369,8 @@ function flexlove.beginFrame() Context.clearFrameElements() end ---- End the current immediate mode frame ---- You do NOT need to call this directly unless you call beginFrame() manually - it will autodetect, but you can if you need more granular control ---- MUST BE PAIRED WITH beginFrame() +--- Finalize the frame in immediate mode, triggering layout calculations and state persistence +--- Only needed when manually controlling frames with beginFrame(); otherwise handled automatically function flexlove.endFrame() if not flexlove._immediateMode then return @@ -436,6 +440,8 @@ flexlove._gameCanvas = nil flexlove._backdropCanvas = nil flexlove._canvasDimensions = { width = 0, height = 0 } +--- Render all UI elements with optional backdrop blur support for glassmorphic effects +--- Place your game scene in gameDrawFunc to enable backdrop blur on UI elements; use postDrawFunc for overlays ---@param gameDrawFunc function|nil pass component draws that should be affected by a backdrop blur ---@param postDrawFunc function|nil pass component draws that should NOT be affected by a backdrop blur function flexlove.draw(gameDrawFunc, postDrawFunc) @@ -566,7 +572,8 @@ local function isAncestor(element, target) return false end ---- Find the topmost element at given coordinates +--- Determine which UI element the user is interacting with at a specific screen position +--- Essential for custom input handling, tooltips, or debugging click targets in complex layouts ---@param x number ---@param y number ---@return Element? @@ -662,6 +669,8 @@ function flexlove.getElementAtPosition(x, y) return blockingElements[1] end +--- Update all UI animations, interactions, and state changes each frame +--- Hook this to love.update() to enable hover effects, animations, text cursors, and scrolling ---@param dt number function flexlove.update(dt) -- Update Performance module with actual delta time for accurate FPS @@ -742,7 +751,8 @@ function flexlove._manageGC() -- "manual" strategy: no automatic GC, user must call flexlove.collectGarbage() end ---- Manual garbage collection control +--- Manually trigger garbage collection to prevent frame drops during critical gameplay moments +--- Use this to control when memory cleanup happens rather than letting it occur unpredictably ---@param mode? string "collect" for full GC, "step" for incremental (default: "collect") ---@param stepSize? number Work units for step mode (default: 200) function flexlove.collectGarbage(mode, stepSize) @@ -760,7 +770,8 @@ function flexlove.collectGarbage(mode, stepSize) end end ---- Set GC strategy +--- Choose how FlexLove manages memory cleanup to balance performance and memory usage for your app's needs +--- Use "manual" for tight control in performance-critical sections, "auto" for hands-off operation ---@param strategy string "auto", "periodic", "manual", or "disabled" function flexlove.setGCStrategy(strategy) if strategy == "auto" or strategy == "periodic" or strategy == "manual" or strategy == "disabled" then @@ -770,7 +781,8 @@ function flexlove.setGCStrategy(strategy) end end ---- Get GC statistics +--- Monitor memory management behavior to diagnose performance issues and tune GC settings +--- Use this to identify memory leaks or optimize garbage collection timing ---@return table stats {gcCount, framesSinceLastGC, currentMemoryMB, strategy} function flexlove.getGCStats() return { @@ -782,6 +794,8 @@ function flexlove.getGCStats() } end +--- Forward text input to focused editable elements like text fields and text areas +--- Hook this to love.textinput() to enable text entry in your UI ---@param text string function flexlove.textinput(text) if flexlove._focusedElement then @@ -789,6 +803,8 @@ function flexlove.textinput(text) end end +--- Handle keyboard input for text editing, navigation, and performance overlay toggling +--- Hook this to love.keypressed() to enable text selection, cursor movement, and the performance HUD ---@param key string ---@param scancode string ---@param isrepeat boolean @@ -800,6 +816,8 @@ function flexlove.keypressed(key, scancode, isrepeat) end end +--- Enable mouse wheel scrolling in scrollable containers and lists +--- Hook this to love.wheelmoved() to allow users to scroll through content naturally ---@param dx number ---@param dy number function flexlove.wheelmoved(dx, dy) @@ -926,7 +944,8 @@ function flexlove.wheelmoved(dx, dy) end end ---- destroys all top-level elements and resets the framework state +--- Clean up all UI elements and reset FlexLove to initial state when changing scenes or shutting down +--- Use this to prevent memory leaks when transitioning between game states or menus function flexlove.destroy() for _, win in ipairs(flexlove.topElements) do win:destroy() @@ -951,6 +970,8 @@ function flexlove.destroy() StateManager:reset() end +--- Create a new UI element with flexbox layout, styling, and interaction capabilities +--- This is your primary API for building interfaces - buttons, panels, text, images, and containers ---@param props ElementProps ---@return Element function flexlove.new(props) @@ -1058,6 +1079,8 @@ function flexlove.new(props) return element end +--- Check how many UI element states are being tracked in immediate mode to detect memory leaks +--- Use this during development to ensure states are properly cleaned up ---@return number function flexlove.getStateCount() if not flexlove._immediateMode then @@ -1066,7 +1089,8 @@ function flexlove.getStateCount() return StateManager.getStateCount() end ---- Clear state for a specific element ID +--- Remove stored state for a specific element when you know it won't be rendered again +--- Use this to immediately free memory for elements you've removed from your UI ---@param id string function flexlove.clearState(id) if not flexlove._immediateMode then @@ -1075,7 +1099,8 @@ function flexlove.clearState(id) StateManager.clearState(id) end ---- Clear all immediate mode states +--- Wipe all element state when transitioning between completely different UI screens +--- Use this for scene transitions to start with a clean slate and prevent state pollution function flexlove.clearAllStates() if not flexlove._immediateMode then return @@ -1083,7 +1108,8 @@ function flexlove.clearAllStates() StateManager.clearAllStates() end ---- Get state (immediate mode) statistics (for debugging) +--- Inspect state management metrics to diagnose performance issues and optimize immediate mode usage +--- Use this to understand state lifecycle and identify unexpected state accumulation ---@return { stateCount: number, frameNumber: number, oldestState: number|nil, newestState: number|nil } function flexlove.getStateStats() if not flexlove._immediateMode then diff --git a/modules/Animation.lua b/modules/Animation.lua index 79af2a6..41495b0 100644 --- a/modules/Animation.lua +++ b/modules/Animation.lua @@ -73,7 +73,8 @@ local Easing = { local Animation = {} Animation.__index = Animation ----Create a new animation instance +--- Build smooth, timed transitions between visual states to create polished, professional UIs +--- Use this to animate position, size, opacity, colors, and other properties with customizable easing ---@param props AnimationProps Animation properties ---@return Animation animation The new animation instance function Animation.new(props) @@ -136,7 +137,8 @@ function Animation.new(props) return self end ----Update the animation with delta time +--- Advance the animation timeline and calculate interpolated values for the current frame +--- Call this each frame to progress the animation; returns true when complete for cleanup ---@param dt number Delta time in seconds ---@param element table? Optional element reference for callbacks ---@return boolean completed True if animation is complete @@ -302,7 +304,8 @@ local function lerpTable(startTable, finalTable, easedT) return result end ----Interpolate animation values at current time +--- Calculate the current animated values between start and end states based on elapsed time +--- Use this to get the interpolated properties to apply to your element ---@return table result Interpolated values {width?, height?, opacity?, x?, y?, backgroundColor?, ...} function Animation:interpolate() -- Return cached result if not dirty (avoids recalculation) @@ -391,7 +394,8 @@ function Animation:interpolate() return result end ----Apply this animation to an element +--- Attach this animation to an element so it automatically updates and applies changes +--- Use this for hands-off animation that integrates with FlexLove's rendering system ---@param element Element The element to apply animation to function Animation:apply(element) if not ErrorHandler then @@ -417,7 +421,8 @@ function Animation:setTransformModule(TransformModule) self._Transform = TransformModule end ----Pause the animation +--- Temporarily halt the animation without losing progress +--- Use this to freeze animations during pause menus or cutscenes function Animation:pause() if self._state == "playing" or self._state == "pending" then self._paused = true @@ -425,7 +430,8 @@ function Animation:pause() end end ----Resume the animation +--- Continue a paused animation from where it left off +--- Use this to unpause animations when returning from pause menus function Animation:resume() if self._state == "paused" then self._paused = false @@ -433,24 +439,28 @@ function Animation:resume() end end ----Check if animation is paused +--- Query pause state to conditionally handle animation logic +--- Use this to sync UI behavior with animation state ---@return boolean paused function Animation:isPaused() return self._paused end ----Reverse the animation direction +--- Flip the animation to play backwards, creating smooth transitions in both directions +--- Use this for hover effects that reverse on mouse-out or toggleable UI elements function Animation:reverse() self._reversed = not self._reversed end ----Check if animation is reversed +--- Determine current playback direction for conditional animation logic +--- Use this to track which direction the animation is playing ---@return boolean reversed function Animation:isReversed() return self._reversed end ----Set animation playback speed +--- Control animation tempo for slow-motion or fast-forward effects +--- Use this for bullet-time, game speed multipliers, or debugging ---@param speed number Speed multiplier (1.0 = normal, 2.0 = double speed, 0.5 = half speed) function Animation:setSpeed(speed) if type(speed) == "number" and speed > 0 then @@ -458,13 +468,15 @@ function Animation:setSpeed(speed) end end ----Get animation playback speed +--- Check current playback speed for debugging or UI display +--- Use this to show animation speed in dev tools ---@return number speed Current speed multiplier function Animation:getSpeed() return self._speed end ----Seek to a specific time in the animation +--- Jump to any point in the animation timeline for previewing or state restoration +--- Use this to skip ahead, rewind, or restore saved animation states ---@param time number Time in seconds (clamped to 0-duration) function Animation:seek(time) if type(time) == "number" then @@ -473,13 +485,15 @@ function Animation:seek(time) end end ----Get current animation state +--- Query animation lifecycle state for conditional logic and debugging +--- Use this to determine if cleanup is needed or to prevent duplicate animations ---@return string state Current state: "pending", "playing", "paused", "completed", "cancelled" function Animation:getState() return self._state end ----Cancel the animation +--- Stop the animation immediately without completing, triggering the onCancel callback +--- Use this to abort animations when UI elements are removed or user cancels an action ---@param element table? Optional element reference for callback function Animation:cancel(element) if self._state ~= "cancelled" and self._state ~= "completed" then @@ -493,7 +507,8 @@ function Animation:cancel(element) end end ----Reset the animation to its initial state +--- Return the animation to the beginning for replay +--- Use this to reuse animation instances without recreating them function Animation:reset() self.elapsed = 0 self._hasStarted = false @@ -502,13 +517,15 @@ function Animation:reset() self._resultDirty = true end ----Get the current progress of the animation +--- Get normalized animation progress for progress bars or synchronized effects +--- Use this to drive secondary animations or display completion percentage ---@return number progress Progress from 0 to 1 function Animation:getProgress() return math.min(self.elapsed / self.duration, 1) end ----Chain another animation after this one completes +--- Create sequential animation flows that play one after another +--- Use this to build complex multi-step animations like slide-in-then-fade ---@param nextAnimation Animation|function Animation instance or factory function that returns an animation ---@return Animation nextAnimation The chained animation (for further chaining) function Animation:chain(nextAnimation) @@ -528,7 +545,8 @@ function Animation:chain(nextAnimation) end end ----Add delay before animation starts +--- Introduce a wait period before animation begins for staggered effects +--- Use this to create cascading animations or timed sequences ---@param seconds number Delay duration in seconds ---@return Animation self For chaining function Animation:delay(seconds) @@ -545,7 +563,8 @@ function Animation:delay(seconds) return self end ----Repeat animation multiple times +--- Loop the animation for pulsing effects, loading indicators, or continuous motion +--- Use this for idle animations and attention-grabbing elements ---@param count number Number of times to repeat (0 = infinite loop) ---@return Animation self For chaining function Animation:repeatCount(count) @@ -562,7 +581,8 @@ function Animation:repeatCount(count) return self end ----Enable yoyo mode (animation reverses direction on each repeat) +--- Make repeating animations play forwards then backwards for smooth oscillation +--- Use this for breathing effects, pulsing highlights, or pendulum motions ---@param enabled boolean? Enable yoyo mode (default: true) ---@return Animation self For chaining function Animation:yoyo(enabled) @@ -573,7 +593,8 @@ function Animation:yoyo(enabled) return self end ---- Create a simple fade animation +--- Quickly create fade in/out effects without manually specifying start/end states +--- Use this convenience method for common opacity transitions in tooltips, notifications, and overlays ---@param duration number Duration in seconds ---@param fromOpacity number Starting opacity (0-1) ---@param toOpacity number Ending opacity (0-1) @@ -601,7 +622,8 @@ function Animation.fade(duration, fromOpacity, toOpacity, easing) }) end ---- Create a simple scale animation +--- Quickly create grow/shrink effects without manually specifying dimensions +--- Use this convenience method for bounce effects, pop-ups, and attention animations ---@param duration number Duration in seconds ---@param fromScale {width:number,height:number} Starting scale ---@param toScale {width:number,height:number} Ending scale diff --git a/modules/AnimationGroup.lua b/modules/AnimationGroup.lua index 85d7922..86abc86 100644 --- a/modules/AnimationGroup.lua +++ b/modules/AnimationGroup.lua @@ -13,7 +13,8 @@ local ErrorHandler = nil ---@field onComplete function? Called when all animations complete: (group) ---@field onStart function? Called when group starts: (group) ---- Create a new animation group +--- Coordinate multiple animations to play together, in sequence, or staggered for complex choreographed effects +--- Use this to synchronize related UI changes like simultaneous fades or sequential reveals ---@param props AnimationGroupProps ---@return AnimationGroup group function AnimationGroup.new(props) @@ -142,7 +143,8 @@ function AnimationGroup:_updateStagger(dt, element) return allFinished end ---- Update the animation group +--- Advance all animations in the group according to their coordination mode +--- Call this each frame to progress parallel, sequential, or staggered animations ---@param dt number Delta time ---@param element table? Optional element reference for callbacks ---@return boolean finished True if group is complete @@ -191,7 +193,8 @@ function AnimationGroup:update(dt, element) return finished end ---- Pause all animations in the group +--- Freeze the entire animation sequence in unison +--- Use this to pause complex multi-part animations during game pauses function AnimationGroup:pause() self._paused = true for _, anim in ipairs(self.animations) do @@ -201,7 +204,8 @@ function AnimationGroup:pause() end end ---- Resume all animations in the group +--- Continue all paused animations simultaneously from their paused states +--- Use this to unpause coordinated animation sequences function AnimationGroup:resume() self._paused = false for _, anim in ipairs(self.animations) do @@ -211,13 +215,15 @@ function AnimationGroup:resume() end end ---- Check if group is paused +--- Determine if the entire group is currently paused +--- Use this to sync other game logic with animation group state ---@return boolean paused function AnimationGroup:isPaused() return self._paused end ---- Reverse all animations in the group +--- Flip all animations to play backwards together +--- Use this to reverse complex transitions like panel opens/closes function AnimationGroup:reverse() for _, anim in ipairs(self.animations) do if type(anim.reverse) == "function" then @@ -226,7 +232,8 @@ function AnimationGroup:reverse() end end ---- Set speed for all animations in the group +--- Control the tempo of all animations simultaneously +--- Use this for slow-motion effects or debugging without adjusting individual animations ---@param speed number Speed multiplier function AnimationGroup:setSpeed(speed) for _, anim in ipairs(self.animations) do @@ -236,7 +243,8 @@ function AnimationGroup:setSpeed(speed) end end ---- Cancel all animations in the group +--- Abort all animations in the group immediately without completion +--- Use this when UI is dismissed mid-animation or transitions are interrupted ---@param element table? Optional element reference for callbacks function AnimationGroup:cancel(element) if self._state ~= "cancelled" and self._state ~= "completed" then @@ -249,7 +257,8 @@ function AnimationGroup:cancel(element) end end ---- Reset the animation group to initial state +--- Restart the entire group from the beginning for reuse +--- Use this to replay animation sequences without recreating objects function AnimationGroup:reset() self._currentIndex = 1 self._staggerElapsed = 0 @@ -265,13 +274,15 @@ function AnimationGroup:reset() end end ---- Get the current state of the group +--- Check the overall lifecycle state of the animation group +--- Use this to conditionally trigger follow-up actions or cleanup ---@return string state "ready", "playing", "completed", "cancelled" function AnimationGroup:getState() return self._state end ---- Get the overall progress of the group (0-1) +--- Calculate completion percentage across all animations in the group +--- Use this for progress bars or to synchronize other effects with the group ---@return number progress function AnimationGroup:getProgress() if #self.animations == 0 then @@ -305,7 +316,8 @@ function AnimationGroup:getProgress() end end ---- Apply this animation group to an element +--- Attach this group to an element for automatic updates and integration +--- Use this for hands-off animation management within FlexLove's system ---@param element Element The element to apply animations to function AnimationGroup:apply(element) if not element or type(element) ~= "table" then diff --git a/modules/Color.lua b/modules/Color.lua index e7ee7f8..aa331ec 100644 --- a/modules/Color.lua +++ b/modules/Color.lua @@ -53,7 +53,8 @@ local NAMED_COLORS = { local Color = {} Color.__index = Color ---- Create a new color instance +--- Build type-safe color objects with automatic validation and clamping +--- Use this to avoid invalid color values and ensure consistent LÖVE-compatible colors (0-1 range) ---@param r number? Red component (0-1), defaults to 0 ---@param g number? Green component (0-1), defaults to 0 ---@param b number? Blue component (0-1), defaults to 0 @@ -75,7 +76,8 @@ function Color.new(r, g, b, a) return self end ----Convert color to RGBA components +--- Extract individual color channels for use with love.graphics.setColor() +--- Use this to pass colors to LÖVE's rendering functions ---@return number r Red component (0-1) ---@return number g Green component (0-1) ---@return number b Blue component (0-1) @@ -84,8 +86,8 @@ function Color:toRGBA() return self.r, self.g, self.b, self.a end ---- Convert hex string to color ---- Supports both 6-digit (#RRGGBB) and 8-digit (#RRGGBBAA) hex formats +--- Parse CSS-style hex colors into Color objects for designer-friendly workflows +--- Use this to work with colors from design tools that export hex values ---@param hexWithTag string Hex color string (e.g. "#RRGGBB" or "#RRGGBBAA") ---@return Color color The parsed color (returns white on error with warning) function Color.fromHex(hexWithTag) @@ -146,7 +148,8 @@ function Color.fromHex(hexWithTag) end end ---- Validate a single color channel value +--- Verify and sanitize individual color components to prevent rendering errors +--- Use this to safely process user input or external color data ---@param value any Value to validate ---@param max number? Maximum value (255 for 0-255 range, 1 for 0-1 range), defaults to 1 ---@return boolean valid True if valid @@ -307,7 +310,8 @@ function Color.isValidColorFormat(value) return nil end ---- Validate a color value +--- Check if a color value is usable before processing to provide clear error messages +--- Use this for config validation and debugging malformed color data ---@param value any Color value to validate ---@param options table? Validation options {allowNamed: boolean, requireAlpha: boolean} ---@return boolean valid True if valid @@ -342,7 +346,8 @@ function Color.validateColor(value, options) return true, nil end ---- Sanitize a color value (always returns a valid Color) +--- Convert any color format to a valid Color object with graceful fallbacks +--- Use this to robustly handle colors from any source without crashes ---@param value any Color value to sanitize (hex, named, table, or Color instance) ---@param default Color? Default color if invalid (defaults to black) ---@return Color color Sanitized color instance (guaranteed non-nil) @@ -418,14 +423,16 @@ function Color.sanitizeColor(value, default) return default end ---- Parse a color from various formats (always returns a valid Color) +--- Universally convert any color format (hex, named, table) into a Color object +--- Use this as your main color input handler to accept flexible color specifications ---@param value any Color value (hex string, named color, table, or Color instance) ---@return Color color Parsed color instance (defaults to black on error) function Color.parse(value) return Color.sanitizeColor(value, Color.new(0, 0, 0, 1)) end ---- Linear interpolation between two colors +--- Smoothly transition between two colors for animations and gradients +--- Use this to create color-based animations without manual channel calculations ---@param colorA Color Starting color ---@param colorB Color Ending color ---@param t number Interpolation factor (0-1) diff --git a/modules/Element.lua b/modules/Element.lua index 35a5de7..7075a3d 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -1415,13 +1415,15 @@ function Element.new(props, deps) return self end ---- Get element bounds (content box) +--- Retrieve the element's screen-space rectangle for collision detection and positioning calculations +--- Use this for custom layout logic, tooltips, or detecting overlaps between elements ---@return { x:number, y:number, width:number, height:number } function Element:getBounds() return { x = self.x, y = self.y, width = self:getBorderBoxWidth(), height = self:getBorderBoxHeight() } end ---- Check if point is inside element bounds +--- Test if a screen coordinate falls within the element's clickable area +--- Use this for custom hit detection or determining which element the mouse is over --- @param x number --- @param y number --- @return boolean @@ -1430,13 +1432,15 @@ function Element:contains(x, y) return bounds.x <= x and bounds.y <= y and bounds.x + bounds.width >= x and bounds.y + bounds.height >= y end ---- Get border-box width (including padding) +--- Get the element's total width including padding for layout calculations +--- Use this when you need the full visual width rather than just content width ---@return number function Element:getBorderBoxWidth() return self._borderBoxWidth or (self.width + self.padding.left + self.padding.right) end ---- Get border-box height (including padding) +--- Get the element's total height including padding for layout calculations +--- Use this when you need the full visual height rather than just content height ---@return number function Element:getBorderBoxHeight() return self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom) @@ -1473,7 +1477,8 @@ function Element:_detectOverflow() end end ---- Set scroll position with bounds clamping (delegates to ScrollManager) +--- Programmatically scroll content to any position for implementing "scroll to top" buttons or navigation anchors +--- Use this to create custom scrolling controls or jump to specific content sections ---@param x number? -- X scroll position (nil to keep current) ---@param y number? -- Y scroll position (nil to keep current) function Element:setScrollPosition(x, y) @@ -1561,7 +1566,8 @@ function Element:_handleWheelScroll(x, y) return false end ---- Get current scroll position (delegates to ScrollManager) +--- Query how far content is scrolled to implement scroll-aware UI like "back to top" buttons +--- Use this to create scroll position indicators or trigger lazy-loading ---@return number scrollX, number scrollY function Element:getScrollPosition() if self._scrollManager then @@ -1570,7 +1576,8 @@ function Element:getScrollPosition() return 0, 0 end ---- Get maximum scroll bounds (delegates to ScrollManager) +--- Find the scroll limits for validation and scroll position clamping +--- Use this to determine if content is fully scrolled or calculate remaining scroll distance ---@return number maxScrollX, number maxScrollY function Element:getMaxScroll() if self._scrollManager then @@ -1579,7 +1586,8 @@ function Element:getMaxScroll() return 0, 0 end ---- Get scroll percentage (0-1) (delegates to ScrollManager) +--- Get normalized scroll progress for scroll-based animations or position indicators +--- Use this to drive progress bars or parallax effects based on scroll position ---@return number percentX, number percentY function Element:getScrollPercentage() if self._scrollManager then @@ -1588,7 +1596,8 @@ function Element:getScrollPercentage() return 0, 0 end ---- Check if element has overflow (delegates to ScrollManager) +--- Determine if content extends beyond visible bounds to conditionally show scrollbars or overflow indicators +--- Use this to decide whether to display scroll hints or enable scroll interactions ---@return boolean hasOverflowX, boolean hasOverflowY function Element:hasOverflow() if self._scrollManager then @@ -1597,7 +1606,8 @@ function Element:hasOverflow() return false, false end ---- Get content dimensions (including overflow) (delegates to ScrollManager) +--- Measure total content size including overflowed areas for scroll calculations +--- Use this to understand how much content exists beyond the visible viewport ---@return number contentWidth, number contentHeight function Element:getContentSize() if self._scrollManager then @@ -1606,7 +1616,8 @@ function Element:getContentSize() return 0, 0 end ---- Scroll by delta amount (delegates to ScrollManager) +--- Scroll content by a relative amount for smooth scrolling animations or gesture-based scrolling +--- Use this to implement custom scroll controls or smooth scroll transitions ---@param dx number? -- X delta (nil for no change) ---@param dy number? -- Y delta (nil for no change) function Element:scrollBy(dx, dy) @@ -1616,7 +1627,8 @@ function Element:scrollBy(dx, dy) end end ---- Scroll to top +--- Jump to the beginning of scrollable content instantly +--- Use this for "back to top" buttons or resetting scroll position function Element:scrollToTop() self:setScrollPosition(nil, 0) end @@ -1634,7 +1646,8 @@ function Element:scrollToLeft() self:setScrollPosition(0, nil) end ---- Scroll to right +--- Jump to the rightmost position of horizontally scrollable content +--- Use this to navigate to the end of horizontal lists or carousels function Element:scrollToRight() if self._scrollManager then local maxScrollX, _ = self._scrollManager:getMaxScroll() @@ -1718,7 +1731,8 @@ function Element:getAvailableContentHeight() return math.max(0, availableHeight) end ---- Add child to element +--- Dynamically insert a child element into the hierarchy for runtime UI construction +--- Use this to build interfaces procedurally or add elements based on application state ---@param child Element function Element:addChild(child) child.parent = self @@ -1790,7 +1804,8 @@ function Element:addChild(child) end end ---- Remove a specific child from this element +--- Remove a child element from the hierarchy to dynamically update UIs +--- Use this to delete elements when they're no longer needed or respond to user actions ---@param child Element function Element:removeChild(child) for i, c in ipairs(self.children) do @@ -1822,7 +1837,8 @@ function Element:removeChild(child) end end ---- Remove all children from this element +--- Delete all child elements at once for resetting containers or clearing lists +--- Use this to efficiently empty containers when rebuilding UI from scratch function Element:clearChildren() -- Clear parent references for all children for _, child in ipairs(self.children) do @@ -2661,21 +2677,24 @@ end -- Input Handling - Focus Management -- ==================== ---- Focus this element for keyboard input +--- Give this element keyboard focus to enable text input or keyboard navigation +--- Use this to automatically focus text fields when showing forms or dialogs function Element:focus() if self._textEditor then self._textEditor:focus() end end ---- Remove focus from this element +--- Remove keyboard focus to stop capturing input events +--- Use this when closing popups or switching focus to other elements function Element:blur() if self._textEditor then self._textEditor:blur() end end ---- Check if this element is focused +--- Query focus state to conditionally render focus indicators or handle keyboard input +--- Use this to style focused elements or determine which element receives keyboard events ---@return boolean function Element:isFocused() if self._textEditor then @@ -2688,7 +2707,8 @@ end -- Input Handling - Text Buffer Management -- ==================== ---- Get current text buffer +--- Retrieve the element's current text content for processing or validation +--- Use this to read user input from text fields or get display text ---@return string function Element:getText() if self._textEditor then @@ -2697,7 +2717,8 @@ function Element:getText() return self.text or "" end ---- Set text buffer and mark dirty +--- Update the element's text content programmatically for dynamic labels or resetting inputs +--- Use this to change text without user input, like clearing fields or updating status messages ---@param text string function Element:setText(text) if self._textEditor then @@ -2709,7 +2730,8 @@ function Element:setText(text) self.text = text end ---- Insert text at position +--- Programmatically insert text at any position for autocomplete or text manipulation +--- Use this to implement suggestions, templates, or text snippets ---@param text string -- Text to insert ---@param position number? -- Position to insert at (default: cursor position) function Element:insertText(text, position) @@ -2899,7 +2921,8 @@ function Element:_trackActiveAnimations() end end ---- Set image tint color +--- Change the tint color of an image element dynamically for hover effects or state indication +--- Use this to recolor images without replacing the asset, like highlighting selected items ---@param color Color Color to tint the image function Element:setImageTint(color) self.imageTint = color @@ -2908,7 +2931,8 @@ function Element:setImageTint(color) end end ---- Set image opacity +--- Adjust image transparency independently from the element for fade effects +--- Use this to create image-specific fade animations or disabled states ---@param opacity number Opacity 0-1 function Element:setImageOpacity(opacity) if opacity ~= nil then @@ -2938,7 +2962,8 @@ function Element:setImageRepeat(repeatMode) end end ---- Rotate element by angle +--- Apply rotation transform to create spinning animations or rotated layouts +--- Use this for loading spinners, compass needles, or angled UI elements ---@param angle number Angle in radians function Element:rotate(angle) if not self.transform then @@ -2947,7 +2972,8 @@ function Element:rotate(angle) self.transform.rotate = angle end ---- Scale element +--- Resize element visually using scale transforms for zoom effects +--- Use this for hover magnification, shrinking animations, or responsive scaling ---@param scaleX number X-axis scale ---@param scaleY number? Y-axis scale (defaults to scaleX) function Element:scale(scaleX, scaleY) @@ -2958,7 +2984,8 @@ function Element:scale(scaleX, scaleY) self.transform.scaleY = scaleY or scaleX end ---- Translate element +--- Offset element position using transforms for smooth movement without layout recalculation +--- Use this for parallax effects, draggable elements, or position animations ---@param x number X translation ---@param y number Y translation function Element:translate(x, y) @@ -2969,7 +2996,8 @@ function Element:translate(x, y) self.transform.translateY = y end ---- Set transform origin +--- Define the pivot point for rotation and scaling transforms +--- Use this to rotate around corners, edges, or custom points rather than the center ---@param originX number X origin (0-1, where 0.5 is center) ---@param originY number Y origin (0-1, where 0.5 is center) function Element:setTransformOrigin(originX, originY) diff --git a/modules/Theme.lua b/modules/Theme.lua index 6e3acae..22afa61 100644 --- a/modules/Theme.lua +++ b/modules/Theme.lua @@ -124,7 +124,8 @@ Theme.__index = Theme local themes = {} local activeTheme = nil ----Create a new theme instance +--- Create reusable design systems with consistent styling, 9-patch assets, and component states +--- Use this to build professional-looking UIs with minimal per-element configuration ---@param definition ThemeDefinition Theme definition table ---@return Theme theme The new theme instance function Theme.new(definition) @@ -312,7 +313,8 @@ function Theme.new(definition) return self end ---- Load a theme from a Lua file +--- Import a theme definition from a file to enable hot-reloading and modular design systems +--- Use this to load bundled or user-created themes dynamically ---@param path string Path to theme definition file (e.g., "space" or "mytheme") ---@return Theme? theme The loaded theme, or nil on error function Theme.load(path) @@ -348,7 +350,8 @@ function Theme.load(path) return theme end ----Set the active theme +--- Switch the global theme to instantly restyle all themed UI elements +--- Use this to implement light/dark mode toggles or user-selectable skins ---@param themeOrName Theme|string Theme instance or theme name to activate function Theme.setActive(themeOrName) if type(themeOrName) == "string" then @@ -371,13 +374,15 @@ function Theme.setActive(themeOrName) end end ---- Get the active theme +--- Access the current theme to query colors, fonts, or create theme-aware components +--- Use this to build UI that adapts to the active design system ---@return Theme? theme The active theme, or nil if none is active function Theme.getActive() return activeTheme end ---- Get a component from the active theme +--- Retrieve pre-configured visual styles for UI components to maintain consistency +--- Use this to apply theme definitions to custom elements ---@param componentName string Name of the component (e.g., "button", "panel") ---@param state string? Optional state (e.g., "hover", "pressed", "disabled") ---@return ThemeComponent? component Returns component or nil if not found @@ -399,7 +404,8 @@ function Theme.getComponent(componentName, state) return component end ---- Get a font from the active theme +--- Access theme-defined fonts for consistent typography across your UI +--- Use this to load fonts specified in your theme definition ---@param fontName string Name of the font family (e.g., "default", "heading") ---@return string? fontPath Returns font path or nil if not found function Theme.getFont(fontName) @@ -410,7 +416,8 @@ function Theme.getFont(fontName) return activeTheme.fonts and activeTheme.fonts[fontName] end ---- Get a color from the active theme +--- Retrieve semantic colors from the theme palette for consistent brand identity +--- Use this instead of hardcoding colors to support themeing and color scheme switches ---@param colorName string Name of the color (e.g., "primary", "secondary") ---@return Color? color Returns Color instance or nil if not found function Theme.getColor(colorName) @@ -461,7 +468,8 @@ function Theme.getAllColors() return activeTheme.colors end ---- Get a color with a fallback if not found +--- Safely get theme colors with guaranteed fallbacks to prevent missing color errors +--- Use this when you need a color value no matter what ---@param colorName string Name of the color to retrieve ---@param fallback Color? Fallback color if not found (default: white) ---@return Color color The color or fallback (guaranteed non-nil) @@ -721,7 +729,8 @@ end -- Export both Theme and ThemeManager Theme.Manager = ThemeManager ----Validate a theme definition for structural correctness (non-aggressive) +--- Check theme definitions for correctness before use to catch configuration errors early +--- Use this during development to verify custom themes are properly structured ---@param theme table? The theme to validate ---@param options table? Optional validation options {strict: boolean} ---@return boolean valid, table errors List of validation errors @@ -908,7 +917,8 @@ function Theme.validateTheme(theme, options) return #errors == 0, errors end ----Sanitize a theme definition by removing invalid values and providing defaults +--- Clean up malformed theme data to make it usable without crashing +--- Use this to robustly handle user-created or external themes ---@param theme table? The theme to sanitize ---@return table sanitized The sanitized theme function Theme.sanitizeTheme(theme) diff --git a/modules/Transform.lua b/modules/Transform.lua index 1f7da3a..c44120e 100644 --- a/modules/Transform.lua +++ b/modules/Transform.lua @@ -86,13 +86,20 @@ function Transform.lerp(from, to, t) if type(to) ~= "table" then to = Transform.new() end - if type(t) ~= "number" or t ~= t or t == math.huge or t == -math.huge then + if type(t) ~= "number" or t ~= t then + -- NaN or invalid type t = 0 + elseif t == math.huge then + -- Positive infinity + t = 1 + elseif t == -math.huge then + -- Negative infinity + t = 0 + else + -- Clamp t to 0-1 range + t = math.max(0, math.min(1, t)) end - -- Clamp t to 0-1 range - t = math.max(0, math.min(1, t)) - return Transform.new({ rotate = (from.rotate or 0) * (1 - t) + (to.rotate or 0) * t, scaleX = (from.scaleX or 1) * (1 - t) + (to.scaleX or 1) * t, diff --git a/testing/__tests__/animation_properties_test.lua b/testing/__tests__/animation_properties_test.lua index c7ed9e8..f6b3237 100644 --- a/testing/__tests__/animation_properties_test.lua +++ b/testing/__tests__/animation_properties_test.lua @@ -27,7 +27,7 @@ function TestAnimationProperties:testColorLerp_MidPoint() local colorA = Color.new(0, 0, 0, 1) -- Black local colorB = Color.new(1, 1, 1, 1) -- White local result = Color.lerp(colorA, colorB, 0.5) - + luaunit.assertAlmostEquals(result.r, 0.5, 0.01) luaunit.assertAlmostEquals(result.g, 0.5, 0.01) luaunit.assertAlmostEquals(result.b, 0.5, 0.01) @@ -38,7 +38,7 @@ function TestAnimationProperties:testColorLerp_StartPoint() local colorA = Color.new(1, 0, 0, 1) -- Red local colorB = Color.new(0, 0, 1, 1) -- Blue local result = Color.lerp(colorA, colorB, 0) - + luaunit.assertAlmostEquals(result.r, 1, 0.01) luaunit.assertAlmostEquals(result.g, 0, 0.01) luaunit.assertAlmostEquals(result.b, 0, 0.01) @@ -48,7 +48,7 @@ function TestAnimationProperties:testColorLerp_EndPoint() local colorA = Color.new(1, 0, 0, 1) -- Red local colorB = Color.new(0, 0, 1, 1) -- Blue local result = Color.lerp(colorA, colorB, 1) - + luaunit.assertAlmostEquals(result.r, 0, 0.01) luaunit.assertAlmostEquals(result.g, 0, 0.01) luaunit.assertAlmostEquals(result.b, 1, 0.01) @@ -58,7 +58,7 @@ function TestAnimationProperties:testColorLerp_Alpha() local colorA = Color.new(1, 1, 1, 0) -- Transparent white local colorB = Color.new(1, 1, 1, 1) -- Opaque white local result = Color.lerp(colorA, colorB, 0.5) - + luaunit.assertAlmostEquals(result.a, 0.5, 0.01) end @@ -72,11 +72,11 @@ end function TestAnimationProperties:testColorLerp_ClampT() local colorA = Color.new(0, 0, 0, 1) local colorB = Color.new(1, 1, 1, 1) - + -- Test t > 1 local result1 = Color.lerp(colorA, colorB, 1.5) luaunit.assertAlmostEquals(result1.r, 1, 0.01) - + -- Test t < 0 local result2 = Color.lerp(colorA, colorB, -0.5) luaunit.assertAlmostEquals(result2.r, 0, 0.01) @@ -90,10 +90,10 @@ function TestAnimationProperties:testPositionAnimation_XProperty() start = { x = 0 }, final = { x = 100 }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertAlmostEquals(result.x, 50, 0.01) end @@ -103,10 +103,10 @@ function TestAnimationProperties:testPositionAnimation_YProperty() start = { y = 0 }, final = { y = 200 }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertAlmostEquals(result.y, 100, 0.01) end @@ -116,10 +116,10 @@ function TestAnimationProperties:testPositionAnimation_XY() start = { x = 10, y = 20 }, final = { x = 110, y = 220 }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertAlmostEquals(result.x, 60, 0.01) luaunit.assertAlmostEquals(result.y, 120, 0.01) end @@ -133,10 +133,10 @@ function TestAnimationProperties:testColorAnimation_BackgroundColor() final = { backgroundColor = Color.new(0, 0, 1, 1) }, -- Blue }) anim:setColorModule(Color) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertNotNil(result.backgroundColor) luaunit.assertAlmostEquals(result.backgroundColor.r, 0.5, 0.01) luaunit.assertAlmostEquals(result.backgroundColor.b, 0.5, 0.01) @@ -157,14 +157,14 @@ function TestAnimationProperties:testColorAnimation_MultipleColors() }, }) anim:setColorModule(Color) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertNotNil(result.backgroundColor) luaunit.assertNotNil(result.borderColor) luaunit.assertNotNil(result.textColor) - + -- Mid-point should be (0.5, 0.5, 0.5) for backgroundColor luaunit.assertAlmostEquals(result.backgroundColor.r, 0.5, 0.01) luaunit.assertAlmostEquals(result.backgroundColor.g, 0.5, 0.01) @@ -178,10 +178,10 @@ function TestAnimationProperties:testColorAnimation_WithoutColorModule() final = { backgroundColor = Color.new(0, 0, 1, 1) }, }) -- Don't set Color module - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertNil(result.backgroundColor) end @@ -192,10 +192,10 @@ function TestAnimationProperties:testColorAnimation_HexColors() final = { backgroundColor = "#0000FF" }, -- Blue }) anim:setColorModule(Color) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertNotNil(result.backgroundColor) luaunit.assertAlmostEquals(result.backgroundColor.r, 0.5, 0.01) end @@ -207,10 +207,10 @@ function TestAnimationProperties:testColorAnimation_NamedColors() final = { backgroundColor = "blue" }, }) anim:setColorModule(Color) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertNotNil(result.backgroundColor) luaunit.assertAlmostEquals(result.backgroundColor.r, 0.5, 0.01) end @@ -223,10 +223,10 @@ function TestAnimationProperties:testNumericAnimation_Gap() start = { gap = 0 }, final = { gap = 20 }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertAlmostEquals(result.gap, 10, 0.01) end @@ -236,10 +236,10 @@ function TestAnimationProperties:testNumericAnimation_ImageOpacity() start = { imageOpacity = 0 }, final = { imageOpacity = 1 }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertAlmostEquals(result.imageOpacity, 0.5, 0.01) end @@ -249,10 +249,10 @@ function TestAnimationProperties:testNumericAnimation_BorderWidth() start = { borderWidth = 1 }, final = { borderWidth = 10 }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertAlmostEquals(result.borderWidth, 5.5, 0.01) end @@ -262,10 +262,10 @@ function TestAnimationProperties:testNumericAnimation_FontSize() start = { fontSize = 12 }, final = { fontSize = 24 }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertAlmostEquals(result.fontSize, 18, 0.01) end @@ -275,10 +275,10 @@ function TestAnimationProperties:testNumericAnimation_MultipleProperties() start = { gap = 0, imageOpacity = 0, borderWidth = 1 }, final = { gap = 20, imageOpacity = 1, borderWidth = 5 }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertAlmostEquals(result.gap, 10, 0.01) luaunit.assertAlmostEquals(result.imageOpacity, 0.5, 0.01) luaunit.assertAlmostEquals(result.borderWidth, 3, 0.01) @@ -292,10 +292,10 @@ function TestAnimationProperties:testTableAnimation_Padding() start = { padding = { top = 0, right = 0, bottom = 0, left = 0 } }, final = { padding = { top = 10, right = 20, bottom = 10, left = 20 } }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertNotNil(result.padding) luaunit.assertAlmostEquals(result.padding.top, 5, 0.01) luaunit.assertAlmostEquals(result.padding.right, 10, 0.01) @@ -309,10 +309,10 @@ function TestAnimationProperties:testTableAnimation_Margin() start = { margin = { top = 0, right = 0, bottom = 0, left = 0 } }, final = { margin = { top = 20, right = 20, bottom = 20, left = 20 } }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertNotNil(result.margin) luaunit.assertAlmostEquals(result.margin.top, 10, 0.01) luaunit.assertAlmostEquals(result.margin.right, 10, 0.01) @@ -324,10 +324,10 @@ function TestAnimationProperties:testTableAnimation_CornerRadius() start = { cornerRadius = { topLeft = 0, topRight = 0, bottomLeft = 0, bottomRight = 0 } }, final = { cornerRadius = { topLeft = 10, topRight = 10, bottomLeft = 10, bottomRight = 10 } }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertNotNil(result.cornerRadius) luaunit.assertAlmostEquals(result.cornerRadius.topLeft, 5, 0.01) luaunit.assertAlmostEquals(result.cornerRadius.topRight, 5, 0.01) @@ -340,10 +340,10 @@ function TestAnimationProperties:testTableAnimation_PartialKeys() start = { padding = { top = 0, left = 0 } }, final = { padding = { top = 10, right = 20, left = 10 } }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertNotNil(result.padding) luaunit.assertAlmostEquals(result.padding.top, 5, 0.01) luaunit.assertAlmostEquals(result.padding.left, 5, 0.01) @@ -357,10 +357,10 @@ function TestAnimationProperties:testTableAnimation_NonNumericValues() start = { padding = { top = 0, special = "value" } }, final = { padding = { top = 10, special = "value" } }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertNotNil(result.padding) luaunit.assertAlmostEquals(result.padding.top, 5, 0.01) end @@ -392,10 +392,10 @@ function TestAnimationProperties:testCombinedAnimation_AllTypes() }, }) anim:setColorModule(Color) - + anim:update(0.5) local result = anim:interpolate() - + -- Check all properties interpolated correctly luaunit.assertAlmostEquals(result.width, 150, 0.01) luaunit.assertAlmostEquals(result.height, 150, 0.01) @@ -415,10 +415,10 @@ function TestAnimationProperties:testCombinedAnimation_WithEasing() easing = "easeInQuad", }) anim:setColorModule(Color) - + anim:update(0.5) local result = anim:interpolate() - + -- With easeInQuad, at t=0.5, eased value should be 0.25 luaunit.assertAlmostEquals(result.x, 25, 0.01) luaunit.assertAlmostEquals(result.backgroundColor.r, 0.25, 0.01) @@ -433,10 +433,10 @@ function TestAnimationProperties:testBackwardCompatibility_WidthHeightOpacity() start = { width = 100, height = 100, opacity = 0 }, final = { width = 200, height = 200, opacity = 1 }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertAlmostEquals(result.width, 150, 0.01) luaunit.assertAlmostEquals(result.height, 150, 0.01) luaunit.assertAlmostEquals(result.opacity, 0.5, 0.01) @@ -444,19 +444,19 @@ end function TestAnimationProperties:testBackwardCompatibility_FadeHelper() local anim = Animation.fade(1, 0, 1) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertAlmostEquals(result.opacity, 0.5, 0.01) end function TestAnimationProperties:testBackwardCompatibility_ScaleHelper() local anim = Animation.scale(1, { width = 100, height = 100 }, { width = 200, height = 200 }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertAlmostEquals(result.width, 150, 0.01) luaunit.assertAlmostEquals(result.height, 150, 0.01) end @@ -469,10 +469,10 @@ function TestAnimationProperties:testEdgeCase_MissingStartValue() start = { x = 0 }, final = { x = 100, y = 100 }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertAlmostEquals(result.x, 50, 0.01) luaunit.assertNil(result.y) -- Should be nil since start.y is missing end @@ -483,10 +483,10 @@ function TestAnimationProperties:testEdgeCase_MissingFinalValue() start = { x = 0, y = 0 }, final = { x = 100 }, }) - + anim:update(0.5) local result = anim:interpolate() - + luaunit.assertAlmostEquals(result.x, 50, 0.01) luaunit.assertNil(result.y) -- Should be nil since final.y is missing end @@ -497,10 +497,10 @@ function TestAnimationProperties:testEdgeCase_EmptyTables() start = {}, final = {}, }) - + anim:update(0.5) local result = anim:interpolate() - + -- Should not error, just return empty result luaunit.assertNotNil(result) end @@ -512,11 +512,11 @@ function TestAnimationProperties:testEdgeCase_CachedResult() start = { x = 0 }, final = { x = 100 }, }) - + anim:update(0.5) local result1 = anim:interpolate() local result2 = anim:interpolate() -- Should use cached result - + luaunit.assertEquals(result1, result2) -- Same table reference luaunit.assertAlmostEquals(result1.x, 50, 0.01) end @@ -527,15 +527,15 @@ function TestAnimationProperties:testEdgeCase_ResultInvalidatedOnUpdate() start = { x = 0 }, final = { x = 100 }, }) - + anim:update(0.5) local result1 = anim:interpolate() local x1 = result1.x -- Store value, not reference - + anim:update(0.25) -- Update again local result2 = anim:interpolate() local x2 = result2.x - + -- Should recalculate -- Note: result1 and result2 are the same cached table, but values should be updated luaunit.assertAlmostEquals(x1, 50, 0.01) @@ -544,4 +544,6 @@ function TestAnimationProperties:testEdgeCase_ResultInvalidatedOnUpdate() luaunit.assertAlmostEquals(result1.x, 75, 0.01) end -os.exit(luaunit.LuaUnit.run()) +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/error_handler_test.lua b/testing/__tests__/error_handler_test.lua index 024f2d6..440bc02 100644 --- a/testing/__tests__/error_handler_test.lua +++ b/testing/__tests__/error_handler_test.lua @@ -92,55 +92,60 @@ end -- Test: warn() prints with correct format (backward compatibility) function TestErrorHandler:test_warn_prints_with_format() - -- Capture print output by mocking print + -- Capture io.write output by mocking io.write local captured = nil - local originalPrint = print - print = function(msg) + local originalWrite = io.write + io.write = function(msg) captured = msg end + ErrorHandler.setLogTarget("console") ErrorHandler.warn("TestModule", "This is a warning") + ErrorHandler.setLogTarget("none") - print = originalPrint + io.write = originalWrite luaunit.assertNotNil(captured, "warn() should print") - luaunit.assertEquals(captured, "[FlexLove - TestModule] Warning: This is a warning") + luaunit.assertStrContains(captured, "[WARNING] [TestModule] This is a warning") end -- Test: warn() with error code function TestErrorHandler:test_warn_with_code() local captured = nil - local originalPrint = print - print = function(msg) + local originalWrite = io.write + io.write = function(msg) captured = msg end + ErrorHandler.setLogTarget("console") ErrorHandler.warn("TestModule", "VAL_001", "Potentially invalid property") + ErrorHandler.setLogTarget("none") - print = originalPrint + io.write = originalWrite luaunit.assertNotNil(captured, "warn() should print") - luaunit.assertStrContains(captured, "[FlexLove - TestModule] Warning [FLEXLOVE_VAL_001]") + luaunit.assertStrContains(captured, "[WARNING] [TestModule] [VAL_001]") luaunit.assertStrContains(captured, "Potentially invalid property") end -- Test: warn() with details function TestErrorHandler:test_warn_with_details() local captured = nil - local originalPrint = print - print = function(msg) - captured = msg + local originalWrite = io.write + io.write = function(msg) + captured = (captured or "") .. msg end + ErrorHandler.setLogTarget("console") ErrorHandler.warn("TestModule", "VAL_001", "Check this property", { property = "height", value = "auto", }) + ErrorHandler.setLogTarget("none") - print = originalPrint + io.write = originalWrite luaunit.assertNotNil(captured, "warn() should print") - luaunit.assertStrContains(captured, "Details:") luaunit.assertStrContains(captured, "Property: height") luaunit.assertStrContains(captured, "Value: auto") end @@ -225,14 +230,16 @@ end -- Test: warnDeprecated prints deprecation warning function TestErrorHandler:test_warnDeprecated_prints_message() local captured = nil - local originalPrint = print - print = function(msg) + local originalWrite = io.write + io.write = function(msg) captured = msg end + ErrorHandler.setLogTarget("console") ErrorHandler.warnDeprecated("TestModule", "oldFunction", "newFunction") + ErrorHandler.setLogTarget("none") - print = originalPrint + io.write = originalWrite luaunit.assertNotNil(captured, "warnDeprecated should print") luaunit.assertStrContains(captured, "'oldFunction' is deprecated. Use 'newFunction' instead") @@ -241,14 +248,16 @@ end -- Test: warnCommonMistake prints helpful message function TestErrorHandler:test_warnCommonMistake_prints_message() local captured = nil - local originalPrint = print - print = function(msg) + local originalWrite = io.write + io.write = function(msg) captured = msg end + ErrorHandler.setLogTarget("console") ErrorHandler.warnCommonMistake("TestModule", "Width is zero", "Set width to positive value") + ErrorHandler.setLogTarget("none") - print = originalPrint + io.write = originalWrite luaunit.assertNotNil(captured, "warnCommonMistake should print") luaunit.assertStrContains(captured, "Width is zero. Suggestion: Set width to positive value") diff --git a/testing/__tests__/flexlove_test.lua b/testing/__tests__/flexlove_test.lua index 501ec0d..380ae4a 100644 --- a/testing/__tests__/flexlove_test.lua +++ b/testing/__tests__/flexlove_test.lua @@ -21,7 +21,7 @@ end function TestFlexLove:testModuleLoads() luaunit.assertNotNil(FlexLove) luaunit.assertNotNil(FlexLove._VERSION) - luaunit.assertEquals(FlexLove._VERSION, "0.2.2") + luaunit.assertEquals(FlexLove._VERSION, "0.2.3") luaunit.assertNotNil(FlexLove._DESCRIPTION) luaunit.assertNotNil(FlexLove._URL) luaunit.assertNotNil(FlexLove._LICENSE) diff --git a/testing/__tests__/image_tiling_test.lua b/testing/__tests__/image_tiling_test.lua index 7f1b8c3..c026588 100644 --- a/testing/__tests__/image_tiling_test.lua +++ b/testing/__tests__/image_tiling_test.lua @@ -16,8 +16,12 @@ TestImageTiling = {} function TestImageTiling:setUp() -- Create a mock image self.mockImage = { - getDimensions = function() return 64, 64 end, - type = function() return "Image" end, + getDimensions = function() + return 64, 64 + end, + type = function() + return "Image" + end, } end @@ -30,7 +34,7 @@ function TestImageTiling:testDrawTiledNoRepeat() local drawCalls = {} local originalDraw = love.graphics.draw love.graphics.draw = function(...) - table.insert(drawCalls, {...}) + table.insert(drawCalls, { ... }) end ImageRenderer.drawTiled(self.mockImage, 100, 100, 200, 200, "no-repeat", 1, nil) @@ -49,11 +53,11 @@ function TestImageTiling:testDrawTiledRepeat() local drawCalls = {} local originalDraw = love.graphics.draw local originalNewQuad = love.graphics.newQuad - + love.graphics.draw = function(...) - table.insert(drawCalls, {...}) + table.insert(drawCalls, { ... }) end - + love.graphics.newQuad = function(...) return { type = "quad", ... } end @@ -75,11 +79,11 @@ function TestImageTiling:testDrawTiledRepeatX() local drawCalls = {} local originalDraw = love.graphics.draw local originalNewQuad = love.graphics.newQuad - + love.graphics.draw = function(...) - table.insert(drawCalls, {...}) + table.insert(drawCalls, { ... }) end - + love.graphics.newQuad = function(...) return { type = "quad", ... } end @@ -100,11 +104,11 @@ function TestImageTiling:testDrawTiledRepeatY() local drawCalls = {} local originalDraw = love.graphics.draw local originalNewQuad = love.graphics.newQuad - + love.graphics.draw = function(...) - table.insert(drawCalls, {...}) + table.insert(drawCalls, { ... }) end - + love.graphics.newQuad = function(...) return { type = "quad", ... } end @@ -124,9 +128,9 @@ function TestImageTiling:testDrawTiledSpace() -- Test space mode (distributes tiles with even spacing) local drawCalls = {} local originalDraw = love.graphics.draw - + love.graphics.draw = function(...) - table.insert(drawCalls, {...}) + table.insert(drawCalls, { ... }) end -- Image is 64x64, bounds are 200x200 @@ -142,9 +146,9 @@ function TestImageTiling:testDrawTiledRound() -- Test round mode (scales tiles to fit exactly) local drawCalls = {} local originalDraw = love.graphics.draw - + love.graphics.draw = function(...) - table.insert(drawCalls, {...}) + table.insert(drawCalls, { ... }) end -- Image is 64x64, bounds are 200x200 @@ -160,9 +164,9 @@ function TestImageTiling:testDrawTiledWithOpacity() -- Test tiling with opacity local setColorCalls = {} local originalSetColor = love.graphics.setColor - + love.graphics.setColor = function(...) - table.insert(setColorCalls, {...}) + table.insert(setColorCalls, { ... }) end ImageRenderer.drawTiled(self.mockImage, 100, 100, 200, 200, "no-repeat", 0.5, nil) @@ -186,9 +190,9 @@ function TestImageTiling:testDrawTiledWithTint() -- Test tiling with tint color local setColorCalls = {} local originalSetColor = love.graphics.setColor - + love.graphics.setColor = function(...) - table.insert(setColorCalls, {...}) + table.insert(setColorCalls, { ... }) end local redTint = Color.new(1, 0, 0, 1) @@ -219,7 +223,7 @@ function TestImageTiling:testElementImageRepeatProperty() local Renderer = require("modules.Renderer") local EventHandler = require("modules.EventHandler") local ImageCache = require("modules.ImageCache") - + local deps = { utils = utils, Color = Color, @@ -231,7 +235,7 @@ function TestImageTiling:testElementImageRepeatProperty() ImageRenderer = ImageRenderer, ErrorHandler = ErrorHandler, } - + local element = Element.new({ width = 200, height = 200, @@ -251,7 +255,7 @@ function TestImageTiling:testElementImageRepeatDefault() local Renderer = require("modules.Renderer") local EventHandler = require("modules.EventHandler") local ImageCache = require("modules.ImageCache") - + local deps = { utils = utils, Color = Color, @@ -263,7 +267,7 @@ function TestImageTiling:testElementImageRepeatDefault() ImageRenderer = ImageRenderer, ErrorHandler = ErrorHandler, } - + local element = Element.new({ width = 200, height = 200, @@ -282,7 +286,7 @@ function TestImageTiling:testElementSetImageRepeat() local Renderer = require("modules.Renderer") local EventHandler = require("modules.EventHandler") local ImageCache = require("modules.ImageCache") - + local deps = { utils = utils, Color = Color, @@ -294,7 +298,7 @@ function TestImageTiling:testElementSetImageRepeat() ImageRenderer = ImageRenderer, ErrorHandler = ErrorHandler, } - + local element = Element.new({ width = 200, height = 200, @@ -313,9 +317,9 @@ function TestImageTiling:testElementImageTintProperty() local Renderer = require("modules.Renderer") local EventHandler = require("modules.EventHandler") local ImageCache = require("modules.ImageCache") - + local redTint = Color.new(1, 0, 0, 1) - + local deps = { utils = utils, Color = Color, @@ -327,7 +331,7 @@ function TestImageTiling:testElementImageTintProperty() ImageRenderer = ImageRenderer, ErrorHandler = ErrorHandler, } - + local element = Element.new({ width = 200, height = 200, @@ -346,7 +350,7 @@ function TestImageTiling:testElementSetImageTint() local Renderer = require("modules.Renderer") local EventHandler = require("modules.EventHandler") local ImageCache = require("modules.ImageCache") - + local deps = { utils = utils, Color = Color, @@ -358,7 +362,7 @@ function TestImageTiling:testElementSetImageTint() ImageRenderer = ImageRenderer, ErrorHandler = ErrorHandler, } - + local element = Element.new({ width = 200, height = 200, @@ -379,7 +383,7 @@ function TestImageTiling:testElementSetImageOpacity() local Renderer = require("modules.Renderer") local EventHandler = require("modules.EventHandler") local ImageCache = require("modules.ImageCache") - + local deps = { utils = utils, Color = Color, @@ -391,7 +395,7 @@ function TestImageTiling:testElementSetImageOpacity() ImageRenderer = ImageRenderer, ErrorHandler = ErrorHandler, } - + local element = Element.new({ width = 200, height = 200, @@ -401,5 +405,6 @@ function TestImageTiling:testElementSetImageOpacity() luaunit.assertEquals(element.imageOpacity, 0.7) end --- Run the tests -os.exit(luaunit.LuaUnit.run()) +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/performance_instrumentation_test.lua b/testing/__tests__/performance_instrumentation_test.lua index ad35f86..1f22c2f 100644 --- a/testing/__tests__/performance_instrumentation_test.lua +++ b/testing/__tests__/performance_instrumentation_test.lua @@ -23,18 +23,18 @@ end function TestPerformanceInstrumentation:testTimerStartStop() Performance.startTimer("test_operation") - + -- Simulate some work local sum = 0 for i = 1, 1000 do sum = sum + i end - + local elapsed = Performance.stopTimer("test_operation") - + luaunit.assertNotNil(elapsed) luaunit.assertTrue(elapsed >= 0) - + local metrics = Performance.getMetrics() luaunit.assertNotNil(metrics.timings["test_operation"]) luaunit.assertEquals(metrics.timings["test_operation"].count, 1) @@ -44,13 +44,15 @@ function TestPerformanceInstrumentation:testMultipleTimers() -- Start multiple timers Performance.startTimer("layout") Performance.startTimer("render") - + local sum = 0 - for i = 1, 100 do sum = sum + i end - + for i = 1, 100 do + sum = sum + i + end + Performance.stopTimer("layout") Performance.stopTimer("render") - + local metrics = Performance.getMetrics() luaunit.assertNotNil(metrics.timings["layout"]) luaunit.assertNotNil(metrics.timings["render"]) @@ -58,15 +60,15 @@ end function TestPerformanceInstrumentation:testFrameTiming() Performance.startFrame() - + -- Simulate frame work local sum = 0 for i = 1, 1000 do sum = sum + i end - + Performance.endFrame() - + local frameMetrics = Performance.getFrameMetrics() luaunit.assertNotNil(frameMetrics) luaunit.assertEquals(frameMetrics.frameCount, 1) @@ -77,10 +79,10 @@ function TestPerformanceInstrumentation:testDrawCallCounting() Performance.incrementCounter("draw_calls", 1) Performance.incrementCounter("draw_calls", 1) Performance.incrementCounter("draw_calls", 1) - + local counter = Performance.getFrameCounter("draw_calls") luaunit.assertEquals(counter, 3) - + -- Reset and check Performance.resetFrameCounters() counter = Performance.getFrameCounter("draw_calls") @@ -89,10 +91,10 @@ end function TestPerformanceInstrumentation:testHUDToggle() luaunit.assertFalse(Performance.getConfig().hudEnabled) - + Performance.toggleHUD() luaunit.assertTrue(Performance.getConfig().hudEnabled) - + Performance.toggleHUD() luaunit.assertFalse(Performance.getConfig().hudEnabled) end @@ -100,10 +102,10 @@ end function TestPerformanceInstrumentation:testEnableDisable() Performance.enable() luaunit.assertTrue(Performance.isEnabled()) - + Performance.disable() luaunit.assertFalse(Performance.isEnabled()) - + -- Timers should not record when disabled Performance.startTimer("disabled_test") local elapsed = Performance.stopTimer("disabled_test") @@ -118,12 +120,12 @@ function TestPerformanceInstrumentation:testMeasureFunction() end return sum end - + local wrapped = Performance.measure("expensive_op", expensiveOperation) local result = wrapped(1000) - + luaunit.assertEquals(result, 500500) -- sum of 1 to 1000 - + local metrics = Performance.getMetrics() luaunit.assertNotNil(metrics.timings["expensive_op"]) luaunit.assertEquals(metrics.timings["expensive_op"].count, 1) @@ -131,7 +133,7 @@ end function TestPerformanceInstrumentation:testMemoryTracking() Performance.updateMemory() - + local memMetrics = Performance.getMemoryMetrics() luaunit.assertNotNil(memMetrics) luaunit.assertTrue(memMetrics.currentKb > 0) @@ -142,7 +144,7 @@ end function TestPerformanceInstrumentation:testExportJSON() Performance.startTimer("test_op") Performance.stopTimer("test_op") - + local json = Performance.exportJSON() luaunit.assertNotNil(json) luaunit.assertTrue(string.find(json, "fps") ~= nil) @@ -152,16 +154,13 @@ end function TestPerformanceInstrumentation:testExportCSV() Performance.startTimer("test_op") Performance.stopTimer("test_op") - + local csv = Performance.exportCSV() luaunit.assertNotNil(csv) luaunit.assertTrue(string.find(csv, "Name,Average") ~= nil) luaunit.assertTrue(string.find(csv, "test_op") ~= nil) end --- Run tests if executed directly -if arg and arg[0]:find("performance_instrumentation_test%.lua$") then +if not _G.RUNNING_ALL_TESTS then os.exit(luaunit.LuaUnit.run()) end - -return TestPerformanceInstrumentation diff --git a/testing/__tests__/performance_warnings_test.lua b/testing/__tests__/performance_warnings_test.lua index a1171cd..eac6430 100644 --- a/testing/__tests__/performance_warnings_test.lua +++ b/testing/__tests__/performance_warnings_test.lua @@ -153,4 +153,6 @@ function TestPerformanceWarnings:testLayoutRecalculationTracking() luaunit.assertNotNil(root) end -return TestPerformanceWarnings +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/__tests__/transform_test.lua b/testing/__tests__/transform_test.lua index d58c9ab..63f4fb8 100644 --- a/testing/__tests__/transform_test.lua +++ b/testing/__tests__/transform_test.lua @@ -243,8 +243,8 @@ function TestTransform:testClone_AllProperties() luaunit.assertAlmostEquals(clone.originX, 0.25, 0.01) luaunit.assertAlmostEquals(clone.originY, 0.75, 0.01) - -- Ensure it's a different object - luaunit.assertNotEquals(clone, original) + -- Ensure it's a different object (use raw comparison) + luaunit.assertFalse(rawequal(clone, original), "Clone should be a different table instance") end function TestTransform:testClone_Nil() @@ -289,4 +289,6 @@ function TestTransform:testTransformAnimation() luaunit.assertAlmostEquals(result.transform.scaleX, 1.5, 0.01) end -os.exit(luaunit.LuaUnit.run()) +if not _G.RUNNING_ALL_TESTS then + os.exit(luaunit.LuaUnit.run()) +end diff --git a/testing/loveStub.lua b/testing/loveStub.lua index 08f5152..72a4e6c 100644 --- a/testing/loveStub.lua +++ b/testing/loveStub.lua @@ -105,6 +105,9 @@ function love_helper.graphics.newCanvas(width, height) getDimensions = function() return width or mockWindowWidth, height or mockWindowHeight end, + release = function() + -- Mock canvas release + end, } end