diff --git a/FlexLove.lua b/FlexLove.lua index a90177d..71b443f 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -329,6 +329,7 @@ local Gui = { scaleFactors = { x = 1.0, y = 1.0 }, defaultTheme = nil, _cachedViewport = { width = 0, height = 0 }, -- Cached viewport dimensions + _focusedElement = nil, -- Currently focused element for keyboard input } -- ==================== @@ -2207,6 +2208,24 @@ function Gui.update(dt) Gui._activeEventElement = nil end +--- Forward text input to focused element +---@param text string -- Character input +function Gui.textinput(text) + if Gui._focusedElement then + Gui._focusedElement:textinput(text) + end +end + +--- Forward key press to focused element +---@param key string -- Key name +---@param scancode string -- Scancode +---@param isrepeat boolean -- Whether this is a key repeat +function Gui.keypressed(key, scancode, isrepeat) + if Gui._focusedElement then + Gui._focusedElement:keypressed(key, scancode, isrepeat) + end +end + --- Destroy all elements and their children function Gui.destroy() for _, win in ipairs(Gui.topElements) do @@ -2222,6 +2241,8 @@ function Gui.destroy() Gui._gameCanvas = nil Gui._backdropCanvas = nil Gui._canvasDimensions = { width = 0, height = 0 } + -- Clear focused element + Gui._focusedElement = nil end -- Simple GUI library for LOVE2D @@ -2639,6 +2660,34 @@ Public API methods to access internal state: ---@field contentBlur {intensity:number, quality:number}? -- Blur the element's content including children (intensity: 0-100, quality: 1-10) ---@field backdropBlur {intensity:number, quality:number}? -- Blur content behind the element (intensity: 0-100, quality: 1-10) ---@field _blurInstance table? -- Internal: cached blur effect instance +---@field editable boolean -- Whether the element is editable (default: false) +---@field multiline boolean -- Whether the element supports multiple lines (default: false) +---@field textWrap boolean|"word"|"char" -- Text wrapping mode (default: false for single-line, "word" for multi-line) +---@field maxLines number? -- Maximum number of lines (default: nil) +---@field maxLength number? -- Maximum text length in characters (default: nil) +---@field placeholder string? -- Placeholder text when empty (default: nil) +---@field passwordMode boolean -- Whether to display text as password (default: false) +---@field inputType "text"|"number"|"email"|"url" -- Input type for validation (default: "text") +---@field textOverflow "clip"|"ellipsis"|"scroll" -- Text overflow behavior (default: "clip") +---@field scrollable boolean -- Whether text is scrollable (default: false for single-line, true for multi-line) +---@field autoGrow boolean -- Whether element auto-grows with text (default: false) +---@field selectOnFocus boolean -- Whether to select all text on focus (default: false) +---@field cursorColor Color? -- Cursor color (default: nil, uses textColor) +---@field selectionColor Color? -- Selection background color (default: nil, uses theme or default) +---@field cursorBlinkRate number -- Cursor blink rate in seconds (default: 0.5) +---@field _cursorPosition number? -- Internal: cursor character position (0-based) +---@field _cursorLine number? -- Internal: cursor line number (1-based) +---@field _cursorColumn number? -- Internal: cursor column within line +---@field _cursorBlinkTimer number? -- Internal: cursor blink timer +---@field _cursorVisible boolean? -- Internal: cursor visibility state +---@field _selectionStart number? -- Internal: selection start position +---@field _selectionEnd number? -- Internal: selection end position +---@field _selectionAnchor number? -- Internal: selection anchor point +---@field _focused boolean? -- Internal: focus state +---@field _textBuffer string? -- Internal: text buffer for editable elements +---@field _lines table? -- Internal: split lines for multi-line text +---@field _wrappedLines table? -- Internal: wrapped line data +---@field _textDirty boolean? -- Internal: flag to recalculate lines/wrapping local Element = {} Element.__index = Element @@ -2696,6 +2745,21 @@ Element.__index = Element ---@field scalingAlgorithm "nearest"|"bilinear"? -- Scaling algorithm for 9-slice corners: "nearest" (sharp/pixelated) or "bilinear" (smooth) (overrides theme setting) ---@field contentBlur {intensity:number, quality:number}? -- Blur the element's content including children (intensity: 0-100, quality: 1-10, default: nil) ---@field backdropBlur {intensity:number, quality:number}? -- Blur content behind the element (intensity: 0-100, quality: 1-10, default: nil) +---@field editable boolean? -- Whether the element is editable (default: false) +---@field multiline boolean? -- Whether the element supports multiple lines (default: false) +---@field textWrap boolean|"word"|"char"? -- Text wrapping mode (default: false for single-line, "word" for multi-line) +---@field maxLines number? -- Maximum number of lines (default: nil) +---@field maxLength number? -- Maximum text length in characters (default: nil) +---@field placeholder string? -- Placeholder text when empty (default: nil) +---@field passwordMode boolean? -- Whether to display text as password (default: false) +---@field inputType "text"|"number"|"email"|"url"? -- Input type for validation (default: "text") +---@field textOverflow "clip"|"ellipsis"|"scroll"? -- Text overflow behavior (default: "clip") +---@field scrollable boolean? -- Whether text is scrollable (default: false for single-line, true for multi-line) +---@field autoGrow boolean? -- Whether element auto-grows with text (default: false) +---@field selectOnFocus boolean? -- Whether to select all text on focus (default: false) +---@field cursorColor Color? -- Cursor color (default: nil, uses textColor) +---@field selectionColor Color? -- Selection background color (default: nil, uses theme or default) +---@field cursorBlinkRate number? -- Cursor blink rate in seconds (default: 0.5) local ElementProps = {} ---@param props ElementProps @@ -2705,6 +2769,13 @@ function Element.new(props) self.children = {} self.callback = props.callback self.id = props.id or "" + + -- Input event callbacks + self.onFocus = props.onFocus + self.onBlur = props.onBlur + self.onTextInput = props.onTextInput + self.onTextChange = props.onTextChange + self.onEnter = props.onEnter -- Initialize click tracking for event system self._pressed = {} -- Track pressed state per mouse button @@ -2775,6 +2846,63 @@ function Element.new(props) self.backdropBlur = props.backdropBlur self._blurInstance = nil + -- Initialize input control properties + self.editable = props.editable or false + self.multiline = props.multiline or false + self.passwordMode = props.passwordMode or false + + -- Validate property combinations: passwordMode disables multiline + if self.passwordMode then + self.multiline = false + end + + self.textWrap = props.textWrap + if self.textWrap == nil then + self.textWrap = self.multiline and "word" or false + end + + self.maxLines = props.maxLines + self.maxLength = props.maxLength + self.placeholder = props.placeholder + self.inputType = props.inputType or "text" + + -- Text behavior properties + self.textOverflow = props.textOverflow or "clip" + self.scrollable = props.scrollable + if self.scrollable == nil then + self.scrollable = self.multiline + end + self.autoGrow = props.autoGrow or false + self.selectOnFocus = props.selectOnFocus or false + + -- Cursor and selection properties + self.cursorColor = props.cursorColor + self.selectionColor = props.selectionColor + self.cursorBlinkRate = props.cursorBlinkRate or 0.5 + + -- Initialize cursor and selection state (only if editable) + if self.editable then + self._cursorPosition = 0 -- Character index (0 = before first char) + self._cursorLine = 1 -- Current line number (1-based) + self._cursorColumn = 0 -- Column within current line + self._cursorBlinkTimer = 0 + self._cursorVisible = true + + -- Selection state + self._selectionStart = nil -- nil = no selection + self._selectionEnd = nil + self._selectionAnchor = nil -- Anchor point for shift+arrow selection + + -- Focus state + self._focused = false + + -- Text buffer state (initialized after self.text is set below) + self._textBuffer = props.text or "" -- Actual text content + self._lines = nil -- Split lines (for multiline) + self._wrappedLines = nil -- Wrapped line data + self._textDirty = true -- Flag to recalculate lines/wrapping + end + -- Set parent first so it's available for size calculations self.parent = props.parent @@ -4553,6 +4681,15 @@ function Element:update(dt) child:update(dt) end + -- Update cursor blink timer (only if editable and focused) + if self.editable and self._focused then + self._cursorBlinkTimer = self._cursorBlinkTimer + dt + if self._cursorBlinkTimer >= self.cursorBlinkRate then + self._cursorBlinkTimer = 0 + self._cursorVisible = not self._cursorVisible + end + end + -- Update animation if exists if self.animation then local finished = self.animation:update(dt) @@ -4677,6 +4814,11 @@ function Element:update(dt) self.callback(self, clickEvent) self._pressed[button] = false + + -- Focus editable elements on left click + if button == 1 and self.editable then + self:focus() + end -- Fire release event local releaseEvent = InputEvent.new({ @@ -5268,6 +5410,639 @@ function Element:updateOpacity(newOpacity) end end +-- ==================== +-- Input Handling - Cursor Management +-- ==================== + +--- Set cursor position +---@param position number -- Character index (0-based) +function Element:setCursorPosition(position) + if not self.editable then return end + self._cursorPosition = position + self:_validateCursorPosition() + self:_resetCursorBlink() +end + +--- Get cursor position +---@return number -- Character index (0-based) +function Element:getCursorPosition() + if not self.editable then return 0 end + return self._cursorPosition +end + +--- Move cursor by delta characters +---@param delta number -- Number of characters to move (positive or negative) +function Element:moveCursorBy(delta) + if not self.editable then return end + self._cursorPosition = self._cursorPosition + delta + self:_validateCursorPosition() + self:_resetCursorBlink() +end + +--- Move cursor to start of text +function Element:moveCursorToStart() + if not self.editable then return end + self._cursorPosition = 0 + self:_resetCursorBlink() +end + +--- Move cursor to end of text +function Element:moveCursorToEnd() + if not self.editable then return end + local textLength = utf8.len(self._textBuffer or "") + self._cursorPosition = textLength + self:_resetCursorBlink() +end + +--- Move cursor to start of current line +function Element:moveCursorToLineStart() + if not self.editable then return end + -- For now, just move to start (will be enhanced for multi-line) + self:moveCursorToStart() +end + +--- Move cursor to end of current line +function Element:moveCursorToLineEnd() + if not self.editable then return end + -- For now, just move to end (will be enhanced for multi-line) + self:moveCursorToEnd() +end + +--- Validate cursor position (ensure it's within text bounds) +function Element:_validateCursorPosition() + if not self.editable then return end + local textLength = utf8.len(self._textBuffer or "") + self._cursorPosition = math.max(0, math.min(self._cursorPosition, textLength)) +end + +--- Reset cursor blink (show cursor immediately) +function Element:_resetCursorBlink() + if not self.editable then return end + self._cursorBlinkTimer = 0 + self._cursorVisible = true +end + +-- ==================== +-- Input Handling - Selection Management +-- ==================== + +--- Set selection range +---@param startPos number -- Start position (inclusive) +---@param endPos number -- End position (inclusive) +function Element:setSelection(startPos, endPos) + if not self.editable then return end + local textLength = utf8.len(self._textBuffer or "") + self._selectionStart = math.max(0, math.min(startPos, textLength)) + self._selectionEnd = math.max(0, math.min(endPos, textLength)) + + -- Ensure start <= end + if self._selectionStart > self._selectionEnd then + self._selectionStart, self._selectionEnd = self._selectionEnd, self._selectionStart + end + + self:_resetCursorBlink() +end + +--- Get selection range +---@return number?, number? -- Start and end positions, or nil if no selection +function Element:getSelection() + if not self.editable then return nil, nil end + if not self:hasSelection() then return nil, nil end + return self._selectionStart, self._selectionEnd +end + +--- Check if there is an active selection +---@return boolean +function Element:hasSelection() + if not self.editable then return false end + return self._selectionStart ~= nil and self._selectionEnd ~= nil and self._selectionStart ~= self._selectionEnd +end + +--- Clear selection +function Element:clearSelection() + if not self.editable then return end + self._selectionStart = nil + self._selectionEnd = nil + self._selectionAnchor = nil +end + +--- Select all text +function Element:selectAll() + if not self.editable then return end + local textLength = utf8.len(self._textBuffer or "") + self._selectionStart = 0 + self._selectionEnd = textLength + self:_resetCursorBlink() +end + +--- Get selected text +---@return string? -- Selected text or nil if no selection +function Element:getSelectedText() + if not self.editable or not self:hasSelection() then return nil end + local startPos, endPos = self:getSelection() + if not startPos or not endPos then return nil end + + -- Convert character indices to byte offsets for utf8.sub + local text = self._textBuffer or "" + return utf8.sub(text, startPos + 1, endPos) +end + +--- Delete selected text +---@return boolean -- True if text was deleted +function Element:deleteSelection() + if not self.editable or not self:hasSelection() then return false end + local startPos, endPos = self:getSelection() + if not startPos or not endPos then return false end + + self:deleteText(startPos, endPos) + self:clearSelection() + self._cursorPosition = startPos + self:_validateCursorPosition() + return true +end + +-- ==================== +-- Input Handling - Focus Management +-- ==================== + +--- Focus this element for keyboard input +function Element:focus() + if not self.editable then return end + + -- Blur previously focused element + if Gui._focusedElement and Gui._focusedElement ~= self then + Gui._focusedElement:blur() + end + + -- Set focus state + self._focused = true + Gui._focusedElement = self + + -- Reset cursor blink + self:_resetCursorBlink() + + -- Select all text if selectOnFocus is enabled + if self.selectOnFocus then + self:selectAll() + else + -- Move cursor to end of text + self:moveCursorToEnd() + end + + -- Trigger onFocus callback if defined + if self.onFocus then + self.onFocus(self) + end +end + +--- Remove focus from this element +function Element:blur() + if not self.editable then return end + + self._focused = false + + -- Clear global focused element if it's this element + if Gui._focusedElement == self then + Gui._focusedElement = nil + end + + -- Trigger onBlur callback if defined + if self.onBlur then + self.onBlur(self) + end +end + +--- Check if this element is focused +---@return boolean +function Element:isFocused() + if not self.editable then return false end + return self._focused == true +end + +-- ==================== +-- Input Handling - Text Buffer Management +-- ==================== + +--- Get current text buffer +---@return string +function Element:getText() + if not self.editable then return self.text or "" end + return self._textBuffer or "" +end + +--- Set text buffer and mark dirty +---@param text string +function Element:setText(text) + if not self.editable then + self.text = text + return + end + + self._textBuffer = text or "" + self.text = self._textBuffer -- Sync display text + self:_markTextDirty() + self:_validateCursorPosition() +end + +--- Insert text at position +---@param text string -- Text to insert +---@param position number? -- Position to insert at (default: cursor position) +function Element:insertText(text, position) + if not self.editable then return end + + position = position or self._cursorPosition + local buffer = self._textBuffer or "" + + -- Convert character position to byte offset + local byteOffset = utf8.offset(buffer, position + 1) or (#buffer + 1) + + -- Insert text + local before = buffer:sub(1, byteOffset - 1) + local after = buffer:sub(byteOffset) + self._textBuffer = before .. text .. after + self.text = self._textBuffer -- Sync display text + + -- Update cursor position + self._cursorPosition = position + utf8.len(text) + + self:_markTextDirty() + self:_validateCursorPosition() +end + +--- Delete text in range +---@param startPos number -- Start position (inclusive) +---@param endPos number -- End position (inclusive) +function Element:deleteText(startPos, endPos) + if not self.editable then return end + + local buffer = self._textBuffer or "" + + -- Ensure valid range + local textLength = utf8.len(buffer) + startPos = math.max(0, math.min(startPos, textLength)) + endPos = math.max(0, math.min(endPos, textLength)) + + if startPos > endPos then + startPos, endPos = endPos, startPos + end + + -- Convert character positions to byte offsets + local startByte = utf8.offset(buffer, startPos + 1) or 1 + local endByte = utf8.offset(buffer, endPos + 1) or (#buffer + 1) + + -- Delete text + local before = buffer:sub(1, startByte - 1) + local after = buffer:sub(endByte) + self._textBuffer = before .. after + self.text = self._textBuffer -- Sync display text + + self:_markTextDirty() +end + +--- Replace text in range +---@param startPos number -- Start position (inclusive) +---@param endPos number -- End position (inclusive) +---@param newText string -- Replacement text +function Element:replaceText(startPos, endPos, newText) + if not self.editable then return end + + self:deleteText(startPos, endPos) + self:insertText(newText, startPos) +end + +--- Mark text as dirty (needs recalculation) +function Element:_markTextDirty() + if not self.editable then return end + self._textDirty = true +end + +--- Update text if dirty (recalculate lines and wrapping) +function Element:_updateTextIfDirty() + if not self.editable or not self._textDirty then return end + + self:_splitLines() + self:_calculateWrapping() + self:_validateCursorPosition() + self._textDirty = false +end + +--- Split text into lines (for multi-line text) +function Element:_splitLines() + if not self.editable then return end + + if not self.multiline then + self._lines = {self._textBuffer or ""} + return + end + + self._lines = {} + local text = self._textBuffer or "" + + -- Split on newlines + for line in (text .. "\n"):gmatch("([^\n]*)\n") do + table.insert(self._lines, line) + end + + -- Ensure at least one line + if #self._lines == 0 then + self._lines = {""} + end +end + +--- Calculate text wrapping +function Element:_calculateWrapping() + if not self.editable or not self.textWrap then + self._wrappedLines = nil + return + end + + self._wrappedLines = {} + local availableWidth = self.width - self.padding.left - self.padding.right + + for lineNum, line in ipairs(self._lines or {}) do + if line == "" then + table.insert(self._wrappedLines, { + text = "", + startIdx = 0, + endIdx = 0, + lineNum = lineNum + }) + else + local wrappedParts = self:_wrapLine(line, availableWidth) + for _, part in ipairs(wrappedParts) do + part.lineNum = lineNum + table.insert(self._wrappedLines, part) + end + end + end +end + +--- Wrap a single line of text +---@param line string -- Line to wrap +---@param maxWidth number -- Maximum width in pixels +---@return table -- Array of wrapped line parts +function Element:_wrapLine(line, maxWidth) + if not self.editable then return {{text = line, startIdx = 0, endIdx = utf8.len(line)}} end + + local font = self:_getFont() + local wrappedParts = {} + local currentLine = "" + local startIdx = 0 + + if self.textWrap == "word" then + -- Word wrapping + local words = {} + for word in line:gmatch("%S+") do + table.insert(words, word) + end + + for i, word in ipairs(words) do + local testLine = currentLine == "" and word or (currentLine .. " " .. word) + local width = font:getWidth(testLine) + + if width > maxWidth and currentLine ~= "" then + -- Current line is full, start new line + table.insert(wrappedParts, { + text = currentLine, + startIdx = startIdx, + endIdx = startIdx + utf8.len(currentLine) + }) + currentLine = word + startIdx = startIdx + utf8.len(currentLine) + 1 + else + currentLine = testLine + end + end + else + -- Character wrapping + local lineLength = utf8.len(line) + for i = 1, lineLength do + local char = utf8.sub(line, i, i) + local testLine = currentLine .. char + local width = font:getWidth(testLine) + + if width > maxWidth and currentLine ~= "" then + table.insert(wrappedParts, { + text = currentLine, + startIdx = startIdx, + endIdx = startIdx + utf8.len(currentLine) + }) + currentLine = char + startIdx = i - 1 + else + currentLine = testLine + end + end + end + + -- Add remaining text + if currentLine ~= "" then + table.insert(wrappedParts, { + text = currentLine, + startIdx = startIdx, + endIdx = startIdx + utf8.len(currentLine) + }) + end + + -- Ensure at least one part + if #wrappedParts == 0 then + table.insert(wrappedParts, { + text = "", + startIdx = 0, + endIdx = 0 + }) + end + + return wrappedParts +end + +--- Get font for text rendering +---@return love.Font +function Element:_getFont() + -- Get font path from theme or element + local fontPath = nil + if self.fontFamily then + local themeToUse = self.theme and themes[self.theme] or Theme.getActive() + if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then + fontPath = themeToUse.fonts[self.fontFamily] + else + -- Assume fontFamily is a direct path + fontPath = self.fontFamily + end + end + + return FONT_CACHE.getFont(self.textSize, fontPath) +end + +-- ==================== +-- Input Handling - Keyboard Input +-- ==================== + +--- Handle text input (character input) +---@param text string -- Character(s) to insert +function Element:textinput(text) + if not self.editable or not self._focused then return end + + -- Trigger onTextInput callback if defined + if self.onTextInput then + local result = self.onTextInput(self, text) + -- If callback returns false, cancel the input + if result == false then return end + end + + -- Capture old text for callback + local oldText = self._textBuffer + + -- Delete selection if exists + local hadSelection = self:hasSelection() + if hadSelection then + self:deleteSelection() + end + + -- Insert text at cursor position + self:insertText(text) + + -- Trigger onTextChange callback if text changed + if self.onTextChange and self._textBuffer ~= oldText then + self.onTextChange(self, self._textBuffer, oldText) + end +end + +--- Handle key press (special keys) +---@param key string -- Key name +---@param scancode string -- Scancode +---@param isrepeat boolean -- Whether this is a key repeat +function Element:keypressed(key, scancode, isrepeat) + if not self.editable or not self._focused then return end + + local modifiers = getModifiers() + local ctrl = modifiers.ctrl or modifiers.super -- Support both Ctrl and Cmd + + -- Handle cursor movement + if key == "left" then + if self:hasSelection() and not modifiers.shift then + -- Move to start of selection + local startPos, _ = self:getSelection() + self._cursorPosition = startPos + self:clearSelection() + else + self:moveCursorBy(-1) + end + self:_resetCursorBlink() + + elseif key == "right" then + if self:hasSelection() and not modifiers.shift then + -- Move to end of selection + local _, endPos = self:getSelection() + self._cursorPosition = endPos + self:clearSelection() + else + self:moveCursorBy(1) + end + self:_resetCursorBlink() + + elseif key == "home" or (ctrl and key == "a" and not self.multiline) then + -- Move to line start (or document start for single-line) + if ctrl or not self.multiline then + self:moveCursorToStart() + else + self:moveCursorToLineStart() + end + if key == "home" then + self:clearSelection() + end + self:_resetCursorBlink() + + elseif key == "end" or (ctrl and key == "e" and not self.multiline) then + -- Move to line end (or document end for single-line) + if ctrl or not self.multiline then + self:moveCursorToEnd() + else + self:moveCursorToLineEnd() + end + if key == "end" then + self:clearSelection() + end + self:_resetCursorBlink() + + -- Handle backspace and delete + elseif key == "backspace" then + local oldText = self._textBuffer + if self:hasSelection() then + -- Delete selection + self:deleteSelection() + elseif self._cursorPosition > 0 then + -- Delete character before cursor + self:deleteText(self._cursorPosition - 1, self._cursorPosition) + self._cursorPosition = self._cursorPosition - 1 + self:_validateCursorPosition() + end + + -- Trigger onTextChange callback + if self.onTextChange and self._textBuffer ~= oldText then + self.onTextChange(self, self._textBuffer, oldText) + end + self:_resetCursorBlink() + + elseif key == "delete" then + local oldText = self._textBuffer + if self:hasSelection() then + -- Delete selection + self:deleteSelection() + else + -- Delete character after cursor + local textLength = utf8.len(self._textBuffer or "") + if self._cursorPosition < textLength then + self:deleteText(self._cursorPosition, self._cursorPosition + 1) + end + end + + -- Trigger onTextChange callback + if self.onTextChange and self._textBuffer ~= oldText then + self.onTextChange(self, self._textBuffer, oldText) + end + self:_resetCursorBlink() + + -- Handle return/enter + elseif key == "return" or key == "kpenter" then + if self.multiline then + -- Insert newline + local oldText = self._textBuffer + if self:hasSelection() then + self:deleteSelection() + end + self:insertText("\n") + + -- Trigger onTextChange callback + if self.onTextChange and self._textBuffer ~= oldText then + self.onTextChange(self, self._textBuffer, oldText) + end + else + -- Trigger onEnter callback for single-line + if self.onEnter then + self.onEnter(self) + end + end + self:_resetCursorBlink() + + -- Handle Ctrl/Cmd+A (select all) + elseif ctrl and key == "a" then + self:selectAll() + self:_resetCursorBlink() + + -- Handle Escape + elseif key == "escape" then + if self:hasSelection() then + -- Clear selection + self:clearSelection() + else + -- Blur element + self:blur() + end + self:_resetCursorBlink() + end +end + Gui.new = Element.new Gui.Element = Element Gui.Animation = Animation diff --git a/examples/01_flex_positioning.lua b/examples/01_flex_positioning.lua new file mode 100644 index 0000000..29834db --- /dev/null +++ b/examples/01_flex_positioning.lua @@ -0,0 +1,219 @@ +--[[ + FlexLove Example 01: Flex Positioning + + This example demonstrates flexbox layouts in FlexLove: + - Flex direction (horizontal/vertical) + - Justify content (main axis alignment) + - Align items (cross axis alignment) + - Flex wrap behavior + + Run with: love /path/to/libs/examples/01_flex_positioning.lua +]] + +-- Map Love to Lv to avoid duplicate definitions +local Lv = love + +-- Load FlexLove from parent directory +local FlexLove = require("../FlexLove") +local Gui = FlexLove.Gui +local Color = FlexLove.Color +local enums = FlexLove.enums + +function Lv.load() + -- Initialize FlexLove with base scaling + Gui.init({ + baseScale = { width = 1920, height = 1080 } + }) + + -- Title + Gui.new({ + x = "2vw", + y = "2vh", + width = "96vw", + height = "6vh", + text = "FlexLove Example 01: Flex Positioning", + textSize = "4vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- ======================================== + -- Section 1: Horizontal Flex with Different JustifyContent Values + -- ======================================== + + local yOffset = 10 + + -- Label for justify-content section + Gui.new({ + x = "2vw", + y = yOffset .. "vh", + width = "96vw", + height = "3vh", + text = "Horizontal Flex - JustifyContent Options", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + yOffset = yOffset + 4 + + -- Demonstrate each justify-content option + local justifyOptions = { + { name = "flex-start", value = enums.JustifyContent.FLEX_START }, + { name = "center", value = enums.JustifyContent.CENTER }, + { name = "flex-end", value = enums.JustifyContent.FLEX_END }, + { name = "space-between", value = enums.JustifyContent.SPACE_BETWEEN }, + { name = "space-around", value = enums.JustifyContent.SPACE_AROUND }, + { name = "space-evenly", value = enums.JustifyContent.SPACE_EVENLY }, + } + + for _, option in ipairs(justifyOptions) do + -- Label for this justify option + Gui.new({ + x = "2vw", + y = yOffset .. "vh", + width = "15vw", + height = "3vh", + text = option.name, + textSize = "1.8vh", + textColor = Color.new(0.8, 0.8, 1, 1), + textAlign = enums.TextAlign.START, + }) + + -- Container demonstrating this justify-content value + local container = Gui.new({ + x = "18vw", + y = yOffset .. "vh", + width = "78vw", + height = "8vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.HORIZONTAL, + justifyContent = option.value, + alignItems = enums.AlignItems.CENTER, + backgroundColor = Color.new(0.15, 0.15, 0.2, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.3, 0.3, 0.4, 1), + }) + + -- Add child elements + local colors = { + Color.new(0.8, 0.3, 0.3, 1), + Color.new(0.3, 0.8, 0.3, 1), + Color.new(0.3, 0.3, 0.8, 1), + Color.new(0.8, 0.8, 0.3, 1), + } + + for j = 1, 4 do + Gui.new({ + parent = container, + width = "8vw", + height = "5vh", + backgroundColor = colors[j], + text = tostring(j), + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + end + + yOffset = yOffset + 9 + end + + -- ======================================== + -- Section 2: Vertical Flex with Different AlignItems Values + -- ======================================== + + yOffset = yOffset + 2 + + -- Label for align-items section + Gui.new({ + x = "2vw", + y = yOffset .. "vh", + width = "96vw", + height = "3vh", + text = "Vertical Flex - AlignItems Options", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + yOffset = yOffset + 4 + + -- Note: Due to space constraints, we'll show a subset in a horizontal layout + local alignOptions = { + { name = "stretch", value = enums.AlignItems.STRETCH }, + { name = "flex-start", value = enums.AlignItems.FLEX_START }, + { name = "center", value = enums.AlignItems.CENTER }, + { name = "flex-end", value = enums.AlignItems.FLEX_END }, + } + + local xOffset = 2 + local containerWidth = 22 + + for _, option in ipairs(alignOptions) do + -- Label for this align option + Gui.new({ + x = xOffset .. "vw", + y = yOffset .. "vh", + width = containerWidth .. "vw", + height = "2.5vh", + text = option.name, + textSize = "1.8vh", + textColor = Color.new(0.8, 1, 0.8, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Container demonstrating this align-items value + local container = Gui.new({ + x = xOffset .. "vw", + y = (yOffset + 3) .. "vh", + width = containerWidth .. "vw", + height = "20vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.VERTICAL, + justifyContent = enums.JustifyContent.FLEX_START, + alignItems = option.value, + gap = 5, + backgroundColor = Color.new(0.15, 0.2, 0.15, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.3, 0.4, 0.3, 1), + }) + + -- Add child elements with varying widths + local widths = { "8vw", "12vw", "6vw" } + local colors = { + Color.new(0.9, 0.4, 0.4, 1), + Color.new(0.4, 0.9, 0.4, 1), + Color.new(0.4, 0.4, 0.9, 1), + } + + for j = 1, 3 do + Gui.new({ + parent = container, + width = option.value == enums.AlignItems.STRETCH and "auto" or widths[j], + height = "4vh", + backgroundColor = colors[j], + text = tostring(j), + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + end + + xOffset = xOffset + containerWidth + 2 + end +end + +function Lv.update(dt) + Gui.update(dt) +end + +function Lv.draw() + -- Dark background + Lv.graphics.clear(0.05, 0.05, 0.08, 1) + Gui.draw() +end + +function Lv.resize(w, h) + Gui.resize(w, h) +end + +-- Note: Mouse event handlers would be added here if needed for interactivity diff --git a/examples/02_grid_layout.lua b/examples/02_grid_layout.lua new file mode 100644 index 0000000..f0b7087 --- /dev/null +++ b/examples/02_grid_layout.lua @@ -0,0 +1,229 @@ +--[[ + FlexLove Example 02: Grid Layout + + This example demonstrates grid layouts in FlexLove: + - Different grid configurations (2x2, 3x3, 4x2) + - Row and column gaps + - AlignItems behavior in grid cells + - Automatic grid cell positioning + + Run with: love /path/to/libs/examples/02_grid_layout.lua +]] + +local Lv = love + +local FlexLove = require("../FlexLove") +local Gui = FlexLove.Gui +local Color = FlexLove.Color +local enums = FlexLove.enums + +function Lv.load() + Gui.init({ + baseScale = { width = 1920, height = 1080 } + }) + + -- Title + Gui.new({ + x = "2vw", + y = "2vh", + width = "96vw", + height = "6vh", + text = "FlexLove Example 02: Grid Layout", + textSize = "4vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- ======================================== + -- Section 1: 2x2 Grid with Gaps + -- ======================================== + + Gui.new({ + x = "2vw", + y = "10vh", + width = "30vw", + height = "3vh", + text = "2x2 Grid (rowGap: 10px, columnGap: 10px)", + textSize = "2vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local grid2x2 = Gui.new({ + x = "2vw", + y = "14vh", + width = "30vw", + height = "30vh", + positioning = enums.Positioning.GRID, + gridRows = 2, + gridColumns = 2, + rowGap = 10, + columnGap = 10, + backgroundColor = Color.new(0.1, 0.1, 0.15, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.3, 0.3, 0.4, 1), + }) + + -- Add 4 cells to 2x2 grid + local colors2x2 = { + Color.new(0.8, 0.3, 0.3, 1), + Color.new(0.3, 0.8, 0.3, 1), + Color.new(0.3, 0.3, 0.8, 1), + Color.new(0.8, 0.8, 0.3, 1), + } + + for j = 1, 4 do + Gui.new({ + parent = grid2x2, + backgroundColor = colors2x2[j], + text = "Cell " .. j, + textSize = "2.5vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + end + + -- ======================================== + -- Section 2: 3x3 Grid with Different Gap Sizes + -- ======================================== + + Gui.new({ + x = "34vw", + y = "10vh", + width = "30vw", + height = "3vh", + text = "3x3 Grid (rowGap: 5px, columnGap: 15px)", + textSize = "2vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local grid3x3 = Gui.new({ + x = "34vw", + y = "14vh", + width = "30vw", + height = "30vh", + positioning = enums.Positioning.GRID, + gridRows = 3, + gridColumns = 3, + rowGap = 5, + columnGap = 15, + backgroundColor = Color.new(0.1, 0.15, 0.1, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.3, 0.4, 0.3, 1), + }) + + -- Add 9 cells to 3x3 grid + for j = 1, 9 do + local hue = (j - 1) / 9 + Gui.new({ + parent = grid3x3, + backgroundColor = Color.new(0.3 + hue * 0.5, 0.5, 0.7 - hue * 0.4, 1), + text = tostring(j), + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + end + + -- ======================================== + -- Section 3: 4x2 Grid with AlignItems + -- ======================================== + + Gui.new({ + x = "66vw", + y = "10vh", + width = "32vw", + height = "3vh", + text = "4x2 Grid (alignItems: center)", + textSize = "2vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local grid4x2 = Gui.new({ + x = "66vw", + y = "14vh", + width = "32vw", + height = "30vh", + positioning = enums.Positioning.GRID, + gridRows = 4, + gridColumns = 2, + rowGap = 8, + columnGap = 8, + alignItems = enums.AlignItems.CENTER, + backgroundColor = Color.new(0.15, 0.1, 0.15, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.4, 0.3, 0.4, 1), + }) + + -- Add 8 cells with varying content + for j = 1, 8 do + Gui.new({ + parent = grid4x2, + backgroundColor = Color.new(0.6, 0.4 + j * 0.05, 0.7 - j * 0.05, 1), + text = "Item " .. j, + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + end + + -- ======================================== + -- Section 4: Grid with Responsive Units (vw/vh gaps) + -- ======================================== + + Gui.new({ + x = "2vw", + y = "46vh", + width = "96vw", + height = "3vh", + text = "Grid with Responsive Gaps (rowGap: 2vh, columnGap: 2vw)", + textSize = "2vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local gridResponsive = Gui.new({ + x = "2vw", + y = "50vh", + width = "96vw", + height = "45vh", + positioning = enums.Positioning.GRID, + gridRows = 2, + gridColumns = 5, + rowGap = "2vh", + columnGap = "2vw", + backgroundColor = Color.new(0.08, 0.08, 0.12, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.25, 0.25, 0.35, 1), + }) + + -- Add 10 cells with gradient colors + for j = 1, 10 do + local progress = (j - 1) / 9 + Gui.new({ + parent = gridResponsive, + backgroundColor = Color.new( + 0.2 + progress * 0.6, + 0.4 + math.sin(progress * 3.14) * 0.4, + 0.8 - progress * 0.4, + 1 + ), + text = "Cell " .. j, + textSize = "2.5vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + }) + end +end + +function Lv.update(dt) + Gui.update(dt) +end + +function Lv.draw() + Lv.graphics.clear(0.05, 0.05, 0.08, 1) + Gui.draw() +end + +function Lv.resize(w, h) + Gui.resize(w, h) +end diff --git a/examples/03_theming_system.lua b/examples/03_theming_system.lua new file mode 100644 index 0000000..a40b168 --- /dev/null +++ b/examples/03_theming_system.lua @@ -0,0 +1,301 @@ +--[[ + FlexLove Example 03: Theming System + + This example demonstrates the theming system in FlexLove: + - Loading and applying themes + - Theme components (button, panel, card) + - Theme states (normal, hover, pressed, disabled, active) + - Theme colors and fonts + + Run with: love /path/to/libs/examples/03_theming_system.lua +]] + +local Lv = love + +local FlexLove = require("../FlexLove") +local Gui = FlexLove.Gui +local Color = FlexLove.Color +local enums = FlexLove.enums + +function Lv.load() + Gui.init({ + baseScale = { width = 1920, height = 1080 } + }) + + -- Load the space theme + Gui.loadTheme("space", "../themes/space") + + -- Title + Gui.new({ + x = "2vw", + y = "2vh", + width = "96vw", + height = "6vh", + text = "FlexLove Example 03: Theming System", + textSize = "4vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- ======================================== + -- Section 1: Theme Components + -- ======================================== + + Gui.new({ + x = "2vw", + y = "10vh", + width = "96vw", + height = "3vh", + text = "Theme Components - Space Theme", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + -- Card component + Gui.new({ + x = "2vw", + y = "14vh", + width = "30vw", + height = "20vh", + theme = "space", + themeComponent = "card", + text = "Card Component", + textSize = "2.5vh", + textColor = Color.new(0.8, 0.9, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Panel component + Gui.new({ + x = "34vw", + y = "14vh", + width = "30vw", + height = "20vh", + theme = "space", + themeComponent = "panel", + text = "Panel Component", + textSize = "2.5vh", + textColor = Color.new(0.8, 0.9, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Panel red component + Gui.new({ + x = "66vw", + y = "14vh", + width = "32vw", + height = "20vh", + theme = "space", + themeComponent = "panelred", + text = "Panel Red Component", + textSize = "2.5vh", + textColor = Color.new(1, 0.8, 0.8, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- ======================================== + -- Section 2: Button States + -- ======================================== + + Gui.new({ + x = "2vw", + y = "36vh", + width = "96vw", + height = "3vh", + text = "Button States - Hover and Click to See State Changes", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + -- Normal button (hover to see hover state, click to see pressed state) + Gui.new({ + x = "2vw", + y = "40vh", + width = "22vw", + height = "8vh", + theme = "space", + themeComponent = "button", + text = "Normal Button", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + callback = function(element, event) + if event.type == "click" then + print("Normal button clicked!") + end + end, + }) + + -- Active button (simulating active state) + local activeButton = Gui.new({ + x = "26vw", + y = "40vh", + width = "22vw", + height = "8vh", + theme = "space", + themeComponent = "button", + text = "Active Button", + textSize = "2vh", + textColor = Color.new(0.3, 1, 0.3, 1), + textAlign = enums.TextAlign.CENTER, + active = true, + callback = function(element, event) + if event.type == "click" then + element.active = not element.active + print("Active button toggled:", element.active) + end + end, + }) + + -- Disabled button + Gui.new({ + x = "50vw", + y = "40vh", + width = "22vw", + height = "8vh", + theme = "space", + themeComponent = "button", + text = "Disabled Button", + textSize = "2vh", + textColor = Color.new(0.5, 0.5, 0.5, 1), + textAlign = enums.TextAlign.CENTER, + disabled = true, + callback = function(element, event) + -- This won't be called because button is disabled + print("This shouldn't print!") + end, + }) + + -- Button with callback feedback + local clickCount = 0 + local counterButton = Gui.new({ + x = "74vw", + y = "40vh", + width = "24vw", + height = "8vh", + theme = "space", + themeComponent = "button", + text = "Click Me! (0)", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + callback = function(element, event) + if event.type == "click" then + clickCount = clickCount + 1 + element.text = "Click Me! (" .. clickCount .. ")" + end + end, + }) + + -- ======================================== + -- Section 3: Theme Colors and Fonts + -- ======================================== + + Gui.new({ + x = "2vw", + y = "50vh", + width = "96vw", + height = "3vh", + text = "Theme Colors and Fonts", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + -- Container showing theme colors + local colorContainer = Gui.new({ + x = "2vw", + y = "54vh", + width = "96vw", + height = "20vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.HORIZONTAL, + justifyContent = enums.JustifyContent.SPACE_EVENLY, + alignItems = enums.AlignItems.CENTER, + backgroundColor = Color.new(0.1, 0.1, 0.15, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.3, 0.3, 0.4, 1), + }) + + -- Primary color swatch + Gui.new({ + parent = colorContainer, + width = "20vw", + height = "15vh", + backgroundColor = Color.new(0.08, 0.75, 0.95, 1), -- Theme primary color + text = "Primary Color", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + }) + + -- Secondary color swatch + Gui.new({ + parent = colorContainer, + width = "20vw", + height = "15vh", + backgroundColor = Color.new(0.15, 0.20, 0.25, 1), -- Theme secondary color + text = "Secondary Color", + textSize = "2vh", + textColor = Color.new(0.8, 0.9, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + }) + + -- Text color swatch + Gui.new({ + parent = colorContainer, + width = "20vw", + height = "15vh", + backgroundColor = Color.new(0.2, 0.2, 0.25, 1), + text = "Text Color", + textSize = "2vh", + textColor = Color.new(0.80, 0.90, 1.00, 1), -- Theme text color + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + }) + + -- Text dark color swatch + Gui.new({ + parent = colorContainer, + width = "20vw", + height = "15vh", + backgroundColor = Color.new(0.25, 0.25, 0.3, 1), + text = "Text Dark Color", + textSize = "2vh", + textColor = Color.new(0.35, 0.40, 0.45, 1), -- Theme textDark color + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + }) + + -- ======================================== + -- Section 4: Font Family from Theme + -- ======================================== + + Gui.new({ + x = "2vw", + y = "76vh", + width = "96vw", + height = "18vh", + theme = "space", + themeComponent = "card", + text = "This text uses the theme's default font (VT323)", + textSize = "3vh", + textColor = Color.new(0.8, 0.9, 1, 1), + textAlign = enums.TextAlign.CENTER, + fontFamily = "default", -- References theme font + }) +end + +function Lv.update(dt) + Gui.update(dt) +end + +function Lv.draw() + Lv.graphics.clear(0.05, 0.05, 0.08, 1) + Gui.draw() +end + +function Lv.resize(w, h) + Gui.resize(w, h) +end diff --git a/examples/04_responsive_units.lua b/examples/04_responsive_units.lua new file mode 100644 index 0000000..cddc503 --- /dev/null +++ b/examples/04_responsive_units.lua @@ -0,0 +1,241 @@ +--[[ + FlexLove Example 04: Responsive Units + + This example demonstrates responsive unit systems in FlexLove: + - Viewport units (vw, vh) + - Percentage units (%) + - Pixel units (px) + - How elements resize with the window + + Run with: love /path/to/libs/examples/04_responsive_units.lua + Try resizing the window to see responsive behavior! +]] + +local Lv = love + +local FlexLove = require("../FlexLove") +local Gui = FlexLove.Gui +local Color = FlexLove.Color +local enums = FlexLove.enums + +function Lv.load() + Gui.init({ + baseScale = { width = 1920, height = 1080 } + }) + + -- Title + Gui.new({ + x = "2vw", + y = "2vh", + width = "96vw", + height = "6vh", + text = "FlexLove Example 04: Responsive Units - Try Resizing!", + textSize = "4vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- ======================================== + -- Section 1: Viewport Width Units (vw) + -- ======================================== + + Gui.new({ + x = "2vw", + y = "10vh", + width = "96vw", + height = "3vh", + text = "Viewport Width (vw) - Scales with window width", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local vwContainer = Gui.new({ + x = "2vw", + y = "14vh", + width = "96vw", + height = "12vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.HORIZONTAL, + justifyContent = enums.JustifyContent.SPACE_EVENLY, + alignItems = enums.AlignItems.CENTER, + backgroundColor = Color.new(0.1, 0.1, 0.15, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.3, 0.3, 0.4, 1), + }) + + -- Elements with vw widths + local vwWidths = { "10vw", "15vw", "20vw", "25vw" } + local colors = { + Color.new(0.8, 0.3, 0.3, 1), + Color.new(0.3, 0.8, 0.3, 1), + Color.new(0.3, 0.3, 0.8, 1), + Color.new(0.8, 0.8, 0.3, 1), + } + + for i, width in ipairs(vwWidths) do + Gui.new({ + parent = vwContainer, + width = width, + height = "8vh", + backgroundColor = colors[i], + text = width, + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + end + + -- ======================================== + -- Section 2: Viewport Height Units (vh) + -- ======================================== + + Gui.new({ + x = "2vw", + y = "28vh", + width = "96vw", + height = "3vh", + text = "Viewport Height (vh) - Scales with window height", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local vhContainer = Gui.new({ + x = "2vw", + y = "32vh", + width = "96vw", + height = "30vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.HORIZONTAL, + justifyContent = enums.JustifyContent.SPACE_EVENLY, + alignItems = enums.AlignItems.FLEX_END, + backgroundColor = Color.new(0.1, 0.15, 0.1, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.3, 0.4, 0.3, 1), + }) + + -- Elements with vh heights + local vhHeights = { "8vh", "12vh", "16vh", "20vh", "24vh" } + + for i, height in ipairs(vhHeights) do + local hue = (i - 1) / 4 + Gui.new({ + parent = vhContainer, + width = "16vw", + height = height, + backgroundColor = Color.new(0.3 + hue * 0.5, 0.5, 0.7 - hue * 0.4, 1), + text = height, + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + end + + -- ======================================== + -- Section 3: Percentage Units (%) + -- ======================================== + + Gui.new({ + x = "2vw", + y = "64vh", + width = "46vw", + height = "3vh", + text = "Percentage (%) - Relative to parent", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local percentContainer = Gui.new({ + x = "2vw", + y = "68vh", + width = "46vw", + height = "28vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.VERTICAL, + justifyContent = enums.JustifyContent.SPACE_EVENLY, + alignItems = enums.AlignItems.STRETCH, + backgroundColor = Color.new(0.15, 0.1, 0.15, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.4, 0.3, 0.4, 1), + padding = { top = 10, right = 10, bottom = 10, left = 10 }, + }) + + -- Child elements with percentage widths + local percentWidths = { "25%", "50%", "75%", "100%" } + + for i, width in ipairs(percentWidths) do + local progress = (i - 1) / 3 + Gui.new({ + parent = percentContainer, + width = width, + height = "5vh", + backgroundColor = Color.new(0.6, 0.4 + progress * 0.4, 0.7 - progress * 0.4, 1), + text = width .. " of parent", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + end + + -- ======================================== + -- Section 4: Pixel Units (px) - Fixed Size + -- ======================================== + + Gui.new({ + x = "50vw", + y = "64vh", + width = "48vw", + height = "3vh", + text = "Pixels (px) - Fixed size (doesn't resize)", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local pxContainer = Gui.new({ + x = "50vw", + y = "68vh", + width = "48vw", + height = "28vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.HORIZONTAL, + justifyContent = enums.JustifyContent.CENTER, + alignItems = enums.AlignItems.CENTER, + gap = 10, + backgroundColor = Color.new(0.1, 0.12, 0.15, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.3, 0.35, 0.4, 1), + }) + + -- Fixed pixel size elements + local pxSizes = { + { w = 80, h = 80 }, + { w = 100, h = 100 }, + { w = 120, h = 120 }, + } + + for i, size in ipairs(pxSizes) do + Gui.new({ + parent = pxContainer, + width = size.w, + height = size.h, + backgroundColor = Color.new(0.8 - i * 0.2, 0.3 + i * 0.2, 0.5, 1), + text = size.w .. "px", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + }) + end +end + +function Lv.update(dt) + Gui.update(dt) +end + +function Lv.draw() + Lv.graphics.clear(0.05, 0.05, 0.08, 1) + Gui.draw() +end + +function Lv.resize(w, h) + Gui.resize(w, h) +end diff --git a/examples/05_animations.lua b/examples/05_animations.lua new file mode 100644 index 0000000..16d057c --- /dev/null +++ b/examples/05_animations.lua @@ -0,0 +1,247 @@ +--[[ + FlexLove Example 05: Animations + + This example demonstrates animation system in FlexLove: + - Fade animations + - Scale animations + - Custom animations with different easing functions + - Animation timing and interpolation + + Run with: love /path/to/libs/examples/05_animations.lua +]] + +local Lv = love + +local FlexLove = require("../FlexLove") +local Gui = FlexLove.Gui +local Color = FlexLove.Color +local Animation = FlexLove.Animation +local enums = FlexLove.enums + +-- Animation control variables +local fadeBox, scaleBox, easingBoxes + +function Lv.load() + Gui.init({ + baseScale = { width = 1920, height = 1080 } + }) + + -- Title + Gui.new({ + x = "2vw", + y = "2vh", + width = "96vw", + height = "6vh", + text = "FlexLove Example 05: Animations", + textSize = "4vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- ======================================== + -- Section 1: Fade Animation + -- ======================================== + + Gui.new({ + x = "2vw", + y = "10vh", + width = "46vw", + height = "3vh", + text = "Fade Animation - Click to trigger", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + fadeBox = Gui.new({ + x = "2vw", + y = "14vh", + width = "46vw", + height = "20vh", + backgroundColor = Color.new(0.3, 0.6, 0.9, 1), + text = "Click me to fade out and back in", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + callback = function(element, event) + if event.type == "click" then + -- Fade out then fade in + local fadeOut = Animation.fade(1.0, 1.0, 0.0) + fadeOut.easing = function(t) return t * t end -- easeInQuad + element.animation = fadeOut + + -- Queue fade in after fade out completes + local startTime = Lv.timer.getTime() + element._fadeCallback = function(el, dt) + if Lv.timer.getTime() - startTime >= 1.0 then + local fadeIn = Animation.fade(1.0, 0.0, 1.0) + fadeIn.easing = function(t) return t * (2 - t) end -- easeOutQuad + el.animation = fadeIn + el._fadeCallback = nil + end + end + end + end, + }) + + -- ======================================== + -- Section 2: Scale Animation + -- ======================================== + + Gui.new({ + x = "50vw", + y = "10vh", + width = "48vw", + height = "3vh", + text = "Scale Animation - Click to trigger", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + scaleBox = Gui.new({ + x = "50vw", + y = "14vh", + width = 400, + height = 200, + backgroundColor = Color.new(0.9, 0.4, 0.4, 1), + text = "Click me to scale up", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + callback = function(element, event) + if event.type == "click" then + -- Scale up + local scaleUp = Animation.scale( + 0.5, + { width = element.width, height = element.height }, + { width = element.width * 1.5, height = element.height * 1.5 } + ) + scaleUp.easing = function(t) return t < 0.5 and 2 * t * t or -1 + (4 - 2 * t) * t end -- easeInOutQuad + element.animation = scaleUp + + -- Queue scale down + local startTime = Lv.timer.getTime() + element._scaleCallback = function(el, dt) + if Lv.timer.getTime() - startTime >= 0.5 then + local scaleDown = Animation.scale( + 0.5, + { width = el.width, height = el.height }, + { width = 400, height = 200 } + ) + scaleDown.easing = function(t) return t < 0.5 and 2 * t * t or -1 + (4 - 2 * t) * t end + el.animation = scaleDown + el._scaleCallback = nil + end + end + end + end, + }) + + -- ======================================== + -- Section 3: Easing Functions Comparison + -- ======================================== + + Gui.new({ + x = "2vw", + y = "36vh", + width = "96vw", + height = "3vh", + text = "Easing Functions - Click any box to see different easing", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local easingContainer = Gui.new({ + x = "2vw", + y = "40vh", + width = "96vw", + height = "56vh", + positioning = enums.Positioning.GRID, + gridRows = 3, + gridColumns = 3, + rowGap = "2vh", + columnGap = "2vw", + backgroundColor = Color.new(0.08, 0.08, 0.12, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.25, 0.25, 0.35, 1), + padding = { top = 20, right = 20, bottom = 20, left = 20 }, + }) + + -- Different easing functions + local easings = { + { name = "Linear", func = function(t) return t end }, + { name = "EaseInQuad", func = function(t) return t * t end }, + { name = "EaseOutQuad", func = function(t) return t * (2 - t) end }, + { name = "EaseInOutQuad", func = function(t) return t < 0.5 and 2 * t * t or -1 + (4 - 2 * t) * t end }, + { name = "EaseInCubic", func = function(t) return t * t * t end }, + { name = "EaseOutCubic", func = function(t) local t1 = t - 1; return t1 * t1 * t1 + 1 end }, + { name = "EaseInQuart", func = function(t) return t * t * t * t end }, + { name = "EaseOutQuart", func = function(t) local t1 = t - 1; return 1 - t1 * t1 * t1 * t1 end }, + { name = "EaseInExpo", func = function(t) return t == 0 and 0 or math.pow(2, 10 * (t - 1)) end }, + } + + easingBoxes = {} + + for i, easing in ipairs(easings) do + local hue = (i - 1) / 8 + local box = Gui.new({ + parent = easingContainer, + backgroundColor = Color.new(0.2 + hue * 0.6, 0.4 + math.sin(hue * 3.14) * 0.4, 0.8 - hue * 0.4, 1), + text = easing.name, + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 8, + callback = function(element, event) + if event.type == "click" then + -- Fade out and in with this easing + local fadeOut = Animation.fade(0.8, 1.0, 0.2) + fadeOut.easing = easing.func + element.animation = fadeOut + + local startTime = Lv.timer.getTime() + element._easingCallback = function(el, dt) + if Lv.timer.getTime() - startTime >= 0.8 then + local fadeIn = Animation.fade(0.8, 0.2, 1.0) + fadeIn.easing = easing.func + el.animation = fadeIn + el._easingCallback = nil + end + end + end + end, + }) + table.insert(easingBoxes, box) + end +end + +function Lv.update(dt) + -- Handle fade callback + if fadeBox and fadeBox._fadeCallback then + fadeBox._fadeCallback(fadeBox, dt) + end + + -- Handle scale callback + if scaleBox and scaleBox._scaleCallback then + scaleBox._scaleCallback(scaleBox, dt) + end + + -- Handle easing callbacks + for _, box in ipairs(easingBoxes) do + if box._easingCallback then + box._easingCallback(box, dt) + end + end + + Gui.update(dt) +end + +function Lv.draw() + Lv.graphics.clear(0.05, 0.05, 0.08, 1) + Gui.draw() +end + +function Lv.resize(w, h) + Gui.resize(w, h) +end diff --git a/examples/06_event_system.lua b/examples/06_event_system.lua new file mode 100644 index 0000000..4c499ce --- /dev/null +++ b/examples/06_event_system.lua @@ -0,0 +1,319 @@ +--[[ + FlexLove Example 06: Event System + + This example demonstrates the event system in FlexLove: + - Click events (left, right, middle mouse buttons) + - Press and release events + - Event properties (position, modifiers, click count) + - Double-click detection + - Keyboard modifiers (Shift, Ctrl, Alt) + + Run with: love /path/to/libs/examples/06_event_system.lua +]] + +local Lv = love + +local FlexLove = require("../FlexLove") +local Gui = FlexLove.Gui +local Color = FlexLove.Color +local enums = FlexLove.enums + +-- Event log +local eventLog = {} +local maxLogEntries = 15 + +local function addLogEntry(text) + table.insert(eventLog, 1, text) + if #eventLog > maxLogEntries then + table.remove(eventLog) + end +end + +function Lv.load() + Gui.init({ + baseScale = { width = 1920, height = 1080 } + }) + + -- Title + Gui.new({ + x = "2vw", + y = "2vh", + width = "96vw", + height = "6vh", + text = "FlexLove Example 06: Event System", + textSize = "4vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- ======================================== + -- Section 1: Click Events + -- ======================================== + + Gui.new({ + x = "2vw", + y = "10vh", + width = "46vw", + height = "3vh", + text = "Click Events - Try left, right, middle mouse buttons", + textSize = "2vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local clickBox = Gui.new({ + x = "2vw", + y = "14vh", + width = "46vw", + height = "20vh", + backgroundColor = Color.new(0.3, 0.5, 0.7, 1), + text = "Click me with different mouse buttons!", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + callback = function(element, event) + local buttonName = event.button == 1 and "Left" or (event.button == 2 and "Right" or "Middle") + local eventTypeName = event.type:sub(1,1):upper() .. event.type:sub(2) + + if event.type == "click" or event.type == "rightclick" or event.type == "middleclick" then + addLogEntry(string.format("%s Click at (%.0f, %.0f) - Count: %d", + buttonName, event.x, event.y, event.clickCount)) + elseif event.type == "press" then + addLogEntry(string.format("%s Button Pressed at (%.0f, %.0f)", + buttonName, event.x, event.y)) + elseif event.type == "release" then + addLogEntry(string.format("%s Button Released at (%.0f, %.0f)", + buttonName, event.x, event.y)) + end + end, + }) + + -- ======================================== + -- Section 2: Keyboard Modifiers + -- ======================================== + + Gui.new({ + x = "50vw", + y = "10vh", + width = "48vw", + height = "3vh", + text = "Keyboard Modifiers - Hold Shift/Ctrl/Alt while clicking", + textSize = "2vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local modifierBox = Gui.new({ + x = "50vw", + y = "14vh", + width = "48vw", + height = "20vh", + backgroundColor = Color.new(0.7, 0.4, 0.5, 1), + text = "Click with modifiers!", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + callback = function(element, event) + if event.type == "click" then + local mods = {} + if event.modifiers.shift then table.insert(mods, "Shift") end + if event.modifiers.ctrl then table.insert(mods, "Ctrl") end + if event.modifiers.alt then table.insert(mods, "Alt") end + if event.modifiers.super then table.insert(mods, "Super") end + + local modText = #mods > 0 and table.concat(mods, "+") or "None" + addLogEntry(string.format("Click with modifiers: %s", modText)) + end + end, + }) + + -- ======================================== + -- Section 3: Double-Click Detection + -- ======================================== + + Gui.new({ + x = "2vw", + y = "36vh", + width = "46vw", + height = "3vh", + text = "Double-Click Detection", + textSize = "2vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local doubleClickBox = Gui.new({ + x = "2vw", + y = "40vh", + width = "46vw", + height = "15vh", + backgroundColor = Color.new(0.5, 0.7, 0.4, 1), + text = "Double-click me!", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + callback = function(element, event) + if event.type == "click" then + if event.clickCount == 1 then + addLogEntry("Single click detected") + elseif event.clickCount == 2 then + addLogEntry("DOUBLE CLICK detected!") + -- Visual feedback for double-click + element.backgroundColor = Color.new(0.9, 0.9, 0.3, 1) + elseif event.clickCount >= 3 then + addLogEntry(string.format("TRIPLE+ CLICK detected! (count: %d)", event.clickCount)) + element.backgroundColor = Color.new(0.9, 0.3, 0.9, 1) + end + + -- Reset color after a delay (simulated in update) + element._resetTime = Lv.timer.getTime() + 0.3 + end + end, + }) + + -- ======================================== + -- Section 4: Event Log Display + -- ======================================== + + Gui.new({ + x = "50vw", + y = "36vh", + width = "48vw", + height = "3vh", + text = "Event Log (most recent first)", + textSize = "2vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + -- Event log container + local logContainer = Gui.new({ + x = "50vw", + y = "40vh", + width = "48vw", + height = "56vh", + backgroundColor = Color.new(0.08, 0.08, 0.12, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.25, 0.25, 0.35, 1), + cornerRadius = 5, + }) + + -- ======================================== + -- Section 5: Interactive Buttons + -- ======================================== + + Gui.new({ + x = "2vw", + y = "57vh", + width = "46vw", + height = "3vh", + text = "Interactive Buttons", + textSize = "2vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local buttonContainer = Gui.new({ + x = "2vw", + y = "61vh", + width = "46vw", + height = "35vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.VERTICAL, + justifyContent = enums.JustifyContent.SPACE_EVENLY, + alignItems = enums.AlignItems.STRETCH, + backgroundColor = Color.new(0.1, 0.1, 0.15, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.3, 0.3, 0.4, 1), + padding = { top = 10, right = 10, bottom = 10, left = 10 }, + gap = 10, + }) + + -- Button 1: Press/Release events + Gui.new({ + parent = buttonContainer, + height = "8vh", + backgroundColor = Color.new(0.4, 0.5, 0.8, 1), + text = "Press and Release Events", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + callback = function(element, event) + if event.type == "press" then + addLogEntry("Button 1: PRESSED") + element.backgroundColor = Color.new(0.2, 0.3, 0.6, 1) + elseif event.type == "release" then + addLogEntry("Button 1: RELEASED") + element.backgroundColor = Color.new(0.4, 0.5, 0.8, 1) + end + end, + }) + + -- Button 2: Click counter + local clickCounter = 0 + Gui.new({ + parent = buttonContainer, + height = "8vh", + backgroundColor = Color.new(0.8, 0.5, 0.4, 1), + text = "Click Counter: 0", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + callback = function(element, event) + if event.type == "click" then + clickCounter = clickCounter + 1 + element.text = "Click Counter: " .. clickCounter + addLogEntry("Button 2: Click #" .. clickCounter) + end + end, + }) + + -- Button 3: Clear log + Gui.new({ + parent = buttonContainer, + height = "8vh", + backgroundColor = Color.new(0.6, 0.4, 0.6, 1), + text = "Clear Event Log", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + callback = function(element, event) + if event.type == "click" then + eventLog = {} + addLogEntry("Log cleared!") + end + end, + }) +end + +function Lv.update(dt) + -- Reset double-click box color + if doubleClickBox and doubleClickBox._resetTime and Lv.timer.getTime() >= doubleClickBox._resetTime then + doubleClickBox.backgroundColor = Color.new(0.5, 0.7, 0.4, 1) + doubleClickBox._resetTime = nil + end + + Gui.update(dt) +end + +function Lv.draw() + Lv.graphics.clear(0.05, 0.05, 0.08, 1) + Gui.draw() + + -- Draw event log + Lv.graphics.setColor(0.8, 0.9, 1, 1) + local logX = Lv.graphics.getWidth() * 0.50 + 10 + local logY = Lv.graphics.getHeight() * 0.40 + 10 + local lineHeight = 20 + + for i, entry in ipairs(eventLog) do + local alpha = 1.0 - (i - 1) / maxLogEntries * 0.5 + Lv.graphics.setColor(0.8, 0.9, 1, alpha) + Lv.graphics.print(entry, logX, logY + (i - 1) * lineHeight) + end +end + +function Lv.resize(w, h) + Gui.resize(w, h) +end diff --git a/examples/07_text_rendering.lua b/examples/07_text_rendering.lua new file mode 100644 index 0000000..d1f1b1c --- /dev/null +++ b/examples/07_text_rendering.lua @@ -0,0 +1,189 @@ +--[[ + FlexLove Example 07: Text Rendering + + This example demonstrates text rendering features in FlexLove: + - Text alignment (start, center, end, justify) + - Text size presets (xxs, xs, sm, md, lg, xl, xxl, 3xl, 4xl) + - Font rendering with custom fonts + - Text wrapping and positioning + + Run with: love /path/to/libs/examples/07_text_rendering.lua +]] + +local Lv = love + +local FlexLove = require("../FlexLove") +local Gui = FlexLove.Gui +local Color = FlexLove.Color +local enums = FlexLove.enums + +function Lv.load() + Gui.init({ + baseScale = { width = 1920, height = 1080 } + }) + + -- Title + Gui.new({ + x = "2vw", + y = "2vh", + width = "96vw", + height = "6vh", + text = "FlexLove Example 07: Text Rendering", + textSize = "4vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- ======================================== + -- Section 1: Text Alignment + -- ======================================== + + Gui.new({ + x = "2vw", + y = "10vh", + width = "96vw", + height = "3vh", + text = "Text Alignment Options", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local alignments = { + { name = "START", value = enums.TextAlign.START }, + { name = "CENTER", value = enums.TextAlign.CENTER }, + { name = "END", value = enums.TextAlign.END }, + } + + local yOffset = 14 + + for _, align in ipairs(alignments) do + Gui.new({ + x = "2vw", + y = yOffset .. "vh", + width = "30vw", + height = "8vh", + backgroundColor = Color.new(0.15, 0.15, 0.2, 1), + text = "Align: " .. align.name, + textSize = "2vh", + textColor = Color.new(0.8, 0.9, 1, 1), + textAlign = align.value, + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.3, 0.3, 0.4, 1), + cornerRadius = 5, + }) + yOffset = yOffset + 9 + end + + -- ======================================== + -- Section 2: Text Size Presets + -- ======================================== + + Gui.new({ + x = "34vw", + y = "10vh", + width = "64vw", + height = "3vh", + text = "Text Size Presets", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local textSizes = { + { name = "XXS", value = "xxs" }, + { name = "XS", value = "xs" }, + { name = "SM", value = "sm" }, + { name = "MD", value = "md" }, + { name = "LG", value = "lg" }, + { name = "XL", value = "xl" }, + { name = "XXL", value = "xxl" }, + { name = "3XL", value = "3xl" }, + { name = "4XL", value = "4xl" }, + } + + local sizeContainer = Gui.new({ + x = "34vw", + y = "14vh", + width = "64vw", + height = "76vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.VERTICAL, + justifyContent = enums.JustifyContent.FLEX_START, + alignItems = enums.AlignItems.STRETCH, + gap = 5, + backgroundColor = Color.new(0.08, 0.08, 0.12, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.25, 0.25, 0.35, 1), + padding = { top = 10, right = 10, bottom = 10, left = 10 }, + }) + + for i, size in ipairs(textSizes) do + local hue = (i - 1) / 8 + Gui.new({ + parent = sizeContainer, + height = "7vh", + backgroundColor = Color.new(0.2 + hue * 0.3, 0.3 + hue * 0.2, 0.5 - hue * 0.2, 1), + text = size.name .. " - The quick brown fox jumps over the lazy dog", + textSize = size.value, + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.START, + cornerRadius = 3, + }) + end + + -- ======================================== + -- Section 3: Custom Font Sizes (vh units) + -- ======================================== + + Gui.new({ + x = "2vw", + y = "41vh", + width = "30vw", + height = "3vh", + text = "Custom Text Sizes (vh units)", + textSize = "2vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local customSizes = { "1vh", "2vh", "3vh", "4vh", "5vh" } + + local customContainer = Gui.new({ + x = "2vw", + y = "45vh", + width = "30vw", + height = "45vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.VERTICAL, + justifyContent = enums.JustifyContent.SPACE_EVENLY, + alignItems = enums.AlignItems.STRETCH, + backgroundColor = Color.new(0.1, 0.12, 0.15, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.3, 0.35, 0.4, 1), + padding = { top = 10, right = 10, bottom = 10, left = 10 }, + }) + + for i, size in ipairs(customSizes) do + Gui.new({ + parent = customContainer, + backgroundColor = Color.new(0.3, 0.4 + i * 0.08, 0.6 - i * 0.08, 1), + text = size .. " text", + textSize = size, + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 3, + padding = { top = 5, right = 10, bottom = 5, left = 10 }, + }) + end +end + +function Lv.update(dt) + Gui.update(dt) +end + +function Lv.draw() + Lv.graphics.clear(0.05, 0.05, 0.08, 1) + Gui.draw() +end + +function Lv.resize(w, h) + Gui.resize(w, h) +end diff --git a/examples/08_absolute_relative_positioning.lua b/examples/08_absolute_relative_positioning.lua new file mode 100644 index 0000000..a1c0e43 --- /dev/null +++ b/examples/08_absolute_relative_positioning.lua @@ -0,0 +1,236 @@ +--[[ + FlexLove Example 08: Absolute vs Relative Positioning + + This example demonstrates positioning modes in FlexLove: + - Absolute positioning (fixed coordinates) + - Relative positioning (relative to siblings) + - Comparison between the two modes + - Practical use cases + + Run with: love /path/to/libs/examples/08_absolute_relative_positioning.lua +]] + +local Lv = love + +local FlexLove = require("../FlexLove") +local Gui = FlexLove.Gui +local Color = FlexLove.Color +local enums = FlexLove.enums + +function Lv.load() + Gui.init({ + baseScale = { width = 1920, height = 1080 } + }) + + -- Title + Gui.new({ + x = "2vw", + y = "2vh", + width = "96vw", + height = "6vh", + text = "FlexLove Example 08: Absolute vs Relative Positioning", + textSize = "4vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- ======================================== + -- Section 1: Absolute Positioning + -- ======================================== + + Gui.new({ + x = "2vw", + y = "10vh", + width = "46vw", + height = "3vh", + text = "Absolute Positioning - Fixed coordinates", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local absoluteContainer = Gui.new({ + x = "2vw", + y = "14vh", + width = "46vw", + height = "40vh", + positioning = enums.Positioning.ABSOLUTE, + backgroundColor = Color.new(0.1, 0.1, 0.15, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.3, 0.3, 0.4, 1), + }) + + -- Absolute positioned children + Gui.new({ + parent = absoluteContainer, + x = 20, + y = 20, + width = 150, + height = 80, + positioning = enums.Positioning.ABSOLUTE, + backgroundColor = Color.new(0.8, 0.3, 0.3, 1), + text = "x:20, y:20", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + }) + + Gui.new({ + parent = absoluteContainer, + x = 200, + y = 50, + width = 150, + height = 80, + positioning = enums.Positioning.ABSOLUTE, + backgroundColor = Color.new(0.3, 0.8, 0.3, 1), + text = "x:200, y:50", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + }) + + Gui.new({ + parent = absoluteContainer, + x = 100, + y = 150, + width = 150, + height = 80, + positioning = enums.Positioning.ABSOLUTE, + backgroundColor = Color.new(0.3, 0.3, 0.8, 1), + text = "x:100, y:150", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + }) + + Gui.new({ + parent = absoluteContainer, + x = 280, + y = 180, + width = 150, + height = 80, + positioning = enums.Positioning.ABSOLUTE, + backgroundColor = Color.new(0.8, 0.8, 0.3, 1), + text = "x:280, y:180", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + }) + + -- ======================================== + -- Section 2: Relative Positioning + -- ======================================== + + Gui.new({ + x = "50vw", + y = "10vh", + width = "48vw", + height = "3vh", + text = "Relative Positioning - Flows with siblings", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local relativeContainer = Gui.new({ + x = "50vw", + y = "14vh", + width = "48vw", + height = "40vh", + positioning = enums.Positioning.RELATIVE, + backgroundColor = Color.new(0.1, 0.15, 0.1, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.3, 0.4, 0.3, 1), + padding = { top = 10, right = 10, bottom = 10, left = 10 }, + }) + + -- Relative positioned children (flow naturally) + local relativeColors = { + Color.new(0.8, 0.3, 0.3, 1), + Color.new(0.3, 0.8, 0.3, 1), + Color.new(0.3, 0.3, 0.8, 1), + Color.new(0.8, 0.8, 0.3, 1), + } + + for i = 1, 4 do + Gui.new({ + parent = relativeContainer, + width = "45%", + height = "8vh", + positioning = enums.Positioning.RELATIVE, + backgroundColor = relativeColors[i], + text = "Element " .. i .. " (relative)", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + margin = { top = 5, right = 5, bottom = 5, left = 5 }, + }) + end + + -- ======================================== + -- Section 3: Comparison with Overlapping + -- ======================================== + + Gui.new({ + x = "2vw", + y = "56vh", + width = "96vw", + height = "3vh", + text = "Absolute Positioning Allows Overlapping", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local overlapContainer = Gui.new({ + x = "2vw", + y = "60vh", + width = "96vw", + height = "36vh", + positioning = enums.Positioning.ABSOLUTE, + backgroundColor = Color.new(0.08, 0.08, 0.12, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.25, 0.25, 0.35, 1), + }) + + -- Create overlapping elements + local overlapColors = { + Color.new(0.9, 0.3, 0.3, 0.7), + Color.new(0.3, 0.9, 0.3, 0.7), + Color.new(0.3, 0.3, 0.9, 0.7), + Color.new(0.9, 0.9, 0.3, 0.7), + } + + for i = 1, 4 do + Gui.new({ + parent = overlapContainer, + x = 50 + (i - 1) * 60, + y = 30 + (i - 1) * 40, + width = 300, + height = 150, + positioning = enums.Positioning.ABSOLUTE, + backgroundColor = overlapColors[i], + text = "Layer " .. i, + textSize = "2.5vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + z = i, -- Z-index for layering + }) + end +end + +function Lv.update(dt) + Gui.update(dt) +end + +function Lv.draw() + Lv.graphics.clear(0.05, 0.05, 0.08, 1) + Gui.draw() +end + +function Lv.resize(w, h) + Gui.resize(w, h) +end diff --git a/examples/09_styling_visual_effects.lua b/examples/09_styling_visual_effects.lua new file mode 100644 index 0000000..d78b441 --- /dev/null +++ b/examples/09_styling_visual_effects.lua @@ -0,0 +1,281 @@ +--[[ + FlexLove Example 09: Styling and Visual Effects + + This example demonstrates styling and visual effects: + - Corner radius (uniform and individual corners) + - Borders (different sides) + - Opacity levels + - Background colors + - Blur effects (contentBlur and backdropBlur) + + Run with: love /path/to/libs/examples/09_styling_visual_effects.lua +]] + +local Lv = love + +local FlexLove = require("../FlexLove") +local Gui = FlexLove.Gui +local Color = FlexLove.Color +local enums = FlexLove.enums + +function Lv.load() + Gui.init({ + baseScale = { width = 1920, height = 1080 } + }) + + -- Title + Gui.new({ + x = "2vw", + y = "2vh", + width = "96vw", + height = "5vh", + text = "FlexLove Example 09: Styling and Visual Effects", + textSize = "3.5vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- ======================================== + -- Section 1: Corner Radius + -- ======================================== + + Gui.new({ + x = "2vw", + y = "9vh", + width = "46vw", + height = "3vh", + text = "Corner Radius", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + -- Uniform corner radius + Gui.new({ + x = "2vw", + y = "13vh", + width = "14vw", + height = "12vh", + backgroundColor = Color.new(0.6, 0.3, 0.7, 1), + cornerRadius = 5, + text = "radius: 5", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + Gui.new({ + x = "17vw", + y = "13vh", + width = "14vw", + height = "12vh", + backgroundColor = Color.new(0.3, 0.6, 0.7, 1), + cornerRadius = 15, + text = "radius: 15", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Individual corner radius + Gui.new({ + x = "32vw", + y = "13vh", + width = "16vw", + height = "12vh", + backgroundColor = Color.new(0.7, 0.6, 0.3, 1), + cornerRadius = { + topLeft = 0, + topRight = 20, + bottomLeft = 20, + bottomRight = 0, + }, + text = "Individual\ncorners", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- ======================================== + -- Section 2: Borders + -- ======================================== + + Gui.new({ + x = "50vw", + y = "9vh", + width = "48vw", + height = "3vh", + text = "Borders", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + -- All borders + Gui.new({ + x = "50vw", + y = "13vh", + width = "14vw", + height = "12vh", + backgroundColor = Color.new(0.2, 0.2, 0.3, 1), + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.8, 0.4, 0.4, 1), + text = "All sides", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Top and bottom borders + Gui.new({ + x = "65vw", + y = "13vh", + width = "14vw", + height = "12vh", + backgroundColor = Color.new(0.2, 0.3, 0.2, 1), + border = { top = true, bottom = true }, + borderColor = Color.new(0.4, 0.8, 0.4, 1), + text = "Top & Bottom", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Left border only + Gui.new({ + x = "80vw", + y = "13vh", + width = "16vw", + height = "12vh", + backgroundColor = Color.new(0.2, 0.2, 0.3, 1), + border = { left = true }, + borderColor = Color.new(0.4, 0.4, 0.8, 1), + text = "Left only", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- ======================================== + -- Section 3: Opacity + -- ======================================== + + Gui.new({ + x = "2vw", + y = "27vh", + width = "96vw", + height = "3vh", + text = "Opacity Levels", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local opacityLevels = { 1.0, 0.75, 0.5, 0.25 } + + for i, opacity in ipairs(opacityLevels) do + Gui.new({ + x = (2 + (i - 1) * 24) .. "vw", + y = "31vh", + width = "22vw", + height = "12vh", + backgroundColor = Color.new(0.8, 0.3, 0.5, 1), + opacity = opacity, + text = "Opacity: " .. opacity, + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 8, + }) + end + + -- ======================================== + -- Section 4: Background Colors + -- ======================================== + + Gui.new({ + x = "2vw", + y = "45vh", + width = "96vw", + height = "3vh", + text = "Background Colors", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + -- Gradient-like colors + for i = 1, 8 do + local hue = (i - 1) / 7 + Gui.new({ + x = (2 + (i - 1) * 12) .. "vw", + y = "49vh", + width = "11vw", + height = "18vh", + backgroundColor = Color.new( + 0.3 + hue * 0.5, + 0.5 + math.sin(hue * 3.14) * 0.3, + 0.8 - hue * 0.5, + 1 + ), + text = tostring(i), + textSize = "3vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + }) + end + + -- ======================================== + -- Section 5: Blur Effects (if supported) + -- ======================================== + + Gui.new({ + x = "2vw", + y = "69vh", + width = "96vw", + height = "3vh", + text = "Blur Effects (contentBlur & backdropBlur)", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + -- Content blur example + Gui.new({ + x = "2vw", + y = "73vh", + width = "46vw", + height = "22vh", + backgroundColor = Color.new(0.3, 0.4, 0.6, 0.8), + contentBlur = { intensity = 5, quality = 3 }, + text = "Content Blur\n(blurs this element)", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + }) + + -- Backdrop blur example + Gui.new({ + x = "50vw", + y = "73vh", + width = "46vw", + height = "22vh", + backgroundColor = Color.new(0.6, 0.4, 0.3, 0.6), + backdropBlur = { intensity = 10, quality = 5 }, + text = "Backdrop Blur\n(blurs background)", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + }) +end + +function Lv.update(dt) + Gui.update(dt) +end + +function Lv.draw() + Lv.graphics.clear(0.05, 0.05, 0.08, 1) + Gui.draw() +end + +function Lv.resize(w, h) + Gui.resize(w, h) +end diff --git a/examples/10_padding_margins.lua b/examples/10_padding_margins.lua new file mode 100644 index 0000000..1630823 --- /dev/null +++ b/examples/10_padding_margins.lua @@ -0,0 +1,206 @@ +--[[ + FlexLove Example 10: Padding and Margins + + This example demonstrates padding and margin spacing: + - Uniform padding + - Individual padding sides + - Uniform margins + - Individual margin sides + - Visual indicators for spacing + + Run with: love /path/to/libs/examples/10_padding_margins.lua +]] + +local Lv = love + +local FlexLove = require("../FlexLove") +local Gui = FlexLove.Gui +local Color = FlexLove.Color +local enums = FlexLove.enums + +function Lv.load() + Gui.init({ + baseScale = { width = 1920, height = 1080 } + }) + + -- Title + Gui.new({ + x = "2vw", + y = "2vh", + width = "96vw", + height = "5vh", + text = "FlexLove Example 10: Padding and Margins", + textSize = "3.5vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Section 1: Padding Examples + Gui.new({ + x = "2vw", + y = "9vh", + width = "46vw", + height = "3vh", + text = "Padding (internal spacing)", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + -- Uniform padding + local container1 = Gui.new({ + x = "2vw", + y = "13vh", + width = "22vw", + height = "18vh", + backgroundColor = Color.new(0.3, 0.4, 0.6, 1), + padding = { top = 20, right = 20, bottom = 20, left = 20 }, + }) + + Gui.new({ + parent = container1, + width = "auto", + height = "auto", + backgroundColor = Color.new(0.8, 0.6, 0.3, 1), + text = "Uniform\npadding: 20px", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Individual padding sides + local container2 = Gui.new({ + x = "26vw", + y = "13vh", + width = "22vw", + height = "18vh", + backgroundColor = Color.new(0.4, 0.6, 0.3, 1), + padding = { top = 30, right = 10, bottom = 30, left = 10 }, + }) + + Gui.new({ + parent = container2, + width = "auto", + height = "auto", + backgroundColor = Color.new(0.8, 0.3, 0.6, 1), + text = "Individual\ntop/bottom: 30px\nleft/right: 10px", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Section 2: Margin Examples + Gui.new({ + x = "50vw", + y = "9vh", + width = "48vw", + height = "3vh", + text = "Margins (external spacing)", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + -- Container to show margins + local marginContainer = Gui.new({ + x = "50vw", + y = "13vh", + width = "46vw", + height = "38vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.VERTICAL, + backgroundColor = Color.new(0.15, 0.15, 0.2, 1), + }) + + -- Elements with different margins + Gui.new({ + parent = marginContainer, + width = "40vw", + height = "6vh", + backgroundColor = Color.new(0.7, 0.3, 0.3, 1), + margin = { top = 10, right = 10, bottom = 10, left = 10 }, + text = "Uniform margin: 10px", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + Gui.new({ + parent = marginContainer, + width = "40vw", + height = "6vh", + backgroundColor = Color.new(0.3, 0.7, 0.3, 1), + margin = { top = 5, right = 30, bottom = 5, left = 30 }, + text = "Horizontal margin: 30px", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + Gui.new({ + parent = marginContainer, + width = "40vw", + height = "6vh", + backgroundColor = Color.new(0.3, 0.3, 0.7, 1), + margin = { top = 20, right = 10, bottom = 20, left = 10 }, + text = "Vertical margin: 20px", + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Section 3: Combined Padding and Margins + Gui.new({ + x = "2vw", + y = "33vh", + width = "46vw", + height = "3vh", + text = "Combined Padding & Margins", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local combinedContainer = Gui.new({ + x = "2vw", + y = "37vh", + width = "46vw", + height = "58vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.VERTICAL, + backgroundColor = Color.new(0.1, 0.1, 0.15, 1), + padding = { top = 15, right = 15, bottom = 15, left = 15 }, + }) + + for i = 1, 3 do + local box = Gui.new({ + parent = combinedContainer, + width = "auto", + height = "14vh", + backgroundColor = Color.new(0.5 + i * 0.1, 0.4, 0.6 - i * 0.1, 1), + margin = { top = 10, right = 0, bottom = 10, left = 0 }, + padding = { top = 15, right = 15, bottom = 15, left = 15 }, + }) + + Gui.new({ + parent = box, + width = "auto", + height = "auto", + backgroundColor = Color.new(0.2, 0.2, 0.3, 1), + text = "Box " .. i .. "\nPadding: 15px\nMargin: 10px", + textSize = "1.8vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + end +end + +function Lv.update(dt) + Gui.update(dt) +end + +function Lv.draw() + Lv.graphics.clear(0.05, 0.05, 0.08, 1) + Gui.draw() +end + +function Lv.resize(w, h) + Gui.resize(w, h) +end diff --git a/examples/11_input_controls.lua b/examples/11_input_controls.lua new file mode 100644 index 0000000..43a4159 --- /dev/null +++ b/examples/11_input_controls.lua @@ -0,0 +1,149 @@ +--[[ + FlexLove Example 11: Input Controls + + This example demonstrates input controls (if available): + - Text input fields + - Keyboard input handling + - Focus management + + Run with: love /path/to/libs/examples/11_input_controls.lua +]] + +local Lv = love + +local FlexLove = require("../FlexLove") +local Gui = FlexLove.Gui +local Color = FlexLove.Color +local enums = FlexLove.enums + +function Lv.load() + Gui.init({ + baseScale = { width = 1920, height = 1080 } + }) + + -- Title + Gui.new({ + x = "2vw", + y = "2vh", + width = "96vw", + height = "6vh", + text = "FlexLove Example 11: Input Controls", + textSize = "4vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Note: Input controls may require additional setup in FlexLove + Gui.new({ + x = "2vw", + y = "10vh", + width = "96vw", + height = "4vh", + text = "Note: This example demonstrates basic input handling patterns", + textSize = "2vh", + textColor = Color.new(0.8, 0.8, 0.8, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Interactive buttons + local counter = 0 + local counterDisplay = Gui.new({ + x = "35vw", + y = "20vh", + width = "30vw", + height = "10vh", + backgroundColor = Color.new(0.2, 0.2, 0.3, 1), + text = "Counter: 0", + textSize = "4vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + }) + + -- Increment button + Gui.new({ + x = "20vw", + y = "35vh", + width = "20vw", + height = "8vh", + backgroundColor = Color.new(0.3, 0.6, 0.3, 1), + text = "Increment (+)", + textSize = "2.5vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 8, + callback = function() + counter = counter + 1 + counterDisplay.text = "Counter: " .. counter + end + }) + + -- Decrement button + Gui.new({ + x = "60vw", + y = "35vh", + width = "20vw", + height = "8vh", + backgroundColor = Color.new(0.6, 0.3, 0.3, 1), + text = "Decrement (-)", + textSize = "2.5vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 8, + callback = function() + counter = counter - 1 + counterDisplay.text = "Counter: " .. counter + end + }) + + -- Reset button + Gui.new({ + x = "40vw", + y = "46vh", + width = "20vw", + height = "8vh", + backgroundColor = Color.new(0.4, 0.4, 0.6, 1), + text = "Reset", + textSize = "2.5vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 8, + callback = function() + counter = 0 + counterDisplay.text = "Counter: " .. counter + end + }) + + -- Keyboard input info + Gui.new({ + x = "2vw", + y = "60vh", + width = "96vw", + height = "30vh", + backgroundColor = Color.new(0.15, 0.15, 0.2, 1), + text = "Keyboard Input:\n\nFlexLove supports keyboard input through Element properties:\n\n" .. + "- Use 'inputType' property for text input fields\n" .. + "- Handle onTextInput, onTextChange, onEnter callbacks\n" .. + "- Manage focus with onFocus and onBlur events\n\n" .. + "See FlexLove documentation for full input API", + textSize = "2vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(0.4, 0.4, 0.5, 1), + }) +end + +function Lv.update(dt) + Gui.update(dt) +end + +function Lv.draw() + Lv.graphics.clear(0.05, 0.05, 0.08, 1) + Gui.draw() +end + +function Lv.resize(w, h) + Gui.resize(w, h) +end diff --git a/examples/12_z_index_layering.lua b/examples/12_z_index_layering.lua new file mode 100644 index 0000000..3c110c1 --- /dev/null +++ b/examples/12_z_index_layering.lua @@ -0,0 +1,204 @@ +--[[ + FlexLove Example 12: Z-Index Layering + + This example demonstrates z-index for element layering: + - Different z-index values + - Overlapping elements + - Layer ordering + + Run with: love /path/to/libs/examples/12_z_index_layering.lua +]] + +local Lv = love + +local FlexLove = require("../FlexLove") +local Gui = FlexLove.Gui +local Color = FlexLove.Color +local enums = FlexLove.enums + +function Lv.load() + Gui.init({ + baseScale = { width = 1920, height = 1080 } + }) + + -- Title + Gui.new({ + x = "2vw", + y = "2vh", + width = "96vw", + height = "6vh", + text = "FlexLove Example 12: Z-Index Layering", + textSize = "4vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Description + Gui.new({ + x = "2vw", + y = "10vh", + width = "96vw", + height = "3vh", + text = "Elements with higher z-index values appear on top", + textSize = "2vh", + textColor = Color.new(0.8, 0.8, 0.8, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Section 1: Overlapping boxes with different z-index + Gui.new({ + x = "2vw", + y = "15vh", + width = "46vw", + height = "3vh", + text = "Overlapping Elements", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + -- Box 1 (z-index: 1) + Gui.new({ + x = "5vw", + y = "20vh", + width = "20vw", + height = "20vh", + z = 1, + backgroundColor = Color.new(0.8, 0.3, 0.3, 1), + text = "Z-Index: 1", + textSize = "2.5vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + }) + + -- Box 2 (z-index: 2) - overlaps Box 1 + Gui.new({ + x = "12vw", + y = "25vh", + width = "20vw", + height = "20vh", + z = 2, + backgroundColor = Color.new(0.3, 0.8, 0.3, 1), + text = "Z-Index: 2", + textSize = "2.5vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + }) + + -- Box 3 (z-index: 3) - overlaps Box 1 and 2 + Gui.new({ + x = "19vw", + y = "30vh", + width = "20vw", + height = "20vh", + z = 3, + backgroundColor = Color.new(0.3, 0.3, 0.8, 1), + text = "Z-Index: 3", + textSize = "2.5vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + }) + + -- Section 2: Cards with different layers + Gui.new({ + x = "50vw", + y = "15vh", + width = "48vw", + height = "3vh", + text = "Layered Cards", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + -- Create a stack of cards + for i = 1, 5 do + Gui.new({ + x = (52 + i * 2) .. "vw", + y = (18 + i * 3) .. "vh", + width = "22vw", + height = "15vh", + z = i, + backgroundColor = Color.new(0.3 + i * 0.1, 0.4, 0.7 - i * 0.1, 1), + text = "Card " .. i .. "\nZ-Index: " .. i, + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 8, + border = { top = true, right = true, bottom = true, left = true }, + borderColor = Color.new(1, 1, 1, 0.3), + }) + end + + -- Section 3: Interactive z-index demo + Gui.new({ + x = "2vw", + y = "53vh", + width = "96vw", + height = "3vh", + text = "Click to Bring to Front", + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + + local maxZ = 10 + + -- Create interactive boxes + for i = 1, 4 do + local box = Gui.new({ + x = (5 + (i - 1) * 22) .. "vw", + y = "58vh", + width = "20vw", + height = "20vh", + z = i, + backgroundColor = Color.new( + 0.4 + i * 0.1, + 0.5 + math.sin(i) * 0.2, + 0.7 - i * 0.1, + 1 + ), + text = "Box " .. i .. "\nClick me!", + textSize = "2.2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 10, + }) + + -- Add click handler to bring to front + box.callback = function(element) + maxZ = maxZ + 1 + element.z = maxZ + element.text = "Box " .. i .. "\nZ: " .. element.z + end + end + + -- Info text + Gui.new({ + x = "2vw", + y = "82vh", + width = "96vw", + height = "14vh", + backgroundColor = Color.new(0.15, 0.15, 0.2, 1), + text = "Z-Index determines the stacking order of elements.\n" .. + "Higher values appear on top of lower values.\n" .. + "Click the boxes above to bring them to the front!", + textSize = "2vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 8, + }) +end + +function Lv.update(dt) + Gui.update(dt) +end + +function Lv.draw() + Lv.graphics.clear(0.05, 0.05, 0.08, 1) + Gui.draw() +end + +function Lv.resize(w, h) + Gui.resize(w, h) +end diff --git a/examples/13_comprehensive_demo.lua b/examples/13_comprehensive_demo.lua new file mode 100644 index 0000000..c679716 --- /dev/null +++ b/examples/13_comprehensive_demo.lua @@ -0,0 +1,274 @@ +--[[ + FlexLove Example 13: Comprehensive Demo + + This example combines multiple FlexLove features into a polished demo: + - Flex and grid layouts + - Themed components + - Animations + - Event handling + - Responsive design + + Run with: love /path/to/libs/examples/13_comprehensive_demo.lua +]] + +local Lv = love + +local FlexLove = require("../FlexLove") +local Gui = FlexLove.Gui +local Color = FlexLove.Color +local Animation = FlexLove.Animation +local enums = FlexLove.enums + +function Lv.load() + Gui.init({ + baseScale = { width = 1920, height = 1080 }, + theme = "space" + }) + + -- Header + local header = Gui.new({ + x = 0, + y = 0, + width = "100vw", + height = "12vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.HORIZONTAL, + justifyContent = enums.JustifyContent.SPACE_BETWEEN, + alignItems = enums.AlignItems.CENTER, + backgroundColor = Color.new(0.1, 0.1, 0.15, 1), + padding = { top = 10, right = 20, bottom = 10, left = 20 }, + border = { bottom = true }, + borderColor = Color.new(0.3, 0.3, 0.4, 1), + }) + + -- Logo/Title + Gui.new({ + parent = header, + width = "auto", + height = "auto", + text = "FlexLove Demo", + textSize = "4vh", + textColor = Color.new(0.8, 0.9, 1, 1), + }) + + -- Header buttons + local headerButtons = Gui.new({ + parent = header, + width = "auto", + height = "auto", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.HORIZONTAL, + gap = 10, + }) + + local buttonNames = { "Home", "Features", "About" } + for _, name in ipairs(buttonNames) do + Gui.new({ + parent = headerButtons, + width = "8vw", + height = "6vh", + backgroundColor = Color.new(0.3, 0.4, 0.6, 1), + text = name, + textSize = "2vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + cornerRadius = 5, + themeComponent = "button", + }) + end + + -- Main content area + local mainContent = Gui.new({ + x = 0, + y = "12vh", + width = "100vw", + height = "88vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.HORIZONTAL, + backgroundColor = Color.new(0.05, 0.05, 0.08, 1), + }) + + -- Sidebar + local sidebar = Gui.new({ + parent = mainContent, + width = "20vw", + height = "88vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.VERTICAL, + backgroundColor = Color.new(0.08, 0.08, 0.12, 1), + padding = { top = 20, right = 10, bottom = 20, left = 10 }, + gap = 10, + border = { right = true }, + borderColor = Color.new(0.2, 0.2, 0.3, 1), + }) + + -- Sidebar menu items + local menuItems = { + { icon = "◆", label = "Dashboard" }, + { icon = "◇", label = "Analytics" }, + { icon = "○", label = "Settings" }, + { icon = "□", label = "Profile" }, + { icon = "△", label = "Help" }, + } + + for _, item in ipairs(menuItems) do + local menuButton = Gui.new({ + parent = sidebar, + width = "auto", + height = "7vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.HORIZONTAL, + alignItems = enums.AlignItems.CENTER, + backgroundColor = Color.new(0.15, 0.15, 0.2, 1), + padding = { top = 10, right = 15, bottom = 10, left = 15 }, + cornerRadius = 5, + }) + + Gui.new({ + parent = menuButton, + width = "auto", + height = "auto", + text = item.icon .. " " .. item.label, + textSize = "2vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + }) + end + + -- Content panel + local contentPanel = Gui.new({ + parent = mainContent, + width = "80vw", + height = "88vh", + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.VERTICAL, + padding = { top = 20, right = 20, bottom = 20, left = 20 }, + gap = 15, + }) + + -- Welcome section + Gui.new({ + parent = contentPanel, + width = "auto", + height = "auto", + text = "Welcome to FlexLove!", + textSize = "3.5vh", + textColor = Color.new(1, 1, 1, 1), + }) + + -- Stats grid + local statsGrid = Gui.new({ + parent = contentPanel, + width = "auto", + height = "20vh", + positioning = enums.Positioning.GRID, + gridRows = 1, + gridColumns = 4, + columnGap = 15, + }) + + local stats = { + { label = "Projects", value = "24", color = Color.new(0.3, 0.6, 0.8, 1) }, + { label = "Users", value = "1.2K", color = Color.new(0.6, 0.3, 0.8, 1) }, + { label = "Revenue", value = "$45K", color = Color.new(0.8, 0.6, 0.3, 1) }, + { label = "Growth", value = "+12%", color = Color.new(0.3, 0.8, 0.6, 1) }, + } + + for _, stat in ipairs(stats) do + local statCard = Gui.new({ + parent = statsGrid, + positioning = enums.Positioning.FLEX, + flexDirection = enums.FlexDirection.VERTICAL, + justifyContent = enums.JustifyContent.CENTER, + alignItems = enums.AlignItems.CENTER, + backgroundColor = stat.color, + cornerRadius = 8, + padding = { top = 15, right = 15, bottom = 15, left = 15 }, + }) + + Gui.new({ + parent = statCard, + width = "auto", + height = "auto", + text = stat.value, + textSize = "4vh", + textColor = Color.new(1, 1, 1, 1), + textAlign = enums.TextAlign.CENTER, + }) + + Gui.new({ + parent = statCard, + width = "auto", + height = "auto", + text = stat.label, + textSize = "1.8vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + textAlign = enums.TextAlign.CENTER, + }) + end + + -- Feature cards + local cardsContainer = Gui.new({ + parent = contentPanel, + width = "auto", + height = "auto", + positioning = enums.Positioning.GRID, + gridRows = 2, + gridColumns = 3, + rowGap = 15, + columnGap = 15, + }) + + local features = { + "Flexbox Layout", + "Grid System", + "Theming", + "Animations", + "Events", + "Responsive" + } + + for i, feature in ipairs(features) do + local card = Gui.new({ + parent = cardsContainer, + positioning = enums.Positioning.FLEX, + justifyContent = enums.JustifyContent.CENTER, + alignItems = enums.AlignItems.CENTER, + backgroundColor = Color.new(0.15, 0.15, 0.2, 1), + cornerRadius = 10, + padding = { top = 20, right = 20, bottom = 20, left = 20 }, + }) + + Gui.new({ + parent = card, + width = "auto", + height = "auto", + text = feature, + textSize = "2.5vh", + textColor = Color.new(0.9, 0.9, 0.9, 1), + textAlign = enums.TextAlign.CENTER, + }) + + -- Add hover animation + card.callback = function(element) + local anim = Animation.new({ + duration = 0.2, + start = { opacity = 1 }, + final = { opacity = 0.8 }, + }) + anim:apply(element) + end + end +end + +function Lv.update(dt) + Gui.update(dt) +end + +function Lv.draw() + Lv.graphics.clear(0.05, 0.05, 0.08, 1) + Gui.draw() +end + +function Lv.resize(w, h) + Gui.resize(w, h) +end diff --git a/testing/__tests__/23_blur_effects_tests.lua b/testing/__tests__/23_blur_effects_tests.lua index 53b4f8e..d42cf93 100644 --- a/testing/__tests__/23_blur_effects_tests.lua +++ b/testing/__tests__/23_blur_effects_tests.lua @@ -105,7 +105,7 @@ function TestBlurEffects:test_blur_instance_quality_change() }) local instance1 = element:getBlurInstance() - + -- Change quality element.contentBlur.quality = 8 local instance2 = element:getBlurInstance() @@ -204,7 +204,7 @@ function TestBlurEffects:test_draw_accepts_backdrop_canvas() -- Create a mock canvas (will fail in test environment, but that's ok) -- element:draw(nil) end) - + -- Test passes if we get here without syntax errors lu.assertTrue(true, "Draw method should accept backdrop canvas parameter") end @@ -221,7 +221,7 @@ function TestBlurEffects:test_quality_affects_taps() height = 200, contentBlur = { intensity = 50, quality = 1 }, }) - + local element2 = FlexLove.Element.new({ width = 200, height = 200, @@ -235,4 +235,4 @@ function TestBlurEffects:test_quality_affects_taps() lu.assertTrue(instance2.taps > instance1.taps, "Higher quality should result in more blur taps") end -return TestBlurEffects +lu.LuaUnit.run() diff --git a/testing/__tests__/24_keyboard_input_tests.lua b/testing/__tests__/24_keyboard_input_tests.lua new file mode 100644 index 0000000..f072886 --- /dev/null +++ b/testing/__tests__/24_keyboard_input_tests.lua @@ -0,0 +1,506 @@ +local lu = require("testing.luaunit") +local FlexLove = require("FlexLove") +local Gui = FlexLove.Gui +local Element = FlexLove.Element + +TestKeyboardInput = {} + +function TestKeyboardInput:setUp() + Gui.init({ baseScale = { width = 1920, height = 1080 } }) +end + +function TestKeyboardInput:tearDown() + Gui.destroy() +end + +-- ==================== +-- Focus Management Tests +-- ==================== + +function TestKeyboardInput:testFocusEditable() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + lu.assertFalse(input:isFocused()) + + input:focus() + + lu.assertTrue(input:isFocused()) + lu.assertEquals(Gui._focusedElement, input) +end + +function TestKeyboardInput:testBlurEditable() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + input:focus() + lu.assertTrue(input:isFocused()) + + input:blur() + + lu.assertFalse(input:isFocused()) + lu.assertNil(Gui._focusedElement) +end + +function TestKeyboardInput:testFocusSwitching() + local input1 = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Input 1", + }) + + local input2 = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Input 2", + }) + + input1:focus() + lu.assertTrue(input1:isFocused()) + lu.assertFalse(input2:isFocused()) + + input2:focus() + lu.assertFalse(input1:isFocused()) + lu.assertTrue(input2:isFocused()) + lu.assertEquals(Gui._focusedElement, input2) +end + +function TestKeyboardInput:testSelectOnFocus() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello World", + selectOnFocus = true, + }) + + lu.assertFalse(input:hasSelection()) + + input:focus() + + lu.assertTrue(input:hasSelection()) + local startPos, endPos = input:getSelection() + lu.assertEquals(startPos, 0) + lu.assertEquals(endPos, 11) -- Length of "Hello World" +end + +-- ==================== +-- Text Input Tests +-- ==================== + +function TestKeyboardInput:testTextInput() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "", + }) + + input:focus() + input:textinput("H") + input:textinput("i") + + lu.assertEquals(input:getText(), "Hi") + lu.assertEquals(input._cursorPosition, 2) +end + +function TestKeyboardInput:testTextInputAtPosition() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + input:focus() + input:setCursorPosition(2) -- After "He" + input:textinput("X") + + lu.assertEquals(input:getText(), "HeXllo") + lu.assertEquals(input._cursorPosition, 3) +end + +function TestKeyboardInput:testTextInputWithSelection() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello World", + }) + + input:focus() + input:setSelection(0, 5) -- Select "Hello" + input:textinput("Hi") + + lu.assertEquals(input:getText(), "Hi World") + lu.assertEquals(input._cursorPosition, 2) + lu.assertFalse(input:hasSelection()) +end + +function TestKeyboardInput:testMaxLengthConstraint() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "", + maxLength = 5, + }) + + input:focus() + input:textinput("Hello") + lu.assertEquals(input:getText(), "Hello") + + input:textinput("X") -- Should not be added + lu.assertEquals(input:getText(), "Hello") +end + +-- ==================== +-- Backspace/Delete Tests +-- ==================== + +function TestKeyboardInput:testBackspace() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + input:focus() + input:setCursorPosition(5) -- At end + input:keypressed("backspace", "backspace", false) + + lu.assertEquals(input:getText(), "Hell") + lu.assertEquals(input._cursorPosition, 4) +end + +function TestKeyboardInput:testBackspaceAtStart() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + input:focus() + input:setCursorPosition(0) -- At start + input:keypressed("backspace", "backspace", false) + + lu.assertEquals(input:getText(), "Hello") -- No change + lu.assertEquals(input._cursorPosition, 0) +end + +function TestKeyboardInput:testDelete() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + input:focus() + input:setCursorPosition(0) -- At start + input:keypressed("delete", "delete", false) + + lu.assertEquals(input:getText(), "ello") + lu.assertEquals(input._cursorPosition, 0) +end + +function TestKeyboardInput:testDeleteAtEnd() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + input:focus() + input:setCursorPosition(5) -- At end + input:keypressed("delete", "delete", false) + + lu.assertEquals(input:getText(), "Hello") -- No change + lu.assertEquals(input._cursorPosition, 5) +end + +function TestKeyboardInput:testBackspaceWithSelection() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello World", + }) + + input:focus() + input:setSelection(0, 5) -- Select "Hello" + input:keypressed("backspace", "backspace", false) + + lu.assertEquals(input:getText(), " World") + lu.assertEquals(input._cursorPosition, 0) + lu.assertFalse(input:hasSelection()) +end + +-- ==================== +-- Cursor Movement Tests +-- ==================== + +function TestKeyboardInput:testArrowLeft() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + input:focus() + input:setCursorPosition(5) + input:keypressed("left", "left", false) + + lu.assertEquals(input._cursorPosition, 4) +end + +function TestKeyboardInput:testArrowRight() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + input:focus() + input:setCursorPosition(0) + input:keypressed("right", "right", false) + + lu.assertEquals(input._cursorPosition, 1) +end + +function TestKeyboardInput:testHomeKey() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + input:focus() + input:setCursorPosition(5) + input:keypressed("home", "home", false) + + lu.assertEquals(input._cursorPosition, 0) +end + +function TestKeyboardInput:testEndKey() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + input:focus() + input:setCursorPosition(0) + input:keypressed("end", "end", false) + + lu.assertEquals(input._cursorPosition, 5) +end + +function TestKeyboardInput:testEscapeClearsSelection() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + input:focus() + input:selectAll() + lu.assertTrue(input:hasSelection()) + + input:keypressed("escape", "escape", false) + + lu.assertFalse(input:hasSelection()) + lu.assertTrue(input:isFocused()) -- Still focused +end + +function TestKeyboardInput:testEscapeBlurs() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + input:focus() + lu.assertTrue(input:isFocused()) + + input:keypressed("escape", "escape", false) + + lu.assertFalse(input:isFocused()) +end + +-- ==================== +-- Callback Tests +-- ==================== + +function TestKeyboardInput:testOnTextChangeCallback() + local changeCount = 0 + local oldTextValue = nil + local newTextValue = nil + + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + onTextChange = function(element, newText, oldText) + changeCount = changeCount + 1 + newTextValue = newText + oldTextValue = oldText + end, + }) + + input:focus() + input:textinput("X") + + lu.assertEquals(changeCount, 1) + lu.assertEquals(oldTextValue, "Hello") + lu.assertEquals(newTextValue, "HelloX") +end + +function TestKeyboardInput:testOnTextInputCallback() + local inputCount = 0 + local lastChar = nil + + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "", + onTextInput = function(element, text) + inputCount = inputCount + 1 + lastChar = text + end, + }) + + input:focus() + input:textinput("A") + input:textinput("B") + + lu.assertEquals(inputCount, 2) + lu.assertEquals(lastChar, "B") +end + +function TestKeyboardInput:testOnEnterCallback() + local enterCalled = false + + local input = Element.new({ + width = 200, + height = 40, + editable = true, + multiline = false, + text = "Hello", + onEnter = function(element) + enterCalled = true + end, + }) + + input:focus() + input:keypressed("return", "return", false) + + lu.assertTrue(enterCalled) +end + +function TestKeyboardInput:testOnFocusCallback() + local focusCalled = false + + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + onFocus = function(element) + focusCalled = true + end, + }) + + input:focus() + + lu.assertTrue(focusCalled) +end + +function TestKeyboardInput:testOnBlurCallback() + local blurCalled = false + + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + onBlur = function(element) + blurCalled = true + end, + }) + + input:focus() + input:blur() + + lu.assertTrue(blurCalled) +end + +-- ==================== +-- GUI-level Input Forwarding Tests +-- ==================== + +function TestKeyboardInput:testGuiTextinputForwarding() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "", + }) + + input:focus() + Gui.textinput("A") + + lu.assertEquals(input:getText(), "A") +end + +function TestKeyboardInput:testGuiKeypressedForwarding() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + input:focus() + input:setCursorPosition(5) + Gui.keypressed("backspace", "backspace", false) + + lu.assertEquals(input:getText(), "Hell") +end + +function TestKeyboardInput:testGuiInputWithoutFocus() + local input = Element.new({ + width = 200, + height = 40, + editable = true, + text = "Hello", + }) + + -- No focus + Gui.textinput("X") + + lu.assertEquals(input:getText(), "Hello") -- No change +end + +lu.LuaUnit.run() diff --git a/testing/runAll.lua b/testing/runAll.lua index 2276d05..e9e9d82 100644 --- a/testing/runAll.lua +++ b/testing/runAll.lua @@ -27,6 +27,7 @@ local testFiles = { "testing/__tests__/21_image_scaler_nearest_tests.lua", "testing/__tests__/22_image_scaler_bilinear_tests.lua", "testing/__tests__/23_blur_effects_tests.lua", + "testing/__tests__/24_keyboard_input_tests.lua", } local success = true