1-- Call is as follows: Boxes may have children. When layout is called, boxes
2-- have their reserved sizes in x,y,width,height. They can then layout their
3-- children as best they can inside those. Elements report their desired sizes
4-- via getWidth(), getHeight(). If nothing is desired, nil is returned, if
5-- percentage of parent is desired, a percentage string is returned. When
6-- layout() returns, it can be added to a guichan container by calling
7-- "addWidgetTo(container)"
8
9Load("scripts/lib/classes.lua")
10
11function dbgPrint(x)
12   -- print(x)
13end
14
15-- dark = Color(38, 38, 78)
16-- clear = Color(200, 200, 120)
17-- black = Color(0, 0, 0)
18
19Element = class(function(instance)
20      instance._id = nil
21      instance.expands = false
22      instance.x = nil
23      instance.y = nil
24      instance.width = nil
25      instance.height = nil
26end)
27
28function Element:id(name)
29   self._id = name
30   return self
31end
32
33function Element:setId(container, widget)
34   if self._id then
35      field,idx = string.match(self._id, "(.+)%[(%d+)%]")
36      if field and idx then
37         local tbl = container[field] or {}
38         tbl[tonumber(idx)] = widget
39         container[field] = tbl
40      else
41         container[self._id] = widget
42      end
43   end
44end
45
46function Element:expanding()
47   self.expands = true
48   return self
49end
50
51function Element:getWidth()
52   error("Element subclass did not define getWidth")
53end
54
55function Element:getHeight()
56   error("Element subclass did not define getHeight")
57end
58
59function Element:addWidgetTo(container)
60   error("Element subclass did not define addWidgetTo")
61end
62
63Box = class(Element,
64            function(instance, children)
65               Element.init(instance)
66               instance.paddingX = 0
67               instance.paddingY = 0
68               instance.children = children
69               for i,child in ipairs(children) do
70                  -- convenience...
71                  if type(child) == "string" then
72                     children[i] = LLabel(child)
73                  end
74               end
75            end
76)
77
78function Box:withPadding(p)
79   if type(p) == "number" then
80      self.paddingX = p
81      self.paddingY = p
82   else
83      self.paddingX = p[1]
84      self.paddingY = p[2]
85   end
86   return self
87end
88
89function Box:expanding()
90   self.expands = true
91   return self
92end
93
94function Box:getWidth()
95   if self.width == nil then
96      self:calculateMinExtent()
97   end
98   return self.width
99end
100
101function Box:getHeight()
102   if self.height == nil then
103      self:calculateMinExtent()
104   end
105   return self.height
106end
107
108Box.DIRECTION_HORIZONTAL = 1
109Box.DIRECTION_VERTICAL = 2
110
111function Box:calculateMinExtent()
112   local w = 0
113   local h = 0
114   local horiz = self.direction == Box.DIRECTION_HORIZONTAL
115   for i,child in ipairs(self.children) do
116      local cw = child:getWidth()
117      if type(cw) == "number" then
118         if horiz then
119            w = w + cw + self.paddingX
120         else
121            w = math.max(w, cw)
122         end
123      end
124      local ch = child:getHeight()
125      if type(ch) == "number" then
126         if horiz then
127            h = math.max(h, ch)
128         else
129            h = h + ch + self.paddingY
130         end
131      end
132   end
133   self.x = 0
134   self.y = 0
135   self.width = w + (self.paddingX * 2)
136   self.height = h + (self.paddingY * 2)
137   dbgPrint("Min: " .. self.width .. " x " .. self.height)
138end
139
140function Box:layout()
141   dbgPrint("XY: " .. self.x .. " - " .. self.y)
142   local horiz = self.direction == Box.DIRECTION_HORIZONTAL
143
144   local padding
145   local totalSpace
146   if horiz then
147      padding = self.paddingX
148      totalSpace = self.width - padding * 2
149   else
150      padding = self.paddingY
151      totalSpace = self.height - padding * 2
152   end
153
154   local availableSpace = totalSpace - (padding * #self.children)
155   local expandingChildren = 0
156
157   for i,child in ipairs(self.children) do
158      child.parent = self
159      local s
160      if horiz then
161         s = child:getWidth()
162      else
163         s = child:getHeight()
164      end
165      if child.expands then
166         expandingChildren = expandingChildren + 1
167      end
168      if type(s) == "string" then
169         local pct = string.match(s, "[0-9]+")
170         availableSpace = math.max(availableSpace - (totalSpace * (pct / 100)), 0)
171      elseif type(s) == "number" then
172         availableSpace = math.max(availableSpace - s, 0)
173      elseif s == nil then
174         -- boxes with no preference expand to fill available space
175         if not child.expands then
176            child.expands = true
177            expandingChildren = expandingChildren + 1
178         end
179      else
180         error("Invalid child extent: need string, number, or nil")
181      end
182   end
183
184   local childW = self.width - self.paddingX * 2
185   local childH = self.height - self.paddingY * 2
186   local expandingChildrenS = 0
187   if expandingChildren > 0 then
188      expandingChildrenS = availableSpace / expandingChildren
189   end
190   local xOff = self.x + self.paddingX
191   local yOff = self.y + self.paddingY
192
193   for i,child in ipairs(self.children) do
194      local s
195      if horiz then
196         s = child:getWidth()
197      else
198         s = child:getHeight()
199      end
200      if type(s) == "string" then
201         local pct = string.match(s, "[0-9]+")
202         local newS = totalSpace * (pct / 100)
203         if child.expands then
204            newS = newS + expandingChildrenS
205         end
206         if horiz then
207            childW = newS
208         else
209            childH = newS
210         end
211      elseif type(s) == "number" then
212         if child.expands then
213            s = s + expandingChildrenS
214         end
215         if horiz then
216            childW = s
217         else
218            childH = s
219         end
220      elseif w == nil then
221         if horiz then
222            childW = expandingChildrenS
223         else
224            childH = expandingChildrenS
225         end
226      end
227
228      dbgPrint(xOff, yOff, childW, childH)
229      child.x = xOff
230      child.y = yOff
231      child.width = childW
232      child.height = childH
233      dbgPrint("child: " .. child.width .. "x" .. child.height .. "+" .. child.x .. "+" .. child.y)
234      if horiz then
235         xOff = xOff + childW + padding
236      else
237         yOff = yOff + childH + padding
238      end
239   end
240end
241
242function Box:addWidgetTo(container, sizeFromContainer)
243   if sizeFromContainer then
244      self.x = 0 -- containers are relative inside
245      self.y = 0
246      self.width = container:getWidth()
247      self.height = container:getHeight()
248      dbgPrint("startsize:" .. self.width .. "x" .. self.height .. "+" .. self.x .. "+" .. self.y)
249   end
250   self:layout()
251   for i,child in ipairs(self.children) do
252      child:addWidgetTo(container)
253   end
254end
255
256HBox = class(Box,
257             function(instance, children)
258                Box.init(instance, children)
259                instance.direction = Box.DIRECTION_HORIZONTAL
260             end
261)
262
263VBox = class(Box,
264             function(instance, children)
265                Box.init(instance, children)
266                instance.direction = Box.DIRECTION_VERTICAL
267             end
268)
269
270LLabel = class(Element,
271               function(instance, text, font, center, vCenter)
272                  Element.init(instance)
273                  instance.label = Label(text)
274                  instance.label:setFont(font or Fonts["large"])
275                  instance.label:adjustSize()
276                  instance.center = center
277                  instance.vCenter = vCenter
278               end
279)
280
281function LLabel:getWidth()
282   return self.label:getWidth()
283end
284
285function LLabel:getHeight()
286   return self.label:getHeight()
287end
288
289function LLabel:layout()
290   if self.center or center == nil then -- center text by default
291      self.x = self.x + (self.width - self.label:getWidth()) / 2
292   end
293   if self.vCenter then
294      self.y = self.y + (self.height - self.label:getHeight()) / 2
295   end
296end
297
298function LLabel:addWidgetTo(container)
299   self:layout()
300   self:setId(container, self.label)
301   container:add(self.label, self.x, self.y)
302end
303
304LFiller = class(Element)
305
306function LFiller:getWidth()
307   return nil
308end
309
310function LFiller:getHeight()
311   return nil
312end
313
314function LFiller:addWidgetTo(container)
315   -- nothing
316end
317
318LText = class(LLabel,
319              function(instance, text)
320                 LLabel.init(instance, text, Fonts["game"])
321              end
322)
323
324LLargeText = class(LLabel)
325
326LImageButton = class(Element,
327                     function(instance, caption, hotkey, callback)
328                        Element.init(instance)
329                        instance.b = ImageButton(caption)
330                        instance.b:setHotKey(hotkey)
331                        instance.b:setBorderSize(0)
332                        if callback then
333                           instance.b:setMouseCallback(function(evtname)
334                                 if evtname == "mousePress" then
335                                    PlaySound("click")
336                                 end
337                           end)
338                           instance.b:setActionCallback(function()
339                                 PlaySound("click")
340                                 callback()
341                           end)
342                        end
343                     end
344)
345
346function LImageButton:getWidth()
347   return self.b:getWidth()
348end
349
350function LImageButton:getHeight()
351   return self.b:getHeight()
352end
353
354function LImageButton:addWidgetTo(container)
355   self.b:setSize(self.width, self.height)
356   self.setId(container, self.b)
357   container:add(self.b, self.x, self.y)
358end
359
360LButton = class(LImageButton,
361                function(instance, caption, hotkey, callback)
362                   LImageButton.init(instance, caption, hotkey, callback)
363                   if (GetPlayerData(GetThisPlayer(), "RaceName") == "human") then
364                      instance.b:setNormalImage(g_hbln)
365                      instance.b:setPressedImage(g_hblp)
366                      instance.b:setDisabledImage(g_hblg)
367                   else
368                      instance.b:setNormalImage(g_obln)
369                      instance.b:setPressedImage(g_oblp)
370                      instance.b:setDisabledImage(g_oblg)
371                   end
372                end
373)
374
375function LButton:getWidth()
376   return 224
377end
378
379function LButton:getHeight()
380   return 28
381end
382
383function LButton:addWidgetTo(container)
384   self.b:setSize(self.width, self.height)
385   self:setId(container, self.b)
386   container:add(self.b, self.x, self.y)
387end
388
389LHalfButton = class(LButton)
390
391function LHalfButton:getWidth()
392   return 106
393end
394
395function LHalfButton:getHeight()
396   return 28
397end
398
399LSlider = class(Element,
400                function(instance, min, max, callback)
401                   Element.init(instance)
402                   instance.s = Slider(min, max)
403                   instance.s:setBaseColor(dark)
404                   instance.s:setForegroundColor(clear)
405                   instance.s:setBackgroundColor(clear)
406                   if callback then
407                      instance.s:setActionCallback(function(s) callback(instance.s, s) end)
408                   end
409                end
410)
411
412function LSlider:getWidth()
413   return nil
414end
415
416function LSlider:getHeight()
417   return nil
418end
419
420function LSlider:setValue(val)
421   self.s:setValue(val)
422   return self
423end
424
425function LSlider:addWidgetTo(container)
426   self.s:setSize(self.width, self.height)
427   self:setId(container, self.s)
428   container:add(self.s, self.x, self.y)
429end
430
431LListBox = class(Element,
432                 function(instance, w, h, list)
433                    Element.init(instance)
434                    instance.bq = ListBoxWidget(60, 60)
435                    instance.bq:setList(list)
436                    instance.bq:setBaseColor(black)
437                    instance.bq:setForegroundColor(clear)
438                    instance.bq:setBackgroundColor(dark)
439                    instance.bq:setFont(Fonts["game"])
440                    list = list or {}
441                    instance.bq.itemslist = list
442                    instance.width = w
443                    instance.height = h
444                 end
445)
446
447function LListBox:getWidth()
448   return self.width
449end
450
451function LListBox:getHeight()
452   return self.height
453end
454
455function LListBox:addWidgetTo(container)
456   self.bq:setSize(self.width, self.height)
457   self:setId(container, self.bq)
458   container:add(self.bq, self.x, self.y)
459end
460
461LCheckBox = class(Element,
462                  function(instance, caption, callback)
463                     Element.init(instance)
464                     instance.b = CheckBox(caption)
465                     instance.b:setForegroundColor(clear)
466                     instance.b:setBackgroundColor(dark)
467                     if callback then
468                        instance.b:setActionCallback(function(s) callback(instance.b, s) end)
469                     end
470                     instance.b:setFont(Fonts["game"])
471                     instance.b:adjustSize()
472                  end
473)
474
475function LCheckBox:getWidth()
476   return self.b:getWidth()
477end
478
479function LCheckBox:getHeight()
480   return self.b:getHeight()
481end
482
483function LCheckBox:addWidgetTo(container)
484   self.b:setSize(self.width, self.height)
485   self:setId(container, self.b)
486   container:add(self.b, self.x, self.y)
487end
488
489function LCheckBox:setMarked(flag)
490   self.b:setMarked(flag)
491   return self
492end
493
494LTextInputField = class(Element,
495                        function(instance, text, callback)
496                           Element.init(instance)
497                           instance.b = TextField(text)
498                           if callback then
499                              instance.b:setActionCallback(callback)
500                           end
501                           instance.b:setFont(Fonts["game"])
502                           instance.b:setBaseColor(clear)
503                           instance.b:setForegroundColor(clear)
504                           instance.b:setBackgroundColor(dark)
505                        end
506)
507
508function LTextInputField:getWidth()
509   return nil
510end
511
512function LTextInputField:getHeight()
513   return 10
514end
515
516function LTextInputField:addWidgetTo(container)
517   self.b:setSize(self.width, self.height)
518   self:setId(container, self.b)
519   container:add(self.b, self.x, self.y)
520end
521
522LDropDown = class(Element,
523                  function(instance, list, callback)
524                     Element.init(instance)
525                     local dd = DropDownWidget()
526                     dd:setFont(Fonts["game"])
527                     dd:setList(list)
528                     dd.list = list
529                     dd:setActionCallback(function(s) callback(dd, s) end)
530                     dd.callback = callback
531                     dd:setBaseColor(dark)
532                     dd:setForegroundColor(clear)
533                     dd:setBackgroundColor(dark)
534                     instance.dd = dd
535                  end
536)
537
538function LDropDown:getWidth()
539   return 60
540end
541
542function LDropDown:getHeight()
543   return 8
544end
545
546function LDropDown:addWidgetTo(container)
547   self.dd:setSize(self.width, self.height)
548   self:setId(container, self.dd)
549   container:add(self.dd, self.x, self.y)
550end
551
552LTextBox = class(Element,
553                 function(instance, text)
554                    Element.init(instance)
555                    instance.b = TextBox(text)
556                    instance.b:setFont(Fonts["game"])
557                    instance.b:setBaseColor(clear)
558                    instance.b:setForegroundColor(clear)
559                    instance.b:setBackgroundColor(dark)
560                    instance.scroll = ScrollArea()
561                    instance.scroll:setContent(instance.b)
562                    instance.scroll:setBaseColor(clear)
563                    instance.scroll:setForegroundColor(clear)
564                    instance.scroll:setBackgroundColor(dark)
565                 end
566)
567
568function LTextBox:getWidth()
569   return nil
570end
571
572function LTextBox:getHeight()
573   return nil
574end
575
576function LTextBox:addWidgetTo(container)
577   self.scroll:setSize(self.width, self.height)
578   self.b:setSize(self.width, self.height)
579   self:setId(container, self.b)
580   container:add(self.scroll, self.x, self.y)
581end
582