Another Text Input field

Hi. I’ve noticed that questions about how to add Text Input fields are posted quite frequently in the forums, but I have never found a complete solution that allows to place the cursor at an arbitrary position to insert text or if it does then it lacks backspace and return support, etc. I started coding one to cater for my specific needs, but I realised it’s more complex than what I thought when started. Anyway, I have a solution that solves my problem but I think it is a quite large piece of code to do what can be done in HTML with just 1 line. I appreciate suggestions to make this code shorter or, ideally, use a 1-liner solution for text input.

Demo here: http://www.youtube.com/watch?v=oD747zdo4vQ

The Main class shows how to use it, and the actual Input text field code is shown after this

-- Main class

function setup()
    local x = 30
    local w = 300
    local h = 25
    
    local obj = InputTarget() -- Sample handler
    
    input1 = Input(x, HEIGHT-100, w, h, "CourierNewPSMT", "ABCDEFGHIJKLMNOPQRSTUVWXYZ") 
    input2 = Input(x, HEIGHT-160, w, h, "AppleColorEmoji", "A string exceeding the visible size of the field")
    input3 = Input(x, HEIGHT-220, w, h, "GillSans-Light", "global handler", globalHandler)
    input4 = Input(x, HEIGHT-280, w, h, "HelveticaNeue", "instance handler 1", obj.hand1, obj)
    input5 = Input(x, HEIGHT-340, w, h, "Verdana", "instance handler 2", obj.hand2, obj)
end

function draw()
    background(228, 228, 224, 255)
    strokeWidth(5)
    Input._draw()
end

-- Example of a global handler function

function globalHandler(txt)
    print("Global -> "..txt)
end

-- Example of an input handler class

InputTarget = class()

function InputTarget:hand1(txt)
    print("obj:hand1 -> "..txt)
end

function InputTarget:hand2(txt)
    print("obj:hand2 -> "..txt)
end

This is the actual Text Input code


--[[--
GLOBAL FUNCTIONS 
These global functions can be declared out of this file if required
--]]--

    
function keyboard(key)
    Input._put(key)
end

function touched(touch)
    Input._touched(touch)
end

--[[ INPUT CLASS ]]--

Input = class()

Input.instances = {}

local padding = 10

--[[ Class functions ]]--

function Input._draw()
    for _,input in ipairs(Input.instances) do
        input:draw()
    end
end
    
function Input._touched(touch)
    for _,input in ipairs(Input.instances) do
        input:touched(touch)
    end
end

function Input._put(key)
    for _,input in ipairs(Input.instances) do
        if input.focus then
            input:put(key)
        end
    end
end

--[[ Instance functions ]]--

function Input:init(x, y, w, h, fontName, txt, onEnter, target)
    table.insert(Input.instances, self)
    self.fontSize = h
    self.fontName = fontName
    font(self.fontName)
    fontSize(self.fontSize)
    self.x = x
    self.y = y
    self.w = w
    self.h = h
    if txt then
        self.txt = txt
    else
        self.txt = ""
    end
    self.pos = string.len(self.txt)
    self.winPos = 0

    self.cursor = Cursor(self.h+padding/2)
    
    self:blur()
    if onEnter == nil then
        self.onEnter = function() print(self.txt) end
    else
        self.onEnter = onEnter
    end
    self.target = target
end

function Input:put(key)
    if key == BACKSPACE then
        self:backspace()
    elseif key == RETURN then
        self:enter()
    else
        self:insert(key)        
    end
end

function Input:backspace()
    if self.pos > 0 then
        local head,tail = self:splitAndDrop(1)
        self.txt = head..tail
        self.pos = self.pos - 1
        if self.pos == self.winPos and self.winPos > 0 then
            self.winPos = self.winPos - 1
        end
    end
end

function Input:insert(key)
    local head,tail = self:splitAndDrop(0)
    self.txt = head..key..tail
    self.pos = self.pos + 1
    while self.pos > self:winSize() do
        self.winPos = self.winPos + 1
    end
end

function Input:enter()
    self:blur()
    if self.target then
        self.onEnter(self.target, self.txt)
    else
        self.onEnter(self.txt)
    end
end

function Input:splitAndDrop(dropCount)
    local head = string.sub(self.txt, 0, self.pos - dropCount)
    local tail = string.sub(self.txt, self.pos + 1)
    return head,tail
end

