UI tool: dynamicRects—just like normal rects, but dynamic!

Working off of the resizing code in @dave1707’s excellent scrolling-textboxes project, these are draggable and resizable rects that are as easy to use as the existing rect(x,y,w,h) command—and in some ways easier.

I think there are a couple-three cool enhancements to dave’s rect-dragging code, too.

  • When you tap or drag a dynamicRect, the draw order changes on the fly, so that whatever rect is currently selected draws itself on top of all the others.
  • When you’re resizing a rect, it will stay visible even if you drag one corner past the opposite corner.
  • The hotspots for dragging the corners extend a little outside the bounds of the rect itself, making them a lot easier to manipulate.
  • The corners are reserved for resizing, but the whole rect can be dragged by putting your finger down anywhere else on its insides.
  • Since dynamicRects are created with names, you can set them to show their names during runtime so you never get confused which is which.
  • You can get a dynamicRect’s exact size and position at any time by tapping it.

When you find the right position and size, you can note down those coordinates and add them directly into the dynamicRect command, which locks it down so that it acts just like a normal rect().

@UberGoober Heres something simple I threw together, don’t know how useful it would be. This is similar to what you say above. You can move a rect around and resize it. Drag a corner to resize, drag from center to move. But instead of writing down the coordinates when you get the size and position you want, you just tap on the center of the rect 5 times to lock it. After that you use it as normal. Tap 5 times again to unlock it if you what to change it.

viewer.mode=STANDARD

function setup()
    boxTab={}
    rectMode(CENTER)
    table.insert(boxTab,box(200,200,100,200,"box1",1))
    table.insert(boxTab,box(200,400,200,100,"box2",2))
    table.insert(boxTab,box(500,500,200,100,"box3",3))
end

function draw()
    background(0)
    for a,b in pairs(boxTab) do
        b:draw()
    end  
end

function touched(t)
    for a,b in pairs(boxTab) do
        b:touched(t)
    end  
end

box=class()

function box:init(x,y,w,h,n,v)    
    self.x=x
    self.y=y
    self.w=w
    self.h=h
    self.sel=0
    self.lock=false
    self.dir=""
    self.name=n
    self.dist=20
end

function box:draw()  
    fill(188, 176, 175)  
    rect(self.x,self.y,self.w,self.h)
    fill(255)
    if self.lock then
        fill(255,0,0)
    end
    text(self.name,self.x,self.y)

end

function box:touched(t)
    if t.state==BEGAN then
        if t.x>self.x-self.w/2 and t.x<self.x+self.w/2 and
                t.y>self.y-self.h/2 and t.y<self.y+self.h/2 and self.lock then
            print(self.name.."  selected")
        end
        if math.abs(t.x-self.x)<self.dist and math.abs(t.y-self.y)<self.dist then
            self.sel=1
            if self.lock then print(self.name.." selected") end
            if t.tapCount==5 then self.lock= not self.lock end
        end
        if math.abs(t.x-(self.x+self.w/2))<self.dist and 
                math.abs(t.y-self.y+self.h/2)<self.dist then self.dir="ur" end
        if math.abs(t.x-self.x-self.w/2)<self.dist and 
                math.abs(t.y-self.y+self.h/2)<self.dist then self.dir="lr" end
        if math.abs(t.x-self.x+self.w/2)<self.dist and 
                math.abs(t.y-self.y+self.h/2)<self.dist then self.dir="ll" end
        if math.abs(t.x-self.x+self.w/2)<self.dist and 
                math.abs(t.y-self.y-self.h/2)<self.dist then self.dir="ul" end
    end
    if t.state==CHANGED and not self.lock then
        if self.sel==1 then
            self.x,self.y=t.x,t.y 
        elseif self.dir=="ur" then  
            self.w,self.h=self.w+t.deltaX*2,self.h+t.deltaY*2
        elseif self.dir=="ul" then  
            self.w,self.h=self.w-t.deltaX*2,self.h+t.deltaY*2
        elseif self.dir=="lr" then  
            self.w,self.h=self.w+t.deltaX*2,self.h-t.deltaY*2
        elseif self.dir=="ll" then  
            self.w,self.h=self.w-t.deltaX*2,self.h-t.deltaY*2
        end
        if self.w<50 then self.w=50 end
        if self.h<50 then self.h=50 end
    end
    if t.state==ENDED then
        self.sel,self.dir=0,""
    end
end

@dave1707 I think there’s something off about how touches work in this project, for me, because often the rects don’t respond at all.

In any case you do still have to write the coordinates down with this approach, if you want them to stay in a consistent place, because the sizes and positions of locked rects don’t persist between launches of the project—which would take some additional coding because the project doesn’t expose that information during runtime.

I intend to have dynamicRects remember their own positions eventually but right now you can get the coordinates of a dynamicRect at any time by tapping it.

