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