GUI Canvas Custom painting

# Canvas and Custom Painting

`Canvas` is the GUI widget to use when a form widget is not enough: gauges, charts, game surfaces, previews, and visual editors should draw their own content through the typed `Painter` API.

When to Use Canvas

Prefer ordinary widgets and layouts for forms. Use `Canvas` when the UI is mostly pixels and geometry. A canvas participates in layouts like any other widget, but its visual content is produced by overriding `paint()`.

Default behavior

`Canvas` uses an expanding size policy by default and does not take keyboard focus. That makes it a good central drawing area next to compact controls such as sliders, check boxes, and buttons.

Define a Canvas Widget

A custom canvas usually stores a small amount of state, exposes typed setters, calls `repaint()` when state changes, and renders everything from `paint()`.

    import klyn.gui.windows
    import klyn.math

    class SpeedWidget extends Canvas:

        private _speed as Int = 0
        private _maxSpeed as Int = 130
        private _limiter as Boolean = false

        public SpeedWidget():
            super()
            this.preferredWidth = 260
            this.preferredHeight = 260
            this.minWidth = 180
            this.minHeight = 180

        public setSpeed(speed as Int) as Void:
            this._speed = speed
            if this._limiter and this._speed > this._maxSpeed:
                this._speed = this._maxSpeed
            this.repaint()

        public override paint(painter as Painter) as Void:
            if painter is null:
                return

            painter.save()
            painter.antialiasingEnabled = true
            painter.textAntialiasingEnabled = true
            # Draw the widget here.
            painter.restore()

`save()` and `restore()` keep drawing state local to the widget. That matters when several widgets are painted in the same frame.

Painter Basics

`Painter` exposes explicit state: fill style, stroke style, line width, font, and antialiasing flags. Styles can be colors or gradients.

    radius as Double = 0.93 * (Double(minSide) / 2.0)
    centerX as Int = this.x + this.width // 2
    centerY as Int = this.y + this.height // 2

    outerGradient = painter.createLinearGradient(
        0,
        Int(Double(centerY) - radius),
        0,
        Int(Double(centerY) + radius)
    )
    outerGradient.setColorAt(0.0, Color(224, 224, 224, 1.0))
    outerGradient.setColorAt(0.5, Color(110, 119, 116, 1.0))
    outerGradient.setColorAt(0.51, Color(10, 14, 10, 1.0))
    outerGradient.setColorAt(1.0, Color(10, 8, 9, 1.0))

    painter.fillStyle = Color(33, 33, 33, 1.0)
    painter.strokeStyle = outerGradient
    painter.lineWidth = 8.0
    painter.fillEllipse(Int(Double(centerX) - radius), Int(Double(centerY) - radius), Int(radius * 2.0), Int(radius * 2.0))
    painter.drawEllipse(Int(Double(centerX) - radius), Int(Double(centerY) - radius), Int(radius * 2.0), Int(radius * 2.0))

Keep geometry derived from `this.width` and `this.height`. This makes the drawing responsive when the window is resized.

Speedometer Example

The speedometer sample in `samples/gui/SpeedDemoWindow.kn` uses a `Canvas` for the dial and ordinary widgets for interaction. The main layout keeps the canvas expansive and the sliders compact.

    topLayout as HBoxLayout = HBoxLayout()
    top as Container = Container(topLayout)
    top.preferredHeight = 260
    top.layoutParams = LayoutParams(1, 1)
    topLayout.add(speedWidget)
    topLayout.add(speedSlider)

    bottomLayout as HBoxLayout = HBoxLayout(6, "center")
    bottom as Container = Container(bottomLayout)
    bottom.preferredHeight = 36
    bottomLayout.add(limiter)
    bottomLayout.add(maxSpeedSlider)

    speedSlider.valueChanged += lambda(event: ValueChangedEvent): speedWidget.setSpeed(event.value)
    maxSpeedSlider.valueChanged += lambda(event: ValueChangedEvent): speedWidget.setMaxSpeed(event.value)
    limiter.stateChanged += lambda(event: ValueChangedEvent): speedWidget.setLimiter(event.value != 0)

The widget keeps the requested speed separate from the displayed speed. When the limiter is disabled, the gauge can immediately return to the slider value without losing state.

Normal window size. The canvas owns the central drawing area while the sliders stay compact.
Compact window size. Text and tick geometry are recomputed from the available radius.
Clock and Timer Example

`samples/gui/ClockSample.kn` adapts the same custom-painting model to an analog clock. The widget derives all geometry from its current bounds, reads the current time during painting, and uses a GUI `Timer` to request a repaint every second.

    class ClockWidget extends Canvas:

        private _timer as Timer

        public ClockWidget():
            super()
            this.preferredWidth = 360
            this.preferredHeight = 360
            this.minWidth = 220
            this.minHeight = 220
            this._timer = Timer(1000)
            this._timer.timeout += lambda(event: ActionEvent): this.repaint()
            this._timer.start()

        public stop() as Void:
            this._timer.stop()

The timer is emitted by the native GUI event loop, so the callback can safely call `repaint()`. Stop long-lived timers when the owning window closes or after `run()` returns.

Analog clock rendered by `ClockSample.kn`. The dial, labels, gradients, and hands are all drawn through `Painter`.

    klyn samples/gui/ClockSample.kn

Capture the Result

`Window.capture()` can render a GUI window to an image file. The screenshots on this page were produced from the sample itself, then converted to PNG for the website.

    window = SpeedDemoWindow()
    window.capture("speed-demo-window.bmp")

This is useful for documentation and visual regression checks because the image comes from the same painting pipeline used on screen.

Painting Performance

A canvas is repainted often during resize and interactive updates. Keep `paint()` deterministic, avoid unnecessary allocations in tight loops, and only call `repaint()` when widget state actually changes.

- Compute size-dependent geometry from the current widget bounds.
- Enable antialiasing for shapes and text when the visual quality requires it.
- Use gradients and alpha colors through painter styles rather than hand-built pixel buffers.
- Keep application logic outside `paint()`; painting should only render current state.

Next Step

Run `samples/gui/SpeedDemoWindow.kn` and `samples/gui/ClockSample.kn` to inspect full custom painting implementations, or return to Slider Widget for the standard scale control used by the speedometer sample.