This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Dog's profile picture

[RESOLVED] Two attempts at drawing a semi-complex menu - both work, but they're clumsy - any suggestions?

Started by Dog, 11 April 2014 - 06:40 PM
Dog #1
Posted 11 April 2014 - 08:40 PM
In a couple of my programs I provide a menu to choose a color assignment. Both methods I've devised work, but seem rather clumsy and I'm not completely happy with my 2nd attempt.

Here are a couple of examples of what I'm 'drawing' (you'll notice the chosen color is highlighted in it's own color and there is a 'color line' on the right that also matches the chosen color).

<images removed upon resolution to save on my upload limit>

What do I think can be better? Well, in the 2nd attempt, I'm wondering if there is a way to use fewer tables and a different parsing method to handle everything. Also, is there any way to do the 'last part' without all the if/elseif/elseif/.&#46;&#46;/end?

First attempt (brute force - no tables)
Spoiler

local function drawColorList(gRating)
  local trColor
  drawElement(7,4,13,8,white,gray,"")  -- menu body
  drawElement(7,4,1,1,white,blue,"")	-- B pip
  if gRating == "B" then
	drawElement(20,4,1,8,white,blue,"") -- B line
	trColor = blue
  else
	trColor = lgray
  end
  drawElement(9,4,4,1,trColor,gray,"Blue")
  drawElement(7,5,1,1,white,lblue,"")	-- L pip
  if gRating == "L" then
	drawElement(20,4,1,8,white,lblue,"") -- L line
	trColor = lblue
  else
	trColor = lgray
  end
  drawElement(9,5,10,1,trColor,gray,"Light Blue")
  drawElement(7,6,1,1,white,brown,"")	-- N pip
  if gRating == "N" then
	drawElement(20,4,1,8,white,brown,"") -- N line
	trColor = brown
  else
	trColor = lgray
  end
  drawElement(9,6,5,1,trColor,gray,"Brown")
  drawElement(7,7,1,1,white,purple,"")	-- P pip
  if gRating == "P" then
	drawElement(20,4,1,8,white,purple,"") -- P line
	trColor = purple
  else
	trColor = lgray
  end
  drawElement(9,7,6,1,trColor,gray,"Purple")
  drawElement(7,8,1,1,white,green,"")	-- G pip
  if gRating == "G" then
	drawElement(20,4,1,8,white,green,"") -- G line
	trColor = green
  else
	trColor = lgray
  end
  drawElement(9,8,5,1,trColor,gray,"Green")
  drawElement(7,9,1,1,white,orange,"")	-- O pip
  if gRating == "O" then
	drawElement(20,4,1,8,white,orange,"") -- O line
	trColor = orange
  else
	trColor = lgray
  end
  drawElement(9,9,6,1,trColor,gray,"Orange")
  drawElement(7,10,1,1,white,red,"")   -- R pip
  if gRating == "R" then
	drawElement(20,4,1,8,white,red,"") -- R line
	trColor = red
  else
	trColor = lgray
  end
  drawElement(9,10,3,1,trColor,gray,"Red")
  drawElement(7,11,1,1,white,lgray,"")   -- Y pip
  if gRating == "Y" then
	drawElement(20,4,1,8,white,lgray,"") -- Y line
	trColor = white
  else
	trColor = lgray
  end
  drawElement(9,11,10,1,trColor,gray,"Light Gray")
end

2nd attempt (trying to be 'more elegant')
Spoiler

local function drawColorList(gRating)
  local i,k,v
  local colorSpots = { blue,
					   lblue,
					   brown,
					   purple,
					   green,
					   orange,
					   red,
					   lgray
					 }
  local colorWords = { "Blue",
					   "Light Blue",
					   "Brown",
					   "Purple",
					   "Green",
					   "Orange",
					   "Red",
					   "Light Gray",
					 }
  local colorBurst = { B = blue,
					   L = lblue,
					   N = brown,
					   P = purple,
					   G = green,
					   O = orange,
					   R = red,
					   Y = lgray,
					 }
  drawElement(7,4,13,8,white,gray,"")	 -- menu body
  for i = 4,11,1 do
	drawElement(7,i,1,1,white,colorSpots[i-3],"")   -- color pips
	drawElement(9,i,1,1,lgray,gray,colorWords[i-3]) -- color words
  end
  for k,v in pairs(colorBurst) do
	if gRating == tostring(k) then
	  drawElement(20,4,1,8,white,v,"")   -- color line
	  break
	end
  end
  if gRating == "B" then
	drawElement(9,4,4,1,blue,gray,"Blue")
  elseif gRating == "L" then
	drawElement(9,5,10,1,lblue,gray,"Light Blue")
  elseif gRating == "N" then
	drawElement(9,6,5,1,brown,gray,"Brown")
  elseif gRating == "P" then
	drawElement(9,7,6,1,purple,gray,"Purple")
  elseif gRating == "G" then
	drawElement(9,8,5,1,green,gray,"Green")
  elseif gRating == "O" then
	drawElement(9,9,6,1,orange,gray,"Orange")
  elseif gRating == "R" then
	drawElement(9,10,3,1,red,gray,"Red")
  elseif gRating == "Y" then
	drawElement(9,11,10,1,lgray,gray,"Light Gray")
  end
