Prechádzať zdrojové kódy

d02 Обновление вендоринга

SVI 3 rokov pred
rodič
commit
9c9f36cc14
100 zmenil súbory, kde vykonal 12642 pridanie a 0 odobranie
  1. 54 0
      vendor/fyne.io/fyne/v2/.gitignore
  2. 1 0
      vendor/fyne.io/fyne/v2/.godocdown.import
  3. 14 0
      vendor/fyne.io/fyne/v2/AUTHORS
  4. 1107 0
      vendor/fyne.io/fyne/v2/CHANGELOG.md
  5. 76 0
      vendor/fyne.io/fyne/v2/CODE_OF_CONDUCT.md
  6. 63 0
      vendor/fyne.io/fyne/v2/CONTRIBUTING.md
  7. 28 0
      vendor/fyne.io/fyne/v2/LICENSE
  8. 189 0
      vendor/fyne.io/fyne/v2/README.md
  9. 15 0
      vendor/fyne.io/fyne/v2/SECURITY.md
  10. 84 0
      vendor/fyne.io/fyne/v2/animation.go
  11. 144 0
      vendor/fyne.io/fyne/v2/app.go
  12. 170 0
      vendor/fyne.io/fyne/v2/app/app.go
  13. 60 0
      vendor/fyne.io/fyne/v2/app/app_darwin.go
  14. 61 0
      vendor/fyne.io/fyne/v2/app/app_darwin.m
  15. 8 0
      vendor/fyne.io/fyne/v2/app/app_debug.go
  16. 67 0
      vendor/fyne.io/fyne/v2/app/app_desktop_darwin.go
  17. 18 0
      vendor/fyne.io/fyne/v2/app/app_desktop_darwin.m
  18. 15 0
      vendor/fyne.io/fyne/v2/app/app_gl.go
  19. 19 0
      vendor/fyne.io/fyne/v2/app/app_goxjs.go
  20. 25 0
      vendor/fyne.io/fyne/v2/app/app_mobile.go
  21. 131 0
      vendor/fyne.io/fyne/v2/app/app_mobile_and.c
  22. 61 0
      vendor/fyne.io/fyne/v2/app/app_mobile_and.go
  23. 40 0
      vendor/fyne.io/fyne/v2/app/app_mobile_ios.go
  24. 16 0
      vendor/fyne.io/fyne/v2/app/app_mobile_ios.m
  25. 9 0
      vendor/fyne.io/fyne/v2/app/app_notlegacy_darwin.go
  26. 20 0
      vendor/fyne.io/fyne/v2/app/app_openurl_js.go
  27. 19 0
      vendor/fyne.io/fyne/v2/app/app_openurl_wasm.go
  28. 13 0
      vendor/fyne.io/fyne/v2/app/app_openurl_web.go
  29. 34 0
      vendor/fyne.io/fyne/v2/app/app_other.go
  30. 8 0
      vendor/fyne.io/fyne/v2/app/app_release.go
  31. 16 0
      vendor/fyne.io/fyne/v2/app/app_software.go
  32. 8 0
      vendor/fyne.io/fyne/v2/app/app_standard.go
  33. 29 0
      vendor/fyne.io/fyne/v2/app/app_theme_js.go
  34. 31 0
      vendor/fyne.io/fyne/v2/app/app_theme_wasm.go
  35. 13 0
      vendor/fyne.io/fyne/v2/app/app_theme_web.go
  36. 126 0
      vendor/fyne.io/fyne/v2/app/app_windows.go
  37. 195 0
      vendor/fyne.io/fyne/v2/app/app_xdg.go
  38. 47 0
      vendor/fyne.io/fyne/v2/app/cloud.go
  39. 28 0
      vendor/fyne.io/fyne/v2/app/meta.go
  40. 153 0
      vendor/fyne.io/fyne/v2/app/preferences.go
  41. 21 0
      vendor/fyne.io/fyne/v2/app/preferences_android.go
  42. 24 0
      vendor/fyne.io/fyne/v2/app/preferences_ios.go
  43. 20 0
      vendor/fyne.io/fyne/v2/app/preferences_mobile.go
  44. 29 0
      vendor/fyne.io/fyne/v2/app/preferences_other.go
  45. 161 0
      vendor/fyne.io/fyne/v2/app/settings.go
  46. 75 0
      vendor/fyne.io/fyne/v2/app/settings_desktop.go
  47. 35 0
      vendor/fyne.io/fyne/v2/app/settings_file.go
  48. 24 0
      vendor/fyne.io/fyne/v2/app/settings_goxjs.go
  49. 12 0
      vendor/fyne.io/fyne/v2/app/settings_mobile.go
  50. 27 0
      vendor/fyne.io/fyne/v2/app/storage.go
  51. 58 0
      vendor/fyne.io/fyne/v2/canvas.go
  52. 86 0
      vendor/fyne.io/fyne/v2/canvas/animation.go
  53. 100 0
      vendor/fyne.io/fyne/v2/canvas/base.go
  54. 29 0
      vendor/fyne.io/fyne/v2/canvas/canvas.go
  55. 88 0
      vendor/fyne.io/fyne/v2/canvas/circle.go
  56. 212 0
      vendor/fyne.io/fyne/v2/canvas/gradient.go
  57. 180 0
      vendor/fyne.io/fyne/v2/canvas/image.go
  58. 102 0
      vendor/fyne.io/fyne/v2/canvas/line.go
  59. 196 0
      vendor/fyne.io/fyne/v2/canvas/raster.go
  60. 60 0
      vendor/fyne.io/fyne/v2/canvas/rectangle.go
  61. 76 0
      vendor/fyne.io/fyne/v2/canvas/text.go
  62. 107 0
      vendor/fyne.io/fyne/v2/canvasobject.go
  63. 9 0
      vendor/fyne.io/fyne/v2/clipboard.go
  64. 39 0
      vendor/fyne.io/fyne/v2/cloud.go
  65. 211 0
      vendor/fyne.io/fyne/v2/container.go
  66. 459 0
      vendor/fyne.io/fyne/v2/container/apptabs.go
  67. 20 0
      vendor/fyne.io/fyne/v2/container/container.go
  68. 485 0
      vendor/fyne.io/fyne/v2/container/doctabs.go
  69. 109 0
      vendor/fyne.io/fyne/v2/container/layouts.go
  70. 55 0
      vendor/fyne.io/fyne/v2/container/scroll.go
  71. 356 0
      vendor/fyne.io/fyne/v2/container/split.go
  72. 814 0
      vendor/fyne.io/fyne/v2/container/tabs.go
  73. 178 0
      vendor/fyne.io/fyne/v2/data/binding/binding.go
  74. 647 0
      vendor/fyne.io/fyne/v2/data/binding/binditems.go
  75. 1786 0
      vendor/fyne.io/fyne/v2/data/binding/bindlists.go
  76. 13 0
      vendor/fyne.io/fyne/v2/data/binding/comparator_helper.go
  77. 638 0
      vendor/fyne.io/fyne/v2/data/binding/convert.go
  78. 103 0
      vendor/fyne.io/fyne/v2/data/binding/convert_helper.go
  79. 37 0
      vendor/fyne.io/fyne/v2/data/binding/listbinding.go
  80. 522 0
      vendor/fyne.io/fyne/v2/data/binding/mapbinding.go
  81. 104 0
      vendor/fyne.io/fyne/v2/data/binding/pref_helper.go
  82. 244 0
      vendor/fyne.io/fyne/v2/data/binding/preference.go
  83. 30 0
      vendor/fyne.io/fyne/v2/data/binding/queue.go
  84. 218 0
      vendor/fyne.io/fyne/v2/data/binding/sprintf.go
  85. 39 0
      vendor/fyne.io/fyne/v2/device.go
  86. 32 0
      vendor/fyne.io/fyne/v2/driver.go
  87. 11 0
      vendor/fyne.io/fyne/v2/driver/desktop/app.go
  88. 11 0
      vendor/fyne.io/fyne/v2/driver/desktop/canvas.go
  89. 47 0
      vendor/fyne.io/fyne/v2/driver/desktop/cursor.go
  90. 10 0
      vendor/fyne.io/fyne/v2/driver/desktop/driver.go
  91. 66 0
      vendor/fyne.io/fyne/v2/driver/desktop/key.go
  92. 58 0
      vendor/fyne.io/fyne/v2/driver/desktop/mouse.go
  93. 61 0
      vendor/fyne.io/fyne/v2/driver/desktop/shortcut.go
  94. 12 0
      vendor/fyne.io/fyne/v2/driver/mobile/device.go
  95. 26 0
      vendor/fyne.io/fyne/v2/driver/mobile/keyboard.go
  96. 15 0
      vendor/fyne.io/fyne/v2/driver/mobile/touch.go
  97. 37 0
      vendor/fyne.io/fyne/v2/event.go
  98. 28 0
      vendor/fyne.io/fyne/v2/fyne.go
  99. 142 0
      vendor/fyne.io/fyne/v2/geometry.go
  100. 33 0
      vendor/fyne.io/fyne/v2/internal/animation/animation.go

+ 54 - 0
vendor/fyne.io/fyne/v2/.gitignore

@@ -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>
+

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

@@ -0,0 +1,1107 @@
+# 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.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.
+

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

@@ -0,0 +1,189 @@
+<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='http://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.3 is the current release of the Fyne API, it added a refined theme design,
+cloud storage, improved text handling for international languages and many
+smaller feature additions.
+We are now working towards the next big release, codenamed
+[Dalwhinnie](https://github.com/fyne-io/fyne/milestone/18)
+and more news will follow in our news feeds and GitHub project.
+
+# Prerequisites
+
+To develop apps using Fyne you will need Go version 1.14 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
+
+# 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
+
+(For Go versions earlier than v1.16 use `go get fyne.io/fyne/v2/cmd/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" height="617" 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" height="617" 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" height="617" 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" height="212" alt="Fyne Hello Dark Theme" />
+    </td><td style="border: none;">
+      <img src="img/hello-dark.png" width="207" height="212" alt="Fyne Hello Dark Theme" />
+    </td></tr>
+  </table>
+</div>
+
+> Note that Windows applications load from a command prompt by default, which means if you click an icon you may see a command window.
+> To fix this add the parameters `-ldflags -H=windowsgui` to your run or build commands.
+
+## 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
+
+(for Go versions before v1.16 use `go get fyne.io/fyne/v2/cmd/fyne`)
+
+# 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/fyne-io/fynedesk) as well :)

+ 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.3.x   | :white_check_mark: |
+| < 2.3.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())
+}

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

@@ -0,0 +1,170 @@
+// 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"
+
+	"golang.org/x/sys/execabs"
+)
+
+// 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
+	exec    func(name string, arg ...string) *execabs.Cmd
+}
+
+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()
+		return
+	}
+}
+
+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() fyne.Preferences {
+	p := fyne.Preferences(newPreferences(a))
+	if pref, ok := p.(interface{ load() }); ok && a.uniqueID != "" {
+		pref.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, p fyne.Preferences, s *store) *internal.Docs {
+	if id != "" {
+		if pref, ok := p.(interface{ load() }); ok {
+			pref.load()
+		}
+		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, exec: execabs.Command, lifecycle: &app.Lifecycle{}}
+	fyne.SetCurrentApp(newApp)
+
+	newApp.prefs = newApp.newDefaultPreferences()
+	newApp.settings = loadSettings()
+	store := &store{a: newApp}
+	store.Docs = makeStoreDocs(id, newApp.prefs, store)
+	newApp.storage = store
+
+	if !d.Device().IsMobile() {
+		newApp.settings.watchSettings()
+	}
+
+	repository.Register("http", intRepo.NewHTTPRepository())
+	repository.Register("https", intRepo.NewHTTPRepository())
+
+	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

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

@@ -0,0 +1,67 @@
+//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"
+
+	"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 := a.exec("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, ctx);
+	jmethodID getSystemService = find_method(env, ctxClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
+
+	return (jobject)(*env)->CallObjectMethod(env, 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, 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(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
+}

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

@@ -0,0 +1,126 @@
+//go:build !ci && !js && !android && !ios && !wasm && !test_web_driver
+// +build !ci,!js,!android,!ios,!wasm,!test_web_driver
+
+package app
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/url"
+	"os"
+	"path/filepath"
+	"strings"
+	"syscall"
+
+	"golang.org/x/sys/windows/registry"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/theme"
+
+	"golang.org/x/sys/execabs"
+)
+
+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 := a.exec("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 := ioutil.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
+}

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

@@ -0,0 +1,195 @@
+//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"
+
+	"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 := a.exec("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
+	}
+
+	switch value {
+	case 0:
+		return theme.VariantLight
+	default:
+		return theme.VariantDark
+	}
+
+}
+
+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, a.prefs, 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
+}

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

@@ -0,0 +1,153 @@
+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
+}
+
+// Declare conformity with Preferences interface
+var _ fyne.Preferences = (*preferences)(nil)
+
+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)
+	})
+
+	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.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
+}

+ 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()
+	})
+}

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

@@ -0,0 +1,161 @@
+package app
+
+import (
+	"bytes"
+	"os"
+	"path/filepath"
+	"sync"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/theme"
+)
+
+// 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"`
+}
+
+// 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) 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&fsnotify.Remove != 0 { // 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
+}

