Fyne — Interfaces#

Interface catalog#

CanvasObject#

  • Package: fyne.io/fyne/v2
  • File: canvasobject.go
  • Methods:
    MinSize() Size
    Move(Position)
    Position() Position
    Resize(Size)
    Size() Size
    Hide()
    Visible() bool
    Show()
    Refresh()
  • Purpose: The root contract for every visual element. Defines geometry (position, size, min-size) and visibility. Any object that can appear on a canvas must satisfy this interface.
  • Implementations: Every widget and primitive in the toolkit — widget.Button, widget.Entry, canvas.Rectangle, canvas.Image, canvas.Text, and all containers embed internal/widget.Base which provides these methods.
  • Design quality: Well-focused. Nine methods, all tightly related to placement and visibility. Notably does NOT include rendering or styling — those are delegated to WidgetRenderer. Follows ISP cleanly.

Widget#

  • Package: fyne.io/fyne/v2
  • File: widget.go
  • Methods:
    CanvasObject  // embedded
    CreateRenderer() WidgetRenderer
  • Purpose: Distinguishes stateful widgets from primitive canvas objects. The single extension point (CreateRenderer) decouples widget state from the render implementation; the framework calls this once per widget instance and caches the result.
  • Implementations: All ~40 public widgets in widget/: Button, Entry, Label, List, Tree, Table, RichText, TextGrid, etc. All embed internal/widget.Base.
  • Design quality: Minimal by design — one method beyond CanvasObject. The interface is thin because rendering is fully delegated to WidgetRenderer. This is textbook ISP: clients that only need geometry use CanvasObject; clients that need rendering use Widget.

WidgetRenderer#

  • Package: fyne.io/fyne/v2
  • File: widget.go
  • Methods:
    Destroy()
    Layout(Size)
    MinSize() Size
    Objects() []CanvasObject
    Refresh()
  • Purpose: The visual representation of a widget, separated from its state. Renderers are created on demand by Widget.CreateRenderer(), cached by internal/cache (one per widget instance), and destroyed when the widget leaves the scene. This split allows GPU resources to be held only while the widget is visible.
  • Implementations: Every widget provides an unexported renderer struct (e.g., buttonRenderer in widget/button.go). The test package provides WindowlessCanvas and SoftwarePainter for headless rendering.
  • Design quality: Five methods in a cohesive group (layout, draw, cleanup). Objects() returns the children the framework should recurse over, so the renderer fully controls what gets painted without breaking the object tree.

App#

  • Package: fyne.io/fyne/v2
  • File: app.go
  • Methods:
    NewWindow(title string) Window
    OpenURL(url *url.URL) error
    Icon() Resource
    SetIcon(Resource)
    Run()
    Quit()
    Driver() Driver
    UniqueID() string
    SendNotification(*Notification)
    Settings() Settings
    Preferences() Preferences
    Storage() Storage
    Lifecycle() Lifecycle
    Metadata() AppMetadata
    CloudProvider() CloudProvider
    SetCloudProvider(CloudProvider)
    Clipboard() Clipboard
  • Purpose: The top-level application contract. Aggregates all cross-cutting concerns: window management, notifications, theming, persistent preferences, storage, and the app lifecycle. Provides access to all major sub-systems through accessor methods.
  • Implementations: app.fyneApp (the only implementation, wired in app/ via build tags). There is one global instance per process, stored in an atomic.Pointer[App].
  • Design quality: Broad (17 methods) but coherent — this is a facade interface, not a focused component interface. Each method group (Settings, Preferences, Storage, Lifecycle) could be its own narrower interface (and is, for internal use). The breadth is a trade-off for developer convenience: users hold a single fyne.App and get everything from it.

Canvas#

  • Package: fyne.io/fyne/v2
  • File: canvas.go
  • Methods:
    Content() CanvasObject
    SetContent(CanvasObject)
    Refresh(CanvasObject)
    Focus(Focusable)
    FocusNext()
    FocusPrevious()
    Unfocus()
    Focused() Focusable
    Size() Size
    Scale() float32
    Overlays() OverlayStack
    OnTypedRune() func(rune)
    SetOnTypedRune(func(rune))
    OnTypedKey() func(*KeyEvent)
    SetOnTypedKey(func(*KeyEvent))
    AddShortcut(shortcut Shortcut, handler func(shortcut Shortcut))
    RemoveShortcut(shortcut Shortcut)
    Capture() image.Image
    PixelCoordinateForPosition(Position) (int, int)
    InteractiveArea() (Position, Size)
  • Purpose: The surface on which all content is drawn. Manages content root, focus management, shortcut registration, overlay stack, and pixel/coordinate mapping. Also provides Capture() for screenshot and testing.
  • Implementations: internal/driver/glfw.glCanvas (desktop), internal/driver/mobile.mobileCanvas, both embedding internal/driver/common.Canvas which provides all shared logic. test.WindowlessCanvas for headless testing.
  • Design quality: 20 methods — the broadest interface in the root package. The focus management methods (Focus, FocusNext, FocusPrevious, Unfocus, Focused) could arguably be a FocusManager sub-interface, but grouping them here gives widget authors a single surface to work with.

Driver#

  • Package: fyne.io/fyne/v2
  • File: driver.go
  • Methods:
    CreateWindow(string) Window
    AllWindows() []Window
    RenderedTextSize(text string, fontSize float32, style TextStyle, source Resource) (Size, float32)
    CanvasForObject(CanvasObject) Canvas
    AbsolutePositionForObject(CanvasObject) Position
    Device() Device
    Run()
    Quit()
    StartAnimation(*Animation)
    StopAnimation(*Animation)
    DoubleTapDelay() time.Duration
    SetDisableScreenBlanking(bool)
    DoFromGoroutine(fn func(), wait bool)
  • Purpose: The platform abstraction for a rendering backend. Manages windows, the event loop, text rendering, coordinate mapping, animations, and the critical DoFromGoroutine mechanism for marshalling work to the UI thread.
  • Implementations: internal/driver/glfw.gLDriver (desktop OpenGL via GLFW), internal/driver/mobile.mobileDriver (iOS/Android), internal/driver/software.SoftwareDriver/embedded driver (headless/CI). Backend is selected at compile time via build tags in app/.
  • Design quality: 13 methods covering genuinely disparate concerns (windows, text measurement, animation, threading). This interface exists primarily as the seam between app/ and the internal/driver/ implementations; application code rarely interacts with Driver directly.

Theme#

  • Package: fyne.io/fyne/v2
  • File: theme.go
  • Methods:
    Color(ThemeColorName, ThemeVariant) color.Color
    Font(TextStyle) Resource
    Icon(ThemeIconName) Resource
    Size(ThemeSizeName) float32
  • Purpose: Provides the visual tokens (colours, fonts, icons, sizes) for the entire toolkit. All widgets query the current theme through these four lookup methods using named constants (e.g., theme.ColorNamePrimary, theme.SizeNamePadding).
  • Implementations: theme.defaultTheme (built-in light/dark), any app-supplied custom Theme implementation (a popular extension point). internal/theme.FeatureTheme adds optional feature-gated colours.
  • Design quality: Exceptionally well-designed. Four methods, fully orthogonal. The use of named string types (ThemeColorName, ThemeIconName, ThemeSizeName) as keys (rather than separate methods per token) means adding new tokens doesn’t break the interface. Contrast with LegacyTheme (21 methods, one per token) — the v2 refactor is a textbook ISP improvement.

DataItem / DataListener#

  • Package: fyne.io/fyne/v2/data/binding
  • File: binding.go
  • Methods:
    // DataItem:
    AddListener(DataListener)
    RemoveListener(DataListener)
    
    // DataListener:
    DataChanged()
  • Purpose: The observer pattern foundation for the data binding system. DataItem is the observable; DataListener is the observer. The base struct in the same package provides the shared listener-list implementation embedded by all concrete binding types.
  • Implementations: Every binding type implements DataItemItem[T], DataList, DataMap, DataTree and their external variants. DataListener is implemented by widget connector types and NewDataListener(fn func()) for inline callbacks.
  • Design quality: Minimal and clean. Two methods on DataItem, one on DataListener. The generics-based Item[T] interface (added in 2.6) extends DataItem with typed Get()/Set(), preserving backward compatibility.

Item[T] / ExternalItem[T]#

  • Package: fyne.io/fyne/v2/data/binding
  • File: items.go
  • Methods:
    // Item[T]:
    DataItem                // embedded
    Get() (T, error)
    Set(T) error
    
    // ExternalItem[T]:
    Item[T]                 // embedded
    Reload() error
  • Purpose: Generic typed bindings (Go 1.18+). Item[T] wraps a managed value; ExternalItem[T] wraps a pointer to an existing variable. Type aliases for concrete types (Bool = Item[bool], String = Item[string], etc.) provide backward-compatible named types.
  • Implementations: item[T] (internal), externalItem[T] (internal). Type aliases like binding.Bool, binding.String, binding.Int are defined as = Item[bool], = Item[string], etc.
  • Design quality: Excellent use of generics. The interface hierarchy (DataItemItem[T]ExternalItem[T]) is a clean three-level embedding chain. The Reload() method on ExternalItem[T] is the only addition for external values — models minimal extension principle.

Repository (and extensions)#

  • Package: fyne.io/fyne/v2/storage/repository
  • File: repository.go
  • Methods (base Repository):
    Exists(fyne.URI) (bool, error)
    Reader(fyne.URI) (fyne.URIReadCloser, error)
    CanRead(fyne.URI) (bool, error)
    Destroy(string)
  • Extension interfaces (all embed Repository):
    • CustomURIRepository: adds ParseURI(string) (fyne.URI, error)
    • WritableRepository: adds Writer, CanWrite, Delete
    • AppendableRepository: extends WritableRepository, adds Appender
    • ListableRepository: adds CanList, List, CreateListable
    • HierarchicalRepository: adds Parent, Child
    • CopyableRepository: adds Copy
    • MovableRepository: adds Move
    • DeleteAllRepository: extends WritableRepository, adds DeleteAll
  • Purpose: A URI-scheme-based storage abstraction. Backends are registered per URI scheme via Register(scheme, Repository). Higher-level storage.* functions use type assertions to discover optional capabilities at runtime.
  • Implementations: Built-in fileRepository (local filesystem, registered for “file” scheme), httpRepository (HTTP read-only), plus app-specific repositories. The storage/repository package itself provides generic fallback implementations (GenericCopy, GenericMove, GenericParent, GenericDeleteAll).
  • Design quality: The interface hierarchy is a deliberate ISP showcase. The base Repository (4 methods) covers the minimum. Optional capabilities are discovered via type assertion rather than empty methods or flags. This is the same pattern as io.Reader/io.ReadWriter/io.ReadWriteSeeker — incrementally composable capability interfaces. The use of type assertions (not embedding in consumers) keeps the runtime dispatch explicit and auditable.

Painter (internal)#

  • Package: fyne.io/fyne/v2/internal/painter/gl
  • File: painter.go
  • Methods:
    Init()
    Capture(fyne.Canvas) image.Image
    Clear()
    Free(fyne.CanvasObject)
    Paint(fyne.CanvasObject, fyne.Position, fyne.Size)
    SetFrameBufferScale(float32)
    SetOutputSize(int, int)
    StartClipping(fyne.Position, fyne.Size)
    StopClipping()
  • Purpose: The internal seam between the canvas event/layout system and the actual rendering backend. Both glPainter (OpenGL via CGo) and softwarePainter (stdlib image/*) implement this interface, making backend substitution transparent to the canvas layer.
  • Implementations: gl.painter (OpenGL, production), software.painter (CPU-only, used in tests and the embedded driver). Switching is done at compile time via build tags.
  • Design quality: 9 methods, all rendering-lifecycle concerns. The interface lives in internal/ — it is an implementation detail, not part of the public API. Its existence is what enables CI to run the full widget test suite without GPU hardware.

Behavior interfaces (Tappable, Draggable, Focusable, Scrollable, Disableable, etc.)#

  • Package: fyne.io/fyne/v2
  • File: canvasobject.go
  • Methods per interface: 1–4 methods each
    Tappable:         Tapped(*PointEvent)
    DoubleTappable:   DoubleTapped(*PointEvent)
    SecondaryTappable:TappedSecondary(*PointEvent)
    Draggable:        Dragged(*DragEvent), DragEnd()
    Focusable:        FocusGained(), FocusLost(), TypedRune(rune), TypedKey(*KeyEvent)
    Scrollable:       Scrolled(*ScrollEvent)
    Disableable:      Enable(), Disable(), Disabled() bool
    Shortcutable:     TypedShortcut(Shortcut)
    Tabbable:         AcceptsTab() bool
    Validatable:      Validate() error
  • Purpose: Opt-in interaction contracts. The canvas and driver hit-test each CanvasObject and perform type assertions to discover capabilities at runtime. A widget gains click-handling by implementing Tappable, without touching any base class.
  • Implementations: Any widget that needs the behaviour implements it. widget.Button implements Tappable + Disableable. widget.Entry implements Focusable + Tappable + Draggable + Scrollable + Shortcutable + Tabbable + Validatable. Desktop-specific capabilities live in driver/desktop/ (Hoverable, Cursorable, Keyable, Mouseable).
  • Design quality: Near-perfect ISP application. Every interface has 1–4 methods. Widgets only implement what they need. The runtime type-assertion dispatch means capabilities compose without inheritance. The split into canvasobject.go (touch/keyboard) and driver/desktop/ (desktop-only) avoids polluting mobile builds with desktop-only method sets.

Interface patterns#

  • Size distribution: Extremely small. The root-package behavior interfaces average 1.8 methods. Even App (the broadest facade, 17 methods) and Canvas (20 methods) are outliers justified by their role as top-level facades. The median across all ~90 interfaces is 2 methods.
  • Embedding: Pervasive and systematic. Widget embeds CanvasObject. Item[T] embeds DataItem. ExternalItem[T] embeds Item[T]. The entire Repository hierarchy composes via embedding: WritableRepository embeds Repository, AppendableRepository embeds WritableRepository. driver/desktop.Canvas embeds fyne.Canvas.
  • Implicit satisfaction: Consumer-defined throughout. The root package defines all interfaces; internal/ and app/ packages implement them. Application code and third-party packages implement behavior interfaces (Tappable, Theme, DataItem) to hook into the framework. No explicit implements declarations exist anywhere.
  • stdlib interfaces used:
    • io.Reader / io.CloserURIReadCloser embeds both
    • io.Writer / io.CloserURIWriteCloser embeds both
    • image.Image → returned by Canvas.Capture() and used in Painter.Capture()
    • color.Color → returned by Theme.Color()
    • fmt.StringerURI has a String() string method (satisfies Stringer implicitly)

Key abstractions#

  1. CanvasObject + behavior interfaces (Tappable, Focusable, etc.): The composition model that allows any object to acquire interaction capabilities without inheritance. This is Fyne’s central architectural choice — it replaces the class hierarchy typical of traditional GUI frameworks (Qt’s QAbstractItem, Java’s JComponent) with small, composable interfaces. The canvas discovers capabilities at runtime via type assertions, which is idiomatic Go and keeps widgets lightweight.

  2. Widget / WidgetRenderer split: The only two-interface pattern in Fyne that is explicitly a framework contract (not just capability opt-in). Widget owns state; WidgetRenderer owns pixels. The cache in internal/cache bridges them. This separation allows the framework to destroy and recreate renderers on theme changes or when widgets leave the viewport, while widget state persists. It is a deliberate Model/View split enforced by Go interfaces.

  3. Theme (v2 design): The 4-method lookup-by-name design is one of the cleanest theme API designs in any Go GUI framework. It is extensible (new named constants don’t break the interface), testable (any struct with 4 methods satisfies it), and avoids the 21-method explosion of LegacyTheme. Worth citing as an ISP success story.

  4. Repository hierarchy: The capability-composition pattern for storage backends is a masterclass in progressive interface extension. The base Repository (4 methods) is genuinely minimally viable. Each extension interface adds exactly the methods for one capability. Type assertions at the storage.* function level replace method dispatch — callers opt into advanced features only when the backend supports them. No method stubs, no panics, no NotImplemented returns.

  5. DataItem / Item[T] / ExternalItem[T] chain: Shows how generics (Go 1.18) extend an existing non-generic interface hierarchy without breaking backward compatibility. The DataItem interface (added in 2.0) is unmodified; typed access is layered on top as Item[T]. Named type aliases (Bool = Item[bool]) preserve the pre-generics API surface. This is a textbook backward-compatible generics migration.


Interface-driven extensibility#

Fyne uses interfaces for extensibility at three levels:

1. Rendering backends (Driver, Painter): The entire platform abstraction is hidden behind fyne.Driver and the internal gl.Painter. Adding a new backend (e.g., a WASM renderer or a Vulkan backend) requires only satisfying these interfaces and adding a build-tagged file in app/. Application code is completely unaffected.

2. Behavior opt-in (Tappable, Theme, DataItem, etc.): Third-party widgets gain framework integration by implementing any behavior interface. This is the primary extension point for widget library authors. There is no registration step — the framework discovers capabilities at runtime via type assertion. The driver/desktop/ sub-package extends this with desktop-only behavior interfaces (Hoverable, Cursorable), which the GLFW driver checks and the mobile driver ignores.

3. Storage backends (Repository hierarchy): The scheme-based Repository registry allows third-party packages to add new URI schemes (e.g., S3, SFTP, in-memory) with full integration into the storage.* API. The type-assertion capability pattern means third parties implement only what their backend supports, with no stubs or no-op implementations required.

Theme customization is the most commonly exercised extensibility point: any application can supply a custom Theme implementation and call app.Settings().SetTheme(myTheme). The 4-method interface makes this trivial — a struct that overrides only Color() while delegating the rest to the default theme is ~15 lines of code.