From 231ac647a6684ec82d46616ec2905219002dd2b0 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 29 Dec 2023 13:19:22 -0800 Subject: [PATCH 01/36] update RadioGroup to support multiple items with the same label --- widget/check_group.go | 14 +++++ widget/radio_group.go | 132 ++++++++++++++++++++++++------------------ 2 files changed, 91 insertions(+), 55 deletions(-) diff --git a/widget/check_group.go b/widget/check_group.go index 7fc6430030..0994ecde80 100644 --- a/widget/check_group.go +++ b/widget/check_group.go @@ -265,3 +265,17 @@ func (r *checkGroupRenderer) updateItems() { item.Refresh() } } + +func removeDuplicates(options []string) []string { + var result []string + found := make(map[string]bool) + + for _, option := range options { + if _, ok := found[option]; !ok { + found[option] = true + result = append(result, option) + } + } + + return result +} diff --git a/widget/radio_group.go b/widget/radio_group.go index d28572b311..ecb6d3d108 100644 --- a/widget/radio_group.go +++ b/widget/radio_group.go @@ -1,6 +1,8 @@ package widget import ( + "log" + "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/internal/widget" @@ -18,7 +20,10 @@ type RadioGroup struct { Options []string Selected string - items []*radioItem + // this index is ONE-BASED so the default zero-value is unselected + // use r.selectedIndex(), r.setSelectedIndex(int) to maniupulate this field + // as if it were a zero-based index (with -1 == nothing selected) + _selIdx int } var _ fyne.Widget = (*RadioGroup)(nil) @@ -32,7 +37,6 @@ func NewRadioGroup(options []string, changed func(string)) *RadioGroup { OnChanged: changed, } r.ExtendBaseWidget(r) - r.update() return r } @@ -46,16 +50,16 @@ func (r *RadioGroup) Append(option string) { // CreateRenderer is a private method to Fyne which links this widget to its renderer func (r *RadioGroup) CreateRenderer() fyne.WidgetRenderer { r.ExtendBaseWidget(r) - r.propertyLock.Lock() - defer r.propertyLock.Unlock() - r.update() - objects := make([]fyne.CanvasObject, len(r.items)) - for i, item := range r.items { - objects[i] = item + items := make([]fyne.CanvasObject, len(r.Options)) + for i, option := range r.Options { + idx := i + items[idx] = newRadioItem(option, func(item *radioItem) { + r.itemTapped(item, idx) + }) } - return &radioGroupRenderer{widget.NewBaseRenderer(objects), r.items, r} + return &radioGroupRenderer{widget.NewBaseRenderer(items), items, r} } // MinSize returns the size that this widget should not shrink below @@ -64,16 +68,6 @@ func (r *RadioGroup) MinSize() fyne.Size { return r.BaseWidget.MinSize() } -// Refresh causes this widget to be redrawn in it's current state. -// -// Implements: fyne.CanvasObject -func (r *RadioGroup) Refresh() { - r.propertyLock.Lock() - r.update() - r.propertyLock.Unlock() - r.BaseWidget.Refresh() -} - // SetSelected sets the radio option, it can be used to set a default option. func (r *RadioGroup) SetSelected(option string) { if r.Selected == option { @@ -81,6 +75,7 @@ func (r *RadioGroup) SetSelected(option string) { } r.Selected = option + // selectedIndex will be updated on refresh to the first matching item if r.OnChanged != nil { r.OnChanged(option) @@ -89,19 +84,23 @@ func (r *RadioGroup) SetSelected(option string) { r.Refresh() } -func (r *RadioGroup) itemTapped(item *radioItem) { +func (r *RadioGroup) itemTapped(item *radioItem, idx int) { if r.Disabled() { return } - if r.Selected == item.Label { + if r.selectedIndex() == idx { if r.Required { return } + log.Printf("trying to unselect item %d", idx) r.Selected = "" + r.setSelectedIndex(-1) item.SetSelected(false) } else { + log.Printf("trying to select item %d", idx) r.Selected = item.Label + r.setSelectedIndex(idx) item.SetSelected(true) } @@ -111,27 +110,47 @@ func (r *RadioGroup) itemTapped(item *radioItem) { r.Refresh() } -func (r *RadioGroup) update() { - r.Options = removeDuplicates(r.Options) - if len(r.items) < len(r.Options) { - for i := len(r.items); i < len(r.Options); i++ { - item := newRadioItem(r.Options[i], r.itemTapped) - r.items = append(r.items, item) - } - } else if len(r.items) > len(r.Options) { - r.items = r.items[:len(r.Options)] +func (r *RadioGroup) Refresh() { + r.updateSelectedIndex() + r.BaseWidget.Refresh() +} + +func (r *RadioGroup) selectedIndex() int { + return r._selIdx - 1 +} + +func (r *RadioGroup) setSelectedIndex(idx int) { + r._selIdx = idx + 1 +} + +// if selectedIndex does not match the public Selected property, +// set it to the index of the first radio item whose label matches Selected +func (r *RadioGroup) updateSelectedIndex() { + sel := r.Selected + sIdx := r.selectedIndex() + if sIdx >= 0 && sIdx < len(r.Options) && r.Options[sIdx] == sel { + return // selected index matches Selected } - for i, item := range r.items { - item.Label = r.Options[i] - item.Selected = item.Label == r.Selected - item.DisableableWidget.disabled = r.disabled - item.Refresh() + if sIdx == -1 && sel == "" { + return // nothing selected + } + + sIdx = -1 + for i, opt := range r.Options { + if sel == opt { + sIdx = i + break + } } + r.setSelectedIndex(sIdx) } type radioGroupRenderer struct { widget.BaseRenderer - items []*radioItem + + // slice of *radioItem, but using fyne.CanvasObject as the type + // so we can directly set it to the BaseRenderer's objects slice + items []fyne.CanvasObject radio *RadioGroup } @@ -194,34 +213,37 @@ func (r *radioGroupRenderer) Refresh() { func (r *radioGroupRenderer) updateItems() { if len(r.items) < len(r.radio.Options) { for i := len(r.items); i < len(r.radio.Options); i++ { - item := newRadioItem(r.radio.Options[i], r.radio.itemTapped) - r.SetObjects(append(r.Objects(), item)) + idx := i + item := newRadioItem(r.radio.Options[idx], func(item *radioItem) { + r.radio.itemTapped(item, idx) + }) r.items = append(r.items, item) } r.Layout(r.radio.Size()) } else if len(r.items) > len(r.radio.Options) { total := len(r.radio.Options) r.items = r.items[:total] - r.SetObjects(r.Objects()[:total]) } - for i, item := range r.items { - item.Label = r.radio.Options[i] - item.Selected = item.Label == r.radio.Selected - item.disabled = r.radio.disabled - item.Refresh() - } -} + r.SetObjects(r.items) -func removeDuplicates(options []string) []string { - var result []string - found := make(map[string]bool) + for i, item := range r.items { + item := item.(*radioItem) + changed := false + if l := r.radio.Options[i]; l != item.Label { + item.Label = r.radio.Options[i] + changed = true + } + if sel := i == r.radio.selectedIndex(); sel != item.Selected { + item.Selected = sel + changed = true + } + if d := r.radio.disabled; d != item.disabled { + item.disabled = d + changed = true + } - for _, option := range options { - if _, ok := found[option]; !ok { - found[option] = true - result = append(result, option) + if changed { + item.Refresh() } } - - return result } From e283aca108ebe8206feccf34bc47813c855c9e92 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 29 Dec 2023 13:20:36 -0800 Subject: [PATCH 02/36] remove debug prints --- widget/radio_group.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/widget/radio_group.go b/widget/radio_group.go index ecb6d3d108..d71dea39b2 100644 --- a/widget/radio_group.go +++ b/widget/radio_group.go @@ -1,8 +1,6 @@ package widget import ( - "log" - "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/internal/widget" @@ -93,12 +91,10 @@ func (r *RadioGroup) itemTapped(item *radioItem, idx int) { if r.Required { return } - log.Printf("trying to unselect item %d", idx) r.Selected = "" r.setSelectedIndex(-1) item.SetSelected(false) } else { - log.Printf("trying to select item %d", idx) r.Selected = item.Label r.setSelectedIndex(idx) item.SetSelected(true) From 673f2619d686b202c06ef1ca332dd8357bdbdf7d Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 6 Jan 2024 08:50:33 -0800 Subject: [PATCH 03/36] update tests --- widget/radio_group_extended_test.go | 26 ++++++++----- widget/radio_group_internal_test.go | 57 ++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 27 deletions(-) diff --git a/widget/radio_group_extended_test.go b/widget/radio_group_extended_test.go index 6d009aad01..567cfe1fb7 100644 --- a/widget/radio_group_extended_test.go +++ b/widget/radio_group_extended_test.go @@ -22,7 +22,7 @@ func newextendedRadioGroup(opts []string, f func(string)) *extendedRadioGroup { ret.Options = opts ret.OnChanged = f ret.ExtendBaseWidget(ret) - ret.update() // Not needed for extending Radio but for the tests to be able to access items without creating a renderer first. + //ret.update() // Not needed for extending Radio but for the tests to be able to access items without creating a renderer first. return ret } @@ -32,7 +32,7 @@ func TestRadioGroup_Extended_Selected(t *testing.T) { radio := newextendedRadioGroup([]string{"Hi"}, func(sel string) { selected = sel }) - radio.items[0].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) + extendedRadioGroupTestTapItem(t, radio, 0) assert.Equal(t, "Hi", selected) } @@ -43,7 +43,8 @@ func TestRadioGroup_Extended_Unselected(t *testing.T) { selected = sel }) radio.Selected = selected - radio.items[0].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) + radio.Refresh() // sets up selectedIndex + extendedRadioGroupTestTapItem(t, radio, 0) assert.Equal(t, "", selected) } @@ -89,7 +90,7 @@ func TestRadioGroup_Extended_Disable(t *testing.T) { }) radio.Disable() - radio.items[0].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) + extendedRadioGroupTestTapItem(t, radio, 0) assert.Equal(t, "", selected, "RadioGroup should have been disabled.") } @@ -101,11 +102,11 @@ func TestRadioGroup_Extended_Enable(t *testing.T) { }) radio.Disable() - radio.items[0].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) + extendedRadioGroupTestTapItem(t, radio, 0) assert.Equal(t, "", selected, "Radio should have been disabled.") radio.Enable() - radio.items[0].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) + extendedRadioGroupTestTapItem(t, radio, 0) assert.Equal(t, "Hi", selected, "Radio should have been re-enabled.") } @@ -131,9 +132,9 @@ func TestRadioGroup_Extended_Hovered(t *testing.T) { t.Run(tt.name, func(t *testing.T) { radio := newextendedRadioGroup(tt.options, nil) radio.Horizontal = tt.isHorizontal - item1 := radio.items[0] + item1 := test.WidgetRenderer(radio).Objects()[0].(*radioItem) render1 := cache.Renderer(item1).(*radioItemRenderer) - render2 := cache.Renderer(radio.items[1]).(*radioItemRenderer) + render2 := cache.Renderer(test.WidgetRenderer(radio).Objects()[1].(*radioItem)).(*radioItemRenderer) assert.False(t, item1.hovered) assert.Equal(t, color.Transparent, render1.focusIndicator.FillColor) @@ -166,7 +167,7 @@ func TestRadioGroup_Extended_Hovered(t *testing.T) { func TestRadioGroupRenderer_Extended_ApplyTheme(t *testing.T) { radio := newextendedRadioGroup([]string{"Test"}, func(string) {}) - render := cache.Renderer(radio.items[0]).(*radioItemRenderer) + render := cache.Renderer(test.WidgetRenderer(radio).Objects()[0].(*radioItem)).(*radioItemRenderer) textSize := render.label.TextSize customTextSize := textSize @@ -177,3 +178,10 @@ func TestRadioGroupRenderer_Extended_ApplyTheme(t *testing.T) { assert.NotEqual(t, textSize, customTextSize) } + +func extendedRadioGroupTestTapItem(t *testing.T, radio *extendedRadioGroup, item int) { + t.Helper() + renderer := test.WidgetRenderer(radio) + radioItem := renderer.Objects()[item].(*radioItem) + radioItem.Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) +} diff --git a/widget/radio_group_internal_test.go b/widget/radio_group_internal_test.go index ac5bffee72..e3ed54e527 100644 --- a/widget/radio_group_internal_test.go +++ b/widget/radio_group_internal_test.go @@ -46,7 +46,7 @@ func TestRadioGroup_Selected(t *testing.T) { radio := NewRadioGroup([]string{"Hi"}, func(sel string) { selected = sel }) - radio.items[0].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) + radioGroupTestTapItem(t, radio, 0) assert.Equal(t, "Hi", selected) } @@ -57,7 +57,8 @@ func TestRadioGroup_Unselected(t *testing.T) { selected = sel }) radio.Selected = selected - radio.items[0].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) + radio.Refresh() // sets up selectedIndex + radioGroupTestTapItem(t, radio, 0) assert.Equal(t, "", selected) } @@ -65,7 +66,7 @@ func TestRadioGroup_Unselected(t *testing.T) { func TestRadioGroup_DisableWhenSelected(t *testing.T) { radio := NewRadioGroup([]string{"Hi"}, nil) radio.SetSelected("Hi") - render := test.WidgetRenderer(radio.items[0]).(*radioItemRenderer) + render := radioGroupTestItemRenderer(t, radio, 0) icon := fyne.CurrentApp().Settings().Theme().Icon("iconNameRadioButtonFill") assert.Equal(t, "primary_"+icon.Name(), render.icon.Resource.Name()) @@ -75,7 +76,7 @@ func TestRadioGroup_DisableWhenSelected(t *testing.T) { func TestRadioGroup_DisableWhenNotSelected(t *testing.T) { radio := NewRadioGroup([]string{"Hi"}, nil) - render := test.WidgetRenderer(radio.items[0]).(*radioItemRenderer) + render := radioGroupTestItemRenderer(t, radio, 0) radio.Disable() resName := render.over.Resource.Name() @@ -87,7 +88,7 @@ func TestRadioGroup_SelectedOther(t *testing.T) { radio := NewRadioGroup([]string{"Hi", "Hi2"}, func(sel string) { selected = sel }) - radio.items[1].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), radio.MinSize().Height-theme.Padding())}) + radioGroupTestTapItem(t, radio, 1) assert.Equal(t, "Hi2", selected) } @@ -156,8 +157,16 @@ func TestRadioGroup_SetSelectedEmpty(t *testing.T) { func TestRadioGroup_DuplicatedOptions(t *testing.T) { radio := NewRadioGroup([]string{"Hi", "Hi", "Hi", "Another", "Another"}, nil) - assert.Equal(t, 2, len(radio.Options)) - assert.Equal(t, 2, len(test.WidgetRenderer(radio).(*radioGroupRenderer).items)) + assert.Equal(t, 5, len(radio.Options)) + assert.Equal(t, 5, len(test.WidgetRenderer(radio).(*radioGroupRenderer).items)) + + radioGroupTestTapItem(t, radio, 1) + assert.Equal(t, "Hi", radio.Selected) + assert.Equal(t, 1, radio.selectedIndex()) + item0 := test.WidgetRenderer(radio).Objects()[0].(*radioItem) + assert.Equal(t, false, item0.focused) + item1 := test.WidgetRenderer(radio).Objects()[0].(*radioItem) + assert.Equal(t, false, item1.focused) } func TestRadioGroup_AppendDuplicate(t *testing.T) { @@ -165,8 +174,8 @@ func TestRadioGroup_AppendDuplicate(t *testing.T) { radio.Append("Hi") - assert.Equal(t, 1, len(radio.Options)) - assert.Equal(t, 1, len(test.WidgetRenderer(radio).(*radioGroupRenderer).items)) + assert.Equal(t, 2, len(radio.Options)) + assert.Equal(t, 2, len(test.WidgetRenderer(radio).(*radioGroupRenderer).items)) } func TestRadioGroup_Disable(t *testing.T) { @@ -176,7 +185,7 @@ func TestRadioGroup_Disable(t *testing.T) { }) radio.Disable() - radio.items[0].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) + radioGroupTestTapItem(t, radio, 0) assert.Equal(t, "", selected, "RadioGroup should have been disabled.") } @@ -188,11 +197,11 @@ func TestRadioGroup_Enable(t *testing.T) { }) radio.Disable() - radio.items[0].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) + radioGroupTestTapItem(t, radio, 0) assert.Equal(t, "", selected, "Radio should have been disabled.") radio.Enable() - radio.items[0].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) + radioGroupTestTapItem(t, radio, 0) assert.Equal(t, "Hi", selected, "Radio should have been re-enabled.") } @@ -228,9 +237,9 @@ func TestRadioGroup_Hovered(t *testing.T) { t.Run(tt.name, func(t *testing.T) { radio := NewRadioGroup(tt.options, nil) radio.Horizontal = tt.isHorizontal - item1 := radio.items[0] - render1 := cache.Renderer(item1).(*radioItemRenderer) - render2 := cache.Renderer(radio.items[1]).(*radioItemRenderer) + item1 := test.WidgetRenderer(radio).Objects()[0].(*radioItem) + render1 := radioGroupTestItemRenderer(t, radio, 0) + render2 := radioGroupTestItemRenderer(t, radio, 1) assert.False(t, item1.hovered) assert.Equal(t, color.Transparent, render1.focusIndicator.FillColor) @@ -282,15 +291,16 @@ func TestRadioGroup_Required(t *testing.T) { radio.Resize(radio.MinSize()) radio.SetSelected("Hi") require.Equal(t, "Hi", radio.Selected) - radio.items[0].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) + radioGroupTestTapItem(t, radio, 0) assert.Equal(t, "Hi", radio.Selected, "tapping selected option of required radio does nothing") - radio.items[1].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), radio.Size().Height-theme.Padding())}) + radioGroupTestTapItem(t, radio, 1) + //radio.items[1].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), radio.Size().Height-theme.Padding())}) assert.Equal(t, "There", radio.Selected) } func TestRadioGroupRenderer_ApplyTheme(t *testing.T) { radio := NewRadioGroup([]string{"Test"}, func(string) {}) - render := cache.Renderer(radio.items[0]).(*radioItemRenderer) + render := radioGroupTestItemRenderer(t, radio, 0) textSize := render.label.TextSize customTextSize := textSize @@ -301,3 +311,14 @@ func TestRadioGroupRenderer_ApplyTheme(t *testing.T) { assert.NotEqual(t, textSize, customTextSize) } + +func radioGroupTestTapItem(t *testing.T, radio *RadioGroup, item int) { + t.Helper() + renderer := test.WidgetRenderer(radio) + radioItem := renderer.Objects()[item].(*radioItem) + radioItem.Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) +} + +func radioGroupTestItemRenderer(t *testing.T, radio *RadioGroup, item int) *radioItemRenderer { + return cache.Renderer(test.WidgetRenderer(radio).Objects()[item].(fyne.Widget)).(*radioItemRenderer) +} From a719e65b6e7028d8ef7c452a116b3d10751e1104 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 6 Jan 2024 08:51:10 -0800 Subject: [PATCH 04/36] remove old uncommented line --- widget/radio_group_internal_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/widget/radio_group_internal_test.go b/widget/radio_group_internal_test.go index e3ed54e527..849dff514a 100644 --- a/widget/radio_group_internal_test.go +++ b/widget/radio_group_internal_test.go @@ -294,7 +294,6 @@ func TestRadioGroup_Required(t *testing.T) { radioGroupTestTapItem(t, radio, 0) assert.Equal(t, "Hi", radio.Selected, "tapping selected option of required radio does nothing") radioGroupTestTapItem(t, radio, 1) - //radio.items[1].Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), radio.Size().Height-theme.Padding())}) assert.Equal(t, "There", radio.Selected) } From d6bc1e2905d6ee55aa3ac0df28227fd907cce42e Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 6 Jan 2024 08:53:39 -0800 Subject: [PATCH 05/36] remove another old commented line --- widget/radio_group_extended_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/widget/radio_group_extended_test.go b/widget/radio_group_extended_test.go index 567cfe1fb7..59bf0228b8 100644 --- a/widget/radio_group_extended_test.go +++ b/widget/radio_group_extended_test.go @@ -22,7 +22,6 @@ func newextendedRadioGroup(opts []string, f func(string)) *extendedRadioGroup { ret.Options = opts ret.OnChanged = f ret.ExtendBaseWidget(ret) - //ret.update() // Not needed for extending Radio but for the tests to be able to access items without creating a renderer first. return ret } From ca10bbe0e7a03a2e98ee20bbc0de5c09f1c6069c Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 6 Jan 2024 08:57:36 -0800 Subject: [PATCH 06/36] Refresh in radio test to set up selectedIndex --- widget/radio_group_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/widget/radio_group_test.go b/widget/radio_group_test.go index a51639b29a..dbea6f6e01 100644 --- a/widget/radio_group_test.go +++ b/widget/radio_group_test.go @@ -151,6 +151,7 @@ func TestRadioGroup_Layout(t *testing.T) { Options: tt.options, Selected: tt.selected, } + radio.Refresh() // set up selectedIndex if tt.disabled { radio.Disable() } From f451eab559ca1e0eeefcf6e563dd37924aaded23 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 3 Jan 2024 11:36:32 +0100 Subject: [PATCH 07/36] Try to use atomics in canvas baseObject --- canvas/base.go | 50 ++++++++++++++++++++++--------------------------- canvas/image.go | 2 +- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/canvas/base.go b/canvas/base.go index 25e6a5222b..2f883a28e4 100644 --- a/canvas/base.go +++ b/canvas/base.go @@ -9,16 +9,17 @@ package canvas // import "fyne.io/fyne/v2/canvas" import ( "sync" + "sync/atomic" "fyne.io/fyne/v2" ) type baseObject struct { - size fyne.Size // The current size of the canvas object - position fyne.Position // The current position of the object - Hidden bool // Is this object currently hidden + size atomic.Pointer[fyne.Size] // The current size of the canvas object + position atomic.Pointer[fyne.Position] // The current position of the object + Hidden bool // Is this object currently hidden - min fyne.Size // The minimum size this object can be + min atomic.Pointer[fyne.Size] // The minimum size this object can be propertyLock sync.RWMutex } @@ -33,46 +34,37 @@ func (o *baseObject) Hide() { // MinSize returns the specified minimum size, if set, or {1, 1} otherwise. func (o *baseObject) MinSize() fyne.Size { - o.propertyLock.RLock() - defer o.propertyLock.RUnlock() - - if o.min.Width == 0 && o.min.Height == 0 { - return fyne.NewSize(1, 1) + min := o.min.Load() + if min == nil || ((*min).Width == 0 && (*min).Height == 0) { + return fyne.Size{Width: 1, Height: 1} } - return o.min + return *min } // Move the object to a new position, relative to its parent. func (o *baseObject) Move(pos fyne.Position) { - o.propertyLock.Lock() - defer o.propertyLock.Unlock() - - o.position = pos + o.position.Store(&pos) } // Position gets the current position of this canvas object, relative to its parent. func (o *baseObject) Position() fyne.Position { - o.propertyLock.RLock() - defer o.propertyLock.RUnlock() + pos := o.position.Load() + if pos == nil { + return fyne.Position{} + } - return o.position + return *pos } // Resize sets a new size for the canvas object. func (o *baseObject) Resize(size fyne.Size) { - o.propertyLock.Lock() - defer o.propertyLock.Unlock() - - o.size = size + o.size.Store(&size) } // SetMinSize specifies the smallest size this object should be. func (o *baseObject) SetMinSize(size fyne.Size) { - o.propertyLock.Lock() - defer o.propertyLock.Unlock() - - o.min = size + o.min.Store(&size) } // Show will set this object to be visible. @@ -85,10 +77,12 @@ func (o *baseObject) Show() { // Size returns the current size of this canvas object. func (o *baseObject) Size() fyne.Size { - o.propertyLock.RLock() - defer o.propertyLock.RUnlock() + size := o.size.Load() + if size == nil { + return fyne.Size{} + } - return o.size + return *size } // Visible returns true if this object is visible, false otherwise. diff --git a/canvas/image.go b/canvas/image.go index 1c46b40087..fa2bb95113 100644 --- a/canvas/image.go +++ b/canvas/image.go @@ -176,7 +176,7 @@ func (i *Image) Resize(s fyne.Size) { return } i.baseObject.Resize(s) - if i.FillMode == ImageFillOriginal && i.size.Height > 2 { // we can just ask for a GPU redraw to align + if i.FillMode == ImageFillOriginal && i.Size().Height > 2 { // we can just ask for a GPU redraw to align Refresh(i) return } From 53b53916582d62dea9026368d4345e1e5ab77ec0 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 3 Jan 2024 14:07:59 +0100 Subject: [PATCH 08/36] Don't escape fyne.Position and fyne.Size to heap --- canvas/base.go | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/canvas/base.go b/canvas/base.go index 2f883a28e4..a345682d3b 100644 --- a/canvas/base.go +++ b/canvas/base.go @@ -8,6 +8,7 @@ package canvas // import "fyne.io/fyne/v2/canvas" import ( + "math" "sync" "sync/atomic" @@ -15,11 +16,11 @@ import ( ) type baseObject struct { - size atomic.Pointer[fyne.Size] // The current size of the canvas object - position atomic.Pointer[fyne.Position] // The current position of the object - Hidden bool // Is this object currently hidden + size atomic.Uint64 // The current size of the canvas object + position atomic.Uint64 // The current position of the object + Hidden bool // Is this object currently hidden - min atomic.Pointer[fyne.Size] // The minimum size this object can be + min atomic.Uint64 // The minimum size this object can be propertyLock sync.RWMutex } @@ -35,36 +36,36 @@ func (o *baseObject) Hide() { // MinSize returns the specified minimum size, if set, or {1, 1} otherwise. func (o *baseObject) MinSize() fyne.Size { min := o.min.Load() - if min == nil || ((*min).Width == 0 && (*min).Height == 0) { + if min == 0 { return fyne.Size{Width: 1, Height: 1} } - return *min + return fyne.NewSize(twoFloat32FromUint64(min)) } // Move the object to a new position, relative to its parent. func (o *baseObject) Move(pos fyne.Position) { - o.position.Store(&pos) + o.position.Store(uint64fromTwoFloat32(pos.X, pos.Y)) } // Position gets the current position of this canvas object, relative to its parent. func (o *baseObject) Position() fyne.Position { pos := o.position.Load() - if pos == nil { + if pos == 0 { return fyne.Position{} } - return *pos + return fyne.NewPos(twoFloat32FromUint64(pos)) } // Resize sets a new size for the canvas object. func (o *baseObject) Resize(size fyne.Size) { - o.size.Store(&size) + o.size.Store(uint64fromTwoFloat32(size.Width, size.Height)) } // SetMinSize specifies the smallest size this object should be. func (o *baseObject) SetMinSize(size fyne.Size) { - o.min.Store(&size) + o.min.Store(uint64fromTwoFloat32(size.Width, size.Height)) } // Show will set this object to be visible. @@ -77,12 +78,7 @@ func (o *baseObject) Show() { // Size returns the current size of this canvas object. func (o *baseObject) Size() fyne.Size { - size := o.size.Load() - if size == nil { - return fyne.Size{} - } - - return *size + return fyne.NewSize(twoFloat32FromUint64(o.size.Load())) } // Visible returns true if this object is visible, false otherwise. @@ -92,3 +88,15 @@ func (o *baseObject) Visible() bool { return !o.Hidden } + +func uint64fromTwoFloat32(a, b float32) uint64 { + x := uint64(math.Float32bits(a)) + y := uint64(math.Float32bits(b)) + return (y << 32) | x +} + +func twoFloat32FromUint64(combined uint64) (float32, float32) { + x := uint32(combined & 0x00000000FFFFFFFF) + y := uint32(combined & 0xFFFFFFFF00000000 >> 32) + return math.Float32frombits(x), math.Float32frombits(y) +} From 3870d57bf41e382e0ab8b72f55a61a4d302f5f72 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 3 Jan 2024 14:11:26 +0100 Subject: [PATCH 09/36] Remove unneeded if-statement --- canvas/base.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/canvas/base.go b/canvas/base.go index a345682d3b..f8b03b5ae1 100644 --- a/canvas/base.go +++ b/canvas/base.go @@ -50,12 +50,7 @@ func (o *baseObject) Move(pos fyne.Position) { // Position gets the current position of this canvas object, relative to its parent. func (o *baseObject) Position() fyne.Position { - pos := o.position.Load() - if pos == 0 { - return fyne.Position{} - } - - return fyne.NewPos(twoFloat32FromUint64(pos)) + return fyne.NewPos(twoFloat32FromUint64(o.position.Load())) } // Resize sets a new size for the canvas object. From 06811b8908dc6a068356f6786e335f5dcf96dc09 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 3 Jan 2024 14:29:41 +0100 Subject: [PATCH 10/36] Apply atomics to insternal basewidget also --- internal/widget/base.go | 46 ++++++++++++++++---------------- internal/widget/scroller.go | 10 +++---- internal/widget/scroller_test.go | 4 +-- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/internal/widget/base.go b/internal/widget/base.go index 3e60143d33..c647535596 100644 --- a/internal/widget/base.go +++ b/internal/widget/base.go @@ -1,7 +1,9 @@ package widget import ( + "math" "sync" + "sync/atomic" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" @@ -11,8 +13,8 @@ import ( // Base provides a helper that handles basic widget behaviours. type Base struct { hidden bool - position fyne.Position - size fyne.Size + position atomic.Uint64 + size atomic.Uint64 impl fyne.Widget propertyLock sync.RWMutex @@ -32,27 +34,19 @@ func (w *Base) ExtendBaseWidget(wid fyne.Widget) { // Size gets the current size of this widget. func (w *Base) Size() fyne.Size { - w.propertyLock.RLock() - defer w.propertyLock.RUnlock() - - return w.size + return fyne.NewSize(twoFloat32FromUint64(w.size.Load())) } // Resize sets a new size for a widget. // Note this should not be used if the widget is being managed by a Layout within a Container. func (w *Base) Resize(size fyne.Size) { - w.propertyLock.RLock() - baseSize := w.size - impl := w.impl - w.propertyLock.RUnlock() - if baseSize == size { + if size == w.Size() { return } - w.propertyLock.Lock() - w.size = size - w.propertyLock.Unlock() + w.size.Store(uint64fromTwoFloat32(size.Width, size.Height)) + impl := w.super() if impl == nil { return } @@ -61,21 +55,15 @@ func (w *Base) Resize(size fyne.Size) { // Position gets the current position of this widget, relative to its parent. func (w *Base) Position() fyne.Position { - w.propertyLock.RLock() - defer w.propertyLock.RUnlock() - - return w.position + return fyne.NewPos(twoFloat32FromUint64(w.position.Load())) } // Move the widget to a new position, relative to its parent. // Note this should not be used if the widget is being managed by a Layout within a Container. func (w *Base) Move(pos fyne.Position) { - w.propertyLock.Lock() - w.position = pos - impl := w.impl - w.propertyLock.Unlock() + w.position.Store(uint64fromTwoFloat32(pos.X, pos.Y)) - Repaint(impl) + Repaint(w.super()) } // MinSize for the widget - it should never be resized below this value. @@ -176,3 +164,15 @@ func Repaint(obj fyne.CanvasObject) { } } } + +func uint64fromTwoFloat32(a, b float32) uint64 { + x := uint64(math.Float32bits(a)) + y := uint64(math.Float32bits(b)) + return (y << 32) | x +} + +func twoFloat32FromUint64(combined uint64) (float32, float32) { + x := uint32(combined & 0x00000000FFFFFFFF) + y := uint32(combined & 0xFFFFFFFF00000000 >> 32) + return math.Float32frombits(x), math.Float32frombits(y) +} diff --git a/internal/widget/scroller.go b/internal/widget/scroller.go index ccc2bc9340..9daa021dd3 100644 --- a/internal/widget/scroller.go +++ b/internal/widget/scroller.go @@ -256,7 +256,7 @@ func (r *scrollContainerRenderer) layoutBars(size fyne.Size) { r.vertArea.Move(fyne.NewPos(r.scroll.Size().Width-r.vertArea.Size().Width, 0)) r.topShadow.Resize(fyne.NewSize(size.Width, 0)) r.bottomShadow.Resize(fyne.NewSize(size.Width, 0)) - r.bottomShadow.Move(fyne.NewPos(0, r.scroll.size.Height)) + r.bottomShadow.Move(fyne.NewPos(0, r.scroll.Size().Height)) } if r.scroll.Direction == ScrollHorizontalOnly || r.scroll.Direction == ScrollBoth { @@ -264,7 +264,7 @@ func (r *scrollContainerRenderer) layoutBars(size fyne.Size) { r.horizArea.Move(fyne.NewPos(0, r.scroll.Size().Height-r.horizArea.Size().Height)) r.leftShadow.Resize(fyne.NewSize(0, size.Height)) r.rightShadow.Resize(fyne.NewSize(0, size.Height)) - r.rightShadow.Move(fyne.NewPos(r.scroll.size.Width, 0)) + r.rightShadow.Move(fyne.NewPos(r.scroll.Size().Width, 0)) } r.updatePosition() @@ -334,7 +334,7 @@ func (r *scrollContainerRenderer) updatePosition() { if r.scroll.Direction == ScrollVerticalOnly || r.scroll.Direction == ScrollBoth { r.handleAreaVisibility(contentSize.Height, scrollSize.Height, r.vertArea) r.handleShadowVisibility(r.scroll.Offset.Y, contentSize.Height, scrollSize.Height, r.topShadow, r.bottomShadow) - cache.Renderer(r.vertArea).Layout(r.scroll.size) + cache.Renderer(r.vertArea).Layout(r.scroll.Size()) } else { r.vertArea.Hide() r.topShadow.Hide() @@ -343,7 +343,7 @@ func (r *scrollContainerRenderer) updatePosition() { if r.scroll.Direction == ScrollHorizontalOnly || r.scroll.Direction == ScrollBoth { r.handleAreaVisibility(contentSize.Width, scrollSize.Width, r.horizArea) r.handleShadowVisibility(r.scroll.Offset.X, contentSize.Width, scrollSize.Width, r.leftShadow, r.rightShadow) - cache.Renderer(r.horizArea).Layout(r.scroll.size) + cache.Renderer(r.horizArea).Layout(r.scroll.Size()) } else { r.horizArea.Hide() r.leftShadow.Hide() @@ -446,7 +446,7 @@ func (s *Scroll) Refresh() { // Resize is called when this scroller should change size. We refresh to ensure the scroll bars are updated. func (s *Scroll) Resize(sz fyne.Size) { - if sz == s.size { + if sz == s.Size() { return } diff --git a/internal/widget/scroller_test.go b/internal/widget/scroller_test.go index 6fae239e26..a8a5e81963 100644 --- a/internal/widget/scroller_test.go +++ b/internal/widget/scroller_test.go @@ -396,7 +396,7 @@ func TestScrollContainer_ShowShadowOnRightIfContentCanScroll(t *testing.T) { scroll.Resize(fyne.NewSize(100, 100)) r := cache.Renderer(scroll).(*scrollContainerRenderer) assert.True(t, r.rightShadow.Visible()) - assert.Equal(t, scroll.size.Width, r.rightShadow.Position().X+r.rightShadow.Size().Width) + assert.Equal(t, scroll.Size().Width, r.rightShadow.Position().X+r.rightShadow.Size().Width) scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DX: -400}}) assert.False(t, r.rightShadow.Visible()) @@ -428,7 +428,7 @@ func TestScrollContainer_ShowShadowOnBottomIfContentCanScroll(t *testing.T) { scroll.Resize(fyne.NewSize(100, 100)) r := cache.Renderer(scroll).(*scrollContainerRenderer) assert.True(t, r.bottomShadow.Visible()) - assert.Equal(t, scroll.size.Height, r.bottomShadow.Position().Y+r.bottomShadow.Size().Height) + assert.Equal(t, scroll.Size().Height, r.bottomShadow.Position().Y+r.bottomShadow.Size().Height) scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DY: -400}}) assert.False(t, r.bottomShadow.Visible()) From 668295cc30818f98c1665ad5196c2bdd7e14f4ed Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 3 Jan 2024 14:34:21 +0100 Subject: [PATCH 11/36] Remove the lock from internal basewidget --- internal/widget/base.go | 54 +++++++++++++---------------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/internal/widget/base.go b/internal/widget/base.go index c647535596..2e7044ba7c 100644 --- a/internal/widget/base.go +++ b/internal/widget/base.go @@ -2,7 +2,6 @@ package widget import ( "math" - "sync" "sync/atomic" "fyne.io/fyne/v2" @@ -12,12 +11,10 @@ import ( // Base provides a helper that handles basic widget behaviours. type Base struct { - hidden bool + hidden atomic.Bool position atomic.Uint64 size atomic.Uint64 - - impl fyne.Widget - propertyLock sync.RWMutex + impl atomic.Pointer[fyne.Widget] } // ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality. @@ -27,9 +24,7 @@ func (w *Base) ExtendBaseWidget(wid fyne.Widget) { return } - w.propertyLock.Lock() - defer w.propertyLock.Unlock() - w.impl = wid + w.impl.Store(&wid) } // Size gets the current size of this widget. @@ -81,10 +76,7 @@ func (w *Base) MinSize() fyne.Size { // Visible returns whether or not this widget should be visible. // Note that this may not mean it is currently visible if a parent has been hidden. func (w *Base) Visible() bool { - w.propertyLock.RLock() - defer w.propertyLock.RUnlock() - - return !w.hidden + return !w.hidden.Load() } // Show this widget so it becomes visible @@ -93,9 +85,11 @@ func (w *Base) Show() { return } - w.setFieldsAndRefresh(func() { - w.hidden = false - }) + impl := w.super() + if impl == nil { + return + } + impl.Refresh() } // Hide this widget so it is no longer visible @@ -104,11 +98,9 @@ func (w *Base) Hide() { return } - w.propertyLock.Lock() - w.hidden = true - impl := w.impl - w.propertyLock.Unlock() + w.hidden.Store(true) + impl := w.super() if impl == nil { return } @@ -126,27 +118,15 @@ func (w *Base) Refresh() { render.Refresh() } -// 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. -func (w *Base) setFieldsAndRefresh(f func()) { - w.propertyLock.Lock() - f() - impl := w.impl - w.propertyLock.Unlock() - - if impl == nil { - return - } - impl.Refresh() -} - // 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 { - w.propertyLock.RLock() - impl := w.impl - w.propertyLock.RUnlock() - return impl + impl := w.impl.Load() + if impl == nil { + return nil + } + + return *impl } // Repaint instructs the containing canvas to redraw, even if nothing changed. From 5e47e955f7bb815b41625aa70859ec866e6fb932 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 3 Jan 2024 14:56:51 +0100 Subject: [PATCH 12/36] Fix Show() being broken --- internal/widget/base.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/widget/base.go b/internal/widget/base.go index 2e7044ba7c..a3a080b889 100644 --- a/internal/widget/base.go +++ b/internal/widget/base.go @@ -85,6 +85,8 @@ func (w *Base) Show() { return } + w.hidden.Store(false) + impl := w.super() if impl == nil { return From 29207ddc260f8aed0c46b565b3e1b899221f5556 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 3 Jan 2024 15:06:17 +0100 Subject: [PATCH 13/36] Store super with atomic --- widget/entry.go | 5 +++-- widget/widget.go | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/widget/entry.go b/widget/entry.go index 422b331a37..2f65adb403 100644 --- a/widget/entry.go +++ b/widget/entry.go @@ -302,10 +302,11 @@ func (e *Entry) ExtendBaseWidget(wid fyne.Widget) { return } + e.impl.Store(&wid) + e.propertyLock.Lock() - defer e.propertyLock.Unlock() - e.BaseWidget.impl = wid e.registerShortcut() + e.propertyLock.Unlock() } // FocusGained is called when the Entry has been given focus. diff --git a/widget/widget.go b/widget/widget.go index 127adfdea7..c05ea6cfca 100644 --- a/widget/widget.go +++ b/widget/widget.go @@ -3,6 +3,7 @@ package widget // import "fyne.io/fyne/v2/widget" import ( "sync" + "sync/atomic" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" @@ -16,7 +17,7 @@ type BaseWidget struct { position fyne.Position Hidden bool - impl fyne.Widget + impl atomic.Pointer[fyne.Widget] propertyLock sync.RWMutex } @@ -27,9 +28,7 @@ func (w *BaseWidget) ExtendBaseWidget(wid fyne.Widget) { return } - w.propertyLock.Lock() - defer w.propertyLock.Unlock() - w.impl = wid + w.impl.Store(&wid) } // Size gets the current size of this widget. @@ -45,7 +44,6 @@ func (w *BaseWidget) Size() fyne.Size { func (w *BaseWidget) Resize(size fyne.Size) { w.propertyLock.RLock() baseSize := w.size - impl := w.impl w.propertyLock.RUnlock() if baseSize == size { return @@ -55,6 +53,7 @@ func (w *BaseWidget) Resize(size fyne.Size) { w.size = size w.propertyLock.Unlock() + impl := w.super() if impl == nil { return } @@ -74,10 +73,9 @@ func (w *BaseWidget) Position() fyne.Position { func (w *BaseWidget) Move(pos fyne.Position) { w.propertyLock.Lock() w.position = pos - impl := w.impl w.propertyLock.Unlock() - internalWidget.Repaint(impl) + internalWidget.Repaint(w.super()) } // MinSize for the widget - it should never be resized below this value. @@ -120,9 +118,9 @@ func (w *BaseWidget) Hide() { w.propertyLock.Lock() w.Hidden = true - impl := w.impl w.propertyLock.Unlock() + impl := w.super() if impl == nil { return } @@ -145,9 +143,9 @@ func (w *BaseWidget) Refresh() { func (w *BaseWidget) setFieldsAndRefresh(f func()) { w.propertyLock.Lock() f() - impl := w.impl w.propertyLock.Unlock() + impl := w.super() if impl == nil { return } @@ -157,10 +155,12 @@ func (w *BaseWidget) setFieldsAndRefresh(f func()) { // super will return the actual object that this represents. // If extended then this is the extending widget, otherwise it is nil. func (w *BaseWidget) super() fyne.Widget { - w.propertyLock.RLock() - impl := w.impl - w.propertyLock.RUnlock() - return impl + impl := w.impl.Load() + if impl == nil { + return nil + } + + return *impl } // DisableableWidget describes an extension to BaseWidget which can be disabled. From e63c6ad7c74374ecced08f5d29f2454ed98d7e58 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 3 Jan 2024 15:17:43 +0100 Subject: [PATCH 14/36] Use atomics for position --- widget/widget.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/widget/widget.go b/widget/widget.go index c05ea6cfca..4cffd2ef80 100644 --- a/widget/widget.go +++ b/widget/widget.go @@ -2,6 +2,7 @@ package widget // import "fyne.io/fyne/v2/widget" import ( + "math" "sync" "sync/atomic" @@ -14,7 +15,7 @@ import ( // BaseWidget provides a helper that handles basic widget behaviours. type BaseWidget struct { size fyne.Size - position fyne.Position + position atomic.Uint64 Hidden bool impl atomic.Pointer[fyne.Widget] @@ -62,19 +63,13 @@ func (w *BaseWidget) Resize(size fyne.Size) { // Position gets the current position of this widget, relative to its parent. func (w *BaseWidget) Position() fyne.Position { - w.propertyLock.RLock() - defer w.propertyLock.RUnlock() - - return w.position + return fyne.NewPos(twoFloat32FromUint64(w.position.Load())) } // Move the widget to a new position, relative to its parent. // Note this should not be used if the widget is being managed by a Layout within a Container. func (w *BaseWidget) Move(pos fyne.Position) { - w.propertyLock.Lock() - w.position = pos - w.propertyLock.Unlock() - + w.position.Store(uint64fromTwoFloat32(pos.X, pos.Y)) internalWidget.Repaint(w.super()) } @@ -208,3 +203,15 @@ func (w *DisableableWidget) Disabled() bool { func NewSimpleRenderer(object fyne.CanvasObject) fyne.WidgetRenderer { return internalWidget.NewSimpleRenderer(object) } + +func uint64fromTwoFloat32(a, b float32) uint64 { + x := uint64(math.Float32bits(a)) + y := uint64(math.Float32bits(b)) + return (y << 32) | x +} + +func twoFloat32FromUint64(combined uint64) (float32, float32) { + x := uint32(combined & 0x00000000FFFFFFFF) + y := uint32(combined & 0xFFFFFFFF00000000 >> 32) + return math.Float32frombits(x), math.Float32frombits(y) +} From 4bc2078624e395dc0aea88fb9e014a0a348ea858 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 3 Jan 2024 15:26:27 +0100 Subject: [PATCH 15/36] Use atomics for basewidget size as well --- widget/entry.go | 7 +++---- widget/progressbarinfinite.go | 5 +++-- widget/richtext.go | 15 +++++++-------- widget/select.go | 2 +- widget/slider.go | 9 +++++---- widget/table.go | 21 ++++++++++++--------- widget/textgrid.go | 2 +- widget/tree.go | 22 +++++----------------- widget/widget.go | 16 ++++------------ 9 files changed, 41 insertions(+), 58 deletions(-) diff --git a/widget/entry.go b/widget/entry.go index 2f65adb403..6d51295821 100644 --- a/widget/entry.go +++ b/widget/entry.go @@ -1668,7 +1668,6 @@ func (r *entryRenderer) Refresh() { content := r.entry.content focusedAppearance := r.entry.focused && !r.entry.disabled scroll := r.entry.Scroll - size := r.entry.size wrapping := r.entry.Wrapping r.entry.propertyLock.RUnlock() @@ -1679,7 +1678,7 @@ func (r *entryRenderer) Refresh() { r.entry.placeholder.Refresh() // correct our scroll wrappers if the wrap mode changed - entrySize := size.Subtract(fyne.NewSize(r.trailingInset(), theme.InputBorderSize()*2)) + entrySize := r.entry.Size().Subtract(fyne.NewSize(r.trailingInset(), theme.InputBorderSize()*2)) if wrapping == fyne.TextWrapOff && scroll == widget.ScrollNone && r.scroll.Content != nil { r.scroll.Hide() r.scroll.Content = nil @@ -1742,7 +1741,7 @@ func (r *entryRenderer) ensureValidationSetup() { if r.entry.validationStatus == nil { r.entry.validationStatus = newValidationStatus(r.entry) r.objects = append(r.objects, r.entry.validationStatus) - r.Layout(r.entry.size) + r.Layout(r.entry.Size()) r.entry.Validate() @@ -1774,7 +1773,7 @@ func (e *entryContent) CreateRenderer() fyne.WidgetRenderer { r := &entryContentRenderer{e.entry.cursorAnim.cursor, []fyne.CanvasObject{}, objects, provider, placeholder, e} r.updateScrollDirections() - r.Layout(e.size) + r.Layout(e.Size()) return r } diff --git a/widget/progressbarinfinite.go b/widget/progressbarinfinite.go index 85650de404..2d11989491 100644 --- a/widget/progressbarinfinite.go +++ b/widget/progressbarinfinite.go @@ -34,13 +34,14 @@ func (p *infProgressRenderer) MinSize() fyne.Size { } func (p *infProgressRenderer) updateBar(done float32) { - progressWidth := p.progress.size.Width + size := p.progress.Size() + progressWidth := size.Width spanWidth := progressWidth + (progressWidth * (maxProgressBarInfiniteWidthRatio / 2)) maxBarWidth := progressWidth * maxProgressBarInfiniteWidthRatio barCenterX := spanWidth*done - (spanWidth-progressWidth)/2 barPos := fyne.NewPos(barCenterX-maxBarWidth/2, 0) - barSize := fyne.NewSize(maxBarWidth, p.progress.size.Height) + barSize := fyne.NewSize(maxBarWidth, size.Height) if barPos.X < 0 { barSize.Width += barPos.X barPos.X = 0 diff --git a/widget/richtext.go b/widget/richtext.go index 6b14fb9f71..725e960ed8 100644 --- a/widget/richtext.go +++ b/widget/richtext.go @@ -107,17 +107,16 @@ func (t *RichText) Refresh() { // // Implements: fyne.Widget func (t *RichText) Resize(size fyne.Size) { + if size == t.Size() { + return + } + + t.size.Store(uint64fromTwoFloat32(size.Width, size.Height)) + t.propertyLock.RLock() - baseSize := t.size 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 t.propertyLock.RUnlock() - if baseSize == size { - return - } - t.propertyLock.Lock() - t.size = size - t.propertyLock.Unlock() if skipResize { if len(segments) < 2 { // we can simplify :) @@ -364,7 +363,7 @@ func (t *RichText) updateRowBounds() { t.propertyLock.RLock() var bounds []rowBoundary - maxWidth := t.size.Width - 2*innerPadding + 2*t.inset.Width + maxWidth := t.Size().Width - 2*innerPadding + 2*t.inset.Width wrapWidth := maxWidth var currentBound *rowBoundary diff --git a/widget/select.go b/widget/select.go index d268eb266e..c56ddff570 100644 --- a/widget/select.go +++ b/widget/select.go @@ -334,7 +334,7 @@ func (s *selectRenderer) Refresh() { if s.combo.popUp != nil { s.combo.popUp.alignment = s.combo.Alignment s.combo.popUp.Move(s.combo.popUpPos()) - s.combo.popUp.Resize(fyne.NewSize(s.combo.size.Width, s.combo.popUp.MinSize().Height)) + s.combo.popUp.Resize(fyne.NewSize(s.combo.Size().Width, s.combo.popUp.MinSize().Height)) s.combo.popUp.Refresh() } s.background.Refresh() diff --git a/widget/slider.go b/widget/slider.go index 07c5685123..c9f7a11d16 100644 --- a/widget/slider.go +++ b/widget/slider.go @@ -243,23 +243,24 @@ func (s *Slider) getRatio(e *fyne.PointEvent) float64 { x := e.Position.X y := e.Position.Y + size := s.Size() switch s.Orientation { case Vertical: - if y > s.size.Height-pad { + if y > size.Height-pad { return 0.0 } else if y < pad { return 1.0 } else { - return 1 - float64(y-pad)/float64(s.size.Height-pad*2) + return 1 - float64(y-pad)/float64(size.Height-pad*2) } case Horizontal: - if x > s.size.Width-pad { + if x > size.Width-pad { return 1.0 } else if x < pad { return 0.0 } else { - return float64(x-pad) / float64(s.size.Width-pad*2) + return float64(x-pad) / float64(size.Width-pad*2) } } return 0.0 diff --git a/widget/table.go b/widget/table.go index 85333506cb..ed5d9af988 100644 --- a/widget/table.go +++ b/widget/table.go @@ -854,13 +854,14 @@ func (t *Table) visibleColumnWidths(colWidth float32, cols int) (visible map[int // theme.Padding is a slow call, so we cache it padding := theme.Padding() stick := t.StickyColumnCount + size := t.Size() if len(t.columnWidths) == 0 { paddedWidth := colWidth + padding offX = float32(math.Floor(float64(t.offset.X/paddedWidth))) * paddedWidth minCol = int(math.Floor(float64(offX / paddedWidth))) - maxCol = int(math.Ceil(float64((t.offset.X + t.size.Width) / paddedWidth))) + maxCol = int(math.Ceil(float64((t.offset.X + size.Width) / paddedWidth))) if minCol > cols-1 { minCol = cols - 1 @@ -896,7 +897,7 @@ func (t *Table) visibleColumnWidths(colWidth float32, cols int) (visible map[int offX = colOffset isVisible = true } - if colOffset < t.offset.X+t.size.Width { + if colOffset < t.offset.X+size.Width { maxCol = i + 1 } else { break @@ -954,13 +955,14 @@ func (t *Table) visibleRowHeights(rowHeight float32, rows int) (visible map[int] // theme.Padding is a slow call, so we cache it padding := theme.Padding() stick := t.StickyRowCount + size := t.Size() if len(t.rowHeights) == 0 { paddedHeight := rowHeight + padding offY = float32(math.Floor(float64(t.offset.Y/paddedHeight))) * paddedHeight minRow = int(math.Floor(float64(offY / paddedHeight))) - maxRow = int(math.Ceil(float64((t.offset.Y + t.size.Height) / paddedHeight))) + maxRow = int(math.Ceil(float64((t.offset.Y + size.Height) / paddedHeight))) if minRow > rows-1 { minRow = rows - 1 @@ -996,7 +998,7 @@ func (t *Table) visibleRowHeights(rowHeight float32, rows int) (visible map[int] offY = rowOffset isVisible = true } - if rowOffset < t.offset.Y+t.size.Height { + if rowOffset < t.offset.Y+size.Height { maxRow = i + 1 } else { break @@ -1448,7 +1450,7 @@ func (r *tableCellsRenderer) moveIndicators() { i++ xPos := x + dividerOff - r.dividers[divs].Resize(fyne.NewSize(separatorThickness, r.cells.t.size.Height)) + r.dividers[divs].Resize(fyne.NewSize(separatorThickness, r.cells.t.Size().Height)) r.dividers[divs].Move(fyne.NewPos(xPos, 0)) r.dividers[divs].Show() divs++ @@ -1459,7 +1461,7 @@ func (r *tableCellsRenderer) moveIndicators() { i++ xPos := x - r.cells.t.content.Offset.X + dividerOff - r.dividers[divs].Resize(fyne.NewSize(separatorThickness, r.cells.t.size.Height)) + r.dividers[divs].Resize(fyne.NewSize(separatorThickness, r.cells.t.Size().Height)) r.dividers[divs].Move(fyne.NewPos(xPos, 0)) r.dividers[divs].Show() divs++ @@ -1471,7 +1473,7 @@ func (r *tableCellsRenderer) moveIndicators() { i++ yPos := y + dividerOff - r.dividers[divs].Resize(fyne.NewSize(r.cells.t.size.Width, separatorThickness)) + r.dividers[divs].Resize(fyne.NewSize(r.cells.t.Size().Width, separatorThickness)) r.dividers[divs].Move(fyne.NewPos(0, yPos)) r.dividers[divs].Show() divs++ @@ -1482,7 +1484,7 @@ func (r *tableCellsRenderer) moveIndicators() { i++ yPos := y - r.cells.t.content.Offset.Y + dividerOff - r.dividers[divs].Resize(fyne.NewSize(r.cells.t.size.Width, separatorThickness)) + r.dividers[divs].Resize(fyne.NewSize(r.cells.t.Size().Width, separatorThickness)) r.dividers[divs].Move(fyne.NewPos(0, yPos)) r.dividers[divs].Show() divs++ @@ -1543,7 +1545,8 @@ func (r *tableCellsRenderer) moveMarker(marker fyne.CanvasObject, row, col int, } y2 := y1 + heights[row] - if x2 < 0 || x1 > r.cells.t.size.Width || y2 < 0 || y1 > r.cells.t.size.Height { + size := r.cells.t.Size() + if x2 < 0 || x1 > size.Width || y2 < 0 || y1 > size.Height { marker.Hide() } else { left := x1 diff --git a/widget/textgrid.go b/widget/textgrid.go index 2856719f14..6b68a99707 100644 --- a/widget/textgrid.go +++ b/widget/textgrid.go @@ -537,7 +537,7 @@ func (t *textGridRenderer) Refresh() { t.updateCellSize() TextGridStyleWhitespace = &CustomTextGridStyle{FGColor: theme.DisabledColor()} - t.updateGridSize(t.text.size) + t.updateGridSize(t.text.Size()) t.refreshGrid() } diff --git a/widget/tree.go b/widget/tree.go index bd71c2efb7..f21ed8099a 100644 --- a/widget/tree.go +++ b/widget/tree.go @@ -233,17 +233,11 @@ func (t *Tree) OpenBranch(uid TreeNodeID) { // Resize sets a new size for a widget. func (t *Tree) Resize(size fyne.Size) { - t.propertyLock.RLock() - s := t.size - t.propertyLock.RUnlock() - - if s == size { + if size == t.Size() { return } - t.propertyLock.Lock() - t.size = size - t.propertyLock.Unlock() + t.size.Store(uint64fromTwoFloat32(size.Width, size.Height)) t.Refresh() // trigger a redraw } @@ -592,17 +586,11 @@ func (c *treeContent) CreateRenderer() fyne.WidgetRenderer { } func (c *treeContent) Resize(size fyne.Size) { - c.propertyLock.RLock() - s := c.size - c.propertyLock.RUnlock() - - if s == size { + if size == c.Size() { return } - c.propertyLock.Lock() - c.size = size - c.propertyLock.Unlock() + c.size.Store(uint64fromTwoFloat32(size.Width, size.Height)) c.Refresh() // trigger a redraw } @@ -976,7 +964,7 @@ func (r *treeNodeRenderer) partialRefresh() { r.background.Hide() } r.background.Refresh() - r.Layout(r.treeNode.size) + r.Layout(r.treeNode.Size()) canvas.Refresh(r.treeNode.super()) } diff --git a/widget/widget.go b/widget/widget.go index 4cffd2ef80..0fed691f21 100644 --- a/widget/widget.go +++ b/widget/widget.go @@ -14,7 +14,7 @@ import ( // BaseWidget provides a helper that handles basic widget behaviours. type BaseWidget struct { - size fyne.Size + size atomic.Uint64 position atomic.Uint64 Hidden bool @@ -34,25 +34,17 @@ func (w *BaseWidget) ExtendBaseWidget(wid fyne.Widget) { // Size gets the current size of this widget. func (w *BaseWidget) Size() fyne.Size { - w.propertyLock.RLock() - defer w.propertyLock.RUnlock() - - return w.size + return fyne.NewSize(twoFloat32FromUint64(w.size.Load())) } // Resize sets a new size for a widget. // Note this should not be used if the widget is being managed by a Layout within a Container. func (w *BaseWidget) Resize(size fyne.Size) { - w.propertyLock.RLock() - baseSize := w.size - w.propertyLock.RUnlock() - if baseSize == size { + if size == w.Size() { return } - w.propertyLock.Lock() - w.size = size - w.propertyLock.Unlock() + w.size.Store(uint64fromTwoFloat32(size.Width, size.Height)) impl := w.super() if impl == nil { From e85aae6f60cdb82fefb95f2aee467f0db8383c49 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 3 Jan 2024 15:32:47 +0100 Subject: [PATCH 16/36] Use atomics for disabled widget --- widget/button.go | 2 +- widget/check.go | 6 +++--- widget/check_group.go | 4 ++-- widget/check_internal_test.go | 4 ++-- widget/entry.go | 11 ++++++----- widget/entry_password.go | 2 +- widget/entry_validation.go | 2 +- widget/radio_group.go | 4 ++-- widget/select.go | 8 ++++---- widget/widget.go | 27 ++++++++++++++++----------- 10 files changed, 38 insertions(+), 32 deletions(-) diff --git a/widget/button.go b/widget/button.go index 40d5dad6b1..7d79fa8fac 100644 --- a/widget/button.go +++ b/widget/button.go @@ -358,7 +358,7 @@ func (r *buttonRenderer) applyTheme() { r.button.applyButtonTheme() r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameForeground switch { - case r.button.disabled: + case r.button.Disabled(): r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameDisabled case r.button.Importance == HighImportance || r.button.Importance == DangerImportance || r.button.Importance == WarningImportance || r.button.Importance == SuccessImportance: if r.button.focused { diff --git a/widget/check.go b/widget/check.go index 19d7dbced4..f22124ea43 100644 --- a/widget/check.go +++ b/widget/check.go @@ -55,7 +55,7 @@ func (c *checkRenderer) Layout(size fyne.Size) { func (c *checkRenderer) applyTheme() { c.label.Color = theme.ForegroundColor() c.label.TextSize = theme.TextSize() - if c.check.disabled { + if c.check.Disabled() { c.label.Color = theme.DisabledColor() } } @@ -86,7 +86,7 @@ func (c *checkRenderer) updateResource() { res.ColorName = theme.ColorNamePrimary bgRes.ColorName = theme.ColorNameBackground } - if c.check.disabled { + if c.check.Disabled() { if c.check.Checked { res = theme.NewThemedResource(theme.CheckButtonCheckedIcon()) } @@ -98,7 +98,7 @@ func (c *checkRenderer) updateResource() { } func (c *checkRenderer) updateFocusIndicator() { - if c.check.disabled { + if c.check.Disabled() { c.focusIndicator.FillColor = color.Transparent } else if c.check.focused { c.focusIndicator.FillColor = theme.FocusColor() diff --git a/widget/check_group.go b/widget/check_group.go index 7fc6430030..2b559a3694 100644 --- a/widget/check_group.go +++ b/widget/check_group.go @@ -168,7 +168,7 @@ func (r *CheckGroup) update() { item.Text = r.Options[i] item.Checked = contains - item.DisableableWidget.disabled = r.disabled + item.DisableableWidget.disabled.Store(r.Disabled()) item.Refresh() } } @@ -261,7 +261,7 @@ func (r *checkGroupRenderer) updateItems() { } item.Text = r.checks.Options[i] item.Checked = contains - item.disabled = r.checks.disabled + item.disabled.Store(r.checks.Disabled()) item.Refresh() } } diff --git a/widget/check_internal_test.go b/widget/check_internal_test.go index 6047db9ac1..2ec4a5a8d9 100644 --- a/widget/check_internal_test.go +++ b/widget/check_internal_test.go @@ -147,7 +147,7 @@ func TestCheck_Focused(t *testing.T) { } check.Disable() - assert.True(t, check.disabled) + assert.True(t, check.Disabled()) assert.Equal(t, color.Transparent, render.focusIndicator.FillColor) check.Enable() @@ -185,7 +185,7 @@ func TestCheck_Hovered(t *testing.T) { } check.Disable() - assert.True(t, check.disabled) + assert.True(t, check.Disabled()) assert.True(t, check.hovered) assert.Equal(t, color.Transparent, render.focusIndicator.FillColor) diff --git a/widget/entry.go b/widget/entry.go index 6d51295821..62e8c7f154 100644 --- a/widget/entry.go +++ b/widget/entry.go @@ -226,7 +226,7 @@ func (e *Entry) Disable() { // // Implements: fyne.Disableable func (e *Entry) Disabled() bool { - return e.DisableableWidget.disabled + return e.DisableableWidget.Disabled() } // DoubleTapped is called when this entry has been double tapped so we should select text below the pointer @@ -1305,7 +1305,8 @@ func (e *Entry) textPosFromRowCol(row, col int) int { func (e *Entry) syncSegments() { colName := theme.ColorNameForeground wrap := e.textWrap() - if e.disabled { + disabled := e.Disabled() + if disabled { colName = theme.ColorNameDisabled } e.textProvider().Wrapping = wrap @@ -1324,7 +1325,7 @@ func (e *Entry) syncSegments() { Text: e.Text, }} colName = theme.ColorNamePlaceHolder - if e.disabled { + if disabled { colName = theme.ColorNameDisabled } e.placeholderProvider().Wrapping = wrap @@ -1666,7 +1667,7 @@ func (r *entryRenderer) Objects() []fyne.CanvasObject { func (r *entryRenderer) Refresh() { r.entry.propertyLock.RLock() content := r.entry.content - focusedAppearance := r.entry.focused && !r.entry.disabled + focusedAppearance := r.entry.focused && !r.entry.Disabled() scroll := r.entry.Scroll wrapping := r.entry.Wrapping r.entry.propertyLock.RUnlock() @@ -1842,7 +1843,7 @@ func (r *entryContentRenderer) Refresh() { provider := r.content.entry.textProvider() placeholder := r.content.entry.placeholderProvider() focused := r.content.entry.focused - focusedAppearance := focused && !r.content.entry.disabled + focusedAppearance := focused && !r.content.entry.disabled.Load() selections := r.selection r.updateScrollDirections() r.content.entry.propertyLock.RUnlock() diff --git a/widget/entry_password.go b/widget/entry_password.go index 501925667c..5e9bae09ba 100644 --- a/widget/entry_password.go +++ b/widget/entry_password.go @@ -76,7 +76,7 @@ func (r *passwordRevealerRenderer) Refresh() { r.icon.Resource = theme.VisibilityOffIcon() } - if r.entry.disabled { + if r.entry.Disabled() { r.icon.Resource = theme.NewDisabledResource(r.icon.Resource) } canvas.Refresh(r.icon) diff --git a/widget/entry_validation.go b/widget/entry_validation.go index caec2f8a33..8aa14e76c6 100644 --- a/widget/entry_validation.go +++ b/widget/entry_validation.go @@ -93,7 +93,7 @@ func (r *validationStatusRenderer) MinSize() fyne.Size { func (r *validationStatusRenderer) Refresh() { r.entry.propertyLock.RLock() defer r.entry.propertyLock.RUnlock() - if r.entry.disabled { + if r.entry.Disabled() { r.icon.Hide() return } diff --git a/widget/radio_group.go b/widget/radio_group.go index d28572b311..8b3a31e43a 100644 --- a/widget/radio_group.go +++ b/widget/radio_group.go @@ -124,7 +124,7 @@ func (r *RadioGroup) update() { for i, item := range r.items { item.Label = r.Options[i] item.Selected = item.Label == r.Selected - item.DisableableWidget.disabled = r.disabled + item.DisableableWidget.disabled.Store(r.Disabled()) item.Refresh() } } @@ -207,7 +207,7 @@ func (r *radioGroupRenderer) updateItems() { for i, item := range r.items { item.Label = r.radio.Options[i] item.Selected = item.Label == r.radio.Selected - item.disabled = r.radio.disabled + item.disabled.Store(r.radio.Disabled()) item.Refresh() } } diff --git a/widget/select.go b/widget/select.go index c56ddff570..19a68091fd 100644 --- a/widget/select.go +++ b/widget/select.go @@ -66,7 +66,7 @@ func (s *Select) CreateRenderer() fyne.WidgetRenderer { txtProv.inset = fyne.NewSize(theme.Padding(), theme.Padding()) txtProv.ExtendBaseWidget(txtProv) txtProv.Truncation = fyne.TextTruncateEllipsis - if s.disabled { + if s.Disabled() { txtProv.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameDisabled } @@ -342,7 +342,7 @@ func (s *selectRenderer) Refresh() { } func (s *selectRenderer) bgColor() color.Color { - if s.combo.disabled { + if s.combo.Disabled() { return theme.DisabledButtonColor() } if s.combo.focused { @@ -355,7 +355,7 @@ func (s *selectRenderer) bgColor() color.Color { } func (s *selectRenderer) updateIcon() { - if s.combo.disabled { + if s.combo.Disabled() { s.icon.Resource = theme.NewDisabledResource(theme.MenuDropDownIcon()) } else { s.icon.Resource = theme.MenuDropDownIcon() @@ -369,7 +369,7 @@ func (s *selectRenderer) updateLabel() { } s.label.Segments[0].(*TextSegment).Style.Alignment = s.combo.Alignment - if s.combo.disabled { + if s.combo.Disabled() { s.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameDisabled } else { s.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameForeground diff --git a/widget/widget.go b/widget/widget.go index 0fed691f21..adf0d07927 100644 --- a/widget/widget.go +++ b/widget/widget.go @@ -155,7 +155,7 @@ func (w *BaseWidget) super() fyne.Widget { type DisableableWidget struct { BaseWidget - disabled bool + disabled atomic.Bool } // Enable this widget, updating any style or features appropriately. @@ -164,9 +164,13 @@ func (w *DisableableWidget) Enable() { return } - w.setFieldsAndRefresh(func() { - w.disabled = false - }) + w.disabled.Store(false) + + impl := w.super() + if impl == nil { + return + } + impl.Refresh() } // Disable this widget so that it cannot be interacted with, updating any style appropriately. @@ -175,17 +179,18 @@ func (w *DisableableWidget) Disable() { return } - w.setFieldsAndRefresh(func() { - w.disabled = true - }) + w.disabled.Store(true) + + impl := w.super() + if impl == nil { + return + } + impl.Refresh() } // Disabled returns true if this widget is currently disabled or false if it can currently be interacted with. func (w *DisableableWidget) Disabled() bool { - w.propertyLock.RLock() - defer w.propertyLock.RUnlock() - - return w.disabled + return w.disabled.Load() } // NewSimpleRenderer creates a new SimpleRenderer to render a widget using a From 4ecc2b273d8dd9782e67ddc5934f4a7febcc2a11 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sat, 6 Jan 2024 18:35:53 +0100 Subject: [PATCH 17/36] Simplify atomics with CAS operation --- internal/widget/base.go | 12 ++++-------- widget/widget.go | 14 +++++--------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/internal/widget/base.go b/internal/widget/base.go index a3a080b889..d3c7e16302 100644 --- a/internal/widget/base.go +++ b/internal/widget/base.go @@ -81,12 +81,10 @@ func (w *Base) Visible() bool { // Show this widget so it becomes visible func (w *Base) Show() { - if w.Visible() { - return + if !w.hidden.CompareAndSwap(true, false) { + return // Visible already } - w.hidden.Store(false) - impl := w.super() if impl == nil { return @@ -96,12 +94,10 @@ func (w *Base) Show() { // Hide this widget so it is no longer visible func (w *Base) Hide() { - if !w.Visible() { - return + if !w.hidden.CompareAndSwap(false, true) { + return // Hidden already } - w.hidden.Store(true) - impl := w.super() if impl == nil { return diff --git a/widget/widget.go b/widget/widget.go index adf0d07927..6bab4583f3 100644 --- a/widget/widget.go +++ b/widget/widget.go @@ -71,7 +71,7 @@ func (w *BaseWidget) MinSize() fyne.Size { r := cache.Renderer(impl) if r == nil { - return fyne.NewSize(0, 0) + return fyne.Size{} } return r.MinSize() @@ -160,12 +160,10 @@ type DisableableWidget struct { // Enable this widget, updating any style or features appropriately. func (w *DisableableWidget) Enable() { - if !w.Disabled() { - return + if !w.disabled.CompareAndSwap(true, false) { + return // Enabled already } - w.disabled.Store(false) - impl := w.super() if impl == nil { return @@ -175,12 +173,10 @@ func (w *DisableableWidget) Enable() { // Disable this widget so that it cannot be interacted with, updating any style appropriately. func (w *DisableableWidget) Disable() { - if w.Disabled() { - return + if !w.disabled.CompareAndSwap(false, true) { + return // Disabled already } - w.disabled.Store(true) - impl := w.super() if impl == nil { return From 3f68b2ceab8fcca98e5af7dc395f85bb20a75599 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sat, 6 Jan 2024 18:59:17 +0100 Subject: [PATCH 18/36] New async size and pos to reduce code duplication --- canvas/base.go | 37 ++++++++++------------------ internal/async/vector.go | 52 ++++++++++++++++++++++++++++++++++++++++ internal/widget/base.go | 26 ++++++-------------- widget/richtext.go | 2 +- widget/tree.go | 4 ++-- widget/widget.go | 26 ++++++-------------- 6 files changed, 81 insertions(+), 66 deletions(-) create mode 100644 internal/async/vector.go diff --git a/canvas/base.go b/canvas/base.go index f8b03b5ae1..0be24dbcd0 100644 --- a/canvas/base.go +++ b/canvas/base.go @@ -8,19 +8,18 @@ package canvas // import "fyne.io/fyne/v2/canvas" import ( - "math" "sync" - "sync/atomic" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" ) type baseObject struct { - size atomic.Uint64 // The current size of the canvas object - position atomic.Uint64 // The current position of the object - Hidden bool // Is this object currently hidden + size async.Size // The current size of the canvas object + position async.Position // The current position of the object + Hidden bool // Is this object currently hidden - min atomic.Uint64 // The minimum size this object can be + min async.Size // The minimum size this object can be propertyLock sync.RWMutex } @@ -36,31 +35,31 @@ func (o *baseObject) Hide() { // MinSize returns the specified minimum size, if set, or {1, 1} otherwise. func (o *baseObject) MinSize() fyne.Size { min := o.min.Load() - if min == 0 { + if min.IsZero() { return fyne.Size{Width: 1, Height: 1} } - return fyne.NewSize(twoFloat32FromUint64(min)) + return min } // Move the object to a new position, relative to its parent. func (o *baseObject) Move(pos fyne.Position) { - o.position.Store(uint64fromTwoFloat32(pos.X, pos.Y)) + o.position.Store(pos) } // Position gets the current position of this canvas object, relative to its parent. func (o *baseObject) Position() fyne.Position { - return fyne.NewPos(twoFloat32FromUint64(o.position.Load())) + return o.position.Load() } // Resize sets a new size for the canvas object. func (o *baseObject) Resize(size fyne.Size) { - o.size.Store(uint64fromTwoFloat32(size.Width, size.Height)) + o.size.Store(size) } // SetMinSize specifies the smallest size this object should be. func (o *baseObject) SetMinSize(size fyne.Size) { - o.min.Store(uint64fromTwoFloat32(size.Width, size.Height)) + o.min.Store(size) } // Show will set this object to be visible. @@ -73,7 +72,7 @@ func (o *baseObject) Show() { // Size returns the current size of this canvas object. func (o *baseObject) Size() fyne.Size { - return fyne.NewSize(twoFloat32FromUint64(o.size.Load())) + return o.size.Load() } // Visible returns true if this object is visible, false otherwise. @@ -83,15 +82,3 @@ func (o *baseObject) Visible() bool { return !o.Hidden } - -func uint64fromTwoFloat32(a, b float32) uint64 { - x := uint64(math.Float32bits(a)) - y := uint64(math.Float32bits(b)) - return (y << 32) | x -} - -func twoFloat32FromUint64(combined uint64) (float32, float32) { - x := uint32(combined & 0x00000000FFFFFFFF) - y := uint32(combined & 0xFFFFFFFF00000000 >> 32) - return math.Float32frombits(x), math.Float32frombits(y) -} diff --git a/internal/async/vector.go b/internal/async/vector.go new file mode 100644 index 0000000000..b859ce00b0 --- /dev/null +++ b/internal/async/vector.go @@ -0,0 +1,52 @@ +package async + +import ( + "math" + "sync/atomic" + + "fyne.io/fyne/v2" +) + +// Position is an atomic version of fyne.Position. +// Loads and stores are guaranteed to happen using a single atomic operation. +type Position struct { + pos atomic.Uint64 +} + +// Load performs an atomic load on the fyne.Position value. +func (p *Position) Load() fyne.Position { + return fyne.NewPos(twoFloat32FromUint64(p.pos.Load())) +} + +// Store performs an atomic store on the fyne.Position value. +func (p *Position) Store(pos fyne.Position) { + p.pos.Store(uint64fromTwoFloat32(pos.X, pos.Y)) +} + +// Size is an atomic version of fyne.Size. +// Loads and stores are guaranteed to happen using a single atomic operation. +type Size struct { + size atomic.Uint64 +} + +// Load performs an atomic load on the fyne.Size value. +func (s *Size) Load() fyne.Size { + return fyne.NewSize(twoFloat32FromUint64(s.size.Load())) +} + +// Store performs an atomic store on the fyne.Size value. +func (s *Size) Store(size fyne.Size) { + s.size.Store(uint64fromTwoFloat32(size.Width, size.Height)) +} + +func uint64fromTwoFloat32(a, b float32) uint64 { + x := uint64(math.Float32bits(a)) + y := uint64(math.Float32bits(b)) + return (y << 32) | x +} + +func twoFloat32FromUint64(combined uint64) (float32, float32) { + x := uint32(combined & 0x00000000FFFFFFFF) + y := uint32(combined & 0xFFFFFFFF00000000 >> 32) + return math.Float32frombits(x), math.Float32frombits(y) +} diff --git a/internal/widget/base.go b/internal/widget/base.go index d3c7e16302..36d2c9aaef 100644 --- a/internal/widget/base.go +++ b/internal/widget/base.go @@ -1,19 +1,19 @@ package widget import ( - "math" "sync/atomic" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/async" "fyne.io/fyne/v2/internal/cache" ) // Base provides a helper that handles basic widget behaviours. type Base struct { hidden atomic.Bool - position atomic.Uint64 - size atomic.Uint64 + position async.Position + size async.Size impl atomic.Pointer[fyne.Widget] } @@ -29,7 +29,7 @@ func (w *Base) ExtendBaseWidget(wid fyne.Widget) { // Size gets the current size of this widget. func (w *Base) Size() fyne.Size { - return fyne.NewSize(twoFloat32FromUint64(w.size.Load())) + return w.size.Load() } // Resize sets a new size for a widget. @@ -39,7 +39,7 @@ func (w *Base) Resize(size fyne.Size) { return } - w.size.Store(uint64fromTwoFloat32(size.Width, size.Height)) + w.size.Store(size) impl := w.super() if impl == nil { @@ -50,13 +50,13 @@ func (w *Base) Resize(size fyne.Size) { // Position gets the current position of this widget, relative to its parent. func (w *Base) Position() fyne.Position { - return fyne.NewPos(twoFloat32FromUint64(w.position.Load())) + return w.position.Load() } // Move the widget to a new position, relative to its parent. // Note this should not be used if the widget is being managed by a Layout within a Container. func (w *Base) Move(pos fyne.Position) { - w.position.Store(uint64fromTwoFloat32(pos.X, pos.Y)) + w.position.Store(pos) Repaint(w.super()) } @@ -142,15 +142,3 @@ func Repaint(obj fyne.CanvasObject) { } } } - -func uint64fromTwoFloat32(a, b float32) uint64 { - x := uint64(math.Float32bits(a)) - y := uint64(math.Float32bits(b)) - return (y << 32) | x -} - -func twoFloat32FromUint64(combined uint64) (float32, float32) { - x := uint32(combined & 0x00000000FFFFFFFF) - y := uint32(combined & 0xFFFFFFFF00000000 >> 32) - return math.Float32frombits(x), math.Float32frombits(y) -} diff --git a/widget/richtext.go b/widget/richtext.go index 725e960ed8..f58dcaa9fe 100644 --- a/widget/richtext.go +++ b/widget/richtext.go @@ -111,7 +111,7 @@ func (t *RichText) Resize(size fyne.Size) { return } - t.size.Store(uint64fromTwoFloat32(size.Width, size.Height)) + t.size.Store(size) t.propertyLock.RLock() segments := t.Segments diff --git a/widget/tree.go b/widget/tree.go index f21ed8099a..ac050c1d06 100644 --- a/widget/tree.go +++ b/widget/tree.go @@ -237,7 +237,7 @@ func (t *Tree) Resize(size fyne.Size) { return } - t.size.Store(uint64fromTwoFloat32(size.Width, size.Height)) + t.size.Store(size) t.Refresh() // trigger a redraw } @@ -590,7 +590,7 @@ func (c *treeContent) Resize(size fyne.Size) { return } - c.size.Store(uint64fromTwoFloat32(size.Width, size.Height)) + c.size.Store(size) c.Refresh() // trigger a redraw } diff --git a/widget/widget.go b/widget/widget.go index 6bab4583f3..1ed97c47ed 100644 --- a/widget/widget.go +++ b/widget/widget.go @@ -2,20 +2,20 @@ package widget // import "fyne.io/fyne/v2/widget" import ( - "math" "sync" "sync/atomic" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/async" "fyne.io/fyne/v2/internal/cache" internalWidget "fyne.io/fyne/v2/internal/widget" ) // BaseWidget provides a helper that handles basic widget behaviours. type BaseWidget struct { - size atomic.Uint64 - position atomic.Uint64 + size async.Size + position async.Position Hidden bool impl atomic.Pointer[fyne.Widget] @@ -34,7 +34,7 @@ func (w *BaseWidget) ExtendBaseWidget(wid fyne.Widget) { // Size gets the current size of this widget. func (w *BaseWidget) Size() fyne.Size { - return fyne.NewSize(twoFloat32FromUint64(w.size.Load())) + return w.size.Load() } // Resize sets a new size for a widget. @@ -44,7 +44,7 @@ func (w *BaseWidget) Resize(size fyne.Size) { return } - w.size.Store(uint64fromTwoFloat32(size.Width, size.Height)) + w.size.Store(size) impl := w.super() if impl == nil { @@ -55,13 +55,13 @@ func (w *BaseWidget) Resize(size fyne.Size) { // Position gets the current position of this widget, relative to its parent. func (w *BaseWidget) Position() fyne.Position { - return fyne.NewPos(twoFloat32FromUint64(w.position.Load())) + return w.position.Load() } // Move the widget to a new position, relative to its parent. // Note this should not be used if the widget is being managed by a Layout within a Container. func (w *BaseWidget) Move(pos fyne.Position) { - w.position.Store(uint64fromTwoFloat32(pos.X, pos.Y)) + w.position.Store(pos) internalWidget.Repaint(w.super()) } @@ -196,15 +196,3 @@ func (w *DisableableWidget) Disabled() bool { func NewSimpleRenderer(object fyne.CanvasObject) fyne.WidgetRenderer { return internalWidget.NewSimpleRenderer(object) } - -func uint64fromTwoFloat32(a, b float32) uint64 { - x := uint64(math.Float32bits(a)) - y := uint64(math.Float32bits(b)) - return (y << 32) | x -} - -func twoFloat32FromUint64(combined uint64) (float32, float32) { - x := uint32(combined & 0x00000000FFFFFFFF) - y := uint32(combined & 0xFFFFFFFF00000000 >> 32) - return math.Float32frombits(x), math.Float32frombits(y) -} From 12486ede8abbbb32c638e5c514858df748878f71 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sat, 6 Jan 2024 19:49:29 +0100 Subject: [PATCH 19/36] Fix tests --- internal/widget/shadowing_renderer_test.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/widget/shadowing_renderer_test.go b/internal/widget/shadowing_renderer_test.go index e37f6e28fd..2e8fbac11a 100644 --- a/internal/widget/shadowing_renderer_test.go +++ b/internal/widget/shadowing_renderer_test.go @@ -17,7 +17,7 @@ func TestShadowingRenderer_Objects(t *testing.T) { }{ "with shadow": { 12, - []fyne.CanvasObject{w.NewShadow(w.ShadowAround, 12)}, + []fyne.CanvasObject{}, }, "without shadow": { 0, @@ -26,13 +26,18 @@ func TestShadowingRenderer_Objects(t *testing.T) { } for name, tt := range tests { t.Run(name, func(t *testing.T) { + shadowIndex := 0 + if tt.level > 0 { + shadowIndex = 1 // Shadow pointers are not the same. Avoid comparing. + } + objects := []fyne.CanvasObject{widget.NewLabel("A"), widget.NewLabel("B")} r := w.NewShadowingRenderer(objects, tt.level) - assert.Equal(t, append(tt.wantPrependedObjects, objects...), r.Objects()) + assert.Equal(t, append(tt.wantPrependedObjects, objects...), r.Objects()[shadowIndex:]) otherObjects := []fyne.CanvasObject{widget.NewLabel("X"), widget.NewLabel("Y")} r.SetObjects(otherObjects) - assert.Equal(t, append(tt.wantPrependedObjects, otherObjects...), r.Objects()) + assert.Equal(t, append(tt.wantPrependedObjects, otherObjects...), r.Objects()[shadowIndex:]) }) } } From a169aebae45c37f832846be968936b295f7036b3 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sat, 6 Jan 2024 20:13:52 +0100 Subject: [PATCH 20/36] Don't call public methods inside locks --- internal/widget/scroller.go | 15 ++++++++------- widget/button.go | 2 +- widget/check.go | 6 +++--- widget/check_group.go | 4 ++-- widget/entry.go | 12 ++++++------ widget/entry_password.go | 2 +- widget/entry_validation.go | 2 +- widget/progressbarinfinite.go | 2 +- widget/radio_group.go | 4 ++-- widget/richtext.go | 6 +++--- widget/select.go | 12 ++++++------ widget/slider.go | 2 +- widget/table.go | 16 +++++++++------- widget/tree.go | 4 ++-- 14 files changed, 46 insertions(+), 43 deletions(-) diff --git a/internal/widget/scroller.go b/internal/widget/scroller.go index 9daa021dd3..f5620272d3 100644 --- a/internal/widget/scroller.go +++ b/internal/widget/scroller.go @@ -251,20 +251,21 @@ type scrollContainerRenderer struct { } func (r *scrollContainerRenderer) layoutBars(size fyne.Size) { + scrollerSize := r.scroll.size.Load() if r.scroll.Direction == ScrollVerticalOnly || r.scroll.Direction == ScrollBoth { r.vertArea.Resize(fyne.NewSize(r.vertArea.MinSize().Width, size.Height)) - r.vertArea.Move(fyne.NewPos(r.scroll.Size().Width-r.vertArea.Size().Width, 0)) + r.vertArea.Move(fyne.NewPos(scrollerSize.Width-r.vertArea.Size().Width, 0)) r.topShadow.Resize(fyne.NewSize(size.Width, 0)) r.bottomShadow.Resize(fyne.NewSize(size.Width, 0)) - r.bottomShadow.Move(fyne.NewPos(0, r.scroll.Size().Height)) + r.bottomShadow.Move(fyne.NewPos(0, scrollerSize.Height)) } if r.scroll.Direction == ScrollHorizontalOnly || r.scroll.Direction == ScrollBoth { r.horizArea.Resize(fyne.NewSize(size.Width, r.horizArea.MinSize().Height)) - r.horizArea.Move(fyne.NewPos(0, r.scroll.Size().Height-r.horizArea.Size().Height)) + r.horizArea.Move(fyne.NewPos(0, scrollerSize.Height-r.horizArea.Size().Height)) r.leftShadow.Resize(fyne.NewSize(0, size.Height)) r.rightShadow.Resize(fyne.NewSize(0, size.Height)) - r.rightShadow.Move(fyne.NewPos(r.scroll.Size().Width, 0)) + r.rightShadow.Move(fyne.NewPos(scrollerSize.Width, 0)) } r.updatePosition() @@ -326,7 +327,7 @@ func (r *scrollContainerRenderer) updatePosition() { if r.scroll.Content == nil { return } - scrollSize := r.scroll.Size() + scrollSize := r.scroll.size.Load() contentSize := r.scroll.Content.Size() r.scroll.Content.Move(fyne.NewPos(-r.scroll.Offset.X, -r.scroll.Offset.Y)) @@ -334,7 +335,7 @@ func (r *scrollContainerRenderer) updatePosition() { if r.scroll.Direction == ScrollVerticalOnly || r.scroll.Direction == ScrollBoth { r.handleAreaVisibility(contentSize.Height, scrollSize.Height, r.vertArea) r.handleShadowVisibility(r.scroll.Offset.Y, contentSize.Height, scrollSize.Height, r.topShadow, r.bottomShadow) - cache.Renderer(r.vertArea).Layout(r.scroll.Size()) + cache.Renderer(r.vertArea).Layout(scrollSize) } else { r.vertArea.Hide() r.topShadow.Hide() @@ -343,7 +344,7 @@ func (r *scrollContainerRenderer) updatePosition() { if r.scroll.Direction == ScrollHorizontalOnly || r.scroll.Direction == ScrollBoth { r.handleAreaVisibility(contentSize.Width, scrollSize.Width, r.horizArea) r.handleShadowVisibility(r.scroll.Offset.X, contentSize.Width, scrollSize.Width, r.leftShadow, r.rightShadow) - cache.Renderer(r.horizArea).Layout(r.scroll.Size()) + cache.Renderer(r.horizArea).Layout(scrollSize) } else { r.horizArea.Hide() r.leftShadow.Hide() diff --git a/widget/button.go b/widget/button.go index 7d79fa8fac..f98f47f534 100644 --- a/widget/button.go +++ b/widget/button.go @@ -358,7 +358,7 @@ func (r *buttonRenderer) applyTheme() { r.button.applyButtonTheme() r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameForeground switch { - case r.button.Disabled(): + case r.button.disabled.Load(): r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameDisabled case r.button.Importance == HighImportance || r.button.Importance == DangerImportance || r.button.Importance == WarningImportance || r.button.Importance == SuccessImportance: if r.button.focused { diff --git a/widget/check.go b/widget/check.go index f22124ea43..120a32f961 100644 --- a/widget/check.go +++ b/widget/check.go @@ -55,7 +55,7 @@ func (c *checkRenderer) Layout(size fyne.Size) { func (c *checkRenderer) applyTheme() { c.label.Color = theme.ForegroundColor() c.label.TextSize = theme.TextSize() - if c.check.Disabled() { + if c.check.disabled.Load() { c.label.Color = theme.DisabledColor() } } @@ -86,7 +86,7 @@ func (c *checkRenderer) updateResource() { res.ColorName = theme.ColorNamePrimary bgRes.ColorName = theme.ColorNameBackground } - if c.check.Disabled() { + if c.check.disabled.Load() { if c.check.Checked { res = theme.NewThemedResource(theme.CheckButtonCheckedIcon()) } @@ -98,7 +98,7 @@ func (c *checkRenderer) updateResource() { } func (c *checkRenderer) updateFocusIndicator() { - if c.check.Disabled() { + if c.check.disabled.Load() { c.focusIndicator.FillColor = color.Transparent } else if c.check.focused { c.focusIndicator.FillColor = theme.FocusColor() diff --git a/widget/check_group.go b/widget/check_group.go index 2b559a3694..78869b1ca0 100644 --- a/widget/check_group.go +++ b/widget/check_group.go @@ -168,7 +168,7 @@ func (r *CheckGroup) update() { item.Text = r.Options[i] item.Checked = contains - item.DisableableWidget.disabled.Store(r.Disabled()) + item.DisableableWidget.disabled.Store(r.disabled.Load()) item.Refresh() } } @@ -261,7 +261,7 @@ func (r *checkGroupRenderer) updateItems() { } item.Text = r.checks.Options[i] item.Checked = contains - item.disabled.Store(r.checks.Disabled()) + item.disabled.Store(r.checks.disabled.Load()) item.Refresh() } } diff --git a/widget/entry.go b/widget/entry.go index 62e8c7f154..3f704be112 100644 --- a/widget/entry.go +++ b/widget/entry.go @@ -226,7 +226,7 @@ func (e *Entry) Disable() { // // Implements: fyne.Disableable func (e *Entry) Disabled() bool { - return e.DisableableWidget.Disabled() + return e.DisableableWidget.disabled.Load() } // DoubleTapped is called when this entry has been double tapped so we should select text below the pointer @@ -1305,7 +1305,7 @@ func (e *Entry) textPosFromRowCol(row, col int) int { func (e *Entry) syncSegments() { colName := theme.ColorNameForeground wrap := e.textWrap() - disabled := e.Disabled() + disabled := e.disabled.Load() if disabled { colName = theme.ColorNameDisabled } @@ -1667,7 +1667,7 @@ func (r *entryRenderer) Objects() []fyne.CanvasObject { func (r *entryRenderer) Refresh() { r.entry.propertyLock.RLock() content := r.entry.content - focusedAppearance := r.entry.focused && !r.entry.Disabled() + focusedAppearance := r.entry.focused && !r.entry.disabled.Load() scroll := r.entry.Scroll wrapping := r.entry.Wrapping r.entry.propertyLock.RUnlock() @@ -1679,7 +1679,7 @@ func (r *entryRenderer) Refresh() { r.entry.placeholder.Refresh() // correct our scroll wrappers if the wrap mode changed - entrySize := r.entry.Size().Subtract(fyne.NewSize(r.trailingInset(), theme.InputBorderSize()*2)) + entrySize := r.entry.size.Load().Subtract(fyne.NewSize(r.trailingInset(), theme.InputBorderSize()*2)) if wrapping == fyne.TextWrapOff && scroll == widget.ScrollNone && r.scroll.Content != nil { r.scroll.Hide() r.scroll.Content = nil @@ -1742,7 +1742,7 @@ func (r *entryRenderer) ensureValidationSetup() { if r.entry.validationStatus == nil { r.entry.validationStatus = newValidationStatus(r.entry) r.objects = append(r.objects, r.entry.validationStatus) - r.Layout(r.entry.Size()) + r.Layout(r.entry.size.Load()) r.entry.Validate() @@ -1774,7 +1774,7 @@ func (e *entryContent) CreateRenderer() fyne.WidgetRenderer { r := &entryContentRenderer{e.entry.cursorAnim.cursor, []fyne.CanvasObject{}, objects, provider, placeholder, e} r.updateScrollDirections() - r.Layout(e.Size()) + r.Layout(e.size.Load()) return r } diff --git a/widget/entry_password.go b/widget/entry_password.go index 5e9bae09ba..a6cd1d014b 100644 --- a/widget/entry_password.go +++ b/widget/entry_password.go @@ -76,7 +76,7 @@ func (r *passwordRevealerRenderer) Refresh() { r.icon.Resource = theme.VisibilityOffIcon() } - if r.entry.Disabled() { + if r.entry.disabled.Load() { r.icon.Resource = theme.NewDisabledResource(r.icon.Resource) } canvas.Refresh(r.icon) diff --git a/widget/entry_validation.go b/widget/entry_validation.go index 8aa14e76c6..deb46592a6 100644 --- a/widget/entry_validation.go +++ b/widget/entry_validation.go @@ -93,7 +93,7 @@ func (r *validationStatusRenderer) MinSize() fyne.Size { func (r *validationStatusRenderer) Refresh() { r.entry.propertyLock.RLock() defer r.entry.propertyLock.RUnlock() - if r.entry.Disabled() { + if r.entry.disabled.Load() { r.icon.Hide() return } diff --git a/widget/progressbarinfinite.go b/widget/progressbarinfinite.go index 2d11989491..abf0357284 100644 --- a/widget/progressbarinfinite.go +++ b/widget/progressbarinfinite.go @@ -34,7 +34,7 @@ func (p *infProgressRenderer) MinSize() fyne.Size { } func (p *infProgressRenderer) updateBar(done float32) { - size := p.progress.Size() + size := p.progress.size.Load() progressWidth := size.Width spanWidth := progressWidth + (progressWidth * (maxProgressBarInfiniteWidthRatio / 2)) maxBarWidth := progressWidth * maxProgressBarInfiniteWidthRatio diff --git a/widget/radio_group.go b/widget/radio_group.go index 8b3a31e43a..fb778e6857 100644 --- a/widget/radio_group.go +++ b/widget/radio_group.go @@ -124,7 +124,7 @@ func (r *RadioGroup) update() { for i, item := range r.items { item.Label = r.Options[i] item.Selected = item.Label == r.Selected - item.DisableableWidget.disabled.Store(r.Disabled()) + item.DisableableWidget.disabled.Store(r.disabled.Load()) item.Refresh() } } @@ -207,7 +207,7 @@ func (r *radioGroupRenderer) updateItems() { for i, item := range r.items { item.Label = r.radio.Options[i] item.Selected = item.Label == r.radio.Selected - item.disabled.Store(r.radio.Disabled()) + item.disabled.Store(r.radio.disabled.Load()) item.Refresh() } } diff --git a/widget/richtext.go b/widget/richtext.go index f58dcaa9fe..df31b618e6 100644 --- a/widget/richtext.go +++ b/widget/richtext.go @@ -107,7 +107,7 @@ func (t *RichText) Refresh() { // // Implements: fyne.Widget func (t *RichText) Resize(size fyne.Size) { - if size == t.Size() { + if size == t.size.Load() { return } @@ -355,7 +355,7 @@ func (t *RichText) rows() int { // updateRowBounds should be invoked every time a segment Text, widget Wrapping or size changes. func (t *RichText) updateRowBounds() { innerPadding := theme.InnerPadding() - fitSize := t.Size() + fitSize := t.size.Load() if t.scr != nil { fitSize = t.scr.Content.MinSize() } @@ -363,7 +363,7 @@ func (t *RichText) updateRowBounds() { t.propertyLock.RLock() var bounds []rowBoundary - maxWidth := t.Size().Width - 2*innerPadding + 2*t.inset.Width + maxWidth := t.size.Load().Width - 2*innerPadding + 2*t.inset.Width wrapWidth := maxWidth var currentBound *rowBoundary diff --git a/widget/select.go b/widget/select.go index 19a68091fd..f38c139152 100644 --- a/widget/select.go +++ b/widget/select.go @@ -63,10 +63,10 @@ func (s *Select) CreateRenderer() fyne.WidgetRenderer { s.PlaceHolder = defaultPlaceHolder } txtProv := NewRichTextWithText(s.Selected) - txtProv.inset = fyne.NewSize(theme.Padding(), theme.Padding()) + txtProv.inset = fyne.NewSquareSize(theme.Padding()) txtProv.ExtendBaseWidget(txtProv) txtProv.Truncation = fyne.TextTruncateEllipsis - if s.Disabled() { + if s.disabled.Load() { txtProv.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameDisabled } @@ -334,7 +334,7 @@ func (s *selectRenderer) Refresh() { if s.combo.popUp != nil { s.combo.popUp.alignment = s.combo.Alignment s.combo.popUp.Move(s.combo.popUpPos()) - s.combo.popUp.Resize(fyne.NewSize(s.combo.Size().Width, s.combo.popUp.MinSize().Height)) + s.combo.popUp.Resize(fyne.NewSize(s.combo.size.Load().Width, s.combo.popUp.MinSize().Height)) s.combo.popUp.Refresh() } s.background.Refresh() @@ -342,7 +342,7 @@ func (s *selectRenderer) Refresh() { } func (s *selectRenderer) bgColor() color.Color { - if s.combo.Disabled() { + if s.combo.disabled.Load() { return theme.DisabledButtonColor() } if s.combo.focused { @@ -355,7 +355,7 @@ func (s *selectRenderer) bgColor() color.Color { } func (s *selectRenderer) updateIcon() { - if s.combo.Disabled() { + if s.combo.disabled.Load() { s.icon.Resource = theme.NewDisabledResource(theme.MenuDropDownIcon()) } else { s.icon.Resource = theme.MenuDropDownIcon() @@ -369,7 +369,7 @@ func (s *selectRenderer) updateLabel() { } s.label.Segments[0].(*TextSegment).Style.Alignment = s.combo.Alignment - if s.combo.Disabled() { + if s.combo.disabled.Load() { s.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameDisabled } else { s.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameForeground diff --git a/widget/slider.go b/widget/slider.go index c9f7a11d16..e2eb40076e 100644 --- a/widget/slider.go +++ b/widget/slider.go @@ -243,7 +243,7 @@ func (s *Slider) getRatio(e *fyne.PointEvent) float64 { x := e.Position.X y := e.Position.Y - size := s.Size() + size := s.size.Load() switch s.Orientation { case Vertical: diff --git a/widget/table.go b/widget/table.go index ed5d9af988..444341bc91 100644 --- a/widget/table.go +++ b/widget/table.go @@ -854,7 +854,7 @@ func (t *Table) visibleColumnWidths(colWidth float32, cols int) (visible map[int // theme.Padding is a slow call, so we cache it padding := theme.Padding() stick := t.StickyColumnCount - size := t.Size() + size := t.size.Load() if len(t.columnWidths) == 0 { paddedWidth := colWidth + padding @@ -955,7 +955,7 @@ func (t *Table) visibleRowHeights(rowHeight float32, rows int) (visible map[int] // theme.Padding is a slow call, so we cache it padding := theme.Padding() stick := t.StickyRowCount - size := t.Size() + size := t.size.Load() if len(t.rowHeights) == 0 { paddedHeight := rowHeight + padding @@ -1443,6 +1443,8 @@ func (r *tableCellsRenderer) moveIndicators() { r.cells.t.dividerLayer.Content.Refresh() } + size := r.cells.t.size.Load() + divs := 0 i := 0 if stickCols > 0 { @@ -1450,7 +1452,7 @@ func (r *tableCellsRenderer) moveIndicators() { i++ xPos := x + dividerOff - r.dividers[divs].Resize(fyne.NewSize(separatorThickness, r.cells.t.Size().Height)) + r.dividers[divs].Resize(fyne.NewSize(separatorThickness, size.Height)) r.dividers[divs].Move(fyne.NewPos(xPos, 0)) r.dividers[divs].Show() divs++ @@ -1461,7 +1463,7 @@ func (r *tableCellsRenderer) moveIndicators() { i++ xPos := x - r.cells.t.content.Offset.X + dividerOff - r.dividers[divs].Resize(fyne.NewSize(separatorThickness, r.cells.t.Size().Height)) + r.dividers[divs].Resize(fyne.NewSize(separatorThickness, size.Height)) r.dividers[divs].Move(fyne.NewPos(xPos, 0)) r.dividers[divs].Show() divs++ @@ -1473,7 +1475,7 @@ func (r *tableCellsRenderer) moveIndicators() { i++ yPos := y + dividerOff - r.dividers[divs].Resize(fyne.NewSize(r.cells.t.Size().Width, separatorThickness)) + r.dividers[divs].Resize(fyne.NewSize(size.Width, separatorThickness)) r.dividers[divs].Move(fyne.NewPos(0, yPos)) r.dividers[divs].Show() divs++ @@ -1484,7 +1486,7 @@ func (r *tableCellsRenderer) moveIndicators() { i++ yPos := y - r.cells.t.content.Offset.Y + dividerOff - r.dividers[divs].Resize(fyne.NewSize(r.cells.t.Size().Width, separatorThickness)) + r.dividers[divs].Resize(fyne.NewSize(size.Width, separatorThickness)) r.dividers[divs].Move(fyne.NewPos(0, yPos)) r.dividers[divs].Show() divs++ @@ -1545,7 +1547,7 @@ func (r *tableCellsRenderer) moveMarker(marker fyne.CanvasObject, row, col int, } y2 := y1 + heights[row] - size := r.cells.t.Size() + size := r.cells.t.size.Load() if x2 < 0 || x1 > size.Width || y2 < 0 || y1 > size.Height { marker.Hide() } else { diff --git a/widget/tree.go b/widget/tree.go index ac050c1d06..e81fb108e1 100644 --- a/widget/tree.go +++ b/widget/tree.go @@ -233,7 +233,7 @@ func (t *Tree) OpenBranch(uid TreeNodeID) { // Resize sets a new size for a widget. func (t *Tree) Resize(size fyne.Size) { - if size == t.Size() { + if size == t.size.Load() { return } @@ -586,7 +586,7 @@ func (c *treeContent) CreateRenderer() fyne.WidgetRenderer { } func (c *treeContent) Resize(size fyne.Size) { - if size == c.Size() { + if size == c.size.Load() { return } From a6f1cc15d7e3976313a0aeff276e822b000b06fe Mon Sep 17 00:00:00 2001 From: Jacob Date: Sat, 6 Jan 2024 20:16:31 +0100 Subject: [PATCH 21/36] Fix a compilation issue --- widget/hyperlink.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/widget/hyperlink.go b/widget/hyperlink.go index db3cbc4f2f..12dd991574 100644 --- a/widget/hyperlink.go +++ b/widget/hyperlink.go @@ -115,7 +115,7 @@ func (hl *Hyperlink) MouseOut() { func (hl *Hyperlink) focusWidth() float32 { innerPad := theme.InnerPadding() - return fyne.Min(hl.size.Width, hl.textSize.Width+innerPad+theme.Padding()*2) - innerPad + return fyne.Min(hl.size.Load().Width, hl.textSize.Width+innerPad+theme.Padding()*2) - innerPad } func (hl *Hyperlink) focusXPos() float32 { @@ -123,9 +123,9 @@ func (hl *Hyperlink) focusXPos() float32 { case fyne.TextAlignLeading: return theme.InnerPadding() / 2 case fyne.TextAlignCenter: - return (hl.size.Width - hl.focusWidth()) / 2 + return (hl.size.Load().Width - hl.focusWidth()) / 2 case fyne.TextAlignTrailing: - return (hl.size.Width - hl.focusWidth()) - theme.InnerPadding()/2 + return (hl.size.Load().Width - hl.focusWidth()) - theme.InnerPadding()/2 default: return 0 // unreached } From 9acf942755b76f455da2b7e3ee5465aea42e5a30 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 9 Jan 2024 16:02:52 -0800 Subject: [PATCH 22/36] run selection initialization code on CreateRenderer --- widget/radio_group.go | 11 +++++++---- widget/radio_group_test.go | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/widget/radio_group.go b/widget/radio_group.go index d71dea39b2..e89a7df48b 100644 --- a/widget/radio_group.go +++ b/widget/radio_group.go @@ -57,7 +57,10 @@ func (r *RadioGroup) CreateRenderer() fyne.WidgetRenderer { }) } - return &radioGroupRenderer{widget.NewBaseRenderer(items), items, r} + render := &radioGroupRenderer{widget.NewBaseRenderer(items), items, r} + r.updateSelectedIndex() + render.updateItems(false) + return render } // MinSize returns the size that this widget should not shrink below @@ -202,11 +205,11 @@ func (r *radioGroupRenderer) MinSize() fyne.Size { } func (r *radioGroupRenderer) Refresh() { - r.updateItems() + r.updateItems(true) canvas.Refresh(r.radio.super()) } -func (r *radioGroupRenderer) updateItems() { +func (r *radioGroupRenderer) updateItems(refresh bool) { if len(r.items) < len(r.radio.Options) { for i := len(r.items); i < len(r.radio.Options); i++ { idx := i @@ -238,7 +241,7 @@ func (r *radioGroupRenderer) updateItems() { changed = true } - if changed { + if refresh && changed { item.Refresh() } } diff --git a/widget/radio_group_test.go b/widget/radio_group_test.go index dbea6f6e01..a51639b29a 100644 --- a/widget/radio_group_test.go +++ b/widget/radio_group_test.go @@ -151,7 +151,6 @@ func TestRadioGroup_Layout(t *testing.T) { Options: tt.options, Selected: tt.selected, } - radio.Refresh() // set up selectedIndex if tt.disabled { radio.Disable() } From 1b3f8edeab8d38c3ef042800df9b8e0539025b3b Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 9 Jan 2024 17:40:22 -0800 Subject: [PATCH 23/36] reduce theme lookups in slider --- widget/slider.go | 36 +++++++++++++++++++----------------- widget/slider_test.go | 6 ++++-- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/widget/slider.go b/widget/slider.go index c6621180ae..068c2c7369 100644 --- a/widget/slider.go +++ b/widget/slider.go @@ -230,16 +230,16 @@ func (s *Slider) TypedKey(key *fyne.KeyEvent) { func (s *Slider) TypedRune(_ rune) { } -func (s *Slider) buttonDiameter() float32 { - return theme.IconInlineSize() - 4 // match radio icons +func (s *Slider) buttonDiameter(inlineIconSize float32) float32 { + return inlineIconSize - 4 // match radio icons } -func (s *Slider) endOffset() float32 { - return s.buttonDiameter()/2 + theme.InnerPadding() - 1.5 // align with radio icons +func (s *Slider) endOffset(inlineIconSize, innerPadding float32) float32 { + return s.buttonDiameter(inlineIconSize)/2 + innerPadding - 1.5 // align with radio icons } func (s *Slider) getRatio(e *fyne.PointEvent) float64 { - pad := s.endOffset() + pad := s.endOffset(theme.IconInlineSize(), theme.InnerPadding()) x := e.Position.X y := e.Position.Y @@ -424,11 +424,10 @@ func (s *sliderRenderer) Refresh() { s.track.FillColor = theme.InputBackgroundColor() if s.slider.disabled { s.thumb.FillColor = theme.DisabledColor() - s.active.FillColor = theme.DisabledColor() } else { s.thumb.FillColor = theme.ForegroundColor() - s.active.FillColor = theme.ForegroundColor() } + s.active.FillColor = s.thumb.FillColor if s.slider.focused && !s.slider.disabled { s.focusIndicator.FillColor = theme.FocusColor() @@ -447,9 +446,12 @@ func (s *sliderRenderer) Refresh() { // Layout the components of the widget. func (s *sliderRenderer) Layout(size fyne.Size) { - trackWidth := theme.InputBorderSize() * 2 - diameter := s.slider.buttonDiameter() - endPad := s.slider.endOffset() + inputBorderSize := theme.InputBorderSize() + trackWidth := inputBorderSize * 2 + inlineIconSize := theme.IconInlineSize() + innerPadding := theme.InnerPadding() + diameter := s.slider.buttonDiameter(inlineIconSize) + endPad := s.slider.endOffset(inlineIconSize, innerPadding) var trackPos, activePos, thumbPos fyne.Position var trackSize, activeSize fyne.Size @@ -457,17 +459,17 @@ func (s *sliderRenderer) Layout(size fyne.Size) { // some calculations are relative to trackSize, so we must update that first switch s.slider.Orientation { case Vertical: - trackPos = fyne.NewPos(size.Width/2-theme.InputBorderSize(), endPad) + trackPos = fyne.NewPos(size.Width/2-inputBorderSize, endPad) trackSize = fyne.NewSize(trackWidth, size.Height-endPad*2) case Horizontal: - trackPos = fyne.NewPos(endPad, size.Height/2-theme.InputBorderSize()) + trackPos = fyne.NewPos(endPad, size.Height/2-inputBorderSize) trackSize = fyne.NewSize(size.Width-endPad*2, trackWidth) } s.track.Move(trackPos) s.track.Resize(trackSize) - activeOffset := s.getOffset() // TODO based on old size...0 + activeOffset := s.getOffset(inlineIconSize, innerPadding) // TODO based on old size...0 switch s.slider.Orientation { case Vertical: activePos = fyne.NewPos(trackPos.X, activeOffset) @@ -489,7 +491,7 @@ func (s *sliderRenderer) Layout(size fyne.Size) { s.thumb.Move(thumbPos) s.thumb.Resize(fyne.NewSize(diameter, diameter)) - focusIndicatorSize := fyne.NewSquareSize(theme.IconInlineSize() + theme.InnerPadding()) + focusIndicatorSize := fyne.NewSquareSize(inlineIconSize + innerPadding) delta := (focusIndicatorSize.Width - diameter) / 2 s.focusIndicator.Resize(focusIndicatorSize) s.focusIndicator.Move(thumbPos.SubtractXY(delta, delta)) @@ -497,7 +499,7 @@ func (s *sliderRenderer) Layout(size fyne.Size) { // MinSize calculates the minimum size of a widget. func (s *sliderRenderer) MinSize() fyne.Size { - dia := s.slider.buttonDiameter() + dia := s.slider.buttonDiameter(theme.IconInlineSize()) s1, s2 := minLongSide+dia, dia switch s.slider.Orientation { @@ -510,8 +512,8 @@ func (s *sliderRenderer) MinSize() fyne.Size { return fyne.Size{Width: 0, Height: 0} } -func (s *sliderRenderer) getOffset() float32 { - endPad := s.slider.endOffset() +func (s *sliderRenderer) getOffset(iconInlineSize, innerPadding float32) float32 { + endPad := s.slider.endOffset(iconInlineSize, innerPadding) w := s.slider size := s.track.Size() if w.Value == w.Min || w.Min == w.Max { diff --git a/widget/slider_test.go b/widget/slider_test.go index b225eea892..45906eb2c5 100644 --- a/widget/slider_test.go +++ b/widget/slider_test.go @@ -67,7 +67,8 @@ func TestSlider_HorizontalLayout(t *testing.T) { assert.Greater(t, wSize.Width, wSize.Height) - assert.Equal(t, wSize.Width-slider.endOffset()*2, tSize.Width) + endOffset := slider.endOffset(theme.IconInlineSize(), theme.InnerPadding()) + assert.Equal(t, wSize.Width-endOffset*2, tSize.Width) assert.Equal(t, theme.InputBorderSize()*2, tSize.Height) assert.Greater(t, wSize.Width, aSize.Width) @@ -103,7 +104,8 @@ func TestSlider_VerticalLayout(t *testing.T) { assert.Greater(t, wSize.Height, wSize.Width) - assert.Equal(t, wSize.Height-slider.endOffset()*2, tSize.Height) + endOffset := slider.endOffset(theme.IconInlineSize(), theme.InnerPadding()) + assert.Equal(t, wSize.Height-endOffset*2, tSize.Height) assert.Equal(t, theme.InputBorderSize()*2, tSize.Width) assert.Greater(t, wSize.Height, aSize.Height) From b08de53104c897333fade34b00f3ce8439f4be59 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 11 Jan 2024 17:12:12 -0800 Subject: [PATCH 24/36] remove unneeded refreshes in test --- widget/radio_group_extended_test.go | 1 - widget/radio_group_internal_test.go | 1 - 2 files changed, 2 deletions(-) diff --git a/widget/radio_group_extended_test.go b/widget/radio_group_extended_test.go index 59bf0228b8..7e71898320 100644 --- a/widget/radio_group_extended_test.go +++ b/widget/radio_group_extended_test.go @@ -42,7 +42,6 @@ func TestRadioGroup_Extended_Unselected(t *testing.T) { selected = sel }) radio.Selected = selected - radio.Refresh() // sets up selectedIndex extendedRadioGroupTestTapItem(t, radio, 0) assert.Equal(t, "", selected) diff --git a/widget/radio_group_internal_test.go b/widget/radio_group_internal_test.go index 849dff514a..5fb3b1e53a 100644 --- a/widget/radio_group_internal_test.go +++ b/widget/radio_group_internal_test.go @@ -57,7 +57,6 @@ func TestRadioGroup_Unselected(t *testing.T) { selected = sel }) radio.Selected = selected - radio.Refresh() // sets up selectedIndex radioGroupTestTapItem(t, radio, 0) assert.Equal(t, "", selected) From d04d5e583a3212497bd34867befb8fefd2ac5f09 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 12 Jan 2024 12:04:01 +0100 Subject: [PATCH 25/36] Add tests for the new async types --- internal/async/vector_test.go | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 internal/async/vector_test.go diff --git a/internal/async/vector_test.go b/internal/async/vector_test.go new file mode 100644 index 0000000000..f9b5ca91d1 --- /dev/null +++ b/internal/async/vector_test.go @@ -0,0 +1,54 @@ +package async_test + +import ( + "testing" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" + "github.com/stretchr/testify/assert" +) + +var globalSize fyne.Size + +func BenchmarkAtomicLoadAndStore(b *testing.B) { + local := fyne.Size{} + size := async.Size{} + + // Each loop iteration replicates how .Resize() in BaseWidget checks size changes. + for i := 0; i < b.N; i++ { + new := fyne.Size{Width: float32(i), Height: float32(i)} + if new == size.Load() { + continue + } + + size.Store(new) + } + + globalSize = local +} + +func TestSize(t *testing.T) { + size := async.Size{} + assert.Equal(t, fyne.Size{}, size.Load()) + + square := fyne.NewSquareSize(100) + size.Store(square) + assert.Equal(t, square, size.Load()) + + uneven := fyne.NewSize(125, 600) + size.Store(uneven) + assert.Equal(t, uneven, size.Load()) +} + +func TestPosition(t *testing.T) { + pos := async.Position{} + assert.Equal(t, fyne.Position{}, pos.Load()) + + even := fyne.NewSquareOffsetPos(100) + pos.Store(even) + assert.Equal(t, even, pos.Load()) + + uneven := fyne.NewPos(125, 600) + pos.Store(uneven) + assert.Equal(t, uneven, pos.Load()) +} From 638800d4204f1ee5ddd7fd4f586a10aff09475d4 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Tue, 9 Jan 2024 09:15:52 +0000 Subject: [PATCH 26/36] Experiment with removing a new theme test... --- container/tabs_test.go | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/container/tabs_test.go b/container/tabs_test.go index 6f7de44a4c..cc79ee91f2 100644 --- a/container/tabs_test.go +++ b/container/tabs_test.go @@ -5,13 +5,9 @@ import ( "github.com/stretchr/testify/assert" - "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/internal/cache" - intTest "fyne.io/fyne/v2/internal/test" - "fyne.io/fyne/v2/test" "fyne.io/fyne/v2/theme" - "fyne.io/fyne/v2/widget" ) func TestTabButton_Icon_Change(t *testing.T) { @@ -24,26 +20,3 @@ func TestTabButton_Icon_Change(t *testing.T) { b.Refresh() assert.NotEqual(t, oldResource, icon.Resource) } - -func TestTab_ThemeChange(t *testing.T) { - a := test.NewApp() - defer test.NewApp() - firstTheme := a.Settings().Theme() - - tabs := NewAppTabs( - NewTabItem("a", widget.NewLabel("a")), - NewTabItem("b", widget.NewLabel("b"))) - w := test.NewWindow(tabs) - w.Resize(fyne.NewSquareSize(150)) - - initial := w.Canvas().Capture() - - a.Settings().SetTheme(intTest.DarkTheme(theme.DefaultTheme())) - tabs.SelectIndex(1) - second := w.Canvas().Capture() - assert.NotEqual(t, initial, second) - - a.Settings().SetTheme(firstTheme) - tabs.SelectIndex(0) - assert.Equal(t, initial, w.Canvas().Capture()) -} From 1e270e2ff89ef536726c84af016612b99b40b836 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 12 Jan 2024 22:03:58 +0100 Subject: [PATCH 27/36] Add tests for negative and floatingpoint --- internal/async/vector_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/async/vector_test.go b/internal/async/vector_test.go index f9b5ca91d1..183bf64ec6 100644 --- a/internal/async/vector_test.go +++ b/internal/async/vector_test.go @@ -38,6 +38,10 @@ func TestSize(t *testing.T) { uneven := fyne.NewSize(125, 600) size.Store(uneven) assert.Equal(t, uneven, size.Load()) + + floats := fyne.NewSize(-22.565, 133.333) + size.Store(floats) + assert.Equal(t, floats, size.Load()) } func TestPosition(t *testing.T) { @@ -51,4 +55,8 @@ func TestPosition(t *testing.T) { uneven := fyne.NewPos(125, 600) pos.Store(uneven) assert.Equal(t, uneven, pos.Load()) + + floats := fyne.NewPos(-22.565, 133.333) + pos.Store(floats) + assert.Equal(t, floats, pos.Load()) } From d0759272feeb129e5ebf1c71fa27945a33a86652 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 12 Jan 2024 22:20:02 +0100 Subject: [PATCH 28/36] Remove unnecessary logical AND operations --- internal/async/vector.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/async/vector.go b/internal/async/vector.go index b859ce00b0..939b53171e 100644 --- a/internal/async/vector.go +++ b/internal/async/vector.go @@ -46,7 +46,7 @@ func uint64fromTwoFloat32(a, b float32) uint64 { } func twoFloat32FromUint64(combined uint64) (float32, float32) { - x := uint32(combined & 0x00000000FFFFFFFF) - y := uint32(combined & 0xFFFFFFFF00000000 >> 32) + x := uint32(combined) + y := uint32(combined >> 32) return math.Float32frombits(x), math.Float32frombits(y) } From 3d93bf6fde1bea494a8415640dcfb38e5d42e212 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sun, 14 Jan 2024 12:49:51 +0100 Subject: [PATCH 29/36] Expose fill icons for Check and Radio to fix TODO --- theme/icons.go | 34 ++++++++++++++++++++++++----- widget/check.go | 10 ++++----- widget/radio_group_internal_test.go | 2 +- widget/radio_item.go | 6 ++--- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/theme/icons.go b/theme/icons.go index cce59dfa70..a4d75162e9 100644 --- a/theme/icons.go +++ b/theme/icons.go @@ -41,15 +41,20 @@ const ( // Since: 2.0 IconNameMenuExpand fyne.ThemeIconName = "menuExpand" + // IconNameCheckButton is the name of theme lookup for unchecked check button icon. + // + // Since: 2.0 + IconNameCheckButton fyne.ThemeIconName = "unchecked" + // IconNameCheckButtonChecked is the name of theme lookup for checked check button icon. // // Since: 2.0 IconNameCheckButtonChecked fyne.ThemeIconName = "checked" - // IconNameCheckButton is the name of theme lookup for unchecked check button icon. + // InconNameCheckButtonFill is the name of theme lookup for filled check button icon. // - // Since: 2.0 - IconNameCheckButton fyne.ThemeIconName = "unchecked" + // Since: 2.5 + IconNameCheckButtonFill fyne.ThemeIconName = "iconNameCheckButtonFill" // IconNameRadioButton is the name of theme lookup for radio button unchecked icon. // @@ -61,6 +66,11 @@ const ( // Since: 2.0 IconNameRadioButtonChecked fyne.ThemeIconName = "radioButtonChecked" + // InconNameRadioButtonFill is the name of theme lookup for filled radio button icon. + // + // Since: 2.5 + IconNameRadioButtonFill fyne.ThemeIconName = "iconNameRadioButtonFill" + // IconNameColorAchromatic is the name of theme lookup for greyscale color icon. // // Since: 2.0 @@ -489,10 +499,10 @@ var ( IconNameCheckButton: NewThemedResource(checkboxIconRes), IconNameCheckButtonChecked: NewThemedResource(checkboxcheckedIconRes), - "iconNameCheckButtonFill": NewThemedResource(checkboxfillIconRes), + IconNameCheckButtonFill: NewThemedResource(checkboxfillIconRes), IconNameRadioButton: NewThemedResource(radiobuttonIconRes), IconNameRadioButtonChecked: NewThemedResource(radiobuttoncheckedIconRes), - "iconNameRadioButtonFill": NewThemedResource(radiobuttonfillIconRes), + IconNameRadioButtonFill: NewThemedResource(radiobuttonfillIconRes), IconNameContentAdd: NewThemedResource(contentaddIconRes), IconNameContentClear: NewThemedResource(cancelIconRes), @@ -834,6 +844,13 @@ func CheckButtonCheckedIcon() fyne.Resource { return safeIconLookup(IconNameCheckButtonChecked) } +// CheckButtonFillIcon returns a resource containing the filled checkbox icon for the current theme. +// +// Since: 2.5 +func CheckButtonFillIcon() fyne.Resource { + return safeIconLookup(IconNameCheckButtonFill) +} + // RadioButtonIcon returns a resource containing the standard radio button icon for the current theme func RadioButtonIcon() fyne.Resource { return safeIconLookup(IconNameRadioButton) @@ -844,6 +861,13 @@ func RadioButtonCheckedIcon() fyne.Resource { return safeIconLookup(IconNameRadioButtonChecked) } +// RadioButtonFillIcon returns a resource containing the filled checkbox icon for the current theme. +// +// Since: 2.5 +func RadioButtonFillIcon() fyne.Resource { + return safeIconLookup(IconNameRadioButtonFill) +} + // ContentAddIcon returns a resource containing the standard content add icon for the current theme func ContentAddIcon() fyne.Resource { return safeIconLookup(IconNameContentAdd) diff --git a/widget/check.go b/widget/check.go index 08820064c1..ef3bdffd67 100644 --- a/widget/check.go +++ b/widget/check.go @@ -154,12 +154,11 @@ func (c *Check) MinSize() fyne.Size { // CreateRenderer is a private method to Fyne which links this widget to its renderer func (c *Check) CreateRenderer() fyne.WidgetRenderer { c.ExtendBaseWidget(c) - c.propertyLock.RLock() - defer c.propertyLock.RUnlock() - // TODO move to `theme.CheckButtonFillIcon()` when we add it in 2.4 - bg := canvas.NewImageFromResource(fyne.CurrentApp().Settings().Theme().Icon("iconNameCheckButtonFill")) + bg := canvas.NewImageFromResource(theme.CheckButtonFillIcon()) icon := canvas.NewImageFromResource(theme.CheckButtonIcon()) + c.propertyLock.RLock() + defer c.propertyLock.RUnlock() text := canvas.NewText(c.Text, theme.ForegroundColor()) text.Alignment = fyne.TextAlignLeading @@ -327,8 +326,7 @@ func (c *checkRenderer) updateLabel() { func (c *checkRenderer) updateResource() { res := theme.NewThemedResource(theme.CheckButtonIcon()) res.ColorName = theme.ColorNameInputBorder - // TODO move to `theme.CheckButtonFillIcon()` when we add it in 2.4 - bgRes := theme.NewThemedResource(fyne.CurrentApp().Settings().Theme().Icon("iconNameCheckButtonFill")) + bgRes := theme.NewThemedResource(theme.CheckButtonFillIcon()) bgRes.ColorName = theme.ColorNameInputBackground if c.check.Checked { diff --git a/widget/radio_group_internal_test.go b/widget/radio_group_internal_test.go index 5fb3b1e53a..0da741f51f 100644 --- a/widget/radio_group_internal_test.go +++ b/widget/radio_group_internal_test.go @@ -66,7 +66,7 @@ func TestRadioGroup_DisableWhenSelected(t *testing.T) { radio := NewRadioGroup([]string{"Hi"}, nil) radio.SetSelected("Hi") render := radioGroupTestItemRenderer(t, radio, 0) - icon := fyne.CurrentApp().Settings().Theme().Icon("iconNameRadioButtonFill") + icon := theme.RadioButtonFillIcon() assert.Equal(t, "primary_"+icon.Name(), render.icon.Resource.Name()) radio.Disable() diff --git a/widget/radio_item.go b/widget/radio_item.go index c387a9b38e..07f7dd0c59 100644 --- a/widget/radio_item.go +++ b/widget/radio_item.go @@ -38,8 +38,7 @@ type radioItem struct { // Implements: fyne.Widget func (i *radioItem) CreateRenderer() fyne.WidgetRenderer { focusIndicator := canvas.NewCircle(color.Transparent) - // TODO move to `theme.RadioButtonFillIcon()` when we add it in 2.4 - icon := canvas.NewImageFromResource(fyne.CurrentApp().Settings().Theme().Icon("iconNameRadioButtonFill")) + icon := canvas.NewImageFromResource(theme.RadioButtonFillIcon()) over := canvas.NewImageFromResource(theme.NewThemedResource(theme.RadioButtonIcon())) label := canvas.NewText(i.Label, theme.ForegroundColor()) label.Alignment = fyne.TextAlignLeading @@ -197,8 +196,7 @@ func (r *radioItemRenderer) update() { out := theme.NewThemedResource(theme.RadioButtonIcon()) out.ColorName = theme.ColorNameInputBorder - // TODO move to `theme.RadioButtonFillIcon()` when we add it in 2.4 - in := theme.NewThemedResource(fyne.CurrentApp().Settings().Theme().Icon("iconNameRadioButtonFill")) + in := theme.NewThemedResource(theme.RadioButtonFillIcon()) in.ColorName = theme.ColorNameInputBackground if r.item.Selected { in.ColorName = theme.ColorNamePrimary From da0e6f9c1f601d6af16875066661c5b96f7d848f Mon Sep 17 00:00:00 2001 From: Jacob Date: Sun, 14 Jan 2024 12:59:44 +0100 Subject: [PATCH 30/36] Code cleanup for Check and Radio --- widget/check.go | 19 ++++++++++--------- widget/radio_item.go | 15 +++++++++------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/widget/check.go b/widget/check.go index ef3bdffd67..659fe2c6cc 100644 --- a/widget/check.go +++ b/widget/check.go @@ -283,21 +283,25 @@ func (c *checkRenderer) MinSize() fyne.Size { // Layout the components of the check widget func (c *checkRenderer) Layout(size fyne.Size) { - focusIndicatorSize := fyne.NewSquareSize(theme.IconInlineSize() + theme.InnerPadding()) + innerPadding := theme.InnerPadding() + borderSize := theme.InputBorderSize() + iconInlineSize := theme.IconInlineSize() + + focusIndicatorSize := fyne.NewSquareSize(iconInlineSize + innerPadding) c.focusIndicator.Resize(focusIndicatorSize) - c.focusIndicator.Move(fyne.NewPos(theme.InputBorderSize(), (size.Height-focusIndicatorSize.Height)/2)) + c.focusIndicator.Move(fyne.NewPos(borderSize, (size.Height-focusIndicatorSize.Height)/2)) - xOff := focusIndicatorSize.Width + theme.InputBorderSize()*2 + xOff := focusIndicatorSize.Width + borderSize*2 labelSize := size.SubtractWidthHeight(xOff, 0) c.label.Resize(labelSize) c.label.Move(fyne.NewPos(xOff, 0)) - iconPos := fyne.NewPos(theme.InnerPadding()/2+theme.InputBorderSize(), (size.Height-theme.IconInlineSize())/2) - iconSize := fyne.NewSquareSize(theme.IconInlineSize()) + iconPos := fyne.NewPos(innerPadding/2+borderSize, (size.Height-iconInlineSize)/2) + iconSize := fyne.NewSquareSize(iconInlineSize) c.bg.Move(iconPos) c.bg.Resize(iconSize) - c.icon.Resize(iconSize) c.icon.Move(iconPos) + c.icon.Resize(iconSize) } // applyTheme updates this Check to the current theme @@ -335,9 +339,6 @@ func (c *checkRenderer) updateResource() { bgRes.ColorName = theme.ColorNameBackground } if c.check.disabled.Load() { - if c.check.Checked { - res = theme.NewThemedResource(theme.CheckButtonCheckedIcon()) - } res.ColorName = theme.ColorNameDisabled bgRes.ColorName = theme.ColorNameBackground } diff --git a/widget/radio_item.go b/widget/radio_item.go index 07f7dd0c59..3aab898854 100644 --- a/widget/radio_item.go +++ b/widget/radio_item.go @@ -157,16 +157,20 @@ type radioItemRenderer struct { } func (r *radioItemRenderer) Layout(size fyne.Size) { - focusIndicatorSize := fyne.NewSquareSize(theme.IconInlineSize() + theme.InnerPadding()) + innerPadding := theme.InnerPadding() + borderSize := theme.InputBorderSize() + iconInlineSize := theme.IconInlineSize() + + focusIndicatorSize := fyne.NewSquareSize(iconInlineSize + innerPadding) r.focusIndicator.Resize(focusIndicatorSize) - r.focusIndicator.Move(fyne.NewPos(theme.InputBorderSize(), (size.Height-focusIndicatorSize.Height)/2)) + r.focusIndicator.Move(fyne.NewPos(borderSize, (size.Height-focusIndicatorSize.Height)/2)) labelSize := fyne.NewSize(size.Width, size.Height) r.label.Resize(labelSize) r.label.Move(fyne.NewPos(focusIndicatorSize.Width+theme.Padding(), 0)) - iconPos := fyne.NewPos(theme.InnerPadding()/2+theme.InputBorderSize(), (size.Height-theme.IconInlineSize())/2) - iconSize := fyne.NewSquareSize(theme.IconInlineSize()) + iconPos := fyne.NewPos(innerPadding/2+borderSize, (size.Height-iconInlineSize)/2) + iconSize := fyne.NewSquareSize(iconInlineSize) r.icon.Resize(iconSize) r.icon.Move(iconPos) r.over.Resize(iconSize) @@ -177,8 +181,7 @@ func (r *radioItemRenderer) MinSize() fyne.Size { inPad := theme.InnerPadding() * 2 return r.label.MinSize(). - Add(fyne.NewSize(inPad, inPad)). - Add(fyne.NewSize(theme.IconInlineSize()+theme.Padding(), 0)) + AddWidthHeight(inPad+theme.IconInlineSize()+theme.Padding(), inPad) } func (r *radioItemRenderer) Refresh() { From fb1b7ecb7544bc100c6ebb92afa3f1bcaf1e01eb Mon Sep 17 00:00:00 2001 From: Jacob Date: Sun, 14 Jan 2024 13:10:57 +0100 Subject: [PATCH 31/36] Update tests and fyne_demo with new icons --- cmd/fyne_demo/tutorials/icons.go | 2 ++ test/markup_renderer.go | 2 ++ widget/testdata/check/layout_checked.xml | 2 +- widget/testdata/check/layout_checked_disabled.xml | 2 +- widget/testdata/check/layout_unchecked.xml | 2 +- widget/testdata/check/layout_unchecked_disabled.xml | 2 +- .../testdata/check_group/disabled_append_none_selected.xml | 6 +++--- widget/testdata/check_group/disabled_none_selected.xml | 4 ++-- widget/testdata/check_group/focus_a_focused_b_selected.xml | 6 +++--- .../testdata/check_group/focus_a_focused_none_selected.xml | 6 +++--- widget/testdata/check_group/focus_b_focused_b_selected.xml | 6 +++--- .../testdata/check_group/focus_b_focused_none_selected.xml | 6 +++--- .../testdata/check_group/focus_disabled_none_selected.xml | 6 +++--- .../testdata/check_group/focus_none_focused_b_selected.xml | 6 +++--- .../check_group/focus_none_focused_none_selected.xml | 6 +++--- widget/testdata/check_group/layout_multiple.xml | 4 ++-- widget/testdata/check_group/layout_multiple_disabled.xml | 4 ++-- widget/testdata/check_group/layout_multiple_horizontal.xml | 4 ++-- .../check_group/layout_multiple_horizontal_disabled.xml | 4 ++-- widget/testdata/check_group/layout_multiple_selected.xml | 4 ++-- .../check_group/layout_multiple_selected_disabled.xml | 4 ++-- .../check_group/layout_multiple_selected_horizontal.xml | 4 ++-- .../layout_multiple_selected_horizontal_disabled.xml | 4 ++-- widget/testdata/check_group/layout_single.xml | 2 +- widget/testdata/check_group/layout_single_disabled.xml | 2 +- widget/testdata/check_group/layout_single_horizontal.xml | 2 +- .../check_group/layout_single_horizontal_disabled.xml | 2 +- widget/testdata/check_group/layout_single_selected.xml | 2 +- .../check_group/layout_single_selected_disabled.xml | 2 +- .../check_group/layout_single_selected_horizontal.xml | 2 +- .../layout_single_selected_horizontal_disabled.xml | 2 +- .../testdata/radio_group/disabled_append_none_selected.xml | 6 +++--- widget/testdata/radio_group/disabled_none_selected.xml | 4 ++-- widget/testdata/radio_group/focus_a_focused_b_selected.xml | 6 +++--- .../testdata/radio_group/focus_a_focused_none_selected.xml | 6 +++--- widget/testdata/radio_group/focus_b_focused_b_selected.xml | 6 +++--- .../testdata/radio_group/focus_b_focused_none_selected.xml | 6 +++--- .../testdata/radio_group/focus_disabled_none_selected.xml | 6 +++--- .../testdata/radio_group/focus_none_focused_b_selected.xml | 6 +++--- .../radio_group/focus_none_focused_none_selected.xml | 6 +++--- widget/testdata/radio_group/layout_multiple.xml | 4 ++-- widget/testdata/radio_group/layout_multiple_disabled.xml | 4 ++-- widget/testdata/radio_group/layout_multiple_horizontal.xml | 4 ++-- .../radio_group/layout_multiple_horizontal_disabled.xml | 4 ++-- widget/testdata/radio_group/layout_multiple_selected.xml | 4 ++-- .../radio_group/layout_multiple_selected_disabled.xml | 4 ++-- .../radio_group/layout_multiple_selected_horizontal.xml | 4 ++-- .../layout_multiple_selected_horizontal_disabled.xml | 4 ++-- widget/testdata/radio_group/layout_single.xml | 2 +- widget/testdata/radio_group/layout_single_disabled.xml | 2 +- widget/testdata/radio_group/layout_single_horizontal.xml | 2 +- .../radio_group/layout_single_horizontal_disabled.xml | 2 +- widget/testdata/radio_group/layout_single_selected.xml | 2 +- .../radio_group/layout_single_selected_disabled.xml | 2 +- .../radio_group/layout_single_selected_horizontal.xml | 2 +- .../layout_single_selected_horizontal_disabled.xml | 2 +- 56 files changed, 108 insertions(+), 104 deletions(-) diff --git a/cmd/fyne_demo/tutorials/icons.go b/cmd/fyne_demo/tutorials/icons.go index 0d0ae952b6..d6a258b904 100644 --- a/cmd/fyne_demo/tutorials/icons.go +++ b/cmd/fyne_demo/tutorials/icons.go @@ -94,8 +94,10 @@ func loadIcons() []iconInfo { {"SearchReplaceIcon", theme.SearchReplaceIcon()}, {"CheckButtonIcon", theme.CheckButtonIcon()}, + {"CheckButtonFillIcon", theme.CheckButtonFillIcon()}, {"CheckButtonCheckedIcon", theme.CheckButtonCheckedIcon()}, {"RadioButtonIcon", theme.RadioButtonIcon()}, + {"RadioButtonFillIcon", theme.RadioButtonFillIcon()}, {"RadioButtonCheckedIcon", theme.RadioButtonCheckedIcon()}, {"ColorAchromaticIcon", theme.ColorAchromaticIcon()}, diff --git a/test/markup_renderer.go b/test/markup_renderer.go index 9a7c770a38..f584d46ff7 100644 --- a/test/markup_renderer.go +++ b/test/markup_renderer.go @@ -414,6 +414,7 @@ func knownResource(rsc fyne.Resource) string { return map[fyne.Resource]string{ theme.CancelIcon(): "cancelIcon", theme.CheckButtonCheckedIcon(): "checkButtonCheckedIcon", + theme.CheckButtonFillIcon(): "checkButtonFillIcon", theme.CheckButtonIcon(): "checkButtonIcon", theme.ColorAchromaticIcon(): "colorAchromaticIcon", theme.ColorChromaticIcon(): "colorChromaticIcon", @@ -473,6 +474,7 @@ func knownResource(rsc fyne.Resource) string { theme.NavigateNextIcon(): "navigateNextIcon", theme.QuestionIcon(): "questionIcon", theme.RadioButtonCheckedIcon(): "radioButtonCheckedIcon", + theme.RadioButtonFillIcon(): "radioButtonFillIcon", theme.RadioButtonIcon(): "radioButtonIcon", theme.SearchIcon(): "searchIcon", theme.SearchReplaceIcon(): "searchReplaceIcon", diff --git a/widget/testdata/check/layout_checked.xml b/widget/testdata/check/layout_checked.xml index 0379374aab..300c8e2293 100644 --- a/widget/testdata/check/layout_checked.xml +++ b/widget/testdata/check/layout_checked.xml @@ -3,7 +3,7 @@ - + Test diff --git a/widget/testdata/check/layout_checked_disabled.xml b/widget/testdata/check/layout_checked_disabled.xml index 05c60940fb..82219faa9d 100644 --- a/widget/testdata/check/layout_checked_disabled.xml +++ b/widget/testdata/check/layout_checked_disabled.xml @@ -3,7 +3,7 @@ - + Test diff --git a/widget/testdata/check/layout_unchecked.xml b/widget/testdata/check/layout_unchecked.xml index c74d857e0f..1034b5fdf5 100644 --- a/widget/testdata/check/layout_unchecked.xml +++ b/widget/testdata/check/layout_unchecked.xml @@ -3,7 +3,7 @@ - + Test diff --git a/widget/testdata/check/layout_unchecked_disabled.xml b/widget/testdata/check/layout_unchecked_disabled.xml index cf3797a579..a79af7d22e 100644 --- a/widget/testdata/check/layout_unchecked_disabled.xml +++ b/widget/testdata/check/layout_unchecked_disabled.xml @@ -3,7 +3,7 @@ - + Test diff --git a/widget/testdata/check_group/disabled_append_none_selected.xml b/widget/testdata/check_group/disabled_append_none_selected.xml index 22a6182f9d..395102e166 100644 --- a/widget/testdata/check_group/disabled_append_none_selected.xml +++ b/widget/testdata/check_group/disabled_append_none_selected.xml @@ -3,19 +3,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/check_group/disabled_none_selected.xml b/widget/testdata/check_group/disabled_none_selected.xml index 7e94fa61b6..8abdaedc86 100644 --- a/widget/testdata/check_group/disabled_none_selected.xml +++ b/widget/testdata/check_group/disabled_none_selected.xml @@ -3,13 +3,13 @@ - + Option A - + Option B diff --git a/widget/testdata/check_group/focus_a_focused_b_selected.xml b/widget/testdata/check_group/focus_a_focused_b_selected.xml index 30626b84af..3d60b48555 100644 --- a/widget/testdata/check_group/focus_a_focused_b_selected.xml +++ b/widget/testdata/check_group/focus_a_focused_b_selected.xml @@ -3,19 +3,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/check_group/focus_a_focused_none_selected.xml b/widget/testdata/check_group/focus_a_focused_none_selected.xml index b79c39aa23..c147d3de21 100644 --- a/widget/testdata/check_group/focus_a_focused_none_selected.xml +++ b/widget/testdata/check_group/focus_a_focused_none_selected.xml @@ -3,19 +3,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/check_group/focus_b_focused_b_selected.xml b/widget/testdata/check_group/focus_b_focused_b_selected.xml index 6a06445516..3defdae7fc 100644 --- a/widget/testdata/check_group/focus_b_focused_b_selected.xml +++ b/widget/testdata/check_group/focus_b_focused_b_selected.xml @@ -3,19 +3,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/check_group/focus_b_focused_none_selected.xml b/widget/testdata/check_group/focus_b_focused_none_selected.xml index 701a337482..a417c2911c 100644 --- a/widget/testdata/check_group/focus_b_focused_none_selected.xml +++ b/widget/testdata/check_group/focus_b_focused_none_selected.xml @@ -3,19 +3,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/check_group/focus_disabled_none_selected.xml b/widget/testdata/check_group/focus_disabled_none_selected.xml index 22a6182f9d..395102e166 100644 --- a/widget/testdata/check_group/focus_disabled_none_selected.xml +++ b/widget/testdata/check_group/focus_disabled_none_selected.xml @@ -3,19 +3,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/check_group/focus_none_focused_b_selected.xml b/widget/testdata/check_group/focus_none_focused_b_selected.xml index aea1789eb5..b39170a60e 100644 --- a/widget/testdata/check_group/focus_none_focused_b_selected.xml +++ b/widget/testdata/check_group/focus_none_focused_b_selected.xml @@ -3,19 +3,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/check_group/focus_none_focused_none_selected.xml b/widget/testdata/check_group/focus_none_focused_none_selected.xml index 682f221e93..7f22358ca3 100644 --- a/widget/testdata/check_group/focus_none_focused_none_selected.xml +++ b/widget/testdata/check_group/focus_none_focused_none_selected.xml @@ -3,19 +3,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/check_group/layout_multiple.xml b/widget/testdata/check_group/layout_multiple.xml index 8c7d391046..25892f1c5c 100644 --- a/widget/testdata/check_group/layout_multiple.xml +++ b/widget/testdata/check_group/layout_multiple.xml @@ -3,13 +3,13 @@ - + Foo - + Bar diff --git a/widget/testdata/check_group/layout_multiple_disabled.xml b/widget/testdata/check_group/layout_multiple_disabled.xml index 9b6934306d..645578ebc5 100644 --- a/widget/testdata/check_group/layout_multiple_disabled.xml +++ b/widget/testdata/check_group/layout_multiple_disabled.xml @@ -3,13 +3,13 @@ - + Foo - + Bar diff --git a/widget/testdata/check_group/layout_multiple_horizontal.xml b/widget/testdata/check_group/layout_multiple_horizontal.xml index 2bfd74f2a6..c8023f8e51 100644 --- a/widget/testdata/check_group/layout_multiple_horizontal.xml +++ b/widget/testdata/check_group/layout_multiple_horizontal.xml @@ -3,13 +3,13 @@ - + Foo - + Barley diff --git a/widget/testdata/check_group/layout_multiple_horizontal_disabled.xml b/widget/testdata/check_group/layout_multiple_horizontal_disabled.xml index e88a895960..e74f293318 100644 --- a/widget/testdata/check_group/layout_multiple_horizontal_disabled.xml +++ b/widget/testdata/check_group/layout_multiple_horizontal_disabled.xml @@ -3,13 +3,13 @@ - + Foo - + Barley diff --git a/widget/testdata/check_group/layout_multiple_selected.xml b/widget/testdata/check_group/layout_multiple_selected.xml index 1f6651a3db..ed6c652cb5 100644 --- a/widget/testdata/check_group/layout_multiple_selected.xml +++ b/widget/testdata/check_group/layout_multiple_selected.xml @@ -3,13 +3,13 @@ - + Foo - + Bar diff --git a/widget/testdata/check_group/layout_multiple_selected_disabled.xml b/widget/testdata/check_group/layout_multiple_selected_disabled.xml index dd12a4e88f..0785167e70 100644 --- a/widget/testdata/check_group/layout_multiple_selected_disabled.xml +++ b/widget/testdata/check_group/layout_multiple_selected_disabled.xml @@ -3,13 +3,13 @@ - + Foo - + Bar diff --git a/widget/testdata/check_group/layout_multiple_selected_horizontal.xml b/widget/testdata/check_group/layout_multiple_selected_horizontal.xml index f1ec2b0716..db7ed8256b 100644 --- a/widget/testdata/check_group/layout_multiple_selected_horizontal.xml +++ b/widget/testdata/check_group/layout_multiple_selected_horizontal.xml @@ -3,13 +3,13 @@ - + Foo - + Bar diff --git a/widget/testdata/check_group/layout_multiple_selected_horizontal_disabled.xml b/widget/testdata/check_group/layout_multiple_selected_horizontal_disabled.xml index 879d0fd6eb..dd5fc14667 100644 --- a/widget/testdata/check_group/layout_multiple_selected_horizontal_disabled.xml +++ b/widget/testdata/check_group/layout_multiple_selected_horizontal_disabled.xml @@ -3,13 +3,13 @@ - + Foo - + Bar diff --git a/widget/testdata/check_group/layout_single.xml b/widget/testdata/check_group/layout_single.xml index 26507a6453..012ef79626 100644 --- a/widget/testdata/check_group/layout_single.xml +++ b/widget/testdata/check_group/layout_single.xml @@ -3,7 +3,7 @@ - + Test diff --git a/widget/testdata/check_group/layout_single_disabled.xml b/widget/testdata/check_group/layout_single_disabled.xml index d4bbdeb1b8..fa22fdfd06 100644 --- a/widget/testdata/check_group/layout_single_disabled.xml +++ b/widget/testdata/check_group/layout_single_disabled.xml @@ -3,7 +3,7 @@ - + Test diff --git a/widget/testdata/check_group/layout_single_horizontal.xml b/widget/testdata/check_group/layout_single_horizontal.xml index 26507a6453..012ef79626 100644 --- a/widget/testdata/check_group/layout_single_horizontal.xml +++ b/widget/testdata/check_group/layout_single_horizontal.xml @@ -3,7 +3,7 @@ - + Test diff --git a/widget/testdata/check_group/layout_single_horizontal_disabled.xml b/widget/testdata/check_group/layout_single_horizontal_disabled.xml index d4bbdeb1b8..fa22fdfd06 100644 --- a/widget/testdata/check_group/layout_single_horizontal_disabled.xml +++ b/widget/testdata/check_group/layout_single_horizontal_disabled.xml @@ -3,7 +3,7 @@ - + Test diff --git a/widget/testdata/check_group/layout_single_selected.xml b/widget/testdata/check_group/layout_single_selected.xml index bb42e6373e..ffcb790f84 100644 --- a/widget/testdata/check_group/layout_single_selected.xml +++ b/widget/testdata/check_group/layout_single_selected.xml @@ -3,7 +3,7 @@ - + Test diff --git a/widget/testdata/check_group/layout_single_selected_disabled.xml b/widget/testdata/check_group/layout_single_selected_disabled.xml index 8aefd3e7c8..57ac177e4f 100644 --- a/widget/testdata/check_group/layout_single_selected_disabled.xml +++ b/widget/testdata/check_group/layout_single_selected_disabled.xml @@ -3,7 +3,7 @@ - + Test diff --git a/widget/testdata/check_group/layout_single_selected_horizontal.xml b/widget/testdata/check_group/layout_single_selected_horizontal.xml index bb42e6373e..ffcb790f84 100644 --- a/widget/testdata/check_group/layout_single_selected_horizontal.xml +++ b/widget/testdata/check_group/layout_single_selected_horizontal.xml @@ -3,7 +3,7 @@ - + Test diff --git a/widget/testdata/check_group/layout_single_selected_horizontal_disabled.xml b/widget/testdata/check_group/layout_single_selected_horizontal_disabled.xml index 8aefd3e7c8..57ac177e4f 100644 --- a/widget/testdata/check_group/layout_single_selected_horizontal_disabled.xml +++ b/widget/testdata/check_group/layout_single_selected_horizontal_disabled.xml @@ -3,7 +3,7 @@ - + Test diff --git a/widget/testdata/radio_group/disabled_append_none_selected.xml b/widget/testdata/radio_group/disabled_append_none_selected.xml index 7eb932e459..f204f12def 100644 --- a/widget/testdata/radio_group/disabled_append_none_selected.xml +++ b/widget/testdata/radio_group/disabled_append_none_selected.xml @@ -4,19 +4,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/radio_group/disabled_none_selected.xml b/widget/testdata/radio_group/disabled_none_selected.xml index f419dd0ecf..b3aa9228a9 100644 --- a/widget/testdata/radio_group/disabled_none_selected.xml +++ b/widget/testdata/radio_group/disabled_none_selected.xml @@ -4,13 +4,13 @@ - + Option A - + Option B diff --git a/widget/testdata/radio_group/focus_a_focused_b_selected.xml b/widget/testdata/radio_group/focus_a_focused_b_selected.xml index 3b42ecab6f..fcd6c30aa3 100644 --- a/widget/testdata/radio_group/focus_a_focused_b_selected.xml +++ b/widget/testdata/radio_group/focus_a_focused_b_selected.xml @@ -4,19 +4,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/radio_group/focus_a_focused_none_selected.xml b/widget/testdata/radio_group/focus_a_focused_none_selected.xml index 11f2eaa8c8..d0ecd4fda4 100644 --- a/widget/testdata/radio_group/focus_a_focused_none_selected.xml +++ b/widget/testdata/radio_group/focus_a_focused_none_selected.xml @@ -4,19 +4,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/radio_group/focus_b_focused_b_selected.xml b/widget/testdata/radio_group/focus_b_focused_b_selected.xml index d070539cab..15fa416c1e 100644 --- a/widget/testdata/radio_group/focus_b_focused_b_selected.xml +++ b/widget/testdata/radio_group/focus_b_focused_b_selected.xml @@ -4,19 +4,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/radio_group/focus_b_focused_none_selected.xml b/widget/testdata/radio_group/focus_b_focused_none_selected.xml index 9751c09166..7995db5bf9 100644 --- a/widget/testdata/radio_group/focus_b_focused_none_selected.xml +++ b/widget/testdata/radio_group/focus_b_focused_none_selected.xml @@ -4,19 +4,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/radio_group/focus_disabled_none_selected.xml b/widget/testdata/radio_group/focus_disabled_none_selected.xml index 62a301b359..9a267062d8 100644 --- a/widget/testdata/radio_group/focus_disabled_none_selected.xml +++ b/widget/testdata/radio_group/focus_disabled_none_selected.xml @@ -4,19 +4,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/radio_group/focus_none_focused_b_selected.xml b/widget/testdata/radio_group/focus_none_focused_b_selected.xml index 51336e960d..8a592de139 100644 --- a/widget/testdata/radio_group/focus_none_focused_b_selected.xml +++ b/widget/testdata/radio_group/focus_none_focused_b_selected.xml @@ -4,19 +4,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/radio_group/focus_none_focused_none_selected.xml b/widget/testdata/radio_group/focus_none_focused_none_selected.xml index ac2236cfae..ca5d65d971 100644 --- a/widget/testdata/radio_group/focus_none_focused_none_selected.xml +++ b/widget/testdata/radio_group/focus_none_focused_none_selected.xml @@ -4,19 +4,19 @@ - + Option A - + Option B - + Option C diff --git a/widget/testdata/radio_group/layout_multiple.xml b/widget/testdata/radio_group/layout_multiple.xml index bd7c1a6df5..5e9d337de2 100644 --- a/widget/testdata/radio_group/layout_multiple.xml +++ b/widget/testdata/radio_group/layout_multiple.xml @@ -4,13 +4,13 @@ - + Foo - + Bar diff --git a/widget/testdata/radio_group/layout_multiple_disabled.xml b/widget/testdata/radio_group/layout_multiple_disabled.xml index 14c7f43950..f2e3053dc9 100644 --- a/widget/testdata/radio_group/layout_multiple_disabled.xml +++ b/widget/testdata/radio_group/layout_multiple_disabled.xml @@ -4,13 +4,13 @@ - + Foo - + Bar diff --git a/widget/testdata/radio_group/layout_multiple_horizontal.xml b/widget/testdata/radio_group/layout_multiple_horizontal.xml index 59f88d34bf..a0f6041358 100644 --- a/widget/testdata/radio_group/layout_multiple_horizontal.xml +++ b/widget/testdata/radio_group/layout_multiple_horizontal.xml @@ -4,13 +4,13 @@ - + Foo - + Barley diff --git a/widget/testdata/radio_group/layout_multiple_horizontal_disabled.xml b/widget/testdata/radio_group/layout_multiple_horizontal_disabled.xml index bee3ac8701..36c60ffd62 100644 --- a/widget/testdata/radio_group/layout_multiple_horizontal_disabled.xml +++ b/widget/testdata/radio_group/layout_multiple_horizontal_disabled.xml @@ -4,13 +4,13 @@ - + Foo - + Barley diff --git a/widget/testdata/radio_group/layout_multiple_selected.xml b/widget/testdata/radio_group/layout_multiple_selected.xml index 74f125cecb..dfd559a4d6 100644 --- a/widget/testdata/radio_group/layout_multiple_selected.xml +++ b/widget/testdata/radio_group/layout_multiple_selected.xml @@ -4,13 +4,13 @@ - + Foo - + Bar diff --git a/widget/testdata/radio_group/layout_multiple_selected_disabled.xml b/widget/testdata/radio_group/layout_multiple_selected_disabled.xml index cc0905f405..240ba2c78c 100644 --- a/widget/testdata/radio_group/layout_multiple_selected_disabled.xml +++ b/widget/testdata/radio_group/layout_multiple_selected_disabled.xml @@ -4,13 +4,13 @@ - + Foo - + Bar diff --git a/widget/testdata/radio_group/layout_multiple_selected_horizontal.xml b/widget/testdata/radio_group/layout_multiple_selected_horizontal.xml index 94176d8932..e77574e249 100644 --- a/widget/testdata/radio_group/layout_multiple_selected_horizontal.xml +++ b/widget/testdata/radio_group/layout_multiple_selected_horizontal.xml @@ -4,13 +4,13 @@ - + Foo - + Bar diff --git a/widget/testdata/radio_group/layout_multiple_selected_horizontal_disabled.xml b/widget/testdata/radio_group/layout_multiple_selected_horizontal_disabled.xml index 28a6728356..6a69f24159 100644 --- a/widget/testdata/radio_group/layout_multiple_selected_horizontal_disabled.xml +++ b/widget/testdata/radio_group/layout_multiple_selected_horizontal_disabled.xml @@ -4,13 +4,13 @@ - + Foo - + Bar diff --git a/widget/testdata/radio_group/layout_single.xml b/widget/testdata/radio_group/layout_single.xml index 3feaffa4c1..d8fd439665 100644 --- a/widget/testdata/radio_group/layout_single.xml +++ b/widget/testdata/radio_group/layout_single.xml @@ -4,7 +4,7 @@ - + Test diff --git a/widget/testdata/radio_group/layout_single_disabled.xml b/widget/testdata/radio_group/layout_single_disabled.xml index 48de7eddbe..efca0270fb 100644 --- a/widget/testdata/radio_group/layout_single_disabled.xml +++ b/widget/testdata/radio_group/layout_single_disabled.xml @@ -4,7 +4,7 @@ - + Test diff --git a/widget/testdata/radio_group/layout_single_horizontal.xml b/widget/testdata/radio_group/layout_single_horizontal.xml index 3feaffa4c1..d8fd439665 100644 --- a/widget/testdata/radio_group/layout_single_horizontal.xml +++ b/widget/testdata/radio_group/layout_single_horizontal.xml @@ -4,7 +4,7 @@ - + Test diff --git a/widget/testdata/radio_group/layout_single_horizontal_disabled.xml b/widget/testdata/radio_group/layout_single_horizontal_disabled.xml index 48de7eddbe..efca0270fb 100644 --- a/widget/testdata/radio_group/layout_single_horizontal_disabled.xml +++ b/widget/testdata/radio_group/layout_single_horizontal_disabled.xml @@ -4,7 +4,7 @@ - + Test diff --git a/widget/testdata/radio_group/layout_single_selected.xml b/widget/testdata/radio_group/layout_single_selected.xml index abd9f33da7..57e4b3862f 100644 --- a/widget/testdata/radio_group/layout_single_selected.xml +++ b/widget/testdata/radio_group/layout_single_selected.xml @@ -4,7 +4,7 @@ - + Test diff --git a/widget/testdata/radio_group/layout_single_selected_disabled.xml b/widget/testdata/radio_group/layout_single_selected_disabled.xml index a17de0ab5b..3a9bd50997 100644 --- a/widget/testdata/radio_group/layout_single_selected_disabled.xml +++ b/widget/testdata/radio_group/layout_single_selected_disabled.xml @@ -4,7 +4,7 @@ - + Test diff --git a/widget/testdata/radio_group/layout_single_selected_horizontal.xml b/widget/testdata/radio_group/layout_single_selected_horizontal.xml index abd9f33da7..57e4b3862f 100644 --- a/widget/testdata/radio_group/layout_single_selected_horizontal.xml +++ b/widget/testdata/radio_group/layout_single_selected_horizontal.xml @@ -4,7 +4,7 @@ - + Test diff --git a/widget/testdata/radio_group/layout_single_selected_horizontal_disabled.xml b/widget/testdata/radio_group/layout_single_selected_horizontal_disabled.xml index a17de0ab5b..3a9bd50997 100644 --- a/widget/testdata/radio_group/layout_single_selected_horizontal_disabled.xml +++ b/widget/testdata/radio_group/layout_single_selected_horizontal_disabled.xml @@ -4,7 +4,7 @@ - + Test From c9d37f1dac6ec1adf6e1fe93638eaa894811dd25 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Thu, 18 Jan 2024 20:20:27 +0000 Subject: [PATCH 32/36] Fix issue with sizing secondary mobile windows --- CHANGELOG.md | 8 ++++++++ internal/driver/mobile/canvas.go | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c2231c0ed..6495228cb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ This file lists the main changes with each version of the Fyne toolkit. More detailed release notes can be found on the [releases page](https://github.com/fyne-io/fyne/releases). +## 2.4.4 - Ongoing + +### Fixed + +* Spaces could be appended to linux Exec command during packaging +* Secondary mobile windows would not size correctly when padded + + ## 2.4.3 - 23 December 2023 ### Fixed diff --git a/internal/driver/mobile/canvas.go b/internal/driver/mobile/canvas.go index 57973ad37c..dc225f942c 100644 --- a/internal/driver/mobile/canvas.go +++ b/internal/driver/mobile/canvas.go @@ -191,7 +191,11 @@ func (c *mobileCanvas) sizeContent(size fyne.Size) { if c.windowHead != nil { topHeight := c.windowHead.MinSize().Height - if len(c.windowHead.(*fyne.Container).Objects) > 1 { + chromeBox := c.windowHead.(*fyne.Container) + if c.padded { + chromeBox = chromeBox.Objects[0].(*fyne.Container) // the padded container + } + if len(chromeBox.Objects) > 1 { c.windowHead.Resize(fyne.NewSize(areaSize.Width, topHeight)) offset = fyne.NewPos(0, topHeight) areaSize = areaSize.Subtract(offset) From efc4c648de73921c25446a8f14745eab9ad8c987 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Thu, 18 Jan 2024 20:24:10 +0000 Subject: [PATCH 33/36] Starting a v2.5 list --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6495228cb4..45996116e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ This file lists the main changes with each version of the Fyne toolkit. More detailed release notes can be found on the [releases page](https://github.com/fyne-io/fyne/releases). +## 2.5.0 - Ongoing + +### Added + + * Activity indicator widget + * InnerWindow and MultipleWindows containers + +### Changed + +### Fixed + + ## 2.4.4 - Ongoing ### Fixed From 592ca660c564f0e243fe4e67f891507655f7c3c5 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sun, 21 Jan 2024 17:30:14 +0100 Subject: [PATCH 34/36] Improve performance when creating progressbar renderers Around a 30+% performance improvement generally. Caches some icon values and allocates the canvas objects inline into the outer struct instead of allocating them in multiple places. --- widget/progressbar.go | 48 ++++++++++++++++++++---------- widget/progressbar_test.go | 15 ++++++++++ widget/progressbarinfinite.go | 43 ++++++++++++++++---------- widget/progressbarinfinite_test.go | 15 ++++++++++ 4 files changed, 90 insertions(+), 31 deletions(-) diff --git a/widget/progressbar.go b/widget/progressbar.go index 390d5d237d..135fb9f159 100644 --- a/widget/progressbar.go +++ b/widget/progressbar.go @@ -15,8 +15,8 @@ import ( type progressRenderer struct { widget.BaseRenderer - background, bar *canvas.Rectangle - label *canvas.Text + background, bar canvas.Rectangle + label canvas.Text progress *ProgressBar } @@ -64,10 +64,13 @@ func (p *progressRenderer) Layout(size fyne.Size) { // applyTheme updates the progress bar to match the current theme func (p *progressRenderer) applyTheme() { - p.background.FillColor = progressBackgroundColor() - p.background.CornerRadius = theme.InputRadiusSize() - p.bar.FillColor = theme.PrimaryColor() - p.bar.CornerRadius = theme.InputRadiusSize() + primaryColor := theme.PrimaryColor() + inputRadius := theme.InputRadiusSize() + + p.background.FillColor = progressBlendColor(primaryColor) + p.background.CornerRadius = inputRadius + p.bar.FillColor = primaryColor + p.bar.CornerRadius = inputRadius p.label.Color = theme.BackgroundColor() p.label.TextSize = theme.TextSize() } @@ -125,13 +128,28 @@ func (p *ProgressBar) CreateRenderer() fyne.WidgetRenderer { p.Max = 1.0 } - background := canvas.NewRectangle(progressBackgroundColor()) - background.CornerRadius = theme.InputRadiusSize() - bar := canvas.NewRectangle(theme.PrimaryColor()) - bar.CornerRadius = theme.InputRadiusSize() - label := canvas.NewText("0%", theme.BackgroundColor()) - label.Alignment = fyne.TextAlignCenter - return &progressRenderer{widget.NewBaseRenderer([]fyne.CanvasObject{background, bar, label}), background, bar, label, p} + cornerRadius := theme.InputRadiusSize() + primaryColor := theme.PrimaryColor() + + renderer := &progressRenderer{ + background: canvas.Rectangle{ + FillColor: progressBlendColor(primaryColor), + CornerRadius: cornerRadius, + }, + bar: canvas.Rectangle{ + FillColor: primaryColor, + CornerRadius: cornerRadius, + }, + label: canvas.Text{ + Text: "0%", + Color: theme.BackgroundColor(), + Alignment: fyne.TextAlignCenter, + }, + progress: p, + } + + renderer.SetObjects([]fyne.CanvasObject{&renderer.background, &renderer.bar, &renderer.label}) + return renderer } // Unbind disconnects any configured data source from this ProgressBar. @@ -162,8 +180,8 @@ func NewProgressBarWithData(data binding.Float) *ProgressBar { return p } -func progressBackgroundColor() color.Color { - r, g, b, a := col.ToNRGBA(theme.PrimaryColor()) +func progressBlendColor(clr color.Color) color.Color { + r, g, b, a := col.ToNRGBA(clr) faded := uint8(a) / 2 return &color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: faded} } diff --git a/widget/progressbar_test.go b/widget/progressbar_test.go index 6a3375e9be..aeac1603ee 100644 --- a/widget/progressbar_test.go +++ b/widget/progressbar_test.go @@ -10,6 +10,21 @@ import ( "github.com/stretchr/testify/assert" ) +var globalProgressRenderer fyne.WidgetRenderer + +func BenchmarkProgressbar(b *testing.B) { + var renderer fyne.WidgetRenderer + widget := &ProgressBar{} + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + renderer = widget.CreateRenderer() + } + + // Avoid having the value optimized out by the compiler. + globalProgressRenderer = renderer +} + func TestNewProgressBarWithData(t *testing.T) { val := binding.NewFloat() val.Set(0.4) diff --git a/widget/progressbarinfinite.go b/widget/progressbarinfinite.go index abf0357284..7d1619f610 100644 --- a/widget/progressbarinfinite.go +++ b/widget/progressbarinfinite.go @@ -19,8 +19,8 @@ const ( type infProgressRenderer struct { widget.BaseRenderer - background, bar *canvas.Rectangle - animation *fyne.Animation + background, bar canvas.Rectangle + animation fyne.Animation running bool progress *ProgressBarInfinite } @@ -52,7 +52,7 @@ func (p *infProgressRenderer) updateBar(done float32) { p.bar.Resize(barSize) p.bar.Move(barPos) - canvas.Refresh(p.bar) + canvas.Refresh(&p.bar) } // Layout the components of the infinite progress bar @@ -65,11 +65,13 @@ func (p *infProgressRenderer) Refresh() { if p.isRunning() { return // we refresh from the goroutine } + cornerRadius := theme.InputRadiusSize() + primaryColor := theme.PrimaryColor() - p.background.FillColor = progressBackgroundColor() - p.background.CornerRadius = theme.InputRadiusSize() - p.bar.FillColor = theme.PrimaryColor() - p.bar.CornerRadius = theme.InputRadiusSize() + p.background.FillColor = progressBlendColor(primaryColor) + p.background.CornerRadius = cornerRadius + p.bar.FillColor = primaryColor + p.bar.CornerRadius = cornerRadius p.background.Refresh() p.bar.Refresh() canvas.Refresh(p.progress.super()) @@ -90,7 +92,8 @@ func (p *infProgressRenderer) start() { p.progress.propertyLock.Lock() defer p.progress.propertyLock.Unlock() - p.animation = fyne.NewAnimation(time.Second*3, p.updateBar) + p.animation.Duration = time.Second * 3 + p.animation.Tick = p.updateBar p.animation.Curve = fyne.AnimationLinear p.animation.RepeatCount = fyne.AnimationRepeatForever p.running = true @@ -157,16 +160,24 @@ func (p *ProgressBarInfinite) MinSize() fyne.Size { // CreateRenderer is a private method to Fyne which links this widget to its renderer func (p *ProgressBarInfinite) CreateRenderer() fyne.WidgetRenderer { p.ExtendBaseWidget(p) - background := canvas.NewRectangle(progressBackgroundColor()) - background.CornerRadius = theme.InputRadiusSize() - bar := canvas.NewRectangle(theme.PrimaryColor()) - bar.CornerRadius = theme.InputRadiusSize() + + primaryColor := theme.PrimaryColor() + cornerRadius := theme.InputRadiusSize() + render := &infProgressRenderer{ - BaseRenderer: widget.NewBaseRenderer([]fyne.CanvasObject{background, bar}), - background: background, - bar: bar, - progress: p, + background: canvas.Rectangle{ + FillColor: progressBlendColor(primaryColor), + CornerRadius: cornerRadius, + }, + bar: canvas.Rectangle{ + FillColor: primaryColor, + CornerRadius: cornerRadius, + }, + progress: p, } + + render.SetObjects([]fyne.CanvasObject{&render.background, &render.bar}) + render.start() return render } diff --git a/widget/progressbarinfinite_test.go b/widget/progressbarinfinite_test.go index 853255ea30..9d9e710ce9 100644 --- a/widget/progressbarinfinite_test.go +++ b/widget/progressbarinfinite_test.go @@ -10,6 +10,21 @@ import ( "github.com/stretchr/testify/assert" ) +var globalProgressInfRenderer fyne.WidgetRenderer + +func BenchmarkProgressbarInf(b *testing.B) { + var renderer fyne.WidgetRenderer + widget := &ProgressBarInfinite{} + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + renderer = widget.CreateRenderer() + } + + // Avoid having the value optimized out by the compiler. + globalProgressInfRenderer = renderer +} + func TestProgressBarInfinite_Creation(t *testing.T) { bar := NewProgressBarInfinite() // ticker should start automatically From 6d6cffc852d079580a09062100a810dce250d8b0 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sun, 21 Jan 2024 21:55:36 +0100 Subject: [PATCH 35/36] Optimize radio item renderer to be 50% faster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit goos: linux goarch: amd64 pkg: fyne.io/fyne/v2/widget cpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ RadioCreateRenderer-8 664.0n ± 2% 351.4n ± 2% -47.09% (p=0.000 n=10) │ old.txt │ new.txt │ │ B/op │ B/op vs base │ RadioCreateRenderer-8 736.0 ± 0% 704.0 ± 0% -4.35% (p=0.000 n=10) │ old.txt │ new.txt │ │ allocs/op │ allocs/op vs base │ RadioCreateRenderer-8 9.000 ± 0% 4.000 ± 0% -55.56% (p=0.000 n=10) --- widget/radio_item.go | 26 ++++++++------------------ widget/radio_item_test.go | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/widget/radio_item.go b/widget/radio_item.go index 3aab898854..6325635da1 100644 --- a/widget/radio_item.go +++ b/widget/radio_item.go @@ -37,19 +37,8 @@ type radioItem struct { // // Implements: fyne.Widget func (i *radioItem) CreateRenderer() fyne.WidgetRenderer { - focusIndicator := canvas.NewCircle(color.Transparent) - icon := canvas.NewImageFromResource(theme.RadioButtonFillIcon()) - over := canvas.NewImageFromResource(theme.NewThemedResource(theme.RadioButtonIcon())) - label := canvas.NewText(i.Label, theme.ForegroundColor()) - label.Alignment = fyne.TextAlignLeading - r := &radioItemRenderer{ - BaseRenderer: widget.NewBaseRenderer([]fyne.CanvasObject{focusIndicator, icon, over, label}), - focusIndicator: focusIndicator, - icon: icon, - over: over, - item: i, - label: label, - } + r := &radioItemRenderer{item: i, label: canvas.Text{Alignment: fyne.TextAlignLeading}} + r.SetObjects([]fyne.CanvasObject{&r.focusIndicator, &r.icon, &r.over, &r.label}) r.update() return r } @@ -149,11 +138,11 @@ func (i *radioItem) toggle() { type radioItemRenderer struct { widget.BaseRenderer + item *radioItem - focusIndicator *canvas.Circle - icon, over *canvas.Image - item *radioItem - label *canvas.Text + focusIndicator canvas.Circle + icon, over canvas.Image + label canvas.Text } func (r *radioItemRenderer) Layout(size fyne.Size) { @@ -191,10 +180,11 @@ func (r *radioItemRenderer) Refresh() { func (r *radioItemRenderer) update() { r.label.Text = r.item.Label - r.label.Color = theme.ForegroundColor() r.label.TextSize = theme.TextSize() if r.item.Disabled() { r.label.Color = theme.DisabledColor() + } else { + r.label.Color = theme.ForegroundColor() } out := theme.NewThemedResource(theme.RadioButtonIcon()) diff --git a/widget/radio_item_test.go b/widget/radio_item_test.go index b02d229f23..0f63906e14 100644 --- a/widget/radio_item_test.go +++ b/widget/radio_item_test.go @@ -10,6 +10,21 @@ import ( "github.com/stretchr/testify/assert" ) +var globalRadioRenderer fyne.WidgetRenderer + +func BenchmarkRadioCreateRenderer(b *testing.B) { + var renderer fyne.WidgetRenderer + widget := &radioItem{} + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + renderer = widget.CreateRenderer() + } + + // Avoid having the value optimized out by the compiler. + globalRadioRenderer = renderer +} + func TestRadioItem_FocusIndicator_Centered_Vertically(t *testing.T) { item := newRadioItem("Hello", nil) render := test.WidgetRenderer(item).(*radioItemRenderer) From 8c59d6e4891c1d8bcec4ad6a0c0667b771e6b9e3 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Mon, 22 Jan 2024 13:01:02 +0000 Subject: [PATCH 36/36] Fix up some broken or missing since lines --- widget/importance.go | 6 +++--- widget/label.go | 21 ++++++++++++++------- widget/richtext.go | 10 +++++++--- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/widget/importance.go b/widget/importance.go index c9d5a468b6..0a395b0d7e 100644 --- a/widget/importance.go +++ b/widget/importance.go @@ -15,15 +15,15 @@ const ( // DangerImportance applies an error theme to the widget. // - // Since 2.3 + // Since: 2.3 DangerImportance // WarningImportance applies a warning theme to the widget. // - // Since 2.3 + // Since: 2.3 WarningImportance // SuccessImportance applies a success theme to the widget. // - // Since 2.4 + // Since: 2.4 SuccessImportance ) diff --git a/widget/label.go b/widget/label.go index 4f8ddc77fb..8ca9f72c3f 100644 --- a/widget/label.go +++ b/widget/label.go @@ -10,15 +10,22 @@ import ( // Label widget is a label component with appropriate padding and layout. type Label struct { BaseWidget - Text string - Alignment fyne.TextAlign // The alignment of the text - Wrapping fyne.TextWrap // The wrapping of the text - TextStyle fyne.TextStyle // The style of the label text - Truncation fyne.TextTruncation // The truncation mode of the text - provider *RichText + Text string + Alignment fyne.TextAlign // The alignment of the text + Wrapping fyne.TextWrap // The wrapping of the text + TextStyle fyne.TextStyle // The style of the label text + + // The truncation mode of the text + // + // Since: 2.4 + Truncation fyne.TextTruncation + // Importance informs how the label should be styled, i.e. warning or disabled + // + // Since: 2.4 Importance Importance - binder basicBinder + provider *RichText + binder basicBinder } // NewLabel creates a new label widget with the set text content diff --git a/widget/richtext.go b/widget/richtext.go index 0fe89a73d6..e6124c54e6 100644 --- a/widget/richtext.go +++ b/widget/richtext.go @@ -28,9 +28,13 @@ const ( // Since: 2.1 type RichText struct { BaseWidget - Segments []RichTextSegment - Wrapping fyne.TextWrap - Scroll widget.ScrollDirection + Segments []RichTextSegment + Wrapping fyne.TextWrap + Scroll widget.ScrollDirection + + // The truncation mode of the text + // + // Since: 2.4 Truncation fyne.TextTruncation inset fyne.Size // this varies due to how the widget works (entry with scroller vs others with padding)