+ 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()
+		}
+	}
+}

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

@@ -0,0 +1,88 @@
+package canvas
+
+import (
+	"image/color"
+
+	"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(c.Position2.X-c.Position1.X, c.Position2.Y-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}
+}

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

@@ -0,0 +1,180 @@
+package canvas
+
+import (
+	"image"
+	"io"
+	"io/ioutil"
+	"path/filepath"
+
+	"fyne.io/fyne/v2"
+	"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
+
+	// 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
+}
+
+// Hide will set this image to not be visible
+func (i *Image) Hide() {
+	i.baseObject.Hide()
+
+	repaint(i)
+}
+
+// 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() {
+	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
+	}
+	if i.FillMode == ImageFillOriginal && i.size.Height > 2 { // don't refresh original scale images after first draw
+		return
+	}
+
+	i.baseObject.Resize(s)
+
+	Refresh(i)
+}
+
+// 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 &Image{
+			File: uri.String()[7:],
+		}
+	}
+
+	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 := ioutil.ReadAll(read)
+	if err != nil {
+		fyne.LogError("Unable to read image data", err)
+		return nil
+	}
+	res := &fyne.StaticResource{
+		StaticName:    name,
+		StaticContent: data,
+	}
+
+	return &Image{
+		Resource: 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,
+	}
+}

+ 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
+		},
+	}
+}

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

@@ -0,0 +1,60 @@
+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
+}
+
+// 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()
+		}
+	}
+}

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

@@ -0,0 +1,459 @@
+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)
+	t.Refresh()
+}
+
+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
+	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.NewMaxLayout()
+	} 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.NewContainerWithLayout(layout, 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.NewContainerWithoutLayout(objects...)
+}

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

@@ -0,0 +1,485 @@
+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)
+	t.Refresh()
+}
+
+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)
+	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)
+	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.indicator.Refresh()
+		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()
+}

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

@@ -0,0 +1,109 @@
+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
+func NewMax(objects ...fyne.CanvasObject) *fyne.Container {
+	return New(layout.NewMaxLayout(), 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...)
+}
+
+// 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)
+}

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

@@ -0,0 +1,356 @@
+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
+}
+
+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() {
+}
+
+func (d *divider) Dragged(event *fyne.DragEvent) {
+	offset := d.split.Offset
+	if d.split.Horizontal {
+		if leadingRatio := float64(d.split.Leading.Size().Width) / float64(d.split.Size().Width); offset < leadingRatio {
+			offset = leadingRatio
+		}
+		if trailingRatio := 1. - (float64(d.split.Trailing.Size().Width) / float64(d.split.Size().Width)); offset > trailingRatio {
+			offset = trailingRatio
+		}
+		offset += float64(event.Dragged.DX) / float64(d.split.Size().Width)
+	} else {
+		if leadingRatio := float64(d.split.Leading.Size().Height) / float64(d.split.Size().Height); offset < leadingRatio {
+			offset = leadingRatio
+		}
+		if trailingRatio := 1. - (float64(d.split.Trailing.Size().Height) / float64(d.split.Size().Height)); offset > trailingRatio {
+			offset = trailingRatio
+		}
+		offset += float64(event.Dragged.DY) / float64(d.split.Size().Height)
+	}
+	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
+}

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

@@ -0,0 +1,814 @@
+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 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()
+}
+
+func (r *baseTabsRenderer) layout(t baseTabs, size fyne.Size) {
+	var (
+		barPos, dividerPos, contentPos    fyne.Position
+		barSize, dividerSize, contentSize fyne.Size
+	)
+
+	barMin := r.bar.MinSize()
+
+	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, theme.Padding())
+		contentPos = fyne.NewPos(0, barHeight+theme.Padding())
+		contentSize = fyne.NewSize(size.Width, size.Height-barHeight-theme.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(theme.Padding(), size.Height)
+		contentPos = fyne.NewPos(barWidth+theme.Padding(), 0)
+		contentSize = fyne.NewSize(size.Width-barWidth-theme.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-theme.Padding())
+		dividerSize = fyne.NewSize(size.Width, theme.Padding())
+		contentPos = fyne.NewPos(0, 0)
+		contentSize = fyne.NewSize(size.Width, size.Height-barHeight-theme.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-theme.Padding(), 0)
+		dividerSize = fyne.NewSize(theme.Padding(), size.Height)
+		contentPos = fyne.NewPos(0, 0)
+		contentSize = fyne.NewSize(size.Width-barWidth-theme.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 {
+	barMin := r.bar.MinSize()
+
+	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+theme.Padding(), contentMin.Height)
+	default:
+		return fyne.NewSize(contentMin.Width, barMin.Height+contentMin.Height+theme.Padding())
+	}
+}
+
+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 {
+		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.ButtonImportance
+	onTapped      func()
+	onClosed      func()
+	text          string
+	textAlignment fyne.TextAlign
+}
+
+func (b *tabButton) CreateRenderer() fyne.WidgetRenderer {
+	b.ExtendBaseWidget(b)
+	background := canvas.NewRectangle(theme.HoverColor())
+	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() {
+		var iconOffset fyne.Position
+		if r.button.iconPosition == buttonIconTop {
+			iconOffset = fyne.NewPos((innerSize.Width-r.iconSize())/2, 0)
+		} else {
+			iconOffset = fyne.NewPos(0, (innerSize.Height-r.iconSize())/2)
+		}
+		r.icon.Resize(fyne.NewSize(r.iconSize(), r.iconSize()))
+		r.icon.Move(innerOffset.Add(iconOffset))
+		labelShift = r.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))
+	}
+	r.close.Move(fyne.NewPos(size.Width-theme.IconInlineSize()-theme.Padding(), (size.Height-theme.IconInlineSize())/2))
+	r.close.Resize(fyne.NewSize(theme.IconInlineSize(), theme.IconInlineSize()))
+}
+
+func (r *tabButtonRenderer) MinSize() fyne.Size {
+	var contentWidth, contentHeight float32
+	textSize := r.label.MinSize()
+	if r.button.iconPosition == buttonIconTop {
+		contentWidth = fyne.Max(textSize.Width, r.iconSize())
+		if r.icon.Visible() {
+			contentHeight += r.iconSize()
+		}
+		if r.label.Text != "" {
+			if r.icon.Visible() {
+				contentHeight += theme.Padding()
+			}
+			contentHeight += textSize.Height
+		}
+	} else {
+		contentHeight = fyne.Max(textSize.Height, r.iconSize())
+		if r.icon.Visible() {
+			contentWidth += r.iconSize()
+		}
+		if r.label.Text != "" {
+			if r.icon.Visible() {
+				contentWidth += theme.Padding()
+			}
+			contentWidth += textSize.Width
+		}
+	}
+	if r.button.onClosed != nil {
+		contentWidth += theme.IconInlineSize() + theme.Padding()
+		contentHeight = fyne.Max(contentHeight, theme.IconInlineSize())
+	}
+	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.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.DisabledTextColor()
+	}
+	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 {
+	switch r.button.iconPosition {
+	case buttonIconTop:
+		return 2 * theme.IconInlineSize()
+	default:
+		return theme.IconInlineSize()
+	}
+}
+
+func (r *tabButtonRenderer) padding() fyne.Size {
+	if r.label.Text != "" && r.button.iconPosition == buttonIconInline {
+		return fyne.NewSize(theme.InnerPadding()*2, theme.InnerPadding()*2)
+	}
+	return fyne.NewSize(theme.InnerPadding(), theme.InnerPadding()*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.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.NewSize(theme.IconInlineSize(), 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.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
+}

+ 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)
+}

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

