Suuuuuuuuuuuuuper Simple Buttons

Until we get Jvm’s ultra-configurable button class, I thought I’d share some primitive code that will put buttons on screen for you ASAP.

Making a button just takes one line of code in the draw() function.

Type button("label text") and you will get a button with that label drawn on screen.

You can make as many buttons as you want this way.

You don’t have to hand-code any button location because you can drag the buttons to where you want them while the program runs. The buttons will remember where they were placed and preserve their location even after you close the project.


--# Main
-- SimpleButton

displayMode(OVERLAY)

-- Use this function to perform your initial setup
function setup()
    --[[ 
    commented-out testing code:
    
    testButtonName = "hello world"
    button(testButtonName)
    local touch = {x=10,y=19}      
    buttonHandler.savePositions(testButtonName, touch)
    print("saved position")
    buttonHandler.dragButton(testButtonName, touch)
    print("dragged button")
    loadedPosition = buttonHandler.positionOf(testButtonName)
    print("loaded position is", loadedPosition.x, loadedPosition.y)
    actionTestButtonName = "action button"
    buttonHandler.defaultButton(actionTestButtonName)
    print("action button defined")
    local actionTest = function() print("test action fired") end
    buttonAction(actionTestButtonName, actionTest)
    print("buttonAction defined")
    buttonHandler.doAction(actionTestButtonName)
    print("action performed")
    print("SUCCESS")
    local actionTest2 = function() print("test action 2 fired") end
    buttonAction("hello world", actionTest)
    buttonAction("sweet", actionTest2)
    ]]
end

function draw()
    background(255, 177, 0, 255)
    --to show a button just add a button(name) statement here
    --you don't need to do anything else
    button("hello world")
    button("sweet")
end


--# SimpleButton
button = function(name)
    if buttonHandler.configured == false then
        buttonHandler.configure()
    end
    pushStyle()
    strokeWidth(3)
    fill(255, 0, 0, 0)
    local buttonColor = color(250, 250, 250, 255)
    stroke(buttonColor)
    rectMode(CENTER)
    if buttonHandler.buttons[name] == nil then
        buttonHandler.defaultButton(name)
    end
    buttonTable = buttonHandler.buttons[name]
    rect(buttonTable.x,buttonTable.y,buttonTable.width,buttonTable.height)
    fill(buttonColor)
    text(name, buttonTable.x, buttonTable.y)
    popStyle()
    shouldRespond = buttonHandler.touchIsInside(name)
    if shouldRespond then
        if CurrentTouch.state ==  BEGAN and activatedButton == nil then
            activatedButton = name
        elseif activatedButton ~= name then
            return
        end
        if buttons_are_draggable then
            if CurrentTouch.state == MOVING then
                buttonHandler.dragButton(name, vec2( CurrentTouch.x, CurrentTouch.y))
            elseif CurrentTouch.state == ENDED then
                buttonHandler.savePositions()
                activatedButton = nil
            end
        elseif CurrentTouch.state == ENDED then
            buttonHandler.doAction(name)
            activatedButton = nil
        end
    end
end

buttonAction = function(name, action)
    if buttonHandler.buttons[name]== nil then
        buttonHandler.defaultButton(name)
    end
    buttonHandler.buttons[name].action = action
end

-- buttonHandler setup
buttonHandler = {}
buttonHandler.defaultWidth = 160
buttonHandler.defaultHeight = 55
buttonHandler.buttons = {}
buttonHandler.configured = false
buttonHandler.configure = function ()
    parameter.boolean("buttons are draggable", false)
    buttonHandler.configured = true
end

buttonHandler.defaultButton = function(name)
    buttonHandler.buttons[name] = {x=math.random(WIDTH),y=math.random(HEIGHT), width=buttonHandler.defaultWidth, height=buttonHandler.defaultHeight}
end

buttonHandler.doAction = function(name)
    if buttonHandler.buttons[name].action == nil then
        print("in 'setup()', use 'buttonAction(name, action)' to define an action for this button")
        return
    end
    buttonHandler.buttons[name].action()
end