function Input:blur()
    self.focus = false
    hideKeyboard()
end

function Input:visiblePart()
    local winSize = self:winSize()
    return string.sub(self.txt, self.winPos + 1, self.winPos + winSize)
end

function Input:winSize()
    font(self.fontName)
    local winSize = string.len(self.txt)
    local part = string.sub(self.txt, self.winPos, self.winPos + winSize)
    local w,_ = textSize(part)
    while w > self.w do
        winSize = winSize - 1
        part = string.sub(self.txt, self.winPos, self.winPos + winSize)
        w,_ = textSize(part)
    end
    return winSize
end

function Input:draw()
    pushMatrix()
        translate(self.x, self.y)
        pushStyle()
            self:showBox()
            self:showText()
            self:posCursor()
            if self.focus then
                self:showBorder()
                self.cursor:draw()
            end
        popStyle()
    popMatrix()
end

function Input:showBox()
    fill(255, 255, 255, 255)
    stroke(255, 255, 255, 255)
    strokeWidth(padding)
    rect(-padding, -padding, self.w+2*padding, self.h+2*padding)
end

function Input:showBorder()
    noFill()
    strokeWidth(2)
    stroke(127, 127, 127, 255)
    rect(-padding, -padding, self.w+2*padding, self.h+2*padding)
end


function Input:showText()
    font(self.fontName)
    textMode(CORNER)
    fill(0, 0, 0, 255)
    text(self:visiblePart(), 0, 0)
end

function Input:touched(touch)
    if self:contains(touch) then
        self.focus = true
        if not isKeyboardShowing() then
            showKeyboard()
        end
        if touch.state ~= ENDED then
            self:moveCursor(touch.x)
        end
    else
        self.focus = false
    end
end

function Input:contains(touch)
    return self.x <= touch.x 
        and touch.x <= (self.x + self.w)
        and self.y <= touch.y 
        and touch.y <= (self.y + self.h)
end

function Input:moveCursor(x)
    pushStyle()
        font(self.fontName)
        local min = math.huge
        local part = self:visiblePart()
        for i = 0,string.len(part) do
            local sub = string.sub(part, 0, i)
            local w,_ = textSize(sub)
            local dist = math.abs(x - (self.x + w))
            if dist < min then
                min = dist
                self.pos = self.winPos + i
            end
        end
    popStyle()
    local rightEnd = self.winPos + self:winSize()
    if self.pos == self.winPos and self.winPos > 0 then
        self.winPos = self.winPos - 1
    elseif self.pos == rightEnd and rightEnd < string.len(self.txt) then
        self.winPos = self.winPos + 1
    end
    self:posCursor()
end

function Input:posCursor()
    pushStyle()
        font(self.fontName)
        local part = self:visiblePart()
        local relPos = self.pos - self.winPos
        local sub = string.sub(part, 0, relPos)
        self.cursor.x,_ = textSize(sub)
    popStyle()
end

Cursor = class()

function Cursor:init(h)
    self.h = h
    self.x = 0
    self.show = true
    tween.delay(0.4, Cursor._blink, self)
end

function Cursor:draw()
    if self.show then
        pushStyle()
            strokeWidth(3)
            stroke(0, 0, 0, 255)
            line(self.x, 0, self.x, self.h)
        popStyle()
    end
end

function Cursor._blink(cursor)
    cursor.show = not cursor.show
    tween.delay(0.4, Cursor._blink, cursor)
end

@lightDye this looks great!
Would you mind removing the fancy coloring from your code post? I cant copy it from ipad1 with this option. Just use the stadard 3 ~ syntax. Thanks in advance.

-- Main class

function setup()
    local x = 30
    local w = 300
    local h = 25

    local obj = InputTarget() -- Sample handler

    input1 = Input(x, HEIGHT-100, w, h, "CourierNewPSMT", "ABCDEFGHIJKLMNOPQRSTUVWXYZ") 
    input2 = Input(x, HEIGHT-160, w, h, "AppleColorEmoji", "A string exceeding the visible size of the field")
    input3 = Input(x, HEIGHT-220, w, h, "GillSans-Light", "global handler", globalHandler)
    input4 = Input(x, HEIGHT-280, w, h, "HelveticaNeue", "instance handler 1", obj.hand1, obj)
    input5 = Input(x, HEIGHT-340, w, h, "Verdana", "instance handler 2", obj.hand2, obj)
