Эх сурвалжийг харах

d05 Права на файлы

user 2 жил өмнө
parent
commit
16d38111e1
100 өөрчлөгдсөн 14994 нэмэгдсэн , 0 устгасан
  1. 83 0
      .env
  2. 140 0
      cmd/server/main.go
  3. 9 0
      pkg/components/scene_net/netstat/netstat.go
  4. 7 0
      pkg/types/iis_shot.go
  5. 27 0
      pkg/types/inetangar.go
  6. 41 0
      server/helper/helper.go
  7. 54 0
      server/serv_bots/warbot/warbot_net/bot_net_login/bot_net_login.go
  8. 1 0
      vendor/fyne.io/fyne/v2/.godocdown.import
  9. 14 0
      vendor/fyne.io/fyne/v2/AUTHORS
  10. 1262 0
      vendor/fyne.io/fyne/v2/CHANGELOG.md
  11. 76 0
      vendor/fyne.io/fyne/v2/CODE_OF_CONDUCT.md
  12. 63 0
      vendor/fyne.io/fyne/v2/CONTRIBUTING.md
  13. 28 0
      vendor/fyne.io/fyne/v2/LICENSE
  14. 188 0
      vendor/fyne.io/fyne/v2/README.md
  15. 15 0
      vendor/fyne.io/fyne/v2/SECURITY.md
  16. 84 0
      vendor/fyne.io/fyne/v2/animation.go
  17. 144 0
      vendor/fyne.io/fyne/v2/app.go
  18. 169 0
      vendor/fyne.io/fyne/v2/app/app.go
  19. 60 0
      vendor/fyne.io/fyne/v2/app/app_darwin.go
  20. 61 0
      vendor/fyne.io/fyne/v2/app/app_darwin.m
  21. 8 0
      vendor/fyne.io/fyne/v2/app/app_debug.go
  22. 69 0
      vendor/fyne.io/fyne/v2/app/app_desktop_darwin.go
  23. 18 0
      vendor/fyne.io/fyne/v2/app/app_desktop_darwin.m
  24. 15 0
      vendor/fyne.io/fyne/v2/app/app_gl.go
  25. 19 0
      vendor/fyne.io/fyne/v2/app/app_goxjs.go
  26. 25 0
      vendor/fyne.io/fyne/v2/app/app_mobile.go
  27. 131 0
      vendor/fyne.io/fyne/v2/app/app_mobile_and.c
  28. 61 0
      vendor/fyne.io/fyne/v2/app/app_mobile_and.go
  29. 40 0
      vendor/fyne.io/fyne/v2/app/app_mobile_ios.go
  30. 16 0
      vendor/fyne.io/fyne/v2/app/app_mobile_ios.m
  31. 9 0
      vendor/fyne.io/fyne/v2/app/app_notlegacy_darwin.go
  32. 20 0
      vendor/fyne.io/fyne/v2/app/app_openurl_js.go
  33. 19 0
      vendor/fyne.io/fyne/v2/app/app_openurl_wasm.go
  34. 13 0
      vendor/fyne.io/fyne/v2/app/app_openurl_web.go
  35. 34 0
      vendor/fyne.io/fyne/v2/app/app_other.go
  36. 8 0
      vendor/fyne.io/fyne/v2/app/app_release.go
  37. 16 0
      vendor/fyne.io/fyne/v2/app/app_software.go
  38. 8 0
      vendor/fyne.io/fyne/v2/app/app_standard.go
  39. 29 0
      vendor/fyne.io/fyne/v2/app/app_theme_js.go
  40. 31 0
      vendor/fyne.io/fyne/v2/app/app_theme_wasm.go
  41. 13 0
      vendor/fyne.io/fyne/v2/app/app_theme_web.go
  42. 124 0
      vendor/fyne.io/fyne/v2/app/app_windows.go
  43. 202 0
      vendor/fyne.io/fyne/v2/app/app_xdg.go
  44. 47 0
      vendor/fyne.io/fyne/v2/app/cloud.go
  45. 28 0
      vendor/fyne.io/fyne/v2/app/meta.go
  46. 208 0
      vendor/fyne.io/fyne/v2/app/preferences.go
  47. 21 0
      vendor/fyne.io/fyne/v2/app/preferences_android.go
  48. 24 0
      vendor/fyne.io/fyne/v2/app/preferences_ios.go
  49. 20 0
      vendor/fyne.io/fyne/v2/app/preferences_mobile.go
  50. 29 0
      vendor/fyne.io/fyne/v2/app/preferences_other.go
  51. 168 0
      vendor/fyne.io/fyne/v2/app/settings.go
  52. 75 0
      vendor/fyne.io/fyne/v2/app/settings_desktop.go
  53. 35 0
      vendor/fyne.io/fyne/v2/app/settings_file.go
  54. 24 0
      vendor/fyne.io/fyne/v2/app/settings_goxjs.go
  55. 12 0
      vendor/fyne.io/fyne/v2/app/settings_mobile.go
  56. 8 0
      vendor/fyne.io/fyne/v2/app/settings_noanimation.go
  57. 27 0
      vendor/fyne.io/fyne/v2/app/storage.go
  58. 58 0
      vendor/fyne.io/fyne/v2/canvas.go
  59. 86 0
      vendor/fyne.io/fyne/v2/canvas/animation.go
  60. 100 0
      vendor/fyne.io/fyne/v2/canvas/base.go
  61. 29 0
      vendor/fyne.io/fyne/v2/canvas/canvas.go
  62. 90 0
      vendor/fyne.io/fyne/v2/canvas/circle.go
  63. 212 0
      vendor/fyne.io/fyne/v2/canvas/gradient.go
  64. 362 0
      vendor/fyne.io/fyne/v2/canvas/image.go
  65. 102 0
      vendor/fyne.io/fyne/v2/canvas/line.go
  66. 196 0
      vendor/fyne.io/fyne/v2/canvas/raster.go
  67. 64 0
      vendor/fyne.io/fyne/v2/canvas/rectangle.go
  68. 76 0
      vendor/fyne.io/fyne/v2/canvas/text.go
  69. 107 0
      vendor/fyne.io/fyne/v2/canvasobject.go
  70. 9 0
      vendor/fyne.io/fyne/v2/clipboard.go
  71. 39 0
      vendor/fyne.io/fyne/v2/cloud.go
  72. 211 0
      vendor/fyne.io/fyne/v2/container.go
  73. 462 0
      vendor/fyne.io/fyne/v2/container/apptabs.go
  74. 20 0
      vendor/fyne.io/fyne/v2/container/container.go
  75. 496 0
      vendor/fyne.io/fyne/v2/container/doctabs.go
  76. 121 0
      vendor/fyne.io/fyne/v2/container/layouts.go
  77. 55 0
      vendor/fyne.io/fyne/v2/container/scroll.go
  78. 369 0
      vendor/fyne.io/fyne/v2/container/split.go
  79. 843 0
      vendor/fyne.io/fyne/v2/container/tabs.go
  80. 178 0
      vendor/fyne.io/fyne/v2/data/binding/binding.go
  81. 647 0
      vendor/fyne.io/fyne/v2/data/binding/binditems.go
  82. 1786 0
      vendor/fyne.io/fyne/v2/data/binding/bindlists.go
  83. 1816 0
      vendor/fyne.io/fyne/v2/data/binding/bindtrees.go
  84. 118 0
      vendor/fyne.io/fyne/v2/data/binding/bool.go
  85. 13 0
      vendor/fyne.io/fyne/v2/data/binding/comparator_helper.go
  86. 638 0
      vendor/fyne.io/fyne/v2/data/binding/convert.go
  87. 103 0
      vendor/fyne.io/fyne/v2/data/binding/convert_helper.go
  88. 43 0
      vendor/fyne.io/fyne/v2/data/binding/listbinding.go
  89. 522 0
      vendor/fyne.io/fyne/v2/data/binding/mapbinding.go
  90. 104 0
      vendor/fyne.io/fyne/v2/data/binding/pref_helper.go
  91. 244 0
      vendor/fyne.io/fyne/v2/data/binding/preference.go
  92. 30 0
      vendor/fyne.io/fyne/v2/data/binding/queue.go
  93. 218 0
      vendor/fyne.io/fyne/v2/data/binding/sprintf.go
  94. 92 0
      vendor/fyne.io/fyne/v2/data/binding/treebinding.go
  95. 39 0
      vendor/fyne.io/fyne/v2/device.go
  96. 32 0
      vendor/fyne.io/fyne/v2/driver.go
  97. 11 0
      vendor/fyne.io/fyne/v2/driver/desktop/app.go
  98. 11 0
      vendor/fyne.io/fyne/v2/driver/desktop/canvas.go
  99. 47 0
      vendor/fyne.io/fyne/v2/driver/desktop/cursor.go
  100. 15 0
      vendor/fyne.io/fyne/v2/driver/desktop/driver.go

+ 83 - 0
.env

@@ -0,0 +1,83 @@
+package main
+
+import (
+	"image/color"
+	"log"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/app"
+	"fyne.io/fyne/v2/canvas"
+	"fyne.io/fyne/v2/container"
+	"fyne.io/fyne/v2/layout"
+	"fyne.io/fyne/v2/widget"
+	//"fyne.io/fyne/v2/layout"
+)
+
+var myApp fyne.App
+var winMain fyne.Window
+var botLogin string
+var botPass string
+var boxLeft *fyne.Container
+
+func main() {
+	myApp = app.New()
+	winMain = myApp.NewWindow("[Бото-ферма WarTank]")
+	winMain.Resize(fyne.NewSize(800, 600))
+	green := color.NRGBA{R: 0, G: 180, B: 0, A: 255}
+
+	text1 := canvas.NewText("Список ботов", green)
+	text2 := canvas.NewText("There", green)
+	text2.Move(fyne.NewPos(20, 20))
+	//content := container.NewWithoutLayout(text1, text2)
+	// content := container.New(layout.NewGridLayout(2), text1, text2)
+	boxLeft = container.NewVBox(text1, text2)
+	btnAddBot := widget.NewButton("Добавить бота", btnAddClick)
+	btnExit := widget.NewButton("Выход", btnExitClick)
+	boxDown := container.NewHBox(btnAddBot, layout.NewSpacer(), btnExit)
+	boxBorder := container.NewBorder(nil, nil, boxLeft, nil, boxLeft)
+	boxMain := container.NewBorder(nil, boxDown, nil, nil, boxBorder)
+
+	winMain.SetContent(boxMain)
+	winMain.Show()
+	myApp.Run()
+}
+
+func btnAddClick() {
+	log.Println("btnAddclick()")
+	winAddBot := myApp.NewWindow("Новый бот")
+	entLogin := widget.NewEntry()
+	entPass := widget.NewEntry()
+	form := &widget.Form{
+		Items: []*widget.FormItem{ // we can specify items in the constructor
+			{Text: "Логин", Widget: entLogin},
+			{Text: "Пароль", Widget: entPass},
+		},
+		OnSubmit: func() { // optional, handle form submission
+			botLogin = entLogin.Text
+			log.Println("Form submitted:", entLogin.Text)
+			botPass = entPass.Text
+			log.Println("multiline:", entPass.Text)
+			winAddBot.Close()
+			btnBot := widget.NewButton(botLogin, btnBotClick(botLogin, botPass))
+			boxLeft.Add(btnBot)
+			// winMain.Canvas().Refresh()
+		},
+	}
+	winAddBot.Resize(fyne.NewSize(640, 480))
+	winAddBot.SetContent(form)
+	winAddBot.Show()
+
+}
+
+func btnBotClick(botName, botPass string) func() {
+	_botName := botName
+	_botPass := botPass
+	return func() {
+		log.Printf("btnBotClick().fn(): botName=%q\tbotPass=%q\n", _botName, _botPass)
+	}
+}
+
+func btnExitClick() {
+	log.Println("btnExitClick()")
+	myApp.Quit()
+}

+ 140 - 0
cmd/server/main.go

@@ -0,0 +1,140 @@
+package netclient
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"strings"
+	"sync"
+	"time"
+
+	"wartank/pkg/components/scene_net/netstat"
+	"wartank/pkg/types"
+)
+
+/*
+	Объект сетевого соединения
+*/
+
+// Ответ после запроса
+type response struct {
+	lstString []string
+	err       error
+}
+
+// NetClient -- объект сетевого соединения
+type NetClient struct {
+	botNet types.ИБотСеть
+	conn   *http.Client
+	stat   *netstat.NetStat
+	chRes  chan *response // Канал ответа от http-клиента
+	block  sync.Mutex
+}
+
+// NewNetClient -- возвращает сетевого клиента
+func NewNetClient(botNet types.ИБотСеть) *NetClient {
+	сам := &NetClient{
+		botNet: botNet,
+		conn:   botNet.Коннект(),
+		stat:   netstat.NewNetStat(botNet),
+		chRes:  make(chan *response, 2),
+	}
+	return сам
+}
+
+// Get -- выполняет безопасный GET-запрос в сеть
+func (сам *NetClient) Get(strLink string) (lstString []string, err error) {
+	сам.block.Lock()
+	defer сам.block.Unlock()
+	// if strLink == "https://wartank.ru/production/Mine" {
+	log.Printf("NetClient.Get(): link=%v\n", strLink)
+	// }
+	ctxCancel, fnCancel := context.WithTimeout(сам.botNet.Кнт(), time.Second*10)
+	defer fnCancel()
+	defer func() { // Возможный перехват паники
+		if _panic := recover(); _panic != nil {
+			err = fmt.Errorf("NetClient.Get().defer(): перехвачена паника для URL(%v), panic=%v", strLink, _panic)
+			lstString = nil
+			time.Sleep(time.Millisecond * 250) // Чтобы не насиловать не работающую сеть
+		}
+	}()
+	go сам.get(strLink)
+	select {
+	case <-ctxCancel.Done(): // Таймаут по ожиданию
+		err = fmt.Errorf("NetClient.get(): таймаут ожидания ответа")
+		сам.botNet.Отмена()
+		return nil, err
+	case resp := <-сам.chRes: // Получен ответ
+		if resp.err != nil {
+			return nil, resp.err
+		}
+		return resp.lstString, nil
+	}
+}
+
+// Внутренний вызов для сокрытия под общей блокировкой
+func (сам *NetClient) get(strLink string) {
+	resp := &response{}
+	defer func() {
+		if resp.err != nil {
+			сам.stat.IncErr()
+		}
+	}()
+	req, err := http.NewRequest("GET", strLink, nil)
+	if err != nil {
+		resp.err = fmt.Errorf("NetClient.get(): при создании запроса, err=\n\t%w", err)
+		return
+	}
+	req.Header.Set("User-Agent", "Mozilla Firefox 94.1")
+	httpResp, err := сам.conn.Do(req)
+	if err != nil {
+		resp.err = fmt.Errorf("NetClient.get(): при выполнении GET-запроса, err=\n\t%w", err)
+		return
+	}
+	defer сам.closeGetBody(strLink, httpResp, resp)
+
+	if httpResp.StatusCode != http.StatusOK {
+		resp.err = fmt.Errorf("NetClient.get(): code=%v, status=%v", httpResp.StatusCode, httpResp.Status)
+		return
+	}
+	binData, err := io.ReadAll(httpResp.Body)
+	if err != nil {
+		resp.err = fmt.Errorf("NetClient.get(): при чтении тела ответа, err=\n\t%w", err)
+		return
+	}
+	if len(binData) == 0 {
+		resp.err = fmt.Errorf("NetClient.get(): пустое тело ответа, err=\n\t%w", err)
+		return
+	}
+	lenData := len(binData) + len(strLink)
+	сам.stat.AddByte(lenData)
+
+	lstString := strings.Split(string(binData), "\n")
+	if len(lstString) == 0 {
+		resp.err = fmt.Errorf("NetClient.get(): lstString is empty")
+		return
+	}
+	resp.lstString = lstString
+}
+
+// Вызывается по завершению вызова, закрывает тело запроса
+func (сам *NetClient) closeGetBody(strLink string, httpResp *http.Response, resp *response) {
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			// log._rintf("NetClient.closeGetBody(): strLink='%v', panic=%v\n", strLink, _panic)
+			сам.botNet.Отмена()
+		}
+	}()
+	err := httpResp.Body.Close()
+	if err != nil {
+		_err := fmt.Errorf("NetClient.closeGetBody(): ошибка при закрытии запроса URL(%q), err=\n\t%w", strLink, err)
+		if resp.err != nil { // Есть и ошибка в закрытии тела запроса и внутренняя
+			resp.err = fmt.Errorf("NetClient.closeGetBody(): двойная ошибка при закрытии запроса URL(%q), err=\n\t%w\n\tinternal err=\n\t%w", strLink, _err, resp.err)
+		}
+		resp.lstString = nil
+		сам.stat.IncErr()
+	}
+	сам.chRes <- resp
+}

+ 9 - 0
pkg/components/scene_net/netstat/netstat.go

@@ -0,0 +1,9 @@
+package types
+
+import "fyne.io/fyne/v2"
+
+// ИГуи -- интерфейс к графической подсистеме
+type ИГуи interface {
+	// ФайнПрилож -- возвращает объект Fyne-приложения
+	ФайнПрилож() fyne.App
+}

+ 7 - 0
pkg/types/iis_shot.go

@@ -0,0 +1,7 @@
+package types
+
+// ИСетьКлиент -- интерфейс к GET-запросу
+type ИСетьКлиент interface {
+	// Get -- теневая функция на блокировку
+	Get(strLink string) (lstString []string, err error)
+}

+ 27 - 0
pkg/types/inetangar.go

@@ -0,0 +1,27 @@
+Package            Version
+------------------ ---------
+astroid            2.15.7
+certifi            2023.7.22
+charset-normalizer 3.2.0
+dill               0.3.7
+flake8             6.1.0
+idna               3.4
+isort              5.12.0
+lazy-object-proxy  1.9.0
+mccabe             0.7.0
+mypy               1.5.1
+mypy-extensions    1.0.0
+pip                23.2.1
+platformdirs       3.10.0
+pycodestyle        2.11.0
+pydocstyle         6.3.0
+pyflakes           3.1.0
+pylama             8.4.1
+pylint             2.17.6
+requests           2.31.0
+setuptools         65.5.0
+snowballstemmer    2.2.0
+tomlkit            0.12.1
+typing_extensions  4.8.0
+urllib3            2.0.5
+wrapt              1.15.0

+ 41 - 0
server/helper/helper.go

@@ -0,0 +1,41 @@
+// package bot_net_conn -- сетевое соединение бота
+package bot_net_conn
+
+import (
+	"net/http"
+	"net/http/cookiejar"
+	"time"
+
+	"wartank/pkg/types"
+	"wartank/server/serv_bots/warbot/warbot_net/bot_cookie"
+)
+
+// БотСетьСокет -- сетевое соединение бота
+type БотСетьСокет struct {
+	клиент http.Client          // Фактический клиент бота
+	куки   bot_cookie.BotCookie // Кукисы бота
+}
+
+// НовБотСетьСокет -- возвращает новое сетевое соединение бота
+func НовБотСетьСокет() *БотСетьСокет {
+	сам := &БотСетьСокет{
+		клиент: http.Client{
+			Transport: nil,
+			Jar:       nil,
+			Timeout:   time.Second * 10,
+		},
+		куки: bot_cookie.NewBotCookie(),
+	}
+	сам.клиент.Jar, _ = cookiejar.New(nil)
+	return сам
+}
+
+// Клиент -- возвращает сетевого клиента
+func (сам *БотСетьСокет) Клиент() *http.Client {
+	return &сам.клиент
+}
+
+// Куки -- возвращает куки клиента
+func (сам *БотСетьСокет) Куки() types.ИБотКуки {
+	return &сам.куки
+}

+ 54 - 0
server/serv_bots/warbot/warbot_net/bot_net_login/bot_net_login.go

@@ -0,0 +1,54 @@
+### Project Specific
+cmd/fyne/fyne
+cmd/fyne/fyne.exe
+cmd/fyne_demo/fyne_demo
+cmd/fyne_demo/fyne_demo.apk
+cmd/fyne_demo/fyne-demo.app
+cmd/fyne_demo/fyne_demo.exe
+cmd/fyne_settings/fyne_settings
+cmd/fyne_settings/fyne_settings.apk
+cmd/fyne_settings/fyne_settings.app
+cmd/fyne_settings/fyne_settings.exe
+cmd/hello/hello
+cmd/hello/hello.apk
+cmd/hello/hello.app
+cmd/hello/hello.exe
+fyne-cross
+
+### Tests
+**/testdata/failed
+
+### Go
+# Output of the coverage tool
+*.out
+
+### macOS
+# General
+.DS_Store
+
+# Thumbnails
+._*
+
+### JetBrains
+.idea
+
+### VSCode
+.vscode
+
+### Vim
+# Swap
+[._]*.s[a-v][a-z]
+[._]*.sw[a-p]
+[._]s[a-v][a-z]
+[._]sw[a-p]
+
+# Session
+Session.vim
+
+# Temporary
+.netrwhist
+*~
+# Auto-generated tag files
+tags
+# Persistent undo
+[._]*.un~

+ 1 - 0
vendor/fyne.io/fyne/v2/.godocdown.import

@@ -0,0 +1 @@
+fyne.io/fyne/v2

+ 14 - 0
vendor/fyne.io/fyne/v2/AUTHORS

@@ -0,0 +1,14 @@
+Andy Williams <andy@andy.xyz>
+Steve OConnor <steveoc64@gmail.com>
+Luca Corbo <lu.corbo@gmail.com>
+Paul Hovey <paul@paulhovey.org>
+Charles Corbett <nafredy@gmail.com>
+Tilo Prütz <tilo@pruetz.net>
+Stephen Houston <smhouston88@gmail.com>
+Storm Hess <stormhess@gloryskulls.com>
+Stuart Scott <stuart.murray.scott@gmail.com>
+Jacob Alzén <jacalz@tutanota.com>
+Charles A. Daniels <charles@cdaniels.net>
+Pablo Fuentes <f.pablo1@hotmail.com>
+Changkun Ou <hi@changkun.de>
+

+ 1262 - 0
vendor/fyne.io/fyne/v2/CHANGELOG.md

@@ -0,0 +1,1262 @@
+# Changelog
+
+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.2 - 22 November 2023
+
+### Fixed
+
+* Markdown only shows one horizontal rule (#4216)
+* Spacer in HBox with hidden item will cause an additional trailing padding (#4259)
+* Application crash when fast clicking the folders inside the file dialog (#4260)
+* failed to initialise OpenGL (#437)
+* App panic when clicking on a notification panel if there's a systray icon (#4385)
+* Systray cannot be shown on Ubuntu (#3678, #4381)
+* failed to initialise OpenGL on Windows dual-chip graphics cards (#437)
+* Reduce memory allocations for each frame painted
+* RichText may not refresh if segments manually replaced
+* Correct URI.Extension() documentation
+* Update for security fixes to x/sys and x/net
+* Inconsistent rendering of Button widget (#4243)
+* PasswordEntry initial text is not obscured (#4312)
+* Pasting text in Entry does not update cursor position display (#4181)
+
+
+## 2.4.1 - 9 October 2023
+
+### Fixed
+
+* Left key on tree now collapses open branch
+* Avoid memory leak in Android driver code
+* Entry Field on Android in Landscape Mode Shows "0" (#4036)
+* DocTabs Indicator remains visible after last tab is removed (#4220)
+* Some SVG resources don't update appearance correctly with the theme (#3900)
+* Fix mobile simulation builds on OpenBSD
+* Fix alignment of menu button on mobile
+* Fix Compilation with Android NDK r26
+* Clicking table headers causes high CPU consumption (#4264)
+* Frequent clicking on table may cause the program to not respond (#4210)
+* Application stops responding when scrolling a table (#4263)
+* Possible crash parsing malformed JSON color (#4270)
+* NewFolderOpen: incomplete filenames (#2165)
+* Resolve issue where storage.List could crash with short URI (#4271)
+* TextTruncateEllipsis abnormally truncates strings with multi-byte UTF-8 characters (#4283)
+* Last character doesn't appear in Select when there is a special character (#4293)
+* Resolve random crash in DocTab (#3909)
+* Selecting items from a list caused the keyboard to popup on Android (#4236)
+
+
+## 2.4.0 - 1 September 2023
+
+### Added
+
+* Rounded corners in rectangle (#1090)
+* Support for emoji in text
+* Layout debugging (with `-tags debug` build flag) (#3314)
+* GridWrap collection widget
+* Add table headers (#1658, #3594)
+* Add mobile back button handling (#2910)
+* Add option to disable UI animations (#1813)
+* Text truncation ellipsis (#1659)
+* Add support for binding tree data, include new `NewTreeWithData`
+* Add support for OpenType fonts (#3245)
+* Add `Window.SetOnDropped` to handle window-wide item drop on desktop
+* Add lists to the types supported by preferences API
+* Keyboard focus handling for all collection widgets
+* Add APIs for refreshing individual items in collections (#3826)
+* Tapping slider moves it to that position (#3650)
+* Add `OnChangeEnded` callback to `Slider` (#3652)
+* Added keyboard controls to `Slider`
+* Add `NewWarningThemedResource` and `NewSuccessThemedResource` along with `NewColoredResource` (#4040)
+* Custom hyperlink callback for rich text hyperlinks (#3335)
+* Added `dialog.NewCustomWithoutButtons`, with a `SetButtons` method (#2127, #2782)
+* Added `SetConfirmImportance` to `dialog.ConfirmDialog`.
+* Added `FormDialog.Submit()` to close and submit the dialog if validation passes
+* Rich Text image alignment (#3810)
+* Bring back `theme.HyperlinkColor` (#3867)
+* Added `Importance` field on `Label` to color the text
+* Navigating in entry quickly with ctrl key (#2462)
+* Support `.desktop` file metadata in `FyneApp.toml` for Linux and BSD
+* Support mobile simulator on FreeBSD
+* Add data binding boolean operators `Not`, `And` and `Or`
+* Added `Entry.Append`, `Select.SetOptions`, `Check.SetText`, `FormDialog.Submit`
+* Add `ShowPopUpAtRelativePosition` and `PopUp.ShowAtRelativePosition`
+* Add desktop support to get key modifiers with `CurrentKeyModifiers`
+* Add geometry helpers `NewSquareSize` and `NewSquareOffsetPos`
+* Add `--pprof` option to fyne build commands to enable profiling
+* Support compiling from Android (termux)
+
+### Changed
+
+* Go 1.17 or later is now required.
+* Theme updated for rounded corners on buttons and input widgets
+* `widget.ButtonImportance` is now `widget.Importance`
+* The `Max` container and layout have been renamed `Stack` for clarity
+* Refreshing an image will now happen in app-thread not render process, apps may wish to add async image load
+* Icons for macOS bundles are now padded and rounded, disable with "-use-raw-icon" (#3752)
+* Update Android target SDK to 33 for Play Store releases
+* Focus handling for List/Tree/Table are now at the parent widget not child elements
+* Accordion widget now fills available space - put it inside a `VBox` container for old behavior (#4126)
+* Deprecated theme.FyneLogo() for later removal (#3296)
+* Improve look of menu shortcuts (#2722)
+* iOS and macOS packages now default to using "XCWildcard" provisioning profile
+* Improving performance of lookup for theme data
+* Improved application startup time
+
+### Fixed
+
+* Rendering performance enhancements
+* `dialog.NewProgressInfinite` is deprecated, but dialog.NewCustom isn't equivalent
+* Mouse cursor desync with Split handle when dragging (#3791)
+* Minor graphic glitch with checkbox (#3792)
+* binding.String===>Quick refresh *b.val will appear with new data reset by a call to OnChange (#3774)
+* Fyne window becomes unresponsive when in background for a while (#2791)
+* Hangs on repeated calls to `Select.SetSelected` in table. (#3684)
+* `Select` has wrong height, padding and border (#4142)
+* `widget.ImageSegment` can't be aligned. (#3505)
+* Memory leak in font metrics cache (#4108)
+* Don't panic when loading preferences with wrong type (#4039)
+* Button with icon has wrong padding on right (#4124)
+* Preferences don't all save when written in `CloseIntercept` (#3170)
+* Text size does not update in Refresh for TextGrid
+* DocTab selection underline not updated when deleting an Item (#3905)
+* Single line Entry throws away selected text on submission (#4026)
+* Significantly improve performance of large `TextGrid` and `Tree` widgets
+* `List.ScrollToBottom` not scrolling to show the totality of the last Item (#3829)
+* Setting `Position1` of canvas.Circle higher than `Position2` causes panic. (#3949)
+* Enhance scroll wheel/touchpad scroll speed on desktop (#3492)
+* Possible build issue on Windows with app metadata
+* `Form` hint text has confusing padding to next widget (#4137)
+* `Entry` Placeholder Style Only Applied On Click (#4035)
+* Backspace and Delete key Do not Fire OnChanged Event (#4117)
+* Fix `ProgressBar` text having the wrong color sometimes
+* Window doesn't render when called for the first time from system tray and the last window was closed (#4163)
+* Possible race condition in preference change listeners
+* Various vulnerabilities resolved through updating dependencies 
+* Wrong background for color dialog (#4199)
+
+
+## 2.3.5 - 6 June 2023
+
+### Fixed
+
+* Panic with unsupported font (#3646)
+* Temporary manifest file not closed after building on Windows
+* Panic when using autogenerated quit menu and having unshown windows (#3870)
+* Using `canvas.ImageScaleFastest` not working on arm64 (#3891)
+* Disabled password Entry should also disable the ActionItem (#3908)
+* Disabled RadioGroup does not display status (#3882)
+* Negative TableCellID Row (#2857)
+* Make sure we have sufficient space for the bar as well if content is tiny (#3898)
+* Leak in image painter when replacing image.Image source regularly
+* Links in Markdown/Rich Text lists breaks formatting (#2911)
+* Crash when reducing window to taskbar with popup opened (#3877)
+* RichText vertical scroll will truncate long content with horizontal lines (#3929)
+* Custom metadata would not apply with `fyne release` command
+* Horizontal CheckGroup overlap when having long text (#3005)
+* Fix focused colour of coloured buttons (#3462)
+* Menu separator not visible with light theme (#3814)
+
+
+## 2.3.4 - 3 May 2023
+
+### Fixed
+
+* Memory leak when switching theme (#3640)
+* Systray MenuItem separators not rendered in macOS root menu (#3759)
+* Systray leaks window handles on Windows (#3760)
+* RadioGroup miscalculates label widths in horizontal mode (#3386)
+* Start of selection in entry is shifted when moving too fast (#3804)
+* Performance issue in widget.List (#3816)
+* Moving canvas items (e.g. Images) does not cause canvas repaint (#2205)
+* Minor graphic glitch with checkbox (#3792)
+* VBox and HBox using heap memory that was not required
+* Menu hover is slow on long menus
+
+## 2.3.3 - 24 March 2023
+
+### Fixed
+
+* Linux, Windows and BSD builds could fail if gles was missing
+
+
+## 2.3.2 - 20 March 2023
+
+### Fixed
+
+* Fyne does not run perfectly on ARM-based MacOS platforms (#3639) *
+* Panic on closing window in form submit on Мac M2 (#3397) *
+* Wobbling slider effect for very small steps (#3648)
+* Fix memory leak in test canvas refresh
+* Optimise text texture memory by switching to single channel
+* Packaging an android fyne app that uses tags can fail (#3641)
+* NewAdaptiveGrid(0) blanks app window on start until first resize on Windows (#3669)
+* Unnecessary refresh when sliding Split container
+* Linux window resize refreshes all content
+* Themed and unthemed svg resources can cache collide
+* When packaging an ampersand in "Name" causes an error (#3195)
+* Svg in ThemedResource without viewBox does not match theme (#3714)
+* Missing menu icons in Windows system tray
+* Systray Menu Separators don't respect the submenu placement (#3642)
+* List row focus indicator disappears on scrolling (#3699)
+* List row focus not reset when row widget is reused to display a new item (#3700)
+* Avoid panic if accidental 5th nil is passed to Border container
+* Mobile simulator not compiling on Apple M1/2
+* Cropped letters in certain cases with the new v2.3.0 theme (#3500)
+
+Many thanks indeed to [Dymium](https://dymium.io) for sponsoring an Apple
+M2 device which allowed us to complete the marked (*) issues.
+
+
+## 2.3.1 - 13 February 2023
+
+### Changed
+
+* Pad app version to ensure Windows packages correctly (#3638)
+
+### Fixed
+
+* Custom shortcuts with fyne.KeyTab is not working (#3087)
+* Running a systray app with root privileges resulted in panic (#3120)
+* Markdown image with no title is not parsed (#3577)
+* Systray app on macOS panic when started while machine sleeps (#3609)
+* Runtime error with VNC on RaspbianOS (#2972)
+* Hovered background in List widget isn't reset when scrolling reuses an existing list item (#3584)
+* cmd/fyne package can't find FyneApp.toml when -src option has given (#3459)
+* TextWrapWord will cause crash in RichText unverified (#3498)
+* crash in widget.(*RichText).lineSizeToColumn (#3292)
+* Crash in widget.(*Entry).SelectedText (#3290)
+* Crash in widget.(*RichText).updateRowBounds.func1 (#3291)
+* window is max size at all times (#3507)
+* systray.Quit() is not called consistently when the app is closing (#3597)
+* Software rendering would ignore scale for text
+* crash when minimize a window which contains a stroked rectangle (#3552)
+* Menu item would not appear disabled initially
+* Wrong icon colour for danger and warning buttons
+* Embedding Fyne apps in iFrame alignment issue
+* Generated metadata can be in wrong directory
+* Android RootURI may not exist when used for storage (#3207)
+
+
+## 2.3.0 - 24 December 2022
+
+### Added
+
+* Shiny new theme that was designed for us
+* Improved text handling to support non-latin alphabets
+* Add cloud storage and preference support
+* Add menu icon and submenu support to system tray menus
+* More button importance levels `ErrorImportance`, `WarningImportance`
+* Support disabling of `AppTabs` and `DocTabs` items
+* Add image support to rich text (#2366)
+* Add CheckGroup.Remove (#3124)
+
+### Changed
+
+* The buttons on the default theme are no longer transparent, but we added more button importance types
+* Expose a storage.ErrNotExists for non existing documents (#3083)
+* Update `go-gl/glfw` to build against latest Glfw 3.3.8
+* List items in `widget.List` now implement the Focusable interface
+
+### Fixed
+
+* Displaying unicode or different language like Bengali doesn't work (#598)
+* Cannot disable container.TabItem (#1904)
+* Update Linux/XDG application theme to follow the FreeDesktop Dark Style Preference (#2657)
+* Running `fyne package -os android` needs NDK 16/19c (#3066)
+* Caret position lost when resizing a MultilineEntry (#3024)
+* Fix possible crash in table resize (#3369)
+* Memory usage surge when selecting/appending MultilineEntry text (#3426)
+* Fyne bundle does not support appending when parameter is a directory
+* Crash parsing invalid file URI (#3275)
+* Systray apps on macOS can only be terminated via the systray menu quit button (#3395)
+* Wayland Scaling support: sizes and distances are scaled wrong (#2850)
+* Google play console minimum API level 31 (#3375)
+* Data bound entry text replacing selection is ignored (#3340)
+* Split Container does not respect item's Visible status (#3232)
+* Android - Entry - OnSubmitted is not working (#3267)
+* Can't set custom CGO_CFLAGS and CGO_LDFLAGS with "fyne package" on darwin (#3276)
+* Text line not displayed in RichText (#3117)
+* Segfault when adding items directly in form struct (#3153)
+* Preferences RemoveValue does not save (#3229)
+* Create new folder directly from FolderDialog (#3174)
+* Slider drag handle is clipped off at minimum size (#2966)
+* Entry text "flickering" while typing (#3461)
+* Rendering of not changed canvas objects after an event (#3211)
+* Form dialog not displaying hint text and validation errors (#2781)
+
+
+## 2.2.4 - 9 November 2022
+
+### Fixes
+
+* Iphone incorrect click coordinates in zoomed screen view (#3122)
+* CachedFontFace seems to be causing crash (#3134)
+* Fix possible compile error if "fyne build" is used without icon metadata
+* Detect and use recent Android NDK toolchain
+* Handle fyne package -release and fyne release properly for Android and iOS
+* Fix issue with mobile simulation when systray used
+* Fix incorrect size and position for radio focus indicator (#3137)
+
+
+## 2.2.3 - 8 July 2022
+
+### Fixed
+
+* Regression: Preferences are not parsed at program start (#3125)
+* Wrappable RichText in a Split container causes crash (#3003, #2961)
+* meta.Version is always 1.0.0 on android & ios (#3109)
+
+
+## 2.2.2 - 30 June 2022
+
+### Fixed
+
+* Windows missing version metadata when packaged (#3046)
+* Fyne package would not build apps using old Fyne versions
+* System tray icon may not be removed on app exit in Windows
+* Emphasis in Markdown gives erroneous output in RichText (#2974)
+* When last visible window is closed, hidden window is set visible (#3059)
+* Do not close app when last window is closed but systrayMenu exists (#3092)
+* Image with ImageFillOriginal not showing (#3102)
+
+
+## 2.2.1 - 12 June 2022
+
+### Fixed
+
+* Fix various race conditions and compatibility issues with System tray menus
+* Resolve issue where macOS systray menu may not appear
+* Updated yaml dependency to fix CVE-2022-28948
+* Tab buttons stop working after removing a tab (#3050)
+* os.SetEnv("FYNE_FONT") doesn't work in v2.2.0 (#3056)
+
+
+## 2.2.0 - 7 June 2022
+
+### Added
+
+* Add SetIcon method on ToolbarAction (#2475)
+* Access compiled app metadata using new `App.Metadata()` method
+* Add support for System tray icon and menu (#283)
+* Support for Android Application Bundle (.aab) (#2663)
+* Initial support for OpenBSD and NetBSD
+* Add keyboard shortcuts to menu (#682)
+* Add technical preview of web driver and `fyne serve` command
+* Added `iossimulator` build target (#1917)
+* Allow dynamic themes via JSON templates (#211)
+* Custom hyperlink callback (#2979)
+* Add support for `.ico` file when compiling for windows (#2412)
+* Add binding.NewStringWithFormat (#2890)
+* Add Entry.SetMinRowsVisible
+* Add Menu.Refresh() and MainMenu.Refresh() (#2853)
+* Packages for Linux and BSD now support installing into the home directory
+* Add `.RemoveAll()` to containers
+* Add an AllString validator for chaining together string validators
+
+### Changed
+
+* Toolbar item constructors now return concrete types instead of ToolbarItem
+* Low importance buttons no longer draw button color as a background
+* ProgressBar widget height is now consistent with other widgets
+* Include check in DocTabs menu to show current tab
+* Don't call OnScrolled if offset did not change (#2646)
+* Prefer ANDROID_NDK_HOME over the ANDROID_HOME ndk-bundle location (#2920)
+* Support serialisation / deserialisation of the widget tree (#5)
+* Better error reporting / handling when OpenGL is not available (#2689)
+* Memory is now better reclaimed on Android when the OS requests it
+* Notifications on Linux and BSD now show the application icon
+* Change listeners for preferences no longer run when setting the same value
+* The file dialog now shows extensions in the list view for better readability
+* Many optimisations and widget performance enhancements
+* Updated various dependencies to their latest versions
+
+### Fixed
+
+* SendNotification does not show app name on Windows (#1940)
+* Copy-paste via keyboard don't work translated keyboard mappings on Windows (#1220)
+* OnScrolled triggered when offset hasn't changed (#1868)
+* Carriage Return (\r) is rendered as space (#2456)
+* storage.List() returns list with nil elements for empty directories (#2858)
+* Entry widget, position of cursor when clicking empty space (#2877)
+* SelectEntry cause UI hang (#2925)
+* Font cutoff with bold italics (#3001)
+* Fyne error: Preferences load error (#2936, 3015)
+* Scrolled List bad redraw when window is maximized (#3013)
+* Linux and BSD packages not being installable if the name contained spaces
+
+
+## 2.1.4 - 17 March 2022
+
+### Fixed
+
+* SetTheme() is not fully effective for widget.Form (#2810)
+* FolderOpenDialog SetDismissText is ineffective (#2830)
+* window.Resize() does not work if SetFixedSize(true) is set after (#2819)
+* Container.Remove() race causes crash (#2826, #2775, #2481)
+* FixedSize Window improperly sized if contains image with ImageFillOriginal (#2800)
+
+
+## 2.1.3 - 24 February 2022
+
+### Fixed
+
+* The text on button can't be show correctly when use imported font (#2512)
+* Fix issues with DocTabs scrolling (#2709)
+* Fix possible crash for tapping extended Radio or Check item
+* Resolve lookup of relative icons in FyneApp.toml
+* Window not shown when SetFixedSize is used without Resize (#2784)
+* Text and links in markdown can be rendered on top of each other (#2695)
+* Incorrect cursor movement in a multiline entry with wrapping (#2698)
+
+
+## 2.1.2 - 6 December 2021
+
+### Fixed
+
+* Scrolling list bound to data programmatically causes nil pointer dereference (#2549)
+* Rich text from markdown can get newlines wrong (#2589)
+* Fix crash on 32bit operating systems (#2603)
+* Compile failure on MacOS 10.12 Sierra (#2478)
+* Don't focus widgets on mobile where keyboard should not display (#2598)
+* storage.List doesn't return complete URI on Android for "content:" scheme (#2619)
+* Last word of the line and first word of the next line are joined in markdown parse (#2647)
+* Support for building `cmd/fyne` on Windows arm64
+* Fixed FreeBSD requiring installed glfw library dependency (#1928)
+* Apple M1: error when using mouse drag to resize window (#2188)
+* Struct binding panics in reload with slice field (#2607)
+* File Dialog favourites can break for certain locations (#2595)
+* Define user friendly names for Android Apps (#2653)
+* Entry validator not updating if content is changed via data binding after SetContent (#2639)
+* CenterOnScreen not working for FixedSize Window (#2550)
+* Panic in boundStringListItem.Get() (#2643)
+* Can't set an app/window icon to be an svg. (#1196)
+* SetFullScreen(false) can give error (#2588)
+
+
+## 2.1.1 - 22 October 2021
+
+### Fixed
+
+* Fix issue where table could select cells beyond data bound
+* Some fast taps could be ignored (#2484)
+* iOS app stops re-drawing mid-frame after a while (#950)
+* Mobile simulation mode did not work on Apple M1 computers
+* TextGrid background color can show gaps in render (#2493)
+* Fix alignment of files in list view of file dialog
+* Crash setting visible window on macOS to fixed size (#2488)
+* fyne bundle ignores -name flag in windows (#2395)
+* Lines with nil colour would crash renderer
+* Android -nm tool not found with NDK 23 (#2498)
+* Runtime panic because out of touchID (#2407)
+* Long text in Select boxes overflows out of the box (#2522)
+* Calling SetText on Label may not refresh correctly
+* Menu can be triggered by # key but not always Alt
+* Cursor position updates twice with delay (#2525)
+* widgets freeze after being in background and then a crash upon pop-up menu (#2536)
+* too many Refresh() calls may now cause visual artifacts in the List widget (#2548)
+* Entry.SetText may panic if called on a multiline entry with selected text (#2482)
+* TextGrid not always drawing correctly when resized (#2501)
+
+
+## 2.1.0 - 17 September 2021
+
+### Added
+
+* DocTabs container for handling multiple open files
+* Lifecycle API for handling foreground, background and other event
+* Add RichText widget and Markdown parser
+* Add TabWidth to TextStyle to specify tab size in spaces
+* Add CheckGroup widget for multi-select
+* Add FyneApp.toml metadata file to ease build commands
+* Include http and https in standard repositories
+* Add selection color to themes
+* Include baseline information in driver font measurement
+* Document storage API (App.Storage().Create() and others)
+* Add "App Files" to file dialog for apps that use document storage
+* Tab overflow on AppTabs
+* Add URI and Unbound type to data bindings
+* Add keyboard support for menus, pop-ups and buttons
+* Add SimpleRenderer to help make simple widgets (#709)
+* Add scroll functions for List, Table, Tree (#1892)
+* Add selection and disabling to MenuItem
+* Add Alignment to widget.Select (#2329)
+* Expose ScanCode for keyboard events originating from hardware (#1523)
+* Support macOS GPU switching (#2423)
+
+### Changed
+
+* Focusable widgets are no longer focused on tap, add canvas.Focus(obj) in Tapped handler if required
+* Move to background based selection for List, Table and Tree
+* Update fyne command line tool to use --posix style parameters
+* Switch from gz to xz compression for unix packages
+* Performance improvements with line, text and raster rendering
+* Items not yet visible can no longer be focused
+* Lines can now be drawn down to 1px (instead of 1dp) (#2298)
+* Support multiple lines of text on button (#2378)
+* Improved text layout speed by caching string size calculations
+* Updated to require Go 1.14 so we can use some new features
+* Window Resize request is now asynchronous
+* Up/Down keys take cursor home/end when on first/last lines respectively
+
+### Fixed
+
+* Correctly align text tabs (#1791)
+* Mobile apps theme does not match system (#472)
+* Toolbar with widget.Label makes the ToolbarAction buttons higher (#2257)
+* Memory leaks in renderers and canvases cache maps (#735)
+* FileDialog SetFilter does not work on Android devices (#2353)
+* Hover fix for List and Tree with Draggable objects
+* Line resize can flip slope (#2208)
+* Deadlocks when using widgets with data (#2348)
+* Changing input type with keyboard visible would not update soft keyboards
+* MainMenu() Close item does NOT call function defined in SetCloseIntercept (#2355)
+* Entry cursor position with mouse is offset vertically by theme.SizeNameInputBorder (#2387)
+* Backspace key is not working on Android AOSP (#1941)
+* macOS: 'NSUserNotification' has been deprecated (#1833)
+* macOS: Native menu would add new items if refreshed
+* iOS builds fail since Go 1.16
+* Re-add support for 32 bit iOS devices, if built with Go 1.14
+* Android builds fail on Apple M1 (#2439)
+* SetFullScreen(true) before ShowAndRun fails (#2446)
+* Interacting with another app when window.SetFullScreen(true) will cause the application to hide itself. (#2448)
+* Sequential writes to preferences does not save to file (#2449)
+* Correct Android keyboard handling (#2447)
+* MIUI-Android: The widget’s Hyperlink cannot open the URL (#1514)
+* Improved performance of data binding conversions and text MinSize
+
+
+## 2.0.4 - 6 August 2021
+
+### Changed
+
+* Disable Form labels when the element it applys to is disabled (#1530)
+* Entry popup menu now fires shortcuts so extended widgets can intercept
+* Update Android builds to SDK 30
+
+### Fixed
+
+* sendnotification show appID for name on windows (#1940)
+* Fix accidental removal of windows builds during cross-compile
+* Removing an item from a container did not update layout
+* Update title bar on Windows 10 to match OS theme (#2184)
+* Tapped triggered after Drag (#2235)
+* Improved documentation and example code for file dialog (#2156)
+* Preferences file gets unexpectedly cleared (#2241)
+* Extra row dividers rendered on using SetColumnWidth to update a table (#2266)
+* Fix resizing fullscreen issue
+* Fullscreen changes my display resolution when showing a dialog (#1832)
+* Entry validation does not work for empty field (#2179)
+* Tab support for focus handling missing on mobile
+* ScrollToBottom not always scrolling all the way when items added to container.Scroller
+* Fixed scrollbar disappearing after changing content (#2303)
+* Calling SetContent a second time with the same content will not show
+* Drawing text can panic when Color is nil (#2347)
+* Optimisations when drawing transparent rectangle or whitespace strings
+
+
+## 2.0.3 - 30 April 2021
+
+### Fixed
+
+* Optimisations for TextGrid rendering
+* Data binding with widget.List sometimes crash while scrolling (#2125)
+* Fix compilation on FreeBSD 13
+* DataLists should notify only once when change.
+* Keyboard will appear on Android in disabled Entry Widget (#2139)
+* Save dialog with filename for Android
+* form widget can't draw hinttext of appended item. (#2028)
+* Don't create empty shortcuts (#2148)
+* Install directory for windows install command contains ".exe"
+* Fix compilation for Linux Wayland apps
+* Fix tab button layout on mobile (#2117)
+* Options popup does not move if a SelectEntry widget moves with popup open
+* Speed improvements to Select and SelectEntry drop down
+* theme/fonts has an apache LICENSE file but it should have SIL OFL (#2193)
+* Fix build requirements for target macOS platforms (#2154)
+* ScrollEvent.Position and ScrollEvent.AbsolutePosition is 0,0 (#2199)
+
+
+## 2.0.2 - 1 April 2021
+
+### Changed
+
+* Text can now be copied from a disable Entry using keyboard shortcuts
+
+### Fixed
+
+* Slider offset position could be incorrect for mobile apps
+* Correct error in example code
+* When graphics init fails then don't try to continue running (#1593)
+* Don't show global settings on mobile in fyne_demo as it's not supported (#2062)
+* Empty selection would render small rectangle in Entry
+* Do not show validation state for disabled Entry
+* dialog.ShowFileSave did not support mobile (#2076)
+* Fix issue that storage could not write to files on iOS and Android
+* mobile app could crash in some focus calls
+* Duplicate symbol error when compiling for Android with NDK 23 (#2064)
+* Add internet permission by default for Android apps (#1715)
+* Child and Parent support in storage were missing for mobile appps
+* Various crashes with Entry and multiline selections (including #1989)
+* Slider calls OnChanged for each value between steps (#1748)
+* fyne command doesn't remove temporary binary from src (#1910)
+* Advanced Color picker on mobile keeps updating values forever after sliding (#2075)
+* exec.Command and widget.Button combination not working (#1857)
+* After clicking a link on macOS, click everywhere in the app will be linked (#2112)
+* Text selection - Shift+Tab bug (#1787)
+
+
+## 2.0.1 - 4 March 2021
+
+### Changed
+
+* An Entry with `Wrapping=fyne.TextWrapOff` no longer blocks scroll events from a parent
+
+### Fixed
+
+* Dialog.Resize() has no effect if called before Dialog.Show() (#1863)
+* SelectTab does not always correctly set the blue underline to the selected tab (#1872)
+* Entry Validation Broken when using Data binding (#1890)
+* Fix background colour not applying until theme change
+* android runtime error with fyne.dialog (#1896)
+* Fix scale calculations for Wayland phones (PinePhone)
+* Correct initial state of entry validation
+* fix entry widget mouse drag selection when scrolled
+* List widget panic when refreshing after changing content length (#1864)
+* Fix image caching that was too aggressive on resize
+* Pointer and cursor misalignment in widget.Entry (#1937)
+* SIGSEGV Sometimes When Closing a Program by Clicking a Button (#1604)
+* Advanced Color Picker shows Black for custom primary color as RGBA (#1970)
+* Canvas.Focus() before window visible causes application to crash (#1893)
+* Menu over Content (#1973)
+* Error compiling fyne on Apple M1 arm64 (#1739)
+* Cells are not getting draw in correct location after column resize. (#1951)
+* Possible panic when selecting text in a widget.Entry (#1983)
+* Form validation doesn't enable submit button (#1965)
+* Creating a window shows it before calling .Show() and .Hide() does not work (#1835)
+* Dialogs are not refreshed correctly on .Show() (#1866)
+* Failed creating setting storage : no such directory (#2023)
+* Erroneous custom filter types not supported error on mobile (#2012)
+* High importance button show no hovered state (#1785)
+* List widget does not render all visible content after content data gets shorter (#1948)
+* Calling Select on List before draw can crash (#1960)
+* Dialog not resizing in newly created window (#1692)
+* Dialog not returning to requested size (#1382)
+* Entry without scrollable content prevents scrolling of outside scroller (#1939)
+* fyne_demo crash after selecting custom Theme and table (#2018)
+* Table widget crash when scrolling rapidly (#1887)
+* Cursor animation sometimes distorts the text (#1778)
+* Extended password entry panics when password revealer is clicked (#2036)
+* Data binding limited to 1024 simultaneous operations (#1838)
+* Custom theme does not refresh when variant changes (#2006)
+
+
+## 2.0 - 22 January 2021
+
+### Changes that are not backward compatible
+
+These changes may break some apps, please read the 
+[upgrading doc](https://developer.fyne.io/api/v2.0/upgrading) for more info
+The import path is now `fyne.io/fyne/v2` when you are ready to make the update.
+
+* Coordinate system to float32
+  * Size and Position units were changed from int to float32
+  * `Text.TextSize` moved to float32 and `fyne.MeasureText` now takes a float32 size parameter
+  * Removed `Size.Union` (use `Size.Max` instead)
+  * Added fyne.Delta for difference-based X, Y float32 representation
+  * DraggedEvent.DraggedX and DraggedY (int, int) to DraggedEvent.Dragged (Delta)
+  * ScrollEvent.DeltaX and DeltaY (int, int) moved to ScrollEvent.Scrolled (Delta)
+
+* Theme API update
+  * `fyne.Theme` moved to `fyne.LegacyTheme` and can be load to a new theme using `theme.FromLegacy`
+  * A new, more flexible, Theme interface has been created that we encourage developers to use
+
+* The second parameter of `theme.NewThemedResource` was removed, it was previously ignored
+* The desktop.Cursor definition was renamed desktop.StandardCursor to make way for custom cursors
+* Button `Style` and `HideShadow` were removed, use `Importance`
+
+* iOS apps preferences will be lost in this upgrade as we move to more advanced storage
+* Dialogs no longer show when created, unless using the ShowXxx convenience methods
+* Entry widget now contains scrolling so should no longer be wrapped in a scroll container
+
+* Removed deprecated types including:
+  - `dialog.FileIcon` (now `widget.FileIcon`)
+  - `widget.Radio` (now `widget.RadioGroup`)
+  - `widget.AccordionContainer` (now `widget.Accordion`)
+  - `layout.NewFixedGridLayout()` (now `layout.NewGridWrapLayout()`)
+  - `widget.ScrollContainer` (now `container.Scroll`)
+  - `widget.SplitContainer` (now `container.Spilt`)
+  - `widget.Group` (replaced by `widget.Card`)
+  - `widget.Box` (now `container.NewH/VBox`, with `Children` field moved to `Objects`)
+  - `widget.TabContainer` and `widget.AppTabs` (now `container.AppTabs`)
+* Many deprecated fields have been removed, replacements listed in API docs 1.4
+  - for specific information you can browse https://developer.fyne.io/api/v1.4/
+
+### Added
+
+* Data binding API to connect data sources to widgets and sync data
+  - Add preferences data binding and `Preferences.AddChangeListener`
+  - Add bind support to `Check`, `Entry`, `Label`, `List`, `ProgressBar` and `Slider` widgets
+* Animation API for handling smooth element transitions
+  - Add animations to buttons, tabs and entry cursor
+* Storage repository API for connecting custom file sources
+  - Add storage functions `Copy`, `Delete` and `Move` for `URI`
+  - Add `CanRead`, `CanWrite` and `CanList` to storage APIs
+* New Theme API for easier customisation of apps
+  - Add ability for custom themes to support light/dark preference
+  - Support for custom icons in theme definition
+  - New `theme.FromLegacy` helper to use old theme API definitions
+* Add fyne.Vector for managing x/y float32 coordinates
+* Add MouseButtonTertiary for middle mouse button events on desktop
+* Add `canvas.ImageScaleFastest` for faster, less precise, scaling
+* Add new `dialog.Form` that will phase out `dialog.Entry`
+* Add keyboard control for main menu
+* Add `Scroll.OnScrolled` event for seeing changes in scroll container
+* Add `TextStyle` and `OnSubmitted` to `Entry` widget
+* Add support for `HintText` and showing validation errors in `Form` widget
+* Added basic support for tab character in `Entry`, `Label` and `TextGrid`
+
+### Changed
+
+* Coordinate system is now float32 - see breaking changes above
+* ScrollEvent and DragEvent moved to Delta from (int, int)
+* Change bundled resources to use more efficient string storage
+* Left and Right mouse buttons on Desktop are being moved to `MouseButtonPrimary` and `MouseButtonSecondary`
+* Many optimisations and widget performance enhancements
+
+* Moving to new `container.New()` and `container.NewWithoutLayout()` constructors (replacing `fyne.NewContainer` and `fyne.NewContainerWithoutLayout`)
+* Moving storage APIs `OpenFileFromURI`, `SaveFileToURI` and `ListerForURI` to `Reader`, `Writer` and `List` functions
+
+### Fixed
+
+* Validating a widget in widget.Form before renderer was created could cause a panic
+* Added file and folder support for mobile simulation support (#1470)
+* Appending options to a disabled widget.RadioGroup shows them as enabled (#1697)
+* Toggling toolbar icons does not refresh (#1809)
+* Black screen when slide up application on iPhone (#1610)
+* Properly align Label in FormItem (#1531)
+* Mobile dropdowns are too low (#1771)
+* Cursor does not go down to next line with wrapping (#1737)
+* Entry: while adding text beyond visible reagion there is no auto-scroll (#912)
+
+
+## 1.4.3 - 4 January 2021
+
+### Fixed
+
+* Fix crash when showing file open dialog on iPadOS
+* Fix possible missing icon on initial show of disabled button
+* Capturing a canvas on macOS retina display would not capture full resolution
+* Fix the release build flag for mobile
+* Fix possible race conditions for canvas capture
+* Improvements to `fyne get` command downloader
+* Fix tree, so it refreshes visible nodes on Refresh()
+* TabContainer Panic when removing selected tab (#1668)
+* Incorrect clipping behaviour with nested scroll containers (#1682)
+* MacOS Notifications are not shown on subsequent app runs (#1699)
+* Fix the behavior when dragging the divider of split container (#1618)
+
+
+## 1.4.2 - 9 December 2020
+
+### Added
+
+* [fyne-cli] Add support for passing custom build tags (#1538)
+
+### Changed
+
+* Run validation on content change instead of on each Refresh in widget.Entry
+
+### Fixed
+
+* [fyne-cli] Android: allow to specify an inline password for the keystore
+* Fixed Card widget MinSize (#1581)
+* Fix missing release tag to enable BuildRelease in Settings.BuildType()
+* Dialog shadow does not resize after Refresh (#1370)
+* Android Duplicate Number Entry (#1256)
+* Support older macOS by default - back to 10.11 (#886)
+* Complete certification of macOS App Store releases (#1443)
+* Fix compilation errors for early stage Wayland testing
+* Fix entry.SetValidationError() not working correctly
+
+
+## 1.4.1 - 20 November 2020
+
+### Changed
+
+* Table columns can now be different sizes using SetColumnWidth
+* Avoid unnecessary validation check on Refresh in widget.Form
+
+### Fixed
+
+* Tree could flicker on mouse hover (#1488)
+* Content of table cells could overflow when sized correctly
+* file:// based URI on Android would fail to list folder (#1495)
+* Images in iOS release were not all correct size (#1498)
+* iOS compile failed with Go 1.15 (#1497)
+* Possible crash when minimising app containing List on Windows
+* File chooser dialog ignores drive Z (#1513)
+* Entry copy/paste is crashing on android 7.1 (#1511)
+* Fyne package creating invalid windows packages (#1521)
+* Menu bar initially doesn't respond to mouse input on macOS (#505) 
+* iOS: Missing CFBundleIconName and asset catalog (#1504)
+* CenterOnScreen causes crash on MacOS when called from goroutine (#1539)
+* desktop.MouseHover Button state is not reliable (#1533)
+* Initial validation status in widget.Form is not respected
+* Fix nil reference in disabled buttons (#1558)
+
+
+## 1.4 - 1 November 2020
+
+### Added (highlights)
+
+* List (#156), Table (#157) and Tree collection Widgets
+* Card, FileItem, Separator widgets
+* ColorPicker dialog
+* User selection of primary colour
+* Container API package to ease using layouts and container widgets
+* Add input validation
+* ListableURI for working with directories etc
+* Added PaddedLayout
+
+* Window.SetCloseIntercept (#467)
+* Canvas.InteractiveArea() to indicate where widgets should avoid
+* TextFormatter for ProgressBar
+* FileDialog.SetLocation() (#821)
+* Added dialog.ShowFolderOpen (#941)
+* Support to install on iOS and android with 'fyne install'
+* Support asset bundling with go:generate
+* Add fyne release command for preparing signed apps
+* Add keyboard and focus support to Radio and Select widgets 
+
+### Changed
+
+* Theme update - new blue highlight, move buttons to outline
+* Android SDK target updated to 29
+* Mobile log entries now start "Fyne" instead of "GoLog"
+* Don't expand Select to its largest option (#1247)
+* Button.HideShadow replaced by Button.Importance = LowImportance
+
+* Deprecate NewContainer in favour of NewContainerWithoutLayout
+* Deprecate HBox and VBox in favour of new container APIs
+* Move Container.AddObject to Container.Add matching Container.Remove
+* Start move from widget.TabContainer to container.AppTabs
+* Replace Radio with RadioGroup
+* Deprecate WidgetRenderer.BackgroundColor
+
+### Fixed
+
+* Support focus traversal in dialog (#948), (#948)
+* Add missing AbsolutePosition in some mouse events (#1274)
+* Don't let scrollbar handle become too small
+* Ensure tab children are resized before being shown (#1331)
+* Don't hang if OpenURL loads browser (#1332)
+* Content not filling dialog (#1360)
+* Overlays not adjusting on orientation change in mobile (#1334)
+* Fix missing key events for some keypad keys (#1325)
+* Issue with non-english folder names in Linux favourites (#1248)
+* Fix overlays escaping screen interactive bounds (#1358)
+* Key events not blocked by overlays (#814)
+* Update scroll container content if it is changed (#1341)
+* Respect SelectEntry datta changes on refresh (#1462)
+* Incorrect SelectEntry dropdown button position (#1361)
+* don't allow both single and double tap events to fire (#1381)
+* Fix issue where long or tall images could jump on load (#1266, #1432)
+* Weird behaviour when resizing or minimizing a ScrollContainer (#1245)
+* Fix panic on NewTextGrid().Text()
+* Fix issue where scrollbar could jump after mousewheel scroll
+* Add missing raster support in software render
+* Respect GOOS/GOARCH in fyne command utilities
+* BSD support in build tools
+* SVG Cache could return the incorrect resource (#1479)
+
+* Many optimisations and widget performance enhancements
+* Various fixes to file creation and saving on mobile devices
+
+
+## 1.3.3 - 10 August 2020
+
+### Added
+
+* Use icons for file dialog favourites (#1186)
+* Add ScrollContainer ScrollToBottom and ScrollToTop
+
+### Changed
+
+* Make file filter case sensitive (#1185)
+
+### Fixed
+
+* Allow popups to create dialogs (#1176)
+* Use default cursor for dragging scrollbars (#1172)
+* Correctly parse SVG files with missing X/Y for rect
+* Fix visibility of Entry placeholder when text is set (#1193)
+* Fix encoding issue with Windows notifications (#1191)
+* Fix issue where content expanding on Windows could freeze (#1189)
+* Fix errors on Windows when reloading Fyne settings (#1165)
+* Dialogs not updating theme correctly (#1201)
+* Update the extended progressbar on refresh (#1219)
+* Segfault if font fails (#1200)
+* Slider rendering incorrectly when window maximized (#1223)
+* Changing form label not refreshed (#1231)
+* Files and folders starting "." show no name (#1235)
+
+
+## 1.3.2 - 11 July 2020
+
+### Added
+
+* Linux packaged apps now include a Makefile to aid install
+
+### Changed
+
+* Fyne package supports specific architectures for Android
+* Reset missing textures on refresh
+* Custom confirm callbacks now called on implicitly shown dialogs
+* SelectEntry can update drop-down list during OnChanged callback
+* TextGrid whitespace color now matches theme changes
+* Order of Window Resize(), SetFixedSize() and CenterOnScreen() does no matter before Show()
+* Containers now refresh their visuals as well as their Children on Refresh()
+
+### Fixed
+
+* Capped StrokeWidth on canvas.Line (#831)
+* Canvas lines, rectangles and circles do not resize and refresh correctly
+* Black flickering on resize on MacOS and OS X (possibly not on Catalina) (#1122)
+* Crash when resizing window under macOS (#1051, #1140)
+* Set SetFixedSize to true, the menus are overlapped (#1105)
+* Ctrl+v into text input field crashes app. Presumably clipboard is empty (#1123, #1132)
+* Slider default value doesn't stay inside range (#1128)
+* The position of window is changed when status change from show to hide, then to show (#1116)
+* Creating a windows inside onClose handler causes Fyne to panic (#1106)
+* Backspace in entry after SetText("") can crash (#1096)
+* Empty main menu causes panic (#1073)
+* Installing using `fyne install` on Linux now works on distrubutions that don't use `/usr/local`
+* Fix recommendations from staticcheck
+* Unable to overwrite file when using dialog.ShowFileSave (#1168)
+
+
+## 1.3 - 5 June 2020
+
+### Added
+
+* File open and save dialogs (#225)
+* Add notifications support (#398)
+* Add text wrap support (#332)
+* Add Accordion widget (#206)
+* Add TextGrid widget (#115)
+* Add SplitContainer widget (#205)
+* Add new URI type and handlers for cross-platform data access
+* Desktop apps can now create splash windows
+* Add ScaleMode to images, new ImageScalePixels feature for retro graphics
+* Allow widgets to influence mouse cursor style (#726)
+* Support changing the text on form submit/cancel buttons
+* Support reporting CapsLock key events (#552)
+* Add OnClosed callback for Dialog
+* Add new image test helpers for validating render output
+* Support showing different types of soft keyboard on mobile devices (#971, #975)
+
+### Changed
+
+* Upgraded underlying GLFW library to fix various issues (#183, #61)
+* Add submenu support and hover effects (#395)
+* Default to non-premultiplied alpha (NRGBA) across toolkit
+* Rename FixedGridLayout to GridWrapLayout (deprecate old API) (#836)
+* Windows redraw and animations continue on window resize and move
+* New...PopUp() methods are being replaced by Show...Popup() or New...Popup().Show()
+* Apps started on a goroutine will now panic as this is not supported
+* On Linux apps now simulate 120DPI instead of 96DPI
+* Improved fyne_settings scale picking user interface
+* Reorganised fyne_demo to accommodate growing collection of widgets and containers
+* Rendering now happens on a different thread to events for more consistent drawing
+* Improved text selection on mobile devices
+
+### Fixed (highlights)
+
+* Panic when trying to paste empty clipboard into entry (#743)
+* Scale does not match user configuration in Windows 10 (#635)
+* Copy/Paste not working on Entry Field in Windows OS (#981)
+* Select widgets with many options overflow UI without scrolling (#675)
+* android: typing in entry expands only after full refresh (#972)
+* iOS app stops re-drawing mid frame after a while (#950)
+* Too many successive GUI updates do not properly update the view (904)
+* iOS apps would not build using Apple's new certificates
+* Preserve aspect ratio in SVG stroke drawing (#976)
+* Fixed many race conditions in widget data handling
+* Various crashes and render glitches in extended widgets
+* Fix security issues reported by gosec (#742)
+
+
+## 1.2.4 - 13 April 2020
+
+### Added
+
+ * Added Direction field to ScrollContainer and NewHScrollContainer, NewVScrollContainer constructors (#763)
+ * Added Scroller.SetMinSize() to enable better defaults for scrolled content
+ * Added "fyne vendor" subcommand to help packaging fyne dependencies in projects
+ * Added "fyne version" subcommand to help with bug reporting (#656)
+ * Clipboard (cut/copy/paste) is now supported on iOS and Android (#414)
+ * Preferences.RemoveValue() now allows deletion of a stored user preference
+
+### Changed
+
+ * Report keys based on name not key code - fixes issue with shortcuts with AZERTY (#790)
+
+### Fixed
+
+ * Mobile builds now support go modules (#660)
+ * Building for mobile would try to run desktop build first
+ * Mobile apps now draw the full safe area on a screen (#799)
+ * Preferences were not stored on mobile apps (#779)
+ * Window on Windows is not controllable after exiting FullScreen mode (#727)
+ * Soft keyboard not working on some Samsung/LG smart phones (#787)
+ * Selecting a tab on extended TabContainer doesn't refresh button (#810)
+ * Appending tab to empty TabContainer causes divide by zero on mobile (#820)
+ * Application crashes on startup (#816)
+ * Form does not always update on theme change (#842)
+
+
+## 1.2.3 - 2 March 2020
+
+### Added
+
+ * Add media and volume icons to default themes (#649)
+ * Add Canvas.PixelCoordinateForPosition to find pixel locations if required
+ * Add ProgressInfinite dialog
+
+### Changed
+
+ * Warn if -executable or -sourceDir flags are used for package on mobile (#652)
+ * Update scale based on device for mobile apps
+ * Windows without a title will now be named "Fyne Application"
+ * Revert fix to quit mobile apps - this is not allowed in guidelines
+
+### Fixed
+
+ * App.UniqueID() did not return current app ID
+ * Fyne package ignored -name flag for ios and android builds (#657)
+ * Possible crash when appending tabs to TabContainer
+ * FixedSize windows not rescaling when dragged between monitors (#654)
+ * Fix issues where older Android devices may not background or rotate (#677)
+ * Crash when setting theme before window content set (#688)
+ * Correct form extend behaviour (#694)
+ * Select drop-down width is wrong if the drop-down is too tall for the window (#706)
+
+
+## 1.2.2 - 29 January 2020
+
+### Added
+
+* Add SelectedText() function to Entry widget
+* New mobile.Device interface exposing ShowVirtualKeyboard() (and Hide...)
+
+### Changed
+
+* Scale calculations are now relative to system scale - the default "1" matches the system
+* Update scale on Linux to be "auto" by default (and numbers are relative to 96DPI standard) (#595)
+* When auto scaling check the monitor in the middle of the window, not top left
+* bundled files now have a standard header to optimise some tools like go report card
+* Shortcuts are now handled by the event queue - fixed possible deadlock
+
+### Fixed
+
+* Scroll horizontally when holding shift key (#579)
+* Updating text and calling refresh for widget doesn't work (#607)
+* Corrected visual behaviour of extended widgets including Entry, Select, Check, Radio and Icon (#615)
+* Entries and Selects that are extended would crash on right click.
+* PasswordEntry created from Entry with Password = true has no revealer
+* Dialog width not always sufficient for title
+* Pasting unicode characters could panic (#597)
+* Setting theme before application start panics on macOS (#626)
+* MenuItem type conflicts with other projects (#632)
+
+
+## 1.2.1 - 24 December 2019
+
+### 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
+
+* Mobile support - iOS and Android, including "fyne package" command
+* Support for OpenGL ES and embedded linux
+* New BaseWidget for building custom widgets
+* Support for diagonal gradients
+* Global settings are now saved and can be set using the new fyne_settings app
+* Support rendering in Go playground using playground.Render() helpers
+* "fyne install" command to package and install apps on the local computer
+* Add horizontal scrolling to ScrollContainer
+* Add preferences API
+* Add show/hide password icon when created from NewPasswordEntry
+* Add NewGridLayoutWithRows to specify a grid layout with a set number of rows
+* Add NewAdaptiveGridLayout which uses a column grid layout when horizontal and rows in vertical
+
+
+### Changed
+
+* New Logo! Thanks to Storm for his work on this :)
+* Applications no longer have a default (Fyne logo) icon
+* Input events now execute one at a time to maintain the correct order
+* Button and other widget callbacks no longer launch new goroutines
+* FYNE_THEME and FYNE_SCALE are now overrides to the global configuration
+* The first opened window no longer exits the app when closed (unless none others are open or Window.SetMaster() is called)
+* "fyne package" now defaults icon to "Icon.png" so the parameter is optional
+* Calling ExtendBaseWidget() sets up the renderer for extended widgets
+* Entry widget now has a visible Disabled state, ReadOnly has been deprecated
+* Bundled images optimised to save space
+* Optimise rendering to reduce refresh on TabContainer and ScrollContainer
+
+
+### Fixed
+
+* Correct the color of Entry widget cursor if theme changes
+* Error where widgets created before main() function could crash (#490)
+* App.Run panics if called without a window (#527)
+* Support context menu for disabled entry widgets (#488)
+* Fix issue where images using fyne.ImageFillOriginal may not show initially (#558)
+
+
+## 1.1.2 - 12 October 2019
+
+### Added
+
+### Changed
+
+* Default scale value for canvases is now 1.0 instead of Auto (DPI based)
+
+### Fixed
+
+* Correct icon name in linux packages
+* Fullscreen before showing a window works again
+* Incorrect MinSize of FixedGrid layout in some situations
+* Update text size on theme change
+* Text handling crashes (#411, #484, #485)
+* Layout of image only buttons
+* TabItem.Content changes are reflected when refreshing TabContainer (#456)
+
+## 1.1.1 - 17 August 2019
+
+### Added
+
+* Add support for custom Windows manifest files in fyne package
+
+### Changed
+
+* Dismiss non-modal popovers on secondary tap
+* Only measure visible objects in layouts and minSize calculations (#343)
+* Don't propagate show/hide in the model - allowing children of tabs to remain hidden
+* Disable cut/copy for password fields
+* Correctly calculate grid layout minsize as width changes
+* Select text at end of line when double tapping beyond width
+
+### Fixed
+
+* Scale could be too large on macOS Retina screens
+* Window with fixed size changes size when un-minimized on Windows (#300)
+* Setting text on a label could crash if it was not yet shown (#381)
+* Multiple Entry widgets could have selections simultaneously (#341)
+* Hover effect of radio widget too low (#383)
+* Missing shadow on Select widget
+* Incorrect rendering of subimages within Image object
+* Size calculation caches could be skipped causing degraded performance
+
+
+## 1.1 - 1 July 2019
+
+### Added
+
+* Menubar and PopUpMenu (#41)
+* PopUp widgets (regular and modal) and canvas overlay support (#242)
+* Add gradient (linear and radial) to canvas
+* Add shadow support for overlays, buttons and scrollcontainer
+* Text can now be selected (#67)
+* Support moving through inputs with Tab / Shift-Tab (#82)
+* canvas.Capture() to save the content of a canvas
+* Horizontal layout for widget.Radio
+* Select widget (#21)
+* Add support for disabling widgets (#234)
+* Support for changing icon color (#246)
+* Button hover effect
+* Pointer drag event to main API
+* support for desktop mouse move events
+* Add a new "hints" build tag that can suggest UI improvements
+
+### Changed
+
+* TabContainer tab location can now be set with SetTabLocation()
+* Dialog windows now appear as modal popups within a window
+* Don't add a button bar to a form if it has no buttons
+* Moved driver/gl package to internal/driver/gl
+* Clicking/Tapping in an entry will position the cursor
+* A container with no layout will not change the position or size of it's content
+* Update the fyne_demo app to reflect the expanding feature set
+
+### Fixed
+
+* Allow scrollbars to be dragged (#133)
+* Unicode char input with Option key on macOS (#247)
+* Resizng fixed size windows (#248)
+* Fixed various bugs in window sizing and padding
+* Button icons do not center align if label is empty (#284)
+
+
+## 1.0.1 - 20 April 2019
+
+### Added
+
+* Support for go modules
+* Transparent backgrounds for widgets
+* Entry.OnCursorChanged()
+* Radio.Append() and Radio.SetSelected() (#229)
+
+### Changed
+
+* Clicking outside a focused element will unfocus it
+* Handle key repeat for non-runes (#165)
+
+### Fixed
+
+* Remove duplicate options from a Radio widget (#230)
+* Issue where paste shortcut is not called for Ctrl-V keyboard combination
+* Cursor position when clearing text in Entry (#214)
+* Antialias of lines and circles (fyne-io/examples#14)
+* Crash on centering of windows (#220)
+* Possible crash when closing secondary windows
+* Possible crash when showing dialog
+* Initial visibility of scroll bar in ScrollContainer
+* Setting window icon when different from app icon.
+* Possible panic on app.Quit() (#175)
+* Various caches and race condition issues (#194, #217, #209).
+
+
+## 1.0 - 19 March 2019
+
+The first major release of the Fyne toolkit delivers a stable release of the
+main functionality required to build basic GUI applications across multiple
+platforms.
+
+### Features
+
+* Canvas API (rect, line, circle, text, image)
+* Widget API (box, button, check, entry, form, group, hyperlink, icon, label, progress bar, radio, scroller, tabs and toolbar)
+* Light and dark themes
+* Pointer, key and shortcut APIs (generic and desktop extension)
+* OpenGL driver for Linux, macOS and Windows
+* Tools for embedding data and packaging releases
+

+ 76 - 0
vendor/fyne.io/fyne/v2/CODE_OF_CONDUCT.md

@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at info@fyne.io. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq

+ 63 - 0
vendor/fyne.io/fyne/v2/CONTRIBUTING.md

@@ -0,0 +1,63 @@
+Thanks very much for your interest in contributing to Fyne!
+The community is what makes this project successful and we are glad to welcome you on board.
+
+There are various ways to contribute, perhaps the following helps you know how to get started.
+
+## Reporting a bug
+
+If you've found something wrong we want to know about it, please help us understand the problem so we can resolve it.
+
+1. Check to see if this already is recorded, if so add some more information [issue list](https://github.com/fyne-io/fyne/issues)
+2. If not then create a new issue using the [bug report template](https://github.com/fyne-io/fyne/issues/new?assignees=&labels=&template=bug_report.md&title=)
+3. Stay involved in the conversation on the issue as it is triaged and progressed.
+
+
+## Fixing an issue
+
+Great! You found an issue and figured you can fix it for us.
+If you can follow these steps then your code should get accepted fast.
+
+1. Read through the "Contributing Code" section further down this page.
+2. Write a unit test to show it is broken.
+3. Create the fix and you should see the test passes.
+4. Run the tests and make sure everything still works as expected using `go test ./...`.
+5. [Open a PR](https://github.com/fyne-io/fyne/compare) and work through the review checklist.
+
+
+## Adding a feature
+
+It's always good news to hear that people want to contribute functionality.
+But first of all check that it fits within our [Vision](https://github.com/fyne-io/fyne/wiki/Vision) and if we are already considering it on our [Roadmap](https://github.com/fyne-io/fyne/wiki/Roadmap).
+If you're not sure then you should join our #fyne-contributors channel on the [Gophers Slack server](https://gophers.slack.com/app_redirect?channel=fyne-contributors).
+
+Once you are ready to code then the following steps should give you a smooth process:
+
+1. Read through the [Contributing Code](#contributing-code) section further down this page.
+2. Think about how you would structure your code and how it can be tested.
+3. Write some code and enjoy the ease of writing Go code for even a complex project :).
+4. Run the tests and make sure everything still works as expected using `go test ./...`.
+5. [Open a PR](https://github.com/fyne-io/fyne/compare) and work through the review checklist.
+
+
+# Contributing Code
+
+We aim to maintain a very high standard of code, through design, test and implementation.
+To manage this we have various checks and processes in place that everyone should follow, including:
+
+* We use the Go standard format (with tabs not spaces) - you can run `gofmt` before committing
+* Imports should be ordered according to the GoImports spec - you can use the `goimports` tool instead of `gofmt`.
+* Everything should have a unit test attached (as much as possible, to keep our coverage up)
+
+For detailed Code style, check [Contributing](https://github.com/fyne-io/fyne/wiki/Contributing#code-style) in our wiki please.
+
+# Decision Process
+
+The following points apply to our decision making process:
+
+* Any decisions or votes will be opened on the #fyne-contributors channel and follows lazy consensus.
+* Any contributors not responding in 4 days will be deemed in agreement.
+* Any PR that has not been responded to within 7 days can be automatically approved.
+* No functionality will be added unless at least 2 developers agree it belongs.
+
+Bear in mind that this is a cross platform project so any new features would normally
+be required to work on multiple desktop and mobile platforms.

+ 28 - 0
vendor/fyne.io/fyne/v2/LICENSE

@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (C) 2018 Fyne.io developers (see AUTHORS)
+All rights reserved.
+
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of Fyne.io nor the names of its contributors may be
+      used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+

+ 188 - 0
vendor/fyne.io/fyne/v2/README.md

@@ -0,0 +1,188 @@
+<p align="center">
+  <a href="https://pkg.go.dev/fyne.io/fyne/v2?tab=doc" title="Go API Reference" rel="nofollow"><img src="https://img.shields.io/badge/go-documentation-blue.svg?style=flat" alt="Go API Reference"></a>
+  <a href="https://img.shields.io/github/v/release/fyne-io/fyne?include_prereleases" title="Latest Release" rel="nofollow"><img src="https://img.shields.io/github/v/release/fyne-io/fyne?include_prereleases" alt="Latest Release"></a>
+  <a href='https://gophers.slack.com/messages/fyne'><img src='https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=blue' alt='Join us on Slack' /></a>
+  <br />
+  <a href="https://goreportcard.com/report/fyne.io/fyne/v2"><img src="https://goreportcard.com/badge/fyne.io/fyne/v2" alt="Code Status" /></a>
+  <a href="https://github.com/fyne-io/fyne/actions"><img src="https://github.com/fyne-io/fyne/workflows/Platform%20Tests/badge.svg" alt="Build Status" /></a>
+  <a href='https://coveralls.io/github/fyne-io/fyne?branch=develop'><img src='https://coveralls.io/repos/github/fyne-io/fyne/badge.svg?branch=develop' alt='Coverage Status' /></a>
+</p>
+
+# About
+
+[Fyne](https://fyne.io) is an easy-to-use UI toolkit and app API written in Go.
+It is designed to build applications that run on desktop and mobile devices with a
+single codebase.
+
+Version 2.4 is the current release of the Fyne API, it added rounded corners, emoji,
+layout debug support and table headers, along with a large number of
+smaller feature additions.
+We are now working towards the next big release, codenamed
+[Elgin](https://github.com/fyne-io/fyne/milestone/21)
+and more news will follow in our news feeds and GitHub project.
+
+# Prerequisites
+
+To develop apps using Fyne you will need Go version 1.17 or later, a C compiler and your system's development tools.
+If you're not sure if that's all installed or you don't know how then check out our
+[Getting Started](https://fyne.io/develop/) document.
+
+Using the standard go tools you can install Fyne's core library using:
+
+    go get fyne.io/fyne/v2@latest
+
+After importing a new module, run the following command before compiling the code for the first time. Avoid running it before writing code that uses the module to prevent accidental removal of dependencies:
+
+    go mod tidy
+
+# Widget demo
+
+To run a showcase of the features of Fyne execute the following:
+
+    go install fyne.io/fyne/v2/cmd/fyne_demo@latest
+    fyne_demo
+
+And you should see something like this (after you click a few buttons):
+
+<p align="center" markdown="1" style="max-width: 100%">
+  <img src="img/widgets-dark.png" width="752" alt="Fyne Demo Dark Theme" style="max-width: 100%" />
+</p>
+
+Or if you are using the light theme:
+
+<p align="center" markdown="1" style="max-width: 100%">
+  <img src="img/widgets-light.png" width="752" alt="Fyne Demo Light Theme" style="max-width: 100%" />
+</p>
+
+And even running on a mobile device:
+
+<p align="center" markdown="1" style="max-width: 100%">
+  <img src="img/widgets-mobile-light.png" width="348" alt="Fyne Demo Mobile Light Theme" style="max-width: 100%" />
+</p>
+
+# Getting Started
+
+Fyne is designed to be really easy to code with.
+If you have followed the prerequisite steps above then all you need is a
+Go IDE (or a text editor).
+
+Open a new file and you're ready to write your first app!
+
+```go
+package main
+
+import (
+	"fyne.io/fyne/v2/app"
+	"fyne.io/fyne/v2/container"
+	"fyne.io/fyne/v2/widget"
+)
+
+func main() {
+	a := app.New()
+	w := a.NewWindow("Hello")
+
+	hello := widget.NewLabel("Hello Fyne!")
+	w.SetContent(container.NewVBox(
+		hello,
+		widget.NewButton("Hi!", func() {
+			hello.SetText("Welcome :)")
+		}),
+	))
+
+	w.ShowAndRun()
+}
+```
+
+And you can run that simply as:
+
+    go run main.go
+
+It should look like this:
+
+<div align="center">
+  <table cellpadding="0" cellspacing="0" style="margin: auto; border-collapse: collapse;">
+    <tr style="border: none;"><td style="border: none;">
+      <img src="img/hello-light.png" width="207" alt="Fyne Hello Dark Theme" />
+    </td><td style="border: none;">
+      <img src="img/hello-dark.png" width="207" alt="Fyne Hello Dark Theme" />
+    </td></tr>
+  </table>
+</div>
+
+## Run in mobile simulation
+
+There is a helpful mobile simulation mode that gives a hint of how your app would work on a mobile device:
+
+    go run -tags mobile main.go
+
+Another option is to use `fyne` command, see [Packaging for mobile](#packaging-for-mobile).
+
+# Installing
+
+Using `go install` will copy the executable into your go `bin` dir.
+To install the application with icons etc into your operating system's standard
+application location you can use the fyne utility and the "install" subcommand.
+
+    go install fyne.io/fyne/v2/cmd/fyne@latest
+    fyne install
+
+# Packaging for mobile
+
+To run on a mobile device it is necessary to package up the application.
+To do this we can use the fyne utility "package" subcommand.
+You will need to add appropriate parameters as prompted, but the basic command is shown below.
+Once packaged you can install using the platform development tools or the fyne "install" subcommand.
+
+    fyne package -os android -appID my.domain.appname
+    fyne install -os android
+
+The built Android application can run either in a real device or an Android emulator.
+However, building for iOS is slightly different.
+If the "-os" argument is "ios", it is build only for a real iOS device.
+Specify "-os" to "iossimulator" allows the application be able to run in an iOS simulator:
+
+    fyne package -os ios -appID my.domain.appname
+    fyne package -os iossimulator -appID my.domain.appname
+
+# Preparing a release
+
+Using the fyne utility "release" subcommand you can package up your app for release
+to app stores and market places. Make sure you have the standard build tools installed
+and have followed the platform documentation for setting up accounts and signing.
+Then you can execute something like the following, notice the `-os ios` parameter allows
+building an iOS app from macOS computer. Other combinations work as well :)
+
+    $ fyne release -os ios -certificate "Apple Distribution" -profile "My App Distribution" -appID "com.example.myapp"
+
+The above command will create a '.ipa' file that can then be uploaded to the iOS App Store.
+
+# Documentation
+
+More documentation is available at the [Fyne developer website](https://developer.fyne.io/) or on [pkg.go.dev](https://pkg.go.dev/fyne.io/fyne/v2?tab=doc).
+
+# Examples
+
+You can find many example applications in the [examples repository](https://github.com/fyne-io/examples/).
+Alternatively a list of applications using fyne can be found at [our website](https://apps.fyne.io/).
+
+# Shipping the Fyne Toolkit
+
+All Fyne apps will work without pre-installed libraries, this is one reason the apps are so portable.
+However, if looking to support Fyne in a bigger way on your operating system then you can install some utilities that help to make a more complete experience.
+
+## Additional apps
+
+It is recommended that you install the following additional apps:
+
+| app           | go install                          | description                                                            |
+| ------------- | ----------------------------------- | ---------------------------------------------------------------------- |
+| fyne_settings | `fyne.io/fyne/v2/cmd/fyne_settings` | A GUI for managing your global Fyne settings like theme and scaling    |
+| apps          | `github.com/fyne-io/apps`           | A graphical installer for the Fyne apps listed at https://apps.fyne.io |
+
+These are optional applications but can help to create a more complete desktop experience.
+
+## FyneDesk (Linux / BSD)
+
+To go all the way with Fyne on your desktop / laptop computer you could install [FyneDesk](https://github.com/fyshos/fynedesk) as well :)
+
+![FyneDesk screenshopt in dark mode](https://fyshos.com/img/desktop.png)

+ 15 - 0
vendor/fyne.io/fyne/v2/SECURITY.md

@@ -0,0 +1,15 @@
+# Security Policy
+
+## Supported Versions
+
+Minor releases will receive security updates and fixes until the next minor or major release.
+
+| Version | Supported          |
+| ------- | ------------------ |
+| 2.4.x   | :white_check_mark: |
+| < 2.4.0 | :x:                |
+
+## Reporting a Vulnerability
+
+Report security vulnerabilities using the [advisories](https://github.com/fyne-io/fyne/security/advisories) page on GitHub.
+The team of core developers will evaluate and address the issue as appropriate.

+ 84 - 0
vendor/fyne.io/fyne/v2/animation.go

@@ -0,0 +1,84 @@
+package fyne
+
+import "time"
+
+// AnimationCurve represents an animation algorithm for calculating the progress through a timeline.
+// Custom animations can be provided by implementing the "func(float32) float32" definition.
+// The input parameter will start at 0.0 when an animation starts and travel up to 1.0 at which point it will end.
+// A linear animation would return the same output value as is passed in.
+type AnimationCurve func(float32) float32
+
+// AnimationRepeatForever is an AnimationCount value that indicates it should not stop looping.
+//
+// Since: 2.0
+const AnimationRepeatForever = -1
+
+var (
+	// AnimationEaseInOut is the default easing, it starts slowly, accelerates to the middle and slows to the end.
+	//
+	// Since: 2.0
+	AnimationEaseInOut = animationEaseInOut
+	// AnimationEaseIn starts slowly and accelerates to the end.
+	//
+	// Since: 2.0
+	AnimationEaseIn = animationEaseIn
+	// AnimationEaseOut starts at speed and slows to the end.
+	//
+	// Since: 2.0
+	AnimationEaseOut = animationEaseOut
+	// AnimationLinear is a linear mapping for animations that progress uniformly through their duration.
+	//
+	// Since: 2.0
+	AnimationLinear = animationLinear
+)
+
+// Animation represents an animated element within a Fyne canvas.
+// These animations may control individual objects or entire scenes.
+//
+// Since: 2.0
+type Animation struct {
+	AutoReverse bool
+	Curve       AnimationCurve
+	Duration    time.Duration
+	RepeatCount int
+	Tick        func(float32)
+}
+
+// NewAnimation creates a very basic animation where the callback function will be called for every
+// rendered frame between time.Now() and the specified duration. The callback values start at 0.0 and
+// will be 1.0 when the animation completes.
+//
+// Since: 2.0
+func NewAnimation(d time.Duration, fn func(float32)) *Animation {
+	return &Animation{Duration: d, Tick: fn}
+}
+
+// Start registers the animation with the application run-loop and starts its execution.
+func (a *Animation) Start() {
+	CurrentApp().Driver().StartAnimation(a)
+}
+
+// Stop will end this animation and remove it from the run-loop.
+func (a *Animation) Stop() {
+	CurrentApp().Driver().StopAnimation(a)
+}
+
+func animationEaseIn(val float32) float32 {
+	return val * val
+}
+
+func animationEaseInOut(val float32) float32 {
+	if val <= 0.5 {
+		return val * val * 2
+	}
+
+	return -1 + (4-val*2)*val
+}
+
+func animationEaseOut(val float32) float32 {
+	return val * (2 - val)
+}
+
+func animationLinear(val float32) float32 {
+	return val
+}

+ 144 - 0
vendor/fyne.io/fyne/v2/app.go

@@ -0,0 +1,144 @@
+package fyne
+
+import (
+	"net/url"
+	"sync/atomic"
+)
+
+// An App is the definition of a graphical application.
+// Apps can have multiple windows, by default they will exit when all windows
+// have been closed. This can be modified using SetMaster() or SetCloseIntercept().
+// To start an application you need to call Run() somewhere in your main() function.
+// Alternatively use the window.ShowAndRun() function for your main window.
+type App interface {
+	// Create a new window for the application.
+	// The first window to open is considered the "master" and when closed
+	// the application will exit.
+	NewWindow(title string) Window
+
+	// Open a URL in the default browser application.
+	OpenURL(url *url.URL) error
+
+	// Icon returns the application icon, this is used in various ways
+	// depending on operating system.
+	// This is also the default icon for new windows.
+	Icon() Resource
+
+	// SetIcon sets the icon resource used for this application instance.
+	SetIcon(Resource)
+
+	// Run the application - this starts the event loop and waits until Quit()
+	// is called or the last window closes.
+	// This should be called near the end of a main() function as it will block.
+	Run()
+
+	// Calling Quit on the application will cause the application to exit
+	// cleanly, closing all open windows.
+	// This function does no thing on a mobile device as the application lifecycle is
+	// managed by the operating system.
+	Quit()
+
+	// Driver returns the driver that is rendering this application.
+	// Typically not needed for day to day work, mostly internal functionality.
+	Driver() Driver
+
+	// UniqueID returns the application unique identifier, if set.
+	// This must be set for use of the Preferences() functions... see NewWithId(string)
+	UniqueID() string
+
+	// SendNotification sends a system notification that will be displayed in the operating system's notification area.
+	SendNotification(*Notification)
+
+	// Settings return the globally set settings, determining theme and so on.
+	Settings() Settings
+
+	// Preferences returns the application preferences, used for storing configuration and state
+	Preferences() Preferences
+
+	// Storage returns a storage handler specific to this application.
+	Storage() Storage
+
+	// Lifecycle returns a type that allows apps to hook in to lifecycle events.
+	//
+	// Since: 2.1
+	Lifecycle() Lifecycle
+
+	// Metadata returns the application metadata that was set at compile time.
+	//
+	// Since: 2.2
+	Metadata() AppMetadata
+
+	// CloudProvider returns the current app cloud provider,
+	// if one has been registered by the developer or chosen by the user.
+	//
+	// Since: 2.3
+	CloudProvider() CloudProvider // get the (if any) configured provider
+
+	// SetCloudProvider allows developers to specify how this application should integrate with cloud services.
+	// See `fyne.io/cloud` package for implementation details.
+	//
+	// Since: 2.3
+	SetCloudProvider(CloudProvider) // configure cloud for this app
+}
+
+// app contains an App variable, but due to atomic.Value restrictions on
+// interfaces we need to use an indirect type, i.e. appContainer.
+var app atomic.Value // appContainer
+
+// appContainer is a dummy container that holds an App instance. This
+// struct exists to guarantee that atomic.Value can store objects with
+// same type.
+type appContainer struct {
+	current App
+}
+
+// SetCurrentApp is an internal function to set the app instance currently running.
+func SetCurrentApp(current App) {
+	app.Store(appContainer{current})
+}
+
+// CurrentApp returns the current application, for which there is only 1 per process.
+func CurrentApp() App {
+	val := app.Load()
+	if val == nil {
+		LogError("Attempt to access current Fyne app when none is started", nil)
+		return nil
+	}
+	return (val).(appContainer).current
+}
+
+// AppMetadata captures the build metadata for an application.
+//
+// Since: 2.2
+type AppMetadata struct {
+	// ID is the unique ID of this application, used by many distribution platforms.
+	ID string
+	// Name is the human friendly name of this app.
+	Name string
+	// Version represents the version of this application, normally following semantic versioning.
+	Version string
+	// Build is the build number of this app, some times appended to the version number.
+	Build int
+	// Icon contains, if present, a resource of the icon that was bundled at build time.
+	Icon Resource
+	// Release if true this binary was build in release mode
+	// Since 2.3
+	Release bool
+	// Custom contain the custom metadata defined either in FyneApp.toml or on the compile command line
+	// Since 2.3
+	Custom map[string]string
+}
+
+// Lifecycle represents the various phases that an app can transition through.
+//
+// Since: 2.1
+type Lifecycle interface {
+	// SetOnEnteredForeground hooks into the app becoming foreground and gaining focus.
+	SetOnEnteredForeground(func())
+	// SetOnExitedForeground hooks into the app losing input focus and going into the background.
+	SetOnExitedForeground(func())
+	// SetOnStarted hooks into an event that says the app is now running.
+	SetOnStarted(func())
+	// SetOnStopped hooks into an event that says the app is no longer running.
+	SetOnStopped(func())
+}

+ 169 - 0
vendor/fyne.io/fyne/v2/app/app.go

@@ -0,0 +1,169 @@
+// Package app provides app implementations for working with Fyne graphical interfaces.
+// The fastest way to get started is to call app.New() which will normally load a new desktop application.
+// If the "ci" tag is passed to go (go run -tags ci myapp.go) it will run an in-memory application.
+package app // import "fyne.io/fyne/v2/app"
+
+import (
+	"os"
+	"strconv"
+	"sync/atomic"
+	"time"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/internal"
+	"fyne.io/fyne/v2/internal/app"
+	intRepo "fyne.io/fyne/v2/internal/repository"
+	"fyne.io/fyne/v2/storage/repository"
+)
+
+// Declare conformity with App interface
+var _ fyne.App = (*fyneApp)(nil)
+
+type fyneApp struct {
+	driver   fyne.Driver
+	icon     fyne.Resource
+	uniqueID string
+
+	cloud     fyne.CloudProvider
+	lifecycle fyne.Lifecycle
+	settings  *settings
+	storage   fyne.Storage
+	prefs     fyne.Preferences
+
+	running uint32 // atomic, 1 == running, 0 == stopped
+}
+
+func (a *fyneApp) CloudProvider() fyne.CloudProvider {
+	return a.cloud
+}
+
+func (a *fyneApp) Icon() fyne.Resource {
+	if a.icon != nil {
+		return a.icon
+	}
+
+	return a.Metadata().Icon
+}
+
+func (a *fyneApp) SetIcon(icon fyne.Resource) {
+	a.icon = icon
+}
+
+func (a *fyneApp) UniqueID() string {
+	if a.uniqueID != "" {
+		return a.uniqueID
+	}
+	if a.Metadata().ID != "" {
+		return a.Metadata().ID
+	}
+
+	fyne.LogError("Preferences API requires a unique ID, use app.NewWithID() or the FyneApp.toml ID field", nil)
+	a.uniqueID = "missing-id-" + strconv.FormatInt(time.Now().Unix(), 10) // This is a fake unique - it just has to not be reused...
+	return a.uniqueID
+}
+
+func (a *fyneApp) NewWindow(title string) fyne.Window {
+	return a.driver.CreateWindow(title)
+}
+
+func (a *fyneApp) Run() {
+	if atomic.CompareAndSwapUint32(&a.running, 0, 1) {
+		a.driver.Run()
+	}
+}
+
+func (a *fyneApp) Quit() {
+	for _, window := range a.driver.AllWindows() {
+		window.Close()
+	}
+
+	a.driver.Quit()
+	a.settings.stopWatching()
+	atomic.StoreUint32(&a.running, 0)
+}
+
+func (a *fyneApp) Driver() fyne.Driver {
+	return a.driver
+}
+
+// Settings returns the application settings currently configured.
+func (a *fyneApp) Settings() fyne.Settings {
+	return a.settings
+}
+
+func (a *fyneApp) Storage() fyne.Storage {
+	return a.storage
+}
+
+func (a *fyneApp) Preferences() fyne.Preferences {
+	if a.UniqueID() == "" {
+		fyne.LogError("Preferences API requires a unique ID, use app.NewWithID() or the FyneApp.toml ID field", nil)
+	}
+	return a.prefs
+}
+
+func (a *fyneApp) Lifecycle() fyne.Lifecycle {
+	return a.lifecycle
+}
+
+func (a *fyneApp) newDefaultPreferences() *preferences {
+	p := newPreferences(a)
+	if a.uniqueID != "" {
+		p.load()
+	}
+	return p
+}
+
+// New returns a new application instance with the default driver and no unique ID (unless specified in FyneApp.toml)
+func New() fyne.App {
+	if meta.ID == "" {
+		internal.LogHint("Applications should be created with a unique ID using app.NewWithID()")
+	}
+	return NewWithID(meta.ID)
+}
+
+func makeStoreDocs(id string, s *store) *internal.Docs {
+	if id != "" {
+		err := os.MkdirAll(s.a.storageRoot(), 0755) // make the space before anyone can use it
+		if err != nil {
+			fyne.LogError("Failed to create app storage space", err)
+		}
+
+		root, _ := s.docRootURI()
+		return &internal.Docs{RootDocURI: root}
+	} else {
+		return &internal.Docs{} // an empty impl to avoid crashes
+	}
+}
+
+func newAppWithDriver(d fyne.Driver, id string) fyne.App {
+	newApp := &fyneApp{uniqueID: id, driver: d, lifecycle: &app.Lifecycle{}}
+	fyne.SetCurrentApp(newApp)
+
+	newApp.prefs = newApp.newDefaultPreferences()
+	newApp.lifecycle.(*app.Lifecycle).SetOnStoppedHookExecuted(func() {
+		if prefs, ok := newApp.prefs.(*preferences); ok {
+			prefs.forceImmediateSave()
+		}
+	})
+	newApp.settings = loadSettings()
+	store := &store{a: newApp}
+	store.Docs = makeStoreDocs(id, store)
+	newApp.storage = store
+
+	if !d.Device().IsMobile() {
+		newApp.settings.watchSettings()
+	}
+
+	httpHandler := intRepo.NewHTTPRepository()
+	repository.Register("http", httpHandler)
+	repository.Register("https", httpHandler)
+
+	return newApp
+}
+
+// marker interface to pass system tray to supporting drivers
+type systrayDriver interface {
+	SetSystemTrayMenu(*fyne.Menu)
+	SetSystemTrayIcon(resource fyne.Resource)
+}

+ 60 - 0
vendor/fyne.io/fyne/v2/app/app_darwin.go

@@ -0,0 +1,60 @@
+//go:build !ci && !js && !wasm && !test_web_driver
+// +build !ci,!js,!wasm,!test_web_driver
+
+package app
+
+/*
+#cgo CFLAGS: -x objective-c
+#cgo LDFLAGS: -framework Foundation
+
+#include <stdbool.h>
+#include <stdlib.h>
+
+bool isBundled();
+void sendNotification(char *title, char *content);
+*/
+import "C"
+import (
+	"fmt"
+	"strings"
+	"unsafe"
+
+	"fyne.io/fyne/v2"
+	"golang.org/x/sys/execabs"
+)
+
+func (a *fyneApp) SendNotification(n *fyne.Notification) {
+	if C.isBundled() {
+		titleStr := C.CString(n.Title)
+		defer C.free(unsafe.Pointer(titleStr))
+		contentStr := C.CString(n.Content)
+		defer C.free(unsafe.Pointer(contentStr))
+
+		C.sendNotification(titleStr, contentStr)
+		return
+	}
+
+	fallbackNotification(n.Title, n.Content)
+}
+
+func escapeNotificationString(in string) string {
+	noSlash := strings.ReplaceAll(in, "\\", "\\\\")
+	return strings.ReplaceAll(noSlash, "\"", "\\\"")
+}
+
+//export fallbackSend
+func fallbackSend(cTitle, cContent *C.char) {
+	title := C.GoString(cTitle)
+	content := C.GoString(cContent)
+	fallbackNotification(title, content)
+}
+
+func fallbackNotification(title, content string) {
+	template := `display notification "%s" with title "%s"`
+	script := fmt.Sprintf(template, escapeNotificationString(content), escapeNotificationString(title))
+
+	err := execabs.Command("osascript", "-e", script).Start()
+	if err != nil {
+		fyne.LogError("Failed to launch darwin notify script", err)
+	}
+}

+ 61 - 0
vendor/fyne.io/fyne/v2/app/app_darwin.m

@@ -0,0 +1,61 @@
+//go:build !ci
+// +build !ci
+
+#import <Foundation/Foundation.h>
+#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400
+#import <UserNotifications/UserNotifications.h>
+#endif
+
+static int notifyNum = 0;
+
+extern void fallbackSend(char *cTitle, char *cBody);
+
+bool isBundled() {
+    return [[NSBundle mainBundle] bundleIdentifier] != nil;
+}
+
+#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400
+void doSendNotification(UNUserNotificationCenter *center, NSString *title, NSString *body) {
+    UNMutableNotificationContent *content = [UNMutableNotificationContent new];
+    [content autorelease];
+    content.title = title;
+    content.body = body;
+
+    notifyNum++;
+    NSString *identifier = [NSString stringWithFormat:@"fyne-notify-%d", notifyNum];
+    UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier
+        content:content trigger:nil];
+
+    [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
+        if (error != nil) {
+            NSLog(@"Could not send notification: %@", error);
+        }
+    }];
+}
+
+void sendNotification(char *cTitle, char *cBody) {
+    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+    NSString *title = [NSString stringWithUTF8String:cTitle];
+    NSString *body = [NSString stringWithUTF8String:cBody];
+
+    UNAuthorizationOptions options = UNAuthorizationOptionAlert;
+    [center requestAuthorizationWithOptions:options
+        completionHandler:^(BOOL granted, NSError *_Nullable error) {
+            if (!granted) {
+                if (error != NULL) {
+                    NSLog(@"Error asking for permission to send notifications %@", error);
+                    // this happens if our app was not signed, so do it the old way
+                    fallbackSend((char *)[title UTF8String], (char *)[body UTF8String]);
+                } else {
+                    NSLog(@"Unable to get permission to send notifications");
+                }
+            } else {
+                doSendNotification(center, title, body);
+            }
+        }];
+}
+#else
+void sendNotification(char *cTitle, char *cBody) {
+	fallbackSend(cTitle, cBody);
+}
+#endif

+ 8 - 0
vendor/fyne.io/fyne/v2/app/app_debug.go

@@ -0,0 +1,8 @@
+//go:build debug
+// +build debug
+
+package app
+
+import "fyne.io/fyne/v2"
+
+const buildMode = fyne.BuildDebug

+ 69 - 0
vendor/fyne.io/fyne/v2/app/app_desktop_darwin.go

@@ -0,0 +1,69 @@
+//go:build !ci && !ios && !js && !wasm && !test_web_driver
+// +build !ci,!ios,!js,!wasm,!test_web_driver
+
+package app
+
+/*
+#cgo CFLAGS: -x objective-c
+#cgo LDFLAGS: -framework Foundation
+
+#include <AppKit/AppKit.h>
+
+bool isBundled();
+bool isDarkMode();
+void watchTheme();
+*/
+import "C"
+import (
+	"net/url"
+	"os"
+	"path/filepath"
+
+	"golang.org/x/sys/execabs"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/theme"
+)
+
+// SetSystemTrayMenu creates a system tray item and attaches the specified menu.
+// By default this will use the application icon.
+func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) {
+	if desk, ok := a.Driver().(systrayDriver); ok {
+		desk.SetSystemTrayMenu(menu)
+	}
+}
+
+// SetSystemTrayIcon sets a custom image for the system tray icon.
+// You should have previously called `SetSystemTrayMenu` to initialise the menu icon.
+func (a *fyneApp) SetSystemTrayIcon(icon fyne.Resource) {
+	a.Driver().(systrayDriver).SetSystemTrayIcon(icon)
+}
+
+func defaultVariant() fyne.ThemeVariant {
+	if C.isDarkMode() {
+		return theme.VariantDark
+	}
+	return theme.VariantLight
+}
+
+func rootConfigDir() string {
+	homeDir, _ := os.UserHomeDir()
+
+	desktopConfig := filepath.Join(filepath.Join(homeDir, "Library"), "Preferences")
+	return filepath.Join(desktopConfig, "fyne")
+}
+
+func (a *fyneApp) OpenURL(url *url.URL) error {
+	cmd := execabs.Command("open", url.String())
+	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+	return cmd.Run()
+}
+
+//export themeChanged
+func themeChanged() {
+	fyne.CurrentApp().Settings().(*settings).setupTheme()
+}
+
+func watchTheme() {
+	C.watchTheme()
+}

+ 18 - 0
vendor/fyne.io/fyne/v2/app/app_desktop_darwin.m

@@ -0,0 +1,18 @@
+//go:build !ci && !ios
+// +build !ci,!ios
+
+extern void themeChanged();
+
+#import <Foundation/Foundation.h>
+
+bool isDarkMode() {
+    NSString *style = [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"];
+    return [@"Dark" isEqualToString:style];
+}
+
+void watchTheme() {
+    [[NSDistributedNotificationCenter defaultCenter] addObserverForName:@"AppleInterfaceThemeChangedNotification" object:nil queue:nil
+        usingBlock:^(NSNotification *note) {
+        themeChanged(); // calls back into Go
+    }];
+}

+ 15 - 0
vendor/fyne.io/fyne/v2/app/app_gl.go

@@ -0,0 +1,15 @@
+//go:build !ci && !android && !ios && !mobile
+// +build !ci,!android,!ios,!mobile
+
+package app
+
+import (
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/internal/driver/glfw"
+)
+
+// NewWithID returns a new app instance using the appropriate runtime driver.
+// The ID string should be globally unique to this app.
+func NewWithID(id string) fyne.App {
+	return newAppWithDriver(glfw.NewGLDriver(), id)
+}

+ 19 - 0
vendor/fyne.io/fyne/v2/app/app_goxjs.go

@@ -0,0 +1,19 @@
+//go:build !ci && (!android || !ios || !mobile) && (js || wasm || test_web_driver)
+// +build !ci
+// +build !android !ios !mobile
+// +build js wasm test_web_driver
+
+package app
+
+import (
+	"fyne.io/fyne/v2"
+)
+
+func (app *fyneApp) SendNotification(_ *fyne.Notification) {
+	// TODO #2735
+	fyne.LogError("Sending notification is not supported yet.", nil)
+}
+
+func rootConfigDir() string {
+	return "/data/"
+}

+ 25 - 0
vendor/fyne.io/fyne/v2/app/app_mobile.go

@@ -0,0 +1,25 @@
+//go:build !ci && (android || ios || mobile)
+// +build !ci
+// +build android ios mobile
+
+package app
+
+import (
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/internal/driver/mobile"
+)
+
+var systemTheme fyne.ThemeVariant
+
+// NewWithID returns a new app instance using the appropriate runtime driver.
+// The ID string should be globally unique to this app.
+func NewWithID(id string) fyne.App {
+	d := mobile.NewGoMobileDriver()
+	a := newAppWithDriver(d, id)
+	d.(mobile.ConfiguredDriver).SetOnConfigurationChanged(func(c *mobile.Configuration) {
+		systemTheme = c.SystemTheme
+
+		a.Settings().(*settings).setupTheme()
+	})
+	return a
+}

+ 131 - 0
vendor/fyne.io/fyne/v2/app/app_mobile_and.c

@@ -0,0 +1,131 @@
+//go:build !ci && android
+// +build !ci,android
+
+#include <android/log.h>
+#include <jni.h>
+#include <stdbool.h>
+#include <stdlib.h>
+
+#define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, "Fyne", __VA_ARGS__)
+
+static jclass find_class(JNIEnv *env, const char *class_name) {
+	jclass clazz = (*env)->FindClass(env, class_name);
+	if (clazz == NULL) {
+		(*env)->ExceptionClear(env);
+		LOG_FATAL("cannot find %s", class_name);
+		return NULL;
+	}
+	return clazz;
+}
+
+static jmethodID find_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
+	jmethodID m = (*env)->GetMethodID(env, clazz, name, sig);
+	if (m == 0) {
+		(*env)->ExceptionClear(env);
+		LOG_FATAL("cannot find method %s %s", name, sig);
+		return 0;
+	}
+	return m;
+}
+
+static jmethodID find_static_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
+	jmethodID m = (*env)->GetStaticMethodID(env, clazz, name, sig);
+	if (m == 0) {
+		(*env)->ExceptionClear(env);
+		LOG_FATAL("cannot find method %s %s", name, sig);
+		return 0;
+	}
+	return m;
+}
+
+jobject getSystemService(uintptr_t jni_env, uintptr_t ctx, char *service) {
+	JNIEnv *env = (JNIEnv*)jni_env;
+	jstring serviceStr = (*env)->NewStringUTF(env, service);
+
+	jclass ctxClass = (*env)->GetObjectClass(env, (jobject)ctx);
+	jmethodID getSystemService = find_method(env, ctxClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
+
+	return (jobject)(*env)->CallObjectMethod(env, (jobject)ctx, getSystemService, serviceStr);
+}
+
+int nextId = 1;
+
+bool isOreoOrLater(JNIEnv *env) {
+    jclass versionClass = find_class(env, "android/os/Build$VERSION" );
+    jfieldID sdkIntFieldID = (*env)->GetStaticFieldID(env, versionClass, "SDK_INT", "I" );
+    int sdkVersion = (*env)->GetStaticIntField(env, versionClass, sdkIntFieldID );
+
+    return sdkVersion >= 26; // O = Oreo, will not be defined for older builds
+}
+
+jobject parseURL(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
+	JNIEnv *env = (JNIEnv*)jni_env;
+
+	jstring uriStr = (*env)->NewStringUTF(env, uriCstr);
+	jclass uriClass = find_class(env, "android/net/Uri");
+	jmethodID parse = find_static_method(env, uriClass, "parse", "(Ljava/lang/String;)Landroid/net/Uri;");
+
+	return (jobject)(*env)->CallStaticObjectMethod(env, uriClass, parse, uriStr);
+}
+
+void openURL(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *url) {
+	JNIEnv *env = (JNIEnv*)jni_env;
+	jobject uri = parseURL(jni_env, ctx, url);
+
+	jclass intentClass = find_class(env, "android/content/Intent");
+	jfieldID viewFieldID = (*env)->GetStaticFieldID(env, intentClass, "ACTION_VIEW", "Ljava/lang/String;" );
+    jstring view = (*env)->GetStaticObjectField(env, intentClass, viewFieldID);
+
+	jmethodID constructor = find_method(env, intentClass, "<init>", "(Ljava/lang/String;Landroid/net/Uri;)V");
+	jobject intent = (*env)->NewObject(env, intentClass, constructor, view, uri);
+
+	jclass contextClass = find_class(env, "android/content/Context");
+	jmethodID start = find_method(env, contextClass, "startActivity", "(Landroid/content/Intent;)V");
+	(*env)->CallVoidMethod(env, (jobject)ctx, start, intent);
+}
+
+void sendNotification(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *title, char *body) {
+	JNIEnv *env = (JNIEnv*)jni_env;
+	jstring titleStr = (*env)->NewStringUTF(env, title);
+	jstring bodyStr = (*env)->NewStringUTF(env, body);
+
+	jclass cls = find_class(env, "android/app/Notification$Builder");
+	jmethodID constructor = find_method(env, cls, "<init>", "(Landroid/content/Context;)V");
+	jobject builder = (*env)->NewObject(env, cls, constructor, ctx);
+
+	jclass mgrCls = find_class(env, "android/app/NotificationManager");
+	jobject mgr = getSystemService((uintptr_t)env, ctx, "notification");
+
+	if (isOreoOrLater(env)) {
+		jstring channelId = (*env)->NewStringUTF(env, "fyne-notif");
+		jstring name = (*env)->NewStringUTF(env, "Fyne Notification");
+        int importance = 4; // IMPORTANCE_HIGH
+
+		jclass chanCls = find_class(env, "android/app/NotificationChannel");
+		jmethodID constructor = find_method(env, chanCls, "<init>", "(Ljava/lang/String;Ljava/lang/CharSequence;I)V");
+		jobject channel = (*env)->NewObject(env, chanCls, constructor, channelId, name, importance);
+
+		jmethodID createChannel = find_method(env, mgrCls, "createNotificationChannel", "(Landroid/app/NotificationChannel;)V");
+		(*env)->CallVoidMethod(env, mgr, createChannel, channel);
+
+		jmethodID setChannelId = find_method(env, cls, "setChannelId", "(Ljava/lang/String;)Landroid/app/Notification$Builder;");
+		(*env)->CallObjectMethod(env, builder, setChannelId, channelId);
+	}
+
+	jmethodID setContentTitle = find_method(env, cls, "setContentTitle", "(Ljava/lang/CharSequence;)Landroid/app/Notification$Builder;");
+	(*env)->CallObjectMethod(env, builder, setContentTitle, titleStr);
+
+	jmethodID setContentText = find_method(env, cls, "setContentText", "(Ljava/lang/CharSequence;)Landroid/app/Notification$Builder;");
+	(*env)->CallObjectMethod(env, builder, setContentText, bodyStr);
+
+	int iconID = 17629184; // constant of "unknown app icon"
+	jmethodID setSmallIcon = find_method(env, cls, "setSmallIcon", "(I)Landroid/app/Notification$Builder;");
+	(*env)->CallObjectMethod(env, builder, setSmallIcon, iconID);
+
+	jmethodID build = find_method(env, cls, "build", "()Landroid/app/Notification;");
+	jobject notif = (*env)->CallObjectMethod(env, builder, build);
+
+	jmethodID notify = find_method(env, mgrCls, "notify", "(ILandroid/app/Notification;)V");
+	(*env)->CallVoidMethod(env, mgr, notify, nextId, notif);
+	nextId++;
+}

+ 61 - 0
vendor/fyne.io/fyne/v2/app/app_mobile_and.go

@@ -0,0 +1,61 @@
+//go:build !ci && android
+// +build !ci,android
+
+package app
+
+/*
+#cgo LDFLAGS: -landroid -llog
+
+#include <stdlib.h>
+
+void openURL(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *url);
+void sendNotification(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *title, char *content);
+*/
+import "C"
+import (
+	"log"
+	"net/url"
+	"os"
+	"path/filepath"
+	"unsafe"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/internal/driver/mobile/app"
+)
+
+func (a *fyneApp) OpenURL(url *url.URL) error {
+	urlStr := C.CString(url.String())
+	defer C.free(unsafe.Pointer(urlStr))
+
+	app.RunOnJVM(func(vm, env, ctx uintptr) error {
+		C.openURL(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), urlStr)
+		return nil
+	})
+	return nil
+}
+
+func (a *fyneApp) SendNotification(n *fyne.Notification) {
+	titleStr := C.CString(n.Title)
+	defer C.free(unsafe.Pointer(titleStr))
+	contentStr := C.CString(n.Content)
+	defer C.free(unsafe.Pointer(contentStr))
+
+	app.RunOnJVM(func(vm, env, ctx uintptr) error {
+		C.sendNotification(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), titleStr, contentStr)
+		return nil
+	})
+}
+
+func defaultVariant() fyne.ThemeVariant {
+	return systemTheme
+}
+
+func rootConfigDir() string {
+	filesDir := os.Getenv("FILESDIR")
+	if filesDir == "" {
+		log.Println("FILESDIR env was not set by android native code")
+		return "/data/data" // probably won't work, but we can't make a better guess
+	}
+
+	return filepath.Join(filesDir, "fyne")
+}

+ 40 - 0
vendor/fyne.io/fyne/v2/app/app_mobile_ios.go

@@ -0,0 +1,40 @@
+//go:build !ci && ios
+// +build !ci,ios
+
+package app
+
+/*
+#cgo CFLAGS: -x objective-c
+#cgo LDFLAGS: -framework Foundation -framework UIKit -framework UserNotifications
+
+#include <stdlib.h>
+
+char *documentsPath(void);
+void openURL(char *urlStr);
+void sendNotification(char *title, char *content);
+*/
+import "C"
+import (
+	"net/url"
+	"path/filepath"
+	"unsafe"
+
+	"fyne.io/fyne/v2"
+)
+
+func rootConfigDir() string {
+	root := C.documentsPath()
+	return filepath.Join(C.GoString(root), "fyne")
+}
+
+func (a *fyneApp) OpenURL(url *url.URL) error {
+	urlStr := C.CString(url.String())
+	C.openURL(urlStr)
+	C.free(unsafe.Pointer(urlStr))
+
+	return nil
+}
+
+func defaultVariant() fyne.ThemeVariant {
+	return systemTheme
+}

+ 16 - 0
vendor/fyne.io/fyne/v2/app/app_mobile_ios.m

@@ -0,0 +1,16 @@
+//go:build !ci && ios
+// +build !ci,ios
+
+#import <UIKit/UIKit.h>
+
+void openURL(char *urlStr) {
+    UIApplication *app = [UIApplication sharedApplication];
+    NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:urlStr]];
+    [app openURL:url options:@{} completionHandler:nil];
+}
+
+char *documentsPath() {
+    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+    NSString *path = paths.firstObject;
+    return [path UTF8String];
+}

+ 9 - 0
vendor/fyne.io/fyne/v2/app/app_notlegacy_darwin.go

@@ -0,0 +1,9 @@
+//go:build !ci && !legacy && !js && !wasm && !test_web_driver
+// +build !ci,!legacy,!js,!wasm,!test_web_driver
+
+package app
+
+/*
+#cgo LDFLAGS: -framework Foundation -framework UserNotifications
+*/
+import "C"

+ 20 - 0
vendor/fyne.io/fyne/v2/app/app_openurl_js.go

@@ -0,0 +1,20 @@
+//go:build !ci && js && !wasm
+// +build !ci,js,!wasm
+
+package app
+
+import (
+	"fmt"
+	"net/url"
+
+	"honnef.co/go/js/dom"
+)
+
+func (app *fyneApp) OpenURL(url *url.URL) error {
+	window := dom.GetWindow().Open(url.String(), "_blank", "")
+	if window == nil {
+		return fmt.Errorf("Unable to open a new window/tab for URL: %v.", url)
+	}
+	window.Focus()
+	return nil
+}

+ 19 - 0
vendor/fyne.io/fyne/v2/app/app_openurl_wasm.go

@@ -0,0 +1,19 @@
+//go:build !ci && wasm
+// +build !ci,wasm
+
+package app
+
+import (
+	"fmt"
+	"net/url"
+	"syscall/js"
+)
+
+func (app *fyneApp) OpenURL(url *url.URL) error {
+	window := js.Global().Call("open", url.String(), "_blank", "")
+	if window.Equal(js.Null()) {
+		return fmt.Errorf("Unable to open a new window/tab for URL: %v.", url)
+	}
+	window.Call("focus")
+	return nil
+}

+ 13 - 0
vendor/fyne.io/fyne/v2/app/app_openurl_web.go

@@ -0,0 +1,13 @@
+//go:build !ci && !js && !wasm && test_web_driver
+// +build !ci,!js,!wasm,test_web_driver
+
+package app
+
+import (
+	"errors"
+	"net/url"
+)
+
+func (app *fyneApp) OpenURL(url *url.URL) error {
+	return errors.New("OpenURL is not supported with the test web driver.")
+}

+ 34 - 0
vendor/fyne.io/fyne/v2/app/app_other.go

@@ -0,0 +1,34 @@
+//go:build ci || (!linux && !darwin && !windows && !freebsd && !openbsd && !netbsd && !js && !wasm && !test_web_driver)
+// +build ci !linux,!darwin,!windows,!freebsd,!openbsd,!netbsd,!js,!wasm,!test_web_driver
+
+package app
+
+import (
+	"errors"
+	"net/url"
+	"os"
+	"path/filepath"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/theme"
+)
+
+func defaultVariant() fyne.ThemeVariant {
+	return theme.VariantDark
+}
+
+func rootConfigDir() string {
+	return filepath.Join(os.TempDir(), "fyne-test")
+}
+
+func (a *fyneApp) OpenURL(_ *url.URL) error {
+	return errors.New("Unable to open url for unknown operating system")
+}
+
+func (a *fyneApp) SendNotification(_ *fyne.Notification) {
+	fyne.LogError("Refusing to show notification for unknown operating system", nil)
+}
+
+func watchTheme() {
+	// no-op
+}

+ 8 - 0
vendor/fyne.io/fyne/v2/app/app_release.go

@@ -0,0 +1,8 @@
+//go:build release
+// +build release
+
+package app
+
+import "fyne.io/fyne/v2"
+
+const buildMode = fyne.BuildRelease

+ 16 - 0
vendor/fyne.io/fyne/v2/app/app_software.go

@@ -0,0 +1,16 @@
+//go:build ci
+// +build ci
+
+package app
+
+import (
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/internal/painter/software"
+	"fyne.io/fyne/v2/test"
+)
+
+// NewWithID returns a new app instance using the test (headless) driver.
+// The ID string should be globally unique to this app.
+func NewWithID(id string) fyne.App {
+	return newAppWithDriver(test.NewDriverWithPainter(software.NewPainter()), id)
+}

+ 8 - 0
vendor/fyne.io/fyne/v2/app/app_standard.go

@@ -0,0 +1,8 @@
+//go:build !debug && !release
+// +build !debug,!release
+
+package app
+
+import "fyne.io/fyne/v2"
+
+const buildMode = fyne.BuildStandard

+ 29 - 0
vendor/fyne.io/fyne/v2/app/app_theme_js.go

@@ -0,0 +1,29 @@
+//go:build !ci && js && !wasm
+// +build !ci,js,!wasm
+
+package app
+
+import (
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/theme"
+
+	"github.com/gopherjs/gopherjs/js"
+)
+
+func defaultVariant() fyne.ThemeVariant {
+	if matchMedia := js.Global.Call("matchMedia", "(prefers-color-scheme: dark)"); matchMedia != js.Undefined {
+		if matches := matchMedia.Get("matches"); matches != js.Undefined && matches.Bool() {
+			return theme.VariantDark
+		}
+		return theme.VariantLight
+	}
+	return theme.VariantDark
+}
+
+func init() {
+	if matchMedia := js.Global.Call("matchMedia", "(prefers-color-scheme: dark)"); matchMedia != js.Undefined {
+		matchMedia.Call("addEventListener", "change", func(o *js.Object) {
+			fyne.CurrentApp().Settings().(*settings).setupTheme()
+		})
+	}
+}

+ 31 - 0
vendor/fyne.io/fyne/v2/app/app_theme_wasm.go

@@ -0,0 +1,31 @@
+//go:build !ci && wasm
+// +build !ci,wasm
+
+package app
+
+import (
+	"syscall/js"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/theme"
+)
+
+func defaultVariant() fyne.ThemeVariant {
+	matches := js.Global().Call("matchMedia", "(prefers-color-scheme: dark)")
+	if matches.Truthy() {
+		if matches.Get("matches").Bool() {
+			return theme.VariantDark
+		}
+		return theme.VariantLight
+	}
+	return theme.VariantDark
+}
+
+func init() {
+	if matchMedia := js.Global().Call("matchMedia", "(prefers-color-scheme: dark)"); matchMedia.Truthy() {
+		matchMedia.Call("addEventListener", "change", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+			fyne.CurrentApp().Settings().(*settings).setupTheme()
+			return nil
+		}))
+	}
+}

+ 13 - 0
vendor/fyne.io/fyne/v2/app/app_theme_web.go

@@ -0,0 +1,13 @@
+//go:build !ci && !js && !wasm && test_web_driver
+// +build !ci,!js,!wasm,test_web_driver
+
+package app
+
+import (
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/theme"
+)
+
+func defaultVariant() fyne.ThemeVariant {
+	return theme.VariantDark
+}

+ 124 - 0
vendor/fyne.io/fyne/v2/app/app_windows.go

@@ -0,0 +1,124 @@
+//go:build !ci && !js && !android && !ios && !wasm && !test_web_driver
+// +build !ci,!js,!android,!ios,!wasm,!test_web_driver
+
+package app
+
+import (
+	"fmt"
+	"net/url"
+	"os"
+	"path/filepath"
+	"strings"
+	"syscall"
+
+	"golang.org/x/sys/execabs"
+	"golang.org/x/sys/windows/registry"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/theme"
+)
+
+const notificationTemplate = `$title = "%s"
+$content = "%s"
+
+[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
+$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
+$toastXml = [xml] $template.GetXml()
+$toastXml.GetElementsByTagName("text")[0].AppendChild($toastXml.CreateTextNode($title)) > $null
+$toastXml.GetElementsByTagName("text")[1].AppendChild($toastXml.CreateTextNode($content)) > $null
+
+$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
+$xml.LoadXml($toastXml.OuterXml)
+$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
+[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("%s").Show($toast);`
+
+func isDark() bool {
+	k, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE)
+	if err != nil { // older version of Windows will not have this key
+		return false
+	}
+	defer k.Close()
+
+	useLight, _, err := k.GetIntegerValue("AppsUseLightTheme")
+	if err != nil { // older version of Windows will not have this value
+		return false
+	}
+
+	return useLight == 0
+}
+
+func defaultVariant() fyne.ThemeVariant {
+	if isDark() {
+		return theme.VariantDark
+	}
+	return theme.VariantLight
+}
+
+func rootConfigDir() string {
+	homeDir, _ := os.UserHomeDir()
+
+	desktopConfig := filepath.Join(filepath.Join(homeDir, "AppData"), "Roaming")
+	return filepath.Join(desktopConfig, "fyne")
+}
+
+func (a *fyneApp) OpenURL(url *url.URL) error {
+	cmd := execabs.Command("rundll32", "url.dll,FileProtocolHandler", url.String())
+	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+	return cmd.Run()
+}
+
+var scriptNum = 0
+
+func (a *fyneApp) SendNotification(n *fyne.Notification) {
+	title := escapeNotificationString(n.Title)
+	content := escapeNotificationString(n.Content)
+	appID := a.UniqueID()
+	if appID == "" || strings.Index(appID, "missing-id") == 0 {
+		appID = a.Metadata().Name
+	}
+
+	script := fmt.Sprintf(notificationTemplate, title, content, appID)
+	go runScript("notify", script)
+}
+
+// SetSystemTrayMenu creates a system tray item and attaches the specified menu.
+// By default this will use the application icon.
+func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) {
+	a.Driver().(systrayDriver).SetSystemTrayMenu(menu)
+}
+
+// SetSystemTrayIcon sets a custom image for the system tray icon.
+// You should have previously called `SetSystemTrayMenu` to initialise the menu icon.
+func (a *fyneApp) SetSystemTrayIcon(icon fyne.Resource) {
+	a.Driver().(systrayDriver).SetSystemTrayIcon(icon)
+}
+
+func escapeNotificationString(in string) string {
+	noSlash := strings.ReplaceAll(in, "`", "``")
+	return strings.ReplaceAll(noSlash, "\"", "`\"")
+}
+
+func runScript(name, script string) {
+	scriptNum++
+	appID := fyne.CurrentApp().UniqueID()
+	fileName := fmt.Sprintf("fyne-%s-%s-%d.ps1", appID, name, scriptNum)
+
+	tmpFilePath := filepath.Join(os.TempDir(), fileName)
+	err := os.WriteFile(tmpFilePath, []byte(script), 0600)
+	if err != nil {
+		fyne.LogError("Could not write script to show notification", err)
+		return
+	}
+	defer os.Remove(tmpFilePath)
+
+	launch := "(Get-Content -Encoding UTF8 -Path " + tmpFilePath + " -Raw) | Invoke-Expression"
+	cmd := execabs.Command("PowerShell", "-ExecutionPolicy", "Bypass", launch)
+	cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
+	err = cmd.Run()
+	if err != nil {
+		fyne.LogError("Failed to launch windows notify script", err)
+	}
+}
+func watchTheme() {
+	// TODO monitor the Windows theme
+}

+ 202 - 0
vendor/fyne.io/fyne/v2/app/app_xdg.go

@@ -0,0 +1,202 @@
+//go:build !ci && !js && !wasm && !test_web_driver && (linux || openbsd || freebsd || netbsd) && !android
+// +build !ci
+// +build !js
+// +build !wasm
+// +build !test_web_driver
+// +build linux openbsd freebsd netbsd
+// +build !android
+
+package app
+
+import (
+	"net/url"
+	"os"
+	"path/filepath"
+	"sync"
+
+	"github.com/godbus/dbus/v5"
+	"golang.org/x/sys/execabs"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/theme"
+)
+
+var once sync.Once
+
+func defaultVariant() fyne.ThemeVariant {
+	return findFreedestktopColorScheme()
+}
+
+func (a *fyneApp) OpenURL(url *url.URL) error {
+	cmd := execabs.Command("xdg-open", url.String())
+	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+	return cmd.Start()
+}
+
+// fetch color variant from dbus portal desktop settings.
+func findFreedestktopColorScheme() fyne.ThemeVariant {
+	dbusConn, err := dbus.SessionBus()
+	if err != nil {
+		fyne.LogError("Unable to connect to session D-Bus", err)
+		return theme.VariantDark
+	}
+
+	dbusObj := dbusConn.Object("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop")
+	call := dbusObj.Call(
+		"org.freedesktop.portal.Settings.Read",
+		dbus.FlagNoAutoStart,
+		"org.freedesktop.appearance",
+		"color-scheme",
+	)
+	if call.Err != nil {
+		// many desktops don't have this exported yet
+		return theme.VariantDark
+	}
+
+	var value uint8
+	if err = call.Store(&value); err != nil {
+		fyne.LogError("failed to read theme variant from D-Bus", err)
+		return theme.VariantDark
+	}
+
+	// See: https://github.com/flatpak/xdg-desktop-portal/blob/1.16.0/data/org.freedesktop.impl.portal.Settings.xml#L32-L46
+	// 0: No preference
+	// 1: Prefer dark appearance
+	// 2: Prefer light appearance
+	switch value {
+	case 2:
+		return theme.VariantLight
+	case 1:
+		return theme.VariantDark
+	default:
+		// Default to light theme to support Gnome's default see https://github.com/fyne-io/fyne/pull/3561
+		return theme.VariantLight
+	}
+}
+
+func (a *fyneApp) SendNotification(n *fyne.Notification) {
+	conn, err := dbus.SessionBus() // shared connection, don't close
+	if err != nil {
+		fyne.LogError("Unable to connect to session D-Bus", err)
+		return
+	}
+
+	appName := fyne.CurrentApp().UniqueID()
+	appIcon := a.cachedIconPath()
+	timeout := int32(0) // we don't support this yet
+
+	obj := conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
+	call := obj.Call("org.freedesktop.Notifications.Notify", 0, appName, uint32(0),
+		appIcon, n.Title, n.Content, []string{}, map[string]dbus.Variant{}, timeout)
+	if call.Err != nil {
+		fyne.LogError("Failed to send message to bus", call.Err)
+	}
+}
+
+func (a *fyneApp) saveIconToCache(dirPath, filePath string) error {
+	err := os.MkdirAll(dirPath, 0700)
+	if err != nil {
+		fyne.LogError("Unable to create application cache directory", err)
+		return err
+	}
+
+	file, err := os.Create(filePath)
+	if err != nil {
+		fyne.LogError("Unable to create icon file", err)
+		return err
+	}
+
+	defer file.Close()
+
+	if icon := a.Icon(); icon != nil {
+		_, err = file.Write(icon.Content())
+		if err != nil {
+			fyne.LogError("Unable to write icon contents", err)
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (a *fyneApp) cachedIconPath() string {
+	if a.Icon() == nil {
+		return ""
+	}
+
+	dirPath := filepath.Join(rootCacheDir(), a.UniqueID())
+	filePath := filepath.Join(dirPath, "icon.png")
+	once.Do(func() {
+		err := a.saveIconToCache(dirPath, filePath)
+		if err != nil {
+			filePath = ""
+		}
+	})
+
+	return filePath
+}
+
+// SetSystemTrayMenu creates a system tray item and attaches the specified menu.
+// By default this will use the application icon.
+func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) {
+	if desk, ok := a.Driver().(systrayDriver); ok { // don't use this on mobile tag
+		desk.SetSystemTrayMenu(menu)
+	}
+}
+
+// SetSystemTrayIcon sets a custom image for the system tray icon.
+// You should have previously called `SetSystemTrayMenu` to initialise the menu icon.
+func (a *fyneApp) SetSystemTrayIcon(icon fyne.Resource) {
+	if desk, ok := a.Driver().(systrayDriver); ok { // don't use this on mobile tag
+		desk.SetSystemTrayIcon(icon)
+	}
+}
+
+func rootConfigDir() string {
+	desktopConfig, _ := os.UserConfigDir()
+	return filepath.Join(desktopConfig, "fyne")
+}
+
+func rootCacheDir() string {
+	desktopCache, _ := os.UserCacheDir()
+	return filepath.Join(desktopCache, "fyne")
+}
+
+func watchTheme() {
+	go watchFreedekstopThemeChange()
+}
+
+func themeChanged() {
+	fyne.CurrentApp().Settings().(*settings).setupTheme()
+}
+
+// connect to dbus to detect color-schem theme changes in portal settings.
+func watchFreedekstopThemeChange() {
+	conn, err := dbus.SessionBus()
+	if err != nil {
+		fyne.LogError("Unable to connect to session D-Bus", err)
+		return
+	}
+
+	if err := conn.AddMatchSignal(
+		dbus.WithMatchObjectPath("/org/freedesktop/portal/desktop"),
+		dbus.WithMatchInterface("org.freedesktop.portal.Settings"),
+		dbus.WithMatchMember("SettingChanged"),
+	); err != nil {
+		fyne.LogError("D-Bus signal match failed", err)
+		return
+	}
+	defer conn.Close()
+
+	dbusChan := make(chan *dbus.Signal)
+	conn.Signal(dbusChan)
+
+	for sig := range dbusChan {
+		for _, v := range sig.Body {
+			if v == "color-scheme" {
+				themeChanged()
+				break
+			}
+		}
+	}
+}

+ 47 - 0
vendor/fyne.io/fyne/v2/app/cloud.go

@@ -0,0 +1,47 @@
+package app
+
+import "fyne.io/fyne/v2"
+
+func (a *fyneApp) SetCloudProvider(p fyne.CloudProvider) {
+	if p == nil {
+		a.cloud = nil
+		return
+	}
+
+	a.transitionCloud(p)
+}
+
+func (a *fyneApp) transitionCloud(p fyne.CloudProvider) {
+	if a.cloud != nil {
+		a.cloud.Cleanup(a)
+	}
+
+	err := p.Setup(a)
+	if err != nil {
+		fyne.LogError("Failed to set up cloud provider "+p.ProviderName(), err)
+		return
+	}
+	a.cloud = p
+
+	listeners := a.prefs.ChangeListeners()
+	if pp, ok := p.(fyne.CloudProviderPreferences); ok {
+		a.prefs = pp.CloudPreferences(a)
+	} else {
+		a.prefs = a.newDefaultPreferences()
+	}
+	if cloud, ok := p.(fyne.CloudProviderStorage); ok {
+		a.storage = cloud.CloudStorage(a)
+	} else {
+		store := &store{a: a}
+		store.Docs = makeStoreDocs(a.uniqueID, store)
+		a.storage = store
+	}
+
+	for _, l := range listeners {
+		a.prefs.AddChangeListener(l)
+		l() // assume that preferences have changed because we replaced the provider
+	}
+
+	// after transition ensure settings listener is fired
+	a.settings.apply()
+}

+ 28 - 0
vendor/fyne.io/fyne/v2/app/meta.go

@@ -0,0 +1,28 @@
+package app
+
+import (
+	"fyne.io/fyne/v2"
+)
+
+var meta = fyne.AppMetadata{
+	ID:      "",
+	Name:    "",
+	Version: "0.0.1",
+	Build:   1,
+	Release: false,
+	Custom:  map[string]string{},
+}
+
+// SetMetadata overrides the packaged application metadata.
+// This data can be used in many places like notifications and about screens.
+func SetMetadata(m fyne.AppMetadata) {
+	meta = m
+
+	if meta.Custom == nil {
+		meta.Custom = map[string]string{}
+	}
+}
+
+func (a *fyneApp) Metadata() fyne.AppMetadata {
+	return meta
+}

+ 208 - 0
vendor/fyne.io/fyne/v2/app/preferences.go

@@ -0,0 +1,208 @@
+package app
+
+import (
+	"encoding/json"
+	"os"
+	"path/filepath"
+	"sync"
+	"time"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/internal"
+)
+
+type preferences struct {
+	*internal.InMemoryPreferences
+
+	prefLock            sync.RWMutex
+	loadingInProgress   bool
+	savedRecently       bool
+	changedDuringSaving bool
+
+	app                 *fyneApp
+	needsSaveBeforeExit bool
+}
+
+// Declare conformity with Preferences interface
+var _ fyne.Preferences = (*preferences)(nil)
+
+// forceImmediateSave writes preferences to file immediately, ignoring the debouncing
+// logic in the change listener. Does nothing if preferences are not backed with a file.
+func (p *preferences) forceImmediateSave() {
+	if !p.needsSaveBeforeExit {
+		return
+	}
+	err := p.save()
+	if err != nil {
+		fyne.LogError("Failed on force saving preferences", err)
+	}
+}
+
+func (p *preferences) resetSavedRecently() {
+	go func() {
+		time.Sleep(time.Millisecond * 100) // writes are not always atomic. 10ms worked, 100 is safer.
+		p.prefLock.Lock()
+		p.savedRecently = false
+		changedDuringSaving := p.changedDuringSaving
+		p.changedDuringSaving = false
+		p.prefLock.Unlock()
+
+		if changedDuringSaving {
+			p.save()
+		}
+	}()
+}
+
+func (p *preferences) save() error {
+	return p.saveToFile(p.storagePath())
+}
+
+func (p *preferences) saveToFile(path string) error {
+	p.prefLock.Lock()
+	p.savedRecently = true
+	p.prefLock.Unlock()
+	defer p.resetSavedRecently()
+	err := os.MkdirAll(filepath.Dir(path), 0700)
+	if err != nil { // this is not an exists error according to docs
+		return err
+	}
+
+	file, err := os.Create(path)
+	if err != nil {
+		if !os.IsExist(err) {
+			return err
+		}
+		file, err = os.Open(path) // #nosec
+		if err != nil {
+			return err
+		}
+	}
+	defer file.Close()
+	encode := json.NewEncoder(file)
+
+	p.InMemoryPreferences.ReadValues(func(values map[string]interface{}) {
+		err = encode.Encode(&values)
+	})
+
+	err2 := file.Sync()
+	if err == nil {
+		err = err2
+	}
+	return err
+}
+
+func (p *preferences) load() {
+	err := p.loadFromFile(p.storagePath())
+	if err != nil {
+		fyne.LogError("Preferences load error:", err)
+	}
+}
+
+func (p *preferences) loadFromFile(path string) (err error) {
+	file, err := os.Open(path) // #nosec
+	if err != nil {
+		if os.IsNotExist(err) {
+			if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
+				return err
+			}
+			return nil
+		}
+		return err
+	}
+	defer func() {
+		if r := file.Close(); r != nil && err == nil {
+			err = r
+		}
+	}()
+	decode := json.NewDecoder(file)
+
+	p.prefLock.Lock()
+	p.loadingInProgress = true
+	p.prefLock.Unlock()
+
+	p.InMemoryPreferences.WriteValues(func(values map[string]interface{}) {
+		err = decode.Decode(&values)
+		if err != nil {
+			return
+		}
+		convertLists(values)
+	})
+
+	p.prefLock.Lock()
+	p.loadingInProgress = false
+	p.prefLock.Unlock()
+
+	return err
+}
+
+func newPreferences(app *fyneApp) *preferences {
+	p := &preferences{}
+	p.app = app
+	p.InMemoryPreferences = internal.NewInMemoryPreferences()
+
+	// don't load or watch if not setup
+	if app.uniqueID == "" && app.Metadata().ID == "" {
+		return p
+	}
+
+	p.needsSaveBeforeExit = true
+	p.AddChangeListener(func() {
+		if p != app.prefs {
+			return
+		}
+		p.prefLock.Lock()
+		shouldIgnoreChange := p.savedRecently || p.loadingInProgress
+		if p.savedRecently && !p.loadingInProgress {
+			p.changedDuringSaving = true
+		}
+		p.prefLock.Unlock()
+
+		if shouldIgnoreChange { // callback after loading file, or too many updates in a row
+			return
+		}
+
+		err := p.save()
+		if err != nil {
+			fyne.LogError("Failed on saving preferences", err)
+		}
+	})
+	p.watch()
+	return p
+}
+
+func convertLists(values map[string]interface{}) {
+	for k, v := range values {
+		if items, ok := v.([]interface{}); ok {
+			if len(items) == 0 {
+				continue
+			}
+
+			switch items[0].(type) {
+			case bool:
+				bools := make([]bool, len(items))
+				for i, item := range items {
+					bools[i] = item.(bool)
+				}
+				values[k] = bools
+			case float64:
+				floats := make([]float64, len(items))
+				for i, item := range items {
+					floats[i] = item.(float64)
+				}
+				values[k] = floats
+			case int:
+				ints := make([]int, len(items))
+				for i, item := range items {
+					ints[i] = item.(int)
+				}
+				values[k] = ints
+			case string:
+				strings := make([]string, len(items))
+				for i, item := range items {
+					strings[i] = item.(string)
+				}
+				values[k] = strings
+			}
+		}
+	}
+}

+ 21 - 0
vendor/fyne.io/fyne/v2/app/preferences_android.go

@@ -0,0 +1,21 @@
+//go:build android
+// +build android
+
+package app
+
+import "path/filepath"
+
+// storagePath returns the location of the settings storage
+func (p *preferences) storagePath() string {
+	// we have no global storage, use app global instead - rootConfigDir looks up in app_mobile_and.go
+	return filepath.Join(p.app.storageRoot(), "preferences.json")
+}
+
+// storageRoot returns the location of the app storage
+func (a *fyneApp) storageRoot() string {
+	return rootConfigDir() // we are in a sandbox, so no app ID added to this path
+}
+
+func (p *preferences) watch() {
+	// no-op on mobile
+}

+ 24 - 0
vendor/fyne.io/fyne/v2/app/preferences_ios.go

@@ -0,0 +1,24 @@
+//go:build ios
+// +build ios
+
+package app
+
+import (
+	"path/filepath"
+)
+import "C"
+
+// storagePath returns the location of the settings storage
+func (p *preferences) storagePath() string {
+	ret := filepath.Join(p.app.storageRoot(), "preferences.json")
+	return ret
+}
+
+// storageRoot returns the location of the app storage
+func (a *fyneApp) storageRoot() string {
+	return rootConfigDir() // we are in a sandbox, so no app ID added to this path
+}
+
+func (p *preferences) watch() {
+	// no-op on mobile
+}

+ 20 - 0
vendor/fyne.io/fyne/v2/app/preferences_mobile.go

@@ -0,0 +1,20 @@
+//go:build mobile
+// +build mobile
+
+package app
+
+import "path/filepath"
+
+// storagePath returns the location of the settings storage
+func (p *preferences) storagePath() string {
+	return filepath.Join(p.app.storageRoot(), "preferences.json")
+}
+
+// storageRoot returns the location of the app storage
+func (a *fyneApp) storageRoot() string {
+	return filepath.Join(rootConfigDir(), a.UniqueID())
+}
+
+func (p *preferences) watch() {
+	// no-op as we are in mobile simulation mode
+}

+ 29 - 0
vendor/fyne.io/fyne/v2/app/preferences_other.go

@@ -0,0 +1,29 @@
+//go:build !ios && !android && !mobile
+// +build !ios,!android,!mobile
+
+package app
+
+import "path/filepath"
+
+// storagePath returns the location of the settings storage
+func (p *preferences) storagePath() string {
+	return filepath.Join(p.app.storageRoot(), "preferences.json")
+}
+
+// storageRoot returns the location of the app storage
+func (a *fyneApp) storageRoot() string {
+	return filepath.Join(rootConfigDir(), a.UniqueID())
+}
+
+func (p *preferences) watch() {
+	watchFile(p.storagePath(), func() {
+		p.prefLock.RLock()
+		shouldIgnoreChange := p.savedRecently
+		p.prefLock.RUnlock()
+		if shouldIgnoreChange {
+			return
+		}
+
+		p.load()
+	})
+}

+ 168 - 0
vendor/fyne.io/fyne/v2/app/settings.go

@@ -0,0 +1,168 @@
+package app
+
+import (
+	"bytes"
+	"os"
+	"path/filepath"
+	"sync"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/theme"
+)
+
+var noAnimations bool // set to true at compile time if no_animations tag is passed
+
+// SettingsSchema is used for loading and storing global settings
+type SettingsSchema struct {
+	// these items are used for global settings load
+	ThemeName         string  `json:"theme"`
+	Scale             float32 `json:"scale"`
+	PrimaryColor      string  `json:"primary_color"`
+	CloudName         string  `json:"cloud_name"`
+	CloudConfig       string  `json:"cloud_config"`
+	DisableAnimations bool    `json:"no_animations"`
+}
+
+// StoragePath returns the location of the settings storage
+func (sc *SettingsSchema) StoragePath() string {
+	return filepath.Join(rootConfigDir(), "settings.json")
+}
+
+// Declare conformity with Settings interface
+var _ fyne.Settings = (*settings)(nil)
+
+type settings struct {
+	propertyLock   sync.RWMutex
+	theme          fyne.Theme
+	themeSpecified bool
+	variant        fyne.ThemeVariant
+
+	changeListeners sync.Map    // map[chan fyne.Settings]bool
+	watcher         interface{} // normally *fsnotify.Watcher or nil - avoid import in this file
+
+	schema SettingsSchema
+}
+
+func (s *settings) BuildType() fyne.BuildType {
+	return buildMode
+}
+
+func (s *settings) PrimaryColor() string {
+	s.propertyLock.RLock()
+	defer s.propertyLock.RUnlock()
+	return s.schema.PrimaryColor
+}
+
+// OverrideTheme allows the settings app to temporarily preview different theme details.
+// Please make sure that you remember the original settings and call this again to revert the change.
+func (s *settings) OverrideTheme(theme fyne.Theme, name string) {
+	s.propertyLock.Lock()
+	defer s.propertyLock.Unlock()
+	s.schema.PrimaryColor = name
+	s.theme = theme
+}
+
+func (s *settings) Theme() fyne.Theme {
+	s.propertyLock.RLock()
+	defer s.propertyLock.RUnlock()
+	return s.theme
+}
+
+func (s *settings) SetTheme(theme fyne.Theme) {
+	s.themeSpecified = true
+	s.applyTheme(theme, s.variant)
+}
+
+func (s *settings) ShowAnimations() bool {
+	return !s.schema.DisableAnimations && !noAnimations
+}
+
+func (s *settings) ThemeVariant() fyne.ThemeVariant {
+	return s.variant
+}
+
+func (s *settings) applyTheme(theme fyne.Theme, variant fyne.ThemeVariant) {
+	s.propertyLock.Lock()
+	defer s.propertyLock.Unlock()
+	s.variant = variant
+	s.theme = theme
+	s.apply()
+}
+
+func (s *settings) Scale() float32 {
+	s.propertyLock.RLock()
+	defer s.propertyLock.RUnlock()
+	if s.schema.Scale < 0.0 {
+		return 1.0 // catching any really old data still using the `-1`  value for "auto" scale
+	}
+	return s.schema.Scale
+}
+
+func (s *settings) AddChangeListener(listener chan fyne.Settings) {
+	s.changeListeners.Store(listener, true) // the boolean is just a dummy value here.
+}
+
+func (s *settings) apply() {
+	s.changeListeners.Range(func(key, _ interface{}) bool {
+		listener := key.(chan fyne.Settings)
+		select {
+		case listener <- s:
+		default:
+			l := listener
+			go func() { l <- s }()
+		}
+		return true
+	})
+}
+
+func (s *settings) fileChanged() {
+	s.load()
+	s.apply()
+}
+
+func (s *settings) loadSystemTheme() fyne.Theme {
+	path := filepath.Join(rootConfigDir(), "theme.json")
+	data, err := fyne.LoadResourceFromPath(path)
+	if err != nil {
+		if !os.IsNotExist(err) {
+			fyne.LogError("Failed to load user theme file: "+path, err)
+		}
+		return theme.DefaultTheme()
+	}
+	if data != nil && data.Content() != nil {
+		th, err := theme.FromJSONReader(bytes.NewReader(data.Content()))
+		if err == nil {
+			return th
+		}
+		fyne.LogError("Failed to parse user theme file: "+path, err)
+	}
+	return theme.DefaultTheme()
+}
+
+func (s *settings) setupTheme() {
+	name := s.schema.ThemeName
+	if env := os.Getenv("FYNE_THEME"); env != "" {
+		name = env
+	}
+
+	variant := defaultVariant()
+	effectiveTheme := s.theme
+	if !s.themeSpecified {
+		effectiveTheme = s.loadSystemTheme()
+	}
+	switch name {
+	case "light":
+		variant = theme.VariantLight
+	case "dark":
+		variant = theme.VariantDark
+	}
+
+	s.applyTheme(effectiveTheme, variant)
+}
+
+func loadSettings() *settings {
+	s := &settings{}
+	s.load()
+
+	return s
+}

+ 75 - 0
vendor/fyne.io/fyne/v2/app/settings_desktop.go

@@ -0,0 +1,75 @@
+//go:build !android && !ios && !mobile && !js && !wasm && !test_web_driver
+// +build !android,!ios,!mobile,!js,!wasm,!test_web_driver
+
+package app
+
+import (
+	"os"
+	"path/filepath"
+
+	"fyne.io/fyne/v2"
+	"github.com/fsnotify/fsnotify"
+)
+
+func watchFileAddTarget(watcher *fsnotify.Watcher, path string) {
+	dir := filepath.Dir(path)
+	ensureDirExists(dir)
+
+	err := watcher.Add(dir)
+	if err != nil {
+		fyne.LogError("Settings watch error:", err)
+	}
+}
+
+func ensureDirExists(dir string) {
+	if stat, err := os.Stat(dir); err == nil && stat.IsDir() {
+		return
+	}
+
+	err := os.MkdirAll(dir, 0700)
+	if err != nil {
+		fyne.LogError("Unable to create settings storage:", err)
+	}
+}
+
+func watchFile(path string, callback func()) *fsnotify.Watcher {
+	watcher, err := fsnotify.NewWatcher()
+	if err != nil {
+		fyne.LogError("Failed to watch settings file:", err)
+		return nil
+	}
+
+	go func() {
+		for event := range watcher.Events {
+			if event.Op.Has(fsnotify.Remove) { // if it was deleted then watch again
+				watcher.Remove(path) // fsnotify returns false positives, see https://github.com/fsnotify/fsnotify/issues/268
+
+				watchFileAddTarget(watcher, path)
+			} else {
+				callback()
+			}
+		}
+
+		err = watcher.Close()
+		if err != nil {
+			fyne.LogError("Settings un-watch error:", err)
+		}
+	}()
+
+	watchFileAddTarget(watcher, path)
+	return watcher
+}
+
+func (s *settings) watchSettings() {
+	s.watcher = watchFile(s.schema.StoragePath(), s.fileChanged)
+
+	watchTheme()
+}
+
+func (s *settings) stopWatching() {
+	if s.watcher == nil {
+		return
+	}
+
+	s.watcher.(*fsnotify.Watcher).Close() // fsnotify returns false positives, see https://github.com/fsnotify/fsnotify/issues/268
+}

+ 35 - 0
vendor/fyne.io/fyne/v2/app/settings_file.go

@@ -0,0 +1,35 @@
+//go:build !js && !wasm && !test_web_driver
+// +build !js,!wasm,!test_web_driver
+
+package app
+
+import (
+	"encoding/json"
+	"io"
+	"os"
+
+	"fyne.io/fyne/v2"
+)
+
+func (s *settings) load() {
+	err := s.loadFromFile(s.schema.StoragePath())
+	if err != nil && err != io.EOF { // we can get an EOF in windows settings writes
+		fyne.LogError("Settings load error:", err)
+	}
+
+	s.setupTheme()
+}
+
+func (s *settings) loadFromFile(path string) error {
+	file, err := os.Open(path) // #nosec
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil
+		}
+		return err
+	}
+	defer file.Close()
+	decode := json.NewDecoder(file)
+
+	return decode.Decode(&s.schema)
+}

+ 24 - 0
vendor/fyne.io/fyne/v2/app/settings_goxjs.go

@@ -0,0 +1,24 @@
+//go:build js || wasm || test_web_driver
+// +build js wasm test_web_driver
+
+package app
+
+// TODO: #2734
+
+func (s *settings) load() {
+	s.setupTheme()
+	s.schema.Scale = 1
+}
+
+func (s *settings) loadFromFile(path string) error {
+	return nil
+}
+
+func watchFile(path string, callback func()) {
+}
+
+func (s *settings) watchSettings() {
+}
+
+func (s *settings) stopWatching() {
+}

+ 12 - 0
vendor/fyne.io/fyne/v2/app/settings_mobile.go

@@ -0,0 +1,12 @@
+//go:build android || ios || mobile
+// +build android ios mobile
+
+package app
+
+func (s *settings) watchSettings() {
+	// no-op on mobile
+}
+
+func (s *settings) stopWatching() {
+	// no-op on mobile
+}

+ 8 - 0
vendor/fyne.io/fyne/v2/app/settings_noanimation.go

@@ -0,0 +1,8 @@
+//go:build no_animations
+// +build no_animations
+
+package app
+
+func init() {
+	noAnimations = true
+}

+ 27 - 0
vendor/fyne.io/fyne/v2/app/storage.go

@@ -0,0 +1,27 @@
+package app
+
+import (
+	"os"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/internal"
+	"fyne.io/fyne/v2/storage"
+)
+
+type store struct {
+	*internal.Docs
+	a *fyneApp
+}
+
+func (s *store) RootURI() fyne.URI {
+	if s.a.UniqueID() == "" {
+		fyne.LogError("Storage API requires a unique ID, use app.NewWithID()", nil)
+		return storage.NewFileURI(os.TempDir())
+	}
+
+	return storage.NewFileURI(s.a.storageRoot())
+}
+
+func (s *store) docRootURI() (fyne.URI, error) {
+	return storage.Child(s.RootURI(), "Documents")
+}

+ 58 - 0
vendor/fyne.io/fyne/v2/canvas.go

@@ -0,0 +1,58 @@
+package fyne
+
+import "image"
+
+// Canvas defines a graphical canvas to which a CanvasObject or Container can be added.
+// Each canvas has a scale which is automatically applied during the render process.
+type Canvas interface {
+	Content() CanvasObject
+	SetContent(CanvasObject)
+
+	Refresh(CanvasObject)
+
+	// Focus makes the provided item focused.
+	// The item has to be added to the contents of the canvas before calling this.
+	Focus(Focusable)
+	// FocusNext focuses the next focusable item.
+	// If no item is currently focused, the first focusable item is focused.
+	// If the last focusable item is currently focused, the first focusable item is focused.
+	//
+	// Since: 2.0
+	FocusNext()
+	// FocusPrevious focuses the previous focusable item.
+	// If no item is currently focused, the last focusable item is focused.
+	// If the first focusable item is currently focused, the last focusable item is focused.
+	//
+	// Since: 2.0
+	FocusPrevious()
+	Unfocus()
+	Focused() Focusable
+
+	// Size returns the current size of this canvas
+	Size() Size
+	// Scale returns the current scale (multiplication factor) this canvas uses to render
+	// The pixel size of a CanvasObject can be found by multiplying by this value.
+	Scale() float32
+
+	// Overlays returns the overlay stack.
+	Overlays() OverlayStack
+
+	OnTypedRune() func(rune)
+	SetOnTypedRune(func(rune))
+	OnTypedKey() func(*KeyEvent)
+	SetOnTypedKey(func(*KeyEvent))
+	AddShortcut(shortcut Shortcut, handler func(shortcut Shortcut))
+	RemoveShortcut(shortcut Shortcut)
+
+	Capture() image.Image
+
+	// PixelCoordinateForPosition returns the x and y pixel coordinate for a given position on this canvas.
+	// This can be used to find absolute pixel positions or pixel offsets relative to an object top left.
+	PixelCoordinateForPosition(Position) (int, int)
+
+	// InteractiveArea returns the position and size of the central interactive area.
+	// Operating system elements may overlap the portions outside this area and widgets should avoid being outside.
+	//
+	// Since: 1.4
+	InteractiveArea() (Position, Size)
+}

+ 86 - 0
vendor/fyne.io/fyne/v2/canvas/animation.go

@@ -0,0 +1,86 @@
+package canvas
+
+import (
+	"image/color"
+	"time"
+
+	"fyne.io/fyne/v2"
+)
+
+const (
+	// DurationStandard is the time a standard interface animation will run.
+	//
+	// Since: 2.0
+	DurationStandard = time.Millisecond * 300
+	// DurationShort is the time a subtle or small transition should use.
+	//
+	// Since: 2.0
+	DurationShort = time.Millisecond * 150
+)
+
+// NewColorRGBAAnimation sets up a new animation that will transition from the start to stop Color over
+// the specified Duration. The colour transition will move linearly through the RGB colour space.
+// The content of fn should apply the color values to an object and refresh it.
+// You should call Start() on the returned animation to start it.
+//
+// Since: 2.0
+func NewColorRGBAAnimation(start, stop color.Color, d time.Duration, fn func(color.Color)) *fyne.Animation {
+	r1, g1, b1, a1 := start.RGBA()
+	r2, g2, b2, a2 := stop.RGBA()
+
+	rStart := int(r1 >> 8)
+	gStart := int(g1 >> 8)
+	bStart := int(b1 >> 8)
+	aStart := int(a1 >> 8)
+	rDelta := float32(int(r2>>8) - rStart)
+	gDelta := float32(int(g2>>8) - gStart)
+	bDelta := float32(int(b2>>8) - bStart)
+	aDelta := float32(int(a2>>8) - aStart)
+
+	return &fyne.Animation{
+		Duration: d,
+		Tick: func(done float32) {
+			fn(color.RGBA{R: scaleChannel(rStart, rDelta, done), G: scaleChannel(gStart, gDelta, done),
+				B: scaleChannel(bStart, bDelta, done), A: scaleChannel(aStart, aDelta, done)})
+		}}
+}
+
+// NewPositionAnimation sets up a new animation that will transition from the start to stop Position over
+// the specified Duration. The content of fn should apply the position value to an object for the change
+// to be visible. You should call Start() on the returned animation to start it.
+//
+// Since: 2.0
+func NewPositionAnimation(start, stop fyne.Position, d time.Duration, fn func(fyne.Position)) *fyne.Animation {
+	xDelta := float32(stop.X - start.X)
+	yDelta := float32(stop.Y - start.Y)
+
+	return &fyne.Animation{
+		Duration: d,
+		Tick: func(done float32) {
+			fn(fyne.NewPos(scaleVal(start.X, xDelta, done), scaleVal(start.Y, yDelta, done)))
+		}}
+}
+
+// NewSizeAnimation sets up a new animation that will transition from the start to stop Size over
+// the specified Duration. The content of fn should apply the size value to an object for the change
+// to be visible. You should call Start() on the returned animation to start it.
+//
+// Since: 2.0
+func NewSizeAnimation(start, stop fyne.Size, d time.Duration, fn func(fyne.Size)) *fyne.Animation {
+	widthDelta := float32(stop.Width - start.Width)
+	heightDelta := float32(stop.Height - start.Height)
+
+	return &fyne.Animation{
+		Duration: d,
+		Tick: func(done float32) {
+			fn(fyne.NewSize(scaleVal(start.Width, widthDelta, done), scaleVal(start.Height, heightDelta, done)))
+		}}
+}
+
+func scaleChannel(start int, diff, done float32) uint8 {
+	return uint8(start + int(diff*done))
+}
+
+func scaleVal(start float32, delta, done float32) float32 {
+	return start + delta*done
+}

+ 100 - 0
vendor/fyne.io/fyne/v2/canvas/base.go

@@ -0,0 +1,100 @@
+// Package canvas contains all of the primitive CanvasObjects that make up a Fyne GUI.
+//
+// The types implemented in this package are used as building blocks in order
+// to build higher order functionality. These types are designed to be
+// non-interactive, by design. If additional functionality is required,
+// it's usually a sign that this type should be used as part of a custom
+// widget.
+package canvas // import "fyne.io/fyne/v2/canvas"
+
+import (
+	"sync"
+
+	"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
+
+	min fyne.Size // The minimum size this object can be
+
+	propertyLock sync.RWMutex
+}
+
+// Hide will set this object to not be visible.
+func (o *baseObject) Hide() {
+	o.propertyLock.Lock()
+	defer o.propertyLock.Unlock()
+
+	o.Hidden = true
+}
+
+// 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)
+	}
+
+	return o.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
+}
+
+// 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()
+
+	return o.position
+}
+
+// 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
+}
+
+// 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
+}
+
+// Show will set this object to be visible.
+func (o *baseObject) Show() {
+	o.propertyLock.Lock()
+	defer o.propertyLock.Unlock()
+
+	o.Hidden = false
+}
+
+// Size returns the current size of this canvas object.
+func (o *baseObject) Size() fyne.Size {
+	o.propertyLock.RLock()
+	defer o.propertyLock.RUnlock()
+
+	return o.size
+}
+
+// Visible returns true if this object is visible, false otherwise.
+func (o *baseObject) Visible() bool {
+	o.propertyLock.RLock()
+	defer o.propertyLock.RUnlock()
+
+	return !o.Hidden
+}

+ 29 - 0
vendor/fyne.io/fyne/v2/canvas/canvas.go

@@ -0,0 +1,29 @@
+package canvas
+
+import "fyne.io/fyne/v2"
+
+// Refresh instructs the containing canvas to refresh the specified obj.
+func Refresh(obj fyne.CanvasObject) {
+	if fyne.CurrentApp() == nil || fyne.CurrentApp().Driver() == nil {
+		return
+	}
+
+	c := fyne.CurrentApp().Driver().CanvasForObject(obj)
+	if c != nil {
+		c.Refresh(obj)
+	}
+}
+
+// repaint instructs the containing canvas to redraw, even if nothing changed.
+func repaint(obj fyne.CanvasObject) {
+	if fyne.CurrentApp() == nil || fyne.CurrentApp().Driver() == nil {
+		return
+	}
+
+	c := fyne.CurrentApp().Driver().CanvasForObject(obj)
+	if c != nil {
+		if paint, ok := c.(interface{ SetDirty() }); ok {
+			paint.SetDirty()
+		}
+	}
+}

+ 90 - 0
vendor/fyne.io/fyne/v2/canvas/circle.go

@@ -0,0 +1,90 @@
+package canvas
+
+import (
+	"image/color"
+	"math"
+
+	"fyne.io/fyne/v2"
+)
+
+// Declare conformity with CanvasObject interface
+var _ fyne.CanvasObject = (*Circle)(nil)
+
+// Circle describes a colored circle primitive in a Fyne canvas
+type Circle struct {
+	Position1 fyne.Position // The current top-left position of the Circle
+	Position2 fyne.Position // The current bottomright position of the Circle
+	Hidden    bool          // Is this circle currently hidden
+
+	FillColor   color.Color // The circle fill color
+	StrokeColor color.Color // The circle stroke color
+	StrokeWidth float32     // The stroke width of the circle
+}
+
+// NewCircle returns a new Circle instance
+func NewCircle(color color.Color) *Circle {
+	return &Circle{
+		FillColor: color,
+	}
+}
+
+// Hide will set this circle to not be visible
+func (c *Circle) Hide() {
+	c.Hidden = true
+
+	repaint(c)
+}
+
+// MinSize for a Circle simply returns Size{1, 1} as there is no
+// explicit content
+func (c *Circle) MinSize() fyne.Size {
+	return fyne.NewSize(1, 1)
+}
+
+// Move the circle object to a new position, relative to its parent / canvas
+func (c *Circle) Move(pos fyne.Position) {
+	size := c.Size()
+	c.Position1 = pos
+	c.Position2 = fyne.NewPos(c.Position1.X+size.Width, c.Position1.Y+size.Height)
+	repaint(c)
+}
+
+// Position gets the current top-left position of this circle object, relative to its parent / canvas
+func (c *Circle) Position() fyne.Position {
+	return c.Position1
+}
+
+// Refresh causes this object to be redrawn with its configured state.
+func (c *Circle) Refresh() {
+	Refresh(c)
+}
+
+// Resize sets a new bottom-right position for the circle object
+// If it has a stroke width this will cause it to Refresh.
+func (c *Circle) Resize(size fyne.Size) {
+	if size == c.Size() {
+		return
+	}
+
+	c.Position2 = fyne.NewPos(c.Position1.X+size.Width, c.Position1.Y+size.Height)
+
+	Refresh(c)
+}
+
+// Show will set this circle to be visible
+func (c *Circle) Show() {
+	c.Hidden = false
+
+	c.Refresh()
+}
+
+// Size returns the current size of bounding box for this circle object
+func (c *Circle) Size() fyne.Size {
+	return fyne.NewSize(float32(math.Abs(float64(c.Position2.X)-float64(c.Position1.X))),
+		float32(math.Abs(float64(c.Position2.Y)-float64(c.Position1.Y))))
+}
+
+// Visible returns true if this circle is visible, false otherwise
+func (c *Circle) Visible() bool {
+	return !c.Hidden
+}

+ 212 - 0
vendor/fyne.io/fyne/v2/canvas/gradient.go

@@ -0,0 +1,212 @@
+package canvas
+
+import (
+	"image"
+	"image/color"
+	"math"
+
+	"fyne.io/fyne/v2"
+)
+
+// LinearGradient defines a Gradient travelling straight at a given angle.
+// The only supported values for the angle are `0.0` (vertical) and `90.0` (horizontal), currently.
+type LinearGradient struct {
+	baseObject
+
+	StartColor color.Color // The beginning color of the gradient
+	EndColor   color.Color // The end color of the gradient
+	Angle      float64     // The angle of the gradient (0/180 for vertical; 90/270 for horizontal)
+}
+
+// Generate calculates an image of the gradient with the specified width and height.
+func (g *LinearGradient) Generate(iw, ih int) image.Image {
+	w, h := float64(iw), float64(ih)
+	var generator func(x, y float64) float64
+	switch g.Angle {
+	case 90: // horizontal flipped
+		generator = func(x, _ float64) float64 {
+			return (w - x) / w
+		}
+	case 270: // horizontal
+		generator = func(x, _ float64) float64 {
+			return x / w
+		}
+	case 45: // diagonal negative flipped
+		generator = func(x, y float64) float64 {
+			return math.Abs((w - x + y) / (w + h)) // ((w+h)-(x+h-y)) / (w+h)
+		}
+	case 225: // diagonal negative
+		generator = func(x, y float64) float64 {
+			return math.Abs((x + h - y) / (w + h))
+		}
+	case 135: // diagonal positive flipped
+		generator = func(x, y float64) float64 {
+			return math.Abs((w + h - (x + y)) / (w + h))
+		}
+	case 315: // diagonal positive
+		generator = func(x, y float64) float64 {
+			return math.Abs((x + y) / (w + h))
+		}
+	case 180: // vertical flipped
+		generator = func(_, y float64) float64 {
+			return (h - y) / h
+		}
+	default: // vertical
+		generator = func(_, y float64) float64 {
+			return y / h
+		}
+	}
+	return computeGradient(generator, iw, ih, g.StartColor, g.EndColor)
+}
+
+// Hide will set this gradient to not be visible
+func (g *LinearGradient) Hide() {
+	g.baseObject.Hide()
+
+	repaint(g)
+}
+
+// Move the gradient to a new position, relative to its parent / canvas
+func (g *LinearGradient) Move(pos fyne.Position) {
+	g.baseObject.Move(pos)
+
+	repaint(g)
+}
+
+// Refresh causes this gradient to be redrawn with its configured state.
+func (g *LinearGradient) Refresh() {
+	Refresh(g)
+}
+
+// RadialGradient defines a Gradient travelling radially from a center point outward.
+type RadialGradient struct {
+	baseObject
+
+	StartColor color.Color // The beginning color of the gradient
+	EndColor   color.Color // The end color of the gradient
+	// The offset of the center for generation of the gradient.
+	// This is not a DP measure but relates to the width/height.
+	// A value of 0.5 would move the center by the half width/height.
+	CenterOffsetX, CenterOffsetY float64
+}
+
+// Generate calculates an image of the gradient with the specified width and height.
+func (g *RadialGradient) Generate(iw, ih int) image.Image {
+	w, h := float64(iw), float64(ih)
+	// define center plus offset
+	centerX := w/2 + w*g.CenterOffsetX
+	centerY := h/2 + h*g.CenterOffsetY
+
+	// handle negative offsets
+	var a, b float64
+	if g.CenterOffsetX < 0 {
+		a = w - centerX
+	} else {
+		a = centerX
+	}
+	if g.CenterOffsetY < 0 {
+		b = h - centerY
+	} else {
+		b = centerY
+	}
+
+	generator := func(x, y float64) float64 {
+		// calculate distance from center for gradient multiplier
+		dx, dy := centerX-x, centerY-y
+		da := math.Sqrt(dx*dx + dy*dy*a*a/b/b)
+		if da > a {
+			return 1
+		}
+		return da / a
+	}
+	return computeGradient(generator, iw, ih, g.StartColor, g.EndColor)
+}
+
+// Hide will set this gradient to not be visible
+func (g *RadialGradient) Hide() {
+	g.baseObject.Hide()
+
+	repaint(g)
+}
+
+// Move the gradient to a new position, relative to its parent / canvas
+func (g *RadialGradient) Move(pos fyne.Position) {
+	g.baseObject.Move(pos)
+
+	repaint(g)
+}
+
+// Refresh causes this gradient to be redrawn with its configured state.
+func (g *RadialGradient) Refresh() {
+	Refresh(g)
+}
+
+func calculatePixel(d float64, startColor, endColor color.Color) color.Color {
+	// fetch RGBA values
+	aR, aG, aB, aA := startColor.RGBA()
+	bR, bG, bB, bA := endColor.RGBA()
+
+	// Get difference
+	dR := float64(bR) - float64(aR)
+	dG := float64(bG) - float64(aG)
+	dB := float64(bB) - float64(aB)
+	dA := float64(bA) - float64(aA)
+
+	// Apply gradations
+	pixel := &color.RGBA64{
+		R: uint16(float64(aR) + d*dR),
+		B: uint16(float64(aB) + d*dB),
+		G: uint16(float64(aG) + d*dG),
+		A: uint16(float64(aA) + d*dA),
+	}
+
+	return pixel
+}
+
+func computeGradient(generator func(x, y float64) float64, w, h int, startColor, endColor color.Color) image.Image {
+	img := image.NewNRGBA(image.Rect(0, 0, w, h))
+
+	if startColor == nil && endColor == nil {
+		return img
+	} else if startColor == nil {
+		startColor = color.Transparent
+	} else if endColor == nil {
+		endColor = color.Transparent
+	}
+
+	for x := 0; x < w; x++ {
+		for y := 0; y < h; y++ {
+			distance := generator(float64(x)+0.5, float64(y)+0.5)
+			img.Set(x, y, calculatePixel(distance, startColor, endColor))
+		}
+	}
+	return img
+}
+
+// NewHorizontalGradient creates a new horizontally travelling linear gradient.
+// The start color will be at the left of the gradient and the end color will be at the right.
+func NewHorizontalGradient(start, end color.Color) *LinearGradient {
+	g := &LinearGradient{StartColor: start, EndColor: end}
+	g.Angle = 270
+	return g
+}
+
+// NewLinearGradient creates a linear gradient at the specified angle.
+// The angle parameter is the degree angle along which the gradient is calculated.
+// A NewHorizontalGradient uses 270 degrees and NewVerticalGradient is 0 degrees.
+func NewLinearGradient(start, end color.Color, angle float64) *LinearGradient {
+	g := &LinearGradient{StartColor: start, EndColor: end}
+	g.Angle = angle
+	return g
+}
+
+// NewRadialGradient creates a new radial gradient.
+func NewRadialGradient(start, end color.Color) *RadialGradient {
+	return &RadialGradient{StartColor: start, EndColor: end}
+}
+
+// NewVerticalGradient creates a new vertically travelling linear gradient.
+// The start color will be at the top of the gradient and the end color will be at the bottom.
+func NewVerticalGradient(start color.Color, end color.Color) *LinearGradient {
+	return &LinearGradient{StartColor: start, EndColor: end}
+}

+ 362 - 0
vendor/fyne.io/fyne/v2/canvas/image.go

@@ -0,0 +1,362 @@
+package canvas
+
+import (
+	"bytes"
+	"errors"
+	"image"
+	_ "image/jpeg" // avoid users having to import when using image widget
+	_ "image/png"  // avoid the same for PNG images
+	"io"
+	"os"
+	"path/filepath"
+	"sync"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/internal/cache"
+	"fyne.io/fyne/v2/internal/scale"
+	"fyne.io/fyne/v2/internal/svg"
+	"fyne.io/fyne/v2/storage"
+)
+
+// ImageFill defines the different type of ways an image can stretch to fill its space.
+type ImageFill int
+
+const (
+	// ImageFillStretch will scale the image to match the Size() values.
+	// This is the default and does not maintain aspect ratio.
+	ImageFillStretch ImageFill = iota
+	// ImageFillContain makes the image fit within the object Size(),
+	// centrally and maintaining aspect ratio.
+	// There may be transparent sections top and bottom or left and right.
+	ImageFillContain // (Fit)
+	// ImageFillOriginal ensures that the container grows to the pixel dimensions
+	// required to fit the original image. The aspect of the image will be maintained so,
+	// as with ImageFillContain there may be transparent areas around the image.
+	// Note that the minSize may be smaller than the image dimensions if scale > 1.
+	ImageFillOriginal
+)
+
+// ImageScale defines the different scaling filters used to scaling images
+type ImageScale int32
+
+const (
+	// ImageScaleSmooth will scale the image using ApproxBiLinear filter (or GL equivalent)
+	ImageScaleSmooth ImageScale = iota
+	// ImageScalePixels will scale the image using NearestNeighbor filter (or GL equivalent)
+	ImageScalePixels
+	// ImageScaleFastest will scale the image using hardware GPU if available
+	//
+	// Since: 2.0
+	ImageScaleFastest
+)
+
+// Declare conformity with CanvasObject interface
+var _ fyne.CanvasObject = (*Image)(nil)
+
+// Image describes a drawable image area that can render in a Fyne canvas
+// The image may be a vector or a bitmap representation, it will fill the area.
+// The fill mode can be changed by setting FillMode to a different ImageFill.
+type Image struct {
+	baseObject
+
+	aspect float32
+	icon   *svg.Decoder
+	isSVG  bool
+	lock   sync.Mutex
+
+	// one of the following sources will provide our image data
+	File     string        // Load the image from a file
+	Resource fyne.Resource // Load the image from an in-memory resource
+	Image    image.Image   // Specify a loaded image to use in this canvas object
+
+	Translucency float64    // Set a translucency value > 0.0 to fade the image
+	FillMode     ImageFill  // Specify how the image should expand to fill or fit the available space
+	ScaleMode    ImageScale // Specify the type of scaling interpolation applied to the image
+}
+
+// Alpha is a convenience function that returns the alpha value for an image
+// based on its Translucency value. The result is 1.0 - Translucency.
+func (i *Image) Alpha() float64 {
+	return 1.0 - i.Translucency
+}
+
+// Aspect will return the original content aspect after it was last refreshed.
+//
+// Since: 2.4
+func (i *Image) Aspect() float32 {
+	if i.aspect == 0 {
+		i.Refresh()
+	}
+	return i.aspect
+}
+
+// Hide will set this image to not be visible
+func (i *Image) Hide() {
+	i.baseObject.Hide()
+
+	repaint(i)
+}
+
+// MinSize returns the specified minimum size, if set, or {1, 1} otherwise.
+func (i *Image) MinSize() fyne.Size {
+	if i.Image == nil || i.aspect == 0 {
+		i.Refresh()
+	}
+	return i.baseObject.MinSize()
+}
+
+// Move the image object to a new position, relative to its parent top, left corner.
+func (i *Image) Move(pos fyne.Position) {
+	i.baseObject.Move(pos)
+
+	repaint(i)
+}
+
+// Refresh causes this image to be redrawn with its configured state.
+func (i *Image) Refresh() {
+	i.lock.Lock()
+	defer i.lock.Unlock()
+
+	rc, err := i.updateReader()
+	if err != nil {
+		fyne.LogError("Failed to load image", err)
+		return
+	}
+	if rc != nil {
+		rcMem := rc
+		defer rcMem.Close()
+	}
+
+	if i.File != "" || i.Resource != nil || i.Image != nil {
+		r, err := i.updateAspectAndMinSize(rc)
+		if err != nil {
+			fyne.LogError("Failed to load image", err)
+			return
+		}
+		rc = io.NopCloser(r)
+	}
+
+	if i.File != "" || i.Resource != nil {
+		size := i.Size()
+		width := size.Width
+		height := size.Height
+
+		if width == 0 || height == 0 {
+			return
+		}
+
+		if i.isSVG {
+			tex, err := i.renderSVG(width, height)
+			if err != nil {
+				fyne.LogError("Failed to render SVG", err)
+				return
+			}
+			i.Image = tex
+		} else {
+			if rc == nil {
+				return
+			}
+
+			img, _, err := image.Decode(rc)
+			if err != nil {
+				fyne.LogError("Failed to render image", err)
+				return
+			}
+			i.Image = img
+		}
+	}
+
+	Refresh(i)
+}
+
+// Resize on an image will scale the content or reposition it according to FillMode.
+// It will normally cause a Refresh to ensure the pixels are recalculated.
+func (i *Image) Resize(s fyne.Size) {
+	if s == i.Size() {
+		return
+	}
+	i.baseObject.Resize(s)
+	if i.FillMode == ImageFillOriginal && i.size.Height > 2 { // we can just ask for a GPU redraw to align
+		Refresh(i)
+		return
+	}
+
+	i.baseObject.Resize(s)
+	if i.isSVG || i.Image == nil {
+		i.Refresh() // we need to rasterise at the new size
+	} else {
+		Refresh(i) // just re-size using GPU scaling
+	}
+}
+
+// NewImageFromFile creates a new image from a local file.
+// Images returned from this method will scale to fit the canvas object.
+// The method for scaling can be set using the Fill field.
+func NewImageFromFile(file string) *Image {
+	return &Image{File: file}
+}
+
+// NewImageFromURI creates a new image from named resource.
+// File URIs will read the file path and other schemes will download the data into a resource.
+// HTTP and HTTPs URIs will use the GET method by default to request the resource.
+// Images returned from this method will scale to fit the canvas object.
+// The method for scaling can be set using the Fill field.
+//
+// Since: 2.0
+func NewImageFromURI(uri fyne.URI) *Image {
+	if uri.Scheme() == "file" && len(uri.String()) > 7 {
+		return NewImageFromFile(uri.Path())
+	}
+
+	var read io.ReadCloser
+
+	read, err := storage.Reader(uri) // attempt unknown / http file type
+	if err != nil {
+		fyne.LogError("Failed to open image URI", err)
+		return &Image{}
+	}
+
+	defer read.Close()
+	return NewImageFromReader(read, filepath.Base(uri.String()))
+}
+
+// NewImageFromReader creates a new image from a data stream.
+// The name parameter is required to uniquely identify this image (for caching etc.).
+// If the image in this io.Reader is an SVG, the name should end ".svg".
+// Images returned from this method will scale to fit the canvas object.
+// The method for scaling can be set using the Fill field.
+//
+// Since: 2.0
+func NewImageFromReader(read io.Reader, name string) *Image {
+	data, err := io.ReadAll(read)
+	if err != nil {
+		fyne.LogError("Unable to read image data", err)
+		return nil
+	}
+
+	res := &fyne.StaticResource{
+		StaticName:    name,
+		StaticContent: data,
+	}
+
+	return NewImageFromResource(res)
+}
+
+// NewImageFromResource creates a new image by loading the specified resource.
+// Images returned from this method will scale to fit the canvas object.
+// The method for scaling can be set using the Fill field.
+func NewImageFromResource(res fyne.Resource) *Image {
+	return &Image{Resource: res}
+}
+
+// NewImageFromImage returns a new Image instance that is rendered from the Go
+// image.Image passed in.
+// Images returned from this method will scale to fit the canvas object.
+// The method for scaling can be set using the Fill field.
+func NewImageFromImage(img image.Image) *Image {
+	return &Image{Image: img}
+}
+
+func (i *Image) name() string {
+	if i.Resource != nil {
+		return i.Resource.Name()
+	} else if i.File != "" {
+		return i.File
+	}
+	return ""
+}
+
+func (i *Image) updateReader() (io.ReadCloser, error) {
+	i.isSVG = false
+	if i.Resource != nil {
+		i.isSVG = svg.IsResourceSVG(i.Resource)
+		return io.NopCloser(bytes.NewReader(i.Resource.Content())), nil
+	} else if i.File != "" {
+		var err error
+
+		fd, err := os.Open(i.File)
+		if err != nil {
+			return nil, err
+		}
+		i.isSVG = svg.IsFileSVG(i.File)
+		return fd, nil
+	}
+	return nil, nil
+}
+
+func (i *Image) updateAspectAndMinSize(reader io.Reader) (io.Reader, error) {
+	var pixWidth, pixHeight int
+
+	if reader != nil {
+		r, width, height, aspect, err := i.imageDetailsFromReader(reader)
+		if err != nil {
+			return nil, err
+		}
+		reader = r
+		i.aspect = aspect
+		pixWidth, pixHeight = width, height
+	} else if i.Image != nil {
+		original := i.Image.Bounds().Size()
+		i.aspect = float32(original.X) / float32(original.Y)
+		pixWidth, pixHeight = original.X, original.Y
+	} else {
+		return nil, errors.New("no matching image source")
+	}
+
+	if i.FillMode == ImageFillOriginal {
+		i.SetMinSize(scale.ToFyneSize(i, pixWidth, pixHeight))
+	}
+	return reader, nil
+}
+
+func (i *Image) imageDetailsFromReader(source io.Reader) (reader io.Reader, width, height int, aspect float32, err error) {
+	if source == nil {
+		return nil, 0, 0, 0, errors.New("no matching reading reader")
+	}
+
+	if i.isSVG {
+		var err error
+
+		i.icon, err = svg.NewDecoder(source)
+		if err != nil {
+			return nil, 0, 0, 0, err
+		}
+		config := i.icon.Config()
+		width, height = config.Width, config.Height
+		aspect = config.Aspect
+	} else {
+		var buf bytes.Buffer
+		tee := io.TeeReader(source, &buf)
+		reader = io.MultiReader(&buf, source)
+
+		config, _, err := image.DecodeConfig(tee)
+		if err != nil {
+			return nil, 0, 0, 0, err
+		}
+		width, height = config.Width, config.Height
+		aspect = float32(width) / float32(height)
+	}
+	return
+}
+
+func (i *Image) renderSVG(width, height float32) (image.Image, error) {
+	c := fyne.CurrentApp().Driver().CanvasForObject(i)
+	screenWidth, screenHeight := int(width), int(height)
+	if c != nil {
+		// We want real output pixel count not just the screen coordinate space (i.e. macOS Retina)
+		screenWidth, screenHeight = c.PixelCoordinateForPosition(fyne.Position{X: width, Y: height})
+	}
+
+	tex := cache.GetSvg(i.name(), screenWidth, screenHeight)
+	if tex != nil {
+		return tex, nil
+	}
+
+	var err error
+	tex, err = i.icon.Draw(screenWidth, screenHeight)
+	if err != nil {
+		return nil, err
+	}
+	cache.SetSvg(i.name(), tex, screenWidth, screenHeight)
+	return tex, nil
+}

+ 102 - 0
vendor/fyne.io/fyne/v2/canvas/line.go

@@ -0,0 +1,102 @@
+package canvas
+
+import (
+	"image/color"
+	"math"
+
+	"fyne.io/fyne/v2"
+)
+
+// Declare conformity with CanvasObject interface
+var _ fyne.CanvasObject = (*Line)(nil)
+
+// Line describes a colored line primitive in a Fyne canvas.
+// Lines are special as they can have a negative width or height to indicate
+// an inverse slope (i.e. slope up vs down).
+type Line struct {
+	Position1 fyne.Position // The current top-left position of the Line
+	Position2 fyne.Position // The current bottom-right position of the Line
+	Hidden    bool          // Is this Line currently hidden
+
+	StrokeColor color.Color // The line stroke color
+	StrokeWidth float32     // The stroke width of the line
+}
+
+// Size returns the current size of bounding box for this line object
+func (l *Line) Size() fyne.Size {
+	return fyne.NewSize(float32(math.Abs(float64(l.Position2.X)-float64(l.Position1.X))),
+		float32(math.Abs(float64(l.Position2.Y)-float64(l.Position1.Y))))
+}
+
+// Resize sets a new bottom-right position for the line object, then it will then be refreshed.
+func (l *Line) Resize(size fyne.Size) {
+	if size == l.Size() {
+		return
+	}
+
+	if l.Position1.X <= l.Position2.X {
+		l.Position2.X = l.Position1.X + size.Width
+	} else {
+		l.Position1.X = l.Position2.X + size.Width
+	}
+	if l.Position1.Y <= l.Position2.Y {
+		l.Position2.Y = l.Position1.Y + size.Height
+	} else {
+		l.Position1.Y = l.Position2.Y + size.Height
+	}
+	Refresh(l)
+}
+
+// Position gets the current top-left position of this line object, relative to its parent / canvas
+func (l *Line) Position() fyne.Position {
+	return fyne.NewPos(fyne.Min(l.Position1.X, l.Position2.X), fyne.Min(l.Position1.Y, l.Position2.Y))
+}
+
+// Move the line object to a new position, relative to its parent / canvas
+func (l *Line) Move(pos fyne.Position) {
+	oldPos := l.Position()
+	deltaX := pos.X - oldPos.X
+	deltaY := pos.Y - oldPos.Y
+
+	l.Position1 = l.Position1.Add(fyne.NewPos(deltaX, deltaY))
+	l.Position2 = l.Position2.Add(fyne.NewPos(deltaX, deltaY))
+	repaint(l)
+}
+
+// MinSize for a Line simply returns Size{1, 1} as there is no
+// explicit content
+func (l *Line) MinSize() fyne.Size {
+	return fyne.NewSize(1, 1)
+}
+
+// Visible returns true if this line// Show will set this circle to be visible is visible, false otherwise
+func (l *Line) Visible() bool {
+	return !l.Hidden
+}
+
+// Show will set this line to be visible
+func (l *Line) Show() {
+	l.Hidden = false
+
+	l.Refresh()
+}
+
+// Hide will set this line to not be visible
+func (l *Line) Hide() {
+	l.Hidden = true
+
+	repaint(l)
+}
+
+// Refresh causes this line to be redrawn with its configured state.
+func (l *Line) Refresh() {
+	Refresh(l)
+}
+
+// NewLine returns a new Line instance
+func NewLine(color color.Color) *Line {
+	return &Line{
+		StrokeColor: color,
+		StrokeWidth: 1,
+	}
+}

+ 196 - 0
vendor/fyne.io/fyne/v2/canvas/raster.go

@@ -0,0 +1,196 @@
+package canvas
+
+import (
+	"image"
+	"image/color"
+	"image/draw"
+
+	"fyne.io/fyne/v2"
+)
+
+// Declare conformity with CanvasObject interface
+var _ fyne.CanvasObject = (*Raster)(nil)
+
+// Raster describes a raster image area that can render in a Fyne canvas
+type Raster struct {
+	baseObject
+
+	// Render the raster image from code
+	Generator func(w, h int) image.Image
+
+	// Set a translucency value > 0.0 to fade the raster
+	Translucency float64
+	// Specify the type of scaling interpolation applied to the raster if it is not full-size
+	// Since: 1.4.1
+	ScaleMode ImageScale
+}
+
+// Alpha is a convenience function that returns the alpha value for a raster
+// based on its Translucency value. The result is 1.0 - Translucency.
+func (r *Raster) Alpha() float64 {
+	return 1.0 - r.Translucency
+}
+
+// Hide will set this raster to not be visible
+func (r *Raster) Hide() {
+	r.baseObject.Hide()
+
+	repaint(r)
+}
+
+// Move the raster to a new position, relative to its parent / canvas
+func (r *Raster) Move(pos fyne.Position) {
+	r.baseObject.Move(pos)
+
+	repaint(r)
+}
+
+// Resize on a raster image causes the new size to be set and then calls Refresh.
+// This causes the underlying data to be recalculated and a new output to be drawn.
+func (r *Raster) Resize(s fyne.Size) {
+	if s == r.Size() {
+		return
+	}
+
+	r.baseObject.Resize(s)
+	Refresh(r)
+}
+
+// Refresh causes this raster to be redrawn with its configured state.
+func (r *Raster) Refresh() {
+	Refresh(r)
+}
+
+// NewRaster returns a new Image instance that is rendered dynamically using
+// the specified generate function.
+// Images returned from this method should draw dynamically to fill the width
+// and height parameters passed to pixelColor.
+func NewRaster(generate func(w, h int) image.Image) *Raster {
+	return &Raster{Generator: generate}
+}
+
+type pixelRaster struct {
+	r *Raster
+
+	img draw.Image
+}
+
+// NewRasterWithPixels returns a new Image instance that is rendered dynamically
+// by iterating over the specified pixelColor function for each x, y pixel.
+// Images returned from this method should draw dynamically to fill the width
+// and height parameters passed to pixelColor.
+func NewRasterWithPixels(pixelColor func(x, y, w, h int) color.Color) *Raster {
+	pix := &pixelRaster{}
+	pix.r = &Raster{
+		Generator: func(w, h int) image.Image {
+			if pix.img == nil || pix.img.Bounds().Size().X != w || pix.img.Bounds().Size().Y != h {
+				// raster first pixel, figure out color type
+				var dst draw.Image
+				rect := image.Rect(0, 0, w, h)
+				switch pixelColor(0, 0, w, h).(type) {
+				case color.Alpha:
+					dst = image.NewAlpha(rect)
+				case color.Alpha16:
+					dst = image.NewAlpha16(rect)
+				case color.CMYK:
+					dst = image.NewCMYK(rect)
+				case color.Gray:
+					dst = image.NewGray(rect)
+				case color.Gray16:
+					dst = image.NewGray16(rect)
+				case color.NRGBA:
+					dst = image.NewNRGBA(rect)
+				case color.NRGBA64:
+					dst = image.NewNRGBA64(rect)
+				case color.RGBA:
+					dst = image.NewRGBA(rect)
+				case color.RGBA64:
+					dst = image.NewRGBA64(rect)
+				default:
+					dst = image.NewRGBA(rect)
+				}
+				pix.img = dst
+			}
+
+			for y := 0; y < h; y++ {
+				for x := 0; x < w; x++ {
+					pix.img.Set(x, y, pixelColor(x, y, w, h))
+				}
+			}
+
+			return pix.img
+		},
+	}
+	return pix.r
+}
+
+type subImg interface {
+	SubImage(r image.Rectangle) image.Image
+}
+
+// NewRasterFromImage returns a new Raster instance that is rendered from the Go
+// image.Image passed in.
+// Rasters returned from this method will map pixel for pixel to the screen
+// starting img.Bounds().Min pixels from the top left of the canvas object.
+// Truncates rather than scales the image.
+// If smaller than the target space, the image will be padded with zero-pixels to the target size.
+func NewRasterFromImage(img image.Image) *Raster {
+	return &Raster{
+		Generator: func(w int, h int) image.Image {
+			bounds := img.Bounds()
+
+			rect := image.Rect(0, 0, w, h)
+
+			switch {
+			case w == bounds.Max.X && h == bounds.Max.Y:
+				return img
+			case w >= bounds.Max.X && h >= bounds.Max.Y:
+				// try quickly truncating
+				if sub, ok := img.(subImg); ok {
+					return sub.SubImage(image.Rectangle{
+						Min: bounds.Min,
+						Max: image.Point{
+							X: bounds.Min.X + w,
+							Y: bounds.Min.Y + h,
+						},
+					})
+				}
+			default:
+				if !rect.Overlaps(bounds) {
+					return image.NewUniform(color.RGBA{})
+				}
+				bounds = bounds.Intersect(rect)
+			}
+
+			// respect the user's pixel format (if possible)
+			var dst draw.Image
+			switch i := img.(type) {
+			case *image.Alpha:
+				dst = image.NewAlpha(rect)
+			case *image.Alpha16:
+				dst = image.NewAlpha16(rect)
+			case *image.CMYK:
+				dst = image.NewCMYK(rect)
+			case *image.Gray:
+				dst = image.NewGray(rect)
+			case *image.Gray16:
+				dst = image.NewGray16(rect)
+			case *image.NRGBA:
+				dst = image.NewNRGBA(rect)
+			case *image.NRGBA64:
+				dst = image.NewNRGBA64(rect)
+			case *image.Paletted:
+				dst = image.NewPaletted(rect, i.Palette)
+			case *image.RGBA:
+				dst = image.NewRGBA(rect)
+			case *image.RGBA64:
+				dst = image.NewRGBA64(rect)
+			default:
+				dst = image.NewRGBA(rect)
+			}
+
+			draw.Draw(dst, bounds, img, bounds.Min, draw.Over)
+			return dst
+		},
+	}
+}

+ 64 - 0
vendor/fyne.io/fyne/v2/canvas/rectangle.go

@@ -0,0 +1,64 @@
+package canvas
+
+import (
+	"image/color"
+
+	"fyne.io/fyne/v2"
+)
+
+// Declare conformity with CanvasObject interface
+var _ fyne.CanvasObject = (*Rectangle)(nil)
+
+// Rectangle describes a colored rectangle primitive in a Fyne canvas
+type Rectangle struct {
+	baseObject
+
+	FillColor   color.Color // The rectangle fill color
+	StrokeColor color.Color // The rectangle stroke color
+	StrokeWidth float32     // The stroke width of the rectangle
+	// The radius of the rectangle corners
+	//
+	// Since: 2.4
+	CornerRadius float32
+}
+
+// Hide will set this rectangle to not be visible
+func (r *Rectangle) Hide() {
+	r.baseObject.Hide()
+
+	repaint(r)
+}
+
+// Move the rectangle to a new position, relative to its parent / canvas
+func (r *Rectangle) Move(pos fyne.Position) {
+	r.baseObject.Move(pos)
+
+	repaint(r)
+}
+
+// Refresh causes this rectangle to be redrawn with its configured state.
+func (r *Rectangle) Refresh() {
+	Refresh(r)
+}
+
+// Resize on a rectangle updates the new size of this object.
+// If it has a stroke width this will cause it to Refresh.
+func (r *Rectangle) Resize(s fyne.Size) {
+	if s == r.Size() {
+		return
+	}
+
+	r.baseObject.Resize(s)
+	if r.StrokeWidth == 0 {
+		return
+	}
+
+	Refresh(r)
+}
+
+// NewRectangle returns a new Rectangle instance
+func NewRectangle(color color.Color) *Rectangle {
+	return &Rectangle{
+		FillColor: color,
+	}
+}

+ 76 - 0
vendor/fyne.io/fyne/v2/canvas/text.go

@@ -0,0 +1,76 @@
+package canvas
+
+import (
+	"image/color"
+
+	"fyne.io/fyne/v2"
+)
+
+// Declare conformity with CanvasObject interface
+var _ fyne.CanvasObject = (*Text)(nil)
+
+// Text describes a text primitive in a Fyne canvas.
+// A text object can have a style set which will apply to the whole string.
+// No formatting or text parsing will be performed
+type Text struct {
+	baseObject
+	Alignment fyne.TextAlign // The alignment of the text content
+
+	Color     color.Color    // The main text draw color
+	Text      string         // The string content of this Text
+	TextSize  float32        // Size of the text - if the Canvas scale is 1.0 this will be equivalent to point size
+	TextStyle fyne.TextStyle // The style of the text content
+}
+
+// Hide will set this text to not be visible
+func (t *Text) Hide() {
+	t.baseObject.Hide()
+
+	repaint(t)
+}
+
+// MinSize returns the minimum size of this text object based on its font size and content.
+// This is normally determined by the render implementation.
+func (t *Text) MinSize() fyne.Size {
+	return fyne.MeasureText(t.Text, t.TextSize, t.TextStyle)
+}
+
+// Move the text to a new position, relative to its parent / canvas
+func (t *Text) Move(pos fyne.Position) {
+	t.baseObject.Move(pos)
+
+	repaint(t)
+}
+
+// Resize on a text updates the new size of this object, which may not result in a visual change, depending on alignment.
+func (t *Text) Resize(s fyne.Size) {
+	if s == t.Size() {
+		return
+	}
+
+	t.baseObject.Resize(s)
+	Refresh(t)
+}
+
+// SetMinSize has no effect as the smallest size this canvas object can be is based on its font size and content.
+func (t *Text) SetMinSize(fyne.Size) {
+	// no-op
+}
+
+// Refresh causes this text to be redrawn with its configured state.
+func (t *Text) Refresh() {
+	Refresh(t)
+}
+
+// NewText returns a new Text implementation
+func NewText(text string, color color.Color) *Text {
+	size := float32(0)
+	if fyne.CurrentApp() != nil { // nil app possible if app not started
+		size = fyne.CurrentApp().Settings().Theme().Size("text") // manually name the size to avoid import loop
+	}
+	return &Text{
+		Color:    color,
+		Text:     text,
+		TextSize: size,
+	}
+}

+ 107 - 0
vendor/fyne.io/fyne/v2/canvasobject.go

@@ -0,0 +1,107 @@
+package fyne
+
+// CanvasObject describes any graphical object that can be added to a canvas.
+// Objects have a size and position that can be controlled through this API.
+// MinSize is used to determine the minimum size which this object should be displayed.
+// An object will be visible by default but can be hidden with Hide() and re-shown with Show().
+//
+// Note: If this object is controlled as part of a Layout you should not call
+// Resize(Size) or Move(Position).
+type CanvasObject interface {
+	// geometry
+
+	// MinSize returns the minimum size this object needs to be drawn.
+	MinSize() Size
+	// Move moves this object to the given position relative to its parent.
+	// This should only be called if your object is not in a container with a layout manager.
+	Move(Position)
+	// Position returns the current position of the object relative to its parent.
+	Position() Position
+	// Resize resizes this object to the given size.
+	// This should only be called if your object is not in a container with a layout manager.
+	Resize(Size)
+	// Size returns the current size of this object.
+	Size() Size
+
+	// visibility
+
+	// Hide hides this object.
+	Hide()
+	// Visible returns whether this object is visible or not.
+	Visible() bool
+	// Show shows this object.
+	Show()
+
+	// Refresh must be called if this object should be redrawn because its inner state changed.
+	Refresh()
+}
+
+// Disableable describes any CanvasObject that can be disabled.
+// This is primarily used with objects that also implement the Tappable interface.
+type Disableable interface {
+	Enable()
+	Disable()
+	Disabled() bool
+}
+
+// DoubleTappable describes any CanvasObject that can also be double tapped.
+type DoubleTappable interface {
+	DoubleTapped(*PointEvent)
+}
+
+// Draggable indicates that a CanvasObject can be dragged.
+// This is used for any item that the user has indicated should be moved across the screen.
+type Draggable interface {
+	Dragged(*DragEvent)
+	DragEnd()
+}
+
+// Focusable describes any CanvasObject that can respond to being focused.
+// It will receive the FocusGained and FocusLost events appropriately.
+// When focused it will also have TypedRune called as text is input and
+// TypedKey called when other keys are pressed.
+//
+// Note: You must not change canvas state (including overlays or focus) in FocusGained or FocusLost
+// or you would end up with a dead-lock.
+type Focusable interface {
+	// FocusGained is a hook called by the focus handling logic after this object gained the focus.
+	FocusGained()
+	// FocusLost is a hook called by the focus handling logic after this object lost the focus.
+	FocusLost()
+
+	// TypedRune is a hook called by the input handling logic on text input events if this object is focused.
+	TypedRune(rune)
+	// TypedKey is a hook called by the input handling logic on key events if this object is focused.
+	TypedKey(*KeyEvent)
+}
+
+// Scrollable describes any CanvasObject that can also be scrolled.
+// This is mostly used to implement the widget.ScrollContainer.
+type Scrollable interface {
+	Scrolled(*ScrollEvent)
+}
+
+// SecondaryTappable describes a CanvasObject that can be right-clicked or long-tapped.
+type SecondaryTappable interface {
+	TappedSecondary(*PointEvent)
+}
+
+// Shortcutable describes any CanvasObject that can respond to shortcut commands (quit, cut, copy, and paste).
+type Shortcutable interface {
+	TypedShortcut(Shortcut)
+}
+
+// Tabbable describes any object that needs to accept the Tab key presses.
+//
+// Since: 2.1
+type Tabbable interface {
+	// AcceptsTab() is a hook called by the key press handling logic.
+	// If it returns true then the Tab key events will be sent using TypedKey.
+	AcceptsTab() bool
+}
+
+// Tappable describes any CanvasObject that can also be tapped.
+// This should be implemented by buttons etc that wish to handle pointer interactions.
+type Tappable interface {
+	Tapped(*PointEvent)
+}

+ 9 - 0
vendor/fyne.io/fyne/v2/clipboard.go

@@ -0,0 +1,9 @@
+package fyne
+
+// Clipboard represents the system clipboard interface
+type Clipboard interface {
+	// Content returns the clipboard content
+	Content() string
+	// SetContent sets the clipboard content
+	SetContent(content string)
+}

+ 39 - 0
vendor/fyne.io/fyne/v2/cloud.go

@@ -0,0 +1,39 @@
+package fyne
+
+// CloudProvider specifies the identifying information of a cloud provider.
+// This information is mostly used by the `fyne.io/cloud ShowSettings' user flow.
+//
+// Since: 2.3
+type CloudProvider interface {
+	// ProviderDescription returns a more detailed description of this cloud provider.
+	ProviderDescription() string
+	// ProviderIcon returns an icon resource that is associated with the given cloud service.
+	ProviderIcon() Resource
+	// ProviderName returns the name of this cloud provider, usually the name of the service it uses.
+	ProviderName() string
+
+	// Cleanup is called when this provider is no longer used and should be disposed.
+	// This is guaranteed to execute before a new provider is `Setup`
+	Cleanup(App)
+	// Setup is called when this provider is being used for the first time.
+	// Returning an error will exit the cloud setup process, though it can be retried.
+	Setup(App) error
+}
+
+// CloudProviderPreferences interface defines the functionality that a cloud provider will include if it is capable
+// of synchronizing user preferences.
+//
+// Since: 2.3
+type CloudProviderPreferences interface {
+	// CloudPreferences returns a preference provider that will sync values to the cloud this provider uses.
+	CloudPreferences(App) Preferences
+}
+
+// CloudProviderStorage interface defines the functionality that a cloud provider will include if it is capable
+// of synchronizing user documents.
+//
+// Since: 2.3
+type CloudProviderStorage interface {
+	// CloudStorage returns a storage provider that will sync documents to the cloud this provider uses.
+	CloudStorage(App) Storage
+}

+ 211 - 0
vendor/fyne.io/fyne/v2/container.go

@@ -0,0 +1,211 @@
+package fyne
+
+import "sync"
+
+// Declare conformity to CanvasObject
+var _ CanvasObject = (*Container)(nil)
+
+// Container is a CanvasObject that contains a collection of child objects.
+// The layout of the children is set by the specified Layout.
+type Container struct {
+	size     Size     // The current size of the Container
+	position Position // The current position of the Container
+	Hidden   bool     // Is this Container hidden
+
+	Layout  Layout // The Layout algorithm for arranging child CanvasObjects
+	lock    sync.Mutex
+	Objects []CanvasObject // The set of CanvasObjects this container holds
+}
+
+// NewContainer returns a new Container instance holding the specified CanvasObjects.
+//
+// Deprecated: Use container.NewWithoutLayout() to create a container that uses manual layout.
+func NewContainer(objects ...CanvasObject) *Container {
+	return NewContainerWithoutLayout(objects...)
+}
+
+// NewContainerWithoutLayout returns a new Container instance holding the specified
+// CanvasObjects that are manually arranged.
+//
+// Deprecated: Use container.NewWithoutLayout() instead
+func NewContainerWithoutLayout(objects ...CanvasObject) *Container {
+	ret := &Container{
+		Objects: objects,
+	}
+
+	ret.size = ret.MinSize()
+	return ret
+}
+
+// NewContainerWithLayout returns a new Container instance holding the specified
+// CanvasObjects which will be laid out according to the specified Layout.
+//
+// Deprecated: Use container.New() instead
+func NewContainerWithLayout(layout Layout, objects ...CanvasObject) *Container {
+	ret := &Container{
+		Objects: objects,
+		Layout:  layout,
+	}
+
+	ret.size = layout.MinSize(objects)
+	ret.layout()
+	return ret
+}
+
+// Add appends the specified object to the items this container manages.
+//
+// Since: 1.4
+func (c *Container) Add(add CanvasObject) {
+	if add == nil {
+		return
+	}
+
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	c.Objects = append(c.Objects, add)
+	c.layout()
+}
+
+// AddObject adds another CanvasObject to the set this Container holds.
+//
+// Deprecated: Use replacement Add() function
+func (c *Container) AddObject(o CanvasObject) {
+	c.Add(o)
+}
+
+// Hide sets this container, and all its children, to be not visible.
+func (c *Container) Hide() {
+	if c.Hidden {
+		return
+	}
+
+	c.Hidden = true
+	repaint(c)
+}
+
+// MinSize calculates the minimum size of a Container.
+// This is delegated to the Layout, if specified, otherwise it will mimic MaxLayout.
+func (c *Container) MinSize() Size {
+	if c.Layout != nil {
+		return c.Layout.MinSize(c.Objects)
+	}
+
+	minSize := NewSize(1, 1)
+	for _, child := range c.Objects {
+		minSize = minSize.Max(child.MinSize())
+	}
+
+	return minSize
+}
+
+// Move the container (and all its children) to a new position, relative to its parent.
+func (c *Container) Move(pos Position) {
+	c.position = pos
+	repaint(c)
+}
+
+// Position gets the current position of this Container, relative to its parent.
+func (c *Container) Position() Position {
+	return c.position
+}
+
+// Refresh causes this object to be redrawn in it's current state
+func (c *Container) Refresh() {
+	c.layout()
+
+	for _, child := range c.Objects {
+		child.Refresh()
+	}
+
+	// this is basically just canvas.Refresh(c) without the package loop
+	o := CurrentApp().Driver().CanvasForObject(c)
+	if o == nil {
+		return
+	}
+	o.Refresh(c)
+}
+
+// Remove updates the contents of this container to no longer include the specified object.
+// This method is not intended to be used inside a loop, to remove all the elements.
+// It is much more efficient to call RemoveAll() instead.
+func (c *Container) Remove(rem CanvasObject) {
+	if len(c.Objects) == 0 {
+		return
+	}
+
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	for i, o := range c.Objects {
+		if o != rem {
+			continue
+		}
+
+		removed := make([]CanvasObject, len(c.Objects)-1)
+		copy(removed, c.Objects[:i])
+		copy(removed[i:], c.Objects[i+1:])
+
+		c.Objects = removed
+		c.layout()
+		return
+	}
+}
+
+// RemoveAll updates the contents of this container to no longer include any objects.
+//
+// Since: 2.2
+func (c *Container) RemoveAll() {
+	c.Objects = nil
+	c.layout()
+}
+
+// Resize sets a new size for the Container.
+func (c *Container) Resize(size Size) {
+	if c.size == size {
+		return
+	}
+
+	c.size = size
+	c.layout()
+}
+
+// Show sets this container, and all its children, to be visible.
+func (c *Container) Show() {
+	if !c.Hidden {
+		return
+	}
+
+	c.Hidden = false
+}
+
+// Size returns the current size of this container.
+func (c *Container) Size() Size {
+	return c.size
+}
+
+// Visible returns true if the container is currently visible, false otherwise.
+func (c *Container) Visible() bool {
+	return !c.Hidden
+}
+
+func (c *Container) layout() {
+	if c.Layout == nil {
+		return
+	}
+
+	c.Layout.Layout(c.Objects, c.size)
+}
+
+// repaint instructs the containing canvas to redraw, even if nothing changed.
+// This method is a duplicate of what is in `canvas/canvas.go` to avoid a dependency loop or public API.
+func repaint(obj *Container) {
+	if CurrentApp() == nil || CurrentApp().Driver() == nil {
+		return
+	}
+
+	c := CurrentApp().Driver().CanvasForObject(obj)
+	if c != nil {
+		if paint, ok := c.(interface{ SetDirty() }); ok {
+			paint.SetDirty()
+		}
+	}
+}

+ 462 - 0
vendor/fyne.io/fyne/v2/container/apptabs.go

@@ -0,0 +1,462 @@
+package container
+
+import (
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/canvas"
+	"fyne.io/fyne/v2/layout"
+	"fyne.io/fyne/v2/theme"
+	"fyne.io/fyne/v2/widget"
+)
+
+// Declare conformity with Widget interface.
+var _ fyne.Widget = (*AppTabs)(nil)
+
+// AppTabs container is used to split your application into various different areas identified by tabs.
+// The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem.
+// Each item is represented by a button at the edge of the container.
+//
+// Since: 1.4
+type AppTabs struct {
+	widget.BaseWidget
+
+	Items []*TabItem
+
+	// Deprecated: Use `OnSelected func(*TabItem)` instead.
+	OnChanged    func(*TabItem)
+	OnSelected   func(*TabItem)
+	OnUnselected func(*TabItem)
+
+	current         int
+	location        TabLocation
+	isTransitioning bool
+
+	popUpMenu *widget.PopUpMenu
+}
+
+// NewAppTabs creates a new tab container that allows the user to choose between different areas of an app.
+//
+// Since: 1.4
+func NewAppTabs(items ...*TabItem) *AppTabs {
+	tabs := &AppTabs{}
+	tabs.BaseWidget.ExtendBaseWidget(tabs)
+	tabs.SetItems(items)
+	return tabs
+}
+
+// CreateRenderer is a private method to Fyne which links this widget to its renderer
+//
+// Implements: fyne.Widget
+func (t *AppTabs) CreateRenderer() fyne.WidgetRenderer {
+	t.BaseWidget.ExtendBaseWidget(t)
+	r := &appTabsRenderer{
+		baseTabsRenderer: baseTabsRenderer{
+			bar:       &fyne.Container{},
+			divider:   canvas.NewRectangle(theme.ShadowColor()),
+			indicator: canvas.NewRectangle(theme.PrimaryColor()),
+		},
+		appTabs: t,
+	}
+	r.action = r.buildOverflowTabsButton()
+
+	// Initially setup the tab bar to only show one tab, all others will be in overflow.
+	// When the widget is laid out, and we know the size, the tab bar will be updated to show as many as can fit.
+	r.updateTabs(1)
+	r.updateIndicator(false)
+	r.applyTheme(t)
+	return r
+}
+
+// Append adds a new TabItem to the end of the tab bar.
+func (t *AppTabs) Append(item *TabItem) {
+	t.SetItems(append(t.Items, item))
+}
+
+// CurrentTab returns the currently selected TabItem.
+//
+// Deprecated: Use `AppTabs.Selected() *TabItem` instead.
+func (t *AppTabs) CurrentTab() *TabItem {
+	if t.current < 0 || t.current >= len(t.Items) {
+		return nil
+	}
+	return t.Items[t.current]
+}
+
+// CurrentTabIndex returns the index of the currently selected TabItem.
+//
+// Deprecated: Use `AppTabs.SelectedIndex() int` instead.
+func (t *AppTabs) CurrentTabIndex() int {
+	return t.current
+}
+
+// DisableIndex disables the TabItem at the specified index.
+//
+// Since: 2.3
+func (t *AppTabs) DisableIndex(i int) {
+	disableIndex(t, i)
+}
+
+// DisableItem disables the specified TabItem.
+//
+// Since: 2.3
+func (t *AppTabs) DisableItem(item *TabItem) {
+	disableItem(t, item)
+}
+
+// EnableIndex enables the TabItem at the specified index.
+//
+// Since: 2.3
+func (t *AppTabs) EnableIndex(i int) {
+	enableIndex(t, i)
+}
+
+// EnableItem enables the specified TabItem.
+//
+// Since: 2.3
+func (t *AppTabs) EnableItem(item *TabItem) {
+	enableItem(t, item)
+}
+
+// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality.
+//
+// Deprecated: Support for extending containers is being removed
+func (t *AppTabs) ExtendBaseWidget(wid fyne.Widget) {
+	t.BaseWidget.ExtendBaseWidget(wid)
+}
+
+// Hide hides the widget.
+//
+// Implements: fyne.CanvasObject
+func (t *AppTabs) Hide() {
+	if t.popUpMenu != nil {
+		t.popUpMenu.Hide()
+		t.popUpMenu = nil
+	}
+	t.BaseWidget.Hide()
+}
+
+// MinSize returns the size that this widget should not shrink below
+//
+// Implements: fyne.CanvasObject
+func (t *AppTabs) MinSize() fyne.Size {
+	t.BaseWidget.ExtendBaseWidget(t)
+	return t.BaseWidget.MinSize()
+}
+
+// Remove tab by value.
+func (t *AppTabs) Remove(item *TabItem) {
+	removeItem(t, item)
+	t.Refresh()
+}
+
+// RemoveIndex removes tab by index.
+func (t *AppTabs) RemoveIndex(index int) {
+	removeIndex(t, index)
+	t.Refresh()
+}
+
+// Select sets the specified TabItem to be selected and its content visible.
+func (t *AppTabs) Select(item *TabItem) {
+	selectItem(t, item)
+	t.Refresh()
+}
+
+// SelectIndex sets the TabItem at the specific index to be selected and its content visible.
+func (t *AppTabs) SelectIndex(index int) {
+	selectIndex(t, index)
+	t.Refresh()
+}
+
+// SelectTab sets the specified TabItem to be selected and its content visible.
+//
+// Deprecated: Use `AppTabs.Select(*TabItem)` instead.
+func (t *AppTabs) SelectTab(item *TabItem) {
+	for i, child := range t.Items {
+		if child == item {
+			t.SelectTabIndex(i)
+			return
+		}
+	}
+}
+
+// SelectTabIndex sets the TabItem at the specific index to be selected and its content visible.
+//
+// Deprecated: Use `AppTabs.SelectIndex(int)` instead.
+func (t *AppTabs) SelectTabIndex(index int) {
+	if index < 0 || index >= len(t.Items) || t.current == index {
+		return
+	}
+	t.current = index
+	t.Refresh()
+
+	if t.OnChanged != nil {
+		t.OnChanged(t.Items[t.current])
+	}
+}
+
+// Selected returns the currently selected TabItem.
+func (t *AppTabs) Selected() *TabItem {
+	return selected(t)
+}
+
+// SelectedIndex returns the index of the currently selected TabItem.
+func (t *AppTabs) SelectedIndex() int {
+	return t.current
+}
+
+// SetItems sets the containers items and refreshes.
+func (t *AppTabs) SetItems(items []*TabItem) {
+	setItems(t, items)
+	t.Refresh()
+}
+
+// SetTabLocation sets the location of the tab bar
+func (t *AppTabs) SetTabLocation(l TabLocation) {
+	t.location = tabsAdjustedLocation(l)
+	t.Refresh()
+}
+
+// Show this widget, if it was previously hidden
+//
+// Implements: fyne.CanvasObject
+func (t *AppTabs) Show() {
+	t.BaseWidget.Show()
+	t.SelectIndex(t.current)
+}
+
+func (t *AppTabs) onUnselected() func(*TabItem) {
+	return t.OnUnselected
+}
+
+func (t *AppTabs) onSelected() func(*TabItem) {
+	return func(tab *TabItem) {
+		if f := t.OnChanged; f != nil {
+			f(tab)
+		}
+		if f := t.OnSelected; f != nil {
+			f(tab)
+		}
+	}
+}
+
+func (t *AppTabs) items() []*TabItem {
+	return t.Items
+}
+
+func (t *AppTabs) selected() int {
+	return t.current
+}
+
+func (t *AppTabs) setItems(items []*TabItem) {
+	t.Items = items
+}
+
+func (t *AppTabs) setSelected(selected int) {
+	t.current = selected
+}
+
+func (t *AppTabs) setTransitioning(transitioning bool) {
+	t.isTransitioning = transitioning
+}
+
+func (t *AppTabs) tabLocation() TabLocation {
+	return t.location
+}
+
+func (t *AppTabs) transitioning() bool {
+	return t.isTransitioning
+}
+
+// Declare conformity with WidgetRenderer interface.
+var _ fyne.WidgetRenderer = (*appTabsRenderer)(nil)
+
+type appTabsRenderer struct {
+	baseTabsRenderer
+	appTabs *AppTabs
+}
+
+func (r *appTabsRenderer) Layout(size fyne.Size) {
+	// Try render as many tabs as will fit, others will appear in the overflow
+	if len(r.appTabs.Items) == 0 {
+		r.updateTabs(0)
+	} else {
+		for i := len(r.appTabs.Items); i > 0; i-- {
+			r.updateTabs(i)
+			barMin := r.bar.MinSize()
+			if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
+				if barMin.Height <= size.Height {
+					// Tab bar is short enough to fit
+					break
+				}
+			} else {
+				if barMin.Width <= size.Width {
+					// Tab bar is thin enough to fit
+					break
+				}
+			}
+		}
+	}
+
+	r.layout(r.appTabs, size)
+	r.updateIndicator(r.appTabs.transitioning())
+	if r.appTabs.transitioning() {
+		r.appTabs.setTransitioning(false)
+	}
+}
+
+func (r *appTabsRenderer) MinSize() fyne.Size {
+	return r.minSize(r.appTabs)
+}
+
+func (r *appTabsRenderer) Objects() []fyne.CanvasObject {
+	return r.objects(r.appTabs)
+}
+
+func (r *appTabsRenderer) Refresh() {
+	r.Layout(r.appTabs.Size())
+
+	r.refresh(r.appTabs)
+
+	canvas.Refresh(r.appTabs)
+}
+
+func (r *appTabsRenderer) buildOverflowTabsButton() (overflow *widget.Button) {
+	overflow = &widget.Button{Icon: moreIcon(r.appTabs), Importance: widget.LowImportance, OnTapped: func() {
+		// Show pop up containing all tabs which did not fit in the tab bar
+
+		itemLen, objLen := len(r.appTabs.Items), len(r.bar.Objects[0].(*fyne.Container).Objects)
+		items := make([]*fyne.MenuItem, 0, itemLen-objLen)
+		for i := objLen; i < itemLen; i++ {
+			index := i // capture
+			// FIXME MenuItem doesn't support icons (#1752)
+			// FIXME MenuItem can't show if it is the currently selected tab (#1753)
+			items = append(items, fyne.NewMenuItem(r.appTabs.Items[i].Text, func() {
+				r.appTabs.SelectIndex(index)
+				if r.appTabs.popUpMenu != nil {
+					r.appTabs.popUpMenu.Hide()
+					r.appTabs.popUpMenu = nil
+				}
+			}))
+		}
+
+		r.appTabs.popUpMenu = buildPopUpMenu(r.appTabs, overflow, items)
+	}}
+
+	return overflow
+}
+
+func (r *appTabsRenderer) buildTabButtons(count int) *fyne.Container {
+	buttons := &fyne.Container{}
+
+	var iconPos buttonIconPosition
+	if fyne.CurrentDevice().IsMobile() {
+		cells := count
+		if cells == 0 {
+			cells = 1
+		}
+		if r.appTabs.location == TabLocationTop || r.appTabs.location == TabLocationBottom {
+			buttons.Layout = layout.NewGridLayoutWithColumns(cells)
+		} else {
+			buttons.Layout = layout.NewGridLayoutWithRows(cells)
+		}
+		iconPos = buttonIconTop
+	} else if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
+		buttons.Layout = layout.NewVBoxLayout()
+		iconPos = buttonIconTop
+	} else {
+		buttons.Layout = layout.NewHBoxLayout()
+		iconPos = buttonIconInline
+	}
+
+	for i := 0; i < count; i++ {
+		item := r.appTabs.Items[i]
+		if item.button == nil {
+			item.button = &tabButton{
+				onTapped: func() { r.appTabs.Select(item) },
+			}
+		}
+		button := item.button
+		button.icon = item.Icon
+		button.iconPosition = iconPos
+		if i == r.appTabs.current {
+			button.importance = widget.HighImportance
+		} else {
+			button.importance = widget.MediumImportance
+		}
+		button.text = item.Text
+		button.textAlignment = fyne.TextAlignCenter
+		button.Refresh()
+		buttons.Objects = append(buttons.Objects, button)
+	}
+	return buttons
+}
+
+func (r *appTabsRenderer) updateIndicator(animate bool) {
+	if r.appTabs.current < 0 {
+		r.indicator.Hide()
+		return
+	}
+
+	var selectedPos fyne.Position
+	var selectedSize fyne.Size
+
+	buttons := r.bar.Objects[0].(*fyne.Container).Objects
+	if r.appTabs.current >= len(buttons) {
+		if a := r.action; a != nil {
+			selectedPos = a.Position()
+			selectedSize = a.Size()
+		}
+	} else {
+		selected := buttons[r.appTabs.current]
+		selectedPos = selected.Position()
+		selectedSize = selected.Size()
+	}
+
+	var indicatorPos fyne.Position
+	var indicatorSize fyne.Size
+
+	switch r.appTabs.location {
+	case TabLocationTop:
+		indicatorPos = fyne.NewPos(selectedPos.X, r.bar.MinSize().Height)
+		indicatorSize = fyne.NewSize(selectedSize.Width, theme.Padding())
+	case TabLocationLeading:
+		indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y)
+		indicatorSize = fyne.NewSize(theme.Padding(), selectedSize.Height)
+	case TabLocationBottom:
+		indicatorPos = fyne.NewPos(selectedPos.X, r.bar.Position().Y-theme.Padding())
+		indicatorSize = fyne.NewSize(selectedSize.Width, theme.Padding())
+	case TabLocationTrailing:
+		indicatorPos = fyne.NewPos(r.bar.Position().X-theme.Padding(), selectedPos.Y)
+		indicatorSize = fyne.NewSize(theme.Padding(), selectedSize.Height)
+	}
+
+	r.moveIndicator(indicatorPos, indicatorSize, animate)
+}
+
+func (r *appTabsRenderer) updateTabs(max int) {
+	tabCount := len(r.appTabs.Items)
+
+	// Set overflow action
+	if tabCount <= max {
+		r.action.Hide()
+		r.bar.Layout = layout.NewStackLayout()
+	} else {
+		tabCount = max
+		r.action.Show()
+
+		// Set layout of tab bar containing tab buttons and overflow action
+		if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
+			r.bar.Layout = layout.NewBorderLayout(nil, r.action, nil, nil)
+		} else {
+			r.bar.Layout = layout.NewBorderLayout(nil, nil, nil, r.action)
+		}
+	}
+
+	buttons := r.buildTabButtons(tabCount)
+
+	r.bar.Objects = []fyne.CanvasObject{buttons}
+	if a := r.action; a != nil {
+		r.bar.Objects = append(r.bar.Objects, a)
+	}
+
+	r.bar.Refresh()
+}

+ 20 - 0
vendor/fyne.io/fyne/v2/container/container.go

@@ -0,0 +1,20 @@
+// Package container provides containers that are used to lay out and organise applications.
+package container
+
+import (
+	"fyne.io/fyne/v2"
+)
+
+// New returns a new Container instance holding the specified CanvasObjects which will be laid out according to the specified Layout.
+//
+// Since: 2.0
+func New(layout fyne.Layout, objects ...fyne.CanvasObject) *fyne.Container {
+	return &fyne.Container{Layout: layout, Objects: objects}
+}
+
+// NewWithoutLayout returns a new Container instance holding the specified CanvasObjects that are manually arranged.
+//
+// Since: 2.0
+func NewWithoutLayout(objects ...fyne.CanvasObject) *fyne.Container {
+	return &fyne.Container{Objects: objects}
+}

+ 496 - 0
vendor/fyne.io/fyne/v2/container/doctabs.go

@@ -0,0 +1,496 @@
+package container
+
+import (
+	"image/color"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/canvas"
+	"fyne.io/fyne/v2/layout"
+	"fyne.io/fyne/v2/theme"
+	"fyne.io/fyne/v2/widget"
+)
+
+// Declare conformity with Widget interface.
+var _ fyne.Widget = (*DocTabs)(nil)
+
+// DocTabs container is used to display various pieces of content identified by tabs.
+// The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem.
+// Each item is represented by a button at the edge of the container.
+//
+// Since: 2.1
+type DocTabs struct {
+	widget.BaseWidget
+
+	Items []*TabItem
+
+	CreateTab      func() *TabItem
+	CloseIntercept func(*TabItem)
+	OnClosed       func(*TabItem)
+	OnSelected     func(*TabItem)
+	OnUnselected   func(*TabItem)
+
+	current         int
+	location        TabLocation
+	isTransitioning bool
+
+	popUpMenu *widget.PopUpMenu
+}
+
+// NewDocTabs creates a new tab container that allows the user to choose between various pieces of content.
+//
+// Since: 2.1
+func NewDocTabs(items ...*TabItem) *DocTabs {
+	tabs := &DocTabs{}
+	tabs.ExtendBaseWidget(tabs)
+	tabs.SetItems(items)
+	return tabs
+}
+
+// Append adds a new TabItem to the end of the tab bar.
+func (t *DocTabs) Append(item *TabItem) {
+	t.SetItems(append(t.Items, item))
+}
+
+// CreateRenderer is a private method to Fyne which links this widget to its renderer
+//
+// Implements: fyne.Widget
+func (t *DocTabs) CreateRenderer() fyne.WidgetRenderer {
+	t.ExtendBaseWidget(t)
+	r := &docTabsRenderer{
+		baseTabsRenderer: baseTabsRenderer{
+			bar:       &fyne.Container{},
+			divider:   canvas.NewRectangle(theme.ShadowColor()),
+			indicator: canvas.NewRectangle(theme.PrimaryColor()),
+		},
+		docTabs:  t,
+		scroller: NewScroll(&fyne.Container{}),
+	}
+	r.action = r.buildAllTabsButton()
+	r.create = r.buildCreateTabsButton()
+	r.box = NewHBox(r.create, r.action)
+	r.scroller.OnScrolled = func(offset fyne.Position) {
+		r.updateIndicator(false)
+	}
+	r.updateAllTabs()
+	r.updateCreateTab()
+	r.updateTabs()
+	r.updateIndicator(false)
+	r.applyTheme(t)
+	return r
+}
+
+// DisableIndex disables the TabItem at the specified index.
+//
+// Since: 2.3
+func (t *DocTabs) DisableIndex(i int) {
+	disableIndex(t, i)
+}
+
+// DisableItem disables the specified TabItem.
+//
+// Since: 2.3
+func (t *DocTabs) DisableItem(item *TabItem) {
+	disableItem(t, item)
+}
+
+// EnableIndex enables the TabItem at the specified index.
+//
+// Since: 2.3
+func (t *DocTabs) EnableIndex(i int) {
+	enableIndex(t, i)
+}
+
+// EnableItem enables the specified TabItem.
+//
+// Since: 2.3
+func (t *DocTabs) EnableItem(item *TabItem) {
+	enableItem(t, item)
+}
+
+// Hide hides the widget.
+//
+// Implements: fyne.CanvasObject
+func (t *DocTabs) Hide() {
+	if t.popUpMenu != nil {
+		t.popUpMenu.Hide()
+		t.popUpMenu = nil
+	}
+	t.BaseWidget.Hide()
+}
+
+// MinSize returns the size that this widget should not shrink below
+//
+// Implements: fyne.CanvasObject
+func (t *DocTabs) MinSize() fyne.Size {
+	t.ExtendBaseWidget(t)
+	return t.BaseWidget.MinSize()
+}
+
+// Remove tab by value.
+func (t *DocTabs) Remove(item *TabItem) {
+	removeItem(t, item)
+	t.Refresh()
+}
+
+// RemoveIndex removes tab by index.
+func (t *DocTabs) RemoveIndex(index int) {
+	removeIndex(t, index)
+	t.Refresh()
+}
+
+// Select sets the specified TabItem to be selected and its content visible.
+func (t *DocTabs) Select(item *TabItem) {
+	selectItem(t, item)
+	t.Refresh()
+}
+
+// SelectIndex sets the TabItem at the specific index to be selected and its content visible.
+func (t *DocTabs) SelectIndex(index int) {
+	selectIndex(t, index)
+	t.Refresh()
+}
+
+// Selected returns the currently selected TabItem.
+func (t *DocTabs) Selected() *TabItem {
+	return selected(t)
+}
+
+// SelectedIndex returns the index of the currently selected TabItem.
+func (t *DocTabs) SelectedIndex() int {
+	return t.current
+}
+
+// SetItems sets the containers items and refreshes.
+func (t *DocTabs) SetItems(items []*TabItem) {
+	setItems(t, items)
+	t.Refresh()
+}
+
+// SetTabLocation sets the location of the tab bar
+func (t *DocTabs) SetTabLocation(l TabLocation) {
+	t.location = tabsAdjustedLocation(l)
+	t.Refresh()
+}
+
+// Show this widget, if it was previously hidden
+//
+// Implements: fyne.CanvasObject
+func (t *DocTabs) Show() {
+	t.BaseWidget.Show()
+	t.SelectIndex(t.current)
+}
+
+func (t *DocTabs) close(item *TabItem) {
+	if f := t.CloseIntercept; f != nil {
+		f(item)
+	} else {
+		t.Remove(item)
+		if f := t.OnClosed; f != nil {
+			f(item)
+		}
+	}
+}
+
+func (t *DocTabs) onUnselected() func(*TabItem) {
+	return t.OnUnselected
+}
+
+func (t *DocTabs) onSelected() func(*TabItem) {
+	return t.OnSelected
+}
+
+func (t *DocTabs) items() []*TabItem {
+	return t.Items
+}
+
+func (t *DocTabs) selected() int {
+	return t.current
+}
+
+func (t *DocTabs) setItems(items []*TabItem) {
+	t.Items = items
+}
+
+func (t *DocTabs) setSelected(selected int) {
+	t.current = selected
+}
+
+func (t *DocTabs) setTransitioning(transitioning bool) {
+	t.isTransitioning = transitioning
+}
+
+func (t *DocTabs) tabLocation() TabLocation {
+	return t.location
+}
+
+func (t *DocTabs) transitioning() bool {
+	return t.isTransitioning
+}
+
+// Declare conformity with WidgetRenderer interface.
+var _ fyne.WidgetRenderer = (*docTabsRenderer)(nil)
+
+type docTabsRenderer struct {
+	baseTabsRenderer
+	docTabs      *DocTabs
+	scroller     *Scroll
+	box          *fyne.Container
+	create       *widget.Button
+	lastSelected int
+}
+
+func (r *docTabsRenderer) Layout(size fyne.Size) {
+	r.updateAllTabs()
+	r.updateCreateTab()
+	r.updateTabs()
+	r.layout(r.docTabs, size)
+
+	// lay out buttons before updating indicator, which is relative to their position
+	buttons := r.scroller.Content.(*fyne.Container)
+	buttons.Layout.Layout(buttons.Objects, buttons.Size())
+	r.updateIndicator(r.docTabs.transitioning())
+
+	if r.docTabs.transitioning() {
+		r.docTabs.setTransitioning(false)
+	}
+}
+
+func (r *docTabsRenderer) MinSize() fyne.Size {
+	return r.minSize(r.docTabs)
+}
+
+func (r *docTabsRenderer) Objects() []fyne.CanvasObject {
+	return r.objects(r.docTabs)
+}
+
+func (r *docTabsRenderer) Refresh() {
+	r.Layout(r.docTabs.Size())
+
+	if c := r.docTabs.current; c != r.lastSelected {
+		if c >= 0 && c < len(r.docTabs.Items) {
+			r.scrollToSelected()
+		}
+		r.lastSelected = c
+	}
+
+	r.refresh(r.docTabs)
+
+	canvas.Refresh(r.docTabs)
+}
+
+func (r *docTabsRenderer) buildAllTabsButton() (all *widget.Button) {
+	all = &widget.Button{Importance: widget.LowImportance, OnTapped: func() {
+		// Show pop up containing all tabs
+
+		items := make([]*fyne.MenuItem, len(r.docTabs.Items))
+		for i := 0; i < len(r.docTabs.Items); i++ {
+			index := i // capture
+			// FIXME MenuItem doesn't support icons (#1752)
+			items[i] = fyne.NewMenuItem(r.docTabs.Items[i].Text, func() {
+				r.docTabs.SelectIndex(index)
+				if r.docTabs.popUpMenu != nil {
+					r.docTabs.popUpMenu.Hide()
+					r.docTabs.popUpMenu = nil
+				}
+			})
+			items[i].Checked = index == r.docTabs.current
+		}
+
+		r.docTabs.popUpMenu = buildPopUpMenu(r.docTabs, all, items)
+	}}
+
+	return all
+}
+
+func (r *docTabsRenderer) buildCreateTabsButton() *widget.Button {
+	create := widget.NewButton("", func() {
+		if f := r.docTabs.CreateTab; f != nil {
+			if tab := f(); tab != nil {
+				r.docTabs.Append(tab)
+				r.docTabs.SelectIndex(len(r.docTabs.Items) - 1)
+			}
+		}
+	})
+	create.Importance = widget.LowImportance
+	return create
+}
+
+func (r *docTabsRenderer) buildTabButtons(count int, buttons *fyne.Container) {
+	buttons.Objects = nil
+
+	var iconPos buttonIconPosition
+	if fyne.CurrentDevice().IsMobile() {
+		cells := count
+		if cells == 0 {
+			cells = 1
+		}
+		if r.docTabs.location == TabLocationTop || r.docTabs.location == TabLocationBottom {
+			buttons.Layout = layout.NewGridLayoutWithColumns(cells)
+		} else {
+			buttons.Layout = layout.NewGridLayoutWithRows(cells)
+		}
+		iconPos = buttonIconTop
+	} else if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
+		buttons.Layout = layout.NewVBoxLayout()
+		iconPos = buttonIconTop
+	} else {
+		buttons.Layout = layout.NewHBoxLayout()
+		iconPos = buttonIconInline
+	}
+
+	for i := 0; i < count; i++ {
+		item := r.docTabs.Items[i]
+		if item.button == nil {
+			item.button = &tabButton{
+				onTapped: func() { r.docTabs.Select(item) },
+				onClosed: func() { r.docTabs.close(item) },
+			}
+		}
+		button := item.button
+		button.icon = item.Icon
+		button.iconPosition = iconPos
+		if i == r.docTabs.current {
+			button.importance = widget.HighImportance
+		} else {
+			button.importance = widget.MediumImportance
+		}
+		button.text = item.Text
+		button.textAlignment = fyne.TextAlignLeading
+		button.Refresh()
+		buttons.Objects = append(buttons.Objects, button)
+	}
+}
+
+func (r *docTabsRenderer) scrollToSelected() {
+	buttons := r.scroller.Content.(*fyne.Container)
+
+	// https://github.com/fyne-io/fyne/issues/3909
+	// very dirty temporary fix to this crash!
+	if r.docTabs.current < 0 || r.docTabs.current >= len(buttons.Objects) {
+		return
+	}
+
+	button := buttons.Objects[r.docTabs.current]
+	pos := button.Position()
+	size := button.Size()
+	offset := r.scroller.Offset
+	viewport := r.scroller.Size()
+	if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
+		if pos.Y < offset.Y {
+			offset.Y = pos.Y
+		} else if pos.Y+size.Height > offset.Y+viewport.Height {
+			offset.Y = pos.Y + size.Height - viewport.Height
+		}
+	} else {
+		if pos.X < offset.X {
+			offset.X = pos.X
+		} else if pos.X+size.Width > offset.X+viewport.Width {
+			offset.X = pos.X + size.Width - viewport.Width
+		}
+	}
+	r.scroller.Offset = offset
+	r.updateIndicator(false)
+}
+
+func (r *docTabsRenderer) updateIndicator(animate bool) {
+	if r.docTabs.current < 0 {
+		r.indicator.FillColor = color.Transparent
+		r.moveIndicator(fyne.NewPos(0, 0), fyne.NewSize(0, 0), animate)
+		return
+	}
+
+	var selectedPos fyne.Position
+	var selectedSize fyne.Size
+
+	buttons := r.scroller.Content.(*fyne.Container).Objects
+
+	if r.docTabs.current >= len(buttons) {
+		if a := r.action; a != nil {
+			selectedPos = a.Position()
+			selectedSize = a.Size()
+			minSize := a.MinSize()
+			if minSize.Width > selectedSize.Width {
+				selectedSize = minSize
+			}
+		}
+	} else {
+		selected := buttons[r.docTabs.current]
+		selectedPos = selected.Position()
+		selectedSize = selected.Size()
+		minSize := selected.MinSize()
+		if minSize.Width > selectedSize.Width {
+			selectedSize = minSize
+		}
+	}
+
+	scrollOffset := r.scroller.Offset
+	scrollSize := r.scroller.Size()
+
+	var indicatorPos fyne.Position
+	var indicatorSize fyne.Size
+
+	switch r.docTabs.location {
+	case TabLocationTop:
+		indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.MinSize().Height)
+		indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), theme.Padding())
+	case TabLocationLeading:
+		indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y-scrollOffset.Y)
+		indicatorSize = fyne.NewSize(theme.Padding(), fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y))
+	case TabLocationBottom:
+		indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.Position().Y-theme.Padding())
+		indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), theme.Padding())
+	case TabLocationTrailing:
+		indicatorPos = fyne.NewPos(r.bar.Position().X-theme.Padding(), selectedPos.Y-scrollOffset.Y)
+		indicatorSize = fyne.NewSize(theme.Padding(), fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y))
+	}
+
+	if indicatorPos.X < 0 {
+		indicatorSize.Width = indicatorSize.Width + indicatorPos.X
+		indicatorPos.X = 0
+	}
+	if indicatorPos.Y < 0 {
+		indicatorSize.Height = indicatorSize.Height + indicatorPos.Y
+		indicatorPos.Y = 0
+	}
+	if indicatorSize.Width < 0 || indicatorSize.Height < 0 {
+		r.indicator.FillColor = color.Transparent
+		r.indicator.Refresh()
+		return
+	}
+
+	r.moveIndicator(indicatorPos, indicatorSize, animate)
+}
+
+func (r *docTabsRenderer) updateAllTabs() {
+	if len(r.docTabs.Items) > 0 {
+		r.action.Show()
+	} else {
+		r.action.Hide()
+	}
+}
+
+func (r *docTabsRenderer) updateCreateTab() {
+	if r.docTabs.CreateTab != nil {
+		r.create.SetIcon(theme.ContentAddIcon())
+		r.create.Show()
+	} else {
+		r.create.Hide()
+	}
+}
+
+func (r *docTabsRenderer) updateTabs() {
+	tabCount := len(r.docTabs.Items)
+	r.buildTabButtons(tabCount, r.scroller.Content.(*fyne.Container))
+
+	// Set layout of tab bar containing tab buttons and overflow action
+	if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
+		r.bar.Layout = layout.NewBorderLayout(nil, r.box, nil, nil)
+		r.scroller.Direction = ScrollVerticalOnly
+	} else {
+		r.bar.Layout = layout.NewBorderLayout(nil, nil, nil, r.box)
+		r.scroller.Direction = ScrollHorizontalOnly
+	}
+
+	r.bar.Objects = []fyne.CanvasObject{r.scroller, r.box}
+	r.bar.Refresh()
+}

+ 121 - 0
vendor/fyne.io/fyne/v2/container/layouts.go

@@ -0,0 +1,121 @@
+package container // import "fyne.io/fyne/v2/container"
+
+import (
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/internal"
+	"fyne.io/fyne/v2/layout"
+)
+
+// NewAdaptiveGrid creates a new container with the specified objects and using the grid layout.
+// When in a horizontal arrangement the rowcols parameter will specify the column count, when in vertical
+// it will specify the rows. On mobile this will dynamically refresh when device is rotated.
+//
+// Since: 1.4
+func NewAdaptiveGrid(rowcols int, objects ...fyne.CanvasObject) *fyne.Container {
+	return New(layout.NewAdaptiveGridLayout(rowcols), objects...)
+}
+
+// NewBorder creates a new container with the specified objects and using the border layout.
+// The top, bottom, left and right parameters specify the items that should be placed around edges,
+// the remaining elements will be in the center. Nil can be used to an edge if it should not be filled.
+//
+// Since: 1.4
+func NewBorder(top, bottom, left, right fyne.CanvasObject, objects ...fyne.CanvasObject) *fyne.Container {
+	all := objects
+	if top != nil {
+		all = append(all, top)
+	}
+	if bottom != nil {
+		all = append(all, bottom)
+	}
+	if left != nil {
+		all = append(all, left)
+	}
+	if right != nil {
+		all = append(all, right)
+	}
+
+	if len(objects) == 1 && objects[0] == nil {
+		internal.LogHint("Border layout requires only 4 parameters, optional items cannot be nil")
+		all = all[1:]
+	}
+	return New(layout.NewBorderLayout(top, bottom, left, right), all...)
+}
+
+// NewCenter creates a new container with the specified objects centered in the available space.
+//
+// Since: 1.4
+func NewCenter(objects ...fyne.CanvasObject) *fyne.Container {
+	return New(layout.NewCenterLayout(), objects...)
+}
+
+// NewGridWithColumns creates a new container with the specified objects and using the grid layout with
+// a specified number of columns. The number of rows will depend on how many children are in the container.
+//
+// Since: 1.4
+func NewGridWithColumns(cols int, objects ...fyne.CanvasObject) *fyne.Container {
+	return New(layout.NewGridLayoutWithColumns(cols), objects...)
+}
+
+// NewGridWithRows creates a new container with the specified objects and using the grid layout with
+// a specified number of rows. The number of columns will depend on how many children are in the container.
+//
+// Since: 1.4
+func NewGridWithRows(rows int, objects ...fyne.CanvasObject) *fyne.Container {
+	return New(layout.NewGridLayoutWithRows(rows), objects...)
+}
+
+// NewGridWrap creates a new container with the specified objects and using the gridwrap layout.
+// Every element will be resized to the size parameter and the content will arrange along a row and flow to a
+// new row if the elements don't fit.
+//
+// Since: 1.4
+func NewGridWrap(size fyne.Size, objects ...fyne.CanvasObject) *fyne.Container {
+	return New(layout.NewGridWrapLayout(size), objects...)
+}
+
+// NewHBox creates a new container with the specified objects and using the HBox layout.
+// The objects will be placed in the container from left to right and always displayed
+// at their horizontal MinSize. Use a different layout if the objects are intended
+// to be larger then their horizontal MinSize.
+//
+// Since: 1.4
+func NewHBox(objects ...fyne.CanvasObject) *fyne.Container {
+	return New(layout.NewHBoxLayout(), objects...)
+}
+
+// NewMax creates a new container with the specified objects filling the available space.
+//
+// Since: 1.4
+//
+// Deprecated: Use container.NewStack() instead.
+func NewMax(objects ...fyne.CanvasObject) *fyne.Container {
+	return NewStack(objects...)
+}
+
+// NewPadded creates a new container with the specified objects inset by standard padding size.
+//
+// Since: 1.4
+func NewPadded(objects ...fyne.CanvasObject) *fyne.Container {
+	return New(layout.NewPaddedLayout(), objects...)
+}
+
+// NewStack returns a new container that stacks objects on top of each other.
+// Objects at the end of the container will be stacked on top of objects before.
+// Having only a single object has no impact as CanvasObjects will
+// fill the available space even without a Stack.
+//
+// Since: 2.4
+func NewStack(objects ...fyne.CanvasObject) *fyne.Container {
+	return New(layout.NewStackLayout(), objects...)
+}
+
+// NewVBox creates a new container with the specified objects and using the VBox layout.
+// The objects will be stacked in the container from top to bottom and always displayed
+// at their vertical MinSize. Use a different layout if the objects are intended
+// to be larger then their vertical MinSize.
+//
+// Since: 1.4
+func NewVBox(objects ...fyne.CanvasObject) *fyne.Container {
+	return New(layout.NewVBoxLayout(), objects...)
+}

+ 55 - 0
vendor/fyne.io/fyne/v2/container/scroll.go

@@ -0,0 +1,55 @@
+package container
+
+import (
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/internal/widget"
+)
+
+// Scroll defines a container that is smaller than the Content.
+// The Offset is used to determine the position of the child widgets within the container.
+//
+// Since: 1.4
+type Scroll = widget.Scroll
+
+// ScrollDirection represents the directions in which a Scroll container can scroll its child content.
+//
+// Since: 1.4
+type ScrollDirection = widget.ScrollDirection
+
+// Constants for valid values of ScrollDirection.
+const (
+	// ScrollBoth supports horizontal and vertical scrolling.
+	ScrollBoth ScrollDirection = widget.ScrollBoth
+	// ScrollHorizontalOnly specifies the scrolling should only happen left to right.
+	ScrollHorizontalOnly = widget.ScrollHorizontalOnly
+	// ScrollVerticalOnly specifies the scrolling should only happen top to bottom.
+	ScrollVerticalOnly = widget.ScrollVerticalOnly
+	// ScrollNone turns off scrolling for this container.
+	//
+	// Since: 2.1
+	ScrollNone = widget.ScrollNone
+)
+
+// NewScroll creates a scrollable parent wrapping the specified content.
+// Note that this may cause the MinSize to be smaller than that of the passed object.
+//
+// Since: 1.4
+func NewScroll(content fyne.CanvasObject) *Scroll {
+	return widget.NewScroll(content)
+}
+
+// NewHScroll create a scrollable parent wrapping the specified content.
+// Note that this may cause the MinSize.Width to be smaller than that of the passed object.
+//
+// Since: 1.4
+func NewHScroll(content fyne.CanvasObject) *Scroll {
+	return widget.NewHScroll(content)
+}
+
+// NewVScroll a scrollable parent wrapping the specified content.
+// Note that this may cause the MinSize.Height to be smaller than that of the passed object.
+//
+// Since: 1.4
+func NewVScroll(content fyne.CanvasObject) *Scroll {
+	return widget.NewVScroll(content)
+}

+ 369 - 0
vendor/fyne.io/fyne/v2/container/split.go

@@ -0,0 +1,369 @@
+package container
+
+import (
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/canvas"
+	"fyne.io/fyne/v2/driver/desktop"
+	"fyne.io/fyne/v2/theme"
+	"fyne.io/fyne/v2/widget"
+)
+
+// Declare conformity with CanvasObject interface
+var _ fyne.CanvasObject = (*Split)(nil)
+
+// Split defines a container whose size is split between two children.
+//
+// Since: 1.4
+type Split struct {
+	widget.BaseWidget
+	Offset     float64
+	Horizontal bool
+	Leading    fyne.CanvasObject
+	Trailing   fyne.CanvasObject
+}
+
+// NewHSplit creates a horizontally arranged container with the specified leading and trailing elements.
+// A vertical split bar that can be dragged will be added between the elements.
+//
+// Since: 1.4
+func NewHSplit(leading, trailing fyne.CanvasObject) *Split {
+	return newSplitContainer(true, leading, trailing)
+}
+
+// NewVSplit creates a vertically arranged container with the specified top and bottom elements.
+// A horizontal split bar that can be dragged will be added between the elements.
+//
+// Since: 1.4
+func NewVSplit(top, bottom fyne.CanvasObject) *Split {
+	return newSplitContainer(false, top, bottom)
+}
+
+func newSplitContainer(horizontal bool, leading, trailing fyne.CanvasObject) *Split {
+	s := &Split{
+		Offset:     0.5, // Sensible default, can be overridden with SetOffset
+		Horizontal: horizontal,
+		Leading:    leading,
+		Trailing:   trailing,
+	}
+	s.BaseWidget.ExtendBaseWidget(s)
+	return s
+}
+
+// CreateRenderer is a private method to Fyne which links this widget to its renderer
+func (s *Split) CreateRenderer() fyne.WidgetRenderer {
+	s.BaseWidget.ExtendBaseWidget(s)
+	d := newDivider(s)
+	return &splitContainerRenderer{
+		split:   s,
+		divider: d,
+		objects: []fyne.CanvasObject{s.Leading, d, s.Trailing},
+	}
+}
+
+// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality.
+//
+// Deprecated: Support for extending containers is being removed
+func (s *Split) ExtendBaseWidget(wid fyne.Widget) {
+	s.BaseWidget.ExtendBaseWidget(wid)
+}
+
+// SetOffset sets the offset (0.0 to 1.0) of the Split divider.
+// 0.0 - Leading is min size, Trailing uses all remaining space.
+// 0.5 - Leading & Trailing equally share the available space.
+// 1.0 - Trailing is min size, Leading uses all remaining space.
+func (s *Split) SetOffset(offset float64) {
+	if s.Offset == offset {
+		return
+	}
+	s.Offset = offset
+	s.Refresh()
+}
+
+var _ fyne.WidgetRenderer = (*splitContainerRenderer)(nil)
+
+type splitContainerRenderer struct {
+	split   *Split
+	divider *divider
+	objects []fyne.CanvasObject
+}
+
+func (r *splitContainerRenderer) Destroy() {
+}
+
+func (r *splitContainerRenderer) Layout(size fyne.Size) {
+	var dividerPos, leadingPos, trailingPos fyne.Position
+	var dividerSize, leadingSize, trailingSize fyne.Size
+
+	if r.split.Horizontal {
+		lw, tw := r.computeSplitLengths(size.Width, r.minLeadingWidth(), r.minTrailingWidth())
+		leadingPos.X = 0
+		leadingSize.Width = lw
+		leadingSize.Height = size.Height
+		dividerPos.X = lw
+		dividerSize.Width = dividerThickness()
+		dividerSize.Height = size.Height
+		trailingPos.X = lw + dividerSize.Width
+		trailingSize.Width = tw
+		trailingSize.Height = size.Height
+	} else {
+		lh, th := r.computeSplitLengths(size.Height, r.minLeadingHeight(), r.minTrailingHeight())
+		leadingPos.Y = 0
+		leadingSize.Width = size.Width
+		leadingSize.Height = lh
+		dividerPos.Y = lh
+		dividerSize.Width = size.Width
+		dividerSize.Height = dividerThickness()
+		trailingPos.Y = lh + dividerSize.Height
+		trailingSize.Width = size.Width
+		trailingSize.Height = th
+	}
+
+	r.divider.Move(dividerPos)
+	r.divider.Resize(dividerSize)
+	r.split.Leading.Move(leadingPos)
+	r.split.Leading.Resize(leadingSize)
+	r.split.Trailing.Move(trailingPos)
+	r.split.Trailing.Resize(trailingSize)
+	canvas.Refresh(r.divider)
+}
+
+func (r *splitContainerRenderer) MinSize() fyne.Size {
+	s := fyne.NewSize(0, 0)
+	for _, o := range r.objects {
+		min := o.MinSize()
+		if r.split.Horizontal {
+			s.Width += min.Width
+			s.Height = fyne.Max(s.Height, min.Height)
+		} else {
+			s.Width = fyne.Max(s.Width, min.Width)
+			s.Height += min.Height
+		}
+	}
+	return s
+}
+
+func (r *splitContainerRenderer) Objects() []fyne.CanvasObject {
+	return r.objects
+}
+
+func (r *splitContainerRenderer) Refresh() {
+	r.objects[0] = r.split.Leading
+	// [1] is divider which doesn't change
+	r.objects[2] = r.split.Trailing
+	r.Layout(r.split.Size())
+	canvas.Refresh(r.split)
+}
+
+func (r *splitContainerRenderer) computeSplitLengths(total, lMin, tMin float32) (float32, float32) {
+	available := float64(total - dividerThickness())
+	if available <= 0 {
+		return 0, 0
+	}
+	ld := float64(lMin)
+	tr := float64(tMin)
+	offset := r.split.Offset
+
+	min := ld / available
+	max := 1 - tr/available
+	if min <= max {
+		if offset < min {
+			offset = min
+		}
+		if offset > max {
+			offset = max
+		}
+	} else {
+		offset = ld / (ld + tr)
+	}
+
+	ld = offset * available
+	tr = available - ld
+	return float32(ld), float32(tr)
+}
+
+func (r *splitContainerRenderer) minLeadingWidth() float32 {
+	if r.split.Leading.Visible() {
+		return r.split.Leading.MinSize().Width
+	}
+	return 0
+}
+
+func (r *splitContainerRenderer) minLeadingHeight() float32 {
+	if r.split.Leading.Visible() {
+		return r.split.Leading.MinSize().Height
+	}
+	return 0
+}
+
+func (r *splitContainerRenderer) minTrailingWidth() float32 {
+	if r.split.Trailing.Visible() {
+		return r.split.Trailing.MinSize().Width
+	}
+	return 0
+}
+
+func (r *splitContainerRenderer) minTrailingHeight() float32 {
+	if r.split.Trailing.Visible() {
+		return r.split.Trailing.MinSize().Height
+	}
+	return 0
+}
+
+// Declare conformity with interfaces
+var _ fyne.CanvasObject = (*divider)(nil)
+var _ fyne.Draggable = (*divider)(nil)
+var _ desktop.Cursorable = (*divider)(nil)
+var _ desktop.Hoverable = (*divider)(nil)
+
+type divider struct {
+	widget.BaseWidget
+	split          *Split
+	hovered        bool
+	startDragOff   *fyne.Position
+	currentDragPos fyne.Position
+}
+
+func newDivider(split *Split) *divider {
+	d := &divider{
+		split: split,
+	}
+	d.ExtendBaseWidget(d)
+	return d
+}
+
+// CreateRenderer is a private method to Fyne which links this widget to its renderer
+func (d *divider) CreateRenderer() fyne.WidgetRenderer {
+	d.ExtendBaseWidget(d)
+	background := canvas.NewRectangle(theme.ShadowColor())
+	foreground := canvas.NewRectangle(theme.ForegroundColor())
+	return &dividerRenderer{
+		divider:    d,
+		background: background,
+		foreground: foreground,
+		objects:    []fyne.CanvasObject{background, foreground},
+	}
+}
+
+func (d *divider) Cursor() desktop.Cursor {
+	if d.split.Horizontal {
+		return desktop.HResizeCursor
+	}
+	return desktop.VResizeCursor
+}
+
+func (d *divider) DragEnd() {
+	d.startDragOff = nil
+}
+
+func (d *divider) Dragged(e *fyne.DragEvent) {
+	if d.startDragOff == nil {
+		d.currentDragPos = d.Position().Add(e.Position)
+		start := e.Position.Subtract(e.Dragged)
+		d.startDragOff = &start
+	} else {
+		d.currentDragPos = d.currentDragPos.Add(e.Dragged)
+	}
+
+	x, y := d.currentDragPos.Components()
+	var offset, leadingRatio, trailingRatio float64
+	if d.split.Horizontal {
+		widthFree := float64(d.split.Size().Width - dividerThickness())
+		leadingRatio = float64(d.split.Leading.MinSize().Width) / widthFree
+		trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Width) / widthFree)
+		offset = float64(x-d.startDragOff.X) / widthFree
+	} else {
+		heightFree := float64(d.split.Size().Height - dividerThickness())
+		leadingRatio = float64(d.split.Leading.MinSize().Height) / heightFree
+		trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Height) / heightFree)
+		offset = float64(y-d.startDragOff.Y) / heightFree
+	}
+
+	if offset < leadingRatio {
+		offset = leadingRatio
+	}
+	if offset > trailingRatio {
+		offset = trailingRatio
+	}
+	d.split.SetOffset(offset)
+}
+
+func (d *divider) MouseIn(event *desktop.MouseEvent) {
+	d.hovered = true
+	d.split.Refresh()
+}
+
+func (d *divider) MouseMoved(event *desktop.MouseEvent) {}
+
+func (d *divider) MouseOut() {
+	d.hovered = false
+	d.split.Refresh()
+}
+
+var _ fyne.WidgetRenderer = (*dividerRenderer)(nil)
+
+type dividerRenderer struct {
+	divider    *divider
+	background *canvas.Rectangle
+	foreground *canvas.Rectangle
+	objects    []fyne.CanvasObject
+}
+
+func (r *dividerRenderer) Destroy() {
+}
+
+func (r *dividerRenderer) Layout(size fyne.Size) {
+	r.background.Resize(size)
+	var x, y, w, h float32
+	if r.divider.split.Horizontal {
+		x = (dividerThickness() - handleThickness()) / 2
+		y = (size.Height - handleLength()) / 2
+		w = handleThickness()
+		h = handleLength()
+	} else {
+		x = (size.Width - handleLength()) / 2
+		y = (dividerThickness() - handleThickness()) / 2
+		w = handleLength()
+		h = handleThickness()
+	}
+	r.foreground.Move(fyne.NewPos(x, y))
+	r.foreground.Resize(fyne.NewSize(w, h))
+}
+
+func (r *dividerRenderer) MinSize() fyne.Size {
+	if r.divider.split.Horizontal {
+		return fyne.NewSize(dividerThickness(), dividerLength())
+	}
+	return fyne.NewSize(dividerLength(), dividerThickness())
+}
+
+func (r *dividerRenderer) Objects() []fyne.CanvasObject {
+	return r.objects
+}
+
+func (r *dividerRenderer) Refresh() {
+	if r.divider.hovered {
+		r.background.FillColor = theme.HoverColor()
+	} else {
+		r.background.FillColor = theme.ShadowColor()
+	}
+	r.background.Refresh()
+	r.foreground.FillColor = theme.ForegroundColor()
+	r.foreground.Refresh()
+	r.Layout(r.divider.Size())
+}
+
+func dividerThickness() float32 {
+	return theme.Padding() * 2
+}
+
+func dividerLength() float32 {
+	return theme.Padding() * 6
+}
+
+func handleThickness() float32 {
+	return theme.Padding() / 2
+}
+
+func handleLength() float32 {
+	return theme.Padding() * 4
+}

+ 843 - 0
vendor/fyne.io/fyne/v2/container/tabs.go

@@ -0,0 +1,843 @@
+package container
+
+import (
+	"sync"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/canvas"
+	"fyne.io/fyne/v2/driver/desktop"
+	"fyne.io/fyne/v2/internal"
+	"fyne.io/fyne/v2/theme"
+	"fyne.io/fyne/v2/widget"
+)
+
+// TabItem represents a single view in a tab view.
+// The Text and Icon are used for the tab button and the Content is shown when the corresponding tab is active.
+//
+// Since: 1.4
+type TabItem struct {
+	Text    string
+	Icon    fyne.Resource
+	Content fyne.CanvasObject
+
+	button *tabButton
+}
+
+// Disabled returns whether or not the TabItem is disabled.
+//
+// Since: 2.3
+func (ti *TabItem) Disabled() bool {
+	if ti.button != nil {
+		return ti.button.Disabled()
+	}
+	return false
+}
+
+func (ti *TabItem) disable() {
+	if ti.button != nil {
+		ti.button.Disable()
+	}
+}
+
+func (ti *TabItem) enable() {
+	if ti.button != nil {
+		ti.button.Enable()
+	}
+}
+
+// TabLocation is the location where the tabs of a tab container should be rendered
+//
+// Since: 1.4
+type TabLocation int
+
+// TabLocation values
+const (
+	TabLocationTop TabLocation = iota
+	TabLocationLeading
+	TabLocationBottom
+	TabLocationTrailing
+)
+
+// NewTabItem creates a new item for a tabbed widget - each item specifies the content and a label for its tab.
+//
+// Since: 1.4
+func NewTabItem(text string, content fyne.CanvasObject) *TabItem {
+	return &TabItem{Text: text, Content: content}
+}
+
+// NewTabItemWithIcon creates a new item for a tabbed widget - each item specifies the content and a label with an icon for its tab.
+//
+// Since: 1.4
+func NewTabItemWithIcon(text string, icon fyne.Resource, content fyne.CanvasObject) *TabItem {
+	return &TabItem{Text: text, Icon: icon, Content: content}
+}
+
+type baseTabs interface {
+	onUnselected() func(*TabItem)
+	onSelected() func(*TabItem)
+
+	items() []*TabItem
+	setItems([]*TabItem)
+
+	selected() int
+	setSelected(int)
+
+	tabLocation() TabLocation
+
+	transitioning() bool
+	setTransitioning(bool)
+}
+
+func tabsAdjustedLocation(l TabLocation) TabLocation {
+	// Mobile has limited screen space, so don't put app tab bar on long edges
+	if d := fyne.CurrentDevice(); d.IsMobile() {
+		if o := d.Orientation(); fyne.IsVertical(o) {
+			if l == TabLocationLeading {
+				return TabLocationTop
+			} else if l == TabLocationTrailing {
+				return TabLocationBottom
+			}
+		} else {
+			if l == TabLocationTop {
+				return TabLocationLeading
+			} else if l == TabLocationBottom {
+				return TabLocationTrailing
+			}
+		}
+	}
+
+	return l
+}
+
+func buildPopUpMenu(t baseTabs, button *widget.Button, items []*fyne.MenuItem) *widget.PopUpMenu {
+	d := fyne.CurrentApp().Driver()
+	c := d.CanvasForObject(button)
+	popUpMenu := widget.NewPopUpMenu(fyne.NewMenu("", items...), c)
+	buttonPos := d.AbsolutePositionForObject(button)
+	buttonSize := button.Size()
+	popUpMin := popUpMenu.MinSize()
+	var popUpPos fyne.Position
+	switch t.tabLocation() {
+	case TabLocationLeading:
+		popUpPos.X = buttonPos.X + buttonSize.Width
+		popUpPos.Y = buttonPos.Y + buttonSize.Height - popUpMin.Height
+	case TabLocationTrailing:
+		popUpPos.X = buttonPos.X - popUpMin.Width
+		popUpPos.Y = buttonPos.Y + buttonSize.Height - popUpMin.Height
+	case TabLocationTop:
+		popUpPos.X = buttonPos.X + buttonSize.Width - popUpMin.Width
+		popUpPos.Y = buttonPos.Y + buttonSize.Height
+	case TabLocationBottom:
+		popUpPos.X = buttonPos.X + buttonSize.Width - popUpMin.Width
+		popUpPos.Y = buttonPos.Y - popUpMin.Height
+	}
+	if popUpPos.X < 0 {
+		popUpPos.X = 0
+	}
+	if popUpPos.Y < 0 {
+		popUpPos.Y = 0
+	}
+	popUpMenu.ShowAtPosition(popUpPos)
+	return popUpMenu
+}
+
+func removeIndex(t baseTabs, index int) {
+	items := t.items()
+	if index < 0 || index >= len(items) {
+		return
+	}
+	setItems(t, append(items[:index], items[index+1:]...))
+	if s := t.selected(); index < s {
+		t.setSelected(s - 1)
+	}
+}
+
+func removeItem(t baseTabs, item *TabItem) {
+	for index, existingItem := range t.items() {
+		if existingItem == item {
+			removeIndex(t, index)
+			break
+		}
+	}
+}
+
+func selected(t baseTabs) *TabItem {
+	selected := t.selected()
+	items := t.items()
+	if selected < 0 || selected >= len(items) {
+		return nil
+	}
+	return items[selected]
+}
+
+func selectIndex(t baseTabs, index int) {
+	selected := t.selected()
+
+	if selected == index {
+		// No change, so do nothing
+		return
+	}
+
+	items := t.items()
+
+	if f := t.onUnselected(); f != nil && selected >= 0 && selected < len(items) {
+		// Notification of unselected
+		f(items[selected])
+	}
+
+	if index < 0 || index >= len(items) {
+		// Out of bounds, so do nothing
+		return
+	}
+
+	t.setTransitioning(true)
+	t.setSelected(index)
+
+	if f := t.onSelected(); f != nil {
+		// Notification of selected
+		f(items[index])
+	}
+}
+
+func selectItem(t baseTabs, item *TabItem) {
+	for i, child := range t.items() {
+		if child == item {
+			selectIndex(t, i)
+			return
+		}
+	}
+}
+
+func setItems(t baseTabs, items []*TabItem) {
+	if internal.HintsEnabled && mismatchedTabItems(items) {
+		internal.LogHint("Tab items should all have the same type of content (text, icons or both)")
+	}
+	t.setItems(items)
+	selected := t.selected()
+	count := len(items)
+	switch {
+	case count == 0:
+		// No items available to be selected
+		selectIndex(t, -1) // Unsure OnUnselected gets called if applicable
+		t.setSelected(-1)
+	case selected < 0:
+		// Current is first tab item
+		selectIndex(t, 0)
+	case selected >= count:
+		// Current doesn't exist, select last tab
+		selectIndex(t, count-1)
+	}
+}
+
+func disableIndex(t baseTabs, index int) {
+	items := t.items()
+	if index < 0 || index >= len(items) {
+		return
+	}
+
+	item := items[index]
+	item.disable()
+
+	if selected(t) == item {
+		// the disabled tab is currently selected, so select the first enabled tab
+		for i, it := range items {
+			if !it.Disabled() {
+				selectIndex(t, i)
+				break
+			}
+		}
+	}
+
+	if selected(t) == item {
+		selectIndex(t, -1) // no other tab is able to be selected
+	}
+}
+
+func disableItem(t baseTabs, item *TabItem) {
+	for i, it := range t.items() {
+		if it == item {
+			disableIndex(t, i)
+			return
+		}
+	}
+}
+
+func enableIndex(t baseTabs, index int) {
+	items := t.items()
+	if index < 0 || index >= len(items) {
+		return
+	}
+
+	item := items[index]
+	item.enable()
+}
+
+func enableItem(t baseTabs, item *TabItem) {
+	for i, it := range t.items() {
+		if it == item {
+			enableIndex(t, i)
+			return
+		}
+	}
+}
+
+type baseTabsRenderer struct {
+	positionAnimation, sizeAnimation *fyne.Animation
+
+	lastIndicatorMutex  sync.RWMutex
+	lastIndicatorPos    fyne.Position
+	lastIndicatorSize   fyne.Size
+	lastIndicatorHidden bool
+
+	action             *widget.Button
+	bar                *fyne.Container
+	divider, indicator *canvas.Rectangle
+}
+
+func (r *baseTabsRenderer) Destroy() {
+}
+
+func (r *baseTabsRenderer) applyTheme(t baseTabs) {
+	if r.action != nil {
+		r.action.SetIcon(moreIcon(t))
+	}
+	r.divider.FillColor = theme.ShadowColor()
+	r.indicator.FillColor = theme.PrimaryColor()
+	r.indicator.CornerRadius = theme.SelectionRadiusSize()
+}
+
+func (r *baseTabsRenderer) layout(t baseTabs, size fyne.Size) {
+	var (
+		barPos, dividerPos, contentPos    fyne.Position
+		barSize, dividerSize, contentSize fyne.Size
+	)
+
+	barMin := r.bar.MinSize()
+
+	padding := theme.Padding()
+	switch t.tabLocation() {
+	case TabLocationTop:
+		barHeight := barMin.Height
+		barPos = fyne.NewPos(0, 0)
+		barSize = fyne.NewSize(size.Width, barHeight)
+		dividerPos = fyne.NewPos(0, barHeight)
+		dividerSize = fyne.NewSize(size.Width, padding)
+		contentPos = fyne.NewPos(0, barHeight+padding)
+		contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding)
+	case TabLocationLeading:
+		barWidth := barMin.Width
+		barPos = fyne.NewPos(0, 0)
+		barSize = fyne.NewSize(barWidth, size.Height)
+		dividerPos = fyne.NewPos(barWidth, 0)
+		dividerSize = fyne.NewSize(padding, size.Height)
+		contentPos = fyne.NewPos(barWidth+theme.Padding(), 0)
+		contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height)
+	case TabLocationBottom:
+		barHeight := barMin.Height
+		barPos = fyne.NewPos(0, size.Height-barHeight)
+		barSize = fyne.NewSize(size.Width, barHeight)
+		dividerPos = fyne.NewPos(0, size.Height-barHeight-padding)
+		dividerSize = fyne.NewSize(size.Width, padding)
+		contentPos = fyne.NewPos(0, 0)
+		contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding)
+	case TabLocationTrailing:
+		barWidth := barMin.Width
+		barPos = fyne.NewPos(size.Width-barWidth, 0)
+		barSize = fyne.NewSize(barWidth, size.Height)
+		dividerPos = fyne.NewPos(size.Width-barWidth-padding, 0)
+		dividerSize = fyne.NewSize(padding, size.Height)
+		contentPos = fyne.NewPos(0, 0)
+		contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height)
+	}
+
+	r.bar.Move(barPos)
+	r.bar.Resize(barSize)
+	r.divider.Move(dividerPos)
+	r.divider.Resize(dividerSize)
+	selected := t.selected()
+	for i, ti := range t.items() {
+		if i == selected {
+			ti.Content.Move(contentPos)
+			ti.Content.Resize(contentSize)
+			ti.Content.Show()
+		} else {
+			ti.Content.Hide()
+		}
+	}
+}
+
+func (r *baseTabsRenderer) minSize(t baseTabs) fyne.Size {
+	pad := theme.Padding()
+	buttonPad := pad
+	barMin := r.bar.MinSize()
+	tabsMin := r.bar.Objects[0].MinSize()
+	accessory := r.bar.Objects[1]
+	accessoryMin := accessory.MinSize()
+	if scroll, ok := r.bar.Objects[0].(*Scroll); ok && len(scroll.Content.(*fyne.Container).Objects) == 0 {
+		tabsMin = fyne.Size{} // scroller forces 32 where we don't need any space
+		buttonPad = 0
+	} else if group, ok := r.bar.Objects[0].(*fyne.Container); ok && len(group.Objects) > 0 {
+		tabsMin = group.Objects[0].MinSize()
+		buttonPad = 0
+	}
+	if !accessory.Visible() || accessoryMin.Width == 0 {
+		buttonPad = 0
+		accessoryMin = fyne.Size{}
+	}
+
+	contentMin := fyne.NewSize(0, 0)
+	for _, content := range t.items() {
+		contentMin = contentMin.Max(content.Content.MinSize())
+	}
+
+	switch t.tabLocation() {
+	case TabLocationLeading, TabLocationTrailing:
+		return fyne.NewSize(barMin.Width+contentMin.Width+pad,
+			fyne.Max(contentMin.Height, accessoryMin.Height+buttonPad+tabsMin.Height))
+	default:
+		return fyne.NewSize(fyne.Max(contentMin.Width, accessoryMin.Width+buttonPad+tabsMin.Width),
+			barMin.Height+contentMin.Height+pad)
+	}
+}
+
+func (r *baseTabsRenderer) moveIndicator(pos fyne.Position, siz fyne.Size, animate bool) {
+	r.lastIndicatorMutex.RLock()
+	isSameState := r.lastIndicatorPos.Subtract(pos).IsZero() && r.lastIndicatorSize.Subtract(siz).IsZero() &&
+		r.lastIndicatorHidden == r.indicator.Hidden
+	r.lastIndicatorMutex.RUnlock()
+	if isSameState {
+		return
+	}
+
+	if r.positionAnimation != nil {
+		r.positionAnimation.Stop()
+		r.positionAnimation = nil
+	}
+	if r.sizeAnimation != nil {
+		r.sizeAnimation.Stop()
+		r.sizeAnimation = nil
+	}
+
+	r.indicator.FillColor = theme.PrimaryColor()
+	if r.indicator.Position().IsZero() {
+		r.indicator.Move(pos)
+		r.indicator.Resize(siz)
+		r.indicator.Refresh()
+		return
+	}
+
+	r.lastIndicatorMutex.Lock()
+	r.lastIndicatorPos = pos
+	r.lastIndicatorSize = siz
+	r.lastIndicatorHidden = r.indicator.Hidden
+	r.lastIndicatorMutex.Unlock()
+
+	if animate && fyne.CurrentApp().Settings().ShowAnimations() {
+		r.positionAnimation = canvas.NewPositionAnimation(r.indicator.Position(), pos, canvas.DurationShort, func(p fyne.Position) {
+			r.indicator.Move(p)
+			r.indicator.Refresh()
+			if pos == p {
+				r.positionAnimation.Stop()
+				r.positionAnimation = nil
+			}
+		})
+		r.sizeAnimation = canvas.NewSizeAnimation(r.indicator.Size(), siz, canvas.DurationShort, func(s fyne.Size) {
+			r.indicator.Resize(s)
+			r.indicator.Refresh()
+			if siz == s {
+				r.sizeAnimation.Stop()
+				r.sizeAnimation = nil
+			}
+		})
+
+		r.positionAnimation.Start()
+		r.sizeAnimation.Start()
+	} else {
+		r.indicator.Move(pos)
+		r.indicator.Resize(siz)
+		r.indicator.Refresh()
+	}
+}
+
+func (r *baseTabsRenderer) objects(t baseTabs) []fyne.CanvasObject {
+	objects := []fyne.CanvasObject{r.bar, r.divider, r.indicator}
+	if i, is := t.selected(), t.items(); i >= 0 && i < len(is) {
+		objects = append(objects, is[i].Content)
+	}
+	return objects
+}
+
+func (r *baseTabsRenderer) refresh(t baseTabs) {
+	r.applyTheme(t)
+
+	r.bar.Refresh()
+	r.divider.Refresh()
+	r.indicator.Refresh()
+}
+
+type buttonIconPosition int
+
+const (
+	buttonIconInline buttonIconPosition = iota
+	buttonIconTop
+)
+
+var _ fyne.Widget = (*tabButton)(nil)
+var _ fyne.Tappable = (*tabButton)(nil)
+var _ desktop.Hoverable = (*tabButton)(nil)
+
+type tabButton struct {
+	widget.DisableableWidget
+	hovered       bool
+	icon          fyne.Resource
+	iconPosition  buttonIconPosition
+	importance    widget.Importance
+	onTapped      func()
+	onClosed      func()
+	text          string
+	textAlignment fyne.TextAlign
+}
+
+func (b *tabButton) CreateRenderer() fyne.WidgetRenderer {
+	b.ExtendBaseWidget(b)
+	background := canvas.NewRectangle(theme.HoverColor())
+	background.CornerRadius = theme.SelectionRadiusSize()
+	background.Hide()
+	icon := canvas.NewImageFromResource(b.icon)
+	if b.icon == nil {
+		icon.Hide()
+	}
+
+	label := canvas.NewText(b.text, theme.ForegroundColor())
+	label.TextStyle.Bold = true
+
+	close := &tabCloseButton{
+		parent: b,
+		onTapped: func() {
+			if f := b.onClosed; f != nil {
+				f()
+			}
+		},
+	}
+	close.ExtendBaseWidget(close)
+	close.Hide()
+
+	objects := []fyne.CanvasObject{background, label, close, icon}
+	r := &tabButtonRenderer{
+		button:     b,
+		background: background,
+		icon:       icon,
+		label:      label,
+		close:      close,
+		objects:    objects,
+	}
+	r.Refresh()
+	return r
+}
+
+func (b *tabButton) MinSize() fyne.Size {
+	b.ExtendBaseWidget(b)
+	return b.BaseWidget.MinSize()
+}
+
+func (b *tabButton) MouseIn(*desktop.MouseEvent) {
+	b.hovered = true
+	b.Refresh()
+}
+
+func (b *tabButton) MouseMoved(*desktop.MouseEvent) {
+}
+
+func (b *tabButton) MouseOut() {
+	b.hovered = false
+	b.Refresh()
+}
+
+func (b *tabButton) Tapped(*fyne.PointEvent) {
+	if b.Disabled() {
+		return
+	}
+
+	b.onTapped()
+}
+
+type tabButtonRenderer struct {
+	button     *tabButton
+	background *canvas.Rectangle
+	icon       *canvas.Image
+	label      *canvas.Text
+	close      *tabCloseButton
+	objects    []fyne.CanvasObject
+}
+
+func (r *tabButtonRenderer) Destroy() {
+}
+
+func (r *tabButtonRenderer) Layout(size fyne.Size) {
+	r.background.Resize(size)
+	padding := r.padding()
+	innerSize := size.Subtract(padding)
+	innerOffset := fyne.NewPos(padding.Width/2, padding.Height/2)
+	labelShift := float32(0)
+	if r.icon.Visible() {
+		iconSize := r.iconSize()
+		var iconOffset fyne.Position
+		if r.button.iconPosition == buttonIconTop {
+			iconOffset = fyne.NewPos((innerSize.Width-iconSize)/2, 0)
+		} else {
+			iconOffset = fyne.NewPos(0, (innerSize.Height-iconSize)/2)
+		}
+		r.icon.Resize(fyne.NewSquareSize(iconSize))
+		r.icon.Move(innerOffset.Add(iconOffset))
+		labelShift = iconSize + theme.Padding()
+	}
+	if r.label.Text != "" {
+		var labelOffset fyne.Position
+		var labelSize fyne.Size
+		if r.button.iconPosition == buttonIconTop {
+			labelOffset = fyne.NewPos(0, labelShift)
+			labelSize = fyne.NewSize(innerSize.Width, r.label.MinSize().Height)
+		} else {
+			labelOffset = fyne.NewPos(labelShift, 0)
+			labelSize = fyne.NewSize(innerSize.Width-labelShift, innerSize.Height)
+		}
+		r.label.Resize(labelSize)
+		r.label.Move(innerOffset.Add(labelOffset))
+	}
+	inlineIconSize := theme.IconInlineSize()
+	r.close.Move(fyne.NewPos(size.Width-inlineIconSize-theme.Padding(), (size.Height-inlineIconSize)/2))
+	r.close.Resize(fyne.NewSquareSize(inlineIconSize))
+}
+
+func (r *tabButtonRenderer) MinSize() fyne.Size {
+	var contentWidth, contentHeight float32
+	textSize := r.label.MinSize()
+	iconSize := r.iconSize()
+	padding := theme.Padding()
+	if r.button.iconPosition == buttonIconTop {
+		contentWidth = fyne.Max(textSize.Width, iconSize)
+		if r.icon.Visible() {
+			contentHeight += iconSize
+		}
+		if r.label.Text != "" {
+			if r.icon.Visible() {
+				contentHeight += padding
+			}
+			contentHeight += textSize.Height
+		}
+	} else {
+		contentHeight = fyne.Max(textSize.Height, iconSize)
+		if r.icon.Visible() {
+			contentWidth += iconSize
+		}
+		if r.label.Text != "" {
+			if r.icon.Visible() {
+				contentWidth += padding
+			}
+			contentWidth += textSize.Width
+		}
+	}
+	if r.button.onClosed != nil {
+		inlineIconSize := theme.IconInlineSize()
+		contentWidth += inlineIconSize + padding
+		contentHeight = fyne.Max(contentHeight, inlineIconSize)
+	}
+	return fyne.NewSize(contentWidth, contentHeight).Add(r.padding())
+}
+
+func (r *tabButtonRenderer) Objects() []fyne.CanvasObject {
+	return r.objects
+}
+
+func (r *tabButtonRenderer) Refresh() {
+	if r.button.hovered && !r.button.Disabled() {
+		r.background.FillColor = theme.HoverColor()
+		r.background.CornerRadius = theme.SelectionRadiusSize()
+		r.background.Show()
+	} else {
+		r.background.Hide()
+	}
+	r.background.Refresh()
+
+	r.label.Text = r.button.text
+	r.label.Alignment = r.button.textAlignment
+	if !r.button.Disabled() {
+		if r.button.importance == widget.HighImportance {
+			r.label.Color = theme.PrimaryColor()
+		} else {
+			r.label.Color = theme.ForegroundColor()
+		}
+	} else {
+		r.label.Color = theme.DisabledColor()
+	}
+	r.label.TextSize = theme.TextSize()
+	if r.button.text == "" {
+		r.label.Hide()
+	} else {
+		r.label.Show()
+	}
+
+	r.icon.Resource = r.button.icon
+	if r.icon.Resource != nil {
+		r.icon.Show()
+		switch res := r.icon.Resource.(type) {
+		case *theme.ThemedResource:
+			if r.button.importance == widget.HighImportance {
+				r.icon.Resource = theme.NewPrimaryThemedResource(res)
+				r.icon.Refresh()
+			}
+		case *theme.PrimaryThemedResource:
+			if r.button.importance != widget.HighImportance {
+				r.icon.Resource = res.Original()
+				r.icon.Refresh()
+			}
+		}
+	} else {
+		r.icon.Hide()
+	}
+
+	if d := fyne.CurrentDevice(); r.button.onClosed != nil && (d.IsMobile() || r.button.hovered || r.close.hovered) {
+		r.close.Show()
+	} else {
+		r.close.Hide()
+	}
+	r.close.Refresh()
+
+	canvas.Refresh(r.button)
+}
+
+func (r *tabButtonRenderer) iconSize() float32 {
+	if r.button.iconPosition == buttonIconTop {
+		return 2 * theme.IconInlineSize()
+	}
+
+	return theme.IconInlineSize()
+}
+
+func (r *tabButtonRenderer) padding() fyne.Size {
+	padding := theme.InnerPadding()
+	if r.label.Text != "" && r.button.iconPosition == buttonIconInline {
+		return fyne.NewSquareSize(padding * 2)
+	}
+	return fyne.NewSize(padding, padding*2)
+}
+
+var _ fyne.Widget = (*tabCloseButton)(nil)
+var _ fyne.Tappable = (*tabCloseButton)(nil)
+var _ desktop.Hoverable = (*tabCloseButton)(nil)
+
+type tabCloseButton struct {
+	widget.BaseWidget
+	parent   *tabButton
+	hovered  bool
+	onTapped func()
+}
+
+func (b *tabCloseButton) CreateRenderer() fyne.WidgetRenderer {
+	b.ExtendBaseWidget(b)
+	background := canvas.NewRectangle(theme.HoverColor())
+	background.CornerRadius = theme.SelectionRadiusSize()
+	background.Hide()
+	icon := canvas.NewImageFromResource(theme.CancelIcon())
+
+	r := &tabCloseButtonRenderer{
+		button:     b,
+		background: background,
+		icon:       icon,
+		objects:    []fyne.CanvasObject{background, icon},
+	}
+	r.Refresh()
+	return r
+}
+
+func (b *tabCloseButton) MinSize() fyne.Size {
+	b.ExtendBaseWidget(b)
+	return b.BaseWidget.MinSize()
+}
+
+func (b *tabCloseButton) MouseIn(*desktop.MouseEvent) {
+	b.hovered = true
+	b.parent.Refresh()
+}
+
+func (b *tabCloseButton) MouseMoved(*desktop.MouseEvent) {
+}
+
+func (b *tabCloseButton) MouseOut() {
+	b.hovered = false
+	b.parent.Refresh()
+}
+
+func (b *tabCloseButton) Tapped(*fyne.PointEvent) {
+	b.onTapped()
+}
+
+type tabCloseButtonRenderer struct {
+	button     *tabCloseButton
+	background *canvas.Rectangle
+	icon       *canvas.Image
+	objects    []fyne.CanvasObject
+}
+
+func (r *tabCloseButtonRenderer) Destroy() {
+}
+
+func (r *tabCloseButtonRenderer) Layout(size fyne.Size) {
+	r.background.Resize(size)
+	r.icon.Resize(size)
+}
+
+func (r *tabCloseButtonRenderer) MinSize() fyne.Size {
+	return fyne.NewSquareSize(theme.IconInlineSize())
+}
+
+func (r *tabCloseButtonRenderer) Objects() []fyne.CanvasObject {
+	return r.objects
+}
+
+func (r *tabCloseButtonRenderer) Refresh() {
+	if r.button.hovered {
+		r.background.FillColor = theme.HoverColor()
+		r.background.CornerRadius = theme.SelectionRadiusSize()
+		r.background.Show()
+	} else {
+		r.background.Hide()
+	}
+	r.background.Refresh()
+	switch res := r.icon.Resource.(type) {
+	case *theme.ThemedResource:
+		if r.button.parent.importance == widget.HighImportance {
+			r.icon.Resource = theme.NewPrimaryThemedResource(res)
+		}
+	case *theme.PrimaryThemedResource:
+		if r.button.parent.importance != widget.HighImportance {
+			r.icon.Resource = res.Original()
+		}
+	}
+	r.icon.Refresh()
+}
+
+func mismatchedTabItems(items []*TabItem) bool {
+	var hasText, hasIcon bool
+	for _, tab := range items {
+		hasText = hasText || tab.Text != ""
+		hasIcon = hasIcon || tab.Icon != nil
+	}
+
+	mismatch := false
+	for _, tab := range items {
+		if (hasText && tab.Text == "") || (hasIcon && tab.Icon == nil) {
+			mismatch = true
+			break
+		}
+	}
+
+	return mismatch
+}
+
+func moreIcon(t baseTabs) fyne.Resource {
+	if l := t.tabLocation(); l == TabLocationLeading || l == TabLocationTrailing {
+		return theme.MoreVerticalIcon()
+	}
+	return theme.MoreHorizontalIcon()
+}

+ 178 - 0
vendor/fyne.io/fyne/v2/data/binding/binding.go

@@ -0,0 +1,178 @@
+//go:generate go run gen.go
+
+// Package binding provides support for binding data to widgets.
+package binding
+
+import (
+	"errors"
+	"reflect"
+	"sync"
+
+	"fyne.io/fyne/v2"
+)
+
+var (
+	errKeyNotFound = errors.New("key not found")
+	errOutOfBounds = errors.New("index out of bounds")
+	errParseFailed = errors.New("format did not match 1 value")
+
+	// As an optimisation we connect any listeners asking for the same key, so that there is only 1 per preference item.
+	prefBinds = newPreferencesMap()
+)
+
+// DataItem is the base interface for all bindable data items.
+//
+// Since: 2.0
+type DataItem interface {
+	// AddListener attaches a new change listener to this DataItem.
+	// Listeners are called each time the data inside this DataItem changes.
+	// Additionally the listener will be triggered upon successful connection to get the current value.
+	AddListener(DataListener)
+	// RemoveListener will detach the specified change listener from the DataItem.
+	// Disconnected listener will no longer be triggered when changes occur.
+	RemoveListener(DataListener)
+}
+
+// DataListener is any object that can register for changes in a bindable DataItem.
+// See NewDataListener to define a new listener using just an inline function.
+//
+// Since: 2.0
+type DataListener interface {
+	DataChanged()
+}
+
+// NewDataListener is a helper function that creates a new listener type from a simple callback function.
+//
+// Since: 2.0
+func NewDataListener(fn func()) DataListener {
+	return &listener{fn}
+}
+
+type listener struct {
+	callback func()
+}
+
+func (l *listener) DataChanged() {
+	l.callback()
+}
+
+type base struct {
+	listeners sync.Map // map[DataListener]bool
+
+	lock sync.RWMutex
+}
+
+// AddListener allows a data listener to be informed of changes to this item.
+func (b *base) AddListener(l DataListener) {
+	b.listeners.Store(l, true)
+	queueItem(l.DataChanged)
+}
+
+// RemoveListener should be called if the listener is no longer interested in being informed of data change events.
+func (b *base) RemoveListener(l DataListener) {
+	b.listeners.Delete(l)
+}
+
+func (b *base) trigger() {
+	b.listeners.Range(func(key, _ interface{}) bool {
+		queueItem(key.(DataListener).DataChanged)
+		return true
+	})
+}
+
+// Untyped supports binding a interface{} value.
+//
+// Since: 2.1
+type Untyped interface {
+	DataItem
+	Get() (interface{}, error)
+	Set(interface{}) error
+}
+
+// NewUntyped returns a bindable interface{} value that is managed internally.
+//
+// Since: 2.1
+func NewUntyped() Untyped {
+	var blank interface{} = nil
+	v := &blank
+	return &boundUntyped{val: reflect.ValueOf(v).Elem()}
+}
+
+type boundUntyped struct {
+	base
+
+	val reflect.Value
+}
+
+func (b *boundUntyped) Get() (interface{}, error) {
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+
+	return b.val.Interface(), nil
+}
+
+func (b *boundUntyped) Set(val interface{}) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if b.val.Interface() == val {
+		return nil
+	}
+
+	b.val.Set(reflect.ValueOf(val))
+
+	b.trigger()
+	return nil
+}
+
+// ExternalUntyped supports binding a interface{} value to an external value.
+//
+// Since: 2.1
+type ExternalUntyped interface {
+	Untyped
+	Reload() error
+}
+
+// BindUntyped returns a bindable interface{} value that is bound to an external type.
+// The parameter must be a pointer to the type you wish to bind.
+//
+// Since: 2.1
+func BindUntyped(v interface{}) ExternalUntyped {
+	t := reflect.TypeOf(v)
+	if t.Kind() != reflect.Ptr {
+		fyne.LogError("Invalid type passed to BindUntyped, must be a pointer", nil)
+		v = nil
+	}
+
+	if v == nil {
+		var blank interface{}
+		v = &blank // never allow a nil value pointer
+	}
+
+	b := &boundExternalUntyped{}
+	b.val = reflect.ValueOf(v).Elem()
+	b.old = b.val.Interface()
+	return b
+}
+
+type boundExternalUntyped struct {
+	boundUntyped
+
+	old interface{}
+}
+
+func (b *boundExternalUntyped) Set(val interface{}) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if b.old == val {
+		return nil
+	}
+	b.val.Set(reflect.ValueOf(val))
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+func (b *boundExternalUntyped) Reload() error {
+	return b.Set(b.val.Interface())
+}

+ 647 - 0
vendor/fyne.io/fyne/v2/data/binding/binditems.go

@@ -0,0 +1,647 @@
+// auto-generated
+// **** THIS FILE IS AUTO-GENERATED, PLEASE DO NOT EDIT IT **** //
+
+package binding
+
+import (
+	"bytes"
+
+	"fyne.io/fyne/v2"
+)
+
+// Bool supports binding a bool value.
+//
+// Since: 2.0
+type Bool interface {
+	DataItem
+	Get() (bool, error)
+	Set(bool) error
+}
+
+// ExternalBool supports binding a bool value to an external value.
+//
+// Since: 2.0
+type ExternalBool interface {
+	Bool
+	Reload() error
+}
+
+// NewBool returns a bindable bool value that is managed internally.
+//
+// Since: 2.0
+func NewBool() Bool {
+	var blank bool = false
+	return &boundBool{val: &blank}
+}
+
+// BindBool returns a new bindable value that controls the contents of the provided bool variable.
+// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.0
+func BindBool(v *bool) ExternalBool {
+	if v == nil {
+		var blank bool = false
+		v = &blank // never allow a nil value pointer
+	}
+	b := &boundExternalBool{}
+	b.val = v
+	b.old = *v
+	return b
+}
+
+type boundBool struct {
+	base
+
+	val *bool
+}
+
+func (b *boundBool) Get() (bool, error) {
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+
+	if b.val == nil {
+		return false, nil
+	}
+	return *b.val, nil
+}
+
+func (b *boundBool) Set(val bool) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if *b.val == val {
+		return nil
+	}
+	*b.val = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalBool struct {
+	boundBool
+
+	old bool
+}
+
+func (b *boundExternalBool) Set(val bool) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if b.old == val {
+		return nil
+	}
+	*b.val = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+func (b *boundExternalBool) Reload() error {
+	return b.Set(*b.val)
+}
+
+// Bytes supports binding a []byte value.
+//
+// Since: 2.2
+type Bytes interface {
+	DataItem
+	Get() ([]byte, error)
+	Set([]byte) error
+}
+
+// ExternalBytes supports binding a []byte value to an external value.
+//
+// Since: 2.2
+type ExternalBytes interface {
+	Bytes
+	Reload() error
+}
+
+// NewBytes returns a bindable []byte value that is managed internally.
+//
+// Since: 2.2
+func NewBytes() Bytes {
+	var blank []byte = nil
+	return &boundBytes{val: &blank}
+}
+
+// BindBytes returns a new bindable value that controls the contents of the provided []byte variable.
+// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.2
+func BindBytes(v *[]byte) ExternalBytes {
+	if v == nil {
+		var blank []byte = nil
+		v = &blank // never allow a nil value pointer
+	}
+	b := &boundExternalBytes{}
+	b.val = v
+	b.old = *v
+	return b
+}
+
+type boundBytes struct {
+	base
+
+	val *[]byte
+}
+
+func (b *boundBytes) Get() ([]byte, error) {
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+
+	if b.val == nil {
+		return nil, nil
+	}
+	return *b.val, nil
+}
+
+func (b *boundBytes) Set(val []byte) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if bytes.Equal(*b.val, val) {
+		return nil
+	}
+	*b.val = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalBytes struct {
+	boundBytes
+
+	old []byte
+}
+
+func (b *boundExternalBytes) Set(val []byte) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if bytes.Equal(b.old, val) {
+		return nil
+	}
+	*b.val = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+func (b *boundExternalBytes) Reload() error {
+	return b.Set(*b.val)
+}
+
+// Float supports binding a float64 value.
+//
+// Since: 2.0
+type Float interface {
+	DataItem
+	Get() (float64, error)
+	Set(float64) error
+}
+
+// ExternalFloat supports binding a float64 value to an external value.
+//
+// Since: 2.0
+type ExternalFloat interface {
+	Float
+	Reload() error
+}
+
+// NewFloat returns a bindable float64 value that is managed internally.
+//
+// Since: 2.0
+func NewFloat() Float {
+	var blank float64 = 0.0
+	return &boundFloat{val: &blank}
+}
+
+// BindFloat returns a new bindable value that controls the contents of the provided float64 variable.
+// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.0
+func BindFloat(v *float64) ExternalFloat {
+	if v == nil {
+		var blank float64 = 0.0
+		v = &blank // never allow a nil value pointer
+	}
+	b := &boundExternalFloat{}
+	b.val = v
+	b.old = *v
+	return b
+}
+
+type boundFloat struct {
+	base
+
+	val *float64
+}
+
+func (b *boundFloat) Get() (float64, error) {
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+
+	if b.val == nil {
+		return 0.0, nil
+	}
+	return *b.val, nil
+}
+
+func (b *boundFloat) Set(val float64) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if *b.val == val {
+		return nil
+	}
+	*b.val = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalFloat struct {
+	boundFloat
+
+	old float64
+}
+
+func (b *boundExternalFloat) Set(val float64) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if b.old == val {
+		return nil
+	}
+	*b.val = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+func (b *boundExternalFloat) Reload() error {
+	return b.Set(*b.val)
+}
+
+// Int supports binding a int value.
+//
+// Since: 2.0
+type Int interface {
+	DataItem
+	Get() (int, error)
+	Set(int) error
+}
+
+// ExternalInt supports binding a int value to an external value.
+//
+// Since: 2.0
+type ExternalInt interface {
+	Int
+	Reload() error
+}
+
+// NewInt returns a bindable int value that is managed internally.
+//
+// Since: 2.0
+func NewInt() Int {
+	var blank int = 0
+	return &boundInt{val: &blank}
+}
+
+// BindInt returns a new bindable value that controls the contents of the provided int variable.
+// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.0
+func BindInt(v *int) ExternalInt {
+	if v == nil {
+		var blank int = 0
+		v = &blank // never allow a nil value pointer
+	}
+	b := &boundExternalInt{}
+	b.val = v
+	b.old = *v
+	return b
+}
+
+type boundInt struct {
+	base
+
+	val *int
+}
+
+func (b *boundInt) Get() (int, error) {
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+
+	if b.val == nil {
+		return 0, nil
+	}
+	return *b.val, nil
+}
+
+func (b *boundInt) Set(val int) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if *b.val == val {
+		return nil
+	}
+	*b.val = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalInt struct {
+	boundInt
+
+	old int
+}
+
+func (b *boundExternalInt) Set(val int) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if b.old == val {
+		return nil
+	}
+	*b.val = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+func (b *boundExternalInt) Reload() error {
+	return b.Set(*b.val)
+}
+
+// Rune supports binding a rune value.
+//
+// Since: 2.0
+type Rune interface {
+	DataItem
+	Get() (rune, error)
+	Set(rune) error
+}
+
+// ExternalRune supports binding a rune value to an external value.
+//
+// Since: 2.0
+type ExternalRune interface {
+	Rune
+	Reload() error
+}
+
+// NewRune returns a bindable rune value that is managed internally.
+//
+// Since: 2.0
+func NewRune() Rune {
+	var blank rune = rune(0)
+	return &boundRune{val: &blank}
+}
+
+// BindRune returns a new bindable value that controls the contents of the provided rune variable.
+// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.0
+func BindRune(v *rune) ExternalRune {
+	if v == nil {
+		var blank rune = rune(0)
+		v = &blank // never allow a nil value pointer
+	}
+	b := &boundExternalRune{}
+	b.val = v
+	b.old = *v
+	return b
+}
+
+type boundRune struct {
+	base
+
+	val *rune
+}
+
+func (b *boundRune) Get() (rune, error) {
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+
+	if b.val == nil {
+		return rune(0), nil
+	}
+	return *b.val, nil
+}
+
+func (b *boundRune) Set(val rune) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if *b.val == val {
+		return nil
+	}
+	*b.val = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalRune struct {
+	boundRune
+
+	old rune
+}
+
+func (b *boundExternalRune) Set(val rune) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if b.old == val {
+		return nil
+	}
+	*b.val = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+func (b *boundExternalRune) Reload() error {
+	return b.Set(*b.val)
+}
+
+// String supports binding a string value.
+//
+// Since: 2.0
+type String interface {
+	DataItem
+	Get() (string, error)
+	Set(string) error
+}
+
+// ExternalString supports binding a string value to an external value.
+//
+// Since: 2.0
+type ExternalString interface {
+	String
+	Reload() error
+}
+
+// NewString returns a bindable string value that is managed internally.
+//
+// Since: 2.0
+func NewString() String {
+	var blank string = ""
+	return &boundString{val: &blank}
+}
+
+// BindString returns a new bindable value that controls the contents of the provided string variable.
+// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.0
+func BindString(v *string) ExternalString {
+	if v == nil {
+		var blank string = ""
+		v = &blank // never allow a nil value pointer
+	}
+	b := &boundExternalString{}
+	b.val = v
+	b.old = *v
+	return b
+}
+
+type boundString struct {
+	base
+
+	val *string
+}
+
+func (b *boundString) Get() (string, error) {
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+
+	if b.val == nil {
+		return "", nil
+	}
+	return *b.val, nil
+}
+
+func (b *boundString) Set(val string) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if *b.val == val {
+		return nil
+	}
+	*b.val = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalString struct {
+	boundString
+
+	old string
+}
+
+func (b *boundExternalString) Set(val string) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if b.old == val {
+		return nil
+	}
+	*b.val = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+func (b *boundExternalString) Reload() error {
+	return b.Set(*b.val)
+}
+
+// URI supports binding a fyne.URI value.
+//
+// Since: 2.1
+type URI interface {
+	DataItem
+	Get() (fyne.URI, error)
+	Set(fyne.URI) error
+}
+
+// ExternalURI supports binding a fyne.URI value to an external value.
+//
+// Since: 2.1
+type ExternalURI interface {
+	URI
+	Reload() error
+}
+
+// NewURI returns a bindable fyne.URI value that is managed internally.
+//
+// Since: 2.1
+func NewURI() URI {
+	var blank fyne.URI = fyne.URI(nil)
+	return &boundURI{val: &blank}
+}
+
+// BindURI returns a new bindable value that controls the contents of the provided fyne.URI variable.
+// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.1
+func BindURI(v *fyne.URI) ExternalURI {
+	if v == nil {
+		var blank fyne.URI = fyne.URI(nil)
+		v = &blank // never allow a nil value pointer
+	}
+	b := &boundExternalURI{}
+	b.val = v
+	b.old = *v
+	return b
+}
+
+type boundURI struct {
+	base
+
+	val *fyne.URI
+}
+
+func (b *boundURI) Get() (fyne.URI, error) {
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+
+	if b.val == nil {
+		return fyne.URI(nil), nil
+	}
+	return *b.val, nil
+}
+
+func (b *boundURI) Set(val fyne.URI) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if compareURI(*b.val, val) {
+		return nil
+	}
+	*b.val = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalURI struct {
+	boundURI
+
+	old fyne.URI
+}
+
+func (b *boundExternalURI) Set(val fyne.URI) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+	if compareURI(b.old, val) {
+		return nil
+	}
+	*b.val = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+func (b *boundExternalURI) Reload() error {
+	return b.Set(*b.val)
+}

+ 1786 - 0
vendor/fyne.io/fyne/v2/data/binding/bindlists.go

@@ -0,0 +1,1786 @@
+// auto-generated
+// **** THIS FILE IS AUTO-GENERATED, PLEASE DO NOT EDIT IT **** //
+
+package binding
+
+import (
+	"bytes"
+
+	"fyne.io/fyne/v2"
+)
+
+// BoolList supports binding a list of bool values.
+//
+// Since: 2.0
+type BoolList interface {
+	DataList
+
+	Append(value bool) error
+	Get() ([]bool, error)
+	GetValue(index int) (bool, error)
+	Prepend(value bool) error
+	Set(list []bool) error
+	SetValue(index int, value bool) error
+}
+
+// ExternalBoolList supports binding a list of bool values from an external variable.
+//
+// Since: 2.0
+type ExternalBoolList interface {
+	BoolList
+
+	Reload() error
+}
+
+// NewBoolList returns a bindable list of bool values.
+//
+// Since: 2.0
+func NewBoolList() BoolList {
+	return &boundBoolList{val: &[]bool{}}
+}
+
+// BindBoolList returns a bound list of bool values, based on the contents of the passed slice.
+// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.0
+func BindBoolList(v *[]bool) ExternalBoolList {
+	if v == nil {
+		return NewBoolList().(ExternalBoolList)
+	}
+
+	b := &boundBoolList{val: v, updateExternal: true}
+
+	for i := range *v {
+		b.appendItem(bindBoolListItem(v, i, b.updateExternal))
+	}
+
+	return b
+}
+
+type boundBoolList struct {
+	listBase
+
+	updateExternal bool
+	val            *[]bool
+}
+
+func (l *boundBoolList) Append(val bool) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	*l.val = append(*l.val, val)
+
+	return l.doReload()
+}
+
+func (l *boundBoolList) Get() ([]bool, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	return *l.val, nil
+}
+
+func (l *boundBoolList) GetValue(i int) (bool, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	if i < 0 || i >= l.Length() {
+		return false, errOutOfBounds
+	}
+
+	return (*l.val)[i], nil
+}
+
+func (l *boundBoolList) Prepend(val bool) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = append([]bool{val}, *l.val...)
+
+	return l.doReload()
+}
+
+func (l *boundBoolList) Reload() error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	return l.doReload()
+}
+
+func (l *boundBoolList) Set(v []bool) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = v
+
+	return l.doReload()
+}
+
+func (l *boundBoolList) doReload() (retErr error) {
+	oldLen := len(l.items)
+	newLen := len(*l.val)
+	if oldLen > newLen {
+		for i := oldLen - 1; i >= newLen; i-- {
+			l.deleteItem(i)
+		}
+		l.trigger()
+	} else if oldLen < newLen {
+		for i := oldLen; i < newLen; i++ {
+			l.appendItem(bindBoolListItem(l.val, i, l.updateExternal))
+		}
+		l.trigger()
+	}
+
+	for i, item := range l.items {
+		if i > oldLen || i > newLen {
+			break
+		}
+
+		var err error
+		if l.updateExternal {
+			item.(*boundExternalBoolListItem).lock.Lock()
+			err = item.(*boundExternalBoolListItem).setIfChanged((*l.val)[i])
+			item.(*boundExternalBoolListItem).lock.Unlock()
+		} else {
+			item.(*boundBoolListItem).lock.Lock()
+			err = item.(*boundBoolListItem).doSet((*l.val)[i])
+			item.(*boundBoolListItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (l *boundBoolList) SetValue(i int, v bool) error {
+	l.lock.RLock()
+	len := l.Length()
+	l.lock.RUnlock()
+
+	if i < 0 || i >= len {
+		return errOutOfBounds
+	}
+
+	l.lock.Lock()
+	(*l.val)[i] = v
+	l.lock.Unlock()
+
+	item, err := l.GetItem(i)
+	if err != nil {
+		return err
+	}
+	return item.(Bool).Set(v)
+}
+
+func bindBoolListItem(v *[]bool, i int, external bool) Bool {
+	if external {
+		ret := &boundExternalBoolListItem{old: (*v)[i]}
+		ret.val = v
+		ret.index = i
+		return ret
+	}
+
+	return &boundBoolListItem{val: v, index: i}
+}
+
+type boundBoolListItem struct {
+	base
+
+	val   *[]bool
+	index int
+}
+
+func (b *boundBoolListItem) Get() (bool, error) {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	if b.index < 0 || b.index >= len(*b.val) {
+		return false, errOutOfBounds
+	}
+
+	return (*b.val)[b.index], nil
+}
+
+func (b *boundBoolListItem) Set(val bool) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	return b.doSet(val)
+}
+
+func (b *boundBoolListItem) doSet(val bool) error {
+	(*b.val)[b.index] = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalBoolListItem struct {
+	boundBoolListItem
+
+	old bool
+}
+
+func (b *boundExternalBoolListItem) setIfChanged(val bool) error {
+	if val == b.old {
+		return nil
+	}
+	(*b.val)[b.index] = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+// BytesList supports binding a list of []byte values.
+//
+// Since: 2.2
+type BytesList interface {
+	DataList
+
+	Append(value []byte) error
+	Get() ([][]byte, error)
+	GetValue(index int) ([]byte, error)
+	Prepend(value []byte) error
+	Set(list [][]byte) error
+	SetValue(index int, value []byte) error
+}
+
+// ExternalBytesList supports binding a list of []byte values from an external variable.
+//
+// Since: 2.2
+type ExternalBytesList interface {
+	BytesList
+
+	Reload() error
+}
+
+// NewBytesList returns a bindable list of []byte values.
+//
+// Since: 2.2
+func NewBytesList() BytesList {
+	return &boundBytesList{val: &[][]byte{}}
+}
+
+// BindBytesList returns a bound list of []byte values, based on the contents of the passed slice.
+// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.2
+func BindBytesList(v *[][]byte) ExternalBytesList {
+	if v == nil {
+		return NewBytesList().(ExternalBytesList)
+	}
+
+	b := &boundBytesList{val: v, updateExternal: true}
+
+	for i := range *v {
+		b.appendItem(bindBytesListItem(v, i, b.updateExternal))
+	}
+
+	return b
+}
+
+type boundBytesList struct {
+	listBase
+
+	updateExternal bool
+	val            *[][]byte
+}
+
+func (l *boundBytesList) Append(val []byte) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	*l.val = append(*l.val, val)
+
+	return l.doReload()
+}
+
+func (l *boundBytesList) Get() ([][]byte, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	return *l.val, nil
+}
+
+func (l *boundBytesList) GetValue(i int) ([]byte, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	if i < 0 || i >= l.Length() {
+		return nil, errOutOfBounds
+	}
+
+	return (*l.val)[i], nil
+}
+
+func (l *boundBytesList) Prepend(val []byte) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = append([][]byte{val}, *l.val...)
+
+	return l.doReload()
+}
+
+func (l *boundBytesList) Reload() error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	return l.doReload()
+}
+
+func (l *boundBytesList) Set(v [][]byte) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = v
+
+	return l.doReload()
+}
+
+func (l *boundBytesList) doReload() (retErr error) {
+	oldLen := len(l.items)
+	newLen := len(*l.val)
+	if oldLen > newLen {
+		for i := oldLen - 1; i >= newLen; i-- {
+			l.deleteItem(i)
+		}
+		l.trigger()
+	} else if oldLen < newLen {
+		for i := oldLen; i < newLen; i++ {
+			l.appendItem(bindBytesListItem(l.val, i, l.updateExternal))
+		}
+		l.trigger()
+	}
+
+	for i, item := range l.items {
+		if i > oldLen || i > newLen {
+			break
+		}
+
+		var err error
+		if l.updateExternal {
+			item.(*boundExternalBytesListItem).lock.Lock()
+			err = item.(*boundExternalBytesListItem).setIfChanged((*l.val)[i])
+			item.(*boundExternalBytesListItem).lock.Unlock()
+		} else {
+			item.(*boundBytesListItem).lock.Lock()
+			err = item.(*boundBytesListItem).doSet((*l.val)[i])
+			item.(*boundBytesListItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (l *boundBytesList) SetValue(i int, v []byte) error {
+	l.lock.RLock()
+	len := l.Length()
+	l.lock.RUnlock()
+
+	if i < 0 || i >= len {
+		return errOutOfBounds
+	}
+
+	l.lock.Lock()
+	(*l.val)[i] = v
+	l.lock.Unlock()
+
+	item, err := l.GetItem(i)
+	if err != nil {
+		return err
+	}
+	return item.(Bytes).Set(v)
+}
+
+func bindBytesListItem(v *[][]byte, i int, external bool) Bytes {
+	if external {
+		ret := &boundExternalBytesListItem{old: (*v)[i]}
+		ret.val = v
+		ret.index = i
+		return ret
+	}
+
+	return &boundBytesListItem{val: v, index: i}
+}
+
+type boundBytesListItem struct {
+	base
+
+	val   *[][]byte
+	index int
+}
+
+func (b *boundBytesListItem) Get() ([]byte, error) {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	if b.index < 0 || b.index >= len(*b.val) {
+		return nil, errOutOfBounds
+	}
+
+	return (*b.val)[b.index], nil
+}
+
+func (b *boundBytesListItem) Set(val []byte) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	return b.doSet(val)
+}
+
+func (b *boundBytesListItem) doSet(val []byte) error {
+	(*b.val)[b.index] = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalBytesListItem struct {
+	boundBytesListItem
+
+	old []byte
+}
+
+func (b *boundExternalBytesListItem) setIfChanged(val []byte) error {
+	if bytes.Equal(val, b.old) {
+		return nil
+	}
+	(*b.val)[b.index] = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+// FloatList supports binding a list of float64 values.
+//
+// Since: 2.0
+type FloatList interface {
+	DataList
+
+	Append(value float64) error
+	Get() ([]float64, error)
+	GetValue(index int) (float64, error)
+	Prepend(value float64) error
+	Set(list []float64) error
+	SetValue(index int, value float64) error
+}
+
+// ExternalFloatList supports binding a list of float64 values from an external variable.
+//
+// Since: 2.0
+type ExternalFloatList interface {
+	FloatList
+
+	Reload() error
+}
+
+// NewFloatList returns a bindable list of float64 values.
+//
+// Since: 2.0
+func NewFloatList() FloatList {
+	return &boundFloatList{val: &[]float64{}}
+}
+
+// BindFloatList returns a bound list of float64 values, based on the contents of the passed slice.
+// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.0
+func BindFloatList(v *[]float64) ExternalFloatList {
+	if v == nil {
+		return NewFloatList().(ExternalFloatList)
+	}
+
+	b := &boundFloatList{val: v, updateExternal: true}
+
+	for i := range *v {
+		b.appendItem(bindFloatListItem(v, i, b.updateExternal))
+	}
+
+	return b
+}
+
+type boundFloatList struct {
+	listBase
+
+	updateExternal bool
+	val            *[]float64
+}
+
+func (l *boundFloatList) Append(val float64) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	*l.val = append(*l.val, val)
+
+	return l.doReload()
+}
+
+func (l *boundFloatList) Get() ([]float64, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	return *l.val, nil
+}
+
+func (l *boundFloatList) GetValue(i int) (float64, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	if i < 0 || i >= l.Length() {
+		return 0.0, errOutOfBounds
+	}
+
+	return (*l.val)[i], nil
+}
+
+func (l *boundFloatList) Prepend(val float64) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = append([]float64{val}, *l.val...)
+
+	return l.doReload()
+}
+
+func (l *boundFloatList) Reload() error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	return l.doReload()
+}
+
+func (l *boundFloatList) Set(v []float64) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = v
+
+	return l.doReload()
+}
+
+func (l *boundFloatList) doReload() (retErr error) {
+	oldLen := len(l.items)
+	newLen := len(*l.val)
+	if oldLen > newLen {
+		for i := oldLen - 1; i >= newLen; i-- {
+			l.deleteItem(i)
+		}
+		l.trigger()
+	} else if oldLen < newLen {
+		for i := oldLen; i < newLen; i++ {
+			l.appendItem(bindFloatListItem(l.val, i, l.updateExternal))
+		}
+		l.trigger()
+	}
+
+	for i, item := range l.items {
+		if i > oldLen || i > newLen {
+			break
+		}
+
+		var err error
+		if l.updateExternal {
+			item.(*boundExternalFloatListItem).lock.Lock()
+			err = item.(*boundExternalFloatListItem).setIfChanged((*l.val)[i])
+			item.(*boundExternalFloatListItem).lock.Unlock()
+		} else {
+			item.(*boundFloatListItem).lock.Lock()
+			err = item.(*boundFloatListItem).doSet((*l.val)[i])
+			item.(*boundFloatListItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (l *boundFloatList) SetValue(i int, v float64) error {
+	l.lock.RLock()
+	len := l.Length()
+	l.lock.RUnlock()
+
+	if i < 0 || i >= len {
+		return errOutOfBounds
+	}
+
+	l.lock.Lock()
+	(*l.val)[i] = v
+	l.lock.Unlock()
+
+	item, err := l.GetItem(i)
+	if err != nil {
+		return err
+	}
+	return item.(Float).Set(v)
+}
+
+func bindFloatListItem(v *[]float64, i int, external bool) Float {
+	if external {
+		ret := &boundExternalFloatListItem{old: (*v)[i]}
+		ret.val = v
+		ret.index = i
+		return ret
+	}
+
+	return &boundFloatListItem{val: v, index: i}
+}
+
+type boundFloatListItem struct {
+	base
+
+	val   *[]float64
+	index int
+}
+
+func (b *boundFloatListItem) Get() (float64, error) {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	if b.index < 0 || b.index >= len(*b.val) {
+		return 0.0, errOutOfBounds
+	}
+
+	return (*b.val)[b.index], nil
+}
+
+func (b *boundFloatListItem) Set(val float64) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	return b.doSet(val)
+}
+
+func (b *boundFloatListItem) doSet(val float64) error {
+	(*b.val)[b.index] = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalFloatListItem struct {
+	boundFloatListItem
+
+	old float64
+}
+
+func (b *boundExternalFloatListItem) setIfChanged(val float64) error {
+	if val == b.old {
+		return nil
+	}
+	(*b.val)[b.index] = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+// IntList supports binding a list of int values.
+//
+// Since: 2.0
+type IntList interface {
+	DataList
+
+	Append(value int) error
+	Get() ([]int, error)
+	GetValue(index int) (int, error)
+	Prepend(value int) error
+	Set(list []int) error
+	SetValue(index int, value int) error
+}
+
+// ExternalIntList supports binding a list of int values from an external variable.
+//
+// Since: 2.0
+type ExternalIntList interface {
+	IntList
+
+	Reload() error
+}
+
+// NewIntList returns a bindable list of int values.
+//
+// Since: 2.0
+func NewIntList() IntList {
+	return &boundIntList{val: &[]int{}}
+}
+
+// BindIntList returns a bound list of int values, based on the contents of the passed slice.
+// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.0
+func BindIntList(v *[]int) ExternalIntList {
+	if v == nil {
+		return NewIntList().(ExternalIntList)
+	}
+
+	b := &boundIntList{val: v, updateExternal: true}
+
+	for i := range *v {
+		b.appendItem(bindIntListItem(v, i, b.updateExternal))
+	}
+
+	return b
+}
+
+type boundIntList struct {
+	listBase
+
+	updateExternal bool
+	val            *[]int
+}
+
+func (l *boundIntList) Append(val int) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	*l.val = append(*l.val, val)
+
+	return l.doReload()
+}
+
+func (l *boundIntList) Get() ([]int, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	return *l.val, nil
+}
+
+func (l *boundIntList) GetValue(i int) (int, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	if i < 0 || i >= l.Length() {
+		return 0, errOutOfBounds
+	}
+
+	return (*l.val)[i], nil
+}
+
+func (l *boundIntList) Prepend(val int) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = append([]int{val}, *l.val...)
+
+	return l.doReload()
+}
+
+func (l *boundIntList) Reload() error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	return l.doReload()
+}
+
+func (l *boundIntList) Set(v []int) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = v
+
+	return l.doReload()
+}
+
+func (l *boundIntList) doReload() (retErr error) {
+	oldLen := len(l.items)
+	newLen := len(*l.val)
+	if oldLen > newLen {
+		for i := oldLen - 1; i >= newLen; i-- {
+			l.deleteItem(i)
+		}
+		l.trigger()
+	} else if oldLen < newLen {
+		for i := oldLen; i < newLen; i++ {
+			l.appendItem(bindIntListItem(l.val, i, l.updateExternal))
+		}
+		l.trigger()
+	}
+
+	for i, item := range l.items {
+		if i > oldLen || i > newLen {
+			break
+		}
+
+		var err error
+		if l.updateExternal {
+			item.(*boundExternalIntListItem).lock.Lock()
+			err = item.(*boundExternalIntListItem).setIfChanged((*l.val)[i])
+			item.(*boundExternalIntListItem).lock.Unlock()
+		} else {
+			item.(*boundIntListItem).lock.Lock()
+			err = item.(*boundIntListItem).doSet((*l.val)[i])
+			item.(*boundIntListItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (l *boundIntList) SetValue(i int, v int) error {
+	l.lock.RLock()
+	len := l.Length()
+	l.lock.RUnlock()
+
+	if i < 0 || i >= len {
+		return errOutOfBounds
+	}
+
+	l.lock.Lock()
+	(*l.val)[i] = v
+	l.lock.Unlock()
+
+	item, err := l.GetItem(i)
+	if err != nil {
+		return err
+	}
+	return item.(Int).Set(v)
+}
+
+func bindIntListItem(v *[]int, i int, external bool) Int {
+	if external {
+		ret := &boundExternalIntListItem{old: (*v)[i]}
+		ret.val = v
+		ret.index = i
+		return ret
+	}
+
+	return &boundIntListItem{val: v, index: i}
+}
+
+type boundIntListItem struct {
+	base
+
+	val   *[]int
+	index int
+}
+
+func (b *boundIntListItem) Get() (int, error) {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	if b.index < 0 || b.index >= len(*b.val) {
+		return 0, errOutOfBounds
+	}
+
+	return (*b.val)[b.index], nil
+}
+
+func (b *boundIntListItem) Set(val int) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	return b.doSet(val)
+}
+
+func (b *boundIntListItem) doSet(val int) error {
+	(*b.val)[b.index] = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalIntListItem struct {
+	boundIntListItem
+
+	old int
+}
+
+func (b *boundExternalIntListItem) setIfChanged(val int) error {
+	if val == b.old {
+		return nil
+	}
+	(*b.val)[b.index] = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+// RuneList supports binding a list of rune values.
+//
+// Since: 2.0
+type RuneList interface {
+	DataList
+
+	Append(value rune) error
+	Get() ([]rune, error)
+	GetValue(index int) (rune, error)
+	Prepend(value rune) error
+	Set(list []rune) error
+	SetValue(index int, value rune) error
+}
+
+// ExternalRuneList supports binding a list of rune values from an external variable.
+//
+// Since: 2.0
+type ExternalRuneList interface {
+	RuneList
+
+	Reload() error
+}
+
+// NewRuneList returns a bindable list of rune values.
+//
+// Since: 2.0
+func NewRuneList() RuneList {
+	return &boundRuneList{val: &[]rune{}}
+}
+
+// BindRuneList returns a bound list of rune values, based on the contents of the passed slice.
+// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.0
+func BindRuneList(v *[]rune) ExternalRuneList {
+	if v == nil {
+		return NewRuneList().(ExternalRuneList)
+	}
+
+	b := &boundRuneList{val: v, updateExternal: true}
+
+	for i := range *v {
+		b.appendItem(bindRuneListItem(v, i, b.updateExternal))
+	}
+
+	return b
+}
+
+type boundRuneList struct {
+	listBase
+
+	updateExternal bool
+	val            *[]rune
+}
+
+func (l *boundRuneList) Append(val rune) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	*l.val = append(*l.val, val)
+
+	return l.doReload()
+}
+
+func (l *boundRuneList) Get() ([]rune, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	return *l.val, nil
+}
+
+func (l *boundRuneList) GetValue(i int) (rune, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	if i < 0 || i >= l.Length() {
+		return rune(0), errOutOfBounds
+	}
+
+	return (*l.val)[i], nil
+}
+
+func (l *boundRuneList) Prepend(val rune) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = append([]rune{val}, *l.val...)
+
+	return l.doReload()
+}
+
+func (l *boundRuneList) Reload() error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	return l.doReload()
+}
+
+func (l *boundRuneList) Set(v []rune) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = v
+
+	return l.doReload()
+}
+
+func (l *boundRuneList) doReload() (retErr error) {
+	oldLen := len(l.items)
+	newLen := len(*l.val)
+	if oldLen > newLen {
+		for i := oldLen - 1; i >= newLen; i-- {
+			l.deleteItem(i)
+		}
+		l.trigger()
+	} else if oldLen < newLen {
+		for i := oldLen; i < newLen; i++ {
+			l.appendItem(bindRuneListItem(l.val, i, l.updateExternal))
+		}
+		l.trigger()
+	}
+
+	for i, item := range l.items {
+		if i > oldLen || i > newLen {
+			break
+		}
+
+		var err error
+		if l.updateExternal {
+			item.(*boundExternalRuneListItem).lock.Lock()
+			err = item.(*boundExternalRuneListItem).setIfChanged((*l.val)[i])
+			item.(*boundExternalRuneListItem).lock.Unlock()
+		} else {
+			item.(*boundRuneListItem).lock.Lock()
+			err = item.(*boundRuneListItem).doSet((*l.val)[i])
+			item.(*boundRuneListItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (l *boundRuneList) SetValue(i int, v rune) error {
+	l.lock.RLock()
+	len := l.Length()
+	l.lock.RUnlock()
+
+	if i < 0 || i >= len {
+		return errOutOfBounds
+	}
+
+	l.lock.Lock()
+	(*l.val)[i] = v
+	l.lock.Unlock()
+
+	item, err := l.GetItem(i)
+	if err != nil {
+		return err
+	}
+	return item.(Rune).Set(v)
+}
+
+func bindRuneListItem(v *[]rune, i int, external bool) Rune {
+	if external {
+		ret := &boundExternalRuneListItem{old: (*v)[i]}
+		ret.val = v
+		ret.index = i
+		return ret
+	}
+
+	return &boundRuneListItem{val: v, index: i}
+}
+
+type boundRuneListItem struct {
+	base
+
+	val   *[]rune
+	index int
+}
+
+func (b *boundRuneListItem) Get() (rune, error) {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	if b.index < 0 || b.index >= len(*b.val) {
+		return rune(0), errOutOfBounds
+	}
+
+	return (*b.val)[b.index], nil
+}
+
+func (b *boundRuneListItem) Set(val rune) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	return b.doSet(val)
+}
+
+func (b *boundRuneListItem) doSet(val rune) error {
+	(*b.val)[b.index] = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalRuneListItem struct {
+	boundRuneListItem
+
+	old rune
+}
+
+func (b *boundExternalRuneListItem) setIfChanged(val rune) error {
+	if val == b.old {
+		return nil
+	}
+	(*b.val)[b.index] = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+// StringList supports binding a list of string values.
+//
+// Since: 2.0
+type StringList interface {
+	DataList
+
+	Append(value string) error
+	Get() ([]string, error)
+	GetValue(index int) (string, error)
+	Prepend(value string) error
+	Set(list []string) error
+	SetValue(index int, value string) error
+}
+
+// ExternalStringList supports binding a list of string values from an external variable.
+//
+// Since: 2.0
+type ExternalStringList interface {
+	StringList
+
+	Reload() error
+}
+
+// NewStringList returns a bindable list of string values.
+//
+// Since: 2.0
+func NewStringList() StringList {
+	return &boundStringList{val: &[]string{}}
+}
+
+// BindStringList returns a bound list of string values, based on the contents of the passed slice.
+// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.0
+func BindStringList(v *[]string) ExternalStringList {
+	if v == nil {
+		return NewStringList().(ExternalStringList)
+	}
+
+	b := &boundStringList{val: v, updateExternal: true}
+
+	for i := range *v {
+		b.appendItem(bindStringListItem(v, i, b.updateExternal))
+	}
+
+	return b
+}
+
+type boundStringList struct {
+	listBase
+
+	updateExternal bool
+	val            *[]string
+}
+
+func (l *boundStringList) Append(val string) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	*l.val = append(*l.val, val)
+
+	return l.doReload()
+}
+
+func (l *boundStringList) Get() ([]string, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	return *l.val, nil
+}
+
+func (l *boundStringList) GetValue(i int) (string, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	if i < 0 || i >= l.Length() {
+		return "", errOutOfBounds
+	}
+
+	return (*l.val)[i], nil
+}
+
+func (l *boundStringList) Prepend(val string) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = append([]string{val}, *l.val...)
+
+	return l.doReload()
+}
+
+func (l *boundStringList) Reload() error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	return l.doReload()
+}
+
+func (l *boundStringList) Set(v []string) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = v
+
+	return l.doReload()
+}
+
+func (l *boundStringList) doReload() (retErr error) {
+	oldLen := len(l.items)
+	newLen := len(*l.val)
+	if oldLen > newLen {
+		for i := oldLen - 1; i >= newLen; i-- {
+			l.deleteItem(i)
+		}
+		l.trigger()
+	} else if oldLen < newLen {
+		for i := oldLen; i < newLen; i++ {
+			l.appendItem(bindStringListItem(l.val, i, l.updateExternal))
+		}
+		l.trigger()
+	}
+
+	for i, item := range l.items {
+		if i > oldLen || i > newLen {
+			break
+		}
+
+		var err error
+		if l.updateExternal {
+			item.(*boundExternalStringListItem).lock.Lock()
+			err = item.(*boundExternalStringListItem).setIfChanged((*l.val)[i])
+			item.(*boundExternalStringListItem).lock.Unlock()
+		} else {
+			item.(*boundStringListItem).lock.Lock()
+			err = item.(*boundStringListItem).doSet((*l.val)[i])
+			item.(*boundStringListItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (l *boundStringList) SetValue(i int, v string) error {
+	l.lock.RLock()
+	len := l.Length()
+	l.lock.RUnlock()
+
+	if i < 0 || i >= len {
+		return errOutOfBounds
+	}
+
+	l.lock.Lock()
+	(*l.val)[i] = v
+	l.lock.Unlock()
+
+	item, err := l.GetItem(i)
+	if err != nil {
+		return err
+	}
+	return item.(String).Set(v)
+}
+
+func bindStringListItem(v *[]string, i int, external bool) String {
+	if external {
+		ret := &boundExternalStringListItem{old: (*v)[i]}
+		ret.val = v
+		ret.index = i
+		return ret
+	}
+
+	return &boundStringListItem{val: v, index: i}
+}
+
+type boundStringListItem struct {
+	base
+
+	val   *[]string
+	index int
+}
+
+func (b *boundStringListItem) Get() (string, error) {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	if b.index < 0 || b.index >= len(*b.val) {
+		return "", errOutOfBounds
+	}
+
+	return (*b.val)[b.index], nil
+}
+
+func (b *boundStringListItem) Set(val string) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	return b.doSet(val)
+}
+
+func (b *boundStringListItem) doSet(val string) error {
+	(*b.val)[b.index] = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalStringListItem struct {
+	boundStringListItem
+
+	old string
+}
+
+func (b *boundExternalStringListItem) setIfChanged(val string) error {
+	if val == b.old {
+		return nil
+	}
+	(*b.val)[b.index] = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+// UntypedList supports binding a list of interface{} values.
+//
+// Since: 2.1
+type UntypedList interface {
+	DataList
+
+	Append(value interface{}) error
+	Get() ([]interface{}, error)
+	GetValue(index int) (interface{}, error)
+	Prepend(value interface{}) error
+	Set(list []interface{}) error
+	SetValue(index int, value interface{}) error
+}
+
+// ExternalUntypedList supports binding a list of interface{} values from an external variable.
+//
+// Since: 2.1
+type ExternalUntypedList interface {
+	UntypedList
+
+	Reload() error
+}
+
+// NewUntypedList returns a bindable list of interface{} values.
+//
+// Since: 2.1
+func NewUntypedList() UntypedList {
+	return &boundUntypedList{val: &[]interface{}{}}
+}
+
+// BindUntypedList returns a bound list of interface{} values, based on the contents of the passed slice.
+// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.1
+func BindUntypedList(v *[]interface{}) ExternalUntypedList {
+	if v == nil {
+		return NewUntypedList().(ExternalUntypedList)
+	}
+
+	b := &boundUntypedList{val: v, updateExternal: true}
+
+	for i := range *v {
+		b.appendItem(bindUntypedListItem(v, i, b.updateExternal))
+	}
+
+	return b
+}
+
+type boundUntypedList struct {
+	listBase
+
+	updateExternal bool
+	val            *[]interface{}
+}
+
+func (l *boundUntypedList) Append(val interface{}) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	*l.val = append(*l.val, val)
+
+	return l.doReload()
+}
+
+func (l *boundUntypedList) Get() ([]interface{}, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	return *l.val, nil
+}
+
+func (l *boundUntypedList) GetValue(i int) (interface{}, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	if i < 0 || i >= l.Length() {
+		return nil, errOutOfBounds
+	}
+
+	return (*l.val)[i], nil
+}
+
+func (l *boundUntypedList) Prepend(val interface{}) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = append([]interface{}{val}, *l.val...)
+
+	return l.doReload()
+}
+
+func (l *boundUntypedList) Reload() error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	return l.doReload()
+}
+
+func (l *boundUntypedList) Set(v []interface{}) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = v
+
+	return l.doReload()
+}
+
+func (l *boundUntypedList) doReload() (retErr error) {
+	oldLen := len(l.items)
+	newLen := len(*l.val)
+	if oldLen > newLen {
+		for i := oldLen - 1; i >= newLen; i-- {
+			l.deleteItem(i)
+		}
+		l.trigger()
+	} else if oldLen < newLen {
+		for i := oldLen; i < newLen; i++ {
+			l.appendItem(bindUntypedListItem(l.val, i, l.updateExternal))
+		}
+		l.trigger()
+	}
+
+	for i, item := range l.items {
+		if i > oldLen || i > newLen {
+			break
+		}
+
+		var err error
+		if l.updateExternal {
+			item.(*boundExternalUntypedListItem).lock.Lock()
+			err = item.(*boundExternalUntypedListItem).setIfChanged((*l.val)[i])
+			item.(*boundExternalUntypedListItem).lock.Unlock()
+		} else {
+			item.(*boundUntypedListItem).lock.Lock()
+			err = item.(*boundUntypedListItem).doSet((*l.val)[i])
+			item.(*boundUntypedListItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (l *boundUntypedList) SetValue(i int, v interface{}) error {
+	l.lock.RLock()
+	len := l.Length()
+	l.lock.RUnlock()
+
+	if i < 0 || i >= len {
+		return errOutOfBounds
+	}
+
+	l.lock.Lock()
+	(*l.val)[i] = v
+	l.lock.Unlock()
+
+	item, err := l.GetItem(i)
+	if err != nil {
+		return err
+	}
+	return item.(Untyped).Set(v)
+}
+
+func bindUntypedListItem(v *[]interface{}, i int, external bool) Untyped {
+	if external {
+		ret := &boundExternalUntypedListItem{old: (*v)[i]}
+		ret.val = v
+		ret.index = i
+		return ret
+	}
+
+	return &boundUntypedListItem{val: v, index: i}
+}
+
+type boundUntypedListItem struct {
+	base
+
+	val   *[]interface{}
+	index int
+}
+
+func (b *boundUntypedListItem) Get() (interface{}, error) {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	if b.index < 0 || b.index >= len(*b.val) {
+		return nil, errOutOfBounds
+	}
+
+	return (*b.val)[b.index], nil
+}
+
+func (b *boundUntypedListItem) Set(val interface{}) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	return b.doSet(val)
+}
+
+func (b *boundUntypedListItem) doSet(val interface{}) error {
+	(*b.val)[b.index] = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalUntypedListItem struct {
+	boundUntypedListItem
+
+	old interface{}
+}
+
+func (b *boundExternalUntypedListItem) setIfChanged(val interface{}) error {
+	if val == b.old {
+		return nil
+	}
+	(*b.val)[b.index] = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}
+
+// URIList supports binding a list of fyne.URI values.
+//
+// Since: 2.1
+type URIList interface {
+	DataList
+
+	Append(value fyne.URI) error
+	Get() ([]fyne.URI, error)
+	GetValue(index int) (fyne.URI, error)
+	Prepend(value fyne.URI) error
+	Set(list []fyne.URI) error
+	SetValue(index int, value fyne.URI) error
+}
+
+// ExternalURIList supports binding a list of fyne.URI values from an external variable.
+//
+// Since: 2.1
+type ExternalURIList interface {
+	URIList
+
+	Reload() error
+}
+
+// NewURIList returns a bindable list of fyne.URI values.
+//
+// Since: 2.1
+func NewURIList() URIList {
+	return &boundURIList{val: &[]fyne.URI{}}
+}
+
+// BindURIList returns a bound list of fyne.URI values, based on the contents of the passed slice.
+// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.1
+func BindURIList(v *[]fyne.URI) ExternalURIList {
+	if v == nil {
+		return NewURIList().(ExternalURIList)
+	}
+
+	b := &boundURIList{val: v, updateExternal: true}
+
+	for i := range *v {
+		b.appendItem(bindURIListItem(v, i, b.updateExternal))
+	}
+
+	return b
+}
+
+type boundURIList struct {
+	listBase
+
+	updateExternal bool
+	val            *[]fyne.URI
+}
+
+func (l *boundURIList) Append(val fyne.URI) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	*l.val = append(*l.val, val)
+
+	return l.doReload()
+}
+
+func (l *boundURIList) Get() ([]fyne.URI, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	return *l.val, nil
+}
+
+func (l *boundURIList) GetValue(i int) (fyne.URI, error) {
+	l.lock.RLock()
+	defer l.lock.RUnlock()
+
+	if i < 0 || i >= l.Length() {
+		return fyne.URI(nil), errOutOfBounds
+	}
+
+	return (*l.val)[i], nil
+}
+
+func (l *boundURIList) Prepend(val fyne.URI) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = append([]fyne.URI{val}, *l.val...)
+
+	return l.doReload()
+}
+
+func (l *boundURIList) Reload() error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+
+	return l.doReload()
+}
+
+func (l *boundURIList) Set(v []fyne.URI) error {
+	l.lock.Lock()
+	defer l.lock.Unlock()
+	*l.val = v
+
+	return l.doReload()
+}
+
+func (l *boundURIList) doReload() (retErr error) {
+	oldLen := len(l.items)
+	newLen := len(*l.val)
+	if oldLen > newLen {
+		for i := oldLen - 1; i >= newLen; i-- {
+			l.deleteItem(i)
+		}
+		l.trigger()
+	} else if oldLen < newLen {
+		for i := oldLen; i < newLen; i++ {
+			l.appendItem(bindURIListItem(l.val, i, l.updateExternal))
+		}
+		l.trigger()
+	}
+
+	for i, item := range l.items {
+		if i > oldLen || i > newLen {
+			break
+		}
+
+		var err error
+		if l.updateExternal {
+			item.(*boundExternalURIListItem).lock.Lock()
+			err = item.(*boundExternalURIListItem).setIfChanged((*l.val)[i])
+			item.(*boundExternalURIListItem).lock.Unlock()
+		} else {
+			item.(*boundURIListItem).lock.Lock()
+			err = item.(*boundURIListItem).doSet((*l.val)[i])
+			item.(*boundURIListItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (l *boundURIList) SetValue(i int, v fyne.URI) error {
+	l.lock.RLock()
+	len := l.Length()
+	l.lock.RUnlock()
+
+	if i < 0 || i >= len {
+		return errOutOfBounds
+	}
+
+	l.lock.Lock()
+	(*l.val)[i] = v
+	l.lock.Unlock()
+
+	item, err := l.GetItem(i)
+	if err != nil {
+		return err
+	}
+	return item.(URI).Set(v)
+}
+
+func bindURIListItem(v *[]fyne.URI, i int, external bool) URI {
+	if external {
+		ret := &boundExternalURIListItem{old: (*v)[i]}
+		ret.val = v
+		ret.index = i
+		return ret
+	}
+
+	return &boundURIListItem{val: v, index: i}
+}
+
+type boundURIListItem struct {
+	base
+
+	val   *[]fyne.URI
+	index int
+}
+
+func (b *boundURIListItem) Get() (fyne.URI, error) {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	if b.index < 0 || b.index >= len(*b.val) {
+		return fyne.URI(nil), errOutOfBounds
+	}
+
+	return (*b.val)[b.index], nil
+}
+
+func (b *boundURIListItem) Set(val fyne.URI) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	return b.doSet(val)
+}
+
+func (b *boundURIListItem) doSet(val fyne.URI) error {
+	(*b.val)[b.index] = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalURIListItem struct {
+	boundURIListItem
+
+	old fyne.URI
+}
+
+func (b *boundExternalURIListItem) setIfChanged(val fyne.URI) error {
+	if compareURI(val, b.old) {
+		return nil
+	}
+	(*b.val)[b.index] = val
+	b.old = val
+
+	b.trigger()
+	return nil
+}

+ 1816 - 0
vendor/fyne.io/fyne/v2/data/binding/bindtrees.go

@@ -0,0 +1,1816 @@
+// auto-generated
+// **** THIS FILE IS AUTO-GENERATED, PLEASE DO NOT EDIT IT **** //
+
+package binding
+
+import (
+	"bytes"
+
+	"fyne.io/fyne/v2"
+)
+
+// BoolTree supports binding a tree of bool values.
+//
+// Since: 2.4
+type BoolTree interface {
+	DataTree
+
+	Append(parent, id string, value bool) error
+	Get() (map[string][]string, map[string]bool, error)
+	GetValue(id string) (bool, error)
+	Prepend(parent, id string, value bool) error
+	Set(ids map[string][]string, values map[string]bool) error
+	SetValue(id string, value bool) error
+}
+
+// ExternalBoolTree supports binding a tree of bool values from an external variable.
+//
+// Since: 2.4
+type ExternalBoolTree interface {
+	BoolTree
+
+	Reload() error
+}
+
+// NewBoolTree returns a bindable tree of bool values.
+//
+// Since: 2.4
+func NewBoolTree() BoolTree {
+	t := &boundBoolTree{val: &map[string]bool{}}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+	return t
+}
+
+// BindBoolTree returns a bound tree of bool values, based on the contents of the passed values.
+// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
+// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.4
+func BindBoolTree(ids *map[string][]string, v *map[string]bool) ExternalBoolTree {
+	if v == nil {
+		return NewBoolTree().(ExternalBoolTree)
+	}
+
+	t := &boundBoolTree{val: v, updateExternal: true}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+
+	for parent, children := range *ids {
+		for _, leaf := range children {
+			t.appendItem(bindBoolTreeItem(v, leaf, t.updateExternal), leaf, parent)
+		}
+	}
+
+	return t
+}
+
+type boundBoolTree struct {
+	treeBase
+
+	updateExternal bool
+	val            *map[string]bool
+}
+
+func (t *boundBoolTree) Append(parent, id string, val bool) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append(ids, id)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundBoolTree) Get() (map[string][]string, map[string]bool, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	return t.ids, *t.val, nil
+}
+
+func (t *boundBoolTree) GetValue(id string) (bool, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	if item, ok := (*t.val)[id]; ok {
+		return item, nil
+	}
+
+	return false, errOutOfBounds
+}
+
+func (t *boundBoolTree) Prepend(parent, id string, val bool) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append([]string{id}, ids...)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundBoolTree) Reload() error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doReload()
+}
+
+func (t *boundBoolTree) Set(ids map[string][]string, v map[string]bool) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	t.ids = ids
+	*t.val = v
+
+	return t.doReload()
+}
+
+func (t *boundBoolTree) doReload() (retErr error) {
+	updated := []string{}
+	fire := false
+	for id := range *t.val {
+		found := false
+		for child := range t.items {
+			if child == id { // update existing
+				updated = append(updated, id)
+				found = true
+				break
+			}
+		}
+		if found {
+			continue
+		}
+
+		// append new
+		t.appendItem(bindBoolTreeItem(t.val, id, t.updateExternal), id, parentIDFor(id, t.ids))
+		updated = append(updated, id)
+		fire = true
+	}
+
+	for id := range t.items {
+		remove := true
+		for _, done := range updated {
+			if done == id {
+				remove = false
+				break
+			}
+		}
+
+		if remove { // remove item no longer present
+			fire = true
+			t.deleteItem(id, parentIDFor(id, t.ids))
+		}
+	}
+	if fire {
+		t.trigger()
+	}
+
+	for id, item := range t.items {
+		var err error
+		if t.updateExternal {
+			item.(*boundExternalBoolTreeItem).lock.Lock()
+			err = item.(*boundExternalBoolTreeItem).setIfChanged((*t.val)[id])
+			item.(*boundExternalBoolTreeItem).lock.Unlock()
+		} else {
+			item.(*boundBoolTreeItem).lock.Lock()
+			err = item.(*boundBoolTreeItem).doSet((*t.val)[id])
+			item.(*boundBoolTreeItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (t *boundBoolTree) SetValue(id string, v bool) error {
+	t.lock.Lock()
+	(*t.val)[id] = v
+	t.lock.Unlock()
+
+	item, err := t.GetItem(id)
+	if err != nil {
+		return err
+	}
+	return item.(Bool).Set(v)
+}
+
+func bindBoolTreeItem(v *map[string]bool, id string, external bool) Bool {
+	if external {
+		ret := &boundExternalBoolTreeItem{old: (*v)[id]}
+		ret.val = v
+		ret.id = id
+		return ret
+	}
+
+	return &boundBoolTreeItem{id: id, val: v}
+}
+
+type boundBoolTreeItem struct {
+	base
+
+	val *map[string]bool
+	id  string
+}
+
+func (t *boundBoolTreeItem) Get() (bool, error) {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	v := *t.val
+	if item, ok := v[t.id]; ok {
+		return item, nil
+	}
+
+	return false, errOutOfBounds
+}
+
+func (t *boundBoolTreeItem) Set(val bool) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doSet(val)
+}
+
+func (t *boundBoolTreeItem) doSet(val bool) error {
+	(*t.val)[t.id] = val
+
+	t.trigger()
+	return nil
+}
+
+type boundExternalBoolTreeItem struct {
+	boundBoolTreeItem
+
+	old bool
+}
+
+func (t *boundExternalBoolTreeItem) setIfChanged(val bool) error {
+	if val == t.old {
+		return nil
+	}
+	(*t.val)[t.id] = val
+	t.old = val
+
+	t.trigger()
+	return nil
+}
+
+// BytesTree supports binding a tree of []byte values.
+//
+// Since: 2.4
+type BytesTree interface {
+	DataTree
+
+	Append(parent, id string, value []byte) error
+	Get() (map[string][]string, map[string][]byte, error)
+	GetValue(id string) ([]byte, error)
+	Prepend(parent, id string, value []byte) error
+	Set(ids map[string][]string, values map[string][]byte) error
+	SetValue(id string, value []byte) error
+}
+
+// ExternalBytesTree supports binding a tree of []byte values from an external variable.
+//
+// Since: 2.4
+type ExternalBytesTree interface {
+	BytesTree
+
+	Reload() error
+}
+
+// NewBytesTree returns a bindable tree of []byte values.
+//
+// Since: 2.4
+func NewBytesTree() BytesTree {
+	t := &boundBytesTree{val: &map[string][]byte{}}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+	return t
+}
+
+// BindBytesTree returns a bound tree of []byte values, based on the contents of the passed values.
+// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
+// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.4
+func BindBytesTree(ids *map[string][]string, v *map[string][]byte) ExternalBytesTree {
+	if v == nil {
+		return NewBytesTree().(ExternalBytesTree)
+	}
+
+	t := &boundBytesTree{val: v, updateExternal: true}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+
+	for parent, children := range *ids {
+		for _, leaf := range children {
+			t.appendItem(bindBytesTreeItem(v, leaf, t.updateExternal), leaf, parent)
+		}
+	}
+
+	return t
+}
+
+type boundBytesTree struct {
+	treeBase
+
+	updateExternal bool
+	val            *map[string][]byte
+}
+
+func (t *boundBytesTree) Append(parent, id string, val []byte) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append(ids, id)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundBytesTree) Get() (map[string][]string, map[string][]byte, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	return t.ids, *t.val, nil
+}
+
+func (t *boundBytesTree) GetValue(id string) ([]byte, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	if item, ok := (*t.val)[id]; ok {
+		return item, nil
+	}
+
+	return nil, errOutOfBounds
+}
+
+func (t *boundBytesTree) Prepend(parent, id string, val []byte) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append([]string{id}, ids...)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundBytesTree) Reload() error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doReload()
+}
+
+func (t *boundBytesTree) Set(ids map[string][]string, v map[string][]byte) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	t.ids = ids
+	*t.val = v
+
+	return t.doReload()
+}
+
+func (t *boundBytesTree) doReload() (retErr error) {
+	updated := []string{}
+	fire := false
+	for id := range *t.val {
+		found := false
+		for child := range t.items {
+			if child == id { // update existing
+				updated = append(updated, id)
+				found = true
+				break
+			}
+		}
+		if found {
+			continue
+		}
+
+		// append new
+		t.appendItem(bindBytesTreeItem(t.val, id, t.updateExternal), id, parentIDFor(id, t.ids))
+		updated = append(updated, id)
+		fire = true
+	}
+
+	for id := range t.items {
+		remove := true
+		for _, done := range updated {
+			if done == id {
+				remove = false
+				break
+			}
+		}
+
+		if remove { // remove item no longer present
+			fire = true
+			t.deleteItem(id, parentIDFor(id, t.ids))
+		}
+	}
+	if fire {
+		t.trigger()
+	}
+
+	for id, item := range t.items {
+		var err error
+		if t.updateExternal {
+			item.(*boundExternalBytesTreeItem).lock.Lock()
+			err = item.(*boundExternalBytesTreeItem).setIfChanged((*t.val)[id])
+			item.(*boundExternalBytesTreeItem).lock.Unlock()
+		} else {
+			item.(*boundBytesTreeItem).lock.Lock()
+			err = item.(*boundBytesTreeItem).doSet((*t.val)[id])
+			item.(*boundBytesTreeItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (t *boundBytesTree) SetValue(id string, v []byte) error {
+	t.lock.Lock()
+	(*t.val)[id] = v
+	t.lock.Unlock()
+
+	item, err := t.GetItem(id)
+	if err != nil {
+		return err
+	}
+	return item.(Bytes).Set(v)
+}
+
+func bindBytesTreeItem(v *map[string][]byte, id string, external bool) Bytes {
+	if external {
+		ret := &boundExternalBytesTreeItem{old: (*v)[id]}
+		ret.val = v
+		ret.id = id
+		return ret
+	}
+
+	return &boundBytesTreeItem{id: id, val: v}
+}
+
+type boundBytesTreeItem struct {
+	base
+
+	val *map[string][]byte
+	id  string
+}
+
+func (t *boundBytesTreeItem) Get() ([]byte, error) {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	v := *t.val
+	if item, ok := v[t.id]; ok {
+		return item, nil
+	}
+
+	return nil, errOutOfBounds
+}
+
+func (t *boundBytesTreeItem) Set(val []byte) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doSet(val)
+}
+
+func (t *boundBytesTreeItem) doSet(val []byte) error {
+	(*t.val)[t.id] = val
+
+	t.trigger()
+	return nil
+}
+
+type boundExternalBytesTreeItem struct {
+	boundBytesTreeItem
+
+	old []byte
+}
+
+func (t *boundExternalBytesTreeItem) setIfChanged(val []byte) error {
+	if bytes.Equal(val, t.old) {
+		return nil
+	}
+	(*t.val)[t.id] = val
+	t.old = val
+
+	t.trigger()
+	return nil
+}
+
+// FloatTree supports binding a tree of float64 values.
+//
+// Since: 2.4
+type FloatTree interface {
+	DataTree
+
+	Append(parent, id string, value float64) error
+	Get() (map[string][]string, map[string]float64, error)
+	GetValue(id string) (float64, error)
+	Prepend(parent, id string, value float64) error
+	Set(ids map[string][]string, values map[string]float64) error
+	SetValue(id string, value float64) error
+}
+
+// ExternalFloatTree supports binding a tree of float64 values from an external variable.
+//
+// Since: 2.4
+type ExternalFloatTree interface {
+	FloatTree
+
+	Reload() error
+}
+
+// NewFloatTree returns a bindable tree of float64 values.
+//
+// Since: 2.4
+func NewFloatTree() FloatTree {
+	t := &boundFloatTree{val: &map[string]float64{}}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+	return t
+}
+
+// BindFloatTree returns a bound tree of float64 values, based on the contents of the passed values.
+// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
+// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.4
+func BindFloatTree(ids *map[string][]string, v *map[string]float64) ExternalFloatTree {
+	if v == nil {
+		return NewFloatTree().(ExternalFloatTree)
+	}
+
+	t := &boundFloatTree{val: v, updateExternal: true}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+
+	for parent, children := range *ids {
+		for _, leaf := range children {
+			t.appendItem(bindFloatTreeItem(v, leaf, t.updateExternal), leaf, parent)
+		}
+	}
+
+	return t
+}
+
+type boundFloatTree struct {
+	treeBase
+
+	updateExternal bool
+	val            *map[string]float64
+}
+
+func (t *boundFloatTree) Append(parent, id string, val float64) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append(ids, id)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundFloatTree) Get() (map[string][]string, map[string]float64, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	return t.ids, *t.val, nil
+}
+
+func (t *boundFloatTree) GetValue(id string) (float64, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	if item, ok := (*t.val)[id]; ok {
+		return item, nil
+	}
+
+	return 0.0, errOutOfBounds
+}
+
+func (t *boundFloatTree) Prepend(parent, id string, val float64) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append([]string{id}, ids...)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundFloatTree) Reload() error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doReload()
+}
+
+func (t *boundFloatTree) Set(ids map[string][]string, v map[string]float64) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	t.ids = ids
+	*t.val = v
+
+	return t.doReload()
+}
+
+func (t *boundFloatTree) doReload() (retErr error) {
+	updated := []string{}
+	fire := false
+	for id := range *t.val {
+		found := false
+		for child := range t.items {
+			if child == id { // update existing
+				updated = append(updated, id)
+				found = true
+				break
+			}
+		}
+		if found {
+			continue
+		}
+
+		// append new
+		t.appendItem(bindFloatTreeItem(t.val, id, t.updateExternal), id, parentIDFor(id, t.ids))
+		updated = append(updated, id)
+		fire = true
+	}
+
+	for id := range t.items {
+		remove := true
+		for _, done := range updated {
+			if done == id {
+				remove = false
+				break
+			}
+		}
+
+		if remove { // remove item no longer present
+			fire = true
+			t.deleteItem(id, parentIDFor(id, t.ids))
+		}
+	}
+	if fire {
+		t.trigger()
+	}
+
+	for id, item := range t.items {
+		var err error
+		if t.updateExternal {
+			item.(*boundExternalFloatTreeItem).lock.Lock()
+			err = item.(*boundExternalFloatTreeItem).setIfChanged((*t.val)[id])
+			item.(*boundExternalFloatTreeItem).lock.Unlock()
+		} else {
+			item.(*boundFloatTreeItem).lock.Lock()
+			err = item.(*boundFloatTreeItem).doSet((*t.val)[id])
+			item.(*boundFloatTreeItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (t *boundFloatTree) SetValue(id string, v float64) error {
+	t.lock.Lock()
+	(*t.val)[id] = v
+	t.lock.Unlock()
+
+	item, err := t.GetItem(id)
+	if err != nil {
+		return err
+	}
+	return item.(Float).Set(v)
+}
+
+func bindFloatTreeItem(v *map[string]float64, id string, external bool) Float {
+	if external {
+		ret := &boundExternalFloatTreeItem{old: (*v)[id]}
+		ret.val = v
+		ret.id = id
+		return ret
+	}
+
+	return &boundFloatTreeItem{id: id, val: v}
+}
+
+type boundFloatTreeItem struct {
+	base
+
+	val *map[string]float64
+	id  string
+}
+
+func (t *boundFloatTreeItem) Get() (float64, error) {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	v := *t.val
+	if item, ok := v[t.id]; ok {
+		return item, nil
+	}
+
+	return 0.0, errOutOfBounds
+}
+
+func (t *boundFloatTreeItem) Set(val float64) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doSet(val)
+}
+
+func (t *boundFloatTreeItem) doSet(val float64) error {
+	(*t.val)[t.id] = val
+
+	t.trigger()
+	return nil
+}
+
+type boundExternalFloatTreeItem struct {
+	boundFloatTreeItem
+
+	old float64
+}
+
+func (t *boundExternalFloatTreeItem) setIfChanged(val float64) error {
+	if val == t.old {
+		return nil
+	}
+	(*t.val)[t.id] = val
+	t.old = val
+
+	t.trigger()
+	return nil
+}
+
+// IntTree supports binding a tree of int values.
+//
+// Since: 2.4
+type IntTree interface {
+	DataTree
+
+	Append(parent, id string, value int) error
+	Get() (map[string][]string, map[string]int, error)
+	GetValue(id string) (int, error)
+	Prepend(parent, id string, value int) error
+	Set(ids map[string][]string, values map[string]int) error
+	SetValue(id string, value int) error
+}
+
+// ExternalIntTree supports binding a tree of int values from an external variable.
+//
+// Since: 2.4
+type ExternalIntTree interface {
+	IntTree
+
+	Reload() error
+}
+
+// NewIntTree returns a bindable tree of int values.
+//
+// Since: 2.4
+func NewIntTree() IntTree {
+	t := &boundIntTree{val: &map[string]int{}}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+	return t
+}
+
+// BindIntTree returns a bound tree of int values, based on the contents of the passed values.
+// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
+// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.4
+func BindIntTree(ids *map[string][]string, v *map[string]int) ExternalIntTree {
+	if v == nil {
+		return NewIntTree().(ExternalIntTree)
+	}
+
+	t := &boundIntTree{val: v, updateExternal: true}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+
+	for parent, children := range *ids {
+		for _, leaf := range children {
+			t.appendItem(bindIntTreeItem(v, leaf, t.updateExternal), leaf, parent)
+		}
+	}
+
+	return t
+}
+
+type boundIntTree struct {
+	treeBase
+
+	updateExternal bool
+	val            *map[string]int
+}
+
+func (t *boundIntTree) Append(parent, id string, val int) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append(ids, id)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundIntTree) Get() (map[string][]string, map[string]int, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	return t.ids, *t.val, nil
+}
+
+func (t *boundIntTree) GetValue(id string) (int, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	if item, ok := (*t.val)[id]; ok {
+		return item, nil
+	}
+
+	return 0, errOutOfBounds
+}
+
+func (t *boundIntTree) Prepend(parent, id string, val int) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append([]string{id}, ids...)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundIntTree) Reload() error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doReload()
+}
+
+func (t *boundIntTree) Set(ids map[string][]string, v map[string]int) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	t.ids = ids
+	*t.val = v
+
+	return t.doReload()
+}
+
+func (t *boundIntTree) doReload() (retErr error) {
+	updated := []string{}
+	fire := false
+	for id := range *t.val {
+		found := false
+		for child := range t.items {
+			if child == id { // update existing
+				updated = append(updated, id)
+				found = true
+				break
+			}
+		}
+		if found {
+			continue
+		}
+
+		// append new
+		t.appendItem(bindIntTreeItem(t.val, id, t.updateExternal), id, parentIDFor(id, t.ids))
+		updated = append(updated, id)
+		fire = true
+	}
+
+	for id := range t.items {
+		remove := true
+		for _, done := range updated {
+			if done == id {
+				remove = false
+				break
+			}
+		}
+
+		if remove { // remove item no longer present
+			fire = true
+			t.deleteItem(id, parentIDFor(id, t.ids))
+		}
+	}
+	if fire {
+		t.trigger()
+	}
+
+	for id, item := range t.items {
+		var err error
+		if t.updateExternal {
+			item.(*boundExternalIntTreeItem).lock.Lock()
+			err = item.(*boundExternalIntTreeItem).setIfChanged((*t.val)[id])
+			item.(*boundExternalIntTreeItem).lock.Unlock()
+		} else {
+			item.(*boundIntTreeItem).lock.Lock()
+			err = item.(*boundIntTreeItem).doSet((*t.val)[id])
+			item.(*boundIntTreeItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (t *boundIntTree) SetValue(id string, v int) error {
+	t.lock.Lock()
+	(*t.val)[id] = v
+	t.lock.Unlock()
+
+	item, err := t.GetItem(id)
+	if err != nil {
+		return err
+	}
+	return item.(Int).Set(v)
+}
+
+func bindIntTreeItem(v *map[string]int, id string, external bool) Int {
+	if external {
+		ret := &boundExternalIntTreeItem{old: (*v)[id]}
+		ret.val = v
+		ret.id = id
+		return ret
+	}
+
+	return &boundIntTreeItem{id: id, val: v}
+}
+
+type boundIntTreeItem struct {
+	base
+
+	val *map[string]int
+	id  string
+}
+
+func (t *boundIntTreeItem) Get() (int, error) {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	v := *t.val
+	if item, ok := v[t.id]; ok {
+		return item, nil
+	}
+
+	return 0, errOutOfBounds
+}
+
+func (t *boundIntTreeItem) Set(val int) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doSet(val)
+}
+
+func (t *boundIntTreeItem) doSet(val int) error {
+	(*t.val)[t.id] = val
+
+	t.trigger()
+	return nil
+}
+
+type boundExternalIntTreeItem struct {
+	boundIntTreeItem
+
+	old int
+}
+
+func (t *boundExternalIntTreeItem) setIfChanged(val int) error {
+	if val == t.old {
+		return nil
+	}
+	(*t.val)[t.id] = val
+	t.old = val
+
+	t.trigger()
+	return nil
+}
+
+// RuneTree supports binding a tree of rune values.
+//
+// Since: 2.4
+type RuneTree interface {
+	DataTree
+
+	Append(parent, id string, value rune) error
+	Get() (map[string][]string, map[string]rune, error)
+	GetValue(id string) (rune, error)
+	Prepend(parent, id string, value rune) error
+	Set(ids map[string][]string, values map[string]rune) error
+	SetValue(id string, value rune) error
+}
+
+// ExternalRuneTree supports binding a tree of rune values from an external variable.
+//
+// Since: 2.4
+type ExternalRuneTree interface {
+	RuneTree
+
+	Reload() error
+}
+
+// NewRuneTree returns a bindable tree of rune values.
+//
+// Since: 2.4
+func NewRuneTree() RuneTree {
+	t := &boundRuneTree{val: &map[string]rune{}}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+	return t
+}
+
+// BindRuneTree returns a bound tree of rune values, based on the contents of the passed values.
+// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
+// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.4
+func BindRuneTree(ids *map[string][]string, v *map[string]rune) ExternalRuneTree {
+	if v == nil {
+		return NewRuneTree().(ExternalRuneTree)
+	}
+
+	t := &boundRuneTree{val: v, updateExternal: true}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+
+	for parent, children := range *ids {
+		for _, leaf := range children {
+			t.appendItem(bindRuneTreeItem(v, leaf, t.updateExternal), leaf, parent)
+		}
+	}
+
+	return t
+}
+
+type boundRuneTree struct {
+	treeBase
+
+	updateExternal bool
+	val            *map[string]rune
+}
+
+func (t *boundRuneTree) Append(parent, id string, val rune) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append(ids, id)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundRuneTree) Get() (map[string][]string, map[string]rune, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	return t.ids, *t.val, nil
+}
+
+func (t *boundRuneTree) GetValue(id string) (rune, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	if item, ok := (*t.val)[id]; ok {
+		return item, nil
+	}
+
+	return rune(0), errOutOfBounds
+}
+
+func (t *boundRuneTree) Prepend(parent, id string, val rune) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append([]string{id}, ids...)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundRuneTree) Reload() error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doReload()
+}
+
+func (t *boundRuneTree) Set(ids map[string][]string, v map[string]rune) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	t.ids = ids
+	*t.val = v
+
+	return t.doReload()
+}
+
+func (t *boundRuneTree) doReload() (retErr error) {
+	updated := []string{}
+	fire := false
+	for id := range *t.val {
+		found := false
+		for child := range t.items {
+			if child == id { // update existing
+				updated = append(updated, id)
+				found = true
+				break
+			}
+		}
+		if found {
+			continue
+		}
+
+		// append new
+		t.appendItem(bindRuneTreeItem(t.val, id, t.updateExternal), id, parentIDFor(id, t.ids))
+		updated = append(updated, id)
+		fire = true
+	}
+
+	for id := range t.items {
+		remove := true
+		for _, done := range updated {
+			if done == id {
+				remove = false
+				break
+			}
+		}
+
+		if remove { // remove item no longer present
+			fire = true
+			t.deleteItem(id, parentIDFor(id, t.ids))
+		}
+	}
+	if fire {
+		t.trigger()
+	}
+
+	for id, item := range t.items {
+		var err error
+		if t.updateExternal {
+			item.(*boundExternalRuneTreeItem).lock.Lock()
+			err = item.(*boundExternalRuneTreeItem).setIfChanged((*t.val)[id])
+			item.(*boundExternalRuneTreeItem).lock.Unlock()
+		} else {
+			item.(*boundRuneTreeItem).lock.Lock()
+			err = item.(*boundRuneTreeItem).doSet((*t.val)[id])
+			item.(*boundRuneTreeItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (t *boundRuneTree) SetValue(id string, v rune) error {
+	t.lock.Lock()
+	(*t.val)[id] = v
+	t.lock.Unlock()
+
+	item, err := t.GetItem(id)
+	if err != nil {
+		return err
+	}
+	return item.(Rune).Set(v)
+}
+
+func bindRuneTreeItem(v *map[string]rune, id string, external bool) Rune {
+	if external {
+		ret := &boundExternalRuneTreeItem{old: (*v)[id]}
+		ret.val = v
+		ret.id = id
+		return ret
+	}
+
+	return &boundRuneTreeItem{id: id, val: v}
+}
+
+type boundRuneTreeItem struct {
+	base
+
+	val *map[string]rune
+	id  string
+}
+
+func (t *boundRuneTreeItem) Get() (rune, error) {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	v := *t.val
+	if item, ok := v[t.id]; ok {
+		return item, nil
+	}
+
+	return rune(0), errOutOfBounds
+}
+
+func (t *boundRuneTreeItem) Set(val rune) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doSet(val)
+}
+
+func (t *boundRuneTreeItem) doSet(val rune) error {
+	(*t.val)[t.id] = val
+
+	t.trigger()
+	return nil
+}
+
+type boundExternalRuneTreeItem struct {
+	boundRuneTreeItem
+
+	old rune
+}
+
+func (t *boundExternalRuneTreeItem) setIfChanged(val rune) error {
+	if val == t.old {
+		return nil
+	}
+	(*t.val)[t.id] = val
+	t.old = val
+
+	t.trigger()
+	return nil
+}
+
+// StringTree supports binding a tree of string values.
+//
+// Since: 2.4
+type StringTree interface {
+	DataTree
+
+	Append(parent, id string, value string) error
+	Get() (map[string][]string, map[string]string, error)
+	GetValue(id string) (string, error)
+	Prepend(parent, id string, value string) error
+	Set(ids map[string][]string, values map[string]string) error
+	SetValue(id string, value string) error
+}
+
+// ExternalStringTree supports binding a tree of string values from an external variable.
+//
+// Since: 2.4
+type ExternalStringTree interface {
+	StringTree
+
+	Reload() error
+}
+
+// NewStringTree returns a bindable tree of string values.
+//
+// Since: 2.4
+func NewStringTree() StringTree {
+	t := &boundStringTree{val: &map[string]string{}}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+	return t
+}
+
+// BindStringTree returns a bound tree of string values, based on the contents of the passed values.
+// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
+// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.4
+func BindStringTree(ids *map[string][]string, v *map[string]string) ExternalStringTree {
+	if v == nil {
+		return NewStringTree().(ExternalStringTree)
+	}
+
+	t := &boundStringTree{val: v, updateExternal: true}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+
+	for parent, children := range *ids {
+		for _, leaf := range children {
+			t.appendItem(bindStringTreeItem(v, leaf, t.updateExternal), leaf, parent)
+		}
+	}
+
+	return t
+}
+
+type boundStringTree struct {
+	treeBase
+
+	updateExternal bool
+	val            *map[string]string
+}
+
+func (t *boundStringTree) Append(parent, id string, val string) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append(ids, id)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundStringTree) Get() (map[string][]string, map[string]string, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	return t.ids, *t.val, nil
+}
+
+func (t *boundStringTree) GetValue(id string) (string, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	if item, ok := (*t.val)[id]; ok {
+		return item, nil
+	}
+
+	return "", errOutOfBounds
+}
+
+func (t *boundStringTree) Prepend(parent, id string, val string) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append([]string{id}, ids...)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundStringTree) Reload() error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doReload()
+}
+
+func (t *boundStringTree) Set(ids map[string][]string, v map[string]string) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	t.ids = ids
+	*t.val = v
+
+	return t.doReload()
+}
+
+func (t *boundStringTree) doReload() (retErr error) {
+	updated := []string{}
+	fire := false
+	for id := range *t.val {
+		found := false
+		for child := range t.items {
+			if child == id { // update existing
+				updated = append(updated, id)
+				found = true
+				break
+			}
+		}
+		if found {
+			continue
+		}
+
+		// append new
+		t.appendItem(bindStringTreeItem(t.val, id, t.updateExternal), id, parentIDFor(id, t.ids))
+		updated = append(updated, id)
+		fire = true
+	}
+
+	for id := range t.items {
+		remove := true
+		for _, done := range updated {
+			if done == id {
+				remove = false
+				break
+			}
+		}
+
+		if remove { // remove item no longer present
+			fire = true
+			t.deleteItem(id, parentIDFor(id, t.ids))
+		}
+	}
+	if fire {
+		t.trigger()
+	}
+
+	for id, item := range t.items {
+		var err error
+		if t.updateExternal {
+			item.(*boundExternalStringTreeItem).lock.Lock()
+			err = item.(*boundExternalStringTreeItem).setIfChanged((*t.val)[id])
+			item.(*boundExternalStringTreeItem).lock.Unlock()
+		} else {
+			item.(*boundStringTreeItem).lock.Lock()
+			err = item.(*boundStringTreeItem).doSet((*t.val)[id])
+			item.(*boundStringTreeItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (t *boundStringTree) SetValue(id string, v string) error {
+	t.lock.Lock()
+	(*t.val)[id] = v
+	t.lock.Unlock()
+
+	item, err := t.GetItem(id)
+	if err != nil {
+		return err
+	}
+	return item.(String).Set(v)
+}
+
+func bindStringTreeItem(v *map[string]string, id string, external bool) String {
+	if external {
+		ret := &boundExternalStringTreeItem{old: (*v)[id]}
+		ret.val = v
+		ret.id = id
+		return ret
+	}
+
+	return &boundStringTreeItem{id: id, val: v}
+}
+
+type boundStringTreeItem struct {
+	base
+
+	val *map[string]string
+	id  string
+}
+
+func (t *boundStringTreeItem) Get() (string, error) {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	v := *t.val
+	if item, ok := v[t.id]; ok {
+		return item, nil
+	}
+
+	return "", errOutOfBounds
+}
+
+func (t *boundStringTreeItem) Set(val string) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doSet(val)
+}
+
+func (t *boundStringTreeItem) doSet(val string) error {
+	(*t.val)[t.id] = val
+
+	t.trigger()
+	return nil
+}
+
+type boundExternalStringTreeItem struct {
+	boundStringTreeItem
+
+	old string
+}
+
+func (t *boundExternalStringTreeItem) setIfChanged(val string) error {
+	if val == t.old {
+		return nil
+	}
+	(*t.val)[t.id] = val
+	t.old = val
+
+	t.trigger()
+	return nil
+}
+
+// URITree supports binding a tree of fyne.URI values.
+//
+// Since: 2.4
+type URITree interface {
+	DataTree
+
+	Append(parent, id string, value fyne.URI) error
+	Get() (map[string][]string, map[string]fyne.URI, error)
+	GetValue(id string) (fyne.URI, error)
+	Prepend(parent, id string, value fyne.URI) error
+	Set(ids map[string][]string, values map[string]fyne.URI) error
+	SetValue(id string, value fyne.URI) error
+}
+
+// ExternalURITree supports binding a tree of fyne.URI values from an external variable.
+//
+// Since: 2.4
+type ExternalURITree interface {
+	URITree
+
+	Reload() error
+}
+
+// NewURITree returns a bindable tree of fyne.URI values.
+//
+// Since: 2.4
+func NewURITree() URITree {
+	t := &boundURITree{val: &map[string]fyne.URI{}}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+	return t
+}
+
+// BindURITree returns a bound tree of fyne.URI values, based on the contents of the passed values.
+// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
+// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.4
+func BindURITree(ids *map[string][]string, v *map[string]fyne.URI) ExternalURITree {
+	if v == nil {
+		return NewURITree().(ExternalURITree)
+	}
+
+	t := &boundURITree{val: v, updateExternal: true}
+	t.ids = make(map[string][]string)
+	t.items = make(map[string]DataItem)
+
+	for parent, children := range *ids {
+		for _, leaf := range children {
+			t.appendItem(bindURITreeItem(v, leaf, t.updateExternal), leaf, parent)
+		}
+	}
+
+	return t
+}
+
+type boundURITree struct {
+	treeBase
+
+	updateExternal bool
+	val            *map[string]fyne.URI
+}
+
+func (t *boundURITree) Append(parent, id string, val fyne.URI) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append(ids, id)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundURITree) Get() (map[string][]string, map[string]fyne.URI, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	return t.ids, *t.val, nil
+}
+
+func (t *boundURITree) GetValue(id string) (fyne.URI, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	if item, ok := (*t.val)[id]; ok {
+		return item, nil
+	}
+
+	return fyne.URI(nil), errOutOfBounds
+}
+
+func (t *boundURITree) Prepend(parent, id string, val fyne.URI) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	t.ids[parent] = append([]string{id}, ids...)
+	v := *t.val
+	v[id] = val
+
+	return t.doReload()
+}
+
+func (t *boundURITree) Reload() error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doReload()
+}
+
+func (t *boundURITree) Set(ids map[string][]string, v map[string]fyne.URI) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+	t.ids = ids
+	*t.val = v
+
+	return t.doReload()
+}
+
+func (t *boundURITree) doReload() (retErr error) {
+	updated := []string{}
+	fire := false
+	for id := range *t.val {
+		found := false
+		for child := range t.items {
+			if child == id { // update existing
+				updated = append(updated, id)
+				found = true
+				break
+			}
+		}
+		if found {
+			continue
+		}
+
+		// append new
+		t.appendItem(bindURITreeItem(t.val, id, t.updateExternal), id, parentIDFor(id, t.ids))
+		updated = append(updated, id)
+		fire = true
+	}
+
+	for id := range t.items {
+		remove := true
+		for _, done := range updated {
+			if done == id {
+				remove = false
+				break
+			}
+		}
+
+		if remove { // remove item no longer present
+			fire = true
+			t.deleteItem(id, parentIDFor(id, t.ids))
+		}
+	}
+	if fire {
+		t.trigger()
+	}
+
+	for id, item := range t.items {
+		var err error
+		if t.updateExternal {
+			item.(*boundExternalURITreeItem).lock.Lock()
+			err = item.(*boundExternalURITreeItem).setIfChanged((*t.val)[id])
+			item.(*boundExternalURITreeItem).lock.Unlock()
+		} else {
+			item.(*boundURITreeItem).lock.Lock()
+			err = item.(*boundURITreeItem).doSet((*t.val)[id])
+			item.(*boundURITreeItem).lock.Unlock()
+		}
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (t *boundURITree) SetValue(id string, v fyne.URI) error {
+	t.lock.Lock()
+	(*t.val)[id] = v
+	t.lock.Unlock()
+
+	item, err := t.GetItem(id)
+	if err != nil {
+		return err
+	}
+	return item.(URI).Set(v)
+}
+
+func bindURITreeItem(v *map[string]fyne.URI, id string, external bool) URI {
+	if external {
+		ret := &boundExternalURITreeItem{old: (*v)[id]}
+		ret.val = v
+		ret.id = id
+		return ret
+	}
+
+	return &boundURITreeItem{id: id, val: v}
+}
+
+type boundURITreeItem struct {
+	base
+
+	val *map[string]fyne.URI
+	id  string
+}
+
+func (t *boundURITreeItem) Get() (fyne.URI, error) {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	v := *t.val
+	if item, ok := v[t.id]; ok {
+		return item, nil
+	}
+
+	return fyne.URI(nil), errOutOfBounds
+}
+
+func (t *boundURITreeItem) Set(val fyne.URI) error {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	return t.doSet(val)
+}
+
+func (t *boundURITreeItem) doSet(val fyne.URI) error {
+	(*t.val)[t.id] = val
+
+	t.trigger()
+	return nil
+}
+
+type boundExternalURITreeItem struct {
+	boundURITreeItem
+
+	old fyne.URI
+}
+
+func (t *boundExternalURITreeItem) setIfChanged(val fyne.URI) error {
+	if compareURI(val, t.old) {
+		return nil
+	}
+	(*t.val)[t.id] = val
+	t.old = val
+
+	t.trigger()
+	return nil
+}

+ 118 - 0
vendor/fyne.io/fyne/v2/data/binding/bool.go

@@ -0,0 +1,118 @@
+package binding
+
+type not struct {
+	Bool
+}
+
+var _ Bool = (*not)(nil)
+
+// Not returns a Bool binding that invert the value of the given data binding.
+// This is providing the logical Not boolean operation as a data binding.
+//
+// Since 2.4
+func Not(data Bool) Bool {
+	return &not{Bool: data}
+}
+
+func (n *not) Get() (bool, error) {
+	v, err := n.Bool.Get()
+	return !v, err
+}
+
+func (n *not) Set(value bool) error {
+	return n.Bool.Set(!value)
+}
+
+type and struct {
+	booleans
+}
+
+var _ Bool = (*and)(nil)
+
+// And returns a Bool binding that return true when all the passed Bool binding are
+// true and false otherwise. It does apply a logical and boolean operation on all passed
+// Bool bindings. This binding is two way. In case of a Set, it will propagate the value
+// identically to all the Bool bindings used for its construction.
+//
+// Since 2.4
+func And(data ...Bool) Bool {
+	return &and{booleans: booleans{data: data}}
+}
+
+func (a *and) Get() (bool, error) {
+	for _, d := range a.data {
+		v, err := d.Get()
+		if err != nil {
+			return false, err
+		}
+		if !v {
+			return false, nil
+		}
+	}
+	return true, nil
+}
+
+func (a *and) Set(value bool) error {
+	for _, d := range a.data {
+		err := d.Set(value)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+type or struct {
+	booleans
+}
+
+var _ Bool = (*or)(nil)
+
+// Or returns a Bool binding that return true when at least one of the passed Bool binding
+// is true and false otherwise. It does apply a logical or boolean operation on all passed
+// Bool bindings. This binding is two way. In case of a Set, it will propagate the value
+// identically to all the Bool bindings used for its construction.
+//
+// Since 2.4
+func Or(data ...Bool) Bool {
+	return &or{booleans: booleans{data: data}}
+}
+
+func (o *or) Get() (bool, error) {
+	for _, d := range o.data {
+		v, err := d.Get()
+		if err != nil {
+			return false, err
+		}
+		if v {
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
+func (o *or) Set(value bool) error {
+	for _, d := range o.data {
+		err := d.Set(value)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+type booleans struct {
+	data []Bool
+}
+
+func (g *booleans) AddListener(listener DataListener) {
+	for _, d := range g.data {
+		d.AddListener(listener)
+	}
+}
+
+func (g *booleans) RemoveListener(listener DataListener) {
+	for _, d := range g.data {
+		d.RemoveListener(listener)
+	}
+}

+ 13 - 0
vendor/fyne.io/fyne/v2/data/binding/comparator_helper.go

@@ -0,0 +1,13 @@
+package binding
+
+import "fyne.io/fyne/v2"
+
+func compareURI(v1, v2 fyne.URI) bool {
+	if v1 == nil && v1 == v2 {
+		return true
+	}
+	if v1 == nil || v2 == nil {
+		return false
+	}
+	return v1.String() == v2.String()
+}

+ 638 - 0
vendor/fyne.io/fyne/v2/data/binding/convert.go

@@ -0,0 +1,638 @@
+// auto-generated
+// **** THIS FILE IS AUTO-GENERATED, PLEASE DO NOT EDIT IT **** //
+
+package binding
+
+import (
+	"fmt"
+
+	"fyne.io/fyne/v2"
+)
+
+type stringFromBool struct {
+	base
+
+	format string
+
+	from Bool
+}
+
+// BoolToString creates a binding that connects a Bool data item to a String.
+// Changes to the Bool will be pushed to the String and setting the string will parse and set the
+// Bool if the parse was successful.
+//
+// Since: 2.0
+func BoolToString(v Bool) String {
+	str := &stringFromBool{from: v}
+	v.AddListener(str)
+	return str
+}
+
+// BoolToStringWithFormat creates a binding that connects a Bool data item to a String and is
+// presented using the specified format. Changes to the Bool will be pushed to the String and setting
+// the string will parse and set the Bool if the string matches the format and its parse was successful.
+//
+// Since: 2.0
+func BoolToStringWithFormat(v Bool, format string) String {
+	if format == "%t" { // Same as not using custom formatting.
+		return BoolToString(v)
+	}
+
+	str := &stringFromBool{from: v, format: format}
+	v.AddListener(str)
+	return str
+}
+
+func (s *stringFromBool) Get() (string, error) {
+	val, err := s.from.Get()
+	if err != nil {
+		return "", err
+	}
+
+	if s.format != "" {
+		return fmt.Sprintf(s.format, val), nil
+	}
+
+	return formatBool(val), nil
+}
+
+func (s *stringFromBool) Set(str string) error {
+	var val bool
+	if s.format != "" {
+		safe := stripFormatPrecision(s.format)
+		n, err := fmt.Sscanf(str, safe+" ", &val) // " " denotes match to end of string
+		if err != nil {
+			return err
+		}
+		if n != 1 {
+			return errParseFailed
+		}
+	} else {
+		new, err := parseBool(str)
+		if err != nil {
+			return err
+		}
+		val = new
+	}
+
+	old, err := s.from.Get()
+	if err != nil {
+		return err
+	}
+	if val == old {
+		return nil
+	}
+	if err = s.from.Set(val); err != nil {
+		return err
+	}
+
+	s.DataChanged()
+	return nil
+}
+
+func (s *stringFromBool) DataChanged() {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+	s.trigger()
+}
+
+type stringFromFloat struct {
+	base
+
+	format string
+
+	from Float
+}
+
+// FloatToString creates a binding that connects a Float data item to a String.
+// Changes to the Float will be pushed to the String and setting the string will parse and set the
+// Float if the parse was successful.
+//
+// Since: 2.0
+func FloatToString(v Float) String {
+	str := &stringFromFloat{from: v}
+	v.AddListener(str)
+	return str
+}
+
+// FloatToStringWithFormat creates a binding that connects a Float data item to a String and is
+// presented using the specified format. Changes to the Float will be pushed to the String and setting
+// the string will parse and set the Float if the string matches the format and its parse was successful.
+//
+// Since: 2.0
+func FloatToStringWithFormat(v Float, format string) String {
+	if format == "%f" { // Same as not using custom formatting.
+		return FloatToString(v)
+	}
+
+	str := &stringFromFloat{from: v, format: format}
+	v.AddListener(str)
+	return str
+}
+
+func (s *stringFromFloat) Get() (string, error) {
+	val, err := s.from.Get()
+	if err != nil {
+		return "", err
+	}
+
+	if s.format != "" {
+		return fmt.Sprintf(s.format, val), nil
+	}
+
+	return formatFloat(val), nil
+}
+
+func (s *stringFromFloat) Set(str string) error {
+	var val float64
+	if s.format != "" {
+		safe := stripFormatPrecision(s.format)
+		n, err := fmt.Sscanf(str, safe+" ", &val) // " " denotes match to end of string
+		if err != nil {
+			return err
+		}
+		if n != 1 {
+			return errParseFailed
+		}
+	} else {
+		new, err := parseFloat(str)
+		if err != nil {
+			return err
+		}
+		val = new
+	}
+
+	old, err := s.from.Get()
+	if err != nil {
+		return err
+	}
+	if val == old {
+		return nil
+	}
+	if err = s.from.Set(val); err != nil {
+		return err
+	}
+
+	s.DataChanged()
+	return nil
+}
+
+func (s *stringFromFloat) DataChanged() {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+	s.trigger()
+}
+
+type stringFromInt struct {
+	base
+
+	format string
+
+	from Int
+}
+
+// IntToString creates a binding that connects a Int data item to a String.
+// Changes to the Int will be pushed to the String and setting the string will parse and set the
+// Int if the parse was successful.
+//
+// Since: 2.0
+func IntToString(v Int) String {
+	str := &stringFromInt{from: v}
+	v.AddListener(str)
+	return str
+}
+
+// IntToStringWithFormat creates a binding that connects a Int data item to a String and is
+// presented using the specified format. Changes to the Int will be pushed to the String and setting
+// the string will parse and set the Int if the string matches the format and its parse was successful.
+//
+// Since: 2.0
+func IntToStringWithFormat(v Int, format string) String {
+	if format == "%d" { // Same as not using custom formatting.
+		return IntToString(v)
+	}
+
+	str := &stringFromInt{from: v, format: format}
+	v.AddListener(str)
+	return str
+}
+
+func (s *stringFromInt) Get() (string, error) {
+	val, err := s.from.Get()
+	if err != nil {
+		return "", err
+	}
+
+	if s.format != "" {
+		return fmt.Sprintf(s.format, val), nil
+	}
+
+	return formatInt(val), nil
+}
+
+func (s *stringFromInt) Set(str string) error {
+	var val int
+	if s.format != "" {
+		safe := stripFormatPrecision(s.format)
+		n, err := fmt.Sscanf(str, safe+" ", &val) // " " denotes match to end of string
+		if err != nil {
+			return err
+		}
+		if n != 1 {
+			return errParseFailed
+		}
+	} else {
+		new, err := parseInt(str)
+		if err != nil {
+			return err
+		}
+		val = new
+	}
+
+	old, err := s.from.Get()
+	if err != nil {
+		return err
+	}
+	if val == old {
+		return nil
+	}
+	if err = s.from.Set(val); err != nil {
+		return err
+	}
+
+	s.DataChanged()
+	return nil
+}
+
+func (s *stringFromInt) DataChanged() {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+	s.trigger()
+}
+
+type stringFromURI struct {
+	base
+
+	from URI
+}
+
+// URIToString creates a binding that connects a URI data item to a String.
+// Changes to the URI will be pushed to the String and setting the string will parse and set the
+// URI if the parse was successful.
+//
+// Since: 2.1
+func URIToString(v URI) String {
+	str := &stringFromURI{from: v}
+	v.AddListener(str)
+	return str
+}
+
+func (s *stringFromURI) Get() (string, error) {
+	val, err := s.from.Get()
+	if err != nil {
+		return "", err
+	}
+
+	return uriToString(val)
+}
+
+func (s *stringFromURI) Set(str string) error {
+	val, err := uriFromString(str)
+	if err != nil {
+		return err
+	}
+
+	old, err := s.from.Get()
+	if err != nil {
+		return err
+	}
+	if val == old {
+		return nil
+	}
+	if err = s.from.Set(val); err != nil {
+		return err
+	}
+
+	s.DataChanged()
+	return nil
+}
+
+func (s *stringFromURI) DataChanged() {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+	s.trigger()
+}
+
+type stringToBool struct {
+	base
+
+	format string
+
+	from String
+}
+
+// StringToBool creates a binding that connects a String data item to a Bool.
+// Changes to the String will be parsed and pushed to the Bool if the parse was successful, and setting
+// the Bool update the String binding.
+//
+// Since: 2.0
+func StringToBool(str String) Bool {
+	v := &stringToBool{from: str}
+	str.AddListener(v)
+	return v
+}
+
+// StringToBoolWithFormat creates a binding that connects a String data item to a Bool and is
+// presented using the specified format. Changes to the Bool will be parsed and if the format matches and
+// the parse is successful it will be pushed to the String. Setting the Bool will push a formatted value
+// into the String.
+//
+// Since: 2.0
+func StringToBoolWithFormat(str String, format string) Bool {
+	if format == "%t" { // Same as not using custom format.
+		return StringToBool(str)
+	}
+
+	v := &stringToBool{from: str, format: format}
+	str.AddListener(v)
+	return v
+}
+
+func (s *stringToBool) Get() (bool, error) {
+	str, err := s.from.Get()
+	if str == "" || err != nil {
+		return false, err
+	}
+
+	var val bool
+	if s.format != "" {
+		n, err := fmt.Sscanf(str, s.format+" ", &val) // " " denotes match to end of string
+		if err != nil {
+			return false, err
+		}
+		if n != 1 {
+			return false, errParseFailed
+		}
+	} else {
+		new, err := parseBool(str)
+		if err != nil {
+			return false, err
+		}
+		val = new
+	}
+
+	return val, nil
+}
+
+func (s *stringToBool) Set(val bool) error {
+	var str string
+	if s.format != "" {
+		str = fmt.Sprintf(s.format, val)
+	} else {
+		str = formatBool(val)
+	}
+
+	old, err := s.from.Get()
+	if str == old {
+		return err
+	}
+
+	if err = s.from.Set(str); err != nil {
+		return err
+	}
+
+	s.DataChanged()
+	return nil
+}
+
+func (s *stringToBool) DataChanged() {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+	s.trigger()
+}
+
+type stringToFloat struct {
+	base
+
+	format string
+
+	from String
+}
+
+// StringToFloat creates a binding that connects a String data item to a Float.
+// Changes to the String will be parsed and pushed to the Float if the parse was successful, and setting
+// the Float update the String binding.
+//
+// Since: 2.0
+func StringToFloat(str String) Float {
+	v := &stringToFloat{from: str}
+	str.AddListener(v)
+	return v
+}
+
+// StringToFloatWithFormat creates a binding that connects a String data item to a Float and is
+// presented using the specified format. Changes to the Float will be parsed and if the format matches and
+// the parse is successful it will be pushed to the String. Setting the Float will push a formatted value
+// into the String.
+//
+// Since: 2.0
+func StringToFloatWithFormat(str String, format string) Float {
+	if format == "%f" { // Same as not using custom format.
+		return StringToFloat(str)
+	}
+
+	v := &stringToFloat{from: str, format: format}
+	str.AddListener(v)
+	return v
+}
+
+func (s *stringToFloat) Get() (float64, error) {
+	str, err := s.from.Get()
+	if str == "" || err != nil {
+		return 0.0, err
+	}
+
+	var val float64
+	if s.format != "" {
+		n, err := fmt.Sscanf(str, s.format+" ", &val) // " " denotes match to end of string
+		if err != nil {
+			return 0.0, err
+		}
+		if n != 1 {
+			return 0.0, errParseFailed
+		}
+	} else {
+		new, err := parseFloat(str)
+		if err != nil {
+			return 0.0, err
+		}
+		val = new
+	}
+
+	return val, nil
+}
+
+func (s *stringToFloat) Set(val float64) error {
+	var str string
+	if s.format != "" {
+		str = fmt.Sprintf(s.format, val)
+	} else {
+		str = formatFloat(val)
+	}
+
+	old, err := s.from.Get()
+	if str == old {
+		return err
+	}
+
+	if err = s.from.Set(str); err != nil {
+		return err
+	}
+
+	s.DataChanged()
+	return nil
+}
+
+func (s *stringToFloat) DataChanged() {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+	s.trigger()
+}
+
+type stringToInt struct {
+	base
+
+	format string
+
+	from String
+}
+
+// StringToInt creates a binding that connects a String data item to a Int.
+// Changes to the String will be parsed and pushed to the Int if the parse was successful, and setting
+// the Int update the String binding.
+//
+// Since: 2.0
+func StringToInt(str String) Int {
+	v := &stringToInt{from: str}
+	str.AddListener(v)
+	return v
+}
+
+// StringToIntWithFormat creates a binding that connects a String data item to a Int and is
+// presented using the specified format. Changes to the Int will be parsed and if the format matches and
+// the parse is successful it will be pushed to the String. Setting the Int will push a formatted value
+// into the String.
+//
+// Since: 2.0
+func StringToIntWithFormat(str String, format string) Int {
+	if format == "%d" { // Same as not using custom format.
+		return StringToInt(str)
+	}
+
+	v := &stringToInt{from: str, format: format}
+	str.AddListener(v)
+	return v
+}
+
+func (s *stringToInt) Get() (int, error) {
+	str, err := s.from.Get()
+	if str == "" || err != nil {
+		return 0, err
+	}
+
+	var val int
+	if s.format != "" {
+		n, err := fmt.Sscanf(str, s.format+" ", &val) // " " denotes match to end of string
+		if err != nil {
+			return 0, err
+		}
+		if n != 1 {
+			return 0, errParseFailed
+		}
+	} else {
+		new, err := parseInt(str)
+		if err != nil {
+			return 0, err
+		}
+		val = new
+	}
+
+	return val, nil
+}
+
+func (s *stringToInt) Set(val int) error {
+	var str string
+	if s.format != "" {
+		str = fmt.Sprintf(s.format, val)
+	} else {
+		str = formatInt(val)
+	}
+
+	old, err := s.from.Get()
+	if str == old {
+		return err
+	}
+
+	if err = s.from.Set(str); err != nil {
+		return err
+	}
+
+	s.DataChanged()
+	return nil
+}
+
+func (s *stringToInt) DataChanged() {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+	s.trigger()
+}
+
+type stringToURI struct {
+	base
+
+	from String
+}
+
+// StringToURI creates a binding that connects a String data item to a URI.
+// Changes to the String will be parsed and pushed to the URI if the parse was successful, and setting
+// the URI update the String binding.
+//
+// Since: 2.1
+func StringToURI(str String) URI {
+	v := &stringToURI{from: str}
+	str.AddListener(v)
+	return v
+}
+
+func (s *stringToURI) Get() (fyne.URI, error) {
+	str, err := s.from.Get()
+	if str == "" || err != nil {
+		return fyne.URI(nil), err
+	}
+
+	return uriFromString(str)
+}
+
+func (s *stringToURI) Set(val fyne.URI) error {
+	str, err := uriToString(val)
+	if err != nil {
+		return err
+	}
+	old, err := s.from.Get()
+	if str == old {
+		return err
+	}
+
+	if err = s.from.Set(str); err != nil {
+		return err
+	}
+
+	s.DataChanged()
+	return nil
+}
+
+func (s *stringToURI) DataChanged() {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+	s.trigger()
+}

+ 103 - 0
vendor/fyne.io/fyne/v2/data/binding/convert_helper.go

@@ -0,0 +1,103 @@
+package binding
+
+import (
+	"strconv"
+	"strings"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/storage"
+)
+
+func stripFormatPrecision(in string) string {
+	// quick exit if certainly not float
+	if !strings.ContainsAny(in, "f") {
+		return in
+	}
+
+	start := -1
+	end := -1
+	runes := []rune(in)
+	for i, r := range runes {
+		switch r {
+		case '%':
+			if i > 0 && start == i-1 { // ignore %%
+				start = -1
+			} else {
+				start = i
+			}
+		case 'f':
+			if start == -1 { // not part of format
+				continue
+			}
+			end = i
+		}
+
+		if end > -1 {
+			break
+		}
+	}
+	if end == start+1 { // no width/precision
+		return in
+	}
+
+	sizeRunes := runes[start+1 : end]
+	width, err := parseFloat(string(sizeRunes))
+	if err != nil {
+		return string(runes[:start+1]) + string(runes[:end])
+	}
+
+	if sizeRunes[0] == '.' { // formats like %.2f
+		return string(runes[:start+1]) + string(runes[end:])
+	}
+	return string(runes[:start+1]) + strconv.Itoa(int(width)) + string(runes[end:])
+}
+
+func uriFromString(in string) (fyne.URI, error) {
+	return storage.ParseURI(in)
+}
+
+func uriToString(in fyne.URI) (string, error) {
+	if in == nil {
+		return "", nil
+	}
+
+	return in.String(), nil
+}
+
+func parseBool(in string) (bool, error) {
+	out, err := strconv.ParseBool(in)
+	if err != nil {
+		return false, err
+	}
+
+	return out, nil
+}
+
+func parseFloat(in string) (float64, error) {
+	out, err := strconv.ParseFloat(in, 64)
+	if err != nil {
+		return 0, err
+	}
+
+	return out, nil
+}
+
+func parseInt(in string) (int, error) {
+	out, err := strconv.ParseInt(in, 0, 64)
+	if err != nil {
+		return 0, err
+	}
+	return int(out), nil
+}
+
+func formatBool(in bool) string {
+	return strconv.FormatBool(in)
+}
+
+func formatFloat(in float64) string {
+	return strconv.FormatFloat(in, 'f', 6, 64)
+}
+
+func formatInt(in int) string {
+	return strconv.FormatInt(int64(in), 10)
+}

+ 43 - 0
vendor/fyne.io/fyne/v2/data/binding/listbinding.go

@@ -0,0 +1,43 @@
+package binding
+
+// DataList is the base interface for all bindable data lists.
+//
+// Since: 2.0
+type DataList interface {
+	DataItem
+	GetItem(index int) (DataItem, error)
+	Length() int
+}
+
+type listBase struct {
+	base
+	items []DataItem
+}
+
+// GetItem returns the DataItem at the specified index.
+func (b *listBase) GetItem(i int) (DataItem, error) {
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+
+	if i < 0 || i >= len(b.items) {
+		return nil, errOutOfBounds
+	}
+
+	return b.items[i], nil
+}
+
+// Length returns the number of items in this data list.
+func (b *listBase) Length() int {
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+
+	return len(b.items)
+}
+
+func (b *listBase) appendItem(i DataItem) {
+	b.items = append(b.items, i)
+}
+
+func (b *listBase) deleteItem(i int) {
+	b.items = append(b.items[:i], b.items[i+1:]...)
+}

+ 522 - 0
vendor/fyne.io/fyne/v2/data/binding/mapbinding.go

@@ -0,0 +1,522 @@
+package binding
+
+import (
+	"errors"
+	"reflect"
+
+	"fyne.io/fyne/v2"
+)
+
+// DataMap is the base interface for all bindable data maps.
+//
+// Since: 2.0
+type DataMap interface {
+	DataItem
+	GetItem(string) (DataItem, error)
+	Keys() []string
+}
+
+// ExternalUntypedMap is a map data binding with all values untyped (interface{}), connected to an external data source.
+//
+// Since: 2.0
+type ExternalUntypedMap interface {
+	UntypedMap
+	Reload() error
+}
+
+// UntypedMap is a map data binding with all values Untyped (interface{}).
+//
+// Since: 2.0
+type UntypedMap interface {
+	DataMap
+	Delete(string)
+	Get() (map[string]interface{}, error)
+	GetValue(string) (interface{}, error)
+	Set(map[string]interface{}) error
+	SetValue(string, interface{}) error
+}
+
+// NewUntypedMap creates a new, empty map binding of string to interface{}.
+//
+// Since: 2.0
+func NewUntypedMap() UntypedMap {
+	return &mapBase{items: make(map[string]reflectUntyped), val: &map[string]interface{}{}}
+}
+
+// BindUntypedMap creates a new map binding of string to interface{} based on the data passed.
+// If your code changes the content of the map this refers to you should call Reload() to inform the bindings.
+//
+// Since: 2.0
+func BindUntypedMap(d *map[string]interface{}) ExternalUntypedMap {
+	if d == nil {
+		return NewUntypedMap().(ExternalUntypedMap)
+	}
+	m := &mapBase{items: make(map[string]reflectUntyped), val: d, updateExternal: true}
+
+	for k := range *d {
+		m.setItem(k, bindUntypedMapValue(d, k, m.updateExternal))
+	}
+
+	return m
+}
+
+// Struct is the base interface for a bound struct type.
+//
+// Since: 2.0
+type Struct interface {
+	DataMap
+	GetValue(string) (interface{}, error)
+	SetValue(string, interface{}) error
+	Reload() error
+}
+
+// BindStruct creates a new map binding of string to interface{} using the struct passed as data.
+// The key for each item is a string representation of each exported field with the value set as an interface{}.
+// Only exported fields are included.
+//
+// Since: 2.0
+func BindStruct(i interface{}) Struct {
+	if i == nil {
+		return NewUntypedMap().(Struct)
+	}
+	t := reflect.TypeOf(i)
+	if t.Kind() != reflect.Ptr ||
+		(reflect.TypeOf(reflect.ValueOf(i).Elem()).Kind() != reflect.Struct) {
+		fyne.LogError("Invalid type passed to BindStruct, must be pointer to struct", nil)
+		return NewUntypedMap().(Struct)
+	}
+
+	s := &boundStruct{orig: i}
+	s.items = make(map[string]reflectUntyped)
+	s.val = &map[string]interface{}{}
+	s.updateExternal = true
+
+	v := reflect.ValueOf(i).Elem()
+	t = v.Type()
+	for j := 0; j < v.NumField(); j++ {
+		f := v.Field(j)
+		if !f.CanSet() {
+			continue
+		}
+
+		key := t.Field(j).Name
+		s.items[key] = bindReflect(f)
+		(*s.val)[key] = f.Interface()
+	}
+
+	return s
+}
+
+type reflectUntyped interface {
+	DataItem
+	get() (interface{}, error)
+	set(interface{}) error
+}
+
+type mapBase struct {
+	base
+
+	updateExternal bool
+	items          map[string]reflectUntyped
+	val            *map[string]interface{}
+}
+
+func (b *mapBase) GetItem(key string) (DataItem, error) {
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+
+	if v, ok := b.items[key]; ok {
+		return v, nil
+	}
+
+	return nil, errKeyNotFound
+}
+
+func (b *mapBase) Keys() []string {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	ret := make([]string, len(b.items))
+	i := 0
+	for k := range b.items {
+		ret[i] = k
+		i++
+	}
+
+	return ret
+}
+
+func (b *mapBase) Delete(key string) {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	delete(b.items, key)
+
+	b.trigger()
+}
+
+func (b *mapBase) Get() (map[string]interface{}, error) {
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+
+	if b.val == nil {
+		return map[string]interface{}{}, nil
+	}
+
+	return *b.val, nil
+}
+
+func (b *mapBase) GetValue(key string) (interface{}, error) {
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+
+	if i, ok := b.items[key]; ok {
+		return i.get()
+	}
+
+	return nil, errKeyNotFound
+}
+
+func (b *mapBase) Reload() error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	return b.doReload()
+}
+
+func (b *mapBase) Set(v map[string]interface{}) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	if b.val == nil { // was not initialized with a blank value, recover
+		b.val = &v
+		b.trigger()
+		return nil
+	}
+
+	*b.val = v
+	return b.doReload()
+}
+
+func (b *mapBase) SetValue(key string, d interface{}) error {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	if i, ok := b.items[key]; ok {
+		return i.set(d)
+	}
+
+	(*b.val)[key] = d
+	item := bindUntypedMapValue(b.val, key, b.updateExternal)
+	b.setItem(key, item)
+	return nil
+}
+
+func (b *mapBase) doReload() (retErr error) {
+	changed := false
+	// add new
+	for key := range *b.val {
+		_, found := b.items[key]
+		if !found {
+			b.setItem(key, bindUntypedMapValue(b.val, key, b.updateExternal))
+			changed = true
+		}
+	}
+
+	// remove old
+	for key := range b.items {
+		_, found := (*b.val)[key]
+		if !found {
+			delete(b.items, key)
+			changed = true
+		}
+	}
+	if changed {
+		b.trigger()
+	}
+
+	for k, item := range b.items {
+		var err error
+
+		if b.updateExternal {
+			err = item.(*boundExternalMapValue).setIfChanged((*b.val)[k])
+		} else {
+			err = item.(*boundMapValue).set((*b.val)[k])
+		}
+
+		if err != nil {
+			retErr = err
+		}
+	}
+	return
+}
+
+func (b *mapBase) setItem(key string, d reflectUntyped) {
+	b.items[key] = d
+
+	b.trigger()
+}
+
+type boundStruct struct {
+	mapBase
+
+	orig interface{}
+}
+
+func (b *boundStruct) Reload() (retErr error) {
+	b.lock.Lock()
+	defer b.lock.Unlock()
+
+	v := reflect.ValueOf(b.orig).Elem()
+	t := v.Type()
+	for j := 0; j < v.NumField(); j++ {
+		f := v.Field(j)
+		if !f.CanSet() {
+			continue
+		}
+		kind := f.Kind()
+		if kind == reflect.Slice || kind == reflect.Struct {
+			fyne.LogError("Data binding does not yet support slice or struct elements in a struct", nil)
+			continue
+		}
+
+		key := t.Field(j).Name
+		old := (*b.val)[key]
+		if f.Interface() == old {
+			continue
+		}
+
+		var err error
+		switch kind {
+		case reflect.Bool:
+			err = b.items[key].(*reflectBool).Set(f.Bool())
+		case reflect.Float32, reflect.Float64:
+			err = b.items[key].(*reflectFloat).Set(f.Float())
+		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+			err = b.items[key].(*reflectInt).Set(int(f.Int()))
+		case reflect.String:
+			err = b.items[key].(*reflectString).Set(f.String())
+		}
+		if err != nil {
+			retErr = err
+		}
+		(*b.val)[key] = f.Interface()
+	}
+	return
+}
+
+func bindUntypedMapValue(m *map[string]interface{}, k string, external bool) reflectUntyped {
+	if external {
+		ret := &boundExternalMapValue{old: (*m)[k]}
+		ret.val = m
+		ret.key = k
+		return ret
+	}
+
+	return &boundMapValue{val: m, key: k}
+}
+
+type boundMapValue struct {
+	base
+
+	val *map[string]interface{}
+	key string
+}
+
+func (b *boundMapValue) get() (interface{}, error) {
+	if v, ok := (*b.val)[b.key]; ok {
+		return v, nil
+	}
+
+	return nil, errKeyNotFound
+}
+
+func (b *boundMapValue) set(val interface{}) error {
+	(*b.val)[b.key] = val
+
+	b.trigger()
+	return nil
+}
+
+type boundExternalMapValue struct {
+	boundMapValue
+
+	old interface{}
+}
+
+func (b *boundExternalMapValue) setIfChanged(val interface{}) error {
+	if val == b.old {
+		return nil
+	}
+	b.old = val
+
+	return b.set(val)
+}
+
+type boundReflect struct {
+	base
+
+	val reflect.Value
+}
+
+func (b *boundReflect) get() (interface{}, error) {
+	return b.val.Interface(), nil
+}
+
+func (b *boundReflect) set(val interface{}) (err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = errors.New("unable to set bool in data binding")
+		}
+	}()
+	b.val.Set(reflect.ValueOf(val))
+
+	b.trigger()
+	return nil
+}
+
+type reflectBool struct {
+	boundReflect
+}
+
+func (r *reflectBool) Get() (val bool, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = errors.New("invalid bool value in data binding")
+		}
+	}()
+
+	val = r.val.Bool()
+	return
+}
+
+func (r *reflectBool) Set(b bool) (err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = errors.New("unable to set bool in data binding")
+		}
+	}()
+
+	r.val.SetBool(b)
+	r.trigger()
+	return
+}
+
+func bindReflectBool(f reflect.Value) reflectUntyped {
+	r := &reflectBool{}
+	r.val = f
+	return r
+}
+
+type reflectFloat struct {
+	boundReflect
+}
+
+func (r *reflectFloat) Get() (val float64, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = errors.New("invalid float64 value in data binding")
+		}
+	}()
+
+	val = r.val.Float()
+	return
+}
+
+func (r *reflectFloat) Set(f float64) (err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = errors.New("unable to set float64 in data binding")
+		}
+	}()
+
+	r.val.SetFloat(f)
+	r.trigger()
+	return
+}
+
+func bindReflectFloat(f reflect.Value) reflectUntyped {
+	r := &reflectFloat{}
+	r.val = f
+	return r
+}
+
+type reflectInt struct {
+	boundReflect
+}
+
+func (r *reflectInt) Get() (val int, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = errors.New("invalid int value in data binding")
+		}
+	}()
+
+	val = int(r.val.Int())
+	return
+}
+
+func (r *reflectInt) Set(i int) (err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = errors.New("unable to set int in data binding")
+		}
+	}()
+
+	r.val.SetInt(int64(i))
+	r.trigger()
+	return
+}
+
+func bindReflectInt(f reflect.Value) reflectUntyped {
+	r := &reflectInt{}
+	r.val = f
+	return r
+}
+
+type reflectString struct {
+	boundReflect
+}
+
+func (r *reflectString) Get() (val string, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = errors.New("invalid string value in data binding")
+		}
+	}()
+
+	val = r.val.String()
+	return
+}
+
+func (r *reflectString) Set(s string) (err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = errors.New("unable to set string in data binding")
+		}
+	}()
+
+	r.val.SetString(s)
+	r.trigger()
+	return
+}
+
+func bindReflectString(f reflect.Value) reflectUntyped {
+	r := &reflectString{}
+	r.val = f
+	return r
+}
+
+func bindReflect(field reflect.Value) reflectUntyped {
+	switch field.Kind() {
+	case reflect.Bool:
+		return bindReflectBool(field)
+	case reflect.Float32, reflect.Float64:
+		return bindReflectFloat(field)
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return bindReflectInt(field)
+	case reflect.String:
+		return bindReflectString(field)
+	}
+	return &boundReflect{val: field}
+}

+ 104 - 0
vendor/fyne.io/fyne/v2/data/binding/pref_helper.go

@@ -0,0 +1,104 @@
+package binding
+
+import (
+	"sync"
+
+	"fyne.io/fyne/v2"
+)
+
+type preferenceItem interface {
+	checkForChange()
+}
+
+type preferenceBindings struct {
+	items sync.Map // map[string]preferenceItem
+}
+
+func (b *preferenceBindings) getItem(key string) preferenceItem {
+	val, loaded := b.items.Load(key)
+	if !loaded {
+		return nil
+	}
+	return val.(preferenceItem)
+}
+
+func (b *preferenceBindings) list() []preferenceItem {
+	ret := []preferenceItem{}
+	b.items.Range(func(_, val interface{}) bool {
+		ret = append(ret, val.(preferenceItem))
+		return true
+	})
+	return ret
+}
+
+func (b *preferenceBindings) setItem(key string, item preferenceItem) {
+	b.items.Store(key, item)
+}
+
+type preferencesMap struct {
+	prefs sync.Map // map[fyne.Preferences]*preferenceBindings
+
+	appPrefs fyne.Preferences // the main application prefs, to check if it changed...
+}
+
+func newPreferencesMap() *preferencesMap {
+	return &preferencesMap{}
+}
+
+func (m *preferencesMap) ensurePreferencesAttached(p fyne.Preferences) *preferenceBindings {
+	binds, loaded := m.prefs.LoadOrStore(p, &preferenceBindings{})
+	if loaded {
+		return binds.(*preferenceBindings)
+	}
+
+	p.AddChangeListener(func() { m.preferencesChanged(fyne.CurrentApp().Preferences()) })
+	return binds.(*preferenceBindings)
+}
+
+func (m *preferencesMap) getBindings(p fyne.Preferences) *preferenceBindings {
+	if p == fyne.CurrentApp().Preferences() {
+		if m.appPrefs == nil {
+			m.appPrefs = p
+		} else if m.appPrefs != p {
+			m.migratePreferences(m.appPrefs, p)
+		}
+	}
+	binds, loaded := m.prefs.Load(p)
+	if !loaded {
+		return nil
+	}
+	return binds.(*preferenceBindings)
+}
+
+func (m *preferencesMap) preferencesChanged(p fyne.Preferences) {
+	binds := m.getBindings(p)
+	if binds == nil {
+		return
+	}
+	for _, item := range binds.list() {
+		item.checkForChange()
+	}
+}
+
+func (m *preferencesMap) migratePreferences(src, dst fyne.Preferences) {
+	old, loaded := m.prefs.Load(src)
+	if !loaded {
+		return
+	}
+
+	m.prefs.Store(dst, old)
+	m.prefs.Delete(src)
+	m.appPrefs = dst
+
+	binds := m.getBindings(dst)
+	if binds == nil {
+		return
+	}
+	for _, b := range binds.list() {
+		if backed, ok := b.(interface{ replaceProvider(fyne.Preferences) }); ok {
+			backed.replaceProvider(dst)
+		}
+	}
+
+	m.preferencesChanged(dst)
+}

+ 244 - 0
vendor/fyne.io/fyne/v2/data/binding/preference.go

@@ -0,0 +1,244 @@
+// auto-generated
+// **** THIS FILE IS AUTO-GENERATED, PLEASE DO NOT EDIT IT **** //
+
+package binding
+
+import (
+	"sync/atomic"
+
+	"fyne.io/fyne/v2"
+)
+
+const keyTypeMismatchError = "A previous preference binding exists with different type for key: "
+
+type prefBoundBool struct {
+	base
+	key   string
+	p     fyne.Preferences
+	cache atomic.Value // bool
+}
+
+// BindPreferenceBool returns a bindable bool value that is managed by the application preferences.
+// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
+//
+// Since: 2.0
+func BindPreferenceBool(key string, p fyne.Preferences) Bool {
+	binds := prefBinds.getBindings(p)
+	if binds != nil {
+		if listen := binds.getItem(key); listen != nil {
+			if l, ok := listen.(Bool); ok {
+				return l
+			}
+			fyne.LogError(keyTypeMismatchError+key, nil)
+		}
+	}
+
+	listen := &prefBoundBool{key: key, p: p}
+	binds = prefBinds.ensurePreferencesAttached(p)
+	binds.setItem(key, listen)
+	return listen
+}
+
+func (b *prefBoundBool) Get() (bool, error) {
+	cache := b.p.Bool(b.key)
+	b.cache.Store(cache)
+	return cache, nil
+}
+
+func (b *prefBoundBool) Set(v bool) error {
+	b.p.SetBool(b.key, v)
+
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+	b.trigger()
+	return nil
+}
+
+func (b *prefBoundBool) checkForChange() {
+	val := b.cache.Load()
+	if val != nil {
+		cache := val.(bool)
+		if b.p.Bool(b.key) == cache {
+			return
+		}
+	}
+	b.trigger()
+}
+
+func (b *prefBoundBool) replaceProvider(p fyne.Preferences) {
+	b.p = p
+}
+
+type prefBoundFloat struct {
+	base
+	key   string
+	p     fyne.Preferences
+	cache atomic.Value // float64
+}
+
+// BindPreferenceFloat returns a bindable float64 value that is managed by the application preferences.
+// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
+//
+// Since: 2.0
+func BindPreferenceFloat(key string, p fyne.Preferences) Float {
+	binds := prefBinds.getBindings(p)
+	if binds != nil {
+		if listen := binds.getItem(key); listen != nil {
+			if l, ok := listen.(Float); ok {
+				return l
+			}
+			fyne.LogError(keyTypeMismatchError+key, nil)
+		}
+	}
+
+	listen := &prefBoundFloat{key: key, p: p}
+	binds = prefBinds.ensurePreferencesAttached(p)
+	binds.setItem(key, listen)
+	return listen
+}
+
+func (b *prefBoundFloat) Get() (float64, error) {
+	cache := b.p.Float(b.key)
+	b.cache.Store(cache)
+	return cache, nil
+}
+
+func (b *prefBoundFloat) Set(v float64) error {
+	b.p.SetFloat(b.key, v)
+
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+	b.trigger()
+	return nil
+}
+
+func (b *prefBoundFloat) checkForChange() {
+	val := b.cache.Load()
+	if val != nil {
+		cache := val.(float64)
+		if b.p.Float(b.key) == cache {
+			return
+		}
+	}
+	b.trigger()
+}
+
+func (b *prefBoundFloat) replaceProvider(p fyne.Preferences) {
+	b.p = p
+}
+
+type prefBoundInt struct {
+	base
+	key   string
+	p     fyne.Preferences
+	cache atomic.Value // int
+}
+
+// BindPreferenceInt returns a bindable int value that is managed by the application preferences.
+// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
+//
+// Since: 2.0
+func BindPreferenceInt(key string, p fyne.Preferences) Int {
+	binds := prefBinds.getBindings(p)
+	if binds != nil {
+		if listen := binds.getItem(key); listen != nil {
+			if l, ok := listen.(Int); ok {
+				return l
+			}
+			fyne.LogError(keyTypeMismatchError+key, nil)
+		}
+	}
+
+	listen := &prefBoundInt{key: key, p: p}
+	binds = prefBinds.ensurePreferencesAttached(p)
+	binds.setItem(key, listen)
+	return listen
+}
+
+func (b *prefBoundInt) Get() (int, error) {
+	cache := b.p.Int(b.key)
+	b.cache.Store(cache)
+	return cache, nil
+}
+
+func (b *prefBoundInt) Set(v int) error {
+	b.p.SetInt(b.key, v)
+
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+	b.trigger()
+	return nil
+}
+
+func (b *prefBoundInt) checkForChange() {
+	val := b.cache.Load()
+	if val != nil {
+		cache := val.(int)
+		if b.p.Int(b.key) == cache {
+			return
+		}
+	}
+	b.trigger()
+}
+
+func (b *prefBoundInt) replaceProvider(p fyne.Preferences) {
+	b.p = p
+}
+
+type prefBoundString struct {
+	base
+	key   string
+	p     fyne.Preferences
+	cache atomic.Value // string
+}
+
+// BindPreferenceString returns a bindable string value that is managed by the application preferences.
+// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
+//
+// Since: 2.0
+func BindPreferenceString(key string, p fyne.Preferences) String {
+	binds := prefBinds.getBindings(p)
+	if binds != nil {
+		if listen := binds.getItem(key); listen != nil {
+			if l, ok := listen.(String); ok {
+				return l
+			}
+			fyne.LogError(keyTypeMismatchError+key, nil)
+		}
+	}
+
+	listen := &prefBoundString{key: key, p: p}
+	binds = prefBinds.ensurePreferencesAttached(p)
+	binds.setItem(key, listen)
+	return listen
+}
+
+func (b *prefBoundString) Get() (string, error) {
+	cache := b.p.String(b.key)
+	b.cache.Store(cache)
+	return cache, nil
+}
+
+func (b *prefBoundString) Set(v string) error {
+	b.p.SetString(b.key, v)
+
+	b.lock.RLock()
+	defer b.lock.RUnlock()
+	b.trigger()
+	return nil
+}
+
+func (b *prefBoundString) checkForChange() {
+	val := b.cache.Load()
+	if val != nil {
+		cache := val.(string)
+		if b.p.String(b.key) == cache {
+			return
+		}
+	}
+	b.trigger()
+}
+
+func (b *prefBoundString) replaceProvider(p fyne.Preferences) {
+	b.p = p
+}

+ 30 - 0
vendor/fyne.io/fyne/v2/data/binding/queue.go

@@ -0,0 +1,30 @@
+package binding
+
+import (
+	"sync"
+
+	"fyne.io/fyne/v2/internal/async"
+)
+
+var (
+	once  sync.Once
+	queue *async.UnboundedFuncChan
+)
+
+func queueItem(f func()) {
+	once.Do(func() {
+		queue = async.NewUnboundedFuncChan()
+		go func() {
+			for f := range queue.Out() {
+				f()
+			}
+		}()
+	})
+	queue.In() <- f
+}
+
+func waitForItems() {
+	done := make(chan struct{})
+	queue.In() <- func() { close(done) }
+	<-done
+}

+ 218 - 0
vendor/fyne.io/fyne/v2/data/binding/sprintf.go

@@ -0,0 +1,218 @@
+package binding
+
+import (
+	"fmt"
+
+	"fyne.io/fyne/v2/storage"
+)
+
+type sprintfString struct {
+	String
+
+	format string
+	source []DataItem
+	err    error
+}
+
+// NewSprintf returns a String binding that format its content using the
+// format string and the provide additional parameter that must be other
+// data bindings. This data binding use fmt.Sprintf and fmt.Scanf internally
+// and will have all the same limitation as those function.
+//
+// Since: 2.2
+func NewSprintf(format string, b ...DataItem) String {
+	ret := &sprintfString{
+		String: NewString(),
+		format: format,
+		source: append(make([]DataItem, 0, len(b)), b...),
+	}
+
+	for _, value := range b {
+		value.AddListener(ret)
+	}
+
+	return ret
+}
+
+func (s *sprintfString) DataChanged() {
+	data := make([]interface{}, 0, len(s.source))
+
+	s.err = nil
+	for _, value := range s.source {
+		switch x := value.(type) {
+		case Bool:
+			b, err := x.Get()
+			if err != nil {
+				s.err = err
+				return
+			}
+
+			data = append(data, b)
+		case Bytes:
+			b, err := x.Get()
+			if err != nil {
+				s.err = err
+				return
+			}
+
+			data = append(data, b)
+		case Float:
+			f, err := x.Get()
+			if err != nil {
+				s.err = err
+				return
+			}
+
+			data = append(data, f)
+		case Int:
+			i, err := x.Get()
+			if err != nil {
+				s.err = err
+				return
+			}
+
+			data = append(data, i)
+		case Rune:
+			r, err := x.Get()
+			if err != nil {
+				s.err = err
+				return
+			}
+
+			data = append(data, r)
+		case String:
+			str, err := x.Get()
+			if err != nil {
+				s.err = err
+				// Set error?
+				return
+			}
+
+			data = append(data, str)
+		case URI:
+			u, err := x.Get()
+			if err != nil {
+				s.err = err
+				return
+			}
+
+			data = append(data, u)
+		}
+	}
+
+	r := fmt.Sprintf(s.format, data...)
+	s.String.Set(r)
+}
+
+func (s *sprintfString) Get() (string, error) {
+	if s.err != nil {
+		return "", s.err
+	}
+	return s.String.Get()
+}
+
+func (s *sprintfString) Set(str string) error {
+	data := make([]interface{}, 0, len(s.source))
+
+	s.err = nil
+	for _, value := range s.source {
+		switch value.(type) {
+		case Bool:
+			data = append(data, new(bool))
+		case Bytes:
+			return fmt.Errorf("impossible to convert '%s' to []bytes type", str)
+		case Float:
+			data = append(data, new(float64))
+		case Int:
+			data = append(data, new(int))
+		case Rune:
+			data = append(data, new(rune))
+		case String:
+			data = append(data, new(string))
+		case URI:
+			data = append(data, new(string))
+		}
+	}
+
+	count, err := fmt.Sscanf(str, s.format, data...)
+	if err != nil {
+		return err
+	}
+
+	if count != len(data) {
+		return fmt.Errorf("impossible to decode more than %v parameters in '%s' with format '%s'", count, str, s.format)
+	}
+
+	for i, value := range s.source {
+		switch x := value.(type) {
+		case Bool:
+			v := data[i].(*bool)
+
+			err := x.Set(*v)
+			if err != nil {
+				return err
+			}
+		case Bytes:
+			return fmt.Errorf("impossible to convert '%s' to []bytes type", str)
+		case Float:
+			v := data[i].(*float64)
+
+			err := x.Set(*v)
+			if err != nil {
+				return err
+			}
+		case Int:
+			v := data[i].(*int)
+
+			err := x.Set(*v)
+			if err != nil {
+				return err
+			}
+		case Rune:
+			v := data[i].(*rune)
+
+			err := x.Set(*v)
+			if err != nil {
+				return err
+			}
+		case String:
+			v := data[i].(*string)
+
+			err := x.Set(*v)
+			if err != nil {
+				return err
+			}
+		case URI:
+			v := data[i].(*string)
+
+			if v == nil {
+				return fmt.Errorf("URI can not be nil in '%s'", str)
+			}
+
+			uri, err := storage.ParseURI(*v)
+			if err != nil {
+				return err
+			}
+
+			err = x.Set(uri)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+// StringToStringWithFormat creates a binding that converts a string to another string using the specified format.
+// Changes to the returned String will be pushed to the passed in String and setting a new string value will parse and
+// set the underlying String if it matches the format and the parse was successful.
+//
+// Since: 2.2
+func StringToStringWithFormat(str String, format string) String {
+	if format == "%s" { // Same as not using custom formatting.
+		return str
+	}
+
+	return NewSprintf(format, str)
+}

+ 92 - 0
vendor/fyne.io/fyne/v2/data/binding/treebinding.go

@@ -0,0 +1,92 @@
+package binding
+
+// DataTreeRootID const is the value used as ID for the root of any tree binding.
+const DataTreeRootID = ""
+
+// DataTree is the base interface for all bindable data trees.
+//
+// Since: 2.4
+type DataTree interface {
+	DataItem
+	GetItem(id string) (DataItem, error)
+	ChildIDs(string) []string
+}
+
+type treeBase struct {
+	base
+
+	ids   map[string][]string
+	items map[string]DataItem
+}
+
+// GetItem returns the DataItem at the specified id.
+func (t *treeBase) GetItem(id string) (DataItem, error) {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	if item, ok := t.items[id]; ok {
+		return item, nil
+	}
+
+	return nil, errOutOfBounds
+}
+
+// ChildIDs returns the ordered IDs of items in this data tree that are children of the specified ID.
+func (t *treeBase) ChildIDs(id string) []string {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	if ids, ok := t.ids[id]; ok {
+		return ids
+	}
+
+	return []string{}
+}
+
+func (t *treeBase) appendItem(i DataItem, id, parent string) {
+	t.items[id] = i
+	ids, ok := t.ids[parent]
+	if !ok {
+		ids = make([]string, 0)
+	}
+
+	for _, in := range ids {
+		if in == id {
+			return
+		}
+	}
+	t.ids[parent] = append(ids, id)
+}
+
+func (t *treeBase) deleteItem(id, parent string) {
+	delete(t.items, id)
+
+	ids, ok := t.ids[parent]
+	if !ok {
+		return
+	}
+
+	off := -1
+	for i, id2 := range ids {
+		if id2 == id {
+			off = i
+			break
+		}
+	}
+	if off == -1 {
+		return
+	}
+	t.ids[parent] = append(ids[:off], ids[off+1:]...)
+}
+
+func parentIDFor(id string, ids map[string][]string) string {
+	for parent, list := range ids {
+		for _, child := range list {
+			if child == id {
+				return parent
+			}
+		}
+	}
+
+	return ""
+}

+ 39 - 0
vendor/fyne.io/fyne/v2/device.go

@@ -0,0 +1,39 @@
+package fyne
+
+// DeviceOrientation represents the different ways that a mobile device can be held
+type DeviceOrientation int
+
+const (
+	// OrientationVertical is the default vertical orientation
+	OrientationVertical DeviceOrientation = iota
+	// OrientationVerticalUpsideDown is the portrait orientation held upside down
+	OrientationVerticalUpsideDown
+	// OrientationHorizontalLeft is used to indicate a landscape orientation with the top to the left
+	OrientationHorizontalLeft
+	// OrientationHorizontalRight is used to indicate a landscape orientation with the top to the right
+	OrientationHorizontalRight
+)
+
+// IsVertical is a helper utility that determines if a passed orientation is vertical
+func IsVertical(orient DeviceOrientation) bool {
+	return orient == OrientationVertical || orient == OrientationVerticalUpsideDown
+}
+
+// IsHorizontal is a helper utility that determines if a passed orientation is horizontal
+func IsHorizontal(orient DeviceOrientation) bool {
+	return !IsVertical(orient)
+}
+
+// Device provides information about the devices the code is running on
+type Device interface {
+	Orientation() DeviceOrientation
+	IsMobile() bool
+	IsBrowser() bool
+	HasKeyboard() bool
+	SystemScaleForWindow(Window) float32
+}
+
+// CurrentDevice returns the device information for the current hardware (via the driver)
+func CurrentDevice() Device {
+	return CurrentApp().Driver().Device()
+}

+ 32 - 0
vendor/fyne.io/fyne/v2/driver.go

@@ -0,0 +1,32 @@
+package fyne
+
+// Driver defines an abstract concept of a Fyne render driver.
+// Any implementation must provide at least these methods.
+type Driver interface {
+	// CreateWindow creates a new UI Window.
+	CreateWindow(string) Window
+	// AllWindows returns a slice containing all app windows.
+	AllWindows() []Window
+
+	// RenderedTextSize returns the size required to render the given string of specified
+	// font size and style. It also returns the height to text baseline, measured from the top.
+	RenderedTextSize(text string, fontSize float32, style TextStyle) (size Size, baseline float32)
+
+	// CanvasForObject returns the canvas that is associated with a given CanvasObject.
+	CanvasForObject(CanvasObject) Canvas
+	// AbsolutePositionForObject returns the position of a given CanvasObject relative to the top/left of a canvas.
+	AbsolutePositionForObject(CanvasObject) Position
+
+	// Device returns the device that the application is currently running on.
+	Device() Device
+	// Run starts the main event loop of the driver.
+	Run()
+	// Quit closes the driver and open windows, then exit the application.
+	// On some some operating systems this does nothing, for example iOS and Android.
+	Quit()
+
+	// StartAnimation registers a new animation with this driver and requests it be started.
+	StartAnimation(*Animation)
+	// StopAnimation stops an animation and unregisters from this driver.
+	StopAnimation(*Animation)
+}

+ 11 - 0
vendor/fyne.io/fyne/v2/driver/desktop/app.go

@@ -0,0 +1,11 @@
+package desktop
+
+import "fyne.io/fyne/v2"
+
+// App defines the desktop specific extensions to a fyne.App.
+//
+// Since: 2.2
+type App interface {
+	SetSystemTrayMenu(menu *fyne.Menu)
+	SetSystemTrayIcon(icon fyne.Resource)
+}

+ 11 - 0
vendor/fyne.io/fyne/v2/driver/desktop/canvas.go

@@ -0,0 +1,11 @@
+package desktop
+
+import "fyne.io/fyne/v2"
+
+// Canvas defines the desktop specific extensions to a fyne.Canvas.
+type Canvas interface {
+	OnKeyDown() func(*fyne.KeyEvent)
+	SetOnKeyDown(func(*fyne.KeyEvent))
+	OnKeyUp() func(*fyne.KeyEvent)
+	SetOnKeyUp(func(*fyne.KeyEvent))
+}

+ 47 - 0
vendor/fyne.io/fyne/v2/driver/desktop/cursor.go

@@ -0,0 +1,47 @@
+package desktop
+
+import "image"
+
+// Cursor interface is used for objects that desire a specific cursor.
+//
+// Since: 2.0
+type Cursor interface {
+	// Image returns the image for the given cursor, or nil if none should be shown.
+	// It also returns the x and y pixels that should act as the hot-spot (measured from top left corner).
+	Image() (image.Image, int, int)
+}
+
+// StandardCursor represents a standard Fyne cursor.
+// These values were previously of type `fyne.Cursor`.
+//
+// Since: 2.0
+type StandardCursor int
+
+// Image is not used for any of the StandardCursor types.
+//
+// Since: 2.0
+func (d StandardCursor) Image() (image.Image, int, int) {
+	return nil, 0, 0
+}
+
+const (
+	// DefaultCursor is the default cursor typically an arrow
+	DefaultCursor StandardCursor = iota
+	// TextCursor is the cursor often used to indicate text selection
+	TextCursor
+	// CrosshairCursor is the cursor often used to indicate bitmaps
+	CrosshairCursor
+	// PointerCursor is the cursor often used to indicate a link
+	PointerCursor
+	// HResizeCursor is the cursor often used to indicate horizontal resize
+	HResizeCursor
+	// VResizeCursor is the cursor often used to indicate vertical resize
+	VResizeCursor
+	// HiddenCursor will cause the cursor to not be shown
+	HiddenCursor
+)
+
+// Cursorable describes any CanvasObject that needs a cursor change
+type Cursorable interface {
+	Cursor() Cursor
+}

+ 15 - 0
vendor/fyne.io/fyne/v2/driver/desktop/driver.go

@@ -0,0 +1,15 @@
+// Package desktop provides desktop specific driver functionality.
+package desktop
+
+import "fyne.io/fyne/v2"
+
+// Driver represents the extended capabilities of a desktop driver
+type Driver interface {
+	// Create a new borderless window that is centered on screen
+	CreateSplashWindow() fyne.Window
+
+	// Gets the set of key modifiers that are currently active
+	//
+	// Since: 2.4
+	CurrentKeyModifiers() fyne.KeyModifier
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно