diff --git a/widget/example/s_widget.go b/widget/example/s_widget.go new file mode 100644 index 00000000..38568e4d --- /dev/null +++ b/widget/example/s_widget.go @@ -0,0 +1,65 @@ +package example + +import ( + "sync" + + "fyne.io/fyne/v2" + wgt "fyne.io/x/fyne/widget" +) + +// SampleSWidget implements a table with row based selection and column names. +type SampleSWidget struct { + wgt.BaseSimpleWidget + propertyLock sync.RWMutex + + Property int +} + +// NewSampleSWidget creates a new SampleSWidget. +func NewSampleSWidget(property int) *SampleSWidget { + w := &SampleSWidget{Property: property} + w.ExtendBaseWidget(w) + return w +} + +// CreateRenderer implements fyne.Widget. +func (s *SampleSWidget) CreateRenderer() fyne.WidgetRenderer { + rend := &sampleSWidgetRenderer{ + widget: s, + } + rend.BaseSimpleRenderer = *wgt.NewBaseSimpleRenderer(s, rend) + return rend +} + +// SetStateSafe sets or changes the state of a widget in a safe way. A Refresh +// is triggered after the state changes have been applied. +func (s *SampleSWidget) SetStateSafe(setState func()) { + s.BaseSimpleWidget.SetStateSafe(&s.propertyLock, setState) +} + +func (s *SampleSWidget) getProperty() int { + s.propertyLock.RLock() + defer s.propertyLock.RUnlock() + + return s.Property +} + +type sampleSWidgetRenderer struct { + wgt.BaseSimpleRenderer + + widget *SampleSWidget +} + +// Build renders the SampleSWidget. +func (s *sampleSWidgetRenderer) Build() (objects []fyne.CanvasObject, layout func(size fyne.Size)) { + // create objects needed for rendering and append them to the objects slice. + // (executed in Widget.CreateRenderer function) + + return objects, func(size fyne.Size) { + property := s.widget.getProperty() + _ = property + + // position, resize or otherwise update objects based on changed properties. + // (executed in WidgetRenderer.Layout) + } +} diff --git a/widget/example/simple_widget.go b/widget/example/simple_widget.go new file mode 100644 index 00000000..b521c7f2 --- /dev/null +++ b/widget/example/simple_widget.go @@ -0,0 +1,53 @@ +package example + +import ( + "sync" + + "fyne.io/fyne/v2" + "fyne.io/x/fyne/widget" +) + +// SampleWidget is a sample widget demonstrating the base structure of a +// widget implementing SimpleWidget using SimpleWidgetBase. +type SampleWidget struct { + widget.SimpleWidgetBase + propertyLock sync.RWMutex + + Property int +} + +// NewSampleWidget creates a new sample widget. +func NewSampleWidget(property int) *SampleWidget { + wgt := &SampleWidget{Property: property} + wgt.ExtendBaseWidget(wgt) + return wgt +} + +// Build renders the SampleWidget. +func (s *SampleWidget) Build() (objects []fyne.CanvasObject, layout func(size fyne.Size)) { + // create objects needed for rendering and append them to the objects slice. + // (executed in Widget.CreateRenderer function) + + return objects, func(size fyne.Size) { + property := s.getProperty() + _ = property + + // position, resize or otherwise update objects based on changed properties. + // (executed in WidgetRenderer.Layout) + } +} + +// SetStateSafe sets or changes the state of a widget in a safe way. A Refresh +// is triggered after the state changes have been applied. +func (s *SampleWidget) SetStateSafe(setState func()) { + s.SimpleWidgetBase.SetStateSafe(&s.propertyLock, setState) +} + +func (s *SampleWidget) getProperty() int { + s.propertyLock.RLock() + defer s.propertyLock.RUnlock() + + return s.Property +} + +// All other methods of a fyne.Widget can be overwritten as usual. E.g. MinSize, Resize, Refresh, etc. diff --git a/widget/s_renderer.go b/widget/s_renderer.go new file mode 100644 index 00000000..fadb658d --- /dev/null +++ b/widget/s_renderer.go @@ -0,0 +1,89 @@ +package widget + +import ( + "sync" + + "fyne.io/fyne/v2" +) + +type SimpleWidgetRenderer interface { + fyne.WidgetRenderer + + Build() (objects []fyne.CanvasObject, layout func(size fyne.Size)) +} + +func NewBaseSimpleRenderer(wgt fyne.Widget, renderer SimpleWidgetRenderer) *BaseSimpleRenderer { + rend := &BaseSimpleRenderer{ + widget: wgt, + impl: renderer, + } + objs, layout := rend.super().Build() + rend.objects = objs + rend.layout = layout + + return rend +} + +type BaseSimpleRenderer struct { + objects []fyne.CanvasObject + propertyLock sync.RWMutex + widget fyne.Widget + impl SimpleWidgetRenderer + + layout func(fyne.Size) +} + +func (s *BaseSimpleRenderer) Build() (objects []fyne.CanvasObject, layout func(size fyne.Size)) { + return nil, func(fyne.Size) {} +} + +// Destroy can be overwritten if there is extra cleanup needed. +func (s *BaseSimpleRenderer) Destroy() {} + +// Layout is a hook that is called if the widget needs to be laid out. +// This should not be overwritten. +func (s *BaseSimpleRenderer) Layout(size fyne.Size) { + s.layout(size) +} + +func (s *BaseSimpleRenderer) MinSize() fyne.Size { + return fyne.NewSize(0, 0) +} + +func (s *BaseSimpleRenderer) Objects() []fyne.CanvasObject { + return s.objects +} + +// Refresh is a hook that is called if the widget has updated and needs to be redrawn. +// By default it triggers a call to Layout. Overwrite to optimize for performance if +// a call to Layout is not needed. +func (s *BaseSimpleRenderer) Refresh() { + s.propertyLock.RLock() + layout := s.layout + s.propertyLock.RUnlock() + + if layout == nil { + return + } + layout(s.widget.Size()) +} + +func (s *BaseSimpleRenderer) getImpl() SimpleWidgetRenderer { + s.propertyLock.RLock() + impl := s.impl + s.propertyLock.RUnlock() + + if impl == nil { + return nil + } + return impl +} + +func (s *BaseSimpleRenderer) super() SimpleWidgetRenderer { + impl := s.getImpl() + if impl == nil { + var x interface{} = s + return x.(SimpleWidgetRenderer) + } + return impl +} diff --git a/widget/s_widget.go b/widget/s_widget.go new file mode 100644 index 00000000..38de3888 --- /dev/null +++ b/widget/s_widget.go @@ -0,0 +1,75 @@ +package widget + +import ( + "sync" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/widget" +) + +// BaseSimpleWidget defines the base for a fyne.Widget implementation. +// To create a new widget base it on BaseSimpleWidget using composition. +// Create a `New` function initialising the widget and make sure to call +// ExtendBaseWidget in the New function. Always use the `New` function to +// create the widget or make sure `ExtendBaseWidget` is called elsewhere. +// +// See ./example/s_wigdet.go for a bootstraped widget implementation. +type BaseSimpleWidget struct { + widget.BaseWidget + + impl fyne.Widget + propertyLock sync.RWMutex +} + +// ExtendBaseWidget is used by an extending widget to make use of BaseSimpleWidget functionality. +func (s *BaseSimpleWidget) ExtendBaseWidget(wid fyne.Widget) { + impl := s.getImpl() + if impl != nil { + return + } + + s.BaseWidget.ExtendBaseWidget(wid) + + s.propertyLock.Lock() + defer s.propertyLock.Unlock() + s.impl = wid +} + +// SetState sets or changes the state of a widget. A Refresh +// is triggered after the state changes have been applied. +func (s *BaseSimpleWidget) SetState(setState func()) { + setState() + s.super().Refresh() +} + +// SetStateSafe sets or changes the state of a widget in a safe way. A Refresh +// is triggered after the state changes have been applied. +// The provided sync.Locker should be the same you use for read protection of the +// widget properties. +func (s *BaseSimpleWidget) SetStateSafe(m sync.Locker, setState func()) { + m.Lock() + setState() + m.Unlock() + + s.super().Refresh() +} + +func (s *BaseSimpleWidget) getImpl() fyne.Widget { + s.propertyLock.RLock() + impl := s.impl + s.propertyLock.RUnlock() + + if impl == nil { + return nil + } + return impl +} + +func (s *BaseSimpleWidget) super() fyne.Widget { + impl := s.getImpl() + if impl == nil { + var x interface{} = s + return x.(fyne.Widget) + } + return impl +} diff --git a/widget/simple_renderer.go b/widget/simple_renderer.go new file mode 100644 index 00000000..096e41c4 --- /dev/null +++ b/widget/simple_renderer.go @@ -0,0 +1,53 @@ +package widget + +import ( + "fyne.io/fyne/v2" +) + +// simpleRenderer is a renderer providing the basic rendering functionality used +// by SimpleWidgetBase. +type simpleRenderer struct { + widget fyne.Widget + objects []fyne.CanvasObject + + layout func(size fyne.Size) + destroy func() + refresh func() + minSize func() fyne.Size +} + +// MinSize returns the minimum size of the widget that is rendered by this renderer. +func (s *simpleRenderer) MinSize() fyne.Size { + return s.minSize() +} + +// Refresh is a hook that is called if the widget has updated and needs to be redrawn. +// This might trigger a Layout. +func (s *simpleRenderer) Refresh() { + s.refresh() +} + +// Layout is a hook that is called if the widget needs to be laid out. +// This should not be overwritten. +func (s *simpleRenderer) Layout(size fyne.Size) { + s.layout(size) +} + +// Objects returns the objects that should be rendered. +// +// Implements: fyne.WidgetRenderer +func (s *simpleRenderer) Objects() []fyne.CanvasObject { + return s.objects +} + +// SetObjects updates the objects of the renderer. +func (s *simpleRenderer) SetObjects(objects []fyne.CanvasObject) { + s.objects = objects +} + +// Destroy does nothing in the base implementation. +// +// Implements: fyne.WidgetRenderer +func (s *simpleRenderer) Destroy() { + s.destroy() +} diff --git a/widget/simple_widget.go b/widget/simple_widget.go new file mode 100644 index 00000000..37eb2239 --- /dev/null +++ b/widget/simple_widget.go @@ -0,0 +1,155 @@ +package widget + +import ( + "sync" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/widget" +) + +// SimpleWidget defines an interface for fyne widgets that are simple +// to implement when based on SimpleWidgetBase. +// For more info on how to implement, see the documentation of the +// SimpleWidgetBase. +type SimpleWidget interface { + fyne.Widget + + Build() ([]fyne.CanvasObject, func(size fyne.Size)) +} + +// SimpleWidgetBase defines the base for a SimpleWidget implementation. +// To create a new widget base it on SimpleWidgetBase using composition. +// Create a `New` function initialising the widget and make sure to call +// ExtendBaseWidget in the New function. Always use the `New` function to +// create the widget or make sure `ExtendBaseWidget` is called elsewhere. +// +// Overwrite the `Build() (objects []fyne.CanvasObject, layout func(size fyne.Size))` +// function. It returns the objects needed to render the widgets content, +// as well as a function `layout` responsible for positioning and resizing the +// different objects. +// Try not to define new objects in the `layout` function as they would be +// recreated every time the widget is refreshed. +// +// Other methods defined by the fyne.Widget interface can be overwritten +// and will be used by the SimpleWidgetBase if overwritten. +// Overwrite `Destroy()` to implement custom cleanup for the widget. +// Implement `Refresh()` to optimize refresh performance. By default `Refresh` +// calls the `Layout` method of the renderer, which is not necessary in most cases. +// +// See ./example/simple_wigdet.go for a bootstraped widget implementation. +type SimpleWidgetBase struct { + widget.BaseWidget + + propertyLock sync.RWMutex + impl SimpleWidget + layout func(fyne.Size) +} + +// Build must be overwritten in a widget. It returns a slice of child objects +// and a layout function. +// The layout function should be used to position and size the objects (widgets +// and canvas objects). New objects should be created in the Build function body, +// so they are not re-created every time the widget gets refreshed. +func (s *SimpleWidgetBase) Build() (objects []fyne.CanvasObject, layout func(size fyne.Size)) { + return nil, func(fyne.Size) {} +} + +// CreateRenderer implements the Widget interface. It creates a simpleRenderer +// and returns it. No renderer needs to be implemented. If the simpleRenderer +// doesn't do it, SimpleWidget is probably not suitable for the use case. +// Usually this should not be overwritten or called manually. +func (s *SimpleWidgetBase) CreateRenderer() fyne.WidgetRenderer { + wdgt := s.super() + objs, layout := wdgt.Build() + + s.propertyLock.Lock() + s.layout = layout + s.propertyLock.Unlock() + + renderer := &simpleRenderer{ + widget: wdgt, + objects: objs, + layout: layout, + destroy: s.Destroy, + refresh: s.Refresh, + minSize: s.MinSize, + } + return renderer +} + +// SetState sets or changes the state of a widget. A Refresh +// is triggered after the state changes have been applied. +func (s *SimpleWidgetBase) SetState(setState func()) { + setState() + s.super().Refresh() +} + +// SetStateSafe sets or changes the state of a widget in a safe way. A Refresh +// is triggered after the state changes have been applied. +// The provided sync.Locker should be the same you use for read protection of the +// widget properties. +func (s *SimpleWidgetBase) SetStateSafe(m sync.Locker, setState func()) { + m.Lock() + setState() + m.Unlock() + + s.super().Refresh() +} + +// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality. +func (s *SimpleWidgetBase) ExtendBaseWidget(wid SimpleWidget) { + impl := s.getImpl() + if impl != nil { + return + } + + s.BaseWidget.ExtendBaseWidget(wid) + + s.propertyLock.Lock() + defer s.propertyLock.Unlock() + s.impl = wid +} + +// Refresh is a hook that is called if the widget has updated and needs to be redrawn. +// By default it triggers a call to Layout. Overwrite to optimize for performance if +// a call to Layout is not needed. +func (s *SimpleWidgetBase) Refresh() { + s.propertyLock.RLock() + layout := s.layout + s.propertyLock.RUnlock() + + if layout == nil { + return + } + layout(s.Size()) +} + +// MinSize for the widget - it should never be resized below this value. +// By default this returns (0, 0). Overwrite this to return a different +// minimum size for the widget. +func (s *SimpleWidgetBase) MinSize() fyne.Size { + return fyne.NewSize(0, 0) +} + +// Destroy can be overwritten if there is extra cleanup needed. +func (s *SimpleWidgetBase) Destroy() {} + +func (s *SimpleWidgetBase) super() SimpleWidget { + impl := s.getImpl() + if impl == nil { + var x interface{} = s + return x.(SimpleWidget) + } + return impl +} + +func (s *SimpleWidgetBase) getImpl() SimpleWidget { + s.propertyLock.RLock() + impl := s.impl + s.propertyLock.RUnlock() + + if impl == nil { + return nil + } + return impl +}