builder.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. // Copyright 2016 The G3N Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. package gui
  5. import (
  6. "fmt"
  7. "io/ioutil"
  8. "os"
  9. "path/filepath"
  10. "sort"
  11. "strconv"
  12. "strings"
  13. "github.com/g3n/engine/gui/assets/icon"
  14. "github.com/g3n/engine/math32"
  15. "gopkg.in/yaml.v2"
  16. )
  17. // Builder builds GUI objects from a declarative description in YAML format
  18. type Builder struct {
  19. desc map[string]*panelDesc // parsed descriptions
  20. imgpath string // base path for image panels files
  21. objpath strStack // stack of object names being built
  22. }
  23. type panelStyle struct {
  24. Borders string
  25. Paddings string
  26. BorderColor string
  27. BgColor string
  28. FgColor string
  29. }
  30. type panelStyles struct {
  31. Normal *panelStyle
  32. Over *panelStyle
  33. Focus *panelStyle
  34. Pressed *panelStyle
  35. Disabled *panelStyle
  36. }
  37. type panelDesc struct {
  38. Type string // Gui object type: Panel, Label, Edit, etc ...
  39. Name string // Optional name for identification
  40. Position string // Optional position as: x y | x,y
  41. Width float32 // Optional width (default = 0)
  42. Height float32 // Optional height (default = 0)
  43. AspectWidth *float32 // Optional aspectwidth (default = nil)
  44. AspectHeight *float32 // Optional aspectwidth (default = nil)
  45. Margins string // Optional margins as 1 or 4 float values
  46. Borders string // Optional borders as 1 or 4 float values
  47. BorderColor string // Optional border color as name or 3 or 4 float values
  48. Paddings string // Optional paddings as 1 or 4 float values
  49. Color string // Optional color as 1 or 4 float values
  50. Enabled bool
  51. Visible bool
  52. Renderable bool
  53. Imagefile string // For Panel, Button
  54. Children []*panelDesc
  55. Layout layoutAttr
  56. Styles *panelStyles
  57. Text string // Label, Button
  58. BgColor string // Label
  59. FontColor string // Label
  60. FontSize *float32 // Label
  61. FontDPI *float32 // Label
  62. LineSpacing *float32 // Label
  63. PlaceHolder string // Edit
  64. MaxLength *uint // Edit
  65. Icon string // Button
  66. Group string // RadioButton
  67. }
  68. type layoutAttr struct {
  69. Type string
  70. }
  71. const (
  72. descTypePanel = "Panel"
  73. descTypeImagePanel = "ImagePanel"
  74. descTypeLabel = "Label"
  75. descTypeIconLabel = "IconLabel"
  76. descTypeButton = "Button"
  77. descTypeCheckBox = "CheckBox"
  78. descTypeRadioButton = "RadioButton"
  79. descTypeEdit = "Edit"
  80. descTypeVList = "VList"
  81. descTypeHList = "HList"
  82. fieldMargins = "margins"
  83. fieldBorders = "borders"
  84. fieldBorderColor = "bordercolor"
  85. fieldPaddings = "paddings"
  86. fieldColor = "color"
  87. fieldBgColor = "bgcolor"
  88. )
  89. // NewBuilder creates and returns a pointer to a new gui Builder object
  90. func NewBuilder() *Builder {
  91. return new(Builder)
  92. }
  93. // ParseString parses a string with gui objects descriptions in YAML format
  94. // It there was a previously parsed description, it is cleared.
  95. func (b *Builder) ParseString(desc string) error {
  96. // Try assuming the description contains a single root panel
  97. var pd panelDesc
  98. err := yaml.Unmarshal([]byte(desc), &pd)
  99. if err != nil {
  100. return err
  101. }
  102. if pd.Type != "" {
  103. b.desc = make(map[string]*panelDesc)
  104. b.desc[""] = &pd
  105. return nil
  106. }
  107. // Try assuming the description is a map of panels
  108. var pdm map[string]*panelDesc
  109. err = yaml.Unmarshal([]byte(desc), &pdm)
  110. if err != nil {
  111. return err
  112. }
  113. b.desc = pdm
  114. return nil
  115. }
  116. // ParseFile builds gui objects from the specified file which
  117. // must contain objects descriptions in YAML format
  118. func (b *Builder) ParseFile(filepath string) error {
  119. // Reads all file data
  120. f, err := os.Open(filepath)
  121. if err != nil {
  122. return err
  123. }
  124. data, err := ioutil.ReadAll(f)
  125. if err != nil {
  126. return err
  127. }
  128. err = f.Close()
  129. if err != nil {
  130. return err
  131. }
  132. // Parses file data
  133. return b.ParseString(string(data))
  134. }
  135. // Names returns a sorted list of names of top level previously parsed objects.
  136. // Only objects with defined types are returned.
  137. // If there is only a single object with no name, its name is returned
  138. // as an empty string
  139. func (b *Builder) Names() []string {
  140. var objs []string
  141. for name, pd := range b.desc {
  142. if pd.Type != "" {
  143. objs = append(objs, name)
  144. }
  145. }
  146. sort.Strings(objs)
  147. return objs
  148. }
  149. // Build builds a gui object and all its children recursively.
  150. // The specified name should be a top level name from a
  151. // from a previously parsed description
  152. // If the descriptions contains a single object with no name,
  153. // It should be specified the empty string to build this object.
  154. func (b *Builder) Build(name string) (IPanel, error) {
  155. pd, ok := b.desc[name]
  156. if !ok {
  157. return nil, fmt.Errorf("Object name:%s not found", name)
  158. }
  159. b.objpath.clear()
  160. b.objpath.push(pd.Name)
  161. return b.build(pd, nil)
  162. }
  163. // Sets the path for image panels relative image files
  164. func (b *Builder) SetImagepath(path string) {
  165. b.imgpath = path
  166. }
  167. // build builds the gui object from the specified description.
  168. // All its children are also built recursively
  169. // Returns the built object or an error
  170. func (b *Builder) build(pd *panelDesc, iparent IPanel) (IPanel, error) {
  171. var err error
  172. var pan IPanel
  173. switch pd.Type {
  174. case descTypePanel:
  175. pan, err = b.buildPanel(pd)
  176. case descTypeImagePanel:
  177. pan, err = b.buildImagePanel(pd)
  178. case descTypeLabel:
  179. pan, err = b.buildLabel(pd)
  180. case descTypeIconLabel:
  181. pan, err = b.buildLabel(pd)
  182. case descTypeButton:
  183. pan, err = b.buildButton(pd)
  184. case descTypeCheckBox:
  185. pan, err = b.buildCheckBox(pd)
  186. case descTypeRadioButton:
  187. pan, err = b.buildRadioButton(pd)
  188. case descTypeEdit:
  189. pan, err = b.buildEdit(pd)
  190. case descTypeVList:
  191. pan, err = b.buildVList(pd)
  192. case descTypeHList:
  193. pan, err = b.buildHList(pd)
  194. default:
  195. err = fmt.Errorf("Invalid panel type:%s", pd.Type)
  196. }
  197. if err != nil {
  198. return nil, err
  199. }
  200. if iparent != nil {
  201. iparent.GetPanel().Add(pan)
  202. }
  203. return pan, nil
  204. }
  205. // buildPanel builds a gui object of type: "Panel"
  206. func (b *Builder) buildPanel(pd *panelDesc) (IPanel, error) {
  207. // Builds panel and set common attributes
  208. pan := NewPanel(pd.Width, pd.Height)
  209. err := b.setCommon(pd, pan)
  210. if err != nil {
  211. return nil, err
  212. }
  213. // Builds panel children recursively
  214. for i := 0; i < len(pd.Children); i++ {
  215. b.objpath.push(pd.Children[i].Name)
  216. child, err := b.build(pd.Children[i], pan)
  217. b.objpath.pop()
  218. if err != nil {
  219. return nil, err
  220. }
  221. pan.Add(child)
  222. }
  223. return pan, nil
  224. }
  225. // buildImagePanel builds a gui object of type: "ImagePanel"
  226. func (b *Builder) buildImagePanel(pd *panelDesc) (IPanel, error) {
  227. // Imagefile must be supplied
  228. if pd.Imagefile == "" {
  229. return nil, b.err("Imagefile", "Imagefile must be supplied")
  230. }
  231. // If path is not absolute join with user supplied image base path
  232. path := pd.Imagefile
  233. if !filepath.IsAbs(path) {
  234. path = filepath.Join(b.imgpath, path)
  235. }
  236. // Builds panel and set common attributes
  237. panel, err := NewImage(path)
  238. if err != nil {
  239. return nil, err
  240. }
  241. err = b.setCommon(pd, panel)
  242. if err != nil {
  243. return nil, err
  244. }
  245. // AspectWidth and AspectHeight attributes
  246. if pd.AspectWidth != nil {
  247. panel.SetContentAspectWidth(*pd.AspectWidth)
  248. }
  249. if pd.AspectHeight != nil {
  250. panel.SetContentAspectHeight(*pd.AspectHeight)
  251. }
  252. // Builds panel children recursively
  253. for i := 0; i < len(pd.Children); i++ {
  254. b.objpath.push(pd.Children[i].Name)
  255. child, err := b.build(pd.Children[i], panel)
  256. b.objpath.pop()
  257. if err != nil {
  258. return nil, err
  259. }
  260. panel.Add(child)
  261. }
  262. return panel, nil
  263. }
  264. // buildLabel builds a gui object of type: "Label"
  265. func (b *Builder) buildLabel(pd *panelDesc) (IPanel, error) {
  266. var label *Label
  267. if pd.Type == descTypeLabel {
  268. label = NewLabel(pd.Text)
  269. } else {
  270. icons, err := b.parseIconNames("text", pd.Text)
  271. if err != nil {
  272. return nil, err
  273. }
  274. label = NewIconLabel(icons)
  275. }
  276. err := b.setCommon(pd, label)
  277. if err != nil {
  278. return nil, err
  279. }
  280. // Set optional background color
  281. c, err := b.parseColor(fieldBgColor, pd.BgColor)
  282. if err != nil {
  283. return nil, err
  284. }
  285. if c != nil {
  286. label.SetBgColor4(c)
  287. }
  288. // Set optional font color
  289. c, err = b.parseColor("fontcolor", pd.FontColor)
  290. if err != nil {
  291. return nil, err
  292. }
  293. if c != nil {
  294. label.SetColor4(c)
  295. }
  296. // Sets optional font size
  297. if pd.FontSize != nil {
  298. label.SetFontSize(float64(*pd.FontSize))
  299. }
  300. // Sets optional font dpi
  301. if pd.FontDPI != nil {
  302. label.SetFontDPI(float64(*pd.FontDPI))
  303. }
  304. // Sets optional line spacing
  305. if pd.LineSpacing != nil {
  306. label.SetLineSpacing(float64(*pd.LineSpacing))
  307. }
  308. return label, nil
  309. }
  310. // buildButton builds a gui object of type: Button
  311. func (b *Builder) buildButton(pd *panelDesc) (IPanel, error) {
  312. // Builds button and set commont attributes
  313. button := NewButton(pd.Text)
  314. err := b.setCommon(pd, button)
  315. if err != nil {
  316. return nil, err
  317. }
  318. // Sets optional icon
  319. if pd.Icon != "" {
  320. cp, err := b.parseIconName("icon", pd.Icon)
  321. if err != nil {
  322. return nil, err
  323. }
  324. button.SetIcon(int(cp))
  325. }
  326. // Sets optional image from file
  327. // If path is not absolute join with user supplied image base path
  328. if pd.Imagefile != "" {
  329. path := pd.Imagefile
  330. if !filepath.IsAbs(path) {
  331. path = filepath.Join(b.imgpath, path)
  332. }
  333. err := button.SetImage(path)
  334. if err != nil {
  335. return nil, err
  336. }
  337. }
  338. return button, nil
  339. }
  340. // buildCheckBox builds a gui object of type: CheckBox
  341. func (b *Builder) buildCheckBox(pd *panelDesc) (IPanel, error) {
  342. // Builds check box and set commont attributes
  343. cb := NewCheckBox(pd.Text)
  344. err := b.setCommon(pd, cb)
  345. if err != nil {
  346. return nil, err
  347. }
  348. return cb, nil
  349. }
  350. // buildRadioButton builds a gui object of type: RadioButton
  351. func (b *Builder) buildRadioButton(pd *panelDesc) (IPanel, error) {
  352. // Builds check box and set commont attributes
  353. rb := NewRadioButton(pd.Text)
  354. err := b.setCommon(pd, rb)
  355. if err != nil {
  356. return nil, err
  357. }
  358. // Sets optional radio button group
  359. if pd.Group != "" {
  360. rb.SetGroup(pd.Group)
  361. }
  362. return rb, nil
  363. }
  364. // buildEdit builds a gui object of type: "Edit"
  365. func (b *Builder) buildEdit(pd *panelDesc) (IPanel, error) {
  366. // Builds button and set commont attributes
  367. edit := NewEdit(int(pd.Width), pd.PlaceHolder)
  368. err := b.setCommon(pd, edit)
  369. if err != nil {
  370. return nil, err
  371. }
  372. edit.SetText(pd.Text)
  373. return edit, nil
  374. }
  375. // buildVList builds a gui object of type: VList
  376. func (b *Builder) buildVList(pd *panelDesc) (IPanel, error) {
  377. // Builds list and set commont attributes
  378. list := NewVList(pd.Width, pd.Height)
  379. err := b.setCommon(pd, list)
  380. if err != nil {
  381. return nil, err
  382. }
  383. // Builds list children
  384. for i := 0; i < len(pd.Children); i++ {
  385. b.objpath.push(pd.Children[i].Name)
  386. child, err := b.build(pd.Children[i], list)
  387. b.objpath.pop()
  388. if err != nil {
  389. return nil, err
  390. }
  391. list.Add(child)
  392. }
  393. return list, nil
  394. }
  395. // buildHList builds a gui object of type: VList
  396. func (b *Builder) buildHList(pd *panelDesc) (IPanel, error) {
  397. // Builds list and set commont attributes
  398. list := NewHList(pd.Width, pd.Height)
  399. err := b.setCommon(pd, list)
  400. if err != nil {
  401. return nil, err
  402. }
  403. // Builds list children
  404. for i := 0; i < len(pd.Children); i++ {
  405. b.objpath.push(pd.Children[i].Name)
  406. child, err := b.build(pd.Children[i], list)
  407. b.objpath.pop()
  408. if err != nil {
  409. return nil, err
  410. }
  411. list.Add(child)
  412. }
  413. return list, nil
  414. }
  415. // setCommon sets the common attributes in the description to the specified panel
  416. func (b *Builder) setCommon(pd *panelDesc, ipan IPanel) error {
  417. // Set optional position
  418. panel := ipan.GetPanel()
  419. if pd.Position != "" {
  420. va, err := b.parseFloats("position", pd.Position, 2, 2)
  421. if va == nil || err != nil {
  422. return err
  423. }
  424. panel.SetPosition(va[0], va[1])
  425. }
  426. // Set optional margin sizes
  427. bs, err := b.parseBorderSizes(fieldMargins, pd.Margins)
  428. if err != nil {
  429. return err
  430. }
  431. if bs != nil {
  432. panel.SetMarginsFrom(bs)
  433. }
  434. // Set optional border sizes
  435. bs, err = b.parseBorderSizes(fieldBorders, pd.Borders)
  436. if err != nil {
  437. return err
  438. }
  439. if bs != nil {
  440. panel.SetBordersFrom(bs)
  441. }
  442. // Set optional border color
  443. c, err := b.parseColor(fieldBorderColor, pd.BorderColor)
  444. if err != nil {
  445. return err
  446. }
  447. if c != nil {
  448. panel.SetBordersColor4(c)
  449. }
  450. // Set optional paddings sizes
  451. bs, err = b.parseBorderSizes(fieldPaddings, pd.Paddings)
  452. if err != nil {
  453. return err
  454. }
  455. if bs != nil {
  456. panel.SetPaddingsFrom(bs)
  457. }
  458. // Set optional color
  459. c, err = b.parseColor(fieldColor, pd.Color)
  460. if err != nil {
  461. return err
  462. }
  463. if c != nil {
  464. panel.SetColor4(c)
  465. }
  466. return nil
  467. }
  468. // parseBorderSizes parses a string field which can contain one float value or
  469. // float values. In the first case all borders has the same width
  470. func (b *Builder) parseBorderSizes(fname, field string) (*BorderSizes, error) {
  471. va, err := b.parseFloats(fname, field, 1, 4)
  472. if va == nil || err != nil {
  473. return nil, err
  474. }
  475. if len(va) == 1 {
  476. return &BorderSizes{va[0], va[0], va[0], va[0]}, nil
  477. }
  478. return &BorderSizes{va[0], va[1], va[2], va[3]}, nil
  479. }
  480. // parseColor parses a string field which can contain a color name or
  481. // a list of 3 or 4 float values for the color components
  482. func (b *Builder) parseColor(fname, field string) (*math32.Color4, error) {
  483. // Checks if field is empty
  484. field = strings.Trim(field, " ")
  485. if field == "" {
  486. return nil, nil
  487. }
  488. // If string has 1 or 2 fields it must be a color name and optional alpha
  489. parts := strings.Fields(field)
  490. if len(parts) == 1 || len(parts) == 2 {
  491. // First part must be a color name
  492. if !math32.IsColor(parts[0]) {
  493. return nil, b.err(fname, fmt.Sprintf("Invalid color name:%s", parts[0]))
  494. }
  495. c := math32.ColorName(parts[0])
  496. c4 := math32.Color4{c.R, c.G, c.B, 1}
  497. if len(parts) == 2 {
  498. val, err := strconv.ParseFloat(parts[1], 32)
  499. if err != nil {
  500. return nil, b.err(fname, fmt.Sprintf("Invalid float32 value:%s", parts[1]))
  501. }
  502. c4.A = float32(val)
  503. }
  504. return &c4, nil
  505. }
  506. // Accept 3 or 4 floats values
  507. va, err := b.parseFloats(fname, field, 3, 4)
  508. if err != nil {
  509. return nil, err
  510. }
  511. if len(va) == 3 {
  512. return &math32.Color4{va[0], va[1], va[2], 1}, nil
  513. }
  514. return &math32.Color4{va[0], va[1], va[2], va[3]}, nil
  515. }
  516. // parseIconNames parses a string with a list of icon names or codepoints and
  517. // returns a string with the icons codepoints encoded in UTF8
  518. func (b *Builder) parseIconNames(fname, field string) (string, error) {
  519. text := ""
  520. parts := strings.Fields(field)
  521. for i := 0; i < len(parts); i++ {
  522. cp, err := b.parseIconName(fname, parts[i])
  523. if err != nil {
  524. return "", err
  525. }
  526. text = text + string(cp)
  527. }
  528. return text, nil
  529. }
  530. // parseIconName parses a string with an icon name or codepoint in hex
  531. // and returns the icon codepoints value and an error
  532. func (b *Builder) parseIconName(fname, field string) (uint, error) {
  533. // Try name first
  534. cp := icon.Codepoint(field)
  535. if cp != 0 {
  536. return cp, nil
  537. }
  538. // Try to parse as hex value
  539. cp2, err := strconv.ParseUint(field, 16, 32)
  540. if err != nil {
  541. return 0, b.err(fname, fmt.Sprintf("Invalid icon codepoint value/name:%v", field))
  542. }
  543. return uint(cp2), nil
  544. }
  545. // parseFloats parses a string with a list of floats with the specified size
  546. // and returns a slice. The specified size is 0 any number of floats is allowed.
  547. // The individual values can be separated by spaces or commas
  548. func (b *Builder) parseFloats(fname, field string, min, max int) ([]float32, error) {
  549. // Checks if field is empty
  550. field = strings.Trim(field, " ")
  551. if field == "" {
  552. return nil, nil
  553. }
  554. // Separate individual fields
  555. var parts []string
  556. if strings.Index(field, ",") < 0 {
  557. parts = strings.Fields(field)
  558. } else {
  559. parts = strings.Split(field, ",")
  560. }
  561. if len(parts) < min || len(parts) > max {
  562. return nil, b.err(fname, "Invalid number of float32 values")
  563. }
  564. // Parse each field value and appends to slice
  565. var values []float32
  566. for i := 0; i < len(parts); i++ {
  567. val, err := strconv.ParseFloat(strings.Trim(parts[i], " "), 32)
  568. if err != nil {
  569. return nil, b.err(fname, err.Error())
  570. }
  571. values = append(values, float32(val))
  572. }
  573. return values, nil
  574. }
  575. // err creates and returns an error for the current object, field name and with the specified message
  576. func (b *Builder) err(fname, msg string) error {
  577. return fmt.Errorf("Error in object:%s field:%s -> %s", b.objpath.path(), fname, msg)
  578. }
  579. // strStack is a stack of strings
  580. type strStack struct {
  581. stack []string
  582. }
  583. // clear removes all elements from the stack
  584. func (ss *strStack) clear() {
  585. ss.stack = []string{}
  586. }
  587. // push pushes a string to the top of the stack
  588. func (ss *strStack) push(v string) {
  589. ss.stack = append(ss.stack, v)
  590. }
  591. // pop removes and returns the string at the top of the stack.
  592. // Returns an empty string if the stack is empty
  593. func (ss *strStack) pop() string {
  594. if len(ss.stack) == 0 {
  595. return ""
  596. }
  597. length := len(ss.stack)
  598. v := ss.stack[length-1]
  599. ss.stack = ss.stack[:length-1]
  600. return v
  601. }
  602. // path returns a string composed of all the strings in the
  603. // stack separated by a forward slash.
  604. func (ss *strStack) path() string {
  605. return strings.Join(ss.stack, "/")
  606. }