Golang Shiny Explained: From Zero to GUI Hero


GO

Brief Overview of the Golang Shiny Package

The Go programming language is well known for its elements of simplicity and efficiency in developing web servers and data-driven applications. However, it’s not really associated with the GUI (Graphical User Interface) development. Nevertheless, the experimental shiny package from the Go team aims at changing this.

Golang Shiny is an interface that can be used to create GUI applications using Go on any platform. Its goal is to give go programmers a way of building interactive applications without resorting to C or platform-specific bindings. One should bear in mind that golang shiny belongs to the golang.org/x/exp repository and consequently this means that it’s experimental hence still under active development; features might 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 a function with a screen.Screen parameter. It will be run on the main thread of the application.

2 shiny/screen

  • Screen: This is what represents your display or graphics context. All drawings and window operations are channeled through this interface.
  • NewWindow: The Screen interface has this method that creates a new window. The NewWindowOptions struct lets user specify details like window dimensions and title.
  • Window: After creating, a Window has methods for handling events, drawing to the screen, and managing window properties. The NextEvent method is used to get the next event from the window's event queue.
  • Buffer: An image buffer where you can draw and manipulate image data before displaying it on the screen. Once drawn on, upload it to the window to display it.

3 Event Handling

The golang shiny package uses an event driven model, where w.NextEvent() fetches you another event from windows’ queue of events which could be any of these:

  • lifecycle.Event: To handle such lifecycle effects of windows as opening, closing or resizing
  • paint.Event: This occurs when there is need for repainting of a window. For example, usually you would draw content then request that your changes be published by window..rendering
  • 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: Such an event takes place depending on what action mouse does i.e click or move but important things here are what kind of mouse action happened?, which button got pressed and where did it happen?

4 Drawing and Rendering

  • Buffer.RGBA().Set: This is how you set pixel colors in a buffer.
  • w.Upload: In this method a buffer is uploaded into the Window. The buffer doesn’t appear immediately; instead, it is shown the next time the window gets a 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 is a simple way to generate graphical programs. In this tutorial you will learn how to start and run an elementary application that creates a window, draws a blue rectangle and responds to some mouse and keyboard 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

This section will give you further insight in building GUI applications using golang shiny package. we shall look at window creation, setting properties of windows, handling input from the user and 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)

Here is an example where a program waits for user inputs. When space key is pressed, it prints out a message on the console and another action happens when 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 well-looking mixture of colors achieved by varying red and green color components across the window is created.

 

Advanced Features

1. Implementing Custom Widgets

Golang Shiny doesn’t have pre-defined UI widgets like buttons or sliders; they can be implemented as custom controls that define how they act and look like.

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
				}
			}
		}
	})
}

The code shows how to create and interact with your own widgets using Golang Shiny library. Our major widget of interest in this case is simply a button. The button is represented by Button struct which includes its position, size, and an indicator if it has been click or not. The Rect() method helps in obtaining the boundaries of the rectangle within which our button lies in respect to x-axis and y-axis respectively.

During the main event loop, we listen for several events including paint.Event and mouse.Event among others. In case there occurs paint.Event then default rendering of the button is done in red unless it was clicked which would have turned its color into greenish hue while mouse.Event help us trace any movement from which we know a click occurred within it boundaries of our defined custom widget by left-clicking on one side. When any button under mouse click undergoes change from pressed to unpressed states (or vice versa), flag toggles and new message appears: “clicked”. Simple visual feedback like changing one’s border indicates how effective are these few lines for basic management of self-created widgets using Golang Shiny Live programming platform.

 

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()
				}
			}
		}
	})
}

In this example codes illustrate how to implement animation using Golang shiny library. It also creates blinking effect by turning off and on a rectangular region after every regular interval with two colors, black and red.

At start-up, a window of size 400x300 pixels is created and labeled “Shiny Animation Example”. A buffer of the same dimensions is set up to store the frame content before it’s uploaded to the window. To make sure that screen contents are refreshed approximately every 33.3 milliseconds, there is an animation ticker which sets at 30 FPS (frames per second).

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.

Besides listening to repaint requests through paint.Event event type, other events like lifecycle.Event can be listened to through this code when such an event will close or could be noticed.

 

3. Understanding Buffers and Textures

Buffers and textures are very important in golang shiny package.

  • A buffer is just an off-screen image that allows you to draw on it before uploading it onto a texture.
  • Textures are basically what you draw upon your window; they are kept on GPU so as to optimize rendering on screen.

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

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 sample app demonstrates Golang Shiny’s abilities in recognizing mouse actions and painting on a buffer. The black lines are drawn by the user on the canvas using a mouse. The program detects the cursor position by handling events from the mouse and then painting its corresponding pixel in black. On every paint event, the contents of the buffer are uploaded to the window so that user can see his/her drawings being rendered in real time.

 

Conclusion

Golang Shiny package is a perfect practical proof for Go’s adaptability beyond web development into GUI applications. As one delves deeper into its core ideas, it becomes clear that Shiny is just not another GUI library, but rather an interface design approach that is unique to Go. Users familiarize themselves with this power and flexibility of shiny through simple shiny applications built for this purpose. The learning curve may appear steep when it comes to working with animations, transformations or even understanding buffers and textures, but it immensely enriching at the end of the day. We have covered only a part of what could be done with Golang Shiny as seen from our simple drawing app development example. This makes for an exciting step forward for developers who want to merge Go’s strength with interactive GUI components.

 

Deepak Prasad

Deepak Prasad

He is the founder of GoLinuxCloud and brings over a decade of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels in various domains, from development to DevOps, Networking, and Security, ensuring robust and efficient solutions for diverse projects. You can connect with him on his LinkedIn profile.

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!!

Leave a Comment