@UberGoober I don’t have any problems with the touch. There’s the variable self.dist that can be increased to make the touch areas larger. The center of the corner touch areas are the corners themselves. This was just a simple demo. The box info can be saved and read back in when the program starts. I’m not sure if I would use something like this anyways, but then I haven’t written anything lately that has a lot of buttons placed around the screen.

Maybe it needs an option to create or destroy buttons on the fly.

@dave1707 I don’t know what to tell you, I’d say at least 1 out of 5 times, when I try to drag a rect to reposition it, it doesn’t respond at all.

@UberGoober I don’t know what to tell you, I don’t have any trouble with the touch areas. Did you try increasing the value in self.dist. Anyways, here’s another version. This one allows you to create or destroy a button on the fly. Put a name in the name box and slide one of the parameters.

viewer.mode=STANDARD

function setup()
    parameter.text("name",name)
    parameter.boolean("create",false,create)
    parameter.boolean("destroy",false,destroy)
    boxTab={}
    rectMode(CENTER)
    table.insert(boxTab,box(200,200,100,200,"box1",1))
    table.insert(boxTab,box(200,400,200,100,"box2",2))
    table.insert(boxTab,box(500,500,200,100,"box3",3))
end

function draw()
    background(0)
    for a,b in pairs(boxTab) do
        b:draw()
    end  
end

function touched(t)
    for a,b in pairs(boxTab) do
        b:touched(t)
    end  
end

function create()
    if name~="" then
        table.insert(boxTab,box(WIDTH/2,HEIGHT/2,200,100,name,3))
    end
    create=false
end

function destroy()
    if name~="" then
        for a,b in pairs(boxTab) do
            if b.name==name then
                table.remove(boxTab,a)
            end
        end
    end
    destroy=false
end

box=class()

function box:init(x,y,w,h,n,v)    
    self.x=x
    self.y=y
    self.w=w
    self.h=h
    self.sel=0
    self.lock=false
    self.dir=""
    self.name=n
    self.dist=20
end

function box:draw()  
    fill(188, 176, 175)  
    rect(self.x,self.y,self.w,self.h)
    fill(255)
    if self.lock then
        fill(255,0,0)
    end
    text(self.name,self.x,self.y)
    
end

function box:touched(t)
    if t.state==BEGAN then
        if t.x>self.x-self.w/2 and t.x<self.x+self.w/2 and
        t.y>self.y-self.h/2 and t.y<self.y+self.h/2 and self.lock then
            print(self.name.."  selected")
        end
        if math.abs(t.x-self.x)<self.dist and math.abs(t.y-self.y)<self.dist then
            self.sel=1
            if self.lock then print(self.name.." selected") end
            if t.tapCount==5 then self.lock= not self.lock end
        end
        if math.abs(t.x-(self.x+self.w/2))<self.dist and 
        math.abs(t.y-(self.y+self.h/2))<self.dist then self.dir="ur" end        
        if math.abs(t.x-(self.x+self.w/2))<self.dist and 
        math.abs(t.y-(self.y-self.h/2))<self.dist then self.dir="lr" end        
        if math.abs(t.x-(self.x-self.w/2))<self.dist and 
        math.abs(t.y-(self.y-self.h/2))<self.dist then self.dir="ll" end       
        if math.abs(t.x-(self.x-self.w/2))<self.dist and 
        math.abs(t.y-(self.y+self.h/2))<self.dist then self.dir="ul" end
    end
    if t.state==CHANGED and not self.lock then
        if self.sel==1 then
            self.x,self.y=t.x,t.y 
        elseif self.dir=="ur" then  
            self.w,self.h=self.w+t.deltaX*2,self.h+t.deltaY*2
        elseif self.dir=="ul" then  
            self.w,self.h=self.w-t.deltaX*2,self.h+t.deltaY*2
        elseif self.dir=="lr" then  
            self.w,self.h=self.w+t.deltaX*2,self.h-t.deltaY*2
        elseif self.dir=="ll" then  
            self.w,self.h=self.w-t.deltaX*2,self.h-t.deltaY*2
        end
        if self.w<50 then self.w=50 end
        if self.h<50 then self.h=50 end
    end
    if t.state==ENDED then
        self.sel,self.dir=0,""
    end
end

@UberGoober It looks like the upper right corner doesn’t work right.

@UberGoober I think I have the four corners fixed in the code above.

@dave1707 It’s fun! I added this at line 88:


if self.dir ~= "" or self.sel ~= 0 then
            print(self.name.." "..self.dir)
            name = self.name
        end

…it makes destroying boxes easier.

If you wouldn’t mind a small idea offered, I’d recommend testing for any inner touches at all to make a rect be selected, instead of just near the center.

@UberGoober Theres a lot more that can be done with this, but it was something I thought I’d try. Adding code to save the boxTab table to a tab should be easy. That way this program could be used to layout a button screen and the table saved to a tab. That tab could then be copied to a new project and coded from there without having to manually place the buttons in the new code.

@dave1707 that’s exactly how my SimpleButtons work :wink: