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()methodpkg/gel/button.go: All sizing/inset codepkg/gel/label.go: Text sizingpkg/gel/inset.go: All inset dimensionspkg/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 usesop.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 looppkg/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)
-
Backup existing code
- Create a backup branch of current state
- Document any local modifications to vendored GIO
-
Replace vendored GIO
- Remove
pkg/gel/gio/entirely - Clone latest GIO into
pkg/gel/gio/ - Update import paths if module path differs
- Remove
-
Update go.mod
- Ensure Go version compatibility (1.21+ recommended)
- Add any new dependencies GIO requires
Phase 2: Core Type Adaptations
-
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.TextSizefromunit.Valuetounit.Sp - Update all
unit.Valueusages to typedunit.Dp/unit.Sp
Files to modify:
theme.gowindow.gobutton.golabel.goinset.goflex.golist.goeditor.goslider.go- And all other widget files
- Create compatibility layer if needed:
-
Float32 to image.Point conversions
- Update all
f32.Point→image.Pointwhere required - Update all
f32.Rectangle→image.Rectanglein clips
Files to modify:
button.go(drawInk function)clickable.go- All clipping operations
- Update all
Phase 3: Operation System Refactoring
-
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()
- Replace
-
Transform operations
- Update
op.Offset(f32.Point)→op.Offset(image.Point) - Use
.Push()instead of.Add()for stack-based transforms
- Update
-
InvalidateOp
- Replace
op.InvalidateOp{}.Add(ops)with command pattern:gtx.Execute(op.InvalidateCmd{At: time.Time{}})
- Replace
Phase 4: Event System Refactoring
-
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 }
- Replace
-
Update gesture handling
- Refactor
Clickablewidget - Refactor
Listscroll handling - Refactor
Editorinput handling - Refactor
Sliderdrag handling
- Refactor
Phase 5: Window/App Refactoring
-
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) } } -
Remove channel-based event reading
- The new GIO uses blocking
Event()not channel
- The new GIO uses blocking
-
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):
-
Foundation widgets:
theme.go- Core themingcolors.go- Color systemwindow.go- Window managementinset.go- Inset layout
-
Layout widgets:
flex.go- Flex layoutstack.go- Stack layoutlist.go- List widget
-
Interactive widgets:
clickable.go- Click handlingbutton.go- Buttonscheckbox.go- Checkboxesswitch.go- Toggle switches
-
Text widgets:
label.go- Labelseditor.go- Text editorinput.go- Text inputpassword.go- Password input
-
Complex widgets:
slider.go- Sliderstable.go- Tablesapp.go- App framework
Phase 7: Testing and Validation
-
Create test harness
- Unit tests for each widget
- Visual regression tests
- Example applications from
pkg/gel/cmd/
-
Fix compilation errors
- Work through errors systematically
- Document any behavioral changes
-
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
-
Consider a hybrid approach: If timeline is tight, consider updating GIO incrementally while maintaining backward compatibility shims
-
Check GIO examples: The latest GIO repository has example code that demonstrates current patterns
-
Join GIO community: The GIO mailing list and IRC can help with migration questions
-
Document local changes: Any customizations to gel widgets should be documented to ensure they're preserved
-
Incremental migration: Consider migrating one widget at a time, testing each before moving to the next