buttonHandler.touchIsInside = function(name)
    insideX = math.abs(CurrentTouch.prevX-buttonHandler.buttons[name].x) < buttonHandler.buttons[name].width /2
    insideY = math.abs(CurrentTouch.prevY-buttonHandler.buttons[name].y) < buttonHandler.buttons[name].height /2
    if insideX and insideY then
        return true
    end
    return false
end

buttonHandler.dragButton = function (name, touch)
    buttonHandler.buttons[name].x = touch.x
    buttonHandler.buttons[name].y = touch.y
end

buttonHandler.positionOf = function (name)
    return buttonHandler.buttons[name]
end

buttonHandler.savePositions = function (name, position)
    dataString = ""
    for name, buttonValues in pairs(buttonHandler.buttons) do
        dataString = dataString.."buttonHandler.buttons['"..name.."'] = {x = "..buttonValues.x
        dataString = dataString..", y = "..buttonValues.y
        dataString = dataString..", width=buttonHandler.defaultWidth, height=buttonHandler.defaultHeight}\
"
    end
    saveProjectTab("ButtonTables",dataString)
end

--# ButtonTables
buttonHandler.buttons['hello world'] = {x = 734.5, y = 651.0, width=buttonHandler.defaultWidth, height=buttonHandler.defaultHeight}
buttonHandler.buttons['sweet'] = {x = 363.5, y = 581.0, width=buttonHandler.defaultWidth, height=buttonHandler.defaultHeight}

You can also add actions to the buttons very easily. Tapping a button while the project is running will tell you how to do it.

Being super-simple, they’re not very configurable beyond that, but they’ll get you up and running.

I hope someone finds this useful!

I also hope somebody feels inspired to make the buttons have rounded corners, and then shares that code, because that would be cool.

If you forum search rounded rectangle you’ll see that this is a challenge that has perplexed Codea users for some time! There are a lot of threads on it. There are basically 4 approaches, as far as I can see, in order from simplest to most complex:

  1. Draw 4 lines with line capmode set to rounded. Simple. (this is what Cider does). Cons: no easy way to get a stroke outline (aside from doubling the shape at a slightly smaller size), no way to get noFill() (ie just a stroke outline), and the circles at the end of the line are slightly thicker than the line itself, owing to different anti-aliasing algorithms for the capmodes. So the corners are sort of, well, knobbly. If you’re not terribly details-oriented, this is probably OK. But it wouldn’t get past Jonny Ive.

  2. Draw 4 overlapping ellipses and 2 rectangles. Also simple. Similar cons to above: no easy way to get an outline other than doubling draws (ie 12 overlapping primitives, not very efficient), no way to get noFill(), and again, the line at the corners has a different quality owing to different anti-aliasing.

  3. Make a mesh whose points are in the shape of a rounded rectangle. Pros: probably the best performing of all the approaches. Cons: no stroke, and no anti-aliasing, so the edges are a little sharp. A larger code base than 1 and 2.

  4. Calculate a rounded rectangle shape on the fragment shader. I didn’t actually try this one, because it seems to flaunt every rule I’ve seen for things not to do on a fragment shader (ie lots of branching if statements), and, conceptually, it’s like trying to crack a walnut by throwing a piece of masonry at it.

What I’ve done is, and I’ll have the code to share in a moment, adapt number 3 (the original was by @LoopSpace ), but added support for stroke, antialiasing, and proper noFill() outline drawing. If, like me, you fill your hours staring really hard at the corners of buttons in iOS, you’ll absolutely love it. If, on the other hand, you’re not really a rounded rectangle junky, just draw 4 lines with the cap mode set to rounded.

i agree on 3/ and as you i’ve made a stroke etc… version

Here’s a comparison image. Bottom right corner has consistent stroke, antialiasing, can do proper nofill(), is a single draw call.

rounded

And the code:


--# Main
-- RoundedRectangle FaceOff!!

function setup()
    strokeWidth(5)
    
    parameter.integer("CornerRadius",0,50,26)
    fill(30, 69, 123, 255)
    stroke(127, 127, 127, 255)
    strokeWidth(3)
end

