From aa64d180600cedbae0d01d1d1cf5916120b55766 Mon Sep 17 00:00:00 2001 From: Rivulet Date: Tue, 23 Dec 2025 12:46:47 -0800 Subject: [PATCH] fixed sum stuff --- apps-meta/Clicker.app | 3 +- apps/com.ruffles.clicker | Bin 334 -> 336 bytes apps/com.ruffles.launcher | Bin 402 -> 650 bytes global-libraries/pixelui_compressed.lua | 14335 ++++++++++++++++++++++ global-libraries/shrekbox.lua | 1053 ++ libs/containers.lua | 13 +- libs/windows.lua | 59 + startup/99_phoneOS.lua | 69 +- system-apps/dookie-clicker/startup.lua | 2 +- 9 files changed, 15514 insertions(+), 20 deletions(-) create mode 100644 global-libraries/pixelui_compressed.lua create mode 100644 global-libraries/shrekbox.lua diff --git a/apps-meta/Clicker.app b/apps-meta/Clicker.app index 3e135e2..5b824f5 100644 --- a/apps-meta/Clicker.app +++ b/apps-meta/Clicker.app @@ -4,8 +4,9 @@ network = false, app = false, http = false, + peripheral = false }, author = "CadenCoaster", - name = "Dookie Clicker", + name = "Bad Clicker", appid = "com.ruffles.clicker", } \ No newline at end of file diff --git a/apps/com.ruffles.clicker b/apps/com.ruffles.clicker index 1975d742e508aed7f2aeeedcfcf756828a142255..070d40e38d65d4edb98580c3343d180c3a8e5f35 100644 GIT binary patch literal 336 zcmV-W0k8gzkv~uKFc8Lfe~QDCBii(jgpewA=_|oPrx%FnXIP$4X@Pzng@-nPMN?Z{ zROnYi?g|&ZIg?gfSEYp$%_X%SZDWQjuE^5$h;der&nS1R(b4M zXId7#w`M?Eb;5PjY&vB9(@O4O{mXzC=0c*0Ps);oHJy!Zf{|WDI!i8#?9W@64c?*M i*mEQ@Z!7PVDOgua52KrKRe3=aRG~ z3`JzI^4vYo&*wWZfLGvEP(H->k%@}K8!UhurH)RiPp zB$I47ghvbzHVCCEEUdjM5yaj_4|QQPS5sRTsa>>9;^Zu}hQ$dr$}y}+cxVCynVRan z!mw0&n|tzRLObI;UBHlZiLD143JK`TCgOkb`R!I&_M}oY;vm_Mn(FavDDKCziQ$|uFsrYh1HuPV8=Ss z(inoXJ<_rjuA`=tKI@+;^cGgX40vJAHClR7k<6WKZDJj*4h7Lga@k~e-omu^4$aD* gi70}rf-h_?+FFtT+N_-gdY@#c?fYm&!Uyd5^~~$4k^lez diff --git a/apps/com.ruffles.launcher b/apps/com.ruffles.launcher index fefef4af07af39025a54bc1e9aee90e56006fbc5..01962c23e7eced15ddb3b5b8223a3a2e0119d826 100644 GIT binary patch literal 650 zcmV;50(Je(mfvsFFc8Pz^H*4TO0+a+d#u!l!Wb_g5JLiq@Zcnu#M;#kwxhI6``a>LQmSi|>5Xb2L& zM3r(haUV+jF!2&^v0!h&GFt)La3NWt`36|9Q;lY=khy9})~D>^t08k-f_+C5S56<0 z#zmI~>`R{Alv*`14>E;Ho4Bong?*MbswjyCy>Th7#gA@v*T_208E0u@t&*dh*L7Ed z)^rtko?mBLA@+rD6K_kQ^Cq@kAUx6qqN1=LgLBn+48%+(}m74Do?=C#q zL9JB=#yq8qv%cIDJ4uly`&Rl_U%zN%W`&YnWmKwmyck`?6@gjZ0LxWB<*t#~GDF~c z6i*`5{#%{`y2jZr*P*l5lBWpysvy%qbYHn$ozKkfW@issSI4fHQ5H(WjB+Zx3zvmJ z{OkY+p|jm5*jn=Vx~|Q#v-T+6+sF`U`f{#ws$(!zlZi-Bl##_n&4o6BTM|yl{zHc1 z?D;4<4zZ*?hD#-1p~!B6y$6G^V~Ct4s$NNNk$3Ronts(L1eqVPrO{z<{9C&3&q?%m zd{2`*?ZA0{r3R2E)Q@5~a6CBheGQU*=yw8-@Z!ftN+HX6OA^@!9l1|>$t{mIraa`n z1j_{mlqH<55yiN`J^AJRE&a8l5YKiL!v;Gi$FZ@RD90LVIwO|&QM))6ED>Z7E-u13 z&!<*K+i2HYzff6Y=%%B}re6O;dhOzW(^b literal 402 zcmV;D0d4+OlEH4VO^Q9TZEkiOP~EQ*g2(?>P!*@eKqk&1GgJV#W0de**z=}@$m1SGX9B;3go59czc6>{E!I~7^FNU5}|1tz0 zoff^fK5#wDu3MJ18aWA>Fr^gF_%GkiAdgyufZ|zp~wJS~> z+Era~zMd;;O8HlQ9}Ny;YQf0H;fBSIE7%a!xhpQY{8mlP^TKgH65JzO3GV0vV1?`j z?f}jT!y=MYg7zKH0XEFU_mt@q8eG?@O<-@KjO+$X6fNl_zqOl2zCp(FH_wZ2hmQwD$`)aX& w5y$9yLhnGx+y|@JF>a}Nqt1a do +local n=e:sub(1,a) +local o +for e=a,1,-1 do +local t=n:sub(e,e) +if t:match("%s")then +o=e-1 +break +end +end +if o and o>=1 then +local i=e:sub(1,o) +i=i:gsub("%s+$","") +if i==""then +i=e:sub(1,a) +o=a +end +t[#t+1]=i +e=e:sub(o+1) +else +t[#t+1]=n +e=e:sub(a+1) +end +e=e:gsub("^%s+","") +if e==""then +break +end +end +if e~=""then +t[#t+1]=e +elseif#t==0 then +t[#t+1]="" +end +end +local function X(e) +if e==nil then +return nil +end +if e==false then +return nil +end +if type(e)~="string"then +return nil +end +local e=e:lower():gsub("%s+","_"):gsub("-","_") +if e=="manual"or e=="none"then +return nil +end +if e=="topright"then +e="top_right" +elseif e=="topleft"then +e="top_left" +elseif e=="bottomright"then +e="bottom_right" +elseif e=="bottomleft"then +e="bottom_left" +end +if e=="top_right"or e=="top_left"or e=="bottom_right"or e=="bottom_left"then +return e +end +return nil +end +local function Y(e) +local i,o,a,t=1,1,1,1 +if e==nil then +return{top=i,right=o,bottom=a,left=t} +end +if type(e)=="number"then +local e=math.max(0,math.floor(e)) +i,o,a,t=e,e,e,e +elseif type(e)=="table"then +if e.all~=nil then +local e=math.max(0,math.floor(e.all)) +i,o,a,t=e,e,e,e +end +if e.vertical~=nil then +local e=math.max(0,math.floor(e.vertical)) +i,a=e,e +end +if e.horizontal~=nil then +local e=math.max(0,math.floor(e.horizontal)) +o,t=e,e +end +if e.top~=nil then +i=math.max(0,math.floor(e.top)) +end +if e.right~=nil then +o=math.max(0,math.floor(e.right)) +end +if e.bottom~=nil then +a=math.max(0,math.floor(e.bottom)) +end +if e.left~=nil then +t=math.max(0,math.floor(e.left)) +end +end +return{top=i,right=o,bottom=a,left=t} +end +local function P(t,i) +local e={} +if i<=0 then +e[1]="" +return e +end +t=tostring(t or"") +if t==""then +e[1]="" +return e +end +local a=1 +while true do +local o=t:find("\n",a,true) +if not o then +z(t:sub(a),i,e) +break +end +z(t:sub(a,o-1),i,e) +a=o+1 +end +if#e==0 then +e[1]="" +end +return e +end +local function o(e) +if not e then +return nil +end +local t={} +for e,a in pairs(e)do +t[e]=a +end +return t +end +local z={ +close={ +label="X", +fg=e.white, +bg=e.red, +hoverFg=e.white, +hoverBg=e.red, +pressFg=e.lightGray, +pressBg=e.red +}, +maximize={ +label="[]", +maximizeLabel="[]", +restoreLabel="][", +fg=e.white, +bg=e.gray, +hoverFg=e.white, +hoverBg=e.lightGray, +pressFg=e.black, +pressBg=e.lightGray +}, +minimize={ +label="_", +fg=e.white, +bg=e.gray, +hoverFg=e.white, +hoverBg=e.lightGray, +pressFg=e.black, +pressBg=e.lightGray +} +} +local function n(a,e) +local a=z[a] +local a=o(a)or{} +if e==nil or e==false or e==true then +return a +end +t(1,e,"table") +if e.label~=nil then +a.label=tostring(e.label) +end +if e.maximizeLabel~=nil then +a.maximizeLabel=tostring(e.maximizeLabel) +end +if e.restoreLabel~=nil then +a.restoreLabel=tostring(e.restoreLabel) +end +if e.fg~=nil then +a.fg=e.fg +end +if e.bg~=nil then +a.bg=e.bg +end +if e.restoreFg~=nil then +a.restoreFg=e.restoreFg +end +if e.restoreBg~=nil then +a.restoreBg=e.restoreBg +end +if e.width~=nil then +local e=math.max(1,math.floor(e.width)) +a.width=e +end +if e.padding~=nil then +a.padding=math.max(0,math.floor(e.padding)) +end +return a +end +local function A(e) +local t=(type(e)=="table"and e.buttons)or nil +local a +local o +local i +if type(e)=="table"then +a=e.closeButton or(type(t)=="table"and t.close)or nil +o=e.maximizeButton or(type(t)=="table"and t.maximize)or nil +i=e.minimizeButton or(type(t)=="table"and t.minimize)or nil +end +return{ +close=n("close",a), +maximize=n("maximize",o), +minimize=n("minimize",i) +} +end +local function n(e,t) +if e==nil then +return t +end +if type(e)=="string"then +return H[e]or t +elseif type(e)=="function"then +return e +end +error("Invalid easing value",3) +end +local function z(e,a) +if e==nil then +return nil +end +if e==false then +return{enabled=false} +end +t(1,e,"table") +local t={ +enabled=e.enabled~=false +} +if e.duration~=nil then +if type(e.duration)~="number"then +error("animation duration must be numeric",3) +end +t.duration=math.max(0,e.duration) +end +if e.easing~=nil then +t.easing=n(e.easing,a) +end +return t +end +local function B(e) +local a=.2 +local o=H.easeOutQuad +if e==nil then +return{ +enabled=true, +duration=a, +easing=o +} +end +t(1,e,"table") +if e.duration~=nil and type(e.duration)~="number"then +error("animation duration must be numeric",3) +end +local t={ +enabled=e.enabled~=false, +duration=math.max(0,e.duration or a), +easing=n(e.easing,o) +} +local a={"maximize","minimize","restore"} +for o=1,#a do +local a=a[o] +local e=z(e[a],t.easing) +if e then +t[a]=e +end +end +return t +end +local function W(e) +if e>=0 then +return math.floor(e+.5) +end +return math.ceil(e-.5) +end +local function M(a,e) +t(nil,a,"string") +t(nil,e,"number") +if e<1 or e~=math.floor(e)then +error(('%s must be a positive integer, got "%s"'):format(a,tostring(e)),3) +end +end +local function R(a) +if not a or a==false then +return nil +end +if a==true then +return{ +color=e.lightGray, +top=true, +right=true, +bottom=true, +left=true, +thickness=1 +} +end +t(1,a,"table") +local e={ +color=a.color or e.lightGray, +top=true, +right=true, +bottom=true, +left=true, +thickness=math.max(1,math.floor(a.thickness or 1)) +} +local function o(a,t) +if t~=nil then +e[a]=not not t +end +end +if a.sides then +e.top=false +e.right=false +e.bottom=false +e.left=false +if#a.sides>0 then +for t=1,#a.sides do +local t=a.sides[t] +if e[t]~=nil then +e[t]=true +end +end +else +for t,a in pairs(a.sides)do +if e[t]~=nil then +e[t]=not not a +end +end +end +else +for e=1,#p do +o(p[e],a[p[e]]) +end +end +if e.thickness<1 then +e.thickness=1 +end +return e +end +local function V(a) +local e=a.border +local t=e and math.max(1,math.floor(e.thickness or 1))or 0 +local n=(e and e.left)and t or 0 +local o=(e and e.right)and t or 0 +local i=(e and e.top)and t or 0 +local e=(e and e.bottom)and t or 0 +local t=math.max(0,a.width-n-o) +local a=math.max(0,a.height-i-e) +return n,o,i,e,t,a +end +local function ae(e,a) +if e==nil then +return{ +enabled=a~=false, +height=1, +bg=nil, +fg=nil, +align="left", +buttons=A(nil), +buttonSpacing=1 +} +end +if e==false then +return{enabled=false,height=0,bg=nil,fg=nil,align="left",buttons=A(nil),buttonSpacing=1} +end +if e==true then +return{enabled=true,height=1,bg=nil,fg=nil,align="left",buttons=A(nil),buttonSpacing=1} +end +t(1,e,"table") +local t=e.enabled +if t==nil then +t=true +end +if not t then +return{enabled=false,height=0,bg=nil,fg=nil,align="left"} +end +local t=e.height +if type(t)~="number"or t<1 then +t=1 +else +t=math.floor(t) +end +local a=e.align and tostring(e.align):lower()or"left" +if a~="left"and a~="center"and a~="right"then +a="left" +end +local o=e.buttonSpacing~=nil and math.max(0,math.floor(e.buttonSpacing))or 1 +return{ +enabled=true, +height=t, +bg=e.bg, +fg=e.fg, +align=a, +buttons=A(e), +buttonSpacing=o +} +end +local function te(a,e,t) +if t~=nil and e~=nil and tt then +return t +end +return a +end +local p="^(%a[%w_]*)%.([%a_][%w_]*)$" +local function n(e) +if type(e)~="string"then +return nil,nil +end +local t,e=e:match(p) +return t,e +end +local function z(e,t) +local e=tonumber(e) +if not e then +error("constraints."..t.." must be numeric",3) +end +if e>1 then +e=e/100 +end +if e<0 then +e=0 +elseif e>1 then +e=1 +end +return e +end +local function p(e,t) +if e==nil then +return nil +end +local a=type(e) +if a=="number"then +return{kind="absolute",value=math.max(1,math.floor(e))} +end +if a=="boolean"then +if e then +return{kind="relative",target="parent",property=t} +end +return nil +end +if a=="string"then +local e,a=n(e) +if not e then +error("constraints."..t.." string references must look like 'parent.'",3) +end +if e~="parent"then +error("constraints."..t.." currently only supports references to the parent",3) +end +return{kind="relative",target=e,property=a,offset=0} +end +if a=="table"then +if e.reference or e.of then +local a=e.reference or e.of +local a,o=n(a) +if not a then +error("constraints."..t.." reference tables must include 'reference' or 'of' matching 'parent.'",3) +end +if a~="parent"then +error("constraints."..t.." references currently only support the parent",3) +end +local e=e.offset and math.floor(e.offset)or 0 +return{kind="relative",target=a,property=o,offset=e} +end +if e.percent~=nil then +local o=z(e.percent,t..".percent") +local a=e.of or("parent."..t) +local a,i=n(a) +if not a then +error("constraints."..t..".percent requires an 'of' reference such as 'parent.width'",3) +end +if a~="parent"then +error("constraints."..t..".percent currently only supports the parent",3) +end +local e=e.offset and math.floor(e.offset)or 0 +return{ +kind="percent", +percent=o, +target=a, +property=i, +offset=e +} +end +if e.match~=nil then +return p(e.match,t) +end +if e.value~=nil then +return p(e.value,t) +end +error("constraints."..t.." table must include percent, reference/of, match, or value fields",3) +end +return nil +end +local function T(e,t) +if e==nil then +return nil +end +local a=type(e) +if a=="boolean"then +if not e then +return nil +end +return{ +kind="center", +target="parent", +property=t=="x"and"centerX"or"centerY", +offset=0 +} +end +if a=="string"then +local e,a=n(e) +if not e then +error("constraints.center"..(t=="x"and"X"or"Y").." string references must look like 'parent.'",3) +end +if e~="parent"then +error("constraints.center"..(t=="x"and"X"or"Y").." currently only supports the parent",3) +end +return{kind="center",target=e,property=a,offset=0} +end +if a=="table"then +local o=e.reference or e.of or e.target or e.align +local a=e.offset and math.floor(e.offset)or 0 +if o then +local e,o=n(o) +if not e then +error("constraints.center"..(t=="x"and"X"or"Y").." reference tables must use 'parent.'",3) +end +if e~="parent"then +error("constraints.center"..(t=="x"and"X"or"Y").." currently only supports the parent",3) +end +return{kind="center",target=e,property=o,offset=a} +end +return{ +kind="center", +target="parent", +property=t=="x"and"centerX"or"centerY", +offset=a +} +end +return nil +end +local function oe(e) +if e==nil then +return nil +end +if type(e)~="table"then +error("constraints must be a table if provided",3) +end +local t={} +if e.minWidth~=nil then +if type(e.minWidth)~="number"then +error("constraints.minWidth must be a number",3) +end +t.minWidth=math.max(1,math.floor(e.minWidth)) +end +if e.maxWidth~=nil then +if type(e.maxWidth)~="number"then +error("constraints.maxWidth must be a number",3) +end +t.maxWidth=math.max(1,math.floor(e.maxWidth)) +end +if e.minHeight~=nil then +if type(e.minHeight)~="number"then +error("constraints.minHeight must be a number",3) +end +t.minHeight=math.max(1,math.floor(e.minHeight)) +end +if e.maxHeight~=nil then +if type(e.maxHeight)~="number"then +error("constraints.maxHeight must be a number",3) +end +t.maxHeight=math.max(1,math.floor(e.maxHeight)) +end +if t.minWidth and t.maxWidth and t.maxWidth1 then +return 1 +end +return e +end +local function F(e,o,t,a) +if not e or e.enabled==false then +return 0,nil +end +o=math.max(0,o or 0) +t=math.max(0,t or 0) +a=math.max(0,a or 0) +if a<=1 or t<=0 then +return 0,nil +end +if t<=2 then +return 0,nil +end +if not e.alwaysVisible and o<=t then +return 0,nil +end +local a=math.max(1,a-1) +local t=math.max(1,math.floor(e.width or 1)) +if t>a then +t=a +end +if t<=0 then +return 0,nil +end +return t,e +end +local function C(n,i,a,t,r,d,u,e) +if not e or t<=0 then +return +end +local h=math.max(1,math.floor(e.width or 1)) +local o=e.trackColor +local s=e.arrowColor +local l=e.thumbColor +local c=math.max(1,math.floor(e.minThumbSize or 1)) +local e=math.max(0,h-1) +local m=T..string.rep(" ",e) +n.text(i,a,m,s,o) +if t>=2 then +local e=p..string.rep(" ",e) +n.text(i,a+t-1,e,s,o) +end +local a=a+1 +local e=math.max(0,t-2) +local t=string.rep(" ",h) +for e=0,e-1 do +n.text(i,a+e,t,o,o) +end +local s=math.max(0,(r or 0)-(d or 0)) +if s<=0 or e<=0 then +return +end +local u=math.max(0,math.min(s,math.floor((u or 0)+.5))) +local t=d/r +local t=math.max(c,math.floor(e*t+.5)) +if t>e then +t=e +end +if t<1 then +t=1 +end +local o=e-t +local e=a +if o>0 then +local t=z(s==0 and 0 or(u/s)) +e=a+math.floor(t*o+.5) +if e>a+o then +e=a+o +end +end +local a=string.rep(" ",h) +for t=0,t-1 do +n.text(i,e+t,a,l,l) +end +end +local function U(t,a,e,i,o) +if a<=0 then +return o or 0 +end +local e=math.max(0,(e or 0)-(i or 0)) +if e<=0 then +return 0 +end +local o=math.max(0,math.min(e,math.floor((o or 0)+.5))) +if t<=0 then +return math.max(0,o-1) +elseif t>=a-1 then +return math.min(e,o+1) +end +local a=a-2 +if a<=0 then +return o +end +local t=t-1 +if t<0 then +t=0 +elseif t>a then +t=a +end +local t=math.floor((t/a)*e+.5) +if t<0 then +t=0 +elseif t>e then +t=e +end +return t +end +local function n(h,s,n,t,e,i,o,a) +if t<=0 or e<=0 then +return +end +local a=a or" " +local t=a:rep(t) +for e=0,e-1 do +h.text(s,n+e,t,i,o) +end +end +local function T(t,i,n,a,o,s) +if a<=0 or o<=0 or not t then +return +end +local s=s or e.black +local e=(i-1)*2+1 +local i=(n-1)*3+1 +local a=a*2 +local o=o*3 +for o=0,o-1 do +local o=i+o +for a=0,a-1 do +t.pixel(e+a,o,s) +end +end +end +local function z(e,t,a,n,o) +if n<=0 or o<=0 then +return +end +local i=J.transparent +for n=0,n-1 do +e.pixel(t+n,a,i) +if o>1 then +e.pixel(t+n,a+o-1,i) +end +end +for o=1,math.max(0,o-2)do +e.pixel(t,a+o,i) +if n>1 then +e.pixel(t+n-1,a+o,i) +end +end +end +local function p(s,t,h,i,o,e,a) +if i<=0 or o<=0 then +return +end +local n=e.color +local r=a or n +local t=(t-1)*2+1 +local a=(h-1)*3+1 +local i=i*2 +local o=o*3 +local l=3 +local u=2 +local h=math.min(e.thickness,o) +local d=math.min(e.thickness,i) +local l=math.min(o,math.max(h,l)) +local u=math.min(i,math.max(d,u)) +local function m(h,e,n) +for e=0,e-1 do +local e=h+e +if e=a+o then break end +for a=0,i-1 do +s.pixel(t+a,e,n) +end +end +end +local function c(h,e,n) +for e=0,e-1 do +local e=h+e +if e=a+o then break end +for a=0,i-1 do +s.pixel(t+a,e,n) +end +end +end +local function f(e,h,n) +for h=0,h-1 do +local e=e+h +if e=t+i then break end +for t=0,o-1 do +s.pixel(e,a+t,n) +end +end +end +local function w(h,e,n) +for e=0,e-1 do +local e=h+e +if e=t+i then break end +for t=0,o-1 do +s.pixel(e,a+t,n) +end +end +end +if e.left then +f(t,u,r) +end +if e.right then +f(t+i-u,u,r) +end +if e.top then +m(a,l,r) +end +if e.bottom then +m(a+o-l,l,r) +end +if e.top then +c(a,h,n) +end +if e.bottom then +c(a+o-h,h,n) +end +if e.left then +w(t,d,n) +end +if e.right then +w(t+i-d,d,n) +end +end +function r:new(i,a) +a=a or{} +t(1,i,"table") +if a~=nil then +t(2,a,"table") +end +local t=o(a)or{} +t.focusable=false +t.width=math.max(12,math.floor(t.width or 24)) +t.height=math.max(3,math.floor(t.height or 5)) +if t.visible==nil then +t.visible=false +end +local e=setmetatable({},r) +e:_init_base(i,t) +e.focusable=false +local i=a.anchor~=nil +local t=X(a.anchor) +if not t and not i then +if a.x~=nil or a.y~=nil then +t=nil +else +t="top_right" +end +end +e.anchor=t +e.anchorMargins=Y(a.anchorMargin) +e.anchorAnimationDuration=math.max(.05,tonumber(a.anchorAnimationDuration)or .2) +e.anchorEasing=a.anchorEasing or"easeOutCubic" +e._anchorDirty=true +e._anchorAnimationHandle=nil +e.title=a.title~=nil and tostring(a.title)or nil +e.message=a.message~=nil and tostring(a.message)or"" +e.icon=a.icon~=nil and tostring(a.icon)or nil +e.severity=ee(a.severity) +local t=a.duration +if t~=nil then +t=tonumber(t)or 0 +else +t=3 +end +if t<0 then +t=0 +end +e.duration=t +e.autoHide=a.autoHide~=false +e.dismissOnClick=a.dismissOnClick~=false +e.onDismiss=a.onDismiss +if e.onDismiss~=nil and type(e.onDismiss)~="function"then +error("config.onDismiss must be a function",2) +end +e.variantOverrides=a.variants and o(a.variants)or nil +e.styleOverride=a.style and o(a.style)or nil +e.paddingLeft,e.paddingRight,e.paddingTop,e.paddingBottom=Z(a.padding) +e._hideTimer=nil +e._wrappedLines={""} +e._lastWrapWidth=nil +e._lastMessage=nil +e:_refreshWrap(true) +return e +end +function r:_applyPadding(e,i) +local e,a,t,o=Z(e) +if i or e~=self.paddingLeft or a~=self.paddingRight or t~=self.paddingTop or o~=self.paddingBottom then +self.paddingLeft=e +self.paddingRight=a +self.paddingTop=t +self.paddingBottom=o +self:_refreshWrap(true) +self._anchorDirty=true +end +end +function r:setPadding(e) +self:_applyPadding(e,false) +end +function r:getAnchor() +return self.anchor +end +function r:getAnchorMargins() +return o(self.anchorMargins) +end +function r:refreshAnchor(e) +if not self.anchor then +self._anchorDirty=false +return +end +self._anchorDirty=true +if e and self.visible then +self:_applyAnchorPosition(true) +else +self:_applyAnchorPosition(false) +end +end +function r:setAnchor(t) +local e=X(t) +if e==nil and t~=nil then +self.anchor=nil +else +self.anchor=e +end +self:refreshAnchor(false) +end +function r:setAnchorMargin(e) +self.anchorMargins=Y(e) +self:refreshAnchor(false) +end +function r:_computeAnchorPosition() +local i=self.anchor +if not i then +return nil,nil +end +local e=self.parent +if not e then +return nil,nil +end +local n=e.width +local o=e.height +if type(n)~="number"or type(o)~="number"then +return nil,nil +end +local h=self.width +local s=self.height +local a=self.anchorMargins or Y(nil) +local t +local e +if i=="top_right"then +t=n-h-(a.right or 0)+1 +e=(a.top or 0)+1 +elseif i=="top_left"then +t=(a.left or 0)+1 +e=(a.top or 0)+1 +elseif i=="bottom_right"then +t=n-h-(a.right or 0)+1 +e=o-s-(a.bottom or 0)+1 +elseif i=="bottom_left"then +t=(a.left or 0)+1 +e=o-s-(a.bottom or 0)+1 +else +return nil,nil +end +if t<1 then +t=1 +end +if e<1 then +e=1 +end +if t+h-1>n then +t=math.max(1,n-h+1) +end +if e+s-1>o then +e=math.max(1,o-s+1) +end +return t,e +end +function r:getAnchorTargetPosition() +return self:_computeAnchorPosition() +end +function r:_applyAnchorPosition(a) +if not self.anchor then +self._anchorDirty=false +return +end +local e,t=self:_computeAnchorPosition() +if not e or not t then +return +end +if self._anchorAnimationHandle then +self._anchorAnimationHandle:cancel() +self._anchorAnimationHandle=nil +end +if a and self.app and self.app.animate then +local i=math.max(2,math.floor(self.width/6)) +local n=math.max(1,math.floor(self.height/3)) +local o=e +local a=t +if self.anchor=="top_right"then +o=e+i +a=math.max(1,t-n) +elseif self.anchor=="top_left"then +o=e-i +a=math.max(1,t-n) +elseif self.anchor=="bottom_right"then +o=e+i +a=t+n +elseif self.anchor=="bottom_left"then +o=e-i +a=t+n +end +s.setPosition(self,o,a) +local r=self.anchorAnimationDuration or .2 +local h=self.anchorEasing or"easeOutCubic" +local o=o +local a=a +local i=e-o +local n=t-a +self._anchorAnimationHandle=self.app:animate({ +duration=r, +easing=h, +update=function(e) +local t=math.floor(o+i*e+.5) +local e=math.floor(a+n*e+.5) +s.setPosition(self,t,e) +end, +onComplete=function() +s.setPosition(self,e,t) +self._anchorAnimationHandle=nil +end, +onCancel=function() +s.setPosition(self,e,t) +self._anchorAnimationHandle=nil +end +}) +self._anchorDirty=false +return +end +if self.x~=e or self.y~=t then +s.setPosition(self,e,t) +end +self._anchorDirty=false +end +function r:_getActiveBorder() +if self.border then +return self.border +end +return nil +end +function r:_refreshWrap(o,t) +local e +if t~=nil then +e=math.max(0,math.floor(t)) +else +local t=self:_getActiveBorder() +local a=(t and t.left)and t.thickness or 0 +local t=(t and t.right)and t.thickness or 0 +e=math.max(0,self.width-a-t-(self.paddingLeft or 0)-(self.paddingRight or 0)) +end +if e<0 then +e=0 +end +if not o and self._lastWrapWidth==e and self._lastMessage==self.message then +return +end +self._wrappedLines=P(self.message,e) +self._lastWrapWidth=e +self._lastMessage=self.message +end +function r:_getStyle() +local a=self.severity +local e=O.info +if a~=nil then +local t=O[a] +if t then +e=t +end +else +a="info" +end +local t=e +if self.variantOverrides then +local a=self.variantOverrides[a] +if a then +t=o(e)or e +for e,a in pairs(a)do +t[e]=a +end +end +end +if self.styleOverride then +if t==e then +t=o(e)or e +end +for a,e in pairs(self.styleOverride)do +t[a]=e +end +end +return t or e +end +function r:_cancelTimer() +if self._hideTimer then +if I.cancelTimer then +pcall(I.cancelTimer,self._hideTimer) +end +self._hideTimer=nil +end +end +function r:_scheduleHide(e) +if not self.autoHide then +return +end +local e=e +if e==nil then +e=self.duration +end +if not e or e<=0 then +return +end +self._hideTimer=I.startTimer(e) +end +function r:setTitle(e) +if e==nil then +self.title=nil +else +self.title=tostring(e) +end +end +function r:getTitle() +return self.title +end +function r:setMessage(e) +if e==nil then +e="" +end +local e=tostring(e) +if self.message~=e then +self.message=e +self:_refreshWrap(true) +end +end +function r:getMessage() +return self.message +end +function r:setSeverity(e) +local e=ee(e) +if self.severity~=e then +self.severity=e +end +end +function r:getSeverity() +return self.severity +end +function r:setIcon(e) +if e==nil or e==""then +self.icon=nil +return +end +self.icon=tostring(e) +end +function r:getIcon() +return self.icon +end +function r:setAutoHide(e) +e=not not e +if self.autoHide~=e then +self.autoHide=e +if not e then +self:_cancelTimer() +end +end +end +function r:isAutoHide() +return self.autoHide +end +function r:setDuration(e) +if e==nil then +return +end +local e=tonumber(e)or 0 +if e<0 then +e=0 +end +self.duration=e +if self.visible and self.autoHide then +self:_cancelTimer() +self:_scheduleHide(e) +end +end +function r:getDuration() +return self.duration +end +function r:setDismissOnClick(e) +self.dismissOnClick=not not e +end +function r:isDismissOnClick() +return self.dismissOnClick +end +function r:setOnDismiss(e) +if e~=nil and type(e)~="function"then +error("onDismiss handler must be a function",2) +end +self.onDismiss=e +end +function r:setVariants(e) +if e~=nil and type(e)~="table"then +error("variants must be a table",2) +end +self.variantOverrides=e and o(e)or nil +end +function r:setStyle(e) +if e~=nil and type(e)~="table"then +error("style must be a table",2) +end +self.styleOverride=e and o(e)or nil +end +function r:present(e) +t(1,e,"table") +if e.title~=nil then +self:setTitle(e.title) +end +if e.message~=nil then +self:setMessage(e.message) +end +if e.icon~=nil then +self:setIcon(e.icon) +end +if e.severity~=nil then +self:setSeverity(e.severity) +end +if e.duration~=nil then +self:setDuration(e.duration) +end +if e.autoHide~=nil then +self:setAutoHide(e.autoHide) +end +if e.style~=nil then +self:setStyle(e.style) +end +if e.variants~=nil then +self:setVariants(e.variants) +end +self:show(e.duration) +end +function r:show(t) +local e=self.visible +self.visible=true +self:_refreshWrap(true) +self:_cancelTimer() +if self.anchor then +if not e then +self:_applyAnchorPosition(true) +elseif self._anchorDirty then +self:_applyAnchorPosition(false) +end +end +local e=nil +if t~=nil then +e=tonumber(t)or 0 +if e<0 then +e=0 +end +end +self:_scheduleHide(e) +end +function r:hide(t) +local e=self.visible +self.visible=false +self:_cancelTimer() +if self._anchorAnimationHandle then +self._anchorAnimationHandle:cancel() +self._anchorAnimationHandle=nil +end +if t~=false and e and self.onDismiss then +self.onDismiss(self) +end +end +function r:setSize(e,t) +s.setSize(self,e,t) +self:_refreshWrap(true) +self._anchorDirty=true +if self.anchor then +self:_applyAnchorPosition(false) +end +end +function r:setBorder(e) +s.setBorder(self,e) +self:_refreshWrap(true) +self._anchorDirty=true +end +function r:_renderLine(i,a,s,t,e,n,o) +if t<=0 then +return +end +local e=e or"" +if#e>t then +e=e:sub(1,t) +end +if#e0 then +local a=a:sub(1,1) +o.text(h,e,a,c,t) +if n>=3 then +o.text(h+1,e," ",c,t) +s=2 +else +s=1 +end +r=h+s +end +local a=math.max(0,n-s) +self:_refreshWrap(false,a) +if self.title and self.title~=""and i>0 and a>0 then +self:_renderLine(o,r,e,a,self.title,f,t) +e=e+1 +i=i-1 +end +if i>0 and a>0 then +local n=self._wrappedLines or{""} +local i=math.min(i,#n) +for i=1,i do +self:_renderLine(o,r,e,a,n[i],d,t) +e=e+1 +end +end +end +function r:handleEvent(e,...) +if not self.visible then +return false +end +if e=="timer"then +local e=... +if self._hideTimer and e==self._hideTimer then +self._hideTimer=nil +self:hide(true) +return true +end +elseif e=="mouse_click"then +local a,e,t=... +if self.dismissOnClick and self:containsPoint(e,t)then +self:hide(true) +return true +end +elseif e=="monitor_touch"then +local a,e,t=... +if self.dismissOnClick and self:containsPoint(e,t)then +self:hide(true) +return true +end +end +return false +end +function r:onFocusChanged() +end +function v:new(n,a) +a=a or{} +t(1,n,"table") +if a~=nil then +t(2,a,"table") +end +local i=o(a)or{} +i.focusable=false +i.width=math.max(3,math.floor(i.width or 8)) +i.height=math.max(3,math.floor(i.height or 5)) +local t=setmetatable({},v) +t:_init_base(n,i) +t.focusable=false +t.color=a.color or e.cyan +t.secondaryColor=a.secondaryColor or e.lightBlue +t.tertiaryColor=a.tertiaryColor or e.blue +t.trailColor=a.trailColor or e.gray +t.trailPalette=a.trailPalette and o(a.trailPalette)or nil +t.segmentCount=math.max(6,math.floor(a.segments or a.segmentCount or 12)) +t.thickness=math.max(1,math.floor(a.thickness or 2)) +t.radiusPixels=a.radius and math.max(2,math.floor(a.radius))or nil +local e=tonumber(a.speed) +if not e or e<=0 then +e=.08 +end +t.speed=math.max(.01,e) +t.fadeSteps=math.max(0,math.floor(a.fadeSteps or 2)) +local e=a.direction +if type(e)=="string"then +local t=e:lower() +if t=="counterclockwise"or t=="anticlockwise"or t=="ccw"then +e=-1 +else +e=1 +end +elseif type(e)=="number"then +e=e>=0 and 1 or-1 +else +e=1 +end +t.direction=e +t._phase=0 +t._tickTimer=nil +t._paused=a.autoStart==false +if not t._paused then +t:_scheduleTick() +end +return t +end +function v:_cancelTick() +if self._tickTimer then +if I.cancelTimer then +pcall(I.cancelTimer,self._tickTimer) +end +self._tickTimer=nil +end +end +function v:_scheduleTick() +self:_cancelTick() +if self._paused then +return +end +if not self.speed or self.speed<=0 then +return +end +self._tickTimer=I.startTimer(self.speed) +end +function v:start() +if not self._paused then +return +end +self._paused=false +self:_scheduleTick() +end +function v:stop() +if self._paused then +return +end +self._paused=true +self:_cancelTick() +end +function v:setSpeed(e) +if e==nil then +return +end +local e=tonumber(e) +if not e then +return +end +if e<=0 then +self.speed=0 +self:_cancelTick() +return +end +e=math.max(.01,e) +if e~=self.speed then +self.speed=e +if not self._paused then +self:_scheduleTick() +end +end +end +function v:setDirection(e) +if e==nil then +return +end +local e=e +if type(e)=="string"then +local t=e:lower() +if t=="counterclockwise"or t=="anticlockwise"or t=="ccw"then +e=-1 +else +e=1 +end +elseif type(e)=="number"then +e=e>=0 and 1 or-1 +else +e=1 +end +if e~=self.direction then +self.direction=e +end +end +function v:setSegments(e) +if e==nil then +return +end +local e=math.max(3,math.floor(e)) +if e~=self.segmentCount then +self.segmentCount=e +self._phase=self._phase%e +end +end +function v:setThickness(e) +if e==nil then +return +end +local e=math.max(1,math.floor(e)) +self.thickness=e +end +function v:setRadius(e) +if e==nil then +self.radiusPixels=nil +return +end +local e=math.max(2,math.floor(e)) +self.radiusPixels=e +end +function v:setColor(e) +if e==nil then +return +end +t(1,e,"number") +self.color=e +end +function v:setSecondaryColor(e) +if e==nil then +self.secondaryColor=nil +return +end +t(1,e,"number") +self.secondaryColor=e +end +function v:setTertiaryColor(e) +if e==nil then +self.tertiaryColor=nil +return +end +t(1,e,"number") +self.tertiaryColor=e +end +function v:setTrailColor(e) +if e==nil then +self.trailColor=nil +return +end +t(1,e,"number") +self.trailColor=e +end +function v:setTrailPalette(e) +if e~=nil then +t(1,e,"table") +end +self.trailPalette=e and o(e)or nil +end +function v:setFadeSteps(e) +if e==nil then +return +end +local e=math.max(0,math.floor(e)) +self.fadeSteps=e +end +function v:_computeTrailColors() +local e={} +local t=self.trailPalette +if type(t)=="table"then +for a=1,#t do +local t=t[a] +if t then +e[#e+1]=t +end +end +end +if#e==0 then +if self.secondaryColor then +e[#e+1]=self.secondaryColor +end +if self.tertiaryColor then +e[#e+1]=self.tertiaryColor +end +end +local t=math.max(0,math.floor(self.fadeSteps or 0)) +if t>0 then +local a=self.trailColor or e[#e]or self.color +for t=1,t do +e[#e+1]=a +end +elseif#e==0 and self.trailColor then +e[1]=self.trailColor +end +if#e==0 then +e[1]=self.color +end +return e +end +function v:draw(r,d) +if not self.visible then +return +end +local o,i,t,a=self:getAbsoluteRect() +if t<=0 or a<=0 then +return +end +local e=self.bg or self.app.background +n(r,o,i,t,a,e,e) +z(r,o,i,t,a) +if self.border then +p(d,o,i,t,a,self.border,e) +end +local l=(self.border and self.border.left)and 1 or 0 +local c=(self.border and self.border.right)and 1 or 0 +local u=(self.border and self.border.top)and 1 or 0 +local m=(self.border and self.border.bottom)and 1 or 0 +local s=o+l +local h=i+u +local o=math.max(0,t-l-c) +local a=math.max(0,a-u-m) +if o<=0 or a<=0 then +return +end +n(r,s,h,o,a,e,e) +local w=s+(o-1)/2 +local l=h+(a-1)/2 +local i=math.floor(math.min(o,a)/2) +local t=self.radiusPixels and math.floor(self.radiusPixels)or i +if t>i then +t=i +end +if t<1 then +t=1 +end +local n=math.max(1,math.min(math.floor(self.thickness or 1),t)) +local i=t+.35 +local t=math.max(0,t-n+.35) +local m=i*i +local f=t*t +local t=math.max(3,math.floor(self.segmentCount or 12)) +local n=self._phase%t +local c=self.direction>=0 and 1 or-1 +local r=math.pi*2 +local u=self:_computeTrailColors() +for a=0,a-1 do +local h=h+a +local i=h-l +for a=0,o-1 do +local s=s+a +local a=s-w +local l=a*a+i*i +local o=e +if l<=m and l>=f then +local a=math.atan(i,a) +if a<0 then +a=a+r +end +local i=math.floor(a/r*t)%t +local a +if c>=0 then +a=(n-i)%t +else +a=(i-n)%t +end +if a==0 then +o=self.color or e +else +local t=math.floor(a+1e-4) +if t<1 then +t=1 +end +o=u[t]or e +end +end +d.pixel(s,h,o) +end +end +end +function v:handleEvent(a,...) +if a=="timer"then +local e=... +if self._tickTimer and e==self._tickTimer then +self._tickTimer=nil +local t=math.max(3,math.floor(self.segmentCount or 12)) +local e=self.direction>=0 and 1 or-1 +local e=(self._phase+e)%t +if e<0 then +e=e+t +end +self._phase=e +if not self._paused then +self:_scheduleTick() +end +return true +end +end +return s.handleEvent(self,a,...) +end +local function O(e) +local t,a=e.x,e.y +local e=e.parent +while e do +t=t+e.x-1 +a=a+e.y-1 +e=e.parent +end +return t,a +end +function s:_init_base(o,a) +t(1,o,"table") +a=a or{} +t(2,a,"table","nil") +self.app=o +self.parent=nil +self.x=math.floor(a.x or 1) +self.y=math.floor(a.y or 1) +self.width=math.floor(a.width or 1) +self.height=math.floor(a.height or 1) +self.bg=a.bg or e.black +self.fg=a.fg or e.white +self.visible=a.visible~=false +self.z=a.z or 0 +self.id=a.id +self.border=R(a.border) +self.focusable=a.focusable==true +self._focused=false +self.constraints=nil +M("width",self.width) +M("height",self.height) +if a.constraints~=nil then +self.constraints=oe(a.constraints) +local e,t=self:_applySizeConstraints(self.width,self.height) +self.width=e +self.height=t +end +end +function s:setSize(e,t) +M("width",e) +M("height",t) +local t,e=self:_applySizeConstraints(e,t) +self.width=t +self.height=e +end +function s:_applyConstraintLayout() +local t=self.constraints +if not t then +return +end +local e=self.parent +local function s(t) +if not e then +return nil +end +if t=="width"then +return e.width +elseif t=="height"then +return e.height +elseif t=="centerX"then +if e.width then +return(e.width-1)/2+1 +end +elseif t=="centerY"then +if e.height then +return(e.height-1)/2+1 +end +elseif t=="right"then +return e.width +elseif t=="bottom"then +return e.height +elseif t=="left"or t=="x"then +return 1 +elseif t=="top"or t=="y"then +return 1 +end +return nil +end +local function a(e,t) +if not e then +return nil +end +if e.kind=="absolute"then +return e.value +end +if e.kind=="relative"then +local t=s(e.property) +if t==nil then +return nil +end +local e=e.offset or 0 +local e=math.floor(t+e) +return math.max(1,e) +end +if e.kind=="percent"then +local t=s(e.property) +if t==nil then +return nil +end +local a=e.offset or 0 +local e=math.floor(t*e.percent+.5)+a +return math.max(1,e) +end +return nil +end +local o=a(t.width,"width") +local a=a(t.height,"height") +local n=e and e.width or nil +local i=e and e.height or nil +if not o and t.widthPercent and n then +o=math.max(1,math.floor(n*t.widthPercent+.5)) +end +if not a and t.heightPercent and i then +a=math.max(1,math.floor(i*t.heightPercent+.5)) +end +local o=o or self.width +local a=a or self.height +local o,a=self:_applySizeConstraints(o,a) +if o~=self.width or a~=self.height then +self:setSize(o,a) +end +n=e and e.width or nil +i=e and e.height or nil +local function h(i,t,a,o,n) +if not i then +return nil +end +if not e or not a or a<=0 then +return nil +end +local t=i.property or(t=="x"and"centerX"or"centerY") +local e +if t=="centerX"or t=="centerY"then +e=math.floor((a-o)/2)+1 +elseif t=="right"or t=="bottom"or t=="width"or t=="height"then +e=a-o+1 +elseif t=="left"or t=="top"or t=="x"or t=="y"then +e=1 +else +local t=s(t) +if t then +e=math.floor(t-math.floor(o/2)) +else +e=math.floor((a-o)/2)+1 +end +end +local t=(i.offset or 0)+n +e=math.floor(e+t) +if e<1 then +e=1 +end +local t=math.max(1,a-o+1) +if e>t then +e=t +end +return e +end +local o=math.floor(t.offsetX or 0) +local s=math.floor(t.offsetY or 0) +local a=self.x +local e=self.y +local o=h(t.centerX,"x",n,self.width,o) +if o then +a=o +end +local t=h(t.centerY,"y",i,self.height,s) +if t then +e=t +end +if a~=self.x or e~=self.y then +self:setPosition(a,e) +end +end +function s:_applySizeConstraints(e,t) +local a=math.floor(e) +local t=math.floor(t) +if a<1 then +a=1 +end +if t<1 then +t=1 +end +local e=self.constraints +if e then +if e.minWidth and ae.maxWidth then +a=e.maxWidth +end +if e.minHeight and te.maxHeight then +t=e.maxHeight +end +end +return a,t +end +function s:setConstraints(e) +if e==nil or e==false then +self.constraints=nil +else +self.constraints=oe(e) +end +local t,e=self:_applySizeConstraints(self.width,self.height) +if t~=self.width or e~=self.height then +self:setSize(t,e) +end +self:_applyConstraintLayout() +end +local function Y(e) +if not e then +return nil +end +if e.kind=="absolute"then +return e.value +elseif e.kind=="relative"then +local t=string.format("%s.%s",e.target or"parent",e.property or"width") +if e.offset and e.offset~=0 then +return{reference=t,offset=e.offset} +end +return t +elseif e.kind=="percent"then +local t=string.format("%s.%s",e.target or"parent",e.property or"width") +local t={percent=e.percent,of=t} +if e.offset and e.offset~=0 then +t.offset=e.offset +end +return t +end +return nil +end +local function M(e) +if not e then +return nil +end +local t=string.format("%s.%s",e.target or"parent",e.property or"center") +if e.offset and e.offset~=0 then +return{reference=t,offset=e.offset} +end +return t +end +function s:getConstraints() +if not self.constraints then +return nil +end +local e=self.constraints +local t={} +if e.minWidth then +t.minWidth=e.minWidth +end +if e.maxWidth then +t.maxWidth=e.maxWidth +end +if e.minHeight then +t.minHeight=e.minHeight +end +if e.maxHeight then +t.maxHeight=e.maxHeight +end +local a=Y(e.width) +if a~=nil then +t.width=a +end +local a=Y(e.height) +if a~=nil then +t.height=a +end +if e.widthPercent then +t.widthPercent=e.widthPercent +end +if e.heightPercent then +t.heightPercent=e.heightPercent +end +local a=M(e.centerX) +if a~=nil then +t.centerX=a +end +local a=M(e.centerY) +if a~=nil then +t.centerY=a +end +if e.offsetX and e.offsetX~=0 then +t.offsetX=e.offsetX +end +if e.offsetY and e.offsetY~=0 then +t.offsetY=e.offsetY +end +if next(t)then +return t +end +return nil +end +function s:setPosition(a,e) +t(1,a,"number") +t(2,e,"number") +self.x=math.floor(a) +self.y=math.floor(e) +end +function s:setZ(e) +t(1,e,"number") +self.z=e +end +function s:setBorder(e) +if e==nil then +self.border=nil +return +end +if e==false then +self.border=nil +return +end +if e==true then +self.border=R(true) +return +end +t(1,e,"table","boolean") +self.border=R(e) +end +function s:isFocused() +return self._focused +end +function s:setFocused(e) +e=not not e +if self._focused==e then +return +end +self._focused=e +self:onFocusChanged(e) +end +function s:onFocusChanged(e) +end +function s:getAbsoluteRect() +local e,t=O(self) +return e,t,self.width,self.height +end +function s:getSize() +return self.width,self.height +end +function s:containsPoint(e,a) +local t,o,i,n=self:getAbsoluteRect() +return e>=t and e=o and a0 and l>0 then +n(h,d,u,t,l,e,e) +T(r,d,u,t,l,e) +elseif o>0 and a>0 then +n(h,s,i,o,a,e,e) +T(r,s,i,o,a,e) +end +z(h,s,i,o,a) +local n=self.title +if type(n)=="string"and#n>0 then +local o=t>0 and t or o +local s=t>0 and d or s +local a=(a>2)and(i+1)or i +if o>0 then +local t=n +if#t>o then +t=t:sub(1,o) +end +if#t=e.x1 and a<=e.x2 and t>=e.y1 and t<=e.y2 then +return o +end +end +return nil +end +function i:_drawTitleButton(d,m,a,s,l,u) +local e=a.buttonRects and a.buttonRects[s] +if not e then +return +end +local t=a.buttonMetrics and a.buttonMetrics[s] +if not t then +return +end +local o=t.style or{} +local c=math.max(0,t.padding or 0) +local i=e.width-c*2 +if i<=0 then +return +end +local r=o.fg or l +local h=o.bg or u +local t=tostring(o.label or"") +if s=="maximize"then +local e=tostring(o.maximizeLabel or t) +local i=tostring(o.restoreLabel or e) +if a.maximizeState=="restore"then +t=i +r=o.restoreFg or r +h=o.restoreBg or h +else +t=e +end +end +if#t>i then +t=t:sub(1,i) +end +local o=h or u or self.bg or self.app.background +n(d,e.x1,e.y1,e.width,a.barHeight,o,o) +T(m,e.x1,e.y1,e.width,a.barHeight,o) +if#t>0 then +local a=e.x1+c +local i=math.floor((i-#t)/2) +if i>0 then +a=a+i +end +d.text(a,e.y1,t,r or l,o) +end +end +function i:_fillTitleBarPixels(t,e,a) +if not t or not e then +return +end +local n=(e.barX-1)*2+1 +local o=(e.barY-1)*3+1 +local s=e.barWidth*2 +local e=math.min(e.barHeight*3,self.height*3) +for i=0,e-1 do +for e=0,s-1 do +t.pixel(n+e,o+i,a) +end +end +end +function i:_hitTestResize(t,s) +if not self.resizable then +return nil +end +local o,e=O(self) +local n=o+math.max(0,self.width-1) +local i=e+math.max(0,self.height-1) +local a=1 +if self.border and self.border.thickness then +a=math.max(1,math.floor(self.border.thickness)) +end +local e={} +local n=t>=n-a+1 and t<=n +local t=t>=o and t<=o+a-1 +if n then +e.right=true +elseif t then +e.left=true +end +if s>=i-a+1 and s<=i then +e.bottom=true +end +if not e.right and not e.left and not e.bottom then +return nil +end +return e +end +function i:_beginResize(t,a,o,i,e) +if not e then +return +end +self:_restoreFromMaximize() +self._resizing=true +self._resizeSource=t +self._resizeIdentifier=a +self._resizeEdges=e +local e=self.constraints or{} +self._resizeStart={ +pointerX=o, +pointerY=i, +width=self.width, +height=self.height, +x=self.x, +y=self.y, +minWidth=e.minWidth or 1, +minHeight=e.minHeight or 1 +} +self:bringToFront() +if self.app then +self.app:setFocus(nil) +end +end +function i:_updateResize(t,a) +if not self._resizing or not self._resizeStart then +return +end +local e=self._resizeStart +local o=t-e.pointerX +local i=a-e.pointerY +local t=e.width +local a=e.height +if self._resizeEdges.right then +t=e.width+o +elseif self._resizeEdges.left then +t=e.width-o +end +if self._resizeEdges.bottom then +a=e.height+i +end +if tt then +e=t +end +else +if e<1 then +e=1 +end +end +if e~=self.x then +self:setPosition(e,self.y) +end +end +end +function i:_endResize() +self._resizing=false +self._resizeSource=nil +self._resizeIdentifier=nil +self._resizeEdges=nil +self._resizeStart=nil +end +function i:_restoreFromMaximize() +if not self._isMaximized and not self._isMinimized then +return +end +self:restore(true) +end +function i:_computeMaximizedGeometry() +local e=self.parent +if e then +local o,i,n,i,t,a=V(e) +local t=math.max(1,t) +local a=math.max(1,a) +local i=o+1 +local o=n+1 +if self.app and e==self.app.root then +i=1 +o=1 +t=e.width +a=e.height +end +return{x=i,y=o,width=t,height=a} +end +local e=self.app and self.app.root or nil +if e then +return{x=1,y=1,width=e.width,height=e.height} +end +return{x=self.x,y=self.y,width=self.width,height=self.height} +end +function i:_computeMinimizedGeometry() +local t=self._restoreRect or{x=self.x,y=self.y,width=self.width,height=self.height} +local e +if self.minimizedHeight then +e=self.minimizedHeight +else +local a,a,o,t=self:_computeInnerOffsets() +local a=self:_getVisibleTitleBarHeight() +e=o+t+math.max(1,a) +if e<1 then +e=1 +end +end +return{ +x=t.x, +y=t.y, +width=t.width, +height=e +} +end +function i:_captureRestoreRect() +local e={ +x=self.x, +y=self.y, +width=self.width, +height=self.height +} +self._restoreRect=e +self._normalRect=o(e) +end +function i:maximize() +if not self.maximizable or self._isMaximized then +return +end +if self._isMinimized then +self:restore(true) +end +self:_captureRestoreRect() +local e=self:_computeMaximizedGeometry() +self._isMaximized=true +self._isMinimized=false +self:bringToFront() +self:_invalidateTitleLayout() +self:_transitionGeometry("maximize",e,function() +self._restoreRect=self._restoreRect or o(e) +if self.onMaximize then +self:onMaximize() +end +end) +end +function i:restore(a) +if not self._isMaximized and not self._isMinimized then +return +end +local t=self._normalRect or self._restoreRect or{ +x=self.x, +y=self.y, +width=self.width, +height=self.height +} +self._isMaximized=false +self._isMinimized=false +self:_invalidateTitleLayout() +local function e() +self._restoreRect=nil +self._normalRect=nil +if self.onRestore then +self:onRestore() +end +end +if a then +self:_stopGeometryAnimation() +self:_applyGeometry(t) +e() +return +end +self:_transitionGeometry("restore",t,e) +end +function i:toggleMaximize() +if self._isMaximized then +self:restore() +else +if self._isMinimized then +self:restore(true) +end +self:maximize() +end +end +function i:minimize() +if not self.minimizable or self._isMinimized then +return +end +if self._isMaximized then +self:restore(true) +end +self:_captureRestoreRect() +local e=self:_computeMinimizedGeometry() +self._isMinimized=true +self._isMaximized=false +self:bringToFront() +self:_invalidateTitleLayout() +self:_transitionGeometry("minimize",e,function() +if self.onMinimize then +self:onMinimize() +end +end) +end +function i:toggleMinimize() +if self._isMinimized then +self:restore() +else +if self._isMaximized then +self:restore(true) +end +self:minimize() +end +end +function i:isMinimized() +return not not self._isMinimized +end +function i:close() +if self.onClose then +local e=self:onClose() +if e==false then +return +end +end +self:_stopGeometryAnimation() +self.visible=false +self:_endDrag() +self:_endResize() +self._isMaximized=false +self._isMinimized=false +self._restoreRect=nil +self._normalRect=nil +end +function i:_getVisibleTitleBarHeight() +local e=self._titleBar +if not e or not e.enabled then +return 0 +end +local a,a,a,a,a,t=self:_computeInnerOffsets() +if t<=0 then +return 0 +end +local e=math.max(1,math.floor(e.height or 1)) +if e>t then +e=t +end +return e +end +function i:setTitleBar(e) +self._titleBar=ae(e,nil) +self:_refreshTitleBarState() +self:_invalidateTitleLayout() +end +function i:getTitleBar() +local e=self._titleBar +if not e then +return nil +end +local e=o(e) +if e and e.buttons then +local t={} +if e.buttons.close then +t.close=o(e.buttons.close) +end +if e.buttons.maximize then +t.maximize=o(e.buttons.maximize) +end +e.buttons=t +end +return e +end +function i:setDraggable(e) +self.draggable=not not e +end +function i:isDraggable() +return not not self.draggable +end +function i:setResizable(e) +self.resizable=not not e +self:_invalidateTitleLayout() +end +function i:isResizable() +return not not self.resizable +end +function i:setClosable(e) +self.closable=not not e +self:_invalidateTitleLayout() +end +function i:isClosable() +return not not self.closable +end +function i:setMaximizable(e) +self.maximizable=not not e +self:_invalidateTitleLayout() +end +function i:isMaximizable() +return not not self.maximizable +end +function i:setMinimizable(e) +self.minimizable=not not e +self:_invalidateTitleLayout() +end +function i:isMinimizable() +return not not self.minimizable +end +function i:setHideBorderWhenMaximized(e) +local e=not not e +if self.hideBorderWhenMaximized==e then +return +end +self.hideBorderWhenMaximized=e +self:_invalidateTitleLayout() +end +function i:hidesBorderWhenMaximized() +return not not self.hideBorderWhenMaximized +end +function i:setMinimizedHeight(e) +if e==nil then +self.minimizedHeight=nil +if self._isMinimized then +self:_applyGeometry(self:_computeMinimizedGeometry()) +end +return +end +t(1,e,"number") +self.minimizedHeight=math.max(1,math.floor(e)) +if self._isMinimized then +self:_applyGeometry(self:_computeMinimizedGeometry()) +end +end +function i:getMinimizedHeight() +return self.minimizedHeight +end +function i:setGeometryAnimation(e) +if e==nil then +self._geometryAnimation=B(nil) +return +end +t(1,e,"table") +self._geometryAnimation=B(e) +end +function i:setOnMinimize(e) +if e~=nil then +t(1,e,"function") +end +self.onMinimize=e +end +function i:setTitle(e) +b.setTitle(self,e) +self:_invalidateTitleLayout() +end +function i:getContentOffset() +local a,e,t=self:_computeInnerOffsets() +local e=self:_getVisibleTitleBarHeight() +return a,t+e +end +function i:setSize(e,t) +b.setSize(self,e,t) +self:_refreshTitleBarState() +self:_invalidateTitleLayout() +end +function i:setBorder(e) +s.setBorder(self,e) +self:_refreshTitleBarState() +self:_invalidateTitleLayout() +end +function i:bringToFront() +local e=self.parent +if not e then +return +end +e._orderCounter=(e._orderCounter or 0)+1 +self._orderIndex=e._orderCounter +end +function i:_pointInTitleBar(t,a) +local e=self:_computeTitleLayout() +if not e then +return false +end +if self._titleButtonRects then +for o,e in pairs(self._titleButtonRects)do +if t>=e.x1 and t<=e.x2 and a>=e.y1 and a<=e.y2 then +return false +end +end +end +local i=e.barX +local o=e.barY +local n=i+math.max(0,e.barWidth-1) +local e=o+math.max(0,e.barHeight-1) +return t>=i and t<=n and a>=o and a<=e +end +function i:_beginDrag(o,e,t,a) +self:_restoreFromMaximize() +local i,n=O(self) +self._dragging=true +self._dragSource=o +self._dragIdentifier=e +self._dragOffsetX=t-i +self._dragOffsetY=a-n +self:bringToFront() +if self.app then +self.app:setFocus(nil) +end +end +function i:_updateDragPosition(a,t) +if not self._dragging then +return +end +local e=self.parent +local i=self._dragOffsetX or 0 +local o=self._dragOffsetY or 0 +local a=a-i +local t=t-o +if e then +local o,i=O(e) +local h=o +local s=i +local n=o+math.max(0,e.width-self.width) +local e=i+math.max(0,e.height-self.height) +a=te(a,h,n) +t=te(t,s,e) +local e=a-o+1 +local t=t-i+1 +self:setPosition(e,t) +else +self:setPosition(a,t) +end +end +function i:_endDrag() +self._dragging=false +self._dragSource=nil +self._dragIdentifier=nil +self._dragOffsetX=0 +self._dragOffsetY=0 +end +function i:draw(a,o) +if not self.visible then +return +end +local r,h,d,l=self:getAbsoluteRect() +local i,c,u,c,s,t=self:_computeInnerOffsets() +local c=r+i +local u=h+u +local i=self.bg or self.app.background +if s>0 and t>0 then +n(a,c,u,s,t,i,i) +T(o,c,u,s,t,i) +else +n(a,r,h,d,l,i,i) +T(o,r,h,d,l,i) +end +z(a,r,h,d,l) +local u=self._titleBar +local t=nil +local s=nil +if u then +t=self:_computeTitleLayout() +if t then +s=u.bg or i +local s=s or i +local h=u.fg or self.fg or e.white +n(a,t.barX,t.textBaseline,t.barWidth,t.barHeight,s,s) +T(o,t.barX,t.textBaseline,t.barWidth,t.barHeight,s) +local n=t.titleWidth or 0 +local i=self.title or"" +if n>0 and i~=""then +if#i>n then +i=i:sub(1,n) +end +local e=n-#i +local n=u.align or"left" +local o=i +if e>0 then +if n=="center"then +local t=math.floor(e/2) +local e=e-t +o=string.rep(" ",t)..i..string.rep(" ",e) +elseif n=="right"then +o=string.rep(" ",e)..i +else +o=i..string.rep(" ",e) +end +end +a.text(t.titleStart,t.textBaseline,o,h,s) +end +local i=u.fg or self.fg or e.white +local e=t.buttonOrder or{} +for n=1,#e do +local e=e[n] +if e=="maximize"and self.maximizable then +self:_drawTitleButton(a,o,t,e,i,s) +elseif e=="close"and self.closable then +self:_drawTitleButton(a,o,t,e,i,s) +elseif e=="minimize"and self.minimizable then +self:_drawTitleButton(a,o,t,e,i,s) +end +end +end +end +if self:_isBorderVisible()then +p(o,r,h,d,l,self.border,i) +end +local t=M(self._children) +Y(t) +if#t==0 then +return +end +if not(a and a.text and o and o.pixel)then +for e=1,#t do +t[e]:draw(a,o) +end +return +end +local i=r +local e=h +local w=r+d-1 +local f=h+l-1 +local u=(i-1)*2+1 +local r=(e-1)*3+1 +local m=u+d*2-1 +local c=r+l*3-1 +local h=a.text +local s=o.pixel +local function l() +a.text=h +o.pixel=s +end +a.text=function(t,n,a,d,r) +if not a or a==""then +return +end +if nf then +return +end +local t=t +local e=1 +local s=#a +if ts then +return +end +local i=w-t+1 +if i<=0 then +return +end +local i=math.min(s,e+i-1) +if im or tc then +return +end +s(e,t,a) +end +local e,t=pcall(function() +for e=1,#t do +t[e]:draw(a,o) +end +end) +l() +if not e then +error(t,0) +end +end +function i:handleEvent(e,...) +if not self.visible then +return false +end +if e=="mouse_click"then +local o,e,t=... +local a=self:_hitTestTitleButton(e,t) +if a=="close"and self.closable then +self:close() +return true +elseif a=="maximize"and self.maximizable then +self:toggleMaximize() +return true +elseif a=="minimize"and self.minimizable then +self:toggleMinimize() +return true +end +local a=self:_hitTestResize(e,t) +if a then +self:_beginResize("mouse",o,e,t,a) +return true +end +if self.draggable and self:_pointInTitleBar(e,t)then +self:_beginDrag("mouse",o,e,t) +return true +end +elseif e=="mouse_drag"then +local t,e,a=... +if self._resizing and self._resizeSource=="mouse"and t==self._resizeIdentifier then +self:_updateResize(e,a) +return true +end +if self._dragging and self._dragSource=="mouse"and t==self._dragIdentifier then +self:_updateDragPosition(e,a) +return true +end +elseif e=="mouse_up"then +local e=... +if self._resizing and self._resizeSource=="mouse"and e==self._resizeIdentifier then +self:_endResize() +return true +end +if self._dragging and self._dragSource=="mouse"and e==self._dragIdentifier then +self:_endDrag() +return true +end +elseif e=="monitor_touch"then +local o,t,e=... +local a=self:_hitTestTitleButton(t,e) +if a=="close"and self.closable then +self:close() +return true +elseif a=="maximize"and self.maximizable then +self:toggleMaximize() +return true +elseif a=="minimize"and self.minimizable then +self:toggleMinimize() +return true +end +local a=self:_hitTestResize(t,e) +if a then +self:_beginResize("monitor",o,t,e,a) +return true +end +if self.draggable and self:_pointInTitleBar(t,e)then +self:_beginDrag("monitor",o,t,e) +return true +end +elseif e=="monitor_drag"then +local t,e,a=... +if self._resizing and self._resizeSource=="monitor"and t==self._resizeIdentifier then +self:_updateResize(e,a) +return true +end +if self._dragging and self._dragSource=="monitor"and t==self._dragIdentifier then +self:_updateDragPosition(e,a) +return true +end +elseif e=="monitor_up"then +local e=... +if self._resizing and self._resizeSource=="monitor"and e==self._resizeIdentifier then +self:_endResize() +return true +end +if self._dragging and self._dragSource=="monitor"and e==self._dragIdentifier then +self:_endDrag() +return true +end +end +return b.handleEvent(self,e,...) +end +local function A(e) +return e=="mouse_click"or e=="mouse_up"or e=="mouse_drag"or e=="mouse_scroll"or e=="monitor_touch"or e=="monitor_up"or e=="monitor_drag"or e=="monitor_scroll" +end +local function O(e,...) +if e=="mouse_click"or e=="mouse_up"or e=="mouse_drag"then +local a,t,e=... +return t,e +elseif e=="mouse_scroll"then +local a,e,t=... +return e,t +elseif e=="monitor_touch"or e=="monitor_up"or e=="monitor_drag"or e=="monitor_scroll"then +local a,t,e=... +return t,e +end +return nil,nil +end +local T=setmetatable({},{__index=i}) +T.__index=T +function T:new(t,a) +a=a or{} +local t=i.new(i,t,a) +setmetatable(t,T) +local i=a.modal~=false +t.modal=i +local o=a.backdropColor +if o==false then +o=nil +end +if i and o==nil then +o=e.gray +end +t.backdropColor=o +if a.backdropPixelColor~=nil then +if a.backdropPixelColor==false then +t.backdropPixelColor=nil +else +t.backdropPixelColor=a.backdropPixelColor +end +else +t.backdropPixelColor=o +end +t.closeOnBackdrop=a.closeOnBackdrop~=false +t.closeOnEscape=a.closeOnEscape~=false +t._modalRaised=false +if a.resizable==nil then +t:setResizable(false) +end +if a.maximizable==nil then +t:setMaximizable(false) +end +if a.minimizable==nil then +t:setMinimizable(false) +end +return t +end +function T:setModal(t) +t=not not t +if self.modal==t then +return +end +self.modal=t +if t and self.backdropColor==nil then +self.backdropColor=e.black +if self.backdropPixelColor==nil then +self.backdropPixelColor=self.backdropColor +end +end +self._modalRaised=false +end +function T:isModal() +return not not self.modal +end +function T:setBackdropColor(t,e) +if t==false then +self.backdropColor=nil +else +self.backdropColor=t +end +if e==false then +self.backdropPixelColor=nil +elseif e~=nil then +self.backdropPixelColor=e +else +self.backdropPixelColor=self.backdropColor +end +end +function T:getBackdropColor() +return self.backdropColor +end +function T:setCloseOnBackdrop(e) +self.closeOnBackdrop=not not e +end +function T:setCloseOnEscape(e) +self.closeOnEscape=not not e +end +function T:draw(e,t) +if not self.visible then +return +end +if self.modal then +if not self._modalRaised then +self:bringToFront() +self._modalRaised=true +end +else +self._modalRaised=false +end +i.draw(self,e,t) +end +function T:_consumeModalEvent(e,...) +if not self.modal then +return false +end +if e=="key"then +local e=... +if self.closeOnEscape and e==a.escape then +self:close() +return true +end +return true +end +if e=="char"or e=="paste"or e=="key_up"then +return true +end +if A(e)then +local o,a=O(e,...) +local t=false +if o and a then +t=self:containsPoint(o,a) +end +if not t and(e=="mouse_click"or e=="monitor_touch")then +if self.closeOnBackdrop then +self:close() +end +end +return true +end +return false +end +function T:handleEvent(e,...) +if not self.visible then +return false +end +local t=i.handleEvent(self,e,...) +if t then +return true +end +if self.modal then +if self:_consumeModalEvent(e,...)then +return true +end +end +return false +end +function T:close() +local e=self.visible +i.close(self) +if e and not self.visible then +self._modalRaised=false +end +end +local function M(e,t,o) +local a=t +local t=o +if type(e)=="number"then +local e=math.max(0,math.floor(e)) +a,t=e,e +elseif type(e)=="table"then +if e.horizontal~=nil then +a=math.max(0,math.floor(e.horizontal)) +elseif e.x~=nil then +a=math.max(0,math.floor(e.x)) +end +if e.vertical~=nil then +t=math.max(0,math.floor(e.vertical)) +elseif e.y~=nil then +t=math.max(0,math.floor(e.y)) +end +end +return a,t +end +local function O(e,t) +if type(e)~="string"then +return t +end +local e=e:lower() +if e~="left"and e~="center"and e~="right"then +return t +end +return e +end +local A=setmetatable({},{__index=T}) +A.__index=A +function A:new(i,a) +a=a or{} +if a.modal==nil then +a.modal=true +end +if a.resizable==nil then +a.resizable=false +end +local t=T.new(T,i,a) +setmetatable(t,A) +t.autoClose=a.autoClose~=false +t.buttonAlign=O(a.buttonAlign,"center") +t.buttonGap=math.max(0,math.floor(a.buttonGap or 2)) +t.buttonHeight=math.max(1,math.floor(a.buttonHeight or 3)) +t.minButtonWidth=math.max(1,math.floor(a.minButtonWidth or 6)) +t.buttonLabelPadding=math.max(0,math.floor(a.buttonLabelPadding or 2)) +t.buttonAreaSpacing=math.max(0,math.floor(a.buttonAreaSpacing or 1)) +t.contentPaddingX,t.contentPaddingY=M(a.contentPadding,2,1) +t.messagePaddingX,t.messagePaddingY=M(a.messagePadding,1,1) +t.messageFg=a.messageFg or e.lightBlue +t.messageBg=a.messageBg or e.white +t.wrapMessage=a.wrap~=false +t._buttons={} +local o +if a.contentBorder==false then +o=nil +else +o={color=a.contentBorderColor or e.lightGray} +end +t._contentFrame=b:new(i,{ +width=1, +height=1, +bg=t.messageBg, +fg=t.messageFg, +border=o +}) +t._contentFrame.focusable=false +t:addChild(t._contentFrame) +t._messageLabel=E:new(i,{ +text=a.message or"", +wrap=t.wrapMessage, +bg=t.messageBg, +fg=t.messageFg, +width=1, +height=1, +align=a.messageAlign or"left", +verticalAlign=a.messageVerticalAlign or"top" +}) +t._messageLabel.focusable=false +t._contentFrame:addChild(t._messageLabel) +t.onResult=a.onResult +t:setMessage(a.message or"") +t:setButtons(a.buttons) +if a.bg==nil then +t.bg=e.gray +end +t:_updateLayout() +return t +end +function A:setMessage(e) +if e==nil then +e="" +end +e=tostring(e) +self.message=e +if self._messageLabel then +self._messageLabel:setText(e) +end +self:_updateLayout() +end +function A:getMessage() +return self.message or"" +end +function A:setOnResult(e) +if e~=nil then +t(1,e,"function") +end +self.onResult=e +end +function A:_createButtonEntry(t,i) +local a={} +local o +local s=self.autoClose +local h +local d +local l +local r +local n +if type(t)=="string"then +o=t +a.id=t +elseif type(t)=="table"then +o=t.label or t.id or("Button "..tostring(i)) +a.id=t.id or t.value or o +if t.autoClose~=nil then +s=not not t.autoClose +end +if t.width~=nil then +h=math.max(1,math.floor(t.width)) +end +if t.height~=nil then +d=math.max(1,math.floor(t.height)) +end +l=t.bg +r=t.fg +n=t.onSelect +else +error("MsgBox button config at index "..tostring(i).." must be a string or table",3) +end +o=tostring(o) +if not a.id or a.id==""then +a.id=tostring(i) +end +local h=h or math.max(self.minButtonWidth,#o+self.buttonLabelPadding*2) +local i=d or self.buttonHeight +local e=S:new(self.app,{ +label=o, +width=h, +height=i, +bg=l or e.white, +fg=r or e.black +}) +e.focusable=false +a.button=e +a.autoClose=s +a.config=t +a.onSelect=n +e.onClick=function() +self:_handleButtonSelection(a) +end +return a +end +function A:setButtons(e) +if self._buttons then +for e=1,#self._buttons do +local e=self._buttons[e] +if e and e.button and e.button.parent then +e.button.parent:removeChild(e.button) +end +end +end +self._buttons={} +if e==nil then +e={{id="ok",label="OK",autoClose=true}} +end +if type(e)~="table"then +error("MsgBox:setButtons expects a table or nil",2) +end +for t=1,#e do +local e=self:_createButtonEntry(e[t],t) +self._buttons[#self._buttons+1]=e +self:addChild(e.button) +end +self:_updateLayout() +end +function A:_handleButtonSelection(e) +if not e then +return +end +if e.onSelect then +e.onSelect(self,e.id,e.button) +end +local t +if self.onResult then +t=self.onResult(self,e.id,e.button) +end +if e.autoClose and t~=false then +self:close() +end +end +function A:setButtonAlign(e) +self.buttonAlign=O(e,self.buttonAlign) +self:_updateLayout() +end +function A:setAutoClose(e) +self.autoClose=not not e +end +function A:setButtonGap(e) +self.buttonGap=math.max(0,math.floor(e or self.buttonGap)) +self:_updateLayout() +end +function A:_updateLayout() +if not self._contentFrame then +return +end +local o,t,a,t,e,i=self:_computeInnerOffsets() +local t=self:_getVisibleTitleBarHeight() +local n=math.max(1,e) +local e=math.max(1,i-t) +local o=o+1 +local s=a+t+1 +local a=#self._buttons +local i=a>0 and self.buttonHeight or 0 +local t=e +local t=t +if a>0 then +local a=self.buttonAreaSpacing +if e<=i then +a=0 +t=math.max(1,e-i) +else +local o=math.max(0,e-i-1) +if a>o then +a=o +end +t=math.max(1,e-i-a) +end +self._buttonRowY=s+t+a +else +self._buttonRowY=nil +end +self._contentFrame:setPosition(o,s) +self._contentFrame:setSize(n,math.max(1,t)) +local e=math.max(1,n-self.messagePaddingX*2) +local i=math.max(1,t-self.messagePaddingY*2) +self._messageLabel:setSize(e,i) +self._messageLabel:setPosition(self.messagePaddingX+1,self.messagePaddingY+1) +if a>0 then +local e=0 +for t=1,a do +local a=self._buttons[t].button +e=e+a.width +if t>1 then +e=e+self.buttonGap +end +end +local i +if self.buttonAlign=="left"then +i=o +elseif self.buttonAlign=="right"then +i=o+math.max(0,n-e) +else +i=o+math.max(0,math.floor((n-e)/2)) +end +local o=i +local t=self._buttonRowY or(s+t) +for e=1,a do +local e=self._buttons[e] +local e=e.button +e:setSize(e.width,self.buttonHeight) +e:setPosition(o,t) +o=o+e.width+self.buttonGap +end +end +end +function A:setSize(t,e) +i.setSize(self,t,e) +self:_updateLayout() +end +function A:setBorder(e) +i.setBorder(self,e) +self:_updateLayout() +end +function A:setTitleBar(e) +i.setTitleBar(self,e) +self:_updateLayout() +end +function S:new(a,e) +local t=setmetatable({},S) +t:_init_base(a,e) +t.label=(e and e.label)or"Button" +t.onPress=e and e.onPress or nil +t.onRelease=e and e.onRelease or nil +t.onClick=e and e.onClick or nil +if e and e.clickEffect~=nil then +t.clickEffect=not not e.clickEffect +else +t.clickEffect=true +end +t._pressed=false +t.focusable=false +return t +end +function S:setLabel(e) +t(1,e,"string") +self.label=e +end +function S:setOnClick(e) +if e~=nil then +t(1,e,"function") +end +self.onClick=e +end +function S:draw(h,c) +if not self.visible then +return +end +local i,o,a,t=self:getAbsoluteRect() +local s=self.bg or e.gray +local r=self.fg or e.white +local e=s +local d=r +if self.clickEffect and self._pressed then +e,d=d,e +end +local l,u=i+1,o+1 +local s=math.max(0,a-2) +local r=math.max(0,t-2) +if s>0 and r>0 then +n(h,l,u,s,r,e,e) +else +n(h,i,o,a,t,e,e) +end +z(h,i,o,a,t) +if self.border then +p(c,i,o,a,t,self.border,e) +end +local t=self.label or"" +local a=s>0 and s or a +if#t>a then +t=t:sub(1,a) +end +local n=0 +if a>#t then +n=math.floor((a-#t)/2) +end +local t=string.rep(" ",n)..t +if#t0 and l or i +local a +if r>0 then +a=u+math.floor((r-1)/2) +else +a=o +end +h.text(i,a,t,d,e) +end +function S:handleEvent(e,...) +if not self.visible then +return false +end +if e=="mouse_click"then +local a,t,e=... +if self:containsPoint(t,e)then +self.app:setFocus(nil) +self._pressed=true +if self.onPress then +self.onPress(self,a,t,e) +end +return true +end +elseif e=="mouse_drag"then +local a,e,t=... +if self._pressed then +if not self:containsPoint(e,t)then +self._pressed=false +if self.onRelease then +self.onRelease(self,a,e,t) +end +return false +end +return true +end +elseif e=="mouse_up"then +local a,e,t=... +if self._pressed then +self._pressed=false +if self:containsPoint(e,t)then +self.app:setFocus(nil) +if self.onRelease then +self.onRelease(self,a,e,t) +end +if self.onClick then +self.onClick(self,a,e,t) +end +return true +end +end +elseif e=="monitor_touch"then +local a,t,e=... +if self:containsPoint(t,e)then +self.app:setFocus(nil) +if self.onPress then +self.onPress(self,1,t,e) +end +if self.onRelease then +self.onRelease(self,1,t,e) +end +if self.onClick then +self.onClick(self,1,t,e) +end +return true +end +end +return false +end +function E:new(i,e) +e=e or{} +local a=o(e)or{} +a.focusable=false +a.height=math.max(1,math.floor(a.height or 1)) +a.width=math.max(1,math.floor(a.width or 1)) +local t=setmetatable({},E) +t:_init_base(i,a) +t.focusable=false +local a=e and e.text +if a==nil then +a="" +end +t.text=tostring(a) +t.wrap=not not(e and e.wrap) +local a=(e and e.align)and tostring(e.align):lower()or"left" +if a~="left"and a~="center"and a~="right"then +a="left" +end +t.align=a +local e=(e and e.verticalAlign)and tostring(e.verticalAlign):lower()or"top" +if e=="center"then +e="middle" +end +if e~="top"and e~="middle"and e~="bottom"then +e="top" +end +t.verticalAlign=e +t._lines={""} +t._lastInnerWidth=nil +t._lastText=nil +t._lastWrap=nil +t:_updateLines(true) +return t +end +function E:_getInnerMetrics() +local e=self.border +local t=(e and e.left)and 1 or 0 +local a=(e and e.right)and 1 or 0 +local o=(e and e.top)and 1 or 0 +local e=(e and e.bottom)and 1 or 0 +local i=math.max(0,self.width-t-a) +local n=math.max(0,self.height-o-e) +return t,a,o,e,i,n +end +function E:_wrapLine(t,a,e) +if a<=0 then +e[#e+1]="" +return +end +t=t:gsub("\r","") +if t==""then +e[#e+1]="" +return +end +local t=t +while#t>a do +local n=t:sub(1,a) +local o +for e=a,1,-1 do +local t=n:sub(e,e) +if t:match("%s")then +o=e-1 +break +end +end +if o and o>=1 then +local i=t:sub(1,o) +i=i:gsub("%s+$","") +if i==""then +i=t:sub(1,a) +o=a +end +e[#e+1]=i +t=t:sub(o+1) +else +e[#e+1]=n +t=t:sub(a+1) +end +t=t:gsub("^%s+","") +if t==""then +break +end +end +if t~=""then +e[#e+1]=t +elseif#e==0 then +e[#e+1]="" +end +end +function E:_updateLines(e) +local t=tostring(self.text or"") +local i=not not self.wrap +local a,a,a,a,o=self:_getInnerMetrics() +if not e and self._lastText==t and self._lastWrap==i and self._lastInnerWidth==o then +return +end +local e={} +if t==""then +e[1]="" +else +local a=1 +while true do +local n=t:find("\n",a,true) +if not n then +local t=t:sub(a) +t=t:gsub("\r","") +if i then +self:_wrapLine(t,o,e) +else +e[#e+1]=t +end +break +end +local t=t:sub(a,n-1) +t=t:gsub("\r","") +if i then +self:_wrapLine(t,o,e) +else +e[#e+1]=t +end +a=n+1 +end +end +if#e==0 then +e[1]="" +end +self._lines=e +self._lastText=t +self._lastWrap=i +self._lastInnerWidth=o +end +function E:setText(e) +if e==nil then +e="" +end +e=tostring(e) +if self.text~=e then +self.text=e +self:_updateLines(true) +end +end +function E:getText() +return self.text +end +function E:setWrap(e) +e=not not e +if self.wrap~=e then +self.wrap=e +self:_updateLines(true) +end +end +function E:isWrapping() +return self.wrap +end +function E:setHorizontalAlign(e) +if e==nil then +e="left" +else +t(1,e,"string") +end +local t=e:lower() +if t~="left"and t~="center"and t~="right"then +error("Invalid horizontal alignment '"..e.."'",2) +end +if self.align~=t then +self.align=t +end +end +function E:setVerticalAlign(a) +if a==nil then +a="top" +else +t(1,a,"string") +end +local e=a:lower() +if e=="center"then +e="middle" +end +if e~="top"and e~="middle"and e~="bottom"then +error("Invalid vertical alignment '"..a.."'",2) +end +if self.verticalAlign~=e then +self.verticalAlign=e +end +end +function E:setSize(t,e) +s.setSize(self,t,e) +self:_updateLines(true) +end +function E:setBorder(e) +s.setBorder(self,e) +self:_updateLines(true) +end +function E:draw(u,w) +if not self.visible then +return +end +local s,h,d,l=self:getAbsoluteRect() +local r=self.bg or self.app.background or e.black +local f=self.fg or e.white +n(u,s,h,d,l,r,r) +z(u,s,h,d,l) +local e,t,o,t,a,i=self:_getInnerMetrics() +local t=s+e +local m=h+o +self:_updateLines(false) +local c=self._lines or{""} +local o=#c +if o==0 then +c={""} +o=1 +end +if a>0 and i>0 then +local e=math.min(o,i) +local n=1 +if o>e then +if self.verticalAlign=="bottom"then +n=o-e+1 +elseif self.verticalAlign=="middle"then +n=math.floor((o-e)/2)+1 +end +end +local o=0 +if i>e then +if self.verticalAlign=="bottom"then +o=i-e +elseif self.verticalAlign=="middle"then +o=math.floor((i-e)/2) +end +end +local i=m+o +for e=0,e-1 do +local e=c[n+e]or"" +if#e>a then +e=e:sub(1,a) +end +local o=t +if self.align=="center"then +o=t+math.floor((a-#e)/2) +elseif self.align=="right"then +o=t+a-#e +end +if ot+a then +o=t+a-#e +end +if#e>0 then +u.text(o,i,e,f,r) +end +i=i+1 +end +end +if self.border then +p(w,s,h,d,l,self.border,r) +end +end +function _:new(n,t) +t=t or{} +local o=o(t)or{} +local i="Option" +if t and t.label~=nil then +i=tostring(t.label) +end +o.focusable=true +o.height=o.height or 1 +o.width=o.width or math.max(4,#i+4) +local a=setmetatable({},_) +a:_init_base(n,o) +a.focusable=true +a.label=i +a.allowIndeterminate=not not(t and t.allowIndeterminate) +a.indeterminate=not not(t and t.indeterminate) +if not a.allowIndeterminate then +a.indeterminate=false +end +a.checked=not a.indeterminate and not not(t and t.checked) +a.onChange=t and t.onChange or nil +a.focusBg=t and t.focusBg or e.lightGray +a.focusFg=t and t.focusFg or e.black +return a +end +function _:_notifyChange() +if self.onChange then +self.onChange(self,self.checked,self.indeterminate) +end +end +function _:_setState(t,e,o) +t=not not t +e=not not e +if e then +t=false +end +if not self.allowIndeterminate then +e=false +end +local a=(self.checked~=t)or(self.indeterminate~=e) +if not a then +return false +end +self.checked=t +self.indeterminate=e +if not o then +self:_notifyChange() +end +return true +end +function _:setLabel(e) +t(1,e,"string") +self.label=e +end +function _:setOnChange(e) +if e~=nil then +t(1,e,"function") +end +self.onChange=e +end +function _:setAllowIndeterminate(e) +e=not not e +if self.allowIndeterminate==e then +return +end +self.allowIndeterminate=e +if not e and self.indeterminate then +self:_setState(self.checked,false,true) +self:_notifyChange() +end +end +function _:setChecked(e) +t(1,e,"boolean") +self:_setState(e,false,false) +end +function _:isChecked() +return self.checked +end +function _:setIndeterminate(e) +if not self.allowIndeterminate then +if e then +error("Indeterminate state is disabled for this CheckBox",2) +end +return +end +t(1,e,"boolean") +self:_setState(self.checked,e,false) +end +function _:isIndeterminate() +return self.indeterminate +end +function _:toggle() +self:_activate() +end +function _:_activate() +if self.allowIndeterminate then +if self.indeterminate then +self:_setState(false,false,false) +elseif self.checked then +self:_setState(false,true,false) +else +self:_setState(true,false,false) +end +else +if self.indeterminate then +self:_setState(true,false,false) +else +self:_setState(not self.checked,false,false) +end +end +end +function _:draw(d,a) +if not self.visible then +return +end +local h,s,t,i=self:getAbsoluteRect() +local o=self.bg or e.black +local e=self.fg or e.white +local o=o +local r=e +if self:isFocused()then +o=self.focusBg or o +r=self.focusFg or r +end +n(d,h,s,t,i,o,o) +z(d,h,s,t,i) +if self.border then +p(a,h,s,t,i,self.border,o) +end +if t<=0 or i<=0 then +return +end +local e=" " +if self.indeterminate then +e="-" +elseif self.checked then +e="x" +end +local a="["..e.."]" +local e={} +e[#e+1]=a +local a=#a +if t>a then +e[#e+1]=" " +a=a+1 +end +if t>a then +local o=self.label or"" +local t=t-a +if#o>t then +o=o:sub(1,t) +end +e[#e+1]=o +a=a+#o +end +local e=table.concat(e) +if#et then +e=e:sub(1,t) +end +local t=s+math.floor((i-1)/2) +d.text(h,t,e,r,o) +end +function _:handleEvent(e,...) +if not self.visible then +return false +end +if e=="mouse_click"then +local a,t,e=... +if self:containsPoint(t,e)then +self.app:setFocus(self) +self:_activate() +return true +end +elseif e=="monitor_touch"then +local a,e,t=... +if self:containsPoint(e,t)then +self.app:setFocus(self) +self:_activate() +return true +end +elseif e=="key"then +if not self:isFocused()then +return false +end +local e=... +if e==a.space or e==a.enter then +self:_activate() +return true +end +end +return false +end +function g:new(i,t) +t=t or{} +local o=o(t)or{} +o.focusable=true +o.height=math.max(1,math.floor(o.height or 3)) +o.width=math.max(4,math.floor(o.width or 10)) +local a=setmetatable({},g) +a:_init_base(i,o) +a.focusable=true +local o=t.value +if o==nil then +o=t.on +end +a.value=not not o +a.labelOn=(t and t.labelOn)or"On" +a.labelOff=(t and t.labelOff)or"Off" +a.trackColorOn=(t and t.trackColorOn)or(t and t.onColor)or e.green +a.trackColorOff=(t and t.trackColorOff)or(t and t.offColor)or e.red +a.trackColorDisabled=(t and t.trackColorDisabled)or e.lightGray +a.thumbColor=(t and t.thumbColor)or e.white +a.knobColorDisabled=(t and t.knobColorDisabled)or e.lightGray +a.onLabelColor=t and t.onLabelColor or nil +a.offLabelColor=t and t.offLabelColor or nil +a.focusBg=t and t.focusBg or e.lightGray +a.focusFg=t and t.focusFg or e.black +a.focusOutline=t and t.focusOutline or a.focusFg or e.white +a.showLabel=not(t and t.showLabel==false) +a.disabled=not not(t and t.disabled) +a.onChange=t and t.onChange or nil +a.knobMargin=math.max(0,math.floor(t.knobMargin or 0)) +if t.knobWidth~=nil then +if type(t.knobWidth)~="number"then +error("Toggle knobWidth must be a number",3) +end +a.knobWidth=math.max(1,math.floor(t.knobWidth)) +else +a.knobWidth=nil +end +if t.transitionDuration~=nil then +if type(t.transitionDuration)~="number"then +error("Toggle transitionDuration must be a number",3) +end +a.transitionDuration=math.max(0,t.transitionDuration) +else +a.transitionDuration=.2 +end +local e=t.transitionEasing +if type(e)=="string"then +e=H[e]or H.easeInOutQuad +elseif type(e)~="function"then +e=H.easeInOutQuad +end +a.transitionEasing=e +a._thumbProgress=a.value and 1 or 0 +a._animationHandle=nil +return a +end +function g:_cancelAnimation() +if self._animationHandle then +self._animationHandle:cancel() +self._animationHandle=nil +end +end +function g:_setThumbProgress(e) +if e==nil then +e=self.value and 1 or 0 +end +if e<0 then +e=0 +elseif e>1 then +e=1 +end +self._thumbProgress=e +end +function g:_animateThumb(e) +e=math.max(0,math.min(1,e or(self.value and 1 or 0))) +if self.disabled then +self:_cancelAnimation() +self:_setThumbProgress(e) +return +end +if not self.app or self.transitionDuration<=0 then +self:_cancelAnimation() +self:_setThumbProgress(e) +return +end +local t=self._thumbProgress +if t==nil then +t=self.value and 1 or 0 +end +if math.abs(t-e)<1e-4 then +self:_cancelAnimation() +self:_setThumbProgress(e) +return +end +self:_cancelAnimation() +local a=e-t +local o=self.transitionEasing or H.easeInOutQuad +self._animationHandle=self.app:animate({ +duration=self.transitionDuration, +easing=o, +update=function(e) +local e=t+a*e +if e<0 then +e=0 +elseif e>1 then +e=1 +end +self._thumbProgress=e +end, +onComplete=function() +self._thumbProgress=e +self._animationHandle=nil +end, +onCancel=function() +self._animationHandle=nil +end +}) +end +function g:_emitChange() +if self.onChange then +self.onChange(self,self.value) +end +end +function g:setOnChange(e) +if e~=nil then +t(1,e,"function") +end +self.onChange=e +end +function g:setValue(e,t) +e=not not e +if self.value==e then +self:_animateThumb(e and 1 or 0) +return +end +self.value=e +self:_animateThumb(e and 1 or 0) +if not t then +self:_emitChange() +end +end +function g:isOn() +return self.value +end +function g:toggle() +if self.disabled then +return +end +self:setValue(not self.value) +end +function g:setLabels(e,a) +if e~=nil then +t(1,e,"string") +self.labelOn=e +end +if a~=nil then +t(2,a,"string") +self.labelOff=a +end +end +function g:setShowLabel(e) +self.showLabel=not not e +end +function g:setDisabled(e) +e=not not e +if self.disabled==e then +return +end +self.disabled=e +if e then +self:_cancelAnimation() +self:_setThumbProgress(self.value and 1 or 0) +else +self:_animateThumb(self.value and 1 or 0) +end +end +function g:isDisabled() +return self.disabled +end +function g:setColors(e,o,a,s,n,i,h) +if e~=nil then +t(1,e,"number") +self.trackColorOn=e +end +if o~=nil then +t(2,o,"number") +self.trackColorOff=o +end +if a~=nil then +t(3,a,"number") +self.thumbColor=a +end +if s~=nil then +t(4,s,"number") +self.onLabelColor=s +end +if n~=nil then +t(5,n,"number") +self.offLabelColor=n +end +if i~=nil then +t(6,i,"number") +self.trackColorDisabled=i +end +if h~=nil then +t(7,h,"number") +self.knobColorDisabled=h +end +end +function g:setTransition(a,e) +if a~=nil then +t(1,a,"number") +self.transitionDuration=math.max(0,a) +end +if e~=nil then +if type(e)=="string"then +local t=H[e] +if not t then +error("Unknown easing '"..e.."'",2) +end +self.transitionEasing=t +elseif type(e)=="function"then +self.transitionEasing=e +else +error("Toggle transition easing must be a function or easing name",2) +end +end +end +function g:setKnobStyle(a,e) +if a~=nil then +t(1,a,"number") +self.knobWidth=math.max(1,math.floor(a)) +end +if e~=nil then +t(2,e,"number") +self.knobMargin=math.max(0,math.floor(e)) +end +end +function g:draw(l,d) +if not self.visible then +return +end +local a,t,s,o=self:getAbsoluteRect() +local i=self.bg or e.black +local w=self.fg or e.white +n(l,a,t,s,o,i,i) +z(l,a,t,s,o) +if self.border then +p(d,a,t,s,o,self.border,i) +end +local h,r,s,r,o,i=V(self) +if o<=0 or i<=0 then +return +end +local a=a+h +local s=t+s +local t=o +local o=i +local h=self._thumbProgress +if h==nil then +h=self.value and 1 or 0 +end +if h<0 then +h=0 +elseif h>1 then +h=1 +end +local y=self.trackColorOn or e.green +local m=self.trackColorOff or e.red +local c=self.trackColorDisabled or m +local i=self.disabled and c or m +n(l,a,s,t,o,i,i) +local i=math.floor(t*h+.5) +if i>0 then +if i>t then +i=t +end +local e=self.disabled and c or y +n(l,a,s,i,o,e,e) +end +local r=self.knobMargin or 0 +if r<0 then +r=0 +end +if r*2>=t then +r=math.max(0,math.floor((t-1)/2)) +end +local u=math.max(1,t-r*2) +local i=self.knobWidth and math.max(1,math.min(math.floor(self.knobWidth),u)) +if not i then +i=math.max(1,math.floor(u/2)) +if u>=4 then +i=math.max(2,i) +end +end +local u=math.max(0,u-i) +local f=math.floor(u*h+.5) +if f>u then +f=u +end +local u=a+r+f +if u+i-1>a+t-1 then +u=a+t-i +elseif u0 then +local e=math.max(0,t-2) +if e>0 and#i>e then +i=i:sub(1,e) +end +local r=self.value and(self.onLabelColor or w)or(self.offLabelColor or w) +local n +if h>=.5 then +n=self.disabled and c or y +else +n=self.disabled and c or m +end +local o=s+math.floor((o-1)/2) +local e=a+math.floor((t-#i)/2) +if ea+t-1 then +e=a+t-#i +end +if#i>0 then +l.text(e,o,i,r,n) +end +end +if self:isFocused()then +local e=self.focusOutline or self.focusFg or e.white +if t>0 then +for t=0,t-1 do +d.pixel(a+t,s,e) +if o>1 then +d.pixel(a+t,s+o-1,e) +end +end +end +if o>0 then +for o=0,o-1 do +d.pixel(a,s+o,e) +if t>1 then +d.pixel(a+t-1,s+o,e) +end +end +end +end +if self.disabled then +local i=self.knobColorDisabled or e.lightGray +for e=0,t-1,2 do +local e=a+e +d.pixel(e,s,i) +if o>1 then +d.pixel(e,s+o-1,i) +end +end +end +end +function g:handleEvent(e,...) +if not self.visible then +return false +end +if e=="mouse_click"or e=="monitor_touch"then +local a,t,e=... +if self:containsPoint(t,e)then +if self.disabled then +return true +end +self.app:setFocus(self) +self:toggle() +return true +end +elseif e=="key"then +if not self:isFocused()or self.disabled then +return false +end +local e=... +if e==a.space or e==a.enter then +self:toggle() +return true +end +elseif e=="char"then +if not self:isFocused()or self.disabled then +return false +end +local e=... +if e==" "then +self:toggle() +return true +end +end +return false +end +function q:new(n,a) +a=a or{} +local o=o(a)or{} +local i="Option" +if a and a.label~=nil then +i=tostring(a.label) +end +o.focusable=true +o.height=o.height or 1 +o.width=o.width or math.max(4,#i+4) +local t=setmetatable({},q) +t:_init_base(n,o) +t.focusable=true +t.label=i +if a and a.value~=nil then +t.value=a.value +else +t.value=i +end +if a and a.group~=nil then +if type(a.group)~="string"then +error("RadioButton group must be a string",2) +end +t.group=a.group +else +t.group=nil +end +t.selected=not not(a and a.selected) +t.onChange=a and a.onChange or nil +t.focusBg=a and a.focusBg or e.lightGray +t.focusFg=a and a.focusFg or e.black +t._registeredGroup=nil +t._dotChar=se +if t.group and t.app then +t:_registerWithGroup() +if t.selected then +t.app:_selectRadioInGroup(t.group,t,true) +else +local e=t.app._radioGroups +if e then +local e=e[t.group] +if e and e.selected and e.selected~=t then +t.selected=false +end +end +end +end +t:_applySelection(t.selected,true) +return t +end +function q:_registerWithGroup() +if self.app and self.group then +self.app:_registerRadioButton(self) +end +end +function q:_unregisterFromGroup() +if self.app and self._registeredGroup then +self.app:_unregisterRadioButton(self) +end +end +function q:_notifyChange() +if self.onChange then +self.onChange(self,self.selected,self.value) +end +end +function q:_applySelection(e,t) +e=not not e +if self.selected==e then +return +end +self.selected=e +if not t then +self:_notifyChange() +end +end +function q:setLabel(e) +t(1,e,"string") +self.label=e +end +function q:setValue(e) +self.value=e +end +function q:getValue() +return self.value +end +function q:setGroup(e) +t(1,e,"string","nil") +if self.group==e then +return +end +self:_unregisterFromGroup() +self.group=e +if self.group then +self:_registerWithGroup() +end +end +function q:getGroup() +return self.group +end +function q:setOnChange(e) +if e~=nil then +t(1,e,"function") +end +self.onChange=e +end +function q:setSelected(e) +e=not not e +if self.group and self.app then +if e then +self.app:_selectRadioInGroup(self.group,self,false) +else +local e=self.app._radioGroups +local e=e and e[self.group] +if e and e.selected==self then +self.app:_selectRadioInGroup(self.group,nil,false) +else +self:_applySelection(false,false) +end +end +return +end +if self.selected==e then +return +end +self:_applySelection(e,false) +end +function q:isSelected() +return self.selected +end +function q:_activate() +if self.group then +if not self.selected then +self:setSelected(true) +end +else +self:setSelected(not self.selected) +end +end +function q:draw(h,d) +if not self.visible then +return +end +local o,i,t,s=self:getAbsoluteRect() +local a=self.bg or e.black +local e=self.fg or e.white +local a=a +local r=e +if self:isFocused()then +a=self.focusBg or a +r=self.focusFg or r +end +n(h,o,i,t,s,a,a) +z(h,o,i,t,s) +if self.border then +p(d,o,i,t,s,self.border,a) +end +local n=i+math.floor((s-1)/2) +local e=self.selected and(self._dotChar or"*")or" " +local e="("..e..")" +local i=self.label or"" +local e=e +if#i>0 then +e=e.." "..i +end +if#e>t then +e=e:sub(1,t) +elseif#e0 then +h.text(o,n,e,r,a) +end +end +function q:handleEvent(e,...) +if not self.visible then +return false +end +if e=="mouse_click"then +local a,e,t=... +if self:containsPoint(e,t)then +self.app:setFocus(self) +self:_activate() +return true +end +elseif e=="monitor_touch"then +local a,e,t=... +if self:containsPoint(e,t)then +self.app:setFocus(self) +self:_activate() +return true +end +elseif e=="key"then +if not self:isFocused()then +return false +end +local e=... +if e==a.space or e==a.enter then +self:_activate() +return true +end +end +return false +end +function k:new(i,a) +a=a or{} +local o=o(a)or{} +o.focusable=false +o.height=o.height or 1 +o.width=o.width or 12 +local t=setmetatable({},k) +t:_init_base(i,o) +t.focusable=false +t.min=type(a.min)=="number"and a.min or 0 +t.max=type(a.max)=="number"and a.max or 1 +if t.max<=t.min then +t.max=t.min+1 +end +local o=a.value +if type(o)~="number"then +o=t.min +end +t.value=t:_clampValue(o) +t.trackColor=(a.trackColor)or e.gray +t.fillColor=(a.fillColor)or e.lightBlue +t.textColor=(a.textColor)or t.fg or e.white +t.label=a.label or nil +t.showPercent=not not a.showPercent +t.indeterminate=not not a.indeterminate +t.indeterminateSpeed=math.max(.1,a.indeterminateSpeed or 1.2) +t._indeterminateProgress=0 +t._animationHandle=nil +if not t.border then +t.border=R(true) +end +if t.indeterminate then +t:_startIndeterminateAnimation() +end +return t +end +function k:_clampValue(e) +if type(e)~="number"then +e=self.min +end +if eself.max then +return self.max +end +return e +end +function k:_stopIndeterminateAnimation() +if self._animationHandle then +self._animationHandle:cancel() +self._animationHandle=nil +end +self._indeterminateProgress=0 +end +function k:_startIndeterminateAnimation() +if not self.app or self._animationHandle then +return +end +local e=self.indeterminateSpeed or 1.2 +self._animationHandle=self.app:animate({ +duration=e, +easing=H.linear, +update=function(t,e) +self._indeterminateProgress=e or 0 +end, +onComplete=function() +self._animationHandle=nil +if self.indeterminate then +self:_startIndeterminateAnimation() +else +self._indeterminateProgress=0 +end +end, +onCancel=function() +self._animationHandle=nil +end +}) +end +function k:setRange(a,e) +t(1,a,"number") +t(2,e,"number") +if e<=a then +error("ProgressBar max must be greater than min",2) +end +self.min=a +self.max=e +self.value=self:_clampValue(self.value) +end +function k:getRange() +return self.min,self.max +end +function k:setValue(e) +if self.indeterminate then +return +end +t(1,e,"number") +e=self:_clampValue(e) +if e~=self.value then +self.value=e +end +end +function k:getValue() +return self.value +end +function k:getPercent() +local e=self.max-self.min +if e<=0 then +return 0 +end +return(self.value-self.min)/e +end +function k:setIndeterminate(e) +e=not not e +if self.indeterminate==e then +return +end +self.indeterminate=e +if e then +self:_startIndeterminateAnimation() +else +self:_stopIndeterminateAnimation() +end +end +function k:isIndeterminate() +return self.indeterminate +end +function k:setLabel(e) +if e~=nil then +t(1,e,"string") +end +self.label=e +end +function k:setShowPercent(e) +self.showPercent=not not e +end +function k:setColors(e,a,o) +if e~=nil then +t(1,e,"number") +self.trackColor=e +end +if a~=nil then +t(2,a,"number") +self.fillColor=a +end +if o~=nil then +t(3,o,"number") +self.textColor=o +end +end +function k:draw(o,a) +if not self.visible then +return +end +local t,s,l,r=self:getAbsoluteRect() +local i=self.trackColor or(self.bg or e.gray) +local h=self.fillColor or e.lightBlue +local f=self.textColor or(self.fg or e.white) +n(o,t,s,l,r,i,i) +z(o,t,s,l,r) +if self.border then +p(a,t,s,l,r,self.border,i) +end +local e=self.border +local c=(e and e.left)and 1 or 0 +local m=(e and e.right)and 1 or 0 +local u=(e and e.top)and 1 or 0 +local e=(e and e.bottom)and 1 or 0 +local a=t+c +local d=s+u +local t=math.max(0,l-c-m) +local s=math.max(0,r-u-e) +if t<=0 or s<=0 then +return +end +n(o,a,d,t,s,i,i) +local l=0 +local u=0 +local r=0 +if self.indeterminate then +r=math.max(1,math.floor(t/3)) +if r>t then +r=t +end +local t=t-r +local e=self._indeterminateProgress or 0 +if e<0 then e=0 end +if e>1 then e=1 end +u=math.floor(t*e+.5) +n(o,a+u,d,r,s,h,h) +else +local e=self:getPercent() +if e<0 then e=0 end +if e>1 then e=1 end +l=math.floor(t*e+.5) +if l>0 then +n(o,a,d,l,s,h,h) +end +end +local e=self.label or"" +if self.showPercent and not self.indeterminate then +local t=math.floor(self:getPercent()*100+.5) +local t=tostring(t).."%" +if e~=""then +e=e.." "..t +else +e=t +end +end +if e~=""and s>0 then +if#e>t then +e=e:sub(1,t) +end +local s=d+math.floor((s-1)/2) +local n=a+math.floor((t-#e)/2) +if n=u and eo then +return +end +if i>a then +return +end +local e=e +local o=0 +if i<1 then +o=1-i +if o>=#e then +return +end +e=e:sub(o+1) +i=1 +end +local a=a-i+1 +if a<=0 then +return +end +if#e>a then +e=e:sub(1,a) +end +r.text(s+i-1,h+n-1,e,c or u,d or l) +end +t.pixel=function(e,n,i) +local t=math.floor(e or 1) +local e=math.floor(n or 1) +if t<1 or t>a or e<1 or e>o then +return +end +d.pixel(s+t-1,h+e-1,i or u) +end +self._ctx=t +self.onDraw(self,t) +end +if self.border then +p(d,s,h,a,o,self.border,self.bg or self.app.background or e.black) +end +end +function u:new(i,a) +a=a or{} +local o=o(a)or{} +o.focusable=true +o.width=o.width or 12 +if o.height==nil then +o.height=a.showValue and 2 or 1 +end +local t=setmetatable({},u) +t:_init_base(i,o) +t.focusable=true +t.min=type(a.min)=="number"and a.min or 0 +t.max=type(a.max)=="number"and a.max or 1 +if t.max<=t.min then +t.max=t.min+1 +end +if a.step~=nil then +if type(a.step)~="number"then +error("Slider step must be a number",2) +end +t.step=a.step>0 and a.step or 0 +else +t.step=0 +end +t.range=not not a.range +t.showValue=not not a.showValue +t.trackColor=a.trackColor or e.gray +t.fillColor=a.fillColor or e.lightBlue +t.handleColor=a.handleColor or e.white +if a.formatValue~=nil then +if type(a.formatValue)~="function"then +error("Slider formatValue must be a function",2) +end +t.formatValue=a.formatValue +else +t.formatValue=nil +end +t.onChange=a.onChange +t._activeHandle=nil +t._focusedHandle=t.range and"lower"or"single" +t._dragging=false +if t.range then +local o +local e +if type(a.value)=="table"then +o=a.value[1] +e=a.value[2] +end +if type(a.startValue)=="number"then +o=a.startValue +end +if type(a.endValue)=="number"then +e=a.endValue +end +if type(o)~="number"then +o=t.min +end +if type(e)~="number"then +e=t.max +end +if o>e then +o,e=e,o +end +t.lowerValue=t:_applyStep(o) +t.upperValue=t:_applyStep(e) +if t.lowerValue>t.upperValue then +t.lowerValue,t.upperValue=t.upperValue,t.lowerValue +end +else +local e=a.value +if type(e)~="number"then +e=t.min +end +t.value=t:_applyStep(e) +end +if not t.border then +t.border=R(true) +end +return t +end +function u:_clampValue(e) +if type(e)~="number"then +e=self.min +end +if eself.max then +return self.max +end +return e +end +function u:_applyStep(e) +e=self:_clampValue(e) +local t=self.step or 0 +if t>0 then +local a=(e-self.min)/t +e=self.min+math.floor(a+.5)*t +e=self:_clampValue(e) +end +return e +end +function u:_getInnerMetrics() +local e=self.border +local a=(e and e.left)and 1 or 0 +local o=(e and e.right)and 1 or 0 +local t=(e and e.top)and 1 or 0 +local e=(e and e.bottom)and 1 or 0 +local i,n=self:getAbsoluteRect() +local s=math.max(0,self.width-a-o) +local o=math.max(0,self.height-t-e) +local h=i+a +local i=n+t +return h,i,s,o,a,t,e +end +function u:_valueToPosition(o,a) +if a<=1 then +return 0 +end +local t=self.max-self.min +local e=0 +if t>0 then +e=(o-self.min)/t +end +if e<0 then +e=0 +elseif e>1 then +e=1 +end +return math.floor(e*(a-1)+.5) +end +function u:_positionToValue(e,t) +if t<=1 then +return self.min +end +if e<0 then +e=0 +elseif e>t-1 then +e=t-1 +end +local e=e/(t-1) +local e=self.min+(self.max-self.min)*e +return self:_applyStep(e) +end +function u:_notifyChange() +if not self.onChange then +return +end +if self.range then +self.onChange(self,self.lowerValue,self.upperValue) +else +self.onChange(self,self.value) +end +end +function u:setOnChange(e) +if e~=nil then +t(1,e,"function") +end +self.onChange=e +end +function u:_setSingleValue(e,t) +e=self:_applyStep(e) +if self.value~=e then +self.value=e +if not t then +self:_notifyChange() +end +return true +end +return false +end +function u:setValue(e) +if self.range then +return +end +t(1,e,"number") +self:_setSingleValue(e,false) +end +function u:getValue() +return self.value +end +function u:_setLowerValue(e,t) +e=self:_applyStep(e) +if eself.upperValue then +e=self.upperValue +end +if self.lowerValue~=e then +self.lowerValue=e +if not t then +self:_notifyChange() +end +return true +end +return false +end +function u:_setUpperValue(e,t) +e=self:_applyStep(e) +if e>self.max then +e=self.max +end +if ea then +e,a=a,e +end +local t=false +t=self:_setLowerValue(e,true)or t +t=self:_setUpperValue(a,true)or t +if t and not o then +self:_notifyChange() +end +end +function u:getRangeValues() +return self.lowerValue,self.upperValue +end +function u:setRangeLimits(e,a) +t(1,e,"number") +t(2,a,"number") +if a<=e then +error("Slider max must be greater than min",2) +end +self.min=e +self.max=a +if self.range then +local e=false +e=self:_setLowerValue(self.lowerValue,true)or e +e=self:_setUpperValue(self.upperValue,true)or e +if e then +self:_notifyChange() +end +else +if self:_setSingleValue(self.value,true)then +self:_notifyChange() +end +end +end +function u:setStep(e) +if e==nil then +e=0 +else +t(1,e,"number") +end +if e<=0 then +self.step=0 +else +self.step=e +end +if self.range then +local e=false +e=self:_setLowerValue(self.lowerValue,true)or e +e=self:_setUpperValue(self.upperValue,true)or e +if e then +self:_notifyChange() +end +else +if self:_setSingleValue(self.value,true)then +self:_notifyChange() +end +end +end +function u:setShowValue(e) +self.showValue=not not e +end +function u:setColors(a,e,o) +if a~=nil then +t(1,a,"number") +self.trackColor=a +end +if e~=nil then +t(2,e,"number") +self.fillColor=e +end +if o~=nil then +t(3,o,"number") +self.handleColor=o +end +end +function u:_formatNumber(o) +local a=self.step or 0 +local e +if a>0 then +local t=0 +local a=a +while a<1 and t<4 do +a=a*10 +t=t+1 +end +local t="%0."..tostring(t).."f" +e=t:format(o) +else +e=string.format("%0.2f",o) +end +if e:find(".",1,true)then +e=e:gsub("0+$","") +e=e:gsub("%.$","") +end +return e +end +function u:_formatDisplayValue() +if self.formatValue then +local t,e +if self.range then +t,e=pcall(self.formatValue,self,self.lowerValue,self.upperValue) +else +t,e=pcall(self.formatValue,self,self.value) +end +if t and type(e)=="string"then +return e +end +end +if self.range then +return self:_formatNumber(self.lowerValue).." - "..self:_formatNumber(self.upperValue) +end +return self:_formatNumber(self.value) +end +function u:_getStepForNudge(t) +local e=self.step or 0 +if e<=0 then +e=(self.max-self.min)/math.max(1,(self.range and 20 or 40)) +end +if e<=0 then +e=1 +end +if t and t>1 then +e=e*t +end +return e +end +function u:_positionFromPoint(e) +local a,o,t=self:_getInnerMetrics() +if t<=0 then +return nil,t +end +local e=math.floor(e-a) +if e<0 then +e=0 +elseif e>t-1 then +e=t-1 +end +return e,t +end +function u:_beginInteraction(e) +local t,a=self:_positionFromPoint(e) +if not t then +return false +end +if self.range then +local n=self:_valueToPosition(self.lowerValue,a) +local s=self:_valueToPosition(self.upperValue,a) +local e=self._focusedHandle or"lower" +local i=math.abs(t-n) +local o=math.abs(t-s) +if i==o then +if t>s then +e="upper" +elseif t=0 and 1 or-1 +local e=math.abs(e) +local e=self:_getStepForNudge(e) +e=e*t +if self.range then +local t=self._focusedHandle or"lower" +if t=="upper"then +self:_setUpperValue(self.upperValue+e) +else +self:_setLowerValue(self.lowerValue+e) +end +else +self:_setSingleValue(self.value+e) +end +end +function u:onFocusChanged(e) +if e then +if self.range then +if self._focusedHandle~="lower"and self._focusedHandle~="upper"then +self._focusedHandle="lower" +end +else +self._focusedHandle="single" +end +end +end +function u:draw(a,f) +if not self.visible then +return +end +local l,d,h,r=self:getAbsoluteRect() +local s=self.bg or self.app.background or e.black +n(a,l,d,h,r,s,s) +z(a,l,d,h,r) +local i,c,t,u=self:_getInnerMetrics() +if t<=0 or u<=0 then +if self.border then +p(f,l,d,h,r,self.border,s) +end +return +end +local o +local m=nil +if self.showValue and u>=2 then +m=c +o=c+u-1 +else +o=c+math.floor((u-1)/2) +end +n(a,i,o,t,1,self.trackColor,self.trackColor) +local u +if self:isFocused()then +u=self._activeHandle or self._focusedHandle +end +local function c(n,s) +if n<0 or n>=t then +return +end +local t=self.handleColor or e.white +if u and s==u then +t=self.fg or e.white +end +a.text(i+n,o," ",t,t) +end +if self.range then +local e=self:_valueToPosition(self.lowerValue,t) +local t=self:_valueToPosition(self.upperValue,t) +if t0 then +n(a,i+e,o,s,1,self.fillColor,self.fillColor) +end +c(e,"lower") +c(t,"upper") +else +local t=self:_valueToPosition(self.value,t) +local e=t+1 +if e>0 then +n(a,i,o,e,1,self.fillColor,self.fillColor) +end +c(t,"single") +end +if self.showValue and m then +local o=self:_formatDisplayValue() +if o and o~=""then +if#o>t then +o=o:sub(1,t) +end +local t=i+math.floor((t-#o)/2) +if t0 then +self:_nudgeValue(1) +elseif e<0 then +self:_nudgeValue(-1) +end +return true +end +elseif t=="key"then +if not self:isFocused()then +return false +end +local e=... +if e==a.left or e==a.down then +self:_nudgeValue(-1) +return true +elseif e==a.right or e==a.up then +self:_nudgeValue(1) +return true +elseif e==a.home then +if self.range then +self:setRangeValues(self.min,self.upperValue) +self._focusedHandle="lower" +else +self:setValue(self.min) +end +return true +elseif e==a["end"]then +if self.range then +self:setRangeValues(self.lowerValue,self.max) +self._focusedHandle="upper" +else +self:setValue(self.max) +end +return true +elseif e==a.tab then +if self.range then +self:_switchFocusedHandle() +return true +end +elseif e==a.pageUp then +self:_nudgeValue(-5) +return true +elseif e==a.pageDown then +self:_nudgeValue(5) +return true +end +elseif t=="key_up"then +if self._activeHandle then +self:_endInteraction() +end +end +return false +end +function l:new(i,a) +a=a or{} +local o=o(a)or{} +o.focusable=true +o.width=math.max(8,math.floor(o.width or 24)) +o.height=math.max(3,math.floor(o.height or 7)) +local t=setmetatable({},l) +t:_init_base(i,o) +t.focusable=true +t.headerBg=a.headerBg or t.bg or e.gray +t.headerFg=a.headerFg or t.fg or e.white +t.rowBg=a.rowBg or t.bg or e.black +t.rowFg=a.rowFg or t.fg or e.white +t.highlightBg=a.highlightBg or e.lightBlue +t.highlightFg=a.highlightFg or e.black +t.zebra=not not a.zebra +t.zebraBg=a.zebraBg or e.gray +t.placeholder=a.placeholder or"No rows" +t.allowRowSelection=a.selectable~=false +t.sortColumn=a.sortColumn +t.sortDirection=(a.sortDirection=="desc")and"desc"or"asc" +t.onSelect=a.onSelect or nil +t.onSort=a.onSort or nil +t.columns={} +t.data={} +t._rows={} +t._columnMetrics={} +t._totalColumnWidth=0 +t.scrollOffset=1 +t.selectedIndex=0 +t.typeSearchTimeout=a.typeSearchTimeout or .75 +t._typeSearch={buffer="",lastTime=0} +t.columns=t:_normalizeColumns(a.columns or{}) +t:_recomputeColumnMetrics() +t:setData(a.data or{}) +if a.selectedIndex then +t:setSelectedIndex(a.selectedIndex,true) +end +if t.sortColumn then +t:setSort(t.sortColumn,t.sortDirection,true) +end +if not t.border then +t.border=R(true) +end +t.scrollbar=L(a.scrollbar,t.bg or e.black,t.fg or e.white) +t:_ensureSelectionVisible() +return t +end +function l:_normalizeColumns(e) +local a={} +if type(e)=="table"then +for t=1,#e do +local e=e[t] +if type(e)~="table"then +error("Table column configuration must be a table",3) +end +local t=e.id or e.key or e.title +if type(t)~="string"or t==""then +error("Table column is missing an id",3) +end +local e={ +id=t, +title=e.title or t, +key=e.key or t, +accessor=e.accessor, +format=e.format, +comparator=e.comparator, +color=e.color, +align=e.align or"left", +sortable=e.sortable~=false, +width=math.max(3,math.floor(e.width or 10)) +} +a[#a+1]=e +end +end +return a +end +function l:_recomputeColumnMetrics() +self._columnMetrics={} +local t=0 +for a=1,#self.columns do +local e=self.columns[a] +e.width=math.max(3,math.floor(e.width or 10)) +self._columnMetrics[a]={ +offset=t, +width=e.width +} +t=t+e.width +end +self._totalColumnWidth=t +end +function l:_ensureColumnsForData() +if#self.columns>0 then +return +end +local e=self.data[1] +if type(e)=="table"then +local t={} +for e,a in pairs(e)do +if type(e)=="string"then +t[#t+1]={ +id=e, +title=e, +key=e, +align="left", +sortable=true, +width=math.max(3,math.min(20,tostring(a or""):len()+2)) +} +end +end +table.sort(t,function(e,t) +return e.id4 then +e[#e+1]="..." +break +end +e[#e+1]=tostring(t).."="..tostring(o) +end +table.sort(e,function(e,t) +return e#self._rows then +self.selectedIndex=1 +end +else +self.selectedIndex=0 +end +self:_clampScroll() +self:_ensureSelectionVisible() +end +function l:_getColumnById(e) +if not e then +return nil +end +for t=1,#self.columns do +if self.columns[t].id==e then +return self.columns[t] +end +end +return nil +end +function l:_applySort(e,a,u) +local t=self:_getColumnById(e) +if not t or t.sortable==false then +return +end +self.sortColumn=t.id +self.sortDirection=a=="desc"and"desc"or"asc" +local d=self.sortDirection=="desc" +local r=t.comparator +table.sort(self._rows,function(h,s) +local n=self.data[h] +local i=self.data[s] +local o=l._resolveColumnValue(t,n) +local a=l._resolveColumnValue(t,i) +local e=0 +if r then +local a,t=pcall(r,o,a,n,i,t) +if a and type(t)=="number"then +e=t +end +end +if e==0 then +if type(o)=="number"and type(a)=="number"then +if oa then +e=1 +else +e=0 +end +else +local t=tostring(o or""):lower() +local a=tostring(a or""):lower() +if ta then +e=1 +else +e=0 +end +end +end +if e==0 then +return h0 +end +return e<0 +end) +if not u and self.onSort then +self.onSort(self,self.sortColumn,self.sortDirection) +end +self:_ensureSelectionVisible() +end +function l:setSort(e,a,t) +if e==nil then +self.sortColumn=nil +self.sortDirection="asc" +self:_refreshRows() +return +end +self:_applySort(e,a or self.sortDirection,t) +end +function l:getSort() +return self.sortColumn,self.sortDirection +end +function l:setOnSort(e) +if e~=nil then +t(1,e,"function") +end +self.onSort=e +end +function l:setScrollbar(t) +self.scrollbar=L(t,self.bg or e.black,self.fg or e.white) +self:_clampScroll() +end +function l:setOnSelect(e) +if e~=nil then +t(1,e,"function") +end +self.onSelect=e +end +function l:getSelectedIndex() +return self.selectedIndex +end +function l:getSelectedRow() +if self.selectedIndex>=1 and self.selectedIndex<=#self._rows then +return self.data[self._rows[self.selectedIndex]] +end +return nil +end +function l:setSelectedIndex(e,a) +if not self.allowRowSelection then +self.selectedIndex=0 +return +end +if#self._rows==0 then +self.selectedIndex=0 +self.scrollOffset=1 +return +end +t(1,e,"number") +e=math.floor(e) +if e<1 then +e=1 +elseif e>#self._rows then +e=#self._rows +end +local t=e~=self.selectedIndex +self.selectedIndex=e +self:_ensureSelectionVisible() +if t and not a then +self:_notifySelect() +end +end +function l:_notifySelect() +if self.onSelect then +self.onSelect(self,self:getSelectedRow(),self.selectedIndex) +end +end +function l:_getInnerMetrics() +local e=self.border +local a=(e and e.left)and 1 or 0 +local i=(e and e.right)and 1 or 0 +local t=(e and e.top)and 1 or 0 +local n=(e and e.bottom)and 1 or 0 +local e,o=self:getAbsoluteRect() +local i=math.max(0,self.width-a-i) +local n=math.max(0,self.height-t-n) +local a=e+a +local e=o+t +return a,e,i,n +end +function l:_computeLayoutMetrics() +local i,s,t,a=self:_getInnerMetrics() +if t<=0 or a<=0 then +return{ +innerX=i, +innerY=s, +innerWidth=t, +innerHeight=a, +headerHeight=0, +rowsHeight=0, +contentWidth=0, +scrollbarWidth=0, +scrollbarStyle=nil, +scrollbarX=i +} +end +local h=a>=1 and 1 or 0 +local r=math.max(0,a-h) +local e,n=F(self.scrollbar,#self._rows,r,t) +if e>0 and t-e<1 then +e=math.max(0,t-1) +if e<=0 then +e=0 +n=nil +end +end +local o=t-e +if o<1 then +o=t +e=0 +n=nil +end +return{ +innerX=i, +innerY=s, +innerWidth=t, +innerHeight=a, +headerHeight=h, +rowsHeight=r, +contentWidth=o, +scrollbarWidth=e, +scrollbarStyle=n, +scrollbarX=i+o +} +end +function l:_getRowsVisible() +local e=self:_computeLayoutMetrics() +if e.innerWidth<=0 or e.innerHeight<=0 or e.contentWidth<=0 then +return 0 +end +local e=e.rowsHeight +if e<0 then +e=0 +end +return e +end +function l:_clampScroll() +local e=self:_getRowsVisible() +if#self._rows==0 or e<=0 then +self.scrollOffset=1 +return +end +local e=math.max(1,#self._rows-e+1) +if self.scrollOffset<1 then +self.scrollOffset=1 +elseif self.scrollOffset>e then +self.scrollOffset=e +end +end +function l:_ensureSelectionVisible() +self:_clampScroll() +if not self.allowRowSelection or self.selectedIndex<1 or self.selectedIndex>#self._rows then +return +end +local e=self:_getRowsVisible() +if e<=0 then +return +end +if self.selectedIndexself.scrollOffset+e-1 then +self.scrollOffset=self.selectedIndex-e+1 +end +self:_clampScroll() +end +function l:_rowFromPoint(a,t) +if not self:containsPoint(a,t)then +return nil +end +local e=self:_computeLayoutMetrics() +if e.innerWidth<=0 or e.innerHeight<=0 or e.contentWidth<=0 then +return nil +end +local o=e.innerY+e.headerHeight +if t=o+e.rowsHeight then +return nil +end +if a=e.innerX+e.contentWidth then +return nil +end +local t=t-o +if t<0 or t>=e.rowsHeight then +return nil +end +local e=self.scrollOffset+t +if e<1 or e>#self._rows then +return nil +end +return e +end +function l:_columnFromPoint(a,t) +if not self:containsPoint(a,t)then +return nil +end +local e=self:_computeLayoutMetrics() +if e.innerWidth<=0 or e.innerHeight<=0 or e.contentWidth<=0 then +return nil +end +if e.headerHeight<=0 or t~=e.innerY then +return nil +end +if a=e.innerX+e.contentWidth then +return nil +end +local n=e.contentWidth +local t=e.innerX +for o=1,#self.columns do +local i=math.max(1,math.min(self.columns[o].width,n)) +if o==#self.columns then +i=e.innerX+e.contentWidth-t +end +if i<=0 then +break +end +if a>=t and a0 then +a.text(i,d,string.rep(" ",o),w,w) +local n=i +local h=o +for e=1,#self.columns do +local s=self.columns[e] +local t=math.max(1,math.min(s.width,h)) +if e==#self.columns then +t=math.max(1,h) +end +if t<=0 then +break +end +local e=s.title or s.id +local r="" +if self.sortColumn==s.id then +r=self.sortDirection=="desc"and"v"or"^" +end +if r~=""and t>=2 then +if#e>=t then +e=e:sub(1,t-1) +end +e=e..r +elseif t>#e then +e=e..string.rep(" ",t-#e) +else +e=e:sub(1,t) +end +a.text(n,d,e,q,w) +n=n+t +h=o-(n-i) +if h<=0 then +break +end +end +end +local v=d+v +local h=j +local w=self.rowBg or s +local b=self.rowFg or b +if h<=0 then +if y>0 then +local e=(r and r.background)or s +n(a,t.scrollbarX,d,y,g,e,e) +end +if self.border then +p(k,f,m,u,c,self.border,s) +end +return +end +if#self._rows==0 then +for e=0,h-1 do +local e=v+e +a.text(i,e,string.rep(" ",o),b,w) +end +if self.placeholder and self.placeholder~=""then +local t=self.placeholder +if#t>o then +t=t:sub(1,o) +end +local n=h>0 and math.min(h-1,math.floor(h/2))or 0 +local n=v+n +local o=i+math.floor((o-#t)/2) +if o#self._rows then +a.text(i,c,string.rep(" ",o),b,w) +else +local n=self._rows[t] +local u=self.data[n] +local m=self.allowRowSelection and t==self.selectedIndex +local h=w +local d=b +if m then +h=self.highlightBg or e.lightGray +d=self.highlightFg or e.black +elseif self.zebra and(t%2==0)then +h=self.zebraBg or h +end +local s=i +local r=o +for e=1,#self.columns do +local t=self.columns[e] +local n=math.max(1,math.min(t.width,r)) +if e==#self.columns then +n=math.max(1,r) +end +if n<=0 then +break +end +local e=l._resolveColumnValue(t,u) +e=self:_formatCell(t,u,e) +if#e>n then +e=e:sub(1,n) +end +if t.align=="right"then +if#e0 then +local e=(r and r.background)or s +n(a,t.scrollbarX,d,y,g,e,e) +if r and h>0 then +local e=math.max(0,self.scrollOffset-1) +C(a,t.scrollbarX,v,h,#self._rows,h,e,r) +end +end +if self.border then +p(k,f,m,u,c,self.border,s) +end +end +function l:_handleTypeSearch(t) +if not t or t==""then +return +end +local e=self._typeSearch +if not e then +e={buffer="",lastTime=0} +self._typeSearch=e +end +local a=I.clock() +local o=self.typeSearchTimeout or .75 +if a-(e.lastTime or 0)>o then +e.buffer="" +end +e.buffer=(e.buffer or"")..t:lower() +e.lastTime=a +self:_searchForPrefix(e.buffer) +end +function l:_searchForPrefix(e) +if not e or e==""then +return +end +if#self._rows==0 then +return +end +local t=self.selectedIndex>=1 and self.selectedIndex or 0 +for a=1,#self._rows do +local t=((t+a-1)%#self._rows)+1 +local o=self.data[self._rows[t]] +local a=self.columns[1] +local a=l._resolveColumnValue(a,o) +local a=tostring(a or""):lower() +if a:sub(1,#e)==e then +self:setSelectedIndex(t) +return +end +end +end +function l:onFocusChanged(e) +if not e and self._typeSearch then +self._typeSearch.buffer="" +self._typeSearch.lastTime=0 +end +end +function l:handleEvent(o,...) +if not self.visible then +return false +end +local function i(a,t) +if not self:containsPoint(a,t)then +return false +end +self.app:setFocus(self) +local e=self:_computeLayoutMetrics() +if e.scrollbarStyle and e.scrollbarWidth>0 and e.rowsHeight>0 then +local i=e.scrollbarX +local o=e.innerY+e.headerHeight +if a>=i and a=o and t0 then +self:setSelectedIndex(math.max(1,(self.selectedIndex>0)and(self.selectedIndex-1)or 1)) +end +return true +elseif e==a.down then +if self.allowRowSelection and#self._rows>0 then +self:setSelectedIndex(math.min(#self._rows,(self.selectedIndex>0 and self.selectedIndex or 0)+1)) +end +return true +elseif e==a.home then +if self.allowRowSelection and#self._rows>0 then +self:setSelectedIndex(1) +else +self.scrollOffset=1 +end +return true +elseif e==a["end"]then +if self.allowRowSelection and#self._rows>0 then +self:setSelectedIndex(#self._rows) +else +self.scrollOffset=math.max(1,#self._rows-self:_getRowsVisible()+1) +self:_clampScroll() +end +return true +elseif e==a.pageUp then +local e=math.max(1,self:_getRowsVisible()-1) +self.scrollOffset=self.scrollOffset-e +self:_clampScroll() +if self.allowRowSelection and self.selectedIndex>0 then +self:setSelectedIndex(math.max(1,self.selectedIndex-e),true) +self:_notifySelect() +end +return true +elseif e==a.pageDown then +local e=math.max(1,self:_getRowsVisible()-1) +self.scrollOffset=self.scrollOffset+e +self:_clampScroll() +if self.allowRowSelection and self.selectedIndex>0 then +self:setSelectedIndex(math.min(#self._rows,self.selectedIndex+e),true) +self:_notifySelect() +end +return true +elseif e==a.enter then +if self.allowRowSelection then +self:_notifySelect() +end +return true +elseif e==a.space then +if self.allowRowSelection then +self:_notifySelect() +end +return true +end +end +return false +end +function c:new(i,a) +a=a or{} +local o=o(a)or{} +o.focusable=true +o.height=math.max(3,math.floor(o.height or 7)) +o.width=math.max(6,math.floor(o.width or 20)) +local t=setmetatable({},c) +t:_init_base(i,o) +t.focusable=true +t.highlightBg=(a and a.highlightBg)or e.lightGray +t.highlightFg=(a and a.highlightFg)or e.black +t.placeholder=(a and a.placeholder)or nil +t.indentWidth=math.max(1,math.floor((a and a.indentWidth)or 2)) +local o=(a and a.toggleSymbols)or{} +t.toggleSymbols={ +expanded=tostring(o.expanded or"-"), +collapsed=tostring(o.collapsed or"+"), +leaf=tostring(o.leaf or" ") +} +t.onSelect=a and a.onSelect or nil +t.onToggle=a and a.onToggle or nil +t.nodes={} +t._flatNodes={} +t.scrollOffset=1 +t.selectedNode=nil +t._selectedIndex=0 +t.typeSearchTimeout=(a and a.typeSearchTimeout)or .75 +t._typeSearch={buffer="",lastTime=0} +if not t.border then +t.border=R(true) +end +t.scrollbar=L(a and a.scrollbar,t.bg or e.black,t.fg or e.white) +t:setNodes((a and a.nodes)or{}) +return t +end +function c:setOnSelect(e) +if e~=nil then +t(1,e,"function") +end +self.onSelect=e +end +function c:setOnToggle(e) +if e~=nil then +t(1,e,"function") +end +self.onToggle=e +end +function c:setScrollbar(t) +self.scrollbar=L(t,self.bg or e.black,self.fg or e.white) +self:_ensureSelectionVisible() +end +function c:_copyNodes(e,i) +local a={} +if type(e)~="table"then +return a +end +for o=1,#e do +local e=e[o] +if e~=nil then +local t +if type(e)=="string"then +t={ +label=e, +data=nil, +expanded=false +} +elseif type(e)=="table"then +t={ +label=e.label and tostring(e.label)or string.format("Node %d",o), +data=e.data, +expanded=not not e.expanded +} +else +t={ +label=tostring(e), +data=nil, +expanded=false +} +end +t.parent=i +if e and type(e.children)=="table"and#e.children>0 then +t.children=self:_copyNodes(e.children,t) +if t.expanded==nil then +t.expanded=false +end +else +t.children={} +t.expanded=false +end +a[#a+1]=t +end +end +return a +end +function c:setNodes(e) +e=e or{} +t(1,e,"table") +local a=self.selectedNode +local t=self._selectedIndex +self.nodes=self:_copyNodes(e,nil) +self.scrollOffset=1 +self.selectedNode=nil +self._selectedIndex=0 +self:_rebuildFlatNodes() +local e=self.selectedNode +if a~=e or self._selectedIndex~=t then +self:_notifySelect() +end +end +function c:getSelectedNode() +return self.selectedNode +end +function c:setSelectedNode(e) +if e==nil then +if self.selectedNode~=nil then +self.selectedNode=nil +self._selectedIndex=0 +self:_notifySelect() +end +return +end +self:_selectNode(e,false) +end +function c:expandNode(e) +self:_toggleNode(e,true) +end +function c:collapseNode(e) +self:_toggleNode(e,false) +end +function c:toggleNode(e) +self:_toggleNode(e,nil) +end +function c:_rebuildFlatNodes() +local t={} +local function a(e,o) +for i=1,#e do +local e=e[i] +t[#t+1]={node=e,depth=o} +if e.expanded and e.children and#e.children>0 then +a(e.children,o+1) +end +end +end +a(self.nodes,0) +self._flatNodes=t +local e=self:_findVisibleIndex(self.selectedNode) +if e then +self._selectedIndex=e +elseif#t>0 then +self._selectedIndex=1 +self.selectedNode=t[1].node +else +self._selectedIndex=0 +self.selectedNode=nil +end +self:_ensureSelectionVisible() +end +function c:_findVisibleIndex(e) +if e==nil then +return nil +end +local t=self._flatNodes +for a=1,#t do +if t[a].node==e then +return a +end +end +return nil +end +function c:_getInnerMetrics() +local e=self.border +local t=(e and e.left)and 1 or 0 +local o=(e and e.right)and 1 or 0 +local a=(e and e.top)and 1 or 0 +local e=(e and e.bottom)and 1 or 0 +local n=math.max(0,self.width-t-o) +local i=math.max(0,self.height-a-e) +return t,o,a,e,n,i +end +function c:_getInnerHeight() +local t,t,t,t,t,e=self:_getInnerMetrics() +if e<1 then +e=1 +end +return e +end +function c:_computeLayoutMetrics() +local o,i=self:getAbsoluteRect() +local e,s,n,s,t,a=self:_getInnerMetrics() +local o=o+e +local s=i+n +if t<=0 or a<=0 then +return{ +innerX=o, +innerY=s, +innerWidth=t, +innerHeight=a, +contentWidth=0, +scrollbarWidth=0, +scrollbarStyle=nil, +scrollbarX=o +} +end +local e,n=F(self.scrollbar,#self._flatNodes,a,t) +if e>0 and t-e<1 then +e=math.max(0,t-1) +if e<=0 then +e=0 +n=nil +end +end +local i=t-e +if i<1 then +i=t +e=0 +n=nil +end +return{ +innerX=o, +innerY=s, +innerWidth=t, +innerHeight=a, +contentWidth=i, +scrollbarWidth=e, +scrollbarStyle=n, +scrollbarX=o+i +} +end +function c:_ensureSelectionVisible() +local e=#self._flatNodes +local t=self:_getInnerHeight() +if e==0 then +self.scrollOffset=1 +return +end +if self._selectedIndex<1 then +self._selectedIndex=1 +elseif self._selectedIndex>e then +self._selectedIndex=e +end +if self.scrollOffset<1 then +self.scrollOffset=1 +end +local e=math.max(1,e-t+1) +if self.scrollOffset>e then +self.scrollOffset=e +end +if self._selectedIndexself.scrollOffset+t-1 then +self.scrollOffset=self._selectedIndex-t+1 +if self.scrollOffset>e then +self.scrollOffset=e +end +end +end +function c:_setSelectedIndex(e,a) +local t=#self._flatNodes +if t==0 then +self.selectedNode=nil +self._selectedIndex=0 +self.scrollOffset=1 +if not a then +self:_notifySelect() +end +return +end +if e<1 then +e=1 +elseif e>t then +e=t +end +self._selectedIndex=e +self.selectedNode=self._flatNodes[e].node +self:_ensureSelectionVisible() +if not a then +self:_notifySelect() +end +end +function c:_selectNode(t,a) +if not t then +return +end +local e=t.parent +while e do +if not e.expanded then +e.expanded=true +end +e=e.parent +end +self:_rebuildFlatNodes() +local e=self:_findVisibleIndex(t) +if e then +self:_setSelectedIndex(e,a) +end +end +function c:_moveSelection(a) +if a==0 then +return +end +local t=#self._flatNodes +if t==0 then +return +end +local e=self._selectedIndex +if e<1 then +e=1 +end +e=e+a +if e<1 then +e=1 +elseif e>t then +e=t +end +self:_setSelectedIndex(e,false) +end +function c:_scrollBy(t) +if t==0 then +return +end +local e=#self._flatNodes +if e==0 then +self.scrollOffset=1 +return +end +local a=self:_getInnerHeight() +local e=math.max(1,e-a+1) +self.scrollOffset=math.min(e,math.max(1,self.scrollOffset+t)) +end +function c:_rowFromPoint(i,t) +if not self:containsPoint(i,t)then +return nil +end +local e=self:_computeLayoutMetrics() +if e.innerWidth<=0 or e.innerHeight<=0 or e.contentWidth<=0 then +return nil +end +local o=e.innerX +local a=e.innerY +if i=o+e.contentWidth then +return nil +end +if t=a+e.innerHeight then +return nil +end +local t=t-a +local t=self.scrollOffset+t +if t<1 or t>#self._flatNodes then +return nil +end +return t,o,e.contentWidth +end +function c:_toggleNode(e,a) +if not e or not e.children or#e.children==0 then +return false +end +local t +if a==nil then +t=not e.expanded +else +t=not not a +end +if e.expanded==t then +return false +end +e.expanded=t +self:_rebuildFlatNodes() +if self.onToggle then +self.onToggle(self,e,t) +end +return true +end +function c:_notifySelect() +if self.onSelect then +self.onSelect(self,self.selectedNode,self._selectedIndex) +end +end +function c:onFocusChanged(e) +if not e and self._typeSearch then +self._typeSearch.buffer="" +self._typeSearch.lastTime=0 +end +end +function c:_searchForPrefix(e) +if not e or e==""then +return +end +local a=self._flatNodes +local t=#a +if t==0 then +return +end +local o=self._selectedIndex>=1 and self._selectedIndex or 0 +for i=1,t do +local t=((o+i-1)%t)+1 +local a=a[t].node +local a=a and a.label or"" +if a:lower():sub(1,#e)==e then +self:_setSelectedIndex(t,false) +return +end +end +end +function c:_handleTypeSearch(t) +if not t or t==""then +return +end +local e=self._typeSearch +if not e then +e={buffer="",lastTime=0} +self._typeSearch=e +end +local a=I.clock() +local o=self.typeSearchTimeout or .75 +if a-(e.lastTime or 0)>o then +e.buffer="" +end +e.buffer=e.buffer..t:lower() +e.lastTime=a +self:_searchForPrefix(e.buffer) +end +function c:draw(s,h) +if not self.visible then +return +end +local a,i,o,t=self:getAbsoluteRect() +local r=self.bg or e.black +local m=self.fg or e.white +n(s,a,i,o,t,r,r) +z(s,a,i,o,t) +if self.border then +p(h,a,i,o,t,self.border,r) +end +local o=self:_computeLayoutMetrics() +local t=o.innerWidth +local h=o.innerHeight +local i=o.contentWidth +local c=o.scrollbarWidth +local d=o.scrollbarStyle +if t<=0 or h<=0 or i<=0 then +return +end +local u=o.innerX +local l=o.innerY +local f=self._flatNodes +local w=#f +if w==0 then +for e=0,h-1 do +s.text(u,l+e,string.rep(" ",i),m,r) +end +local t=self.placeholder +if type(t)=="string"and#t>0 then +local t=t +if#t>i then +t=t:sub(1,i) +end +local a=u+math.floor((i-#t)/2) +if a0 then +local e=(d and d.background)or r +n(s,o.scrollbarX,l,c,h,e,e) +if d then +C(s,o.scrollbarX,l,h,0,h,0,d) +end +end +return +end +for t=0,h-1 do +local d=l+t +local h=self.scrollOffset+t +if h>w then +s.text(u,d,string.rep(" ",i),m,r) +else +local t=f[h] +local o=t.node +local t=t.depth or 0 +local t=t*self.indentWidth +if t>i-1 then +t=i-1 +end +if t<0 then +t=0 +end +local a=t>0 and string.rep(" ",t)or"" +local n +if o and o.children and#o.children>0 then +n=o.expanded and self.toggleSymbols.expanded or self.toggleSymbols.collapsed +else +n=self.toggleSymbols.leaf +end +n=tostring(n or" ") +local t=i-t +local a=a +if t>0 then +local e=n:sub(1,1) +a=a..e +t=t-1 +end +if t>0 then +a=a.." " +t=t-1 +end +if t>0 then +local e=(o and o.label)or"" +if#e>t then +e=e:sub(1,t) +end +a=a..e +t=t-#e +end +if t>0 then +a=a..string.rep(" ",t) +elseif#a>i then +a=a:sub(1,i) +end +local o=r +local t=m +if h==self._selectedIndex then +o=self.highlightBg or e.lightGray +t=self.highlightFg or e.black +end +s.text(u,d,a,t,o) +end +end +if c>0 then +local e=(d and d.background)or r +n(s,o.scrollbarX,l,c,h,e,e) +if d then +C(s,o.scrollbarX,l,h,#self._flatNodes,h,math.max(0,self.scrollOffset-1),d) +end +end +end +function c:handleEvent(t,...) +if not self.visible then +return false +end +if t=="mouse_click"then +local e,a,t=... +local o,n,i=self:_rowFromPoint(a,t) +if o then +self.app:setFocus(self) +local e=self:_computeLayoutMetrics() +if e.scrollbarStyle and e.scrollbarWidth>0 then +local o=e.scrollbarX +if a>=o and a=e.innerY and ti-1 then +t=i-1 +end +local o=n+t +if e.node and e.node.children and#e.node.children>0 and t=o and a0 then +local o=e.scrollbarX +if a>=o and a=e.innerY and ti-1 then +e=i-1 +end +local o=n+e +if t.node and t.node.children and#t.node.children>0 and e=o and a0 then +self:_scrollBy(1) +elseif e<0 then +self:_scrollBy(-1) +end +return true +end +elseif t=="key"then +if not self:isFocused()then +return false +end +local e=... +if e==a.up then +self:_moveSelection(-1) +return true +elseif e==a.down then +self:_moveSelection(1) +return true +elseif e==a.pageUp then +self:_moveSelection(-self:_getInnerHeight()) +return true +elseif e==a.pageDown then +self:_moveSelection(self:_getInnerHeight()) +return true +elseif e==a.home then +self:_setSelectedIndex(1,false) +return true +elseif e==a["end"]then +self:_setSelectedIndex(#self._flatNodes,false) +return true +elseif e==a.left then +local e=self.selectedNode +if e then +if e.children and#e.children>0 and e.expanded then +self:_toggleNode(e,false) +return true +elseif e.parent then +self:_selectNode(e.parent,false) +return true +end +end +elseif e==a.right then +local e=self.selectedNode +if e and e.children and#e.children>0 then +if not e.expanded then +self:_toggleNode(e,true) +else +local e=e.children[1] +if e then +self:_selectNode(e,false) +end +end +return true +end +elseif e==a.enter or e==a.space then +local e=self.selectedNode +if e and e.children and#e.children>0 then +self:_toggleNode(e,nil) +else +self:_notifySelect() +end +return true +end +elseif t=="char"then +local e=... +if self:isFocused()and e and#e>0 then +self:_handleTypeSearch(e:sub(1,1)) +return true +end +elseif t=="paste"then +local e=... +if self:isFocused()and e and#e>0 then +self:_handleTypeSearch(e:sub(1,1)) +return true +end +end +return false +end +local function O(e,a,t) +if et then +return t +end +return e +end +local function M(r,a,t,h,s,n) +if not r then +return +end +n=n or e.white +local i=math.abs(h-a) +local l=a=o then +e=e+o +a=a+l +end +if n<=i then +e=e+i +t=t+d +end +end +end +function w:new(i,t) +t=t or{} +local o=o(t)or{} +o.focusable=true +o.height=math.max(3,math.floor(o.height or 8)) +o.width=math.max(6,math.floor(o.width or 18)) +local a=setmetatable({},w) +a:_init_base(i,o) +a.focusable=true +a.data={} +a.labels={} +a.chartType="bar" +a.showAxis=not(t and t.showAxis==false) +a.showLabels=not(t and t.showLabels==false) +a.placeholder=(t and t.placeholder)or"No data" +a.barColor=(t and t.barColor)or e.lightBlue +a.highlightColor=(t and t.highlightColor)or e.orange +a.axisColor=(t and t.axisColor)or(a.fg or e.white) +a.lineColor=(t and t.lineColor)or(a.fg or e.white) +a.selectable=not(t and t.selectable==false) +if t and type(t.rangePadding)=="number"then +a.rangePadding=math.max(0,t.rangePadding) +else +a.rangePadding=.05 +end +if t and type(t.minValue)=="number"then +a.minValue=t.minValue +else +a.minValue=nil +end +if t and type(t.maxValue)=="number"then +a.maxValue=t.maxValue +else +a.maxValue=nil +end +a.onSelect=t and t.onSelect or nil +a.selectedIndex=nil +a._lastLayout=nil +if t and t.chartType then +a:setChartType(t.chartType) +end +if t and t.labels then +a:setLabels(t.labels) +end +if t and t.data then +a:setData(t.data) +end +if a.selectable then +if t and t.selectedIndex then +a:setSelectedIndex(t.selectedIndex,true) +else +a:_clampSelection(true) +end +else +a.selectedIndex=nil +end +return a +end +function w:_emitSelect() +if self.onSelect then +local e=self.selectedIndex +local t=e and self.data[e]or nil +self.onSelect(self,e,t) +end +end +function w:_clampSelection(a) +if not self.selectable then +if self.selectedIndex~=nil then +self.selectedIndex=nil +if not a then +self:_emitSelect() +end +end +return +end +local t=#self.data +if t==0 then +if self.selectedIndex~=nil then +self.selectedIndex=nil +if not a then +self:_emitSelect() +end +end +return +end +local e=self.selectedIndex +if type(e)~="number"then +e=1 +else +e=math.floor(e) +if e<1 then +e=1 +elseif e>t then +e=t +end +end +if self.selectedIndex~=e then +self.selectedIndex=e +if not a then +self:_emitSelect() +end +end +end +function w:setData(e) +t(1,e,"table") +local a={} +for t=1,#e do +local e=e[t] +if type(e)~="number"then +e=tonumber(e)or 0 +end +a[t]=e +end +self.data=a +if self.selectable then +self:_clampSelection(false) +elseif self.selectedIndex~=nil then +self.selectedIndex=nil +self:_emitSelect() +end +end +function w:getData() +return self.data +end +function w:setLabels(e) +if e==nil then +self.labels={} +return +end +t(1,e,"table") +local t={} +for a=1,#e do +local e=e[a] +if e~=nil then +t[a]=tostring(e) +end +end +self.labels=t +end +function w:getLabels() +return self.labels +end +function w:getLabel(e) +if type(e)~="number"then +return nil +end +if not self.labels then +return nil +end +return self.labels[math.floor(e)] +end +function w:setChartType(e) +if e==nil then +return +end +t(1,e,"string") +local e=e:lower() +if e~="bar"and e~="line"then +error("Chart type must be 'bar' or 'line'",2) +end +self.chartType=e +end +function w:setShowAxis(e) +self.showAxis=not not e +end +function w:setShowLabels(e) +self.showLabels=not not e +end +function w:setPlaceholder(e) +if e~=nil then +t(1,e,"string") +end +self.placeholder=e or"" +end +function w:setSelectable(e,t) +if e==nil then +e=true +else +e=not not e +end +if self.selectable==e then +return +end +self.selectable=e +if not e then +if self.selectedIndex~=nil then +self.selectedIndex=nil +if not t then +self:_emitSelect() +end +end +else +self:_clampSelection(t) +end +end +function w:setRange(e,a) +if e~=nil then +t(1,e,"number") +end +if a~=nil then +t(2,a,"number") +end +self.minValue=e +self.maxValue=a +end +function w:setRangePadding(e) +t(1,e,"number") +if e<0 then +e=0 +end +self.rangePadding=e +end +function w:setOnSelect(e) +if e~=nil then +t(1,e,"function") +end +self.onSelect=e +end +function w:setSelectedIndex(e,a) +if e==nil then +if self.selectedIndex~=nil then +self.selectedIndex=nil +if not a then +self:_emitSelect() +end +end +return false +end +if not self.selectable then +return false +end +t(1,e,"number") +local t=#self.data +if t==0 then +if self.selectedIndex~=nil then +self.selectedIndex=nil +if not a then +self:_emitSelect() +end +end +return false +end +local e=math.floor(e) +if e<1 then +e=1 +elseif e>t then +e=t +end +if self.selectedIndex==e then +return false +end +self.selectedIndex=e +if not a then +self:_emitSelect() +end +return true +end +function w:getSelectedIndex() +return self.selectedIndex +end +function w:getSelectedValue() +local e=self.selectedIndex +if not e then +return nil +end +return self.data[e] +end +function w:onFocusChanged(e) +if e and self.selectable then +self:_clampSelection(true) +end +end +function w:_indexFromPoint(t) +local e=self._lastLayout +if not e or not e.bars then +return nil +end +local a=e.bars +for o=1,#a do +local e=a[o] +if t>=e.left and t<=e.right then +return o +end +end +if t=e.innerX+e.innerWidth then +return nil +end +if e.innerWidth<=0 then +return nil +end +local t=t-e.innerX +local t=math.floor(t*e.dataCount/e.innerWidth)+1 +if t<1 or t>e.dataCount then +return nil +end +return t +end +function w:_moveSelection(a) +if a==0 then +return false +end +if not self.selectable then +return false +end +local t=#self.data +if t==0 then +return false +end +local e=self.selectedIndex or(a>0 and 0 or t+1) +e=e+a +if e<1 then +e=1 +elseif e>t then +e=t +end +return self:setSelectedIndex(e,false) +end +function w:draw(r,v) +if not self.visible then +return +end +local o,h,d,i=self:getAbsoluteRect() +local s=self.bg or e.black +local w=self.fg or e.white +n(r,o,h,d,i,s,s) +z(r,o,h,d,i) +if self.border then +p(v,o,h,d,i,self.border,s) +end +local t=self.border +local a=(t and t.thickness)or 0 +local l=(t and t.left)and a or 0 +local c=(t and t.right)and a or 0 +local u=(t and t.top)and a or 0 +local t=(t and t.bottom)and a or 0 +local o=o+l +local h=h+u +local a=math.max(0,d-l-c) +local t=math.max(0,i-u-t) +self._lastLayout=nil +if a<=0 or t<=0 then +return +end +local d=#self.data +if d==0 then +local i=self.placeholder or"" +if i~=""then +local i=i +if#i>a then +i=i:sub(1,a) +end +local a=o+math.floor((a-#i)/2) +if a=2)and 1 or 0 +local c=(self.showAxis and(t-i)>=2)and 1 or 0 +local l=t-c-i +if l<1 then +l=t +c=0 +i=0 +end +local m=h +local u=m+l-1 +local f=c>0 and(u+1)or nil +local c +if i>0 then +if f then +c=f+1 +else +c=u+1 +end +if c>h+t-1 then +c=h+t-1 +end +end +local t=math.huge +local i=-math.huge +for e=1,d do +local e=self.data[e]or 0 +if ei then +i=e +end +end +if t==math.huge then +t=0 +end +if i==-math.huge then +i=0 +end +local t=type(self.minValue)=="number"and self.minValue or t +local h=type(self.maxValue)=="number"and self.maxValue or i +if h==t then +h=h+1 +t=t-1 +end +local i=h-t +if i<=0 then +i=1 +h=t+i +end +local y=self.rangePadding or 0 +if y>0 then +local e=h-t +local e=e*y +if e==0 then +e=y +end +t=t-e +h=h+e +i=h-t +if i<=0 then +i=1 +h=t+i +end +end +local h={} +for i=1,d do +local t=o+math.floor((i-1)*a/d) +local e=o+math.floor(i*a/d)-1 +if eo+a-1 then +e=o+a-1 +end +local a=e-t+1 +if a<1 then +a=1 +end +h[i]={ +left=t, +right=e, +width=a, +center=t+math.floor((a-1)/2) +} +end +if self.chartType=="bar"then +for a=1,d do +local o=self.data[a]or 0 +local e=0 +if i>0 then +e=(o-t)/i +end +e=O(e,0,1) +local e=math.floor(e*l+.5) +if l>0 and e<=0 and o>t then +e=1 +end +if e>l then +e=l +end +if e<1 then +e=1 +end +local o=u-e+1 +if o0 then +e=(n-t)/i +end +e=O(e,0,1) +local t=math.max(l-1,0) +local e=u-math.floor(e*t+.5) +if eu then +e=u +end +a[o]={x=h[o].center,y=e} +end +for t=2,#a do +local e=a[t-1] +local t=a[t] +M(v,e.x,e.y,t.x,t.y,self.lineColor or w) +end +for t=1,#a do +local o=a[t] +local a=self.lineColor or w +local i="o" +if self.selectable and self.selectedIndex==t then +a=self.highlightColor or e.orange +i="O" +end +r.text(o.x,o.y,i,a,s) +end +end +if f then +n(r,o,f,a,1,s,s) +local e=string.rep("-",a) +r.text(o,f,e,self.axisColor or w,s) +end +if c then +n(r,o,c,a,1,s,s) +local t=self.labels or{} +for i=1,d do +local t=t[i] +if t and t~=""then +t=tostring(t) +local a=h[i] +local o=a.width +if o>0 and#t>o then +t=t:sub(1,o) +end +local o=a.left+math.floor((a.width-#t)/2) +if oa.right then +o=a.right-#t+1 +end +local e=(self.selectable and self.selectedIndex==i)and(self.highlightColor or e.orange)or(self.axisColor or w) +r.text(o,c,t,e,s) +end +end +end +self._lastLayout={ +innerX=o, +innerWidth=a, +dataCount=d, +bars=h +} +end +function w:handleEvent(e,...) +if not self.visible then +return false +end +if e=="mouse_click"or e=="monitor_touch"then +local a,e,t=... +if self:containsPoint(e,t)then +self.app:setFocus(self) +local e=self:_indexFromPoint(e) +if e and self.selectable then +self:setSelectedIndex(e,false) +end +return true +end +elseif e=="mouse_scroll"then +local e,a,t=... +if self:containsPoint(a,t)then +self.app:setFocus(self) +self:_computeTabLayout() +if self:_isPointInTabStrip(a,t)and self._scrollState and self._scrollState.scrollable then +local e=e>0 and 1 or-1 +if self:_adjustScroll(e)then +return true +end +end +if self.selectable then +if e>0 then +self:_moveSelection(1) +elseif e<0 then +self:_moveSelection(-1) +end +end +return true +end +elseif e=="key"then +if not self:isFocused()then +return false +end +if not self.selectable then +return false +end +local e=... +if e==a.left then +self:_moveSelection(-1) +return true +elseif e==a.right then +self:_moveSelection(1) +return true +elseif e==a.home then +self:setSelectedIndex(1,false) +return true +elseif e==a["end"]then +local e=#self.data +if e>0 then +self:setSelectedIndex(e,false) +end +return true +elseif e==a.enter or e==a.space then +self:_emitSelect() +return true +end +end +return false +end +function f:new(i,a) +a=a or{} +local o=o(a)or{} +o.focusable=true +o.height=o.height or 5 +o.width=o.width or 16 +local t=setmetatable({},f) +t:_init_base(i,o) +t.focusable=true +t.items={} +if a and type(a.items)=="table"then +for e=1,#a.items do +local e=a.items[e] +if e~=nil then +t.items[#t.items+1]=tostring(e) +end +end +end +if type(a.selectedIndex)=="number"then +t.selectedIndex=math.floor(a.selectedIndex) +elseif#t.items>0 then +t.selectedIndex=1 +else +t.selectedIndex=0 +end +t.highlightBg=(a and a.highlightBg)or e.lightGray +t.highlightFg=(a and a.highlightFg)or e.black +t.placeholder=(a and a.placeholder)or nil +t.onSelect=a and a.onSelect or nil +t.scrollOffset=1 +t.typeSearchTimeout=(a and a.typeSearchTimeout)or .75 +t._typeSearch={buffer="",lastTime=0} +if not t.border then +t.border=R(true) +end +t.scrollbar=L(a and a.scrollbar,t.bg or e.black,t.fg or e.white) +t:_normalizeSelection(true) +return t +end +function f:_getInnerMetrics() +local e=self.border +local t=(e and e.left)and 1 or 0 +local o=(e and e.right)and 1 or 0 +local a=(e and e.top)and 1 or 0 +local e=(e and e.bottom)and 1 or 0 +local i=math.max(0,self.width-t-o) +local n=math.max(0,self.height-a-e) +return t,o,a,e,i,n +end +function f:_getInnerHeight() +local t,t,t,t,t,e=self:_getInnerMetrics() +if e<1 then +e=1 +end +return e +end +function f:_computeLayoutMetrics() +local o,a=self:getAbsoluteRect() +local t,s,n,s,e,i=self:_getInnerMetrics() +local o=o+t +local s=a+n +if e<=0 or i<=0 then +return{ +innerX=o, +innerY=s, +innerWidth=e, +innerHeight=i, +contentWidth=0, +scrollbarWidth=0, +scrollbarStyle=nil, +scrollbarX=o +} +end +local t,n=F(self.scrollbar,#self.items,i,e) +if t>0 and e-t<1 then +t=math.max(0,e-1) +if t<=0 then +t=0 +n=nil +end +end +local a=e-t +if a<1 then +a=e +t=0 +n=nil +end +return{ +innerX=o, +innerY=s, +innerWidth=e, +innerHeight=i, +contentWidth=a, +scrollbarWidth=t, +scrollbarStyle=n, +scrollbarX=o+a +} +end +function f:_clampScroll() +local e=self:_getInnerHeight() +local e=math.max(1,#self.items-e+1) +if self.scrollOffset<1 then +self.scrollOffset=1 +elseif self.scrollOffset>e then +self.scrollOffset=e +end +end +function f:_ensureSelectionVisible() +if self.selectedIndex<1 or self.selectedIndex>#self.items then +self:_clampScroll() +return +end +local e=self:_getInnerHeight() +if self.selectedIndexself.scrollOffset+e-1 then +self.scrollOffset=self.selectedIndex-e+1 +end +self:_clampScroll() +end +function f:_normalizeSelection(t) +local e=#self.items +if e==0 then +self.selectedIndex=0 +self.scrollOffset=1 +return +end +if self.selectedIndex<1 then +self.selectedIndex=1 +elseif self.selectedIndex>e then +self.selectedIndex=e +end +self:_ensureSelectionVisible() +if not t then +self:_notifySelect() +end +end +function f:getItems() +local e={} +for t=1,#self.items do +e[t]=self.items[t] +end +return e +end +function f:setItems(a) +t(1,a,"table") +local e={} +for t=1,#a do +local t=a[t] +if t~=nil then +e[#e+1]=tostring(t) +end +end +local a=self:getSelectedItem() +local t=self.selectedIndex +self.items=e +if#e==0 then +self.selectedIndex=0 +self.scrollOffset=1 +if(t~=0 or a~=nil)and self.onSelect then +self.onSelect(self,nil,0) +end +return +end +self:_normalizeSelection(true) +local e=self:getSelectedItem() +if(t~=self.selectedIndex)or(a~=e)then +self:_notifySelect() +end +end +function f:getSelectedItem() +if self.selectedIndex>=1 and self.selectedIndex<=#self.items then +return self.items[self.selectedIndex] +end +return nil +end +function f:setSelectedIndex(e,a) +if#self.items==0 then +self.selectedIndex=0 +self.scrollOffset=1 +return +end +t(1,e,"number") +e=math.floor(e) +if e<1 then +e=1 +elseif e>#self.items then +e=#self.items +end +if self.selectedIndex~=e then +self.selectedIndex=e +self:_ensureSelectionVisible() +if not a then +self:_notifySelect() +end +else +self:_ensureSelectionVisible() +end +end +function f:getSelectedIndex() +return self.selectedIndex +end +function f:setOnSelect(e) +if e~=nil then +t(1,e,"function") +end +self.onSelect=e +end +function f:setPlaceholder(e) +if e~=nil then +t(1,e,"string") +end +self.placeholder=e +end +function f:setHighlightColors(a,e) +if a~=nil then +t(1,a,"number") +self.highlightBg=a +end +if e~=nil then +t(2,e,"number") +self.highlightFg=e +end +end +function f:setScrollbar(t) +self.scrollbar=L(t,self.bg or e.black,self.fg or e.white) +self:_clampScroll() +end +function f:_notifySelect() +if self.onSelect then +self.onSelect(self,self:getSelectedItem(),self.selectedIndex) +end +end +function f:onFocusChanged(e) +if not e and self._typeSearch then +self._typeSearch.buffer="" +self._typeSearch.lastTime=0 +end +end +function f:_itemIndexFromPoint(a,t) +if not self:containsPoint(a,t)then +return nil +end +local e=self:_computeLayoutMetrics() +if e.innerWidth<=0 or e.innerHeight<=0 or e.contentWidth<=0 then +return nil +end +local i=e.innerX +local o=e.innerY +if a=i+e.contentWidth then +return nil +end +if t=o+e.innerHeight then +return nil +end +local e=t-o +local e=self.scrollOffset+e +if e<1 or e>#self.items then +return nil +end +return e +end +function f:_moveSelection(t) +if#self.items==0 then +return +end +local e=self.selectedIndex +if e<1 then +e=1 +end +e=e+t +if e<1 then +e=1 +elseif e>#self.items then +e=#self.items +end +self:setSelectedIndex(e) +end +function f:_scrollBy(e) +if e==0 then +return +end +self.scrollOffset=self.scrollOffset+e +self:_clampScroll() +end +function f:_handleTypeSearch(t) +if not t or t==""then +return +end +local e=self._typeSearch +if not e then +e={buffer="",lastTime=0} +self._typeSearch=e +end +local a=I.clock() +local o=self.typeSearchTimeout or .75 +if a-(e.lastTime or 0)>o then +e.buffer="" +end +e.buffer=e.buffer..t:lower() +e.lastTime=a +self:_searchForPrefix(e.buffer) +end +function f:_searchForPrefix(e) +if not e or e==""then +return +end +local t=#self.items +if t==0 then +return +end +local o=self.selectedIndex>=1 and self.selectedIndex or 0 +for a=1,t do +local a=((o+a-1)%t)+1 +local t=self.items[a] +if t and t:lower():sub(1,#e)==e then +self:setSelectedIndex(a) +return +end +end +end +function f:draw(a,h) +if not self.visible then +return +end +local t,o,s,i=self:getAbsoluteRect() +local r=self.bg or e.black +local u=self.fg or e.white +n(a,t,o,s,i,r,r) +z(a,t,o,s,i) +if self.border then +p(h,t,o,s,i,self.border,r) +end +local t=self:_computeLayoutMetrics() +local s=t.innerWidth +local i=t.innerHeight +local o=t.contentWidth +if s<=0 or i<=0 or o<=0 then +return +end +local d=t.innerX +local h=t.innerY +local l=t.scrollbarWidth +local s=t.scrollbarStyle +local c=#self.items +local r=r +local f=self.highlightBg or e.lightGray +local m=self.highlightFg or e.black +if c==0 then +for e=0,i-1 do +a.text(d,h+e,string.rep(" ",o),u,r) +end +if l>0 then +local e=(s and s.background)or r +n(a,t.scrollbarX,h,l,i,e,e) +end +local n=self.placeholder +if type(n)=="string"and#n>0 then +local t=n +if#t>o then +t=t:sub(1,o) +end +local o=d+math.floor((o-#t)/2) +if oc then +a.text(d,i,string.rep(" ",o),u,r) +else +local e=self.items[t]or"" +if#e>o then +e=e:sub(1,o) +end +local e=e +if#e0 then +local e=(s and s.background)or r +n(a,t.scrollbarX,h,l,i,e,e) +if s then +C(a,t.scrollbarX,h,i,#self.items,i,math.max(0,self.scrollOffset-1),s) +end +end +end +function f:handleEvent(o,...) +if not self.visible then +return false +end +if o=="mouse_click"then +local e,a,t=... +if self:containsPoint(a,t)then +self.app:setFocus(self) +local e=self:_computeLayoutMetrics() +if e.scrollbarStyle and e.scrollbarWidth>0 then +local o=e.scrollbarX +if a>=o and a=e.innerY and t0 then +local o=e.scrollbarX +if a>=o and a=e.innerY and t0 then +self:_scrollBy(1) +elseif e<0 then +self:_scrollBy(-1) +end +return true +end +elseif o=="key"then +if not self:isFocused()then +return false +end +local e=... +if e==a.up then +self:_moveSelection(-1) +return true +elseif e==a.down then +self:_moveSelection(1) +return true +elseif e==a.pageUp then +self:_moveSelection(-self:_getInnerHeight()) +return true +elseif e==a.pageDown then +self:_moveSelection(self:_getInnerHeight()) +return true +elseif e==a.home then +if#self.items>0 then +self:setSelectedIndex(1) +end +return true +elseif e==a["end"]then +if#self.items>0 then +self:setSelectedIndex(#self.items) +end +return true +elseif e==a.enter or e==a.space then +self:_notifySelect() +return true +end +elseif o=="char"then +local e=... +if self:isFocused()and e and#e>0 then +self:_handleTypeSearch(e:sub(1,1)) +return true +end +elseif o=="paste"then +local e=... +if self:isFocused()and e and#e>0 then +self:_handleTypeSearch(e:sub(1,1)) +return true +end +end +return false +end +function x:new(i,t) +t=t or{} +local o=o(t)or{} +o.focusable=true +o.height=o.height or 3 +o.width=o.width or 16 +local a=setmetatable({},x) +a:_init_base(i,o) +a.focusable=true +a.items={} +if t and type(t.items)=="table"then +for e=1,#t.items do +local e=t.items[e] +if e~=nil then +a.items[#a.items+1]=tostring(e) +end +end +end +a.dropdownBg=(t and t.dropdownBg)or e.black +a.dropdownFg=(t and t.dropdownFg)or e.white +a.highlightBg=(t and t.highlightBg)or e.lightBlue +a.highlightFg=(t and t.highlightFg)or e.black +a.placeholder=(t and t.placeholder)or"Select..." +a.onChange=t and t.onChange or nil +if t and type(t.selectedIndex)=="number"then +a.selectedIndex=math.floor(t.selectedIndex) +elseif#a.items>0 then +a.selectedIndex=1 +else +a.selectedIndex=0 +end +a:_normalizeSelection() +if not a.border then +a.border=R(true) +end +a._open=false +a._hoverIndex=nil +return a +end +function x:_normalizeSelection() +if#self.items==0 then +self.selectedIndex=0 +return +end +if self.selectedIndex<1 then +self.selectedIndex=1 +elseif self.selectedIndex>#self.items then +self.selectedIndex=#self.items +end +end +function x:setItems(a) +t(1,a,"table") +local e={} +for t=1,#a do +local t=a[t] +if t~=nil then +e[#e+1]=tostring(t) +end +end +local t=self:getSelectedItem() +local a=self.selectedIndex +self.items=e +if#e==0 then +self.selectedIndex=0 +if a~=0 or t~=nil then +self:_notifyChange() +end +self:_setOpen(false) +return +end +self:_normalizeSelection() +local e=self:getSelectedItem() +if a~=self.selectedIndex or t~=e then +self:_notifyChange() +end +if self._open then +self._hoverIndex=self.selectedIndex +end +end +function x:getSelectedItem() +if self.selectedIndex>=1 and self.selectedIndex<=#self.items then +return self.items[self.selectedIndex] +end +return nil +end +function x:setSelectedIndex(e,a) +if e==nil then +return +end +t(1,e,"number") +if#self.items==0 then +self.selectedIndex=0 +return +end +e=math.floor(e) +if e<1 then +e=1 +elseif e>#self.items then +e=#self.items +end +if self.selectedIndex~=e then +self.selectedIndex=e +if not a then +self:_notifyChange() +end +end +if self._open then +self._hoverIndex=self.selectedIndex +end +end +function x:setOnChange(e) +if e~=nil then +t(1,e,"function") +end +self.onChange=e +end +function x:_notifyChange() +if self.onChange then +self.onChange(self,self:getSelectedItem(),self.selectedIndex) +end +end +function x:_setOpen(e) +e=not not e +if e and#self.items==0 then +e=false +end +if self._open==e then +return +end +self._open=e +if e then +if self.app then +self.app:_registerPopup(self) +end +if self.selectedIndex>=1 and self.selectedIndex<=#self.items then +self._hoverIndex=self.selectedIndex +elseif#self.items>0 then +self._hoverIndex=1 +else +self._hoverIndex=nil +end +else +if self.app then +self.app:_unregisterPopup(self) +end +self._hoverIndex=nil +end +end +function x:onFocusChanged(e) +if not e then +self:_setOpen(false) +end +end +function x:_isPointInDropdown(o,a) +if not self._open or#self.items==0 then +return false +end +local t,e,i,n=self:getAbsoluteRect() +local e=e+n +return o>=t and o=e and a#self.items then +return nil +end +return e +end +function x:_handlePress(e,t) +local i,i,a,o=self:getAbsoluteRect() +if a<=0 or o<=0 then +return false +end +if self:containsPoint(e,t)then +self.app:setFocus(self) +if self._open then +self:_setOpen(false) +else +self:_setOpen(true) +end +return true +end +if self:_isPointInDropdown(e,t)then +local e=self:_indexFromPoint(e,t) +if e then +self:setSelectedIndex(e) +end +self.app:setFocus(self) +self:_setOpen(false) +return true +end +if self._open then +self:_setOpen(false) +end +return false +end +function x:draw(s,d) +if not self.visible then +return +end +local o,a,h,i=self:getAbsoluteRect() +local t=self.bg or e.black +local r=self.fg or e.white +n(s,o,a,h,i,t,t) +z(s,o,a,h,i) +if self.border then +p(d,o,a,h,i,self.border,t) +end +local e=self.border +local n=(e and e.left)and 1 or 0 +local u=(e and e.right)and 1 or 0 +local d=(e and e.top)and 1 or 0 +local e=(e and e.bottom)and 1 or 0 +local l=o+n +local n=math.max(0,h-n-u) +local u=a+d +local e=math.max(0,i-d-e) +local h=n>0 and 1 or 0 +local o=math.max(0,n-h) +local i +if e>0 then +i=u+math.floor((e-1)/2) +else +i=a +end +local e=self:getSelectedItem() +if not e or e==""then +e=self.placeholder or"" +end +if o>0 then +if#e>o then +e=e:sub(1,o) +end +local a=math.max(0,o-#e) +local e=e..string.rep(" ",a) +s.text(l,i,e,r,t) +end +if h>0 then +local e=self._open and string.char(30)or string.char(31) +local a=l+n-1 +s.text(a,i,e,r,t) +end +end +function x:_drawDropdown(r,c) +if not self._open or#self.items==0 or self.visible==false then +return +end +local t,i,a,e=self:getAbsoluteRect() +local i=i+e +local l=#self.items +local e=self.border +local s=(e and e.left)and 1 or 0 +local h=(e and e.right)and 1 or 0 +local u=t+s +local s=math.max(0,a-s-h) +local d=self._hoverIndex or(self.selectedIndex>0 and self.selectedIndex or nil) +local e=(e and e.bottom)and 1 or 0 +local h=l+e +n(r,t,i,a,h,self.dropdownBg,self.dropdownBg) +z(r,t,i,a,h) +for e=1,l do +local t=i+e-1 +local a=self.items[e]or"" +local e=d~=nil and d==e +local o=e and(self.highlightBg or self.dropdownBg)or self.dropdownBg +local i=e and(self.highlightFg or self.dropdownFg)or self.dropdownFg +if s>0 then +local e=a +if#e>s then +e=e:sub(1,s) +end +local a=math.max(0,s-#e) +local e=e..string.rep(" ",a) +r.text(u,t,e,i,o) +end +end +if self.border then +local e=o(self.border) +if e then +e.top=false +p(c,t,i,a,h,e,self.dropdownBg) +end +end +end +function x:handleEvent(e,...) +if not self.visible then +return false +end +if e=="mouse_click"then +local a,e,t=... +return self:_handlePress(e,t) +elseif e=="monitor_touch"then +local a,e,t=... +return self:_handlePress(e,t) +elseif e=="mouse_scroll"then +local e,t,a=... +if self:containsPoint(t,a)or self:_isPointInDropdown(t,a)then +self.app:setFocus(self) +if e>0 then +self:setSelectedIndex(self.selectedIndex+1) +elseif e<0 then +self:setSelectedIndex(self.selectedIndex-1) +end +return true +end +elseif e=="mouse_move"then +local t,e=... +if self._open then +self._hoverIndex=self:_indexFromPoint(t,e) +end +elseif e=="mouse_drag"then +local a,e,t=... +if self._open then +self._hoverIndex=self:_indexFromPoint(e,t) +end +elseif e=="key"then +if not self:isFocused()then +return false +end +local e=... +if e==a.down then +self:setSelectedIndex(self.selectedIndex+1) +return true +elseif e==a.up then +self:setSelectedIndex(self.selectedIndex-1) +return true +elseif e==a.home then +self:setSelectedIndex(1) +return true +elseif e==a["end"]then +self:setSelectedIndex(#self.items) +return true +elseif e==a.enter or e==a.space then +if self._open then +self:_setOpen(false) +else +self:_setOpen(true) +end +return true +elseif e==a.escape then +if self._open then +self:_setOpen(false) +return true +end +end +elseif e=="char"then +if not self:isFocused()or#self.items==0 then +return false +end +local e=... +if e and#e>0 then +local a=e:sub(1,1):lower() +local e=self.selectedIndex>=1 and self.selectedIndex or 0 +for t=1,#self.items do +local e=((e+t-1)%#self.items)+1 +local t=self.items[e] +if t and t:sub(1,1):lower()==a then +self:setSelectedIndex(e) +return true +end +end +end +end +return false +end +local O="x" +local W=string.char(7) +local function M(t) +if t==nil then +return nil +end +local e=type(t) +if e=="number"then +local e=math.floor(t) +if e<0 then +e=0 +elseif e>255 then +e=255 +end +return string.char(e) +end +local e=tostring(t) +if e==""then +return nil +end +return e +end +local function Y(a) +local t={ +enabled=false, +char=O, +spacing=1, +fg=nil, +bg=nil +} +local function o(e) +if e==nil then +return +end +local a=type(e) +if a=="boolean"then +t.enabled=e +elseif a=="string"or a=="number"then +t.enabled=true +local e=M(e) +if e then +t.char=e +end +elseif a=="table"then +if e.enabled~=nil then +t.enabled=not not e.enabled +end +local a=e.char or e.label or e.symbol or e.glyph or e.text or e.value +if a==nil and e.code~=nil then +a=e.code +end +a=M(a) +if a then +t.char=a +end +local a=e.spacing or e.gap or e.padding +if a~=nil then +t.spacing=math.max(0,math.floor(a)) +end +if e.fg or e.color or e.foreground or e.textColor then +t.fg=e.fg or e.color or e.foreground or e.textColor +end +if e.bg or e.background or e.fill then +t.bg=e.bg or e.background or e.fill +end +else +t.enabled=not not e +end +end +if a then +if a.tabCloseButton~=nil then +o(a.tabCloseButton) +elseif a.closeButton~=nil then +o(a.closeButton) +end +if a.enableCloseButton~=nil then +t.enabled=not not a.enableCloseButton +end +local e=M(a.closeButtonChar) +if e then +t.char=e +end +if a.closeButtonSpacing~=nil then +t.spacing=math.max(0,math.floor(a.closeButtonSpacing)) +end +if a.closeButtonFg~=nil then +t.fg=a.closeButtonFg +end +if a.closeButtonBg~=nil then +t.bg=a.closeButtonBg +end +end +if not t.char or t.char==""then +t.char=O +end +t.spacing=math.max(0,math.floor(t.spacing or 0)) +return t +end +local function V(a) +local t=nil +local o=1 +local function i(e) +if e==nil then +return +end +local a=type(e) +if a=="boolean"then +if e then +t=W +else +t=nil +end +elseif a=="table"then +if e.enabled==false then +t=nil +else +local a=e.char or e.symbol or e.glyph or e.text or e.value +if a==nil and e.code~=nil then +a=e.code +end +a=M(a) +if a then +t=a +elseif e.enabled==true and not t then +t=W +end +local e=e.spacing or e.gap or e.padding +if e~=nil then +o=math.max(0,math.floor(e)) +end +end +else +local e=M(e) +if e then +t=e +end +end +end +if a then +if a.tabIndicator~=nil then +i(a.tabIndicator) +elseif a.currentTabIndicator~=nil then +i(a.currentTabIndicator) +elseif a.indicator~=nil then +i(a.indicator) +end +local e=M(a.indicatorChar) +if e then +t=e +end +if a.indicatorSpacing~=nil then +o=math.max(0,math.floor(a.indicatorSpacing)) +end +end +if t~=nil and t~=""then +return t,o +end +return nil,o +end +function d:new(i,t) +t=t or{} +local o=o(t)or{} +if t and t.focusable==false then +o.focusable=false +else +o.focusable=true +end +o.width=math.max(8,math.floor(o.width or 18)) +o.height=math.max(3,math.floor(o.height or 7)) +local a=setmetatable({},d) +a:_init_base(i,o) +a.focusable=o.focusable~=false +a.tabSpacing=math.max(0,math.floor((t and t.tabSpacing)or 1)) +a.tabPadding=math.max(0,math.floor((t and t.tabPadding)or 2)) +a.tabHeight=math.max(1,math.floor((t and t.tabHeight)or 3)) +a.tabBg=(t and t.tabBg)or a.bg or e.black +a.tabFg=(t and t.tabFg)or a.fg or e.white +a.activeTabBg=(t and t.activeTabBg)or e.white +a.activeTabFg=(t and t.activeTabFg)or e.black +a.hoverTabBg=(t and t.hoverTabBg)or e.lightGray +a.hoverTabFg=(t and t.hoverTabFg)or e.black +a.disabledTabFg=(t and t.disabledTabFg)or e.lightGray +a.bodyBg=(t and t.bodyBg)or a.bg or e.black +a.bodyFg=(t and t.bodyFg)or a.fg or e.white +a.separatorColor=(t and t.separatorColor)or e.gray +a.bodyRenderer=(t and t.bodyRenderer)or(t and t.renderBody)or nil +a.emptyText=t and t.emptyText or nil +a.onSelect=t and t.onSelect or nil +a.autoShrink=t and t.autoShrink==false and false or true +local e=nil +if t then +if type(t.onCloseTab)=="function"then +e=t.onCloseTab +elseif type(t.onTabClose)=="function"then +e=t.onTabClose +end +end +a.onCloseTab=e +a.tabCloseButton=Y(t) +local o,e=V(t) +a.tabIndicatorChar=o +a.tabIndicatorSpacing=math.max(0,math.floor((e or 0))) +a.tabs={} +if t and type(t.tabs)=="table"then +a.tabs=a:_normalizeTabs(t.tabs) +end +if t and type(t.selectedIndex)=="number"then +a.selectedIndex=math.floor(t.selectedIndex) +elseif#a.tabs>0 then +a.selectedIndex=1 +else +a.selectedIndex=0 +end +a._hoverIndex=nil +a._tabRects={} +a._layoutCache=nil +a._scrollIndex=1 +a._scrollState={scrollable=false,first=1,last=0,canScrollLeft=false,canScrollRight=false} +a._tabStripRect=nil +a:_normalizeSelection(true) +return a +end +function d:_normalizeTabEntry(e,a) +if e==nil then +return nil +end +local t=type(e) +if t=="string"then +return{ +id=a, +label=e, +value=e, +disabled=false, +closeable=true +} +elseif t=="table"then +local t=e.label or e.text or e.title +if t==nil then +if e.id~=nil then +t=tostring(e.id) +elseif e.value~=nil then +t=tostring(e.value) +else +t=string.format("Tab %d",a) +end +else +t=tostring(t) +end +local e={ +id=e.id~=nil and e.id or e.value or a, +label=t, +value=e.value~=nil and e.value or e.id or e, +disabled=not not e.disabled, +content=e.content, +tooltip=e.tooltip, +contentRenderer=e.contentRenderer or e.render, +closeable=e.closeable~=false +} +if e.contentRenderer~=nil and type(e.contentRenderer)~="function"then +e.contentRenderer=nil +end +return e +else +return{ +id=a, +label=tostring(e), +value=e, +disabled=false, +closeable=true +} +end +end +function d:_normalizeTabs(a) +local e={} +for t=1,#a do +local t=self:_normalizeTabEntry(a[t],t) +if t then +e[#e+1]=t +end +end +return e +end +function d:_findFirstEnabled() +for t=1,#self.tabs do +local e=self.tabs[t] +if e and not e.disabled then +return t +end +end +return 0 +end +function d:_resolveSelectableIndex(t) +local a=#self.tabs +if a==0 then +return 0 +end +t=math.max(1,math.min(a,math.floor(t))) +local e=self.tabs[t] +if e and not e.disabled then +return t +end +for t=t+1,a do +e=self.tabs[t] +if e and not e.disabled then +return t +end +end +for t=t-1,1,-1 do +e=self.tabs[t] +if e and not e.disabled then +return t +end +end +return 0 +end +function d:_normalizeSelection(o) +local a=self.selectedIndex or 0 +local t=#self.tabs +local e=a +if t==0 then +e=0 +else +e=math.floor(e) +if e<1 or e>t then +e=math.max(1,math.min(t,e)) +end +if t>0 then +local t=self.tabs[e] +if not t or t.disabled then +e=self:_resolveSelectableIndex(e) +end +if e==0 then +e=self:_findFirstEnabled() +end +end +end +if e<0 then +e=0 +end +local t=e~=a +self.selectedIndex=e +if not o then +if t then +self:_notifySelect() +elseif a~=0 and e==0 then +self:_notifySelect() +end +end +end +function d:setTabs(a) +t(1,a,"table") +local n=self.selectedIndex or 0 +local e=self:getSelectedTab() +local i=e and e.id +local o=e and e.label +self.tabs=self:_normalizeTabs(a) +if i~=nil then +for t=1,#self.tabs do +local e=self.tabs[t] +if e and e.id==i and not e.disabled then +self.selectedIndex=t +break +end +end +end +if(self.selectedIndex or 0)<1 or(self.selectedIndex or 0)>#self.tabs then +if o~=nil then +for t=1,#self.tabs do +local e=self.tabs[t] +if e and e.label==o and not e.disabled then +self.selectedIndex=t +break +end +end +end +end +if(self.selectedIndex or 0)<1 or(self.selectedIndex or 0)>#self.tabs then +self.selectedIndex=n +end +self:_normalizeSelection(false) +self._scrollIndex=1 +self:_invalidateLayout() +end +function d:getTabs() +local t={} +for e=1,#self.tabs do +t[e]=o(self.tabs[e]) +end +return t +end +function d:addTab(e) +local e=self:_normalizeTabEntry(e,#self.tabs+1) +if not e then +return +end +self.tabs[#self.tabs+1]=e +if self.selectedIndex==0 then +self.selectedIndex=#self.tabs +self:_normalizeSelection(false) +else +self:_normalizeSelection(true) +end +self:_invalidateLayout() +end +function d:removeTab(e) +t(1,e,"number") +e=math.floor(e) +if e<1 or e>#self.tabs then +return +end +table.remove(self.tabs,e) +if self.selectedIndex==e then +self.selectedIndex=e +self:_normalizeSelection(false) +elseif self.selectedIndex>e then +self.selectedIndex=self.selectedIndex-1 +self:_normalizeSelection(true) +else +self:_normalizeSelection(true) +end +self:_ensureScrollIndexValid() +self:_invalidateLayout() +end +function d:setTabEnabled(e,a) +t(1,e,"number") +t(2,a,"boolean") +e=math.floor(e) +if e<1 or e>#self.tabs then +return +end +local t=self.tabs[e] +if not t then +return +end +if a then +if t.disabled then +t.disabled=false +if self.selectedIndex==0 then +self.selectedIndex=e +self:_normalizeSelection(false) +else +self:_normalizeSelection(true) +end +end +else +if not t.disabled then +t.disabled=true +if self.selectedIndex==e then +self:_normalizeSelection(false) +else +self:_normalizeSelection(true) +end +end +end +end +function d:setTabLabel(e,a) +t(1,e,"number") +t(2,a,"string") +e=math.floor(e) +if e<1 or e>#self.tabs then +return +end +local e=self.tabs[e] +if not e then +return +end +if e.label~=a then +e.label=a +self:_invalidateLayout() +end +end +function d:selectTabById(o,a) +for e=1,#self.tabs do +local t=self.tabs[e] +if t and t.id==o then +self:setSelectedIndex(e,a) +return true +end +end +return false +end +function d:getSelectedIndex() +return self.selectedIndex or 0 +end +function d:getSelectedTab() +local e=self.selectedIndex or 0 +if e>=1 and e<=#self.tabs then +return self.tabs[e] +end +return nil +end +function d:setSelectedIndex(e,a) +if#self.tabs==0 then +if self.selectedIndex~=0 then +self.selectedIndex=0 +if not a then +self:_notifySelect() +end +end +return +end +t(1,e,"number") +e=math.floor(e) +if e<1 then +e=1 +elseif e>#self.tabs then +e=#self.tabs +end +if self.tabs[e]and self.tabs[e].disabled then +e=self:_resolveSelectableIndex(e) +end +if e==0 then +if self.selectedIndex~=0 then +self.selectedIndex=0 +if not a then +self:_notifySelect() +end +end +return +end +if self.selectedIndex~=e then +self.selectedIndex=e +if not a then +self:_notifySelect() +end +end +end +function d:setOnSelect(e) +if e~=nil then +t(1,e,"function") +end +self.onSelect=e +end +function d:setOnCloseTab(e) +if e~=nil then +t(1,e,"function") +end +self.onCloseTab=e +end +function d:setBodyRenderer(e) +if e~=nil then +t(1,e,"function") +end +self.bodyRenderer=e +end +function d:setEmptyText(e) +if e~=nil then +t(1,e,"string") +end +self.emptyText=e +end +function d:setTabCloseButton(t) +local e +if t==nil then +e={tabCloseButton=false} +else +e={tabCloseButton=t} +end +self.tabCloseButton=Y(e) +self:_invalidateLayout() +end +function d:setTabIndicator(a,e) +if e~=nil then +t(2,e,"number") +end +local t={} +if a==nil then +t.tabIndicator=false +else +t.tabIndicator=a +end +if e~=nil then +t.indicatorSpacing=e +end +local e,t=V(t) +self.tabIndicatorChar=e +self.tabIndicatorSpacing=math.max(0,math.floor((t or 0))) +self:_invalidateLayout() +end +function d:setTabClosable(e,a) +t(1,e,"number") +if a~=nil then +t(2,a,"boolean") +end +e=math.floor(e) +if e<1 or e>#self.tabs then +return +end +local e=self.tabs[e] +if not e then +return +end +local t=(a~=false) +if e.closeable~=t then +e.closeable=t +self:_invalidateLayout() +end +end +function d:setAutoShrink(e) +if e==nil then +e=true +else +t(1,e,"boolean") +end +local e=not not e +if self.autoShrink~=e then +self.autoShrink=e +self._scrollIndex=1 +self:_invalidateLayout() +end +end +function d:_invalidateLayout() +self._tabRects={} +self._layoutCache=nil +self._tabStripRect=nil +self._scrollState=self._scrollState or{scrollable=false,first=1,last=0,canScrollLeft=false,canScrollRight=false} +end +function d:_ensureScrollIndexValid() +local t=#self.tabs +if t<=0 then +self._scrollIndex=1 +return +end +local e=self._scrollIndex or 1 +if e<1 then +e=1 +elseif e>t then +e=t +end +self._scrollIndex=e +end +function d:_isPointInTabStrip(t,a) +local e=self._tabStripRect +if not e then +return false +end +return t>=e.x and t=e.y and a0 then +if not a.canScrollRight then +return false +end +e=math.min(o,e+t) +else +if not a.canScrollLeft then +return false +end +e=math.max(1,e+t) +end +if e~=self._scrollIndex then +self._scrollIndex=e +self._hoverIndex=nil +self:_invalidateLayout() +local t=self.selectedIndex or 0 +self:_computeTabLayout() +local e=self._scrollState +if e and e.scrollable then +if te.last and e.last>=1 then +self:setSelectedIndex(e.last,true) +end +end +return true +end +return false +end +function d:_notifySelect() +if self.onSelect then +self.onSelect(self,self:getSelectedTab(),self.selectedIndex or 0) +end +end +function d:_emitSelect() +if self.onSelect then +self.onSelect(self,self:getSelectedTab(),self.selectedIndex or 0) +end +end +function d:_computeTabLayout() +local n,i,o,s=self:getAbsoluteRect() +local e=self.border +local a=(e and e.left)and 1 or 0 +local r=(e and e.right)and 1 or 0 +local t=(e and e.top)and 1 or 0 +local e=(e and e.bottom)and 1 or 0 +local h=n+a +local d=i+t +local a=math.max(0,o-a-r) +local e=math.max(0,s-t-e) +local o=math.min(e,self.tabHeight or 3) +if o<0 then +o=0 +end +local t=math.max(0,e-o) +local r={ +innerX=h, +innerY=d, +innerWidth=a, +innerHeight=e, +tabHeight=o, +bodyX=h, +bodyY=d+o, +bodyWidth=a, +bodyHeight=t +} +self._layoutCache=r +if o>0 and a>0 then +self._tabStripRect={x=h,y=d,width=a,height=o} +else +self._tabStripRect=nil +end +local t=#self.tabs +if a<=0 or o<=0 or t==0 then +self._tabRects={} +self._scrollState={scrollable=false,first=1,last=0,canScrollLeft=false,canScrollRight=false} +return r,self._tabRects +end +local u=math.max(0,self.tabSpacing or 0) +local l=math.max(0,self.tabPadding or 0) +local e=self.tabIndicatorChar +local i=math.max(0,self.tabIndicatorSpacing or 0) +local n=0 +if e and e~=""then +n=#e+i +end +local i=self.tabCloseButton or{enabled=false,char=O,spacing=1} +local e=i.char or O +if not e or e==""then +e=O +end +local w=i.enabled and e~=nil and e~="" +local m=math.max(1,#e) +local f=math.max(0,i.spacing or 0) +local s={} +local i=0 +local c=0 +for e=1,t do +local t=self.tabs[e] +local a=t and t.label and tostring(t.label)or string.format("Tab %d",e) +if a==""then +a=string.format("Tab %d",e) +end +local h=math.max(1,#a) +local o=w and t and t.closeable~=false +local t=o and m or 0 +local m=o and f or 0 +local u=l +local l=l +local d=u+l+n+h+t+m +local r=n+h+t +s[e]={ +index=e, +label=a, +labelLength=h, +padLeft=u, +padRight=l, +minPadLeft=0, +minPadRight=0, +indicatorWidth=n, +closeable=o, +closeCharWidth=t, +closeSpacing=m, +minCloseSpacing=0, +width=d, +minWidth=r +} +i=i+d +c=c+r +end +local e=u*math.max(0,t-1) +local n=i+e +local l=math.max(0,n-a) +if l>0 and self.autoShrink then +local e=i-c +if e>0 then +local h=math.min(l,e) +local o=h +while o>0 do +local a=false +for e=1,t do +if o<=0 then +break +end +local e=s[e] +if e.padLeft>e.minPadLeft then +e.padLeft=e.padLeft-1 +e.width=e.width-1 +o=o-1 +a=true +elseif e.padRight>e.minPadRight then +e.padRight=e.padRight-1 +e.width=e.width-1 +o=o-1 +a=true +elseif e.closeSpacing>e.minCloseSpacing then +e.closeSpacing=e.closeSpacing-1 +e.width=e.width-1 +o=o-1 +a=true +end +end +if not a then +break +end +end +local e=h-o +if e>0 then +i=i-e +n=n-e +l=math.max(0,n-a) +end +end +end +local c=l>0 +if not c then +self._scrollIndex=1 +end +local function n(i) +local e=0 +local o=i-1 +for i=i,t do +local t=s[i] +local n=math.min(t.width,a) +local t=n +if e>0 then +t=t+u +end +if e+t>a then +if e==0 then +o=i +e=n +end +break +end +e=e+t +o=i +end +if o1 do +local o=math.min(s[e-1].width,a) +local o=t+u+o +if o>a then +break +end +e=e-1 +t=o +end +return e +end +local l={} +local e +local i +if c then +self:_ensureScrollIndexValid() +e=math.max(1,math.min(self._scrollIndex or 1,t)) +local a=self.selectedIndex or 0 +local o=n(e) +if a>=1 and a<=t then +if ao then +e=m(a) +o=n(e) +while a>o and e=t then +break +end +end +if a1, +canScrollRight=ih then +a=h-n+1 +if a<1 then +break +end +end +local t={ +x1=n, +y1=d, +x2=n+a-1, +y2=d+o-1, +width=a, +padLeft=e.padLeft, +padRight=e.padRight, +indicatorWidth=e.indicatorWidth, +closeable=e.closeable, +closeCharWidth=e.closeCharWidth, +closeSpacingWidth=e.closeSpacing, +closeWidth=e.closeCharWidth+e.closeSpacing, +labelLength=e.labelLength, +label=e.label +} +t.labelAvailable=a-e.padLeft-e.padRight-e.indicatorWidth-t.closeWidth +if t.labelAvailable<0 then +t.labelAvailable=0 +end +if e.closeCharWidth>0 then +local e=t.x2-e.closeCharWidth+1 +if eh then +break +end +end +self._tabRects=l +r.firstTabIndex=e +r.lastTabIndex=i +return r,l +end +function d:_tabIndexFromPoint(a,t) +self:_computeTabLayout() +for o,e in pairs(self._tabRects)do +if e and a>=e.x1 and a<=e.x2 and t>=e.y1 and t<=e.y2 then +return o +end +end +return nil +end +function d:_hitTestTabArea(a,t) +self:_computeTabLayout() +for o,e in pairs(self._tabRects)do +if e and a>=e.x1 and a<=e.x2 and t>=e.y1 and t<=e.y2 then +if e.closeRect and a>=e.closeRect.x1 and a<=e.closeRect.x2 and t>=e.closeRect.y1 and t<=e.closeRect.y2 then +return o,"close" +end +return o,"tab" +end +end +return nil,nil +end +function d:_canCloseTab(t) +if not t or t.disabled then +return false +end +local e=self.tabCloseButton +if not e or not e.enabled then +return false +end +if not e.char or e.char==""then +return false +end +if t.closeable==false then +return false +end +return true +end +function d:_tryCloseTab(e) +if type(e)~="number"then +return false +end +e=math.floor(e) +if e<1 or e>#self.tabs then +return false +end +local t=self.tabs[e] +if not self:_canCloseTab(t)then +return false +end +if self.onCloseTab then +local e=self.onCloseTab(self,t,e) +if e==false then +return false +end +end +self:removeTab(e) +self._hoverIndex=nil +return true +end +function d:_moveSelection(a) +if#self.tabs==0 or a==0 then +return +end +a=a>0 and 1 or-1 +local t=#self.tabs +local e=self.selectedIndex +if e<1 or e>t then +e=a>0 and 0 or t+1 +end +for o=1,t do +e=e+a +if e<1 then +e=t +elseif e>t then +e=1 +end +local t=self.tabs[e] +if t and not t.disabled then +self:setSelectedIndex(e) +return +end +end +end +function d:_renderBody(i,n,a) +local o=a.bodyWidth or 0 +local h=a.bodyHeight or 0 +if o<=0 or h<=0 then +return +end +local t=self:getSelectedTab() +if not t then +return +end +local s=t.contentRenderer +if s~=nil and type(s)=="function"then +s(self,t,i,n,a) +return +end +if type(t.content)=="function"then +t.content(self,t,i,n,a) +return +end +if self.bodyRenderer then +self.bodyRenderer(self,t,i,n,a) +return +end +if type(t.content)=="string"then +local n=P(t.content,o) +local t=math.min(h,#n) +local s=self.bodyFg or self.tabFg or e.white +local h=self.bodyBg or self.bg or e.black +for t=1,t do +local e=n[t] +if#e>o then +e=e:sub(1,o) +end +if#e0 and t.innerWidth>0 then +n(s,t.innerX,t.innerY,t.innerWidth,t.tabHeight,o,o) +end +if self._hoverIndex and not self._tabRects[self._hoverIndex]then +self._hoverIndex=nil +end +local e=self.tabCloseButton or{enabled=false,char=O,spacing=1} +local r=e.char or O +if not r or r==""then +r=O +end +local g=e.enabled and r~=nil and r~="" +local k=e.fg +local q=e.bg +local u=self.tabIndicatorChar +local f=math.max(0,self.tabIndicatorSpacing or 0) +local m="" +local c="" +if u and u~=""then +m=u +c=string.rep(" ",#u) +if f>0 then +local e=string.rep(" ",f) +m=m..e +c=c..e +end +end +for l,e in pairs(self._tabRects)do +local a=self.tabs[l] +if a and e then +local i=o +local o=d +if l==self.selectedIndex and self.selectedIndex>0 then +i=self.activeTabBg or i +o=self.activeTabFg or o +if self:isFocused()then +i=self.hoverTabBg or i +o=self.hoverTabFg or o +end +elseif self._hoverIndex and self._hoverIndex==l and not a.disabled then +i=self.hoverTabBg or i +o=self.hoverTabFg or o +end +if a.disabled then +o=self.disabledTabFg or o +end +n(s,e.x1,e.y1,e.width,t.tabHeight,i,i) +local n=e.padLeft +if n==nil then +n=math.max(0,self.tabPadding or 0) +end +local h=e.padRight +if h==nil then +h=n +end +local w=e.label or a.label or string.format("Tab %d",l) +w=tostring(w) +local d=e.indicatorWidth or 0 +local y=e.closeWidth or 0 +local a=e.labelAvailable +if a==nil then +a=e.width-n-h-d-y +end +a=math.max(0,a) +local y=e.x1+n +local p=e.y1+math.max(0,math.floor((t.tabHeight-1)/2)) +local h="" +if d>0 then +local e +if l==self.selectedIndex and self.selectedIndex>0 then +e=m +else +e=c +end +if e==""then +e=u or"" +if f>0 then +e=e..string.rep(" ",f) +end +end +if#e>d then +h=e:sub(1,d) +else +h=e..string.rep(" ",d-#e) +end +end +if#h>a then +h=h:sub(1,a) +end +local t=#h +local d=math.max(0,a-t) +local t=w +if d>0 then +if#t>d then +t=t:sub(1,d) +end +if#t0 and y<=e.x2 then +s.text(y,p,t,o,i) +end +if g and e.closeable and e.closeCharWidth and e.closeCharWidth>0 and e.closeRect then +local t=r +if#t>e.closeCharWidth then +t=t:sub(1,e.closeCharWidth) +elseif#t0 then +local h=y+math.max(0,a)-1 +local a=e.closeRect.x1-t +if a<=h then +local e=h-a+1 +t=t-e +a=a+e +end +if t>0 then +if a0 then +s.text(a,p,string.rep(" ",t),o,i) +end +end +end +end +end +end +if t.bodyHeight>0 and t.bodyWidth>0 then +n(s,t.bodyX,t.bodyY,t.bodyWidth,t.bodyHeight,h,h) +local e=self:getSelectedTab() +if e then +self:_renderBody(s,v,t) +elseif self.emptyText then +local e=P(self.emptyText,t.bodyWidth) +local a=math.min(t.bodyHeight,#e) +for a=1,a do +local e=e[a] +if#e>t.bodyWidth then +e=e:sub(1,t.bodyWidth) +end +if#e0 then +self:_moveSelection(1) +elseif e<0 then +self:_moveSelection(-1) +end +return true +end +elseif t=="mouse_move"then +local e,t=... +if self:containsPoint(e,t)then +self._hoverIndex=self:_tabIndexFromPoint(e,t) +elseif self._hoverIndex then +self._hoverIndex=nil +end +elseif t=="mouse_drag"then +local a,e,t=... +if self:containsPoint(e,t)then +self._hoverIndex=self:_tabIndexFromPoint(e,t) +elseif self._hoverIndex then +self._hoverIndex=nil +end +elseif t=="key"then +if not self:isFocused()then +return false +end +local e=... +if e==a.left then +self:_moveSelection(-1) +return true +elseif e==a.right then +self:_moveSelection(1) +return true +elseif e==a.up then +self:_moveSelection(-1) +return true +elseif e==a.down then +self:_moveSelection(1) +return true +elseif e==a.home then +self:setSelectedIndex(1) +return true +elseif e==a["end"]then +self:setSelectedIndex(#self.tabs) +return true +elseif e==a.tab then +self:_moveSelection(1) +return true +elseif e==a.enter or e==a.space then +self:_emitSelect() +return true +end +end +return false +end +function m:new(i,t) +t=t or{} +local o=o(t)or{} +o.focusable=true +o.width=math.max(1,math.floor(o.width or 1)) +o.height=math.max(1,math.floor(o.height or 1)) +local a=setmetatable({},m) +a:_init_base(i,o) +a.focusable=true +a.menuBg=(t and t.menuBg)or e.black +a.menuFg=(t and t.menuFg)or e.white +a.highlightBg=(t and t.highlightBg)or e.lightGray +a.highlightFg=(t and t.highlightFg)or e.black +a.shortcutFg=(t and t.shortcutFg)or a.menuFg +a.disabledFg=(t and t.disabledFg)or e.lightGray +a.separatorColor=(t and t.separatorColor)or a.disabledFg +a.maxWidth=math.max(8,math.floor((t and t.maxWidth)or 32)) +if a.border==nil then +a.border=R(true) +end +a.onSelect=t and t.onSelect or nil +a.items=a:_normalizeItems(t and t.items or{}) +a._levels={} +a._open=false +a._previousFocus=nil +return a +end +function m:setItems(e) +self.items=self:_normalizeItems(e) +if self._open then +self:close() +end +end +function m:setOnSelect(e) +if e~=nil then +t(1,e,"function") +end +self.onSelect=e +end +function m:isOpen() +return self._open +end +function m:draw(e,e) +end +function m:_normalizeItem(e) +if e==nil then +return nil +end +if e=="-"then +return{type="separator"} +end +local t=type(e) +if t=="string"then +return{type="item",label=e,shortcut=nil,disabled=false} +end +if t~="table"then +return nil +end +if e.separator or e.type=="separator"then +return{type="separator"} +end +local t=e.label or e.text +if t==nil then +return nil +end +t=tostring(t) +local t={ +type="item", +label=t, +shortcut=e.shortcut and tostring(e.shortcut)or nil, +disabled=not not e.disabled, +action=e.onSelect or e.action or e.callback, +id=e.id, +value=e.value, +data=e.data +} +local e=e.submenu or e.items +if e then +local e=self:_normalizeItems(e) +if#e>0 then +t.submenu=e +end +end +return t +end +function m:_normalizeItems(t) +if type(t)~="table"then +return{} +end +local e={} +local a=true +for o=1,#t do +local t=self:_normalizeItem(t[o]) +if t then +if t.type=="separator"then +if not a and#e>0 then +e[#e+1]=t +a=true +end +else +e[#e+1]=t +a=false +end +end +end +while#e>0 and e[#e].type=="separator"do +e[#e]=nil +end +return e +end +function m:_firstEnabledIndex(e) +for t=1,#e do +local e=e[t] +if e and e.type=="item"and not e.disabled then +return t +end +end +return nil +end +function m:_maxWidthForLevel() +local e=self.maxWidth +local t=self.app and self.app.root +if t and t.width then +e=math.max(1,math.min(e,t.width)) +else +e=math.max(4,e) +end +return e +end +function m:_measureItems(i,s) +if not i or#i==0 then +return nil +end +local o=0 +local a=0 +for e=1,#i do +local e=i[e] +if e.type=="item"then +local t=#(e.label or"") +if t>o then +o=t +end +local e=e.shortcut +if e and e~=""then +local e=#e +if e>a then +a=e +end +end +end +end +local r=(self.border and self.border.left)and 1 or 0 +local h=(self.border and self.border.right)and 1 or 0 +local u=(self.border and self.border.top)and 1 or 0 +local l=(self.border and self.border.bottom)and 1 or 0 +local t=2 +local n=(a>0)and 2 or 0 +local e=o+t+n+a +if es then +e=s-r-h +if ei then +a=i +end +n=(a>0)and 2 or 0 +local i=e-t-n-a +if i<1 then +i=1 +end +if o>i then +o=i +end +e=o+t+n+a +d=e+r+h +end +end +if a==0 then +n=0 +end +return{ +itemWidth=e, +labelWidth=o, +shortcutWidth=a, +shortcutGap=n, +arrowWidth=t, +leftPad=r, +rightPad=h, +topPad=u, +bottomPad=l, +itemCount=#i, +totalWidth=d, +totalHeight=#i+u+l +} +end +function m:_buildLevel(r,a,n,u,l,e) +e=e or self:_measureItems(r,self:_maxWidthForLevel()) +if not e or e.itemCount==0 then +return nil +end +local t=self.app and self.app.root or nil +local i=t and t.width or nil +local t=t and t.height or nil +local h=e.totalWidth +local s=e.totalHeight +local o=math.floor(a) +local a=math.floor(n) +if i then +if o<1 then +o=1 +end +if o+h-1>i then +o=math.max(1,i-h+1) +end +end +if t then +if a<1 then +a=1 +end +if a+s-1>t then +a=math.max(1,t-s+1) +end +end +local t=o+e.leftPad +local d=a+e.topPad +local n=t+e.itemWidth-1 +if n0 then +i=n-e.shortcutWidth-1 +if is then +local s=((self.border and self.border.left)and 1 or 0) +local a=a.rect.x-o.rect.width+s +local e=self:_buildLevel(t.submenu,a,h,e,i,n) +if e then +o=e +end +end +self:_closeLevelsAfter(e) +self._levels[#self._levels+1]=o +end +function m:_findItemAtPoint(i,a) +local e=self._levels +if not e or#e==0 then +return nil +end +for o=#e,1,-1 do +local e=e[o] +local t=e.rect +if i>=t.x and i=t.y and a=e.contentY and a=1 and t<=#e.items then +return o,t +end +end +return o,nil +end +end +return nil +end +function m:_setHighlight(e,o,i) +local a=self._levels[e] +if not a then +return +end +if not o then +a.highlightIndex=nil +self:_closeLevelsAfter(e) +return +end +local t=a.items[o] +if not t or t.type~="item"or t.disabled then +a.highlightIndex=nil +self:_closeLevelsAfter(e) +return +end +a.highlightIndex=o +if t.submenu and#t.submenu>0 then +if i then +self:_openSubmenu(e,o) +end +else +self:_closeLevelsAfter(e) +end +end +function m:_handlePointerHover(e,t) +local e,t=self:_findItemAtPoint(e,t) +if not e then +return false +end +self:_setHighlight(e,t,true) +return true +end +function m:_handlePointerPress(a,e,t) +local e,a=self:_findItemAtPoint(e,t) +if not e then +self:close() +return false +end +if not a then +self:_closeLevelsAfter(e) +local e=self._levels[e] +if e then +e.highlightIndex=nil +end +return true +end +local o=self._levels[e] +local t=o and o.items[a] +if not t then +self:_closeLevelsAfter(e) +return true +end +if t.type=="separator"then +self:_closeLevelsAfter(e) +o.highlightIndex=nil +return true +end +if t.disabled then +self:_setHighlight(e,a,false) +return true +end +self:_setHighlight(e,a,false) +if t.submenu and#t.submenu>0 then +self:_openSubmenu(e,a) +return true +end +self:_activateItem(e,t) +return true +end +function m:_moveHighlight(i) +local e=self._levels +if not e or#e==0 then +return +end +local o=#e +local a=e[o] +local t=#a.items +if t==0 then +return +end +local e=a.highlightIndex or 0 +for n=1,t do +e=e+i +if e<1 then +e=t +elseif e>t then +e=1 +end +local t=a.items[e] +if t and t.type=="item"and not t.disabled then +self:_setHighlight(o,e,true) +return +end +end +end +function m:_activateHighlightedSubmenu() +local e=self._levels +if not e or#e==0 then +return +end +local a=#e +local t=e[a] +local e=t.highlightIndex +if not e then +return +end +local t=t.items[e] +if t and t.submenu and#t.submenu>0 then +self:_openSubmenu(a,e) +local e=self._levels[a+1] +if e and not e.highlightIndex then +e.highlightIndex=self:_firstEnabledIndex(e.items) +end +end +end +function m:_activateHighlightedItem() +local e=self._levels +if not e or#e==0 then +return +end +local t=#e +local e=e[t] +local a=e.highlightIndex +if not a then +return +end +local e=e.items[a] +if not e or e.type~="item"or e.disabled then +return +end +if e.submenu and#e.submenu>0 then +self:_openSubmenu(t,a) +local e=self._levels[t+1] +if e and not e.highlightIndex then +e.highlightIndex=self:_firstEnabledIndex(e.items) +end +return +end +self:_activateItem(t,e) +end +function m:_typeSearch(a) +if not a or a==""then +return +end +local e=self._levels +if not e or#e==0 then +return +end +local o=#e +local e=e[o] +local t=#e.items +if t==0 then +return +end +local i=e.highlightIndex or 0 +local n=a:lower() +for a=1,t do +local t=((i+a-1)%t)+1 +local e=e.items[t] +if e and e.type=="item"and not e.disabled then +local e=(e.label or""):lower() +if e:sub(1,1)==n then +self:_setHighlight(o,t,true) +return +end +end +end +end +function m:_activateItem(t,e) +if not e or e.type~="item"or e.disabled then +return +end +if e.action then +e.action(self,e) +end +if self.onSelect then +self.onSelect(self,e) +end +self:close() +end +function m:_setOpen(e) +e=not not e +if e then +if self._open then +return +end +self._open=true +if self.app then +self._previousFocus=self.app:getFocus() +self.app:_registerPopup(self) +self.app:setFocus(self) +end +else +if not self._open then +return +end +self._open=false +if self.app then +self.app:_unregisterPopup(self) +if self.app:getFocus()==self then +local e=self._previousFocus +if e and e.app==self.app and e.visible~=false then +self.app:setFocus(e) +else +self.app:setFocus(nil) +end +end +end +self._previousFocus=nil +self._levels={} +end +end +function m:open(o,i,a) +t(1,o,"number") +t(2,i,"number") +if a~=nil then +t(3,a,"table") +end +local e +if a and a.items then +e=self:_normalizeItems(a.items) +else +e=self.items +end +if not e or#e==0 then +self:close() +return false +end +local t=self:_measureItems(e,self:_maxWidthForLevel()) +if not t then +self:close() +return false +end +local a=math.floor(o) +local o=math.floor(i) +local a=a-t.leftPad +local o=o-t.topPad +local e=self:_buildLevel(e,a,o,nil,nil,t) +if not e then +self:close() +return false +end +self._levels={e} +self:_setOpen(true) +return true +end +function m:close() +self:_setOpen(false) +end +function m:_drawDropdown(o,r) +if not self._open or self.visible==false then +return +end +local t=self._levels +if not t or#t==0 then +return +end +for a=1,#t do +local t=t[a] +local a=t.rect +n(o,a.x,a.y,a.width,a.height,self.menuBg,self.menuBg) +z(o,a.x,a.y,a.width,a.height) +local i=t.items +for n=1,#i do +local a=i[n] +local i=t.contentY+n-1 +local h=t.highlightIndex==n and a.type=="item"and not a.disabled +local n=h and(self.highlightBg or self.menuBg)or self.menuBg +local s=self.menuFg or e.white +if a.type=="separator"then +local a=self.separatorColor or s +local e=string.rep("-",t.metrics.itemWidth) +o.text(t.contentX,i,e,a,n) +else +local s=a.disabled and(self.disabledFg or e.lightGray)or(h and(self.highlightFg or s)or s) +o.text(t.contentX,i,string.rep(" ",t.metrics.itemWidth),s,n) +local e=a.label or"" +if#e>t.metrics.labelWidth then +e=e:sub(1,t.metrics.labelWidth) +end +if#e>0 then +o.text(t.contentX,i,e,s,n) +end +if t.shortcutX then +local e=a.shortcut or"" +if#e>t.metrics.shortcutWidth then +e=e:sub(#e-t.metrics.shortcutWidth+1) +end +local a=math.max(0,t.metrics.shortcutWidth-#e) +if a>0 then +e=string.rep(" ",a)..e +end +local a=self.shortcutFg or s +o.text(t.shortcutX,i,e,a,n) +end +if a.submenu and a.submenu[1]~=nil then +o.text(t.arrowX,i,">",s,n) +end +end +end +if self.border then +p(r,a.x,a.y,a.width,a.height,self.border,self.menuBg) +end +end +end +function m:handleEvent(e,...) +if not self.visible or not self._open then +return false +end +if e=="mouse_click"then +local e,t,a=... +return self:_handlePointerPress(e,t,a) +elseif e=="monitor_touch"then +local a,e,t=... +return self:_handlePointerPress(1,e,t) +elseif e=="mouse_move"then +local e,t=... +return self:_handlePointerHover(e,t) +elseif e=="mouse_drag"then +local a,t,e=... +return self:_handlePointerHover(t,e) +elseif e=="mouse_scroll"then +self:close() +return false +elseif e=="key"then +if not self:isFocused()then +return false +end +local e=... +if e==a.down then +self:_moveHighlight(1) +return true +elseif e==a.up then +self:_moveHighlight(-1) +return true +elseif e==a.right then +self:_activateHighlightedSubmenu() +return true +elseif e==a.left then +if#self._levels>1 then +self:_closeLevelsAfter(#self._levels-1) +else +self:close() +end +return true +elseif e==a.enter or e==a.space then +self:_activateHighlightedItem() +return true +elseif e==a.escape then +self:close() +return true +end +elseif e=="char"then +if not self:isFocused()then +return false +end +local e=... +if e and#e>0 then +self:_typeSearch(e:sub(1,1)) +return true +end +elseif e=="paste"then +if not self:isFocused()then +return false +end +local e=... +if e and#e>0 then +self:_typeSearch(e:sub(1,1)) +return true +end +end +return false +end +local o={} +o.__index=o +setmetatable(o,{__index=s}) +local W={ +["and"]=true, +["break"]=true, +["do"]=true, +["else"]=true, +["elseif"]=true, +["end"]=true, +["false"]=true, +["for"]=true, +["function"]=true, +["goto"]=true, +["if"]=true, +["in"]=true, +["local"]=true, +["nil"]=true, +["not"]=true, +["or"]=true, +["repeat"]=true, +["return"]=true, +["then"]=true, +["true"]=true, +["until"]=true, +["while"]=true +} +local P={ +print=true, +ipairs=true, +pairs=true, +next=true, +math=true, +table=true, +string=true, +coroutine=true, +os=true, +tonumber=true, +tostring=true, +type=true, +pcall=true, +xpcall=true, +select=true +} +local function Y(t) +if t==nil or t==""then +return{""} +end +local e={} +local a=1 +local i=#t +while a<=i do +local o=t:find("\n",a,true) +if not o then +e[#e+1]=t:sub(a) +break +end +e[#e+1]=t:sub(a,o-1) +a=o+1 +if a>i then +e[#e+1]="" +break +end +end +if#e==0 then +e[1]="" +end +return e +end +local function B(e) +return table.concat(e,"\n") +end +local function O(e,t,a) +if ea then +return a +end +return e +end +local function M(e,t,o,a) +if eo then +return 1 +end +if ta then +return 1 +end +return 0 +end +local function V(t,e,o,a,i,n) +if M(t,e,o,a)<0 then +return false +end +if M(t,e,i,n)>=0 then +return false +end +return true +end +local function X(a) +if a==nil then +return nil +end +if a==true then +a="lua" +end +if type(a)=="string"then +if a=="lua"then +return{ +language="lua", +keywords=W, +builtins=P, +keywordColor=e.orange, +commentColor=e.lightGray, +stringColor=e.yellow, +numberColor=e.cyan, +builtinColor=e.lightBlue +} +end +return nil +end +if type(a)=="table"then +local t={} +for a,e in pairs(a)do +t[a]=e +end +if t.language=="lua"then +t.keywords=t.keywords or W +t.builtins=t.builtins or P +if t.keywordColor==nil then +t.keywordColor=e.orange +end +if t.commentColor==nil then +t.commentColor=e.lightGray +end +if t.stringColor==nil then +t.stringColor=e.yellow +end +if t.numberColor==nil then +t.numberColor=e.cyan +end +if t.builtinColor==nil then +t.builtinColor=e.lightBlue +end +end +return t +end +return nil +end +function o:new(n,a) +a=a or{} +local i={} +for e,t in pairs(a)do +i[e]=t +end +i.focusable=true +i.width=math.max(4,math.floor(i.width or 16)) +i.height=math.max(1,math.floor(i.height or(a.multiline~=false and 5 or 1))) +local t=setmetatable({},o) +t:_init_base(n,i) +t.focusable=true +t.placeholder=a.placeholder or"" +t.placeholderColor=a.placeholderColor or a.placeholderFg +t.onChange=a.onChange or nil +t.onCursorMove=a.onCursorMove or nil +t.maxLength=a.maxLength or nil +t.multiline=a.multiline~=false +t.numericOnly=not not a.numericOnly +if t.numericOnly then +t.multiline=false +end +t.tabWidth=math.max(1,math.floor(a.tabWidth or 4)) +t.selectionBg=a.selectionBg or e.lightGray +t.selectionFg=a.selectionFg or e.black +t.overlayBg=a.overlayBg or e.gray +t.overlayFg=a.overlayFg or e.white +t.overlayActiveBg=a.overlayActiveBg or e.orange +t.overlayActiveFg=a.overlayActiveFg or e.black +t.autocomplete=a.autocomplete +t.autocompleteAuto=not not a.autocompleteAuto +t.autocompleteMaxItems=math.max(1,math.floor(a.autocompleteMaxItems or 5)) +t.autocompleteBg=a.autocompleteBg or e.gray +t.autocompleteFg=a.autocompleteFg or e.white +t.autocompleteHighlightBg=a.autocompleteHighlightBg or e.lightBlue +t.autocompleteHighlightFg=a.autocompleteHighlightFg or e.black +t.autocompleteBorder=R(a.autocompleteBorder==false and false or a.autocompleteBorder or true) +t.autocompleteMaxWidth=math.max(4,math.floor(a.autocompleteMaxWidth or math.max(t.width or i.width or 16,16))) +t.autocompleteGhostColor=a.autocompleteGhostColor or e.lightGray +t.syntax=X(a.syntax) +t._lines={""} +t.text="" +t._cursorLine=1 +t._cursorCol=1 +t._preferredCol=1 +t._selectionAnchor=nil +t._scrollX=0 +t._scrollY=0 +t._shiftDown=false +t._ctrlDown=false +t._dragging=false +t._dragButton=nil +t._dragAnchor=nil +t._find={ +visible=false, +activeField="find", +findText="", +replaceText="", +matchCase=false, +matches={}, +index=0 +} +t._autocompleteState={ +visible=false, +items={}, +selectedIndex=1, +anchorLine=1, +anchorCol=1, +prefix="", +ghost="", +trigger="auto", +rect=nil +} +t._open=false +t.scrollbar=L(a.scrollbar,t.bg or e.black,t.fg or e.white) +t:_setTextInternal(a.text or"",true,true) +if a.cursorPos then +t:_moveCursorToIndex(a.cursorPos) +end +t:_ensureCursorVisible() +return t +end +function o:setOnCursorMove(e) +if e~=nil then +t(1,e,"function") +end +self.onCursorMove=e +end +function o:setScrollbar(t) +self.scrollbar=L(t,self.bg or e.black,self.fg or e.white) +end +function o:setPlaceholderColor(e) +if e~=nil then +t(1,e,"number") +end +self.placeholderColor=e +end +function o:setNumericOnly(e) +if e==nil then +e=true +else +t(1,e,"boolean") +end +self.numericOnly=not not e +if self.numericOnly then +self.multiline=false +end +local e=self.text +if self.numericOnly then +e=self:_sanitizeNumericInput(e) +if not self:_isNumericText(e)then +e="" +end +end +if e~=self.text then +self:_setTextInternal(e,true,false) +end +end +function o:onFocusChanged(e) +if not e then +self:_hideAutocomplete() +end +self:_ensureCursorVisible() +end +function o:_applyMaxLength(e) +if not self.maxLength then +return e +end +if#e<=self.maxLength then +return e +end +return e:sub(1,self.maxLength) +end +function o:_positionToIndex(t,e) +t=O(t or 1,1,#self._lines) +local e=(e or 1)-1 +if e<0 then +e=0 +end +for t=1,t-1 do +e=e+#self._lines[t]+1 +end +return e+1 +end +function o:_getSelectionIndices() +if self:_hasSelection()then +local o,t,a,e=self:_getSelectionRange() +local t=self:_positionToIndex(o,t) +local e=self:_positionToIndex(a,e) +return t,e +end +local e=self:_positionToIndex(self._cursorLine,self._cursorCol) +return e,e +end +function o:_simulateReplacementText(e) +local t,a=self:_getSelectionIndices() +local t=self.text:sub(1,t-1) +local a=self.text:sub(a) +return t..(e or"")..a +end +function o:_sanitizeNumericInput(e) +if not e or e==""then +return"" +end +local e=tostring(e):gsub("[^0-9%+%-%.]","") +return e +end +function o:_isNumericText(e) +if e==nil or e==""then +return true +end +if e=="+"or e=="-"then +return true +end +if e=="."or e=="+."or e=="-."then +return true +end +if e:match("^[+-]?%d+$")then +return true +end +if e:match("^[+-]?%d+%.%d*$")then +return true +end +if e:match("^[+-]?%d*%.%d+$")then +return true +end +return false +end +function o:_allowsNumericInsertion(e) +local e=self:_simulateReplacementText(e) +return self:_isNumericText(e) +end +function o:_syncTextFromLines() +self.text=B(self._lines) +end +function o:_setTextInternal(e,a,t) +e=tostring(e or"") +if self.numericOnly then +e=self:_sanitizeNumericInput(e) +if not self:_isNumericText(e)then +e="" +end +end +e=self:_applyMaxLength(e) +self._lines=Y(e) +self:_syncTextFromLines() +if a then +self._cursorLine=#self._lines +self._cursorCol=(#self._lines[#self._lines]or 0)+1 +else +self._cursorLine=O(self._cursorLine,1,#self._lines) +local e=self._lines[self._cursorLine]or"" +self._cursorCol=O(self._cursorCol,1,#e+1) +end +self._preferredCol=self._cursorCol +self._selectionAnchor=nil +self:_ensureCursorVisible() +if not t then +self:_notifyChange() +self:_notifyCursorChange() +end +end +function o:_indexToPosition(e) +e=O(e or 1,1,#self.text+1) +local e=e-1 +for t=1,#self._lines do +local a=self._lines[t] +local a=#a +if e<=a then +return t,e+1 +end +e=e-(a+1) +end +local e=#self._lines +local t=#self._lines[e] +return e,t+1 +end +function o:_moveCursorToIndex(e) +local e,t=self:_indexToPosition(e) +self:_setCursorPosition(e,t) +end +function o:getCursorPosition() +return self._cursorLine,self._cursorCol +end +function o:getLineCount() +return#self._lines +end +function o:_getInnerMetrics() +local e=self.border +local t=(e and e.left)and 1 or 0 +local n=(e and e.right)and 1 or 0 +local a=(e and e.top)and 1 or 0 +local e=(e and e.bottom)and 1 or 0 +local o,i=self:getAbsoluteRect() +local o=o+t +local i=i+a +local s=math.max(0,self.width-t-n) +local n=math.max(0,self.height-a-e) +return o,i,s,n,t,a,e +end +function o:_getOverlayHeight(e) +if not self._find.visible then +return 0 +end +return math.min(2,e) +end +function o:_computeLayoutMetrics() +local n,i,a,e=self:getAbsoluteRect() +local s,r,t,o=self:_getInnerMetrics() +if t<=0 or o<=0 then +s=n +r=i +t=math.max(1,a) +o=math.max(1,e) +end +local h=self:_getOverlayHeight(o) +local n=math.max(1,o-h) +local e,a=F(self.scrollbar,#self._lines,n,t) +if e>0 and t-e<1 then +if a and(a.alwaysVisible or#self._lines>n)then +e=math.max(0,t-1) +else +e=0 +a=nil +end +end +if e<=0 then +e=0 +a=nil +end +local i=t-e +if i<1 then +i=t +e=0 +a=nil +end +return{ +innerX=s, +innerY=r, +innerWidth=t, +innerHeight=o, +contentWidth=i, +contentHeight=n, +overlayHeight=h, +scrollbarWidth=e, +scrollbarStyle=a, +scrollbarX=s+i +} +end +function o:_getContentSize() +local e=self:_computeLayoutMetrics() +return math.max(1,e.contentWidth),math.max(1,e.contentHeight) +end +function o:_ensureCursorVisible() +local t,e=self:_getContentSize() +local o=self._scrollY+1 +local a=self._scrollY+e +if self._cursorLinea then +self._scrollY=self._cursorLine-e +end +if self._scrollY<0 then +self._scrollY=0 +end +local e=math.max(0,#self._lines-e) +if self._scrollY>e then +self._scrollY=e +end +local a=self._scrollX+1 +local e=self._scrollX+t +if self._cursorCole then +self._scrollX=self._cursorCol-t +end +if self._scrollX<0 then +self._scrollX=0 +end +local e=self._lines[self._cursorLine]or"" +local e=math.max(0,#e+1-t) +if self._scrollX>e then +self._scrollX=e +end +end +function o:_notifyChange() +if self.onChange then +self.onChange(self,self.text) +end +end +function o:_notifyCursorChange() +if self.onCursorMove then +self.onCursorMove(self,self._cursorLine,self._cursorCol,self:getSelectionLength()) +end +end +function o:_hasSelection() +if not self._selectionAnchor then +return false +end +if self._selectionAnchor.line~=self._cursorLine then +return true +end +return self._selectionAnchor.col~=self._cursorCol +end +function o:getSelectionLength() +if not self:_hasSelection()then +return 0 +end +local t,e,a,o=self:_getSelectionRange() +local e=self:_collectRange(t,e,a,o) +return#e +end +function o:getSelectionText() +if not self:_hasSelection()then +return"" +end +local e,a,t,o=self:_getSelectionRange() +return self:_collectRange(e,a,t,o) +end +function o:_getSelectionRange() +if not self:_hasSelection()then +return nil +end +local e=self._selectionAnchor +local o,a=e.line,e.col +local e,t=self._cursorLine,self._cursorCol +if M(o,a,e,t)<=0 then +return o,a,e,t +else +return e,t,o,a +end +end +function o:_collectRange(t,i,a,o) +if t==a then +return(self._lines[t]or""):sub(i,o-1) +end +local e={} +e[#e+1]=(self._lines[t]or""):sub(i) +for t=t+1,a-1 do +e[#e+1]=self._lines[t]or"" +end +e[#e+1]=(self._lines[a]or""):sub(1,o-1) +return table.concat(e,"\n") +end +function o:_clearSelection() +self._selectionAnchor=nil +end +function o:_removeRange(e,a,t,o) +if e==t then +local t=self._lines[e] +self._lines[e]=t:sub(1,a-1)..t:sub(o) +else +local i=self._lines[e]:sub(1,a-1) +local a=self._lines[t]:sub(o) +for e=t,e+1,-1 do +table.remove(self._lines,e) +end +self._lines[e]=i..a +end +if#self._lines==0 then +self._lines[1]="" +end +end +function o:_insertAt(e,a,t) +if t==nil or t==""then +return e,a +end +local t=Y(t) +local o=self._lines[e] +local i=o:sub(1,a-1) +local a=o:sub(a) +self._lines[e]=i..t[1] +local e=e +for a=2,#t do +e=e+1 +table.insert(self._lines,e,t[a]) +end +self._lines[e]=self._lines[e]..a +local t=(#self._lines[e]-#a)+1 +return e,t +end +function o:_deleteSelection(n) +local e,t,a,o=self:_getSelectionRange() +if not e then +return 0 +end +local i=self:_collectRange(e,t,a,o) +self:_removeRange(e,t,a,o) +self._cursorLine=e +self._cursorCol=t +self._preferredCol=self._cursorCol +self:_clearSelection() +self:_syncTextFromLines() +self:_ensureCursorVisible() +if not n then +self:_notifyChange() +end +self:_notifyCursorChange() +return#i +end +function o:_replaceSelection(e,a) +local t=0 +if self:_hasSelection()then +t=self:_deleteSelection(true) +end +local t=#self.text +if self.maxLength then +local t=self.maxLength-t +if#e>t then +e=e:sub(1,t) +end +end +local t,e=self:_insertAt(self._cursorLine,self._cursorCol,e) +self._cursorLine=t +self._cursorCol=e +self._preferredCol=self._cursorCol +self:_clearSelection() +self:_syncTextFromLines() +self:_ensureCursorVisible() +if not a then +self:_notifyChange() +end +self:_notifyCursorChange() +return true +end +function o:_insertTextAtCursor(e) +if not e or e==""then +return false +end +if self.numericOnly then +local t=self:_sanitizeNumericInput(e) +if t==""then +return false +end +if not self:_allowsNumericInsertion(t)then +return false +end +e=t +end +return self:_replaceSelection(e,false) +end +function o:_insertCharacter(e) +if not e or e==""then +return false +end +return self:_insertTextAtCursor(e) +end +function o:_insertNewline() +if self.numericOnly then +return false +end +if not self.multiline then +return false +end +return self:_insertTextAtCursor("\n") +end +function o:_insertTab() +if self.numericOnly then +return false +end +local e=string.rep(" ",self.tabWidth) +return self:_insertTextAtCursor(e) +end +function o:_deleteBackward() +if self:_hasSelection()then +return self:_deleteSelection(false)>0 +end +if self._cursorLine==1 and self._cursorCol==1 then +return false +end +if self._cursorCol>1 then +local e=self._lines[self._cursorLine] +self._lines[self._cursorLine]=e:sub(1,self._cursorCol-2)..e:sub(self._cursorCol) +self._cursorCol=self._cursorCol-1 +else +local e=self._lines[self._cursorLine-1] +local t=self._lines[self._cursorLine] +local a=#e +self._lines[self._cursorLine-1]=e..t +table.remove(self._lines,self._cursorLine) +self._cursorLine=self._cursorLine-1 +self._cursorCol=a+1 +end +self._preferredCol=self._cursorCol +self:_syncTextFromLines() +self:_ensureCursorVisible() +self:_notifyChange() +self:_notifyCursorChange() +return true +end +function o:_deleteForward() +if self:_hasSelection()then +return self:_deleteSelection(false)>0 +end +local e=self._lines[self._cursorLine] +if self._cursorCol<=#e then +self._lines[self._cursorLine]=e:sub(1,self._cursorCol-1)..e:sub(self._cursorCol+1) +else +if self._cursorLine>=#self._lines then +return false +end +local t=table.remove(self._lines,self._cursorLine+1) +self._lines[self._cursorLine]=e..t +end +self:_syncTextFromLines() +self:_ensureCursorVisible() +self:_notifyChange() +self:_notifyCursorChange() +return true +end +function o:_setCursorPosition(t,a,e) +e=e or{} +t=O(t,1,#self._lines) +local o=self._lines[t]or"" +a=O(a,1,#o+1) +if e.extendSelection then +if not self._selectionAnchor then +self._selectionAnchor={line=self._cursorLine,col=self._cursorCol} +end +else +self:_clearSelection() +end +self._cursorLine=t +self._cursorCol=a +if not e.preservePreferred then +self._preferredCol=a +end +if self._selectionAnchor and self._selectionAnchor.line==self._cursorLine and self._selectionAnchor.col==self._cursorCol then +self:_clearSelection() +end +self:_ensureCursorVisible() +self:_notifyCursorChange() +if not e.keepAutocomplete then +self:_hideAutocomplete() +end +end +function o:_moveCursorLeft(e) +if self:_hasSelection()and not e then +local t,e=self:_getSelectionRange() +self:_setCursorPosition(t,e) +return +end +if self._cursorCol>1 then +self:_setCursorPosition(self._cursorLine,self._cursorCol-1,{extendSelection=e}) +elseif self._cursorLine>1 then +local t=self._cursorLine-1 +local a=(#self._lines[t]or 0)+1 +self:_setCursorPosition(t,a,{extendSelection=e}) +end +end +function o:_moveCursorRight(e) +if self:_hasSelection()and not e then +local a,a,e,t=self:_getSelectionRange() +self:_setCursorPosition(e,t) +return +end +local t=self._lines[self._cursorLine] +if self._cursorCol<=#t then +self:_setCursorPosition(self._cursorLine,self._cursorCol+1,{extendSelection=e}) +elseif self._cursorLine<#self._lines then +self:_setCursorPosition(self._cursorLine+1,1,{extendSelection=e}) +end +end +function o:_moveCursorVertical(e,t) +local e=O(self._cursorLine+e,1,#self._lines) +local a=self._lines[e]or"" +local a=O(self._preferredCol,1,#a+1) +self:_setCursorPosition(e,a,{extendSelection=t,preservePreferred=true}) +end +function o:_moveCursorUp(e) +self:_moveCursorVertical(-1,e) +end +function o:_moveCursorDown(e) +self:_moveCursorVertical(1,e) +end +function o:_moveCursorLineStart(e) +self:_setCursorPosition(self._cursorLine,1,{extendSelection=e}) +end +function o:_moveCursorLineEnd(t) +local e=self._lines[self._cursorLine] +self:_setCursorPosition(self._cursorLine,#e+1,{extendSelection=t}) +end +function o:_moveCursorDocumentStart(e) +self:_setCursorPosition(1,1,{extendSelection=e}) +end +function o:_moveCursorDocumentEnd(t) +local e=#self._lines +local a=#self._lines[e] +self:_setCursorPosition(e,a+1,{extendSelection=t}) +end +function o:_selectAll() +self._selectionAnchor={line=1,col=1} +self:_setCursorPosition(#self._lines,(#self._lines[#self._lines]or 0)+1,{extendSelection=true,keepAutocomplete=true}) +end +function o:_scrollLines(e) +if e==0 then +return +end +local a,t=self:_getContentSize() +local t=math.max(0,#self._lines-t) +self._scrollY=O(self._scrollY+e,0,t) +end +function o:_scrollColumns(e) +if e==0 then +return +end +local t=select(1,self:_getContentSize()) +local a=self._lines[self._cursorLine]or"" +local t=math.max(0,#a-t) +self._scrollX=O(self._scrollX+e,0,t) +end +function o:_cursorFromPoint(a,t) +local e=self:_computeLayoutMetrics() +local o=e.innerX +local i=e.innerY +local n=math.max(1,e.contentWidth) +local e=math.max(1,e.contentHeight) +local a=O(a-o,0,n-1) +local e=O(t-i,0,e-1) +local e=O(self._scrollY+e+1,1,#self._lines) +local t=self._lines[e]or"" +local t=O(self._scrollX+a+1,1,#t+1) +return e,t +end +function o:_computeSyntaxColors(o) +local t=self.syntax +if not t then +return nil +end +local a={} +local i=t.defaultColor or self.fg or e.white +for e=1,#o do +a[e]=i +end +local e=1 +while e<=#o do +local i=o:sub(e,e) +if i=='"'or i=="'"then +local i=i +a[e]=t.stringColor or a[e] +e=e+1 +while e<=#o do +a[e]=t.stringColor or a[e] +local t=o:sub(e,e) +if t==i and o:sub(e-1,e-1)~="\\"then +e=e+1 +break +end +e=e+1 +end +else +e=e+1 +end +end +for e,n,o in o:gmatch("()(%d+%.?%d*)()")do +if t.numberColor then +for e=e,o-1 do +if a[e]==i then +a[e]=t.numberColor +end +end +end +end +for s,n,o in o:gmatch("()([%a_][%w_]*)()")do +local e=n:lower() +if t.keywords and t.keywords[e]then +if t.keywordColor then +for e=s,o-1 do +if a[e]==i then +a[e]=t.keywordColor +end +end +end +elseif t.builtins and t.builtins[n]then +if t.builtinColor then +for e=s,o-1 do +if a[e]==i then +a[e]=t.builtinColor +end +end +end +end +end +local e=o:find("--",1,true) +if e then +local t=t.commentColor or i +for e=e,#o do +a[e]=t +end +end +return a +end +local function R(t,a,o,i) +if a==""then +return +end +local e=t[#t] +if e and e.fg==o and e.bg==i then +e.text=e.text..a +else +t[#t+1]={text=a,fg=o,bg=i} +end +end +function o:_buildLineSegments(n,t,r,h,e) +local a=self._lines[n]or"" +local o=self:_computeSyntaxColors(a) +local i=self._scrollX+1 +local s={} +for t=0,t-1 do +local t=i+t +local i +if t<=#a then +i=a:sub(t,t) +else +i=" " +end +local o=o and o[t]or r +local a=h +if e and V(n,t,e.startLine,e.startCol,e.endLine,e.endCol)then +a=self.selectionBg +o=self.selectionFg +end +R(s,i,o,a) +end +return s,a,o +end +function o:_drawSegments(a,t,o,e) +local t=t +for i=1,#e do +local e=e[i] +if e.text~=""then +a.text(t,o,e.text,e.fg,e.bg) +t=t+#e.text +end +end +end +function o:_drawFindOverlay(h,a,n,t,i) +if not self._find.visible then +return +end +local o=self:_getOverlayHeight(i) +if o<=0 then +return +end +local r=self.overlayBg or self.bg or e.gray +local d=self.overlayFg or self.fg or e.white +local c=self.overlayActiveBg or e.orange +local u=self.overlayActiveFg or e.black +local n=n+i-o +for e=0,o-1 do +h.text(a,n+e,string.rep(" ",t),d,r) +end +local e=self._find +local i=#e.matches +local i=i>0 and string.format("%d/%d",math.max(1,e.index),i)or"0/0" +local s=e.matchCase and"CASE"or"case" +local i=string.format("Find: %s %s %s",e.findText,i,s) +local l="Replace: "..e.replaceText +local s=i +if#s>t then +s=s:sub(1,t) +end +local i=l +if#i>t then +i=i:sub(1,t) +end +h.text(a,n,s..string.rep(" ",math.max(0,t-#s)),d,r) +h.text(a,n+math.max(o-1,0),i..string.rep(" ",math.max(0,t-#i)),d,r) +local i,s,l +if e.activeField=="find"then +i=a+6 +s=n +l=e.findText +else +i=a+9 +s=n+math.max(o-1,0) +l=e.replaceText +end +local e=l +if#e>t-(i-a)then +e=e:sub(1,t-(i-a)) +end +h.text(i,s,e..string.rep(" ",math.max(0,t-(i-a)-#e)),u,c) +if o>=2 then +local e="Ctrl+G next | Ctrl+Shift+G prev | Tab switch | Enter apply | Esc close" +if#e>t then +e=e:sub(1,t) +end +h.text(a,n+o-1,e..string.rep(" ",math.max(0,t-#e)),d,r) +end +end +function o:_setAutocompleteVisible(e) +local t=self._autocompleteState +e=not not e +if t.visible==e then +if not e then +t.rect=nil +end +return +end +t.visible=e +if e then +self._open=true +if self.app then +self.app:_registerPopup(self) +end +else +self._open=false +t.rect=nil +if self.app then +self.app:_unregisterPopup(self) +end +end +end +function o:_refreshAutocompleteGhost() +local e=self._autocompleteState +e.ghost=self:_computeAutocompleteGhost(e.items[e.selectedIndex],e.prefix,e.trigger) +end +function o:_hideAutocomplete() +local e=self._autocompleteState +if e.visible then +self:_setAutocompleteVisible(false) +else +e.rect=nil +end +e.items={} +e.ghost="" +e.prefix="" +e.trigger="auto" +e.selectedIndex=1 +e.anchorLine=self._cursorLine +e.anchorCol=self._cursorCol +end +function o:_isPointInAutocomplete(t,a) +local e=self._autocompleteState and self._autocompleteState.rect +if not e then +return false +end +return t>=e.x and t=e.y and a=e.contentY+e.itemCount then +return nil +end +if a=e.contentX+e.itemWidth then +return nil +end +local t=t-e.contentY+1 +if t<1 or t>e.itemCount then +return nil +end +return t +end +function o:_drawDropdown(c,y) +local t=self._autocompleteState +if not self.visible or not self._open then +if t then +t.rect=nil +end +return +end +if not t or not t.visible or#t.items==0 then +if t then +t.rect=nil +end +return +end +local a=self:_computeLayoutMetrics() +local w=a.innerX +local o=a.innerY +local h=a.contentWidth +local a=a.contentHeight +if h<=0 or a<=0 then +t.rect=nil +return +end +local a=O(t.anchorLine-(self._scrollY+1),0,a-1) +local f=o+a +local a=self.autocompleteBorder +local m=(a and a.top)and 1 or 0 +local o=(a and a.bottom)and 1 or 0 +local u=(a and a.left)and 1 or 0 +local r=(a and a.right)and 1 or 0 +local d=#t.items +local i=d+m+o +if i<=0 then +t.rect=nil +return +end +local o=0 +for e=1,d do +local e=t.items[e] +local e=e and e.label or"" +if#e>o then +o=#e +end +end +local s=self.autocompleteMaxWidth or h +s=math.max(1,s) +local l=math.min(h,s) +local o=math.max(l,o) +if o>s then +o=s +end +local l=self.app and self.app.root and self.app.root.width or(w+h-1) +if o+u+r>l then +o=math.max(1,l-u-r) +end +local r=o+u+r +if r<=0 or o<=0 then +t.rect=nil +return +end +local s=O(t.anchorCol-self._scrollX-1,0,h-1) +local s=w+s +local s=s +if s+r-1>l then +s=math.max(1,l-r+1) +end +if s<1 then +s=1 +end +local h=f+1 +if m>0 then +h=h+1 +end +local w=self.app and self.app.root and self.app.root.height or(f+i) +local h=h +if h+i-1>w then +local e=f-i +if e>=1 then +h=e +else +h=math.max(1,w-i+1) +end +end +local f=s+u +local u=h+m +t.rect={ +x=s, +y=h, +width=r, +height=i, +contentX=f, +contentY=u, +itemWidth=o, +itemCount=d +} +local l=self.autocompleteBg or self.bg or e.gray +n(c,s,h,r,i,l,l) +z(c,s,h,r,i) +local n=self.autocompleteFg or self.fg or e.white +local m=self.autocompleteHighlightBg or e.lightBlue +local v=self.autocompleteHighlightFg or e.black +for a=1,d do +local i=u+a-1 +if i<1 or i>w then +break +end +local e=t.items[a] +local e=e and e.label or"" +if#e>o then +e=e:sub(1,o) +end +local o=o-#e +if o>0 then +e=e..string.rep(" ",o) +end +local o=(a==t.selectedIndex)and m or l +local t=(a==t.selectedIndex)and v or n +c.text(f,i,e,t,o) +end +if a then +p(y,s,h,r,i,a,l) +end +end +function o:_updateAutocomplete(n) +if not self.autocomplete then +self:_hideAutocomplete() +return +end +local e=self._lines[self._cursorLine]or"" +local a=self._cursorCol-1 +local t=a +while t>=1 do +local e=e:sub(t,t) +if not e:match("[%w_]")then +break +end +t=t-1 +end +t=t+1 +local o=e:sub(t,a) +if o==""and n~="manual"then +self:_hideAutocomplete() +return +end +local e={} +if type(self.autocomplete)=="function"then +local a,t=pcall(self.autocomplete,self,o) +if a and type(t)=="table"then +e=t +end +elseif type(self.autocomplete)=="table"then +e=self.autocomplete +end +local a={} +local i=o:lower() +for t=1,#e do +local e=e[t] +if type(e)=="string"then +local t=e:lower() +if o==""or t:sub(1,#i)==i then +a[#a+1]={label=e,insert=e} +end +elseif type(e)=="table"and e.label then +local t=e.label +local n=t:lower() +if o==""or n:sub(1,#i)==i then +a[#a+1]={label=t,insert=e.insert or t} +end +end +end +if#a==0 then +self:_hideAutocomplete() +return +end +local e=self._autocompleteState +local i +if e.visible and e.items and e.selectedIndex and e.items[e.selectedIndex]then +local e=e.items[e.selectedIndex] +i=e.insert or e.label +end +e.trigger=n or"auto" +self:_setAutocompleteVisible(true) +e.items={} +local s=math.min(self.autocompleteMaxItems,#a) +local n=1 +for t=1,s do +local a=a[t] +e.items[t]=a +if i then +local e=a.insert or a.label +if e==i then +n=t +end +end +end +e.selectedIndex=n +e.anchorLine=self._cursorLine +e.anchorCol=t +e.prefix=o +self:_refreshAutocompleteGhost() +e.rect=nil +end +function o:_computeAutocompleteGhost(e,t,a) +if not e then +return"" +end +local e=e.insert or e.label or"" +if e==""then +return"" +end +if t==""then +if a=="manual"then +return e +end +return"" +end +local o=e:lower() +local a=t:lower() +if o:sub(1,#t)~=a then +return"" +end +return e:sub(#t+1) +end +function o:_acceptAutocomplete() +local e=self._autocompleteState +if not e.visible or#e.items==0 then +return false +end +local t=e.items[e.selectedIndex] +if not t then +return false +end +local o,a=self._cursorLine,self._cursorCol +self._selectionAnchor={line=e.anchorLine,col=e.anchorCol} +self._cursorLine=o +self._cursorCol=a +self:_replaceSelection(t.insert or t.label or"",false) +self:_hideAutocomplete() +return true +end +function o:_moveAutocompleteSelection(a) +local e=self._autocompleteState +if not e.visible then +return +end +local t=#e.items +if t==0 then +return +end +e.selectedIndex=((e.selectedIndex-1+a)%t)+1 +self:_refreshAutocompleteGhost() +end +function o:_toggleFindOverlay(e) +local t=self._find +if t.visible and(not e or t.activeField==e)then +self:_closeFindOverlay() +return +end +t.visible=true +if e then +t.activeField=e +end +if self:_hasSelection()and e=="find"then +t.findText=self:getSelectionText() +end +self:_updateFindMatches(true) +end +function o:_closeFindOverlay() +local e=self._find +if e.visible then +e.visible=false +e.matches={} +e.index=0 +end +end +function o:_toggleFindField() +local e=self._find +if not e.visible then +return +end +if e.activeField=="find"then +e.activeField="replace" +else +e.activeField="find" +end +end +function o:_editFindFieldText(t) +local e=self._find +if not e.visible then +return +end +t=tostring(t or"") +t=t:gsub("[\r\n]"," ") +if e.activeField=="find"then +e.findText=e.findText..t +self:_updateFindMatches(true) +elseif e.activeField=="replace"then +e.replaceText=e.replaceText..t +end +end +function o:_handleOverlayBackspace() +local e=self._find +if not e.visible then +return false +end +if e.activeField=="find"then +if#e.findText==0 then +return false +end +e.findText=e.findText:sub(1,-2) +self:_updateFindMatches(true) +else +if#e.replaceText==0 then +return false +end +e.replaceText=e.replaceText:sub(1,-2) +end +return true +end +function o:_updateFindMatches(t) +local e=self._find +e.matches={} +e.index=t and 0 or e.index +if not e.visible or e.findText==""then +return +end +local t=e.findText +local o=e.matchCase +for i=1,#self._lines do +local a=self._lines[i] +local n=o and a or a:lower() +local t=o and t or t:lower() +local a=1 +while true do +local t,o=n:find(t,a,true) +if not t then +break +end +e.matches[#e.matches+1]={ +line=i, +col=t, +length=o-t+1 +} +a=t+1 +end +end +end +function o:_selectMatch(e) +if not e then +return +end +self._selectionAnchor={line=e.line,col=e.col} +self:_setCursorPosition(e.line,e.col+e.length,{extendSelection=true,keepAutocomplete=true}) +self:_ensureCursorVisible() +self:_notifyCursorChange() +end +function o:_gotoMatch(o) +local e=self._find +if not e.visible then +return false +end +self:_updateFindMatches(false) +if#e.matches==0 then +return false +end +if e.index<1 then +local t=1 +for a=1,#e.matches do +local e=e.matches[a] +if M(e.line,e.col,self._cursorLine,self._cursorCol)>=0 then +t=a +break +end +end +e.index=t +else +e.index=((e.index-1+o)%#e.matches)+1 +end +self:_selectMatch(e.matches[e.index]) +return true +end +function o:_gotoNextMatch() +return self:_gotoMatch(1) +end +function o:_gotoPreviousMatch() +return self:_gotoMatch(-1) +end +function o:_replaceCurrentMatch() +local e=self._find +if not e.visible or#e.matches==0 then +return false +end +if e.index<1 or e.index>#e.matches then +e.index=1 +end +local t=e.matches[e.index] +self._selectionAnchor={line=t.line,col=t.col} +self:_setCursorPosition(t.line,t.col+t.length,{extendSelection=true,keepAutocomplete=true}) +self:_replaceSelection(e.replaceText or"",false) +self:_updateFindMatches(true) +return true +end +function o:_replaceAll() +local e=self._find +if not e.visible or e.findText==""then +return false +end +self:_updateFindMatches(true) +if#e.matches==0 then +return false +end +for t=#e.matches,1,-1 do +local t=e.matches[t] +local a=t.line +local o=t.col +local i=self._lines[a] +self._lines[a]=i:sub(1,o-1)..(e.replaceText or"")..i:sub(o+t.length) +end +self:_syncTextFromLines() +self:_ensureCursorVisible() +self:_notifyChange() +self:_notifyCursorChange() +self:_updateFindMatches(true) +return true +end +function o:_handleEscape() +if self._find.visible then +self:_closeFindOverlay() +return true +end +if self:_hasSelection()then +self:_clearSelection() +self:_notifyCursorChange() +return true +end +if self._autocompleteState.visible then +self:_hideAutocomplete() +return true +end +return false +end +function o:_handleKey(e,t) +if self._find.visible then +if e==a.tab then +self:_toggleFindField() +return true +elseif e==a.backspace then +return self:_handleOverlayBackspace() +elseif e==a.enter then +if self._find.activeField=="find"then +self:_gotoNextMatch() +else +self:_replaceCurrentMatch() +end +return true +elseif e==a.delete then +local e=self._find +if e.activeField=="find"then +e.findText="" +self:_updateFindMatches(true) +else +e.replaceText="" +end +return true +end +end +if self._ctrlDown then +if e==a.a then +self:_selectAll() +return true +elseif e==a.f then +self:_toggleFindOverlay("find") +return true +elseif e==a.h then +self:_toggleFindOverlay("replace") +return true +elseif e==a.g then +if self._shiftDown then +self:_gotoPreviousMatch() +else +self:_gotoNextMatch() +end +return true +elseif e==a.space then +self:_updateAutocomplete("manual") +return true +elseif e==a.r and self._shiftDown then +self:_replaceAll() +return true +elseif e==a.f and self._shiftDown then +local e=self._find +e.matchCase=not e.matchCase +self:_updateFindMatches(true) +return true +end +end +if self._autocompleteState.visible then +if e==a.enter or e==a.tab then +return self:_acceptAutocomplete() +elseif e==a.up then +self:_moveAutocompleteSelection(-1) +return true +elseif e==a.down then +self:_moveAutocompleteSelection(1) +return true +elseif e==a.escape then +self:_hideAutocomplete() +return true +end +end +if e==a.left then +self:_moveCursorLeft(self._shiftDown) +return true +elseif e==a.right then +self:_moveCursorRight(self._shiftDown) +return true +elseif e==a.up then +self:_moveCursorUp(self._shiftDown) +return true +elseif e==a.down then +self:_moveCursorDown(self._shiftDown) +return true +elseif e==a.home then +if self._ctrlDown then +self:_moveCursorDocumentStart(self._shiftDown) +else +self:_moveCursorLineStart(self._shiftDown) +end +return true +elseif e==a["end"]then +if self._ctrlDown then +self:_moveCursorDocumentEnd(self._shiftDown) +else +self:_moveCursorLineEnd(self._shiftDown) +end +return true +elseif e==a.backspace then +return self:_deleteBackward() +elseif e==a.delete then +return self:_deleteForward() +elseif e==a.enter then +return self:_insertNewline() +elseif e==a.tab then +return self:_insertTab() +elseif e==a.pageUp then +self:_scrollLines(-math.max(1,select(2,self:_getContentSize())-1)) +return true +elseif e==a.pageDown then +self:_scrollLines(math.max(1,select(2,self:_getContentSize())-1)) +return true +elseif e==a.escape then +return self:_handleEscape() +end +return false +end +function o:draw(a,q) +if not self.visible then +return +end +local b,y,m,w=self:getAbsoluteRect() +local s=self.bg or e.black +local f=self.fg or e.white +n(a,b,y,m,w,s,s) +z(a,b,y,m,w) +local o=self:_computeLayoutMetrics() +local d=o.innerX +local h=o.innerY +local t=o.innerWidth +local j=o.innerHeight +local i=o.contentWidth +local r=o.contentHeight +local c=o.overlayHeight +local u=o.scrollbarWidth +local v=o.scrollbarStyle +local k +local g=false +if self:_hasSelection()then +local t,o,a,e=self:_getSelectionRange() +k={ +startLine=t, +startCol=o, +endLine=a, +endCol=e +} +g=true +end +local t=self._autocompleteState +local l=s +for n=0,r-1 do +local o=self._scrollY+n+1 +local n=h+n +if o>#self._lines then +a.text(d,n,string.rep(" ",i),f,l) +else +local h,s,r=self:_buildLineSegments(o,i,f,l,k) +self:_drawSegments(a,d,n,h) +if self:isFocused()and o==self._cursorLine then +local e=self._cursorCol-self._scrollX-1 +if e>=0 and e=#t then +t="" +else +t=t:sub(e+1) +o=o+e +end +end +if t~=""then +if o<0 then +local e=-o +if e>=#t then +t="" +else +t=t:sub(e+1) +o=0 +end +end +if t~=""and o0 then +if#t>i then +t=t:sub(1,i) +end +if t~=""then +a.text(d+o,n,t,self.autocompleteGhostColor or e.lightGray,l) +end +end +end +end +end +end +end +end +if self.text==""and not self:isFocused()and self.placeholder~=""then +local t=self.placeholder +if#t>i then +t=t:sub(1,i) +end +local e=self.placeholderColor or e.lightGray +a.text(d,h,t..string.rep(" ",math.max(0,i-#t)),e,l) +end +self:_drawFindOverlay(a,d,h,i,j) +if v then +local t=o.scrollbarX +local e=v.background or s +n(a,t,h,u,r,e,e) +C(a,t,h,r,#self._lines,r,self._scrollY,v) +if c>0 then +n(a,t,h+r,u,c,e,e) +end +elseif u>0 then +n(a,o.scrollbarX,h,u,r+c,s,s) +end +if self.border then +p(q,b,y,m,w,self.border,s) +end +end +function o:handleEvent(t,...) +if not self.visible then +return false +end +if t=="mouse_click"then +local o,a,t=... +local i=self._autocompleteState +if i and i.visible and self:_isPointInAutocomplete(a,t)then +self.app:setFocus(self) +local e=self:_autocompleteIndexFromPoint(a,t) +if e then +if i.selectedIndex~=e then +i.selectedIndex=e +self:_refreshAutocompleteGhost() +end +if o==1 then +return self:_acceptAutocomplete() +elseif o==2 then +self:_hideAutocomplete() +return true +end +elseif o==2 then +self:_hideAutocomplete() +return true +end +return true +end +if self:containsPoint(a,t)then +self.app:setFocus(self) +local e=self:_computeLayoutMetrics() +if e.scrollbarStyle and e.scrollbarWidth>0 then +local o=e.scrollbarX +if a>=o and a=e.innerY and t0 then +local o=e.scrollbarX +if a>=o and a=e.innerY and t0 then +self:_moveAutocompleteSelection(1) +elseif e<0 then +self:_moveAutocompleteSelection(-1) +end +return true +end +if self:containsPoint(t,o)then +self:_scrollLines(e) +return true +end +elseif t=="char"then +local e=... +if self:isFocused()then +if self._find.visible then +self:_editFindFieldText(e) +return true +end +local e=self:_insertCharacter(e) +if e and self.autocompleteAuto then +self:_updateAutocomplete("auto") +end +return e +end +elseif t=="paste"then +local e=... +if self:isFocused()then +if self._find.visible then +self:_editFindFieldText(e) +return true +end +local e=self:_insertTextAtCursor(e) +if e and self.autocompleteAuto then +self:_updateAutocomplete("auto") +end +return e +end +elseif t=="key"then +local e,t=... +if e==a.leftShift or e==a.rightShift then +self._shiftDown=true +return true +elseif e==a.leftCtrl or e==a.rightCtrl then +self._ctrlDown=true +return true +end +if self:isFocused()then +return self:_handleKey(e,t) +end +elseif t=="key_up"then +local e=... +if e==a.leftShift or e==a.rightShift then +self._shiftDown=false +if not self:_hasSelection()then +self:_clearSelection() +end +return true +elseif e==a.leftCtrl or e==a.rightCtrl then +self._ctrlDown=false +return true +elseif e==a.escape then +if self:_handleEscape()then +return true +end +end +end +return false +end +function o:setText(e,a) +t(1,e,"string") +self:_setTextInternal(e,true,a) +end +function o:getText() +return self.text +end +function o:setOnChange(e) +if e~=nil then +t(1,e,"function") +end +self.onChange=e +end +function y.create(a) +if a~=nil then +t(1,a,"table") +end +a=a or{} +local s=false +local i +local o=a.window +if o==nil then +i=G.current() +local t,e=i.getSize() +o=ne.create(i,1,1,t,e,true) +o.setVisible(true) +s=true +end +local t=J.new(o) +t.profiler.start_frame() +t.profiler.start_region("user") +local d=t.add_pixel_layer(5,"pixelui_pixels") +local r=t.add_text_layer(10,"pixelui_ui") +local l,u=o.getSize() +local n=a.background or e.black +t.fill(n) +local c=math.max(.01,a.animationInterval or .05) +local t=setmetatable({ +window=o, +box=t, +layer=r, +pixelLayer=d, +background=n, +running=false, +_autoWindow=s, +_parentTerminal=i, +_focusWidget=nil, +_popupWidgets={}, +_popupLookup={}, +_animations={}, +_animationTimer=nil, +_animationInterval=c, +_radioGroups={}, +_threads={}, +_threadTimers={}, +_threadTicker=nil, +_threadIdCounter=0 +},h) +t.root=b:new(t,{ +x=1, +y=1, +width=l, +height=u, +bg=n, +fg=e.white, +border=a.rootBorder, +z=-math.huge +}) +return t +end +function h:getRoot() +return self.root +end +function h:setBackground(e) +t(1,e,"number") +self.background=e +self.box.fill(e) +end +function h:getLayer() +return self.layer +end +function h:getPixelLayer() +return self.pixelLayer +end +function h:createFrame(e) +return b:new(self,e) +end +function h:createWindow(e) +return i:new(self,e) +end +function h:createDialog(e) +return T:new(self,e) +end +function h:createMsgBox(e) +return A:new(self,e) +end +function h:createButton(e) +return S:new(self,e) +end +function h:createLabel(e) +return E:new(self,e) +end +function h:createCheckBox(e) +return _:new(self,e) +end +function h:createToggle(e) +return g:new(self,e) +end +function h:createTextBox(e) +return o:new(self,e) +end +function h:createComboBox(e) +return x:new(self,e) +end +function h:createTabControl(e) +return d:new(self,e) +end +function h:createContextMenu(e) +return m:new(self,e) +end +function h:createList(e) +return f:new(self,e) +end +function h:createTable(e) +return l:new(self,e) +end +function h:createTreeView(e) +return c:new(self,e) +end +function h:createChart(e) +return w:new(self,e) +end +function h:createRadioButton(e) +return q:new(self,e) +end +function h:createProgressBar(e) +return k:new(self,e) +end +function h:createNotificationToast(e) +return r:new(self,e) +end +function h:createLoadingRing(e) +return v:new(self,e) +end +function h:createFreeDraw(e) +return D:new(self,e) +end +function h:createSlider(e) +return u:new(self,e) +end +function h:_ensureAnimationTimer() +if not self._animationTimer then +self._animationTimer=I.startTimer(self._animationInterval) +end +end +function h:_updateAnimations() +local t=self._animations +if not t or#t==0 then +return +end +local o=I.clock() +local a=1 +while a<=#t do +local e=t[a] +if e._cancelled then +if e.onCancel then +e.onCancel(e.handle) +end +e._finished=true +table.remove(t,a) +else +if not e.startTime then +e.startTime=o +end +local i=o-e.startTime +local o +if e.duration<=0 then +o=1 +else +o=math.min(1,i/e.duration) +end +local i=e.easing(o) +if e.update then +e.update(i,o,e.handle) +end +if o>=1 then +e._finished=true +if e.onComplete then +e.onComplete(e.handle) +end +table.remove(t,a) +else +a=a+1 +end +end +end +end +function h:_clearAnimations(t) +local e=self._animations +if not e or#e==0 then +self._animations={} +self._animationTimer=nil +return +end +if t then +for t=1,#e do +local e=e[t] +if e and not e._finished then +if e.onCancel then +e.onCancel(e.handle) +end +e._finished=true +end +end +end +self._animations={} +self._animationTimer=nil +end +function h:animate(e) +t(1,e,"table") +local a=e.update +if a~=nil and type(a)~="function"then +error("options.update must be a function",2) +end +local i=e.onComplete +if i~=nil and type(i)~="function"then +error("options.onComplete must be a function",2) +end +local o=e.onCancel +if o~=nil and type(o)~="function"then +error("options.onCancel must be a function",2) +end +local t=e.easing +if t==nil then +t=H.linear +elseif type(t)=="string"then +t=H[t] +if not t then +error("Unknown easing '"..e.easing.."'",2) +end +elseif type(t)~="function"then +error("options.easing must be a function or easing name",2) +end +if e.duration~=nil and type(e.duration)~="number"then +error("options.duration must be a number",2) +end +local e=math.max(.01,e.duration or .3) +local e={ +update=a, +onComplete=i, +onCancel=o, +easing=t, +duration=e, +startTime=I.clock() +} +local t={} +function t:cancel() +if e._finished or e._cancelled then +return +end +e._cancelled=true +end +e.handle=t +self._animations[#self._animations+1]=e +if a then +a(0,0,t) +end +self:_ensureAnimationTimer() +return t +end +local a="running" +local p="completed" +local z="error" +local n="cancelled" +local R={} +local function O(e,a,...) +if not e then +return +end +for t=1,#e do +local e=e[t] +local t,e=pcall(e,...) +if not t then +print(a..tostring(e)) +end +end +end +function j:getId() +return self.id +end +function j:getName() +return self.name +end +function j:setName(e) +t(1,e,"string") +self.name=e +end +function j:getStatus() +return self.status +end +function j:isRunning() +return self.status==a +end +function j:isFinished() +local e=self.status +return e==p or e==z or e==n +end +function j:isCancelled() +return self._cancelRequested or self.status==n +end +function j:cancel() +if self.status~=a then +return false +end +self._cancelRequested=true +if self.waiting=="timer"and self.timerId then +local e=self.app._threadTimers +if e then +e[self.timerId]=nil +end +self.timerId=nil +end +self.waiting=nil +self._ready=true +self.app:_ensureThreadPump() +return true +end +function j:getResult() +if not self.result then +return nil +end +return K(self.result,1,self.result.n or#self.result) +end +function j:getResults() +if not self.result then +return nil +end +local e={n=self.result.n} +local t=self.result.n or#self.result +for t=1,t do +e[t]=self.result[t] +end +return e +end +function j:getError() +return self.error +end +function j:setMetadata(e,a) +t(1,e,"string") +local t=self.metadata[e] +if t==a then +return +end +self.metadata[e]=a +self:_emitMetadata(e,a) +end +function j:getMetadata(e) +t(1,e,"string") +return self.metadata[e] +end +function j:getAllMetadata() +local e={} +for a,t in pairs(self.metadata)do +e[a]=t +end +return e +end +function j:onStatusChange(e) +if e==nil then +return +end +t(1,e,"function") +local t=self._statusListeners +t[#t+1]=e +local e,t=pcall(e,self,self.status) +if not e then +print("Thread status listener error: "..tostring(t)) +end +end +function j:onMetadataChange(e) +if e==nil then +return +end +t(1,e,"function") +local t=self._metadataListeners +t[#t+1]=e +for a,t in pairs(self.metadata)do +local e,t=pcall(e,self,a,t) +if not e then +print("Thread metadata listener error: "..tostring(t)) +end +end +end +function j:_emitMetadata(e,t) +O(self._metadataListeners,"Thread metadata listener error: ",self,e,t) +end +function j:_setStatus(e) +if self.status==e then +return +end +self.status=e +O(self._statusListeners,"Thread status listener error: ",self,e) +end +local function O(e) +return setmetatable({_handle=e},N) +end +function N:checkCancelled() +if self._handle._cancelRequested then +error(R,0) +end +end +function N:isCancelled() +return self._handle._cancelRequested==true +end +function N:sleep(a) +if a~=nil then +t(1,a,"number") +else +a=0 +end +if a<0 then +a=0 +end +self:checkCancelled() +local e=self._handle +if e.timerId then +local t=e.app._threadTimers +if t then +t[e.timerId]=nil +end +e.timerId=nil +end +e.waiting="timer" +local a=I.startTimer(a) +e.timerId=a +local t=e.app._threadTimers +if not t then +t={} +e.app._threadTimers=t +end +t[a]=e +e._ready=false +return coroutine.yield("sleep") +end +function N:yield() +self:checkCancelled() +self._handle.waiting="yield" +return coroutine.yield("yield") +end +function N:setMetadata(t,e) +self._handle:setMetadata(t,e) +end +function N:setStatus(e) +self._handle:setMetadata("status",e) +end +function N:setDetail(e) +self._handle:setMetadata("detail",e) +end +function N:setProgress(e) +if e~=nil then +t(1,e,"number") +end +self._handle:setMetadata("progress",e) +end +function N:getHandle() +return self._handle +end +function h:_ensureThreadPump() +if not self._threads or self._threadTicker then +return +end +for e=1,#self._threads do +local e=self._threads[e] +if e and e.status==a and e._ready then +self._threadTicker=I.startTimer(0) +return +end +end +end +function h:_cleanupThread(e) +if e.timerId and self._threadTimers then +self._threadTimers[e.timerId]=nil +e.timerId=nil +end +e.waiting=nil +e._ready=false +e._resumeValue=nil +end +function h:_resumeThread(e) +if e.status~=a then +return +end +if e._cancelRequested then +e:_setStatus(n) +self:_cleanupThread(e) +return +end +local t=e._resumeValue +e._resumeValue=nil +local t=Q(coroutine.resume(e.co,t)) +local a=t[1] +if not a then +local t=t[2] +if t==R then +e:_setStatus(n) +else +if type(t)=="string"and debug and debug.traceback then +t=debug.traceback(e.co,t) +end +e.error=t +print("PixelUI thread error: "..tostring(t)) +e:_setStatus(z) +end +self:_cleanupThread(e) +return +end +if coroutine.status(e.co)=="dead"then +local a={n=t.n-1} +for o=2,t.n do +a[o-1]=t[o] +end +e.result=a +e:_setStatus(p) +self:_cleanupThread(e) +return +end +local t=t[2] +e.waiting=nil +if t=="sleep"then +return +elseif t=="yield"then +e._ready=true +else +e._ready=true +end +self:_ensureThreadPump() +end +function h:_serviceThreads() +if not self._threads or#self._threads==0 then +return +end +local t={} +for e=1,#self._threads do +local e=self._threads[e] +if e and e.status==a and e._ready then +e._ready=false +t[#t+1]=e +end +end +for e=1,#t do +self:_resumeThread(t[e]) +end +self:_ensureThreadPump() +end +function h:_shutdownThreads() +if not self._threads then +return +end +for e=1,#self._threads do +local e=self._threads[e] +if e and e.status==a then +e._cancelRequested=true +e:_setStatus(n) +self:_cleanupThread(e) +end +end +self._threadTimers={} +self._threadTicker=nil +end +function h:spawnThread(o,e) +t(1,o,"function") +if e~=nil then +t(2,e,"table") +else +e={} +end +if not self._threads then +self._threads={} +end +if not self._threadTimers then +self._threadTimers={} +end +self._threadIdCounter=(self._threadIdCounter or 0)+1 +local t=self._threadIdCounter +local i=e.name or("Thread "..tostring(t)) +local t=setmetatable({ +app=self, +id=t, +name=i, +status=a, +co=nil, +waiting=nil, +timerId=nil, +_ready=true, +_cancelRequested=false, +_resumeValue=nil, +metadata={}, +result=nil, +error=nil, +_statusListeners={}, +_metadataListeners={} +},j) +local a=coroutine.create(function() +local e=O(t) +t._context=e +local e=Q(o(e,self)) +return K(e,1,e.n) +end) +t.co=a +self._threads[#self._threads+1]=t +if e.onStatus then +t:onStatusChange(e.onStatus) +end +if e.onMetadata then +t:onMetadataChange(e.onMetadata) +end +self:_ensureThreadPump() +return t +end +function h:getThreads() +local e={} +if not self._threads then +return e +end +for t=1,#self._threads do +e[t]=self._threads[t] +end +return e +end +function h:_registerPopup(e) +if not e then +return +end +local t=self._popupLookup +if not t[e]then +t[e]=true +table.insert(self._popupWidgets,e) +end +end +function h:_unregisterPopup(e) +if not e then +return +end +local t=self._popupLookup +if not t[e]then +return +end +t[e]=nil +local t=self._popupWidgets +for a=#t,1,-1 do +if t[a]==e then +table.remove(t,a) +break +end +end +end +function h:_drawPopups() +local a=self._popupWidgets +if not a or#a==0 then +return +end +local o=self.layer +local i=self.pixelLayer +local t=1 +while t<=#a do +local e=a[t] +if e and e._open and e.visible~=false then +e:_drawDropdown(o,i) +t=t+1 +else +if e then +self._popupLookup[e]=nil +end +table.remove(a,t) +end +end +end +function h:_dispatchPopupEvent(o,...) +local t=self._popupWidgets +if not t or#t==0 then +return false +end +for a=#t,1,-1 do +local e=t[a] +if e and e._open and e.visible~=false then +if e:handleEvent(o,...)then +return true +end +else +if e then +self._popupLookup[e]=nil +end +table.remove(t,a) +end +end +return false +end +function h:_registerRadioButton(e) +if not e or not e.group then +return +end +local a=e.group +local o=self._radioGroups +local t=o[a] +if not t then +t={buttons={},lookup={},selected=nil} +o[a]=t +end +if not t.lookup[e]then +t.lookup[e]=true +t.buttons[#t.buttons+1]=e +end +e._registeredGroup=a +if t.selected then +if t.selected==e then +e:_applySelection(true,true) +else +e:_applySelection(false,true) +end +elseif e.selected then +self:_selectRadioInGroup(a,e,true) +end +end +function h:_unregisterRadioButton(t) +if not t then +return +end +local a=t._registeredGroup +if not a then +return +end +local e=self._radioGroups[a] +if not e then +t._registeredGroup=nil +return +end +e.lookup[t]=nil +for a=#e.buttons,1,-1 do +if e.buttons[a]==t then +table.remove(e.buttons,a) +break +end +end +if e.selected==t then +e.selected=nil +for t=1,#e.buttons do +local e=e.buttons[t] +if e then +e:_applySelection(false,true) +end +end +end +t._registeredGroup=nil +if not next(e.lookup)then +self._radioGroups[a]=nil +end +end +function h:_selectRadioInGroup(o,t,a) +if not o then +return +end +a=not not a +local i=self._radioGroups +local e=i[o] +if not e then +e={buttons={},lookup={},selected=nil} +i[o]=e +end +if t then +if not e.lookup[t]then +e.lookup[t]=true +e.buttons[#e.buttons+1]=t +end +t._registeredGroup=o +end +e.selected=t +for o=1,#e.buttons do +local e=e.buttons[o] +if e then +if e==t then +e:_applySelection(true,a) +else +e:_applySelection(false,a) +end +end +end +end +function h:setFocus(e) +if e~=nil then +t(1,e,"table") +if e.app~=self then +error("Cannot focus widget from a different PixelUI app",2) +end +if not e.focusable then +e=nil +end +end +if self._focusWidget==e then +return +end +if self._focusWidget then +local e=self._focusWidget +e:setFocused(false) +end +self._focusWidget=e +if e then +e:setFocused(true) +end +end +function h:getFocus() +return self._focusWidget +end +function h:render() +self.box.fill(self.background) +self.pixelLayer.clear() +self.layer.clear() +self.root:draw(self.layer,self.pixelLayer) +self:_drawPopups() +self.box.render() +end +function h:step(e,...) +if not e then +return +end +local t=false +if e=="timer"then +local o=... +if self._threadTicker and o==self._threadTicker then +self._threadTicker=nil +self:_serviceThreads() +t=true +end +local i=self._threadTimers +if i then +local e=i[o] +if e then +i[o]=nil +if e.status==a and e.timerId==o then +e.timerId=nil +e.waiting=nil +e._ready=true +e._resumeValue=true +end +t=true +end +end +if self._animationTimer and o==self._animationTimer then +self:_updateAnimations() +if self._animations and#self._animations>0 then +self._animationTimer=I.startTimer(self._animationInterval) +else +self._animationTimer=nil +end +t=true +end +end +if not t and e=="term_resize"then +if self._autoWindow then +local e=self._parentTerminal or G.current() +local e,t=e.getSize() +if self.window.reposition then +self.window.reposition(1,1,e,t) +end +end +local t,e=self.window.getSize() +self.root:setSize(t,e) +end +if not t and(e=="char"or e=="paste"or e=="key"or e=="key_up")then +local a=self._focusWidget +if a and a.visible~=false then +t=a:handleEvent(e,...) +end +end +if not t and(e=="mouse_click"or e=="mouse_up"or e=="mouse_drag"or e=="mouse_move"or e=="mouse_scroll"or e=="monitor_touch")then +t=self:_dispatchPopupEvent(e,...) +end +if not t then +t=self.root:handleEvent(e,...) +end +if not t and(e=="mouse_click"or e=="monitor_touch")then +self:setFocus(nil) +end +self:_serviceThreads() +self:render() +end +function h:run() +self.running=true +self:render() +while self.running do +local e={ie()} +if e[1]=="terminate"then +self.running=false +else +self:step(table.unpack(e)) +end +end +self:_shutdownThreads() +end +function h:stop() +self.running=false +self:_clearAnimations(true) +self:_shutdownThreads() +end +y.widgets={ +Frame=function(e,t) +return b:new(e,t) +end, +Window=function(t,e) +return i:new(t,e) +end, +Dialog=function(t,e) +return T:new(t,e) +end, +MsgBox=function(e,t) +return A:new(e,t) +end, +Button=function(t,e) +return S:new(t,e) +end, +Label=function(t,e) +return E:new(t,e) +end, +CheckBox=function(e,t) +return _:new(e,t) +end, +Toggle=function(t,e) +return g:new(t,e) +end, +TextBox=function(t,e) +return o:new(t,e) +end, +ComboBox=function(e,t) +return x:new(e,t) +end, +TabControl=function(t,e) +return d:new(t,e) +end, +ContextMenu=function(t,e) +return m:new(t,e) +end, +List=function(e,t) +return f:new(e,t) +end, +Table=function(t,e) +return l:new(t,e) +end, +TreeView=function(t,e) +return c:new(t,e) +end, +Chart=function(e,t) +return w:new(e,t) +end, +RadioButton=function(t,e) +return q:new(t,e) +end, +ProgressBar=function(t,e) +return k:new(t,e) +end, +Slider=function(e,t) +return u:new(e,t) +end, +LoadingRing=function(t,e) +return v:new(t,e) +end, +FreeDraw=function(t,e) +return D:new(t,e) +end, +NotificationToast=function(e,t) +return r:new(e,t) +end +} +y.Widget=s +y.Frame=b +y.Window=i +y.Dialog=T +y.MsgBox=A +y.Button=S +y.Label=E +y.CheckBox=_ +y.Toggle=g +y.TextBox=o +y.ComboBox=x +y.TabControl=d +y.ContextMenu=m +y.List=f +y.Table=l +y.TreeView=c +y.Chart=w +y.RadioButton=q +y.ProgressBar=k +y.Slider=u +y.LoadingRing=v +y.FreeDraw=D +y.NotificationToast=r +y.easings=H +y.ThreadHandle=j +y.ThreadContext=N +y.threadStatus={ +running=a, +completed=p, +error=z, +cancelled=n +} +return y diff --git a/global-libraries/shrekbox.lua b/global-libraries/shrekbox.lua new file mode 100644 index 0000000..7d3931e --- /dev/null +++ b/global-libraries/shrekbox.lua @@ -0,0 +1,1053 @@ +local shrekbox = {} + +---@class Layer +---@field _render fun(rblit:blitmap) +---@field pixel fun(lx:integer,ly:integer,color:ccTweaked.colors.color) +---@field text fun(lx:integer,ly:integer,text:string,fg:ccTweaked.colors.color?,bg:ccTweaked.colors.color?) +---@field set_pos fun(sx:integer,sy:integer) +---@field get_pos fun():integer,integer +---@field stl_coords fun(sx:number,sy:number):number,number +---@field lts_coords fun(sx:number,sy:number):number,number +---@field buffer ccTweaked.colors.color[][] +---@field size fun():number,number +---@field clear fun() +---@field scale_x number +---@field scale_y number +---@field z number +---@field label string? +---@field hidden boolean? + +---@alias blittable {[1]:string,[2]:string,[3]:string} +---@alias blitmap table> +---@alias blitbuffer {[1]:string[],[2]:blitchar[],[3]:blitchar[]}[] + +---@generic A +---@generic B +---@generic V +---@param f fun(a:A,b:B,v:V):boolean? +local function iter_2d(t, f) + for a, at in pairs(t) do + for b, v in pairs(at) do + if f(a, b, v) then return end + end + end +end + +local function assert_int(n) + if n ~= math.floor(n) then + error(("Number %f is not an integer!"):format(n), 2) + end +end + +---@generic A +---@generic B +---@generic V +---@param t table> +---@param a A +---@param b B +---@param v V +local function set_index_2d(t, a, b, v) + t[a] = t[a] or {} + t[a][b] = v +end +---@generic A +---@generic B +---@generic V +---@param t table> +---@param a A +---@param b B +---@return V? +local function get_index_2d(t, a, b) + if not t[a] then return end + return t[a][b] +end + +---@alias oblit_part {v:blittable,y:integer,x:integer} +---@alias oblit oblit_part[] +---@alias background_lookup ccTweaked.colors.color[][] + + +---@param win ccTweaked.Window +---@param oblit oblit +---@param bg blitchar +local function normalize_blit_strings(a, b, c, width, bg) + a = a or "" + b = b or "" + c = c or "" + local lenA = #a + local lenB = #b + local lenC = #c + local target = width + local function adjust(str, len, padChar) + if len > target then + return str:sub(1, target) + elseif len < target then + return str .. string.rep(padChar, target - len) + end + return str + end + -- Default to space when background char is unavailable. + local bgChar = bg or " " + a = adjust(a, lenA, " ") + b = adjust(b, lenB, bgChar) + c = adjust(c, lenC, bgChar) + return a, b, c +end + +local function render_blit(win, oblit, bg) + local w, h = win.getSize() + for i = 1, h do + local v = oblit[i] + win.setCursorPos(1, i) + if v then + local text = table.concat(v[1]) + local fg = table.concat(v[2]):gsub(shrekbox.transparent_char, bg) + local bgStr = table.concat(v[3]):gsub(shrekbox.transparent_char, bg) + text, fg, bgStr = normalize_blit_strings(text, fg, bgStr, w, bg) + win.blit(text, fg, bgStr) + v[1] = {} + else + local blank = string.rep(" ", w) + local bgRow = string.rep(bg, w) + win.blit(blank, bgRow, bgRow) + end + end +end + +local blit_lut = {} +for i = 0, 15 do + local n = 2 ^ i + local s = colors.toBlit(n) + blit_lut[n] = s + blit_lut[s] = n +end +shrekbox._blit_lut = blit_lut +-- Transparency +shrekbox.transparent = 0 +shrekbox.transparent_char = " " +blit_lut[shrekbox.transparent] = shrekbox.transparent_char +blit_lut[shrekbox.transparent_char] = shrekbox.transparent + +shrekbox.contrast = -1 +shrekbox.contrast_char = "_" +blit_lut[shrekbox.contrast] = shrekbox.contrast_char +blit_lut[shrekbox.contrast_char] = shrekbox.contrast + + +---@alias blitchar "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"|"a"|"b"|"c"|"d"|"e"|"f"|" "|"_" + +local closest_color_to_lookup = {} +---@type table +local palette_colors = {} +---@type table +local contrast_lookup = {} +---@param a number[] +---@param b number[] +---@return number +local function color_dist(a, b) + return (a[1] - b[1]) ^ 2 + (a[2] - b[2]) ^ 2 + (a[3] - b[3]) ^ 2 +end + +--- Array of colors from darkest (1) to brightest (16) +---@type blitchar[] +local color_bright_order = {} +--- Lookup from blitcolor to brightness index into color_bright_order +---@type table +local bright_pos_lookup = {} + +--- Array of colors from desaturated (1) to saturated (16) +---@type blitchar[] +local color_sat_order = {} +--- Lookup from blitcolor to saturation index into color_sat_order +---@type table +local sat_pos_lookup = {} + +---Shift a given blitchar color brighter/darker by a number of levels +---@param ch blitchar +---@param dir integer +function shrekbox.shift_brightness(ch, dir) + local index = bright_pos_lookup[ch] + index = math.max(1, math.min(index + dir, 16)) + return color_bright_order[index] +end + +---Shift a given blitchar color saturated/desaturated by a number of levels +---@param ch blitchar +---@param dir integer +function shrekbox.shift_saturation(ch, dir) + local index = sat_pos_lookup[ch] + index = math.max(1, math.min(index + dir, 16)) + return color_sat_order[index] +end + +---@return integer +---@return ... +function shrekbox.round(n, ...) + ---@diagnostic disable-next-line: missing-return-value + if not n then return end + return math.floor(n + 0.5), shrekbox.round(...) +end + +---@param c number[] +---@return number +local function brightness(c) + return c[1] ^ 2 + c[2] ^ 2 + c[3] ^ 2 +end + +local function rgb_2_hsl(c) + local r, g, b = c[1], c[2], c[3] + local cmax = math.max(r, g, b) + local cmin = math.min(r, g, b) + local delta = cmax - cmin + local hue, sat = 0, 0 + local lum = (cmax + cmin) / 2 + sat = lum > 0.5 and (delta / (2 - cmax - cmin)) or delta / (cmax + cmin) + if delta == 0 then + hue = 0 + sat = 0 + elseif delta == r then + hue = (g - b) / delta + (g < b and 6 or 0) + elseif delta == g then + hue = (b - r) / delta + 2 + elseif delta == b then + hue = (r - g) / delta + 4 + end + hue = hue / 6 + return { hue, sat, lum } +end +local expect = require("cc.expect").expect +---@param color blitchar +---@param a blitchar +---@param b blitchar +---@return blitchar +local function closer_color(color, a, b) + expect(1, color, "string") + expect(2, a, "string") + expect(3, b, "string") + a, b = a < b and a or b, b >= a and b or a + if color == a then return a end + if color == b then return b end + if a == b then return a end + if color == shrekbox.transparent_char then + return color + elseif a == shrekbox.transparent_char then + return b + elseif b == shrekbox.transparent_char then + return a + end + local lookup_index = a .. b + local lookup = get_index_2d(closest_color_to_lookup, lookup_index, color) + if lookup then return lookup end + local rgb = palette_colors[color] + if not rgb then error(("Invalid color %s"):format(color), 2) end + if not palette_colors[a] then error(("Invalid color a %s"):format(a), 2) end + if not palette_colors[b] then error(("Invalid color b %s"):format(b), 2) end + local a_dist = color_dist(rgb, palette_colors[a]) + local b_dist = color_dist(rgb, palette_colors[b]) + local closest = a_dist < b_dist and a or b + set_index_2d(closest_color_to_lookup, lookup_index, color, closest) + return closest +end +local function get_palette_colors() + local brighness_calc = {} + local saturation_calc = {} + for i = 0, 15 do + local color = 2 ^ i + local ch = blit_lut[color] + local rgb = { term.getPaletteColor(color) } + palette_colors[ch] = rgb + local bright = brightness(rgb) + brighness_calc[#brighness_calc + 1] = { bright, ch } + local hsl = rgb_2_hsl(rgb) + saturation_calc[#saturation_calc + 1] = { hsl[2], ch } + end + table.sort(brighness_calc, function(a, b) + return a[1] > b[1] + end) + table.sort(saturation_calc, function(a, b) + return a[1] > b[1] + end) + for i = 1, 16 do + color_bright_order[i] = brighness_calc[i][2] + bright_pos_lookup[brighness_calc[i][2]] = i + color_sat_order[i] = saturation_calc[i][2] + sat_pos_lookup[saturation_calc[i][2]] = i + end + local darkest, brightest = color_bright_order[1], color_bright_order[16] + closest_color_to_lookup = {} + for i = 0, 15 do + local color = 2 ^ i + local ch = blit_lut[color] + contrast_lookup[ch] = closer_color(ch, darkest, brightest) == darkest and brightest or darkest + end +end +get_palette_colors() + +---@type table +local textel_blit_lut = {} +local textel_majority_color_lut = {} +---@param textel_layout string[] +---@return blitchar +---@return blitchar +local function get_majority_color(textel_layout) + local color_frequency_map = {} + local max_count = 0 + local max_color = "0" + local max_2_count = 0 + local max_2_color = "0" + for i = 1, 6 do + local ch = textel_layout[i] + color_frequency_map[ch] = (color_frequency_map[ch] or 0) + 1 + if color_frequency_map[ch] > max_count then + if max_color ~= ch then + max_2_count = max_count + max_2_color = max_color + end + max_color = ch + max_count = color_frequency_map[ch] + elseif color_frequency_map[ch] > max_2_count then + max_2_count = color_frequency_map[ch] + max_2_color = ch + end + end + return max_color, max_2_color +end +local function clone_blit(t) + return { table.unpack(t, 1, 3) } +end +textel_blit_lut[shrekbox.transparent_char:rep(6)] = {} +---@param textel_layout string[] +---@return string[] +---@return blitchar common +local function get_textel_blit(textel_layout, box) + local textel_layout_s = table.concat(textel_layout, "") + if textel_blit_lut[textel_layout_s] then + return clone_blit(textel_blit_lut[textel_layout_s]), textel_majority_color_lut[textel_layout_s] + end + box.profiler.start_region("textel_blit") + local ch = textel_layout[6] + local textel = 0 + local max_color, max_2_color = get_majority_color(textel_layout) + local majority_color = max_color + ch = closer_color(ch, max_color, max_2_color) + if ch == max_color then + -- swap colors + max_color, max_2_color = max_2_color, max_color + end + local interp_colors = ch + for i = 5, 1, -1 do + ch = textel_layout[i] + ch = closer_color(ch, max_color, max_2_color) + interp_colors = interp_colors .. ch + textel = bit32.lshift(textel, 1) + (ch == max_color and 1 or 0) + end + ch = string.char(textel + 128) + local blit = { + ch, + max_color, + max_2_color + } + textel_blit_lut[textel_layout_s] = clone_blit(blit) + textel_majority_color_lut[textel_layout_s] = majority_color + box.profiler.end_region("textel_blit") + return blit, majority_color +end + +local transparency_lookup = { + [shrekbox.transparent_char] = true, + [shrekbox.contrast_char] = true +} +---@param fg blitchar +---@param bg blitchar +---@return boolean +local function is_blit_transparent(fg, bg) + return transparency_lookup[fg] or transparency_lookup[bg] +end +---@param box ShrekBox +---@param rblit blitbuffer +---@param x integer +---@param y integer +local function apply_transparent(ch, fg, bg, color, box, rblit, x, y) + box.profiler.start_region("trans") + bg = bg == shrekbox.transparent_char and color or bg + fg = (fg == shrekbox.transparent_char and color) or + (fg == shrekbox.contrast_char and contrast_lookup[bg]) or fg + rblit[y][1][x] = ch + rblit[y][2][x] = fg + rblit[y][3][x] = bg + box.profiler.end_region("trans") +end + +---@param box ShrekBox +---@param layer Layer +local function insert_default_layer_funcs(box, layer) + --- Get the size of this layer (in layer units) + ---@return number + ---@return number + function layer.size() + local sw, sh = box._get_window().getSize() + return sw * layer.scale_x, sh * layer.scale_y + end + + --- Screen To Layer Coordinates + ---@param sx number + ---@param sy number + ---@return number + ---@return number + function layer.stl_coords(sx, sy) + local lpx, lpy = layer.get_pos() + return (sx - lpx + 1) * layer.scale_x, (sy - lpy + 1) * layer.scale_y + end + + --- Layer to screen coordinates + ---@param lx number + ---@param ly number + ---@return number + ---@return number + function layer.lts_coords(lx, ly) + local lpx, lpy = layer.get_pos() + return lx / layer.scale_x - lpx + 1, ly / layer.scale_y - lpy + 1 + end +end + +local buffer_meta = { + __index = function(t, k) + t[k] = {} + return t[k] + end +} +---@return table> +local function generate_buffer() + return setmetatable({}, buffer_meta) +end + +local rblit_buffer_meta = { + __index = function(t, k) + t[k] = generate_buffer() + return t[k] + end +} +local function generate_rblit_buffer() + return setmetatable({}, rblit_buffer_meta) +end + +local function emtpy_textel() + return { ' ', ' ', ' ', ' ', ' ', ' ' } +end + +---@param buffer table> +local function pixel_layer_render(buffer, textel_buffer) + for ly, line in pairs(buffer) do + for lx, color in pairs(line) do + local iy = math.ceil(ly / 3) + local dy = (ly - 1) % 3 + local ix = math.ceil(lx / 2) + local dx = (lx - 1) % 2 + local i = (dy * 2 + dx) + 1 + local t = textel_buffer[iy][ix] or emtpy_textel() + t[i] = blit_lut[color] + textel_buffer[iy][ix] = t + -- if t[1] == shrekbox.transparent_char and t[1] == t[2] and t[2] == t[3] and t[4] == t[5] and t[5] == t[6] then + -- textel_buffer[iy][ix] = nil + -- end + end + end +end + +---@param box ShrekBox +---@param label string? +---@return Layer +local function new_pixel_layer(box, label) + local px, py = 1, 1 + local layer + local textel_buffer = generate_buffer() + layer = { + label = label, + scale_x = 2, + scale_y = 3, + z = 0, + clear = function() + textel_buffer = generate_buffer() + end, + buffer = generate_buffer(), + pixel = function(lx, ly, color) + box.profiler.start_region("pixel") + assert_int(lx) + assert_int(ly) + layer.buffer[ly][lx] = color + box.profiler.end_region("pixel") + end, + _render = function(rblit) + local tid = layer.label or ("pr_" .. layer.z) + box.profiler.start_region(tid) + pixel_layer_render(layer.buffer, textel_buffer) + box.profiler.start_region("gen_blit") + iter_2d(textel_buffer, function(y, x, v) + x = x + px - 1 + y = y + py - 1 + if not box.pos_on_screen(x, y) then return end + local ch, fg, bg = rblit[y][1][x], rblit[y][2][x], rblit[y][3][x] + if ch and is_blit_transparent(fg, bg) then + local c = get_majority_color(v) + apply_transparent(ch, fg, bg, c, box, rblit, x, y) + return + elseif ch then + return + end + local textel_blit, majority = get_textel_blit(v, box) + rblit[y][1][x] = textel_blit[1] + rblit[y][2][x] = textel_blit[2] + rblit[y][3][x] = textel_blit[3] + end) + box.profiler.end_region("gen_blit") + layer.buffer = generate_buffer() + box.profiler.end_region(tid) + end, + text = function(lx, ly, text, fg, bg) + assert_int(lx) + assert_int(ly) + error("NYI", 2) + end, + set_pos = function(sx, sy) + assert_int(sx) + assert_int(sy) + px, py = sx, sy + end, + get_pos = function() + return px, py + end + } + insert_default_layer_funcs(box, layer) + return layer +end + +local function empty_bixtel() + return { ' ', ' ', ' ' } +end +---@param buffer table> +local function bixel_layer_render(buffer, bixtel_buffer) + for ly, line in pairs(buffer) do + for lx, color in pairs(line) do + local iy = math.ceil(ly / 3) + local dy = (ly - 1) % 3 + local i = dy + 1 + local t = bixtel_buffer[iy][lx] or empty_bixtel() + t[i] = blit_lut[color] + bixtel_buffer[iy][lx] = t + -- if t[1] == shrekbox.transparent_char and t[1] == t[2] and t[2] == t[3] then + -- set_index_2d(bixtel_buffer, iy, lx, nil) + -- end + end + end +end +---@param box ShrekBox +---@param label string? +---@return Layer +local function new_bixel_layer(box, label) + local px, py = 1, 1 + local layer + local bixtel_buffer = generate_buffer() + layer = { + label = label, + scale_x = 1, + scale_y = 1.5, + z = 0, + clear = function() + bixtel_buffer = generate_buffer() + end, + buffer = generate_buffer(), + pixel = function(lx, ly, color) + box.profiler.start_region("bixel") + assert_int(lx) + assert_int(ly) + layer.buffer[ly][lx] = color + box.profiler.end_region("bixel") + end, + _render = function(rblit) + local tid = layer.label or ("br_" .. layer.z) + box.profiler.start_region(tid) + bixel_layer_render(layer.buffer, bixtel_buffer) + iter_2d(bixtel_buffer, function(y, x, v) + x = x + px - 1 + y = y + py - 1 + local upper_y = (y * 2) - 1 + local lower_y = y * 2 + if not box.pos_on_screen(x, y) then return end + local uch, ufg, ubg = rblit[upper_y][1][x], rblit[upper_y][2][x], rblit[upper_y][3][x] + local lch, lfg, lbg = rblit[lower_y][1][x], rblit[lower_y][2][x], rblit[lower_y][3][x] + local col_1 = v[1] + local col_2 = v[2] + local col_3 = v[3] + if uch and is_blit_transparent(ufg, ubg) then + apply_transparent(uch, ufg, ubg, col_1, box, rblit, x, upper_y) + elseif not uch then + rblit[upper_y][1][x] = "\143" + rblit[upper_y][2][x] = col_1 + rblit[upper_y][3][x] = col_2 + end + if lch and is_blit_transparent(lfg, lbg) then + apply_transparent(lch, lfg, lbg, col_3, box, rblit, x, lower_y) + elseif not lch then + rblit[lower_y][1][x] = "\131" + rblit[lower_y][2][x] = col_2 + rblit[lower_y][3][x] = col_3 + end + end) + layer.buffer = generate_buffer() + box.profiler.end_region(tid) + end, + text = function(lx, ly, text, fg, bg) + assert_int(lx) + assert_int(ly) + error("NYI", 2) + end, + set_pos = function(sx, sy) + assert_int(sx) + assert_int(sy) + px, py = sx, sy + end, + get_pos = function() + return px, py + end + } + insert_default_layer_funcs(box, layer) + return layer +end + +---@param box ShrekBox +---@return Layer +local function new_text_layer(box, label) + local text_buffer = {} + local px, py = 1, 1 + local layer + layer = { + label = label, + scale_x = 1, + scale_y = 1, + z = 0, + clear = function() + text_buffer = {} + layer.buffer = generate_buffer() + end, + buffer = generate_buffer(), + text = function(lx, ly, text, fg, bg) + assert_int(lx) + assert_int(ly) + local fgc = fg and blit_lut[fg] or shrekbox.contrast_char + local bgc = bg and blit_lut[bg] or shrekbox.transparent_char + for i = 1, #text do + local ch = text:sub(i, i) + set_index_2d(text_buffer, ly, lx + i - 1, { + ch, + fgc, + bgc + }) + end + end, + pixel = function(lx, ly, color) + assert_int(lx) + assert_int(ly) + if color == shrekbox.transparent then + set_index_2d(text_buffer, ly, lx, nil) + return + end + set_index_2d(text_buffer, ly, lx, { + " ", + blit_lut[color], + blit_lut[color] + }) + end, + _render = function(rblit) + local tid = layer.label or ("tr_" .. layer.z) + box.profiler.start_region(tid) + iter_2d(text_buffer, function(y, x, v) + x = x + px - 1 + y = y + py - 1 + if not box.pos_on_screen(x, y) then return end + local ofg, obg = v[2], v[3] + local ch, efg, ebg = rblit[y][1][x], rblit[y][2][x], rblit[y][3][x] + if ch and is_blit_transparent(efg, ebg) then + apply_transparent(ch, efg, ebg, obg, box, rblit, x, y) + return + elseif ch then + return + end + rblit[y][1][x] = v[1] + rblit[y][2][x] = ofg + rblit[y][3][x] = obg + end) + box.profiler.end_region(tid) + end, + set_pos = function(sx, sy) + assert_int(sx) + assert_int(sy) + px, py = sx, sy + end, + get_pos = function() + return px, py + end + } + insert_default_layer_funcs(box, layer) + return layer +end + + +---@diagnostic disable-next-line: undefined-global +local is_craftos_pc = not not periphemu +local epoch_unit = is_craftos_pc and "nano" or "utc" +local time_ms_divider = is_craftos_pc and 1000000 or 1 +local time_divider = is_craftos_pc and 1000000000 or 1000 +---@param root_name string +---@return Profiler +local function new_profiler(root_name) + ---@class Profile + ---@field total number + ---@field frame number + ---@field name string + ---@field t0 number? + ---@field children Profile[] + ---@field child_map table + ---@field parent Profile? + ---@field depth integer + ---@field count number + ---@field total_count number + ---@field average_time_ms number + ---@type Profile + local active_profile = nil + ---@class Profiler + local profiler = {} + ---@type table + local hidden_regions = {} + ---@type table + local collapsed_regions = {} + ---@type table + local yield_regions = {} + local total_frames = 1 + local active = false + + ---@param name string + local function add_profile(name) + ---@type Profile + local region_timer = { + total = 0, + frame = 0, + name = name, + parent = active_profile, + children = {}, + child_map = {}, + depth = 0, + count = 0, + total_count = 0, + average_time_ms = 1 + } + if active_profile then + region_timer.depth = active_profile.depth + 1 + active_profile.children[#active_profile.children + 1] = region_timer + active_profile.child_map[name] = region_timer + end + return region_timer + end + local root_profile = add_profile(root_name) + + ---Start a new region, creating it if it hasn't been made before. + ---Any code within this region will be timed. + ---Multiple calls in the same frame (and parent region) are timed together. + ---@param name string + function profiler.start_region(name) + if not active then return end + local child_region + if not active_profile and name == root_profile.name then + child_region = root_profile + else + child_region = active_profile.child_map[name] + end + if not child_region then + child_region = add_profile(name) + end + active_profile = child_region + ---@diagnostic disable-next-line: param-type-mismatch + active_profile.t0 = os.epoch(epoch_unit) + end + + ---Mark the end of a started region, will error if there is some other active region + ---@param name string + function profiler.end_region(name, _allow_empty) + if not active then return end + if not active_profile and _allow_empty then + return + end + assert(active_profile.name == name, ("Attempt to end region not active! %s"):format(name)) + local delta = os.epoch(epoch_unit) - active_profile.t0 + active_profile.frame = active_profile.frame + delta + active_profile.count = active_profile.count + 1 + active_profile = active_profile.parent + end + + ---Start a new yield region, creating it if it hasn't been made before. + ---Any code within this region will be timed and subtracted from the frametime. + ---Multiple calls in the same frame (and parent region) are timed together. + ---@param name string + function profiler.start_yield(name) + yield_regions[name] = true + profiler.start_region(name) + end + + ---Mark the end of a started yield region, will error if there is some other active region + ---@param name string + function profiler.end_yield(name) + profiler.end_region(name) + end + + function profiler.start_frame() + active = true + profiler.start_region(root_profile.name) + end + + ---@param profile Profile + local function profiler_end_frame(profile) + profile.total = profile.total + profile.frame + profile.total_count = profile.total_count + profile.count + if yield_regions[profile.name] then + local parent = assert(profile.parent) + repeat + parent.total = parent.total - profile.frame + parent = parent.parent + until not parent + end + profile.count = 0 + profile.frame = 0 + for i, v in ipairs(profile.children) do + profiler_end_frame(v) + end + end + function profiler.end_frame() + total_frames = total_frames + 1 + profiler.end_region(root_profile.name, true) + profiler_end_frame(root_profile) + end + + ---@param layer Layer + ---@param profile Profile + local function render_profile(layer, profile, y) + if hidden_regions[profile.name] then + return y + end + local s = ("|"):rep(profile.depth) + if collapsed_regions[profile.name] then + s = s .. "+" + else + s = s .. "\\" + end + local average_time_ms = profile.total / total_frames / time_ms_divider + profile.average_time_ms = average_time_ms + local average_count = profile.total_count / total_frames + local parent_time_ms = profile.parent and profile.parent.average_time_ms or average_time_ms + local percent = average_time_ms / parent_time_ms * 100 + if percent ~= percent then percent = 100 end + percent = shrekbox.round(percent) + if yield_regions[profile.name] then + s = s .. "[YLD] " + percent = 0 + else + s = s .. ("[%3d] "):format(percent) + end + s = s .. ("%s) avg:%.2fms, avg#:%.1f, #:%d"):format(profile.name, + average_time_ms, average_count, profile.count) + layer.text(1, y, s, colors.white, colors.black) + y = y + 1 + if not collapsed_regions[profile.name] then + for i, v in ipairs(profile.children) do + y = render_profile(layer, v, y) + end + end + return y + end + + ---@param layer Layer + ---@param y integer + function profiler.render(layer, y) + render_profile(layer, root_profile, y) + end + + ---Hide a profile and it's children by name + ---@param name string + ---@param hide boolean? true default + function profiler.hide(name, hide) + if hide == nil then hide = true end + hidden_regions[name] = hide + end + + ---Collapse (hide) a profile's children by name + ---@param name string + ---@param collapse boolean? true default + function profiler.collapse(name, collapse) + if collapse == nil then collapse = true end + collapsed_regions[name] = collapse + end + + ---@return Profile + function profiler._get_root() + return root_profile + end + + return profiler +end + +---@param win ccTweaked.Window +function shrekbox.new(win) + ---@class ShrekBox + local box = { + overlay = false + } + + local overlay_layer = new_text_layer(box, "overlay") + overlay_layer.z = math.huge + local background_layer = new_text_layer(box, "bg_fill") + overlay_layer.z = -math.huge + local layers = {} + local profiler = new_profiler("total") + -- By default collapse the internal shrekbox timings + -- You can uncollapse this by doing box.profiler.collapse("shrekbox", false) + profiler.collapse("shrekbox") + box.profiler = profiler + + function box.sort_layers() + table.sort(layers, function(a, b) + return a.z > b.z -- sort this the opposite way, render front to back + end) + end + + function box._get_window() + return win + end + + ---@param z number + ---@param label string? + function box.add_pixel_layer(z, label) + local layer = new_pixel_layer(box, label) + layers[#layers + 1] = layer + layer.z = z + box.sort_layers() + return layer + end + + ---@param z number + ---@param label string? + function box.add_bixel_layer(z, label) + local layer = new_bixel_layer(box, label) + layers[#layers + 1] = layer + layer.z = z + box.sort_layers() + return layer + end + + ---@param z number + ---@param label string? + function box.add_text_layer(z, label) + local layer = new_text_layer(box, label) + layers[#layers + 1] = layer + layer.z = z + box.sort_layers() + return layer + end + + ---Fill the background layer with a color + ---@param color ccTweaked.colors.color + function box.fill(color) + local ww, wh = win.getSize() + local s = (" "):rep(ww) + for y = 1, wh do + background_layer.text(1, y, s, color, color) + end + end + + local total_time = 1 + local last_frametime = 0 + local total_frames = 0 + local function render_debug() + local real_fps = total_frames / (total_time / time_divider) + local root = profiler._get_root() + local theoretical_fps = root.total_count / (root.total / time_divider) + if theoretical_fps ~= theoretical_fps then theoretical_fps = 0 end + overlay_layer.text(1, 1, + ("%3dfps (t:%3dfps)"):format(real_fps, theoretical_fps), + colors.white, colors.black) + profiler.render(overlay_layer, 2) + end + + ---@type blitbuffer + local rblit = generate_rblit_buffer() + local t0 = os.epoch(epoch_unit) + local function _render() + profiler.end_region("user", true) + win.setVisible(false) + win.setCursorPos(1, 1) + ---@diagnostic disable-next-line: param-type-mismatch + local t1 = os.epoch(epoch_unit) + last_frametime = (t1 - t0) + total_time = total_time + last_frametime + total_frames = total_frames + 1 + ---@diagnostic disable-next-line: param-type-mismatch + t0 = os.epoch(epoch_unit) + overlay_layer.clear() + if box.overlay then + render_debug() + overlay_layer._render(rblit) + end + profiler.end_frame() + profiler.start_frame() + profiler.start_region("shrekbox") + for i, v in ipairs(layers) do -- rendering from front to back + if not v.hidden then + v._render(rblit) + end + end + background_layer._render(rblit) + profiler.start_region("blit!") + render_blit(win, rblit, "a") + profiler.end_region("blit!") + profiler.end_region("shrekbox") + win.setVisible(true) + profiler.start_region("user") + end + + function box.render() + local ok, err = pcall(_render) + if not ok then + term.clear() + term.setCursorPos(1, 1) + print("An error occured while rendering!") + error(err, 0) + end + end + + ---Check if a position is on the visible window + ---@param sx number + ---@param sy number + ---@return boolean + function box.pos_on_screen(sx, sy) + local sw, sh = win.getSize() + if sx < 1 or sy < 1 then return false end + if sx > sw or sy > sh then return false end + return true + end + + box.fill(colors.black) + + return box +end + +function shrekbox.load_file(fn) + local f = assert(fs.open(fn, "r")) + local s = f.readAll() --[[@as string]] + f.close() + return s +end + +function shrekbox.save_file(fn, s) + local f = assert(fs.open(fn, "w")) + f.write(s) + f.close() +end + +return shrekbox diff --git a/libs/containers.lua b/libs/containers.lua index a7968d0..7af4dd7 100644 --- a/libs/containers.lua +++ b/libs/containers.lua @@ -1323,7 +1323,18 @@ function lib.getENV(fspath, term_override, perms) global.network = network end if app and perms.app then - global.app = app + global.app = deepcopy(app) + global.app.launch = function(id,app_perms) app.launch(id,app_perms,perms) end + end + if global and not perms.peripheral then + global.peripheral.wrap = function () end + global.peripheral.find = function () end + global.peripheral.getNames = function () return {} end + global.peripheral.getType = function () end + global.peripheral.isPresent = function () return false end + global.peripheral.hasType = function () end + global.peripheral.call = function () end + global.peripheral.getMethods = function () end end global.settings = settings global._ENV = global diff --git a/libs/windows.lua b/libs/windows.lua index 9441ea9..564fbf3 100644 --- a/libs/windows.lua +++ b/libs/windows.lua @@ -7,6 +7,60 @@ local function add(t, v) end end end +local function normalize_color(c) + if type(c) ~= "number" then error("bad color", 2) end + if c < 1 then error("bad color", 2) end + + -- If c is already a power of two, this keeps it. + -- If it's a mask, this picks the highest power-of-two <= c. + local p = 1 + while p * 2 <= c do p = p * 2 end + return p +end + +-- Returns: bestColor, bestCount, totalCells, countsTable +-- countsTable maps colorBit -> count +local function most_common_bg(win, opts) + opts = opts or {} + + local w, h + if win.getSize then + w, h = win.getSize() + else + w, h = win.w, win.h + end + + local ignore = opts.ignore + if ignore ~= nil then ignore = normalize_color(ignore) end + + local counts = {} + local total = 0 + + for x = 1, w do + local col = win.buffer and win.buffer[x] + for y = 1, h do + local cell = col and col[y] + local bc = cell and cell.bc or win.bgColor or colors.black + bc = normalize_color(bc) + + if not ignore or bc ~= ignore then + counts[bc] = (counts[bc] or 0) + 1 + total = total + 1 + end + end + end + + local fallback = normalize_color(opts.fallback or win.bgColor or colors.black) + local best, bestN = fallback, -1 + + for c, n in pairs(counts) do + if n > bestN or (n == bestN and c == fallback) then + best, bestN = c, n + end + end + + return best, bestN, total, counts +end -- hex <-> color bit lookups (Lua 5.1 safe) local HEX = "0123456789abcdef" @@ -64,6 +118,11 @@ function lib.create(name, w, h, x, y) closing = false, _palette = {}, -- optional local palette store } + + function t.getMostCommonBackgroundColor(opts) + local c, n, total, counts = most_common_bg(t, opts) + return c, color_to_hex[c], n, total, counts + end local function init_col(xi) local w = t.w diff --git a/startup/99_phoneOS.lua b/startup/99_phoneOS.lua index 8615376..3f48f83 100644 --- a/startup/99_phoneOS.lua +++ b/startup/99_phoneOS.lua @@ -14,7 +14,6 @@ function _G.app.getApps() local out = {} for _,f in ipairs(fs.list("/apps-meta")) do if fs.exists(fs.combine("/apps-meta",f)) then - print(f) local file = fs.open(fs.combine("/apps-meta",f),"r") if file then out[#out+1] = textutils.unserialise(file.readAll()) @@ -24,13 +23,54 @@ function _G.app.getApps() end return out end -function _G.app.launch(id,perms) +function _G.app.closeApp(pid) + if pid == focusedapp then + app.focusapp(-1) + apps[pid] = nil + else + apps[pid] = nil + end +end +function _G.app.getRunningApps() + local pids = {} + for k,_ in pairs(apps) do + pids[#pids+1] = k + end + return pids +end +function _G.app.getTitle(pid) + if not apps[pid] then return false, "Process not found" end + return apps[pid].title +end +function _G.app.getDetail(pid) + if not apps[pid] then return false, "Process not found" end + local registered_apps = app.getApps() + local appid = apps[pid].id + local appdetails = nil + local msg = "Details not found (likely doesn't have a metadata file)" + for _,registered_app in ipairs(registered_apps) do + if registered_app.appid == appid then + appdetails = registered_app + msg = nil + break + end + end + return appdetails, msg +end +function _G.app.launch(id,perms,parentperms) + if parentperms then + for k,v in pairs(perms) do + if v and not parentperms[k] then + return false, "Unable to grant permission: "..k + end + end + end local w,h = term.getSize() if not fs.exists(fs.combine("/apps",id)) then return false,"App Not Found" end local win = windows.create(id,w,h-2,1,2) local env = containers.getENV(fs.combine("/apps",id), win, perms) local appobj = {win=win,co=nil,id=id,title=id} - function env.setAppTitle(str) appobj.title=str end + function env.setAppTitle(str) appobj.title=tostring(str) end appobj.co = coroutine.create(function () containers.start(env) end) local pid = -1 for k=1,#apps+1 do @@ -75,7 +115,7 @@ local function render() term.setCursorPos(1,1) term.setBackgroundColor(colors.black) if focusedapp then - term.setBackgroundColor(focusedapp.win.getBackgroundColour()) + term.setBackgroundColor(focusedapp.win.getMostCommonBackgroundColor()) end term.clearLine() term.write(os.date("%I:%M %p")) @@ -87,6 +127,7 @@ local function render() term.setCursorPos(w-right:len()+1,1) term.write(right) term.setCursorPos((w/2)-(home_button:len()/2)+1,h) + term.clearLine() term.write(home_button) term.setCursorPos(1,h) if focusedapp then @@ -119,16 +160,7 @@ local function process() event[4] = event[4]-1 end if focusedapp then - if focusedapp.event then - local success, content = coroutine.resume(focusedapp.co,table.unpack(focusedapp.event)) - if success then - -- print(focusedapp.title,focusedapp.filter) - focusedapp.event = nil - focusedapp.filter = content - else - app.focusapp(-1) - end - elseif focusedapp.filter == nil or focusedapp.filter == event[1] then + if focusedapp.filter == nil or focusedapp.filter == event[1] then local success, content = coroutine.resume(focusedapp.co,table.unpack(event)) if success then focusedapp.filter = content @@ -138,13 +170,16 @@ local function process() end end for k,v in pairs(apps) do - if v.filter == nil or v.filter == event[1] and v.event == nil and v ~= focusedapp then - v.event = event + if (v.filter == nil or v.filter == event[1]) and v ~= focusedapp and event[1] ~= "mouse_click" and event[1] ~= "mouse_up" and event[1] ~= "mouse_drag" and event[1] ~= "mouse_scroll" then + local success, content = coroutine.resume(v.co,table.unpack(event)) + if success then + v.filter = content + end end end end end print("launching launcher app..") -launcherapp = app.launch(launcherappid,{http=true,app=true,repo=true,network=true}) +launcherapp = app.launch(launcherappid,{http=true,app=true,repo=true,network=true,peripheral=true}) print("launched launcher app") parallel.waitForAny(process,render,network.run) \ No newline at end of file diff --git a/system-apps/dookie-clicker/startup.lua b/system-apps/dookie-clicker/startup.lua index 5f11095..ca60ecc 100644 --- a/system-apps/dookie-clicker/startup.lua +++ b/system-apps/dookie-clicker/startup.lua @@ -3,7 +3,7 @@ local basalt = require("globals.basalt") -- Get the main frame (your window) local count = 0 local main = basalt.getMainFrame() -main:setBackground(colors.black) +main:setBackground(colors.gray) local w,h = term.getSize() main:addLabel({ x = w/2-(7/2)+1,