Table of Contents
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 aNewWindowOptions
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. TheNextEvent
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
.
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