function draw()
    background(126, 174, 224, 255)
   roundRect1(WIDTH*0.05, HEIGHT*0.55, WIDTH*0.45, HEIGHT*0.45, CornerRadius)
  roundRect2(WIDTH*0.55, HEIGHT*0.55, WIDTH*0.45, HEIGHT*0.45, CornerRadius)
 roundRect3(WIDTH*0.05, HEIGHT*0.05, WIDTH*0.45, HEIGHT*0.45, CornerRadius, 8)
  RoundedRectangle{x=WIDTH*0.75,y=HEIGHT*0.25,w=WIDTH*0.45,h=HEIGHT*0.45, radius = CornerRadius, corners= ~ 8}
end

--# RoundRect
function roundRect1(x, y, w, h, r)
    pushStyle()
    insetPos = vec2(x+r,y+r)
    insetSize = vec2(w-2*r,h-2*r)
    red,g,b,a = fill()
    stroke(red,g,b,a)
    fill(0)
    rectMode(CORNER)
    rect(insetPos.x,insetPos.y,insetSize.x,insetSize.y)
    
    if r > 0 then
        smooth()
        lineCapMode(ROUND)
        strokeWidth(r*2)
        
        line(insetPos.x, insetPos.y,
        insetPos.x + insetSize.x, insetPos.y)
        line(insetPos.x, insetPos.y,
        insetPos.x, insetPos.y + insetSize.y)
        line(insetPos.x, insetPos.y + insetSize.y,
        insetPos.x + insetSize.x, insetPos.y + insetSize.y)
        line(insetPos.x + insetSize.x, insetPos.y,
        insetPos.x + insetSize.x, insetPos.y + insetSize.y)
    end
    popStyle()
end

--# RoundRect2
-- rounded rect

function roundRect2(x,y,w,h,r)

    local oldFill = color( fill() )
    local oldStroke = color( stroke() )
    local s = strokeWidth()
    local ds = math.max(s-1,0)

    local function rawRect(x,y,w,h,r)
        translate(x,y)
        rect(r,0,w-2*r,h)
        rect(0,r,w,h-2*r)
        ellipse(r  ,r,r*2)
        ellipse(w-r,r,r*2)
        ellipse(r  ,h-r,r*2)
        ellipse(w-r,h-r,r*2)
        translate(-x,-y)
    end
    pushStyle()
    resetStyle()
    noStroke()
    fill(oldStroke)
    rawRect(x,y,w,h,r)
    fill(oldFill)
    rawRect(x+ds, y+ds, w-ds*2, h-ds*2, r-ds)
    popStyle()
end

--# RoundRect3

local __RRects = {}

