diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5edd255a..033f46fa 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -52,7 +52,10 @@ body: description: What version of giu are you running? options: - master - - v0.5.6 (latest) + - (latest) + - v0.6.1 + - v0.6 + - v0.5.6 validations: required: true - type: input diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d0f610d4..32446199 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] steps: - name: Checkout out source code - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v3 with: fetch-depth: 0 submodules: 'true' @@ -30,13 +30,13 @@ jobs: sudo apt-get --allow-releaseinfo-change update sudo apt-get install -y libgtk-3-dev libasound2-dev libxxf86vm-dev - name: Set up Go environment - uses: actions/setup-go@v2.1.4 + uses: actions/setup-go@v3 with: - go-version: 1.16.x + go-version: 1.18.x id: go - name: Cache Go modules - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index b63fe60a..2844aeb7 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -10,12 +10,12 @@ jobs: run: | sudo apt-get --allow-releaseinfo-change update sudo apt-get install -y libgtk-3-dev libasound2-dev libxxf86vm-dev - - uses: actions/checkout@v2.3.5 + - uses: actions/checkout@v3 with: fetch-depth: 2 - - uses: actions/setup-go@v2 + - uses: actions/setup-go@v3 with: - go-version: '1.16' + go-version: '1.18.x' - name: Run coverage run: go test -race -coverprofile=coverage.txt -covermode=atomic - name: Upload coverage to Codecov diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml index 0ebb0bc0..19d41cfb 100644 --- a/.github/workflows/golangci-lint.yaml +++ b/.github/workflows/golangci-lint.yaml @@ -13,16 +13,19 @@ jobs: name: lint runs-on: ubuntu-latest steps: + - uses: actions/setup-go@v3 + with: + go-version: '1.18.x' - name: Set up LibGL, Mesa & X11 libraries run: | sudo apt-get --allow-releaseinfo-change update sudo apt-get install -y libgtk-3-dev libasound2-dev libxxf86vm-dev - - uses: actions/checkout@v2.3.5 + - uses: actions/checkout@v3 - name: golangci-lint - uses: golangci/golangci-lint-action@v2.5.2 + uses: golangci/golangci-lint-action@v3 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: v1.42.1 + version: v1.45 # Optional: working directory, useful for monorepos # working-directory: somedir @@ -33,11 +36,9 @@ jobs: # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true - # Optional: if set to true then the action will use pre-installed Go. - # skip-go-installation: true - # Optional: if set to true then the action don't cache or restore ~/go/pkg. # skip-pkg-cache: true # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. + # skip-build-cache: true diff --git a/.gitignore b/.gitignore index 2c148ca8..16914be1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ # vim cache files *.swp +# JetBrains idea configuration +.idea + # Test binary, built with `go test -c` *.test diff --git a/.golangci.yml b/.golangci.yml index 530561d9..b372da93 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -73,7 +73,7 @@ linters: - prealloc - predeclared - promlinter - #- revive + #- revive - rowserrcheck - staticcheck - structcheck diff --git a/Alignment.go b/Alignment.go index 96ee184f..6667710a 100644 --- a/Alignment.go +++ b/Alignment.go @@ -7,26 +7,77 @@ import ( "github.com/AllenDang/imgui-go" ) +// AlignmentType represents a bype of alignment to use with AlignSetter. type AlignmentType byte const ( + // AlignLeft is here just for clearity. + // if set, no action is taken so don't use it. AlignLeft AlignmentType = iota + // AlignCenter centers widget. AlignCenter + // AlignRight aligns a widget to right side of window. AlignRight ) -type AlignmentSetter struct { - alignType AlignmentType - layout Layout - id string +// AlignManually allows to apply alignment manually. +// As long as AlignSetter is really EXPERIMENTAL feature +// and may fail randomly, the following method is supposed to +// always work, as long as you set it up correctly. +// To use it just pass a single widget with its exact width. +// be sure to apply widget's size by using "Size" method! +// forceApplyWidth argument allows you to ask giu to force-set width +// of `widget` +// NOTE that forcing width doesn't work for each widget type! For example +// Button won't work because its size is set by argument to imgui call +// not PushWidth api. +func AlignManually(alignmentType AlignmentType, widget Widget, widgetWidth float32, forceApplyWidth bool) Widget { + return Custom(func() { + spacingX, _ := GetItemSpacing() + availableW, _ := GetAvailableRegion() + + var dummyX float32 + + switch alignmentType { + case AlignLeft: + widget.Build() + return + case AlignCenter: + dummyX = (availableW-widgetWidth)/2 - spacingX + case AlignRight: + dummyX = availableW - widgetWidth - spacingX + } + + Dummy(dummyX, 0).Build() + + if forceApplyWidth { + PushItemWidth(widgetWidth) + defer PopItemWidth() + } + + imgui.SameLine() + widget.Build() + }) } -// Align sets widgets alignment. +var _ Widget = &AlignmentSetter{} + +// AlignmentSetter allows to align to right / center a widget or widgets group. +// NOTE: Because of AlignSetter uses experimental GetWidgetWidth, +// it is experimental too. // usage: see examples/align // +// list of known bugs: // - BUG: DatePickerWidget doesn't work properly // - BUG: there is some bug with SelectableWidget // - BUG: ComboWidget and ComboCustomWidgets doesn't work properly. +type AlignmentSetter struct { + alignType AlignmentType + layout Layout + id string +} + +// Align sets widgets alignment. func Align(at AlignmentType) *AlignmentSetter { return &AlignmentSetter{ alignType: at, @@ -40,13 +91,16 @@ func (a *AlignmentSetter) To(widgets ...Widget) *AlignmentSetter { return a } -// ID allows to manually set AlignmentSetter ID (it shouldn't be used -// in a normal conditions). +// ID allows to manually set AlignmentSetter ID +// NOTE: there isn't any known reason to use this method, however +// it is here for some random cases. YOU DON'T NEED TO USE IT +// in normal conditions. func (a *AlignmentSetter) ID(id string) *AlignmentSetter { a.id = id return a } +// Build implements Widget interface. func (a *AlignmentSetter) Build() { if a.layout == nil { return @@ -107,16 +161,16 @@ func (a *AlignmentSetter) Build() { // widget will be processed) // // here is a list of known bugs: -// - BUG: clicking bug - when widget is clickable, it is unable to be -// clicked see: +// - BUG: user can interact with invisible widget (created by GetWidgetWidth) // - https://github.com/AllenDang/giu/issues/341 // - https://github.com/ocornut/imgui/issues/4588 -// - BUG: text pasted into input text is pasted twice -// (see: https://github.com/AllenDang/giu/issues/340) // // if you find anything else, please report it on // https://github.com/AllenDang/giu Any contribution is appreciated! func GetWidgetWidth(w Widget) (result float32) { + imgui.PushID(GenAutoID("GetWIdgetWidthMeasurement")) + defer imgui.PopID() + // save cursor position before rendering currentPos := GetCursorPos() diff --git a/Canvas.go b/Canvas.go index 603df6f2..b329291b 100644 --- a/Canvas.go +++ b/Canvas.go @@ -113,7 +113,7 @@ func (c *Canvas) AddQuadFilled(p1, p2, p3, p4 image.Point, col color.Color) { c.drawlist.AddQuadFilled(ToVec2(p1), ToVec2(p2), ToVec2(p3), ToVec2(p4), ToVec4Color(col)) } -// Stateful path API, add points then finish with PathFillConvex() or PathStroke() +// Stateful path API, add points then finish with PathFillConvex() or PathStroke(). func (c *Canvas) PathClear() { c.drawlist.PathClear() diff --git a/ClickableWidgets.go b/ClickableWidgets.go index b2405c8e..0181b0ae 100644 --- a/ClickableWidgets.go +++ b/ClickableWidgets.go @@ -20,16 +20,20 @@ type ButtonWidget struct { onClick func() } -// Build implements Widget interface. -func (b *ButtonWidget) Build() { - if b.disabled { - imgui.BeginDisabled(true) - defer imgui.EndDisabled() +// Button creates a new button widget. +func Button(label string) *ButtonWidget { + return &ButtonWidget{ + id: GenAutoID(label), + width: 0, + height: 0, + onClick: nil, } +} - if imgui.ButtonV(Context.FontAtlas.tStr(b.id), imgui.Vec2{X: b.width, Y: b.height}) && b.onClick != nil { - b.onClick() - } +// Buttonf creates button with formated label +// NOTE: works like fmt.Sprintf (see `go doc fmt`). +func Buttonf(format string, args ...any) *ButtonWidget { + return Button(fmt.Sprintf(format, args...)) } // OnClick sets callback called when button is clicked @@ -52,20 +56,16 @@ func (b *ButtonWidget) Size(width, height float32) *ButtonWidget { return b } -// Button creates a new button widget. -func Button(label string) *ButtonWidget { - return &ButtonWidget{ - id: GenAutoID(label), - width: 0, - height: 0, - onClick: nil, +// Build implements Widget interface. +func (b *ButtonWidget) Build() { + if b.disabled { + imgui.BeginDisabled(true) + defer imgui.EndDisabled() } -} -// Buttonf creates button with formated label -// NOTE: works like fmt.Sprintf (see `go doc fmt`). -func Buttonf(format string, args ...interface{}) *ButtonWidget { - return Button(fmt.Sprintf(format, args...)) + if imgui.ButtonV(Context.FontAtlas.tStr(b.id), imgui.Vec2{X: b.width, Y: b.height}) && b.onClick != nil { + b.onClick() + } } var _ Widget = &ArrowButtonWidget{} @@ -77,12 +77,6 @@ type ArrowButtonWidget struct { onClick func() } -// OnClick adds callback called when button is clicked. -func (b *ArrowButtonWidget) OnClick(onClick func()) *ArrowButtonWidget { - b.onClick = onClick - return b -} - // ArrowButton creates ArrowButtonWidget. func ArrowButton(dir Direction) *ArrowButtonWidget { return &ArrowButtonWidget{ @@ -92,6 +86,12 @@ func ArrowButton(dir Direction) *ArrowButtonWidget { } } +// OnClick adds callback called when button is clicked. +func (b *ArrowButtonWidget) OnClick(onClick func()) *ArrowButtonWidget { + b.onClick = onClick + return b +} + // ID allows to manually set widget's id. func (b *ArrowButtonWidget) ID(id string) *ArrowButtonWidget { b.id = id @@ -107,16 +107,13 @@ func (b *ArrowButtonWidget) Build() { var _ Widget = &SmallButtonWidget{} +// SmallButtonWidget is like a button but without frame padding. type SmallButtonWidget struct { id string onClick func() } -func (b *SmallButtonWidget) OnClick(onClick func()) *SmallButtonWidget { - b.onClick = onClick - return b -} - +// SmallButton constructs a new small button widget. func SmallButton(id string) *SmallButtonWidget { return &SmallButtonWidget{ id: GenAutoID(id), @@ -124,10 +121,18 @@ func SmallButton(id string) *SmallButtonWidget { } } -func SmallButtonf(format string, args ...interface{}) *SmallButtonWidget { +// SmallButtonf allows to set formated label for small button. +// It calls SmallButton(fmt.Sprintf(label, args...)). +func SmallButtonf(format string, args ...any) *SmallButtonWidget { return SmallButton(fmt.Sprintf(format, args...)) } +// OnClick adds OnClick event. +func (b *SmallButtonWidget) OnClick(onClick func()) *SmallButtonWidget { + b.onClick = onClick + return b +} + // Build implements Widget interface. func (b *SmallButtonWidget) Build() { if imgui.SmallButton(Context.FontAtlas.tStr(b.id)) && b.onClick != nil { @@ -137,6 +142,9 @@ func (b *SmallButtonWidget) Build() { var _ Widget = &InvisibleButtonWidget{} +// InvisibleButtonWidget is a clickable region. +// NOTE: you may want to display other widgets on this button. +// to do so, you may move drawing cursor back by Get/SetCursor(Screen)Pos. type InvisibleButtonWidget struct { id string width float32 @@ -144,30 +152,34 @@ type InvisibleButtonWidget struct { onClick func() } +// InvisibleButton constructs a new invisible button widget. +func InvisibleButton() *InvisibleButtonWidget { + return &InvisibleButtonWidget{ + id: GenAutoID("InvisibleButton"), + width: 0, + height: 0, + onClick: nil, + } +} + +// Size sets button's size. func (b *InvisibleButtonWidget) Size(width, height float32) *InvisibleButtonWidget { b.width, b.height = width, height return b } +// OnClick sets click event. func (b *InvisibleButtonWidget) OnClick(onClick func()) *InvisibleButtonWidget { b.onClick = onClick return b } +// ID allows to manually set widget's id (no need to use in normal conditions). func (b *InvisibleButtonWidget) ID(id string) *InvisibleButtonWidget { b.id = id return b } -func InvisibleButton() *InvisibleButtonWidget { - return &InvisibleButtonWidget{ - id: GenAutoID("InvisibleButton"), - width: 0, - height: 0, - onClick: nil, - } -} - // Build implements Widget interface. func (b *InvisibleButtonWidget) Build() { if imgui.InvisibleButton(Context.FontAtlas.tStr(b.id), imgui.Vec2{X: b.width, Y: b.height}) && b.onClick != nil { @@ -177,6 +189,7 @@ func (b *InvisibleButtonWidget) Build() { var _ Widget = &ImageButtonWidget{} +// ImageButtonWidget is similar to ButtonWidget but with image texture instead of text label. type ImageButtonWidget struct { texture *Texture width float32 @@ -189,9 +202,24 @@ type ImageButtonWidget struct { onClick func() } +// ImageButton constructs image buton widget. +func ImageButton(texture *Texture) *ImageButtonWidget { + return &ImageButtonWidget{ + texture: texture, + width: 50, + height: 50, + uv0: image.Point{X: 0, Y: 0}, + uv1: image.Point{X: 1, Y: 1}, + framePadding: -1, + bgColor: colornames.Black, + tintColor: colornames.White, + onClick: nil, + } +} + // Build implements Widget interface. func (b *ImageButtonWidget) Build() { - if b.texture == nil && b.texture.id == 0 { + if b.texture == nil || b.texture.id == 0 { return } @@ -206,58 +234,55 @@ func (b *ImageButtonWidget) Build() { } } +// Size sets BUTTONS size. +// NOTE: image size is button size - 2 * frame padding. func (b *ImageButtonWidget) Size(width, height float32) *ImageButtonWidget { b.width, b.height = width, height return b } +// OnClick sets click event. func (b *ImageButtonWidget) OnClick(onClick func()) *ImageButtonWidget { b.onClick = onClick return b } +// UV sets image's uv. func (b *ImageButtonWidget) UV(uv0, uv1 image.Point) *ImageButtonWidget { b.uv0, b.uv1 = uv0, uv1 return b } +// BgColor sets button's background color. func (b *ImageButtonWidget) BgColor(bgColor color.Color) *ImageButtonWidget { b.bgColor = bgColor return b } +// TintColor sets tit color for image. func (b *ImageButtonWidget) TintColor(tintColor color.Color) *ImageButtonWidget { b.tintColor = tintColor return b } +// FramePadding sets button's frame padding (set 0 to fit image to the frame). func (b *ImageButtonWidget) FramePadding(padding int) *ImageButtonWidget { b.framePadding = padding return b } -func ImageButton(texture *Texture) *ImageButtonWidget { - return &ImageButtonWidget{ - texture: texture, - width: 50, - height: 50, - uv0: image.Point{X: 0, Y: 0}, - uv1: image.Point{X: 1, Y: 1}, - framePadding: -1, - bgColor: colornames.Black, - tintColor: colornames.White, - onClick: nil, - } -} - var _ Widget = &ImageButtonWithRgbaWidget{} +// ImageButtonWithRgbaWidget does similar to ImageButtonWIdget, +// but implements image.Image instead of giu.Texture. It is probably +// more useful than the original ImageButtonWIdget. type ImageButtonWithRgbaWidget struct { *ImageButtonWidget rgba image.Image id string } +// ImageButtonWithRgba creates a new widget. func ImageButtonWithRgba(rgba image.Image) *ImageButtonWithRgbaWidget { return &ImageButtonWithRgbaWidget{ id: GenAutoID("ImageButtonWithRgba"), @@ -266,31 +291,37 @@ func ImageButtonWithRgba(rgba image.Image) *ImageButtonWithRgbaWidget { } } +// Size sets button's size. func (b *ImageButtonWithRgbaWidget) Size(width, height float32) *ImageButtonWithRgbaWidget { b.ImageButtonWidget.Size(width, height) return b } +// OnClick sets click events. func (b *ImageButtonWithRgbaWidget) OnClick(onClick func()) *ImageButtonWithRgbaWidget { b.ImageButtonWidget.OnClick(onClick) return b } +// UV sets image's uv color. func (b *ImageButtonWithRgbaWidget) UV(uv0, uv1 image.Point) *ImageButtonWithRgbaWidget { b.ImageButtonWidget.UV(uv0, uv1) return b } +// BgColor sets button's background color. func (b *ImageButtonWithRgbaWidget) BgColor(bgColor color.Color) *ImageButtonWithRgbaWidget { b.ImageButtonWidget.BgColor(bgColor) return b } +// TintColor sets image's tint color. func (b *ImageButtonWithRgbaWidget) TintColor(tintColor color.Color) *ImageButtonWithRgbaWidget { b.ImageButtonWidget.TintColor(tintColor) return b } +// FramePadding sets frame padding (see (*ImageButtonWidget).TintColor). func (b *ImageButtonWithRgbaWidget) FramePadding(padding int) *ImageButtonWithRgbaWidget { b.ImageButtonWidget.FramePadding(padding) return b @@ -299,14 +330,14 @@ func (b *ImageButtonWithRgbaWidget) FramePadding(padding int) *ImageButtonWithRg // Build implements Widget interface. func (b *ImageButtonWithRgbaWidget) Build() { if state := Context.GetState(b.id); state == nil { - Context.SetState(b.id, &ImageState{}) + Context.SetState(b.id, &imageState{}) NewTextureFromRgba(b.rgba, func(tex *Texture) { - Context.SetState(b.id, &ImageState{texture: tex}) + Context.SetState(b.id, &imageState{texture: tex}) }) } else { var isOk bool - imgState, isOk := state.(*ImageState) + imgState, isOk := state.(*imageState) Assert(isOk, "ImageButtonWithRgbaWidget", "Build", "got unexpected type of widget's state") b.ImageButtonWidget.texture = imgState.texture } @@ -323,10 +354,12 @@ type CheckboxWidget struct { onChange func() } -// Build implements Widget interface. -func (c *CheckboxWidget) Build() { - if imgui.Checkbox(Context.FontAtlas.tStr(c.text), c.selected) && c.onChange != nil { - c.onChange() +// Checkbox creates a new CheckboxWidget. +func Checkbox(text string, selected *bool) *CheckboxWidget { + return &CheckboxWidget{ + text: GenAutoID(text), + selected: selected, + onChange: nil, } } @@ -336,45 +369,50 @@ func (c *CheckboxWidget) OnChange(onChange func()) *CheckboxWidget { return c } -// Checkbox creates a new CheckboxWidget. -func Checkbox(text string, selected *bool) *CheckboxWidget { - return &CheckboxWidget{ - text: GenAutoID(text), - selected: selected, - onChange: nil, +// Build implements Widget interface. +func (c *CheckboxWidget) Build() { + if imgui.Checkbox(Context.FontAtlas.tStr(c.text), c.selected) && c.onChange != nil { + c.onChange() } } var _ Widget = &RadioButtonWidget{} +// RadioButtonWidget is a small, round button. +// It is common to use it for single-choice questions. +// see examples/widgets. type RadioButtonWidget struct { text string active bool onChange func() } -// Build implements Widget interface. -func (r *RadioButtonWidget) Build() { - if imgui.RadioButton(Context.FontAtlas.tStr(r.text), r.active) && r.onChange != nil { - r.onChange() +// RadioButton creates a radio buton. +func RadioButton(text string, active bool) *RadioButtonWidget { + return &RadioButtonWidget{ + text: GenAutoID(text), + active: active, + onChange: nil, } } +// OnChange adds callback when button's state gets changed. func (r *RadioButtonWidget) OnChange(onChange func()) *RadioButtonWidget { r.onChange = onChange return r } -func RadioButton(text string, active bool) *RadioButtonWidget { - return &RadioButtonWidget{ - text: GenAutoID(text), - active: active, - onChange: nil, +// Build implements Widget interface. +func (r *RadioButtonWidget) Build() { + if imgui.RadioButton(Context.FontAtlas.tStr(r.text), r.active) && r.onChange != nil { + r.onChange() } } var _ Widget = &SelectableWidget{} +// SelectableWidget is a window-width button with a label which can get selected (highlighted). +// useful for certain lists. type SelectableWidget struct { label string selected bool @@ -385,6 +423,7 @@ type SelectableWidget struct { onDClick func() } +// Selectable constructs a selectable widget. func Selectable(label string) *SelectableWidget { return &SelectableWidget{ label: GenAutoID(label), @@ -396,25 +435,30 @@ func Selectable(label string) *SelectableWidget { } } -func Selectablef(format string, args ...interface{}) *SelectableWidget { +// Selectablef creates a selectable widget with formated label. +func Selectablef(format string, args ...any) *SelectableWidget { return Selectable(fmt.Sprintf(format, args...)) } +// Selected sets if selectable widget is selected. func (s *SelectableWidget) Selected(selected bool) *SelectableWidget { s.selected = selected return s } +// Flags add flags. func (s *SelectableWidget) Flags(flags SelectableFlags) *SelectableWidget { s.flags = flags return s } +// Size sets selectable's size. func (s *SelectableWidget) Size(width, height float32) *SelectableWidget { s.width, s.height = width, height return s } +// OnClick sets on click event. func (s *SelectableWidget) OnClick(onClick func()) *SelectableWidget { s.onClick = onClick return s @@ -422,6 +466,7 @@ func (s *SelectableWidget) OnClick(onClick func()) *SelectableWidget { // OnDClick handles mouse left button's double click event. // SelectableFlagsAllowDoubleClick will set once tonDClick callback is notnull. +// NOTE: IT IS DEPRECATED and could be removed. Use EventHandler instead. func (s *SelectableWidget) OnDClick(onDClick func()) *SelectableWidget { s.onDClick = onDClick return s @@ -445,6 +490,9 @@ func (s *SelectableWidget) Build() { var _ Widget = &TreeNodeWidget{} +// TreeNodeWidget is a a wide button with open/close state. +// if is opened, the `layout` is displayed below the widget. +// It can be used to create certain lists, advanced settings sections e.t.c. type TreeNodeWidget struct { label string flags TreeNodeFlags @@ -452,6 +500,7 @@ type TreeNodeWidget struct { eventHandler func() } +// TreeNode creates a new tree node widget. func TreeNode(label string) *TreeNodeWidget { return &TreeNodeWidget{ label: Context.FontAtlas.tStr(label), @@ -461,10 +510,12 @@ func TreeNode(label string) *TreeNodeWidget { } } -func TreeNodef(format string, args ...interface{}) *TreeNodeWidget { +// TreeNodef adds TreeNode with formatted label. +func TreeNodef(format string, args ...any) *TreeNodeWidget { return TreeNode(fmt.Sprintf(format, args...)) } +// Flags sets flags. func (t *TreeNodeWidget) Flags(flags TreeNodeFlags) *TreeNodeWidget { t.flags = flags return t @@ -472,11 +523,13 @@ func (t *TreeNodeWidget) Flags(flags TreeNodeFlags) *TreeNodeWidget { // Event create TreeNode with eventHandler // You could detect events (e.g. IsItemClicked IsMouseDoubleClicked etc...) and handle them for TreeNode inside eventHandler. +// Deprecated: Use EventHandler instead! func (t *TreeNodeWidget) Event(handler func()) *TreeNodeWidget { t.eventHandler = handler return t } +// Layout sets layout to be displayed when tree node is opened. func (t *TreeNodeWidget) Layout(widgets ...Widget) *TreeNodeWidget { t.layout = Layout(widgets) return t diff --git a/CodeEditor.go b/CodeEditor.go index 442f843a..5f2cdc61 100644 --- a/CodeEditor.go +++ b/CodeEditor.go @@ -39,6 +39,7 @@ type CodeEditorWidget struct { border bool } +// CodeEditor creates new code editor widget. func CodeEditor() *CodeEditorWidget { return &CodeEditorWidget{ title: GenAutoID("##CodeEditor"), @@ -135,6 +136,7 @@ func (ce *CodeEditorWidget) GetCurrentLineText() string { } // GetCursorPos returns cursor position. +// (in characters). func (ce *CodeEditorWidget) GetCursorPos() (x, y int) { return ce.getState().editor.GetCursorPos() } @@ -159,10 +161,13 @@ func (ce *CodeEditorWidget) SelectWordUnderCursor() { ce.getState().editor.SelectWordUnderCursor() } +// IsTextChanged returns true if the editable text was changed in the frame. func (ce *CodeEditorWidget) IsTextChanged() bool { return ce.getState().editor.IsTextChanged() } +// GetScreenCursorPos returns cursor position on the screen. +// (in pixels). func (ce *CodeEditorWidget) GetScreenCursorPos() (x, y int) { return ce.getState().editor.GetScreenCursorPos() } diff --git a/Context.go b/Context.go index 5936af8b..df0b1818 100644 --- a/Context.go +++ b/Context.go @@ -4,11 +4,14 @@ import ( "sync" "github.com/AllenDang/imgui-go" + "gopkg.in/eapache/queue.v1" ) // Context represents a giu context. var Context context +// Disposable should be implemented by all states stored in context. +// Dispose method is called when state is removed from context. type Disposable interface { Dispose() } @@ -19,6 +22,10 @@ type state struct { } type context struct { + // TODO: should be handled by mainthread tbh + // see https://github.com/faiface/mainthread/pull/4 + isRunning bool + renderer imgui.Renderer platform imgui.Platform @@ -32,6 +39,8 @@ type context struct { InputHandler InputHandler FontAtlas FontAtlas + + textureLoadingQueue *queue.Queue } func CreateContext(p imgui.Platform, r imgui.Renderer) context { @@ -69,7 +78,7 @@ func (c *context) IO() imgui.IO { } func (c *context) invalidAllState() { - c.state.Range(func(k, v interface{}) bool { + c.state.Range(func(k, v any) bool { if s, ok := v.(*state); ok { s.valid = false } @@ -78,7 +87,7 @@ func (c *context) invalidAllState() { } func (c *context) cleanState() { - c.state.Range(func(k, v interface{}) bool { + c.state.Range(func(k, v any) bool { if s, ok := v.(*state); ok { if !s.valid { c.state.Delete(k) @@ -96,7 +105,7 @@ func (c *context) SetState(id string, data Disposable) { c.state.Store(id, &state{valid: true, data: data}) } -func (c *context) GetState(id string) interface{} { +func (c *context) GetState(id string) any { if v, ok := c.state.Load(id); ok { if s, ok := v.(*state); ok { s.valid = true diff --git a/Events.go b/Events.go index 502ef3d5..6d5364a8 100644 --- a/Events.go +++ b/Events.go @@ -2,14 +2,17 @@ package giu import "github.com/AllenDang/imgui-go" +// MouseButton represents imgui.MoseButton. type MouseButton int +// mouse buttons. const ( MouseButtonLeft MouseButton = 0 MouseButtonRight MouseButton = 1 MouseButtonMiddle MouseButton = 2 ) +// IsItemHovered returns true if mouse is over the item. func IsItemHovered() bool { return imgui.IsItemHovered() } diff --git a/ExtraWidgets.go b/ExtraWidgets.go index 1f537796..524ed6a7 100644 --- a/ExtraWidgets.go +++ b/ExtraWidgets.go @@ -161,8 +161,6 @@ func (v *VSplitterWidget) Build() { canvas.AddRectFilled(pt.Add(ptMin), pt.Add(ptMax), c, 0, 0) } -var _ Widget = &TreeTableRowWidget{} - type TreeTableRowWidget struct { label string flags TreeNodeFlags @@ -187,8 +185,8 @@ func (ttr *TreeTableRowWidget) Flags(flags TreeNodeFlags) *TreeTableRowWidget { return ttr } -// Build implements Widget interface. -func (ttr *TreeTableRowWidget) Build() { +// BuildTreeTableRow executes table row building steps. +func (ttr *TreeTableRowWidget) BuildTreeTableRow() { imgui.TableNextRow(0, 0) imgui.TableNextColumn() @@ -214,7 +212,7 @@ func (ttr *TreeTableRowWidget) Build() { if len(ttr.children) > 0 && open { for _, c := range ttr.children { - c.Build() + c.BuildTreeTableRow() } imgui.TreePop() @@ -287,13 +285,13 @@ func (tt *TreeTableWidget) Build() { if len(tt.columns) > 0 { for _, col := range tt.columns { - col.Build() + col.BuildTableColumn() } imgui.TableHeadersRow() } for _, row := range tt.rows { - row.Build() + row.BuildTreeTableRow() } imgui.EndTable() @@ -349,7 +347,7 @@ func (c *ConditionWidget) Build() { } // RangeBuilder batch create widgets and render only which is visible. -func RangeBuilder(id string, values []interface{}, builder func(int, interface{}) Widget) Layout { +func RangeBuilder(id string, values []any, builder func(int, any) Widget) Layout { var layout Layout layout = append(layout, Custom(func() { imgui.PushID(id) })) @@ -448,11 +446,13 @@ func (l *ListBoxWidget) Build() { child := Child().Border(l.border).Size(l.width, l.height).Layout(Layout{ Custom(func() { - var clipper imgui.ListClipper + clipper := imgui.NewListClipper() + defer clipper.Delete() + clipper.Begin(len(l.items)) for clipper.Step() { - for i := clipper.DisplayStart; i < clipper.DisplayEnd; i++ { + for i := clipper.DisplayStart(); i < clipper.DisplayEnd(); i++ { selected := i == state.selectedIndex item := l.items[i] Selectable(item).Selected(selected).Flags(SelectableFlagsAllowDoubleClick).OnClick(func() { @@ -496,18 +496,21 @@ func (l *ListBoxWidget) Build() { var _ Widget = &DatePickerWidget{} type DatePickerWidget struct { - id string - date *time.Time - width float32 - onChange func() + id string + date *time.Time + width float32 + onChange func() + format string + startOfWeek time.Weekday } func DatePicker(id string, date *time.Time) *DatePickerWidget { return &DatePickerWidget{ - id: GenAutoID(id), - date: date, - width: 100, - onChange: func() {}, // small hack - prevent giu from setting nil cb (skip nil check later) + id: GenAutoID(id), + date: date, + width: 100, + startOfWeek: time.Sunday, + onChange: func() {}, // small hack - prevent giu from setting nil cb (skip nil check later) } } @@ -523,6 +526,30 @@ func (d *DatePickerWidget) OnChange(onChange func()) *DatePickerWidget { return d } +func (d *DatePickerWidget) Format(format string) *DatePickerWidget { + d.format = format + return d +} + +func (d *DatePickerWidget) StartOfWeek(weekday time.Weekday) *DatePickerWidget { + d.startOfWeek = weekday + return d +} + +func (d *DatePickerWidget) getFormat() string { + if d.format == "" { + return "2006-01-02" // default + } + return d.format +} + +func (d *DatePickerWidget) offsetDay(offset int) time.Weekday { + day := (int(d.startOfWeek) + offset) % 7 + // offset may be negative, thus day can be negative + day = (day + 7) % 7 + return time.Weekday(day) +} + // Build implements Widget interface. func (d *DatePickerWidget) Build() { if d.date == nil { @@ -537,7 +564,7 @@ func (d *DatePickerWidget) Build() { defer PopItemWidth() } - if imgui.BeginComboV(d.id+"##Combo", d.date.Format("2006-01-02"), imgui.ComboFlagsHeightLargest) { + if imgui.BeginComboV(d.id+"##Combo", d.date.Format(d.getFormat()), imgui.ComboFlagsHeightLargest) { // --- [Build year widget] --- imgui.AlignTextToFramePadding() @@ -574,14 +601,11 @@ func (d *DatePickerWidget) Build() { days := d.getDaysGroups() // Create calendar (widget) - columns := []*TableColumnWidget{ - TableColumn("S"), - TableColumn("M"), - TableColumn("T"), - TableColumn("W"), - TableColumn("T"), - TableColumn("F"), - TableColumn("S"), + columns := make([]*TableColumnWidget, 7) + + for i := 0; i < 7; i++ { + firstChar := d.offsetDay(i).String()[0:1] + columns[i] = TableColumn(firstChar) } // Build day widgets @@ -615,17 +639,12 @@ func (d *DatePickerWidget) getDaysGroups() (days [][]int) { lastDay := firstDay.AddDate(0, 1, 0).Add(time.Nanosecond * -1) // calculate first week - days = append(days, []int{}) + days = append(days, make([]int, 7)) monthDay := 1 - for i := 0; i < 7; i++ { - // check for the first month weekday - if i < int(firstDay.Weekday()) { - days[0] = append(days[0], 0) - continue - } - - days[0] = append(days[0], monthDay) + emptyDaysInFirstWeek := (int(firstDay.Weekday()) - int(d.startOfWeek) + 7) % 7 + for i := emptyDaysInFirstWeek; i < 7; i++ { + days[0][i] = monthDay monthDay++ } @@ -659,16 +678,10 @@ func (d *DatePickerWidget) calendarField(day int) Widget { } Selectable(fmt.Sprintf("%02d", day)).Selected(isToday).OnClick(func() { - *d.date, _ = time.ParseInLocation( - "2006-01-02", - fmt.Sprintf("%d-%02d-%02d", - d.date.Year(), - d.date.Month(), - day, - ), - time.Local, - ) - + *d.date = time.Date( + d.date.Year(), d.date.Month(), day, + 0, 0, 0, 0, + d.date.Location()) d.onChange() }).Build() diff --git a/Flags.go b/Flags.go index 3a78b28b..8877c5d9 100644 --- a/Flags.go +++ b/Flags.go @@ -2,8 +2,10 @@ package giu import "github.com/AllenDang/imgui-go" +// InputTextFlags represents input text flags. type InputTextFlags int +// input text flags. const ( // InputTextFlagsNone sets everything default. InputTextFlagsNone InputTextFlags = imgui.InputTextFlagsNone @@ -48,8 +50,10 @@ const ( InputTextFlagsCharsScientific InputTextFlags = imgui.InputTextFlagsCharsScientific ) +// WindowFlags represents a window flags (see (*WindowWidget).Flags. type WindowFlags int +// window flags. const ( // WindowFlagsNone default = 0. WindowFlagsNone WindowFlags = imgui.WindowFlagsNone @@ -112,8 +116,10 @@ const ( WindowFlagsNoInputs WindowFlags = imgui.WindowFlagsNoInputs ) +// ComboFlags represents imgui.ComboFlags. type ComboFlags int +// combo flags list. const ( // ComboFlagsNone default = 0. ComboFlagsNone ComboFlags = imgui.ComboFlagsNone @@ -137,6 +143,7 @@ const ( // SelectableFlags represents imgui.SelectableFlags. type SelectableFlags int +// selectable flags list. const ( // SelectableFlagsNone default = 0. SelectableFlagsNone SelectableFlags = imgui.SelectableFlagsNone @@ -153,6 +160,7 @@ const ( // TabItemFlags represents tab item flags. type TabItemFlags int +// tab item flags list. const ( // TabItemFlagsNone default = 0. TabItemFlagsNone TabItemFlags = imgui.TabItemFlagsNone @@ -170,8 +178,10 @@ const ( TabItemFlagsNoPushID TabItemFlags = imgui.TabItemFlagsNoPushID ) +// TabBarFlags represents imgui.TabBarFlags. type TabBarFlags int +// tab bar flags list. const ( // TabBarFlagsNone default = 0. TabBarFlagsNone TabBarFlags = imgui.TabBarFlagsNone @@ -204,6 +214,7 @@ const ( // TreeNodeFlags represents tree node widget flags. type TreeNodeFlags int +// tree node flags list. const ( // TreeNodeFlagsNone default = 0. TreeNodeFlagsNone TreeNodeFlags = imgui.TreeNodeFlagsNone @@ -250,6 +261,7 @@ const ( // FocusedFlags represents imgui.FocusedFlags. type FocusedFlags int +// focused flags list. const ( FocusedFlagsNone = imgui.FocusedFlagsNone FocusedFlagsChildWindows = imgui.FocusedFlagsChildWindows // Return true if any children of the window is focused @@ -263,6 +275,7 @@ const ( // HoveredFlags represents a hovered flags. type HoveredFlags int +// hovered flags list. const ( // HoveredFlagsNone Return true if directly over the item/window, not obstructed by another window, // not obstructed by an active popup or modal blocking inputs under them. @@ -287,6 +300,7 @@ const ( // ColorEditFlags for ColorEdit3V(), etc. type ColorEditFlags int +// list of color edit flags. const ( // ColorEditFlagsNone default = 0. ColorEditFlagsNone ColorEditFlags = imgui.ColorEditFlagsNone @@ -372,9 +386,10 @@ const ( TableFlagsScrollY TableFlags = TableFlags(imgui.TableFlags_ScrollY) TableFlagsSortMulti TableFlags = TableFlags(imgui.TableFlags_SortMulti) TableFlagsSortTristate TableFlags = TableFlags(imgui.TableFlags_SortTristate) - TableFlagsSizingMask_ TableFlags = TableFlags(imgui.TableFlags_SizingMask_) + TableFlagsSizingMask TableFlags = TableFlags(imgui.TableFlags_SizingMask_) ) +// TableRowFlags represents table row flags. type TableRowFlags int // table row flags:. @@ -387,6 +402,7 @@ const ( // TableColumnFlags represents a flags for table column (see (*TableColumnWidget).Flags()). type TableColumnFlags int +// table column flags list. const ( // Input configuration flags. TableColumnFlagsNone TableColumnFlags = TableColumnFlags(imgui.TableColumnFlags_None) @@ -414,14 +430,17 @@ const ( TableColumnFlagsIsHovered TableColumnFlags = TableColumnFlags(imgui.TableColumnFlags_IsHovered) // [Internal] Combinations and masks. - TableColumnFlagsWidthMask_ TableColumnFlags = TableColumnFlags(imgui.TableColumnFlags_WidthMask_) - TableColumnFlagsIndentMask_ TableColumnFlags = TableColumnFlags(imgui.TableColumnFlags_IndentMask_) - TableColumnFlagsStatusMask_ TableColumnFlags = TableColumnFlags(imgui.TableColumnFlags_StatusMask_) - TableColumnFlagsNoDirectResize_ TableColumnFlags = TableColumnFlags(imgui.TableColumnFlags_NoDirectResize_) + TableColumnFlagsWidthMask TableColumnFlags = TableColumnFlags(imgui.TableColumnFlags_WidthMask_) + TableColumnFlagsIndentMask TableColumnFlags = TableColumnFlags(imgui.TableColumnFlags_IndentMask_) + TableColumnFlagsStatusMask TableColumnFlags = TableColumnFlags(imgui.TableColumnFlags_StatusMask_) + TableColumnFlagsNoDirectResize TableColumnFlags = TableColumnFlags(imgui.TableColumnFlags_NoDirectResize_) ) +// SliderFlags represents imgui.SliderFlags +// TODO: Hard-reffer to these constants. type SliderFlags int +// slider flags. const ( SliderFlagsNone SliderFlags = 0 // Clamp value to min/max bounds when input manually with CTRL+Click. By default CTRL+Click allows going out of bounds. @@ -438,8 +457,10 @@ const ( SliderFlagsInvalidMask SliderFlags = 0x7000000F ) +// PlotFlags represents imgui.ImPlotFlags. type PlotFlags int +// plot flags. const ( PlotFlagsNone = PlotFlags(imgui.ImPlotFlags_None) PlotFlagsNoTitle = PlotFlags(imgui.ImPlotFlags_NoTitle) @@ -458,8 +479,10 @@ const ( PlotFlagsCanvasOnly = PlotFlags(imgui.ImPlotFlags_CanvasOnly) ) +// PlotAxisFlags represents imgui.ImPlotAxisFlags. type PlotAxisFlags int +// plot axis flags. const ( PlotAxisFlagsNone PlotAxisFlags = PlotAxisFlags(imgui.ImPlotAxisFlags_None) PlotAxisFlagsNoLabel PlotAxisFlags = PlotAxisFlags(imgui.ImPlotAxisFlags_NoLabel) diff --git a/FontAtlasProsessor.go b/FontAtlasProsessor.go index 7aeb722a..241b40ce 100644 --- a/FontAtlasProsessor.go +++ b/FontAtlasProsessor.go @@ -14,6 +14,7 @@ import ( const ( preRegisterString = " \"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" windows = "windows" + defaultFontSize = 14 ) // FontInfo represents a giu implementation of imgui font. @@ -28,17 +29,35 @@ func (f *FontInfo) String() string { return fmt.Sprintf("%s:%.2f", f.fontName, f.size) } +func (f *FontInfo) SetSize(size float32) *FontInfo { + result := *f + result.size = size + + for _, i := range Context.FontAtlas.extraFonts { + if i.String() == result.String() { + return &result + } + } + + Context.FontAtlas.extraFonts = append(Context.FontAtlas.extraFonts, result) + Context.FontAtlas.shouldRebuildFontAtlas = true + + return &result +} + type FontAtlas struct { shouldRebuildFontAtlas bool stringMap sync.Map // key is rune, value indicates whether it's a new rune. defaultFonts []FontInfo extraFonts []FontInfo extraFontMap map[string]*imgui.Font + fontSize float32 } func newFontAtlas() FontAtlas { result := FontAtlas{ extraFontMap: make(map[string]*imgui.Font), + fontSize: defaultFontSize, } // Pre register numbers @@ -48,32 +67,36 @@ func newFontAtlas() FontAtlas { switch runtime.GOOS { case "darwin": // English font - result.registerDefaultFont("Menlo", 14) + result.registerDefaultFont("Menlo", result.fontSize) // Chinese font - result.registerDefaultFont("STHeiti", 13) + result.registerDefaultFont("STHeiti", result.fontSize-1) // Jananese font - result.registerDefaultFont("ヒラギノ角ゴシック W0", 17) + result.registerDefaultFont("ヒラギノ角ゴシック W0", result.fontSize+3) // Korean font - result.registerDefaultFont("AppleSDGothicNeo", 16) + result.registerDefaultFont("AppleSDGothicNeo", result.fontSize+2) case windows: // English font - result.registerDefaultFont("Calibri", 16) + result.registerDefaultFont("Calibri", result.fontSize+2) // Chinese font - result.registerDefaultFont("MSYH", 16) + result.registerDefaultFont("MSYH", result.fontSize+2) // Japanese font - result.registerDefaultFont("MSGOTHIC", 16) + result.registerDefaultFont("MSGOTHIC", result.fontSize+2) // Korean font - result.registerDefaultFont("MALGUNSL", 16) + result.registerDefaultFont("MALGUNSL", result.fontSize+2) case "linux": // English fonts result.registerDefaultFonts([]FontInfo{ { fontName: "FreeSans.ttf", - size: 15, + size: result.fontSize + 1, }, { fontName: "FiraCode-Medium", - size: 15, + size: result.fontSize + 1, + }, + { + fontName: "sans", + size: result.fontSize + 1, }, }) } @@ -81,6 +104,11 @@ func newFontAtlas() FontAtlas { return result } +// SetDefaultFontSize sets the default font size. Invoke this before MasterWindow.NewMasterWindow(..). +func (a *FontAtlas) SetDefaultFontSize(size float32) { + a.fontSize = size +} + // SetDefaultFont changes default font. func (a *FontAtlas) SetDefaultFont(fontName string, size float32) { fontPath, err := findfont.Find(fontName) @@ -103,6 +131,10 @@ func (a *FontAtlas) SetDefaultFontFromBytes(fontBytes []byte, size float32) { }, a.defaultFonts...) } +func (a *FontAtlas) GetDefaultFonts() []FontInfo { + return a.defaultFonts +} + // AddFont adds font by name, if the font is found, return *FontInfo, otherwise return nil. // To use added font, use giu.Style().SetFont(...). func (a *FontAtlas) AddFont(fontName string, size float32) *FontInfo { @@ -200,7 +232,7 @@ func (a *FontAtlas) rebuildFontAtlas() { var sb strings.Builder - a.stringMap.Range(func(k, v interface{}) bool { + a.stringMap.Range(func(k, v any) bool { a.stringMap.Store(k, true) if ks, ok := k.(rune); ok { sb.WriteRune(ks) @@ -266,6 +298,7 @@ func (a *FontAtlas) rebuildFontAtlas() { } else { f = fonts.AddFontFromMemoryTTFV(fontInfo.fontByte, fontInfo.size, imgui.DefaultFontConfig, ranges.Data()) } + a.extraFontMap[fontInfo.String()] = &f } diff --git a/ImageWidgets.go b/ImageWidgets.go index a09aeefd..e411a1d2 100644 --- a/ImageWidgets.go +++ b/ImageWidgets.go @@ -1,15 +1,14 @@ package giu import ( - "bytes" ctx "context" "fmt" "image" "image/color" + "net/http" "time" "github.com/AllenDang/imgui-go" - resty "github.com/go-resty/resty/v2" ) var _ Widget = &ImageWidget{} @@ -40,11 +39,13 @@ func Image(texture *Texture) *ImageWidget { } } +// Uv allows to specify uv parameters. func (i *ImageWidget) Uv(uv0, uv1 image.Point) *ImageWidget { i.uv0, i.uv1 = uv0, uv1 return i } +// TintColor sets image's tint color. func (i *ImageWidget) TintColor(tintColor color.Color) *ImageWidget { i.tintColor = tintColor return i @@ -64,7 +65,9 @@ func (i *ImageWidget) OnClick(cb func()) *ImageWidget { // Size sets image size. func (i *ImageWidget) Size(width, height float32) *ImageWidget { - i.width, i.height = width, height + // Size image with DPI scaling + factor := Context.GetPlatform().GetContentScale() + i.width, i.height = width*factor, height*factor return i } @@ -98,14 +101,15 @@ func (i *ImageWidget) Build() { imgui.ImageV(i.texture.id, size, ToVec2(i.uv0), ToVec2(i.uv1), ToVec4Color(i.tintColor), ToVec4Color(i.borderColor)) } -type ImageState struct { +type imageState struct { loading bool failure bool cancel ctx.CancelFunc texture *Texture } -func (is *ImageState) Dispose() { +// Dispose cleans imageState (implements Disposable interface). +func (is *imageState) Dispose() { is.texture = nil // Cancel ongoing image downloaidng if is.loading && is.cancel != nil { @@ -115,12 +119,17 @@ func (is *ImageState) Dispose() { var _ Widget = &ImageWithRgbaWidget{} +// ImageWithRgbaWidget wrapps ImageWidget. +// It is more useful because it doesn't make you to care about +// imgui textures. You can just pass golang-native image.Image and +// display it in giu. type ImageWithRgbaWidget struct { id string rgba image.Image img *ImageWidget } +// ImageWithRgba creates ImageWithRgbaWidget. func ImageWithRgba(rgba image.Image) *ImageWithRgbaWidget { return &ImageWithRgbaWidget{ id: GenAutoID("ImageWithRgba"), @@ -129,11 +138,13 @@ func ImageWithRgba(rgba image.Image) *ImageWithRgbaWidget { } } +// Size sets image's size. func (i *ImageWithRgbaWidget) Size(width, height float32) *ImageWithRgbaWidget { i.img.Size(width, height) return i } +// OnClick sets click callback. func (i *ImageWithRgbaWidget) OnClick(cb func()) *ImageWithRgbaWidget { i.img.OnClick(cb) return i @@ -142,9 +153,9 @@ func (i *ImageWithRgbaWidget) OnClick(cb func()) *ImageWithRgbaWidget { // Build implements Widget interface. func (i *ImageWithRgbaWidget) Build() { if i.rgba != nil { - var imgState *ImageState + var imgState *imageState if state := Context.GetState(i.id); state == nil { - imgState = &ImageState{} + imgState = &imageState{} Context.SetState(i.id, imgState) NewTextureFromRgba(i.rgba, func(tex *Texture) { @@ -152,7 +163,7 @@ func (i *ImageWithRgbaWidget) Build() { }) } else { var isOk bool - imgState, isOk = state.(*ImageState) + imgState, isOk = state.(*imageState) Assert(isOk, "ImageWithRgbaWidget", "Build", "unexpected type of widget's state recovered") } @@ -164,12 +175,18 @@ func (i *ImageWithRgbaWidget) Build() { var _ Widget = &ImageWithFileWidget{} +// ImageWithFileWidget allows to display an image directly +// from .png file. +// NOTE: Be aware that project using this solution may not be portable +// because files are not included in executable binaries! +// You may want to use "embed" package and ImageWithRgba instead. type ImageWithFileWidget struct { id string imgPath string img *ImageWidget } +// ImageWithFile constructs a new ImageWithFileWidget. func ImageWithFile(imgPath string) *ImageWithFileWidget { return &ImageWithFileWidget{ id: fmt.Sprintf("ImageWithFile_%s", imgPath), @@ -178,11 +195,13 @@ func ImageWithFile(imgPath string) *ImageWithFileWidget { } } +// Size sets image's size. func (i *ImageWithFileWidget) Size(width, height float32) *ImageWithFileWidget { i.img.Size(width, height) return i } +// OnClick sets click callback. func (i *ImageWithFileWidget) OnClick(cb func()) *ImageWithFileWidget { i.img.OnClick(cb) return i @@ -190,7 +209,7 @@ func (i *ImageWithFileWidget) OnClick(cb func()) *ImageWithFileWidget { // Build implements Widget interface. func (i *ImageWithFileWidget) Build() { - imgState := &ImageState{} + imgState := &imageState{} if state := Context.GetState(i.id); state == nil { // Prevent multiple invocation to LoadImage. Context.SetState(i.id, imgState) @@ -203,7 +222,7 @@ func (i *ImageWithFileWidget) Build() { } } else { var isOk bool - imgState, isOk = state.(*ImageState) + imgState, isOk = state.(*imageState) Assert(isOk, "ImageWithFileWidget", "Build", "wrong type of widget's state got") } @@ -213,6 +232,8 @@ func (i *ImageWithFileWidget) Build() { var _ Widget = &ImageWithURLWidget{} +// ImageWithURLWidget allows to display an image using +// an URL as image source. type ImageWithURLWidget struct { id string imgURL string @@ -224,6 +245,7 @@ type ImageWithURLWidget struct { img *ImageWidget } +// ImageWithURL creates ImageWithURLWidget. func ImageWithURL(url string) *ImageWithURLWidget { return &ImageWithURLWidget{ id: fmt.Sprintf("ImageWithURL_%s", url), @@ -241,31 +263,37 @@ func (i *ImageWithURLWidget) OnReady(onReady func()) *ImageWithURLWidget { return i } +// OnFailure sets event trigger when image failed to download/load. func (i *ImageWithURLWidget) OnFailure(onFailure func(error)) *ImageWithURLWidget { i.onFailure = onFailure return i } +// OnClick sets click callback. func (i *ImageWithURLWidget) OnClick(cb func()) *ImageWithURLWidget { i.img.OnClick(cb) return i } +// Timeout sets download timeout. func (i *ImageWithURLWidget) Timeout(downloadTimeout time.Duration) *ImageWithURLWidget { i.downloadTimeout = downloadTimeout return i } +// Size sets image's size. func (i *ImageWithURLWidget) Size(width, height float32) *ImageWithURLWidget { i.img.Size(width, height) return i } +// LayoutForLoading allows to set layout rendered while loading an image. func (i *ImageWithURLWidget) LayoutForLoading(widgets ...Widget) *ImageWithURLWidget { i.whenLoading = Layout(widgets) return i } +// LayoutForFailure allows to specify layout when image failed to download. func (i *ImageWithURLWidget) LayoutForFailure(widgets ...Widget) *ImageWithURLWidget { i.whenFailure = Layout(widgets) return i @@ -273,47 +301,55 @@ func (i *ImageWithURLWidget) LayoutForFailure(widgets ...Widget) *ImageWithURLWi // Build implements Widget interface. func (i *ImageWithURLWidget) Build() { - imgState := &ImageState{} + imgState := &imageState{} if state := Context.GetState(i.id); state == nil { Context.SetState(i.id, imgState) // Prevent multiple invocation to download image. downloadContext, cancalFunc := ctx.WithCancel(ctx.Background()) - Context.SetState(i.id, &ImageState{loading: true, cancel: cancalFunc}) + Context.SetState(i.id, &imageState{loading: true, cancel: cancalFunc}) + + errorFn := func(err error) { + Context.SetState(i.id, &imageState{failure: true}) + + // Trigger onFailure event + if i.onFailure != nil { + i.onFailure(err) + } + } go func() { // Load image from url - client := resty.New() - client.SetTimeout(i.downloadTimeout) - resp, err := client.R().SetContext(downloadContext).Get(i.imgURL) + client := &http.Client{Timeout: i.downloadTimeout} + req, err := http.NewRequestWithContext(downloadContext, "GET", i.imgURL, http.NoBody) if err != nil { - Context.SetState(i.id, &ImageState{failure: true}) - - // Trigger onFailure event - if i.onFailure != nil { - i.onFailure(err) - } - + errorFn(err) return } - img, _, err := image.Decode(bytes.NewReader(resp.Body())) + resp, err := client.Do(req) if err != nil { - Context.SetState(i.id, &ImageState{failure: true}) + errorFn(err) + return + } - // Trigger onFailure event - if i.onFailure != nil { - i.onFailure(err) + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + errorFn(closeErr) } + }() + img, _, err := image.Decode(resp.Body) + if err != nil { + errorFn(err) return } rgba := ImageToRgba(img) NewTextureFromRgba(rgba, func(tex *Texture) { - Context.SetState(i.id, &ImageState{ + Context.SetState(i.id, &imageState{ loading: false, failure: false, texture: tex, @@ -327,7 +363,7 @@ func (i *ImageWithURLWidget) Build() { }() } else { var isOk bool - imgState, isOk = state.(*ImageState) + imgState, isOk = state.(*imageState) Assert(isOk, "ImageWithURLWidget", "Build", "wrong type of widget's state recovered.") } diff --git a/InputHandler.go b/InputHandler.go index e4090e00..f951f773 100644 --- a/InputHandler.go +++ b/InputHandler.go @@ -2,6 +2,7 @@ package giu // input menager is used to register a keyboard shortcuts in an app. +// Shortcut represents a keyboard shortcut. type Shortcut struct { Key Key Modifier Modifier @@ -24,7 +25,7 @@ const ( // GlobalShortcut is registered for all the app. GlobalShortcut ShortcutType = true - // LocLShortcut is registered for current window only. + // LocalShortcut is registered for current window only. LocalShortcut ShortcutType = false ) diff --git a/ListClipper.go b/ListClipper.go index d9602c07..d945cb33 100644 --- a/ListClipper.go +++ b/ListClipper.go @@ -13,15 +13,18 @@ type ListClipperWrapper struct { layout Layout } +// ListClipper creates list clipper. func ListClipper() *ListClipperWrapper { return &ListClipperWrapper{} } +// Layout sets layout for list clipper. func (l *ListClipperWrapper) Layout(layout ...Widget) *ListClipperWrapper { l.layout = layout return l } +// Build implements widget interface. func (l *ListClipperWrapper) Build() { // read all the layout widgets and (eventually) split nested layouts var layout Layout @@ -29,11 +32,13 @@ func (l *ListClipperWrapper) Build() { layout = append(layout, w) }) - var clipper imgui.ListClipper + clipper := imgui.NewListClipper() + defer clipper.Delete() + clipper.Begin(len(layout)) for clipper.Step() { - for i := clipper.DisplayStart; i < clipper.DisplayEnd; i++ { + for i := clipper.DisplayStart(); i < clipper.DisplayEnd(); i++ { layout[i].Build() } } diff --git a/Markdown.go b/Markdown.go new file mode 100644 index 00000000..71b80994 --- /dev/null +++ b/Markdown.go @@ -0,0 +1,131 @@ +package giu + +import ( + "image" + "image/color" + "net/http" + "strings" + "time" + + "github.com/AllenDang/imgui-go" + "github.com/faiface/mainthread" +) + +// MarkdownWidget implements DearImGui markdown extension +// https://github.com/juliettef/imgui_markdown +// It is like LabelWidget but with md formatting. +type MarkdownWidget struct { + md *string + linkCb func(url string) + headers []imgui.MarkdownHeaderData +} + +// Markdown creates new markdown widget. +func Markdown(md *string) *MarkdownWidget { + return &MarkdownWidget{ + md: md, + linkCb: OpenURL, + } +} + +// OnLink sets another than default link callback. +func (m *MarkdownWidget) OnLink(cb func(url string)) *MarkdownWidget { + m.linkCb = cb + return m +} + +// Header sets header formatting +// NOTE: level (counting from 0!) is header level. (for instance, header `# H1` will have level 0). +func (m *MarkdownWidget) Header(level int, font *FontInfo, separator bool) *MarkdownWidget { + // ensure if header data are at least as long as level + if m.headers == nil { + m.headers = make([]imgui.MarkdownHeaderData, level) + } + + if level <= len(m.headers) { + m.headers = append(m.headers, make([]imgui.MarkdownHeaderData, len(m.headers)-level+1)...) + } + + if font != nil { + if f, ok := Context.FontAtlas.extraFontMap[font.String()]; ok { + m.headers[level].Font = *f + } + } + + m.headers[level].HasSeparator = separator + + return m +} + +// Build implements Widget interface. +func (m *MarkdownWidget) Build() { + imgui.Markdown(Context.FontAtlas.tStrPtr(m.md), m.linkCb, loadImage, m.headers) +} + +func loadImage(path string) imgui.MarkdownImageData { + var img *image.RGBA + var err error + + switch { + case strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://"): + // Load image from url + client := &http.Client{Timeout: 5 * time.Second} + resp, respErr := client.Get(path) + if respErr != nil { + return imgui.MarkdownImageData{} + } + + defer func() { + closeErr := resp.Body.Close() + Assert((closeErr == nil), "MarkdownWidget", "loadImage", "Could not close http request!") + }() + + rgba, _, imgErr := image.Decode(resp.Body) + if imgErr != nil { + return imgui.MarkdownImageData{} + } + + img = ImageToRgba(rgba) + default: + img, err = LoadImage(path) + if err != nil { + return imgui.MarkdownImageData{} + } + } + + size := img.Bounds() + + // nolint:gocritic // TODO/BUG: figure out, why it doesn't work as expected and consider + // if current workaround is save + /* + tex := &Texture{} + NewTextureFromRgba(img, func(t *Texture) { + fmt.Println("creating texture") + tex.id = t.id + }) + */ + + var id imgui.TextureID + mainthread.Call(func() { + var err error + id, err = Context.renderer.LoadImage(img) + if err != nil { + return + } + }) + + return imgui.MarkdownImageData{ + TextureID: &id, + Scale: true, + Size: imgui.Vec2{ + X: float32(size.Dx()), + Y: float32(size.Dy()), + }, + UseLinkCallback: true, + // default values + Uv0: ToVec2(image.Point{0, 0}), + Uv1: ToVec2(image.Point{1, 1}), + TintColor: ToVec4Color(color.RGBA{255, 255, 255, 255}), + BorderColor: ToVec4Color(color.RGBA{0, 0, 0, 0}), + } +} diff --git a/MasterWindow.go b/MasterWindow.go index 6f91bb87..ae7a8fbf 100644 --- a/MasterWindow.go +++ b/MasterWindow.go @@ -9,11 +9,13 @@ import ( "github.com/AllenDang/imgui-go" "github.com/faiface/mainthread" "github.com/go-gl/glfw/v3.3/glfw" + "gopkg.in/eapache/queue.v1" ) // MasterWindowFlags wrapps imgui.GLFWWindowFlags. type MasterWindowFlags imgui.GLFWWindowFlags +// master window flags. const ( // Specifies the window will be fixed size. MasterWindowFlagsNotResizable MasterWindowFlags = MasterWindowFlags(imgui.GLFWWindowFlagsNotResizable) @@ -54,7 +56,7 @@ func NewMasterWindow(title string, width, height int, flags MasterWindowFlags) * io := imgui.CurrentIO() - io.SetConfigFlags(imgui.ConfigFlagEnablePowerSavingMode) + io.SetConfigFlags(imgui.ConfigFlagEnablePowerSavingMode | imgui.BackendFlagsRendererHasVtxOffset) // Disable imgui.ini io.SetIniFilename("") @@ -71,6 +73,9 @@ func NewMasterWindow(title string, width, height int, flags MasterWindowFlags) * Context = CreateContext(p, r) + // init texture loading queue + Context.textureLoadingQueue = queue.New() + mw := &MasterWindow{ clearColor: [4]float32{0, 0, 0, 1}, width: width, @@ -173,7 +178,12 @@ func (w *MasterWindow) sizeChange(width, height int) { } func (w *MasterWindow) render() { + if !w.platform.IsVisible() || w.platform.IsMinimized() { + return + } + Context.invalidAllState() + defer Context.cleanState() Context.FontAtlas.rebuildFontAtlas() @@ -189,8 +199,6 @@ func (w *MasterWindow) render() { r.Render(p.DisplaySize(), p.FramebufferSize(), imgui.RenderedDrawData()) p.PostRender() - - Context.cleanState() } // Run the main loop to create new frame, process events and call update ui func. @@ -201,6 +209,15 @@ func (w *MasterWindow) run() { shouldQuit := false for !shouldQuit { mainthread.Call(func() { + // process texture load requests + if Context.textureLoadingQueue != nil && Context.textureLoadingQueue.Length() > 0 { + for Context.textureLoadingQueue.Length() > 0 { + request, ok := Context.textureLoadingQueue.Remove().(textureLoadRequest) + Assert(ok, "MasterWindow", "Run", "processing texture requests: wrong type of texture request") + NewTextureFromRgba(request.img, request.cb) + } + } + p.ProcessEvents() w.render() @@ -266,6 +283,7 @@ func (w *MasterWindow) SetCloseCallback(cb func() bool) { w.platform.SetCloseCallback(cb) } +// SetDropCallback sets callback when file was droppend into the window. func (w *MasterWindow) SetDropCallback(cb func([]string)) { w.platform.SetDropCallback(cb) } @@ -276,6 +294,7 @@ func (w *MasterWindow) SetDropCallback(cb func([]string)) { // up the master window. func (w *MasterWindow) Run(loopFunc func()) { mainthread.Run(func() { + Context.isRunning = true w.updateFunc = loopFunc Context.isAlive = true @@ -292,6 +311,8 @@ func (w *MasterWindow) Run(loopFunc func()) { imgui.ImPlotDestroyContext() w.context.Destroy() }) + + Context.isRunning = false }) } @@ -350,6 +371,8 @@ func (w *MasterWindow) SetShouldClose(v bool) { w.platform.SetShouldStop(v) } +// SetInputHandler allows to change default input handler. +// see InputHandler.go. func (w *MasterWindow) SetInputHandler(handler InputHandler) { Context.InputHandler = handler w.platform.SetInputCallback(func(key glfw.Key, modifier glfw.ModifierKey, action glfw.Action) { diff --git a/Msgbox.go b/Msgbox.go index fe923441..5e55b149 100644 --- a/Msgbox.go +++ b/Msgbox.go @@ -140,7 +140,10 @@ func (m *MsgboxWidget) getState() *msgboxState { panic("Msgbox is not prepared. Invoke giu.PrepareMsgbox in the end of the layout.") } - return stateRaw.(*msgboxState) + result, isOk := stateRaw.(*msgboxState) + Assert(isOk, "MsgboxWidget", "getState", "unexpected type of widget's state recovered") + + return result } // Msgbox opens message box. diff --git a/Popups.go b/Popups.go index d2a85e22..b74c7b1f 100644 --- a/Popups.go +++ b/Popups.go @@ -4,22 +4,29 @@ import ( "github.com/AllenDang/imgui-go" ) +// OpenPopup opens a popup with specified id. +// NOTE: you need to build this popup first (see Pop(Modal)Widget). func OpenPopup(name string) { imgui.OpenPopup(name) } +// CloseCurrentPopup closes currently opened popup. +// If no popups opened, no action will be taken. func CloseCurrentPopup() { imgui.CloseCurrentPopup() } var _ Widget = &PopupWidget{} +// PopupWidget is a window which appears next to the mouse cursor. +// For instance it is used to display color palette in ColorSelectWidget. type PopupWidget struct { name string flags WindowFlags layout Layout } +// Popup creates new popup widget. func Popup(name string) *PopupWidget { return &PopupWidget{ name: Context.FontAtlas.tStr(name), @@ -28,11 +35,13 @@ func Popup(name string) *PopupWidget { } } +// Flags sets pupup's flags. func (p *PopupWidget) Flags(flags WindowFlags) *PopupWidget { p.flags = flags return p } +// Layout sets popup's layout. func (p *PopupWidget) Layout(widgets ...Widget) *PopupWidget { p.layout = Layout(widgets) return p @@ -48,6 +57,8 @@ func (p *PopupWidget) Build() { var _ Widget = &PopupModalWidget{} +// PopupModalWidget is a popup window that block every interactions behind it, cannot be closed by +// user, adds a dimming background, has a title bar. type PopupModalWidget struct { name string open *bool @@ -55,6 +66,7 @@ type PopupModalWidget struct { layout Layout } +// PopupModal creates new popup modal widget. func PopupModal(name string) *PopupModalWidget { return &PopupModalWidget{ name: Context.FontAtlas.tStr(name), @@ -64,16 +76,21 @@ func PopupModal(name string) *PopupModalWidget { } } +// IsOpen allows to control popup's state +// NOTE: changing opens' value will not result in changing popup's state +// if OpenPopup(...) wasn't called! func (p *PopupModalWidget) IsOpen(open *bool) *PopupModalWidget { p.open = open return p } +// Flags allows to specify popup's flags. func (p *PopupModalWidget) Flags(flags WindowFlags) *PopupModalWidget { p.flags = flags return p } +// Layout sets layout. func (p *PopupModalWidget) Layout(widgets ...Widget) *PopupModalWidget { p.layout = Layout(widgets) return p diff --git a/README.md b/README.md index ce2d400f..5e0eadcf 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ func loop() { } func main() { - wnd := g.NewMasterWindow("Hello world", 400, 200, g.MasterWindowFlagsNotResizable, nil) + wnd := g.NewMasterWindow("Hello world", 400, 200, g.MasterWindowFlagsNotResizable) wnd.Run(loop) } ``` @@ -88,7 +88,7 @@ Here is the result: ### What is immediate mode GUI? -Immediate mode GUI system means the UI control doesn't retain its state and value. For example, calling `giu.InputText("ID", &str)` will display a input text box on screen, and the user entered value will be stored in `&str`. Input text box doesn't know anything about it. +Immediate mode GUI system means the UI control doesn't retain its state and value. For example, calling `giu.InputText(&str)` will display a input text box on screen, and the user entered value will be stored in `&str`. Input text box doesn't know anything about it. And the `loop` method in the _Hello world_ example is in charge of **drawing** all widgets based on the parameters passed into them. This method will be invoked 30 times per second to reflect interactive states (like clicked, hovered, value-changed, etc.). It will be the place you define the UI structure. @@ -144,19 +144,26 @@ Or, install [TDM-GCC](https://jmeubank.github.io/tdm-gcc/). First you need to install the required dependencies: ```bash -# apt install libx11-dev libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libglx-dev libgl1-mesa-dev libxxf86vm-dev +sudo apt install libx11-dev libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libglx-dev libgl1-mesa-dev libxxf86vm-dev ``` +on Red Hat based distributions: +```bash +sudo dnf install libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel libXi-devel libGL-devel libXxf86vm-devel +``` + +you may also need to install C/C++ compiller (like g++) if it isn't already installed. Follow go compilator prompts. + Then, a simple `go build` will work. Cross-compiling is a bit more complicated. Let's say that you want to build for arm64. This is what you would need to do: ```bash -# dpkg --add-architecture arm64 -# apt update -# apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \ +sudo dpkg --add-architecture arm64 +sudo apt update +sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \ libx11-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 libglx-dev:arm64 libgl1-mesa-dev:arm64 libxxf86vm-dev:arm64 -$ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ HOST=aarch64-linux-gnu go build -v +GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ HOST=aarch64-linux-gnu go build -v ``` ## Deploying @@ -173,14 +180,21 @@ go build -ldflags "-s -w" . go build -ldflags "-s -w -H=windowsgui -extldflags=-static" . ``` -### Build Windows version on MacOS. +### Build Windows version on MacOS/Linux. 1. Install mingw-64. +on Mac: ```sh brew install mingw-w64 ``` +on Linux: + +```sh +sudo dnf install mingw64-gcc mingw64-gcc-c++ mingw-winpthreads-static +``` + 2. Prepare and embed the application icon into the executable and build. ```sh diff --git a/SliderWidgets.go b/SliderWidgets.go index c070a3dc..24060076 100644 --- a/SliderWidgets.go +++ b/SliderWidgets.go @@ -8,6 +8,7 @@ import ( var _ Widget = &SliderIntWidget{} +// SliderIntWidget is a slider around int32 values. type SliderIntWidget struct { label string value *int32 @@ -18,6 +19,7 @@ type SliderIntWidget struct { onChange func() } +// SliderInt constructs new SliderIntWidget. func SliderInt(value *int32, min, max int32) *SliderIntWidget { return &SliderIntWidget{ label: GenAutoID("##SliderInt"), @@ -30,28 +32,35 @@ func SliderInt(value *int32, min, max int32) *SliderIntWidget { } } +// Format sets data format displayed on the slider +// NOTE: on C side of imgui, it will be processed like: +// fmt.Sprintf(format, currentValue) so you can do e.g. +// SLiderInt(...).Format("My age is %d") and %d will be replaced with current value. func (s *SliderIntWidget) Format(format string) *SliderIntWidget { s.format = format return s } +// Size sets slider's width. func (s *SliderIntWidget) Size(width float32) *SliderIntWidget { s.width = width return s } +// OnChange sets callback when slider's position gets changed. func (s *SliderIntWidget) OnChange(onChange func()) *SliderIntWidget { s.onChange = onChange - return s } +// Label sets slider label (id). func (s *SliderIntWidget) Label(label string) *SliderIntWidget { s.label = Context.FontAtlas.tStr(label) return s } -func (s *SliderIntWidget) Labelf(format string, args ...interface{}) *SliderIntWidget { +// Labelf sets formated label. +func (s *SliderIntWidget) Labelf(format string, args ...any) *SliderIntWidget { return s.Label(fmt.Sprintf(format, args...)) } @@ -69,6 +78,7 @@ func (s *SliderIntWidget) Build() { var _ Widget = &VSliderIntWidget{} +// VSliderIntWidget stands from Vertical SliderIntWidget. type VSliderIntWidget struct { label string width float32 @@ -81,6 +91,7 @@ type VSliderIntWidget struct { onChange func() } +// VSliderInt creates new vslider int. func VSliderInt(value *int32, min, max int32) *VSliderIntWidget { return &VSliderIntWidget{ label: GenAutoID("##VSliderInt"), @@ -94,32 +105,38 @@ func VSliderInt(value *int32, min, max int32) *VSliderIntWidget { } } +// Size sets slider's size. func (vs *VSliderIntWidget) Size(width, height float32) *VSliderIntWidget { vs.width, vs.height = width, height return vs } +// Flags sets flags. func (vs *VSliderIntWidget) Flags(flags SliderFlags) *VSliderIntWidget { vs.flags = flags return vs } +// Format sets format (see comment on (*SliderIntWidget).Format). func (vs *VSliderIntWidget) Format(format string) *VSliderIntWidget { vs.format = format return vs } +// OnChange sets callback called when slider's position gets changed. func (vs *VSliderIntWidget) OnChange(onChange func()) *VSliderIntWidget { vs.onChange = onChange return vs } +// Label sets slider's label (id). func (vs *VSliderIntWidget) Label(label string) *VSliderIntWidget { vs.label = Context.FontAtlas.tStr(label) return vs } -func (vs *VSliderIntWidget) Labelf(format string, args ...interface{}) *VSliderIntWidget { +// Labelf sets formated label. +func (vs *VSliderIntWidget) Labelf(format string, args ...any) *VSliderIntWidget { return vs.Label(fmt.Sprintf(format, args...)) } @@ -140,6 +157,8 @@ func (vs *VSliderIntWidget) Build() { var _ Widget = &SliderFloatWidget{} +// SliderFloatWidget does similar to SliderIntWidget but slides around +// float32 values. type SliderFloatWidget struct { label string value *float32 @@ -150,6 +169,7 @@ type SliderFloatWidget struct { onChange func() } +// SliderFloat creates new slider float widget. func SliderFloat(value *float32, min, max float32) *SliderFloatWidget { return &SliderFloatWidget{ label: GenAutoID("##SliderFloat"), @@ -162,28 +182,34 @@ func SliderFloat(value *float32, min, max float32) *SliderFloatWidget { } } +// Format sets format of text displayed on the slider. +// default is %.3f. func (sf *SliderFloatWidget) Format(format string) *SliderFloatWidget { sf.format = format return sf } +// OnChange is callback called when slider's position gets changed. func (sf *SliderFloatWidget) OnChange(onChange func()) *SliderFloatWidget { sf.onChange = onChange return sf } +// Size sets slider's width. func (sf *SliderFloatWidget) Size(width float32) *SliderFloatWidget { sf.width = width return sf } +// Label sets slider's label (id). func (sf *SliderFloatWidget) Label(label string) *SliderFloatWidget { sf.label = Context.FontAtlas.tStr(label) return sf } -func (sf *SliderFloatWidget) Labelf(format string, args ...interface{}) *SliderFloatWidget { +// Labelf sets formated label. +func (sf *SliderFloatWidget) Labelf(format string, args ...any) *SliderFloatWidget { return sf.Label(fmt.Sprintf(format, args...)) } diff --git a/SplitLayout.go b/SplitLayout.go index c0c653fc..30969260 100644 --- a/SplitLayout.go +++ b/SplitLayout.go @@ -1,26 +1,35 @@ package giu -import "github.com/AllenDang/imgui-go" +import ( + "image/color" + "github.com/AllenDang/imgui-go" +) + +// SplitDirection represents a direction (vertical/horizontal) of splitting layout. type SplitDirection uint8 const ( + // DirectionHorizontal is a horizontal line. DirectionHorizontal SplitDirection = 1 << iota + // DirectionVertical is a vertical line. DirectionVertical ) -var _ Disposable = &SplitLayoutState{} +var _ Disposable = &splitLayoutState{} -type SplitLayoutState struct { +type splitLayoutState struct { delta float32 sashPos float32 } // Dispose implements disposable interface. -func (s *SplitLayoutState) Dispose() { +func (s *splitLayoutState) Dispose() { // noop } +// SplitLayoutWidget creates two childs with a line between them. +// This line can be moved by the user to adjust child sizes. type SplitLayoutWidget struct { id string direction SplitDirection @@ -34,6 +43,7 @@ type SplitLayoutWidget struct { border bool } +// SplitLayout creates split layout widget. func SplitLayout(direction SplitDirection, sashPos float32, layout1, layout2 Widget) *SplitLayoutWidget { return &SplitLayoutWidget{ direction: direction, @@ -45,58 +55,19 @@ func SplitLayout(direction SplitDirection, sashPos float32, layout1, layout2 Wid } } +// Border sets if childs should have borders. func (s *SplitLayoutWidget) Border(b bool) *SplitLayoutWidget { s.border = b return s } +// ID allows to manually set splitter's id. func (s *SplitLayoutWidget) ID(id string) *SplitLayoutWidget { s.id = id return s } -func (s *SplitLayoutWidget) restoreItemSpacing(layout Widget) Layout { - return Layout{ - Custom(func() { - PushItemSpacing(s.originItemSpacingX, s.originItemSpacingY) - PushFramePadding(s.originFramePaddingX, s.originFramePaddingY) - // Restore Child bg color - bgColor := imgui.CurrentStyle().GetColor(imgui.StyleColorChildBg) - PushStyleColor(StyleColorChildBg, Vec4ToRGBA(bgColor)) - }), - layout, - Custom(func() { - PopStyleColor() - PopStyleV(2) - }), - } -} - -// Build Child panel. If layout is a SplitLayout, set the frame padding to zero. -func (s *SplitLayoutWidget) buildChild(width, height float32, layout Widget) Widget { - return Layout{ - Custom(func() { - _, isSplitLayoutWidget := layout.(*SplitLayoutWidget) - hasFramePadding := isSplitLayoutWidget || !s.border - hasBorder := !isSplitLayoutWidget && s.border - - if hasFramePadding { - PushFramePadding(0, 0) - } - - Child(). - Border(hasBorder). - Size(width, height). - Layout(s.restoreItemSpacing(layout)). - Build() - - if hasFramePadding { - PopStyle() - } - }), - } -} - +// Build implements widget interface. func (s *SplitLayoutWidget) Build() { splitLayoutState := s.getState() s.originItemSpacingX, s.originItemSpacingY = GetItemInnerSpacing() @@ -142,13 +113,59 @@ func (s *SplitLayoutWidget) Build() { PopStyle() } -func (s *SplitLayoutWidget) getState() (state *SplitLayoutState) { +func (s *SplitLayoutWidget) restoreItemSpacing(layout Widget) Layout { + return Layout{ + Custom(func() { + PushItemSpacing(s.originItemSpacingX, s.originItemSpacingY) + PushFramePadding(s.originFramePaddingX, s.originFramePaddingY) + // Restore Child bg color + bgColor := imgui.CurrentStyle().GetColor(imgui.StyleColorChildBg) + PushStyleColor(StyleColorChildBg, Vec4ToRGBA(bgColor)) + }), + layout, + Custom(func() { + PopStyleColor() + PopStyleV(2) + }), + } +} + +// Build Child panel. If layout is a SplitLayout, set the frame padding to zero. +func (s *SplitLayoutWidget) buildChild(width, height float32, layout Widget) Widget { + return Layout{ + Custom(func() { + _, isSplitLayoutWidget := layout.(*SplitLayoutWidget) + hasFramePadding := isSplitLayoutWidget || !s.border + hasBorder := !isSplitLayoutWidget && s.border + + if hasFramePadding { + PushFramePadding(0, 0) + } + + PushStyleColor(StyleColorChildBg, color.RGBA{R: 0, G: 0, B: 0, A: 0}) + + Child(). + Border(hasBorder). + Size(width, height). + Layout(s.restoreItemSpacing(layout)). + Build() + + PopStyleColor() + + if hasFramePadding { + PopStyle() + } + }), + } +} + +func (s *SplitLayoutWidget) getState() (state *splitLayoutState) { if st := Context.GetState(s.id); st == nil { - state = &SplitLayoutState{delta: 0.0, sashPos: s.sashPos} + state = &splitLayoutState{delta: 0.0, sashPos: s.sashPos} Context.SetState(s.id, state) } else { var isOk bool - state, isOk = st.(*SplitLayoutState) + state, isOk = st.(*splitLayoutState) Assert(isOk, "SplitLayoutWidget", "Build", "got unexpected type of widget's state") } diff --git a/StackWidget.go b/StackWidget.go index e2e5e4d5..b63cf3ea 100644 --- a/StackWidget.go +++ b/StackWidget.go @@ -20,6 +20,7 @@ func Stack(visible int32, layouts ...Widget) *StackWidget { } } +// Build implements widget interface. func (s *StackWidget) Build() { // save visible cursor position visiblePos := GetCursorScreenPos() diff --git a/Style.go b/Style.go index e0681cf1..094579c8 100644 --- a/Style.go +++ b/Style.go @@ -147,10 +147,17 @@ func PopItemWidth() { imgui.PopItemWidth() } +// PushTextWrapPos adds the position, where the text should be frapped. +// use PushTextWrapPos, render text. If text reaches frame end, +// rendering will be continued at the start pos in line below. +// NOTE: Don't forget to call PopWrapTextPos +// NOTE: it is done automatically in LabelWidget (see (*LabelWIdget).Wrapped()). func PushTextWrapPos() { imgui.PushTextWrapPos() } +// PopTextWrapPos should be caled as many times as PushTextWrapPos +// on each frame. func PopTextWrapPos() { imgui.PopTextWrapPos() } @@ -158,6 +165,7 @@ func PopTextWrapPos() { // MouseCursorType represents a type (layout) of mouse cursor. type MouseCursorType int +// cursor types. const ( // MouseCursorNone no mouse cursor. MouseCursorNone MouseCursorType = -1 @@ -210,64 +218,64 @@ func GetFramePadding() (x, y float32) { } // StyleColorID identifies a color in the UI style. -type StyleColorID int +type StyleColorID imgui.StyleColorID // StyleColor identifier. const ( - StyleColorText StyleColorID = 0 - StyleColorTextDisabled StyleColorID = 1 - StyleColorWindowBg StyleColorID = 2 - StyleColorChildBg StyleColorID = 3 - StyleColorPopupBg StyleColorID = 4 - StyleColorBorder StyleColorID = 5 - StyleColorBorderShadow StyleColorID = 6 - StyleColorFrameBg StyleColorID = 7 - StyleColorFrameBgHovered StyleColorID = 8 - StyleColorFrameBgActive StyleColorID = 9 - StyleColorTitleBg StyleColorID = 10 - StyleColorTitleBgActive StyleColorID = 11 - StyleColorTitleBgCollapsed StyleColorID = 12 - StyleColorMenuBarBg StyleColorID = 13 - StyleColorScrollbarBg StyleColorID = 14 - StyleColorScrollbarGrab StyleColorID = 15 - StyleColorScrollbarGrabHovered StyleColorID = 16 - StyleColorScrollbarGrabActive StyleColorID = 17 - StyleColorCheckMark StyleColorID = 18 - StyleColorSliderGrab StyleColorID = 19 - StyleColorSliderGrabActive StyleColorID = 20 - StyleColorButton StyleColorID = 21 - StyleColorButtonHovered StyleColorID = 22 - StyleColorButtonActive StyleColorID = 23 - StyleColorHeader StyleColorID = 24 - StyleColorHeaderHovered StyleColorID = 25 - StyleColorHeaderActive StyleColorID = 26 - StyleColorSeparator StyleColorID = 27 - StyleColorSeparatorHovered StyleColorID = 28 - StyleColorSeparatorActive StyleColorID = 29 - StyleColorResizeGrip StyleColorID = 30 - StyleColorResizeGripHovered StyleColorID = 31 - StyleColorResizeGripActive StyleColorID = 32 - StyleColorTab StyleColorID = 33 - StyleColorTabHovered StyleColorID = 34 - StyleColorTabActive StyleColorID = 35 - StyleColorTabUnfocused StyleColorID = 36 - StyleColorTabUnfocusedActive StyleColorID = 37 - StyleColorPlotLines StyleColorID = 38 - StyleColorPlotLinesHovered StyleColorID = 39 - StyleColorProgressBarActive StyleColorID = 40 - StyleColorPlotHistogram StyleColorID = 40 - StyleColorPlotHistogramHovered StyleColorID = 41 - StyleColorTableHeaderBg StyleColorID = 42 - StyleColorTableBorderStrong StyleColorID = 43 - StyleColorTableBorderLight StyleColorID = 44 - StyleColorTableRowBg StyleColorID = 45 - StyleColorTableRowBgAlt StyleColorID = 46 - StyleColorTextSelectedBg StyleColorID = 47 - StyleColorDragDropTarget StyleColorID = 48 - StyleColorNavHighlight StyleColorID = 49 - StyleColorNavWindowingHighlight StyleColorID = 50 - StyleColorNavWindowingDimBg StyleColorID = 51 - StyleColorModalWindowDimBg StyleColorID = 52 + StyleColorText StyleColorID = StyleColorID(imgui.StyleColorText) + StyleColorTextDisabled StyleColorID = StyleColorID(imgui.StyleColorTextDisabled) + StyleColorWindowBg StyleColorID = StyleColorID(imgui.StyleColorWindowBg) + StyleColorChildBg StyleColorID = StyleColorID(imgui.StyleColorChildBg) + StyleColorPopupBg StyleColorID = StyleColorID(imgui.StyleColorPopupBg) + StyleColorBorder StyleColorID = StyleColorID(imgui.StyleColorBorder) + StyleColorBorderShadow StyleColorID = StyleColorID(imgui.StyleColorBorderShadow) + StyleColorFrameBg StyleColorID = StyleColorID(imgui.StyleColorFrameBg) + StyleColorFrameBgHovered StyleColorID = StyleColorID(imgui.StyleColorFrameBgHovered) + StyleColorFrameBgActive StyleColorID = StyleColorID(imgui.StyleColorFrameBgActive) + StyleColorTitleBg StyleColorID = StyleColorID(imgui.StyleColorTitleBg) + StyleColorTitleBgActive StyleColorID = StyleColorID(imgui.StyleColorTitleBgActive) + StyleColorTitleBgCollapsed StyleColorID = StyleColorID(imgui.StyleColorTitleBgCollapsed) + StyleColorMenuBarBg StyleColorID = StyleColorID(imgui.StyleColorMenuBarBg) + StyleColorScrollbarBg StyleColorID = StyleColorID(imgui.StyleColorScrollbarBg) + StyleColorScrollbarGrab StyleColorID = StyleColorID(imgui.StyleColorScrollbarGrab) + StyleColorScrollbarGrabHovered StyleColorID = StyleColorID(imgui.StyleColorScrollbarGrabHovered) + StyleColorScrollbarGrabActive StyleColorID = StyleColorID(imgui.StyleColorScrollbarGrabActive) + StyleColorCheckMark StyleColorID = StyleColorID(imgui.StyleColorCheckMark) + StyleColorSliderGrab StyleColorID = StyleColorID(imgui.StyleColorSliderGrab) + StyleColorSliderGrabActive StyleColorID = StyleColorID(imgui.StyleColorSliderGrabActive) + StyleColorButton StyleColorID = StyleColorID(imgui.StyleColorButton) + StyleColorButtonHovered StyleColorID = StyleColorID(imgui.StyleColorButtonHovered) + StyleColorButtonActive StyleColorID = StyleColorID(imgui.StyleColorButtonActive) + StyleColorHeader StyleColorID = StyleColorID(imgui.StyleColorHeader) + StyleColorHeaderHovered StyleColorID = StyleColorID(imgui.StyleColorHeaderHovered) + StyleColorHeaderActive StyleColorID = StyleColorID(imgui.StyleColorHeaderActive) + StyleColorSeparator StyleColorID = StyleColorID(imgui.StyleColorSeparator) + StyleColorSeparatorHovered StyleColorID = StyleColorID(imgui.StyleColorSeparatorHovered) + StyleColorSeparatorActive StyleColorID = StyleColorID(imgui.StyleColorSeparatorActive) + StyleColorResizeGrip StyleColorID = StyleColorID(imgui.StyleColorResizeGrip) + StyleColorResizeGripHovered StyleColorID = StyleColorID(imgui.StyleColorResizeGripHovered) + StyleColorResizeGripActive StyleColorID = StyleColorID(imgui.StyleColorResizeGripActive) + StyleColorTab StyleColorID = StyleColorID(imgui.StyleColorTab) + StyleColorTabHovered StyleColorID = StyleColorID(imgui.StyleColorTabHovered) + StyleColorTabActive StyleColorID = StyleColorID(imgui.StyleColorTabActive) + StyleColorTabUnfocused StyleColorID = StyleColorID(imgui.StyleColorTabUnfocused) + StyleColorTabUnfocusedActive StyleColorID = StyleColorID(imgui.StyleColorTabUnfocusedActive) + StyleColorPlotLines StyleColorID = StyleColorID(imgui.StyleColorPlotLines) + StyleColorPlotLinesHovered StyleColorID = StyleColorID(imgui.StyleColorPlotLinesHovered) + StyleColorProgressBarActive StyleColorID = StyleColorPlotLinesHovered + StyleColorPlotHistogram StyleColorID = StyleColorID(imgui.StyleColorPlotHistogram) + StyleColorPlotHistogramHovered StyleColorID = StyleColorID(imgui.StyleColorPlotHistogramHovered) + StyleColorTableHeaderBg StyleColorID = StyleColorID(imgui.StyleColorTableHeaderBg) + StyleColorTableBorderStrong StyleColorID = StyleColorID(imgui.StyleColorTableBorderStrong) + StyleColorTableBorderLight StyleColorID = StyleColorID(imgui.StyleColorTableBorderLight) + StyleColorTableRowBg StyleColorID = StyleColorID(imgui.StyleColorTableRowBg) + StyleColorTableRowBgAlt StyleColorID = StyleColorID(imgui.StyleColorTableRowBgAlt) + StyleColorTextSelectedBg StyleColorID = StyleColorID(imgui.StyleColorTextSelectedBg) + StyleColorDragDropTarget StyleColorID = StyleColorID(imgui.StyleColorDragDropTarget) + StyleColorNavHighlight StyleColorID = StyleColorID(imgui.StyleColorNavHighlight) + StyleColorNavWindowingHighlight StyleColorID = StyleColorID(imgui.StyleColorNavWindowingHighlight) + StyleColorNavWindowingDimBg StyleColorID = StyleColorID(imgui.StyleColorNavWindowingDimBg) + StyleColorModalWindowDimBg StyleColorID = StyleColorID(imgui.StyleColorModalWindowDimBg) ) // StyleVarID identifies a style variable in the UI style. @@ -351,7 +359,7 @@ var _ Widget = &StyleSetter{} // StyleSetter is a user-friendly way to manage imgui styles. type StyleSetter struct { colors map[StyleColorID]color.Color - styles map[StyleVarID]interface{} + styles map[StyleVarID]any font *FontInfo disabled bool layout Layout @@ -361,7 +369,7 @@ type StyleSetter struct { func Style() *StyleSetter { var ss StyleSetter ss.colors = make(map[StyleColorID]color.Color) - ss.styles = make(map[StyleVarID]interface{}) + ss.styles = make(map[StyleVarID]any) return &ss } @@ -392,6 +400,9 @@ func (ss *StyleSetter) SetFont(font *FontInfo) *StyleSetter { return ss } +// SetFontSize sets size of the font. +// NOTE: Be aware, that StyleSetter needs to add a new font to font atlas for +// each font's size. func (ss *StyleSetter) SetFontSize(size float32) *StyleSetter { var font FontInfo if ss.font != nil { @@ -400,11 +411,7 @@ func (ss *StyleSetter) SetFontSize(size float32) *StyleSetter { font = Context.FontAtlas.defaultFonts[0] } - font.size = size - - Context.FontAtlas.extraFonts = append(Context.FontAtlas.extraFonts, font) - - ss.font = &font + ss.font = font.SetSize(size) return ss } @@ -455,9 +462,10 @@ func (ss *StyleSetter) Build() { } } - isFontPushed := false if ss.font != nil { - isFontPushed = PushFont(ss.font) + if PushFont(ss.font) { + defer PopFont() + } } imgui.BeginDisabled(ss.disabled) @@ -466,10 +474,6 @@ func (ss *StyleSetter) Build() { imgui.EndDisabled() - if isFontPushed { - PopFont() - } - imgui.PopStyleColorV(len(ss.colors)) imgui.PopStyleVarV(len(ss.styles)) } diff --git a/TableWidgets.go b/TableWidgets.go index b76cfa3c..0a1a7ee5 100644 --- a/TableWidgets.go +++ b/TableWidgets.go @@ -6,8 +6,6 @@ import ( "github.com/AllenDang/imgui-go" ) -var _ Widget = &TableRowWidget{} - type TableRowWidget struct { flags TableRowFlags minRowHeight float64 @@ -39,8 +37,8 @@ func (r *TableRowWidget) MinHeight(height float64) *TableRowWidget { return r } -// Build implements Widget interface. -func (r *TableRowWidget) Build() { +// BuildTableRow executes table row build steps. +func (r *TableRowWidget) BuildTableRow() { imgui.TableNextRow(imgui.TableRowFlags(r.flags), r.minRowHeight) for _, w := range r.layout { @@ -60,8 +58,6 @@ func (r *TableRowWidget) Build() { } } -var _ Widget = &TableColumnWidget{} - type TableColumnWidget struct { label string flags TableColumnFlags @@ -93,8 +89,8 @@ func (c *TableColumnWidget) UserID(id uint32) *TableColumnWidget { return c } -// Build implements Widget interface. -func (c *TableColumnWidget) Build() { +// BuildTableColumn executes table column build steps. +func (c *TableColumnWidget) BuildTableColumn() { imgui.TableSetupColumn(c.label, imgui.TableColumnFlags(c.flags), c.innerWidthOrWeight, c.userID) } @@ -124,6 +120,12 @@ func Table() *TableWidget { } } +// ID sets the internal id of table widget. +func (t *TableWidget) ID(id string) *TableWidget { + t.id = id + return t +} + // FastMode Displays visible rows only to boost performance. func (t *TableWidget) FastMode(b bool) *TableWidget { t.fastMode = b @@ -180,26 +182,28 @@ func (t *TableWidget) Build() { if len(t.columns) > 0 { for _, col := range t.columns { - col.Build() + col.BuildTableColumn() } imgui.TableHeadersRow() } if t.fastMode { - var clipper imgui.ListClipper + clipper := imgui.NewListClipper() + defer clipper.Delete() + clipper.Begin(len(t.rows)) for clipper.Step() { - for i := clipper.DisplayStart; i < clipper.DisplayEnd; i++ { + for i := clipper.DisplayStart(); i < clipper.DisplayEnd(); i++ { row := t.rows[i] - row.Build() + row.BuildTableRow() } } clipper.End() } else { for _, row := range t.rows { - row.Build() + row.BuildTableRow() } } diff --git a/TextWidgets.go b/TextWidgets.go index bbd04b73..b4ea6f17 100644 --- a/TextWidgets.go +++ b/TextWidgets.go @@ -10,7 +10,7 @@ import ( var _ Widget = &InputTextMultilineWidget{} -// InputTextMultilineWidget represents multiline text input widget +// InputTextMultilineWidget is a large (multiline) text input // see examples/widgets/. type InputTextMultilineWidget struct { label string @@ -41,25 +41,10 @@ func (i *InputTextMultilineWidget) Label(label string) *InputTextMultilineWidget } // Labelf is formatting version of Label. -func (i *InputTextMultilineWidget) Labelf(format string, args ...interface{}) *InputTextMultilineWidget { +func (i *InputTextMultilineWidget) Labelf(format string, args ...any) *InputTextMultilineWidget { return i.Label(fmt.Sprintf(format, args...)) } -// Build implements Widget interface. -func (i *InputTextMultilineWidget) Build() { - if imgui.InputTextMultilineV( - Context.FontAtlas.tStr(i.label), - Context.FontAtlas.tStrPtr(i.text), - imgui.Vec2{ - X: i.width, - Y: i.height, - }, - int(i.flags), i.cb, - ) && i.onChange != nil { - i.onChange() - } -} - // Flags sets InputTextFlags (see Flags.go). func (i *InputTextMultilineWidget) Flags(flags InputTextFlags) *InputTextMultilineWidget { i.flags = flags @@ -84,6 +69,21 @@ func (i *InputTextMultilineWidget) Size(width, height float32) *InputTextMultili return i } +// Build implements Widget interface. +func (i *InputTextMultilineWidget) Build() { + if imgui.InputTextMultilineV( + Context.FontAtlas.tStr(i.label), + Context.FontAtlas.tStrPtr(i.text), + imgui.Vec2{ + X: i.width, + Y: i.height, + }, + int(i.flags), i.cb, + ) && i.onChange != nil { + i.onChange() + } +} + var _ Widget = &BulletWidget{} // BulletWidget adds a small, white dot (bullet). @@ -116,7 +116,7 @@ func BulletText(text string) *BulletTextWidget { } // BulletTextf is a formatting version of BulletText. -func BulletTextf(format string, args ...interface{}) *BulletTextWidget { +func BulletTextf(format string, args ...any) *BulletTextWidget { return BulletText(fmt.Sprintf(format, args...)) } @@ -125,8 +125,20 @@ func (bt *BulletTextWidget) Build() { imgui.BulletText(bt.text) } +var _ Disposable = &inputTextState{} + +type inputTextState struct { + autoCompleteCandidates fuzzy.Matches +} + +// Dispose implements disposable interface. +func (s *inputTextState) Dispose() { + s.autoCompleteCandidates = nil +} + var _ Widget = &InputTextWidget{} +// InputTextWidget is a single-line text iinput. type InputTextWidget struct { label string hint string @@ -138,14 +150,7 @@ type InputTextWidget struct { onChange func() } -type inputTextState struct { - autoCompleteCandidates fuzzy.Matches -} - -func (s *inputTextState) Dispose() { - s.autoCompleteCandidates = nil -} - +// InputText creates new input text widget. func InputText(value *string) *InputTextWidget { return &InputTextWidget{ label: GenAutoID("##InputText"), @@ -158,12 +163,14 @@ func InputText(value *string) *InputTextWidget { } } +// Label adds label (alternatively you can use it to set widget's id). func (i *InputTextWidget) Label(label string) *InputTextWidget { i.label = Context.FontAtlas.tStr(label) return i } -func (i *InputTextWidget) Labelf(format string, args ...interface{}) *InputTextWidget { +// Labelf adds formatted label. +func (i *InputTextWidget) Labelf(format string, args ...any) *InputTextWidget { return i.Label(fmt.Sprintf(format, args...)) } @@ -174,26 +181,31 @@ func (i *InputTextWidget) AutoComplete(candidates []string) *InputTextWidget { return i } +// Hint sets hint text. func (i *InputTextWidget) Hint(hint string) *InputTextWidget { i.hint = Context.FontAtlas.tStr(hint) return i } +// Size sets field's width. func (i *InputTextWidget) Size(width float32) *InputTextWidget { i.width = width return i } +// Flags sets flags. func (i *InputTextWidget) Flags(flags InputTextFlags) *InputTextWidget { i.flags = flags return i } +// Callback sets input text callback. func (i *InputTextWidget) Callback(cb imgui.InputTextCallback) *InputTextWidget { i.cb = cb return i } +// OnChange sets callback when text was changed. func (i *InputTextWidget) OnChange(onChange func()) *InputTextWidget { i.onChange = onChange return i @@ -258,6 +270,7 @@ func (i *InputTextWidget) Build() { var _ Widget = &InputIntWidget{} +// InputIntWidget is an input text field acceptiong intager values only. type InputIntWidget struct { label string value *int32 @@ -266,6 +279,10 @@ type InputIntWidget struct { onChange func() } +// InputInt creates input int widget +// NOTE: value is int32, so its size is up to 10^32-1. +// to process greater values, you need to use InputTextWidget +// with InputTextFlagsCharsDecimal and strconv.ParseInt in OnChange callback. func InputInt(value *int32) *InputIntWidget { return &InputIntWidget{ label: GenAutoID("##InputInt"), @@ -276,25 +293,30 @@ func InputInt(value *int32) *InputIntWidget { } } +// Label sets label (id). func (i *InputIntWidget) Label(label string) *InputIntWidget { i.label = Context.FontAtlas.tStr(label) return i } -func (i *InputIntWidget) Labelf(format string, args ...interface{}) *InputIntWidget { +// Labelf sets formatted label. +func (i *InputIntWidget) Labelf(format string, args ...any) *InputIntWidget { return i.Label(fmt.Sprintf(format, args...)) } +// Size sets input's width. func (i *InputIntWidget) Size(width float32) *InputIntWidget { i.width = width return i } +// Flags sets flags. func (i *InputIntWidget) Flags(flags InputTextFlags) *InputIntWidget { i.flags = flags return i } +// OnChange adds on change callback. func (i *InputIntWidget) OnChange(onChange func()) *InputIntWidget { i.onChange = onChange return i @@ -314,6 +336,7 @@ func (i *InputIntWidget) Build() { var _ Widget = &InputFloatWidget{} +// InputFloatWidget does similar to InputIntWIdget, but accepts float numbers. type InputFloatWidget struct { label string value *float32 @@ -323,6 +346,7 @@ type InputFloatWidget struct { onChange func() } +// InputFloat constructs InputFloatWidget. func InputFloat(value *float32) *InputFloatWidget { return &InputFloatWidget{ label: GenAutoID("##InputFloatWidget"), @@ -334,30 +358,36 @@ func InputFloat(value *float32) *InputFloatWidget { } } +// Label sets label of input field. func (i *InputFloatWidget) Label(label string) *InputFloatWidget { i.label = Context.FontAtlas.tStr(label) return i } -func (i *InputFloatWidget) Labelf(format string, args ...interface{}) *InputFloatWidget { +// Labelf sets formatted label. +func (i *InputFloatWidget) Labelf(format string, args ...any) *InputFloatWidget { return i.Label(fmt.Sprintf(format, args...)) } +// Size sets input field's width. func (i *InputFloatWidget) Size(width float32) *InputFloatWidget { i.width = width return i } +// Flags sets flags. func (i *InputFloatWidget) Flags(flags InputTextFlags) *InputFloatWidget { i.flags = flags return i } +// Format sets data format (e.g. %.3f). func (i *InputFloatWidget) Format(format string) *InputFloatWidget { i.format = format return i } +// OnChange sets callback called when text is changed. func (i *InputFloatWidget) OnChange(onChange func()) *InputFloatWidget { i.onChange = onChange return i @@ -377,12 +407,14 @@ func (i *InputFloatWidget) Build() { var _ Widget = &LabelWidget{} +// LabelWidget is a plain text label. type LabelWidget struct { label string fontInfo *FontInfo wrapped bool } +// Label constructs label widget. func Label(label string) *LabelWidget { return &LabelWidget{ label: Context.FontAtlas.tStr(label), @@ -390,15 +422,18 @@ func Label(label string) *LabelWidget { } } -func Labelf(format string, args ...interface{}) *LabelWidget { +// Labelf allows to add formatted label. +func Labelf(format string, args ...any) *LabelWidget { return Label(fmt.Sprintf(format, args...)) } +// Wrapped determinates if label is frapped. func (l *LabelWidget) Wrapped(wrapped bool) *LabelWidget { l.wrapped = wrapped return l } +// Font sets specific font (does like Style().SetFont). func (l *LabelWidget) Font(font *FontInfo) *LabelWidget { l.fontInfo = font return l diff --git a/Texture.go b/Texture.go index f7ee73ce..48dc6b64 100644 --- a/Texture.go +++ b/Texture.go @@ -9,20 +9,40 @@ import ( "github.com/faiface/mainthread" ) +// Texture represents imgui.TextureID. +// It is base unit of images in imgui. type Texture struct { id imgui.TextureID } +type textureLoadRequest struct { + img image.Image + cb func(*Texture) +} + type loadImageResult struct { id imgui.TextureID err error } +// EnqueueNewTextureFromRgba adds loading texture request to loading queue +// it allows us to run this method in main loop +// NOTE: remember to call it after NewMasterWindow! +func EnqueueNewTextureFromRgba(rgba image.Image, loadCb func(t *Texture)) { + Assert((Context.textureLoadingQueue != nil), "", "EnqueueNewTextureFromRgba", "you need to call EnqueueNewTextureFromRgba after giu.NewMasterWindow call!") + Context.textureLoadingQueue.Add(textureLoadRequest{rgba, loadCb}) +} + // NewTextureFromRgba creates a new texture from image.Image and, when it is done, calls loadCallback(loadedTexture). func NewTextureFromRgba(rgba image.Image, loadCallback func(*Texture)) { + Assert(Context.isRunning, "", "NewTextureFromRgba", "cannot load texture befor (*MasterWindow).Run call!") + loadTexture(rgba, loadCallback) +} + +func loadTexture(rgba image.Image, loadCallback func(*Texture)) { go func() { Update() - result := mainthread.CallVal(func() interface{} { + result := mainthread.CallVal(func() any { texID, err := Context.renderer.LoadImage(ImageToRgba(rgba)) return &loadImageResult{id: texID, err: err} }) diff --git a/Utils.go b/Utils.go index 95a5a32b..b5c14b02 100644 --- a/Utils.go +++ b/Utils.go @@ -11,6 +11,7 @@ import ( "path/filepath" "github.com/AllenDang/imgui-go" + "github.com/pkg/browser" ) // LoadImage loads image from file and returns *image.RGBA. @@ -80,13 +81,12 @@ func Vec4ToRGBA(vec4 imgui.Vec4) color.RGBA { // Update updates giu app // it is done by default after each frame. -// Hoeever because frames stops rendering, when no user +// However because frames stops rendering, when no user // action is done, it may be necessary to // Update ui manually at some point. func Update() { if Context.isAlive { Context.platform.Update() - Context.IO().SetFrameCountSinceLastInput(0) } } @@ -118,6 +118,8 @@ func GetMousePos() image.Point { return image.Pt(int(pos.X), int(pos.Y)) } +// GetAvailableRegion returns region available for rendering. +// it is always WindowSize-WindowPadding*2. func GetAvailableRegion() (width, height float32) { region := imgui.ContentRegionAvail() return region.X, region.Y @@ -171,33 +173,44 @@ func SetItemDefaultFocus() { imgui.SetItemDefaultFocus() } -// SetKeyboardFocusHere sets keyboard focus at the widget. +// SetKeyboardFocusHere sets keyboard focus at *NEXT* widget. func SetKeyboardFocusHere() { SetKeyboardFocusHereV(0) } +// SetKeyboardFocusHereV sets keyboard on the next widget. Use positive 'offset' to access sub components of a multiple component widget. Use -1 to access previous widget. func SetKeyboardFocusHereV(i int) { imgui.SetKeyboardFocusHereV(i) } +// PushClipRect pushes a clipping rectangle for both ImGui logic (hit-testing etc.) and low-level ImDrawList rendering. func PushClipRect(clipRectMin, clipRectMax image.Point, intersectWithClipRect bool) { imgui.PushClipRect(ToVec2(clipRectMin), ToVec2(clipRectMax), intersectWithClipRect) } +// PopClipRect should be called to end PushClipRect. func PopClipRect() { imgui.PopClipRect() } -func Assert(cond bool, t, method, msg string, args ...interface{}) { +// Assert checks if cond. If not cond, it alls golang panic. +func Assert(cond bool, t, method, msg string, args ...any) { if !cond { fatal(t, method, msg, args...) } } -func fatal(widgetName, method, message string, args ...interface{}) { +func fatal(widgetName, method, message string, args ...any) { if widgetName != "" { widgetName = fmt.Sprintf("(*%s)", widgetName) } log.Panicf("giu: %s.%s: %s", widgetName, method, fmt.Sprintf(message, args...)) } + +// OpenURL opens `url` in default browser. +func OpenURL(url string) { + if err := browser.OpenURL(url); err != nil { + log.Printf("Error opening %s: %v", url, err) + } +} diff --git a/Widgets.go b/Widgets.go index 47a6d143..1434cf5f 100644 --- a/Widgets.go +++ b/Widgets.go @@ -34,7 +34,7 @@ func (l *RowWidget) Build() { switch w.(type) { case *TooltipWidget, *ContextMenuWidget, *PopupModalWidget, - *PopupWidget, *TabItemWidget: + *PopupWidget: // noop default: if _, isLabel := w.(*LabelWidget); isLabel { @@ -98,6 +98,12 @@ func (c *ChildWidget) Layout(widgets ...Widget) *ChildWidget { return c } +// ID sets the interval id of child widgets. +func (c *ChildWidget) ID(id string) *ChildWidget { + c.id = id + return c +} + func Child() *ChildWidget { return &ChildWidget{ id: GenAutoID("Child"), @@ -395,7 +401,7 @@ func MenuItem(label string) *MenuItemWidget { } } -func MenuItemf(format string, args ...interface{}) *MenuItemWidget { +func MenuItemf(format string, args ...any) *MenuItemWidget { return MenuItem(fmt.Sprintf(format, args...)) } @@ -437,7 +443,7 @@ func Menu(label string) *MenuWidget { } } -func Menuf(format string, args ...interface{}) *MenuWidget { +func Menuf(format string, args ...any) *MenuWidget { return Menu(fmt.Sprintf(format, args...)) } @@ -487,7 +493,7 @@ func (p *ProgressBarWidget) Overlay(overlay string) *ProgressBarWidget { return p } -func (p *ProgressBarWidget) Overlayf(format string, args ...interface{}) *ProgressBarWidget { +func (p *ProgressBarWidget) Overlayf(format string, args ...any) *ProgressBarWidget { return p.Overlay(fmt.Sprintf(format, args...)) } @@ -538,8 +544,6 @@ func Dummy(width, height float32) *DummyWidget { } } -var _ Widget = &TabItemWidget{} - type TabItemWidget struct { label string open *bool @@ -556,7 +560,7 @@ func TabItem(label string) *TabItemWidget { } } -func TabItemf(format string, args ...interface{}) *TabItemWidget { +func TabItemf(format string, args ...any) *TabItemWidget { return TabItem(fmt.Sprintf(format, args...)) } @@ -575,8 +579,8 @@ func (t *TabItemWidget) Layout(widgets ...Widget) *TabItemWidget { return t } -// Build implements Widget interface. -func (t *TabItemWidget) Build() { +// BuildTabItem executes tab item build steps. +func (t *TabItemWidget) BuildTabItem() { if imgui.BeginTabItemV(t.label, t.open, int(t.flags)) { t.layout.Build() imgui.EndTabItem() @@ -617,7 +621,7 @@ func (t *TabBarWidget) TabItems(items ...*TabItemWidget) *TabBarWidget { func (t *TabBarWidget) Build() { if imgui.BeginTabBarV(t.id, int(t.flags)) { for _, ti := range t.tabItems { - ti.Build() + ti.BuildTabItem() } imgui.EndTabBar() } @@ -650,7 +654,7 @@ func Tooltip(tip string) *TooltipWidget { } } -func Tooltipf(format string, args ...interface{}) *TooltipWidget { +func Tooltipf(format string, args ...any) *TooltipWidget { return Tooltip(fmt.Sprintf(format, args...)) } diff --git a/Window.go b/Window.go index bcff573e..c8c0cfdc 100644 --- a/Window.go +++ b/Window.go @@ -22,6 +22,7 @@ func SingleWindow() *WindowWidget { Size(size[0], size[1]) } +// SingleWindowWithMenuBar creates a SingleWindow and allows to add menubar on its top. func SingleWindowWithMenuBar() *WindowWidget { size := Context.platform.DisplaySize() title := fmt.Sprintf("SingleWindow_%d", Context.GetWidgetIndex()) diff --git a/build_windows.go b/build_windows.go new file mode 100644 index 00000000..8d946295 --- /dev/null +++ b/build_windows.go @@ -0,0 +1,7 @@ +//go:build windows +// +build windows + +package giu + +// #cgo LDFLAGS: -static +import "C" diff --git a/cmd/gmdeploy/main.go b/cmd/gmdeploy/main.go index 56f3ea82..da0559a6 100644 --- a/cmd/gmdeploy/main.go +++ b/cmd/gmdeploy/main.go @@ -90,7 +90,7 @@ func main() { fmt.Printf("%s.app is generated at %s/build/%s/\n", appName, projectPath, targetOS) case "linux": // nolint:gosec // Compile: cannot fix - cmd := exec.Command("bash", "-c", fmt.Sprintf("go build -ldflags='-s -w' -o %s", filepath.Join(appName))) + cmd := exec.Command("bash", "-c", fmt.Sprintf("go build -ldflags='-s -w' -o %s", appName)) cmd.Dir = projectPath runCmd(cmd) diff --git a/cmd/gmdeploy/utils.go b/cmd/gmdeploy/utils.go index c74ddd05..49f599c2 100644 --- a/cmd/gmdeploy/utils.go +++ b/cmd/gmdeploy/utils.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "log" "os" "os/exec" @@ -9,7 +8,7 @@ import ( func save(name, data string) { const newFileMode = 0o644 - if err := ioutil.WriteFile(name, []byte(data), newFileMode); err != nil { + if err := os.WriteFile(name, []byte(data), newFileMode); err != nil { log.Fatalf("Failed to save %s:%v\n", name, err) } } diff --git a/examples/align/align.go b/examples/align/align.go index 76b4de32..91d92049 100644 --- a/examples/align/align.go +++ b/examples/align/align.go @@ -31,6 +31,19 @@ func loop() { giu.Button("button 2"), ), ), + + giu.Label("manual alignment"), + giu.AlignManually( + giu.AlignCenter, + giu.Button("I'm button with 100 width"). + Size(100, 30), + 100, false, + ), + giu.AlignManually( + giu.AlignCenter, + giu.InputText(&text), + 100, true, + ), ) } diff --git a/examples/gifdecode/gifdecode.go b/examples/gifdecode/gifdecode.go new file mode 100644 index 00000000..6b2f1c4d --- /dev/null +++ b/examples/gifdecode/gifdecode.go @@ -0,0 +1,65 @@ +package main + +import ( + "bytes" + _ "embed" + "image/gif" + "log" + "time" + + "github.com/AllenDang/giu" +) + +//go:embed golang.gif +var gifFileData []byte + +const gifFilepath = "./golang.gif" + +var ( + frames []*giu.Texture + gifImg *gif.GIF + currentFrame int +) + +func loop() { + // load textures + if frames[0] == nil { + for i, frame := range gifImg.Image { + // lol, this is the most magic thing in go i've ever seen :D + i := i + giu.NewTextureFromRgba(giu.ImageToRgba(frame), func(t *giu.Texture) { + frames[i] = t + }) + } + } + + giu.SingleWindow().Layout( + giu.Image(frames[currentFrame]), + ) +} + +func main() { + var err error + + wnd := giu.NewMasterWindow("GIF renderer [example]", 640, 480, 0) + + gifImg, err = gif.DecodeAll(bytes.NewReader(gifFileData)) + if err != nil { + log.Fatal(err) + } + + frames = make([]*giu.Texture, len(gifImg.Image)) + + go func() { + for { + time.Sleep(time.Duration(gifImg.Delay[currentFrame]*10) * time.Millisecond) + giu.Update() + currentFrame++ + if currentFrame == len(frames) { + currentFrame = 0 + } + } + }() + + wnd.Run(loop) +} diff --git a/examples/gifdecode/golang.gif b/examples/gifdecode/golang.gif new file mode 100644 index 00000000..23355aa1 Binary files /dev/null and b/examples/gifdecode/golang.gif differ diff --git a/examples/loadimage/loadimage.go b/examples/loadimage/loadimage.go index 0a1ff885..ef36a0cb 100644 --- a/examples/loadimage/loadimage.go +++ b/examples/loadimage/loadimage.go @@ -10,10 +10,15 @@ import ( g "github.com/AllenDang/giu" ) -var rgba *image.RGBA +var ( + rgba *image.RGBA + tex *g.Texture +) func loop() { g.SingleWindow().Layout( + g.Label("Display image from texture"), + g.Image(tex), g.Label("Display image from rgba"), g.ImageWithRgba(rgba).OnClick(func() { fmt.Println("rgba image was clicked") @@ -62,6 +67,9 @@ func main() { rgba, _ = g.LoadImage("./fallback.png") wnd := g.NewMasterWindow("Load Image", 600, 500, g.MasterWindowFlagsNotResizable) + g.EnqueueNewTextureFromRgba(rgba, func(t *g.Texture) { + tex = t + }) wnd.SetIcon([]image.Image{rgba}) wnd.Run(loop) } diff --git a/examples/markdown/gopher.png b/examples/markdown/gopher.png new file mode 100644 index 00000000..2f1e3c42 Binary files /dev/null and b/examples/markdown/gopher.png differ diff --git a/examples/markdown/markdown.go b/examples/markdown/markdown.go new file mode 100644 index 00000000..07a219d8 --- /dev/null +++ b/examples/markdown/markdown.go @@ -0,0 +1,76 @@ +package main + +import ( + "strings" + + "github.com/AllenDang/giu" +) + +var markdown string = getExampleMarkdownText() + +func getExampleMarkdownText() string { + return strings.Join([]string{ + "Wrapping:", + "Text wraps automatically. To add a new line, use 'Return'.", + "", + "Headers:", + "# H1", + "## H2", + "### H3", + "", + "Emphasis:", + "*emphasis*", + "_emphasis_", + "**strong emphasis**", + "__strong emphasis__", + "", + "Indents:", + "On a new line, at the start of the line, add two spaces per indent.", + " Indent level 1", + " Indent level 2", + "", + "Unordered lists:", + "On a new line, at the start of the line, add two spaces, an asterisks and a space.", + "For nested lists, add two additional spaces in front of the asterisk per list level increment.", + " * Unordered List level 1", + " * Unordered List level 2", + "", + "Link:", + "Here is [a link to some cool website!](https://github.com/AllenDang/giu) you must click it!", + "Image:", + "![gopher image](./gopher.png)", + "![gopher image link](https://raw.githubusercontent.com/AllenDang/giu/master/examples/loadimage/gopher.png)", + "", + "Horizontal Rule:", + "***", + "___", + }, "\n") +} + +func loop() { + giu.SingleWindow().Layout( + giu.SplitLayout(giu.DirectionHorizontal, 320, + giu.Layout{ + giu.Row( + giu.Label("Markdown Edition:"), + giu.Button("Reset").OnClick(func() { + markdown = getExampleMarkdownText() + }), + ), + giu.Custom(func() { + availableW, availableH := giu.GetAvailableRegion() + giu.InputTextMultiline(&markdown).Size(availableW, availableH).Build() + }), + }, + giu.Markdown(&markdown). + Header(0, (giu.GetDefaultFonts())[0].SetSize(28), true). + Header(1, (giu.GetDefaultFonts())[0].SetSize(26), false). + Header(2, nil, true), + ), + ) +} + +func main() { + wnd := giu.NewMasterWindow("ImGui Markdown [Demo]", 640, 480, 0) + wnd.Run(loop) +} diff --git a/examples/widgets/widgets.go b/examples/widgets/widgets.go index 8d192de6..b09cdc75 100644 --- a/examples/widgets/widgets.go +++ b/examples/widgets/widgets.go @@ -90,9 +90,10 @@ func loop() { fmt.Println(checked2) }), g.Dummy(30, 0), - g.RadioButton("Radio 1", radioOp == 0).OnChange(func() { radioOp = 0 }), - g.RadioButton("Radio 2", radioOp == 1).OnChange(func() { radioOp = 1 }), - g.RadioButton("Radio 3", radioOp == 2).OnChange(func() { radioOp = 2 }), + g.Label("Do you like giu?"), + g.RadioButton("Yes, of course", radioOp == 0).OnChange(func() { radioOp = 0 }), + g.RadioButton("I'm going to test it now", radioOp == 1).OnChange(func() { radioOp = 1 }), + g.RadioButton("No", false), ), g.ProgressBar(0.8).Size(g.Auto, 0).Overlay("Progress"), diff --git a/go.mod b/go.mod index b77d0b6e..9cce27fa 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,24 @@ module github.com/AllenDang/giu -go 1.16 +go 1.18 require ( github.com/AllenDang/go-findfont v0.0.0-20200702051237-9f180485aeb8 - github.com/AllenDang/imgui-go v1.12.1-0.20211020111217-535ee3ce6d02 + github.com/AllenDang/imgui-go v1.12.1-0.20220322114136-499bbf6a42ad github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 - github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259 // indirect - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be - github.com/go-resty/resty/v2 v2.6.0 - github.com/kylelemons/godebug v1.1.0 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/sahilm/fuzzy v0.1.0 - github.com/stretchr/testify v1.7.0 - golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d + github.com/stretchr/testify v1.7.1 + golang.org/x/image v0.0.0-20220302094943-723b81ca9867 + gopkg.in/eapache/queue.v1 v1.1.0 +) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index ea2af738..a0cb4d63 100644 --- a/go.sum +++ b/go.sum @@ -1,38 +1,38 @@ github.com/AllenDang/go-findfont v0.0.0-20200702051237-9f180485aeb8 h1:dKZMqib/yUDoCFigmz2agG8geZ/e3iRq304/KJXqKyw= github.com/AllenDang/go-findfont v0.0.0-20200702051237-9f180485aeb8/go.mod h1:b4uuDd0s6KRIPa84cEEchdQ9ICh7K0OryZHbSzMca9k= -github.com/AllenDang/imgui-go v1.12.1-0.20211020111217-535ee3ce6d02 h1:MP/SWCuehFCGBrcVnR6ydkUlD1HQInoZidOCp7fzkTM= -github.com/AllenDang/imgui-go v1.12.1-0.20211020111217-535ee3ce6d02/go.mod h1:2jS7bvvG+PejKdNu4eg2UYqx7Ky8IXGAhxOfjq9qTNk= +github.com/AllenDang/imgui-go v1.12.1-0.20220322114136-499bbf6a42ad h1:Kr961C2uEEAklK+jBRiZVnQH0AgS7o6pXrIgUTUUGiM= +github.com/AllenDang/imgui-go v1.12.1-0.20220322114136-499bbf6a42ad/go.mod h1:kuPs9RWleaUuK7D49bE6HPxyRA36Lp4ICKGp+5OnnbY= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 h1:baVdMKlASEHrj19iqjARrPbaRisD7EuZEVJj6ZMLl1Q= github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3/go.mod h1:VEPNJUlxl5KdWjDvz6Q1l+rJlxF2i6xqDeGuGAxa87M= -github.com/go-gl/gl v0.0.0-20210315015930-ae072cafe09d/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= -github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259 h1:8q7+xl2D2qHPLTII1t4vSMNP2VKwDcn+Avf2WXvdB1A= -github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4= -github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q= +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 h1:TL70PMkdPCt9cRhKTqsm+giRpgrd0IGEj763nNr2VFY= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc= +golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/eapache/queue.v1 v1.1.0 h1:EldqoJEGtXYiVCMRo2C9mePO2UUGnYn2+qLmlQSqPdc= +gopkg.in/eapache/queue.v1 v1.1.0/go.mod h1:wNtmx1/O7kZSR9zNT1TTOJ7GLpm3Vn7srzlfylFbQwU= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=