end

function draw()
    background(228, 228, 224, 255)
    strokeWidth(5)
    Input._draw()
end

-- Example of a global handler function

function globalHandler(txt)
    print("Global -> "..txt)
end

-- Example of an input handler class

InputTarget = class()

function InputTarget:hand1(txt)
    print("obj:hand1 -> "..txt)
end

function InputTarget:hand2(txt)
    print("obj:hand2 -> "..txt)
end


--[[--
GLOBAL FUNCTIONS 
These global functions can be declared out of this file if required
--]]--


function keyboard(key)
    Input._put(key)
end

function touched(touch)
    Input._touched(touch)
end

--[[ INPUT CLASS ]]--

Input = class()

Input.instances = {}

local padding = 10

--[[ Class functions ]]--

function Input._draw()
    for _,input in ipairs(Input.instances) do
        input:draw()
    end
end

function Input._touched(touch)
    for _,input in ipairs(Input.instances) do
        input:touched(touch)
    end
end

function Input._put(key)
    for _,input in ipairs(Input.instances) do
        if input.focus then
            input:put(key)
        end
    end
end

--[[ Instance functions ]]--

function Input:init(x, y, w, h, fontName, txt, onEnter, target)
    table.insert(Input.instances, self)
    self.fontSize = h
    self.fontName = fontName
    font(self.fontName)
    fontSize(self.fontSize)
    self.x = x
    self.y = y
    self.w = w
    self.h = h
    if txt then
        self.txt = txt
    else
        self.txt = ""
    end
    self.pos = string.len(self.txt)
    self.winPos = 0

    self.cursor = Cursor(self.h+padding/2)

    self:blur()
    if onEnter == nil then
        self.onEnter = function() print(self.txt) end
    else
        self.onEnter = onEnter
    end
    self.target = target
end

function Input:put(key)
    if key == BACKSPACE then
        self:backspace()
    elseif key == RETURN then
        self:enter()
    else
        self:insert(key)        
    end
end

function Input:backspace()
    if self.pos > 0 then
        local head,tail = self:splitAndDrop(1)
        self.txt = head..tail
        self.pos = self.pos - 1
        if self.pos == self.winPos and self.winPos > 0 then
            self.winPos = self.winPos - 1
        end
    end
end

function Input:insert(key)
    local head,tail = self:splitAndDrop(0)
    self.txt = head..key..tail
    self.pos = self.pos + 1
    while self.pos > self:winSize() do
        self.winPos = self.winPos + 1
    end
end

function Input:enter()
    self:blur()
    if self.target then
        self.onEnter(self.target, self.txt)
    else
        self.onEnter(self.txt)
    end
end

function Input:splitAndDrop(dropCount)
    local head = string.sub(self.txt, 0, self.pos - dropCount)
    local tail = string.sub(self.txt, self.pos + 1)
    return head,tail
end

function Input:blur()
    self.focus = false
    hideKeyboard()
end

function Input:visiblePart()
    local winSize = self:winSize()
    return string.sub(self.txt, self.winPos + 1, self.winPos + winSize)
end

function Input:winSize()
    font(self.fontName)
    local winSize = string.len(self.txt)
    local part = string.sub(self.txt, self.winPos, self.winPos + winSize)
    local w,_ = textSize(part)
    while w > self.w do
        winSize = winSize - 1
        part = string.sub(self.txt, self.winPos, self.winPos + winSize)
        w,_ = textSize(part)
    end
    return winSize
end

function Input:draw()
    pushMatrix()
        translate(self.x, self.y)
        pushStyle()
            self:showBox()
            self:showText()
            self:posCursor()
            if self.focus then
                self:showBorder()
                self.cursor:draw()
            end
        popStyle()
    popMatrix()
end

function Input:showBox()
    fill(255, 255, 255, 255)
    stroke(255, 255, 255, 255)
    strokeWidth(padding)
    rect(-padding, -padding, self.w+2*padding, self.h+2*padding)
end

function Input:showBorder()
    noFill()
    strokeWidth(2)
    stroke(127, 127, 127, 255)
    rect(-padding, -padding, self.w+2*padding, self.h+2*padding)
end


function Input:showText()
    font(self.fontName)
    textMode(CORNER)
    fill(0, 0, 0, 255)
    text(self:visiblePart(), 0, 0)
end

