Files
prevara/REFACTORING_PLAN.md
2025-11-30 16:45:22 +00:00

12 KiB

GIO Refactoring Plan for pkg/gel

Executive Summary

The pkg/gel widget toolkit contains a vendored copy of the GIO library that is significantly outdated. This document outlines the breaking changes between the old vendored version and the latest GIO release, and provides a systematic plan for refactoring the gel toolkit to use the current GIO API.

Breaking Changes Analysis

1. Unit Package (unit)

Critical Change: Value type removed, replaced with typed units

Old API New API
unit.Value{V: float32, U: Unit} unit.Dp / unit.Sp (typed floats)
unit.Dp(v float32) Value unit.Dp(v) is now just unit.Dp type
unit.Sp(v float32) Value unit.Sp(v) is now just unit.Sp type
unit.Px(v float32) Value Removed - use raw int for pixels
metric.Px(unit.Value) int metric.Dp(unit.Dp) int / metric.Sp(unit.Sp) int
context.Px(unit.Value) int context.Dp(unit.Dp) int / context.Sp(unit.Sp) int
value.Scale(s float32) Value Manual multiplication: unit.Dp(float32(dp) * s)

Impact: All widget sizing code using unit.Value must be refactored. This affects:

  • pkg/gel/window.go: scaledConfig.Px() method
  • pkg/gel/button.go: All sizing/inset code
  • pkg/gel/label.go: Text sizing
  • pkg/gel/inset.go: All inset dimensions
  • pkg/gel/theme.go: TextSize field
  • All widget files using dimensions

2. Layout Package (layout)

Context changes:

Old API New API
Context.Queue event.Queue Context.Source input.Source (embedded)
Context.Events(tag) []Event source.Event(filters...) (Event, bool)
NewContext(ops, FrameEvent) Removed - construct Context directly
Inset{Top, Right, Bottom, Left unit.Value} Inset{Top, Bottom, Left, Right unit.Dp}
Spacer{Width, Height unit.Value} Spacer{Width, Height unit.Dp}
FRect(r image.Rectangle) f32.Rectangle Removed

Impact:

  • pkg/gel/window.go: l.NewContext() usage
  • All widgets checking events via context

3. Op Package (op)

Critical Change: State management overhauled

Old API New API
op.Save(ops) StateOp op.Offset(pt).Push(ops) TransformStack
stateOp.Load() transformStack.Pop()
op.Offset(f32.Point) TransformOp op.Offset(image.Point) TransformOp
transformOp.Add(ops) transformOp.Push(ops) for stack-based
op.InvalidateOp{}.Add(ops) op.InvalidateCmd{} (via Source.Execute)
ops.Write(n int) []byte Internal only via ops.Write(&o.Internal, ...)
ops.Write1/Write2 Removed from public API
internal/opconst internal/ops

Impact:

  • pkg/gel/button.go: drawInk() function uses op.Save/Load
  • All clipping/transform code
  • Every widget that manages transform state

4. App/Window Package (app)

Critical Change: Event loop redesigned

Old API New API
app.NewWindow(options...) *Window Window{} zero value, lazy init
window.Events() <-chan event.Event window.Event() event.Event (blocking)
system.FrameEvent.Frame(ops) Still exists but context construction changed
system.StageEvent Removed
system.FrameEvent.Queue event.Queue Embedded in Context as Source
Option func(*wm.Options) Option func(unit.Metric, *Config)
app.Size(w, h unit.Value) app.Size(w, h unit.Dp)

Impact:

  • pkg/gel/window.go: Complete rewrite of event loop
  • pkg/gel/window.go: Window.Events() channel-based pattern

5. IO/Event Package (io/event)

Critical Change: Filter-based event system

Old API New API
event.Queue.Events(tag) []Event source.Event(filters...) (Event, bool)
No Filter concept event.Filter interface
event.Tag interface{} event.Tag any

Impact: All event handling code needs filter-based pattern

6. IO/Router Package (io/router)

Critical Change: Renamed and redesigned

Old API New API
io/router package io/input package
router.Router input.Router
router.TextInputState input.TextInputState
Queue(events) returns bool Queue(events...) void

7. Gesture Package (gesture)

Changes to click/scroll handling:

Old API New API
gesture.Click.Events(queue) click.Update(source) ([]gesture.ClickEvent, bool)
gesture.Scroll.Events(...) Filter-based via Source

8. Clip Package (op/clip)

Float to int conversion:

Old API New API
clip.RRect{Rect: f32.Rectangle, ...} clip.RRect{Rect: image.Rectangle, ...}
clip.Rect(f32.Rectangle) clip.Rect(image.Rectangle)
Uses f32.Point for corners Uses int for corner radii

Impact: All clipping code in widgets

9. Pointer Package (io/pointer)

Cursor type change:

Old API New API
pointer.CursorName string pointer.Cursor typed constant

Refactoring Plan

Phase 1: Update Vendored GIO (Preparation)

  1. Backup existing code

    • Create a backup branch of current state
    • Document any local modifications to vendored GIO
  2. Replace vendored GIO

    • Remove pkg/gel/gio/ entirely
    • Clone latest GIO into pkg/gel/gio/
    • Update import paths if module path differs
  3. Update go.mod

    • Ensure Go version compatibility (1.21+ recommended)
    • Add any new dependencies GIO requires

Phase 2: Core Type Adaptations

  1. Unit system migration (pkg/gel/)

    • Create compatibility layer if needed:
      // Temporary bridge during migration
      func DpScale(base unit.Dp, scale float32) unit.Dp {
          return unit.Dp(float32(base) * scale)
      }
      
    • Update Theme.TextSize from unit.Value to unit.Sp
    • Update all unit.Value usages to typed unit.Dp/unit.Sp

    Files to modify:

    • theme.go
    • window.go
    • button.go
    • label.go
    • inset.go
    • flex.go
    • list.go
    • editor.go
    • slider.go
    • And all other widget files
  2. Float32 to image.Point conversions

    • Update all f32.Pointimage.Point where required
    • Update all f32.Rectangleimage.Rectangle in clips

    Files to modify:

    • button.go (drawInk function)
    • clickable.go
    • All clipping operations

Phase 3: Operation System Refactoring

  1. State save/load pattern

    • Replace op.Save(ops).Load() pattern:
      // Old
      defer op.Save(c.Ops).Load()
      
      // New
      defer op.Offset(image.Point{}).Push(c.Ops).Pop()
      // Or for transforms:
      stack := op.Offset(offset).Push(gtx.Ops)
      defer stack.Pop()
      
  2. Transform operations

    • Update op.Offset(f32.Point)op.Offset(image.Point)
    • Use .Push() instead of .Add() for stack-based transforms
  3. InvalidateOp

    • Replace op.InvalidateOp{}.Add(ops) with command pattern:
      gtx.Execute(op.InvalidateCmd{At: time.Time{}})
      

Phase 4: Event System Refactoring

  1. Context event access

    • Replace gtx.Events(tag) with filter-based pattern:
      // Old
      for _, e := range gtx.Events(tag) { ... }
      
      // New
      for {
          e, ok := gtx.Source.Event(pointer.Filter{Target: tag, Kinds: pointer.Press|pointer.Release})
          if !ok { break }
          // handle e
      }
      
  2. Update gesture handling

    • Refactor Clickable widget
    • Refactor List scroll handling
    • Refactor Editor input handling
    • Refactor Slider drag handling

Phase 5: Window/App Refactoring

  1. Event loop redesign (pkg/gel/window.go)

    // Old pattern
    for ev := range w.Window.Events() {
        switch e := ev.(type) { ... }
    }
    
    // New pattern
    for {
        switch e := w.Window.Event().(type) {
        case app.DestroyEvent:
            return e.Err
        case app.FrameEvent:
            gtx := app.NewContext(&ops, e)
            // ... layout ...
            e.Frame(gtx.Ops)
        }
    }
    
  2. Remove channel-based event reading

    • The new GIO uses blocking Event() not channel
  3. Update window options

    • app.Size(unit.Value, unit.Value)app.Size(unit.Dp, unit.Dp)

Phase 6: Widget-by-Widget Updates

