Golang Shiny Explained: From Zero to GUI Hero


Written By - admin

Brief Overview of the Golang Shiny Package

The Go programming language, while known for its simplicity and efficiency in building web servers and data-driven applications, hasn't been traditionally associated with Graphical User Interface (GUI) development. However, the experimental shiny package from the Go team promises to change that.

Golang Shiny is a platform-independent interface for developing GUI applications in Go. It aims to provide Go developers with a native method to create interactive applications without leaning on C or platform-specific bindings. Being part of the golang.org/x/exp repository, it's essential to recognize that golang shiny is experimental, which means it's in active development, and features may change over time.

Setup and Initialize your Project

To start a new project using golang shiny, create a new directory with a name of your choice. I will create ~/projects/shiny:

mkdir ~/projects/shiny && cd ~/projects/shiny

Initialize the gomodule:

go mod init shiny

In your main Go file, you can now import the necessary subpackages from golang shiny to begin building your application.

package main

import (
    "golang.org/x/exp/shiny/screen"
    // other imports...
)

func main() {
    // Your application logic here...
}

Download the imports:

go mod tidy

 

Core Concepts

1 shiny/driver

  • driver.Main: This is the main entry point for any golang shiny application. It takes in a function with a screen.Screen parameter. This function will be run on the main thread of the application.

2 shiny/screen

  • Screen: This represents the display screen or graphics context in which you're working. All drawing and window operations work through this interface.
  • NewWindow: This method of the Screen interface creates a new window. It takes a NewWindowOptions struct that lets you specify details like window dimensions and title.
  • Window: Once created, a Window offers methods to handle events, draw to the screen, and manage window properties. The NextEvent method is used to get the next event from the window's event queue.
  • Buffer: This represents an image buffer where you can draw and manipulate image data before displaying it on the screen. Once you've drawn on the buffer, you upload it to the window to display it.

3 Event Handling

The golang shiny package uses an event-driven model. When you use w.NextEvent(), you're getting the next event from the window's event queue. These can be:

  • lifecycle.Event: This handles the window's lifecycle, such as if it's being opened, closed, or resized.
  • paint.Event: This event is sent when the window needs to be repainted. In response, you generally draw your content and then tell the window to publish the updated content.
  • key.Event: This is triggered when a keyboard key is pressed or released. You can check which key is pressed and handle it accordingly.
  • mouse.Event: This is triggered when a mouse action occurs, like a click or a move. You can determine the type of mouse action, which button was pressed, and the location of the event.

4 Drawing and Rendering

  • Buffer.RGBA().Set: This is how you set pixel colors in a buffer.
  • w.Upload: This takes a buffer and draws it to the window. This doesn't immediately display the buffer; instead, the buffer's content is ready to be shown on the next paint.Event.

5 Error Handling

The handleError function is a basic way to manage errors in this example, printing them to the console. In a more robust application, error handling would likely be more nuanced.

 

Creating Your First GO Shiny Application

The golang shiny package offers a straightforward way to create graphical applications. This tutorial will guide you through the process of setting up and running a simple application that creates a window, draws a blue rectangle, and responds to basic keyboard and mouse events.

1. Setting Up

Before you begin, ensure you've imported the necessary packages:

import (
	"fmt"
	"image"
	"image/color"
	"golang.org/x/exp/shiny/driver"
	"golang.org/x/exp/shiny/screen"
	"golang.org/x/mobile/event/key"
	"golang.org/x/mobile/event/lifecycle"
	"golang.org/x/mobile/event/mouse"
	"golang.org/x/mobile/event/paint"
)

2. Initiating the Golang Shiny Driver

All golang shiny applications begin with the driver.Main function. It's the primary entry point:

driver.Main(func(s screen.Screen) {
    // Your application code here
})

Inside this function, you'll define your application's behavior. The screen.Screen parameter represents the graphics context you'll be working within.

3. Creating a Window

Use the NewWindow method to create a window:

w, err := s.NewWindow(&screen.NewWindowOptions{
    Width:  400,
    Height: 300,
    Title:  "Shiny Tutorial",
})