function Input:touched(touch)
    if self:contains(touch) then
        self.focus = true
        if not isKeyboardShowing() then
            showKeyboard()
        end
        if touch.state ~= ENDED then
            self:moveCursor(touch.x)
        end
    else
        self.focus = false
    end
end

function Input:contains(touch)
    return self.x <= touch.x 
        and touch.x <= (self.x + self.w)
        and self.y <= touch.y 
        and touch.y <= (self.y + self.h)
end

function Input:moveCursor(x)
    pushStyle()
        font(self.fontName)
        local min = math.huge
        local part = self:visiblePart()
        for i = 0,string.len(part) do
            local sub = string.sub(part, 0, i)
            local w,_ = textSize(sub)
            local dist = math.abs(x - (self.x + w))
            if dist < min then
                min = dist
                self.pos = self.winPos + i
            end
        end
    popStyle()
    local rightEnd = self.winPos + self:winSize()
    if self.pos == self.winPos and self.winPos > 0 then
        self.winPos = self.winPos - 1
    elseif self.pos == rightEnd and rightEnd < string.len(self.txt) then
        self.winPos = self.winPos + 1
    end
    self:posCursor()
end

function Input:posCursor()
    pushStyle()
        font(self.fontName)
        local part = self:visiblePart()
        local relPos = self.pos - self.winPos
        local sub = string.sub(part, 0, relPos)
        self.cursor.x,_ = textSize(sub)
    popStyle()
end

Cursor = class()

function Cursor:init(h)
    self.h = h
    self.x = 0
    self.show = true
    tween.delay(0.4, Cursor._blink, self)
end

function Cursor:draw()
    if self.show then
        pushStyle()
            strokeWidth(3)
            stroke(0, 0, 0, 255)
            line(self.x, 0, self.x, self.h)
        popStyle()
    end
end

function Cursor._blink(cursor)
    cursor.show = not cursor.show
    tween.delay(0.4, Cursor._blink, cursor)
end

is multiline also possible? havent tried yet…

@LightDye ok i managed to copy the code anyway.
Feedback: this is excellent work: quick and lean! Thanks for sharing!
Maybe a small improvement would make it perfect: moving the string when bigger than the box is not perfectly easy: the text stops moving sometimes.

Omg i wanted to release something like this as well, but I just don’t have time to work on it right now :o xD

but nice job @LightDye

@se24vad hello!!

Do you want to put the code in CC please . is that right now I’m on the ipad, and I want to see what the code, but I can not copy . Thanks :slight_smile:

@Jmv38 How did you manage to copy the code. On my iPad 1, I can’t seem to grab anything to copy.

no matter, I could copy @se24vad

@dave1707. guess I did not copy it, but touch the screen several times until I could do it, a little strange

@Luismi @Jmv38 I figured out how to copy it. I long press in the text above the code. When I get a little copy area ,I then drag my finger down into the code and move it around until it selects all of the code.

is a little strange no ? @dave1707

oh yea of course :slight_smile:

@Luismi I like code that’s posted with 3 ~'s before and after the code. That’s the easiest to copy.

Thanks for the comments. I wasn’t aware that colouring cause difficulties to copy the code. I just removed it to make it easier for those who haven’t been able to copy it yet. I’ll have a look later how to publish through Codea Comunity, because I haven’t use the service yet.

@se24vad Unfortunately this code is not multiline capable.

@Jmv38 You are right, I’ll see what I can do to prevent the intermitent pauses.

Emoji does not work. 1) It appears blacked out 2) It messes up the touch to move and cursor placement.

@JakAttak Emoji doesn’t work is for this simple reason: for emoji the fill should be set to ‘white’ so that’s rather hard to incoorporate into a black text textbox
And the movement of the emoji is… emoji is multiple chars long, so the string sees this not as an emoji and thus the width of the emoji, but it sees it as the seperate chars.
A solution would be to use tables instead of string, but that still doesn’t solve the blacked out.

But when I get some spare time, I’ll continiue on my textbox which uses tables, multiline, wordwrap

@dave1707 i used the same trick as you to copy the code!

The caret stays flashing if you press the hide keyboard button.

@SkyTheCoder, if you want that behaviour all you need to do is adding this snippet at the begining of the Input:draw()

    if not isKeyboardShowing() then
        self:blur()
    end

Code is now available in Codea Community.

@LightDye i used your code for my next version of XFC. Thanks!