Priority order (dependencies first):

  1. Foundation widgets:

    • theme.go - Core theming
    • colors.go - Color system
    • window.go - Window management
    • inset.go - Inset layout
  2. Layout widgets:

    • flex.go - Flex layout
    • stack.go - Stack layout
    • list.go - List widget
  3. Interactive widgets:

    • clickable.go - Click handling
    • button.go - Buttons
    • checkbox.go - Checkboxes
    • switch.go - Toggle switches
  4. Text widgets:

    • label.go - Labels
    • editor.go - Text editor
    • input.go - Text input
    • password.go - Password input
  5. Complex widgets:

    • slider.go - Sliders
    • table.go - Tables
    • app.go - App framework

Phase 7: Testing and Validation

  1. Create test harness

    • Unit tests for each widget
    • Visual regression tests
    • Example applications from pkg/gel/cmd/
  2. Fix compilation errors

    • Work through errors systematically
    • Document any behavioral changes
  3. Runtime testing

    • Test on Linux, macOS, Windows
    • Test on mobile if applicable

Detailed Code Migration Examples

Example 1: Button sizing migration

// OLD
cornerRadius: w.TextSize.Scale(0.25),
inset: &l.Inset{
    Top:    w.TextSize.Scale(0.5),
    Bottom: w.TextSize.Scale(0.5),
    Left:   w.TextSize.Scale(0.5),
    Right:  w.TextSize.Scale(0.5),
},

// NEW
cornerRadius: unit.Dp(float32(w.TextSize) * 0.25),
inset: &l.Inset{
    Top:    unit.Dp(float32(w.TextSize) * 0.5),
    Bottom: unit.Dp(float32(w.TextSize) * 0.5),
    Left:   unit.Dp(float32(w.TextSize) * 0.5),
    Right:  unit.Dp(float32(w.TextSize) * 0.5),
},

Example 2: Transform state migration

// OLD (button.go drawInk)
defer op.Save(c.Ops).Load()
op.Offset(p.Position.Add(f32.Point{X: -rr, Y: -rr})).Add(c.Ops)

// NEW
pos := image.Pt(int(p.Position.X-rr), int(p.Position.Y-rr))
defer op.Offset(pos).Push(c.Ops).Pop()

Example 3: Clip migration

// OLD
clip.RRect{
    Rect: f32.Rectangle{Max: f32.Point{X: size, Y: size}},
    NE: rr, NW: rr, SE: rr, SW: rr,
}.Add(c.Ops)

// NEW
clip.RRect{
    Rect: image.Rect(0, 0, int(size), int(size)),
    NE: int(rr), NW: int(rr), SE: int(rr), SW: int(rr),
}.Push(c.Ops).Pop()

Example 4: Event handling migration

// OLD (clickable.go pattern)
for _, e := range gtx.Events(c) {
    switch e := e.(type) {
    case pointer.Event:
        // handle
    }
}

// NEW
for {
    e, ok := gtx.Source.Event(
        pointer.Filter{Target: c, Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave},
    )
    if !ok {
        break
    }
    switch e := e.(type) {
    case pointer.Event:
        // handle
    }
}

Risk Assessment

Risk Likelihood Impact Mitigation
Subtle behavior changes High Medium Comprehensive testing
Missing API equivalents Medium High Check GIO changelog/examples
Performance regression Low Medium Benchmark critical paths
Platform-specific issues Medium Medium Test on all target platforms

Estimated Effort

Phase Effort Estimate
Phase 1: Preparation 2-4 hours
Phase 2: Core types 4-8 hours
Phase 3: Operations 4-6 hours
Phase 4: Events 6-10 hours
Phase 5: Window/App 4-6 hours
Phase 6: Widgets 8-16 hours
Phase 7: Testing 8-12 hours
Total 36-62 hours

Recommendations

  1. Consider a hybrid approach: If timeline is tight, consider updating GIO incrementally while maintaining backward compatibility shims

  2. Check GIO examples: The latest GIO repository has example code that demonstrates current patterns

  3. Join GIO community: The GIO mailing list and IRC can help with migration questions

  4. Document local changes: Any customizations to gel widgets should be documented to ensure they're preserved

  5. Incremental migration: Consider migrating one widget at a time, testing each before moving to the next