Advertisement
Guest User

Untitled

a guest
Mar 27th, 2015
216
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 50.05 KB | None | 0 0
  1. --[[*********************************************
  2. gui library
  3. by GopherAtl
  4. do whatever you want, just don't be a dick. Give
  5. me credit whenever you redistribute, modified or
  6. otherwise.
  7. For the latest updates and documentations, check
  8. out the github repo and it's wiki at
  9. https://github.com/OpenPrograms/Gopher-Programs
  10. --***********************************************]]
  11.  
  12. local event=require("event")
  13. local component=require("component")
  14. local term=require("term")
  15. local computer=require("computer")
  16. local shell=require("shell")
  17. local filesystem=require("filesystem")
  18. local keyboard=require("keyboard")
  19. local unicode=require("unicode")
  20. local gfxbuffer=require("gml.gfxbuffer")
  21. local process = require("process")
  22.  
  23. local doubleClickThreshold=.25
  24.  
  25. local gml={VERSION="1.0"}
  26.  
  27. local defaultStyle=nil
  28.  
  29. --clipboard is global between guis and gui sessions, as long as you don't reboot.
  30. local clipboard=nil
  31.  
  32. local validElements = {
  33.   ["*"]=true,
  34.   gui=true,       --top gui container
  35.   label=true,     --text labels, non-focusable (naturally), non-readable
  36.   button=true,    --buttons, text label, clickable
  37.   textfield=true, --single-line text input, can scroll left-right, never has scrollbar, just scrolls with cursor
  38.   scrollbar=true, --scroll bar, scrolls. Can be horizontal or vertical.
  39.   textbox=true,   --multi-line text input, line wraps, scrolls up-down, has scroll bar if needed
  40.   listbox=true,   --list, vertical stack of labels with a scrollbar
  41.   folder=true,    --
  42. }
  43.  
  44. local validStates = {
  45.   ["*"]=true,
  46.   enabled=true,
  47.   disabled=true,
  48.   checked=true,
  49.   focus=true,
  50.   empty=true,
  51.   selected=true,
  52. }
  53.  
  54. local validDepths = {
  55.   ["*"]=true,
  56.   [1]=true,
  57.   [4]=true,
  58.   [8]=true,
  59. }
  60.  
  61. local screen = {
  62.       posX=1, posY=1,
  63.       bodyX=1,bodyY=1,
  64.       hidden=false,
  65.       isHidden=function() return false end,
  66.       renderTarget=component.gpu
  67.     }
  68.  
  69. screen.width,screen.height=component.gpu.getResolution()
  70. screen.bodyW,screen.bodyH=screen.width,screen.height
  71.  
  72. --**********************
  73. --utility functions
  74.  
  75. function round(v)
  76.   return math.floor(v+.5)
  77. end
  78.  
  79.  
  80. --**********************
  81. --api functions
  82.  
  83. function gml.loadStyle(name)
  84.   --search for file
  85.   local fullname=name
  86.   if name:match(".gss$") then
  87.     name=name:match("^(.*)%.gss$")
  88.   else
  89.     fullname=name..".gss"
  90.   end
  91.  
  92.   local filepath
  93.  
  94.   --search for styles in working directory, running program directory, /lib /usr/lib. Just because.
  95.   local dirs={shell.getWorkingDirectory(),process.running():match("^(.*/).+$"), "/lib/", "/usr/lib/"}
  96.   if dirs[1]~="/" then
  97.     dirs[1]=dirs[1].."/"
  98.   end
  99.   for i=1,#dirs do
  100.     if filesystem.exists(dirs[i]..fullname) and not filesystem.isDirectory(dirs[i]..fullname) then
  101.       filepath=dirs[i]..fullname
  102.       break
  103.     end
  104.   end
  105.  
  106.   if not filepath then
  107.     error("Could not find gui stylesheet \""..name.."\"",2)
  108.   end
  109.  
  110.   --found it, open and parse
  111.   local file=assert(io.open(filepath,"r"))
  112.  
  113.   local text=file:read("*all")
  114.   file:close()
  115.   text=text:gsub("/%*.-%*/",""):gsub("\r\n","\n")
  116.  
  117.   local styleTree={}
  118.  
  119.   --util method used in loop later when building styleTree
  120.   local function descend(node,to)
  121.     if node[to]==nil then
  122.       node[to]={}
  123.     end
  124.     return node[to]
  125.   end
  126.  
  127.  
  128.   for selectorStr, body in text:gmatch("%s*([^{]*)%s*{([^}]*)}") do
  129.     --parse the selectors!
  130.     local selectors={}
  131.     for element in selectorStr:gmatch("([^,^%s]+)") do
  132.       --could have a !depth modifier
  133.       local depth,state,class, temp
  134.       temp,depth=element:match("(%S+)!(%S+)")
  135.       element=temp or element
  136.       temp,state=element:match("(%S+):(%S+)")
  137.       element=temp or element
  138.       temp,class=element:match("(%S+)%.(%S+)")
  139.       element=temp or element
  140.       if element and validElements[element]==nil then
  141.         error("Encountered invalid element "..element.." loading style "..name)
  142.       end
  143.       if state and validStates[state]==nil then
  144.         error("Encountered invalid state "..state.." loading style "..name)
  145.       end
  146.       if depth and validDepths[tonumber(depth)]==nil then
  147.         error("Encountered invalid depth "..depth.." loading style "..name)
  148.       end
  149.  
  150.       selectors[#selectors+1]={element=element or "*",depth=tonumber(depth) or "*",state=state or "*",class=class or "*"}
  151.     end
  152.  
  153.     local props={}
  154.     for prop,val in body:gmatch("(%S*)%s*:%s*(.-);") do
  155.       if tonumber(val) then
  156.         val=tonumber(val)
  157.       elseif val:match("U%+%x+") then
  158.         val=unicode.char(tonumber("0x"..val:match("U%+(.*)")))
  159.       elseif val:match("^%s*[tT][rR][uU][eE]%s*$") then
  160.         val=true
  161.       elseif val:match("^%s*[fF][aA][lL][sS][eE]%s*$") then
  162.         val=false
  163.       elseif val:match("%s*(['\"]).*(%1)%s*") then
  164.         _,val=val:match("%s*(['\"])(.*)%1%s*")
  165.       else
  166.         error("invalid property value '"..val.."'!")
  167.       end
  168.  
  169.       props[prop]=val
  170.     end
  171.  
  172.     for i=1,#selectors do
  173.       local sel=selectors[i]
  174.       local node=styleTree
  175.  
  176.  
  177.       node=descend(node,sel.depth)
  178.       node=descend(node,sel.state)
  179.       node=descend(node,sel.class)
  180.       node=descend(node,sel.element)
  181.       --much as I'd like to save mem, dupe selectors cause merges, which, if
  182.       --instances are duplicated in the final style tree, could result in spraying
  183.       --props in inappropriate places
  184.       for k,v in pairs(props) do
  185.         node[k]=v
  186.       end
  187.     end
  188.  
  189.   end
  190.  
  191.   return styleTree
  192. end
  193.  
  194.  
  195. --**********************
  196. --internal style-related utility functions
  197.  
  198. local function tableCopy(t1)
  199.   local copy={}
  200.   for k,v in pairs(t1) do
  201.     if type(v)=="table" then
  202.       copy[k]=tableCopy(v)
  203.     else
  204.       copy[j]=v
  205.     end
  206.   end
  207. end
  208.  
  209. local function mergeStyles(t1, t2)
  210.   for k,v in pairs(t2) do
  211.     if t1[k]==nil then
  212.       t1[k]=tableCopy(v)
  213.     elseif type(t1[k])=="table" then
  214.       if type(v)=="table" then
  215.         tableMerge(t1[k],v)
  216.       else
  217.         error("inexplicable error in mergeStyles - malformed style table, attempt to merge "..type(v).." with "..type(t1[k]))
  218.       end
  219.     elseif type(v)=="table" then
  220.       error("inexplicable error in mergeStyles - malformed style table, attempt to merge "..type(v).." with "..type(t1[k]))
  221.     else
  222.       t1[k]=v
  223.     end
  224.   end
  225. end
  226.  
  227.  
  228. function getAppliedStyles(element)
  229.   local styleRoot=element.style
  230.   assert(styleRoot)
  231.  
  232.   --descend, unless empty, then back up... so... wtf
  233.   local depth,state,class,elementType=element.renderTarget.getDepth(),element.state or "*",element.class or "*", element.type
  234.  
  235.   local nodes={styleRoot}
  236.   local function filterDown(nodes,key)
  237.     local newNodes={}
  238.     for i=1,#nodes do
  239.       if key~="*" and nodes[i][key] then
  240.         newNodes[#newNodes+1]=nodes[i][key]
  241.       end
  242.       if nodes[i]["*"] then
  243.         newNodes[#newNodes+1]=nodes[i]["*"]
  244.       end
  245.     end
  246.     return newNodes
  247.   end
  248.   nodes=filterDown(nodes,depth)
  249.   nodes=filterDown(nodes,state)
  250.   nodes=filterDown(nodes,class)
  251.   nodes=filterDown(nodes,elementType)
  252.   return nodes
  253. end
  254.  
  255.  
  256. function extractProperty(element,styles,property)
  257.   if element[property] then
  258.     return element[property]
  259.   end
  260.   for j=1,#styles do
  261.     local v=styles[j][property]
  262.     if v~=nil then
  263.       return v
  264.     end
  265.   end
  266. end
  267.  
  268. local function extractProperties(element,styles,...)
  269.   local props={...}
  270.  
  271.   --nodes is now a list of all terminal branches that could possibly apply to me
  272.   local vals={}
  273.   for i=1,#props do
  274.     vals[#vals+1]=extractProperty(element,styles,props[i])
  275.     if #vals~=i then
  276.       for k,v in pairs(styles[1]) do print('"'..k..'"',v,k==props[i] and "<-----!!!" or "") end
  277.       error("Could not locate value for style property "..props[i].."!")
  278.     end
  279.   end
  280.   return table.unpack(vals)
  281. end
  282.  
  283. local function findStyleProperties(element,...)
  284.   local props={...}
  285.   local nodes=getAppliedStyles(element)
  286.   return extractProperties(element,nodes,...)
  287. end
  288.  
  289.  
  290. --**********************
  291. --drawing and related functions
  292.  
  293.  
  294. local function parsePosition(x,y,width,height,maxWidth, maxHeight)
  295.  
  296.   width=math.min(width,maxWidth)
  297.   height=math.min(height,maxHeight)
  298.  
  299.   if x=="left" then
  300.     x=1
  301.   elseif x=="right" then
  302.     x=maxWidth-width+1
  303.   elseif x=="center" then
  304.     x=math.max(1,math.floor((maxWidth-width)/2))
  305.   elseif x<0 then
  306.     x=maxWidth-width+2+x
  307.   elseif x<1 then
  308.     x=1
  309.   elseif x+width-1>maxWidth then
  310.     x=maxWidth-width+1
  311.   end
  312.  
  313.   if y=="top" then
  314.     y=1
  315.   elseif y=="bottom" then
  316.     y=maxHeight-height+1
  317.   elseif y=="center" then
  318.     y=math.max(1,math.floor((maxHeight-height)/2))
  319.   elseif y<0 then
  320.     y=maxHeight-height+2+y
  321.   elseif y<1 then
  322.     y=1
  323.   elseif y+height-1>maxHeight then
  324.     y=maxHeight-height+1
  325.   end
  326.  
  327.   return x,y,width,height
  328. end
  329.  
  330. --draws a frame, based on the relevant style properties, and
  331. --returns the effective client area inside the frame
  332. local function drawBorder(element,styles)
  333.   local screenX,screenY=element:getScreenPosition()
  334.  
  335.   local borderFG, borderBG,
  336.         border,borderLeft,borderRight,borderTop,borderBottom,
  337.         borderChL,borderChR,borderChT,borderChB,
  338.         borderChTL,borderChTR,borderChBL,borderChBR =
  339.       extractProperties(element,styles,
  340.         "border-color-fg","border-color-bg",
  341.         "border","border-left","border-right","border-top","border-bottom",
  342.         "border-ch-left","border-ch-right","border-ch-top","border-ch-bottom",
  343.         "border-ch-topleft","border-ch-topright","border-ch-bottomleft","border-ch-bottomright")
  344.  
  345.   local width,height=element.width,element.height
  346.  
  347.   local bodyX,bodyY=screenX,screenY
  348.   local bodyW,bodyH=width,height
  349.  
  350.   local gpu=element.renderTarget
  351.  
  352.   if border then
  353.     gpu.setBackground(borderBG)
  354.     gpu.setForeground(borderFG)
  355.  
  356.     --as needed, leave off top and bottom borders if height doesn't permit them
  357.     if borderTop and bodyW>1 then
  358.       bodyY=bodyY+1
  359.       bodyH=bodyH-1
  360.       --do the top bits
  361.       local str=(borderLeft and borderChTL or borderChT)..borderChT:rep(bodyW-2)..(borderRight and borderChTR or borderChB)
  362.       gpu.set(screenX,screenY,str)
  363.     end
  364.     if borderBottom and bodyW>1 then
  365.       bodyH=bodyH-1
  366.       --do the top bits
  367.       local str=(borderLeft and borderChBL or borderChB)..borderChB:rep(bodyW-2)..(borderRight and borderChBR or borderChB)
  368.       gpu.set(screenX,screenY+height-1,str)
  369.     end
  370.     if borderLeft then
  371.       bodyX=bodyX+1
  372.       bodyW=bodyW-1
  373.       for y=bodyY,bodyY+bodyH-1 do
  374.         gpu.set(screenX,y,borderChL)
  375.       end
  376.     end
  377.     if borderRight then
  378.       bodyW=bodyW-1
  379.       for y=bodyY,bodyY+bodyH-1 do
  380.         gpu.set(screenX+width-1,y,borderChR)
  381.       end
  382.     end
  383.   end
  384.  
  385.   return bodyX,bodyY,bodyW,bodyH
  386. end
  387.  
  388. --calculates the body coords of an element based on it's true coords
  389. --and border style properties
  390. local function calcBody(element)
  391.   local x,y,w,h=element.posX,element.posY,element.width,element.height
  392.   local border,borderTop,borderBottom,borderLeft,borderRight =
  393.      findStyleProperties(element,"border","border-top","border-bottom","border-left","border-right")
  394.  
  395.   if border then
  396.     if borderTop then
  397.       y=y+1
  398.       h=h-1
  399.     end
  400.     if borderBottom then
  401.       h=h-1
  402.     end
  403.     if borderLeft then
  404.       x=x+1
  405.       w=w-1
  406.     end
  407.     if borderRight then
  408.       w=w-1
  409.     end
  410.   end
  411.   return x,y,w,h
  412. end
  413.  
  414. local function correctForBorder(element,px,py)
  415.   px=px-(element.bodyX and element.bodyX-element.posX or 0)
  416.   py=py-(element.bodyY and element.bodyY-element.posY or 0)
  417.   return px,py
  418. end
  419.  
  420. local function frameAndSave(element)
  421.   local t={}
  422.   local x,y,width,height=element.posX,element.posY,element.width,element.height
  423.  
  424.   local pcb=term.getCursorBlink()
  425.   local curx,cury=term.getCursor()
  426.   local pfg,pbg=element.renderTarget.getForeground(),element.renderTarget.getBackground()
  427.   local rtg=element.renderTarget.get
  428.   --preserve background
  429.   for ly=1,height do
  430.     t[ly]={}
  431.     local str, cfg, cbg=rtg(x,y+ly-1)
  432.     for lx=2,width do
  433.       local ch, fg, bg=rtg(x+lx-1,y+ly-1)
  434.       if fg==cfg and bg==cbg then
  435.         str=str..ch
  436.       else
  437.         t[ly][#t[ly]+1]={str,cfg,cbg}
  438.         str,cfg,cbg=ch,fg,bg
  439.       end
  440.     end
  441.     t[ly][#t[ly]+1]={str,cfg,cbg}
  442.   end
  443.   local styles=getAppliedStyles(element)
  444.  
  445.   local bodyX,bodyY,bodyW,bodyH=drawBorder(element,styles)
  446.  
  447.   local fillCh,fillFG,fillBG=extractProperties(element,styles,"fill-ch","fill-color-fg","fill-color-bg")
  448.  
  449.   local blankRow=fillCh:rep(bodyW)
  450.  
  451.   element.renderTarget.setForeground(fillFG)
  452.   element.renderTarget.setBackground(fillBG)
  453.   term.setCursorBlink(false)
  454.  
  455.   element.renderTarget.fill(bodyX,bodyY,bodyW,bodyH,fillCh)
  456.  
  457.   return {curx,cury,pcb,pfg,pbg, t}
  458.  
  459. end
  460.  
  461. local function restoreFrame(renderTarget,x,y,prevState)
  462.  
  463.   local curx,cury,pcb,pfg,pbg, behind=table.unpack(prevState)
  464.  
  465.   for ly=1,#behind do
  466.     local lx=x
  467.     for i=1,#behind[ly] do
  468.       local str,fg,bg=table.unpack(behind[ly][i])
  469.       renderTarget.setForeground(fg)
  470.       renderTarget.setBackground(bg)
  471.       renderTarget.set(lx,ly+y-1,str)
  472.       lx=lx+unicode.len(str)
  473.     end
  474.   end
  475.  
  476.  
  477.   term.setCursor(curx,cury)
  478.   renderTarget.setForeground(pfg)
  479.   renderTarget.setBackground(pbg)
  480.   renderTarget.flush()
  481.  
  482.   term.setCursorBlink(pcb)
  483.  
  484. end
  485.  
  486. local function elementHide(element)
  487.   if element.visible then
  488.     element.visible=false
  489.     element.gui:redrawRect(element.posX,element.posY,element.width,1)
  490.   end
  491.   element.hidden=true
  492. end
  493.  
  494. local function elementShow(element)
  495.   element.hidden=false
  496.   if not element.visible then
  497.     element:draw()
  498.   end
  499. end
  500.  
  501.  
  502. local function drawLabel(label)
  503.   if not label:isHidden() then
  504.     local screenX,screenY=label:getScreenPosition()
  505.     local fg, bg=findStyleProperties(label,"text-color","text-background")
  506.     label.renderTarget.setForeground(fg)
  507.     label.renderTarget.setBackground(bg)
  508.     label.renderTarget.set(screenX,screenY, unicode.sub(label.text, 1,label.width) .. (" "):rep(label.width-unicode.len(label.text)))
  509.     label.visible=true
  510.   end
  511. end
  512.  
  513.  
  514.  
  515. local function drawButton(button)
  516.   if not button:isHidden() then
  517.     local styles=getAppliedStyles(button)
  518.     local gpu=button.renderTarget
  519.  
  520.     local fg,bg,
  521.           fillFG,fillBG,fillCh=
  522.       findStyleProperties(button,
  523.         "text-color","text-background",
  524.         "fill-color-fg","fill-color-bg","fill-ch")
  525.  
  526.     local bodyX,bodyY,bodyW,bodyH=drawBorder(button,styles)
  527.  
  528.     gpu.setBackground(fillBG)
  529.     gpu.setForeground(fillFG)
  530.     local bodyRow=fillCh:rep(bodyW)
  531.     for i=1,bodyH do
  532.       gpu.set(bodyX,bodyY+i-1,bodyRow)
  533.     end
  534.  
  535.     --now center the label
  536.     gpu.setBackground(bg)
  537.     gpu.setForeground(fg)
  538.     --calc position
  539.     local text=button.text
  540.     local textX=bodyX
  541.     local textY=bodyY+math.floor((bodyH-1)/2)
  542.     if unicode.len(text)>bodyW then
  543.       text=unicode.sub(text, 1,bodyW)
  544.     else
  545.       textX=bodyX+math.floor((bodyW-unicode.len(text))/2)
  546.     end
  547.     gpu.set(textX,textY,text)
  548.   end
  549. end
  550.  
  551.  
  552. local function drawTextField(tf)
  553.   if not tf:isHidden() then
  554.     local textFG,textBG,selectedFG,selectedBG=
  555.         findStyleProperties(tf,"text-color","text-background","selected-color","selected-background")
  556.     local screenX,screenY=tf:getScreenPosition()
  557.     local gpu=tf.renderTarget
  558.  
  559.     --grab the subset of text visible
  560.     local text=tf.text
  561.  
  562.     local visibleText=unicode.sub(text, tf.scrollIndex,tf.scrollIndex+tf.width-1)
  563.     visibleText=visibleText..(" "):rep(tf.width-unicode.len(visibleText))
  564.     --this may be split into as many as 3 parts - pre-selection, selection, and post-selection
  565.     --if there is any selection at all...
  566.     if tf.state=="focus" and not tf.dragging then
  567.       term.setCursorBlink(false)
  568.     end
  569.     if tf.selectEnd~=0 then
  570.       local visSelStart, visSelEnd, preSelText,selText,postSelText
  571.       visSelStart=math.max(1,tf.selectStart-tf.scrollIndex+1)
  572.       visSelEnd=math.min(tf.width,tf.selectEnd-tf.scrollIndex+1)
  573.  
  574.       selText=unicode.sub(visibleText, visSelStart,visSelEnd)
  575.  
  576.       if visSelStart>1 then
  577.         preSelText=unicode.sub(visibleText, 1,visSelStart-1)
  578.       end
  579.  
  580.       if visSelEnd<tf.width then
  581.         postSelText=unicode.sub(visibleText, visSelEnd+1,tf.width)
  582.       end
  583.  
  584.       gpu.setForeground(selectedFG)
  585.       gpu.setBackground(selectedBG)
  586.       gpu.set(screenX+visSelStart-1,screenY,selText)
  587.  
  588.       if preSelText or postSelText then
  589.         gpu.setForeground(textFG)
  590.         gpu.setBackground(textBG)
  591.         if preSelText then
  592.           gpu.set(screenX,screenY,preSelText)
  593.         end
  594.         if postSelText then
  595.           gpu.set(screenX+visSelEnd,screenY,postSelText)
  596.         end
  597.       end
  598.     else
  599.       --no selection, just draw
  600.       gpu.setForeground(textFG)
  601.       gpu.setBackground(textBG)
  602.       gpu.set(screenX,screenY,visibleText)
  603.     end
  604.     if tf.state=="focus" and not tf.dragging then
  605.       term.setCursor(screenX+tf.cursorIndex-tf.scrollIndex,screenY)
  606.       term.setCursorBlink(true)
  607.     end
  608.   end
  609. end
  610.  
  611.  
  612. local function drawScrollBarH(bar)
  613.   if not bar:isHidden() then
  614.     local leftCh,rightCh,btnFG,btnBG,
  615.           barCh, barFG, barBG,
  616.           gripCh, gripFG, gripBG =
  617.       findStyleProperties(bar,
  618.           "button-ch-left","button-ch-right","button-color-fg","button-color-bg",
  619.           "bar-ch","bar-color-fg","bar-color-bg",
  620.           "grip-ch-h","grip-color-fg","grip-color-bg")
  621.  
  622.     local gpu=bar.renderTarget
  623.     local screenX,screenY=bar:getScreenPosition()
  624.  
  625.     local w,gs,ge=bar.width,bar.gripStart+screenX,bar.gripEnd+screenX
  626.     --buttons
  627.     gpu.setBackground(btnBG)
  628.     gpu.setForeground(btnFG)
  629.     gpu.set(screenX,screenY,leftCh)
  630.     gpu.set(screenX+w-1,screenY,rightCh)
  631.  
  632.     --scroll area
  633.     gpu.setBackground(barBG)
  634.     gpu.setForeground(barFG)
  635.  
  636.     gpu.set(screenX+1,screenY,barCh:rep(w-2))
  637.  
  638.     --grip
  639.     gpu.setBackground(gripBG)
  640.     gpu.setForeground(gripFG)
  641.     gpu.set(gs,screenY,gripCh:rep(ge-gs+1))
  642.   end
  643. end
  644.  
  645. local function drawScrollBarV(bar)
  646.   if not bar:isHidden() then
  647.     local upCh,dnCh,btnFG,btnBG,
  648.           barCh, barFG, barBG,
  649.           gripCh, gripFG, gripBG =
  650.       findStyleProperties(bar,
  651.           "button-ch-up","button-ch-down","button-color-fg","button-color-bg",
  652.           "bar-ch","bar-color-fg","bar-color-bg",
  653.           "grip-ch-v","grip-color-fg","grip-color-bg")
  654.  
  655.     local gpu=bar.renderTarget
  656.     local screenX,screenY=bar:getScreenPosition()
  657.     local h,gs,ge=bar.height,bar.gripStart+screenY,bar.gripEnd+screenY
  658.     --buttons
  659.     gpu.setBackground(btnBG)
  660.     gpu.setForeground(btnFG)
  661.     gpu.set(screenX,screenY,upCh)
  662.     gpu.set(screenX,screenY+h-1,dnCh)
  663.  
  664.     --scroll area
  665.     gpu.setBackground(barBG)
  666.     gpu.setForeground(barFG)
  667.  
  668.     for screenY=screenY+1,gs-1 do
  669.       gpu.set(screenX,screenY,barCh)
  670.     end
  671.     for screenY=ge+1,screenY+h-2 do
  672.       gpu.set(screenX,screenY,barCh)
  673.     end
  674.  
  675.     --grip
  676.     gpu.setBackground(gripBG)
  677.     gpu.setForeground(gripFG)
  678.     for screenY=gs,ge do
  679.       gpu.set(screenX,screenY,gripCh)
  680.     end
  681.   end
  682. end
  683.  
  684.  
  685. --**********************
  686. --object creation functions and their utility functions
  687.  
  688. local function loadHandlers(gui)
  689.   local handlers=gui.handlers
  690.   for i=1,#handlers do
  691.     event.listen(handlers[i][1],handlers[i][2])
  692.   end
  693. end
  694.  
  695. local function unloadHandlers(gui)
  696.   local handlers=gui.handlers
  697.   for i=1,#handlers do
  698.     event.ignore(handlers[i][1],handlers[i][2])
  699.   end
  700. end
  701.  
  702. local function guiAddHandler(gui,eventType,func)
  703.   checkArg(1,gui,"table")
  704.   checkArg(2,eventType,"string")
  705.   checkArg(3,func,"function")
  706.  
  707.   gui.handlers[#gui.handlers+1]={eventType,func}
  708.   if gui.running then
  709.     event.listen(eventType,func)
  710.   end
  711. end
  712.  
  713.  
  714. local function cleanup(gui)
  715.   --remove handlers
  716.   unloadHandlers(gui)
  717.  
  718.   --hide gui, redraw beneath?
  719.   if gui.prevTermState then
  720.     restoreFrame(gui.renderTarget,gui.posX,gui.posY,gui.prevTermState)
  721.     gui.prevTermState=nil
  722.   end
  723. end
  724.  
  725. local function contains(element,x,y)
  726.   local ex,ey,ew,eh=element.posX,element.posY,element.width,element.height
  727.  
  728.   return x>=ex and x<=ex+ew-1 and y>=ey and y<=ey+eh-1
  729. end
  730.  
  731. local function runGui(gui)
  732.   gui.running=true
  733.   --draw gui background, preserving underlying screen
  734.   gui.prevTermState=frameAndSave(gui)
  735.   gui.hidden=false
  736.  
  737.   --drawing components
  738.   local firstFocusable, prevFocusable
  739.   for i=1,#gui.components do
  740.     if not gui.components[i].hidden then
  741.       if gui.components[i].focusable and not gui.components[i].hidden then
  742.         if firstFocusable==nil then
  743.           firstFocusable=gui.components[i]
  744.         else
  745.           gui.components[i].tabPrev=prevFocusable
  746.           prevFocusable.tabNext=gui.components[i]
  747.         end
  748.         prevFocusable=gui.components[i]
  749.       end
  750.       gui.components[i]:draw()
  751.     end
  752.   end
  753.   if firstFocusable then
  754.     firstFocusable.tabPrev=prevFocusable
  755.     prevFocusable.tabNext=firstFocusable
  756.     if not gui.focusElement and not gui.components[i].hidden then
  757.       gui.focusElement=gui.components[i]
  758.       gui.focusElement.state="focus"
  759.     end
  760.   end
  761.   if gui.focusElement and gui.focusElement.gotFocus then
  762.     gui.focusElement.gotFocus()
  763.   end
  764.  
  765.   loadHandlers(gui)
  766.  
  767.   --run the gui's onRun, if any
  768.   if gui.onRun then
  769.     gui.onRun()
  770.   end
  771.  
  772.   local function getComponentAt(tx,ty)
  773.     for i=1,#gui.components do
  774.       local c=gui.components[i]
  775.       if not c:isHidden() and c:contains(tx,ty) then
  776.         return c
  777.       end
  778.     end
  779.   end
  780.  
  781.   local lastClickTime, lastClickPos, lastClickButton, dragButton, dragging=0,{0,0},nil,nil,false
  782.   local draggingObj=nil
  783.  
  784.   while true do
  785.     gui.renderTarget:flush()
  786.     local e={event.pull()}
  787.     if e[1]=="gui_close" then
  788.       break
  789.     elseif e[1]=="touch" then
  790.       --figure out what was touched!
  791.       local tx, ty, button=e[3],e[4],e[5]
  792.       if gui:contains(tx,ty) then
  793.         tx=tx-gui.bodyX+1
  794.         ty=ty-gui.bodyY+1
  795.         lastClickPos={tx,ty}
  796.         local tickTime=computer.uptime()
  797.         dragButton=button
  798.         local target=getComponentAt(tx,ty)
  799.         clickedOn=target
  800.         if target then
  801.           if target.focusable and target~=gui.focusElement then
  802.             gui:changeFocusTo(clickedOn)
  803.           end
  804.           if lastClickPos[1]==tx and lastClickPos[2]==ty and lastClickButton==button and
  805.               tickTime - lastClickTime<doubleClickThreshold then
  806.             if target.onDoubleClick then
  807.               target:onDoubleClick(tx-target.posX+1,ty-target.posY+1,button)
  808.             end
  809.           elseif target.onClick then
  810.             target:onClick(tx-target.posX+1,ty-target.posY+1,button)
  811.           end
  812.         end
  813.         lastClickTime=tickTime
  814.         lastClickButton=button
  815.       end
  816.     elseif e[1]=="drag" then
  817.       --if we didn't click /on/ something to start this drag, we do nada
  818.       if clickedOn then
  819.         local tx,ty=e[3],e[4]
  820.         tx=tx-gui.bodyX+1
  821.         ty=ty-gui.bodyY+1
  822.         --is this is the beginning of a drag?
  823.         if not dragging then
  824.           if clickedOn.onBeginDrag then
  825.             draggingObj=clickedOn:onBeginDrag(lastClickPos[1]-clickedOn.posX+1,lastClickPos[2]-clickedOn.posY+1,dragButton)
  826.             dragging=true
  827.           end
  828.         end
  829.         --now do the actual drag bit
  830.         --draggingObj is for drag proxies, which are for drag and drop operations like moving files
  831.         if draggingObj and draggingObj.onDrag then
  832.           draggingObj:onDrag(tx,ty)
  833.         end
  834.         --
  835.         if clickedOn and clickedOn.onDrag then
  836.           tx,ty=tx-clickedOn.posX+1,ty-clickedOn.posY+1
  837.           clickedOn:onDrag(tx,ty)
  838.         end
  839.       end
  840.     elseif e[1]=="drop" then
  841.       local tx,ty=e[3],e[4]
  842.       tx=tx-gui.bodyX+1
  843.       ty=ty-gui.bodyY+1
  844.       if draggingObj and draggingObj.onDrop then
  845.         local dropOver=getComponentAt(tx,ty)
  846.         draggingObj:onDrop(tx,ty,dropOver)
  847.       end
  848.       if clickedOn and clickedOn.onDrop then
  849.         tx,ty=tx-clickedOn.posX+1,ty-clickedOn.posY+1
  850.         clickedOn:onDrop(tx,ty,dropOver)
  851.       end
  852.       draggingObj=nil
  853.       dragging=false
  854.  
  855.     elseif e[1]=="key_down" then
  856.       local char,code=e[3],e[4]
  857.       --tab
  858.       if code==15 and gui.focusElement then
  859.         local newFocus=gui.focusElement
  860.         if keyboard.isShiftDown() then
  861.           repeat
  862.             newFocus=newFocus.tabPrev
  863.           until newFocus.hidden==false
  864.         else
  865.           repeat
  866.             newFocus=newFocus.tabNext
  867.           until newFocus.hidden==false
  868.         end
  869.         if newFocus~=gui.focusElement then
  870.           gui:changeFocusTo(newFocus)
  871.         end
  872.       elseif char==3 then
  873.         --copy!
  874.         if gui.focusElement and gui.focusElement.doCopy then
  875.           clipboard=gui.focusElement:doCopy() or clipboard
  876.         end
  877.       elseif char==22 then
  878.         --paste!
  879.         if gui.focusElement.doPaste and type(clipboard)=="string" then
  880.           gui.focusElement:doPaste(clipboard)
  881.         end
  882.       elseif char==24 then
  883.         --cut!
  884.         if gui.focusElement.doCut then
  885.           clipboard=gui.focusElement:doCut() or clipboard
  886.         end
  887.       elseif gui.focusElement and gui.focusElement.keyHandler then
  888.         gui.focusElement:keyHandler(char,code)
  889.       end
  890.      
  891.       if gui.focusElement and gui.focusElement.onKey then
  892.         gui.focusElement.onKey(char,code)
  893.       end
  894.     end
  895.   end
  896.  
  897.   running=false
  898.  
  899.   cleanup(gui)
  900.  
  901.   if gui.onExit then
  902.     gui.onExit()
  903.   end
  904. end
  905.  
  906. local function baseComponent(gui,x,y,width,height,type,focusable)
  907.   local c={
  908.       visible=false,
  909.       hidden=false,
  910.       gui=gui,
  911.       style=gui.style,
  912.       focusable=focusable,
  913.       type=type,
  914.       renderTarget=gui.renderTarget,
  915.     }
  916.  
  917.   c.isHidden=function(c)
  918.      return c.hidden or c.gui:isHidden()
  919.     end
  920.  
  921.   c.posX, c.posY, c.width, c.height =
  922.     parsePosition(x, y, width, height, gui.bodyW, gui.bodyH)
  923.  
  924.   c.getScreenPosition=function(element)
  925.       local e=element
  926.       local x,y=e.posX,e.posY
  927.       while e.gui and e.gui~=screen do
  928.         e=e.gui
  929.         x=x+e.bodyX-1
  930.         y=y+e.bodyY-1
  931.       end
  932.       return x,y
  933.     end
  934.  
  935.   c.hide=elementHide
  936.   c.show=elementShow
  937.   c.contains=contains
  938.  
  939.   return c
  940. end
  941.  
  942.  
  943. local function addLabel(gui,x,y,width,labelText)
  944.   local label=baseComponent(gui,x,y,width,1,"label",false)
  945.  
  946.   label.text=labelText
  947.  
  948.   label.draw=drawLabel
  949.  
  950.   gui:addComponent(label)
  951.   return label
  952. end
  953.  
  954. local function addButton(gui,x,y,width,height,buttonText,onClick)
  955.   local button=baseComponent(gui,x,y,width,height,"button",true)
  956.  
  957.   button.text=buttonText
  958.   button.onClick=onClick
  959.  
  960.   button.draw=drawButton
  961.   button.keyHandler=function(button,char,code)
  962.       if code==28 then
  963.          button:onClick(0,0,-1)
  964.       end
  965.     end
  966.   gui:addComponent(button)
  967.   return button
  968. end
  969.  
  970. local function updateSelect(tf, prevCI )
  971.   if tf.selectEnd==0 then
  972.     --begin selecting
  973.     tf.selectOrigin=prevCI
  974.   end
  975.   if tf.cursorIndex==tf.selectOrigin then
  976.     tf.selectEnd=0
  977.   elseif tf.cursorIndex>tf.selectOrigin then
  978.     tf.selectStart=tf.selectOrigin
  979.     tf.selectEnd=tf.cursorIndex-1
  980.   else
  981.     tf.selectStart=tf.cursorIndex
  982.     tf.selectEnd=tf.selectOrigin-1
  983.   end
  984. end
  985.  
  986. local function removeSelectedTF(tf)
  987.   tf.text=unicode.sub(tf.text, 1,tf.selectStart-1) .. unicode.sub(tf.text, tf.selectEnd+1)
  988.   tf.cursorIndex=tf.selectStart
  989.   tf.selectEnd=0
  990. end
  991.  
  992. local function insertTextTF(tf,text)
  993.   if tf.selectEnd~=0 then
  994.     tf:removeSelected()
  995.   end
  996.   tf.text=unicode.sub(tf.text, 1,tf.cursorIndex-1)..text..unicode.sub(tf.text, tf.cursorIndex)
  997.   tf.cursorIndex=tf.cursorIndex+unicode.len(text)
  998.   if tf.cursorIndex-tf.scrollIndex+1>tf.width then
  999.     local ts=tf.scrollIndex+math.floor(tf.width/3)
  1000.     if tf.cursorIndex-ts+1>tf.width then
  1001.       ts=tf.cursorIndex-tf.width+math.floor(tf.width/3)
  1002.     end
  1003.     tf.scrollIndex=ts
  1004.   end
  1005. end
  1006.  
  1007. local function addTextField(gui,x,y,width,text)
  1008.   local tf=baseComponent(gui,x,y,width,1,"textfield",true)
  1009.  
  1010.   tf.text=text or ""
  1011.   tf.cursorIndex=1
  1012.   tf.scrollIndex=1
  1013.   tf.selectStart=1
  1014.   tf.selectEnd=0
  1015.   tf.draw=drawTextField
  1016.   tf.insertText=insertTextTF
  1017.   tf.removeSelected=removeSelectedTF
  1018.  
  1019.   tf.doPaste=function(tf,text)
  1020.       tf:insertText(text)
  1021.       tf:draw()
  1022.     end
  1023.   tf.doCopy=function(tf)
  1024.       if tf.selectEnd~=0 then
  1025.         return unicode.sub(tf.text, tf.selectStart,tf.selectEnd)
  1026.       end
  1027.       return nil
  1028.     end
  1029.   tf.doCut=function(tf)
  1030.       local text=tf:doCopy()
  1031.       tf:removeSelected()
  1032.       tf:draw()
  1033.       return text
  1034.     end
  1035.  
  1036.   tf.onClick=function(tf,tx,ty,button)
  1037.       tf.selectEnd=0
  1038.       tf.cursorIndex=math.min(tx+tf.scrollIndex-1,unicode.len(tf.text)+1)
  1039.       tf:draw()
  1040.     end
  1041.  
  1042.   tf.onBeginDrag=function(tf,tx,ty,button)
  1043.       --drag events are in gui coords, not component, so correct
  1044.       if button==0 then
  1045.         tf.selectOrigin=math.min(tx+tf.scrollIndex,unicode.len(tf.text)+1)
  1046.         tf.dragging=tf.selectOrigin
  1047.         term.setCursorBlink(false)
  1048.  
  1049.       end
  1050.     end
  1051.  
  1052.   tf.onDrag=function(tf,tx,ty)
  1053.       if tf.dragging then
  1054.         local dragX=tx
  1055.         local prevCI=tf.cursorIndex
  1056.         tf.cursorIndex=math.max(math.min(dragX+tf.scrollIndex-1,unicode.len(tf.text)+1),1)
  1057.         if prevCI~=cursorIndex then
  1058.           updateSelect(tf,tf.selectOrigin)
  1059.           tf:draw()
  1060.         end
  1061.         if dragX<1 or dragX>tf.width then
  1062.           --it's dragging outside.
  1063.           local dragMagnitude=dragX-1
  1064.           if dragMagnitude>=0 then
  1065.             dragMagnitude=dragX-tf.width
  1066.           end
  1067.           local dragDir=dragMagnitude<0 and -1 or 1
  1068.           dragMagnitude=math.abs(dragMagnitude)
  1069.           local dragStep, dragRate
  1070.           if dragMagnitude>5 then
  1071.             dragRate=.1
  1072.             dragStep=dragMagnitude/5*dragDir
  1073.           else
  1074.             dragRate=(6-dragMagnitude)/10
  1075.             dragStep=dragDir
  1076.           end
  1077.           if tf.dragTimer then
  1078.             event.cancel(tf.dragTimer)
  1079.           end
  1080.           tf.dragTimer=event.timer(dragRate,function()
  1081.               assert(tf.gui.running)
  1082.                 tf.cursorIndex=math.max(math.min(tf.cursorIndex+dragStep,unicode.len(tf.text)+1),1)
  1083.               if tf.cursorIndex<tf.scrollIndex then
  1084.                 tf.scrollIndex=tf.cursorIndex
  1085.               elseif tf.cursorIndex>tf.scrollIndex+tf.width-2 then
  1086.                 tf.scrollIndex=tf.cursorIndex-tf.width+1
  1087.               end
  1088.               updateSelect(tf,tf.selectOrigin)
  1089.               tf:draw()
  1090.             end, math.huge)
  1091.         else
  1092.           if tf.dragTimer then
  1093.             event.cancel(tf.dragTimer)
  1094.           end
  1095.         end
  1096.  
  1097.       end
  1098.     end
  1099.  
  1100.   tf.onDrop=function(tf)
  1101.     if tf.dragging then
  1102.       tf.dragging=nil
  1103.       if tf.dragTimer then
  1104.         event.cancel(tf.dragTimer)
  1105.       end
  1106.       local screenX,screenY=tf:getScreenPosition()
  1107.       term.setCursor(screenX+tf.cursorIndex-tf.scrollIndex,screenY)
  1108.       term.setCursorBlink(true)
  1109.     end
  1110.   end
  1111.  
  1112.   tf.keyHandler=function(tfclear,char,code)
  1113.       local screenX,screenY=tf:getScreenPosition()
  1114.       local dirty=false
  1115.       if not keyboard.isControl(char) then
  1116.         tf:insertText(unicode.char(char))
  1117.         dirty=true
  1118.       elseif code==28 and tf.tabNext then
  1119.         gui:changeFocusTo(tf.tabNext)
  1120.       elseif code==keyboard.keys.left then
  1121.         local prevCI=tf.cursorIndex
  1122.         if tf.cursorIndex>1 then
  1123.           tf.cursorIndex=tf.cursorIndex-1
  1124.           if tf.cursorIndex<tf.scrollIndex then
  1125.             tf.scrollIndex=math.max(1,tf.scrollIndex-math.floor(tf.width/3))
  1126.             dirty=true
  1127.           else
  1128.             term.setCursor(screenX+tf.cursorIndex-tf.scrollIndex,screenY)
  1129.           end
  1130.           term.setCursor(screenX+tf.cursorIndex-tf.scrollIndex,screenY)
  1131.         end
  1132.         if keyboard.isShiftDown() then
  1133.           updateSelect(tf,prevCI)
  1134.           dirty=true
  1135.         elseif tf.selectEnd~=0 then
  1136.           tf.selectEnd=0
  1137.           dirty=true
  1138.         end
  1139.       elseif code==keyboard.keys.right then
  1140.         local prevCI=tf.cursorIndex
  1141.         if tf.cursorIndex<unicode.len(tf.text)+1 then
  1142.           tf.cursorIndex=tf.cursorIndex+1
  1143.  
  1144.           if tf.cursorIndex>=tf.scrollIndex+tf.width then
  1145.             tf.scrollIndex=tf.scrollIndex+math.floor(tf.width/3)
  1146.             dirty=true
  1147.           else
  1148.             term.setCursor(screenX+tf.cursorIndex-tf.scrollIndex,screenY)
  1149.           end
  1150.         end
  1151.         if keyboard.isShiftDown() then
  1152.           updateSelect(tf,prevCI)
  1153.           dirty=true
  1154.         elseif tf.selectEnd~=0 then
  1155.           tf.selectEnd=0
  1156.           dirty=true
  1157.         end
  1158.       elseif code==keyboard.keys.home then
  1159.         local prevCI=tf.cursorIndex
  1160.         if tf.cursorIndex~=1 then
  1161.           tf.cursorIndex=1
  1162.           if tf.scrollIndex~=1 then
  1163.             tf.scrollIndex=1
  1164.             dirty=true
  1165.           else
  1166.             term.setCursor(screenX+tf.cursorIndex-tf.scrollIndex,screenY)
  1167.           end
  1168.         end
  1169.         if keyboard.isShiftDown() then
  1170.           updateSelect(tf,prevCI)
  1171.           dirty=true
  1172.         elseif tf.selectEnd~=0 then
  1173.           tf.selectEnd=0
  1174.           dirty=true
  1175.         end
  1176.       elseif code==keyboard.keys["end"] then
  1177.         local prevCI=tf.cursorIndex
  1178.         if tf.cursorIndex~=unicode.len(tf.text)+1 then
  1179.           tf.cursorIndex=unicode.len(tf.text)+1
  1180.           if tf.scrollIndex+tf.width-1<=tf.cursorIndex then
  1181.             tf.scrollIndex=tf.cursorIndex-tf.width+1
  1182.             dirty=true
  1183.           else
  1184.             term.setCursor(screenX+tf.cursorIndex-tf.scrollIndex,screenY)
  1185.           end
  1186.         end
  1187.         if keyboard.isShiftDown() then
  1188.           updateSelect(tf,prevCI)
  1189.           dirty=true
  1190.         elseif tf.selectEnd~=0 then
  1191.           tf.selectEnd=0
  1192.           dirty=true
  1193.         end
  1194.       elseif code==keyboard.keys.back then
  1195.         if tf.selectEnd~=0 then
  1196.           tf:removeSelected()
  1197.           dirty=true
  1198.         elseif tf.cursorIndex>1 then
  1199.           tf.text=unicode.sub(tf.text,1,tf.cursorIndex-2)..unicode.sub(tf.text,tf.cursorIndex)
  1200.           tf.cursorIndex=tf.cursorIndex-1
  1201.           if tf.cursorIndex<tf.scrollIndex then
  1202.             tf.scrollIndex=math.max(1,tf.scrollIndex-math.floor(tf.width/3))
  1203.           end
  1204.           dirty=true
  1205.         end
  1206.       elseif code==keyboard.keys.delete then
  1207.         if tf.selectEnd~=0 then
  1208.           tf:removeSelected()
  1209.           dirty=true
  1210.         elseif tf.cursorIndex<=unicode.len(tf.text) then
  1211.           tf.text=unicode.sub(tf.text,1,tf.cursorIndex-1)..unicode.sub(tf.text,tf.cursorIndex+1)
  1212.           dirty=true
  1213.         end
  1214.       end
  1215.       if dirty then
  1216.         tf:draw()
  1217.       end
  1218.     end
  1219.  
  1220.  
  1221.   tf.gotFocus=function()
  1222.     --we may want to scroll here, cursor to end of text on gaining focus
  1223.     local effText=tf.text
  1224.  
  1225.     if unicode.len(effText)>tf.width then
  1226.       tf.scrollIndex=unicode.len(effText)-tf.width+3
  1227.     else
  1228.       tf.scrollIndex=1
  1229.     end
  1230.     tf.cursorIndex=unicode.len(effText)+1
  1231.     tf:draw()
  1232.   end
  1233.  
  1234.   tf.lostFocus=function()
  1235.     tf.scrollIndex=1
  1236.     tf.selectEnd=0
  1237.     term.setCursorBlink(false)
  1238.     tf:draw()
  1239.   end
  1240.  
  1241.   gui:addComponent(tf)
  1242.   return tf
  1243. end
  1244.  
  1245. local function updateScrollBarGrip(sb)
  1246.   local gripStart,gripEnd
  1247.   local pos,max,length=sb.scrollPos,sb.scrollMax,sb.length
  1248.  
  1249.   --grip size
  1250.   -- gripSize / height-2 == height / scrollMax
  1251.   local gripSize=math.max(1,math.min(math.floor(math.min(1,length / (max+length-2)) * (length-2)),length-2))
  1252.   if gripSize==length-2 then
  1253.     --grip fills everything
  1254.     sb.gripStart=1
  1255.     sb.gripEnd=length-2
  1256.   else
  1257.     --grip position
  1258.     pos=round((pos-1)/(max-1)*(length-2-gripSize))+1
  1259.  
  1260.     --from pos and size, figure gripStart and gripEnd
  1261.     sb.gripStart=pos
  1262.     sb.gripEnd=pos+gripSize-1
  1263.   end
  1264.  
  1265. end
  1266.  
  1267.  
  1268. local function scrollBarBase(gui,x,y,width,height,scrollMax,onScroll)
  1269.   local sb=baseComponent(gui,x,y,width,height,"scrollbar",false)
  1270.   sb.scrollMax=scrollMax or 1
  1271.   sb.scrollPos=1
  1272.   sb.length=math.max(sb.width,sb.height)
  1273.   assert(sb.length>2,"Scroll bars must be at least 3 long.")
  1274.  
  1275.   sb.onScroll=onScroll
  1276.  
  1277.  
  1278.   updateScrollBarGrip(sb)
  1279.  
  1280.   sb._onClick=function(sb,tpos,button)
  1281.       local newPos=sb.scrollPos
  1282.       if tpos==1 then
  1283.         --up button
  1284.         newPos=math.max(1,sb.scrollPos-1)
  1285.       elseif tpos==sb.length then
  1286.         newPos=math.min(sb.scrollMax,sb.scrollPos+1)
  1287.       elseif tpos<sb.gripStart then
  1288.         --before grip, scroll up a page
  1289.         newPos=math.max(1,sb.scrollPos-sb.length+1)
  1290.       elseif tpos>sb.gripEnd then
  1291.         --before grip, scroll up a page
  1292.         newPos=math.min(sb.scrollMax,sb.scrollPos+sb.length-1)
  1293.       end
  1294.       if newPos~=sb.scrollPos then
  1295.         sb.scrollPos=newPos
  1296.         updateScrollBarGrip(sb)
  1297.         sb:draw()
  1298.         if sb.onScroll then
  1299.           sb:onScroll(sb.scrollPos)
  1300.         end
  1301.       end
  1302.     end
  1303.  
  1304.   sb._onBeginDrag=function(sb,tpos,button)
  1305.       if button==0 and sb.length>3 and (sb.length/sb.scrollMax<1) then
  1306.         sb.dragging=true
  1307.         sb.lastDragPos=tpos
  1308.       end
  1309.     end
  1310.  
  1311.   sb._onDrag=function(sb,tpos)
  1312.       if sb.dragging then
  1313.         local py=sb.lastDragPos
  1314.         local dif=tpos-py
  1315.         if dif~=0 then
  1316.           --calc the grip position for this y position
  1317.           --first clamp to range of scroll area
  1318.           local scroll=math.min(math.max(tpos,2),sb.length-1)-2
  1319.           --scale to 0-1
  1320.           scroll=scroll/(sb.length-3)
  1321.           --scale to maxScroll
  1322.           scroll=round(scroll*(sb.scrollMax-1)+1)
  1323.           --see if this is different from our current scroll position
  1324.           if scroll~=sb.scrollPos then
  1325.             --it is. We actually scrolled, then.
  1326.             sb.scrollPos=scroll
  1327.             updateScrollBarGrip(sb)
  1328.             sb:draw()
  1329.             if onScroll then
  1330.               sb:onScroll()
  1331.             end
  1332.           end
  1333.         end
  1334.       end
  1335.     end
  1336.  
  1337.   sb.onDrop=function(sb)
  1338.       sb.dragging=false
  1339.     end
  1340.  
  1341.   return sb
  1342. end
  1343.  
  1344. local function addScrollBarV(gui,x,y,height,scrollMax, onScroll)
  1345.   local sb=scrollBarBase(gui,x,y,1,height,scrollMax,onScroll)
  1346.  
  1347.   sb.draw=drawScrollBarV
  1348.  
  1349.   sb.onClick=function(sb,tx,ty,button) sb:_onClick(ty,button) end
  1350.   sb.onBeginDrag=function(sb,tx,ty,button) sb:_onBeginDrag(ty,button) end
  1351.   sb.onDrag=function(sb,tx,ty,button) sb:_onDrag(ty,button) end
  1352.  
  1353.   gui:addComponent(sb)
  1354.   return sb
  1355. end
  1356.  
  1357. local function addScrollBarH(gui,x,y,width,scrollMax,onScroll)
  1358.  
  1359.   local sb=scrollBarBase(gui,x,y,width,1,scrollMax,onScroll)
  1360.  
  1361.   sb.draw=drawScrollBarH
  1362.  
  1363.   sb.onClick=function(sb,tx,ty,button) sb:_onClick(tx,button) end
  1364.   sb.onBeginDrag=function(sb,tx,ty,button) sb:_onBeginDrag(tx,button) end
  1365.   sb.onDrag=function(sb,tx,ty,button) sb:_onDrag(tx,button) end
  1366.  
  1367.   gui:addComponent(sb)
  1368.   return sb
  1369. end
  1370.  
  1371.  
  1372. local function compositeBase(gui,x,y,width,height,objType,focusable)
  1373.   local comp=baseComponent(gui,x,y,width,height,objType,focusable)
  1374.   comp.bodyX,comp.bodyY,comp.bodyW,comp.bodyH=calcBody(comp)
  1375.  
  1376.   comp.components={}
  1377.  
  1378.   function comp.addComponent(obj,component)
  1379.     obj.components[#obj.components+1]=component
  1380.   end
  1381.  
  1382.   return comp
  1383. end
  1384.  
  1385. local function scrollListBox(sb)
  1386.   local lb=sb.listBox
  1387.  
  1388.   for i=1,#lb.labels do
  1389.     local listI=sb.scrollPos+i-1
  1390.     local l=lb.labels[i]
  1391.     if listI<=#lb.list then
  1392.       l.state=lb.selectedLabel==listI and "selected" or nil
  1393.       l.text=lb.list[listI]
  1394.     else
  1395.       l.state=nil
  1396.       l.text=""
  1397.     end
  1398.     l:draw()
  1399.   end
  1400. end
  1401.  
  1402.  
  1403. local function clickListBox(lb,tx,ty,button)
  1404.   if tx==lb.width then
  1405.     lb.scrollBar:_onClick(ty,button)
  1406.   else
  1407.     tx,ty=correctForBorder(lb,tx,ty)
  1408.     if ty>=1 and ty<=lb.bodyH then
  1409.       --ty is now index of the label clicked on
  1410.       --but is it valid?
  1411.       if ty<=#lb.list then
  1412.         lb:select(ty+lb.scrollBar.scrollPos-1)
  1413.       end
  1414.     end
  1415.   end
  1416.  
  1417. end
  1418.  
  1419. local function listBoxSelect(lb,index)
  1420.   if index<1 or index>#lb.list then
  1421.     error("index out of range to listBoxSelect",2)
  1422.   end
  1423.   local prevSelected=lb.selectedLabel
  1424.   if index==prevSelected then
  1425.     return
  1426.   end
  1427.  
  1428.   lb.selectedLabel=index
  1429.   --do I need to scroll?
  1430.   local scrolled=false
  1431.   local scrollIndex=lb.scrollBar.scrollPos
  1432.   if index<scrollIndex then
  1433.     scrollIndex=index
  1434.     scrolled=true
  1435.   elseif index>scrollIndex+lb.bodyH-1 then
  1436.     scrollIndex=index-lb.bodyH+1
  1437.     scrolled=true
  1438.   end
  1439.   if scrolled then
  1440.     --update scroll position
  1441.     lb.scrollBar.scrollPos=scrollIndex
  1442.     scrollListBox(lb.scrollBar)
  1443.   else
  1444.     if prevSelected>=scrollIndex and prevSelected<=scrollIndex+lb.bodyH-1 then
  1445.       local pl=lb.labels[prevSelected-scrollIndex+1]
  1446.       pl.state=nil
  1447.       pl:draw()
  1448.     end
  1449.     local l=lb.labels[index-scrollIndex+1]
  1450.     l.state="selected"
  1451.     l:draw()
  1452.   end
  1453.  
  1454.   if lb.onChange then
  1455.     lb:onChange(prevSelected,index)
  1456.   end
  1457. end
  1458.  
  1459.  
  1460. local function getListBoxSelected(lb)
  1461.   return lb.list[lb.selectedLabel]
  1462. end
  1463.  
  1464. local function updateListBoxList(lb,newList)
  1465.   lb.list=newList
  1466.   lb.scrollBar.scrollPos=1
  1467.   lb.scrollBar.scrollMax=math.max(1,#newList-lb.bodyH+1)
  1468.   updateScrollBarGrip(lb.scrollBar)
  1469.   lb.selectedLabel=1
  1470.   scrollListBox(lb.scrollBar)
  1471.   lb:draw()
  1472. end
  1473.  
  1474. local function addListBox(gui,x,y,width,height,list)
  1475.   local lb=compositeBase(gui,x,y,width,height,"listbox",true)
  1476.   lb.list=list
  1477.  
  1478.   lb.scrollBar=addScrollBarV(lb,lb.bodyW,lb.bodyY,lb.bodyH,math.max(1,#list-lb.bodyH+1),scrollListBox)
  1479.   lb.scrollBar.class="listbox"
  1480.   lb.scrollBar.listBox=lb
  1481.  
  1482.   lb.scrollBar.posY=1
  1483.   lb.scrollBar.height=lb.height
  1484.   lb.scrollBar.length=lb.height
  1485.  
  1486.   lb.selectedLabel=1
  1487.   updateScrollBarGrip(lb.scrollBar)
  1488.  
  1489.   lb.labels={}
  1490.   lb.list=list
  1491.   lb.onBeginDrag=function(lb,tx,ty,button) if tx==lb.width then lb.scrollBar:_onBeginDrag(ty,button) end end
  1492.   lb.onDrag=function(lb,...) lb.scrollBar:onDrag(...) end
  1493.   lb.onDrop=function(lb,...) lb.scrollBar:onDrop(...) end
  1494.  
  1495.   for i=1,lb.bodyH do
  1496.     lb.labels[i]=addLabel(lb,1,i,lb.bodyW-1,list[i] or "")
  1497.     lb.labels[i].class="listbox"
  1498.   end
  1499.   lb.labels[1].state="selected"
  1500.  
  1501.   lb.select=listBoxSelect
  1502.   lb.getSelected=getListBoxSelected
  1503.  
  1504.   lb.keyHandler=function(lb,char,code)
  1505.     if code==keyboard.keys.up then
  1506.       if lb.selectedLabel>1 then
  1507.         lb:select(lb.selectedLabel-1)
  1508.       end
  1509.     elseif code==keyboard.keys.down then
  1510.       if lb.selectedLabel<#lb.list then
  1511.         lb:select(lb.selectedLabel+1)
  1512.       end
  1513.     elseif code==keyboard.keys.enter and lb.onEnter then
  1514.       lb:onEnter()
  1515.     end
  1516.   end
  1517.  
  1518.   lb.updateList=updateListBoxList
  1519.  
  1520.   lb.onClick=clickListBox
  1521.   lb.draw=function(lb)
  1522.     if not lb:isHidden() then
  1523.       local styles=getAppliedStyles(lb)
  1524.       drawBorder(lb,styles)
  1525.       lb.scrollBar:draw()
  1526.       for i=1,#lb.labels do
  1527.         lb.labels[i]:draw()
  1528.       end
  1529.     end
  1530.   end
  1531.  
  1532.   gui:addComponent(lb)
  1533.   return lb
  1534. end
  1535.  
  1536.  
  1537. -- *****************************
  1538. -- FOLDER
  1539. -- *****************************
  1540.  
  1541. local function scrollFolder(sb)
  1542.   local folder=sb.folder
  1543.  
  1544.   local modShift = (sb.scrollPos-1) % folder.cellStepY
  1545.   local startRow = math.floor(sb.scrollPos / folder.cellStepY)
  1546.   for j=1,#folder.grid do
  1547.     for i=1, #folder.grid[j] do
  1548.       local n = (startRow+j-1)*folder.maxColumns + i
  1549.       local cell = folder.grid[j][i]
  1550.       cell.posY = -modShift + (j-1)*folder.cellStepY
  1551.       cell.data = #folder.items>=n and folder.items[n] or nil
  1552.      
  1553.       cell.state = nil
  1554.       if(folder.selected) then
  1555.         for _,v in pairs(folder.selected) do
  1556.           if n==v then cell.state = "selected" end
  1557.         end
  1558.       end
  1559.  
  1560.       cell:draw()
  1561.     end
  1562.   end
  1563. end
  1564.  
  1565. -- Table with icons, like Windows folders
  1566. local function addFolder(gui,x,y, width, height, items, spaceX, spaceY, iconPattern)
  1567.  
  1568.   -- Standart values
  1569.   spaceX = spaceX or 1
  1570.   spaceY = spaceY or 0
  1571.   iconPattern = iconPattern or {"¦-- ? --¬","¦       ¦","¦       ¦","L-     --"}
  1572.  
  1573.   -- Create base component. Copy parameters
  1574.   local folder = compositeBase(gui,x,y,width,height,"folder",true)
  1575.   folder.items  = items  -- All items in folder for view
  1576.   folder.spaceX = spaceX -- Horisontal space between cells
  1577.   folder.spaceY = spaceY --   Vertical space between cells
  1578.  
  1579.   -- Compute icon pattern size
  1580.   local iconW = 3
  1581.   for _,v in pairs(iconPattern) do
  1582.     local lnLen = unicode.len(v)
  1583.     if lnLen > iconW then iconW = lnLen end
  1584.   end
  1585.   local iconH = #iconPattern
  1586.   folder.iconW = iconW
  1587.   folder.iconH = iconH
  1588.   folder.cellStepX = iconW + spaceX
  1589.   folder.cellStepY = iconH + spaceY
  1590.  
  1591.   local maxColumns   = math.floor( (folder.bodyW-1)/(iconW + spaceX) )
  1592.   local maxRows      = math.floor( (folder.bodyH-1)/(iconH + spaceY) )
  1593.   folder.maxColumns  = maxColumns -- Count of columns in folder box
  1594.   folder.maxRows     = maxRows    -- Count of rows. folder.grid have 1 more row for scrolling
  1595.  
  1596. --  folder.scrollBar.posY=1
  1597. --  folder.scrollBar.height=folder.height
  1598. --  folder.scrollBar.length=folder.height
  1599.  
  1600.   folder.scrollBar = addScrollBarV(folder,folder.bodyW,folder.bodyY,folder.bodyH,1,scrollFolder)
  1601.   folder.scrollBar.class="folder"
  1602.   folder.scrollBar.folder=folder
  1603.  
  1604.   folder.grid={}
  1605.   for _y=1, maxRows+1 do
  1606.     table.insert(folder.grid, {})
  1607.     for _x=1, maxColumns do
  1608.       local icoX = folder.bodyX + (iconW + spaceX)*(_x-1)
  1609.       local icoY = folder.bodyY + (iconH + spaceY)*(_y-1)
  1610.       local cell  = compositeBase(folder,icoX,icoY,folder.iconW,folder.iconH,"folder",true)      
  1611.      
  1612.       cell.onClick = function(obj,tx,ty,button)
  1613.         if obj.state then obj.state = nil else obj.state = "selected" end
  1614.         obj:draw()
  1615.       end
  1616.      
  1617.       cell.draw = function(obj)
  1618.         if not obj:isHidden() then
  1619.           local screenX,screenY=obj:getScreenPosition()
  1620.           local fg, bg, sfg, sbg=findStyleProperties(obj, "text-color","text-background","selected-color","selected-background")
  1621.           if obj.state == "selected" then
  1622.             obj.renderTarget.setForeground(sfg)
  1623.             obj.renderTarget.setBackground(sbg)
  1624.           else
  1625.             obj.renderTarget.setForeground(fg)
  1626.             obj.renderTarget.setBackground(bg)
  1627.           end
  1628.           if obj.data then
  1629.             for i=1,#iconPattern do
  1630.               obj.renderTarget.set(screenX,screenY+i-1, iconPattern[i])
  1631.             end
  1632.            
  1633.             for i=1,#obj.data do
  1634.               local ln = obj.data[i]
  1635.               local lnLen = unicode.len(ln)
  1636.               obj.renderTarget.set(screenX + math.floor((iconW-lnLen)/2),screenY+i-1, ln)
  1637.             end
  1638.           else
  1639.             obj.renderTarget.set(screenX + math.floor(iconW/2), screenY + math.ceil(iconH/2), "?")
  1640.           end
  1641.         end
  1642.       end
  1643.      
  1644.       folder:addComponent(cell)
  1645.       table.insert(folder.grid[_y], cell)
  1646.     end
  1647.   end
  1648.  
  1649.   -- Change content of folder
  1650.   folder.updateItems = function(fl, newItems)
  1651.     fl.items=newItems
  1652.    
  1653.     local itemsCount   = newItems and #newItems or 0
  1654.     local columns      = math.min (itemsCount, fl.maxColumns)
  1655.     local rows         = math.ceil(itemsCount/ fl.maxColumns)
  1656.     local contentWidth = fl.iconW*columns + fl.spaceX*(columns-1)
  1657.     local contentHeight= fl.iconH*rows    + fl.spaceY*(rows   -1)
  1658.    
  1659.     local excessLines =  contentHeight - fl.bodyH
  1660.     fl.scrollBar.scrollPos = 1
  1661.     fl.scrollBar.scrollMax = math.max(1, excessLines + 1)
  1662.     updateScrollBarGrip(fl.scrollBar)
  1663.     scrollFolder(fl.scrollBar)
  1664.   end
  1665.   if items then folder:updateItems(items) end
  1666.  
  1667.   folder.onClick = function(folder,tx,ty,button)
  1668.     if tx==folder.width then
  1669.       folder.scrollBar:_onClick(ty,button)
  1670.     else      
  1671.       for _y=1, #folder.grid do
  1672.         for _x=1, #folder.grid[_y] do
  1673.           local cell = folder.grid[_y][_x]
  1674.           if cell:contains(tx,ty) then
  1675.             cell:onClick(tx-cell.posX+1,ty-cell.posY+1,button)
  1676.           end
  1677.         end
  1678.       end
  1679.     end
  1680.   end
  1681.  
  1682.  
  1683.   folder.draw=function(fl)
  1684.     if not fl:isHidden() then
  1685.       local styles=getAppliedStyles(fl)
  1686.       drawBorder(fl,styles)
  1687.       fl.scrollBar:draw()
  1688.       for j=1,#fl.grid do
  1689.         for i=1, #fl.grid[j] do
  1690.           fl.grid[j][i]:draw()
  1691.         end
  1692.       end
  1693.     end
  1694.   end
  1695.  
  1696.   gui:addComponent(folder)
  1697.   return folder
  1698. end
  1699.  
  1700.  
  1701. function gml.create(x,y,width,height,renderTarget)
  1702.  
  1703.   local newGui=compositeBase(screen,x,y,width,height,"gui",false)
  1704.   newGui.handlers={}
  1705.   newGui.hidden=true
  1706.   newGui.renderTarget=gfxbuffer.create(renderTarget)
  1707.  
  1708.   local running=false
  1709.   function newGui.close()
  1710.     computer.pushSignal("gui_close")
  1711.   end
  1712.  
  1713.   function newGui.addComponent(obj,component)
  1714.     newGui.components[#obj.components+1]=component
  1715.     if obj.focusElement==nil and component.focusable then
  1716.       component.state="focus"
  1717.       obj.focusElement=component
  1718.     end
  1719.   end
  1720.  
  1721.  
  1722.   newGui.addHandler=guiAddHandler
  1723.  
  1724.   function newGui.redrawRect(gui,x,y,w,h)
  1725.     local fillCh,fillFG,fillBG=findStyleProperties(newGui,"fill-ch","fill-color-fg","fill-color-bg")
  1726.     local blank=(fillCh):rep(w)
  1727.     gui.renderTarget.setForeground(fillFG)
  1728.     gui.renderTarget.setBackground(fillBG)
  1729.  
  1730.     x=x+newGui.bodyX-1
  1731.     for y=y+newGui.bodyY-1,y+h+newGui.bodyY-2 do
  1732.       gui.renderTarget.set(x,y,blank)
  1733.     end
  1734.   end
  1735.  
  1736.   function newGui.changeFocusTo(gui,target)
  1737.     if gui.focusElement then
  1738.       gui.focusElement.state=nil
  1739.       if gui.focusElement.lostFocus then
  1740.         gui.focusElement.lostFocus()
  1741.       elseif not gui.hidden then
  1742.         gui.focusElement:draw()
  1743.       end
  1744.     end
  1745.     gui.focusElement=target
  1746.     target.state="focus"
  1747.     if target.gotFocus then
  1748.       target.gotFocus()
  1749.     elseif not gui.hidden then
  1750.       target:draw()
  1751.     end
  1752.   end
  1753.  
  1754.   newGui.run=runGui
  1755.   newGui.contains=contains
  1756.   newGui.addLabel=addLabel
  1757.   newGui.addButton=addButton
  1758.   newGui.addTextField=addTextField
  1759.   newGui.addScrollBarV=addScrollBarV
  1760.   newGui.addScrollBarH=addScrollBarH
  1761.   newGui.addListBox=addListBox
  1762.   newGui.addFolder=addFolder
  1763.   newGui.draw=function(gui)
  1764.       local styles=getAppliedStyles(gui)
  1765.       local bodyX,bodyY,bodyW,bodyH=drawBorder(gui,styles)
  1766.       local fillCh,fillFG,fillBG=extractProperties(gui,styles,"fill-ch","fill-color-fg","fill-color-bg")
  1767.  
  1768.       gui.renderTarget.setForeground(fillFG)
  1769.       gui.renderTarget.setBackground(fillBG)
  1770.       term.setCursorBlink(false)
  1771.  
  1772.       gui.renderTarget.fill(bodyX,bodyY,bodyW,bodyH,fillCh)
  1773.  
  1774.       for i=1,#gui.components do
  1775.         gui.components[i]:draw()
  1776.         gui.renderTarget:flush()
  1777.       end
  1778.      
  1779.       if gui.onDraw then
  1780.         gui.onDraw()
  1781.       end
  1782.     end
  1783.  
  1784.   return newGui
  1785. end
  1786.  
  1787.  
  1788.  
  1789.  
  1790. --**********************
  1791.  
  1792. defaultStyle=gml.loadStyle("gml/default")
  1793. screen.style=defaultStyle
  1794.  
  1795. return gml
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement