diff --git a/CHANGELOG.md b/CHANGELOG.md index f69ba7c3fc..d76a9df3dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,24 @@ 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). +## 1.2.1 - Under development + +### Added + +* Add TouchDown, TouchUp and TouchCancel API in driver/mobile for device specific events +* Add support for adding and removing tabs from a tab container (#444) + +### Fixed + +* Issues when settings changes may not be monitored (#576) +* Layout of hidden tab container contents on mobile (#578) +* Mobile apps would not quit when Quit() was called (#580) +* Shadows disappeared when theme changes (#589) +* iOS apps could stop rendering after many refreshes (#584) +* Fyne package could fail on Windows (#586) +* Horizontal only scroll container may not refresh using scroll wheel + + ## 1.2 - 12 December 2019 ### Added diff --git a/app.go b/app.go index ac73c28812..407f5924e6 100644 --- a/app.go +++ b/app.go @@ -31,6 +31,8 @@ type App interface { // Calling Quit on the application will cause the application to exit // cleanly, closing all open windows. + // This should be used with caution, many platforms discourage exiting an application + // from within the code and some mobile systems do not allow it. Quit() // Driver returns the driver that is rendering this application. diff --git a/cmd/fyne/build.go b/cmd/fyne/build.go index 95f16a1695..03d9485370 100644 --- a/cmd/fyne/build.go +++ b/cmd/fyne/build.go @@ -17,10 +17,15 @@ func (b *builder) build() error { goos = runtime.GOOS } - cmd := exec.Command("go", "build", b.srcdir) + var cmd *exec.Cmd + if goos == "windows" { + cmd = exec.Command("go", "build", "-ldflags", "-H=windowsgui", ".") + } else { + cmd = exec.Command("go", "build", ".") + } cmd.Dir = b.srcdir env := os.Environ() - env = append(env, "GOOS=", goos) + env = append(env, "GOOS="+goos) cmd.Env = env out, err := cmd.CombinedOutput() diff --git a/cmd/fyne_demo/screens/widget.go b/cmd/fyne_demo/screens/widget.go index ddc47f730b..04c098ac36 100644 --- a/cmd/fyne_demo/screens/widget.go +++ b/cmd/fyne_demo/screens/widget.go @@ -114,13 +114,12 @@ func makeScrollTab() fyne.CanvasObject { })) } - scroll := widget.NewScrollContainer(list) - scroll.Resize(fyne.NewSize(200, 300)) + horiz := widget.NewScrollContainer(list) + vert := widget.NewScrollContainer(list2) - scroll2 := widget.NewScrollContainer(list2) - scroll2.Resize(fyne.NewSize(200, 100)) - - return fyne.NewContainerWithLayout(layout.NewGridLayout(1), scroll, scroll2) + scrolls := fyne.NewContainerWithLayout(layout.NewBorderLayout(horiz, nil, nil, nil), + horiz, vert) + return fyne.NewContainerWithLayout(layout.NewAdaptiveGridLayout(2), scrolls, makeScrollBothTab()) } func makeScrollBothTab() fyne.CanvasObject { @@ -151,7 +150,6 @@ func WidgetScreen() fyne.CanvasObject { widget.NewTabItem("Progress", makeProgressTab()), widget.NewTabItem("Form", makeFormTab()), widget.NewTabItem("Scroll", makeScrollTab()), - widget.NewTabItem("Full Scroll", makeScrollBothTab()), ), ) } diff --git a/internal/driver/gomobile/driver.go b/internal/driver/gomobile/driver.go index c0215b3c15..37538b7f9c 100644 --- a/internal/driver/gomobile/driver.go +++ b/internal/driver/gomobile/driver.go @@ -1,6 +1,7 @@ package gomobile import ( + "fmt" "runtime" "strconv" "time" @@ -26,6 +27,7 @@ const tapSecondaryDelay = 300 * time.Millisecond type mobileDriver struct { app app.App + quit bool glctx gl.Context windows []fyne.Window @@ -70,7 +72,7 @@ func (d *mobileDriver) CanvasForObject(fyne.CanvasObject) fyne.Canvas { return nil } - // TODO figure out how we handle multiple windows... + // TODO don't just assume it refers to the topmost window return d.currentWindow().Canvas() } @@ -94,14 +96,13 @@ func (d *mobileDriver) Quit() { return } - // TODO? often mobile apps should not allow this... - d.app.Send(lifecycle.Event{From: lifecycle.StageAlive, To: lifecycle.StageDead, DrawContext: nil}) + // TODO should this be disabled for iOS? + d.quit = true } func (d *mobileDriver) Run() { app.Main(func(a app.App) { d.app = a - quit := false var currentSize size.Event for e := range a.Events() { @@ -123,7 +124,7 @@ func (d *mobileDriver) Run() { d.glctx = nil } if e.Crosses(lifecycle.StageAlive) == lifecycle.CrossOff { - quit = true + d.quit = true } case size.Event: currentSize = e @@ -136,9 +137,17 @@ func (d *mobileDriver) Run() { canvas.painter.Init() // we cannot init until the context is set above } - d.freeDirtyTextures(canvas) - d.paintWindow(current, currentSize) - a.Publish() + if d.freeDirtyTextures(canvas) { + d.paintWindow(current, currentSize) + a.Publish() + + err := d.glctx.GetError() + if err != 0 { + fyne.LogError(fmt.Sprintf("OpenGL Error: %d", err), nil) + } + } + + time.Sleep(time.Millisecond * 10) a.Send(paint.Event{}) case touch.Event: switch e.Type { @@ -157,7 +166,7 @@ func (d *mobileDriver) Run() { } } - if quit { + if d.quit { break } } @@ -165,9 +174,6 @@ func (d *mobileDriver) Run() { } func (d *mobileDriver) onStart() { - for _, win := range d.AllWindows() { - win.Canvas().(*mobileCanvas).painter.Init() // we cannot init until the context is set above - } } func (d *mobileDriver) onStop() { @@ -422,17 +428,19 @@ func (d *mobileDriver) typeUpCanvas(canvas *mobileCanvas, r rune, code key.Code) } -func (d *mobileDriver) freeDirtyTextures(canvas *mobileCanvas) { +func (d *mobileDriver) freeDirtyTextures(canvas *mobileCanvas) bool { + freed := false for { select { case object := <-canvas.refreshQueue: + freed = true freeWalked := func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool { canvas.painter.Free(obj) return false } driver.WalkCompleteObjectTree(object, freeWalked, nil) default: - return + return freed } } } @@ -448,7 +456,5 @@ func (d *mobileDriver) Device() fyne.Device { // NewGoMobileDriver sets up a new Driver instance implemented using the Go // Mobile extension and OpenGL bindings. func NewGoMobileDriver() fyne.Driver { - driver := new(mobileDriver) - - return driver + return new(mobileDriver) } diff --git a/internal/painter/gl/gl_common.go b/internal/painter/gl/gl_common.go index 6d4bba2199..dd3cff8ead 100644 --- a/internal/painter/gl/gl_common.go +++ b/internal/painter/gl/gl_common.go @@ -5,10 +5,9 @@ import ( "fyne.io/fyne" "fyne.io/fyne/canvas" + "fyne.io/fyne/internal/cache" "fyne.io/fyne/internal/painter" "fyne.io/fyne/theme" - "fyne.io/fyne/widget" - "github.com/goki/freetype" "github.com/goki/freetype/truetype" "github.com/srwiley/rasterx" @@ -84,7 +83,7 @@ func (p *glPainter) newGlLineTexture(obj fyne.CanvasObject) Texture { func (p *glPainter) newGlRectTexture(rect fyne.CanvasObject) Texture { col := theme.BackgroundColor() if wid, ok := rect.(fyne.Widget); ok { - widCol := widget.Renderer(wid).BackgroundColor() + widCol := cache.Renderer(wid).BackgroundColor() if widCol != nil { col = widCol } diff --git a/internal/painter/gl/gl_core.go b/internal/painter/gl/gl_core.go index 2f34b693e6..a3b13c9229 100644 --- a/internal/painter/gl/gl_core.go +++ b/internal/painter/gl/gl_core.go @@ -87,6 +87,7 @@ func glInit() { } gl.Disable(gl.DEPTH_TEST) + gl.Enable(gl.BLEND) } func compileShader(source string, shaderType uint32) (uint32, error) { @@ -198,7 +199,6 @@ func (p *glPainter) glFreeBuffer(vbo Buffer) { } func (p *glPainter) glDrawTexture(texture Texture, alpha float32) { - gl.Enable(gl.BLEND) // here we have to choose between blending the image alpha or fading it... // TODO find a way to support both if alpha != 1.0 { diff --git a/internal/painter/gl/gl_es.go b/internal/painter/gl/gl_es.go index 74ceaa4073..32f94f121a 100644 --- a/internal/painter/gl/gl_es.go +++ b/internal/painter/gl/gl_es.go @@ -88,6 +88,7 @@ func glInit() { } gl.Disable(gl.DEPTH_TEST) + gl.Enable(gl.BLEND) } func compileShader(source string, shaderType uint32) (uint32, error) { @@ -199,7 +200,6 @@ func (p *glPainter) glFreeBuffer(vbo Buffer) { } func (p *glPainter) glDrawTexture(texture Texture, alpha float32) { - gl.Enable(gl.BLEND) // here we have to choose between blending the image alpha or fading it... // TODO find a way to support both if alpha != 1.0 { diff --git a/internal/painter/gl/gl_gomobile.go b/internal/painter/gl/gl_gomobile.go index 5aadcbbed2..bf7d730e8c 100644 --- a/internal/painter/gl/gl_gomobile.go +++ b/internal/painter/gl/gl_gomobile.go @@ -126,6 +126,7 @@ const ( func (p *glPainter) Init() { p.glctx().Disable(gl.DEPTH_TEST) + p.glctx().Enable(gl.BLEND) sharedBuffer = p.glctx().CreateBuffer() vertexShader, err := p.compileShader(vertexShaderSource, gl.VERTEX_SHADER) @@ -179,9 +180,12 @@ func (p *glPainter) glCreateBuffer(points []float32) Buffer { return Buffer(sharedBuffer) } +func (p *glPainter) glFreeBuffer(_ Buffer) { + // we don't free a shared buffer! +} + func (p *glPainter) glDrawTexture(texture Texture, alpha float32) { ctx := p.glctx() - ctx.Enable(gl.BLEND) // here we have to choose between blending the image alpha or fading it... // TODO find a way to support both diff --git a/internal/painter/gl/gl_gomobile_ios.go b/internal/painter/gl/gl_gomobile_ios.go deleted file mode 100644 index 8c42e88a0c..0000000000 --- a/internal/painter/gl/gl_gomobile_ios.go +++ /dev/null @@ -1,12 +0,0 @@ -// +build ios - -package gl - -import "golang.org/x/mobile/gl" - -func (p *glPainter) glFreeBuffer(vbo Buffer) { - ctx := p.glctx() - - ctx.BindBuffer(gl.ARRAY_BUFFER, gl.Buffer(vbo)) - ctx.DeleteBuffer(gl.Buffer(vbo)) -} diff --git a/internal/painter/gl/gl_gomobile_other.go b/internal/painter/gl/gl_gomobile_other.go deleted file mode 100644 index bf44738359..0000000000 --- a/internal/painter/gl/gl_gomobile_other.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build android mobile - -package gl - -func (p *glPainter) glFreeBuffer(_ Buffer) { - // we don't free a shared buffer! -} diff --git a/widget/button.go b/widget/button.go index 57ceea366a..6ab986d0f7 100644 --- a/widget/button.go +++ b/widget/button.go @@ -13,7 +13,7 @@ import ( type buttonRenderer struct { icon *canvas.Image label *canvas.Text - shadow fyne.CanvasObject + shadow *shadow objects []fyne.CanvasObject button *Button @@ -125,6 +125,10 @@ func (b *buttonRenderer) Refresh() { b.icon.Hide() } + if b.shadow != nil { + b.shadow.depth = theme.Padding() / 2 + } + b.Layout(b.button.Size()) canvas.Refresh(b.button) } @@ -206,7 +210,7 @@ func (b *Button) CreateRenderer() fyne.WidgetRenderer { objects := []fyne.CanvasObject{ text, } - var shadow fyne.CanvasObject + var shadow *shadow if !b.HideShadow { shadow = newShadow(shadowAround, theme.Padding()/2) objects = append(objects, shadow) diff --git a/widget/scroller.go b/widget/scroller.go index 679d3fd352..9f2e56f789 100644 --- a/widget/scroller.go +++ b/widget/scroller.go @@ -229,8 +229,8 @@ type scrollContainerRenderer struct { scroll *ScrollContainer vertArea *scrollBarArea horizArea *scrollBarArea - leftShadow, rightShadow fyne.CanvasObject - topShadow, bottomShadow fyne.CanvasObject + leftShadow, rightShadow *shadow + topShadow, bottomShadow *shadow objects []fyne.CanvasObject } @@ -265,7 +265,7 @@ func (r *scrollContainerRenderer) Layout(size fyne.Size) { } func (r *scrollContainerRenderer) MinSize() fyne.Size { - return fyne.NewSize(25, 25) // TODO consider the smallest useful scroll view? + return fyne.NewSize(32, 32) // TODO consider the smallest useful scroll view? } func (r *scrollContainerRenderer) Objects() []fyne.CanvasObject { @@ -273,6 +273,11 @@ func (r *scrollContainerRenderer) Objects() []fyne.CanvasObject { } func (r *scrollContainerRenderer) Refresh() { + r.leftShadow.depth = theme.Padding() * 2 + r.rightShadow.depth = theme.Padding() * 2 + r.topShadow.depth = theme.Padding() * 2 + r.bottomShadow.depth = theme.Padding() * 2 + r.Layout(r.scroll.Size()) } @@ -315,7 +320,8 @@ func (r *scrollContainerRenderer) updatePosition() { Renderer(r.vertArea).Layout(r.scroll.size) Renderer(r.horizArea).Layout(r.scroll.size) - canvas.Refresh(r.vertArea) // this is required to force the canvas to update, we have no "Redraw()" + canvas.Refresh(r.vertArea) // this is required to force the canvas to update, we have no "Redraw()" + canvas.Refresh(r.horizArea) // this is required like above but if we are horizontal } // ScrollContainer defines a container that is smaller than the Content. diff --git a/widget/shadow.go b/widget/shadow.go index 674d70f52d..d7003acd3d 100644 --- a/widget/shadow.go +++ b/widget/shadow.go @@ -81,6 +81,45 @@ func (r *shadowRenderer) createShadows() { } } +func updateShadowStart(g *canvas.LinearGradient) { + if g == nil { + return + } + + g.StartColor = theme.ShadowColor() + g.Refresh() +} + +func updateShadowEnd(g *canvas.LinearGradient) { + if g == nil { + return + } + + g.EndColor = theme.ShadowColor() + g.Refresh() +} + +func updateShadowRadial(g *canvas.RadialGradient) { + if g == nil { + return + } + + g.StartColor = theme.ShadowColor() + g.Refresh() +} + +func (r *shadowRenderer) refreshShadows() { + updateShadowEnd(r.l) + updateShadowStart(r.r) + updateShadowStart(r.b) + updateShadowEnd(r.t) + + updateShadowRadial(r.tl) + updateShadowRadial(r.tr) + updateShadowRadial(r.bl) + updateShadowRadial(r.br) +} + func (r *shadowRenderer) BackgroundColor() color.Color { return color.Transparent } @@ -132,5 +171,5 @@ func (r *shadowRenderer) Objects() []fyne.CanvasObject { } func (r *shadowRenderer) Refresh() { - r.createShadows() + r.refreshShadows() } diff --git a/widget/shadow_test.go b/widget/shadow_test.go index 8e41150c59..8ddcb5bf8d 100644 --- a/widget/shadow_test.go +++ b/widget/shadow_test.go @@ -5,6 +5,7 @@ import ( "testing" "fyne.io/fyne" + "fyne.io/fyne/internal/cache" "fyne.io/fyne/theme" "github.com/stretchr/testify/assert" ) @@ -119,3 +120,20 @@ func TestShadow_BackgroundColor(t *testing.T) { func TestShadow_MinSize(t *testing.T) { assert.Equal(t, fyne.NewSize(0, 0), newShadow(shadowAround, theme.Padding()).MinSize()) } + +func TestShadow_Theme(t *testing.T) { + shadow := newShadow(shadowAround, theme.Padding()) + light := theme.LightTheme() + fyne.CurrentApp().Settings().SetTheme(light) + shadow.Refresh() + assert.Equal(t, light.ShadowColor(), cache.Renderer(shadow).(*shadowRenderer).l.EndColor) + assert.Equal(t, light.ShadowColor(), cache.Renderer(shadow).(*shadowRenderer).r.StartColor) + assert.Equal(t, light.ShadowColor(), cache.Renderer(shadow).(*shadowRenderer).tr.StartColor) + + dark := theme.DarkTheme() + fyne.CurrentApp().Settings().SetTheme(dark) + shadow.Refresh() + assert.Equal(t, dark.ShadowColor(), cache.Renderer(shadow).(*shadowRenderer).r.StartColor) + assert.Equal(t, dark.ShadowColor(), cache.Renderer(shadow).(*shadowRenderer).r.StartColor) + assert.Equal(t, dark.ShadowColor(), cache.Renderer(shadow).(*shadowRenderer).tr.StartColor) +} diff --git a/widget/tabcontainer.go b/widget/tabcontainer.go index a7a55f7f31..0ea198164e 100644 --- a/widget/tabcontainer.go +++ b/widget/tabcontainer.go @@ -7,6 +7,7 @@ import ( "fyne.io/fyne/canvas" "fyne.io/fyne/driver/desktop" "fyne.io/fyne/internal" + "fyne.io/fyne/internal/cache" "fyne.io/fyne/layout" "fyne.io/fyne/theme" ) @@ -88,6 +89,8 @@ func (t *TabContainer) SelectTabIndex(index int) { } } + r := cache.Renderer(t).(*tabContainerRenderer) + r.Layout(t.size) t.refresh(t) } @@ -100,26 +103,6 @@ func (t *TabContainer) makeButton(item *TabItem) *tabButton { return &tabButton{Text: item.Text, Icon: item.Icon, OnTap: func() { t.SelectTab(item) }} } -/* -// Prepend inserts a new CanvasObject at the top of the group -func (t *TabContainer) Prepend(object fyne.CanvasObject) { - t.Items = append(t.Items, item) - t.tabBar.Append(t.makeButton(item)) - t.children = append(t.children, item.Content) - - t.CreateRenderer().Refresh()} -} - -// Append adds a new CanvasObject to the end of the group -func (t *TabContainer) Append(item TabItem) { - t.Items = append(t.Items, item) -// t.tabBar.Append(t.makeButton(item)) -// t.children = append(t.children, item.Content) - - t.CreateRenderer().Refresh() -} -*/ - // CreateRenderer is a private method to Fyne which links this widget to its renderer func (t *TabContainer) CreateRenderer() fyne.WidgetRenderer { t.ExtendBaseWidget(t) @@ -163,10 +146,38 @@ func (t *TabContainer) buildTabBar(buttons []fyne.CanvasObject) *fyne.Container return tabBar } +// Append adds a new TabItem to the rightmost side of the tab panel +func (t *TabContainer) Append(item *TabItem) { + r := cache.Renderer(t).(*tabContainerRenderer) + t.Items = append(t.Items, item) + r.tabBar.Objects = append(r.tabBar.Objects, t.makeButton(item)) + + t.Refresh() +} + +// Remove tab by value +func (t *TabContainer) Remove(item *TabItem) { + for index, existingItem := range t.Items { + if existingItem == item { + t.RemoveIndex(index) + break + } + } +} + +// RemoveIndex removes tab by index +func (t *TabContainer) RemoveIndex(index int) { + r := cache.Renderer(t).(*tabContainerRenderer) + t.Items = append(t.Items[:index], t.Items[index+1:]...) + r.tabBar.Objects = append(r.tabBar.Objects[:index], r.tabBar.Objects[index+1:]...) + + t.Refresh() +} + // SetTabLocation sets the location of the tab bar func (t *TabContainer) SetTabLocation(l TabLocation) { t.tabLocation = l - r := Renderer(t).(*tabContainerRenderer) + r := cache.Renderer(t).(*tabContainerRenderer) buttons := r.tabBar.Objects if fyne.CurrentDevice().IsMobile() || l == TabLocationLeading || l == TabLocationTrailing { for _, b := range buttons { diff --git a/widget/tabcontainer_test.go b/widget/tabcontainer_test.go index a82af9167d..881d93b0e6 100644 --- a/widget/tabcontainer_test.go +++ b/widget/tabcontainer_test.go @@ -366,6 +366,30 @@ func TestTabContainerRenderer_Layout(t *testing.T) { } } +func TestTabContainer_DynamicTabs(t *testing.T) { + item := NewTabItem("Text1", canvas.NewCircle(theme.BackgroundColor())) + tabs := NewTabContainer(item) + r := Renderer(tabs).(*tabContainerRenderer) + + appendedItem := NewTabItem("Text2", canvas.NewCircle(theme.BackgroundColor())) + + tabs.Append(appendedItem) + assert.Equal(t, len(tabs.Items), 2) + assert.Equal(t, len(r.tabBar.Objects), 2) + assert.Equal(t, tabs.Items[1].Text, appendedItem.Text) + + tabs.RemoveIndex(1) + assert.Equal(t, len(tabs.Items), 1) + assert.Equal(t, len(r.tabBar.Objects), 1) + assert.Equal(t, tabs.Items[0].Text, item.Text) + + tabs.Append(appendedItem) + tabs.Remove(tabs.Items[0]) + assert.Equal(t, len(tabs.Items), 1) + assert.Equal(t, len(r.tabBar.Objects), 1) + assert.Equal(t, tabs.Items[0].Text, appendedItem.Text) +} + func Test_tabButton_Hovered(t *testing.T) { b := &tabButton{} r := Renderer(b)