Keyboard


Textbox = class()

function Textbox:init(x,y,w)
    self.text = ""
    self.x = x
    self.y = y
    self.width = w
    -- in font properties you can set fill,font,fontSize
    self.fontProperties = {font="AmericanTypewriter-Bold",fill=color(255,255,255)} 
    self:setFontSize(30)
    self.borderColor = color(255, 255, 255, 255)

    -- internal state
    self.active = false
    self.cursorPos = 0  -- 0 means before the first letter, 1 after the first, so on
    self.startPos = 1   -- the first char we show on the box (index into self.text)
end

function Textbox:setFontSize(x)
    self.fontProperties.fontSize = x
    -- calculate the height based on font properties
    pushStyle()
    self:applyTextProperties()
    local w,h = textSize("dummy")
    popStyle()
    self.height = h
end

-- call back for when a key is pressed
function Textbox:keyboard(key)
    -- if not active, ignore
    if not self.active then return nil end
    
    if key == BACKSPACE then
        -- if we press backspace. Note if we're already at the start, nothing to do
        if self.cursorPos > 0 then
            local prefix = self.text:sub(1,self.cursorPos-1)
            local posfix = self.text:sub(self.cursorPos+1,self.text:len())
            self.text = prefix..posfix
            self.cursorPos = self.cursorPos - 1
            -- need to improve this behavior for when the cursor is at the
            -- start of what's showing. We should go back as much as we can instead of 1
            if self.cursorPos < self.startPos then
                local width = self:maxTextWidth()
                self.startPos = 1
                self:moveCursor(self.cursorPos)
            end
        end
    else
        local prefix = self.text:sub(1,self.cursorPos)
        local posfix = self.text:sub(self.cursorPos+1,self.text:len())
        self.text = prefix..key..posfix
        self:moveCursor(self.cursorPos + key:len())
    end
end

function Textbox:applyTextProperties()
    textMode(CORNER)
    font(self.fontProperties.font)
    fontSize(self.fontProperties.fontSize)
    fill(self.fontProperties.fill)
end

-- when the text box is active, the keyboard shows up (and coursor and other elements too)
function Textbox:activate()
    self.active = true
    -- move the cursor to the end
    self:moveCursor(self.text:len())
    showKeyboard()
end

function Textbox:inactivate()
    self.active = false
    self.startPos = 1
    hideKeyboard()
end

-- newPos is specified in chars, not pixels
function Textbox:moveCursor(newPos)
    self.cursorPos = newPos
    local width = self:maxTextWidth()
    local cursorText = self.text:sub(self.startPos,self.cursorPos)
    
    pushStyle()
    self:applyTextProperties()
    local cursorLength = textSize(cursorText)
    while cursorLength > width do
        self.startPos = self.startPos + 1
        cursorText = self.text:sub(self.startPos,self.cursorPos)
        cursorLength = textSize(cursorText)
    end
    popStyle()
end

-- how much space the text can occupy. Can be less than width if we're showing the reset button
function Textbox:maxTextWidth()
    local width = self.width
    if self.active then
        local resetDiam,resetX,resetY = self:resetButtonCoords()
        width = resetX - self.x - 5
    end
    return width
end

-- return coords and dimensions for the reset button
function Textbox:resetButtonCoords()
    local resetDiam = self.height-4
    local resetX = self.x+self.width-resetDiam-2
    local resetY = self.y+2
    return resetDiam,resetX,resetY
end

-- the text that we actually display, cropped up if needed
function Textbox:displayText()
    local dispText = self.text:sub(self.startPos)
    
    pushStyle()
    self:applyTextProperties()
    local width = self:maxTextWidth()
    while textSize(dispText) > width do
        dispText = dispText:sub(1,dispText:len()-1)
    end
    popStyle()
    return dispText
end

function Textbox:draw()
    pushStyle()
    noSmooth()
    
    -- draw the box
    stroke(self.borderColor)
    strokeWidth(1)
    noFill()
    rectMode(CORNER)
    rect(self.x,self.y,self.width,self.height)
    
    -- draw the text
    self:applyTextProperties()
    local dispText = self:displayText()
    text(dispText,self.x + 5,self.y)
    
    if not self.active then
        popStyle()
        return nil
    end
    
    -- draw the cursor
    if math.floor(ElapsedTime*3)%2 == 0 then
        stroke(151, 167, 165, 255)
        strokeWidth(1)
        local prefix = dispText:sub(1,self.cursorPos - self.startPos + 1)
        local len = textSize(prefix) + 5
        line(self.x+len,self.y+4,self.x+len,self.y+self.height-4)
    end
    
    -- draw the reset button
    stroke(self.borderColor)
    strokeWidth(1)
    noFill()
    rectMode(CORNER)
    ellipseMode(CORNER)
    local resetDiam,resetX,resetY = self:resetButtonCoords()
    local sq2 = (math.sqrt(2)-1)/2*1.1
    ellipse(resetX,resetY,resetDiam)
    line(resetX+resetDiam*sq2,resetY+resetDiam*sq2,
        resetX+resetDiam*(1-sq2),resetY+resetDiam*(1-sq2))
    line(resetX+resetDiam*sq2,resetY+resetDiam*(1-sq2),
        resetX+resetDiam*(1-sq2),resetY+resetDiam*sq2)
     popStyle()
end

function Textbox:touched(touch)
    -- check if it was the reset button that was pressed
    local resetDiam,resetX,resetY = self:resetButtonCoords()
    if touch.x>=resetX and touch.x<=self.x+self.width and
        touch.y>=self.y and touch.y<=self.y+self.height then
        if self.active and touch.state==ENDED then
            -- the user touched the reset button
            self.text = ""
            self.cursorPos = 0
            self.startPos = 1
        end
    end
    
    if touch.x>=self.x and touch.x<=self.x+self.width and
        touch.y>=self.y and touch.y<=self.y+self.height then
        if not self.active and touch.state == ENDED then
            self:activate()
        elseif self.active then
            -- place the cursor at the touch x coord
            local dispText = self:displayText()
            self.cursorPos = self.startPos-1
            local touchX = touch.x - self.x
            pushStyle()
            self:applyTextProperties()
            for idx = 1,dispText:len() do
                local len = textSize(dispText:sub(1,idx))
                if len > touchX then break end
                self.cursorPos = self.cursorPos + 1
            end
            popStyle()
        end
    elseif touch.state == BEGAN and self.active then
        -- BEGAN above is important as it makes sure a box is inactivated, and hides
        -- the keyboard, before another box is activated at ENDED, which will show
        -- the keyboard again. If the other was inverted, then you'd showkeyboard only
        -- to hide it right after
        self:inactivate()
    end
end

Nice update!

Very Sweet, @ruilov. I did something similar when it came to my Window Manager. Keep up the awesome work!