function roundRect3(x,y,w,h,s,c,a)
    c = c or 0
    w = w or 0
    h = h or 0
    if w < 0 then
        x = x + w
        w = -w
    end
    if h < 0 then
        y = y + h
        h = -h
    end
    w = math.max(w,2*s)
    h = math.max(h,2*s)
    a = a or 0
    pushMatrix()
    translate(x,y)
    rotate(a)
    local label = table.concat({w,h,s,c},",")
    if __RRects[label] then
        __RRects[label]:setColors(fill())
        __RRects[label]:draw()
    else
    local rr = mesh()
    local v = {}
    local ce = vec2(w/2,h/2)
    local n = math.max(3, s//2)
    local o,dx,dy
    for j = 1,4 do
        dx = -1 + 2*(j%2)
        dy = -1 + 2*(math.floor(j/2)%2)
        o = ce + vec2(dx * (w/2 - s), dy * (h/2 - s))
        if math.floor(c/2^(j-1))%2 == 0 then
    for i = 1,n do
        table.insert(v,o)
        table.insert(v,o + vec2(dx * s * math.cos((i-1) * math.pi/(2*n)), dy * s * math.sin((i-1) * math.pi/(2*n))))
        table.insert(v,o + vec2(dx * s * math.cos(i * math.pi/(2*n)), dy * s * math.sin(i * math.pi/(2*n))))
    end
    else
        table.insert(v,o)
        table.insert(v,o + vec2(dx * s,0))
        table.insert(v,o + vec2(dx * s,dy * s))
        table.insert(v,o)
        table.insert(v,o + vec2(0,dy * s))
        table.insert(v,o + vec2(dx * s,dy * s))
    end
    end
    rr.vertices = v
    rr:addRect(ce.x,ce.y,w,h-2*s)
    rr:addRect(ce.x,ce.y + (h-s)/2,w-2*s,s)
    rr:addRect(ce.x,ce.y - (h-s)/2,w-2*s,s)
    rr:setColors(fill())
    rr:draw()
    __RRects[label] = rr
    end
    popMatrix()
end

--# RoundRect4
local __RRects = {}
--[[
true mesh rounded rectangle. Adapted from Andrew Stacey's code.
Adds anti-aliasing, optional fill and stroke components, optional texture that preserves aspect ratio of original image
usage: RoundedRectangle{key = arg, key2 = arg2}
required: x;y;w;h:  dimensions of the rectangle
optional: radius:   corner rounding radius, defaults to 6; 
          corners:  bitwise flag indicating which corners to round, defaults to 15 (all corners). 
                    Corners are numbered 1,2,4,8 starting in lower-left corner proceeding clockwise
                    eg to round the two bottom corners use: 1 | 8
                    to round all the corners except the top-left use: ~ 2
          tex:      texture image
use standard fill(), stroke(), strokeWidth(), noFill() etc to set body fill color, outline stroke color and stroke width
  ]]
function RoundedRectangle(t) 
    local s = t.radius or 6
    local c = t.corners or 15
    local w = math.max(t.w,2*s)
    local h = math.max(t.h,2*s)
    local label = table.concat({w,h,s,c},",")
    
    if not __RRects[label] then
        local rr = mesh()
        rr.shader = shader(rrectshad.vert, rrectshad.frag)
        local v = {}
        local no = {}

        local n = math.max(3, s//2)
        local o,dx,dy
        local edge, cent = vec3(0,0,1), vec3(0,0,0)
        for j = 1,4 do
            dx = 1 - 2*(((j+1)//2)%2)
            dy = -1 + 2*((j//2)%2)
            o = vec2(dx * (w * 0.5 - s), dy * (h * 0.5 - s))
            --  if math.floor(c/2^(j-1))%2 == 0 then
            local bit = 2^(j-1)
            if c & bit == bit then
                for i = 1,n do
                    
                    v[#v+1] = o
                    v[#v+1] = o + vec2(dx * s * math.cos((i-1) * math.pi/(2*n)), dy * s * math.sin((i-1) * math.pi/(2*n)))
                    v[#v+1] = o + vec2(dx * s * math.cos(i * math.pi/(2*n)), dy * s * math.sin(i * math.pi/(2*n)))
                    no[#no+1] = cent
                    no[#no+1] = edge
                    no[#no+1] = edge
                end
            else
                v[#v+1] = o
                v[#v+1] = o + vec2(dx * s,0)
                v[#v+1] = o + vec2(dx * s,dy * s)
                v[#v+1] = o
                v[#v+1] = o + vec2(0,dy * s)
                v[#v+1] = o + vec2(dx * s,dy * s)
                local new = {cent, edge, edge, cent, edge, edge}
                for i=1,#new do
                    no[#no+1] = new[i]
                end
            end
        end
        -- print("vertices", #v)
        --  r = (#v/6)+1
        rr.vertices = v
        
        rr:addRect(0,0,w-2*s,h-2*s)
        rr:addRect(0,(h-s)/2,w-2*s,s)
        rr:addRect(0,-(h-s)/2,w-2*s,s)
        rr:addRect(-(w-s)/2, 0, s, h - 2*s)
        rr:addRect((w-s)/2, 0, s, h - 2*s)
        --mark edges
        local new = {cent,cent,cent, cent,cent,cent,
        edge,cent,cent, edge,cent,edge,
        cent,edge,edge, cent,edge,cent,
        edge,edge,cent, edge,cent,cent,
        cent,cent,edge, cent,edge,edge}
        for i=1,#new do
            no[#no+1] = new[i]
        end
        rr.normals = no
        --texture
        if t.tex then
            rr.shader.fragmentProgram = rrectshad.fragTex
            rr.texture = t.tex
            local t = {}
            local ww,hh = w*0.5, h*0.5
            local aspect = vec2(w * (rr.texture.width/w), h * (rr.texture.height/h))
            
            for i,v in ipairs(rr.vertices) do
                t[i] = vec2((v.x + ww)/aspect.x, (v.y + hh)/aspect.y)
            end
            rr.texCoords = t
        end
        local sc = 1/math.max(2, s)
        rr.shader.scale = sc --set the scale, so that we get consistent one pixel anti-aliasing, regardless of size of corners
        __RRects[label] = rr
    end
    __RRects[label].shader.fillColor = color(fill())
    if strokeWidth() == 0 then
        __RRects[label].shader.strokeColor = color(fill())
    else
        __RRects[label].shader.strokeColor = color(stroke())
    end
    local sc = 1/math.max(2, s)
    
    __RRects[label].shader.strokeWidth = math.min( 1 - sc*3, strokeWidth() * sc)
    pushMatrix()
    translate(t.x,t.y)
    
    __RRects[label]:draw()
    popMatrix()
end

rrectshad ={
vert=[[
uniform mat4 modelViewProjection;

attribute vec4 position;

//attribute vec4 color;
attribute vec2 texCoord;
attribute vec3 normal;

//varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
varying vec3 vNormal;

void main()
{
    //  vColor = color;
    vTexCoord = texCoord;
    vNormal = normal;
    gl_Position = modelViewProjection * position;
}
]],
frag=[[
precision highp float;

uniform lowp vec4 fillColor;
uniform lowp vec4 strokeColor;
uniform float scale;
uniform float strokeWidth;

//varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
varying vec3 vNormal;

void main()
{
    lowp vec4 col = mix(strokeColor, fillColor, smoothstep(1. - strokeWidth - scale * 0.5, 1. - strokeWidth - scale * 1.5, vNormal.z)); //0.95, 0.92,
     col = mix(vec4(col.rgb, 0.), col, smoothstep(1., 1.-scale, vNormal.z) );
   // col *= smoothstep(1., 1.-scale, vNormal.z);
    gl_FragColor = col;
}
]],
fragTex=[[
precision highp float;

uniform lowp sampler2D texture;
uniform lowp vec4 fillColor;
uniform lowp vec4 strokeColor;
uniform float scale;
uniform float strokeWidth;

//varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
varying vec3 vNormal;

void main()
{
    vec4 pixel = texture2D(texture, vTexCoord) * fillColor;
    lowp vec4 col = mix(strokeColor, pixel, smoothstep(1. - strokeWidth - scale * 0.5, 1. - strokeWidth - scale * 1.5, vNormal.z)); //0.95, 0.92,
    // col = mix(vec4(0.), col, smoothstep(1., 1.-scale, vNormal.z) );
    col *= smoothstep(1., 1.-scale, vNormal.z);
    gl_FragColor = col;
}
]]
}

Button with rounded corners. This may not be perfect, but it’s simple. Drag the output window down to show all the parameters.

EDIT: More code can be added to make the button more square.

function setup()
    rectMode(CENTER)
    parameter.integer("x",100,WIDTH-100,200)
    parameter.integer("y",100,HEIGHT-100,200)
    parameter.integer("w",50,200,100)
    parameter.integer("h",50,200,100)
    parameter.integer("sw",2,12,6)
end

function draw()
    background(0)
    fill(255)
    stroke(255,0,0)
    strokeWidth(sw)
    rect(x,y,w,h)
    strokeWidth(sw-1)
    ellipse(x-w/2,y,h+1)
    ellipse(x+w/2,y,h+1)
    noStroke()
    rect(x,y,w,h-sw*2+2)
    ellipse(x,y,h-sw*2)
    fill(0,0,255)
    text("BUTTON",x,y)
end

The way it works is, I indicate the edges of the mesh with a vertex attribute (I actually use the z component of the normal, as this isn’t used in 2D drawing, and it’s less hassle than, and same memory usage as, setting up a custom buffer). Then, I blur those edges a little with good old smoothstep. As a bonus, if you pass in a texture, it will also calculate texCoords, allowing you to use the rounded rectangle shape as a “mask” as it were for other images (although in fact, no masking is happening, it is a genuine shape). So you can get cool effects like this:

blur

(The image inside the rectangle has been blurred with my Gaussian blur shader. The Gaussian blur also did the drop-shadow. I’m supposed to be writing interface elements. Instead I’m just doing eye-candy).

Man, @Yojimbo2000, that’s pretty!