ExtraWidgets.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  1. package giu
  2. import (
  3. "fmt"
  4. "image"
  5. "time"
  6. "github.com/AllenDang/imgui-go"
  7. )
  8. var _ Widget = &HSplitterWidget{}
  9. type HSplitterWidget struct {
  10. id string
  11. width float32
  12. height float32
  13. delta *float32
  14. }
  15. func HSplitter(delta *float32) *HSplitterWidget {
  16. return &HSplitterWidget{
  17. id: GenAutoID("HSplitter"),
  18. width: 0,
  19. height: 0,
  20. delta: delta,
  21. }
  22. }
  23. func (h *HSplitterWidget) Size(width, height float32) *HSplitterWidget {
  24. aw, ah := GetAvailableRegion()
  25. if width == 0 {
  26. h.width = aw
  27. } else {
  28. h.width = width
  29. }
  30. if height == 0 {
  31. h.height = ah
  32. } else {
  33. h.height = height
  34. }
  35. return h
  36. }
  37. func (h *HSplitterWidget) ID(id string) *HSplitterWidget {
  38. h.id = id
  39. return h
  40. }
  41. // Build implements Widget interface
  42. // nolint:dupl // will fix later
  43. func (h *HSplitterWidget) Build() {
  44. // Calc line position.
  45. width := 40
  46. height := 2
  47. pt := GetCursorScreenPos()
  48. centerX := int(h.width / 2)
  49. centerY := int(h.height / 2)
  50. ptMin := image.Pt(centerX-width/2, centerY-height/2)
  51. ptMax := image.Pt(centerX+width/2, centerY+height/2)
  52. style := imgui.CurrentStyle()
  53. c := Vec4ToRGBA(style.GetColor(imgui.StyleColorScrollbarGrab))
  54. // Place a invisible button to capture event.
  55. imgui.InvisibleButton(h.id, imgui.Vec2{X: h.width, Y: h.height})
  56. if imgui.IsItemActive() {
  57. *(h.delta) = imgui.CurrentIO().GetMouseDelta().Y
  58. } else {
  59. *(h.delta) = 0
  60. }
  61. if imgui.IsItemHovered() {
  62. imgui.SetMouseCursor(imgui.MouseCursorResizeNS)
  63. c = Vec4ToRGBA(style.GetColor(imgui.StyleColorScrollbarGrabActive))
  64. }
  65. // Draw a line in the very center
  66. canvas := GetCanvas()
  67. canvas.AddRectFilled(pt.Add(ptMin), pt.Add(ptMax), c, 0, 0)
  68. }
  69. var _ Widget = &VSplitterWidget{}
  70. type VSplitterWidget struct {
  71. id string
  72. width float32
  73. height float32
  74. delta *float32
  75. }
  76. func VSplitter(delta *float32) *VSplitterWidget {
  77. return &VSplitterWidget{
  78. id: GenAutoID("VSplitter"),
  79. width: 0,
  80. height: 0,
  81. delta: delta,
  82. }
  83. }
  84. func (v *VSplitterWidget) Size(width, height float32) *VSplitterWidget {
  85. aw, ah := GetAvailableRegion()
  86. if width == 0 {
  87. v.width = aw
  88. } else {
  89. v.width = width
  90. }
  91. if height == 0 {
  92. v.height = ah
  93. } else {
  94. v.height = height
  95. }
  96. return v
  97. }
  98. func (v *VSplitterWidget) ID(id string) *VSplitterWidget {
  99. v.id = id
  100. return v
  101. }
  102. // Build implements Widget interface
  103. // nolint:dupl // will fix later
  104. func (v *VSplitterWidget) Build() {
  105. // Calc line position.
  106. width := 2
  107. height := 40
  108. pt := GetCursorScreenPos()
  109. centerX := int(v.width / 2)
  110. centerY := int(v.height / 2)
  111. ptMin := image.Pt(centerX-width/2, centerY-height/2)
  112. ptMax := image.Pt(centerX+width/2, centerY+height/2)
  113. style := imgui.CurrentStyle()
  114. c := Vec4ToRGBA(style.GetColor(imgui.StyleColorScrollbarGrab))
  115. // Place a invisible button to capture event.
  116. imgui.InvisibleButton(v.id, imgui.Vec2{X: v.width, Y: v.height})
  117. if imgui.IsItemActive() {
  118. *(v.delta) = imgui.CurrentIO().GetMouseDelta().X
  119. } else {
  120. *(v.delta) = 0
  121. }
  122. if imgui.IsItemHovered() {
  123. imgui.SetMouseCursor(imgui.MouseCursorResizeEW)
  124. c = Vec4ToRGBA(style.GetColor(imgui.StyleColorScrollbarGrabActive))
  125. }
  126. // Draw a line in the very center
  127. canvas := GetCanvas()
  128. canvas.AddRectFilled(pt.Add(ptMin), pt.Add(ptMax), c, 0, 0)
  129. }
  130. type TreeTableRowWidget struct {
  131. label string
  132. flags TreeNodeFlags
  133. layout Layout
  134. children []*TreeTableRowWidget
  135. }
  136. func TreeTableRow(label string, widgets ...Widget) *TreeTableRowWidget {
  137. return &TreeTableRowWidget{
  138. label: GenAutoID(label),
  139. layout: widgets,
  140. }
  141. }
  142. func (ttr *TreeTableRowWidget) Children(rows ...*TreeTableRowWidget) *TreeTableRowWidget {
  143. ttr.children = rows
  144. return ttr
  145. }
  146. func (ttr *TreeTableRowWidget) Flags(flags TreeNodeFlags) *TreeTableRowWidget {
  147. ttr.flags = flags
  148. return ttr
  149. }
  150. // BuildTreeTableRow executes table row building steps.
  151. func (ttr *TreeTableRowWidget) BuildTreeTableRow() {
  152. imgui.TableNextRow(0, 0)
  153. imgui.TableNextColumn()
  154. open := false
  155. if len(ttr.children) > 0 {
  156. open = imgui.TreeNodeV(tStr(ttr.label), int(ttr.flags))
  157. } else {
  158. ttr.flags |= TreeNodeFlagsLeaf | TreeNodeFlagsNoTreePushOnOpen
  159. imgui.TreeNodeV(tStr(ttr.label), int(ttr.flags))
  160. }
  161. for _, w := range ttr.layout {
  162. switch w.(type) {
  163. case *TooltipWidget,
  164. *ContextMenuWidget, *PopupModalWidget:
  165. // noop
  166. default:
  167. imgui.TableNextColumn()
  168. }
  169. w.Build()
  170. }
  171. if len(ttr.children) > 0 && open {
  172. for _, c := range ttr.children {
  173. c.BuildTreeTableRow()
  174. }
  175. imgui.TreePop()
  176. }
  177. }
  178. var _ Widget = &TreeTableWidget{}
  179. type TreeTableWidget struct {
  180. id string
  181. flags TableFlags
  182. size imgui.Vec2
  183. columns []*TableColumnWidget
  184. rows []*TreeTableRowWidget
  185. freezeRow int
  186. freezeColumn int
  187. }
  188. func TreeTable() *TreeTableWidget {
  189. return &TreeTableWidget{
  190. id: GenAutoID("TreeTable"),
  191. flags: TableFlagsBordersV | TableFlagsBordersOuterH | TableFlagsResizable | TableFlagsRowBg | TableFlagsNoBordersInBody,
  192. rows: nil,
  193. columns: nil,
  194. }
  195. }
  196. // Freeze columns/rows so they stay visible when scrolled.
  197. func (tt *TreeTableWidget) Freeze(col, row int) *TreeTableWidget {
  198. tt.freezeColumn = col
  199. tt.freezeRow = row
  200. return tt
  201. }
  202. func (tt *TreeTableWidget) Size(width, height float32) *TreeTableWidget {
  203. tt.size = imgui.Vec2{X: width, Y: height}
  204. return tt
  205. }
  206. func (tt *TreeTableWidget) Flags(flags TableFlags) *TreeTableWidget {
  207. tt.flags = flags
  208. return tt
  209. }
  210. func (tt *TreeTableWidget) Columns(cols ...*TableColumnWidget) *TreeTableWidget {
  211. tt.columns = cols
  212. return tt
  213. }
  214. func (tt *TreeTableWidget) Rows(rows ...*TreeTableRowWidget) *TreeTableWidget {
  215. tt.rows = rows
  216. return tt
  217. }
  218. // Build implements Widget interface.
  219. func (tt *TreeTableWidget) Build() {
  220. if len(tt.rows) == 0 {
  221. return
  222. }
  223. colCount := len(tt.columns)
  224. if colCount == 0 {
  225. colCount = len(tt.rows[0].layout) + 1
  226. }
  227. if imgui.BeginTable(tt.id, colCount, imgui.TableFlags(tt.flags), tt.size, 0) {
  228. if tt.freezeColumn >= 0 && tt.freezeRow >= 0 {
  229. imgui.TableSetupScrollFreeze(tt.freezeColumn, tt.freezeRow)
  230. }
  231. if len(tt.columns) > 0 {
  232. for _, col := range tt.columns {
  233. col.BuildTableColumn()
  234. }
  235. imgui.TableHeadersRow()
  236. }
  237. for _, row := range tt.rows {
  238. row.BuildTreeTableRow()
  239. }
  240. imgui.EndTable()
  241. }
  242. }
  243. var _ Widget = &CustomWidget{}
  244. type CustomWidget struct {
  245. builder func()
  246. }
  247. // Build implements Widget interface.
  248. func (c *CustomWidget) Build() {
  249. if c.builder != nil {
  250. c.builder()
  251. }
  252. }
  253. func Custom(builder func()) *CustomWidget {
  254. return &CustomWidget{
  255. builder: builder,
  256. }
  257. }
  258. var _ Widget = &ConditionWidget{}
  259. type ConditionWidget struct {
  260. cond bool
  261. layoutIf Layout
  262. layoutElse Layout
  263. }
  264. func Condition(cond bool, layoutIf, layoutElse Layout) *ConditionWidget {
  265. return &ConditionWidget{
  266. cond: cond,
  267. layoutIf: layoutIf,
  268. layoutElse: layoutElse,
  269. }
  270. }
  271. // Build implements Widget interface.
  272. func (c *ConditionWidget) Build() {
  273. if c.cond {
  274. if c.layoutIf != nil {
  275. c.layoutIf.Build()
  276. }
  277. } else {
  278. if c.layoutElse != nil {
  279. c.layoutElse.Build()
  280. }
  281. }
  282. }
  283. // RangeBuilder batch create widgets and render only which is visible.
  284. func RangeBuilder(id string, values []any, builder func(int, any) Widget) Layout {
  285. var layout Layout
  286. layout = append(layout, Custom(func() { imgui.PushID(id) }))
  287. if len(values) > 0 && builder != nil {
  288. for i, v := range values {
  289. valueRef := v
  290. widget := builder(i, valueRef)
  291. layout = append(layout, widget)
  292. }
  293. }
  294. layout = append(layout, Custom(func() { imgui.PopID() }))
  295. return layout
  296. }
  297. type ListBoxState struct {
  298. selectedIndex int
  299. }
  300. func (s *ListBoxState) Dispose() {
  301. // Nothing to do here.
  302. }
  303. var _ Widget = &ListBoxWidget{}
  304. type ListBoxWidget struct {
  305. id string
  306. width float32
  307. height float32
  308. border bool
  309. items []string
  310. menus []string
  311. onChange func(selectedIndex int)
  312. onDClick func(selectedIndex int)
  313. onMenu func(selectedIndex int, menu string)
  314. }
  315. func ListBox(id string, items []string) *ListBoxWidget {
  316. return &ListBoxWidget{
  317. id: id,
  318. width: 0,
  319. height: 0,
  320. border: true,
  321. items: items,
  322. menus: nil,
  323. onChange: nil,
  324. onDClick: nil,
  325. onMenu: nil,
  326. }
  327. }
  328. func (l *ListBoxWidget) Size(width, height float32) *ListBoxWidget {
  329. l.width, l.height = width, height
  330. return l
  331. }
  332. func (l *ListBoxWidget) Border(b bool) *ListBoxWidget {
  333. l.border = b
  334. return l
  335. }
  336. func (l *ListBoxWidget) ContextMenu(menuItems []string) *ListBoxWidget {
  337. l.menus = menuItems
  338. return l
  339. }
  340. func (l *ListBoxWidget) OnChange(onChange func(selectedIndex int)) *ListBoxWidget {
  341. l.onChange = onChange
  342. return l
  343. }
  344. func (l *ListBoxWidget) OnDClick(onDClick func(selectedIndex int)) *ListBoxWidget {
  345. l.onDClick = onDClick
  346. return l
  347. }
  348. func (l *ListBoxWidget) OnMenu(onMenu func(selectedIndex int, menu string)) *ListBoxWidget {
  349. l.onMenu = onMenu
  350. return l
  351. }
  352. // Build implements Widget interface
  353. // nolint:gocognit // will fix later
  354. func (l *ListBoxWidget) Build() {
  355. var state *ListBoxState
  356. if s := Context.GetState(l.id); s == nil {
  357. state = &ListBoxState{selectedIndex: 0}
  358. Context.SetState(l.id, state)
  359. } else {
  360. var isOk bool
  361. state, isOk = s.(*ListBoxState)
  362. Assert(isOk, "ListBoxWidget", "Build", "wrong state type recovered")
  363. }
  364. child := Child().Border(l.border).Size(l.width, l.height).Layout(Layout{
  365. Custom(func() {
  366. clipper := imgui.NewListClipper()
  367. defer clipper.Delete()
  368. clipper.Begin(len(l.items))
  369. for clipper.Step() {
  370. for i := clipper.DisplayStart(); i < clipper.DisplayEnd(); i++ {
  371. selected := i == state.selectedIndex
  372. item := l.items[i]
  373. Selectable(item).Selected(selected).Flags(SelectableFlagsAllowDoubleClick).OnClick(func() {
  374. if state.selectedIndex != i {
  375. state.selectedIndex = i
  376. if l.onChange != nil {
  377. l.onChange(i)
  378. }
  379. }
  380. }).Build()
  381. if IsItemHovered() && IsMouseDoubleClicked(MouseButtonLeft) && l.onDClick != nil {
  382. l.onDClick(state.selectedIndex)
  383. }
  384. // Build context menus
  385. var menus Layout
  386. for _, m := range l.menus {
  387. index := i
  388. menu := m
  389. menus = append(menus, MenuItem(fmt.Sprintf("%s##%d", menu, index)).OnClick(func() {
  390. if l.onMenu != nil {
  391. l.onMenu(index, menu)
  392. }
  393. }))
  394. }
  395. if len(menus) > 0 {
  396. ContextMenu().Layout(menus).Build()
  397. }
  398. }
  399. }
  400. clipper.End()
  401. }),
  402. })
  403. child.Build()
  404. }
  405. var _ Widget = &DatePickerWidget{}
  406. type DatePickerWidget struct {
  407. id string
  408. date *time.Time
  409. width float32
  410. onChange func()
  411. format string
  412. startOfWeek time.Weekday
  413. }
  414. func DatePicker(id string, date *time.Time) *DatePickerWidget {
  415. return &DatePickerWidget{
  416. id: GenAutoID(id),
  417. date: date,
  418. width: 100,
  419. startOfWeek: time.Sunday,
  420. onChange: func() {}, // small hack - prevent giu from setting nil cb (skip nil check later)
  421. }
  422. }
  423. func (d *DatePickerWidget) Size(width float32) *DatePickerWidget {
  424. d.width = width
  425. return d
  426. }
  427. func (d *DatePickerWidget) OnChange(onChange func()) *DatePickerWidget {
  428. if onChange != nil {
  429. d.onChange = onChange
  430. }
  431. return d
  432. }
  433. func (d *DatePickerWidget) Format(format string) *DatePickerWidget {
  434. d.format = format
  435. return d
  436. }
  437. func (d *DatePickerWidget) StartOfWeek(weekday time.Weekday) *DatePickerWidget {
  438. d.startOfWeek = weekday
  439. return d
  440. }
  441. func (d *DatePickerWidget) getFormat() string {
  442. if d.format == "" {
  443. return "2006-01-02" // default
  444. }
  445. return d.format
  446. }
  447. func (d *DatePickerWidget) offsetDay(offset int) time.Weekday {
  448. day := (int(d.startOfWeek) + offset) % 7
  449. // offset may be negative, thus day can be negative
  450. day = (day + 7) % 7
  451. return time.Weekday(day)
  452. }
  453. // Build implements Widget interface.
  454. func (d *DatePickerWidget) Build() {
  455. if d.date == nil {
  456. return
  457. }
  458. imgui.PushID(d.id)
  459. defer imgui.PopID()
  460. if d.width > 0 {
  461. PushItemWidth(d.width)
  462. defer PopItemWidth()
  463. }
  464. if imgui.BeginComboV(d.id+"##Combo", d.date.Format(d.getFormat()), imgui.ComboFlagsHeightLargest) {
  465. // --- [Build year widget] ---
  466. imgui.AlignTextToFramePadding()
  467. const yearButtonSize = 25
  468. Row(
  469. Label(tStr(" Year")),
  470. Labelf("%14d", d.date.Year()),
  471. Button("-##"+d.id+"year").OnClick(func() {
  472. *d.date = d.date.AddDate(-1, 0, 0)
  473. d.onChange()
  474. }).Size(yearButtonSize, yearButtonSize),
  475. Button("+##"+d.id+"year").OnClick(func() {
  476. *d.date = d.date.AddDate(1, 0, 0)
  477. d.onChange()
  478. }).Size(yearButtonSize, yearButtonSize),
  479. ).Build()
  480. // --- [Build month widgets] ---
  481. Row(
  482. Label("Month"),
  483. Labelf("%10s(%02d)", d.date.Month().String(), d.date.Month()),
  484. Button("-##"+d.id+"month").OnClick(func() {
  485. *d.date = d.date.AddDate(0, -1, 0)
  486. d.onChange()
  487. }).Size(yearButtonSize, yearButtonSize),
  488. Button("+##"+d.id+"month").OnClick(func() {
  489. *d.date = d.date.AddDate(0, 1, 0)
  490. d.onChange()
  491. }).Size(yearButtonSize, yearButtonSize),
  492. ).Build()
  493. // --- [Build day widgets] ---
  494. days := d.getDaysGroups()
  495. // Create calendar (widget)
  496. columns := make([]*TableColumnWidget, 7)
  497. for i := 0; i < 7; i++ {
  498. firstChar := d.offsetDay(i).String()[0:1]
  499. columns[i] = TableColumn(firstChar)
  500. }
  501. // Build day widgets
  502. var rows []*TableRowWidget
  503. for _, week := range days {
  504. var row []Widget
  505. for _, day := range week {
  506. day := day // hack for golang ranges
  507. if day == 0 {
  508. row = append(row, Label(" "))
  509. continue
  510. }
  511. row = append(row, d.calendarField(day))
  512. }
  513. rows = append(rows, TableRow(row...))
  514. }
  515. Table().Flags(TableFlagsBorders | TableFlagsSizingStretchSame).Columns(columns...).Rows(rows...).Build()
  516. imgui.EndCombo()
  517. }
  518. }
  519. // store month days sorted in weeks.
  520. func (d *DatePickerWidget) getDaysGroups() (days [][]int) {
  521. firstDay := time.Date(d.date.Year(), d.date.Month(), 1, 0, 0, 0, 0, time.Local)
  522. lastDay := firstDay.AddDate(0, 1, 0).Add(time.Nanosecond * -1)
  523. // calculate first week
  524. days = append(days, make([]int, 7))
  525. monthDay := 1
  526. emptyDaysInFirstWeek := (int(firstDay.Weekday()) - int(d.startOfWeek) + 7) % 7
  527. for i := emptyDaysInFirstWeek; i < 7; i++ {
  528. days[0][i] = monthDay
  529. monthDay++
  530. }
  531. // Build rest rows
  532. for ; monthDay <= lastDay.Day(); monthDay++ {
  533. if len(days[len(days)-1]) == 7 {
  534. days = append(days, []int{})
  535. }
  536. days[len(days)-1] = append(days[len(days)-1], monthDay)
  537. }
  538. // Pad last row
  539. lastRowLen := len(days[len(days)-1])
  540. if lastRowLen < 7 {
  541. for i := lastRowLen; i < 7; i++ {
  542. days[len(days)-1] = append(days[len(days)-1], 0)
  543. }
  544. }
  545. return days
  546. }
  547. func (d *DatePickerWidget) calendarField(day int) Widget {
  548. today := time.Now()
  549. highlightColor := imgui.CurrentStyle().GetColor(imgui.StyleColorPlotHistogram)
  550. return Custom(func() {
  551. isToday := d.date.Year() == today.Year() && d.date.Month() == today.Month() && day == today.Day()
  552. if isToday {
  553. imgui.PushStyleColor(imgui.StyleColorText, highlightColor)
  554. }
  555. Selectable(fmt.Sprintf("%02d", day)).Selected(isToday).OnClick(func() {
  556. *d.date = time.Date(
  557. d.date.Year(), d.date.Month(), day,
  558. 0, 0, 0, 0,
  559. d.date.Location())
  560. d.onChange()
  561. }).Build()
  562. if isToday {
  563. imgui.PopStyleColor()
  564. }
  565. })
  566. }