This method requires a NewWindowOptions struct, which lets you define properties like dimensions and title.

4. Drawing on the Window

Before drawing on the window, we need an intermediary—a buffer:

b, err := s.NewBuffer(image.Point{400, 300})

The buffer acts like a canvas. Here, we're painting our buffer blue:

for x := 0; x < 400; x++ {
    for y := 0; y < 300; y++ {
        b.RGBA().Set(x, y, color.RGBA{0, 0, 255, 255})
    }
}

Then, upload the buffer to the window:

w.Upload(image.Point{0, 0}, b, b.Bounds())

5. Handling Events

Golang Shiny applications are event-driven. The w.NextEvent() method retrieves the next event from the queue:

e := w.NextEvent()

Use a switch statement to handle different event types:

Lifecycle Events: Monitor the state of the window.

case lifecycle.Event:
    if e.To == lifecycle.StageDead {
        return
    }

Keyboard Events: Respond to key presses.

case key.Event:
    if e.Code == key.CodeEscape {
        return
    }

Mouse Events: Track mouse interactions.

case mouse.Event:
    if e.Button == mouse.ButtonLeft && e.Direction == mouse.DirPress {
        fmt.Println("Mouse left button clicked at:", e.X, e.Y)
    }

Paint Events: Refresh the window's content.

case paint.Event:
    w.Publish()

Here is a sample application using golang shiny package:

package main

import (
	"fmt"
	"image"
	"image/color"
	"golang.org/x/exp/shiny/driver"
	"golang.org/x/exp/shiny/screen"
	"golang.org/x/mobile/event/key"
	"golang.org/x/mobile/event/lifecycle"
	"golang.org/x/mobile/event/mouse"
	"golang.org/x/mobile/event/paint"
)

func main() {
	driver.Main(func(s screen.Screen) {
		w, err := s.NewWindow(&screen.NewWindowOptions{
			Width:  400,
			Height: 300,
			Title:  "Shiny Tutorial",
		})
		if err != nil {
			handleError(err)
			return
		}
		defer w.Release()

		// Drawing a blue rectangle as initial content
		b, err := s.NewBuffer(image.Point{400, 300})
		if err != nil {
			handleError(err)
			return
		}
		defer b.Release()

		for x := 0; x < 400; x++ {
			for y := 0; y < 300; y++ {
				b.RGBA().Set(x, y, color.RGBA{0, 0, 255, 255})
			}
		}

		w.Upload(image.Point{0, 0}, b, b.Bounds())

		for {
			e := w.NextEvent()

			switch e := e.(type) {
			case lifecycle.Event:
				if e.To == lifecycle.StageDead {
					return
				}
			case paint.Event:
                w.Upload(image.Point{0, 0}, b, b.Bounds())
				w.Publish()
			case key.Event:
				if e.Code == key.CodeEscape {
					return
				}
				if e.Code == key.CodeSpacebar && e.Direction == key.DirPress {
					fmt.Println("Space key pressed!")
				}
			case mouse.Event:
				if e.Button == mouse.ButtonLeft && e.Direction == mouse.DirPress {
					fmt.Println("Mouse left button clicked at:", e.X, e.Y)
				}
			// Additional event cases can be added here.
			}
		}
	})
}

func handleError(err error) {
	fmt.Println("Error:", err)
}

 

Building GUI Applications with Shiny

In this section, we'll dive deeper into building GUI applications using the golang shiny package. We'll cover the creation of windows, setting their properties, handling user inputs, and working with graphics.

 

1. Creating Basic Windows and Setting Properties

package main

import (
	"golang.org/x/exp/shiny/driver"
	"golang.org/x/exp/shiny/screen"
)

func main() {
	driver.Main(func(s screen.Screen) {
		_, err := s.NewWindow(&screen.NewWindowOptions{
			Width:  500,
			Height: 400,
			Title:  "Basic Shiny Window",
		})
		if err != nil {
			panic(err)
		}
	})
}

 

2. Handling User Inputs (Mouse, Keyboard)

