title |
---|
Architecture |
Gio implements an Immediate Mode User Interface.. This approach can be implemented in multiple ways, however the overarching similarity is that the program:
- listens for events such as mouse or keyboard input,
- updates its internal state based on the event (e.g. sets Checked = true for a checkbox),
- runs code that redraws and layouts the whole state.
A minimal immediate mode command-line UI could look like this:
// state of the program
var showlist bool
var items []string
for {
// Wait for new events
select {
case ev := <-eventQueue:
clearScreen()
// handle the checkbox
if DoCheckbox(ev, &showlist) {
Listbox{
Items: items
}.Do(ev)
}
}
}
func DoCheckbox(ev Event, checked *bool) bool {
// see whether we need to handle the event
if e, ok := ev.(KeyboardInput); ok {
if e.Key == Space {
*checked = !*checked
}
}
// draw the checkbox
if *checked {
fmt.Println("[x]")
} else {
fmt.Println("[ ]")
}
// return whether we are checked for convenience
return *checked
}
type Listbox struct {
Items []string
}
func (list *Listbox) Do(ev Event) {
for i, item := range list.Items {
fmt.Printf("#%d: %q\n",i, item)
}
}
This of course is not a very useful library, however it demonstrates the core loop of a immediate mode UI:
- get an event
- handle the widgets while updating the state and drawing the widgets
The main differentiation from non-immediate user interfaces is that the widgets and layout are directly drawn by the code, not by a separate configuration or setup “before drawing”.
This becomes less simple when other aspects of the GUI are taken into account:
- How do you get the events?
- When do you redraw the state?
- What do the widget structures look like?
- How do you track the focus?
- How do you structure the events?
- How do you communicate with the graphics card?
- How do you handle input?
- How do you draw text?
- Where does the widget state belong?
- ... and many more.
The rest of this document tries to answer how Gio does it. If you wish to know more about immediate mode UI, these references are a good start:
- https://caseymuratori.com/blog_0001
- http://sol.gfxile.net/imgui/
- http://www.johno.se/book/imgui.html
- https://github.com/ocornut/imgui
- https://eliasnaur.com/blog/immediate-mode-gui-programming
Since a GUI library needs to talk to some sort of display system to display information:
window := app.NewWindow(app.Size(unit.Dp(800), unit.Dp(650)))
for {
select {
case e := <-window.Events():
switch e := e.(type) {
case system.DestroyEvent:
// The window was closed.
return e.Err
case system.FrameEvent:
// A request to draw the window state.
ops := new(op.Ops)
// Draw the state into ops based on events in e.Queue.
e.Frame(ops)
}
}
}
app.NewWindow
chooses the appropriate "handling driver" depending on the environment and build context. It might choose Wayland, WinAPI,Cocoa or many others.
It then sends events from the display system to the windows.Events()
channel.
There is a need to communicate information about window events, the GPU, input and about the general structure of the screen. Gio uses op.Ops.
In abstract terms an Ops
value contains a sequence of operations that tell the window driver what to display and how to handle user input.
By convention, graphical primitives are represented by data types that have an Add
method which adds the operations necessary to draw itself to its argument Ops
value. Like any Go struct literal, zero-valued fields can be useful to represent optional values.
ops := new(op.Ops)
red := color.RGBA{R:0xFF, A:0xFF}
paint.ColorOp{Color: red}.Add(ops)
You might be thinking that it would be more usual to have an ops.Add(ColorOp{Color: red})
method instead of using op.ColorOp{Color: red}.Add(ops)
. It's like this so that the Add
method doesn't have to take an interface-typed argument, which would often require an allocation to call. This is a key aspect of Gio's "zero allocation" design.
To tell the graphics API what to draw, Gio uses op.Ops to serialize drawing commands.
Coordinates are based on the top-left corner by default, although it’s possible to transform the coordinate system. This means f32.Point{X:0, Y:0}
is the top left corner of the window. All drawing operations use pixel units, see Units section for more information.
Gio encodes operations as Go structs which know how to encode data into op.Ops
. For example, the following code will draw a 10x10 pixel colored rectangle at the top level corner of the window:
func drawRedRect(ops *op.Ops) {
paint.ColorOp{Color: color.RGBA{R: 0x80, G: 0x00, B: 0x00, A: 0xFF}}.Add(ops)
paint.PaintOp{Rect: f32.Rect(0, 0, 100, 100)}.Add(ops)
}
Operation op.TransformOp allows us to translate the position of the operations that come after it.
For example, the following would draw 100 units to the right compared to the previous example:
func drawRedRect10PixelsRight(ops *op.Ops) {
op.TransformOp{}.Offset(f32.Point{X: 100, Y: 0}).Add(ops)
drawRedRect(ops)
}
Note: in the future, TransformOp will allow other transformations such as scaling and rotation too.
In some cases we want the drawing to be clipped to some smaller rectangle to avoid accidentally drawing over other things.
Package gioui.org/op/clip, provides exactly that.
clip.Rect clips all subsequent drawing to a particular rounded rectangle.
Note: that we first need to get the actual operation for the clipping with Op
before calling Add
. This level of indirection is useful if we want to use the same clipping operation multiple times - under the hood, Op records a macro that encodes the clipping path.
This could be used as a basis for a button background:
func redButtonBackground(ops *op.Ops) {
r := 3 // roundness
bounds := f32.Rect(0, 0, 30, 100)
clip.Rect{Rect: bounds, SE: r, SW: r, NW: r, NE: r}.Op(ops).Add(ops)
drawRedRectangle(ops)
}
Some of the gio operations affect all operations that follow them. For example, ColorOp
sets the “brush” color that is used in subsequent PaintOp
operations. This drawing context also includes coordinate transformation (set by TransformOp
) and clipping (set by ClipOp
).
We often need to set up some drawing context and then restore it to its previous state, leaving later operations unaffected. We can use op.StackOp to do this. A Push operation saves the current drawing context; a Pop operation restores it.
For example, the clipButtonOutline
function in the previous section has the unfortunate side-effect of clipping all later operations to the outline of the button background!
Let’s make a version of it that doesn’t affect any callers:
func redButtonBackground(ops *op.Ops) {
var stack op.StackOp
stack.Push(ops)
defer stack.Pop()
r := 3 // roundness
bounds := f32.Rect(0, 0, 30, 100)
clip.Rect{Rect: bounds, SE: r, SW: r, NW: r, NE: r}.Op(ops).Add(ops)
drawRedRectangle(ops)
}
Drawing happens from back to front. In this example the green rectangle is drawn on top of red rectangle:
func drawOverlappingRectangles(ops *op.Ops) {
// red rectangle
paint.ColorOp{Color: color.RGBA{R: 0xFF, G: 0x00, B: 0x00, A: 0xFF}}.Add(ops)
paint.PaintOp{Rect: f32.Rect(0, 0, 100, 10)}.Add(ops)
// green rectangle
paint.ColorOp{Color: color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}}.Add(ops)
paint.PaintOp{Rect: f32.Rect(0, 0, 10, 100)}.Add(ops)
}
Sometimes you may want to change this order. For example, you may want to delay drawing to apply a transform that is calculated during drawing, or you may want to perform a list of operations several times. For this purpose there is op.MacroOp.
func drawFiveRectangles(ops *op.Ops) {
// Record all the operations performed by drawRedRect
// into the macro.
var macro op.MacroOp
macro.Record(ops)
drawRedRect(ops)
macro.Stop()
// “play back” the macro 5 times, each time vertically offset
// 40 pixels more down the screen.
for i := 0; i < 5; i++ {
macro.Add(ops)
op.TransformOp{}.Offset(f32.Point{X: 0, Y: 40}).Add(ops)
}
}
When you are animating something you may need to trigger a redraw immediately rather than wait for events. For that there is op.InvalidateOp
The following code will animate a green “progress bar” that fills up from left to right over 5 seconds from when the program starts:
var startTime = time.Now()
var duration = 5*time.Second
func drawProgressBar(ops *op.Ops, now time.Time) {
// Calculate how much of the progress bar to draw, based
// on the current time.
elapsed := now.Sub(startTime)
progress := elapsed.Seconds() / duration.Seconds())
if progress < 1 {
// The progress bar hasn’t yet finished animating.
op.InvalidateOp{}.Add(ops)
} else {
progress = 1
}
paint.ColorOp{Color: color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}}.Add(ops)
width := 100*float32(progress)
paint.PaintOp{Rect: f32.Rect(0, 0, width, 10)}.Add(ops)
}
While MacroOp allows you to record and replay operations on a single operation list, CallOp allows for reuse of a separate operation list. This is useful for caching operations that are expensive to re-create, or for animating the disappearance of otherwise removed widgets:
func drawWithCache(ops *op.Ops) {
// Save the operations in an independent ops value (the cache).
cache := new(op.Ops)
paint.ColorOp{Color: color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}}.Add(cache)
paint.PaintOp{Rect: f32.Rect(0, 0, 100, 100)}.Add(cache)
// Draw the operations from the cache.
op.CallOp{Ops: cache}.Add(ops)
}
paint.ImageOp can be used to draw images. Like ColorOp, it sets part of the drawing context (the “brush”) that’s used for any subsequent PaintOp. It is used similarly to ColorOp.
Note that RGBA and image.Uniform images are efficient and treated specially. Other Image implementations will undergo a potentially expensive conversion to the underlying image model.
func drawImage(ops *op.Ops, img image.Image) {
imageOp := paint.NewImageOp(img)
imageOp.Add(ops)
paint.PaintOp{Rect: f32.Rect(0, 0, 100, 100)}.Add(ops)
}
Note, the image must not be mutated until another FrameEvent happens, because the image is read asynchronously while the frame is being drawn.
TODO: describe how text shaper works
Input is delivered to the widgets via a system.FrameEvent
which contains a Queue
.
Some of the most common events are:
key.Event
, key.Focus
- for keyboard input.
key.EditEvent
- for text editing.
pointer.Event
- for mouse and touch input.
The program can respond to these events however it likes - for example, by updating its local data structures or running a user-triggered action. The Frame event is special - when the program receives a Frame event, it is responsible for updating the display by calling the e.Frame function with an operations list representing the new state. This operations list is generated immediately in response to the Frame event which is the main reason that Gio is known as an “immediate mode” GUI.
There are also event-processors, such as gioui.org/gesture
, that detect higher-level actions such as a double-click from individual click events.
To distribute input among multiple different widgets, Gio needs to know where to send the input. However, since the Gio framework is stateless, there's no obvious place where it can do that.
Instead, some operations associate input event types (for example, keyboard presses) with arbitrary tags (interface{} values) chosen by the program. A program creates these operations when it’s drawing the graphics - they are treated similarly to other graphics operations. When a frame is being generated, the input can be retrieved by using the tags from the previous frame.
The following example demonstrates registering and handling pointer input:
var tag = new(bool) // We could use &pressed for this instead.
var pressed = false
func doButton(ops *op.Ops, q event.Queue) {
// Make sure we don’t pollute the graphics context.
var stack op.StackOp
stack.Push(ops)
defer stack.Pop()
// Process events that arrived between the last frame and this one.
for _, ev := range q.Events(tag) {
if x, ok := ev.(pointer.Event); ok {
switch x.Type {
case pointer.Press:
pressed = true
case pointer.Release:
pressed = false
}
}
}
// Confine the area of interest to a 100x100 rectangle.
pointer.Rect(image.Rect(0, 0, 100, 100)).Add(ops)
// Declare the tag.
pointer.InputOp{Tag: tag}.Add(ops)
var c color.RGBA
if pressed {
c = color.RGBA{R: 0xFF, A: 0xFF}
} else {
c = color.RGBA{G: 0xFF, A: 0xFF}
}
paint.ColorOp{Color: c}.Add(ops)
paint.PaintOp{Rect: f32.Rect(0, 0, 100, 100)}.Add(ops)
}
It's convenient to use a Go pointer value for the input tag, as it's cheap to convert a pointer to an interface{} and it's easy to make the value specific to a local data structure, which avoids the risk of tag conflict. However, using other kinds of tag can work, bearing in mind that all the handlers using the same tag will see the events.
For more details take a look at https://gioui.org/io/pointer (pointer/mouse events) and https://gioui.org/io/key (keyboard events).
A single frame consists of getting input, registering for input and drawing the new state:
func main() {
go func() {
w := app.NewWindow()
if err := loop(w); err != nil {
log.Fatal(err)
}
}()
app.Main()
}
func loop(w *app.Window) error {
ops := new(op.Ops)
for e := range w.Events() {
switch e := e.(type) {
case system.DestroyEvent:
return e.Err
case system.FrameEvent:
ops.Reset()
// Handle button input and draw.
doButton(ops, e.Queue)
// Update display.
e.Frame(ops)
}
}
}
Writing a program using these concepts could get really verbose, however these low-level pieces are intended for writing widgets. Most programs end up using widgets rather than the low-level operations.
We’ve been mentioning widgets quite a while now. In principle widgets are composable and drawable UI elements that react to input. Or to put more concretely.
They get input from e.Queue
They might hold some state
They calculate their size
They draw themselves to op.Ops
By convention, widgets have a Layout
method that does all of the above. Some widgets have separate methods for querying their state or to pass events back to the program.
Some kinds of widget state have several visual representations. For example, the stateful Clickable is used for buttons and icon buttons. In fact, the material package implements just the Material Design and is intended to be supplemented by other packages implementing different designs.
To build out more complex UI from these primitives we need some structure that describes the layout in a composable way.
It’s possible to specify a layout statically, but display sizes vary greatly, so we need to be able to calculate the layout dynamically - that is constrain the available display size and then calculate the rest of the layout. We also need a comfortable way of passing events through the composed structure and similarly we need a way to pass op.Ops
through the system.
layout.Context conveniently bundles these aspects together. It carries the state that is needed by almost all layouts and widgets.
To summarise the terminology:
Constraints are an “incoming” parameter to a widget. The constraints hold a widget’s maximum (and minimum) size. Dimensions are an “outgoing” return value from a widget, used for tracking or returning the most recent layout size. Ops holds the generated draw operations. Events holds events generated since the last drawing operation.
Drawing operations use pixel coordinates, ignoring any transformation applied. However, for most use-cases you don’t want to tie user-interface to exact screen pixels. People may have screen-scaling enabled and people may use different devices.
For this purpose there is gioui.org/unit
, it contains support for:
Px
- device dependent pixel. The actual physical pixel.
Dp
- device independent pixel. Takes into account screen-density and the screen-scaling settings.
Sp
- device independent pixel for font-size. Takes into account screen-density, screen-scaling and font-scaling settings.
layout.Context
has method Px
to convert from unit.Value
to pixels.
For more information on pixel-density see: https://material.io/design/layout/pixel-density.html. https://webplatform.github.io/docs/tutorials/understanding-css-units/
As an example, here is how to implement a very simple button.
First let’s draw our button:
type Button struct {
pressed bool
}
func (b *Button) Layout(gtx *layout.Context) {
col := color.RGBA{A: 0xff, R: 0xff}
if b.pressed {
col = color.RGBA{A: 0xff, G: 0xff}
}
drawSquare(gtx.Ops, col)
}
func drawSquare(ops *op.Ops, color color.RGBA) {
square := f32.Rect(0, 0, 500, 500)
paint.ColorOp{Color: color}.Add(ops)
paint.PaintOp{Rect: square}.Add(ops)
}
We now also need to handle the input:
type Button struct {
pressed bool
}
func (b *Button) Layout(gtx *layout.Context) {
// here we loop through all the events associated with this button.
for _, e := range gtx.Events(b) {
if e, ok := e.(pointer.Event); ok {
switch e.Type {
case pointer.Press:
b.pressed = true
case pointer.Release:
b.pressed = false
}
}
}
// Confine the area for pointer events.
pointer.Rect(image.Rect(0, 0, 500, 500)).Add(gtx.Ops)
pointer.InputOp{Tag: b}.Add(gtx.Ops)
// Draw the button.
col := color.RGBA{A: 0xff, R: 0xff}
if b.pressed {
col = color.RGBA{A: 0xff, G: 0xff}
}
drawSquare(gtx.Ops, col)
}
For complicated UI-s you will need to layout widgets in multiple ways. gioui.org/layout
provides the common ones.
TODO:
TODO:
layout.Stack
lays out child elements on top of each other, according to the alignment direction. The elements of stack layout can be:
Expanded
- which uses at least as much space as the largest stacked item.
Stacked
- which uses the size based on the maximum constraints as the stack.
The elements are either Expanded
, which will use the size of the largest child
func stackedLayout(gtx *layout.Context) {
layout.Stack{}.Layout(gtx,
// Force widget to the same size as the second.
layout.Expanded(func() {
layoutWidget(gtx, 10, 10)
}),
// Rigid 50x50 widget.
layout.Stacked(func() {
layoutWidget(gtx, 50, 50)
}),
)
}
TODO:
TODO:
The same abstract widget can have many visual representations. Starting from simple changes like colors and more complicated, like entirely custom graphics. To give an application a consistent look it is useful to have an abstraction that contains “the look”.
Package material implements a look based on the Material Design, and the Theme struct encapsulates the parameters for varying colors, sizes and fonts.
Example of using the theme:
func LayoutApplication(gtx *layout.Context, th *material.Theme) {
material.H1(th, “Hello!”).Layout(gtx)
}
The kitchen shows all the different widgets available.
Sometimes the provided layouts are not sufficient. To create a custom layout for widgets there are special functions and structures to manipulate layout.Context. In general, layouting code performs the following steps for each sub-widget:
use StackOp.Push set layout.Context.Constraints set op.TransformOp call widget.Layout(gtx, ...) use dimensions returned by widget use StackOp.Pop
For complicated layouting code you would also need to use macros. As an example look at layout.Flex. Which roughly implements:
record widgets in macros calculate sizes for non-rigid widgets draw widgets based on the calculated sizes by replaying their macros
(full code currently here https://github.com/egonelbre/expgio/tree/master/split)
As an example, to display two widgets side-by-side, you could write a widget that looks like:
type Split struct {
}
func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
savedConstraints := gtx.Constraints
defer func() {
gtx.Constraints = savedConstraints
gtx.Dimensions.Size = image.Pt(
gtx.Constraints.Width.Max,
gtx.Constraints.Height.Max,
)
}()
gtx.Constraints.Height.Min = gtx.Constraints.Height.Max
leftsize := gtx.Constraints.Width.Max / 2
rightsize := gtx.Constraints.Width.Max - leftsize
{
var stack op.StackOp
stack.Push(gtx.Ops)
gtx.Constraints.Width.Min = leftsize
gtx.Constraints.Width.Max = leftsize
left()
stack.Pop()
}
{
var stack op.StackOp
stack.Push(gtx.Ops)
gtx.Constraints.Width.Min = rightsize
gtx.Constraints.Width.Max = rightsize
op.TransformOp{}.Offset(f32.Point{
X: float32(leftsize),
}).Add(gtx.Ops)
right()
stack.Pop()
}
}
The usage code would look like:
split.Layout(gtx, func() {
// draw the left side
}, func() {
// draw the right side
})
Of course, you do not need to implement such layouting yourself, there are plenty of them available in layout.
(full code currently here https://github.com/egonelbre/expgio/tree/master/split-interactive)
To make it more useful we could make the split draggable.
First let’s make the ratio adjustable. We should try to make zero values useful, in this case 0
could mean that it’s split in the center.
type Split struct {
// Ratio tracks the allocation of space between the two halves.
// 0 is center, -1 completely to the left, 1 completely to the right.
Ratio float32
}
func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
savedConstraints := gtx.Constraints
defer func() {
gtx.Constraints = savedConstraints
gtx.Dimensions.Size = image.Point{
X: savedConstraints.Width.Max,
Y: savedConstraints.Height.Max,
}
}()
gtx.Constraints.Height.Min = gtx.Constraints.Height.Max
proportion := (s.Ratio + 1) / 2
leftsize := int(proportion*float32(gtx.Constraints.Width.Max))
rightoffset := leftsize
rightsize := gtx.Constraints.Width.Max - rightoffset
{
var stack op.StackOp
stack.Push(gtx.Ops)
gtx.Constraints.Width.Min = leftsize
gtx.Constraints.Width.Max = leftsize
left()
stack.Pop()
}
{
var stack op.StackOp
stack.Push(gtx.Ops)
gtx.Constraints.Width.Min = rightsize
gtx.Constraints.Width.Max = rightsize
op.TransformOp{}.Offset(f32.Point{
X: float32(rightoffset),
}).Add(gtx.Ops)
right()
stack.Pop()
}
}
Because we also need to have an area designated for moving the split, let’s add a bar into the center:
type Split struct {
// Ratio keeps the current layout.
// 0 is center, -1 completely to the left, 1 completely to the right.
Ratio float32
// Bar is the width for resizing the layout
Bar unit.Value
}
var defaultBarWidth = unit.Dp(10)
func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
savedConstraints := gtx.Constraints
defer func() {
gtx.Constraints = savedConstraints
gtx.Dimensions.Size = image.Point{
X: savedConstraints.Width.Max,
Y: savedConstraints.Height.Max,
}
}()
gtx.Constraints.Height.Min = gtx.Constraints.Height.Max
bar := gtx.Px(s.Bar)
if bar <= 1 {
bar = gtx.Px(defaultBarWidth)
}
proportion := (s.Ratio + 1) / 2
leftsize := int(proportion*float32(gtx.Constraints.Width.Max) - float32(bar))
rightoffset := leftsize + bar
rightsize := gtx.Constraints.Width.Max - rightoffset
{
var stack op.StackOp
stack.Push(gtx.Ops)
gtx.Constraints.Width.Min = leftsize
gtx.Constraints.Width.Max = leftsize
left()
stack.Pop()
}
{
var stack op.StackOp
stack.Push(gtx.Ops)
gtx.Constraints.Width.Min = rightsize
gtx.Constraints.Width.Max = rightsize
op.TransformOp{}.Offset(f32.Point{
X: float32(rightoffset),
}).Add(gtx.Ops)
right()
stack.Pop()
}
}
Now we need to handle input events:
type Split struct {
// Ratio keeps the current layout.
// 0 is center, -1 completely to the left, 1 completely to the right.
Ratio float32
// Bar is the width for resizing the layout
Bar unit.Value
// drag says that some pointer is dragging things
drag bool
// dragID specifies which pointer (e.g. mouse, or which finger) is dragging
dragID pointer.ID
// dragX is the last dragging position
dragX float32
}
... snip ...
// handle events
for _, ev := range gtx.Events(s) {
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Type {
case pointer.Press:
// ensure that we don’t start grabbing twice
if s.drag {
break
}
// setup our initial state for dragging
s.drag = true
s.dragID = e.PointerID
s.dragX = e.Position.X
case pointer.Move:
// ensure that the correct pointer handles things
if !s.drag || s.dragID != e.PointerID {
break
}
// calculate how much we need to adjust ratio
deltaX := e.Position.X - s.dragX
s.dragX = e.Position.X
deltaRatio := deltaX * 2 / float32(gtx.Constraints.Width.Max)
s.Ratio += deltaRatio
case pointer.Release:
fallthrough
case pointer.Cancel:
// finish dragging
if !s.drag || s.dragID != e.PointerID {
break
}
s.drag = false
}
}
// Compute area where the bar is draggable.
barRect := image.Rect(leftsize, 0, rightoffset, gtx.Constraints.Height.Max)
// Declare bar area for input.
pointer.Rect(barRect).Add(gtx.Ops)
// Grab tells the input system to ensure this widget gets priority.
pointer.InputOp{Tag: s, Grab: s.drag}.Add(gtx.Ops)
Putting the whole Layout function together:
func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
savedConstraints := gtx.Constraints
defer func() {
gtx.Constraints = savedConstraints
gtx.Dimensions.Size = image.Point{
X: savedConstraints.Width.Max,
Y: savedConstraints.Height.Max,
}
}()
gtx.Constraints.Height.Min = gtx.Constraints.Height.Max
bar := gtx.Px(s.Bar)
if bar <= 1 {
bar = gtx.Px(defaultBarWidth)
}
proportion := (s.Ratio + 1) / 2
leftsize := int(proportion*float32(gtx.Constraints.Width.Max) - float32(bar))
rightoffset := leftsize + bar
rightsize := gtx.Constraints.Width.Max - rightoffset
{ // handle input
for _, ev := range gtx.Events(s) {
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Type {
case pointer.Press:
if s.drag {
break
}
s.drag = true
s.dragID = e.PointerID
s.dragX = e.Position.X
case pointer.Move:
if !s.drag || s.dragID != e.PointerID {
break
}
deltaX := e.Position.X - s.dragX
s.dragX = e.Position.X
deltaRatio := deltaX * 2 / float32(gtx.Constraints.Width.Max)
s.Ratio += deltaRatio
case pointer.Release:
fallthrough
case pointer.Cancel:
if !s.drag || s.dragID != e.PointerID {
break
}
s.drag = false
}
}
// register for input
barRect := image.Rect(leftsize, 0, rightoffset, gtx.Constraints.Width.Max)
pointer.Rect(barRect).Add(gtx.Ops)
pointer.InputOp{Tag: s, Grab: s.drag}.Add(gtx.Ops)
}
{
var stack op.StackOp
stack.Push(gtx.Ops)
gtx.Constraints.Width.Min = leftsize
gtx.Constraints.Width.Max = leftsize
left()
stack.Pop()
}
{
var stack op.StackOp
stack.Push(gtx.Ops)
gtx.Constraints.Width.Min = rightsize
gtx.Constraints.Width.Max = rightsize
op.TransformOp{}.Offset(f32.Point{X: float32(rightoffset)}).Add(gtx.Ops)
right()
stack.Pop()
}
}
You may have noticed that widget constraints and dimensions sizes are in integer units, while drawing commands such as PaintOp use floating point units. That’s because they refer to two distinct coordinate systems, the layout coordinate system and the drawing coordinate system. The distinction is subtle, but important.
The layout coordinate system is in integer pixels, because it's important that widgets never unintentionally overlap in the middle of a physical pixel. In fact, the decision to use integer coordinates was motivated by conflation issues in other UI libraries caused by allowing fractional layouts.
As a bonus, integer coordinates are perfectly deterministic across all platforms which leads to easier debugging and testing of layouts.
On the other hand, drawing commands need the generality of floating point coordinates for smooth animation and for expression inherently fractional shapes such as bézier curves.
It's possible to draw shapes that overlap at fractional pixel coordinates, but only intentionally: drawing commands directly derived from layout constraints have integer coordinates by construction.
The problem: You’ve created a nice new widget. You lay it out, say, in a Flex Rigid. The next Rigid draws on top of it.
The explanation: Gio communicates the size of widgets dynamically via layout.Context.Dimensions (commonly “gtx.Dimensions”). High level widgets (such as Labels) “return” or pass on their dimensions in gtx.Dimensions, but lower-level operations, such as paint.PaintOp, do not set Dimensions.
The solution: Update gtx.Dimensions in your widget’s Layout function before you return.
TODO: Example code & screenshots illustrating the problem and solution.
The problem: You lay out a list and then it just sits there and doesn’t scroll.
The explanation: A lot of widgets in Gio are context free -- you can and should declare them every time through your Layout function. Lists are not like that. They record their scroll position internally, and that needs to persist between calls to Layout.
The solution: Declare your List once outside the event handling loop and reuse it across frames.
The problem: You define a field in your widget struct with the widget. You update the child widget state, either implicitly or explicitly. The child widget stubbornly refuses to reflect your updates.
This is related to the problem with Lists that won’t scroll, above.
One possible explanation: You might be seeing a common “gotcha” in Go code, where you’ve defined a method on a value receiver, not a pointer receiver, so all the updates you’re making to your widget are only visible inside that function, and thrown away when it returns.
The solution: Make sure you’re using pointer receivers when appropriate. Usually Layout and Update methods should have pointer receivers.
Egon Elbre Roger Peppe Larry Clapp Mikhail Gusarov Lucas Rodrigues