Skip to content

Refactor Widget Rendering Architecture

Tilo Prütz edited this page Jun 5, 2019 · 3 revisions

This page is about a proposed change on the widget/renderer architecture.

Why?

Currently widgets and their renderers are coupled tightly. Every widget creates its own renderer (via CreateRenderer()) with a lot of details. The widgets even create the fyne.CanvasObjects the renderer has to use.

The renderer contains all the min size computation (MinSize()) and layout logic (Layout(), Refresh()). Thus it is important for the render engine (the driver) to access the renderer to perform the layout. But it can't because it only knows about the widget (which is part of the canvas object tree). That's why the driver relies on the widget to trigger the layout on Resize(). This requires the widget to access its renderer. This is not intended by the design (the widget does not know its renderer … it might have more than one). To circumvent this the widget packages includes a renderer cache. Thus every widget can access its renderer by widget.Renderer(wid).

This design makes it impossible to extend existing widgets by inheritance without tweaks like

func (l *TappableLabel)CreateRenderer() fyne.WidgetRenderer {
  return widget.Renderer(&l.Label)
}

which reveal the internal cache details even more.

It is completely impossible to inherit from a widget/renderer to extend its layout like this:

type MyButton struct {
  *widget.Button
  verticalIcon bool
}

func (b *MyButton)CreateRenderer() fyne.WidgetRenderer {
  return &myButtonRenderer{…}
}

type myButtonRenderer {
  button *MyButton
}

func (r *myButtonRenderer)Layout(size fyne.Size) {
  if r.button.verticalIcon {
    // do vertical layout
    …
  } else {
    // delegate horizontal layout to default button
    widget.Renderer(r.button.(*widget.Button)).Layout(size)
  }
}

Yes, this widget.Renderer() usage with casts is awkward.

The example above does not work because many implemented methods of the fyne.Widget interface use widget.Renderer to do something with the renderer. But the inherited methods use the wrong renderer (the one of the base object instead of the derived object's renderer).

The result is unexpected and broken rendering.

The separation of widget and renderer is a good approach to separate the behaviour of a widget (what is part of the widget, what happens on user input, is it scrollable etc.) from the presenting logic (compute size requirements, doing the layout). But this approach is not implemented consistently enough.

That's why I propose to separate widgets and renderers more strict and to change widgets to only handle the behaviour and deliver the details that are required for rendering (which texts, icons, images etc.).

How?

The widget.Renderer() method (and the whole renderer cache) has to leave the widget package. I have ideas that make these superfluous but for the first step they could move to the driver package (where driver.renderer() would be unexported) because the driver should trigger the layout. Thus the widgets lose the possibility to talk to their renderers.

Further Ideas

Just a couple of further ideas which might be worth a thought:

  • rename the renderers to layouters because this is what they do
  • make the renderers stateless
    • CreateRenderer() would be replaced by Layouter()
    • Layouter() would return a singleton of the appropriate layouter
    • the layouter would have the method MinSize(fyne.Widget) and Layout(fyne.Widget,fyne.Size)
    • the renderers cache could be removed
  • remove the refreshQueue channel
    • fyne.Widget would have a method State() (or alike) which returns some thing that represents if the state has changed – this must be comparable with == (an int would do for the first implementation)
    • the driver would cache the widget's states in an own tree and could thus determine on tree walk which widgets need to be updated
    • widgets would simply change their state if they need to be refreshed