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