diff --git a/internal/widget/base.go b/internal/widget/base.go index 36d2c9aaef..2d17d4bcc6 100644 --- a/internal/widget/base.go +++ b/internal/widget/base.go @@ -14,6 +14,7 @@ type Base struct { hidden atomic.Bool position async.Position size async.Size + minCache async.Size impl atomic.Pointer[fyne.Widget] } @@ -63,6 +64,11 @@ func (w *Base) Move(pos fyne.Position) { // MinSize for the widget - it should never be resized below this value. func (w *Base) MinSize() fyne.Size { + minCache := w.minCache.Load() + if !minCache.IsZero() { + return minCache + } + impl := w.super() r := cache.Renderer(impl) @@ -70,7 +76,9 @@ func (w *Base) MinSize() fyne.Size { return fyne.NewSize(0, 0) } - return r.MinSize() + minSize := r.MinSize() + w.minCache.Store(minSize) + return minSize } // Visible returns whether or not this widget should be visible. @@ -112,10 +120,18 @@ func (w *Base) Refresh() { return } + w.minCache.Store(fyne.Size{}) render := cache.Renderer(impl) render.Refresh() } +// ResetMinSizeCache resets the cached MinSize for this widget. +// +// Since: 2.5.0 +func (w *Base) ResetMinSizeCache() { + w.minCache.Store(fyne.Size{}) +} + // super will return the actual object that this represents. // If extended then this is the extending widget, otherwise it is nil. func (w *Base) super() fyne.Widget { diff --git a/widget/check.go b/widget/check.go index b8c0ce3253..c8a182bb0a 100644 --- a/widget/check.go +++ b/widget/check.go @@ -24,8 +24,6 @@ type Check struct { hovered bool binder basicBinder - - minSize fyne.Size // cached for hover/tap position calculations } // NewCheck creates a new check widget with the set label and change handler @@ -119,8 +117,9 @@ func (c *Check) MouseMoved(me *desktop.MouseEvent) { // only hovered if cached minSize has not been initialized (test code) // or the pointer is within the "active" area of the widget (its minSize) - c.hovered = c.minSize.IsZero() || - (me.Position.X <= c.minSize.Width && me.Position.Y <= c.minSize.Height) + minSize := c.MinSize() + c.hovered = minSize.IsZero() || + (me.Position.X <= minSize.Width && me.Position.Y <= minSize.Height) if oldHovered != c.hovered { c.Refresh() @@ -132,8 +131,10 @@ func (c *Check) Tapped(pe *fyne.PointEvent) { if c.Disabled() { return } - if !c.minSize.IsZero() && - (pe.Position.X > c.minSize.Width || pe.Position.Y > c.minSize.Height) { + + minSize := c.MinSize() + if !minSize.IsZero() && + (pe.Position.X > minSize.Width || pe.Position.Y > minSize.Height) { // tapped outside the active area of the widget return } @@ -151,8 +152,7 @@ func (c *Check) Tapped(pe *fyne.PointEvent) { // MinSize returns the size that this widget should not shrink below func (c *Check) MinSize() fyne.Size { c.ExtendBaseWidget(c) - c.minSize = c.BaseWidget.MinSize() - return c.minSize + return c.BaseWidget.MinSize() } // CreateRenderer is a private method to Fyne which links this widget to its renderer diff --git a/widget/entry.go b/widget/entry.go index 40ed9d232a..2581013900 100644 --- a/widget/entry.go +++ b/widget/entry.go @@ -96,7 +96,6 @@ type Entry struct { ActionItem fyne.CanvasObject `json:"-"` binder basicBinder conversionError error - minCache fyne.Size multiLineRows int // override global default number of visible lines // undoStack stores the data necessary for undo/redo functionality @@ -402,20 +401,8 @@ func (e *Entry) KeyUp(key *fyne.KeyEvent) { // // Implements: fyne.Widget func (e *Entry) MinSize() fyne.Size { - e.propertyLock.RLock() - cached := e.minCache - e.propertyLock.RUnlock() - if !cached.IsZero() { - return cached - } - e.ExtendBaseWidget(e) - min := e.BaseWidget.MinSize() - - e.propertyLock.Lock() - e.minCache = min - e.propertyLock.Unlock() - return min + return e.BaseWidget.MinSize() } // MouseDown called on mouse click, this triggers a mouse click which can move the cursor, @@ -480,14 +467,6 @@ func (e *Entry) Redo() { e.Refresh() } -func (e *Entry) Refresh() { - e.propertyLock.Lock() - e.minCache = fyne.Size{} - e.propertyLock.Unlock() - - e.BaseWidget.Refresh() -} - // SelectedText returns the text currently selected in this Entry. // If there is no selection it will return the empty string. func (e *Entry) SelectedText() string { diff --git a/widget/richtext.go b/widget/richtext.go index d9474253a2..7636a4a043 100644 --- a/widget/richtext.go +++ b/widget/richtext.go @@ -45,7 +45,6 @@ type RichText struct { visualCache map[RichTextSegment][]fyne.CanvasObject cacheLock sync.Mutex - minCache fyne.Size } // NewRichText returns a new RichText widget that renders the given text and segments. @@ -89,19 +88,18 @@ func (t *RichText) CreateRenderer() fyne.WidgetRenderer { // MinSize calculates the minimum size of a rich text widget. // This is based on the contained text with a standard amount of padding added. func (t *RichText) MinSize() fyne.Size { - // we don't return the minCache here, as any internal segments could have caused it to change... t.ExtendBaseWidget(t) - min := t.BaseWidget.MinSize() - t.minCache = min - return min + // We don't return the cached value as any internal segments could have caused it to change... + t.ResetMinSizeCache() + return t.BaseWidget.MinSize() } // Refresh triggers a redraw of the rich text. // // Implements: fyne.Widget func (t *RichText) Refresh() { - t.minCache = fyne.Size{} + t.ResetMinSizeCache() t.updateRowBounds() for _, s := range t.Segments { @@ -123,10 +121,11 @@ func (t *RichText) Resize(size fyne.Size) { } t.size.Store(size) + minSize := t.MinSize() t.propertyLock.RLock() segments := t.Segments - skipResize := !t.minCache.IsZero() && size.Width >= t.minCache.Width && size.Height >= t.minCache.Height && t.Wrapping == fyne.TextWrapOff && t.Truncation == fyne.TextTruncateOff + skipResize := !minSize.IsZero() && size.Width >= minSize.Width && size.Height >= minSize.Height && t.Wrapping == fyne.TextWrapOff && t.Truncation == fyne.TextTruncateOff t.propertyLock.RUnlock() if skipResize { diff --git a/widget/select_entry.go b/widget/select_entry.go index 806d17c8c0..d6161252f9 100644 --- a/widget/select_entry.go +++ b/widget/select_entry.go @@ -55,7 +55,7 @@ func (e *SelectEntry) Disable() { // Implements: fyne.Widget func (e *SelectEntry) MinSize() fyne.Size { e.ExtendBaseWidget(e) - return e.Entry.MinSize() + return e.BaseWidget.MinSize() } // Move changes the relative position of the select entry. diff --git a/widget/widget.go b/widget/widget.go index c5335485d8..639f70bd9f 100644 --- a/widget/widget.go +++ b/widget/widget.go @@ -16,6 +16,7 @@ import ( // BaseWidget provides a helper that handles basic widget behaviours. type BaseWidget struct { size async.Size + minCache async.Size position async.Position Hidden bool @@ -69,6 +70,11 @@ func (w *BaseWidget) Move(pos fyne.Position) { // MinSize for the widget - it should never be resized below this value. func (w *BaseWidget) MinSize() fyne.Size { + minCache := w.minCache.Load() + if !minCache.IsZero() { + return minCache + } + impl := w.super() r := cache.Renderer(impl) @@ -76,7 +82,9 @@ func (w *BaseWidget) MinSize() fyne.Size { return fyne.Size{} } - return r.MinSize() + minSize := r.MinSize() + w.minCache.Store(minSize) + return minSize } // Visible returns whether or not this widget should be visible. @@ -123,6 +131,7 @@ func (w *BaseWidget) Refresh() { return } + w.minCache.Store(fyne.Size{}) w.propertyLock.Lock() w.themeCache = nil w.propertyLock.Unlock() @@ -156,6 +165,13 @@ func (w *BaseWidget) themeWithLock() fyne.Theme { return cached } +// ResetMinSizeCache resets the cached MinSize for this widget. +// +// Since: 2.5.0 +func (w *BaseWidget) ResetMinSizeCache() { + w.minCache.Store(fyne.Size{}) +} + // SetFieldsAndRefresh helps to make changes to a widget that should be followed by a refresh. // This method is a guaranteed thread-safe way of directly manipulating widget fields. // Widgets extending BaseWidget should use this in their setter functions. @@ -170,6 +186,8 @@ func (w *BaseWidget) SetFieldsAndRefresh(f func()) { if impl == nil { return } + + w.minCache.Store(fyne.Size{}) impl.Refresh() } diff --git a/widget/widget_test.go b/widget/widget_test.go index 780ff2ec8e..cb09024bbd 100644 --- a/widget/widget_test.go +++ b/widget/widget_test.go @@ -79,21 +79,49 @@ func TestSimpleRenderer(t *testing.T) { test.AssertImageMatches(t, "simple_renderer.png", window.Canvas().Capture()) } +func TestMinSizeCache(t *testing.T) { + label := NewLabel("a") + + wid := newTestWidget(label) + assert.Equal(t, 0, wid.minSizeCalls) + + minSize := wid.MinSize() + assert.NotEqual(t, 0, minSize) + assert.Equal(t, 1, wid.minSizeCalls) + + wid.MinSize() + assert.Equal(t, 1, wid.minSizeCalls) +} + type testWidget struct { BaseWidget - obj fyne.CanvasObject + obj fyne.CanvasObject + minSizeCalls int } -func newTestWidget(o fyne.CanvasObject) fyne.Widget { +func newTestWidget(o fyne.CanvasObject) *testWidget { t := &testWidget{obj: o} t.ExtendBaseWidget(t) return t } func (t *testWidget) CreateRenderer() fyne.WidgetRenderer { - return NewSimpleRenderer(t.obj) + return &testRenderer{ + WidgetRenderer: NewSimpleRenderer(t.obj), + widget: t, + } } func waitForBinding() { time.Sleep(time.Millisecond * 100) // data resolves on background thread } + +type testRenderer struct { + widget *testWidget + fyne.WidgetRenderer +} + +func (r *testRenderer) MinSize() fyne.Size { + r.widget.minSizeCalls++ + return r.WidgetRenderer.MinSize() +}