In this example, the program waits for user inputs. When the space key is pressed, a message is printed to the console, and a similar action happens when the left mouse button is clicked.

package main

import (
	"fmt"
	"golang.org/x/exp/shiny/driver"
	"golang.org/x/exp/shiny/screen"
	"golang.org/x/mobile/event/key"
	"golang.org/x/mobile/event/mouse"
)

func main() {
	driver.Main(func(s screen.Screen) {
		w, err := s.NewWindow(&screen.NewWindowOptions{
			Title: "Input Handling with Shiny",
		})
		if err != nil {
			panic(err)
		}

		for {
			e := w.NextEvent()

			switch ev := e.(type) {
			case key.Event:
				if ev.Code == key.CodeEscape && ev.Direction == key.DirPress {
					return
				}
				if ev.Code == key.CodeSpacebar && ev.Direction == key.DirPress {
					fmt.Println("Space key pressed!")
				}
			case mouse.Event:
				if ev.Button == mouse.ButtonLeft && ev.Direction == mouse.DirPress {
					fmt.Println("Left mouse button clicked!")
				}
			}
		}
	})
}

 

3. Working with Graphics: Drawing, Images, and Colors

package main

import (
	"image"
	"image/color"
	"golang.org/x/exp/shiny/driver"
	"golang.org/x/exp/shiny/screen"
	"golang.org/x/mobile/event/lifecycle"
	"golang.org/x/mobile/event/paint"
)

func main() {
	driver.Main(func(s screen.Screen) {
		w, err := s.NewWindow(&screen.NewWindowOptions{
			Width:  500,
			Height: 500,
			Title:  "Gradient with Shiny",
		})
		if err != nil {
			panic(err)
		}
		defer w.Release()

		buf, err := s.NewBuffer(image.Point{500, 500})
		if err != nil {
			panic(err)
		}
		defer buf.Release()

		for x := 0; x < 500; x++ {
			for y := 0; y < 500; y++ {
				r := uint8(255 * x / 500)
				g := uint8(255 * y / 500)
				b := uint8(255 - (255 * x / 500))
				buf.RGBA().Set(x, y, color.RGBA{r, g, b, 255})
			}
		}

		for {
			e := w.NextEvent()
			switch e := e.(type) {
			case paint.Event:
				w.Upload(image.Point{0, 0}, buf, buf.Bounds())
				w.Publish()
			case lifecycle.Event:
				if e.To == lifecycle.StageDead {
					return
				}
			}
		}
	})
}

In this example, a gradient is created by varying the red and green color components across the window. The result is a visually appealing mix of colors.

 

Advanced Features

1. Implementing Custom Widgets

Golang Shiny doesn't have built-in UI widgets like buttons or sliders. However, you can implement custom widgets by defining their behavior and appearance.

package main

import (
	"fmt"
	"image"
	"image/color"
	"image/draw"

	"golang.org/x/exp/shiny/driver"
	"golang.org/x/exp/shiny/screen"
	"golang.org/x/mobile/event/key"
	"golang.org/x/mobile/event/lifecycle"
	"golang.org/x/mobile/event/mouse"
	"golang.org/x/mobile/event/paint"
)

type Button struct {
	Position image.Point
	Size     image.Point
	Clicked  bool
}

func (b *Button) Rect() image.Rectangle {
	return image.Rect(b.Position.X, b.Position.Y, b.Position.X+b.Size.X, b.Position.Y+b.Size.Y)
}