end

drawElement (for reference)
Spoiler

local function drawElement(x,y,w,h,txColor,bgColor,text) -- x,y = cursor pos / w,h = width, height
  local deText,i = tostring(text)
  term.setCursorPos(x,y)
  term.setBackgroundColor(bgColor)
  if w > #deText or h > 1 then	   -- We're 'drawing' something more than text
	for i = 1,h,1 do				 --
	  term.write(string.rep(" ",w))  -- Draw the 'element' (box/rectangle/line-seg)
	  term.setCursorPos(x,y+i)	   --
	end
  end
  if deText ~= "" and deText ~= "nil" then
	term.setTextColor(txColor)
	if w < #deText then w = #deText end -- Ensure minimum length
	  local xW = (x + math.floor(w/2)) - math.floor(#deText/2) -- Center the text horizontally
	  local xH = y + math.floor(h/2)		   -- Center the text vertically
	  term.setCursorPos(xW,xH)
	  term.write(deText)
	end
  end
end

I've tried consolidating the code further without luck and I'm not sure how to proceed, or even if I should bother. Any ideas?
Edited on 11 April 2014 - 11:45 PM
CometWolf #2
Posted 11 April 2014 - 10:05 PM
I don't really get the structures of your tables… Why is the gRating variable limited to one letter? and also, why aren't you using all the colors? Using them all makes it super easy to work with, since you can just grab what you need from the colors table. I'd also imagine just keeping it all in one table would be a lot easier to work with.
Also, this

  for k,v in pairs(colorBurst) do
		if gRating == tostring(k) then
		  drawElement(20,4,1,8,white,v,"")   -- color line
		  break
		end
  end
is a completely redundant loop.

if colorBurst[gRating] then
  drawElement(20,4,1,8,white,colorBurst[gRating],"")
end

All in all, your drawElement function seems a little overkill.

  for i = 4,11,1 do
		drawElement(7,i,1,1,white,colorSpots[i-3],"")   -- color pips
		drawElement(9,i,1,1,lgray,gray,colorWords[i-3]) -- color words
  end

  for i = 4,11,1 do
		term.setBackgroundColor(colorSpots[i-3])
		term.setCursorPos(7,i)
		write" "
		term.setBackgroundColor(colors.lightGray)
		term.setTextColor(colors.gray)
		write(" "..colorWords[i-3].." ")
  end
Not really a tip per say, but i feel this makes the code more readable :P/>

As for the table stuff, something like this

local tColor = {
  B = {
	text = "Blue",
	color = blue,
  },
  L = {
	text = "Light Blue",
	color = lblue,
  },
  N = {
	text = "Brown",
	color = brown,
  },
  P = {
	text = "Purple",
	color = purple,
  },
  G = {
	text = "Green",
	color = green,
  },
  O = {
	text = "Orange",
	color = orange,
  },
  R = {
	text = "Red",
	color = red,
  },
  Y = {
	text = "Light Gray",
	color = lgray,
  },
}
Makes a lot more sense usage wise, in my eyes. Cause now i can compress the rest of your code into this

local longest = 0
for k,v in pairs(tColors) do
  longest = math.max(#v.text,longest)
end

local posY = 4
for k,v in pairs(tColors) do
  term.setBackgroundColor(v.color)
  term.setCursorPos(7,posY)
  write" "
  term.setBackgroundColor(colors.lightGray)
  term.setTextColor(k == gRating and v.color or colors.gray)
  write(" "..v.text..string.rep(" ",longest-#v.text).." ")
  posY+1
end

Incase you're wondering, i've done these a few times :P/>
Edited on 11 April 2014 - 08:06 PM
Dog #3
Posted 11 April 2014 - 11:06 PM
Hey, CometWolf,

Thanks for the in depth response. I'll try to answer your questions in order:
gRating is one letter because that's essentially 'legacy' from an old program - I took the structure of that 'system' from the old program and haven't really revisited it since.
I'm not using all the colors because I don't want users selecting colors that match the main colors of the UI or 'reserved' colors (that way elements don't blend into the background or other elements)

Ultimately, I was hoping for a single or two table solution as opposed to the mess I've created :P/>

Also this … is a completely redundant loop
I'm getting there. Yours is the second example I've received of using that form of syntax - I understand it and can use it, I'm just not thinking that way as a matter of habit yet. Thank you for the reinforcement.

drawElement() probably is overkill. That's actually a 'reduced' version - the full version also draws ON/OFF switches. I did that to reduce the length of my code and make it easier 'for me' to work with. It's probably a bit more expensive than just setting cursor pos, color, etc., but it helps me get more done in less time (from a programming perspective); it makes moving chunks of code and changing lines or sections quicker and easier for me at this stage of my programming skill.

I've looked up math.max and I'm confounded as to what is happening in your first loop. I think I get what it's doing, I just don't get how it's doing it.

Also, this line

term.setTextColor(k == gRating and v.color or colors.gray)

I've been trying to understand this use of and/or and I can't seem to get my head wrapped around it. Is there a resource you recommend that would help me get this straight?

So far, here's where I'm at in regard to your example (comments included in code)

local longest = 0
for k,v in pairs(tColors) do
  longest = math.max(#v.text,longest) -- what is this doing?
end

local posY = 4
for k,v in pairs(tColors) do
  term.setBackgroundColor(v.color) -- this loop sets the color pips, right?
  term.setCursorPos(7,posY)
  write" "
  term.setBackgroundColor(colors.lightGray) -- ?? menu b/g color?
  term.setTextColor(k == gRating and v.color or colors.gray) -- the last part, colors.gray - is that the 'unhighlighted' text color?
  write(" "..v.text..string.rep(" ",longest-#v.text).." ") -- lost on this - you're writing the color name, then adding fill afterward?
  posY+1 -- increment posY
end

If I'm reading this right, this still leaves drawing the color line separately, correct?

Thanks for your help, CometWolf!
CometWolf #4
Posted 11 April 2014 - 11:31 PM
Ah, i didn't notice that color line on the side lol, lemme revise that real quick.
Putting this at the end

local xColorLine = 7+longest+3
paintutils.drawLine(xColorLine,4,xColorLine,posY-1,tColors[gRating].color)
Should do it

Now then..
I've looked up math.max and I'm confounded as to what is happening in your first loop. I think I get what it's doing, I just don't get how it's doing it.
math.max returns the largest of the numbers passed.

print(math.max(2,8,1,4,20,4,15)) --would print 20
What im doing in that loop is looking at the length of each of the strings to be rendered on the menu, and determining which is the longest. This number is then used to know how long the gray background for the menu should be.

Also, this line
]term.setTextColor(k == gRating and v.color or colors.gray)
I've been trying to understand this use of and/or and I can't seem to get my head wrapped around it. Is there a resource you recommend that would help me get this straight?
Only resource i can think of when it comes to this stuff would be TheOriginalBit, he seems to be really good at em :P/> I just recently started using it myself actually.
Anyways, it's the equivalent of doing this

if k == gRating then
  term.setTextColor(v.color
else
  term.setTextColor(colors.gray)
end
Lemme see if i can break it down for you.
the AND operator returns the second argument if the first argument is not nil or false, otherwise it returns nil. Meanwhile the or statement checks the first argument for nil/false, if it finds none the first argument is returned, otherwise the second is used.
so

true and "herp" or "derp" --returns "herp" because the first and is true, thus making the first or "herp", meaning it's not nil/false.
false and true or "derp" --returns "derp" because the first and is false, thus making the first or false.
false or true --returns true because the first or is false
true and "derp" --returns "derp" because the first and is true
These can be chained indefintly aswell, so they can compact your code a lot from time to time.


  term.setBackgroundColor(v.color) -- this loop sets the color pips, right?
  term.setCursorPos(7,posY)
  write" "
Yep.


  term.setBackgroundColor(colors.lightGray) -- ?? menu b/g color?
Yeah, i figured i might aswell just render it with the text instead of drawing it on it's own.


  term.setTextColor(k == gRating and v.color or colors.gray) -- the last part, colors.gray - is that the 'unhighlighted' text color?
That's correct. Like i explained above this will use v.color if k == gRating, otherwise it will use colors.gray


  write(" "..v.text..string.rep(" ",longest-#v.text).." ") -- lost on this - you're writing the color name, then adding fill afterward?
First i add a blank space to the text string, then i make it as long as the longest string in the menu by adding as many spaces as the length of the longest string subtracted by the length of the current string. And finally i add one last space.
Dog #5
Posted 12 April 2014 - 12:32 AM
OK, I'm starting to get it. I see how you're drawing the menu as you go as opposed to wasting expense drawing a blank menu, then populating it. I think I've got a good handle on what everything is doing now and (mostly) how/why it's doing it. This is definitely thinking on a scale/level I'm not used to yet.

Thank you for taking the time to explain the and/or thing to me - it makes a lot more sense now. Definitely gonna have to play with that. Your herp/derp example had me literally laughing out loud considering my programming skill is currently somewhere between herp and derp.

Unless you have any objections, I'm going to use this in some of my programs as a continuing reference for myself.

Thanks, again, CometWolf. You and everyone participating in Ask A Pro have always been patient, kind, and informative - it makes asking questions a joy.

I have one final question if you're so inclined - do you know why the menu is 'drawn' out of order (not in the order of the table itself) and/or any way to force the order? When not using numbered (ordered?) tables this has always confounded me - the order produced is reliable (it doesn't change from use to use or program to program) but it's not the order I 'prefer'. I take it this is just a limitation of non-numbered tables?
Edited on 11 April 2014 - 10:35 PM
Bomb Bloke #6
Posted 12 April 2014 - 01:00 AM
When you use non-numeric table keys, Lua takes a hash of their names and uses those to index them in memory. They're hence ordered according to those hashes (as opposed to alphabetically or in the order they went into the table).

Generally, if you care about order, you're better off sticking with numeric indexes.

The "condition" and "trueValue" or "falseValue" thing is often called the Lua ternary.
Dog #7
Posted 12 April 2014 - 01:05 AM
When you use non-numeric table keys, Lua takes a hash of their names and uses those to index them in memory. They're hence ordered according to those hashes (as opposed to alphabetically or in the order they went into the table).

Generally, if you care about order, you're better off sticking with numeric indexes.
Thanks, Bomb Bloke - that clears it up nicely.

The "condition" and "trueValue" or "falseValue" thing is often called the Lua ternary.
Great resource. Thanks, again!
CometWolf #8
Posted 12 April 2014 - 01:17 AM
Happy to help, im too bored of my own projects to work on them anyways :P/>
Feel free to use it for anything you like.

Personally i use numbered indexes for my menus, shouldn't be that tough to change up the code i posted to do that aswell.

local tColor = {
  [1] = {
        text = "Blue",
        color = blue,
        gRating = "B"
  },
  [2] = {
        text = "Light Blue",
        color = lblue,
        gRating = "L"
  },
  [3] = {
        text = "Brown",
        color = brown,
        gRating = "N"
  },
  [4] = {
        text = "Purple",
        color = purple,
        gRating = "P"
  },
  [5] = {
        text = "Green",
        color = green,
        gRating = "G"
  },
  [6] = {
        text = "Orange",
        color = orange,
        gRating = "O"
  },
  [7] = {
        text = "Red",
        color = red,
        gRating = "R"
  },
  [8] = {
        text = "Light Gray",
        color = lgray,
        gRating = "Y"
  },
}

local longest = 0
for k,v in pairs(tColors) do
  longest = math.max(#v.text,longest)
end
local posY = 4
local gRatingCol
for i=1,#tColor do
  local v = tColors[i]
  term.setBackgroundColor(v.color)
  term.setCursorPos(7,posY)
  write" "
  term.setBackgroundColor(colors.lightGray)
  if gRating == v.gRating then
    gRatingCol = v.color
    term.setTextColor(gRatingCol)
  else
    term.setTextColor(colors.gray)
  end
  write(" "..v.text..string.rep(" ",longest-#v.text).." ")
  posY+1
end
local xColorLine = 7+longest+3
paintutils.drawLine(xColorLine,4,xColorLine,posY-1,gRatingCol)
Dog #9
Posted 12 April 2014 - 01:44 AM
CometWolf - that's exactly what I was aiming for, thanks.

'Final' question - does my program gain anything from doing it this way as opposed to either of my attempts? I know I'm gaining knowledge and understanding (personally), but I'm curious if this 'better' or just 'different' in regards to the program itself.
Edited on 11 April 2014 - 11:50 PM
Bomb Bloke #10
Posted 12 April 2014 - 02:18 AM
A few things to consider when writing code are "how long it is", "how much RAM it uses", and "how many instructions it performs".

Generally you want to keep all three of these things to a minimum. It makes it easier for people to read. It makes it easier to edit. It makes it run faster, and it makes it easier to track down bugs.

Consider, for example, the difference in difficulty between adding a menu item to your initial code draft, and adding a menu item to your final script.

Sometimes these traits are somewhat mutually exclusive, though. You can often trade one for another. For eg, your initial draft declares just two variables, whereas that figure shoots up when you start implementating tables. (That's a bit of a poor example though, as the script itself has to be loaded into RAM too, and so the reductions in code likely outweigh the extra memory used by the additional variables - but I'm sure you get the idea.)
Dog #11
Posted 12 April 2014 - 02:40 AM
I see your point. I hadn't considered the ease of editing CometWolf's implementation - that's a huge plus. I was definitely more focused on the 'expense' side of things when I asked (with a little concern about maintaining code I didn't create mixed in)…should have been thinking about the 'big picture' I guess.

It's probably going to take some time for me to fully ingest what I've learned (I'm still processing info from previous visits to Ask A Pro), so I may be back with more questions at some point.

Thank you, Bomb Bloke!