@@ -0,0 +1,37 @@
+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) {
+	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 {
+	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)
+}

+ 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
+}

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

@@ -0,0 +1,10 @@
+// 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
+}

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

@@ -0,0 +1,66 @@
+package desktop
+
+import (
+	"fyne.io/fyne/v2"
+)
+
+const (
+	// KeyNone represents no key
+	KeyNone fyne.KeyName = ""
+	// KeyShiftLeft represents the left shift key
+	KeyShiftLeft fyne.KeyName = "LeftShift"
+	// KeyShiftRight represents the right shift key
+	KeyShiftRight fyne.KeyName = "RightShift"
+	// KeyControlLeft represents the left control key
+	KeyControlLeft fyne.KeyName = "LeftControl"
+	// KeyControlRight represents the right control key
+	KeyControlRight fyne.KeyName = "RightControl"
+	// KeyAltLeft represents the left alt key
+	KeyAltLeft fyne.KeyName = "LeftAlt"
+	// KeyAltRight represents the right alt key
+	KeyAltRight fyne.KeyName = "RightAlt"
+	// KeySuperLeft represents the left "Windows" key (or "Command" key on macOS)
+	KeySuperLeft fyne.KeyName = "LeftSuper"
+	// KeySuperRight represents the right "Windows" key (or "Command" key on macOS)
+	KeySuperRight fyne.KeyName = "RightSuper"
+	// KeyMenu represents the left or right menu / application key
+	KeyMenu fyne.KeyName = "Menu"
+	// KeyPrintScreen represents the key used to cause a screen capture
+	KeyPrintScreen fyne.KeyName = "PrintScreen"
+
+	// KeyCapsLock represents the caps lock key, tapping once is the down event then again is the up
+	KeyCapsLock fyne.KeyName = "CapsLock"
+)
+
+// Modifier captures any key modifiers (shift etc.) pressed during a key event
+//
+// Deprecated: Use fyne.KeyModifier instead.
+type Modifier = fyne.KeyModifier
+
+const (
+	// ShiftModifier represents a shift key being held
+	//
+	// Deprecated: Use fyne.KeyModifierShift instead.
+	ShiftModifier = fyne.KeyModifierShift
+	// ControlModifier represents the ctrl key being held
+	//
+	// Deprecated: Use fyne.KeyModifierControl instead.
+	ControlModifier = fyne.KeyModifierControl
+	// AltModifier represents either alt keys being held
+	//
+	// Deprecated: Use fyne.KeyModifierAlt instead.
+	AltModifier = fyne.KeyModifierAlt
+	// SuperModifier represents either super keys being held
+	//
+	// Deprecated: Use fyne.KeyModifierSuper instead.
+	SuperModifier = fyne.KeyModifierSuper
+)
+
+// Keyable describes any focusable canvas object that can accept desktop key events.
+// This is the traditional key down and up event that is not applicable to all devices.
+type Keyable interface {
+	fyne.Focusable
+
+	KeyDown(*fyne.KeyEvent)
+	KeyUp(*fyne.KeyEvent)
+}

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

