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. TheNewWindowOptions
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. TheNextEvent
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
.
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.