github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/framework/framework.go (about) 1 package framework 2 3 import ( 4 "errors" 5 "image" 6 "image/color" 7 "math" 8 "strconv" 9 "sync/atomic" 10 11 "github.com/gop9/olt/framework/command" 12 "github.com/gop9/olt/framework/font" 13 "github.com/gop9/olt/framework/label" 14 "github.com/gop9/olt/framework/rect" 15 nstyle "github.com/gop9/olt/framework/style" 16 gkey "github.com/gop9/olt/gio/io/key" 17 "github.com/gop9/olt/gio/io/pointer" 18 ) 19 20 /////////////////////////////////////////////////////////////////////////////////// 21 // CONTEXT & PANELS 22 /////////////////////////////////////////////////////////////////////////////////// 23 24 type UpdateFn func(*Window) 25 26 type Window struct { 27 LastWidgetBounds rect.Rect 28 Data interface{} 29 title string 30 ctx *context 31 idx int 32 flags WindowFlags 33 Bounds rect.Rect 34 Scrollbar image.Point 35 cmds command.Buffer 36 widgets widgetBuffer 37 layout *panel 38 close, first bool 39 moving bool 40 scaling bool 41 // trigger rectangle of nonblocking windows 42 header rect.Rect 43 // root of the node tree 44 rootNode *treeNode 45 // current tree node see TreePush/TreePop 46 curNode *treeNode 47 // parent window of a popup 48 parent *Window 49 // helper windows to implement groups 50 groupWnd map[string]*Window 51 // editor of the active property widget (see PropertyInt, PropertyFloat) 52 editor *TextEditor 53 // update function 54 updateFn UpdateFn 55 usingSub bool 56 began bool 57 rowCtor rowConstructor 58 menuItemWidth int 59 lastLayoutCnt int 60 adjust map[int]map[int]*adjustCol 61 undockedSz image.Point 62 } 63 64 type FittingWidthFn func(width int) 65 66 type adjustCol struct { 67 id int 68 font font.Face 69 width int 70 first bool 71 } 72 73 type treeNode struct { 74 Open bool 75 Children map[string]*treeNode 76 Parent *treeNode 77 } 78 79 type panel struct { 80 Cnt int 81 Flags WindowFlags 82 Bounds rect.Rect 83 Offset *image.Point 84 AtX int 85 AtY int 86 MaxX int 87 Width int 88 Height int 89 FooterH int 90 HeaderH int 91 Border int 92 Clip rect.Rect 93 Menu menuState 94 Row rowLayout 95 ReservedHeight int 96 } 97 98 type menuState struct { 99 X int 100 Y int 101 W int 102 H int 103 Offset image.Point 104 } 105 106 type rowLayout struct { 107 Type int 108 Index int 109 Index2 int 110 CalcMaxWidth bool 111 Height int 112 Columns int 113 Ratio []float64 114 WidthArr []int 115 ItemWidth int 116 ItemRatio float64 117 ItemHeight int 118 ItemOffset int 119 Filled float64 120 Item rect.Rect 121 TreeDepth int 122 123 DynamicFreeX, DynamicFreeY, DynamicFreeW, DynamicFreeH float64 124 } 125 126 type WindowFlags int 127 128 const ( 129 WindowBorder WindowFlags = 1 << iota 130 WindowBorderHeader 131 WindowMovable 132 WindowScalable 133 WindowClosable 134 WindowDynamic 135 WindowNoScrollbar 136 WindowNoHScrollbar 137 WindowTitle 138 WindowContextualReplace 139 WindowNonmodal 140 141 windowSub 142 windowGroup 143 windowPopup 144 windowNonblock 145 windowContextual 146 windowCombo 147 windowMenu 148 windowTooltip 149 windowEnabled 150 windowHDynamic 151 windowDocked 152 153 WindowDefaultFlags = WindowBorder | WindowMovable | WindowScalable | WindowClosable | WindowTitle 154 ) 155 156 func createTreeNode(initialState bool, parent *treeNode) *treeNode { 157 return &treeNode{initialState, map[string]*treeNode{}, parent} 158 } 159 160 func createWindow(ctx *context, title string) *Window { 161 rootNode := createTreeNode(false, nil) 162 r := &Window{ctx: ctx, title: title, rootNode: rootNode, curNode: rootNode, groupWnd: map[string]*Window{}, first: true} 163 r.rowCtor.win = r 164 r.widgets.cur = make(map[rect.Rect]frozenWidget) 165 return r 166 } 167 168 type frozenWidget struct { 169 ws nstyle.WidgetStates 170 frameCount int 171 } 172 173 type widgetBuffer struct { 174 cur map[rect.Rect]frozenWidget 175 frameCount int 176 } 177 178 func (wbuf *widgetBuffer) PrevState(bounds rect.Rect) nstyle.WidgetStates { 179 return wbuf.cur[bounds].ws 180 } 181 182 func (wbuf *widgetBuffer) Add(ws nstyle.WidgetStates, bounds rect.Rect) { 183 wbuf.cur[bounds] = frozenWidget{ws, wbuf.frameCount} 184 } 185 186 func (wbuf *widgetBuffer) reset() { 187 for k, v := range wbuf.cur { 188 if v.frameCount != wbuf.frameCount { 189 delete(wbuf.cur, k) 190 } 191 } 192 wbuf.frameCount++ 193 } 194 195 func (w *Window) Master() MasterWindow { 196 return w.ctx.mw 197 } 198 199 func (win *Window) style() *nstyle.Window { 200 switch { 201 case win.flags&windowCombo != 0: 202 return &win.ctx.Style.ComboWindow 203 case win.flags&windowContextual != 0: 204 return &win.ctx.Style.ContextualWindow 205 case win.flags&windowMenu != 0: 206 return &win.ctx.Style.MenuWindow 207 case win.flags&windowGroup != 0: 208 return &win.ctx.Style.GroupWindow 209 case win.flags&windowTooltip != 0: 210 return &win.ctx.Style.TooltipWindow 211 default: 212 return &win.ctx.Style.NormalWindow 213 } 214 } 215 216 func (win *Window) WindowStyle() *nstyle.Window { 217 return win.style() 218 } 219 220 func panelBegin(ctx *context, win *Window, title string) { 221 in := &ctx.Input 222 style := &ctx.Style 223 font := style.Font 224 wstyle := win.style() 225 226 // window dragging 227 if win.moving { 228 if in == nil || !in.Mouse.Down(pointer.ButtonLeft) { 229 if win.flags&windowDocked == 0 && in != nil { 230 win.ctx.DockedWindows.Dock(win, in.Mouse.Pos, win.ctx.Windows[0].Bounds, win.ctx.Style.Scaling) 231 } 232 win.moving = false 233 } else { 234 win.move(in.Mouse.Delta, in.Mouse.Pos) 235 } 236 } 237 238 win.usingSub = false 239 in.Mouse.clip = nk_null_rect 240 layout := win.layout 241 242 window_padding := wstyle.Padding 243 item_spacing := wstyle.Spacing 244 scaler_size := wstyle.ScalerSize 245 246 *layout = panel{} 247 248 /* panel space with border */ 249 if win.flags&WindowBorder != 0 { 250 layout.Bounds = shrinkRect(win.Bounds, wstyle.Border) 251 } else { 252 layout.Bounds = win.Bounds 253 } 254 255 /* setup panel */ 256 layout.Border = layout.Bounds.X - win.Bounds.X 257 258 layout.AtX = layout.Bounds.X 259 layout.AtY = layout.Bounds.Y 260 layout.Width = layout.Bounds.W 261 layout.Height = layout.Bounds.H 262 layout.MaxX = 0 263 layout.Row.Index = 0 264 layout.Row.Index2 = 0 265 layout.Row.CalcMaxWidth = false 266 layout.Row.Columns = 0 267 layout.Row.Height = 0 268 layout.Row.Ratio = nil 269 layout.Row.ItemWidth = 0 270 layout.Row.ItemRatio = 0 271 layout.Row.TreeDepth = 0 272 layout.Flags = win.flags 273 layout.ReservedHeight = 0 274 win.lastLayoutCnt = 0 275 276 for _, cols := range win.adjust { 277 for _, col := range cols { 278 col.first = false 279 } 280 } 281 282 /* calculate window header */ 283 if win.flags&windowMenu != 0 || win.flags&windowContextual != 0 { 284 layout.HeaderH = window_padding.Y 285 layout.Row.Height = window_padding.Y 286 } else { 287 layout.HeaderH = window_padding.Y 288 layout.Row.Height = window_padding.Y 289 } 290 291 /* calculate window footer height */ 292 if win.flags&windowNonblock == 0 && (((win.flags&WindowNoScrollbar == 0) && (win.flags&WindowNoHScrollbar == 0)) || (win.flags&WindowScalable != 0)) { 293 layout.FooterH = scaler_size.Y + wstyle.FooterPadding.Y 294 } else { 295 layout.FooterH = 0 296 } 297 298 /* calculate the window size */ 299 if win.flags&WindowNoScrollbar == 0 { 300 layout.Width = layout.Bounds.W - wstyle.ScrollbarSize.X 301 } 302 layout.Height = layout.Bounds.H - (layout.HeaderH + window_padding.Y) 303 layout.Height -= layout.FooterH 304 305 /* window header */ 306 307 var dwh drawableWindowHeader 308 dwh.Dynamic = layout.Flags&WindowDynamic != 0 309 dwh.Bounds = layout.Bounds 310 dwh.HeaderActive = (win.idx != 0) && (win.flags&WindowTitle != 0) 311 dwh.LayoutWidth = layout.Width 312 dwh.Style = win.style() 313 314 var closeButton rect.Rect 315 316 if dwh.HeaderActive { 317 /* calculate header bounds */ 318 dwh.Header.X = layout.Bounds.X 319 dwh.Header.Y = layout.Bounds.Y 320 dwh.Header.W = layout.Bounds.W 321 322 /* calculate correct header height */ 323 layout.HeaderH = FontHeight(font) + 2.0*wstyle.Header.Padding.Y 324 325 layout.HeaderH += 2.0 * wstyle.Header.LabelPadding.Y 326 layout.Row.Height += layout.HeaderH 327 dwh.Header.H = layout.HeaderH 328 329 /* update window height */ 330 layout.Height = layout.Bounds.H - (dwh.Header.H + 2*item_spacing.Y) 331 332 layout.Height -= layout.FooterH 333 334 dwh.Hovered = ctx.Input.Mouse.HoveringRect(dwh.Header) 335 dwh.Focused = win.toplevel() 336 337 /* window header title */ 338 t := FontWidth(font, title) 339 340 dwh.Label.X = dwh.Header.X + wstyle.Header.Padding.X 341 dwh.Label.X += wstyle.Header.LabelPadding.X 342 dwh.Label.Y = dwh.Header.Y + wstyle.Header.LabelPadding.Y 343 dwh.Label.H = FontHeight(font) + 2*wstyle.Header.LabelPadding.Y 344 dwh.Label.W = t + 2*wstyle.Header.Spacing.X 345 dwh.LayoutHeaderH = layout.HeaderH 346 dwh.RowHeight = layout.Row.Height 347 dwh.Title = title 348 349 win.widgets.Add(nstyle.WidgetStateInactive, layout.Bounds) 350 dwh.Draw(&win.ctx.Style, &win.cmds) 351 352 // window close button 353 closeButton.Y = dwh.Header.Y + wstyle.Header.Padding.Y 354 closeButton.H = layout.HeaderH - 2*wstyle.Header.Padding.Y 355 closeButton.W = closeButton.H 356 if win.flags&WindowClosable != 0 { 357 if wstyle.Header.Align == nstyle.HeaderRight { 358 closeButton.X = (dwh.Header.W + dwh.Header.X) - (closeButton.W + wstyle.Header.Padding.X) 359 dwh.Header.W -= closeButton.W + wstyle.Header.Spacing.X + wstyle.Header.Padding.X 360 } else { 361 closeButton.X = dwh.Header.X + wstyle.Header.Padding.X 362 dwh.Header.X += closeButton.W + wstyle.Header.Spacing.X + wstyle.Header.Padding.X 363 } 364 365 if doButton(win, label.S(wstyle.Header.CloseSymbol), closeButton, &wstyle.Header.CloseButton, in, false) { 366 win.close = true 367 } 368 } 369 } else { 370 dwh.LayoutHeaderH = layout.HeaderH 371 dwh.RowHeight = layout.Row.Height 372 win.widgets.Add(nstyle.WidgetStateInactive, layout.Bounds) 373 dwh.Draw(&win.ctx.Style, &win.cmds) 374 } 375 376 if (win.flags&WindowMovable != 0) && win.toplevel() { 377 var move rect.Rect 378 move.X = win.Bounds.X 379 move.Y = win.Bounds.Y 380 move.W = win.Bounds.W 381 move.H = FontHeight(font) + 2.0*wstyle.Header.Padding.Y + 2.0*wstyle.Header.LabelPadding.Y 382 383 if in.Mouse.IsClickDownInRect(pointer.ButtonLeft, move, true) && !in.Mouse.IsClickDownInRect(pointer.ButtonLeft, closeButton, true) { 384 win.moving = true 385 } 386 } 387 388 var dwb drawableWindowBody 389 390 dwb.NoScrollbar = win.flags&WindowNoScrollbar != 0 391 dwb.Style = win.style() 392 393 /* calculate and set the window clipping rectangle*/ 394 if win.flags&WindowDynamic == 0 { 395 layout.Clip.X = layout.Bounds.X + window_padding.X 396 layout.Clip.W = layout.Width - 2*window_padding.X 397 } else { 398 layout.Clip.X = layout.Bounds.X 399 layout.Clip.W = layout.Width 400 } 401 402 layout.Clip.H = layout.Bounds.H - (layout.FooterH + layout.HeaderH) 403 // FooterH already includes the window padding 404 layout.Clip.H -= window_padding.Y 405 layout.Clip.Y = layout.Bounds.Y 406 407 /* combo box and menu do not have header space */ 408 if win.flags&windowCombo == 0 && win.flags&windowMenu == 0 { 409 layout.Clip.Y += layout.HeaderH 410 } 411 412 clip := unify(win.cmds.Clip, layout.Clip) 413 layout.Clip = clip 414 415 dwb.Bounds = layout.Bounds 416 dwb.LayoutWidth = layout.Width 417 dwb.Clip = layout.Clip 418 win.cmds.Clip = dwb.Clip 419 win.widgets.Add(nstyle.WidgetStateInactive, dwb.Bounds) 420 dwb.Draw(&win.ctx.Style, &win.cmds) 421 422 layout.Row.Type = layoutInvalid 423 } 424 425 func (win *Window) specialPanelBegin() { 426 win.began = true 427 w := win.Master() 428 ctx := w.context() 429 if win.flags&windowContextual != 0 { 430 prevbody := win.Bounds 431 prevbody.H = win.layout.Height 432 // if the contextual menu ended up with its bottom right corner outside 433 // the main window's bounds and it could be moved to be inside the main 434 // window by popping it a different way do it. 435 // Since the size of the contextual menu is only knowable after displaying 436 // it once this must be done on the second frame. 437 max := ctx.Windows[0].Bounds.Max() 438 if (win.header.H <= 0 || win.header.W <= 0 || win.header.Contains(prevbody.Min())) && ((prevbody.Max().X > max.X) || (prevbody.Max().Y > max.Y)) && (win.Bounds.X-prevbody.W >= 0) && (win.Bounds.Y-prevbody.H >= 0) { 439 win.Bounds.X = win.Bounds.X - prevbody.W 440 win.Bounds.Y = win.Bounds.Y - prevbody.H 441 } 442 } 443 444 if win.flags&windowHDynamic != 0 && !win.first { 445 uw := win.menuItemWidth + 2*win.style().Padding.X + 2*win.style().Border 446 if uw < win.Bounds.W { 447 win.Bounds.W = uw 448 } 449 } 450 451 if win.flags&windowCombo != 0 && win.flags&WindowDynamic != 0 { 452 prevbody := win.Bounds 453 prevbody.H = win.layout.Height 454 // If the combo window ends up with the right corner below the 455 // main winodw's lower bound make it non-dynamic and resize it to its 456 // maximum possible size that will show the whole combo box. 457 max := ctx.Windows[0].Bounds.Max() 458 if prevbody.Y+prevbody.H > max.Y { 459 prevbody.H = max.Y - prevbody.Y 460 win.Bounds = prevbody 461 win.flags &= ^windowCombo 462 } 463 } 464 465 if win.flags&windowNonblock != 0 && !win.first { 466 /* check if user clicked outside the popup and close if so */ 467 in_panel := ctx.Input.Mouse.IsClickInRect(pointer.ButtonLeft, win.ctx.Windows[0].layout.Bounds) 468 prevbody := win.Bounds 469 prevbody.H = win.layout.Height 470 in_body := ctx.Input.Mouse.IsClickInRect(pointer.ButtonLeft, prevbody) 471 in_header := ctx.Input.Mouse.IsClickInRect(pointer.ButtonLeft, win.header) 472 if !in_body && in_panel || in_header { 473 win.close = true 474 } 475 } 476 477 if win.flags&windowPopup != 0 { 478 win.cmds.PushScissor(nk_null_rect) 479 480 panelBegin(ctx, win, win.title) 481 win.layout.Offset = &win.Scrollbar 482 } 483 484 if win.first && (win.flags&windowContextual != 0 || win.flags&windowHDynamic != 0) { 485 ctx.trashFrame = true 486 } 487 488 win.first = false 489 } 490 491 var nk_null_rect = rect.Rect{-8192.0, -8192.0, 16384.0, 16384.0} 492 493 func panelEnd(ctx *context, window *Window) { 494 var footer = rect.Rect{0, 0, 0, 0} 495 496 layout := window.layout 497 style := &ctx.Style 498 in := &Input{} 499 if window.toplevel() { 500 ctx.Input.Mouse.clip = nk_null_rect 501 in = &ctx.Input 502 } 503 outclip := nk_null_rect 504 if window.flags&windowGroup != 0 { 505 outclip = window.parent.cmds.Clip 506 } 507 window.cmds.PushScissor(outclip) 508 509 wstyle := window.style() 510 511 /* cache configuration data */ 512 item_spacing := wstyle.Spacing 513 window_padding := wstyle.Padding 514 scrollbar_size := wstyle.ScrollbarSize 515 scaler_size := wstyle.ScalerSize 516 517 /* update the current cursor Y-position to point over the last added widget */ 518 layout.AtY += layout.Row.Height 519 520 /* draw footer and fill empty spaces inside a dynamically growing panel */ 521 if layout.Flags&WindowDynamic != 0 { 522 layout.Height = layout.AtY - layout.Bounds.Y 523 layout.Height = min(layout.Height, layout.Bounds.H) 524 525 // fill horizontal scrollbar space 526 { 527 var bounds rect.Rect 528 bounds.X = window.Bounds.X 529 bounds.Y = layout.AtY - item_spacing.Y 530 bounds.W = window.Bounds.W 531 bounds.H = window.Bounds.Y + layout.Height + item_spacing.Y + window.style().Padding.Y - bounds.Y 532 533 window.cmds.FillRect(bounds, 0, wstyle.Background) 534 } 535 536 if (layout.Offset.X == 0) || (layout.Flags&WindowNoScrollbar != 0) { 537 /* special case for dynamic windows without horizontal scrollbar 538 * or hidden scrollbars */ 539 footer.X = window.Bounds.X 540 footer.Y = window.Bounds.Y + layout.Height + item_spacing.Y + window.style().Padding.Y 541 footer.W = window.Bounds.W + scrollbar_size.X 542 layout.FooterH = 0 543 footer.H = 0 544 545 if (layout.Offset.X == 0) && layout.Flags&WindowNoScrollbar == 0 { 546 /* special case for windows like combobox, menu require draw call 547 * to fill the empty scrollbar background */ 548 var bounds rect.Rect 549 bounds.X = layout.Bounds.X + layout.Width 550 bounds.Y = layout.Clip.Y 551 bounds.W = scrollbar_size.X 552 bounds.H = layout.Height 553 554 window.cmds.FillRect(bounds, 0, wstyle.Background) 555 } 556 } else { 557 /* dynamic window with visible scrollbars and therefore bigger footer */ 558 footer.X = window.Bounds.X 559 footer.W = window.Bounds.W + scrollbar_size.X 560 footer.H = layout.FooterH 561 if (layout.Flags&windowCombo != 0) || (layout.Flags&windowMenu != 0) || (layout.Flags&windowContextual != 0) { 562 footer.Y = window.Bounds.Y + layout.Height 563 } else { 564 footer.Y = window.Bounds.Y + layout.Height + layout.FooterH 565 } 566 window.cmds.FillRect(footer, 0, wstyle.Background) 567 568 if layout.Flags&windowCombo == 0 && layout.Flags&windowMenu == 0 { 569 /* fill empty scrollbar space */ 570 var bounds rect.Rect 571 bounds.X = layout.Bounds.X 572 bounds.Y = window.Bounds.Y + layout.Height 573 bounds.W = layout.Bounds.W 574 bounds.H = layout.Row.Height 575 window.cmds.FillRect(bounds, 0, wstyle.Background) 576 } 577 } 578 } 579 580 /* scrollbars */ 581 if layout.Flags&WindowNoScrollbar == 0 { 582 var bounds rect.Rect 583 var scroll_target float64 584 var scroll_offset float64 585 var scroll_step float64 586 var scroll_inc float64 587 { 588 /* vertical scrollbar */ 589 bounds.X = layout.Bounds.X + layout.Width 590 bounds.Y = layout.Clip.Y 591 bounds.W = scrollbar_size.Y 592 bounds.H = layout.Clip.H 593 if layout.Flags&WindowBorder != 0 { 594 bounds.H -= 1 595 } 596 597 scroll_offset = float64(layout.Offset.Y) 598 scroll_step = float64(layout.Clip.H) * 0.10 599 scroll_inc = float64(layout.Clip.H) * 0.01 600 scroll_target = float64(layout.AtY - layout.Clip.Y) 601 scroll_offset = doScrollbarv(window, bounds, layout.Bounds, scroll_offset, scroll_target, scroll_step, scroll_inc, &ctx.Style.Scrollv, in, style.Font) 602 layout.Offset.Y = int(scroll_offset) 603 } 604 if layout.Flags&WindowNoHScrollbar == 0 { 605 /* horizontal scrollbar */ 606 bounds.X = layout.Bounds.X + window_padding.X 607 if layout.Flags&windowSub != 0 { 608 bounds.H = scrollbar_size.X 609 bounds.Y = layout.Bounds.Y 610 if layout.Flags&WindowBorder != 0 { 611 bounds.Y++ 612 } 613 bounds.Y += layout.HeaderH + layout.Menu.H + layout.Height 614 bounds.W = layout.Clip.W 615 } else if layout.Flags&WindowDynamic != 0 { 616 bounds.H = min(scrollbar_size.X, layout.FooterH) 617 bounds.W = layout.Bounds.W 618 bounds.Y = footer.Y 619 } else { 620 bounds.H = min(scrollbar_size.X, layout.FooterH) 621 bounds.Y = layout.Bounds.Y + window.Bounds.H 622 bounds.Y -= max(layout.FooterH, scrollbar_size.X) 623 bounds.W = layout.Width - 2*window_padding.X 624 } 625 626 scroll_offset = float64(layout.Offset.X) 627 scroll_target = float64(layout.MaxX - bounds.X) 628 scroll_step = float64(layout.MaxX) * 0.05 629 scroll_inc = float64(layout.MaxX) * 0.005 630 scroll_offset = doScrollbarh(window, bounds, scroll_offset, scroll_target, scroll_step, scroll_inc, &ctx.Style.Scrollh, in, style.Font) 631 layout.Offset.X = int(scroll_offset) 632 } 633 } 634 635 var dsab drawableScalerAndBorders 636 dsab.Style = window.style() 637 dsab.Bounds = window.Bounds 638 dsab.Border = layout.Border 639 dsab.HeaderH = layout.HeaderH 640 641 /* scaler */ 642 if layout.Flags&WindowScalable != 0 { 643 dsab.DrawScaler = true 644 645 dsab.ScalerRect.W = max(0, scaler_size.X) 646 dsab.ScalerRect.H = max(0, scaler_size.Y) 647 dsab.ScalerRect.X = (layout.Bounds.X + layout.Bounds.W) - (window_padding.X + dsab.ScalerRect.W) 648 /* calculate scaler bounds */ 649 if layout.Flags&WindowDynamic != 0 { 650 dsab.ScalerRect.Y = footer.Y + layout.FooterH - scaler_size.Y 651 } else { 652 dsab.ScalerRect.Y = layout.Bounds.Y + layout.Bounds.H - (scaler_size.Y + window_padding.Y) 653 } 654 655 /* do window scaling logic */ 656 if window.toplevel() { 657 if window.scaling { 658 if in == nil || !in.Mouse.Down(pointer.ButtonLeft) { 659 window.scaling = false 660 } else { 661 window.scale(in.Mouse.Delta) 662 } 663 } else if in != nil && in.Mouse.IsClickDownInRect(pointer.ButtonLeft, dsab.ScalerRect, true) { 664 window.scaling = true 665 } 666 } 667 } 668 669 /* window border */ 670 if layout.Flags&WindowBorder != 0 { 671 dsab.DrawBorders = true 672 673 if layout.Flags&WindowDynamic != 0 { 674 dsab.PaddingY = layout.FooterH + footer.Y 675 } else { 676 dsab.PaddingY = layout.Bounds.Y + layout.Bounds.H 677 } 678 /* select correct border color */ 679 dsab.BorderColor = wstyle.BorderColor 680 681 /* draw border between header and window body */ 682 if window.flags&WindowBorderHeader != 0 { 683 dsab.DrawHeaderBorder = true 684 } 685 } 686 687 window.widgets.Add(nstyle.WidgetStateInactive, dsab.Bounds) 688 dsab.Draw(&window.ctx.Style, &window.cmds) 689 690 layout.Flags |= windowEnabled 691 window.flags = layout.Flags 692 693 /* helper to make sure you have a 'nk_tree_push' 694 * for every 'nk_tree_pop' */ 695 if layout.Row.TreeDepth != 0 { 696 panic("Some TreePush not closed by TreePop") 697 } 698 } 699 700 // MenubarBegin adds a menubar to the current window. 701 // A menubar is an area displayed at the top of the window that is unaffected by scrolling. 702 // Remember to call MenubarEnd when you are done adding elements to the menubar. 703 func (win *Window) MenubarBegin() { 704 layout := win.layout 705 706 layout.Menu.X = layout.AtX 707 layout.Menu.Y = layout.Bounds.Y + layout.HeaderH 708 layout.Menu.W = layout.Width 709 layout.Menu.Offset = *layout.Offset 710 layout.Offset.Y = 0 711 } 712 713 func (win *Window) move(delta image.Point, pos image.Point) { 714 if win.flags&windowDocked != 0 { 715 if delta.X != 0 && delta.Y != 0 { 716 win.ctx.DockedWindows.Undock(win) 717 } 718 return 719 } 720 if canDock, bounds := win.ctx.DockedWindows.Dock(nil, pos, win.ctx.Windows[0].Bounds, win.ctx.Style.Scaling); canDock { 721 win.ctx.finalCmds.FillRect(bounds, 0, color.RGBA{0x0, 0x0, 0x50, 0x50}) 722 } 723 win.Bounds.X = win.Bounds.X + delta.X 724 win.Bounds.X = clampInt(0, win.Bounds.X, win.ctx.Windows[0].Bounds.X+win.ctx.Windows[0].Bounds.W-FontHeight(win.ctx.Style.Font)) 725 win.Bounds.Y = win.Bounds.Y + delta.Y 726 win.Bounds.Y = clampInt(0, win.Bounds.Y, win.ctx.Windows[0].Bounds.Y+win.ctx.Windows[0].Bounds.H-FontHeight(win.ctx.Style.Font)) 727 } 728 729 func (win *Window) scale(delta image.Point) { 730 if win.flags&windowDocked != 0 { 731 win.ctx.DockedWindows.Scale(win, delta, win.ctx.Style.Scaling) 732 return 733 } 734 window_size := win.style().MinSize 735 win.Bounds.W = max(window_size.X, win.Bounds.W+delta.X) 736 737 /* dragging in y-direction is only possible if static window */ 738 if win.layout.Flags&WindowDynamic == 0 { 739 win.Bounds.H = max(window_size.Y, win.Bounds.H+delta.Y) 740 } 741 } 742 743 // MenubarEnd signals that all widgets have been added to the menubar. 744 func (win *Window) MenubarEnd() { 745 layout := win.layout 746 747 layout.Menu.H = layout.AtY - layout.Menu.Y 748 layout.Clip.Y = layout.Bounds.Y + layout.HeaderH + layout.Menu.H + layout.Row.Height 749 layout.Height -= layout.Menu.H 750 *layout.Offset = layout.Menu.Offset 751 layout.Clip.H -= layout.Menu.H + layout.Row.Height 752 layout.AtY = layout.Menu.Y + layout.Menu.H 753 win.cmds.PushScissor(layout.Clip) 754 } 755 756 func (win *Window) widget() (valid bool, bounds rect.Rect, calcFittingWidth FittingWidthFn) { 757 /* allocate space and check if the widget needs to be updated and drawn */ 758 calcFittingWidth = panelAllocSpace(&bounds, win) 759 760 if !win.layout.Clip.Intersect(&bounds) { 761 return false, bounds, calcFittingWidth 762 } 763 764 return (bounds.W > 0 && bounds.H > 0), bounds, calcFittingWidth 765 } 766 767 func (win *Window) widgetFitting(item_padding image.Point) (valid bool, bounds rect.Rect) { 768 /* update the bounds to stand without padding */ 769 style := win.style() 770 layout := win.layout 771 valid, bounds, _ = win.widget() 772 if layout.Row.Index == 1 { 773 bounds.W += style.Padding.X 774 bounds.X -= style.Padding.X 775 } else { 776 bounds.X -= item_padding.X 777 } 778 779 if layout.Row.Columns > 0 && layout.Row.Index == layout.Row.Columns { 780 bounds.W += style.Padding.X 781 } else { 782 bounds.W += item_padding.X 783 } 784 return valid, bounds 785 } 786 787 func panelAllocSpace(bounds *rect.Rect, win *Window) FittingWidthFn { 788 if win.usingSub { 789 panic(UsingSubErr) 790 } 791 /* check if the end of the row has been hit and begin new row if so */ 792 layout := win.layout 793 if layout.Row.Columns > 0 && layout.Row.Index >= layout.Row.Columns { 794 panelAllocRow(win) 795 } 796 797 /* calculate widget position and size */ 798 layoutWidgetSpace(bounds, win.ctx, win, true) 799 800 win.LastWidgetBounds = *bounds 801 802 layout.Row.Index++ 803 804 if win.layout.Row.CalcMaxWidth { 805 col := win.adjust[win.layout.Cnt][win.layout.Row.Index2-1] 806 return func(width int) { 807 if width > col.width { 808 col.width = width 809 } 810 } 811 } 812 return nil 813 } 814 815 func panelAllocRow(win *Window) { 816 layout := win.layout 817 spacing := win.style().Spacing 818 row_height := layout.Row.Height - spacing.Y 819 panelLayout(win.ctx, win, row_height, layout.Row.Columns, 0) 820 } 821 822 func panelLayout(ctx *context, win *Window, height int, cols int, cnt int) { 823 /* prefetch some configuration data */ 824 layout := win.layout 825 826 style := win.style() 827 item_spacing := style.Spacing 828 829 if height == 0 { 830 height = layout.Height - (layout.AtY - layout.Bounds.Y) - 1 831 if layout.Row.Index != 0 && (win.flags&windowPopup == 0) { 832 height -= layout.Row.Height 833 } else { 834 height -= item_spacing.Y 835 } 836 if layout.ReservedHeight > 0 { 837 height -= layout.ReservedHeight 838 } 839 } 840 841 /* update the current row and set the current row layout */ 842 layout.Cnt = cnt 843 layout.Row.Index = 0 844 layout.Row.Index2 = 0 845 layout.Row.CalcMaxWidth = false 846 847 layout.AtY += layout.Row.Height 848 layout.Row.Columns = cols 849 layout.Row.Height = height + item_spacing.Y 850 layout.Row.ItemOffset = 0 851 if layout.Flags&WindowDynamic != 0 { 852 win.cmds.FillRect(rect.Rect{layout.Bounds.X, layout.AtY, layout.Bounds.W, height + item_spacing.Y}, 0, style.Background) 853 } 854 } 855 856 const ( 857 layoutDynamicFixed = iota 858 layoutDynamicFree 859 layoutDynamic 860 layoutStaticFree 861 layoutStatic 862 layoutInvalid 863 ) 864 865 var InvalidLayoutErr = errors.New("invalid layout") 866 var UsingSubErr = errors.New("parent window used while populating a sub window") 867 868 func layoutWidgetSpace(bounds *rect.Rect, ctx *context, win *Window, modify bool) { 869 layout := win.layout 870 871 /* cache some configuration data */ 872 style := win.style() 873 spacing := style.Spacing 874 padding := style.Padding 875 876 /* calculate the usable panel space */ 877 panel_padding := 2 * padding.X 878 879 panel_spacing := int(float64(layout.Row.Columns-1) * float64(spacing.X)) 880 panel_space := layout.Width - panel_padding - panel_spacing 881 882 /* calculate the width of one item inside the current layout space */ 883 item_offset := 0 884 item_width := 0 885 item_spacing := 0 886 887 switch layout.Row.Type { 888 case layoutInvalid: 889 panic(InvalidLayoutErr) 890 case layoutDynamicFixed: 891 /* scaling fixed size widgets item width */ 892 item_width = int(float64(panel_space) / float64(layout.Row.Columns)) 893 894 item_offset = layout.Row.Index * item_width 895 item_spacing = layout.Row.Index * spacing.X 896 case layoutDynamicFree: 897 /* panel width depended free widget placing */ 898 bounds.X = layout.AtX + int(float64(layout.Width)*layout.Row.DynamicFreeX) 899 bounds.X -= layout.Offset.X 900 bounds.Y = layout.AtY + int(float64(layout.Row.Height)*layout.Row.DynamicFreeY) 901 bounds.Y -= layout.Offset.Y 902 bounds.W = int(float64(layout.Width) * layout.Row.DynamicFreeW) 903 bounds.H = int(float64(layout.Row.Height) * layout.Row.DynamicFreeH) 904 return 905 case layoutDynamic: 906 /* scaling arrays of panel width ratios for every widget */ 907 var ratio float64 908 if layout.Row.Ratio[layout.Row.Index] < 0 { 909 ratio = layout.Row.ItemRatio 910 } else { 911 ratio = layout.Row.Ratio[layout.Row.Index] 912 } 913 914 item_spacing = layout.Row.Index * spacing.X 915 item_width = int(ratio * float64(panel_space)) 916 item_offset = layout.Row.ItemOffset 917 if modify { 918 layout.Row.ItemOffset += item_width 919 layout.Row.Filled += ratio 920 } 921 case layoutStaticFree: 922 /* free widget placing */ 923 atx, aty := layout.AtX, layout.AtY 924 if atx < layout.Clip.X { 925 atx = layout.Clip.X 926 } 927 if aty < layout.Clip.Y { 928 aty = layout.Clip.Y 929 } 930 931 bounds.X = atx + layout.Row.Item.X 932 933 bounds.W = layout.Row.Item.W 934 if ((bounds.X + bounds.W) > layout.MaxX) && modify { 935 layout.MaxX = (bounds.X + bounds.W) 936 } 937 bounds.X -= layout.Offset.X 938 bounds.Y = aty + layout.Row.Item.Y 939 bounds.Y -= layout.Offset.Y 940 bounds.H = layout.Row.Item.H 941 return 942 case layoutStatic: 943 /* non-scaling array of panel pixel width for every widget */ 944 item_spacing = layout.Row.Index * spacing.X 945 946 if len(layout.Row.WidthArr) > 0 { 947 item_width = layout.Row.WidthArr[layout.Row.Index] 948 } else { 949 item_width = layout.Row.ItemWidth 950 } 951 item_offset = layout.Row.ItemOffset 952 if modify { 953 layout.Row.ItemOffset += item_width 954 } 955 956 default: 957 panic("internal error unknown layout") 958 } 959 960 /* set the bounds of the newly allocated widget */ 961 bounds.W = item_width 962 963 bounds.H = layout.Row.Height - spacing.Y 964 bounds.Y = layout.AtY - layout.Offset.Y 965 bounds.X = layout.AtX + item_offset + item_spacing + padding.X 966 if ((bounds.X + bounds.W) > layout.MaxX) && modify { 967 layout.MaxX = bounds.X + bounds.W 968 } 969 bounds.X -= layout.Offset.X 970 } 971 972 func (ctx *context) scale(x int) int { 973 return int(float64(x) * ctx.Style.Scaling) 974 } 975 976 func rowLayoutCtr(win *Window, height int, cols int, width int) { 977 /* update the current row and set the current row layout */ 978 panelLayout(win.ctx, win, height, cols, 0) 979 win.layout.Row.Type = layoutDynamicFixed 980 981 win.layout.Row.ItemWidth = width 982 win.layout.Row.ItemRatio = 0.0 983 win.layout.Row.Ratio = nil 984 win.layout.Row.ItemOffset = 0 985 win.layout.Row.Filled = 0 986 } 987 988 // Reserves space for num rows of the specified height at the bottom 989 // of the panel. 990 // If a row of height == 0 is inserted it will take reserved space 991 // into account. 992 func (win *Window) LayoutReserveRow(height int, num int) { 993 win.LayoutReserveRowScaled(win.ctx.scale(height), num) 994 } 995 996 // Like LayoutReserveRow but with a scaled height. 997 func (win *Window) LayoutReserveRowScaled(height int, num int) { 998 win.layout.ReservedHeight += height*num + win.style().Spacing.Y*num 999 } 1000 1001 // Changes row layout and starts a new row. 1002 // Use the returned value to configure the new row layout: 1003 // win.Row(10).Static(100, 120, 100) 1004 // If height == 0 all the row is stretched to fill all the remaining space. 1005 func (win *Window) Row(height int) *rowConstructor { 1006 win.rowCtor.height = win.ctx.scale(height) 1007 return &win.rowCtor 1008 } 1009 1010 // Same as Row but with scaled units. 1011 func (win *Window) RowScaled(height int) *rowConstructor { 1012 win.rowCtor.height = height 1013 return &win.rowCtor 1014 } 1015 1016 type rowConstructor struct { 1017 win *Window 1018 height int 1019 } 1020 1021 // Starts new row that has cols columns of equal width that automatically 1022 // resize to fill the available space. 1023 func (ctr *rowConstructor) Dynamic(cols int) { 1024 rowLayoutCtr(ctr.win, ctr.height, cols, 0) 1025 } 1026 1027 // Starts new row with a fixed number of columns of width proportional 1028 // to the size of the window. 1029 func (ctr *rowConstructor) Ratio(ratio ...float64) { 1030 layout := ctr.win.layout 1031 panelLayout(ctr.win.ctx, ctr.win, ctr.height, len(ratio), 0) 1032 1033 /* calculate width of undefined widget ratios */ 1034 r := 0.0 1035 n_undef := 0 1036 layout.Row.Ratio = ratio 1037 for i := range ratio { 1038 if ratio[i] < 0.0 { 1039 n_undef++ 1040 } else { 1041 r += ratio[i] 1042 } 1043 } 1044 1045 r = saturateFloat(1.0 - r) 1046 layout.Row.Type = layoutDynamic 1047 layout.Row.ItemWidth = 0 1048 layout.Row.ItemRatio = 0.0 1049 if r > 0 && n_undef > 0 { 1050 layout.Row.ItemRatio = (r / float64(n_undef)) 1051 } 1052 1053 layout.Row.ItemOffset = 0 1054 layout.Row.Filled = 0 1055 1056 } 1057 1058 // Starts new row with a fixed number of columns with the specfieid widths. 1059 // If no widths are specified the row will never autowrap 1060 // and the width of the next widget can be specified using 1061 // LayoutSetWidth/LayoutSetWidthScaled/LayoutFitWidth. 1062 func (ctr *rowConstructor) Static(width ...int) { 1063 for i := range width { 1064 width[i] = ctr.win.ctx.scale(width[i]) 1065 } 1066 1067 ctr.StaticScaled(width...) 1068 } 1069 1070 func (win *Window) staticZeros(width []int) { 1071 layout := win.layout 1072 1073 nzero := 0 1074 used := 0 1075 for i := range width { 1076 if width[i] == 0 { 1077 nzero++ 1078 } 1079 used += width[i] 1080 } 1081 1082 if nzero > 0 { 1083 style := win.style() 1084 spacing := style.Spacing 1085 padding := style.Padding 1086 panel_padding := 2 * padding.X 1087 panel_spacing := int(float64(len(width)-1) * float64(spacing.X)) 1088 panel_space := layout.Width - panel_padding - panel_spacing 1089 1090 unused := panel_space - used 1091 1092 zerowidth := unused / nzero 1093 1094 for i := range width { 1095 if width[i] == 0 { 1096 width[i] = zerowidth 1097 } 1098 } 1099 } 1100 } 1101 1102 // Like Static but with scaled sizes. 1103 func (ctr *rowConstructor) StaticScaled(width ...int) { 1104 layout := ctr.win.layout 1105 1106 cnt := 0 1107 1108 if len(width) == 0 { 1109 if len(layout.Row.WidthArr) == 0 { 1110 cnt = layout.Cnt 1111 } else { 1112 cnt = ctr.win.lastLayoutCnt + 1 1113 ctr.win.lastLayoutCnt = cnt 1114 } 1115 } 1116 1117 panelLayout(ctr.win.ctx, ctr.win, ctr.height, len(width), cnt) 1118 1119 ctr.win.staticZeros(width) 1120 1121 layout.Row.WidthArr = width 1122 layout.Row.Type = layoutStatic 1123 layout.Row.ItemWidth = 0 1124 layout.Row.ItemRatio = 0.0 1125 layout.Row.ItemOffset = 0 1126 layout.Row.Filled = 0 1127 } 1128 1129 // Reset static row 1130 func (win *Window) LayoutResetStatic(width ...int) { 1131 for i := range width { 1132 width[i] = win.ctx.scale(width[i]) 1133 } 1134 win.LayoutResetStaticScaled(width...) 1135 } 1136 1137 func (win *Window) LayoutResetStaticScaled(width ...int) { 1138 layout := win.layout 1139 if layout.Row.Type != layoutStatic { 1140 panic(WrongLayoutErr) 1141 } 1142 win.staticZeros(width) 1143 layout.Row.Index = 0 1144 layout.Row.Index2 = 0 1145 layout.Row.CalcMaxWidth = false 1146 layout.Row.Columns = len(width) 1147 layout.Row.WidthArr = width 1148 layout.Row.ItemWidth = 0 1149 layout.Row.ItemRatio = 0.0 1150 layout.Row.ItemOffset = 0 1151 layout.Row.Filled = 0 1152 } 1153 1154 // Starts new row that will contain widget_count widgets. 1155 // The size and position of widgets inside this row will be specified 1156 // by callling LayoutSpacePush/LayoutSpacePushScaled. 1157 func (ctr *rowConstructor) SpaceBegin(widget_count int) (bounds rect.Rect) { 1158 layout := ctr.win.layout 1159 panelLayout(ctr.win.ctx, ctr.win, ctr.height, widget_count, 0) 1160 layout.Row.Type = layoutStaticFree 1161 1162 layout.Row.Ratio = nil 1163 layout.Row.ItemWidth = 0 1164 layout.Row.ItemRatio = 0.0 1165 layout.Row.ItemOffset = 0 1166 layout.Row.Filled = 0 1167 1168 style := ctr.win.style() 1169 spacing := style.Spacing 1170 padding := style.Padding 1171 1172 bounds.W = layout.Width - 2*padding.X 1173 bounds.H = layout.Row.Height - spacing.Y 1174 1175 return bounds 1176 } 1177 1178 // Starts new row that will contain widget_count widgets. 1179 // The size and position of widgets inside this row will be specified 1180 // by callling LayoutSpacePushRatio. 1181 func (ctr *rowConstructor) SpaceBeginRatio(widget_count int) { 1182 layout := ctr.win.layout 1183 panelLayout(ctr.win.ctx, ctr.win, ctr.height, widget_count, 0) 1184 layout.Row.Type = layoutDynamicFree 1185 1186 layout.Row.Ratio = nil 1187 layout.Row.ItemWidth = 0 1188 layout.Row.ItemRatio = 0.0 1189 layout.Row.ItemOffset = 0 1190 layout.Row.Filled = 0 1191 } 1192 1193 // LayoutSetWidth adds a new column with the specified width to a static 1194 // layout. 1195 func (win *Window) LayoutSetWidth(width int) { 1196 layout := win.layout 1197 if layout.Row.Type != layoutStatic || len(layout.Row.WidthArr) > 0 { 1198 panic(WrongLayoutErr) 1199 } 1200 layout.Row.Index2++ 1201 layout.Row.CalcMaxWidth = false 1202 layout.Row.ItemWidth = win.ctx.scale(width) 1203 } 1204 1205 // LayoutSetWidthScaled adds a new column width the specified scaled width 1206 // to a static layout. 1207 func (win *Window) LayoutSetWidthScaled(width int) { 1208 layout := win.layout 1209 if layout.Row.Type != layoutStatic || len(layout.Row.WidthArr) > 0 { 1210 panic(WrongLayoutErr) 1211 } 1212 layout.Row.Index2++ 1213 layout.Row.CalcMaxWidth = false 1214 layout.Row.ItemWidth = width 1215 } 1216 1217 // LayoutFitWidth adds a new column to a static layout. 1218 // The width of the column will be large enough to fit the largest widget 1219 // exactly. The largest widget will only be calculated once per id, if the 1220 // dataset changes the id should change. 1221 func (win *Window) LayoutFitWidth(id int, minwidth int) { 1222 layout := win.layout 1223 if layout.Row.Type != layoutStatic || len(layout.Row.WidthArr) > 0 { 1224 panic(WrongLayoutErr) 1225 } 1226 if win.adjust == nil { 1227 win.adjust = make(map[int]map[int]*adjustCol) 1228 } 1229 adjust, ok := win.adjust[layout.Cnt] 1230 if !ok { 1231 adjust = make(map[int]*adjustCol) 1232 win.adjust[layout.Cnt] = adjust 1233 } 1234 col, ok := adjust[layout.Row.Index2] 1235 if !ok || col.id != id || col.font != win.ctx.Style.Font { 1236 if !ok { 1237 col = &adjustCol{id: id, width: minwidth} 1238 win.adjust[layout.Cnt][layout.Row.Index2] = col 1239 } 1240 col.id = id 1241 col.font = win.ctx.Style.Font 1242 col.width = minwidth 1243 col.first = true 1244 win.ctx.trashFrame = true 1245 win.LayoutSetWidth(minwidth) 1246 layout.Row.CalcMaxWidth = true 1247 return 1248 } 1249 win.LayoutSetWidthScaled(col.width) 1250 layout.Row.CalcMaxWidth = col.first 1251 } 1252 1253 var WrongLayoutErr = errors.New("Command not available with current layout") 1254 1255 // Sets position and size of the next widgets in a Space row layout 1256 func (win *Window) LayoutSpacePush(rect rect.Rect) { 1257 if win.layout.Row.Type != layoutStaticFree { 1258 panic(WrongLayoutErr) 1259 } 1260 rect.X = win.ctx.scale(rect.X) 1261 rect.Y = win.ctx.scale(rect.Y) 1262 rect.W = win.ctx.scale(rect.W) 1263 rect.H = win.ctx.scale(rect.H) 1264 win.layout.Row.Item = rect 1265 } 1266 1267 // Like LayoutSpacePush but with scaled units 1268 func (win *Window) LayoutSpacePushScaled(rect rect.Rect) { 1269 if win.layout.Row.Type != layoutStaticFree { 1270 panic(WrongLayoutErr) 1271 } 1272 win.layout.Row.Item = rect 1273 } 1274 1275 // Sets position and size of the next widgets in a Space row layout 1276 func (win *Window) LayoutSpacePushRatio(x, y, w, h float64) { 1277 if win.layout.Row.Type != layoutDynamicFree { 1278 panic(WrongLayoutErr) 1279 } 1280 win.layout.Row.DynamicFreeX, win.layout.Row.DynamicFreeY, win.layout.Row.DynamicFreeH, win.layout.Row.DynamicFreeW = x, y, w, h 1281 } 1282 1283 func (win *Window) layoutPeek(bounds *rect.Rect) { 1284 layout := win.layout 1285 y := layout.AtY 1286 off := layout.Row.ItemOffset 1287 index := layout.Row.Index 1288 if layout.Row.Columns > 0 && layout.Row.Index >= layout.Row.Columns { 1289 layout.AtY += layout.Row.Height 1290 layout.Row.ItemOffset = 0 1291 layout.Row.Index = 0 1292 layout.Row.Index2 = 0 1293 } 1294 1295 layoutWidgetSpace(bounds, win.ctx, win, false) 1296 layout.AtY = y 1297 layout.Row.ItemOffset = off 1298 layout.Row.Index = index 1299 } 1300 1301 // Returns the position and size of the next widget that will be 1302 // added to the current row. 1303 // Note that the return value is in scaled units. 1304 func (win *Window) WidgetBounds() rect.Rect { 1305 var bounds rect.Rect 1306 win.layoutPeek(&bounds) 1307 return bounds 1308 } 1309 1310 // Returns remaining available height of win in scaled units. 1311 func (win *Window) LayoutAvailableHeight() int { 1312 return win.layout.Clip.H - (win.layout.AtY - win.layout.Bounds.Y) - win.style().Spacing.Y - win.layout.Row.Height 1313 } 1314 1315 func (win *Window) LayoutAvailableWidth() int { 1316 switch win.layout.Row.Type { 1317 case layoutDynamicFree, layoutStaticFree: 1318 return win.layout.Clip.W 1319 default: 1320 style := win.style() 1321 panel_spacing := int(float64(win.layout.Row.Columns-1) * float64(style.Spacing.X)) 1322 return win.layout.Width - style.Padding.X*2 - panel_spacing - win.layout.AtX 1323 } 1324 } 1325 1326 // Will return (false, false) if the last widget is visible, (true, 1327 // false) if it is above the visible area, (false, true) if it is 1328 // below the visible area 1329 func (win *Window) Invisible() (above, below bool) { 1330 return win.LastWidgetBounds.Y < win.layout.Clip.Y, win.LastWidgetBounds.Y+win.LastWidgetBounds.H > (win.layout.Clip.Y + win.layout.Clip.H) 1331 } 1332 1333 func (win *Window) At() image.Point { 1334 return image.Point{win.layout.AtX - win.layout.Clip.X, win.layout.AtY - win.layout.Clip.Y} 1335 } 1336 1337 /////////////////////////////////////////////////////////////////////////////////// 1338 // TREE 1339 /////////////////////////////////////////////////////////////////////////////////// 1340 1341 type TreeType int 1342 1343 const ( 1344 TreeNode TreeType = iota 1345 TreeTab 1346 ) 1347 1348 func (win *Window) TreePush(type_ TreeType, title string, initialOpen bool) bool { 1349 return win.TreePushNamed(type_, title, title, initialOpen) 1350 } 1351 1352 // Creates a new collapsable section inside win. Returns true 1353 // when the section is open. Widgets that are inside this collapsable 1354 // section should be added to win only when this function returns true. 1355 // Once you are done adding elements to the collapsable section 1356 // call TreePop. 1357 // Initial_open will determine whether this collapsable section 1358 // will be initially open. 1359 // Type_ will determine the style of this collapsable section. 1360 func (win *Window) TreePushNamed(type_ TreeType, name, title string, initial_open bool) bool { 1361 labelBounds, _, ok := win.TreePushCustom(type_, name, initial_open) 1362 1363 style := win.style() 1364 z := &win.ctx.Style 1365 out := &win.cmds 1366 1367 var text textWidget 1368 if type_ == TreeTab { 1369 var background *nstyle.Item = &z.Tab.Background 1370 if background.Type == nstyle.ItemImage { 1371 text.Background = color.RGBA{0, 0, 0, 0} 1372 } else { 1373 text.Background = background.Data.Color 1374 } 1375 } else { 1376 text.Background = style.Background 1377 } 1378 1379 text.Text = z.Tab.Text 1380 widgetText(out, labelBounds, title, &text, "LC", z.Font) 1381 1382 return ok 1383 } 1384 1385 func (win *Window) TreePushCustom(type_ TreeType, name string, initial_open bool) (bounds rect.Rect, out *command.Buffer, ok bool) { 1386 /* cache some data */ 1387 layout := win.layout 1388 style := &win.ctx.Style 1389 panel_padding := win.style().Padding 1390 1391 if type_ == TreeTab { 1392 /* calculate header bounds and draw background */ 1393 panelLayout(win.ctx, win, FontHeight(style.Font)+2*style.Tab.Padding.Y, 1, 0) 1394 win.layout.Row.Type = layoutDynamicFixed 1395 win.layout.Row.ItemWidth = 0 1396 win.layout.Row.ItemRatio = 0.0 1397 win.layout.Row.Ratio = nil 1398 win.layout.Row.ItemOffset = 0 1399 win.layout.Row.Filled = 0 1400 } 1401 1402 widget_state, header, _ := win.widget() 1403 1404 /* find or create tab persistent state (open/closed) */ 1405 1406 node := win.curNode.Children[name] 1407 if node == nil { 1408 node = createTreeNode(initial_open, win.curNode) 1409 win.curNode.Children[name] = node 1410 } 1411 1412 /* update node state */ 1413 in := &Input{} 1414 if win.toplevel() { 1415 if widget_state { 1416 in = &win.ctx.Input 1417 in.Mouse.clip = win.cmds.Clip 1418 } 1419 } 1420 1421 ws := win.widgets.PrevState(header) 1422 if buttonBehaviorDo(&ws, header, in, false) { 1423 node.Open = !node.Open 1424 } 1425 1426 /* calculate the triangle bounds */ 1427 var sym rect.Rect 1428 sym.H = FontHeight(style.Font) 1429 sym.W = sym.H 1430 sym.Y = header.Y + style.Tab.Padding.Y 1431 sym.X = header.X + panel_padding.X + style.Tab.Padding.X 1432 1433 win.widgets.Add(ws, header) 1434 labelBounds := drawTreeNode(win, win.style(), type_, header, sym) 1435 1436 /* calculate the triangle points and draw triangle */ 1437 symbolType := style.Tab.SymMaximize 1438 if node.Open { 1439 symbolType = style.Tab.SymMinimize 1440 } 1441 styleButton := &style.Tab.NodeButton 1442 if type_ == TreeTab { 1443 styleButton = &style.Tab.TabButton 1444 } 1445 doButton(win, label.S(symbolType), sym, styleButton, in, false) 1446 1447 out = &win.cmds 1448 if !widget_state { 1449 out = nil 1450 } 1451 1452 /* increase x-axis cursor widget position pointer */ 1453 if node.Open { 1454 layout.AtX = header.X + layout.Offset.X + style.Tab.Indent 1455 layout.Width = max(layout.Width, 2*panel_padding.X) 1456 layout.Width -= (style.Tab.Indent + panel_padding.X) 1457 layout.Row.TreeDepth++ 1458 win.curNode = node 1459 return labelBounds, out, true 1460 } else { 1461 return labelBounds, out, false 1462 } 1463 } 1464 1465 func (win *Window) treeOpenClose(open bool, path []string) { 1466 node := win.curNode 1467 for i := range path { 1468 var ok bool 1469 node, ok = node.Children[path[i]] 1470 if !ok { 1471 return 1472 } 1473 } 1474 if node != nil { 1475 node.Open = open 1476 } 1477 } 1478 1479 // Opens the collapsable section specified by path 1480 func (win *Window) TreeOpen(path ...string) { 1481 win.treeOpenClose(true, path) 1482 } 1483 1484 // Closes the collapsable section specified by path 1485 func (win *Window) TreeClose(path ...string) { 1486 win.treeOpenClose(false, path) 1487 } 1488 1489 // Returns true if the specified node is open 1490 func (win *Window) TreeIsOpen(name string) bool { 1491 node := win.curNode.Children[name] 1492 if node != nil { 1493 return node.Open 1494 } 1495 return false 1496 } 1497 1498 // TreePop signals that the program is done adding elements to the 1499 // current collapsable section. 1500 func (win *Window) TreePop() { 1501 layout := win.layout 1502 panel_padding := win.style().Padding 1503 layout.AtX -= panel_padding.X + win.ctx.Style.Tab.Indent 1504 layout.Width += panel_padding.X + win.ctx.Style.Tab.Indent 1505 if layout.Row.TreeDepth == 0 { 1506 panic("TreePop called without opened tree nodes") 1507 } 1508 win.curNode = win.curNode.Parent 1509 layout.Row.TreeDepth-- 1510 } 1511 1512 /////////////////////////////////////////////////////////////////////////////////// 1513 // NON-INTERACTIVE WIDGETS 1514 /////////////////////////////////////////////////////////////////////////////////// 1515 1516 // LabelColored draws a text label with the specified background color. 1517 func (win *Window) LabelColored(str string, alignment label.Align, color color.RGBA) { 1518 var bounds rect.Rect 1519 var text textWidget 1520 1521 style := &win.ctx.Style 1522 fitting := panelAllocSpace(&bounds, win) 1523 item_padding := style.Text.Padding 1524 if fitting != nil { 1525 fitting(2*item_padding.X + FontWidth(win.ctx.Style.Font, str)) 1526 } 1527 1528 text.Padding.X = item_padding.X 1529 text.Padding.Y = item_padding.Y 1530 text.Background = win.style().Background 1531 text.Text = color 1532 win.widgets.Add(nstyle.WidgetStateInactive, bounds) 1533 widgetText(&win.cmds, bounds, str, &text, alignment, win.ctx.Style.Font) 1534 1535 } 1536 1537 // LabelWrapColored draws a text label with the specified background 1538 // color autowrappping the text. 1539 func (win *Window) LabelWrapColored(str string, color color.RGBA) { 1540 var bounds rect.Rect 1541 var text textWidget 1542 1543 style := &win.ctx.Style 1544 panelAllocSpace(&bounds, win) 1545 item_padding := style.Text.Padding 1546 1547 text.Padding.X = item_padding.X 1548 text.Padding.Y = item_padding.Y 1549 text.Background = win.style().Background 1550 text.Text = color 1551 win.widgets.Add(nstyle.WidgetStateInactive, bounds) 1552 widgetTextWrap(&win.cmds, bounds, []rune(str), &text, win.ctx.Style.Font) 1553 } 1554 1555 // Label draws a text label. 1556 func (win *Window) Label(str string, alignment label.Align) { 1557 win.LabelColored(str, alignment, win.ctx.Style.Text.Color) 1558 } 1559 1560 // LabelWrap draws a text label, autowrapping its contents. 1561 func (win *Window) LabelWrap(str string) { 1562 win.LabelWrapColored(str, win.ctx.Style.Text.Color) 1563 } 1564 1565 // Image draws an image. 1566 func (win *Window) Image(img *image.RGBA) { 1567 s, bounds, fitting := win.widget() 1568 if fitting != nil { 1569 fitting(img.Bounds().Dx()) 1570 } 1571 if !s { 1572 return 1573 } 1574 win.widgets.Add(nstyle.WidgetStateInactive, bounds) 1575 win.cmds.DrawImage(bounds, img) 1576 } 1577 1578 // Spacing adds empty space 1579 func (win *Window) Spacing(cols int) { 1580 for i := 0; i < cols; i++ { 1581 win.widget() 1582 } 1583 } 1584 1585 // CustomState returns the widget state of a custom widget. 1586 func (win *Window) CustomState() nstyle.WidgetStates { 1587 bounds := win.WidgetBounds() 1588 s := true 1589 if !win.layout.Clip.Intersect(&bounds) { 1590 s = false 1591 } 1592 1593 ws := win.widgets.PrevState(bounds) 1594 basicWidgetStateControl(&ws, win.inputMaybe(s), bounds) 1595 return ws 1596 } 1597 1598 // Custom adds a custom widget. 1599 func (win *Window) Custom(state nstyle.WidgetStates) (bounds rect.Rect, out *command.Buffer) { 1600 var s bool 1601 1602 if s, bounds, _ = win.widget(); !s { 1603 return 1604 } 1605 prevstate := win.widgets.PrevState(bounds) 1606 exitstate := basicWidgetStateControl(&prevstate, win.inputMaybe(s), bounds) 1607 if state != nstyle.WidgetStateActive { 1608 state = exitstate 1609 } 1610 win.widgets.Add(state, bounds) 1611 return bounds, &win.cmds 1612 } 1613 1614 func (win *Window) Commands() *command.Buffer { 1615 return &win.cmds 1616 } 1617 1618 /////////////////////////////////////////////////////////////////////////////////// 1619 // BUTTON 1620 /////////////////////////////////////////////////////////////////////////////////// 1621 1622 func buttonBehaviorDo(state *nstyle.WidgetStates, r rect.Rect, i *Input, repeat bool) (ret bool) { 1623 exitstate := basicWidgetStateControl(state, i, r) 1624 1625 if *state == nstyle.WidgetStateActive { 1626 if exitstate == nstyle.WidgetStateHovered { 1627 if repeat { 1628 ret = i.Mouse.Down(pointer.ButtonLeft) 1629 } else { 1630 ret = i.Mouse.Released(pointer.ButtonLeft) 1631 } 1632 } 1633 if !i.Mouse.Down(pointer.ButtonLeft) { 1634 *state = exitstate 1635 } 1636 } 1637 1638 return ret 1639 } 1640 1641 func symbolWidth(sym label.SymbolType, font font.Face) int { 1642 switch sym { 1643 case label.SymbolX: 1644 return FontWidth(font, "x") 1645 case label.SymbolUnderscore: 1646 return FontWidth(font, "_") 1647 case label.SymbolPlus: 1648 return FontWidth(font, "+") 1649 case label.SymbolMinus: 1650 return FontWidth(font, "-") 1651 default: 1652 return FontWidth(font, "M") 1653 } 1654 } 1655 1656 func buttonWidth(lbl label.Label, style *nstyle.Button, font font.Face) int { 1657 w := 2*style.Padding.X + 2*style.TouchPadding.X + 2*style.Border 1658 switch lbl.Kind { 1659 case label.TextLabel: 1660 w += FontWidth(font, lbl.Text) 1661 case label.SymbolLabel: 1662 w += symbolWidth(lbl.Symbol, font) 1663 case label.ImageLabel: 1664 w += lbl.Img.Bounds().Dx() + 2*style.ImagePadding.X 1665 case label.SymbolTextLabel: 1666 w += FontWidth(font, lbl.Text) + symbolWidth(lbl.Symbol, font) + 2*style.Padding.X 1667 case label.ImageTextLabel: 1668 } 1669 return w 1670 } 1671 1672 func doButton(win *Window, lbl label.Label, r rect.Rect, style *nstyle.Button, in *Input, repeat bool) bool { 1673 out := win.widgets 1674 if lbl.Kind == label.ColorLabel { 1675 button := *style 1676 button.Normal = nstyle.MakeItemColor(lbl.Color) 1677 button.Hover = nstyle.MakeItemColor(lbl.Color) 1678 button.Active = nstyle.MakeItemColor(lbl.Color) 1679 button.Padding = image.Point{0, 0} 1680 style = &button 1681 } 1682 1683 /* calculate button content space */ 1684 var content rect.Rect 1685 content.X = r.X + style.Padding.X + style.Border 1686 content.Y = r.Y + style.Padding.Y + style.Border 1687 content.W = r.W - 2*style.Padding.X + style.Border 1688 content.H = r.H - 2*style.Padding.Y + style.Border 1689 1690 /* execute button behavior */ 1691 var bounds rect.Rect 1692 bounds.X = r.X - style.TouchPadding.X 1693 bounds.Y = r.Y - style.TouchPadding.Y 1694 bounds.W = r.W + 2*style.TouchPadding.X 1695 bounds.H = r.H + 2*style.TouchPadding.Y 1696 state := out.PrevState(bounds) 1697 ok := buttonBehaviorDo(&state, bounds, in, repeat) 1698 1699 switch lbl.Kind { 1700 case label.TextLabel: 1701 if lbl.Align == "" { 1702 lbl.Align = "CC" 1703 } 1704 out.Add(state, bounds) 1705 drawTextButton(win, bounds, content, state, style, lbl.Text, lbl.Align) 1706 1707 case label.SymbolLabel: 1708 out.Add(state, bounds) 1709 drawSymbolButton(win, bounds, content, state, style, lbl.Symbol) 1710 1711 case label.ImageLabel: 1712 content.X += style.ImagePadding.X 1713 content.Y += style.ImagePadding.Y 1714 content.W -= 2 * style.ImagePadding.X 1715 content.H -= 2 * style.ImagePadding.Y 1716 1717 out.Add(state, bounds) 1718 drawImageButton(win, bounds, content, state, style, lbl.Img) 1719 1720 case label.SymbolTextLabel: 1721 if lbl.Align == "" { 1722 lbl.Align = "CC" 1723 } 1724 font := win.ctx.Style.Font 1725 var tri rect.Rect 1726 tri.Y = content.Y + (content.H / 2) - FontHeight(font)/2 1727 tri.W = FontHeight(font) 1728 tri.H = FontHeight(font) 1729 if lbl.Align[0] == 'L' { 1730 tri.X = (content.X + content.W) - (2*style.Padding.X + tri.W) 1731 tri.X = max(tri.X, 0) 1732 } else { 1733 tri.X = content.X + 2*style.Padding.X 1734 } 1735 1736 out.Add(state, bounds) 1737 drawTextSymbolButton(win, bounds, content, tri, state, style, lbl.Text, lbl.Symbol) 1738 1739 case label.ImageTextLabel: 1740 if lbl.Align == "" { 1741 lbl.Align = "CC" 1742 } 1743 var icon rect.Rect 1744 icon.Y = bounds.Y + style.Padding.Y 1745 icon.H = bounds.H - 2*style.Padding.Y 1746 icon.W = icon.H 1747 if lbl.Align[0] == 'L' { 1748 icon.X = (bounds.X + bounds.W) - (2*style.Padding.X + icon.W) 1749 icon.X = max(icon.X, 0) 1750 } else { 1751 icon.X = bounds.X + 2*style.Padding.X 1752 } 1753 1754 icon.X += style.ImagePadding.X 1755 icon.Y += style.ImagePadding.Y 1756 icon.W -= 2 * style.ImagePadding.X 1757 icon.H -= 2 * style.ImagePadding.Y 1758 1759 out.Add(state, bounds) 1760 drawTextImageButton(win, bounds, content, icon, state, style, lbl.Text, lbl.Img) 1761 1762 case label.ColorLabel: 1763 out.Add(state, bounds) 1764 drawSymbolButton(win, bounds, bounds, state, style, label.SymbolNone) 1765 1766 } 1767 1768 return ok 1769 } 1770 1771 // Button adds a button 1772 func (win *Window) Button(lbl label.Label, repeat bool) bool { 1773 style := &win.ctx.Style 1774 state, bounds, fitting := win.widget() 1775 if fitting != nil { 1776 buttonWidth(lbl, &style.Button, style.Font) 1777 } 1778 if !state { 1779 return false 1780 } 1781 in := win.inputMaybe(state) 1782 return doButton(win, lbl, bounds, &style.Button, in, repeat) 1783 } 1784 1785 func (win *Window) ButtonText(text string) bool { 1786 return win.Button(label.T(text), false) 1787 } 1788 1789 /////////////////////////////////////////////////////////////////////////////////// 1790 // SELECTABLE 1791 /////////////////////////////////////////////////////////////////////////////////// 1792 1793 func selectableWidth(str string, style *nstyle.Selectable, font font.Face) int { 1794 return 2*style.Padding.X + 2*style.TouchPadding.X + FontWidth(font, str) 1795 } 1796 1797 func doSelectable(win *Window, bounds rect.Rect, str string, align label.Align, value *bool, style *nstyle.Selectable, in *Input) bool { 1798 if str == "" { 1799 return false 1800 } 1801 old_value := *value 1802 1803 /* remove padding */ 1804 var touch rect.Rect 1805 touch.X = bounds.X - style.TouchPadding.X 1806 touch.Y = bounds.Y - style.TouchPadding.Y 1807 touch.W = bounds.W + style.TouchPadding.X*2 1808 touch.H = bounds.H + style.TouchPadding.Y*2 1809 1810 /* update button */ 1811 state := win.widgets.PrevState(bounds) 1812 if buttonBehaviorDo(&state, touch, in, false) { 1813 *value = !*value 1814 } 1815 1816 win.widgets.Add(state, bounds) 1817 drawSelectable(win, state, style, *value, bounds, str, align) 1818 return old_value != *value 1819 } 1820 1821 // SelectableLabel adds a selectable label. Value is a pointer 1822 // to a flag that will be changed to reflect the selected state of 1823 // this label. 1824 // Returns true when the label is clicked. 1825 func (win *Window) SelectableLabel(str string, align label.Align, value *bool) bool { 1826 style := &win.ctx.Style 1827 state, bounds, fitting := win.widget() 1828 if fitting != nil { 1829 fitting(selectableWidth(str, &style.Selectable, style.Font)) 1830 } 1831 if !state { 1832 return false 1833 } 1834 in := win.inputMaybe(state) 1835 return doSelectable(win, bounds, str, align, value, &style.Selectable, in) 1836 } 1837 1838 /////////////////////////////////////////////////////////////////////////////////// 1839 // SCROLLBARS 1840 /////////////////////////////////////////////////////////////////////////////////// 1841 1842 type orientation int 1843 1844 const ( 1845 vertical orientation = iota 1846 horizontal 1847 ) 1848 1849 func scrollbarBehavior(state *nstyle.WidgetStates, in *Input, scroll, cursor, empty0, empty1 rect.Rect, scroll_offset float64, target float64, scroll_step float64, o orientation) float64 { 1850 exitstate := basicWidgetStateControl(state, in, cursor) 1851 1852 if *state == nstyle.WidgetStateActive { 1853 if !in.Mouse.Down(pointer.ButtonLeft) { 1854 *state = exitstate 1855 } else { 1856 switch o { 1857 case vertical: 1858 pixel := in.Mouse.Delta.Y 1859 delta := (float64(pixel) / float64(scroll.H)) * target 1860 scroll_offset = clampFloat(0, scroll_offset+delta, target-float64(scroll.H)) 1861 case horizontal: 1862 pixel := in.Mouse.Delta.X 1863 delta := (float64(pixel) / float64(scroll.W)) * target 1864 scroll_offset = clampFloat(0, scroll_offset+delta, target-float64(scroll.W)) 1865 } 1866 } 1867 } else if in.Mouse.IsClickInRect(pointer.ButtonLeft, empty0) { 1868 switch o { 1869 case vertical: 1870 scroll_offset -= float64(scroll.H) 1871 case horizontal: 1872 scroll_offset -= float64(scroll.W) 1873 } 1874 1875 if scroll_offset < 0 { 1876 scroll_offset = 0 1877 } 1878 } else if in.Mouse.IsClickInRect(pointer.ButtonLeft, empty1) { 1879 var max float64 1880 switch o { 1881 case vertical: 1882 scroll_offset += float64(scroll.H) 1883 max = target - float64(scroll.H) 1884 case horizontal: 1885 scroll_offset += float64(scroll.W) 1886 max = target - float64(scroll.W) 1887 } 1888 1889 if scroll_offset > max { 1890 scroll_offset = max 1891 } 1892 } 1893 1894 return scroll_offset 1895 } 1896 1897 func scrollwheelBehavior(win *Window, scroll, scrollwheel_bounds rect.Rect, scroll_offset, target, scroll_step float64) float64 { 1898 in := win.scrollwheelInput() 1899 1900 if ((in.Mouse.ScrollDelta < 0) || (in.Mouse.ScrollDelta > 0)) && in.Mouse.HoveringRect(scrollwheel_bounds) { 1901 /* update cursor by mouse scrolling */ 1902 old_scroll_offset := scroll_offset 1903 scroll_offset = scroll_offset + scroll_step*float64(-in.Mouse.ScrollDelta) 1904 scroll_offset = clampFloat(0, scroll_offset, target-float64(scroll.H)) 1905 used_delta := (scroll_offset - old_scroll_offset) / scroll_step 1906 residual := float64(in.Mouse.ScrollDelta) + used_delta 1907 if residual < 0 { 1908 in.Mouse.ScrollDelta = int(math.Ceil(residual)) 1909 } else { 1910 in.Mouse.ScrollDelta = int(math.Floor(residual)) 1911 } 1912 } 1913 return scroll_offset 1914 } 1915 1916 func doScrollbarv(win *Window, scroll, scrollwheel_bounds rect.Rect, offset float64, target float64, step float64, button_pixel_inc float64, style *nstyle.Scrollbar, in *Input, font font.Face) float64 { 1917 var cursor rect.Rect 1918 var scroll_step float64 1919 var scroll_offset float64 1920 var scroll_off float64 1921 var scroll_ratio float64 1922 1923 if scroll.W < 1 { 1924 scroll.W = 1 1925 } 1926 1927 if scroll.H < 2*scroll.W { 1928 scroll.H = 2 * scroll.W 1929 } 1930 1931 if target <= float64(scroll.H) { 1932 return 0 1933 } 1934 1935 /* optional scrollbar buttons */ 1936 if style.ShowButtons { 1937 var button rect.Rect 1938 button.X = scroll.X 1939 button.W = scroll.W 1940 button.H = scroll.W 1941 1942 scroll_h := float64(scroll.H - 2*button.H) 1943 scroll_step = minFloat(step, button_pixel_inc) 1944 1945 /* decrement button */ 1946 button.Y = scroll.Y 1947 1948 if doButton(win, label.S(style.DecSymbol), button, &style.DecButton, in, true) { 1949 offset = offset - scroll_step 1950 } 1951 1952 /* increment button */ 1953 button.Y = scroll.Y + scroll.H - button.H 1954 1955 if doButton(win, label.S(style.IncSymbol), button, &style.IncButton, in, true) { 1956 offset = offset + scroll_step 1957 } 1958 1959 scroll.Y = scroll.Y + button.H 1960 scroll.H = int(scroll_h) 1961 } 1962 1963 /* calculate scrollbar constants */ 1964 scroll_step = minFloat(step, float64(scroll.H)) 1965 1966 scroll_offset = clampFloat(0, offset, target-float64(scroll.H)) 1967 scroll_ratio = float64(scroll.H) / target 1968 scroll_off = scroll_offset / target 1969 1970 originalScroll := scroll 1971 1972 /* calculate scrollbar cursor bounds */ 1973 cursor.H = int(scroll_ratio*float64(scroll.H) - 2) 1974 if minh := FontHeight(font); cursor.H < minh { 1975 cursor.H = minh 1976 scroll.H -= minh 1977 } 1978 cursor.Y = scroll.Y + int(scroll_off*float64(scroll.H)) + 1 1979 cursor.W = scroll.W - 2 1980 cursor.X = scroll.X + 1 1981 1982 emptyNorth := scroll 1983 emptyNorth.H = cursor.Y - scroll.Y 1984 1985 emptySouth := scroll 1986 emptySouth.Y = cursor.Y + cursor.H 1987 emptySouth.H = (scroll.Y + scroll.H) - emptySouth.Y 1988 1989 /* update scrollbar */ 1990 out := &win.widgets 1991 state := out.PrevState(scroll) 1992 scroll_offset = scrollbarBehavior(&state, in, scroll, cursor, emptyNorth, emptySouth, scroll_offset, target, scroll_step, vertical) 1993 scroll_offset = scrollwheelBehavior(win, originalScroll, scrollwheel_bounds, scroll_offset, target, scroll_step) 1994 1995 scroll_off = scroll_offset / target 1996 cursor.Y = scroll.Y + int(scroll_off*float64(scroll.H)) 1997 1998 out.Add(state, scroll) 1999 drawScrollbar(win, state, style, originalScroll, cursor) 2000 2001 return scroll_offset 2002 } 2003 2004 func doScrollbarh(win *Window, scroll rect.Rect, offset float64, target float64, step float64, button_pixel_inc float64, style *nstyle.Scrollbar, in *Input, font font.Face) float64 { 2005 var cursor rect.Rect 2006 var scroll_step float64 2007 var scroll_offset float64 2008 var scroll_off float64 2009 var scroll_ratio float64 2010 2011 /* scrollbar background */ 2012 if scroll.H < 1 { 2013 scroll.H = 1 2014 } 2015 2016 if scroll.W < 2*scroll.H { 2017 scroll.Y = 2 * scroll.H 2018 } 2019 2020 if target <= float64(scroll.W) { 2021 return 0 2022 } 2023 2024 /* optional scrollbar buttons */ 2025 if style.ShowButtons { 2026 var scroll_w float64 2027 var button rect.Rect 2028 button.Y = scroll.Y 2029 button.W = scroll.H 2030 button.H = scroll.H 2031 2032 scroll_w = float64(scroll.W - 2*button.W) 2033 scroll_step = minFloat(step, button_pixel_inc) 2034 2035 /* decrement button */ 2036 button.X = scroll.X 2037 2038 if doButton(win, label.S(style.DecSymbol), button, &style.DecButton, in, true) { 2039 offset = offset - scroll_step 2040 } 2041 2042 /* increment button */ 2043 button.X = scroll.X + scroll.W - button.W 2044 2045 if doButton(win, label.S(style.IncSymbol), button, &style.IncButton, in, true) { 2046 offset = offset + scroll_step 2047 } 2048 2049 scroll.X = scroll.X + button.W 2050 scroll.W = int(scroll_w) 2051 } 2052 2053 /* calculate scrollbar constants */ 2054 scroll_step = minFloat(step, float64(scroll.W)) 2055 2056 scroll_offset = clampFloat(0, offset, target-float64(scroll.W)) 2057 scroll_ratio = float64(scroll.W) / target 2058 scroll_off = scroll_offset / target 2059 2060 /* calculate cursor bounds */ 2061 cursor.W = int(scroll_ratio*float64(scroll.W) - 2) 2062 cursor.X = scroll.X + int(scroll_off*float64(scroll.W)) + 1 2063 cursor.H = scroll.H - 2 2064 cursor.Y = scroll.Y + 1 2065 2066 emptyWest := scroll 2067 emptyWest.W = cursor.X - scroll.X 2068 2069 emptyEast := scroll 2070 emptyEast.X = cursor.X + cursor.W 2071 emptyEast.W = (scroll.X + scroll.W) - emptyEast.X 2072 2073 /* update scrollbar */ 2074 out := &win.widgets 2075 state := out.PrevState(scroll) 2076 scroll_offset = scrollbarBehavior(&state, in, scroll, cursor, emptyWest, emptyEast, scroll_offset, target, scroll_step, horizontal) 2077 2078 scroll_off = scroll_offset / target 2079 cursor.X = scroll.X + int(scroll_off*float64(scroll.W)) 2080 2081 out.Add(state, scroll) 2082 drawScrollbar(win, state, style, scroll, cursor) 2083 2084 return scroll_offset 2085 } 2086 2087 /////////////////////////////////////////////////////////////////////////////////// 2088 // TOGGLE BOXES 2089 /////////////////////////////////////////////////////////////////////////////////// 2090 2091 type toggleType int 2092 2093 const ( 2094 toggleCheck = toggleType(iota) 2095 toggleOption 2096 ) 2097 2098 func toggleBehavior(in *Input, b rect.Rect, state *nstyle.WidgetStates, active bool) bool { 2099 //TODO: rewrite using basicWidgetStateControl 2100 if in.Mouse.HoveringRect(b) { 2101 *state = nstyle.WidgetStateHovered 2102 } else { 2103 *state = nstyle.WidgetStateInactive 2104 } 2105 if *state == nstyle.WidgetStateHovered && in.Mouse.Clicked(pointer.ButtonLeft, b) { 2106 *state = nstyle.WidgetStateActive 2107 active = !active 2108 } 2109 2110 return active 2111 } 2112 2113 func toggleWidth(str string, type_ toggleType, style *nstyle.Toggle, font font.Face) int { 2114 w := 2*style.Padding.X + 2*style.TouchPadding.X + FontWidth(font, str) 2115 sw := FontHeight(font) + style.Padding.X 2116 w += sw 2117 if type_ == toggleOption { 2118 w += sw / 4 2119 } else { 2120 w += sw / 6 2121 } 2122 return w 2123 } 2124 2125 func doToggle(win *Window, r rect.Rect, active bool, str string, type_ toggleType, style *nstyle.Toggle, in *Input, font font.Face) bool { 2126 var bounds rect.Rect 2127 var select_ rect.Rect 2128 var cursor rect.Rect 2129 var label rect.Rect 2130 var cursor_pad int 2131 2132 r.W = max(r.W, FontHeight(font)+2*style.Padding.X) 2133 r.H = max(r.H, FontHeight(font)+2*style.Padding.Y) 2134 2135 /* add additional touch padding for touch screen devices */ 2136 bounds.X = r.X - style.TouchPadding.X 2137 bounds.Y = r.Y - style.TouchPadding.Y 2138 bounds.W = r.W + 2*style.TouchPadding.X 2139 bounds.H = r.H + 2*style.TouchPadding.Y 2140 2141 /* calculate the selector space */ 2142 select_.W = min(r.H, FontHeight(font)+style.Padding.Y) 2143 2144 select_.H = select_.W 2145 select_.X = r.X + style.Padding.X 2146 select_.Y = r.Y + (r.H/2 - select_.H/2) 2147 if type_ == toggleOption { 2148 cursor_pad = select_.W / 4 2149 } else { 2150 cursor_pad = select_.H / 6 2151 } 2152 2153 /* calculate the bounds of the cursor inside the selector */ 2154 select_.H = max(select_.W, cursor_pad*2) 2155 2156 cursor.H = select_.H - cursor_pad*2 2157 cursor.W = cursor.H 2158 cursor.X = select_.X + cursor_pad 2159 cursor.Y = select_.Y + cursor_pad 2160 2161 /* label behind the selector */ 2162 label.X = r.X + select_.W + style.Padding.X*2 2163 label.Y = select_.Y 2164 label.W = max(r.X+r.W, label.X+style.Padding.X) 2165 label.W -= (label.X + style.Padding.X) 2166 label.H = select_.W 2167 2168 /* update selector */ 2169 state := win.widgets.PrevState(bounds) 2170 active = toggleBehavior(in, bounds, &state, active) 2171 2172 win.widgets.Add(state, r) 2173 drawTogglebox(win, type_, state, style, active, label, select_, cursor, str) 2174 2175 return active 2176 } 2177 2178 // OptionText adds a radio button to win. If is_active is true the 2179 // radio button will be drawn selected. Returns true when the button 2180 // is clicked once. 2181 // You are responsible for ensuring that only one radio button is selected at once. 2182 func (win *Window) OptionText(text string, is_active bool) bool { 2183 style := &win.ctx.Style 2184 state, bounds, fitting := win.widget() 2185 if fitting != nil { 2186 fitting(toggleWidth(text, toggleOption, &style.Option, style.Font)) 2187 } 2188 if !state { 2189 return false 2190 } 2191 in := win.inputMaybe(state) 2192 is_active = doToggle(win, bounds, is_active, text, toggleOption, &style.Option, in, style.Font) 2193 return is_active 2194 } 2195 2196 // CheckboxText adds a checkbox button to win. Active will contain 2197 // the checkbox value. 2198 // Returns true when value changes. 2199 func (win *Window) CheckboxText(text string, active *bool) bool { 2200 state, bounds, fitting := win.widget() 2201 if fitting != nil { 2202 fitting(toggleWidth(text, toggleCheck, &win.ctx.Style.Checkbox, win.ctx.Style.Font)) 2203 } 2204 if !state { 2205 return false 2206 } 2207 in := win.inputMaybe(state) 2208 old_active := *active 2209 *active = doToggle(win, bounds, *active, text, toggleCheck, &win.ctx.Style.Checkbox, in, win.ctx.Style.Font) 2210 return *active != old_active 2211 } 2212 2213 /////////////////////////////////////////////////////////////////////////////////// 2214 // SLIDER 2215 /////////////////////////////////////////////////////////////////////////////////// 2216 2217 func sliderBehavior(state *nstyle.WidgetStates, cursor *rect.Rect, in *Input, style *nstyle.Slider, bounds rect.Rect, slider_min float64, slider_value float64, slider_max float64, slider_step float64, slider_steps int) float64 { 2218 exitstate := basicWidgetStateControl(state, in, bounds) 2219 2220 if *state == nstyle.WidgetStateActive { 2221 if !in.Mouse.Down(pointer.ButtonLeft) { 2222 *state = exitstate 2223 } else { 2224 d := in.Mouse.Pos.X - (cursor.X + cursor.W/2.0) 2225 var pxstep float64 = float64(bounds.W-(2*style.Padding.X)) / float64(slider_steps) 2226 2227 if math.Abs(float64(d)) >= pxstep { 2228 steps := float64(int(math.Abs(float64(d)) / pxstep)) 2229 if d > 0 { 2230 slider_value += slider_step * steps 2231 } else { 2232 slider_value -= slider_step * steps 2233 } 2234 slider_value = clampFloat(slider_min, slider_value, slider_max) 2235 } 2236 } 2237 } 2238 2239 return slider_value 2240 } 2241 2242 func doSlider(win *Window, bounds rect.Rect, minval float64, val float64, maxval float64, step float64, style *nstyle.Slider, in *Input) float64 { 2243 var slider_range float64 2244 var cursor_offset float64 2245 var cursor rect.Rect 2246 2247 /* remove padding from slider bounds */ 2248 bounds.X = bounds.X + style.Padding.X 2249 2250 bounds.Y = bounds.Y + style.Padding.Y 2251 bounds.H = max(bounds.H, 2*style.Padding.Y) 2252 bounds.W = max(bounds.W, 1+bounds.H+2*style.Padding.X) 2253 bounds.H -= 2 * style.Padding.Y 2254 bounds.W -= 2 * style.Padding.Y 2255 2256 /* optional buttons */ 2257 if style.ShowButtons { 2258 var button rect.Rect 2259 button.Y = bounds.Y 2260 button.W = bounds.H 2261 button.H = bounds.H 2262 2263 /* decrement button */ 2264 button.X = bounds.X 2265 2266 if doButton(win, label.S(style.DecSymbol), button, &style.DecButton, in, false) { 2267 val -= step 2268 } 2269 2270 /* increment button */ 2271 button.X = (bounds.X + bounds.W) - button.W 2272 2273 if doButton(win, label.S(style.IncSymbol), button, &style.IncButton, in, false) { 2274 val += step 2275 } 2276 2277 bounds.X = bounds.X + button.W + style.Spacing.X 2278 bounds.W = bounds.W - (2*button.W + 2*style.Spacing.X) 2279 } 2280 2281 /* make sure the provided values are correct */ 2282 slider_value := clampFloat(minval, val, maxval) 2283 slider_range = maxval - minval 2284 slider_steps := int(slider_range / step) 2285 2286 /* calculate slider virtual cursor bounds */ 2287 cursor_offset = (slider_value - minval) / step 2288 2289 cursor.H = bounds.H 2290 tempW := float64(bounds.W) / float64(slider_steps+1) 2291 cursor.W = int(tempW) 2292 cursor.X = bounds.X + int((tempW * cursor_offset)) 2293 cursor.Y = bounds.Y 2294 2295 out := &win.widgets 2296 state := out.PrevState(bounds) 2297 slider_value = sliderBehavior(&state, &cursor, in, style, bounds, minval, slider_value, maxval, step, slider_steps) 2298 out.Add(state, bounds) 2299 drawSlider(win, state, style, bounds, cursor, minval, slider_value, maxval) 2300 return slider_value 2301 } 2302 2303 // Adds a slider with a floating point value to win. 2304 // Returns true when the slider's value is changed. 2305 func (win *Window) SliderFloat(min_value float64, value *float64, max_value float64, value_step float64) bool { 2306 style := &win.ctx.Style 2307 state, bounds, _ := win.widget() 2308 if !state { 2309 return false 2310 } 2311 in := win.inputMaybe(state) 2312 2313 old_value := *value 2314 *value = doSlider(win, bounds, min_value, old_value, max_value, value_step, &style.Slider, in) 2315 return old_value > *value || old_value < *value 2316 } 2317 2318 // Adds a slider with an integer value to win. 2319 // Returns true when the slider's value changes. 2320 func (win *Window) SliderInt(min int, val *int, max int, step int) bool { 2321 value := float64(*val) 2322 ret := win.SliderFloat(float64(min), &value, float64(max), float64(step)) 2323 *val = int(value) 2324 return ret 2325 } 2326 2327 /////////////////////////////////////////////////////////////////////////////////// 2328 // PROGRESS BAR 2329 /////////////////////////////////////////////////////////////////////////////////// 2330 2331 func progressBehavior(state *nstyle.WidgetStates, in *Input, r rect.Rect, maxval int, value int, modifiable bool) int { 2332 if !modifiable { 2333 *state = nstyle.WidgetStateInactive 2334 return value 2335 } 2336 2337 exitstate := basicWidgetStateControl(state, in, r) 2338 2339 if *state == nstyle.WidgetStateActive { 2340 if !in.Mouse.Down(pointer.ButtonLeft) { 2341 *state = exitstate 2342 } else { 2343 ratio := maxFloat(0, float64(in.Mouse.Pos.X-r.X)) / float64(r.W) 2344 value = int(float64(maxval) * ratio) 2345 if value < 0 { 2346 value = 0 2347 } 2348 } 2349 } 2350 2351 if maxval > 0 && value > maxval { 2352 value = maxval 2353 } 2354 return value 2355 } 2356 2357 func doProgress(win *Window, bounds rect.Rect, value int, maxval int, modifiable bool, style *nstyle.Progress, in *Input) int { 2358 var prog_scale float64 2359 var cursor rect.Rect 2360 2361 /* calculate progressbar cursor */ 2362 cursor = padRect(bounds, style.Padding) 2363 prog_scale = float64(value) / float64(maxval) 2364 cursor.W = int(float64(cursor.W) * prog_scale) 2365 2366 /* update progressbar */ 2367 if value > maxval { 2368 value = maxval 2369 } 2370 2371 state := win.widgets.PrevState(bounds) 2372 value = progressBehavior(&state, in, bounds, maxval, value, modifiable) 2373 win.widgets.Add(state, bounds) 2374 drawProgress(win, state, style, bounds, cursor, value, maxval) 2375 2376 return value 2377 } 2378 2379 // Adds a progress bar to win. if is_modifiable is true the progress 2380 // bar will be user modifiable through click-and-drag. 2381 // Returns true when the progress bar values is modified. 2382 func (win *Window) Progress(cur *int, maxval int, is_modifiable bool) bool { 2383 style := &win.ctx.Style 2384 state, bounds, _ := win.widget() 2385 if !state { 2386 return false 2387 } 2388 2389 in := win.inputMaybe(state) 2390 old_value := *cur 2391 *cur = doProgress(win, bounds, *cur, maxval, is_modifiable, &style.Progress, in) 2392 return *cur != old_value 2393 } 2394 2395 /////////////////////////////////////////////////////////////////////////////////// 2396 // PROPERTY 2397 /////////////////////////////////////////////////////////////////////////////////// 2398 2399 type FilterFunc func(c rune) bool 2400 2401 func FilterDefault(c rune) bool { 2402 return true 2403 } 2404 2405 func FilterDecimal(c rune) bool { 2406 return !((c < '0' || c > '9') && c != '-') 2407 } 2408 2409 func FilterFloat(c rune) bool { 2410 return !((c < '0' || c > '9') && c != '.' && c != '-') 2411 } 2412 2413 func (ed *TextEditor) propertyBehavior(ws *nstyle.WidgetStates, in *Input, property rect.Rect, label rect.Rect, edit rect.Rect, empty rect.Rect) (drag bool, delta int) { 2414 if ed.propertyStatus == propertyDefault { 2415 if buttonBehaviorDo(ws, edit, in, false) { 2416 ed.propertyStatus = propertyEdit 2417 } else if in.Mouse.IsClickDownInRect(pointer.ButtonLeft, label, true) { 2418 ed.propertyStatus = propertyDrag 2419 } else if in.Mouse.IsClickDownInRect(pointer.ButtonLeft, empty, true) { 2420 ed.propertyStatus = propertyDrag 2421 } 2422 } 2423 2424 if ed.propertyStatus == propertyDrag { 2425 if in.Mouse.Released(pointer.ButtonLeft) { 2426 ed.propertyStatus = propertyDefault 2427 } else { 2428 delta = in.Mouse.Delta.X 2429 drag = true 2430 } 2431 } 2432 2433 if ed.propertyStatus == propertyDefault { 2434 if in.Mouse.HoveringRect(property) { 2435 *ws = nstyle.WidgetStateHovered 2436 } else { 2437 *ws = nstyle.WidgetStateInactive 2438 } 2439 } else { 2440 *ws = nstyle.WidgetStateActive 2441 } 2442 2443 return 2444 } 2445 2446 type doPropertyRet int 2447 2448 const ( 2449 doPropertyStay = doPropertyRet(iota) 2450 doPropertyInc 2451 doPropertyDec 2452 doPropertyDrag 2453 doPropertySet 2454 ) 2455 2456 func digits(n int) int { 2457 if n <= 0 { 2458 n = 1 2459 } 2460 return int(math.Log2(float64(n)) + 1) 2461 } 2462 2463 func propertyWidth(max int, style *nstyle.Property, font font.Face) int { 2464 return 2*FontHeight(font)/2 + digits(max)*FontWidth(font, "0") + 4*style.Padding.X + 2*style.Border 2465 } 2466 2467 func (win *Window) doProperty(property rect.Rect, name string, text string, filter FilterFunc, in *Input) (ret doPropertyRet, delta int, ed *TextEditor) { 2468 ret = doPropertyStay 2469 style := &win.ctx.Style.Property 2470 font := win.ctx.Style.Font 2471 2472 // left decrement button 2473 var left rect.Rect 2474 left.H = FontHeight(font) / 2 2475 left.W = left.H 2476 left.X = property.X + style.Border + style.Padding.X 2477 left.Y = property.Y + style.Border + property.H/2.0 - left.H/2 2478 2479 // text label 2480 size := FontWidth(font, name) 2481 var lblrect rect.Rect 2482 lblrect.X = left.X + left.W + style.Padding.X 2483 lblrect.W = size + 2*style.Padding.X 2484 lblrect.Y = property.Y + style.Border 2485 lblrect.H = property.H - 2*style.Border 2486 2487 /* right increment button */ 2488 var right rect.Rect 2489 right.Y = left.Y 2490 right.W = left.W 2491 right.H = left.H 2492 right.X = property.X + property.W - (right.W + style.Padding.X) 2493 2494 ws := win.widgets.PrevState(property) 2495 oldws := ws 2496 if ws == nstyle.WidgetStateActive && win.editor != nil { 2497 ed = win.editor 2498 } else { 2499 ed = &TextEditor{} 2500 ed.init(win) 2501 ed.Buffer = []rune(text) 2502 } 2503 2504 size = FontWidth(font, string(ed.Buffer)) + FontWidth(font, "i") 2505 2506 /* edit */ 2507 var edit rect.Rect 2508 edit.W = size + 2*style.Padding.X 2509 edit.X = right.X - (edit.W + style.Padding.X) 2510 edit.Y = property.Y + style.Border + 1 2511 edit.H = property.H - (2*style.Border + 2) 2512 2513 /* empty left space activator */ 2514 var empty rect.Rect 2515 empty.W = edit.X - (lblrect.X + lblrect.W) 2516 empty.X = lblrect.X + lblrect.W 2517 empty.Y = property.Y 2518 empty.H = property.H 2519 2520 /* update property */ 2521 old := ed.propertyStatus == propertyEdit 2522 2523 drag, delta := ed.propertyBehavior(&ws, in, property, lblrect, edit, empty) 2524 if drag { 2525 ret = doPropertyDrag 2526 } 2527 if ws == nstyle.WidgetStateActive { 2528 ed.Active = true 2529 win.editor = ed 2530 } else if oldws == nstyle.WidgetStateActive { 2531 win.editor = nil 2532 } 2533 ed.win.widgets.Add(ws, property) 2534 drawProperty(ed.win, style, property, lblrect, ws, name) 2535 2536 /* execute right and left button */ 2537 if doButton(ed.win, label.S(style.SymLeft), left, &style.DecButton, in, false) { 2538 ret = doPropertyDec 2539 } 2540 if doButton(ed.win, label.S(style.SymRight), right, &style.IncButton, in, false) { 2541 ret = doPropertyInc 2542 } 2543 2544 active := ed.propertyStatus == propertyEdit 2545 if !old && active { 2546 /* property has been activated so setup buffer */ 2547 ed.Cursor = len(ed.Buffer) 2548 } 2549 ed.Flags = EditAlwaysInsertMode | EditNoHorizontalScroll 2550 ed.doEdit(edit, &style.Edit, in, false, false, false) 2551 active = ed.Active 2552 2553 if active && in.Keyboard.Pressed(gkey.CodeReturnEnter) { 2554 active = !active 2555 } 2556 2557 if old && !active { 2558 /* property is now not active so convert edit text to value*/ 2559 ed.propertyStatus = propertyDefault 2560 ed.Active = false 2561 ret = doPropertySet 2562 } 2563 2564 return 2565 } 2566 2567 // Adds a property widget to win for floating point properties. 2568 // A property widget will display a text label, a small text editor 2569 // for the property value and one up and one down button. 2570 // The value can be modified by editing the text value, by clicking 2571 // the up/down buttons (which will increase/decrease the value by step) 2572 // or by clicking and dragging over the label. 2573 // Returns true when the property's value is changed 2574 func (win *Window) PropertyFloat(name string, min float64, val *float64, max, step, inc_per_pixel float64, prec int) (changed bool) { 2575 s, bounds, fitting := win.widget() 2576 if fitting != nil { 2577 fitting(propertyWidth(int(max+1), &win.ctx.Style.Property, win.ctx.Style.Font)) 2578 } 2579 if !s { 2580 return 2581 } 2582 in := win.inputMaybe(s) 2583 text := strconv.FormatFloat(*val, 'G', prec, 32) 2584 ret, delta, ed := win.doProperty(bounds, name, text, FilterFloat, in) 2585 switch ret { 2586 case doPropertyDec: 2587 *val -= step 2588 case doPropertyInc: 2589 *val += step 2590 case doPropertyDrag: 2591 *val += float64(delta) * inc_per_pixel 2592 case doPropertySet: 2593 *val, _ = strconv.ParseFloat(string(ed.Buffer), 64) 2594 } 2595 changed = ret != doPropertyStay 2596 if changed { 2597 *val = clampFloat(min, *val, max) 2598 } 2599 return 2600 } 2601 2602 // Same as PropertyFloat but with integer values. 2603 func (win *Window) PropertyInt(name string, min int, val *int, max, step, inc_per_pixel int) (changed bool) { 2604 s, bounds, fitting := win.widget() 2605 if fitting != nil { 2606 fitting(propertyWidth(max, &win.ctx.Style.Property, win.ctx.Style.Font)) 2607 } 2608 if !s { 2609 return 2610 } 2611 in := win.inputMaybe(s) 2612 text := strconv.Itoa(*val) 2613 ret, delta, ed := win.doProperty(bounds, name, text, FilterDecimal, in) 2614 switch ret { 2615 case doPropertyDec: 2616 *val -= step 2617 case doPropertyInc: 2618 *val += step 2619 case doPropertyDrag: 2620 *val += delta * inc_per_pixel 2621 case doPropertySet: 2622 *val, _ = strconv.Atoi(string(ed.Buffer)) 2623 } 2624 changed = ret != doPropertyStay 2625 if changed { 2626 *val = clampInt(min, *val, max) 2627 } 2628 return 2629 } 2630 2631 /////////////////////////////////////////////////////////////////////////////////// 2632 // POPUP 2633 /////////////////////////////////////////////////////////////////////////////////// 2634 2635 func (ctx *context) nonblockOpen(flags WindowFlags, body rect.Rect, header rect.Rect, updateFn UpdateFn) *Window { 2636 popup := createWindow(ctx, "") 2637 popup.idx = len(ctx.Windows) 2638 popup.updateFn = updateFn 2639 ctx.Windows = append(ctx.Windows, popup) 2640 2641 popup.Bounds = body 2642 popup.layout = &panel{} 2643 popup.flags = flags 2644 popup.flags |= WindowBorder | windowPopup 2645 popup.flags |= WindowDynamic | windowSub 2646 popup.flags |= windowNonblock 2647 2648 popup.header = header 2649 2650 if updateFn == nil { 2651 popup.specialPanelBegin() 2652 } 2653 return popup 2654 } 2655 2656 func (ctx *context) popupOpen(title string, flags WindowFlags, rect rect.Rect, scale bool, updateFn UpdateFn) { 2657 popup := createWindow(ctx, title) 2658 popup.idx = len(ctx.Windows) 2659 popup.updateFn = updateFn 2660 if updateFn == nil { 2661 panic("nil update function") 2662 } 2663 ctx.Windows = append(ctx.Windows, popup) 2664 ctx.dockedWindowFocus = 0 2665 2666 if scale { 2667 rect.X = ctx.scale(rect.X) 2668 rect.Y = ctx.scale(rect.Y) 2669 rect.W = ctx.scale(rect.W) 2670 rect.H = ctx.scale(rect.H) 2671 } 2672 2673 if ((rect.X+rect.W <= 0) && (rect.Y+rect.H <= 0)) || ((rect.X >= ctx.Windows[0].Bounds.W) && (rect.Y >= ctx.Windows[0].Bounds.H)) { 2674 // out of bounds 2675 rect.X = 0 2676 rect.Y = 0 2677 } 2678 2679 if rect.X == 0 && rect.Y == 0 && flags&WindowNonmodal != 0 { 2680 rect.X, rect.Y = ctx.autoPosition() 2681 } 2682 2683 popup.Bounds = rect 2684 popup.layout = &panel{} 2685 popup.flags = flags | WindowBorder | windowSub | windowPopup 2686 } 2687 2688 func (ctx *context) autoPosition() (int, int) { 2689 x, y := ctx.autopos.X, ctx.autopos.Y 2690 2691 z := FontHeight(ctx.Style.Font) + 2.0*ctx.Style.NormalWindow.Header.Padding.Y 2692 2693 ctx.autopos.X += ctx.scale(z) 2694 ctx.autopos.Y += ctx.scale(z) 2695 2696 if ctx.Windows[0].Bounds.W != 0 && ctx.Windows[0].Bounds.H != 0 { 2697 if ctx.autopos.X >= ctx.Windows[0].Bounds.W || ctx.autopos.Y >= ctx.Windows[0].Bounds.H { 2698 ctx.autopos.X = 0 2699 ctx.autopos.Y = 0 2700 } 2701 } 2702 2703 return x, y 2704 } 2705 2706 // Programmatically closes this window 2707 func (win *Window) Close() { 2708 if win.idx != 0 { 2709 win.close = true 2710 } 2711 } 2712 2713 /////////////////////////////////////////////////////////////////////////////////// 2714 // CONTEXTUAL 2715 /////////////////////////////////////////////////////////////////////////////////// 2716 2717 // Opens a contextual menu with maximum size equal to 'size'. 2718 // Specify size == image.Point{} if you want a menu big enough to fit its larges MenuItem 2719 func (win *Window) ContextualOpen(flags WindowFlags, size image.Point, trigger_bounds rect.Rect, updateFn UpdateFn) *Window { 2720 if popup := win.ctx.Windows[len(win.ctx.Windows)-1]; popup.header == trigger_bounds { 2721 popup.specialPanelBegin() 2722 return popup 2723 } 2724 if size == (image.Point{}) { 2725 size.X = nk_null_rect.W 2726 size.Y = nk_null_rect.H 2727 flags = flags | windowHDynamic 2728 } 2729 size.X = win.ctx.scale(size.X) 2730 size.Y = win.ctx.scale(size.Y) 2731 if trigger_bounds.W > 0 && trigger_bounds.H > 0 { 2732 if !win.Input().Mouse.Clicked(pointer.ButtonRight, trigger_bounds) { 2733 return nil 2734 } 2735 } 2736 2737 var body rect.Rect 2738 body.X = win.ctx.Input.Mouse.Pos.X 2739 body.Y = win.ctx.Input.Mouse.Pos.Y 2740 body.W = size.X 2741 body.H = size.Y 2742 2743 if flags&WindowContextualReplace != 0 { 2744 if popup := win.ctx.Windows[len(win.ctx.Windows)-1]; popup.flags&windowContextual != 0 { 2745 body.X = popup.Bounds.X 2746 body.Y = popup.Bounds.Y 2747 } 2748 } 2749 2750 atomic.AddInt32(&win.ctx.changed, 1) 2751 return win.ctx.nonblockOpen(flags|windowContextual|WindowNoScrollbar, body, trigger_bounds, updateFn) 2752 } 2753 2754 // MenuItem adds a menu item 2755 func (win *Window) MenuItem(lbl label.Label) bool { 2756 style := &win.ctx.Style 2757 state, bounds := win.widgetFitting(style.ContextualButton.Padding) 2758 if !state { 2759 return false 2760 } 2761 2762 if win.flags&windowHDynamic != 0 { 2763 w := FontWidth(style.Font, lbl.Text) + 2*style.ContextualButton.Padding.X 2764 if w > win.menuItemWidth { 2765 win.menuItemWidth = w 2766 } 2767 } 2768 2769 in := win.inputMaybe(state) 2770 if doButton(win, lbl, bounds, &style.ContextualButton, in, false) { 2771 win.Close() 2772 return true 2773 } 2774 2775 return false 2776 } 2777 2778 /////////////////////////////////////////////////////////////////////////////////// 2779 // TOOLTIP 2780 /////////////////////////////////////////////////////////////////////////////////// 2781 2782 const tooltipWindowTitle = "__##Tooltip##__" 2783 2784 // Displays a tooltip window. 2785 func (win *Window) TooltipOpen(width int, scale bool, updateFn UpdateFn) { 2786 in := &win.ctx.Input 2787 2788 if scale { 2789 width = win.ctx.scale(width) 2790 } 2791 2792 var bounds rect.Rect 2793 bounds.W = width 2794 bounds.H = nk_null_rect.H 2795 bounds.X = (in.Mouse.Pos.X + 1) 2796 bounds.Y = (in.Mouse.Pos.Y + 1) 2797 2798 win.ctx.popupOpen(tooltipWindowTitle, WindowDynamic|WindowNoScrollbar|windowTooltip, bounds, false, updateFn) 2799 } 2800 2801 // Shows a tooltip window containing the specified text. 2802 func (win *Window) Tooltip(text string) { 2803 if text == "" { 2804 return 2805 } 2806 2807 /* fetch configuration data */ 2808 padding := win.ctx.Style.TooltipWindow.Padding 2809 item_spacing := win.ctx.Style.TooltipWindow.Spacing 2810 2811 /* calculate size of the text and tooltip */ 2812 text_width := FontWidth(win.ctx.Style.Font, text) + win.ctx.scale(4*padding.X) + win.ctx.scale(2*item_spacing.X) 2813 text_height := FontHeight(win.ctx.Style.Font) 2814 2815 win.TooltipOpen(text_width, false, func(tw *Window) { 2816 tw.RowScaled(text_height).Dynamic(1) 2817 tw.Label(text, "LC") 2818 }) 2819 } 2820 2821 /////////////////////////////////////////////////////////////////////////////////// 2822 // COMBO-BOX 2823 /////////////////////////////////////////////////////////////////////////////////// 2824 2825 // Adds a drop-down list to win. 2826 func (win *Window) Combo(lbl label.Label, height int, updateFn UpdateFn) *Window { 2827 s, header, _ := win.widget() 2828 if !s { 2829 return nil 2830 } 2831 2832 in := win.inputMaybe(s) 2833 state := win.widgets.PrevState(header) 2834 is_clicked := buttonBehaviorDo(&state, header, in, false) 2835 2836 switch lbl.Kind { 2837 case label.ColorLabel: 2838 win.widgets.Add(state, header) 2839 drawComboColor(win, state, header, is_clicked, lbl.Color) 2840 case label.ImageLabel: 2841 win.widgets.Add(state, header) 2842 drawComboImage(win, state, header, is_clicked, lbl.Img) 2843 case label.ImageTextLabel: 2844 win.widgets.Add(state, header) 2845 drawComboImageText(win, state, header, is_clicked, lbl.Text, lbl.Img) 2846 case label.SymbolLabel: 2847 win.widgets.Add(state, header) 2848 drawComboSymbol(win, state, header, is_clicked, lbl.Symbol) 2849 case label.SymbolTextLabel: 2850 win.widgets.Add(state, header) 2851 drawComboSymbolText(win, state, header, is_clicked, lbl.Symbol, lbl.Text) 2852 case label.TextLabel: 2853 win.widgets.Add(state, header) 2854 drawComboText(win, state, header, is_clicked, lbl.Text) 2855 } 2856 2857 if popup := win.ctx.Windows[len(win.ctx.Windows)-1]; updateFn == nil && popup.header == header { 2858 popup.specialPanelBegin() 2859 return popup 2860 } 2861 2862 if !is_clicked { 2863 return nil 2864 } 2865 2866 height = win.ctx.scale(height) 2867 2868 var body rect.Rect 2869 body.X = header.X 2870 body.W = header.W 2871 body.Y = header.Y + header.H - 1 2872 body.H = height 2873 2874 return win.ctx.nonblockOpen(windowCombo, body, header, updateFn) 2875 } 2876 2877 // Adds a drop-down list to win. The contents are specified by items, 2878 // with selected being the index of the selected item. 2879 func (win *Window) ComboSimple(items []string, selected int, item_height int) int { 2880 if len(items) == 0 { 2881 return selected 2882 } 2883 2884 item_height = win.ctx.scale(item_height) 2885 item_padding := win.ctx.Style.Combo.ButtonPadding.Y 2886 window_padding := win.style().Padding.Y 2887 max_height := (len(items)+1)*item_height + item_padding*3 + window_padding*2 2888 if w := win.Combo(label.T(items[selected]), max_height, nil); w != nil { 2889 w.RowScaled(item_height).Dynamic(1) 2890 for i := range items { 2891 if w.MenuItem(label.TA(items[i], "LC")) { 2892 selected = i 2893 } 2894 } 2895 } 2896 return selected 2897 } 2898 2899 /////////////////////////////////////////////////////////////////////////////////// 2900 // MENU 2901 /////////////////////////////////////////////////////////////////////////////////// 2902 2903 // Adds a menu to win with a text label. 2904 // If width == 0 the width will be automatically adjusted to fit the largest MenuItem 2905 func (win *Window) Menu(lbl label.Label, width int, updateFn UpdateFn) *Window { 2906 state, header, fitting := win.widget() 2907 if fitting != nil { 2908 fitting(buttonWidth(lbl, &win.ctx.Style.MenuButton, win.ctx.Style.Font)) 2909 } 2910 if !state { 2911 return nil 2912 } 2913 2914 in := &Input{} 2915 if win.toplevel() { 2916 in = &win.ctx.Input 2917 in.Mouse.clip = win.cmds.Clip 2918 } 2919 is_clicked := doButton(win, lbl, header, &win.ctx.Style.MenuButton, in, false) 2920 2921 if popup := win.ctx.Windows[len(win.ctx.Windows)-1]; updateFn == nil && popup.header == header { 2922 popup.specialPanelBegin() 2923 return popup 2924 } 2925 2926 if !is_clicked { 2927 return nil 2928 } 2929 2930 flags := windowMenu | WindowNoScrollbar 2931 2932 width = win.ctx.scale(width) 2933 2934 if width == 0 { 2935 width = nk_null_rect.W 2936 flags = flags | windowHDynamic 2937 } 2938 2939 var body rect.Rect 2940 body.X = header.X 2941 body.W = width 2942 body.Y = header.Y + header.H 2943 body.H = (win.layout.Bounds.Y + win.layout.Bounds.H) - body.Y 2944 2945 return win.ctx.nonblockOpen(flags, body, header, updateFn) 2946 } 2947 2948 /////////////////////////////////////////////////////////////////////////////////// 2949 // GROUPS 2950 /////////////////////////////////////////////////////////////////////////////////// 2951 2952 // Creates a group of widgets. 2953 // Group are useful for creating lists as well as splitting a main 2954 // window into tiled subwindows. 2955 // Items that you want to add to the group should be added to the 2956 // returned window. 2957 func (win *Window) GroupBegin(title string, flags WindowFlags) *Window { 2958 sw := win.groupWnd[title] 2959 if sw == nil { 2960 sw = createWindow(win.ctx, title) 2961 sw.parent = win 2962 win.groupWnd[title] = sw 2963 sw.Scrollbar.X = 0 2964 sw.Scrollbar.Y = 0 2965 sw.layout = &panel{} 2966 } 2967 2968 sw.curNode = sw.rootNode 2969 sw.widgets.reset() 2970 sw.cmds.Reset() 2971 sw.idx = win.idx 2972 2973 sw.cmds.Clip = win.cmds.Clip 2974 2975 state, bounds, _ := win.widget() 2976 if !state { 2977 return nil 2978 } 2979 2980 flags |= windowSub | windowGroup 2981 2982 if win.flags&windowEnabled != 0 { 2983 flags |= windowEnabled 2984 } 2985 2986 sw.Bounds = bounds 2987 sw.flags = flags 2988 2989 panelBegin(win.ctx, sw, title) 2990 2991 sw.layout.Offset = &sw.Scrollbar 2992 2993 win.usingSub = true 2994 2995 return sw 2996 } 2997 2998 // Signals that you are done adding widgets to a group. 2999 func (win *Window) GroupEnd() { 3000 panelEnd(win.ctx, win) 3001 win.parent.usingSub = false 3002 3003 // immediate drawing 3004 win.parent.cmds.Commands = append(win.parent.cmds.Commands, win.cmds.Commands...) 3005 }