@@ -0,0 +1,58 @@
+package desktop
+
+import "fyne.io/fyne/v2"
+
+// MouseButton represents a single button in a desktop MouseEvent
+type MouseButton int
+
+const (
+	// MouseButtonPrimary is the most common mouse button - on some systems the only one.
+	// This will normally be on the left side of a mouse.
+	//
+	// Since: 2.0
+	MouseButtonPrimary MouseButton = 1 << iota
+
+	// MouseButtonSecondary is the secondary button on most mouse input devices.
+	// This will normally be on the right side of a mouse.
+	//
+	// Since: 2.0
+	MouseButtonSecondary
+
+	// MouseButtonTertiary is the middle button on the mouse, assuming it has one.
+	//
+	// Since: 2.0
+	MouseButtonTertiary
+
+	// LeftMouseButton is the most common mouse button - on some systems the only one.
+	//
+	// Deprecated: use MouseButtonPrimary which will adapt to mouse configuration.
+	LeftMouseButton = MouseButtonPrimary
+
+	// RightMouseButton is the secondary button on most mouse input devices.
+	//
+	// Deprecated: use MouseButtonSecondary which will adapt to mouse configuration.
+	RightMouseButton = MouseButtonSecondary
+)
+
+// MouseEvent contains data relating to desktop mouse events
+type MouseEvent struct {
+	fyne.PointEvent
+	Button   MouseButton
+	Modifier fyne.KeyModifier
+}
+
+// Mouseable represents desktop mouse events that can be sent to CanvasObjects
+type Mouseable interface {
+	MouseDown(*MouseEvent)
+	MouseUp(*MouseEvent)
+}
+
+// Hoverable is used when a canvas object wishes to know if a pointer device moves over it.
+type Hoverable interface {
+	// MouseIn is a hook that is called if the mouse pointer enters the element.
+	MouseIn(*MouseEvent)
+	// MouseMoved is a hook that is called if the mouse pointer moved over the element.
+	MouseMoved(*MouseEvent)
+	// MouseOut is a hook that is called if the mouse pointer leaves the element.
+	MouseOut()
+}

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

@@ -0,0 +1,61 @@
+package desktop
+
+import (
+	"runtime"
+	"strings"
+
+	"fyne.io/fyne/v2"
+)
+
+// Declare conformity with Shortcut interface
+var _ fyne.Shortcut = (*CustomShortcut)(nil)
+var _ fyne.KeyboardShortcut = (*CustomShortcut)(nil)
+
+// CustomShortcut describes a shortcut desktop event.
+type CustomShortcut struct {
+	fyne.KeyName
+	Modifier fyne.KeyModifier
+}
+
+// Key returns the key name of this shortcut.
+// @implements KeyboardShortcut
+func (cs *CustomShortcut) Key() fyne.KeyName {
+	return cs.KeyName
+}
+
+// Mod returns the modifier of this shortcut.
+// @implements KeyboardShortcut
+func (cs *CustomShortcut) Mod() fyne.KeyModifier {
+	return cs.Modifier
+}
+
+// ShortcutName returns the shortcut name associated to the event
+func (cs *CustomShortcut) ShortcutName() string {
+	id := &strings.Builder{}
+	id.WriteString("CustomDesktop:")
+	id.WriteString(modifierToString(cs.Modifier))
+	id.WriteString("+")
+	id.WriteString(string(cs.KeyName))
+	return id.String()
+}
+
+func modifierToString(mods fyne.KeyModifier) string {
+	s := []string{}
+	if (mods & fyne.KeyModifierShift) != 0 {
+		s = append(s, string("Shift"))
+	}
+	if (mods & fyne.KeyModifierControl) != 0 {
+		s = append(s, string("Control"))
+	}
+	if (mods & fyne.KeyModifierAlt) != 0 {
+		s = append(s, string("Alt"))
+	}
+	if (mods & fyne.KeyModifierSuper) != 0 {
+		if runtime.GOOS == "darwin" {
+			s = append(s, string("Command"))
+		} else {
+			s = append(s, string("Super"))
+		}
+	}
+	return strings.Join(s, "+")
+}

+ 12 - 0
vendor/fyne.io/fyne/v2/driver/mobile/device.go

