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