func main() {
	driver.Main(func(s screen.Screen) {
		w, err := s.NewWindow(&screen.NewWindowOptions{
			Width:  400,
			Height: 300,
			Title:  "Shiny Tutorial",
		})
		if err != nil {
			fmt.Println("Error:", err)
			return
		}
		defer w.Release()

		button := &Button{
			Position: image.Pt(150, 100),
			Size:     image.Pt(100, 50),
		}

		for {
			e := w.NextEvent()

			switch e := e.(type) {
			case lifecycle.Event:
				if e.To == lifecycle.StageDead {
					return
				}
			case paint.Event:
				b, err := s.NewBuffer(image.Point{400, 300})
				if err != nil {
					fmt.Println("Error:", err)
					return
				}
				defer b.Release()

				col := color.RGBA{255, 0, 0, 255} // Red by default
				if button.Clicked {
					col = color.RGBA{0, 255, 0, 255} // Green when clicked
				}

				draw.Draw(b.RGBA(), b.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src) // clear to white background
				draw.Draw(b.RGBA(), button.Rect(), &image.Uniform{col}, image.Point{}, draw.Src)      // draw the button

				w.Upload(image.Point{0, 0}, b, b.Bounds())
				w.Publish()
			case mouse.Event:
				if e.Button == mouse.ButtonLeft && e.Direction == mouse.DirPress {
					pt := image.Pt(int(e.X), int(e.Y))
					if pt.In(button.Rect()) {

						button.Clicked = !button.Clicked
						fmt.Println("Button clicked!")
					}
				}
			case key.Event:
				if e.Code == key.CodeEscape {
					return
				}
			}
		}
	})
}

This code demonstrates how to create and interact with custom widgets using the Golang Shiny library. Our primary widget of interest here is a simple button. This button is represented by a Button struct, which contains its position, size, and a state indicating whether it's been clicked. The Rect() method helps in obtaining the button's rectangle boundaries.

During the main event loop, we listen to various events, most notably paint.Event and mouse.Event. When a paint.Event occurs, we render the button in red by default; however, if the button is clicked, its color changes to green. To detect a button click, the mouse.Event is used. If a left mouse button press is detected within the button's boundaries, the Clicked state toggles, and a message is printed. The interaction with the custom button and its visual feedback showcases a basic yet effective way of creating and managing custom widgets in a Golang Shiny application.

 

2. Handling Animations and Transformations

Animating the color of a rectangle:

package main

import (
	"image"
	"image/color"
	"golang.org/x/exp/shiny/driver"
	"golang.org/x/exp/shiny/screen"
	"golang.org/x/mobile/event/lifecycle"
	"golang.org/x/mobile/event/paint"
	"time"
)

func main() {
	driver.Main(func(s screen.Screen) {
		w, err := s.NewWindow(&screen.NewWindowOptions{
			Width:  400,
			Height: 300,
			Title:  "Shiny Animation Example",
		})
		if err != nil {
			panic(err)
		}
		defer w.Release()

		buffer, err := s.NewBuffer(image.Point{400, 300})
		if err != nil {
			panic(err)
		}
		defer buffer.Release()

		ticker := time.NewTicker(time.Second / 30) // 30 FPS
		defer ticker.Stop()

		col := color.RGBA{R: 255, A: 255}

		for {
			select {
			case <-ticker.C:
				// Animate color from red to black and back
				if col.R == 255 {
					col.R = 0
				} else {
					col.R = 255
				}
				for x := 100; x < 300; x++ {
					for y := 100; y < 200; y++ {
						buffer.RGBA().Set(x, y, col)
					}
				}
				w.Upload(image.Point{}, buffer, buffer.Bounds())
				w.Publish()

			default:
				e := w.NextEvent()
				switch e := e.(type) {
				case lifecycle.Event:
					if e.To == lifecycle.StageDead {
						return
					}
				case paint.Event:
					w.Publish()
				}
			}
		}
	})
}

The provided code demonstrates the creation of a simple animation using the Golang Shiny library. At the core of the animation is a rectangular area whose color toggles between red and black at a regular interval, creating a blinking effect.

Upon initialization, a window of size 400x300 pixels is created with the title "Shiny Animation Example". A buffer of the same dimensions is set up to store the frame content before it's uploaded to the window. The heartbeat of the animation is defined by a ticker set at 30 FPS (frames per second), implying that the screen content will refresh approximately every 33.3 milliseconds.

The primary animation logic lies within the select statement which listens for ticker ticks. On every tick, the color of the rectangle is switched, and the respective color is drawn onto the buffer, which is then uploaded and published to the window. This creates an animated blinking effect of the rectangular region between red and black.

