Skip to content

Instantly share code, notes, and snippets.

@egonelbre
Last active January 7, 2023 02:33
Show Gist options
  • Select an option

  • Save egonelbre/f458f857d95d8330cbc1fb05d638ef5c to your computer and use it in GitHub Desktop.

Select an option

Save egonelbre/f458f857d95d8330cbc1fb05d638ef5c to your computer and use it in GitHub Desktop.

Revisions

  1. egonelbre revised this gist May 19, 2020. 1 changed file with 247 additions and 167 deletions.
    414 changes: 247 additions & 167 deletions architecture.md
    Original file line number Diff line number Diff line change
    @@ -2,25 +2,24 @@
    title: Architecture
    ---

    ## Immediate Mode UI
    # Introduction

    Gio is based on the concept of Immediate Mode User Interface.
    This approach can be implemented in multiple ways,
    however the overarching similarity is that the program:
    Gio implements an [Immediate Mode User Interface](https://eliasnaur.com/blog/immediate-mode-gui-programming)..
    This approach can be implemented in multiple ways, however the overarching similarity is that the program:

    1. listens for events such as mouse or keyboard input,
    2. updates its internal state based on the event (e.g. sets Checked = true for a checkbox),
    3. runs code that re-renders and layouts the whole state.
    3. runs code that redraws and layouts the whole state.

    In pseudo-code a minimal immediate mode UI can look like this:
    A minimal immediate mode command-line UI could look like this:

    ``` go
    // state of the program
    var showlist bool
    var items []string

    for {
    // waiting for new events
    // Wait for new events
    select {
    case ev := <-eventQueue:
    clearScreen()
    @@ -69,19 +68,19 @@ This of course is not a very useful library, however it demonstrates the core lo
    1. get an event
    2. 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 determined based on the code, not by a separate configuration or setup “before rendering”.
    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:

    1. how do you get the events?
    2. when do you re-render the state?
    3. what do the widget structures look like?
    4. how do you track the focus?
    5. how do you structure the events?
    6. how do you communicate with the graphics card?
    7. how do you handle input?
    8. how do you render text?
    9. where does the widget state belong?
    1. How do you get the events?
    2. When do you redraw the state?
    3. What do the widget structures look like?
    4. How do you track the focus?
    5. How do you structure the events?
    6. How do you communicate with the graphics card?
    7. How do you handle input?
    8. How do you draw text?
    9. Where does the widget state belong?
    10. ... 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:
    @@ -92,7 +91,7 @@ The rest of this document tries to answer how Gio does it. If you wish to know m
    * https://github.com/ocornut/imgui
    * https://eliasnaur.com/blog/immediate-mode-gui-programming

    ## Window
    # Window

    Since a GUI library needs to talk to some sort of display system to display information:

    @@ -107,7 +106,9 @@ for {
    return e.Err
    case system.FrameEvent:
    // A request to draw the window state.
    // Update the visible state based on events in e.Queue.
    ops := new(op.Ops)
    // Draw the state into ops based on events in e.Queue.
    e.Frame(ops)
    }
    }
    }
    @@ -117,7 +118,7 @@ for {

    It then sends events from the display system to the `windows.Events()` channel.

    ## Operations
    # Operations

    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](https://gioui.org/op#Ops).

    @@ -126,34 +127,34 @@ In abstract terms an `Ops` value contains a sequence of operations that tell the
    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.

    ``` go
    var ops op.Ops
    ops := new(op.Ops)

    red := color.RGBA{R:0xFF, A:0xFF}
    paint.ColorOp{Color: red}.Add(&ops)
    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.

    ## Rendering
    # Drawing

    To tell the graphics API what to draw, Gio uses [op.Ops](https://gioui.org/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](https://godoc.org/gioui.org/op#TransformOp). This means `f32.Point{X:0, Y:0}` is the top left corner of the window.
    Coordinates are based on the top-left corner by default, although it’s possible to [transform the coordinate system](https://gioui.org/op#TransformOp). This means `f32.Point{X:0, Y:0}` is the top left corner of the window. All drawing operations use pixel units, see [Units](#Units) section for more information.

    Gio encodes operations as Go structs which know how to encode data into `op.Ops`. data into `op.Ops`. For example, the following code will draw a 10x10 pixel colored rectangle at the top level corner of the window: encoding a colored rectangle looks like:
    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:

    ``` go
    func drawRedRect(ops *op.Ops) {
    paint.ColorOp{Color: color.RGBA{R: 0x80, G: 0x00, B: 0x00, A: 0xFF}}.Add(ops)
    paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X:100, Y:100}}}.Add(ops)
    paint.PaintOp{Rect: f32.Rect(0, 0, 100, 100)}.Add(ops)
    }
    ```

    ### Transformation
    ## Transformation

    Operation [op.TransformOp](https://gioui.org/op#TransformOp) allows us to translate the position of the rendering operations that come after it.
    Operation [op.TransformOp](https://gioui.org/op#TransformOp) allows us to translate the position of the operations that come after it.

    For example, the following would render 10 units to the right compared to the previous example:
    For example, the following would draw 100 units to the right compared to the previous example:

    ``` go
    func drawRedRect10PixelsRight(ops *op.Ops) {
    @@ -162,28 +163,30 @@ func drawRedRect10PixelsRight(ops *op.Ops) {
    }
    ```

    ### Clipping
    Note: in the future, TransformOp will allow other transformations such as scaling and rotation too.

    In some cases we want the rendering to be clipped to some smaller rectangle to avoid accidentally drawing over other things.
    ## Clipping

    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](https://gioui.org/op/clip), provides exactly that.

    [clip.Rect](https://gioui.org/op/clip#Rect) clips all subsequent rendering to a particular rounded rectangle.
    [clip.Rect](https://gioui.org/op/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](xxx) that encodes the clipping path.
    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](https://gioui.org/op#MacroOp) that encodes the clipping path.

    This could be used as a basis for a button background:

    ``` go
    func redButtonBackground(ops *op.Ops) {
    r := 3 // roundness
    bounds := f32.Rectangle{Max: f32.Point{X:30, Y:100}}
    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)
    }
    ```

    ### Push and Pop
    ## Push and Pop

    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`](https://gioui.org/op#TransformOp)) and clipping (set by [`ClipOp`](https://gioui.org/op/clip#ClipOp)).

    @@ -198,29 +201,29 @@ func redButtonBackground(ops *op.Ops) {
    stack.Push(ops)
    defer stack.Pop()
    r := 3 // roundness
    bounds := f32.Rectangle{Max: f32.Point{X:30, Y:100}}
    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 Order and Macros
    ## Drawing Order and Macros

    Drawing happens from back to front. In this example the green rectangle is drawn on top of red rectangle:

    ``` go
    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.Rectangle{Max: f32.Point{X:100, Y:10}}}.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.Rectangle{Max: f32.Point{X:10, Y:100}}}.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 rendering to apply a transform that is calculated after the rendering, or you may want to do the same set of operations several times. For this purpose there is [op.MacroOp](https://gioui.org/op#MacroOp).
    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](https://gioui.org/op#MacroOp).

    ``` go
    func drawFiveRectangles(ops *op.Ops) {
    @@ -239,9 +242,9 @@ func drawFiveRectangles(ops *op.Ops) {
    }
    ```

    ### Animating and requesting a redraw
    ## Animating and requesting a redraw

    When you are animating something you may need to retrigger rendering immediately rather than wait for events. For that there is [op.InvalidateOp](https://gioui.org/op#InvalidateOp)
    When you are animating something you may need to trigger a redraw immediately rather than wait for events. For that there is [op.InvalidateOp](https://gioui.org/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:
    ```
    @@ -262,41 +265,48 @@ func drawProgressBar(ops *op.Ops, now time.Time) {
    paint.ColorOp{Color: color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}}.Add(ops)
    width := 100*float32(progress)
    paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X:width, Y:10}}}.Add(ops)
    paint.PaintOp{Rect: f32.Rect(0, 0, width, 10)}.Add(ops)
    }
    ```

    ### Caching with CallOp
    ## Reusing operations with CallOp

    While MacroOp allows you to record and replay operations on a single operation list, [CallOp](https://gioui.org/op#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:

    If you wish to cache a rendering to redraw a frame later, you can use op.Ops and then use op.CallOp to render it. This can be useful to animate away a widget that has been removed:

    ``` go
    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.Rectangle{Max: f32.Point{X:10, Y:100}}}.Add(cache)
    paint.PaintOp{Rect: f32.Rect(0, 0, 100, 100)}.Add(cache)

    // Render the operations from the cache.
    // Draw the operations from the cache.
    op.CallOp{Ops: cache}.Add(ops)
    }
    ```

    ### Images
    ## Images

    [paint.ImageOp](https://gioui.org/op/paint#ImageOp) can be used to draw images. Like ColorOp, it sets the drawing context that’s used for any subsequent PaintOp. It is used similarly to ColorOp. Note that [RGBA](https://golang.org/pkg/image#RGBA) and image.Uniform(https://golang.org/pkg/image#Uniform) images are efficient and treated specially. Other Image implementations will undergo a potentially expensive conversion to convert them to the underlying image model.
    [paint.ImageOp](https://gioui.org/op/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](https://golang.org/pkg/image#RGBA) and [image.Uniform](https://golang.org/pkg/image#Uniform) images are efficient and treated specially. Other Image implementations will undergo a potentially expensive conversion to the underlying image model.

    ``` go
    func drawImage(ops *op.Ops, img image.Image) {
    imageOp := paint.NewImageOp(img)
    imageOp.Add(ops)
    paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X:100, Y:100}}}.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 rendered.
    Note, the image must not be mutated until another FrameEvent happens, because the image is read asynchronously while the frame is being drawn.

    ## Text

    TODO: describe how text shaper works

    ## Input
    # Input

    Input is delivered to the widgets via a [`system.FrameEvent`](https://gioui.org/io/system#FrameEvent) which contains a [`Queue`](https://gioui.org/io/system#FrameEvent.Queue).

    @@ -305,18 +315,18 @@ Some of the most common events are:
    [`key.EditEvent`](https://gioui.org/io/key#EditEvent) - for text editing.
    [`pointer.Event`](https://gioui.org/io/pointer#Event) - for mouse and touch input.

    The program can do arbitrary things when these events arrive, such as updating its local state. The Frame event is special - when the program receives a Frame event, it is responsible for rendering the graphics by calling the e.Frame function with an Ops value holding all the graphics to render.
    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`](https://gioui.org/gesture), that detect higher-level actions such as a double-click from individual click events.

    To handle input for multiple different widgets, Gio needs to have widgets register themselves to receive input. However, since the Gio framework doesn't provide a persistent data structure as state, there's no obvious place where widgets can register themselves to receive input.
    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.

    Gio achieves this by associating input with an arbitrary tag (an interface{} value) provided by the program. When the next frame is being rendered, the input can be retrieved by using the same tag.
    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.

    For example to handle input and register for listening for it, the code would look something like:
    The following example demonstrates registering and handling pointer input:

    ``` go
    var tag = new(bool) // We could use &pressed for this instead.
    var tag = new(bool) // We could use &pressed for this instead.
    var pressed = false

    func doButton(ops *op.Ops, q event.Queue) {
    @@ -325,6 +335,7 @@ func doButton(ops *op.Ops, q event.Queue) {
    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 {
    @@ -336,7 +347,9 @@ func doButton(ops *op.Ops, q event.Queue) {
    }
    }

    // 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
    @@ -346,17 +359,17 @@ func doButton(ops *op.Ops, q event.Queue) {
    c = color.RGBA{G: 0xFF, A: 0xFF}
    }
    paint.ColorOp{Color: c}.Add(ops)
    paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X:100, Y:100}}}.Add(ops)
    paint.PaintOp{Rect: f32.Rect(0, 0, 100, 100)}.Add(ops)
    }
    ```

    It's convenient to use a 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.
    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://godoc.org/gioui.org/io/pointer (pointer/mouse events) and https://godoc.org/gioui.org/io/key (keyboard events).
    For more details take a look at https://gioui.org/io/pointer (pointer/mouse events) and https://gioui.org/io/key (keyboard events).

    ## Putting the low-level pieces together
    # Putting the low-level pieces together

    A single frame consists of getting input, registering for input and rendering the new state:
    A single frame consists of getting input, registering for input and drawing the new state:

    ``` go
    func main() {
    @@ -378,54 +391,68 @@ func loop(w *app.Window) error {
    case system.FrameEvent:
    ops.Reset()

    // handle button input and render
    // Handle button input and draw.
    doButton(ops, e.Queue)

    // render the frame
    // 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 themselves. Most programs end up using widgets rather than the low-level operations.
    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.

    ## Widget
    # Widget

    We’ve been mentioning widgets quite a while now. In principle widgets are composable and renderable UI elements that react to input. Or to put more concretely.
    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 render themselves to `op.Ops`
    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](gioui.org/widget#Clickable.Clicked).

    By convention they have: (TODO: explain the difference between Layout and Update)
    method called `Layout(gtx *layout.Context, ...)` to render themselves,
    method called `Update(gtx *layout.Context, ...)` to update themselves.
    Some kinds of widget state have several visual representations. For example, the stateful [Clickable](gioui.org/widget#Clickable) is used for [buttons](gioui.org/widget/material#ButtonStyle.Layout) and [icon buttons](gioui.org/widget/material#IconButtonStyle.Layout). In fact, the [material package](gioui.org/widget/material) implements just the [Material Design](https://material.io) and is intended to be supplemented by other packages implementing different designs.

    This gives a separation between “widget state” and “widget style and rendering”. The state is very often reusable. The common widget states are in [gioui.org/widget](https://gioui.org/widget). Code that combines state with style is in [gioui.org/widget/material](https://gioui.org/widget/material).
    # Context

    ## Context
    To build out more complex UI from these primitives we need some structure that describes the layout in a composable way.

    To build out more complex UI from these primitives we need more structure and describe 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.

    Static layouts are a thing of the past and we need to calculate how big things can be, i.e. constrain their size and then figure out 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. It would be really inconvenient to pass them separately.
    [layout.Context](https://gioui.org/layout#Context) conveniently bundles these aspects together. It carries the state that is needed by almost all layouts and widgets.

    [layout.Context](https://gioui.org/layout#Context) is what carries the state that is needed by almost all layouts and widgets. Summarizing:
    To summarise the terminology:

    Constraints - an “incoming” parameter to a widget: give a widget’s maximum (and minimum) size..
    Dimensions - an “outgoing” return value from a widget, used for tracking or returning the most recent layout size.
    Ops - for communicating with the windowing system.
    Events - to get events associated with some handle
    Now - to get the current time
    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.

    It contains the constraints on how much screen is available -- both how much and how little screen real-estate the caller wants the widget to consume.
    # Units

    ## A Button From Scratch
    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.

    As an example, here is how to write a very simple button.
    For this purpose there is [`gioui.org/unit`](https://gioui.org/unit), it contains support for:

    First let’s write draw our button:
    [`Px`](https://gioui.org/unit#Px) - device dependent pixel. The actual physical pixel.
    [`Dp`](https://gioui.org/unit#Dp) - device independent pixel. Takes into account screen-density and the screen-scaling settings.
    [`Sp`](https://gioui.org/unit#Sp) - device independent pixel for font-size. Takes into account screen-density, screen-scaling and font-scaling settings.

    [`layout.Context`](https://gioui.org/layout#Context) has method [`Px`](https://gioui.org/layout#Context.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/

    # Custom Widget

    As an example, here is how to implement a very simple button.

    First let’s draw our button:

    ``` go
    type Button struct {
    @@ -441,9 +468,7 @@ col := color.RGBA{A: 0xff, R: 0xff}
    }

    func drawSquare(ops *op.Ops, color color.RGBA) {
    square := f32.Rectangle{
    Max: f32.Point{X: 500, Y: 500},
    }
    square := f32.Rect(0, 0, 500, 500)
    paint.ColorOp{Color: color}.Add(ops)
    paint.PaintOp{Rect: square}.Add(ops)
    }
    @@ -469,13 +494,12 @@ func (b *Button) Layout(gtx *layout.Context) {
    }
    }

    // register rectangle that receives
    pointer.Rect(
    image.Rectangle{Max: image.Point{X: 500, Y: 500}},
    ).Add(gtx.Ops)
    // 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
    // Draw the button.
    col := color.RGBA{A: 0xff, R: 0xff}
    if b.pressed {
    col = color.RGBA{A: 0xff, G: 0xff}
    @@ -484,29 +508,86 @@ func (b *Button) Layout(gtx *layout.Context) {
    }
    ```

    ## Layouting
    # Layout

    For complicated UI-s you will need to layout widgets in multiple ways. [`gioui.org/layout`](https://gioui.org/layout) provides the common ones.

    TODO:

    ## Inset

    TODO:

    ## Stack

    [`layout.Stack`](https://gioui.org/layout#Stack) lays out child elements on top of each other, according to the alignment direction. The elements of stack layout can be:

    [`Expanded`](https://gioui.org/layout#Expanded) - which uses at least as much space as the largest stacked item.
    [`Stacked`](https://gioui.org/layout#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

    ``` go
    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)
    }),
    )
    }
    ```

    ## Flex

    TODO:

    ## List

    TODO:

    # Themes

    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](https://gioui.org/widget/material) implements a look based on the [Material Design](https://material.io/design), and the [Theme](https://gioui.org/widget/material#Theme) struct encapsulates the parameters for varying colors, sizes and fonts.

    Example of using the theme:

    ``` go
    func LayoutApplication(gtx *layout.Context, th *material.Theme) {
    material.H1(th, “Hello!”).Layout(gtx)
    }
    ```

    The [kitchen](https://git.sr.ht/~eliasnaur/gio/tree/master/example/kitchen/kitchen.go) shows all the different widgets available.

    To create a layout for widgets there are special functions and structures to manipulate layout.Context. The common variants are conveniently in [layout](https://gioui.org/layout).
    # Custom Layout

    Layouting in general happens as the following:
    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:

    push state
    use StackOp.Push
    set layout.Context.Constraints
    set op.TransformOp
    call widget.Layout(gtx, ...)
    pop state
    use dimensions returned by widget
    use StackOp.Pop

    For more complicated layout approaches it needs to be combined with op.MacroOp, such as in [flex](https://godoc.org/gioui.org/layout#Flex):
    For complicated layouting code you would also need to use macros. As an example look at [layout.Flex](https://gioui.org/layout#Flex). Which roughly implements:

    record widget rendering using MacroOp
    record widgets in macros
    calculate sizes for non-rigid widgets
    draw widgets based on the calculated sizes using the macros
    draw widgets based on the calculated sizes by replaying their macros

    ### Example: Split View Widget
    ## Example: Split View Widget

    (full code currently here https://github.com/egonelbre/expgio/tree/master/split)

    As an example, to split the screen into two you could write a widget that looks like:
    As an example, to display two widgets side-by-side, you could write a widget that looks like:

    ``` go
    type Split struct {
    @@ -516,10 +597,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.Dimensions.Size = image.Pt(
    gtx.Constraints.Width.Max,
    gtx.Constraints.Height.Max,
    )
    }()
    gtx.Constraints.Height.Min = gtx.Constraints.Height.Max

    @@ -558,15 +639,15 @@ The usage code would look like:

    ``` go
    split.Layout(gtx, func() {
    // render the left side
    // draw the left side
    }, func() {
    // render the right side
    // draw the right side
    })
    ```

    Of course, you do not need to implement such layouting yourself, there are plenty of them available in [layout](https://gioui.org/layout).

    ### Example: Split View Widget Input
    ## Example: Interactive Split View Widget

    (full code currently here https://github.com/egonelbre/expgio/tree/master/split-interactive)

    @@ -576,7 +657,7 @@ First let’s make the ratio adjustable. We should try to make zero values usefu

    ``` go
    type Split struct {
    // Ratio keeps the current layout.
    // 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
    }
    @@ -633,10 +714,10 @@ type Split struct {
    // 0 is center, -1 completely to the left, 1 completely to the right.
    Ratio float32
    // Bar is the width for resizing the layout
    Bar int
    Bar unit.Value
    }
    const defaultBarWidth = 4
    var defaultBarWidth = unit.Dp(10)
    func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    savedConstraints := gtx.Constraints
    @@ -649,9 +730,9 @@ func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    }()
    gtx.Constraints.Height.Min = gtx.Constraints.Height.Max
    bar := s.Bar
    if bar <= 0 {
    bar = defaultBarWidth
    bar := gtx.Px(s.Bar)
    if bar <= 1 {
    bar = gtx.Px(defaultBarWidth)
    }
    proportion := (s.Ratio + 1) / 2
    @@ -695,7 +776,7 @@ type Split struct {
    // 0 is center, -1 completely to the left, 1 completely to the right.
    Ratio float32
    // Bar is the width for resizing the layout
    Bar int
    Bar unit.Value

    // drag says that some pointer is dragging things
    drag bool
    @@ -751,15 +832,15 @@ for _, ev := range gtx.Events(s) {
    }


    // Register input
    barRect := image.Rect(leftsize, 0, rightoffset, gtx.Constraints.Width.Max)
    // Register bar rectangle for input.
    // 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, it will look like:
    Putting the whole Layout function together:

    ``` go
    func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    @@ -773,9 +854,9 @@ func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    }()
    gtx.Constraints.Height.Min = gtx.Constraints.Height.Max

    bar := s.Bar
    if bar <= 0 {
    bar = defaultBarWidth
    bar := gtx.Px(s.Bar)
    if bar <= 1 {
    bar = gtx.Px(defaultBarWidth)
    }

    proportion := (s.Ratio + 1) / 2
    @@ -853,25 +934,36 @@ func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    }
    ```

    Of course, we might need some additional checks to avoid Ratio values like `-5`, but that’s tiny improvements.

    ## Themes

    Since many widgets need different colors, it's useful to place all the relevant colors into a single struct [Theme](https://gioui.org/widget/material#Theme). It contains the relevant settings for a Material design based UI.

    Since also there needs to be custom code per style, it also contains widgets based on the [Material Design Components](https://material.io/design).

    ## Text

    TODO: describe how shaper works

    ## Units

    TODO: describe how units are handled

    ## Common widget developer errors

    ### The system is drawing on top of my custom widget, or otherwise ignoring its size.
    # Coordinate systems

    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](https://github.com/flutter/flutter/issues/15035) 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.

    # Common widget developer errors

    ## The system is drawing on top of my custom widget, or otherwise ignoring its size.

    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.

    @@ -881,41 +973,29 @@ The solution: Update gtx.Dimensions in your widget’s Layout function before yo

    TODO: Example code & screenshots illustrating the problem and solution.

    ### My list.List won’t scroll
    ## My list.List won’t scroll

    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 allocate new ones every time through your Layout function. Lists are not like that. They record their scroll position and state internally, and that state needs to persist between calls to Layout.
    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: Put your List object as a field inside your object struct to make it persistent across calls to Layout.
    The solution: Declare your List once outside the event handling loop and reuse it across frames.

    ### The system is ignoring updates to a widget
    ## The system is ignoring updates to a widget

    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.

    The (possible) explanation: You might be seeing a common “gotcha” in Go code, where you’ve defined a method that doesn’t take 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. (Or of course you might have some other bug.)
    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.

    ---

    ### OFFTOPIC ###

    # Convenience Wishlist

    // convenience constructor for f32.Rectangle
    `f32.Rect(minx, miny, maxx, maxy)`

    // convenience conversion from and to image.Rectangle
    `f32.FromImageRect()` // or similar
    `f32.Rectangle.ToImage()` or `image.Rect(f32.Rectangle.Ints())`

    Same for points.

    // clip rect also have Add, since often you want to immediately use it
    // rather than preserve.
    `clip.Rect.Add`

    // confusion
    rename widget.Bool -> widget.Toggle
    # Document contributors list
    Egon Elbre
    Roger Peppe
    Larry Clapp
    Mikhail Gusarov
    Lucas Rodrigues
  2. egonelbre revised this gist May 17, 2020. 1 changed file with 313 additions and 227 deletions.
    540 changes: 313 additions & 227 deletions architecture.md
    Original file line number Diff line number Diff line change
    @@ -1,20 +1,18 @@
    https://about.sourcegraph.com/go/gophercon-2019-simple-portable-efficient-graphical-interfaces-in-go

    ---
    title: Architecture
    ---

    ## Immediate Mode UI

    Gio is based on the concept of Immediate Mode UI.
    Gio is based on the concept of Immediate Mode User Interface.
    This approach can be implemented in multiple ways,
    however the overarching similarity is that the program:

    1. listens for events such as mouse or keyboard input,
    2. updates it's internal based on the event (e.g. sets a Checked = true for checkbox),
    3. runs code that re-renders the whole state.
    2. updates its internal state based on the event (e.g. sets Checked = true for a checkbox),
    3. runs code that re-renders and layouts the whole state.

    In pseudo-code a minimal immediate mode UI can look like:
    In pseudo-code a minimal immediate mode UI can look like this:

    ``` go
    // state of the program
    @@ -71,31 +69,32 @@ This of course is not a very useful library, however it demonstrates the core lo
    1. get an event
    2. handle the widgets while updating the state and drawing the widgets

    This simplicity of course contains a lot of different trade-offs that can be made:

    1. how do you get the events,
    2. when do you re-render the state,
    3. what do the widget structures look like,
    4. how do you track the focus,
    5. how do you structure the events,
    6. how do you communicate with the graphics card,
    7. how do you communicate with the operating system,
    8. how do you render text,
    9. should it be completely stateless widgets or some can be staful,
    10. ... and many more.
    The main differentiation from non-immediate user interfaces is that the widgets and layout are determined based on the code, not by a separate configuration or setup “before rendering”.

    The rest of the document tries to answer how Gio does it.
    This becomes less simple when other aspects of the GUI are taken into account:

    Immediate Mode References:
    1. how do you get the events?
    2. when do you re-render the state?
    3. what do the widget structures look like?
    4. how do you track the focus?
    5. how do you structure the events?
    6. how do you communicate with the graphics card?
    7. how do you handle input?
    8. how do you render text?
    9. where does the widget state belong?
    10. ... 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:

    * http://sol.gfxile.net/imgui/
    * 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

    ## Window

    Since a GUI library needs to talk to some sort of operating system to display information:
    Since a GUI library needs to talk to some sort of display system to display information:

    ``` go
    window := app.NewWindow(app.Size(unit.Dp(800), unit.Dp(650)))
    @@ -104,288 +103,292 @@ for {
    case e := <-window.Events():
    switch e := e.(type) {
    case system.DestroyEvent:
    // The window was closed.
    return e.Err
    case system.FrameEvent:
    // update state based on events in e.Queue
    // A request to draw the window state.
    // Update the visible state based on events in e.Queue.
    }
    }
    }
    ```

    `app.NewWindow` chooses the appropriate " handling driver" depending on the OS.
    It might choose Wayland, WinAPI or Cocoa and many others.

    It then wires together events coming from the OS to `windows.Events()`.
    `app.NewWindow` chooses the appropriate "handling driver" depending on the environment and build context. It might choose Wayland, WinAPI,Cocoa or many others.

    Additionally, it will initialize communication with the GPU, e.g. OpenGL, EGL or DirectX 11.
    It then sends events from the display system to the `windows.Events()` channel.

    ## Operations

    There is a need to communicate information to the windowing, GPU, input and about the general structure of the screen. Gio uses [op.Ops](https://gioui.org/op#Ops).
    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](https://gioui.org/op#Ops).

    In abstract terms it contains instructions for the window driver how to handle inputs, rendering. In general - it contains operations for the state of the system.
    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, different operations use an `Add` method on the operation itself and any properties use the fields. This means that zero value can be very useful for optional values.
    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.

    ``` go
    var ops op.Ops

    ExampleOp{Value: 123}.Add(&ops)
    ```

    ## Input

    Input is delivered to the widgets via a [`system.FrameEvent`](https://gioui.org/io/system#FrameEvent) which contains `Queue`.
    It might contain a keyboard event, such as [`key.Event`](https://gioui.org/io/key#Event).

    Based on these events widgets can modify the state.

    There are also event-processors, such as [`gioui.org/gesture`](https://gioui.org/gesture), that detects higher-level actions such as a "double-click" from individual click events.

    To handle input for multiple different widgets Gio needs to have “widgets” register themselves to receive input. However since immediate mode GUI is mostly stateless it means there cannot be a place where widgets directly register themselves.

    This handling is achieved by widgets adding an operation to the op.Ops with using some “persistent key” and later rereading using the same key. For example pointer input looks like this:

    ``` go
    r := image.Rectangle{...}
    pointer.Rect(r).Add(ops)
    pointer.InputOp{Key: h}.Add(ops)
    ```

    This will tell the windowing system to collate inputs on rectangle r with key `k`.

    On the next frame the widget can use `h` to get the appropriate event:

    ``` go
    select {
    case e := <-window.Events():
    switch e := e.(type) {
    case system.DestroyEvent:
    return e.Err
    case system.FrameEvent:
    for _, ev := range e.Queue(h) {
    // handle event for that widget
    }
    }
    }
    red := color.RGBA{R:0xFF, A:0xFF}
    paint.ColorOp{Color: red}.Add(&ops)
    ```

    It can be convenient to use widgets pointer as the key `h`, however, as long as they don’t conflict with other potential keys, there wouldn’t be any problem using them.

    For more details take a look at https://godoc.org/gioui.org/io/pointer.

    Input events are handled in a similar way, however with https://godoc.org/gioui.org/io/key
    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.

    ## Rendering

    To tell the graphics API what to draw, Gio uses [op.Ops](https://gioui.org/op#Ops) to serialize drawing commands.

    Coordinates are based on the top-left corner. This means `f32.Point{X:0, Y:0}` is the top left corner.
    Coordinates are based on the top-left corner by default, although it’s possible to [transform the coordinate system](https://godoc.org/gioui.org/op#TransformOp). This means `f32.Point{X:0, Y:0}` is the top left corner of the window.

    Gio uses structs of different operations to encode data into `op.Ops`. For example, encoding a colored rectangle looks like:
    Gio encodes operations as Go structs which know how to encode data into `op.Ops`. data into `op.Ops`. For example, the following code will draw a 10x10 pixel colored rectangle at the top level corner of the window: encoding a colored rectangle looks like:

    ``` go
    var ops op.Ops

    ColorOp{Color: color.RGBA{R: 0x80, G: 0x00, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:10}}}.Add(&ops)
    func drawRedRect(ops *op.Ops) {
    paint.ColorOp{Color: color.RGBA{R: 0x80, G: 0x00, B: 0x00, A: 0xFF}}.Add(ops)
    paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X:100, Y:100}}}.Add(ops)
    }
    ```

    [gioui.org/gpu](https://gioui.org/gpu) is able to decode the resulting `ops.Bytes()` and handle these operations for different APIs.

    ### Transformation

    Operation [op.TransformOp](https://gioui.org/op#TransformOp) allows us to offset, rotate, scale the rendering operations that come after it.
    Operation [op.TransformOp](https://gioui.org/op#TransformOp) allows us to translate the position of the rendering operations that come after it.

    For example, the following would render 10 units to the right compared to the previous example:

    ``` go
    var ops op.Ops

    TransformOp{}.Offset(f32.Point{X: 10, Y: 0}).Add(&ops)
    ColorOp{Color: color.RGBA{R: 0x80, G: 0x00, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:10}}}.Add(&ops)
    func drawRedRect10PixelsRight(ops *op.Ops) {
    op.TransformOp{}.Offset(f32.Point{X: 100, Y: 0}).Add(ops)
    drawRedRect(ops)
    }
    ```

    ### Clipping

    In some cases we want the rendering to be clipped to some smaller rectangle to avoid accidental drawing over other things.
    In some cases we want the rendering to be clipped to some smaller rectangle to avoid accidentally drawing over other things.

    Package [gioui.org/op/clip](https://gioui.org/op/clip), provides exactly that.

    [clip.Rect](https://gioui.org/op/clip#Rect) allows to clip all the rendering to a particular rounded rectangle.
    [clip.Rect](https://gioui.org/op/clip#Rect) clips all subsequent rendering 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](xxx) that encodes the clipping path.

    This could be used as a basis for a button background:

    ``` go
    var ops op.Ops

    r := 3 // roundness
    bounds := f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:100, Y:20}}
    clip.Rect{Rect: bounds, SE: r, SW: r, NW: r, NE: r}.Add(&ops)
    ColorOp{Color: color.RGBA{R: 0x80, G: 0x00, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: bounds}.Add(&ops)
    func redButtonBackground(ops *op.Ops) {
    r := 3 // roundness
    bounds := f32.Rectangle{Max: f32.Point{X:30, Y:100}}
    clip.Rect{Rect: bounds, SE: r, SW: r, NW: r, NE: r}.Op(ops).Add(ops)
    drawRedRectangle(ops)
    }
    ```

    ### Push and Pop

    For drawing we want to also undo any transformation, clipping or color settings that we set before. We can use [op.StackOp](https://gioui.org/op#StackOp) to do push and pop the state:
    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`](https://gioui.org/op#TransformOp)) and clipping (set by [`ClipOp`](https://gioui.org/op/clip#ClipOp)).

    ``` go
    ops := new(op.Ops)
    var stack op.StackOp
    stack.Push(ops)
    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](https://gioui.org/op#StackOp) to do this. A Push operation saves the current drawing context; a Pop operation restores it.

    // Apply a transform to subsequent operations.
    op.TransformOp{}.Offset(...).Add(ops)
    ...
    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:

    // Restore the previous transform.
    stack.Pop()
    ``` go
    func redButtonBackground(ops *op.Ops) {
    var stack op.StackOp
    stack.Push(ops)
    defer stack.Pop()
    r := 3 // roundness
    bounds := f32.Rectangle{Max: f32.Point{X:30, Y:100}}
    clip.Rect{Rect: bounds, SE: r, SW: r, NW: r, NE: r}.Op(ops).Add(ops)
    drawRedRectangle(ops)
    }
    ```

    ### Drawing Order and Macros

    Drawing happens from back to front. In this example the green rectangle is drawn on top of red rectangle:

    ``` go
    var ops op.Ops

    // red rectangle
    ColorOp{Color: color.RGBA{R: 0xFF, G: 0x00, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:100, Y:10}}}.Add(&ops)

    // green rectangle
    ColorOp{Color: color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:100}}}.Add(&ops)
    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.Rectangle{Max: f32.Point{X:100, Y:10}}}.Add(ops)

    // green rectangle
    paint.ColorOp{Color: color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}}.Add(ops)
    paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X:10, Y:100}}}.Add(ops)
    }
    ```

    Sometimes you may want to change this order. For example you may want to switch the order around or you may want to delay rendering to apply a transform that is calculated after the rendering. For this purpose there is [op.MacroOp](https://gioui.org/op#MacroOp)

    var macro op.MacroOp
    macro.Record(ops)
    // Record operations by adding them.
    op.InvalidateOp{}.Add(ops)
    ...
    // End recording.
    macro.Stop()

    Sometimes you may want to change this order. For example, you may want to delay rendering to apply a transform that is calculated after the rendering, or you may want to do the same set of operations several times. For this purpose there is [op.MacroOp](https://gioui.org/op#MacroOp).

    ``` go
    var ops op.Ops
    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)
    }
    }
    ```

    // start recording
    var macro op.MacroOp
    {
    macro.Record(&ops)
    ### Animating and requesting a redraw

    // red rectangle
    ColorOp{Color: color.RGBA{R: 0xFF, G: 0x00, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:100, Y:10}}}.Add(&ops)
    When you are animating something you may need to retrigger rendering immediately rather than wait for events. For that there is [op.InvalidateOp](https://gioui.org/op#InvalidateOp)

    // stop recording
    macro.Stop()
    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.Rectangle{Max: f32.Point{X:width, Y:10}}}.Add(ops)
    }
    ```

    // green rectangle
    ColorOp{Color: color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:100}}}.Add(&ops)
    ### Caching with CallOp

    // finally draw the recorded red rectangle with a transform
    op.TransformOp{}.Offset(...).Add(ops)
    macro.Add()
    ```
    If you wish to cache a rendering to redraw a frame later, you can use op.Ops and then use op.CallOp to render it. This can be useful to animate away a widget that has been removed:

    Also mention macros to make one widget aware of another widget’s dimensions: Draw the widget in a macro, get its dimensions, draw the second widget, and then Add() the macro.
    ``` go
    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.Rectangle{Max: f32.Point{X:10, Y:100}}}.Add(cache)

    // Render the operations from the cache.
    op.CallOp{Ops: cache}.Add(ops)
    }
    ```

    ### Animating or re-triggering a full render
    ### Images

    When you are animating something you may need to retrigger rendering immediately rather than wait for events. For that there is [op.InvalidateOp](https://gioui.org/op#InvalidateOp)
    [paint.ImageOp](https://gioui.org/op/paint#ImageOp) can be used to draw images. Like ColorOp, it sets the drawing context that’s used for any subsequent PaintOp. It is used similarly to ColorOp. Note that [RGBA](https://golang.org/pkg/image#RGBA) and image.Uniform(https://golang.org/pkg/image#Uniform) images are efficient and treated specially. Other Image implementations will undergo a potentially expensive conversion to convert them to the underlying image model.

    ```
    // declared outside
    var progress float32
    // animating
    if progress < 1 {
    progress += 0.1
    op.InvalidateOp{}.Add(ops) // since we haven’t yet finished drawing
    } else {
    progress = 1
    ``` go
    func drawImage(ops *op.Ops, img image.Image) {
    imageOp := paint.NewImageOp(img)
    imageOp.Add(ops)
    paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X:100, Y:100}}}.Add(ops)
    }
    ColorOp{Color: color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:100*progress}}}.Add(&ops)
    ```

    Note, in most cases you should also take into account time-delta from previous frame.

    ### Caching Rendering
    Note, the image must not be mutated until another FrameEvent happens, because the image is read asynchronously while the frame is being rendered.

    If you wish to cache a rendering to redraw a frame later, you can use op.Ops and then use op.CallOp to render it. This can become useful to animate away a widget that has been removed:
    ## Input

    ``` go
    var ops op.Ops
    Input is delivered to the widgets via a [`system.FrameEvent`](https://gioui.org/io/system#FrameEvent) which contains a [`Queue`](https://gioui.org/io/system#FrameEvent.Queue).

    var cache op.Ops
    ColorOp{Color: color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:100*progress}}}.Add(&cache)
    Some of the most common events are:
    [`key.Event`](https://gioui.org/io/key#Event), [`key.Focus`](https://gioui.org/io/key#Focus) - for keyboard input.
    [`key.EditEvent`](https://gioui.org/io/key#EditEvent) - for text editing.
    [`pointer.Event`](https://gioui.org/io/pointer#Event) - for mouse and touch input.

    op.CallOp{Ops: &cache}.Add(&ops)
    ```
    The program can do arbitrary things when these events arrive, such as updating its local state. The Frame event is special - when the program receives a Frame event, it is responsible for rendering the graphics by calling the e.Frame function with an Ops value holding all the graphics to render.

    ### Images
    There are also event-processors, such as [`gioui.org/gesture`](https://gioui.org/gesture), that detect higher-level actions such as a double-click from individual click events.

    [paint.ImageOp](https://gioui.org/op/paint#ImageOp) can be used to draw images. It is used similarly to color handling:
    To handle input for multiple different widgets, Gio needs to have widgets register themselves to receive input. However, since the Gio framework doesn't provide a persistent data structure as state, there's no obvious place where widgets can register themselves to receive input.

    ``` go
    var ops op.Ops
    Gio achieves this by associating input with an arbitrary tag (an interface{} value) provided by the program. When the next frame is being rendered, the input can be retrieved by using the same tag.

    imageOp := paint.NewImageOp(srcimage)
    For example to handle input and register for listening for it, the code would look something like:

    imageOp.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:10}}}.Add(&ops)
    ``` go
    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()

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

    pointer.Rect(image.Rect(0, 0, 100, 100)).Add(ops)
    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.Rectangle{Max: f32.Point{X:100, Y:100}}}.Add(ops)
    }
    ```

    Clip and Transform can be used to stretch or rotate the image.
    It's convenient to use a 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.

    Note, the image must not be mutated until another FrameEvent happens, because some of the work with the image is asynchronous.
    For more details take a look at https://godoc.org/gioui.org/io/pointer (pointer/mouse events) and https://godoc.org/gioui.org/io/key (keyboard events).

    ## Putting all the low-level pieces together
    ## Putting the low-level pieces together

    A single frame consists of getting input and rendering the new state:
    A single frame consists of getting input, registering for input and rendering the new state:

    ``` go
    red := color.RGBA{R: 0xFF, G: 0x00, B: 0x00, A: 0xFF}
    blue := color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}
    func main() {
    go func() {
    w := app.NewWindow()
    if err := loop(w); err != nil {
    log.Fatal(err)
    }
    }()
    app.Main()
    }

    for {
    e := <-w.Events()
    switch e := e.(type) {
    case system.DestroyEvent:
    return e.Err
    case system.FrameEvent:
    var ops op.Ops
    ops.Reset()

    color := red
    // TODO: show how event.Queue works
    if e.Queue contains space {
    color = blue
    }
    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()

    ColorOp{Color: color}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{
    Min: f32.Point{},
    Max: f32.Point{X:10, Y:10},
    }}.Add(&ops)
    // handle button input and render
    doButton(ops, e.Queue)

    e.Frame(gtx.Ops)
    }
    // render the frame
    e.Frame(ops)
    }
    }
    }
    ```

    Writing a program using these concepts could get really annoying, however these low-level pieces are intended for writing Widgets themselves. Most programs end up using widgets rather than the low-level pieces.
    Writing a program using these concepts could get really verbose, however these low-level pieces are intended for writing Widgets themselves. Most programs end up using widgets rather than the low-level operations.

    ## Widget

    @@ -396,7 +399,7 @@ They might hold some state
    They calculate their size
    They render themselves to `op.Ops`

    By convention they have:
    By convention they have: (TODO: explain the difference between Layout and Update)
    method called `Layout(gtx *layout.Context, ...)` to render themselves,
    method called `Update(gtx *layout.Context, ...)` to update themselves.

    @@ -410,13 +413,13 @@ Static layouts are a thing of the past and we need to calculate how big things c

    [layout.Context](https://gioui.org/layout#Context) is what carries the state that is needed by almost all layouts and widgets. Summarizing:

    Constraints - says how large a widget should be.
    Dimensions - is used for tracking the most recent layout size.
    Constraints - an “incoming” parameter to a widget: give a widget’s maximum (and minimum) size..
    Dimensions - an “outgoing” return value from a widget, used for tracking or returning the most recent layout size.
    Ops - for communicating with the windowing system.
    Events - to get events associated with some handle
    Now - to get the current time

    It contains the constraints on how much screen is available.
    It contains the constraints on how much screen is available -- both how much and how little screen real-estate the caller wants the widget to consume.

    ## A Button From Scratch

    @@ -470,7 +473,7 @@ func (b *Button) Layout(gtx *layout.Context) {
    pointer.Rect(
    image.Rectangle{Max: image.Point{X: 500, Y: 500}},
    ).Add(gtx.Ops)
    pointer.InputOp{Key: b}.Add(gtx.Ops)
    pointer.InputOp{Tag: b}.Add(gtx.Ops)

    // draw the button
    col := color.RGBA{A: 0xff, R: 0xff}
    @@ -501,6 +504,8 @@ draw widgets based on the calculated sizes using the macros

    ### Example: Split View Widget

    (full code currently here https://github.com/egonelbre/expgio/tree/master/split)

    As an example, to split the screen into two you could write a widget that looks like:

    ``` go
    @@ -509,10 +514,17 @@ type Split struct {

    func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    savedConstraints := gtx.Constraints
    defer func() { gtx.Constraints = savedConstraints }()
    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

    leftsize := gtx.Constraints.Width.Min / 2
    rightsize := gtx.Constraints.Width.Min - leftsize
    leftsize := gtx.Constraints.Width.Max / 2
    rightsize := gtx.Constraints.Width.Max - leftsize

    {
    var stack op.StackOp
    @@ -556,9 +568,11 @@ Of course, you do not need to implement such layouting yourself, there are plent

    ### Example: Split View Widget Input

    (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 ratio adjustable. We should try to make zero values useful, in this case `0` could mean that it’s split in the center.
    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.

    ``` go
    type Split struct {
    @@ -569,13 +583,20 @@ type Split struct {

    func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    savedConstraints := gtx.Constraints
    defer func() { gtx.Constraints = savedConstraints }()
    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.Min))
    leftsize := int(proportion*float32(gtx.Constraints.Width.Max))

    rightoffset := leftsize
    rightsize := gtx.Constraints.Width.Min - rightoffset
    rightsize := gtx.Constraints.Width.Max - rightoffset

    {
    var stack op.StackOp
    @@ -619,18 +640,25 @@ const defaultBarWidth = 4
    func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    savedConstraints := gtx.Constraints
    defer func() { gtx.Constraints = savedConstraints }()
    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 := s.Bar
    if bar <= 0 {
    bar = defaultBarWidth
    }
    proportion := (s.Ratio + 1) / 2
    leftsize := int(proportion*float32(gtx.Constraints.Width.Min) - float32(bar))
    leftsize := int(proportion*float32(gtx.Constraints.Width.Max) - float32(bar))
    rightoffset := leftsize + bar
    rightsize := gtx.Constraints.Width.Min - rightoffset
    rightsize := gtx.Constraints.Width.Max - rightoffset
    {
    var stack op.StackOp
    @@ -708,7 +736,7 @@ for _, ev := range gtx.Events(s) {
    deltaX := e.Position.X - s.dragX
    s.dragX = e.Position.X

    deltaRatio := deltaX * 2 / float32(gtx.Constraints.Width.Min)
    deltaRatio := deltaX * 2 / float32(gtx.Constraints.Width.Max)
    s.Ratio += deltaRatio

    case pointer.Release:
    @@ -728,26 +756,33 @@ barRect := image.Rect(leftsize, 0, rightoffset, gtx.Constraints.Width.Max)
    // Register bar rectangle for input.
    pointer.Rect(barRect).Add(gtx.Ops)
    // Grab tells the input system to ensure this widget gets priority.
    pointer.InputOp{Key: s, Grab: s.drag}.Add(gtx.Ops)
    pointer.InputOp{Tag: s, Grab: s.drag}.Add(gtx.Ops)
    ```

    Putting the whole Layout function together, it will look like:

    ``` go
    func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    savedConstraints := gtx.Constraints
    defer func() { gtx.Constraints = savedConstraints }()
    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 := s.Bar
    if bar <= 0 {
    bar = defaultBarWidth
    }

    proportion := (s.Ratio + 1) / 2
    leftsize := int(proportion*float32(gtx.Constraints.Width.Min) - float32(bar))
    leftsize := int(proportion*float32(gtx.Constraints.Width.Max) - float32(bar))

    rightoffset := leftsize + bar
    rightsize := gtx.Constraints.Width.Min - rightoffset
    rightsize := gtx.Constraints.Width.Max - rightoffset

    { // handle input
    for _, ev := range gtx.Events(s) {
    @@ -774,7 +809,7 @@ func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    deltaX := e.Position.X - s.dragX
    s.dragX = e.Position.X

    deltaRatio := deltaX * 2 / float32(gtx.Constraints.Width.Min)
    deltaRatio := deltaX * 2 / float32(gtx.Constraints.Width.Max)
    s.Ratio += deltaRatio

    case pointer.Release:
    @@ -790,7 +825,7 @@ func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    // register for input
    barRect := image.Rect(leftsize, 0, rightoffset, gtx.Constraints.Width.Max)
    pointer.Rect(barRect).Add(gtx.Ops)
    pointer.InputOp{Key: s, Grab: s.drag}.Add(gtx.Ops)
    pointer.InputOp{Tag: s, Grab: s.drag}.Add(gtx.Ops)
    }

    {
    @@ -833,3 +868,54 @@ TODO: describe how shaper works
    ## Units

    TODO: describe how units are handled

    ## Common widget developer errors

    ### The system is drawing on top of my custom widget, or otherwise ignoring its size.

    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.

    ### My list.List won’t scroll

    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 allocate new ones every time through your Layout function. Lists are not like that. They record their scroll position and state internally, and that state needs to persist between calls to Layout.

    The solution: Put your List object as a field inside your object struct to make it persistent across calls to Layout.

    ### The system is ignoring updates to a widget

    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.

    The (possible) explanation: You might be seeing a common “gotcha” in Go code, where you’ve defined a method that doesn’t take 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. (Or of course you might have some other bug.)

    The solution: Make sure you’re using pointer receivers when appropriate. Usually Layout and Update methods should have pointer receivers.


    ### OFFTOPIC ###

    # Convenience Wishlist

    // convenience constructor for f32.Rectangle
    `f32.Rect(minx, miny, maxx, maxy)`

    // convenience conversion from and to image.Rectangle
    `f32.FromImageRect()` // or similar
    `f32.Rectangle.ToImage()` or `image.Rect(f32.Rectangle.Ints())`

    Same for points.

    // clip rect also have Add, since often you want to immediately use it
    // rather than preserve.
    `clip.Rect.Add`

    // confusion
    rename widget.Bool -> widget.Toggle
  3. egonelbre revised this gist May 15, 2020. 1 changed file with 584 additions and 53 deletions.
    637 changes: 584 additions & 53 deletions architecture.md
    Original file line number Diff line number Diff line change
    @@ -119,6 +119,20 @@ It then wires together events coming from the OS to `windows.Events()`.

    Additionally, it will initialize communication with the GPU, e.g. OpenGL, EGL or DirectX 11.

    ## Operations

    There is a need to communicate information to the windowing, GPU, input and about the general structure of the screen. Gio uses [op.Ops](https://gioui.org/op#Ops).

    In abstract terms it contains instructions for the window driver how to handle inputs, rendering. In general - it contains operations for the state of the system.

    By convention, different operations use an `Add` method on the operation itself and any properties use the fields. This means that zero value can be very useful for optional values.

    ``` go
    var ops op.Ops

    ExampleOp{Value: 123}.Add(&ops)
    ```

    ## Input

    Input is delivered to the widgets via a [`system.FrameEvent`](https://gioui.org/io/system#FrameEvent) which contains `Queue`.
    @@ -128,20 +142,49 @@ Based on these events widgets can modify the state.

    There are also event-processors, such as [`gioui.org/gesture`](https://gioui.org/gesture), that detects higher-level actions such as a "double-click" from individual click events.

    _TODO: describe how they work._
    To handle input for multiple different widgets Gio needs to have “widgets” register themselves to receive input. However since immediate mode GUI is mostly stateless it means there cannot be a place where widgets directly register themselves.

    _TODO: summarize https://godoc.org/gioui.org/io/pointer#InputOp_
    This handling is achieved by widgets adding an operation to the op.Ops with using some “persistent key” and later rereading using the same key. For example pointer input looks like this:

    ``` go
    r := image.Rectangle{...}
    pointer.Rect(r).Add(ops)
    pointer.InputOp{Key: h}.Add(ops)
    ```

    This will tell the windowing system to collate inputs on rectangle r with key `k`.

    On the next frame the widget can use `h` to get the appropriate event:

    ``` go
    select {
    case e := <-window.Events():
    switch e := e.(type) {
    case system.DestroyEvent:
    return e.Err
    case system.FrameEvent:
    for _, ev := range e.Queue(h) {
    // handle event for that widget
    }
    }
    }
    ```

    It can be convenient to use widgets pointer as the key `h`, however, as long as they don’t conflict with other potential keys, there wouldn’t be any problem using them.

    For more details take a look at https://godoc.org/gioui.org/io/pointer.

    Input events are handled in a similar way, however with https://godoc.org/gioui.org/io/key

    ## Rendering

    Since the system needs to tell different graphics API-s, there's an abstraction of [op.Ops](https://gioui.org/op#Ops).
    This records different operations in some specific format that the graphics API can decode.
    To tell the graphics API what to draw, Gio uses [op.Ops](https://gioui.org/op#Ops) to serialize drawing commands.

    TODO: explain coordinate system
    Coordinates are based on the top-left corner. This means `f32.Point{X:0, Y:0}` is the top left corner.

    As an example encoding a colored rectangle into that structure would look like:
    Gio uses structs of different operations to encode data into `op.Ops`. For example, encoding a colored rectangle looks like:

    ```
    ``` go
    var ops op.Ops

    ColorOp{Color: color.RGBA{R: 0x80, G: 0x00, B: 0x00, A: 0xFF}}.Add(&ops)
    @@ -150,9 +193,165 @@ PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:10}}}.Add(&

    [gioui.org/gpu](https://gioui.org/gpu) is able to decode the resulting `ops.Bytes()` and handle these operations for different APIs.

    There are also operations such as [op.TransformOp](https://gioui.org/op#TransformOp) that allow to offset all the following rendering. And [clip.Rect](https://gioui.org/op/clip#Rect) to prevent drawing outside of some boundary.
    ### Transformation

    Operation [op.TransformOp](https://gioui.org/op#TransformOp) allows us to offset, rotate, scale the rendering operations that come after it.

    For example, the following would render 10 units to the right compared to the previous example:

    ``` go
    var ops op.Ops

    TransformOp{}.Offset(f32.Point{X: 10, Y: 0}).Add(&ops)
    ColorOp{Color: color.RGBA{R: 0x80, G: 0x00, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:10}}}.Add(&ops)
    ```

    ### Clipping

    In some cases we want the rendering to be clipped to some smaller rectangle to avoid accidental drawing over other things.

    Package [gioui.org/op/clip](https://gioui.org/op/clip), provides exactly that.

    [clip.Rect](https://gioui.org/op/clip#Rect) allows to clip all the rendering to a particular rounded rectangle.

    This could be used as a basis for a button background:

    ``` go
    var ops op.Ops

    r := 3 // roundness
    bounds := f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:100, Y:20}}
    clip.Rect{Rect: bounds, SE: r, SW: r, NW: r, NE: r}.Add(&ops)
    ColorOp{Color: color.RGBA{R: 0x80, G: 0x00, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: bounds}.Add(&ops)
    ```

    ### Push and Pop

    For drawing we want to also undo any transformation, clipping or color settings that we set before. We can use [op.StackOp](https://gioui.org/op#StackOp) to do push and pop the state:

    ``` go
    ops := new(op.Ops)
    var stack op.StackOp
    stack.Push(ops)

    // Apply a transform to subsequent operations.
    op.TransformOp{}.Offset(...).Add(ops)
    ...

    // Restore the previous transform.
    stack.Pop()
    ```

    ### Drawing Order and Macros

    Drawing happens from back to front. In this example the green rectangle is drawn on top of red rectangle:

    ``` go
    var ops op.Ops

    // red rectangle
    ColorOp{Color: color.RGBA{R: 0xFF, G: 0x00, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:100, Y:10}}}.Add(&ops)

    // green rectangle
    ColorOp{Color: color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:100}}}.Add(&ops)
    ```

    Sometimes you may want to change this order. For example you may want to switch the order around or you may want to delay rendering to apply a transform that is calculated after the rendering. For this purpose there is [op.MacroOp](https://gioui.org/op#MacroOp)

    ## Low-Level Frame
    var macro op.MacroOp
    macro.Record(ops)
    // Record operations by adding them.
    op.InvalidateOp{}.Add(ops)
    ...
    // End recording.
    macro.Stop()


    ``` go
    var ops op.Ops

    // start recording
    var macro op.MacroOp
    {
    macro.Record(&ops)

    // red rectangle
    ColorOp{Color: color.RGBA{R: 0xFF, G: 0x00, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:100, Y:10}}}.Add(&ops)

    // stop recording
    macro.Stop()
    }

    // green rectangle
    ColorOp{Color: color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:100}}}.Add(&ops)

    // finally draw the recorded red rectangle with a transform
    op.TransformOp{}.Offset(...).Add(ops)
    macro.Add()
    ```

    Also mention macros to make one widget aware of another widget’s dimensions: Draw the widget in a macro, get its dimensions, draw the second widget, and then Add() the macro.

    ### Animating or re-triggering a full render

    When you are animating something you may need to retrigger rendering immediately rather than wait for events. For that there is [op.InvalidateOp](https://gioui.org/op#InvalidateOp)

    ```
    // declared outside
    var progress float32
    // animating
    if progress < 1 {
    progress += 0.1
    op.InvalidateOp{}.Add(ops) // since we haven’t yet finished drawing
    } else {
    progress = 1
    }
    ColorOp{Color: color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:100*progress}}}.Add(&ops)
    ```

    Note, in most cases you should also take into account time-delta from previous frame.

    ### Caching Rendering

    If you wish to cache a rendering to redraw a frame later, you can use op.Ops and then use op.CallOp to render it. This can become useful to animate away a widget that has been removed:

    ``` go
    var ops op.Ops

    var cache op.Ops
    ColorOp{Color: color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:100*progress}}}.Add(&cache)

    op.CallOp{Ops: &cache}.Add(&ops)
    ```

    ### Images

    [paint.ImageOp](https://gioui.org/op/paint#ImageOp) can be used to draw images. It is used similarly to color handling:

    ``` go
    var ops op.Ops

    imageOp := paint.NewImageOp(srcimage)

    imageOp.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:10}}}.Add(&ops)
    ```

    Clip and Transform can be used to stretch or rotate the image.

    Note, the image must not be mutated until another FrameEvent happens, because some of the work with the image is asynchronous.

    ## Putting all the low-level pieces together

    A single frame consists of getting input and rendering the new state:

    @@ -186,36 +385,76 @@ for {
    }
    ```

    Of course writing a program in these terms would be really annoying.
    Writing a program using these concepts could get really annoying, however these low-level pieces are intended for writing Widgets themselves. Most programs end up using widgets rather than the low-level pieces.

    ## Widget

    We’ve been mentioning widgets quite a while now. In principle widgets are composable and renderable 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 render themselves to `op.Ops`

    By convention they have:
    method called `Layout(gtx *layout.Context, ...)` to render themselves,
    method called `Update(gtx *layout.Context, ...)` to update themselves.

    This gives a separation between “widget state” and “widget style and rendering”. The state is very often reusable. The common widget states are in [gioui.org/widget](https://gioui.org/widget). Code that combines state with style is in [gioui.org/widget/material](https://gioui.org/widget/material).

    ## Context

    To simplify writing code, there's a structure that can be passed around operations and the available screen space.
    This is called [layout.Context](https://gioui.org/layout#Context).
    To build out more complex UI from these primitives we need more structure and describe the layout in a composable way.

    Static layouts are a thing of the past and we need to calculate how big things can be, i.e. constrain their size and then figure out 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. It would be really inconvenient to pass them separately.

    [layout.Context](https://gioui.org/layout#Context) is what carries the state that is needed by almost all layouts and widgets. Summarizing:

    Constraints - says how large a widget should be.
    Dimensions - is used for tracking the most recent layout size.
    Ops - for communicating with the windowing system.
    Events - to get events associated with some handle
    Now - to get the current time

    It contains the constraints on how much screen is available.

    _TODO: explain constraints struct in depth_
    ## A Button From Scratch

    As an example, here is how to write a very simple button.

    ## Widget
    First let’s write draw our button:

    Describe how widgets should interact with
    ``` go
    type Button struct {
    pressed bool
    }

    gtx.Constraints
    gtx.Dimensions
    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)
    }

    ### Example: Button From Scratch
    func drawSquare(ops *op.Ops, color color.RGBA) {
    square := f32.Rectangle{
    Max: f32.Point{X: 500, Y: 500},
    }
    paint.ColorOp{Color: color}.Add(ops)
    paint.PaintOp{Rect: square}.Add(ops)
    }
    ```

    TODO: make it a walkthrough
    We now also need to handle the input:

    ``` go
    type Button struct {
    pressed bool
    }

    func (b *Button) Layout(gtx *layout.Context) {
    // TODO: explain gtx.Events(b)
    // 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 {
    @@ -227,78 +466,370 @@ func (b *Button) Layout(gtx *layout.Context) {
    }
    }

    col := color.RGBA{A: 0xff, R: 0xff}
    if b.pressed {
    col = color.RGBA{A: 0xff, G: 0xff}
    }
    // register rectangle that receives
    pointer.Rect(
    image.Rectangle{Max: image.Point{X: 500, Y: 500}},
    ).Add(gtx.Ops)

    // TODO: explain pointer.InputOp{Key: b}
    pointer.InputOp{Key: b}.Add(gtx.Ops)
    drawSquare(gtx.Ops, col)
    }

    func drawSquare(ops *op.Ops, color color.RGBA) {
    square := f32.Rectangle{
    Max: f32.Point{X: 500, Y: 500},
    // draw the button
    col := color.RGBA{A: 0xff, R: 0xff}
    if b.pressed {
    col = color.RGBA{A: 0xff, G: 0xff}
    }
    paint.ColorOp{Color: color}.Add(ops)
    paint.PaintOp{Rect: square}.Add(ops)
    drawSquare(gtx.Ops, col)
    }
    ```

    ## Layouting

    TODO: Describe how layouting roughly works
    To create a layout for widgets there are special functions and structures to manipulate layout.Context. The common variants are conveniently in [layout](https://gioui.org/layout).

    Layouting in general happens as the following:

    push state
    set layout.Context.Constraints
    set op.TransformOp
    call widget.Layout(gtx, ...)
    pop state

    For more complicated layout approaches it needs to be combined with op.MacroOp, such as in [flex](https://godoc.org/gioui.org/layout#Flex):

    record widget rendering using MacroOp
    calculate sizes for non-rigid widgets
    draw widgets based on the calculated sizes using the macros

    ### Example: Split View Widget

    As an example, to split the screen into two you could write a widget that looks like:

    ``` go
    type SplitView struct {
    Ratio float32
    type Split struct {
    }

    func (splitView *SplitView) Layout(gtx *layout.Context, left, right layout.Widget) {
    var stack op.StackOp
    func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    savedConstraints := gtx.Constraints
    defer func() { gtx.Constraints = savedConstraints }()

    stack.Push(gtx.Ops)
    gtx.Constraints = // TODO: constrain the `left` rendering to the left side
    left()
    stack.Pop()
    leftsize := gtx.Constraints.Width.Min / 2
    rightsize := gtx.Constraints.Width.Min - leftsize

    stack.Push(gtx.Ops)
    gtx.Constraints = // TODO: constrain the `right` rendering to the right side
    op.TransformOp{}.Offset(offset).Add(gtx.Ops)
    right()
    stack.Pop()
    {
    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()
    }
    }
    ```

    Of course, you do not need to implement such layouting yourself, there are plenty of them available in [layout](https://gioui.org/layout)
    The usage code would look like:

    ``` go
    split.Layout(gtx, func() {
    // render the left side
    }, func() {
    // render the right side
    })
    ```

    Of course, you do not need to implement such layouting yourself, there are plenty of them available in [layout](https://gioui.org/layout).

    ### Example: Split View Widget Input

    _TODO: Extend the above with ability to change ratio by dragging_
    To make it more useful we could make the split draggable.

    First let’s make ratio adjustable. We should try to make zero values useful, in this case `0` could mean that it’s split in the center.

    ``` go
    type Split struct {
    // Ratio keeps the current layout.
    // 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 }()

    proportion := (s.Ratio + 1) / 2
    leftsize := int(proportion*float32(gtx.Constraints.Width.Min))

    rightoffset := leftsize
    rightsize := gtx.Constraints.Width.Min - 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 int
    }
    ## Text
    const defaultBarWidth = 4
    TODO: describe how shaper works
    func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    savedConstraints := gtx.Constraints
    defer func() { gtx.Constraints = savedConstraints }()
    bar := s.Bar
    if bar <= 0 {
    bar = defaultBarWidth
    }
    proportion := (s.Ratio + 1) / 2
    leftsize := int(proportion*float32(gtx.Constraints.Width.Min) - float32(bar))
    rightoffset := leftsize + bar
    rightsize := gtx.Constraints.Width.Min - 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:

    ``` go
    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 int

    // 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.Min)
    s.Ratio += deltaRatio

    case pointer.Release:
    fallthrough
    case pointer.Cancel:
    // finish dragging
    if !s.drag || s.dragID != e.PointerID {
    break
    }
    s.drag = false
    }
    }


    // Register input
    barRect := image.Rect(leftsize, 0, rightoffset, gtx.Constraints.Width.Max)
    // Register bar rectangle for input.
    pointer.Rect(barRect).Add(gtx.Ops)
    // Grab tells the input system to ensure this widget gets priority.
    pointer.InputOp{Key: s, Grab: s.drag}.Add(gtx.Ops)
    ```

    Putting the whole Layout function together, it will look like:

    ``` go
    func (s *Split) Layout(gtx *layout.Context, left, right layout.Widget) {
    savedConstraints := gtx.Constraints
    defer func() { gtx.Constraints = savedConstraints }()

    bar := s.Bar
    if bar <= 0 {
    bar = defaultBarWidth
    }

    proportion := (s.Ratio + 1) / 2
    leftsize := int(proportion*float32(gtx.Constraints.Width.Min) - float32(bar))

    rightoffset := leftsize + bar
    rightsize := gtx.Constraints.Width.Min - 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.Min)
    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{Key: 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()
    }
    }
    ```

    Of course, we might need some additional checks to avoid Ratio values like `-5`, but that’s tiny improvements.

    ## Themes

    Since many widgets need different colors, it's useful to place all the relevant colors into a single struct [Theme](https://gioui.org/widget/material#Theme). It contains the relevant settings for a Material design based UI.

    That [gioui.org/widget/material](https://gioui.org/widget/material) package also contains widgets that are based on [Design](https://material.io/design).
    Since also there needs to be custom code per style, it also contains widgets based on the [Material Design Components](https://material.io/design).

    ## Text

    TODO: describe how shaper works

    ## Units

    TODO: describe how units are handled

  4. egonelbre revised this gist May 14, 2020. 1 changed file with 65 additions and 6 deletions.
    71 changes: 65 additions & 6 deletions architecture.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    https://about.sourcegraph.com/go/gophercon-2019-simple-portable-efficient-graphical-interfaces-in-go

    ---
    title: Architecture
    ---
    @@ -6,7 +8,7 @@ title: Architecture

    Gio is based on the concept of Immediate Mode UI.
    This approach can be implemented in multiple ways,
    however the over-arching similarity is that the program:
    however the overarching similarity is that the program:

    1. listens for events such as mouse or keyboard input,
    2. updates it's internal based on the event (e.g. sets a Checked = true for checkbox),
    @@ -79,9 +81,10 @@ This simplicity of course contains a lot of different trade-offs that can be mad
    6. how do you communicate with the graphics card,
    7. how do you communicate with the operating system,
    8. how do you render text,
    8. ...
    9. should it be completely stateless widgets or some can be staful,
    10. ... and many more.

    The rest of the document tries to answer these questions.
    The rest of the document tries to answer how Gio does it.

    Immediate Mode References:

    @@ -110,7 +113,7 @@ for {
    ```

    `app.NewWindow` chooses the appropriate " handling driver" depending on the OS.
    It might choose Wayland, WinAPI or Cococa and many others.
    It might choose Wayland, WinAPI or Cocoa and many others.

    It then wires together events coming from the OS to `windows.Events()`.

    @@ -127,11 +130,15 @@ There are also event-processors, such as [`gioui.org/gesture`](https://gioui.org

    _TODO: describe how they work._

    _TODO: summarize https://godoc.org/gioui.org/io/pointer#InputOp_

    ## Rendering

    Since the system needs to tell different graphics API-s, there's an abstraction of [op.Ops](https://gioui.org/op#Ops).
    This records different operations in some specific format that the graphics API can decode.

    TODO: explain coordinate system

    As an example encoding a colored rectangle into that structure would look like:

    ```
    @@ -186,14 +193,65 @@ Of course writing a program in these terms would be really annoying.
    To simplify writing code, there's a structure that can be passed around operations and the available screen space.
    This is called [layout.Context](https://gioui.org/layout#Context).

    It contains the constraints how much screen is available and where to draw things.
    It contains the constraints on how much screen is available.

    _TODO: explain constraints struct in depth_


    ## Widget

    TODO: Describe how a simple button looks like.
    Describe how widgets should interact with

    gtx.Constraints
    gtx.Dimensions

    ### Example: Button From Scratch

    TODO: make it a walkthrough

    ``` go
    type Button struct {
    pressed bool
    }

    func (b *Button) Layout(gtx *layout.Context) {
    // TODO: explain gtx.Events(b)
    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
    }
    }
    }

    col := color.RGBA{A: 0xff, R: 0xff}
    if b.pressed {
    col = color.RGBA{A: 0xff, G: 0xff}
    }
    pointer.Rect(
    image.Rectangle{Max: image.Point{X: 500, Y: 500}},
    ).Add(gtx.Ops)

    // TODO: explain pointer.InputOp{Key: b}
    pointer.InputOp{Key: b}.Add(gtx.Ops)
    drawSquare(gtx.Ops, col)
    }

    func drawSquare(ops *op.Ops, color color.RGBA) {
    square := f32.Rectangle{
    Max: f32.Point{X: 500, Y: 500},
    }
    paint.ColorOp{Color: color}.Add(ops)
    paint.PaintOp{Rect: square}.Add(ops)
    }
    ```

    ## Layouting

    TODO: Describe how layouting roughly works

    ### Example: Split View Widget

    @@ -243,3 +301,4 @@ That [gioui.org/widget/material](https://gioui.org/widget/material) package also
    ## Units

    TODO: describe how units are handled

  5. egonelbre created this gist May 14, 2020.
    245 changes: 245 additions & 0 deletions architecture.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,245 @@
    ---
    title: Architecture
    ---

    ## Immediate Mode UI

    Gio is based on the concept of Immediate Mode UI.
    This approach can be implemented in multiple ways,
    however the over-arching similarity is that the program:

    1. listens for events such as mouse or keyboard input,
    2. updates it's internal based on the event (e.g. sets a Checked = true for checkbox),
    3. runs code that re-renders the whole state.

    In pseudo-code a minimal immediate mode UI can look like:

    ``` go
    // state of the program
    var showlist bool
    var items []string

    for {
    // waiting 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:

    1. get an event
    2. handle the widgets while updating the state and drawing the widgets

    This simplicity of course contains a lot of different trade-offs that can be made:

    1. how do you get the events,
    2. when do you re-render the state,
    3. what do the widget structures look like,
    4. how do you track the focus,
    5. how do you structure the events,
    6. how do you communicate with the graphics card,
    7. how do you communicate with the operating system,
    8. how do you render text,
    8. ...

    The rest of the document tries to answer these questions.

    Immediate Mode References:

    * http://sol.gfxile.net/imgui/
    * https://caseymuratori.com/blog_0001
    * http://www.johno.se/book/imgui.html
    * https://github.com/ocornut/imgui

    ## Window

    Since a GUI library needs to talk to some sort of operating system to display information:

    ``` go
    window := app.NewWindow(app.Size(unit.Dp(800), unit.Dp(650)))
    for {
    select {
    case e := <-window.Events():
    switch e := e.(type) {
    case system.DestroyEvent:
    return e.Err
    case system.FrameEvent:
    // update state based on events in e.Queue
    }
    }
    }
    ```

    `app.NewWindow` chooses the appropriate " handling driver" depending on the OS.
    It might choose Wayland, WinAPI or Cococa and many others.

    It then wires together events coming from the OS to `windows.Events()`.

    Additionally, it will initialize communication with the GPU, e.g. OpenGL, EGL or DirectX 11.

    ## Input

    Input is delivered to the widgets via a [`system.FrameEvent`](https://gioui.org/io/system#FrameEvent) which contains `Queue`.
    It might contain a keyboard event, such as [`key.Event`](https://gioui.org/io/key#Event).

    Based on these events widgets can modify the state.

    There are also event-processors, such as [`gioui.org/gesture`](https://gioui.org/gesture), that detects higher-level actions such as a "double-click" from individual click events.

    _TODO: describe how they work._

    ## Rendering

    Since the system needs to tell different graphics API-s, there's an abstraction of [op.Ops](https://gioui.org/op#Ops).
    This records different operations in some specific format that the graphics API can decode.

    As an example encoding a colored rectangle into that structure would look like:

    ```
    var ops op.Ops
    ColorOp{Color: color.RGBA{R: 0x80, G: 0x00, B: 0x00, A: 0xFF}}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{Min: f32.Point{}, Max: f32.Point{X:10, Y:10}}}.Add(&ops)
    ```

    [gioui.org/gpu](https://gioui.org/gpu) is able to decode the resulting `ops.Bytes()` and handle these operations for different APIs.

    There are also operations such as [op.TransformOp](https://gioui.org/op#TransformOp) that allow to offset all the following rendering. And [clip.Rect](https://gioui.org/op/clip#Rect) to prevent drawing outside of some boundary.

    ## Low-Level Frame

    A single frame consists of getting input and rendering the new state:

    ``` go
    red := color.RGBA{R: 0xFF, G: 0x00, B: 0x00, A: 0xFF}
    blue := color.RGBA{R: 0x00, G: 0xFF, B: 0x00, A: 0xFF}

    for {
    e := <-w.Events()
    switch e := e.(type) {
    case system.DestroyEvent:
    return e.Err
    case system.FrameEvent:
    var ops op.Ops
    ops.Reset()

    color := red
    // TODO: show how event.Queue works
    if e.Queue contains space {
    color = blue
    }

    ColorOp{Color: color}.Add(&ops)
    PaintOp{Rect: f32.Rectangle{
    Min: f32.Point{},
    Max: f32.Point{X:10, Y:10},
    }}.Add(&ops)

    e.Frame(gtx.Ops)
    }
    }
    ```

    Of course writing a program in these terms would be really annoying.

    ## Context

    To simplify writing code, there's a structure that can be passed around operations and the available screen space.
    This is called [layout.Context](https://gioui.org/layout#Context).

    It contains the constraints how much screen is available and where to draw things.

    _TODO: explain constraints struct in depth_


    ## Widget

    TODO: Describe how a simple button looks like.

    ### Example: Split View Widget

    As an example, to split the screen into two you could write a widget that looks like:

    ``` go
    type SplitView struct {
    Ratio float32
    }

    func (splitView *SplitView) Layout(gtx *layout.Context, left, right layout.Widget) {
    var stack op.StackOp

    stack.Push(gtx.Ops)
    gtx.Constraints = // TODO: constrain the `left` rendering to the left side
    left()
    stack.Pop()

    stack.Push(gtx.Ops)
    gtx.Constraints = // TODO: constrain the `right` rendering to the right side
    op.TransformOp{}.Offset(offset).Add(gtx.Ops)
    right()
    stack.Pop()
    }
    ```

    Of course, you do not need to implement such layouting yourself, there are plenty of them available in [layout](https://gioui.org/layout)

    ### Example: Split View Widget Input

    _TODO: Extend the above with ability to change ratio by dragging_

    ``` go

    ```

    ## Text

    TODO: describe how shaper works

    ## Themes

    Since many widgets need different colors, it's useful to place all the relevant colors into a single struct [Theme](https://gioui.org/widget/material#Theme). It contains the relevant settings for a Material design based UI.

    That [gioui.org/widget/material](https://gioui.org/widget/material) package also contains widgets that are based on [Design](https://material.io/design).

    ## Units

    TODO: describe how units are handled