Gspot.lua 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309
  1. -- Original author: https://github.com/trubblegum
  2. -- This is a modified version of https://github.com/trubblegum/Gspot/blob/cf0a49d7d2073686d7ddb32a4fa04e90593d36c4/Gspot.lua
  3. -- The original program did not include a copyright notice.
  4. -- Modifications © Copyright 2015-2016, 2018 Pedro Gimeno Fortea.
  5. --
  6. -- This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
  7. -- Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
  8. -- 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
  9. -- 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
  10. -- 3. This notice may not be removed or altered from any source distribution.
  11. -- Simplify version checking
  12. local version = love._version_major * 10000 + love._version_minor * 100 + love._version_revision
  13. -- Return the position of the first byte of the given UTF-8 char.
  14. local function utf8char_begin(s, idx)
  15. -- Precondition:
  16. --assert(idx >= 1 and idx <= #s + 1)
  17. local byte = s:byte(idx)
  18. while byte and byte >= 0x80 and byte < 0xC0 do
  19. idx = idx - 1
  20. byte = s:byte(idx)
  21. end
  22. -- Postcondition:
  23. --assert(idx >= 1 and idx <= #s + 1)
  24. return idx
  25. end
  26. -- Return the position immediately after the current UTF-8 char.
  27. local function utf8char_after(s, idx)
  28. -- Precondition:
  29. --assert(idx >= 1 and idx <= #s + 1)
  30. if idx <= #s then
  31. idx = idx + 1
  32. local byte = s:byte(idx)
  33. while byte and byte >= 0x80 and byte < 0xC0 do
  34. idx = idx + 1
  35. byte = s:byte(idx)
  36. end
  37. end
  38. -- Postcondition:
  39. --assert(idx >= 1 and idx <= #s + 1)
  40. return idx
  41. end
  42. -- Return the number of UTF-8 characters on the string
  43. local function utf8len(s)
  44. local p = 0
  45. for i=1,string.len(s) do
  46. p = (s:byte(i) >= 0x80 and s:byte(i) < 0xC0) and p or p+1
  47. end
  48. return p
  49. end
  50. -- Apply a scissor to the current scissor (intersect the rects)
  51. local clipScissor
  52. if version < 001000 then
  53. function clipScissor(nx, ny, nw, nh)
  54. local ox, oy, ow, oh = love.graphics.getScissor()
  55. if ox then
  56. -- Intersect both rects
  57. nw = nx + nw
  58. nh = ny + nh
  59. nx, ny = math.max(nx, ox), math.max(ny, oy)
  60. nw = math.max(0, math.min(nw, ox + ow) - nx)
  61. nh = math.max(0, math.min(nh, oy + oh) - ny)
  62. end
  63. -- Set new scissor
  64. love.graphics.setScissor(nx, ny, nw, nh)
  65. -- Return old scissor
  66. return ox, oy, ow, oh
  67. end
  68. else
  69. function clipScissor(nx, ny, nw, nh)
  70. local ox, oy, ow, oh = love.graphics.getScissor()
  71. love.graphics.intersectScissor(nx, ny, nw, nh)
  72. -- Return old scissor
  73. return ox, oy, ow, oh
  74. end
  75. end
  76. local DIV = version >= 110000 and 255 or 1
  77. -- 11.0 compatibility
  78. local compat_setColor, setColor, fileExists
  79. if version < 110000 then
  80. setColor = love.graphics.setColor
  81. fileExists = love.filesystem.exists
  82. else
  83. compat_setColor = function(...)
  84. local n = select('#', ...)
  85. if n >= 3 then
  86. local r, g, b, a = ...
  87. if n >= 4 then
  88. love.graphics.setColor(r / DIV, g / DIV, b / DIV, a / DIV)
  89. else
  90. love.graphics.setColor(r / DIV, g / DIV, b / DIV)
  91. end
  92. return
  93. end
  94. assert(n == 1, "Invalid number of parameters in setColor()")
  95. local t = ...
  96. assert(type(t) == "table", "setColor() only accepts a table parameter")
  97. if #t >= 4 then
  98. love.graphics.setColor(t[1] / DIV, t[2] / DIV, t[3] / DIV, t[4] / DIV)
  99. else
  100. love.graphics.setColor(t[1] / DIV, t[2] / DIV, t[3] / DIV)
  101. end
  102. end
  103. setColor = compat_setColor
  104. local info = {type = false, size = false, modtime = false}
  105. fileExists = function(path)
  106. return love.filesystem.getInfo(path, info) ~= nil
  107. and (info.type == "file" or info.type == "symlink")
  108. end
  109. end
  110. -- Deal with love.mouse API changes in 0.10
  111. local mouseL = version >= 001000 and 1 or 'l'
  112. local mouseR = version >= 001000 and 2 or 'r'
  113. -- 0.10.0 blurs text if rendered on fractional coordinates
  114. -- 0.9 redefines love.graphics.print[f] after first call.
  115. -- To prevent caching the wrong version, we call it now with no text.
  116. love.graphics.print("")
  117. love.graphics.printf("", 0, 0, 0)
  118. local lgprint = version < 001000 and love.graphics.print or function(text, x, y, ...) return love.graphics.print(text, math.floor(x+0.5), math.floor(y+0.5), ...) end
  119. local lgprintf = version < 001000 and love.graphics.printf or function(text, x, y, ...) return love.graphics.printf(text, math.floor(x+0.5), math.floor(y+0.5), ...) end
  120. local Gspot = {}
  121. -- Default unit
  122. local DEFAULT_UNIT = 16
  123. local DEFAULT_BORDER_OFFSET = 0.5
  124. local DEFAULT_BORDER_STYLE = "smooth" -- 'smooth' or 'rough'
  125. local DEFAULT_BORDER_WIDTH = DEFAULT_UNIT/4
  126. Gspot.style = { -- see Gspot.setComponentMax for colour values
  127. unit = DEFAULT_UNIT,
  128. rectradius = DEFAULT_UNIT / 4,
  129. rectsegments = DEFAULT_UNIT*10,
  130. border = {
  131. color = {},
  132. style = DEFAULT_BORDER_STYLE,
  133. width = DEFAULT_BORDER_WIDTH,
  134. left = DEFAULT_BORDER_OFFSET,
  135. right = DEFAULT_BORDER_OFFSET,
  136. top = DEFAULT_BORDER_OFFSET,
  137. bottom = DEFAULT_BORDER_OFFSET
  138. },
  139. font = love.graphics.newFont(10),
  140. fg = {},
  141. bg = {},
  142. labelfg = nil, -- defaults to fg when absent
  143. default = {},
  144. hilite = {},
  145. focus = {},
  146. }
  147. Gspot.setComponentMax = function(this, value)
  148. -- 'this' isn't used at all here - it modifies the base class instead.
  149. assert(tostring(value) == "255" or value == "native", "Gspot:setColorRange must be 255 or \"native\"")
  150. if (Gspot.nativeColorMax or false) ~= (value == "native") then
  151. local st = Gspot.style
  152. st.fg[1],st.fg[2],st.fg[3],st.fg[4] = 255,255,255,255
  153. st.bg[1],st.bg[2],st.bg[3],st.bg[4] = 64, 64, 64,255
  154. st.default[1],st.default[2],st.default[3],st.default[4] = 96,96,96,255
  155. st.hilite[1],st.hilite[2],st.hilite[3],st.hilite[4] = 128,128,128,255
  156. st.focus[1],st.focus[2],st.focus[3],st.focus[4] = 160,160,160,255
  157. st.border.color[1],st.border.color[2],st.border.color[3],st.border.color[4] = 0,0,0,0
  158. if value == "native" then
  159. Gspot.nativeColorMax = true
  160. for i = 1, 4 do
  161. st.fg[i] = st.fg[i] / DIV
  162. st.bg[i] = st.bg[i] / DIV
  163. st.default[i] = st.default[i] / DIV
  164. st.hilite[i] = st.hilite[i] / DIV
  165. st.focus[i] = st.focus[i] / DIV
  166. end
  167. setColor = love.graphics.setColor
  168. else
  169. Gspot.nativeColorMax = nil
  170. setColor = compat_setColor
  171. end
  172. end
  173. return Gspot:load()
  174. end
  175. Gspot.load = function(this)
  176. local def = {
  177. style = {
  178. unit = this.style.unit,
  179. rectradius = this.style.rectradius,
  180. rectsegments = this.style.rectsegments,
  181. border = {
  182. color = {
  183. this.style.border.color[1],
  184. this.style.border.color[2],
  185. this.style.border.color[3],
  186. this.style.border.color[4]
  187. },
  188. style = this.style.border.style,
  189. width = this.style.border.width,
  190. left = this.style.border.left,
  191. right = this.style.border.right,
  192. top = this.style.border.top,
  193. bottom = this.style.border.bottom
  194. },
  195. font = this.style.font,
  196. fg = this.style.fg,
  197. bg = this.style.bg,
  198. labelfg = this.style.labelfg,
  199. default = this.style.default,
  200. hilite = this.style.hilite,
  201. focus = this.style.focus,
  202. hs = this.style.hs or this.style.unit,
  203. },
  204. dblclickinterval = 0.25,
  205. -- no messin' past here
  206. maxid = 0, -- legacy
  207. mem = {},
  208. elements = {},
  209. mousein = nil,
  210. focus = nil,
  211. drag = nil,
  212. orepeat = nil,
  213. }
  214. def.mousedt = def.dblclickinterval -- Double click timer (make it expired)
  215. return setmetatable(def, {__index = this, __call = this.load})
  216. end
  217. Gspot.update = function(this, dt)
  218. this.mousedt = this.mousedt + dt
  219. local mouse = {}
  220. mouse.x, mouse.y = this:getmouse()
  221. local mousein = this.mousein
  222. this.mousein = false
  223. this.mouseover = false
  224. if this.drag then
  225. local element = this.drag
  226. if love.mouse.isDown(mouseL) then
  227. if type(element.drag) == 'function' then element:drag(mouse.x, mouse.y)
  228. else
  229. element.pos.y = mouse.y - element.offset.y
  230. element.pos.x = mouse.x - element.offset.x
  231. end
  232. elseif love.mouse.isDown(mouseR) then
  233. if type(element.rdrag) == 'function' then element:rdrag(mouse.x, mouse.y)
  234. else
  235. element.pos.y = mouse.y - element.offset.y
  236. element.pos.x = mouse.x - element.offset.x
  237. end
  238. end
  239. for i, bucket in ipairs(this.elements) do
  240. if bucket ~= element and bucket:containspoint(mouse) then this.mouseover = bucket end
  241. end
  242. end
  243. for i = #this.elements, 1, -1 do
  244. local element = this.elements[i]
  245. if element.display then
  246. if element:containspoint(mouse) then
  247. if element.parent and element.parent:type() == 'Gspot.element.scrollgroup' and element ~= element.parent.scrollv and element ~= element.parent.scrollh then
  248. if element.parent:containspoint(mouse) then this.mousein = element break end
  249. else this.mousein = element break end
  250. end
  251. end
  252. end
  253. for i = #this.elements, 1, -1 do
  254. local element = this.elements[i]
  255. if element.display then
  256. if element.update then
  257. if element.updateinterval then
  258. element.dt = element.dt + dt
  259. if element.dt >= element.updateinterval then
  260. element.dt = 0
  261. element:update(dt)
  262. end
  263. else element:update(dt) end
  264. end
  265. end
  266. end
  267. if this.mousein ~= mousein then
  268. if this.mousein and this.mousein.enter then this.mousein:enter() end
  269. if mousein and mousein.leave then mousein:leave() end
  270. end
  271. end
  272. Gspot.draw = function(this)
  273. local ostyle_font = love.graphics.getFont()
  274. local ostyle_r, ostyle_g, ostyle_b, ostyle_a = love.graphics.getColor()
  275. local ostyle_scissor_x, ostyle_scissor_y, ostyle_scissor_w, ostyle_scissor_h = love.graphics.getScissor()
  276. for i, element in ipairs(this.elements) do
  277. if element.display then
  278. local pos, scissor = element:getpos()
  279. if scissor then clipScissor(scissor.x, scissor.y, scissor.w, scissor.h) end
  280. love.graphics.setFont(element.style.font)
  281. element:draw(pos)
  282. love.graphics.setScissor(ostyle_scissor_x, ostyle_scissor_y, ostyle_scissor_w, ostyle_scissor_h)
  283. end
  284. end
  285. if this.mousein and this.mousein.tip then
  286. local element = this.mousein
  287. local pos = element:getpos()
  288. local _, countlf = element.tip:gsub("\n", "\n")
  289. local tippos = {x = pos.x + (this.style.unit / 2), y = pos.y + (this.style.unit / 2), w = element.style.font:getWidth(element.tip) + this.style.unit, h = this.style.unit / 4 + element.style.font:getHeight() * (countlf + 1)}
  290. love.graphics.setFont(this.style.font) -- use the default font
  291. setColor(this.style.bg)
  292. this.mousein:rect({x = math.max(0, math.min(tippos.x, love.graphics.getWidth() - (element.style.font:getWidth(element.tip) + this.style.unit))), y = math.max(0, math.min(tippos.y, love.graphics.getHeight() - this.style.unit)), w = tippos.w, h = tippos.h})
  293. setColor(this.style.fg)
  294. lgprint(element.tip, math.max(this.style.unit / 2, math.min(tippos.x + (this.style.unit / 2), love.graphics.getWidth() - (element.style.font:getWidth(element.tip) + (this.style.unit / 2)))), math.max((this.style.unit - element.style.font:getHeight()) / 2, math.min(tippos.y + ((this.style.unit - element.style.font:getHeight()) / 2), (love.graphics.getHeight() - this.style.unit) + ((this.style.unit - element.style.font:getHeight()) / 2))))
  295. end
  296. love.graphics.setFont(ostyle_font)
  297. love.graphics.setColor(ostyle_r, ostyle_g, ostyle_b, ostyle_a)
  298. end
  299. Gspot.mousepress = function(this, x, y, button)
  300. this:unfocus()
  301. if this.mousein then
  302. local element = this.mousein
  303. if element.elementtype ~= 'hidden' then element:getparent():setlevel() end
  304. if button == mouseL then
  305. if element.drag then
  306. this.drag = element
  307. element.offset = {x = x - element:getpos().x, y = y - element:getpos().y}
  308. end
  309. if this.mousedt < this.dblclickinterval and element.dblclick then element:dblclick(x, y, button)
  310. elseif element.click then element:click(x, y)end
  311. elseif button == mouseR and element.rclick then element:rclick(x, y)
  312. elseif button == 'wu' and element.wheelup then element:wheelup(x, y)
  313. elseif button == 'wd' and element.wheeldown then element:wheeldown(x, y)
  314. end
  315. end
  316. this.mousedt = 0
  317. end
  318. Gspot.mouserelease = function(this, x, y, button)
  319. if this.drag then
  320. local element = this.drag
  321. if button == mouseR then
  322. if element.rdrop then element:rdrop(this.mouseover) end
  323. if this.mouseover and this.mouseover.rcatch then this.mouseover:rcatch(element.id) end
  324. else
  325. if element.drop then element:drop(this.mouseover) end
  326. if this.mouseover and this.mouseover.catch then this.mouseover:catch(element) end
  327. end
  328. end
  329. this.drag = nil
  330. end
  331. Gspot.mousewheel = function(this, x, y)
  332. if y ~= 0 and this.mousein then
  333. local element = this.mousein
  334. local call = y > 0 and element.wheelup or element.wheeldown
  335. if call then
  336. local mx, my = love.mouse.getPosition()
  337. call(element, mx, my)
  338. end
  339. end
  340. end
  341. Gspot.keypress = function(this, key)
  342. if this.focus then
  343. if (key == 'return' or key == 'kpenter') and this.focus.done then this.focus:done() end
  344. if this.focus and this.focus.keypress then this.focus:keypress(key) end
  345. end
  346. end
  347. Gspot.textinput = function(this, key)
  348. -- Due to a bug in love or some library, textinput can give us
  349. -- invalid UTF-8
  350. -- (for example, on Linux with Spanish keyboard:
  351. -- AltGr + Shift + "+" generates "\xAF" which does not
  352. -- start with \xC0-\xFF)
  353. if not key or key == "" or key:byte(1) >= 0x80 and key:byte(1) < 0xC0 then return end
  354. if this.focus and this.focus.textinput then this.focus:textinput(key) end
  355. end
  356. Gspot.getmouse = function(this)
  357. return love.mouse.getPosition()
  358. end
  359. -- legacy
  360. Gspot.newid = function(this)
  361. this.maxid = this.maxid + 1
  362. return this.maxid
  363. end
  364. -- /legacy
  365. Gspot.clone = function(this, t)
  366. local c = {}
  367. for i, v in pairs(t) do
  368. if v then
  369. if type(v) == 'table' then c[i] = this:clone(v) else c[i] = v end
  370. end
  371. end
  372. return setmetatable(c, getmetatable(t))
  373. end
  374. Gspot.getindex = function(tab, val)
  375. for i, v in pairs(tab) do if v == val then return i end end
  376. end
  377. Gspot.add = function(this, element)
  378. element.id = this:newid() -- legacy
  379. table.insert(this.elements, element)
  380. if element.parent then element.parent:addchild(element) end
  381. return element
  382. end
  383. Gspot.rem = function(this, element)
  384. if element.parent then element.parent:remchild(element) end
  385. while #element.children > 0 do
  386. for i, child in ipairs(element.children) do this:rem(child) end
  387. end
  388. if element == this.mousein then this.mousein = nil end
  389. if element == this.drag then this.drag = nil end
  390. if element == this.focus then this:unfocus() end
  391. return table.remove(this.elements, this.getindex(this.elements, element))
  392. end
  393. Gspot.setfocus = function(this, element)
  394. if element then
  395. this.focus = element
  396. local rep = element.keyrepeat
  397. if rep ~= nil then
  398. if element.keydelay then -- legacy stuff
  399. rep = element.keydelay > 0
  400. elseif rep == 0 then
  401. rep = false
  402. end
  403. this.orepeat = love.keyboard.hasKeyRepeat()
  404. love.keyboard.setKeyRepeat(rep)
  405. end
  406. end
  407. end
  408. Gspot.unfocus = function(this)
  409. this.focus = nil
  410. if this.orepeat ~= nil then
  411. love.keyboard.setKeyRepeat(this.orepeat)
  412. this.orepeat = nil
  413. end
  414. end
  415. local pos = {
  416. load = function(this, Gspot, t)
  417. assert(type(t) == 'table' or not t, 'invalid pos constructor argument : must be of type table or nil')
  418. t = t or {}
  419. t = t.pos or t
  420. local circ = false
  421. local pos = {}
  422. if t.r or t[5] or (#t == 3 and not t.w) then
  423. pos.r = t.r or t[5] or t[3]
  424. circ = true
  425. pos.w = t.w or t[3] or pos.r * 2
  426. pos.h = t.h or t[4] or pos.r * 2
  427. else
  428. pos.w = t.w or t[3] or Gspot.style.unit
  429. pos.h = t.h or t[4] or Gspot.style.unit
  430. end
  431. pos.x = t.x or t[1] or 0
  432. pos.y = t.y or t[2] or 0
  433. return setmetatable(pos, this), circ
  434. end,
  435. __unm = function(a)
  436. local c = {x = a.x, y = a.y, w = a.w, h = a.h, r = a.r}
  437. c.x = 0 - a.x
  438. c.y = 0 - a.y
  439. return setmetatable(c, getmetatable(a))
  440. end,
  441. __add = function(a, b)
  442. local c = {x = a.x, y = a.y, w = a.w, h = a.h, r = a.r}
  443. local d
  444. local success, e = pcall(function(b) return b.x end, b)
  445. if success then d = e else d = b end
  446. c.x = a.x + d
  447. local success, e = pcall(function(b) return b.y end, b)
  448. if success then d = e else d = b end
  449. c.y = a.y + d
  450. return setmetatable(c, getmetatable(a))
  451. end,
  452. __sub = function(a, b)
  453. local c = {x = a.x, y = a.y, w = a.w, h = a.h, r = a.r}
  454. local d
  455. local success, e = pcall(function(b) return b.x end, b)
  456. if success then d = e else d = b end
  457. c.x = a.x - d
  458. local success, e = pcall(function(b) return b.y end, b)
  459. if success then d = e else d = b end
  460. c.y = a.y - d
  461. return setmetatable(c, getmetatable(a))
  462. end,
  463. __mul = function(a, b)
  464. local c = {x = a.x, y = a.y, w = a.w, h = a.h, r = a.r}
  465. local d
  466. local success, e = pcall(function(b) return b.x end, b)
  467. if success then d = e else d = b end
  468. c.x = a.x * d
  469. local success, e = pcall(function(b) return b.y end, b)
  470. if success then d = e else d = b end
  471. c.y = c.y * d
  472. return setmetatable(c, getmetatable(a))
  473. end,
  474. __div = function(a, b)
  475. local c = {x = a.x, y = a.y, w = a.w, h = a.h, r = a.r}
  476. local d
  477. local success, e = pcall(function(b) return b.x end, b)
  478. if success then d = e else d = b end
  479. c.x = a.x / d
  480. local success, e = pcall(function(b) return b.y end, b)
  481. if success then d = e else d = b end
  482. c.y = a.y / d
  483. return setmetatable(c, getmetatable(a))
  484. end,
  485. __pow = function(a, b)
  486. local c = {x = a.x, y = a.y, w = a.w, h = a.h, r = a.r}
  487. local d
  488. local success, e = pcall(function(b) return b.x end, b)
  489. if success then d = e else d = b end
  490. c.x = a.x ^ d
  491. local success, e = pcall(function(b) return b.y end, b)
  492. if success then d = e else d = b end
  493. c.y = a.y ^ d
  494. return setmetatable(c, getmetatable(a))
  495. end,
  496. __tostring = function(this) return this:type() .. ' : x = ' .. tostring(this.x) .. ', y = ' .. tostring(this.y) .. ', w = ' .. tostring(this.w) .. ', h = ' .. tostring(this.h) .. ', r = ' .. tostring(this.r) end,
  497. __index = {
  498. type = function(this) return 'Gspot.pos' end,
  499. },
  500. }
  501. Gspot.pos = setmetatable(pos, {__call = pos.load})
  502. Gspot.util = {
  503. setshape = function(this, shape)
  504. assert(shape == 'circle' or shape == 'rect' or shape == 'roundrect'or not shape, 'shape must be "rect", "circle", "roundrect" or nil')
  505. this.shape = shape
  506. if this.shape == 'circle' and not this.pos.r then this.pos.r = this.pos.w / 2 end
  507. end,
  508. drawshape = function(this, pos)
  509. pos = pos or this:getpos()
  510. if this.shape == 'circle' then
  511. local segments = this.segments or math.max(pos.r, 8)
  512. love.graphics.circle('fill', pos.x + pos.r, pos.y + pos.r, pos.r, segments)
  513. if this:hasBorder() then
  514. local border = this.style.border
  515. -- Backup line style
  516. local tmp_linewidth = love.graphics.getLineWidth()
  517. local tmp_linestyle = love.graphics.getLineStyle()
  518. -- Set new line style (if applicable)
  519. love.graphics.setLineWidth(border.width or tmp_linewidth)
  520. love.graphics.setLineStyle(border.style or tmp_linestyle)
  521. -- Actually draw the border
  522. local r = pos.r + border.top
  523. love.graphics.circle('line', pos.x + r, pos.y + r, r, segments)
  524. -- Reverting to backup line style
  525. love.graphics.setLineWidth(tmp_linewidth)
  526. love.graphics.setLineStyle(tmp_linestyle)
  527. end
  528. else
  529. this:rect(pos)
  530. end
  531. end,
  532. rect = function(this, pos, mode)
  533. pos = this.Gspot:pos(pos.pos or pos or this.pos)
  534. assert(pos:type() == 'Gspot.pos')
  535. mode = mode or 'fill'
  536. if this.shape == 'roundrect' then
  537. love.graphics.rectangle(mode, pos.x, pos.y, pos.w, pos.h, this.style.rectradius, this.style.rectradius, this.style.rectsegments)
  538. else
  539. love.graphics.rectangle(mode, pos.x, pos.y, pos.w, pos.h)
  540. end
  541. if this:hasBorder() then
  542. local border = this.style.border
  543. local tmp_linewidth = love.graphics.getLineWidth()
  544. local tmp_linestyle = love.graphics.getLineStyle()
  545. local tmp_color = {love.graphics.getColor()}
  546. love.graphics.setLineWidth(border.width or tmp_linewidth)
  547. love.graphics.setLineStyle(border.style or tmp_linestyle)
  548. setColor(border.color or tmp_color)
  549. local x, y, w, h
  550. x = pos.x -border.left
  551. y = pos.y -border.top
  552. w = pos.w +border.right +border.left
  553. h = pos.h +border.bottom +border.top
  554. if this.shape == 'roundrect' then
  555. love.graphics.rectangle("line", x, y, w, h, this.style.rectradius, this.style.rectradius, this.style.rectsegments)
  556. else
  557. love.graphics.rectangle("line", x, y, w, h)
  558. end
  559. love.graphics.setLineWidth(tmp_linewidth)
  560. love.graphics.setLineStyle(tmp_linestyle)
  561. setColor(tmp_color)
  562. end
  563. end,
  564. setimage = function(this, img)
  565. if type(img) == 'string' and fileExists(img) then img = love.graphics.newImage(img) end
  566. if pcall(function(img) return img:type() == 'Image' end, img) then this.img = img
  567. else this.img = nil end
  568. end,
  569. drawimg = function(this, pos)
  570. local r, g, b, a = love.graphics.getColor()
  571. setColor(255, 255, 255, 255)
  572. love.graphics.draw(this.img, (pos.x + (pos.w / 2)) - (this.img:getWidth()) / 2, (pos.y + (pos.h / 2)) - (this.img:getHeight() / 2))
  573. love.graphics.setColor(r, g, b, a)
  574. end,
  575. setfont = function(this, font, size)
  576. if type(font) == 'string' and fileExists(font) then
  577. font = love.graphics.newFont(font, size)
  578. elseif type(font) == 'number' then
  579. font = love.graphics.newFont(font)
  580. end
  581. if type(font) == 'userdata' and type(font.type) == 'function' and font:type() == 'Font' then
  582. this.style.font = font
  583. if this.autosize then
  584. this.pos.w = font:getWidth(this.label) + (this.style.unit / 2)
  585. this.pos.h = font:getHeight()
  586. end
  587. else
  588. this.style.font = nil
  589. this.style = this.Gspot:clone(this.style)
  590. end
  591. end,
  592. getpos = function(this, scissor)
  593. local pos = this.Gspot:pos(this)
  594. if this.parent then
  595. local ppos = 0
  596. ppos, scissor = this.parent:getpos()
  597. pos = pos + ppos
  598. if this.parent:type() == 'Gspot.element.scrollgroup' and this ~= this.parent.scrollv and this ~= this.parent.scrollh then
  599. scissor = this.Gspot:clone(this.parent:getpos())
  600. if this.parent.scrollv then pos.y = pos.y - this.parent.scrollv.values.current end
  601. if this.parent.scrollh then pos.x = pos.x - this.parent.scrollh.values.current end
  602. end
  603. end
  604. return pos, scissor
  605. end,
  606. containspoint = function(this, point)
  607. local contains = true
  608. local pos = point.pos or point
  609. local thispos, scissor = this:getpos()
  610. if this.elementtype == 'text' then
  611. -- Text is treated specially, because it's drawn
  612. -- centered vertically without moving its position,
  613. -- therefore the apparent position is different from
  614. -- the element's pos. Size should be correct, though.
  615. local x, y = pos.x, pos.y -- save values (avoids creating garbage)
  616. pos.x = pos.x - math.floor((this.style.unit / 4) + 0.5)
  617. pos.y = pos.y - math.floor(((this.style.unit - this.style.font:getHeight()) / 2) + 0.5)
  618. if not this.withinrect(pos, thispos, scissor) then contains = false end
  619. pos.x, pos.y = x, y -- restore
  620. elseif this.shape == 'circle' then
  621. if not this.withinradius(pos, thispos + this.pos.r, scissor) then contains = false end
  622. elseif not this.withinrect(pos, thispos, scissor) then
  623. contains = false
  624. end
  625. return contains
  626. end,
  627. withinrect = function(pos, rect, scissor)
  628. pos = pos.pos or pos
  629. rect = rect.pos or rect
  630. if scissor then
  631. return pos.x >= rect.x and pos.x < (rect.x + rect.w) and pos.y >= rect.y and pos.y < (rect.y + rect.h)
  632. and pos.x >= scissor.x and pos.x < scissor.x + scissor.w and pos.y >= scissor.y and pos.y < scissor.y + scissor.h
  633. end
  634. return pos.x >= rect.x and pos.x < (rect.x + rect.w) and pos.y >= rect.y and pos.y < (rect.y + rect.h)
  635. end,
  636. getdist = function(pos, target)
  637. pos = pos.pos or pos
  638. target = target.pos or target
  639. return math.sqrt((pos.x-target.x) * (pos.x-target.x) + (pos.y-target.y) * (pos.y-target.y))
  640. end,
  641. withinradius = function(pos, circ, scissor)
  642. pos = pos.pos or pos
  643. circ = circ.pos or circ
  644. if (pos.x - circ.x) * (pos.x - circ.x) + (pos.y - circ.y) * (pos.y - circ.y) < circ.r * circ.r then
  645. if scissor then
  646. return pos.x >= scissor.x and pos.x < scissor.x + scissor.w and pos.y >= scissor.y and pos.y < scissor.y + scissor.h
  647. else
  648. return true
  649. end
  650. end
  651. return false
  652. end,
  653. getparent = function(this)
  654. if this.parent then return this.parent:getparent()
  655. else return this end
  656. end,
  657. getmaxw = function(this)
  658. local maxw = 0
  659. for i, child in ipairs(this.children) do
  660. if (child ~= this.scrollv and child ~= this.scrollh) and child.pos.x + child.pos.w > maxw then maxw = child.pos.x + child.pos.w end
  661. end
  662. return maxw
  663. end,
  664. getmaxh = function(this)
  665. local maxh = 0
  666. for i, child in ipairs(this.children) do
  667. if (child ~= this.scrollv and child ~= this.scrollh) and child.pos.y + child.pos.h > maxh then maxh = child.pos.y + child.pos.h end
  668. end
  669. return maxh
  670. end,
  671. addchild = function(this, child, autostack)
  672. if autostack then
  673. if type(autostack) == 'number' or autostack == 'grid' then
  674. local limitx = (type(autostack) == 'number' and autostack) or this.pos.w
  675. local maxx, maxy = 0, 0
  676. for i, element in ipairs(this.children) do
  677. if element ~= this.scrollh and element ~= this.scrollv then
  678. if element.pos.y > maxy then maxy = element.pos.y end
  679. if element.pos.x + element.pos.w + child.pos.w <= limitx then maxx = element.pos.x + element.pos.w
  680. else maxx, maxy = 0, element.pos.y + element.pos.h end
  681. end
  682. end
  683. child.pos.x, child.pos.y = maxx, maxy
  684. elseif autostack == 'horizontal' then child.pos.x = this:getmaxw()
  685. elseif autostack == 'vertical' then child.pos.y = this:getmaxh() end
  686. end
  687. table.insert(this.children, child)
  688. child.parent = this
  689. child.style = this.Gspot:clone(child.style)
  690. setmetatable(child.style, {__index = this.style})
  691. if this.scrollh then this.scrollh.values.max = math.max(this:getmaxw() - this.pos.w, 0) end
  692. if this.scrollv then this.scrollv.values.max = math.max(this:getmaxh() - this.pos.h, 0) end
  693. return child
  694. end,
  695. remchild = function(this, child)
  696. child.pos = child:getpos()
  697. table.remove(this.children, this.Gspot.getindex(this.children, child))
  698. child.parent = nil
  699. setmetatable(child.style, {__index = this.Gspot.style})
  700. end,
  701. replace = function(this, replacement)
  702. this.Gspot.elements[this.Gspot.getindex(this.Gspot.elements, this)] = replacement
  703. return replacement
  704. end,
  705. getlevel = function(this)
  706. for i, element in pairs(this.Gspot.elements) do
  707. if element == this then return i end
  708. end
  709. end,
  710. setlevel = function(this, level)
  711. if level then
  712. table.insert(this.Gspot.elements, level, table.remove(this.Gspot.elements, this.Gspot.getindex(this.Gspot.elements, this)))
  713. for i, child in ipairs(this.children) do child:setlevel(level + i) end
  714. else
  715. table.insert(this.Gspot.elements, table.remove(this.Gspot.elements, this.Gspot.getindex(this.Gspot.elements, this)))
  716. for i, child in ipairs(this.children) do child:setlevel() end
  717. end
  718. end,
  719. show = function(this)
  720. this.display = true
  721. for i, child in pairs(this.children) do child:show() end
  722. end,
  723. hide = function(this)
  724. this.display = false
  725. for i, child in pairs(this.children) do child:hide() end
  726. end,
  727. focus = function(this)
  728. this.Gspot:setfocus(this)
  729. end,
  730. type = function(this)
  731. return 'Gspot.element.'..this.elementtype
  732. end,
  733. hasBorder = function(this)
  734. return this.style.border.color[4] > 0
  735. end,
  736. }
  737. Gspot.element = {
  738. load = function(this, Gspot, elementtype, label, pos, parent)
  739. assert(Gspot[elementtype], 'invalid element constructor argument : element.elementtype must be an existing element type')
  740. assert(type(label) == 'string' or type(label) == 'number' or not label, 'invalid element constructor argument : element.label must be of type string or number')
  741. assert(type(pos) == 'table' or not pos, 'invalid element constructor argument : element.pos must be of type table or nil')
  742. assert((type(parent) == 'table' and parent:type():sub(1, 13) == 'Gspot.element') or not parent, 'invalid element constructor argument : element.parent must be of type element or nil')
  743. local pos, circ = Gspot:pos(pos)
  744. local element = {elementtype = elementtype, label = label, pos = pos, display = true, dt = 0, parent = parent, children = {}, Gspot = Gspot}
  745. if circ then element.shape = 'circle' else element.shape = 'rect' end
  746. if parent then element.style = setmetatable({}, {__index = parent.style})
  747. else element.style = setmetatable({}, {__index = Gspot.style}) end
  748. element.style.border = setmetatable({}, {__index = Gspot.style.border})
  749. return setmetatable(element, {__index = Gspot[elementtype], __tostring = function(this) return this:type() .. ' (' .. this:getlevel() .. ')' end})
  750. end,
  751. }
  752. setmetatable(Gspot.element, {__call = Gspot.element.load})
  753. Gspot.scrollvalues = function(this, values)
  754. local val = {}
  755. val.min = values.min or values[1] or 0
  756. val.max = values.max or values[2] or 0
  757. val.current = values.current or values[3] or val.min
  758. val.step = values.step or values[4] or this.style.unit
  759. val.axis = values.axis or values[5] or 'vertical'
  760. return val
  761. end
  762. -- elements
  763. Gspot.group = {
  764. load = function(this, Gspot, label, pos, parent)
  765. return Gspot:add(Gspot:element('group', label, pos, parent))
  766. end,
  767. draw = function(this, pos)
  768. setColor(this.style.bg)
  769. this:drawshape(pos)
  770. if this.label then
  771. setColor(this.style.labelfg or this.style.fg)
  772. lgprint(this.label, pos.x + ((pos.w - this.style.font:getWidth(this.label)) / 2), pos.y + ((this.style.unit - this.style.font:getHeight()) / 2))
  773. end
  774. end,
  775. }
  776. setmetatable(Gspot.group, {__index = Gspot.util, __call = Gspot.group.load})
  777. Gspot.collapsegroup = {
  778. load = function(this, Gspot, label, pos, parent)
  779. local element = Gspot:group(label, pos, parent)
  780. element.view = true
  781. element.orig = Gspot:clone(element.pos)
  782. element.toggle = function(this)
  783. this.view = not element.view
  784. this.pos.h = (this.view and this.orig.h) or this.style.unit
  785. for i, child in ipairs(this.children) do
  786. if child ~= this.control then
  787. if this.view then child:show() else child:hide() end
  788. end
  789. end
  790. this.control.label = (this.view and '-') or '='
  791. end
  792. element.control = Gspot:button('-', {element.pos.w - element.style.unit}, element)
  793. element.control.click = function(this)
  794. this.parent:toggle()
  795. end
  796. return element
  797. end,
  798. }
  799. setmetatable(Gspot.collapsegroup, {__index = Gspot.util, __call = Gspot.collapsegroup.load})
  800. Gspot.text = {
  801. load = function(this, Gspot, label, pos, parent, autosize)
  802. local element = Gspot:element('text', label, pos, parent)
  803. if autosize then
  804. element.pos.w = element.style.font:getWidth(label) + (element.style.unit / 2)
  805. element.autosize = autosize
  806. end
  807. element:setfont(element.style.font)
  808. return Gspot:add(element)
  809. end,
  810. setfont = function(this, font, size)
  811. this.Gspot.util.setfont(this, font, size)
  812. if not this.autosize then -- height needs adjustment regardless
  813. local width, lines = this.style.font:getWrap(this.label, this.pos.w - (this.style.unit / 2))
  814. if type(lines) == "table" then lines = #lines end
  815. lines = math.max(lines, 1)
  816. this.pos.h = this.style.font:getHeight() * lines + this.style.unit / 4
  817. end
  818. end,
  819. draw = function(this, pos)
  820. setColor(this.style.labelfg or this.style.fg)
  821. if this.autosize then lgprint(this.label, pos.x + (this.style.unit / 4), pos.y + (this.style.unit / 8))
  822. else lgprintf(this.label, pos.x + (this.style.unit / 4), pos.y + (this.style.unit / 8), (this.autosize and pos.w) or pos.w - (this.style.unit / 2), 'left') end
  823. end,
  824. }
  825. setmetatable(Gspot.text, {__index = Gspot.util, __call = Gspot.text.load})
  826. Gspot.typetext = {
  827. load = function(this, Gspot, label, pos, parent, autosize)
  828. local element = Gspot:text('', pos, parent, autosize)
  829. element.values = {text = label, cursor = 1}
  830. element.updateinterval = 0.1
  831. element.update = function(this, dt)
  832. this.values.cursor = utf8char_after(this.values.text, this.values.cursor + 1) - 1
  833. this.label = this.values.text:sub(1, this.values.cursor)
  834. end
  835. return Gspot:add(element)
  836. end,
  837. }
  838. setmetatable(Gspot.typetext, {__index = Gspot.util, __call = Gspot.typetext.load})
  839. Gspot.image = {
  840. load = function(this, Gspot, label, pos, parent, img)
  841. local element = Gspot:element('image', label, pos, parent)
  842. element:setimage(img)
  843. return Gspot:add(element)
  844. end,
  845. setimage = function(this, img)
  846. this.Gspot.util.setimage(this, img)
  847. if this.img then
  848. this.pos.w = this.img:getWidth()
  849. this.pos.h = this.img:getHeight()
  850. end
  851. end,
  852. draw = function(this, pos)
  853. if this.img then
  854. this:drawimg(pos)
  855. end
  856. if this.label then
  857. setColor(this.style.labelfg or this.style.fg)
  858. lgprint(this.label, pos.x + ((pos.w - this.style.font:getWidth(this.label)) / 2), (pos.y + pos.h) + ((this.style.unit - this.style.font:getHeight()) / 2))
  859. end
  860. end,
  861. }
  862. setmetatable(Gspot.image, {__index = Gspot.util, __call = Gspot.image.load})
  863. Gspot.button = {
  864. load = function(this, Gspot, label, pos, parent, autosize)
  865. if autosize then this.autosize = autosize end
  866. return Gspot:add(Gspot:element('button', label, pos, parent))
  867. end,
  868. draw = function(this, pos)
  869. if this.parent and this.value == this.parent.value then
  870. if this == this.Gspot.mousein then setColor(this.style.focus)
  871. else setColor(this.style.hilite) end
  872. else
  873. if this == this.Gspot.mousein then setColor(this.style.hilite)
  874. else setColor(this.style.default) end
  875. end
  876. this:drawshape(pos)
  877. setColor(this.style.labelfg or this.style.fg)
  878. if this.shape == 'circle' then
  879. if this.img then this:drawimg(pos) end
  880. if this.label then lgprint(this.label, (pos.x + pos.r) - (this.style.font:getWidth(this.label) / 2), (this.img and (pos.y + (pos.r * 2)) + ((this.style.unit - this.style.font:getHeight()) / 2)) or (pos.y + pos.r) - (this.style.font:getHeight() / 2)) end
  881. else
  882. if this.img then this:drawimg(pos) end
  883. if this.label then lgprint(this.label, (pos.x + (pos.w / 2)) - (this.style.font:getWidth(this.label) / 2), (this.img and pos.y + ((this.style.unit - this.style.font:getHeight()) / 2)) or (pos.y + (pos.h / 2)) - (this.style.font:getHeight() / 2)) end
  884. end
  885. end,
  886. }
  887. setmetatable(Gspot.button, {__index = Gspot.util, __call = Gspot.button.load})
  888. Gspot.imgbutton = {
  889. load = function(this, Gspot, label, pos, parent, img)
  890. local element = Gspot:button(label, pos, parent)
  891. element:setimage(img)
  892. return Gspot:add(element)
  893. end,
  894. }
  895. setmetatable(Gspot.imgbutton, {__index = Gspot.util, __call = Gspot.imgbutton.load})
  896. Gspot.option = {
  897. load = function(this, Gspot, label, pos, parent, value)
  898. local element = Gspot:button(label, pos, parent)
  899. element.value = value
  900. element.click = function(this) this.parent.value = this.value end
  901. return element
  902. end,
  903. }
  904. setmetatable(Gspot.option, {__index = Gspot.util, __call = Gspot.option.load})
  905. Gspot.checkbox = {
  906. load = function(this, Gspot, label, pos, parent, value)
  907. local element = Gspot:element('checkbox', label, pos, parent)
  908. element.value = value
  909. return Gspot:add(element)
  910. end,
  911. click = function(this) this.value = not this.value end,
  912. draw = function(this, pos)
  913. if this == this.Gspot.mousein then setColor(this.style.hilite)
  914. else setColor(this.style.default) end
  915. this:drawshape(pos)
  916. if this.value then
  917. setColor(this.style.fg)
  918. this:drawshape(this.Gspot:pos({x = pos.x + (pos.w / 4), y = pos.y + (pos.h / 4), w = pos.w / 2, h = pos.h / 2, r = pos.r and pos.r / 2}))
  919. end
  920. if this.label then
  921. setColor(this.style.labelfg or this.style.fg)
  922. lgprint(this.label, pos.x + pos.w + (this.style.unit / 2), pos.y + ((this.pos.h - this.style.font:getHeight()) / 2))
  923. end
  924. end,
  925. }
  926. setmetatable(Gspot.checkbox, {__index = Gspot.util, __call = Gspot.checkbox.load})
  927. Gspot.input = {
  928. load = function(this, Gspot, label, pos, parent, value, ispassword, passwordchar)
  929. local element = Gspot:element('input', label, pos, parent)
  930. element.value = (value and tostring(value)) or ''
  931. element.cursor = element.value:len()
  932. element.textorigin = 0
  933. element.cursorlife = 0
  934. element.keyrepeat = true
  935. element.ispassword = ispassword or false
  936. element.passwordchar = (passwordchar and tostring(passwordchar)) or '*'
  937. return Gspot:add(element)
  938. end,
  939. update = function(this, dt)
  940. if this.cursor > #this.value then this.cursor = #this.value end
  941. if this.Gspot.focus == this then
  942. if this.cursorlife >= 1 then this.cursorlife = 0
  943. else this.cursorlife = this.cursorlife + dt end
  944. end
  945. end,
  946. draw = function(this, pos)
  947. if this == this.Gspot.focus then
  948. setColor(this.style.bg)
  949. elseif this == this.Gspot.mousein then
  950. setColor(this.style.hilite)
  951. else
  952. setColor(this.style.default)
  953. end
  954. this:drawshape(pos)
  955. -- Margin of edit box is unit/4 on each side, so total margin is unit/2
  956. local editw = pos.w - this.style.unit / 2
  957. if editw >= 1 then -- won't be visible otherwise and we need room for the cursor
  958. -- We don't want to undo the current scissor, to avoid printing text where it shouldn't be
  959. -- (e.g. partially visible edit box inside a viewport) so we clip the current scissor.
  960. local sx, sy, sw, sh = clipScissor(pos.x + this.style.unit / 4, pos.y, editw, pos.h)
  961. setColor(this.style.fg)
  962. local str = this.ispassword and string.rep(this.passwordchar,utf8len(tostring(this.value))) or tostring(this.value)
  963. -- cursorx is the position relative to the start of the edit box
  964. -- (add pos.x + this.style.unit/4 to obtain the screen X coordinate)
  965. local cursorx = this.textorigin + this.style.font:getWidth(str:sub(1, this.cursor))
  966. -- adjust text origin so that the cursor is always within the edit box
  967. if cursorx < 0 then
  968. this.textorigin = math.min(0, this.textorigin - cursorx)
  969. cursorx = 0
  970. end
  971. if cursorx > editw - 1 then
  972. this.textorigin = math.min(0, this.textorigin - cursorx + editw - 1)
  973. cursorx = editw - 1
  974. end
  975. -- print the whole text and let the scissor do the clipping
  976. lgprint(str, pos.x + this.style.unit / 4 + this.textorigin, pos.y + (pos.h - this.style.font:getHeight()) / 2)
  977. if this == this.Gspot.focus and this.cursorlife < 0.5 then
  978. love.graphics.rectangle("fill", pos.x + this.style.unit / 4 + cursorx, pos.y + this.style.unit / 8, 1, pos.h - this.style.unit / 4)
  979. end
  980. -- restore current scissor
  981. love.graphics.setScissor(sx, sy, sw, sh)
  982. end
  983. if this.label then
  984. setColor(this.style.labelfg or this.style.fg)
  985. lgprint(this.label, pos.x - ((this.style.unit / 2) + this.style.font:getWidth(this.label)), pos.y + ((this.pos.h - this.style.font:getHeight()) / 2))
  986. end
  987. end,
  988. click = function(this) this:focus() end,
  989. done = function(this) this.Gspot:unfocus() end,
  990. keypress = function(this, key)
  991. local save_cursorlife = this.cursorlife
  992. this.cursorlife = 0
  993. -- fragments attributed to vrld's Quickie : https://github.com/vrld/Quickie
  994. if key == 'backspace' then
  995. local cur = this.cursor
  996. if cur > 0 then
  997. this.cursor = utf8char_begin(this.value, cur) - 1
  998. this.value = this.value:sub(1, this.cursor)..this.value:sub(cur + 1)
  999. end
  1000. elseif key == 'delete' then
  1001. local cur = utf8char_after(this.value, this.cursor + 1)
  1002. this.value = this.value:sub(1, this.cursor)..this.value:sub(cur)
  1003. elseif key == 'left' then
  1004. if this.cursor > 0 then
  1005. this.cursor = utf8char_begin(this.value, this.cursor) - 1
  1006. end
  1007. elseif key == 'right' then
  1008. this.cursor = utf8char_after(this.value, this.cursor + 1) - 1
  1009. elseif key == 'home' then
  1010. this.cursor = 0
  1011. elseif key == 'end' then
  1012. this.cursor = this.value:len()
  1013. elseif key == 'tab' and this.next and this.next.elementtype then
  1014. this.next:focus()
  1015. elseif key == 'escape' then
  1016. this.Gspot:unfocus()
  1017. else
  1018. -- all of the above reset the blink timer, but otherwise it's retained
  1019. this.cursorlife = save_cursorlife
  1020. end
  1021. -- /fragments
  1022. end,
  1023. textinput = function(this, key)
  1024. this.value = this.value:sub(1, this.cursor) .. key .. this.value:sub(this.cursor + 1)
  1025. this.cursor = this.cursor + #key
  1026. -- reset blink timer
  1027. this.cursorlife = 0
  1028. end,
  1029. }
  1030. setmetatable(Gspot.input, {__index = Gspot.util, __call = Gspot.input.load})
  1031. Gspot.scroll = {
  1032. load = function(this, Gspot, label, pos, parent, values)
  1033. local element = Gspot:element('scroll', label, pos, parent)
  1034. element.values = Gspot:scrollvalues(values)
  1035. return Gspot:add(element)
  1036. end,
  1037. update = function(this, dt)
  1038. local mouse = {}
  1039. mouse.x, mouse.y = this.Gspot:getmouse()
  1040. if this.withinrect(mouse, this:getpos()) and not this.Gspot.drag then this.Gspot.mousein = this end
  1041. end,
  1042. step = function(this, step)
  1043. if step > 0 then this.values.current = math.max(this.values.current - this.values.step, this.values.min)
  1044. elseif step < 0 then this.values.current = math.min(this.values.current + this.values.step, this.values.max)
  1045. end
  1046. end,
  1047. drag = function(this, x, y)
  1048. local pos = this:getpos()
  1049. local hs = this.style.hs
  1050. if hs == 'auto' then
  1051. if this.values.axis == 'vertical' then
  1052. local h = this.parent and this.parent.pos.h or pos.h
  1053. hs = math.max(this.style.unit / 4, math.min(pos.h, pos.h * h / (this.values.max - this.values.min + h)))
  1054. else
  1055. local w = this.parent and this.parent.pos.w or pos.w
  1056. hs = math.max(this.style.unit / 4, math.min(pos.w, pos.w * w / (this.values.max - this.values.min + w)))
  1057. end
  1058. end
  1059. if this.values.axis == 'vertical' and pos.h == hs or this.values.axis ~= 'vertical' and pos.w == hs then
  1060. this.values.current = 0
  1061. else
  1062. this.values.current = this.values.min + ((this.values.max - this.values.min) * ((this.values.axis == 'vertical' and ((math.min(math.max(pos.y, y - math.floor(hs / 2)), (pos.y + pos.h - hs)) - pos.y) / (pos.h - hs))) or ((math.min(math.max(pos.x, x - math.floor(hs / 2)), (pos.x + pos.w - hs)) - pos.x) / (pos.w - hs))))
  1063. end
  1064. end,
  1065. wheelup = function(this)
  1066. if this.values.axis == 'horizontal' then this:step(-1) else this:step(1) end
  1067. end,
  1068. wheeldown = function(this)
  1069. if this.values.axis == 'horizontal' then this:step(1) else this:step(-1) end
  1070. end,
  1071. keypress = function(this, key)
  1072. if key == 'left' and this.values.axis == 'horizontal' then
  1073. this:step(1)
  1074. elseif key == 'right' and this.values.axis == 'horizontal' then
  1075. this:step(-1)
  1076. elseif key == 'up' and this.values.axis == 'vertical' then
  1077. this:step(-1)
  1078. elseif key == 'down' and this.values.axis == 'vertical' then
  1079. this:step(1)
  1080. elseif key == 'tab' and this.next and this.next.elementtype then
  1081. this.next:focus()
  1082. elseif key == 'escape' then
  1083. this.Gspot:unfocus()
  1084. end
  1085. end,
  1086. done = function(this) this.Gspot:unfocus() end,
  1087. draw = function(this, pos)
  1088. if this == this.Gspot.mousein or this == this.Gspot.drag or this == this.Gspot.focus then setColor(this.style.default)
  1089. else setColor(this.style.bg) end
  1090. this:rect(pos)
  1091. if this == this.Gspot.mousein or this == this.Gspot.drag or this == this.Gspot.focus then setColor(this.style.fg)
  1092. else setColor(this.style.hilite) end
  1093. local hs = this.style.hs
  1094. if hs == 'auto' then
  1095. if this.values.axis == 'vertical' then
  1096. local h = this.parent and this.parent.pos.h or pos.h
  1097. hs = math.max(this.style.unit / 4, math.min(pos.h, pos.h * h / (this.values.max - this.values.min + h)))
  1098. else
  1099. local w = this.parent and this.parent.pos.w or pos.w
  1100. hs = math.max(this.style.unit / 4, math.min(pos.w, pos.w * w / (this.values.max - this.values.min + w)))
  1101. end
  1102. end
  1103. local handlepos = this.Gspot:pos({x = (this.values.axis == 'horizontal' and math.min(pos.x + (pos.w - hs), math.max(pos.x, pos.x + ((pos.w - hs) * (this.values.current / (this.values.max - this.values.min)))))) or pos.x, y = (this.values.axis == 'vertical' and math.min(pos.y + (pos.h - hs), math.max(pos.y, pos.y + ((pos.h - hs) * (this.values.current / (this.values.max - this.values.min)))))) or pos.y, w = this.values.axis == 'horizontal' and hs or this.style.unit, h = this.values.axis == 'vertical' and hs or this.style.unit, r = pos.r})
  1104. this:drawshape(handlepos)
  1105. if this.label then
  1106. setColor(this.style.labelfg or this.style.fg)
  1107. lgprint(this.label, (this.values.axis == 'horizontal' and pos.x - ((this.style.unit / 2) + this.style.font:getWidth(this.label))) or pos.x + ((pos.w - this.style.font:getWidth(this.label)) / 2), (this.values.axis == 'vertical' and (pos.y + pos.h) + ((this.style.unit - this.style.font:getHeight()) / 2)) or pos.y + ((this.style.unit - this.style.font:getHeight()) / 2))
  1108. end
  1109. end,
  1110. }
  1111. Gspot.scroll.rdrag = Gspot.scroll.drag
  1112. setmetatable(Gspot.scroll, {__index = Gspot.util, __call = Gspot.scroll.load})
  1113. Gspot.scrollgroup = {
  1114. load = function(this, Gspot, label, pos, parent, axis)
  1115. axis = axis or 'both'
  1116. local element = Gspot:element('scrollgroup', label, pos, parent)
  1117. element.maxh = 0
  1118. element = Gspot:add(element)
  1119. if axis ~= 'horizontal' then element.scrollv = Gspot:scroll(nil, {x = element.pos.w, y = 0, w = element.style.unit, h = element.pos.h}, element, {0, 0, 0, element.style.unit, 'vertical'}) end
  1120. if axis ~= 'vertical' then element.scrollh = Gspot:scroll(nil, {x = 0, y = element.pos.h, w = element.pos.w, h = element.style.unit}, element, {0, 0, 0, element.style.unit, 'horizontal'}) end
  1121. return element
  1122. end,
  1123. draw = function(this, pos)
  1124. setColor(this.style.bg)
  1125. this:drawshape(pos)
  1126. if this.label then
  1127. setColor(this.style.labelfg or this.style.fg)
  1128. lgprint(this.label, pos.x + ((pos.w - this.style.font:getWidth(this.label)) / 2), pos.y + ((this.style.unit - this.style.font:getHeight()) / 2))
  1129. end
  1130. end,
  1131. }
  1132. setmetatable(Gspot.scrollgroup, {__index = Gspot.util, __call = Gspot.scrollgroup.load})
  1133. Gspot.hidden = {
  1134. load = function(this, Gspot, label, pos, parent)
  1135. return Gspot:add(Gspot:element('hidden', label, pos, parent))
  1136. end,
  1137. draw = function(this, pos)
  1138. --
  1139. end,
  1140. }
  1141. setmetatable(Gspot.hidden, {__index = Gspot.util, __call = Gspot.hidden.load})
  1142. Gspot.radius = {
  1143. load = function(this, Gspot, label, pos, parent)
  1144. return Gspot:add(Gspot:element('radius', label, pos, parent))
  1145. end,
  1146. draw = function(this, pos)
  1147. --
  1148. end,
  1149. }
  1150. setmetatable(Gspot.radius, {__index = Gspot.util, __call = Gspot.radius.load})
  1151. Gspot.feedback = {
  1152. load = function(this, Gspot, label, pos, parent, autopos)
  1153. pos = pos or {}
  1154. autopos = (autopos == nil and true) or autopos
  1155. if autopos then
  1156. for i, element in ipairs(Gspot.elements) do
  1157. if element.elementtype == 'feedback' and element.autopos then element.pos.y = element.pos.y + element.style.unit end
  1158. end
  1159. end
  1160. pos.x = pos.x or pos[1] or 0
  1161. pos.y = pos.y or pos[2] or 0
  1162. pos.w = 0
  1163. pos.h = 0
  1164. local element = Gspot:add(Gspot:element('feedback', label, pos, parent))
  1165. element.style.fg = {255, 255, 255, 255}
  1166. element.alpha = 255
  1167. element.life = 5
  1168. element.autopos = autopos
  1169. return element
  1170. end,
  1171. update = function(this, dt)
  1172. this.alpha = this.alpha - ((255 * dt) / this.life)
  1173. if this.alpha < 0 then
  1174. this.Gspot:rem(this)
  1175. return
  1176. end
  1177. local color = this.style.fg
  1178. this.style.fg = {color[1], color[2], color[3], this.alpha}
  1179. end,
  1180. draw = function(this, pos)
  1181. setColor(this.style.fg)
  1182. lgprint(this.label, pos.x + (this.style.unit / 4), pos.y + ((this.style.unit - this.style.font:getHeight()) / 2))
  1183. end,
  1184. }
  1185. setmetatable(Gspot.feedback, {__index = Gspot.util, __call = Gspot.feedback.load})
  1186. Gspot.progress = {
  1187. load = function(this, Gspot, label, pos, parent)
  1188. local element = Gspot:add(Gspot:element('progress', label, pos, parent))
  1189. element.loaders = {}
  1190. element.values = Gspot.scrollvalues(element, {0, 0, 0, 1})
  1191. return element
  1192. end,
  1193. update = function(this, dt)
  1194. for i, loader in ipairs(this.loaders) do
  1195. if loader.status == 'waiting' then
  1196. local success, result = pcall(function(loader) return loader.func() end, loader)
  1197. loader.status = (success and 'done') or error
  1198. loader.result = result
  1199. this.values.current = this.values.current + 1
  1200. break
  1201. end
  1202. if i == #this.loaders then this:done() end
  1203. end
  1204. end,
  1205. draw = function(this, pos)
  1206. setColor(this.style.default)
  1207. this:drawshape(pos)
  1208. setColor(this.style.fg)
  1209. this:rect({x = pos.x, y = pos.y, w = pos.w * (this.values.current / this.values.max), h = pos.h})
  1210. if this.label then
  1211. setColor(this.style.labelfg or this.style.fg)
  1212. lgprint(this.label, pos.x - ((this.style.unit / 2) + this.style.font:getWidth(this.label)), pos.y + ((this.pos.h - this.style.font:getHeight()) / 2))
  1213. end
  1214. end,
  1215. done = function(this)
  1216. this.Gspot:rem(this)
  1217. end,
  1218. add = function(this, loader)
  1219. table.insert(this.loaders, {status = 'waiting', func = loader})
  1220. this.values.max = this.values.max + 1
  1221. end,
  1222. }
  1223. setmetatable(Gspot.progress, {__index = Gspot.util, __call = Gspot.progress.load})
  1224. return Gspot:setComponentMax("native")