In addition to the animation loop, the code listens for other events such as lifecycle.Event to handle the window's lifecycle (like when the window should close) and paint.Event for potential repaint requests.

 

3. Understanding Buffers and Textures

Buffers and textures are central to the golang shiny package.

  • Buffers are off-screen images. You draw onto a buffer and then upload the buffer to a texture.
  • Textures are what you draw onto the window. They are GPU-resident and are optimized for drawing to the screen.

Example: Drawing an image onto a window using buffer and texture:

img, _, err := image.Decode(os.Open("path_to_image.jpg"))
if err != nil {
    log.Fatal(err)
}

buf, err := screen.NewBuffer(img.Bounds().Size())
if err != nil {
    log.Fatal(err)
}
defer buf.Release()

buf.Upload(image.Point{}, img, img.Bounds())

tex, err := screen.NewTexture(img.Bounds().Size())
if err != nil {
    log.Fatal(err)
}
defer tex.Release()

tex.Upload(image.Point{}, buf, buf.Bounds())

// Inside the paint.Event case in the event loop:
w.Copy(image.Point{}, tex, tex.Bounds(), screen.Over, nil)

 

Build a simple Drawing Application

In this example, we'll create a simple canvas where users can draw with the mouse.

package main

import (
	"image"
	"image/color"
	"image/draw"
	"golang.org/x/exp/shiny/driver"
	"golang.org/x/exp/shiny/screen"
	"golang.org/x/mobile/event/mouse"
	"golang.org/x/mobile/event/paint"
)

var isDrawing bool

func main() {
	driver.Main(func(s screen.Screen) {
		w, err := s.NewWindow(&screen.NewWindowOptions{
			Width:  800,
			Height: 600,
			Title:  "Simple Drawing App",
		})
		if err != nil {
			panic(err)
		}
		defer w.Release()

		buffer, err := s.NewBuffer(image.Point{800, 600})
		if err != nil {
			panic(err)
		}
		defer buffer.Release()

		// Set the initial background to white
		draw.Draw(buffer.RGBA(), buffer.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)

		var drawColor color.Color = color.Black

		for {
			e := w.NextEvent()
			switch e := e.(type) {
			case paint.Event:
				w.Upload(image.Point{}, buffer, buffer.Bounds())
				w.Publish()

			case mouse.Event:
				switch e.Direction {
				case mouse.DirPress:
					isDrawing = true
				case mouse.DirRelease:
					isDrawing = false
				}

				if isDrawing {
					x, y := int(e.X), int(e.Y)
					drawPoint(buffer, x, y, drawColor)
					w.Send(paint.Event{}) // Request a window repaint after drawing
				}
			}
		}
	})
}

func drawPoint(b screen.Buffer, x, y int, col color.Color) {
	for dx := -1; dx <= 1; dx++ {
		for dy := -1; dy <= 1; dy++ {
			b.RGBA().Set(x+dx, y+dy, col)
		}
	}
}

This simple drawing app showcases the basic capabilities of Golang Shiny to detect mouse events and render on a buffer. The user interacts with the canvas using the mouse to draw black lines. The program utilizes mouse event handling to detect the position of the mouse cursor and paint the corresponding pixel black. On each paint event, the buffer's contents are uploaded to the window, allowing the user's drawings to be visualized in real time.

 

Conclusion

The Golang Shiny package stands as a testament to Go's flexibility, venturing into the domain of GUI applications. Diving into its core concepts, it becomes evident that Shiny is not just another GUI library, but a uniquely Go-centric approach to interface design. Through the creation of simple Shiny applications, users are introduced to the power and versatility the package offers. From handling animations and transformations to understanding the nuances of buffers and textures, the learning curve may seem steep, but the outcome is profoundly rewarding. Our journey into building a simple drawing app showcased just a fraction of what's possible with Golang Shiny. It's an exciting step for developers looking to merge the robustness of Go with interactive GUI components.

 

Further Reading

Official Golang Shiny Documentation

 

Categories GO

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!