Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add New GPU Texture Backend for advanced Usages (see paint example !) #855

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e4c323b
phase1
cjbrigato Sep 24, 2024
336a565
Rebase master and needed updates
cjbrigato Sep 25, 2024
21e717e
linting a bit
cjbrigato Sep 25, 2024
106c23e
linting, linting, linting like there is no tommorow
cjbrigato Sep 25, 2024
c496911
linting...
cjbrigato Sep 25, 2024
ba2915e
Add the marvelous StatefulReflectiveBoundTexture:
cjbrigato Sep 25, 2024
3f69f4e
minor logic flaw in reseting lastError of StatefulReflectiveBoundTexture
cjbrigato Sep 25, 2024
b791da3
Add asyncimage example
cjbrigato Sep 26, 2024
80d2349
Exports package level error, avoid useless globals
cjbrigato Sep 26, 2024
d1485b1
Add ImageWithX facilities to ReflectiveBoundTexture
cjbrigato Sep 26, 2024
63f0d09
Change the Design of ImageWidgets
cjbrigato Sep 26, 2024
f0ce97f
Linter
cjbrigato Sep 26, 2024
c538af1
Fixes ImageButtonWithRgba+Widget
cjbrigato Sep 26, 2024
8d79d67
Add missing documentation for all new objets provided
cjbrigato Sep 26, 2024
634e315
Linter, as always
cjbrigato Sep 26, 2024
28fb9bd
asyncimage: add horizontal scrollbar un image child window
cjbrigato Sep 26, 2024
9d57d2b
Add base 'paint' example to demonstrate more usage of ReflectiveBound…
cjbrigato Sep 27, 2024
61b1f51
'gucio321' linter for examples :p, interface checks
cjbrigato Sep 27, 2024
cd76014
'gucio32' example linter on paint
cjbrigato Sep 27, 2024
a46b694
naming things is hard
cjbrigato Sep 27, 2024
f719e52
embed assets. you can now do go run .\examples\paint
cjbrigato Sep 29, 2024
8006738
linter
cjbrigato Sep 29, 2024
9963037
Add 'undo' function
cjbrigato Sep 30, 2024
5f49f7a
precompute canvas at undo level to avoid blinking
cjbrigato Sep 30, 2024
a45a2aa
Fix unecessary double clicking canvas
cjbrigato Sep 30, 2024
4137a7e
add brush size setting in toolbar
cjbrigato Sep 30, 2024
6019b1c
Avoid dup code
cjbrigato Sep 30, 2024
93fbec3
refacto toolbar loop
cjbrigato Sep 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 7 additions & 15 deletions ClickableWidgets.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,16 +284,18 @@ var _ Widget = &ImageButtonWithRgbaWidget{}
// more useful than the original ImageButtonWidget.
type ImageButtonWithRgbaWidget struct {
*ImageButtonWidget
rgba image.Image
id ID
id ID
rbt *ReflectiveBoundTexture
}

// ImageButtonWithRgba creates a new widget.
func ImageButtonWithRgba(rgba image.Image) *ImageButtonWithRgbaWidget {
func ImageButtonWithRgba(rgba image.Image, rbt *ReflectiveBoundTexture) *ImageButtonWithRgbaWidget {
_ = rbt.SetSurfaceFromRGBA(ImageToRgba(rgba), false)

return &ImageButtonWithRgbaWidget{
id: GenAutoID("ImageButtonWithRgba"),
ImageButtonWidget: ImageButton(nil),
rgba: rgba,
rbt: rbt,
ImageButtonWidget: ImageButton(rbt.ToImageWidget().texture),
}
}

Expand Down Expand Up @@ -335,16 +337,6 @@ func (b *ImageButtonWithRgbaWidget) FramePadding(padding int) *ImageButtonWithRg

// Build implements Widget interface.
func (b *ImageButtonWithRgbaWidget) Build() {
if state := GetState[imageState](Context, b.id.String()); state == nil {
SetState(Context, b.id.String(), &imageState{})

NewTextureFromRgba(b.rgba, func(tex *Texture) {
SetState(Context, b.id.String(), &imageState{texture: tex})
})
} else {
b.ImageButtonWidget.texture = state.texture
}

b.ImageButtonWidget.Build()
}

Expand Down
193 changes: 59 additions & 134 deletions ImageWidgets.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package giu

import (
ctx "context"
"fmt"
"image"
"image/color"
"net/http"
"reflect"
"time"

"github.com/AllenDang/cimgui-go/imgui"
Expand All @@ -17,13 +14,14 @@ var _ Widget = &ImageWidget{}
// ImageWidget adds an image.
// The default size is the size of the image,
// to set a specific size, use .Size(width, height).
// NOTE: ImageWidget is going to be deprecated. ImageWithRGBAWidget
// should be used instead, however, because it is a native
// imgui's solution it is still there.
// NOTE: ImageWidget is not something useful in its own,
// but an important underlying object of higher level objects.
// Also, it is an imgui native solution.
type ImageWidget struct {
texture *Texture
width float32
height float32
scale imgui.Vec2
uv0, uv1 imgui.Vec2
tintColor, borderColor color.Color
onClick func()
Expand All @@ -35,6 +33,7 @@ func Image(texture *Texture) *ImageWidget {
texture: texture,
width: 0,
height: 0,
scale: imgui.Vec2{X: 1, Y: 1},
uv0: imgui.Vec2{X: 0, Y: 0},
uv1: imgui.Vec2{X: 1, Y: 1},
tintColor: color.RGBA{255, 255, 255, 255},
Expand Down Expand Up @@ -74,6 +73,13 @@ func (i *ImageWidget) Size(width, height float32) *ImageWidget {
return i
}

// Scale multiply dimensions after size.
func (i *ImageWidget) Scale(scaleX, scaleY float32) *ImageWidget {
// Size image with DPI scaling
i.scale = imgui.Vec2{X: scaleX, Y: scaleY}
return i
}

// Build implements Widget interface.
func (i *ImageWidget) Build() {
if i.width == 0 && i.height == 0 {
Expand All @@ -96,6 +102,9 @@ func (i *ImageWidget) Build() {
size.Y = rect.Y
}

size.X *= i.scale.X
size.Y *= i.scale.Y

if i.texture == nil || i.texture.tex == nil {
Dummy(size.X, size.Y).Build()
return
Expand All @@ -107,51 +116,36 @@ func (i *ImageWidget) Build() {
mousePos := GetMousePos()

if cursorPos.X <= mousePos.X && cursorPos.Y <= mousePos.Y &&
cursorPos.X+int(i.width) >= mousePos.X && cursorPos.Y+int(i.height) >= mousePos.Y {
cursorPos.X+int(size.X) >= mousePos.X && cursorPos.Y+int(size.Y) >= mousePos.Y {
i.onClick()
}
}

imgui.ImageV(i.texture.tex.ID, size, i.uv0, i.uv1, ToVec4Color(i.tintColor), ToVec4Color(i.borderColor))
}

type imageState struct {
loading bool
failure bool
cancel ctx.CancelFunc
texture *Texture
img *image.RGBA
}

// Dispose cleans imageState (implements Disposable interface).
func (is *imageState) Dispose() {
is.texture = nil
// Cancel ongoing image downloading
if is.loading && is.cancel != nil {
is.cancel()
}
}

var _ Widget = &ImageWithRgbaWidget{}

// ImageWithRgbaWidget wraps 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 ID
rgba *image.RGBA
img *ImageWidget
id ID
rbt *ReflectiveBoundTexture
img *ImageWidget
}

// ImageWithRgba creates ImageWithRgbaWidget.
// The default size is the size of the image,
// to set a specific size, use .Size(width, height).
func ImageWithRgba(rgba image.Image) *ImageWithRgbaWidget {
Copy link
Collaborator

@gucio321 gucio321 Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that to keep giu as simple as possible, I'd suggest to auto-create this second variable and sotre it in imageState (as it was with *Texture)
If you think that user might want to have access to it, add a getter to ImageWithXXXWidget.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

func ImageWithRgba(rgba image.Image, rbt *ReflectiveBoundTexture) *ImageWithRgbaWidget {
_ = rbt.SetSurfaceFromRGBA(ImageToRgba(rgba), false)

return &ImageWithRgbaWidget{
id: GenAutoID("ImageWithRgba"),
rgba: ImageToRgba(rgba),
img: Image(nil),
id: GenAutoID("ImageWithRgba"),
rbt: rbt,
img: rbt.ToImageWidget(),
}
}

Expand All @@ -175,20 +169,6 @@ func (i *ImageWithRgbaWidget) OnClick(cb func()) *ImageWithRgbaWidget {

// Build implements Widget interface.
func (i *ImageWithRgbaWidget) Build() {
if i.rgba != nil {
var imgState *imageState
if imgState = GetState[imageState](Context, i.id.String()); imgState == nil || !reflect.DeepEqual(i.rgba, imgState.img) {
imgState = &imageState{}
SetState(Context, i.id.String(), imgState)

NewTextureFromRgba(i.rgba, func(tex *Texture) {
imgState.texture = tex
})
}

i.img.texture = imgState.texture
}

i.img.Build()
}

Expand All @@ -200,19 +180,21 @@ var _ Widget = &ImageWithFileWidget{}
// 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
id string
rbt *ReflectiveBoundTexture
img *ImageWidget
}

// ImageWithFile constructs a new ImageWithFileWidget.
// The default size is the size of the image,
// to set a specific size, use .Size(width, height).
func ImageWithFile(imgPath string) *ImageWithFileWidget {
func ImageWithFile(imgPath string, rbt *ReflectiveBoundTexture) *ImageWithFileWidget {
_ = rbt.SetSurfaceFromFile(imgPath, false)

return &ImageWithFileWidget{
id: fmt.Sprintf("ImageWithFile_%s", imgPath),
imgPath: imgPath,
img: Image(nil),
id: fmt.Sprintf("ImageWithFile_%s", imgPath),
rbt: rbt,
img: rbt.ToImageWidget(),
}
}

Expand All @@ -236,21 +218,6 @@ func (i *ImageWithFileWidget) OnClick(cb func()) *ImageWithFileWidget {

// Build implements Widget interface.
func (i *ImageWithFileWidget) Build() {
var imgState *imageState
if imgState = GetState[imageState](Context, i.id); imgState == nil {
// Prevent multiple invocation to LoadImage.
imgState = &imageState{}
SetState(Context, i.id, imgState)

img, err := LoadImage(i.imgPath)
if err == nil {
NewTextureFromRgba(img, func(tex *Texture) {
imgState.texture = tex
})
}
}

i.img.texture = imgState.texture
i.img.Build()
}

Expand All @@ -260,22 +227,25 @@ var _ Widget = &ImageWithURLWidget{}
// an URL as image source.
type ImageWithURLWidget struct {
id string
srbt *StatefulReflectiveBoundTexture
imgURL string
downloadTimeout time.Duration
whenLoading Layout
whenFailure Layout
onReady func()
onFailure func(error)
onLoading func()
img *ImageWidget
}

// ImageWithURL creates ImageWithURLWidget.
// The default size is the size of the image,
// to set a specific size, use .Size(width, height).
func ImageWithURL(url string) *ImageWithURLWidget {
func ImageWithURL(url string, srbt *StatefulReflectiveBoundTexture) *ImageWithURLWidget {
return &ImageWithURLWidget{
id: fmt.Sprintf("ImageWithURL_%s", url),
imgURL: url,
srbt: srbt,
downloadTimeout: 10 * time.Second,
whenLoading: Layout{Dummy(100, 100)},
whenFailure: Layout{Dummy(100, 100)},
Expand All @@ -295,6 +265,12 @@ func (i *ImageWithURLWidget) OnFailure(onFailure func(error)) *ImageWithURLWidge
return i
}

// OnLoading sets event trigger when image starts download/load.
func (i *ImageWithURLWidget) OnLoading(onLoading func()) *ImageWithURLWidget {
i.onLoading = onLoading
return i
}

// OnClick sets click callback.
func (i *ImageWithURLWidget) OnClick(cb func()) *ImageWithURLWidget {
i.img.OnClick(cb)
Expand Down Expand Up @@ -327,79 +303,28 @@ func (i *ImageWithURLWidget) LayoutForFailure(widgets ...Widget) *ImageWithURLWi

// Build implements Widget interface.
func (i *ImageWithURLWidget) Build() {
var imgState *imageState
if imgState = GetState[imageState](Context, i.id); imgState == nil {
imgState = &imageState{}
SetState(Context, i.id, imgState)

// Prevent multiple invocation to download image.
downloadContext, cancelFunc := ctx.WithCancel(ctx.Background())

SetState(Context, i.id, &imageState{loading: true, cancel: cancelFunc})

errorFn := func(err error) {
SetState(Context, i.id, &imageState{failure: true})
if i.onFailure != nil {
i.srbt.onFailure = i.onFailure
}

// Trigger onFailure event
if i.onFailure != nil {
i.onFailure(err)
}
}
if i.onLoading != nil {
i.srbt.onLoading = i.onLoading
}

go func() {
// Load image from url
client := &http.Client{Timeout: i.downloadTimeout}

req, err := http.NewRequestWithContext(downloadContext, "GET", i.imgURL, http.NoBody)
if err != nil {
errorFn(err)
return
}

resp, err := client.Do(req)
if err != nil {
errorFn(err)
return
}

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)

EnqueueNewTextureFromRgba(rgba, func(tex *Texture) {
SetState(Context, i.id, &imageState{
loading: false,
failure: false,
texture: tex,
})
})

// Trigger onReady event
if i.onReady != nil {
i.onReady()
}
}()
if i.onReady != nil {
i.srbt.onSuccess = i.onReady
}

imgState = GetState[imageState](Context, i.id)
_ = i.srbt.SetSurfaceFromURL(i.imgURL, i.downloadTimeout, false)

switch {
case imgState.failure:
state := i.srbt.GetState()
switch state {
case SsFailure:
i.whenFailure.Build()
case imgState.loading:
case SsLoading:
i.whenLoading.Build()
default:
i.img.texture = imgState.texture
case SsSuccess:
i.img.texture = i.srbt.ToImageWidget().texture
i.img.Build()
}
}
Loading
Loading