Skip to content

Graphics

mike-ward edited this page Jun 14, 2026 · 2 revisions

Graphics

Go-gui exposes two paths to custom rendering: DrawCanvas for immediate-mode procedural 2D drawing, and a set of container-level visual effects (gradients, shadows, blur, color filters, custom GPU shaders). The SVG widget sits between the two — it is a widget like any other, but it renders full vector assets through a built-in pipeline.


DrawCanvas

gui.DrawCanvas is a fixed-size drawing surface with a func(*DrawContext) callback that fires every frame the canvas is visible. The framework tessellates geometry on the CPU and uploads batched vertex buffers to the GPU.

type DrawCanvasCfg struct {
    ID      string
    Version uint64              // increment to force a redraw
    Width   float32
    Height  float32
    Color   gui.Color           // background fill
    Radius  float32             // corner radius
    Clip    bool                // clip children to rounded rect
    Sizing  gui.SizingMode
    IDFocus uint32
    OnDraw  func(dc *gui.DrawContext)
    // ... event callbacks (OnClick, OnKeyDown, OnGesture, …)
}

Version is a cache key. When Version is unchanged from the previous frame the framework reuses the cached vertex buffer. Increment it whenever the drawing data changes:

gui.DrawCanvas(gui.DrawCanvasCfg{
    ID:      "my-canvas",
    Version: uint64(app.Tick),      // drives redraw from state
    Width:   480,
    Height:  300,
    Color:   gui.RGBA(20, 20, 30, 255),
    Radius:  8,
    Clip:    true,
    OnDraw: func(dc *gui.DrawContext) {
        // draw crosshair guides
        guide := gui.RGBA(80, 80, 100, 255)
        dc.DashedLine(0, app.MarkerY, dc.Width, app.MarkerY, guide, 1, 6, 4)
        dc.DashedLine(app.MarkerX, 0, app.MarkerX, dc.Height, guide, 1, 6, 4)

        // draw marker
        dc.FilledCircle(app.MarkerX, app.MarkerY, 10, gui.RGBA(255, 200, 80, 255))
        dc.Circle(app.MarkerX, app.MarkerY, 10, gui.White, 1.5)
    },
})

The DrawContext also exposes dc.Width and dc.Height — the canvas dimensions after padding — so drawing code doesn't need to hard-code sizes.


DrawContext methods

All coordinates are in canvas-local pixels (origin = top-left of the padded area).

Method Description
FilledRect(x, y, w, h, color) Solid filled rectangle
Line(x0, y0, x1, y1, color, width) Single stroked line segment
Polyline(points, color, width) Stroked open path; points is []float32{x0,y0,x1,y1,...}
FilledPolygon(points, color) Filled closed polygon
Circle(cx, cy, r, color, width) Stroked circle
FilledCircle(cx, cy, r, color) Solid filled circle
Arc(cx, cy, rx, ry, start, sweep, color, width) Stroked elliptic arc; angles in radians
FilledArc(cx, cy, rx, ry, start, sweep, color) Filled arc sector
CubicBezier(x0,y0, c1x,c1y, c2x,c2y, x1,y1, color, width) Stroked cubic Bézier
DashedLine(x0, y0, x1, y1, color, width, dashLen, gapLen) Dashed line segment
Text(x, y, text, style) Text at top-left (x, y) using gui.TextStyle
TextWidth(text, style) float32 Measured text width
FontHeight(style) float32 Line height for the style
Image(x, y, w, h, src, bgOpacity, bgColor) Stretch image to rect; src is a file path

SVG

gui.Svg renders an SVG asset into a fixed-size widget. The framework parses SVG, applies CSS cascading, tessellates curves, and uploads to the GPU — all without a browser engine.

gui.Svg(gui.SvgCfg{
    ID:     "logo",
    Width:  100,
    Height: 100,
    Sizing: gui.FixedFixed,
    SvgData: `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <circle cx="50" cy="50" r="40" fill="#3b82f6" opacity="0.8"/>
  <circle cx="50" cy="50" r="10" fill="#ec4899"/>
</svg>`,
})

Authoring tips:

  • Use fill="currentColor" so the widget's Color config tints monochrome assets at render time.
  • Omit width/height from the root element and rely on viewBox — the layout decides the display size.
  • CSS @keyframes animations, var() custom properties, and calc() are all supported.

The SVG pipeline is documented in the source at gui/svg/. The widget is also used by gui.SvgSpinner for the 106 built-in spinner animations.


Gradients

Set Gradient (or BorderGradient) on any ContainerCfg. The framework renders content into an FBO and composites the gradient over it.

gui.Column(gui.ContainerCfg{
    Width:  200,
    Height: 80,
    Sizing: gui.FixedFixed,
    Radius: gui.SomeF(8),
    Gradient: &gui.GradientDef{
        Direction: gui.GradientToRight,
        Stops: []gui.GradientStop{
            {Pos: 0, Color: gui.ColorFromString("#3b82f6")},
            {Pos: 1, Color: gui.ColorFromString("#8b5cf6")},
        },
    },
    Content: headerViews,
})

Linear gradient directions:

Constant Direction
GradientToRight Left → right
GradientToLeft Right → left
GradientToBottom Top → bottom
GradientToTop Bottom → top
GradientToTopRight Diagonal
GradientToBottomRight Diagonal
GradientToBottomLeft Diagonal
GradientToTopLeft Diagonal

For a radial gradient, set Type: gui.GradientRadial (direction is ignored):

Gradient: &gui.GradientDef{
    Type: gui.GradientRadial,
    Stops: []gui.GradientStop{
        {Pos: 0, Color: gui.RGBA(255, 200, 80, 255)},
        {Pos: 1, Color: gui.RGBA(30, 30, 40, 0)},
    },
},

BorderGradient works the same way but applies to the border stroke instead of the fill.


Box shadows

Set Shadow on any container to paint a drop shadow behind it.

gui.Column(gui.ContainerCfg{
    Width:  200,
    Height: 80,
    Sizing: gui.FixedFixed,
    Color:  t.ColorBackground,
    Radius: gui.SomeF(10),
    Shadow: &gui.BoxShadow{
        Color:      gui.RGBA(0, 0, 0, 55),
        OffsetX:    0,
        OffsetY:    10,
        BlurRadius: 22,
    },
    Content: cardViews,
})

The shadow renders outside the container's bounds; no extra padding is needed in the parent. Colored shadows (e.g. RGBA(80, 120, 255, 85)) produce glow effects.


Gaussian blur

BlurRadius float32 on ContainerCfg renders the container's content into an off-screen buffer and applies a Gaussian blur before compositing. Used for frosted-glass backgrounds and bloom glows:

gui.Column(gui.ContainerCfg{
    Width:       100,
    Height:      100,
    Sizing:      gui.FixedFixed,
    Color:       gui.RGBA(0, 255, 128, 255),
    BlurRadius:  15,
    ColorFilter: gui.ColorFilterBrightness(1.3),
    Radius:      gui.SomeF(50),
    Content:     []gui.View{label},
})

Combining BlurRadius with ColorFilterBrightness replicates a bloom glow.


Color filters

ColorFilter *ColorFilter on ContainerCfg applies a 4×4 color matrix transform as a post-processing pass on the container's rendered content.

Constructor Effect
gui.ColorFilterGrayscale() Desaturate to greyscale
gui.ColorFilterSepia() Warm sepia tone
gui.ColorFilterContrast(v) Scale contrast; 1.0 = unchanged
gui.ColorFilterBrightness(v) Scale brightness; 1.0 = unchanged
gui.Column(gui.ContainerCfg{
    Width:       120,
    Sizing:      gui.FixedFit,
    Color:       t.ColorPanel,
    ColorFilter: gui.ColorFilterGrayscale(),
    Content:     photoViews,
})

Multiple effects can be combined by layering containers — an outer blur with an inner contrast filter, for instance.


Custom shaders

Shader *Shader on ContainerCfg replaces the backend's default fragment shader for that container. Provide both Metal (macOS/iOS) and GLSL (other backends) source strings. Pass uniform values via Params []float32.

The shader receives these built-in inputs:

  • uv — normalised texture coordinates (0,0 to 1,1)
  • p0p3Params[0..15] packed as four vec4s
  • tex — the container's rendered content before the shader

Use gui.BuildGLSLFragment(body string) string to wrap a GLSL body with the standard preamble (version, uniforms, coordinate unpacking):

shader := &gui.Shader{
    GLSL: gui.BuildGLSLFragment(`
        float t = p0.x;
        vec2 st = uv * 0.5 + 0.5;
        vec3 c = 0.5 + 0.5 * cos(t + st.xyx + vec3(0, 2, 4));
        frag_color = vec4(c, 1.0);
    `),
    Metal: `
        float t = in.p0.x;
        float2 st = in.uv * 0.5 + 0.5;
        float3 c = 0.5 + 0.5 * cos(t + st.xyx + float3(0, 2, 4));
        frag_color = float4(c, 1.0);
    `,
    Params: []float32{elapsed},
}

elapsed := float32(time.Since(startTime).Milliseconds()) / 1000.0

gui.Column(gui.ContainerCfg{
    Width:  200,
    Height: 200,
    Sizing: gui.FixedFixed,
    Radius: gui.SomeF(16),
    Shader:  shader,
})

To keep elapsed updating every frame, drive it from a repeating Animate added via w.QueueCommand (see the custom_shader example at examples/custom_shader/).


Markdown extensions

The w.Markdown() widget (see Display Widgets) supports two extensions beyond standard CommonMark.

LaTeX math

Inline $...$ and display $$...$$ math is rendered via the codecogs API. Math expressions are cached by hash — identical equations fetch once. Blocked TeX commands prevent server-side command execution.

w.Markdown(gui.MarkdownCfg{
    Text: "# Heat equation\n\n$$\\frac{\\partial u}{\\partial t} "
        + "= \\alpha \\nabla^2 u$$",
    MathEnabled: true,
})

Mermaid diagrams

Fenced code blocks with mermaid language tag are rendered as PNG diagrams via the Kroki API. Multiple diagrams fetch concurrently (up to 8 at a time, 30 s timeout, 10 MiB response cap). Each diagram renders asynchronously — a placeholder displays while it loads.

w.Markdown(gui.MarkdownCfg{
    Text: "```mermaid\ngraph TD;\n    A-->B;\n    B-->C;\n```",
    MermaidEnabled: true,
})

Enable both for full extended Markdown support with MathEnabled and MermaidEnabled on MarkdownCfg.

Clone this wiki locally