@@ -0,0 +1,12 @@
+// Package mobile provides mobile specific driver functionality.
+package mobile
+
+// Device describes functionality only available on mobile
+type Device interface {
+	// Request that the mobile device show the touch screen keyboard (standard layout)
+	ShowVirtualKeyboard()
+	// Request that the mobile device show the touch screen keyboard (custom layout)
+	ShowVirtualKeyboardType(KeyboardType)
+	// Request that the mobile device dismiss the touch screen keyboard
+	HideVirtualKeyboard()
+}

+ 26 - 0
vendor/fyne.io/fyne/v2/driver/mobile/keyboard.go

@@ -0,0 +1,26 @@
+package mobile
+
+import (
+	"fyne.io/fyne/v2"
+)
+
+// KeyboardType represents a type of virtual keyboard
+type KeyboardType int32
+
+const (
+	// DefaultKeyboard is the keyboard with default input style and "return" return key
+	DefaultKeyboard KeyboardType = iota
+	// SingleLineKeyboard is the keyboard with default input style and "Done" return key
+	SingleLineKeyboard
+	// NumberKeyboard is the keyboard with number input style and "Done" return key
+	NumberKeyboard
+	// PasswordKeyboard is used to ensure that text is not leaked to 3rd party keyboard providers
+	PasswordKeyboard
+)
+
+// Keyboardable describes any CanvasObject that needs a keyboard
+type Keyboardable interface {
+	fyne.Focusable
+
+	Keyboard() KeyboardType
+}

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

@@ -0,0 +1,15 @@
+package mobile
+
+import "fyne.io/fyne/v2"
+
+// TouchEvent contains data relating to mobile touch events
+type TouchEvent struct {
+	fyne.PointEvent
+}
+
+// Touchable represents mobile touch events that can be sent to CanvasObjects
+type Touchable interface {
+	TouchDown(*TouchEvent)
+	TouchUp(*TouchEvent)
+	TouchCancel(*TouchEvent)
+}

+ 37 - 0
vendor/fyne.io/fyne/v2/event.go

@@ -0,0 +1,37 @@
+package fyne
+
+// HardwareKey contains information associated with physical key events
+// Most applications should use KeyName for cross-platform compatibility.
+type HardwareKey struct {
+	// ScanCode represents a hardware ID for (normally desktop) keyboard events.
+	ScanCode int
+}
+
+// KeyEvent describes a keyboard input event.
+type KeyEvent struct {
+	// Name describes the keyboard event that is consistent across platforms.
+	Name KeyName
+	// Physical is a platform specific field that reports the hardware information of physical keyboard events.
+	Physical HardwareKey
+}
+
+// PointEvent describes a pointer input event. The position is relative to the
+// top-left of the CanvasObject this is triggered on.
+type PointEvent struct {
+	AbsolutePosition Position // The absolute position of the event
+	Position         Position // The relative position of the event
+}
+
+// ScrollEvent defines the parameters of a pointer or other scroll event.
+// The DeltaX and DeltaY represent how large the scroll was in two dimensions.
+type ScrollEvent struct {
+	PointEvent
+	Scrolled Delta
+}
+
+// DragEvent defines the parameters of a pointer or other drag event.
+// The DraggedX and DraggedY fields show how far the item was dragged since the last event.
+type DragEvent struct {
+	PointEvent
+	Dragged Delta
+}

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

@@ -0,0 +1,28 @@
+// Package fyne describes the objects and components available to any Fyne app.
+// These can all be created, manipulated and tested without rendering (for speed).
+// Your main package should use the app package to create an application with
+// a default driver that will render your UI.
+//
+// A simple application may look like this:
+//
+//	package main
+//
+//	import "fyne.io/fyne/v2/app"
+//	import "fyne.io/fyne/v2/container"
+//	import "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()
+//	}
+package fyne // import "fyne.io/fyne/v2"

+ 142 - 0
vendor/fyne.io/fyne/v2/geometry.go

