diff --git a/ReflectiveBoundTexture.go b/ReflectiveBoundTexture.go new file mode 100644 index 00000000..a4750f3a --- /dev/null +++ b/ReflectiveBoundTexture.go @@ -0,0 +1,258 @@ +package giu + +import ( + "errors" + "hash/crc32" + "image" + "image/color" + "sync" + + "github.com/AllenDang/cimgui-go/imgui" +) + +// ErrNilRGBA is an error that indicates the RGBA surface result is nil. +var ErrNilRGBA = errors.New("surface RGBA Result is nil") + +// defaultSurface returns a default RGBA surface with a uniform color. +func defaultSurface() *image.RGBA { + surface, _ := NewUniformLoader(128.0, 128.0, color.RGBA{255, 255, 255, 255}).ServeRGBA() + return surface +} + +// ReflectiveBoundTexture represents a texture that can be dynamically updated and bound to a GPU. +type ReflectiveBoundTexture struct { + Surface *image.RGBA // Surface is the RGBA image data for the texture. + tex *Texture // tex is the GPU texture resource. + lastSum uint32 // lastSum is the checksum of the last surface data. + mu sync.Mutex // mu is a mutex to protect concurrent access to the texture. + fsroot string // fsroot is the root filesystem path for the texture when using file scheme in URL Surface Loader +} + +/* Return a waranted: + * - Initialized + * - With proper resources bindings against gpu (free old, bound new) + * - Up to date Texture. + */ + +// commit updates the ReflectiveBoundTexture by checking if the surface has changed, +// and if so, rebinds the texture to the GPU. It returns the updated ReflectiveBoundTexture +// and a boolean indicating whether the texture has changed. +// +// The method locks the texture for concurrent access, calculates the checksum of the +// current surface, and compares it with the last stored checksum. If the checksums differ, +// it unbinds the old texture, binds the new one, and updates the checksum. +// +// Returns: +// - *ReflectiveBoundTexture: The updated texture object. +// - bool: True if the texture has changed, false otherwise. +func (i *ReflectiveBoundTexture) commit() (*ReflectiveBoundTexture, bool) { + i.mu.Lock() + defer i.mu.Unlock() + + if i.Surface == nil { + i.Surface = defaultSurface() + } + + var hasChanged bool + if sum := crc32.ChecksumIEEE(i.Surface.Pix); sum != i.lastSum { + hasChanged = true + + i.unbind() + i.bind() + i.lastSum = sum + } + + return i, hasChanged +} + +// SetSurfaceFromRGBA sets the surface of the ReflectiveBoundTexture from the provided RGBA image. +// If the provided image is nil, it returns an error. If the commit flag is true, it commits the changes. +// +// Parameters: +// - img: The RGBA image to set as the surface. +// - commit: A boolean flag indicating whether to commit the changes. +// +// Returns: +// - error: An error if the provided image is nil, otherwise nil. +func (i *ReflectiveBoundTexture) SetSurfaceFromRGBA(img *image.RGBA, commit bool) error { + if img != nil { + i.Surface = img + } else { + return ErrNilRGBA + } + + if commit { + i.commit() + } + + return nil +} + +// ToImageWidget converts the ReflectiveBoundTexture to an ImageWidget. +// +// Returns: +// - *ImageWidget: The ImageWidget representation of the ReflectiveBoundTexture. +func (i *ReflectiveBoundTexture) ToImageWidget() *ImageWidget { + return Image(i.Texture()) +} + +// ImguiImageVOptionStruct represents the options for rendering an image in ImGui. +type ImguiImageVOptionStruct struct { + Uv0 imgui.Vec2 // The UV coordinate of the top-left corner of the image. + Uv1 imgui.Vec2 // The UV coordinate of the bottom-right corner of the image. + TintCol imgui.Vec4 // The tint color to apply to the image. + BorderCol imgui.Vec4 // The border color to apply to the image. +} + +// GetImGuiImageVDefaultOptionsStruct returns the default options for rendering an image in ImGui. +// +// Returns: +// - ImguiImageVOptionStruct: The default options for rendering an image. +func (i *ReflectiveBoundTexture) GetImGuiImageVDefaultOptionsStruct() ImguiImageVOptionStruct { + return ImguiImageVOptionStruct{ + Uv0: imgui.Vec2{X: 0, Y: 0}, + Uv1: imgui.Vec2{X: 1, Y: 1}, + TintCol: imgui.Vec4{X: 1, Y: 1, Z: 1, W: 1}, + BorderCol: imgui.Vec4{X: 0, Y: 0, Z: 0, W: 0}, + } +} + +// ImguiImage renders the ReflectiveBoundTexture as an image in ImGui. +// +// Parameters: +// - width: The width of the image. If set to -1, it will use the available content region width. +// - height: The height of the image. If set to -1, it will use the available content region height. +// - options: The options for rendering the image. +func (i *ReflectiveBoundTexture) ImguiImage(width, height float32) { + size := imgui.Vec2{X: width, Y: height} + + if size.X == -1 { + rect := imgui.ContentRegionAvail() + size.X = rect.X + } + + if size.Y == -1 { + rect := imgui.ContentRegionAvail() + size.Y = rect.Y + } + + imgui.Image(i.Texture().ID(), size) +} + +// ImguiImageV renders the ReflectiveBoundTexture as an image in ImGui with additional options. +// +// Parameters: +// - width: The width of the image. If set to -1, it will use the available content region width. +// - height: The height of the image. If set to -1, it will use the available content region height. +// - options: The options for rendering the image, including UV coordinates, tint color, and border color. +func (i *ReflectiveBoundTexture) ImguiImageV(width, height float32, options ImguiImageVOptionStruct) { + size := imgui.Vec2{X: width, Y: height} + + if size.X == -1 { + rect := imgui.ContentRegionAvail() + size.X = rect.X + } + + if size.Y == -1 { + rect := imgui.ContentRegionAvail() + size.Y = rect.Y + } + + imgui.ImageV(i.Texture().ID(), size, options.Uv0, options.Uv1, options.TintCol, options.BorderCol) +} + +// ImguiImageButtonV renders the ReflectiveBoundTexture as an image button in ImGui with additional options. +// +// Parameters: +// - id: The ID of the image button. +// - width: The width of the image button. If set to -1, it will use the available content region width. +// - height: The height of the image button. If set to -1, it will use the available content region height. +// - options: The options for rendering the image button, including UV coordinates, tint color, and border color. +func (i *ReflectiveBoundTexture) ImguiImageButtonV(id string, width, height float32, options ImguiImageVOptionStruct) { + size := imgui.Vec2{X: width, Y: height} + + if size.X == -1 { + rect := imgui.ContentRegionAvail() + size.X = rect.X + } + + if size.Y == -1 { + rect := imgui.ContentRegionAvail() + size.Y = rect.Y + } + + imgui.ImageButtonV(id, i.Texture().ID(), size, options.Uv0, options.Uv1, options.TintCol, options.BorderCol) +} + +// unbind releases the texture associated with the ReflectiveBoundTexture from the backend. +func (i *ReflectiveBoundTexture) unbind() { + if i.tex != nil { + Context.Backend().DeleteTexture(i.tex.ID()) + i.tex = nil + } +} + +// bind creates a new texture from the RGBA surface and assigns it to the ReflectiveBoundTexture. +func (i *ReflectiveBoundTexture) bind() { + NewTextureFromRgba(i.Surface, func(tex *Texture) { + i.tex = tex + }) +} + +// GetSurfaceWidth returns the width of the RGBA surface. +func (i *ReflectiveBoundTexture) GetSurfaceWidth() int { + return i.Surface.Bounds().Dx() +} + +// GetSurfaceHeight returns the height of the RGBA surface. +func (i *ReflectiveBoundTexture) GetSurfaceHeight() int { + return i.Surface.Bounds().Dy() +} + +// GetSurfaceSize returns the size of the RGBA surface as an image.Point. +func (i *ReflectiveBoundTexture) GetSurfaceSize() image.Point { + return i.Surface.Bounds().Size() +} + +// Texture commits any pending changes to the RGBA surface and returns the associated texture. +func (i *ReflectiveBoundTexture) Texture() *Texture { + i.commit() + return i.tex +} + +// TextureID commits any pending changes and returns the ImGui TextureID of the associated texture. +func (i *ReflectiveBoundTexture) TextureID() imgui.TextureID { + return i.Texture().ID() +} + +// GetRGBA returns the RGBA surface of the ReflectiveBoundTexture. +// If the commit parameter is true, it commits any pending changes before returning the surface. +// +// Parameters: +// - commit: A boolean indicating whether to commit any pending changes. +// +// Returns: +// - *image.RGBA: The RGBA surface of the ReflectiveBoundTexture. +func (i *ReflectiveBoundTexture) GetRGBA(commit bool) *image.RGBA { + if commit { + i.commit() + } + + return i.Surface +} + +// ForceRelease forces releasing resources against all finalizers, +// effectively losing the object but ensuring both RAM and VRAM +// are freed. +func (i *ReflectiveBoundTexture) ForceRelease() { + i.unbind() + i.Surface = nil + + var u uint32 + i.lastSum = u +} + +// ForceCommit forces committing. +func (i *ReflectiveBoundTexture) ForceCommit() (*ReflectiveBoundTexture, bool) { + return i.commit() +} diff --git a/StatefulReflectiveBoundTexture.go b/StatefulReflectiveBoundTexture.go new file mode 100644 index 00000000..487d6a5f --- /dev/null +++ b/StatefulReflectiveBoundTexture.go @@ -0,0 +1,180 @@ +package giu + +import ( + "errors" + "fmt" +) + +// ErrNeedReset is an error indicating that the surface cannot be loaded without a reset. +// The method (*StatefulReflectiveBoundTexture).ResetState() should be called. +var ErrNeedReset = errors.New("cannot load surface without a reset. Should call (*StatefulReflectiveBoundTexture).ResetState()") + +// ErrIsLoading is an error indicating that the surface state cannot be reset while loading. +var ErrIsLoading = errors.New("cannot reset surface state while loading") + +// SurfaceState represents the state of the surface. +type SurfaceState int + +//go:generate stringer -type=SurfaceState +const ( + // SurfaceStateNone indicates that the surface state is none. + SurfaceStateNone SurfaceState = iota + // SurfaceStateLoading indicates that the surface is currently loading. + SurfaceStateLoading + // SurfaceStateFailure indicates that the surface loading has failed. + SurfaceStateFailure + // SurfaceStateSuccess indicates that the surface loading was successful. + SurfaceStateSuccess +) + +// StatefulReflectiveBoundTexture is a ReflectiveBoundTexture with added async, states, and event callbacks. +type StatefulReflectiveBoundTexture struct { + ReflectiveBoundTexture + state SurfaceState + lastError error + onReset func() + onLoading func() + onSuccess func() + onFailure func(error) +} + +// GetState returns the current state of the surface. +// +// Returns: +// - SurfaceState: The current state of the surface. +func (s *StatefulReflectiveBoundTexture) GetState() SurfaceState { + return s.state +} + +// GetLastError returns the last error that occurred during surface loading. +// +// Returns: +// - error: The last error that occurred, or nil if no error occurred. +func (s *StatefulReflectiveBoundTexture) GetLastError() error { + return s.lastError +} + +// OnReset sets the callback function to be called when the surface state is reset. +// +// Parameters: +// - fn: The callback function to be called on reset. +// +// Returns: +// - *StatefulReflectiveBoundTexture: The current instance of StatefulReflectiveBoundTexture. +func (s *StatefulReflectiveBoundTexture) OnReset(fn func()) *StatefulReflectiveBoundTexture { + s.onReset = fn + return s +} + +// OnLoading sets the callback function to be called when the surface is loading. +// +// Parameters: +// - fn: The callback function to be called on loading. +// +// Returns: +// - *StatefulReflectiveBoundTexture: The current instance of StatefulReflectiveBoundTexture. +func (s *StatefulReflectiveBoundTexture) OnLoading(fn func()) *StatefulReflectiveBoundTexture { + s.onLoading = fn + return s +} + +// OnSuccess sets the callback function to be called when the surface loading is successful. +// +// Parameters: +// - fn: The callback function to be called on success. +// +// Returns: +// - *StatefulReflectiveBoundTexture: The current instance of StatefulReflectiveBoundTexture. +func (s *StatefulReflectiveBoundTexture) OnSuccess(fn func()) *StatefulReflectiveBoundTexture { + s.onSuccess = fn + return s +} + +// OnFailure sets the callback function to be called when the surface loading fails. +// +// Parameters: +// - fn: The callback function to be called on failure, with the error as a parameter. +// +// Returns: +// - *StatefulReflectiveBoundTexture: The current instance of StatefulReflectiveBoundTexture. +func (s *StatefulReflectiveBoundTexture) OnFailure(fn func(error)) *StatefulReflectiveBoundTexture { + s.onFailure = fn + return s +} + +// ResetState resets the state of the StatefulReflectiveBoundTexture. +// +// Returns: +// - error: An error if the state is currently loading, otherwise nil. +func (s *StatefulReflectiveBoundTexture) ResetState() error { + switch s.state { + case SurfaceStateNone: + return nil + case SurfaceStateLoading: + return ErrIsLoading + default: + s.state = SurfaceStateNone + s.lastError = nil + + if s.onReset != nil { + go s.onReset() + } + } + + return nil +} + +// LoadSurfaceAsync loads the surface asynchronously using the provided SurfaceLoader. +// It sets the state to loading, and upon completion, updates the state to success or failure +// based on the result. It also triggers the appropriate callback functions. +// +// Parameters: +// - loader: The SurfaceLoader to use for loading the surface. +// - commit: A boolean flag indicating whether to commit the changes. +// +// Returns: +// - error: An error if the state is not SsNone, otherwise nil. +func (s *StatefulReflectiveBoundTexture) LoadSurfaceAsync(loader SurfaceLoader, commit bool) error { + if s.state != SurfaceStateNone { + return ErrNeedReset + } + + s.state = SurfaceStateLoading + if s.onLoading != nil { + go s.onLoading() + } + + go func() { + img, err := loader.ServeRGBA() + if err != nil { + s.state = SurfaceStateFailure + s.lastError = fmt.Errorf("in ReflectiveBoundTexture LoadSurface after loader.ServeRGBA: %w", err) + + if s.onFailure != nil { + go s.onFailure(s.lastError) + } + + return + } + + e := s.SetSurfaceFromRGBA(img, commit) + if e != nil { + s.state = SurfaceStateFailure + s.lastError = fmt.Errorf("in ReflectiveBoundTexture LoadSurface after SetSurfaceFromRGBA: %w", err) + + if s.onFailure != nil { + go s.onFailure(s.lastError) + } + + return + } + + s.state = SurfaceStateSuccess + + if s.onSuccess != nil { + go s.onSuccess() + } + }() + + return nil +} diff --git a/SurfaceLoaders.go b/SurfaceLoaders.go new file mode 100644 index 00000000..098df972 --- /dev/null +++ b/SurfaceLoaders.go @@ -0,0 +1,360 @@ +package giu + +import ( + go_ctx "context" + "fmt" + "image" + "image/color" + "image/draw" + "io/fs" + "log" + "net/http" + "time" +) + +// SurfaceLoader is an interface that defines a method to serve an RGBA image. +type SurfaceLoader interface { + // ServeRGBA serves an RGBA image. + // + // Returns: + // - *image.RGBA: The RGBA image. + // - error: An error if the image could not be served. + ServeRGBA() (*image.RGBA, error) +} + +// SurfaceLoaderFunc is a function type that serves an RGBA image. +type SurfaceLoaderFunc func() (*image.RGBA, error) + +// LoadSurfaceFunc loads a surface using a SurfaceLoaderFunc. +// +// Parameters: +// - fn: The SurfaceLoaderFunc to use for loading the surface. +// - commit: A boolean flag indicating whether to commit the changes. +// +// Returns: +// - error: An error if the surface could not be loaded. +func (i *ReflectiveBoundTexture) LoadSurfaceFunc(fn SurfaceLoaderFunc, commit bool) error { + img, err := fn() + if err != nil { + return err + } + + return i.SetSurfaceFromRGBA(img, commit) +} + +// LoadSurface loads a surface using a SurfaceLoader. +// +// Parameters: +// - loader: The SurfaceLoader to use for loading the surface. +// - commit: A boolean flag indicating whether to commit the changes. +// +// Returns: +// - error: An error if the surface could not be loaded. +func (i *ReflectiveBoundTexture) LoadSurface(loader SurfaceLoader, commit bool) error { + img, err := loader.ServeRGBA() + if err != nil { + return fmt.Errorf("in ReflectiveBoundTexture LoadSurface after loader.ServeRGBA: %w", err) + } + + return i.SetSurfaceFromRGBA(img, commit) +} + +// LoadSurface loads a surface asynchronously using a SurfaceLoader. +// +// Parameters: +// - loader: The SurfaceLoader to use for loading the surface. +// - commit: A boolean flag indicating whether to commit the changes. +// +// Returns: +// - error: An error if the surface could not be loaded. +func (s *StatefulReflectiveBoundTexture) LoadSurface(loader SurfaceLoader, commit bool) error { + return s.LoadSurfaceAsync(loader, commit) +} + +var _ SurfaceLoader = &FileLoader{} + +// FileLoader is a struct that implements the SurfaceLoader interface for loading images from a file. +type FileLoader struct { + path string +} + +// ServeRGBA loads an RGBA image from the file specified by the path in fileLoader. +// +// Returns: +// - *image.RGBA: The loaded RGBA image. +// - error: An error if the image could not be loaded. +func (f *FileLoader) ServeRGBA() (*image.RGBA, error) { + img, err := LoadImage(f.path) + if err != nil { + return nil, err + } + + return img, nil +} + +// NewFileLoader creates a new SurfaceLoader that loads images from the specified file path. +// +// Parameters: +// - path: The path to the file to load the image from. +// +// Returns: +// - SurfaceLoader: A new SurfaceLoader for loading images from the specified file path. +func NewFileLoader(path string) *FileLoader { + return &FileLoader{ + path: path, + } +} + +// SetSurfaceFromFile loads an image from the specified file path and sets it as the surface of the ReflectiveBoundTexture. +// +// Parameters: +// - path: The path to the file to load the image from. +// - commit: A boolean flag indicating whether to commit the changes. +// +// Returns: +// - error: An error if the image could not be loaded or set as the surface. +func (i *ReflectiveBoundTexture) SetSurfaceFromFile(path string, commit bool) error { + return i.LoadSurface(NewFileLoader(path), commit) +} + +// SetSurfaceFromFile loads an image from the specified file path and sets it as the surface of the StatefulReflectiveBoundTexture. +// +// Parameters: +// - path: The path to the file to load the image from. +// - commit: A boolean flag indicating whether to commit the changes. +// +// Returns: +// - error: An error if the image could not be loaded or set as the surface. +func (s *StatefulReflectiveBoundTexture) SetSurfaceFromFile(path string, commit bool) error { + return s.LoadSurface(NewFileLoader(path), commit) +} + +var _ SurfaceLoader = &FsFileLoader{} + +// FsFileLoader is a struct that implements the SurfaceLoader interface for loading images from a file and embedded fs. +type FsFileLoader struct { + file fs.File +} + +// ServeRGBA loads an RGBA image from the file specified by the path in fileLoader. +// +// Returns: +// - *image.RGBA: The loaded RGBA image. +// - error: An error if the image could not be loaded. +func (f *FsFileLoader) ServeRGBA() (*image.RGBA, error) { + img, err := PNGToRgba(f.file) + if err != nil { + return nil, err + } + + return img, nil +} + +// NewFsFileLoader creates a new SurfaceLoader that loads images from the specified file interface. +// +// Parameters: +// - file: the file interface representing the file +// +// Returns: +// - SurfaceLoader: A new SurfaceLoader for loading images from the specified file path. +func NewFsFileLoader(file fs.File) *FsFileLoader { + return &FsFileLoader{ + file: file, + } +} + +// SetSurfaceFromFsFile loads an image from the specified file interface and sets it as the surface of the ReflectiveBoundTexture. +// +// Parameters: +// - file: the file interface representing the file +// - commit: A boolean flag indicating whether to commit the changes. +// +// Returns: +// - error: An error if the image could not be loaded or set as the surface. +func (i *ReflectiveBoundTexture) SetSurfaceFromFsFile(file fs.File, commit bool) error { + return i.LoadSurface(NewFsFileLoader(file), commit) +} + +// SetSurfaceFromFsFile loads an image from the specified file interface and sets it as the surface of the StatefulReflectiveBoundTexture. +// +// Parameters: +// - file: the file interface representing the file +// - commit: A boolean flag indicating whether to commit the changes. +// +// Returns: +// - error: An error if the image could not be loaded or set as the surface. +func (s *StatefulReflectiveBoundTexture) SetSurfaceFromFsFile(file fs.File, commit bool) error { + return s.LoadSurface(NewFsFileLoader(file), commit) +} + +var _ SurfaceLoader = &URLLoader{} + +// URLLoader is a SurfaceLoader that loads images from a specified URL. +type URLLoader struct { + url string + timeout time.Duration + httpdir string +} + +// ServeRGBA loads an image from the URL and returns it as an RGBA image. +// +// Returns: +// - *image.RGBA: The loaded RGBA image. +// - error: An error if the image could not be loaded. +func (u *URLLoader) ServeRGBA() (*image.RGBA, error) { + t := &http.Transport{} + t.RegisterProtocol("file", http.NewFileTransport(http.Dir(u.httpdir))) + + client := &http.Client{ + Transport: t, + Timeout: u.timeout, + } + + req, err := http.NewRequestWithContext(go_ctx.Background(), http.MethodGet, u.url, http.NoBody) + if err != nil { + return nil, fmt.Errorf("urlLoader serveRGBA after http.NewRequestWithContext: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("urlLoader serveRGBA after client.Do: %w", err) + } + + defer func() { + err := resp.Body.Close() + if err != nil { + log.Println(err) + } + }() + + img, _, err := image.Decode(resp.Body) + if err != nil { + return nil, fmt.Errorf("urlLoader serveRGBA after image.Decode: %w", err) + } + + return ImageToRgba(img), nil +} + +// NewURLLoader creates a new SurfaceLoader that loads images from the specified URL. +// +// Parameters: +// - url: The URL to load the image from. +// - httpdir: The root directory for file:// URLs. +// - timeout: The timeout duration for the HTTP request. +// +// Returns: +// - SurfaceLoader: A new SurfaceLoader for loading images from the specified URL. +func NewURLLoader(url, httpdir string, timeout time.Duration) *URLLoader { + return &URLLoader{ + url: url, + timeout: timeout, + httpdir: httpdir, + } +} + +// SetFSRoot sets the root directory for file:// URLs. +// +// Parameters: +// - root: The root directory to set. +func (i *ReflectiveBoundTexture) SetFSRoot(root string) { + i.fsroot = root +} + +// GetFSRoot returns the root directory for file:// URLs. +// +// Returns: +// - string: The root directory. +func (i *ReflectiveBoundTexture) GetFSRoot() string { + return i.fsroot +} + +// SetSurfaceFromURL loads an image from the specified URL and sets it as the surface of the ReflectiveBoundTexture. +// +// Parameters: +// - url: The URL to load the image from. +// - timeout: The timeout duration for the HTTP request. +// - commit: A boolean flag indicating whether to commit the changes. +// +// Returns: +// - error: An error if the image could not be loaded or set as the surface. +func (i *ReflectiveBoundTexture) SetSurfaceFromURL(url string, timeout time.Duration, commit bool) error { + return i.LoadSurface(NewURLLoader(url, i.fsroot, timeout), commit) +} + +// SetSurfaceFromURL loads an image from the specified URL and sets it as the surface of the StatefulReflectiveBoundTexture. +// +// Parameters: +// - url: The URL to load the image from. +// - timeout: The timeout duration for the HTTP request. +// - commit: A boolean flag indicating whether to commit the changes. +// +// Returns: +// - error: An error if the image could not be loaded or set as the surface. +func (s *StatefulReflectiveBoundTexture) SetSurfaceFromURL(url string, timeout time.Duration, commit bool) error { + return s.LoadSurface(NewURLLoader(url, s.fsroot, timeout), commit) +} + +var _ SurfaceLoader = &UniformLoader{} + +// UniformLoader is a SurfaceLoader that creates a uniform color image. +type UniformLoader struct { + width, height int + color color.Color +} + +// ServeRGBA creates a uniform color image and returns it as an RGBA image. +// +// Returns: +// - *image.RGBA: The created RGBA image. +// - error: An error if the image could not be created. +func (u *UniformLoader) ServeRGBA() (*image.RGBA, error) { + img := image.NewRGBA(image.Rect(0, 0, u.width, u.height)) + draw.Draw(img, img.Bounds(), &image.Uniform{u.color}, image.Point{}, draw.Src) + + return img, nil +} + +// NewUniformLoader creates a new SurfaceLoader that creates a uniform color image. +// +// Parameters: +// - width: The width of the image. +// - height: The height of the image. +// - c: The color of the image. +// +// Returns: +// - SurfaceLoader: A new SurfaceLoader for creating a uniform color image. +func NewUniformLoader(width, height int, c color.Color) *UniformLoader { + return &UniformLoader{ + width: width, + height: height, + color: c, + } +} + +// SetSurfaceUniform creates a uniform color image and sets it as the surface of the ReflectiveBoundTexture. +// +// Parameters: +// - width: The width of the image. +// - height: The height of the image. +// - c: The color of the image. +// - commit: A boolean flag indicating whether to commit the changes. +// +// Returns: +// - error: An error if the image could not be created or set as the surface. +func (i *ReflectiveBoundTexture) SetSurfaceUniform(width, height int, c color.Color, commit bool) error { + return i.LoadSurface(NewUniformLoader(width, height, c), commit) +} + +// SetSurfaceUniform creates a uniform color image and sets it as the surface of the StatefulReflectiveBoundTexture. +// +// Parameters: +// - width: The width of the image. +// - height: The height of the image. +// - c: The color of the image. +// - commit: A boolean flag indicating whether to commit the changes. +// +// Returns: +// - error: An error if the image could not be created or set as the surface. +func (s *StatefulReflectiveBoundTexture) SetSurfaceUniform(width, height int, c color.Color, commit bool) error { + return s.LoadSurface(NewUniformLoader(width, height, c), commit) +} diff --git a/Utils.go b/Utils.go index fdf59a0d..f6330945 100644 --- a/Utils.go +++ b/Utils.go @@ -6,6 +6,7 @@ import ( "image/color" "image/draw" "image/png" + "io/fs" "log" "os" "path/filepath" @@ -24,6 +25,16 @@ func (i ID) String() string { return string(i) } +// PNGToRgba loads image file interface and assume caller takes care of interface proper closing. +func PNGToRgba(file fs.File) (*image.RGBA, error) { + img, err := png.Decode(file) + if err != nil { + return nil, fmt.Errorf("LoadImage: error decoding png image: %w", err) + } + + return ImageToRgba(img), nil +} + // LoadImage loads image from file and returns *image.RGBA. func LoadImage(imgPath string) (*image.RGBA, error) { imgFile, err := os.Open(filepath.Clean(imgPath)) @@ -37,12 +48,7 @@ func LoadImage(imgPath string) (*image.RGBA, error) { } }() - img, err := png.Decode(imgFile) - if err != nil { - return nil, fmt.Errorf("LoadImage: error decoding png image: %w", err) - } - - return ImageToRgba(img), nil + return PNGToRgba(imgFile) } // ImageToRgba converts image.Image to *image.RGBA. diff --git a/examples/asyncimage/asyncimage.go b/examples/asyncimage/asyncimage.go new file mode 100644 index 00000000..8c3a0f88 --- /dev/null +++ b/examples/asyncimage/asyncimage.go @@ -0,0 +1,204 @@ +// Package main shows how to handle image async with state and events +package main + +import ( + "errors" + "fmt" + "image/color" + _ "image/jpeg" + _ "image/png" + "log" + "time" + + "github.com/AllenDang/cimgui-go/imgui" + + g "github.com/AllenDang/giu" +) + +var ( + fallback = &g.ReflectiveBoundTexture{} + dynamicImage = &g.StatefulReflectiveBoundTexture{} + imageScaleX = float32(1.0) + imageScaleY = float32(1.0) + linkedScale = true + dynamicImageURL = "https://www.pngfind.com/pngs/b/465-4652097_gopher-png.png" + showWindow = true + wnd *g.MasterWindow + footerLabel string + inputRootfs = "." +) + +func textHeader(str string, col color.RGBA) *g.StyleSetter { + return g.Style().SetColor(g.StyleColorText, col).To( + g.Align(g.AlignCenter).To( + g.Label(str), + ), + ) +} + +func canLoadHeader() *g.AlignmentSetter { + return g.Align(g.AlignCenter).To( + + g.Row( + g.InputText(&dynamicImageURL), + g.Button("Load Image from URL").OnClick(func() { + err := dynamicImage.ResetState() + if err == nil { + _ = dynamicImage.SetSurfaceFromURL(dynamicImageURL, time.Second*5, false) + } + }), + )) +} + +func headerWidget() g.Widget { + if dynamicImage.GetState() == g.SurfaceStateLoading { + return textHeader("Image Is Currently loading...", color.RGBA{0x80, 0x80, 0xFF, 255}) + } + + return canLoadHeader() +} + +func footerWidget(label string) g.Widget { + return g.Row(textHeader(label, color.RGBA{0xFF, 0xFF, 0xFF, 255})) +} + +func shouldReturnImage() g.Widget { + if dynamicImage.GetState() != g.SurfaceStateSuccess { + return fallback.ToImageWidget().Size(-1, -1) + } + + return g.Custom(func() { dynamicImage.ToImageWidget().Scale(imageScaleX, imageScaleY).Build() }) +} + +func shouldReturnPanel() g.Widget { + return g.Custom(func() { + imgui.SeparatorText("Image Scale") + + if imgui.Button("Reset##Scaling") { + imageScaleX = 1.0 + imageScaleY = 1.0 + } + + imgui.SameLine() + imgui.Checkbox("Linked##Scaling", &linkedScale) + + if linkedScale { + imgui.SliderFloat("scale XY##Scaling", &imageScaleX, 0.1, 4.0) + imageScaleY = imageScaleX + } else { + imgui.SliderFloat("scale X##Scaling", &imageScaleX, 0.1, 4.0) + imgui.SliderFloat("scale Y##Scaling", &imageScaleY, 0.1, 4.0) + } + + imgui.SeparatorText("FileSystem URLS") + + if dynamicImage.GetState() == g.SurfaceStateLoading { + imgui.Text("Unavailable while loading image...") + } else { + imgui.Text("Loading URLS Works with file:/// scheme too.") + imgui.Text("By default, root is executable working dir") + imgui.Text("-> Try loading this in the url bar:") + imgui.Text("file:///files/sonic.png ->") + + if imgui.Button("or CLICK HERE") { + inputRootfs = "." + dynamicImage.SetFSRoot(inputRootfs) + + dynamicImageURL = "file:///files/sonic.png" + err := dynamicImage.ResetState() + + if err == nil { + _ = dynamicImage.SetSurfaceFromURL(dynamicImageURL, time.Second*5, false) + } + + linkedScale = true + imageScaleX = 0.356 + imageScaleY = 0.356 + } + + imgui.Separator() + imgui.Text("Set rootFS to / for full filesystem access") + + rootfs := dynamicImage.GetFSRoot() + imgui.Text(fmt.Sprintf("Current ROOTFS: %s", rootfs)) + g.InputText(&inputRootfs).Build() + imgui.SameLine() + + if imgui.Button("SET rootfs") { + dynamicImage.SetFSRoot(inputRootfs) + } + } + }) +} + +func loop() { + if !showWindow { + wnd.SetShouldClose(true) + } + + g.PushColorWindowBg(color.RGBA{30, 30, 30, 255}) + g.Window("Async Images").IsOpen(&showWindow).Pos(10, 30).Size(1280, 720).Flags(g.WindowFlagsNoResize).Layout( + headerWidget(), + g.Separator(), + g.Row( + g.Child().Size(400, 625).Layout( + shouldReturnPanel(), + ), + g.Child().Flags(g.WindowFlagsHorizontalScrollbar).Size(-1, 625).Layout( + shouldReturnImage(), + )), + g.Separator(), + footerWidget(footerLabel), + ) + g.PopStyleColor() +} + +func noOSDecoratedWindowsConfig() g.MasterWindowFlags { + imgui.CreateContext() + io := imgui.CurrentIO() + io.SetConfigViewportsNoAutoMerge(true) + io.SetConfigViewportsNoDefaultParent(true) + io.SetConfigWindowsMoveFromTitleBarOnly(true) + + return g.MasterWindowFlagsHidden | g.MasterWindowFlagsTransparent | g.MasterWindowFlagsFrameless +} + +func initDynamicImage() error { + // SurfaceURL works from files:// too ! + // Note : the "root" of the scheme is willingly the Executable / working directory + if err := fallback.SetSurfaceFromURL("file:///files/fallback.png", time.Second*5, false); err != nil { + return fmt.Errorf("error at initDynamicImage: %w", err) + } + + dynamicImage.OnReset(func() { + log.Println("DynamicImage was reset !") + }).OnLoading(func() { + log.Println("DynamicImage Started Loading a new surface...") + }).OnFailure(func(e error) { + if !errors.Is(e, g.ErrNeedReset) { + footerLabel = fmt.Sprintf("DynamicImage failed loading with error: %v", e) + log.Printf("DynamicImage failed loading with error: %+v\n", e) + } + }).OnSuccess(func() { + footerLabel = "DynamicImage has successfully loaded new surface !" + + log.Println("DynamicImage has successfully loaded new surface !") + }) + + return nil +} + +func main() { + // This prepare creating a fully imgui window with no native decoration. + // Flags are to be used with NewMasterWindow. + // Should NOT use SingleLayoutWindow ! + mwFlags := noOSDecoratedWindowsConfig() + + if err := initDynamicImage(); err != nil { + log.Fatalf("Error in DynamicImage initialization: %v", err) + } + + wnd = g.NewMasterWindow("Load Image", 1280, 720, mwFlags) + wnd.SetTargetFPS(60) + wnd.Run(loop) +} diff --git a/examples/asyncimage/files/fallback.png b/examples/asyncimage/files/fallback.png new file mode 100644 index 00000000..292f2a37 Binary files /dev/null and b/examples/asyncimage/files/fallback.png differ diff --git a/examples/asyncimage/files/gopher.png b/examples/asyncimage/files/gopher.png new file mode 100644 index 00000000..80355628 Binary files /dev/null and b/examples/asyncimage/files/gopher.png differ diff --git a/examples/asyncimage/files/sonic.png b/examples/asyncimage/files/sonic.png new file mode 100644 index 00000000..d1d09c78 Binary files /dev/null and b/examples/asyncimage/files/sonic.png differ diff --git a/examples/loadimageAdvanced/fallback.png b/examples/loadimageAdvanced/fallback.png new file mode 100644 index 00000000..292f2a37 Binary files /dev/null and b/examples/loadimageAdvanced/fallback.png differ diff --git a/examples/loadimageAdvanced/gopher.png b/examples/loadimageAdvanced/gopher.png new file mode 100644 index 00000000..80355628 Binary files /dev/null and b/examples/loadimageAdvanced/gopher.png differ diff --git a/examples/loadimageAdvanced/loadimageAdvanced.go b/examples/loadimageAdvanced/loadimageAdvanced.go new file mode 100644 index 00000000..b6df9cd1 --- /dev/null +++ b/examples/loadimageAdvanced/loadimageAdvanced.go @@ -0,0 +1,87 @@ +// Package main provides examples of loading and presenting images in advanced ways +package main + +import ( + "fmt" + "image" + _ "image/jpeg" + _ "image/png" + "log" + "time" + + "github.com/AllenDang/cimgui-go/imgui" + + g "github.com/AllenDang/giu" +) + +var ( + fromrgba = &g.ReflectiveBoundTexture{} + fromfile = &g.ReflectiveBoundTexture{} + fromurl = &g.ReflectiveBoundTexture{} + rgba *image.RGBA + sonicOffsetX = int32(1180) + sonicOffsetY = int32(580) +) + +func loop() { + var startPos image.Point + + g.SingleWindow().Layout( + g.Custom(func() { + startPos = g.GetCursorScreenPos() + }), + g.Label("Display which has size of contentAvaiable (stretch)"), + fromfile.ToImageWidget().OnClick(func() { + fmt.Println("contentAvailable image was clicked") + }).Size(-1, -1), + + g.Label("Display image from preloaded rgba"), + fromrgba.ToImageWidget().OnClick(func() { + fmt.Println("rgba image was clicked") + }), + + g.Label("Display image from file"), + fromfile.ToImageWidget().OnClick(func() { + fmt.Println("image from file was clicked") + }), + + g.Label("Display image from url + 0.25 scale"), + fromurl.ToImageWidget().OnClick(func() { + fmt.Println("image from url clicked") + }).Scale(0.25, 0.25), + + g.Separator(), + g.Label("Advanced Drawing manipulation"), + g.DragInt("Sonic Offset X", &sonicOffsetX, 0, 1280), + g.DragInt("Sonic Offset Y", &sonicOffsetY, 0, 720), + g.Custom(func() { + size := fromurl.GetSurfaceSize() + sonicOffset := image.Point{int(sonicOffsetX), int(sonicOffsetY)} + posWithOffset := startPos.Add(sonicOffset) + computedPosX := (float32(posWithOffset.X)) + imgui.ScrollX() + computedPosY := (float32(posWithOffset.Y)) + imgui.ScrollY() + scale := imgui.Vec2{X: 0.10, Y: 0.10} + pMin := imgui.Vec2{X: computedPosX, Y: computedPosY} + pMax := imgui.Vec2{X: computedPosX + float32(size.X)*scale.X, Y: computedPosY + float32(size.Y)*scale.Y} + imgui.ForegroundDrawListViewportPtr().AddImage(fromurl.Texture().ID(), pMin, pMax) + }), + g.Separator(), + g.Label("For more advanced image examples (async/statefull/dynamic) check the asyncimage example!"), + ) +} + +func main() { + var err error + + rgba, err = g.LoadImage("./fallback.png") + if err != nil { + log.Fatalf("Cannot loadIamge fallback.png") + } + + _ = fromfile.SetSurfaceFromFile("gopher.png", false) + _ = fromrgba.SetSurfaceFromRGBA(rgba, false) + _ = fromurl.SetSurfaceFromURL("https://static.wikia.nocookie.net/smashbros/images/0/0e/Art_Sonic_TSR.png/revision/latest?cb=20200210122913&path-prefix=fr", time.Second*5, false) + + wnd := g.NewMasterWindow("Load Image", 1280, 720, 0) + wnd.Run(loop) +} diff --git a/examples/paint/canvas.go b/examples/paint/canvas.go new file mode 100644 index 00000000..beb32401 --- /dev/null +++ b/examples/paint/canvas.go @@ -0,0 +1,311 @@ +package main + +import ( + "fmt" + "image" + "image/color" + + "github.com/AllenDang/cimgui-go/imgui" + + g "github.com/AllenDang/giu" +) + +var ( + canvasDetectedHeight float32 + canvasComputedWidth float32 + canvasMarginComputedWidth float32 + canvasInited = false + canvas *Canvas + buffer = []DrawCommand{} + currentColor = color.RGBA{0, 0, 0, 255} + currentTool = 0 + brushSize = float32(12.0) + wasDrawing = false + lastTo image.Point +) + +func flushDrawCommands(c *Canvas) { + var bcopy []DrawCommand + bcopy = append(bcopy, buffer...) + + go c.AppendDrawCommands(&bcopy) + + buffer = nil + buffer = []DrawCommand{} +} + +// Canvas represents a drawable surface where draw commands are executed. +// It holds the image data, the backend texture, and manages the state of drawing operations. +type Canvas struct { + // DrawCommands is a slice of drawCommand that records all drawing actions performed on the canvas. + DrawCommands []DrawCommand + + // Image is the RGBA image that represents the current state of the canvas. + Image *image.RGBA + + // Backend is the texture that interfaces with the graphical backend to display the canvas. + Backend *g.ReflectiveBoundTexture + + // LastPaintedIndex is the index of the last draw command that was painted on the canvas. + LastPaintedIndex int + + // LastComputedLen is the length of the draw commands that have been processed. + LastComputedLen int + + // UndoIndexes is a slice of integers that keeps track of the indexes of draw commands for undo operations. + UndoIndexes []int + + // inited is a boolean flag indicating whether the canvas has been initialized. + inited bool +} + +// GetDrawCommands returns a slice of drawCommand starting from the specified index. +// It allows retrieval of draw commands that have been added since a given point in time. +func (c *Canvas) GetDrawCommands(sinceIndex int) []DrawCommand { + return c.DrawCommands[sinceIndex:] +} + +// PushImageToBackend updates the backend texture with the current state of the canvas image. +// The commit parameter determines whether the changes should be committed immediately. +func (c *Canvas) PushImageToBackend(commit bool) error { + err := c.Backend.SetSurfaceFromRGBA(c.Image, commit) + if err != nil { + return fmt.Errorf("failed to push image to backend: %w", err) + } + + return nil +} + +// AppendDrawCommands adds a slice of drawCommand to the canvas's existing draw commands. +// It appends the provided commands to the DrawCommands slice. +func (c *Canvas) AppendDrawCommands(cmds *[]DrawCommand) { + c.DrawCommands = append(c.DrawCommands, *cmds...) +} + +// Compute processes the draw commands on the canvas and updates the image accordingly. +// It initializes the canvas if it hasn't been initialized yet, and then processes any +// new draw commands that have been added since the last computation. +func (c *Canvas) Compute() { + // Initialize the canvas if it hasn't been initialized + if !c.inited { + // Perform initial flood fill operations to set up the canvas + Floodfill(c.Image, color.RGBA{255, 255, 254, 255}, 1, 1) + Floodfill(c.Image, color.RGBA{255, 255, 255, 255}, 2, 2) + + // Push the initial image state to the backend + err := c.PushImageToBackend(false) + if err != nil { + return + } + + // Mark the canvas as initialized + c.inited = true + + return + } + + // Return if there are no draw commands to process + if len(c.DrawCommands) < 1 { + return + } + + // Return if all draw commands have already been processed + if len(c.DrawCommands) <= c.LastComputedLen { + return + } + + // Get the new draw commands that need to be processed + draws := c.GetDrawCommands(c.LastComputedLen) + for _, r := range draws { + switch r.Tool { + case 0: + // Process line drawing commands + line := r.ToLine() + DrawLine(line.P1.X, line.P1.Y, line.P2.X, line.P2.Y, line.C, line.Thickness, c.Image) + case 1: + // Process fill commands + f := r.ToFill() + Floodfill(c.Image, f.C, f.P1.X, f.P1.Y) + } + } + + // Update the backend with the new image state + _ = c.PushImageToBackend(false) + + // Update the last computed length to the current number of draw commands + c.LastComputedLen = len(c.DrawCommands) +} + +func undoCanvas() { + if len(canvas.UndoIndexes) > 0 { + lastUndoIndex := canvas.UndoIndexes[len(canvas.UndoIndexes)-1] + uind := canvas.UndoIndexes[:len(canvas.UndoIndexes)-1] + dc := canvas.DrawCommands[:lastUndoIndex] + canvas.Backend.ForceRelease() + canvas, _ = NewCanvas(canvasDetectedHeight) + canvas.UndoIndexes = uind + canvas.DrawCommands = dc + canvas.Compute() + } +} + +func clearCanvas() error { + var err error + + canvas.Backend.ForceRelease() + canvas, err = NewCanvas(canvasDetectedHeight) + + return err +} + +// NewCanvas creates a new Canvas with a specified height. +// It initializes the canvas with a default surface and binds it to a ReflectiveBoundTexture backend. +// Returns a pointer to the Canvas and an error if the surface cannot be set. +func NewCanvas(height float32) (*Canvas, error) { + backend := &g.ReflectiveBoundTexture{} + img := defaultSurface(height) + + err := backend.SetSurfaceFromRGBA(img, false) + if err != nil { + return nil, fmt.Errorf("failed to set surface from RGBA: %w", err) + } + + c := &Canvas{Image: img, Backend: backend} + + return c, nil +} + +func fittingCanvasSize16By9(height float32) image.Point { + width := height * (16.0 / 9.0) + return image.Point{X: int(width), Y: int(height)} +} + +func defaultSurface(height float32) *image.RGBA { + p := fittingCanvasSize16By9(height) + surface, _ := g.NewUniformLoader(p.X, p.Y, color.RGBA{255, 255, 255, 255}).ServeRGBA() + + return surface +} + +var defaultColors = []color.RGBA{ + // UPLINE + {0, 0, 0, 255}, + {127, 127, 127, 255}, + {136, 0, 21, 255}, + {237, 28, 36, 255}, + {255, 127, 39, 255}, + {255, 242, 0, 255}, + {34, 177, 76, 255}, + {0, 162, 232, 255}, + {63, 72, 204, 255}, + {163, 73, 164, 255}, + // DOWNLINE + {255, 255, 255, 255}, + {195, 195, 195, 255}, + {185, 122, 87, 255}, + {255, 174, 201, 255}, + {255, 201, 14, 255}, + {239, 228, 176, 255}, + {181, 230, 29, 255}, + {153, 217, 234, 255}, + {112, 146, 190, 255}, + {200, 191, 231, 255}, +} + +func computeCanvasBounds() { + avail := imgui.ContentRegionAvail() + canvasDetectedHeight = avail.Y + canvasSize := fittingCanvasSize16By9(canvasDetectedHeight) + canvasComputedWidth = float32(canvasSize.X) + canvasMarginComputedWidth = (avail.X - canvasComputedWidth) / 2.0 +} + +// CanvasWidget creates a widget for the canvas, handling drawing operations and user interactions. +// It manages mouse events to draw on the canvas and updates the drawing buffer accordingly. +func CanvasWidget() g.Widget { + canvas.Compute() + + return g.Custom(func() { + // Check if the user has stopped drawing + if wasDrawing && !g.IsMouseDown(g.MouseButtonLeft) { + wasDrawing = false + + flushDrawCommands(canvas) + + lastTo = image.Point{0, 0} + } + + // Get the current screen position of the cursor + scr := g.GetCursorScreenPos() + + // Render the canvas image + canvas.Backend.ToImageWidget().Build() + + // Check if the canvas is hovered by the mouse + if g.IsItemHovered() { + mousepos := g.GetMousePos() + if mousepos.X >= scr.X && mousepos.X <= scr.X+int(canvasComputedWidth) && mousepos.Y >= scr.Y && mousepos.Y <= scr.Y+int(canvasDetectedHeight) { + inpos := image.Point{mousepos.X - scr.X, mousepos.Y - scr.Y} + + // Start drawing on mouse click + if imgui.IsMouseClickedBool(imgui.MouseButtonLeft) { + wasDrawing = true + + canvas.UndoIndexes = append(canvas.UndoIndexes, len(canvas.DrawCommands)) + lastTo = image.Point{0, 0} + + buffer = append(buffer, DrawCommand{Tool: currentTool, Color: currentColor, BrushSize: brushSize, From: inpos, To: inpos}) + lastTo = inpos + + flushDrawCommands(canvas) + } + + // Continue drawing while the mouse is held down + if g.IsMouseDown(g.MouseButtonLeft) && wasDrawing { + delta := imgui.CurrentIO().MouseDelta() + dx := int(delta.X) + dy := int(delta.Y) + + if dx == 0 || dy == 0 { + flushDrawCommands(canvas) + } + + buffer = append(buffer, DrawCommand{Tool: currentTool, Color: currentColor, BrushSize: brushSize, From: lastTo, To: inpos}) + lastTo = inpos + + if len(buffer) >= 8 { + flushDrawCommands(canvas) + } + } + } + } + }) +} + +// CanvasRow creates a row layout for the canvas widget, initializing the canvas if necessary. +// It ensures the canvas is properly sized and positioned within the GUI. +func CanvasRow() g.Widget { + return g.Custom(func() { + // Initialize the canvas if it hasn't been initialized yet + if !canvasInited { + computeCanvasBounds() + + var err error + + canvas, err = NewCanvas(canvasDetectedHeight) + if err != nil { + return + } + + canvasInited = true + + return + } + + // Build the row layout with the canvas widget + g.Row( + g.Dummy(canvasMarginComputedWidth, canvasDetectedHeight), + CanvasWidget(), + ).Build() + }) +} diff --git a/examples/paint/commands.go b/examples/paint/commands.go new file mode 100644 index 00000000..5a9275aa --- /dev/null +++ b/examples/paint/commands.go @@ -0,0 +1,39 @@ +package main + +import ( + "image" + "image/color" +) + +// DrawCommand represents a generic drawing command with attributes for tool type, color, brush size, and points. +type DrawCommand struct { + Tool int // Tool indicates the type of drawing tool used (e.g., line, fill). + Color color.Color // Color specifies the color used for the drawing command. + BrushSize float32 // BrushSize defines the size of the brush for the drawing command. + From image.Point // From is the starting point of the drawing command. + To image.Point // To is the ending point of the drawing command. +} + +// LineCommand represents a command to draw a line with specific attributes. +type LineCommand struct { + P1 image.Point // P1 is the starting point of the line. + P2 image.Point // P2 is the ending point of the line. + C color.Color // C specifies the color of the line. + Thickness float32 // Thickness defines the thickness of the line. +} + +// FillCommand represents a command to fill an area with a specific color. +type FillCommand struct { + P1 image.Point // P1 is the starting point for the fill operation. + C color.Color // C specifies the color used for the fill. +} + +// ToLine converts a DrawCommand into a LineCommand. +func (d *DrawCommand) ToLine() LineCommand { + return LineCommand{P1: d.From, P2: d.To, C: d.Color, Thickness: d.BrushSize} +} + +// ToFill converts a DrawCommand into a FillCommand. +func (d *DrawCommand) ToFill() FillCommand { + return FillCommand{P1: d.From, C: d.Color} +} diff --git a/examples/paint/draw.go b/examples/paint/draw.go new file mode 100644 index 00000000..46d154f4 --- /dev/null +++ b/examples/paint/draw.go @@ -0,0 +1,107 @@ +package main + +import ( + "image" + "image/color" + "math" +) + +// DrawLine draws a line from (x1, y1) to (x2, y2) with the specified color and line width on the given *image.RGBA. +func DrawLine(x1, y1, x2, y2 int, c color.Color, linewidth float32, img *image.RGBA) { + // Use Bresenham's line algorithm to get all points along the line + dx := math.Abs(float64(x2 - x1)) + dy := math.Abs(float64(y2 - y1)) + + sx := -1 + if x1 < x2 { + sx = 1 + } + + sy := -1 + if y1 < y2 { + sy = 1 + } + + err := dx - dy + + for { + // Draw a circle at each point to simulate line width + drawCircle(img, x1, y1, linewidth/2, c) + + if x1 == x2 && y1 == y2 { + break + } + + e2 := 2 * err + if e2 > -dy { + err -= dy + x1 += sx + } + + if e2 < dx { + err += dx + y1 += sy + } + } +} + +// drawCircle draws a filled circle with the specified radius and color at (cx, cy) on the *image.RGBA. +func drawCircle(img *image.RGBA, cx, cy int, radius float32, c color.Color) { + r := int(radius) + for x := -r; x <= r; x++ { + for y := -r; y <= r; y++ { + if x*x+y*y <= r*r { + img.Set(cx+x, cy+y, c) + } + } + } +} + +// Floodfill fills an area of an image with a given color starting at point (x, y). +// The fill continues for all adjacent pixels of the same starting color. +func Floodfill(input *image.RGBA, c color.Color, x, y int) { + // Get the color of the starting pixel + startColor := input.At(x, y) + + // If the starting pixel is already the target color, return + if colorsEqual(startColor, c) { + return + } + + // A queue to process pixels + queue := []image.Point{{X: x, Y: y}} + bounds := input.Bounds() + + // Process the queue iteratively + for len(queue) > 0 { + // Dequeue a pixel + point := queue[0] + queue = queue[1:] + + px, py := point.X, point.Y + + // Ignore if out of bounds + if px < bounds.Min.X || px >= bounds.Max.X || py < bounds.Min.Y || py >= bounds.Max.Y { + continue + } + + // Check the color of the current pixel + if !colorsEqual(input.At(px, py), startColor) { + continue + } + + // Set the new color + input.Set(px, py, c) + + // Add the neighboring pixels to the queue + queue = append(queue, image.Point{X: px + 1, Y: py}, image.Point{X: px - 1, Y: py}, image.Point{X: px, Y: py + 1}, image.Point{X: px, Y: py - 1}) + } +} + +// colorsEqual compares two colors and returns true if they are equal. +func colorsEqual(c1, c2 color.Color) bool { + r1, g1, b1, a1 := c1.RGBA() + r2, g2, b2, a2 := c2.RGBA() + + return r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2 +} diff --git a/examples/paint/icons/brush.png b/examples/paint/icons/brush.png new file mode 100644 index 00000000..7c760537 Binary files /dev/null and b/examples/paint/icons/brush.png differ diff --git a/examples/paint/icons/clear.png b/examples/paint/icons/clear.png new file mode 100644 index 00000000..a5d02657 Binary files /dev/null and b/examples/paint/icons/clear.png differ diff --git a/examples/paint/icons/floppy-disk.png b/examples/paint/icons/floppy-disk.png new file mode 100644 index 00000000..0453416d Binary files /dev/null and b/examples/paint/icons/floppy-disk.png differ diff --git a/examples/paint/icons/open-folder.png b/examples/paint/icons/open-folder.png new file mode 100644 index 00000000..b5f0367a Binary files /dev/null and b/examples/paint/icons/open-folder.png differ diff --git a/examples/paint/icons/paint-bucket.png b/examples/paint/icons/paint-bucket.png new file mode 100644 index 00000000..5b1e0fe8 Binary files /dev/null and b/examples/paint/icons/paint-bucket.png differ diff --git a/examples/paint/icons/pencil.png b/examples/paint/icons/pencil.png new file mode 100644 index 00000000..ffa262f8 Binary files /dev/null and b/examples/paint/icons/pencil.png differ diff --git a/examples/paint/icons/undo.png b/examples/paint/icons/undo.png new file mode 100644 index 00000000..4d517446 Binary files /dev/null and b/examples/paint/icons/undo.png differ diff --git a/examples/paint/paint.go b/examples/paint/paint.go new file mode 100644 index 00000000..6410bca9 --- /dev/null +++ b/examples/paint/paint.go @@ -0,0 +1,58 @@ +// Package main demonstrate use of advanced image object via paint clone +package main + +import ( + "image/color" + _ "image/jpeg" + _ "image/png" + + "github.com/AllenDang/cimgui-go/imgui" + + g "github.com/AllenDang/giu" +) + +var ( + showWindow = true + wnd *g.MasterWindow +) + +const ( + windowWidth = 1280 + windowHeight = 720 + toolbarHeight = 100 +) + +func loop() { + if !showWindow { + wnd.SetShouldClose(true) + } + + g.PushColorWindowBg(color.RGBA{30, 30, 30, 255}) + g.Window("GIU Paint").IsOpen(&showWindow).Pos(10, 30).Size(windowWidth, windowHeight).Flags(g.WindowFlagsNoResize).Layout( + showToolbar(), + g.Separator(), + CanvasRow(), + ) + g.PopStyleColor() +} + +func noOSDecoratedWindowsConfig() g.MasterWindowFlags { + imgui.CreateContext() + io := imgui.CurrentIO() + io.SetConfigViewportsNoAutoMerge(true) + io.SetConfigViewportsNoDefaultParent(true) + io.SetConfigWindowsMoveFromTitleBarOnly(true) + + return g.MasterWindowFlagsHidden | g.MasterWindowFlagsTransparent | g.MasterWindowFlagsFrameless +} + +func main() { + // This prepare creating a fully imgui window with no native decoration. + // Flags are to be used with NewMasterWindow. + // Should NOT use SingleLayoutWindow ! + mwFlags := noOSDecoratedWindowsConfig() + + wnd = g.NewMasterWindow("Paint Demo", 1280, 720, mwFlags) + wnd.SetTargetFPS(60) + wnd.Run(loop) +} diff --git a/examples/paint/toolbar.go b/examples/paint/toolbar.go new file mode 100644 index 00000000..01a808ff --- /dev/null +++ b/examples/paint/toolbar.go @@ -0,0 +1,206 @@ +package main + +import ( + "embed" + "fmt" + "image/color" + "io/fs" + + "github.com/AllenDang/cimgui-go/imgui" + + g "github.com/AllenDang/giu" +) + +var ( + pickerRefColor color.RGBA + penButtonImg = &g.ReflectiveBoundTexture{} + fillButtonImg = &g.ReflectiveBoundTexture{} + undoButtonImg = &g.ReflectiveBoundTexture{} + clearButtonImg = &g.ReflectiveBoundTexture{} + openButtonImg = &g.ReflectiveBoundTexture{} + saveButtonImg = &g.ReflectiveBoundTexture{} + brushButtonImg = &g.ReflectiveBoundTexture{} + toolbarInited = false +) + +//go:embed all:icons +var icons embed.FS + +type assetLoadingInfo struct { + file string + backend *g.ReflectiveBoundTexture +} + +func assets() (fs.FS, error) { + f, err := fs.Sub(icons, "icons") + if err != nil { + return nil, fmt.Errorf("error in assets: %w", err) + } + + return f, nil +} + +func loadAsset(path string, backend *g.ReflectiveBoundTexture) error { + assets, _ := assets() + + file, err := assets.Open(path) + if err != nil { + return fmt.Errorf("LoadAsset: error opening image file %s: %w", file, err) + } + + defer func() { + if err := file.Close(); err != nil { + panic(fmt.Sprintf("error closing image file: %s", file)) + } + }() + + err = backend.SetSurfaceFromFsFile(file, false) + if err != nil { + return fmt.Errorf("error in loadAsset: %w", err) + } + + return nil +} + +var loadableAssets = []assetLoadingInfo{ + {file: "pencil.png", backend: penButtonImg}, + {file: "paint-bucket.png", backend: fillButtonImg}, + {file: "undo.png", backend: undoButtonImg}, + {file: "brush.png", backend: brushButtonImg}, + {file: "clear.png", backend: clearButtonImg}, + {file: "open-folder.png", backend: openButtonImg}, + {file: "floppy-disk.png", backend: saveButtonImg}, +} + +func initToolbar() error { + for _, info := range loadableAssets { + if err := loadAsset(info.file, info.backend); err != nil { + return err + } + } + + toolbarInited = true + + return nil +} + +func showToolbar() g.Widget { + if !toolbarInited { + _ = initToolbar() + } + + return g.Child().Size(-1, toolbarHeight).Layout( + buttonColorMaker(), + ) +} + +func colorPopup(ce *color.RGBA, fe g.ColorEditFlags) { + p := g.ToVec4Color(pickerRefColor) + pcol := []float32{p.X, p.Y, p.Z, p.W} + + if imgui.BeginPopup("Custom Color") { + c := g.ToVec4Color(*ce) + col := [4]float32{ + c.X, + c.Y, + c.Z, + c.W, + } + refCol := pcol + + if imgui.ColorPicker4V( + g.Context.FontAtlas.RegisterString("##COLOR_POPUP##me"), + &col, + imgui.ColorEditFlags(fe), + refCol, + ) { + *ce = g.Vec4ToRGBA(imgui.Vec4{ + X: col[0], + Y: col[1], + Z: col[2], + W: col[3], + }) + } + + imgui.EndPopup() + } +} + +func buttonColorMaker() *g.RowWidget { + startUl := imgui.CursorPos() + sz := imgui.Vec2{} + + return g.Row(g.Custom(func() { + for i := range defaultColors { + if i%2 == 0 { + col := g.ToVec4Color(defaultColors[i]) + if imgui.ColorButtonV(fmt.Sprintf("%d##cur_color%d", i, i), col, 0, imgui.Vec2{X: 0, Y: 0}) { + currentColor = defaultColors[i] + } + + sz = imgui.ItemRectSize() + imgui.SameLineV(0, 0) + } + } + + col := g.ToVec4Color(currentColor) + if imgui.ColorButtonV(fmt.Sprintf("##CHOSENcur_color%d", currentColor), col, 0, sz.Mul(2.0)) { + pickerRefColor = currentColor + + imgui.OpenPopupStr("Custom Color") + } + + colorPopup(¤tColor, g.ColorEditFlagsNoAlpha) + imgui.SameLine() + + if imgui.ImageButton("##pen_tool", penButtonImg.Texture().ID(), sz.Mul(1.7)) { + currentTool = 0 + } + + imgui.SameLine() + + if imgui.ImageButton("##fill_tool", fillButtonImg.Texture().ID(), sz.Mul(1.7)) { + currentTool = 1 + } + + imgui.SameLine() + + if imgui.ImageButton("##undo_tool", undoButtonImg.Texture().ID(), sz.Mul(1.7)) { + undoCanvas() + } + + imgui.SameLine() + + if imgui.ImageButton("##clear_tool", clearButtonImg.Texture().ID(), sz.Mul(1.7)) { + _ = clearCanvas() + } + + imgui.SameLine() + imgui.ImageButton("##open_tool", openButtonImg.Texture().ID(), sz.Mul(1.7)) + imgui.SameLine() + imgui.ImageButton("##save_tool", saveButtonImg.Texture().ID(), sz.Mul(1.7)) + + if imgui.ImageButton("##brush_tool", brushButtonImg.Texture().ID(), sz.Mul(0.9)) { + brushSize = 12.0 + } + + imgui.SameLine() + imgui.PushItemWidth(225.0) + imgui.SliderFloat("##Brush Size", &brushSize, float32(0.1), float32(72.0)) + imgui.PopItemWidth() + imgui.SetCursorPos(startUl) + + for i := range defaultColors { + if i%2 != 0 { + col := g.ToVec4Color(defaultColors[i]) + if imgui.ColorButtonV(fmt.Sprintf("%d##cur_color%d", i, i), col, 0, imgui.Vec2{X: 0, Y: 0}) { + currentColor = defaultColors[i] + } + + imgui.SameLineV(0, 0) + } + } + }, + ), + ) +} diff --git a/surfacestate_string.go b/surfacestate_string.go new file mode 100644 index 00000000..50d6c101 --- /dev/null +++ b/surfacestate_string.go @@ -0,0 +1,26 @@ +// Code generated by "stringer -type=SurfaceState"; DO NOT EDIT. + +package giu + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[SurfaceStateNone-0] + _ = x[SurfaceStateLoading-1] + _ = x[SurfaceStateFailure-2] + _ = x[SurfaceStateSuccess-3] +} + +const _SurfaceState_name = "SurfaceStateNoneSurfaceStateLoadingSurfaceStateFailureSurfaceStateSuccess" + +var _SurfaceState_index = [...]uint8{0, 16, 35, 54, 73} + +func (i SurfaceState) String() string { + if i < 0 || i >= SurfaceState(len(_SurfaceState_index)-1) { + return "SurfaceState(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _SurfaceState_name[_SurfaceState_index[i]:_SurfaceState_index[i+1]] +}