@@ -0,0 +1,142 @@
+package fyne
+
+var _ Vector2 = (*Delta)(nil)
+var _ Vector2 = (*Position)(nil)
+var _ Vector2 = (*Size)(nil)
+
+// Vector2 marks geometry types that can operate as a coordinate vector.
+type Vector2 interface {
+	Components() (float32, float32)
+	IsZero() bool
+}
+
+// Delta is a generic X, Y coordinate, size or movement representation.
+type Delta struct {
+	DX, DY float32
+}
+
+// NewDelta returns a newly allocated Delta representing a movement in the X and Y axis.
+func NewDelta(dx float32, dy float32) Delta {
+	return Delta{DX: dx, DY: dy}
+}
+
+// Components returns the X and Y elements of this Delta.
+func (v Delta) Components() (float32, float32) {
+	return v.DX, v.DY
+}
+
+// IsZero returns whether the Position is at the zero-point.
+func (v Delta) IsZero() bool {
+	return v.DX == 0.0 && v.DY == 0.0
+}
+
+// Position describes a generic X, Y coordinate relative to a parent Canvas
+// or CanvasObject.
+type Position struct {
+	X float32 // The position from the parent's left edge
+	Y float32 // The position from the parent's top edge
+}
+
+// NewPos returns a newly allocated Position representing the specified coordinates.
+func NewPos(x float32, y float32) Position {
+	return Position{x, y}
+}
+
+// Add returns a new Position that is the result of offsetting the current
+// position by p2 X and Y.
+func (p Position) Add(v Vector2) Position {
+	x, y := v.Components()
+	return Position{p.X + x, p.Y + y}
+}
+
+// AddXY returns a new Position by adding x and y to the current one.
+func (p Position) AddXY(x, y float32) Position {
+	return Position{p.X + x, p.Y + y}
+}
+
+// Components returns the X and Y elements of this Position
+func (p Position) Components() (float32, float32) {
+	return p.X, p.Y
+}
+
+// IsZero returns whether the Position is at the zero-point.
+func (p Position) IsZero() bool {
+	return p.X == 0.0 && p.Y == 0.0
+}
+
+// Subtract returns a new Position that is the result of offsetting the current
+// position by p2 -X and -Y.
+func (p Position) Subtract(v Vector2) Position {
+	x, y := v.Components()
+	return Position{p.X - x, p.Y - y}
+}
+
+// SubtractXY returns a new Position by subtracting x and y from the current one.
+func (p Position) SubtractXY(x, y float32) Position {
+	return Position{p.X - x, p.Y - y}
+}
+
+// Size describes something with width and height.
+type Size struct {
+	Width  float32 // The number of units along the X axis.
+	Height float32 // The number of units along the Y axis.
+}
+
+// NewSize returns a newly allocated Size of the specified dimensions.
+func NewSize(w float32, h float32) Size {
+	return Size{w, h}
+}
+
+// Add returns a new Size that is the result of increasing the current size by
+// s2 Width and Height.
+func (s Size) Add(v Vector2) Size {
+	w, h := v.Components()
+	return Size{s.Width + w, s.Height + h}
+}
+
+// AddWidthHeight returns a new Size by adding width and height to the current one.
+func (s Size) AddWidthHeight(width, height float32) Size {
+	return Size{s.Width + width, s.Height + height}
+}
+
+// IsZero returns whether the Size has zero width and zero height.
+func (s Size) IsZero() bool {
+	return s.Width == 0.0 && s.Height == 0.0
+}
+
+// Max returns a new Size that is the maximum of the current Size and s2.
+func (s Size) Max(v Vector2) Size {
+	x, y := v.Components()
+
+	maxW := Max(s.Width, x)
+	maxH := Max(s.Height, y)
+
+	return NewSize(maxW, maxH)
+}
+
+// Min returns a new Size that is the minimum of the current Size and s2.
+func (s Size) Min(v Vector2) Size {
+	x, y := v.Components()
+
+	minW := Min(s.Width, x)
+	minH := Min(s.Height, y)
+
+	return NewSize(minW, minH)
+}
+
+// Components returns the Width and Height elements of this Size
+func (s Size) Components() (float32, float32) {
+	return s.Width, s.Height
+}
+
+// Subtract returns a new Size that is the result of decreasing the current size
+// by s2 Width and Height.
+func (s Size) Subtract(v Vector2) Size {
+	w, h := v.Components()
+	return Size{s.Width - w, s.Height - h}
+}
+
+// SubtractWidthHeight returns a new Size by subtracting width and height from the current one.
+func (s Size) SubtractWidthHeight(width, height float32) Size {
+	return Size{s.Width - width, s.Height - height}
+}

+ 33 - 0
vendor/fyne.io/fyne/v2/internal/animation/animation.go

@@ -0,0 +1,33 @@
+package animation
+
+import (
+	"sync/atomic"
+	"time"
+
+	"fyne.io/fyne/v2"
+)
+
+type anim struct {
+	a           *fyne.Animation
+	end         time.Time
+	repeatsLeft int
+	reverse     bool
+	start       time.Time
+	total       int64
+	stopped     uint32 // atomic, 0 == false 1 == true
+}
+
+func newAnim(a *fyne.Animation) *anim {
+	animate := &anim{a: a, start: time.Now(), end: time.Now().Add(a.Duration)}
+	animate.total = animate.end.Sub(animate.start).Milliseconds()
+	animate.repeatsLeft = a.RepeatCount
+	return animate
+}
+
+func (a *anim) setStopped() {
+	atomic.StoreUint32(&a.stopped, 1)
+}
+
+func (a *anim) isStopped() bool {
+	return atomic.LoadUint32(&a.stopped) == 1
+}

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov