Răsfoiți Sursa

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

SVI 2 ani în urmă
părinte
comite
f47044b3b4
100 a modificat fișierele cu 3600 adăugiri și 1241 ștergeri
  1. 4 4
      go.mod
  2. 10 42
      go.sum
  3. 90 1
      vendor/fyne.io/fyne/v2/CHANGELOG.md
  4. 0 4
      vendor/fyne.io/fyne/v2/README.md
  5. 2 2
      vendor/fyne.io/fyne/v2/SECURITY.md
  6. 15 16
      vendor/fyne.io/fyne/v2/app/app.go
  7. 3 1
      vendor/fyne.io/fyne/v2/app/app_desktop_darwin.go
  8. 3 5
      vendor/fyne.io/fyne/v2/app/app_windows.go
  9. 11 4
      vendor/fyne.io/fyne/v2/app/app_xdg.go
  10. 1 1
      vendor/fyne.io/fyne/v2/app/cloud.go
  11. 56 1
      vendor/fyne.io/fyne/v2/app/preferences.go
  12. 12 5
      vendor/fyne.io/fyne/v2/app/settings.go
  13. 1 1
      vendor/fyne.io/fyne/v2/app/settings_desktop.go
  14. 8 0
      vendor/fyne.io/fyne/v2/app/settings_noanimation.go
  15. 3 1
      vendor/fyne.io/fyne/v2/canvas/circle.go
  16. 202 20
      vendor/fyne.io/fyne/v2/canvas/image.go
  17. 4 0
      vendor/fyne.io/fyne/v2/canvas/rectangle.go
  18. 17 14
      vendor/fyne.io/fyne/v2/container/apptabs.go
  19. 2 2
      vendor/fyne.io/fyne/v2/container/container.go
  20. 5 1
      vendor/fyne.io/fyne/v2/container/doctabs.go
  21. 13 1
      vendor/fyne.io/fyne/v2/container/layouts.go
  22. 31 18
      vendor/fyne.io/fyne/v2/container/split.go
  23. 47 36
      vendor/fyne.io/fyne/v2/container/tabs.go
  24. 1816 0
      vendor/fyne.io/fyne/v2/data/binding/bindtrees.go
  25. 118 0
      vendor/fyne.io/fyne/v2/data/binding/bool.go
  26. 86 0
      vendor/fyne.io/fyne/v2/data/binding/treebinding.go
  27. 5 0
      vendor/fyne.io/fyne/v2/driver/desktop/driver.go
  28. 10 0
      vendor/fyne.io/fyne/v2/driver/mobile/driver.go
  29. 10 0
      vendor/fyne.io/fyne/v2/driver/mobile/key.go
  30. 18 0
      vendor/fyne.io/fyne/v2/geometry.go
  31. 2 2
      vendor/fyne.io/fyne/v2/internal/app/focus_manager.go
  32. 13 1
      vendor/fyne.io/fyne/v2/internal/app/lifecycle.go
  33. 1 0
      vendor/fyne.io/fyne/v2/internal/cache/base.go
  34. 8 2
      vendor/fyne.io/fyne/v2/internal/cache/canvases.go
  35. 3 1
      vendor/fyne.io/fyne/v2/internal/cache/svg.go
  36. 24 1
      vendor/fyne.io/fyne/v2/internal/cache/text.go
  37. 4 2
      vendor/fyne.io/fyne/v2/internal/clip.go
  38. 100 25
      vendor/fyne.io/fyne/v2/internal/driver/common/canvas.go
  39. 3 3
      vendor/fyne.io/fyne/v2/internal/driver/common/window.go
  40. 31 25
      vendor/fyne.io/fyne/v2/internal/driver/glfw/canvas.go
  41. 15 27
      vendor/fyne.io/fyne/v2/internal/driver/glfw/driver.go
  42. 21 11
      vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_desktop.go
  43. 6 0
      vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_notwayland.go
  44. 1 3
      vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_wayland.go
  45. 3 0
      vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_web.go
  46. 20 30
      vendor/fyne.io/fyne/v2/internal/driver/glfw/loop.go
  47. 0 5
      vendor/fyne.io/fyne/v2/internal/driver/glfw/loop_desktop.go
  48. 5 6
      vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_bar.go
  49. 2 0
      vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_bar_item.go
  50. 1 1
      vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_darwin.go
  51. 10 0
      vendor/fyne.io/fyne/v2/internal/driver/glfw/scroll_speed_darwin.go
  52. 10 0
      vendor/fyne.io/fyne/v2/internal/driver/glfw/scroll_speed_default.go
  53. 45 32
      vendor/fyne.io/fyne/v2/internal/driver/glfw/window.go
  54. 51 5
      vendor/fyne.io/fyne/v2/internal/driver/glfw/window_desktop.go
  55. 7 3
      vendor/fyne.io/fyne/v2/internal/driver/glfw/window_goxjs.go
  56. 2 2
      vendor/fyne.io/fyne/v2/internal/driver/glfw/window_notwindows.go
  57. 4 3
      vendor/fyne.io/fyne/v2/internal/driver/glfw/window_windows.go
  58. 16 0
      vendor/fyne.io/fyne/v2/internal/driver/mobile/app/GoNativeActivity.java
  59. 22 1
      vendor/fyne.io/fyne/v2/internal/driver/mobile/app/android.c
  60. 26 0
      vendor/fyne.io/fyne/v2/internal/driver/mobile/app/android.go
  61. 2 2
      vendor/fyne.io/fyne/v2/internal/driver/mobile/app/app.go
  62. 4 0
      vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_desktop.go
  63. 4 0
      vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_ios.go
  64. 4 0
      vendor/fyne.io/fyne/v2/internal/driver/mobile/app/shiny.go
  65. 2 2
      vendor/fyne.io/fyne/v2/internal/driver/mobile/app/x11.c
  66. 7 2
      vendor/fyne.io/fyne/v2/internal/driver/mobile/app/x11.go
  67. 8 6
      vendor/fyne.io/fyne/v2/internal/driver/mobile/canvas.go
  68. 29 12
      vendor/fyne.io/fyne/v2/internal/driver/mobile/driver.go
  69. 2 0
      vendor/fyne.io/fyne/v2/internal/driver/mobile/event/key/key.go
  70. 0 2
      vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/consts.go
  71. 8 9
      vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/dll_windows.go
  72. 1 1
      vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/fn.go
  73. 16 10
      vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/gl.go
  74. 6 5
      vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/interface.go
  75. 3 3
      vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.c
  76. 3 4
      vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.go
  77. 1 1
      vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.h
  78. 0 181
      vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work114.go
  79. 5 4
      vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work_windows.go
  80. 5 1
      vendor/fyne.io/fyne/v2/internal/driver/mobile/window.go
  81. 8 6
      vendor/fyne.io/fyne/v2/internal/driver/util.go
  82. 12 2
      vendor/fyne.io/fyne/v2/internal/overlay_stack.go
  83. 28 5
      vendor/fyne.io/fyne/v2/internal/painter/draw.go
  84. 121 268
      vendor/fyne.io/fyne/v2/internal/painter/font.go
  85. 1 1
      vendor/fyne.io/fyne/v2/internal/painter/gl/context.go
  86. 118 48
      vendor/fyne.io/fyne/v2/internal/painter/gl/draw.go
  87. 6 1
      vendor/fyne.io/fyne/v2/internal/painter/gl/gl.go
  88. 0 9
      vendor/fyne.io/fyne/v2/internal/painter/gl/gl_const_darwin.go
  89. 0 17
      vendor/fyne.io/fyne/v2/internal/painter/gl/gl_const_mobile.go
  90. 6 7
      vendor/fyne.io/fyne/v2/internal/painter/gl/gl_core.go
  91. 6 7
      vendor/fyne.io/fyne/v2/internal/painter/gl/gl_es.go
  92. 16 9
      vendor/fyne.io/fyne/v2/internal/painter/gl/gl_gomobile.go
  93. 6 7
      vendor/fyne.io/fyne/v2/internal/painter/gl/gl_goxjs.go
  94. 18 14
      vendor/fyne.io/fyne/v2/internal/painter/gl/painter.go
  95. 30 10
      vendor/fyne.io/fyne/v2/internal/painter/gl/shaders.go
  96. 8 41
      vendor/fyne.io/fyne/v2/internal/painter/gl/texture.go
  97. 11 136
      vendor/fyne.io/fyne/v2/internal/painter/image.go
  98. 30 30
      vendor/fyne.io/fyne/v2/internal/painter/software/draw.go
  99. 6 6
      vendor/fyne.io/fyne/v2/internal/painter/software/painter.go
  100. 0 1
      vendor/fyne.io/fyne/v2/internal/painter/vector.go

+ 4 - 4
go.mod

@@ -3,14 +3,14 @@ module wartank
 go 1.20
 
 require (
-	fyne.io/fyne/v2 v2.3.5
+	fyne.io/fyne/v2 v2.4.0
 	github.com/charmbracelet/bubbletea v0.24.2
 	github.com/sirupsen/logrus v1.9.3
 	github.com/syndtr/goleveldb v1.0.0
 )
 
 require (
-	fyne.io/systray v1.10.1-0.20230602210930-b6a2d6ca2a7b // indirect
+	fyne.io/systray v1.10.1-0.20230722100817-88df1e0ffa9a // indirect
 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 	github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
@@ -21,9 +21,9 @@ require (
 	github.com/fyne-io/image v0.0.0-20230811065323-ed435dc8bca6 // indirect
 	github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
 	github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
+	github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 // indirect
 	github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 // indirect
 	github.com/godbus/dbus/v5 v5.1.0 // indirect
-	github.com/goki/freetype v1.0.1 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/gopherjs/gopherjs v1.17.2 // indirect
 	github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
@@ -43,7 +43,7 @@ require (
 	github.com/tevino/abool v1.2.0 // indirect
 	github.com/yuin/goldmark v1.5.6 // indirect
 	golang.org/x/image v0.11.0 // indirect
-	golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda // indirect
+	golang.org/x/mobile v0.0.0-20230901161150-52620a4a7557 // indirect
 	golang.org/x/net v0.14.0 // indirect
 	golang.org/x/sync v0.3.0 // indirect
 	golang.org/x/sys v0.11.0 // indirect

+ 10 - 42
go.sum

@@ -37,14 +37,12 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-fyne.io/fyne/v2 v2.3.5 h1:Q8WOtsms+esLrBKJGdj6P+klu+UXzRq63uPxFSQm4nc=
-fyne.io/fyne/v2 v2.3.5/go.mod h1:fbrL+kwOQ6sdVhnURktTHIRIEXwysQSLeejyFyABmNI=
-fyne.io/systray v1.10.1-0.20230602210930-b6a2d6ca2a7b h1:MP1cUnIdF1cxrMhK9iw9H0JP3zopyD1zi84BqU6WTsE=
-fyne.io/systray v1.10.1-0.20230602210930-b6a2d6ca2a7b/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE=
+fyne.io/fyne/v2 v2.4.0 h1:LlyOyHmvkSo9IBm3aY+NVWSBIw+GMnssmyyIMK8F7zM=
+fyne.io/fyne/v2 v2.4.0/go.mod h1:AWM1iPM2YfliduZ4u/kQzP9E6ARIWm0gg+57GpYzWro=
+fyne.io/systray v1.10.1-0.20230722100817-88df1e0ffa9a h1:6Xf9fP3/mt72NrqlQhJWhQGcNf6GoG9X96NTaXr+K6A=
+fyne.io/systray v1.10.1-0.20230722100817-88df1e0ffa9a/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -68,7 +66,6 @@ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:Yyn
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -80,21 +77,16 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fredbi/uri v0.1.0/go.mod h1:1xC40RnIOGCaQzswaOvrzvG/3M3F0hyDVb3aO/1iGy0=
 github.com/fredbi/uri v1.0.0 h1:s4QwUAZ8fz+mbTsukND+4V5f+mJ/wjaTokwstGUAemg=
 github.com/fredbi/uri v1.0.0/go.mod h1:1xC40RnIOGCaQzswaOvrzvG/3M3F0hyDVb3aO/1iGy0=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg=
 github.com/fyne-io/gl-js v0.0.0-20230506162202-1fdaa286a934 h1:dZC5aKobSN07hf71oMivxUmAofFja5GrfPK2rBlttX4=
 github.com/fyne-io/gl-js v0.0.0-20230506162202-1fdaa286a934/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg=
-github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504/go.mod h1:gLRWYfYnMA9TONeppRSikMdXlHQ97xVsPojddUv3b/E=
 github.com/fyne-io/glfw-js v0.0.0-20220517201726-bebc2019cd33 h1:0Ayg0/do/sqX2R7NonoLZvWxGrd9utTVf3A0QvCbC88=
 github.com/fyne-io/glfw-js v0.0.0-20220517201726-bebc2019cd33/go.mod h1:gLRWYfYnMA9TONeppRSikMdXlHQ97xVsPojddUv3b/E=
-github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2/go.mod h1:eO7W361vmlPOrykIg+Rsh1SZ3tQBaOsfzZhsIOb/Lm0=
 github.com/fyne-io/image v0.0.0-20230811065323-ed435dc8bca6 h1:kZNUHSV3ZTddRiWy5JHK6RgB3zdH/875SYXmt3EoNvQ=
 github.com/fyne-io/image v0.0.0-20230811065323-ed435dc8bca6/go.mod h1:aX1w6epS9BQn2bePY+3rkQejetaffeFhXl0s8QjXJJk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -106,19 +98,15 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
-github.com/go-text/typesetting v0.0.0-20230405155246-bf9c697c6e16/go.mod h1:zvWM81wAVW6QfVDI6yxfbCuoLnobSYTuMsrXU/u11y8=
+github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 h1:VkKnvzbvHqgEfm351rfr8Uclu5fnwq8HP2ximUzJsBM=
+github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8/go.mod h1:h29xCucjNsDcYb7+0rJokxVwYAq+9kQ19WiFuBKkYtc=
 github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
 github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
-github.com/go-text/typesetting-utils v0.0.0-20230326210548-458646692de6/go.mod h1:RaqFwjcYyM5BjbYGwON0H5K0UqwO3sJlo9ukKha80ZE=
 github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
 github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
-github.com/goki/freetype v1.0.1 h1:10DgpEu+QEh/hpvAxgx//RT8ayWwHJI+nZj3QNcn8uk=
-github.com/goki/freetype v1.0.1/go.mod h1:ni9Dgz8vA6o+13u1Ke0q3kJcCJ9GuXb1dtlfKho98vs=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -216,25 +204,22 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/jackmordaunt/icns/v2 v2.2.1/go.mod h1:6aYIB9eSzyfHHMKqDf17Xrs1zetQPReAkiUSHzdw4cI=
-github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
 github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
 github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
-github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@@ -245,7 +230,6 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
 github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -268,9 +252,6 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo
 github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
 github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
 github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
-github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -291,14 +272,12 @@ github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
 github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
-github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@@ -309,10 +288,8 @@ github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t6
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
-github.com/srwiley/oksvg v0.0.0-20220731023508-a61f04f16b76/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
-github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
 github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
 github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -332,7 +309,6 @@ github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFd
 github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
 github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
 github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
-github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -377,8 +353,6 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
-golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A=
 golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
 golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -396,8 +370,8 @@ golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPI
 golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
 golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
-golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda h1:O+EUvnBNPwI4eLthn8W5K+cS8zQZfgTABPLNm6Bna34=
-golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
+golang.org/x/mobile v0.0.0-20230901161150-52620a4a7557 h1:mLrcd+qwh23kzD7ej1VxCa+A23UNr+BCjSj2tNX8/NM=
+golang.org/x/mobile v0.0.0-20230901161150-52620a4a7557/go.mod h1:f0gjFM6UTH7y1WEZBm/kquBYsogL+NQtllKFy4Rdulc=
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
 golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
@@ -447,7 +421,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
@@ -491,10 +464,8 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -527,7 +498,6 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -551,7 +521,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
 golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
@@ -720,9 +689,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=

+ 90 - 1
vendor/fyne.io/fyne/v2/CHANGELOG.md

@@ -3,6 +3,96 @@
 This file lists the main changes with each version of the Fyne toolkit.
 More detailed release notes can be found on the [releases page](https://github.com/fyne-io/fyne/releases). 
 
+## 2.4.0 - 1 September 2023
+
+## Added
+
+* Rounded corners in rectangle (#1090)
+* Support for emoji in text
+* Layout debugging (with `-tags debug` build flag) (#3314)
+* GridWrap collection widget
+* Add table headers (#1658, #3594)
+* Add mobile back button handling (#2910)
+* Add option to disable UI animations (#1813)
+* Text truncation ellipsis (#1659)
+* Add support for binding tree data, include new `NewTreeWithData`
+* Add support for OpenType fonts (#3245)
+* Add `Window.SetOnDropped` to handle window-wide item drop on desktop
+* Add lists to the types supported by preferences API
+* Keyboard focus handling for all collection widgets
+* Add APIs for refreshing individual items in collections (#3826)
+* Tapping slider moves it to that position (#3650)
+* Add `OnChangeEnded` callback to `Slider` (#3652)
+* Added keyboard controls to `Slider`
+* Add `NewWarningThemedResource` and `NewSuccessThemedResource` along with `NewColoredResource` (#4040)
+* Custom hyperlink callback for rich text hyperlinks (#3335)
+* Added `dialog.NewCustomWithoutButtons`, with a `SetButtons` method (#2127, #2782)
+* Added `SetConfirmImportance` to `dialog.ConfirmDialog`.
+* Added `FormDialog.Submit()` to close and submit the dialog if validation passes
+* Rich Text image alignment (#3810)
+* Bring back `theme.HyperlinkColor` (#3867)
+* Added `Importance` field on `Label` to color the text
+* Navigating in entry quickly with ctrl key (#2462)
+* Support `.desktop` file metadata in `FyneApp.toml` for Linux and BSD
+* Support mobile simulator on FreeBSD
+* Add data binding boolean operators `Not`, `And` and `Or`
+* Added `Entry.Append`, `Select.SetOptions`, `Check.SetText`, `FormDialog.Submit`
+* Add `ShowPopUpAtRelativePosition` and `PopUp.ShowAtRelativePosition`
+* Add desktop support to get key modifiers with `CurrentKeyModifiers`
+* Add geometry helpers `NewSquareSize` and `NewSquareOffsetPos`
+* Add `--pprof` option to fyne build commands to enable profiling
+* Support compiling from Android (termux)
+
+## Changed
+
+* Go 1.17 or later is now required.
+* Theme updated for rounded corners on buttons and input widgets
+* `widget.ButtonImportance` is now `widget.Importance`
+* The `Max` container and layout have been renamed `Stack` for clarity
+* Refreshing an image will now happen in app-thread not render process, apps may wish to add async image load
+* Icons for macOS bundles are now padded and rounded, disable with "-use-raw-icon" (#3752)
+* Update Android target SDK to 33 for Play Store releases
+* Focus handling for List/Tree/Table are now at the parent widget not child elements
+* Accordion widget now fills available space - put it inside a `VBox` container for old behavior (#4126)
+* Deprecated theme.FyneLogo() for later removal (#3296)
+* Improve look of menu shortcuts (#2722)
+* iOS and macOS packages now default to using "XCWildcard" provisioning profile
+* Improving performance of lookup for theme data
+* Improved application startup time
+
+## Fixed
+
+* Rendering performance enhancements
+* `dialog.NewProgressInfinite` is deprecated, but dialog.NewCustom isn't equivalent
+* Mouse cursor desync with Split handle when dragging (#3791)
+* Minor graphic glitch with checkbox (#3792)
+* binding.String===>Quick refresh *b.val will appear with new data reset by a call to OnChange (#3774)
+* Fyne window becomes unresponsive when in background for a while (#2791)
+* Hangs on repeated calls to `Select.SetSelected` in table. (#3684)
+* `Select` has wrong height, padding and border (#4142)
+* `widget.ImageSegment` can't be aligned. (#3505)
+* Memory leak in font metrics cache (#4108)
+* Don't panic when loading preferences with wrong type (#4039)
+* Button with icon has wrong padding on right (#4124)
+* Preferences don't all save when written in `CloseIntercept` (#3170)
+* Text size does not update in Refresh for TextGrid
+* DocTab selection underline not updated when deleting an Item (#3905)
+* Single line Entry throws away selected text on submission (#4026)
+* Significantly improve performance of large `TextGrid` and `Tree` widgets
+* `List.ScrollToBottom` not scrolling to show the totality of the last Item (#3829)
+* Setting `Position1` of canvas.Circle higher than `Position2` causes panic. (#3949)
+* Enhance scroll wheel/touchpad scroll speed on desktop (#3492)
+* Possible build issue on Windows with app metadata
+* `Form` hint text has confusing padding to next widget (#4137)
+* `Entry` Placeholder Style Only Applied On Click (#4035)
+* Backspace and Delete key Do not Fire OnChanged Event (#4117)
+* Fix `ProgressBar` text having the wrong color sometimes
+* Window doesn't render when called for the first time from system tray and the last window was closed (#4163)
+* Possible race condition in preference change listeners
+* Various vulnerabilities resolved through updating dependencies 
+* Wrong background for color dialog (#4199)
+
+
 ## 2.3.5 - 6 June 2023
 
 ### Fixed
@@ -40,7 +130,6 @@ More detailed release notes can be found on the [releases page](https://github.c
 * VBox and HBox using heap memory that was not required
 * Menu hover is slow on long menus
 
-
 ## 2.3.3 - 24 March 2023
 
 ### Fixed

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

@@ -42,8 +42,6 @@ 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%">
@@ -131,8 +129,6 @@ 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.

+ 2 - 2
vendor/fyne.io/fyne/v2/SECURITY.md

@@ -6,8 +6,8 @@ Minor releases will receive security updates and fixes until the next minor or m
 
 | Version | Supported          |
 | ------- | ------------------ |
-| 2.3.x   | :white_check_mark: |
-| < 2.3.0 | :x:                |
+| 2.4.x   | :white_check_mark: |
+| < 2.4.0 | :x:                |
 
 ## Reporting a Vulnerability
 

+ 15 - 16
vendor/fyne.io/fyne/v2/app/app.go

@@ -14,8 +14,6 @@ import (
 	"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
@@ -33,7 +31,6 @@ type fyneApp struct {
 	prefs     fyne.Preferences
 
 	running uint32 // atomic, 1 == running, 0 == stopped
-	exec    func(name string, arg ...string) *execabs.Cmd
 }
 
 func (a *fyneApp) CloudProvider() fyne.CloudProvider {
@@ -72,7 +69,6 @@ func (a *fyneApp) NewWindow(title string) fyne.Window {
 func (a *fyneApp) Run() {
 	if atomic.CompareAndSwapUint32(&a.running, 0, 1) {
 		a.driver.Run()
-		return
 	}
 }
 
@@ -110,10 +106,10 @@ 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()
+func (a *fyneApp) newDefaultPreferences() *preferences {
+	p := newPreferences(a)
+	if a.uniqueID != "" {
+		p.load()
 	}
 	return p
 }
@@ -126,11 +122,8 @@ func New() fyne.App {
 	return NewWithID(meta.ID)
 }
 
-func makeStoreDocs(id string, p fyne.Preferences, s *store) *internal.Docs {
+func makeStoreDocs(id string, 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)
@@ -144,21 +137,27 @@ func makeStoreDocs(id string, p fyne.Preferences, s *store) *internal.Docs {
 }
 
 func newAppWithDriver(d fyne.Driver, id string) fyne.App {
-	newApp := &fyneApp{uniqueID: id, driver: d, exec: execabs.Command, lifecycle: &app.Lifecycle{}}
+	newApp := &fyneApp{uniqueID: id, driver: d, lifecycle: &app.Lifecycle{}}
 	fyne.SetCurrentApp(newApp)
 
 	newApp.prefs = newApp.newDefaultPreferences()
+	newApp.lifecycle.(*app.Lifecycle).SetOnStoppedHookExecuted(func() {
+		if prefs, ok := newApp.prefs.(*preferences); ok {
+			prefs.forceImmediateSave()
+		}
+	})
 	newApp.settings = loadSettings()
 	store := &store{a: newApp}
-	store.Docs = makeStoreDocs(id, newApp.prefs, store)
+	store.Docs = makeStoreDocs(id, store)
 	newApp.storage = store
 
 	if !d.Device().IsMobile() {
 		newApp.settings.watchSettings()
 	}
 
-	repository.Register("http", intRepo.NewHTTPRepository())
-	repository.Register("https", intRepo.NewHTTPRepository())
+	httpHandler := intRepo.NewHTTPRepository()
+	repository.Register("http", httpHandler)
+	repository.Register("https", httpHandler)
 
 	return newApp
 }

+ 3 - 1
vendor/fyne.io/fyne/v2/app/app_desktop_darwin.go

@@ -19,6 +19,8 @@ import (
 	"os"
 	"path/filepath"
 
+	"golang.org/x/sys/execabs"
+
 	"fyne.io/fyne/v2"
 	"fyne.io/fyne/v2/theme"
 )
@@ -52,7 +54,7 @@ func rootConfigDir() string {
 }
 
 func (a *fyneApp) OpenURL(url *url.URL) error {
-	cmd := a.exec("open", url.String())
+	cmd := execabs.Command("open", url.String())
 	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 	return cmd.Run()
 }

+ 3 - 5
vendor/fyne.io/fyne/v2/app/app_windows.go

@@ -5,19 +5,17 @@ package app
 
 import (
 	"fmt"
-	"io/ioutil"
 	"net/url"
 	"os"
 	"path/filepath"
 	"strings"
 	"syscall"
 
+	"golang.org/x/sys/execabs"
 	"golang.org/x/sys/windows/registry"
 
 	"fyne.io/fyne/v2"
 	"fyne.io/fyne/v2/theme"
-
-	"golang.org/x/sys/execabs"
 )
 
 const notificationTemplate = `$title = "%s"
@@ -64,7 +62,7 @@ func rootConfigDir() string {
 }
 
 func (a *fyneApp) OpenURL(url *url.URL) error {
-	cmd := a.exec("rundll32", "url.dll,FileProtocolHandler", url.String())
+	cmd := execabs.Command("rundll32", "url.dll,FileProtocolHandler", url.String())
 	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 	return cmd.Run()
 }
@@ -106,7 +104,7 @@ func runScript(name, script string) {
 	fileName := fmt.Sprintf("fyne-%s-%s-%d.ps1", appID, name, scriptNum)
 
 	tmpFilePath := filepath.Join(os.TempDir(), fileName)
-	err := ioutil.WriteFile(tmpFilePath, []byte(script), 0600)
+	err := os.WriteFile(tmpFilePath, []byte(script), 0600)
 	if err != nil {
 		fyne.LogError("Could not write script to show notification", err)
 		return

+ 11 - 4
vendor/fyne.io/fyne/v2/app/app_xdg.go

@@ -15,6 +15,7 @@ import (
 	"sync"
 
 	"github.com/godbus/dbus/v5"
+	"golang.org/x/sys/execabs"
 
 	"fyne.io/fyne/v2"
 	"fyne.io/fyne/v2/theme"
@@ -27,7 +28,7 @@ func defaultVariant() fyne.ThemeVariant {
 }
 
 func (a *fyneApp) OpenURL(url *url.URL) error {
-	cmd := a.exec("xdg-open", url.String())
+	cmd := execabs.Command("xdg-open", url.String())
 	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 	return cmd.Start()
 }
@@ -58,13 +59,19 @@ func findFreedestktopColorScheme() fyne.ThemeVariant {
 		return theme.VariantDark
 	}
 
+	// See: https://github.com/flatpak/xdg-desktop-portal/blob/1.16.0/data/org.freedesktop.impl.portal.Settings.xml#L32-L46
+	// 0: No preference
+	// 1: Prefer dark appearance
+	// 2: Prefer light appearance
 	switch value {
-	case 0:
+	case 2:
 		return theme.VariantLight
-	default:
+	case 1:
 		return theme.VariantDark
+	default:
+		// Default to light theme to support Gnome's default see https://github.com/fyne-io/fyne/pull/3561
+		return theme.VariantLight
 	}
-
 }
 
 func (a *fyneApp) SendNotification(n *fyne.Notification) {

+ 1 - 1
vendor/fyne.io/fyne/v2/app/cloud.go

@@ -33,7 +33,7 @@ func (a *fyneApp) transitionCloud(p fyne.CloudProvider) {
 		a.storage = cloud.CloudStorage(a)
 	} else {
 		store := &store{a: a}
-		store.Docs = makeStoreDocs(a.uniqueID, a.prefs, store)
+		store.Docs = makeStoreDocs(a.uniqueID, store)
 		a.storage = store
 	}
 

+ 56 - 1
vendor/fyne.io/fyne/v2/app/preferences.go

@@ -19,12 +19,25 @@ type preferences struct {
 	savedRecently       bool
 	changedDuringSaving bool
 
-	app *fyneApp
+	app                 *fyneApp
+	needsSaveBeforeExit bool
 }
 
 // Declare conformity with Preferences interface
 var _ fyne.Preferences = (*preferences)(nil)
 
+// forceImmediateSave writes preferences to file immediately, ignoring the debouncing
+// logic in the change listener. Does nothing if preferences are not backed with a file.
+func (p *preferences) forceImmediateSave() {
+	if !p.needsSaveBeforeExit {
+		return
+	}
+	err := p.save()
+	if err != nil {
+		fyne.LogError("Failed on force saving preferences", err)
+	}
+}
+
 func (p *preferences) resetSavedRecently() {
 	go func() {
 		time.Sleep(time.Millisecond * 100) // writes are not always atomic. 10ms worked, 100 is safer.
@@ -109,6 +122,10 @@ func (p *preferences) loadFromFile(path string) (err error) {
 
 	p.InMemoryPreferences.WriteValues(func(values map[string]interface{}) {
 		err = decode.Decode(&values)
+		if err != nil {
+			return
+		}
+		convertLists(values)
 	})
 
 	p.prefLock.Lock()
@@ -128,6 +145,7 @@ func newPreferences(app *fyneApp) *preferences {
 		return p
 	}
 
+	p.needsSaveBeforeExit = true
 	p.AddChangeListener(func() {
 		if p != app.prefs {
 			return
@@ -151,3 +169,40 @@ func newPreferences(app *fyneApp) *preferences {
 	p.watch()
 	return p
 }
+
+func convertLists(values map[string]interface{}) {
+	for k, v := range values {
+		if items, ok := v.([]interface{}); ok {
+			if len(items) == 0 {
+				continue
+			}
+
+			switch items[0].(type) {
+			case bool:
+				bools := make([]bool, len(items))
+				for i, item := range items {
+					bools[i] = item.(bool)
+				}
+				values[k] = bools
+			case float64:
+				floats := make([]float64, len(items))
+				for i, item := range items {
+					floats[i] = item.(float64)
+				}
+				values[k] = floats
+			case int:
+				ints := make([]int, len(items))
+				for i, item := range items {
+					ints[i] = item.(int)
+				}
+				values[k] = ints
+			case string:
+				strings := make([]string, len(items))
+				for i, item := range items {
+					strings[i] = item.(string)
+				}
+				values[k] = strings
+			}
+		}
+	}
+}

+ 12 - 5
vendor/fyne.io/fyne/v2/app/settings.go

@@ -10,14 +10,17 @@ import (
 	"fyne.io/fyne/v2/theme"
 )
 
+var noAnimations bool // set to true at compile time if no_animations tag is passed
+
 // SettingsSchema is used for loading and storing global settings
 type SettingsSchema struct {
 	// these items are used for global settings load
-	ThemeName    string  `json:"theme"`
-	Scale        float32 `json:"scale"`
-	PrimaryColor string  `json:"primary_color"`
-	CloudName    string  `json:"cloud_name"`
-	CloudConfig  string  `json:"cloud_config"`
+	ThemeName         string  `json:"theme"`
+	Scale             float32 `json:"scale"`
+	PrimaryColor      string  `json:"primary_color"`
+	CloudName         string  `json:"cloud_name"`
+	CloudConfig       string  `json:"cloud_config"`
+	DisableAnimations bool    `json:"no_animations"`
 }
 
 // StoragePath returns the location of the settings storage
@@ -70,6 +73,10 @@ func (s *settings) SetTheme(theme fyne.Theme) {
 	s.applyTheme(theme, s.variant)
 }
 
+func (s *settings) ShowAnimations() bool {
+	return !s.schema.DisableAnimations && !noAnimations
+}
+
 func (s *settings) ThemeVariant() fyne.ThemeVariant {
 	return s.variant
 }

+ 1 - 1
vendor/fyne.io/fyne/v2/app/settings_desktop.go

@@ -41,7 +41,7 @@ func watchFile(path string, callback func()) *fsnotify.Watcher {
 
 	go func() {
 		for event := range watcher.Events {
-			if event.Op&fsnotify.Remove != 0 { // if it was deleted then watch again
+			if event.Op.Has(fsnotify.Remove) { // if it was deleted then watch again
 				watcher.Remove(path) // fsnotify returns false positives, see https://github.com/fsnotify/fsnotify/issues/268
 
 				watchFileAddTarget(watcher, path)

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

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

+ 3 - 1
vendor/fyne.io/fyne/v2/canvas/circle.go

@@ -2,6 +2,7 @@ package canvas
 
 import (
 	"image/color"
+	"math"
 
 	"fyne.io/fyne/v2"
 )
@@ -79,7 +80,8 @@ func (c *Circle) Show() {
 
 // 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)
+	return fyne.NewSize(float32(math.Abs(float64(c.Position2.X)-float64(c.Position1.X))),
+		float32(math.Abs(float64(c.Position2.Y)-float64(c.Position1.Y))))
 }
 
 // Visible returns true if this circle is visible, false otherwise

+ 202 - 20
vendor/fyne.io/fyne/v2/canvas/image.go

@@ -1,12 +1,20 @@
 package canvas
 
 import (
+	"bytes"
+	"errors"
 	"image"
+	_ "image/jpeg" // avoid users having to import when using image widget
+	_ "image/png"  // avoid the same for PNG images
 	"io"
-	"io/ioutil"
+	"os"
 	"path/filepath"
+	"sync"
 
 	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/internal/cache"
+	"fyne.io/fyne/v2/internal/scale"
+	"fyne.io/fyne/v2/internal/svg"
 	"fyne.io/fyne/v2/storage"
 )
 
@@ -51,6 +59,11 @@ var _ fyne.CanvasObject = (*Image)(nil)
 type Image struct {
 	baseObject
 
+	aspect float32
+	icon   *svg.Decoder
+	isSVG  bool
+	lock   sync.Mutex
+
 	// one of the following sources will provide our image data
 	File     string        // Load the image from a file
 	Resource fyne.Resource // Load the image from an in-memory resource
@@ -59,7 +72,6 @@ type Image struct {
 	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
@@ -68,6 +80,16 @@ func (i *Image) Alpha() float64 {
 	return 1.0 - i.Translucency
 }
 
+// Aspect will return the original content aspect after it was last refreshed.
+//
+// Since: 2.4
+func (i *Image) Aspect() float32 {
+	if i.aspect == 0 {
+		i.Refresh()
+	}
+	return i.aspect
+}
+
 // Hide will set this image to not be visible
 func (i *Image) Hide() {
 	i.baseObject.Hide()
@@ -75,6 +97,14 @@ func (i *Image) Hide() {
 	repaint(i)
 }
 
+// MinSize returns the specified minimum size, if set, or {1, 1} otherwise.
+func (i *Image) MinSize() fyne.Size {
+	if i.Image == nil || i.aspect == 0 {
+		i.Refresh()
+	}
+	return i.baseObject.MinSize()
+}
+
 // Move the image object to a new position, relative to its parent top, left corner.
 func (i *Image) Move(pos fyne.Position) {
 	i.baseObject.Move(pos)
@@ -84,6 +114,58 @@ func (i *Image) Move(pos fyne.Position) {
 
 // Refresh causes this image to be redrawn with its configured state.
 func (i *Image) Refresh() {
+	i.lock.Lock()
+	defer i.lock.Unlock()
+
+	rc, err := i.updateReader()
+	if err != nil {
+		fyne.LogError("Failed to load image", err)
+		return
+	}
+	if rc != nil {
+		rcMem := rc
+		defer rcMem.Close()
+	}
+
+	if i.File != "" || i.Resource != nil || i.Image != nil {
+		r, err := i.updateAspectAndMinSize(rc)
+		if err != nil {
+			fyne.LogError("Failed to load image", err)
+			return
+		}
+		rc = io.NopCloser(r)
+	}
+
+	if i.File != "" || i.Resource != nil {
+		size := i.Size()
+		width := size.Width
+		height := size.Height
+
+		if width == 0 || height == 0 {
+			return
+		}
+
+		if i.isSVG {
+			tex, err := i.renderSVG(width, height)
+			if err != nil {
+				fyne.LogError("Failed to render SVG", err)
+				return
+			}
+			i.Image = tex
+		} else {
+			if rc == nil {
+				return
+			}
+
+			img, _, err := image.Decode(rc)
+			if err != nil {
+				fyne.LogError("Failed to render image", err)
+				return
+			}
+			i.Image = img
+		}
+	}
+
 	Refresh(i)
 }
 
@@ -93,22 +175,25 @@ 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
+	i.baseObject.Resize(s)
+	if i.FillMode == ImageFillOriginal && i.size.Height > 2 { // we can just ask for a GPU redraw to align
+		Refresh(i)
 		return
 	}
 
 	i.baseObject.Resize(s)
-
-	Refresh(i)
+	if i.isSVG || i.Image == nil {
+		i.Refresh() // we need to rasterise at the new size
+	} else {
+		Refresh(i) // just re-size using GPU scaling
+	}
 }
 
 // NewImageFromFile creates a new image from a local file.
 // Images returned from this method will scale to fit the canvas object.
 // The method for scaling can be set using the Fill field.
 func NewImageFromFile(file string) *Image {
-	return &Image{
-		File: file,
-	}
+	return &Image{File: file}
 }
 
 // NewImageFromURI creates a new image from named resource.
@@ -120,9 +205,7 @@ func NewImageFromFile(file string) *Image {
 // Since: 2.0
 func NewImageFromURI(uri fyne.URI) *Image {
 	if uri.Scheme() == "file" && len(uri.String()) > 7 {
-		return &Image{
-			File: uri.String()[7:],
-		}
+		return NewImageFromFile(uri.Path())
 	}
 
 	var read io.ReadCloser
@@ -145,28 +228,25 @@ func NewImageFromURI(uri fyne.URI) *Image {
 //
 // Since: 2.0
 func NewImageFromReader(read io.Reader, name string) *Image {
-	data, err := ioutil.ReadAll(read)
+	data, err := io.ReadAll(read)
 	if err != nil {
 		fyne.LogError("Unable to read image data", err)
 		return nil
 	}
+
 	res := &fyne.StaticResource{
 		StaticName:    name,
 		StaticContent: data,
 	}
 
-	return &Image{
-		Resource: res,
-	}
+	return NewImageFromResource(res)
 }
 
 // NewImageFromResource creates a new image by loading the specified resource.
 // Images returned from this method will scale to fit the canvas object.
 // The method for scaling can be set using the Fill field.
 func NewImageFromResource(res fyne.Resource) *Image {
-	return &Image{
-		Resource: res,
-	}
+	return &Image{Resource: res}
 }
 
 // NewImageFromImage returns a new Image instance that is rendered from the Go
@@ -174,7 +254,109 @@ func NewImageFromResource(res fyne.Resource) *Image {
 // 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,
+	return &Image{Image: img}
+}
+
+func (i *Image) name() string {
+	if i.Resource != nil {
+		return i.Resource.Name()
+	} else if i.File != "" {
+		return i.File
+	}
+	return ""
+}
+
+func (i *Image) updateReader() (io.ReadCloser, error) {
+	i.isSVG = false
+	if i.Resource != nil {
+		i.isSVG = svg.IsResourceSVG(i.Resource)
+		return io.NopCloser(bytes.NewReader(i.Resource.Content())), nil
+	} else if i.File != "" {
+		var err error
+
+		fd, err := os.Open(i.File)
+		if err != nil {
+			return nil, err
+		}
+		i.isSVG = svg.IsFileSVG(i.File)
+		return fd, nil
+	}
+	return nil, nil
+}
+
+func (i *Image) updateAspectAndMinSize(reader io.Reader) (io.Reader, error) {
+	var pixWidth, pixHeight int
+
+	if reader != nil {
+		r, width, height, aspect, err := i.imageDetailsFromReader(reader)
+		if err != nil {
+			return nil, err
+		}
+		reader = r
+		i.aspect = aspect
+		pixWidth, pixHeight = width, height
+	} else if i.Image != nil {
+		original := i.Image.Bounds().Size()
+		i.aspect = float32(original.X) / float32(original.Y)
+		pixWidth, pixHeight = original.X, original.Y
+	} else {
+		return nil, errors.New("no matching image source")
+	}
+
+	if i.FillMode == ImageFillOriginal {
+		i.SetMinSize(scale.ToFyneSize(i, pixWidth, pixHeight))
+	}
+	return reader, nil
+}
+
+func (i *Image) imageDetailsFromReader(source io.Reader) (reader io.Reader, width, height int, aspect float32, err error) {
+	if source == nil {
+		return nil, 0, 0, 0, errors.New("no matching reading reader")
+	}
+
+	if i.isSVG {
+		var err error
+
+		i.icon, err = svg.NewDecoder(source)
+		if err != nil {
+			return nil, 0, 0, 0, err
+		}
+		config := i.icon.Config()
+		width, height = config.Width, config.Height
+		aspect = config.Aspect
+	} else {
+		var buf bytes.Buffer
+		tee := io.TeeReader(source, &buf)
+		reader = io.MultiReader(&buf, source)
+
+		config, _, err := image.DecodeConfig(tee)
+		if err != nil {
+			return nil, 0, 0, 0, err
+		}
+		width, height = config.Width, config.Height
+		aspect = float32(width) / float32(height)
+	}
+	return
+}
+
+func (i *Image) renderSVG(width, height float32) (image.Image, error) {
+	c := fyne.CurrentApp().Driver().CanvasForObject(i)
+	screenWidth, screenHeight := int(width), int(height)
+	if c != nil {
+		// We want real output pixel count not just the screen coordinate space (i.e. macOS Retina)
+		screenWidth, screenHeight = c.PixelCoordinateForPosition(fyne.Position{X: width, Y: height})
+	}
+
+	tex := cache.GetSvg(i.name(), screenWidth, screenHeight)
+	if tex != nil {
+		return tex, nil
+	}
+
+	var err error
+	tex, err = i.icon.Draw(screenWidth, screenHeight)
+	if err != nil {
+		return nil, err
 	}
+	cache.SetSvg(i.name(), tex, screenWidth, screenHeight)
+	return tex, nil
 }

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

@@ -16,6 +16,10 @@ type Rectangle struct {
 	FillColor   color.Color // The rectangle fill color
 	StrokeColor color.Color // The rectangle stroke color
 	StrokeWidth float32     // The stroke width of the rectangle
+	// The radius of the rectangle corners
+	//
+	// Since: 2.4
+	CornerRadius float32
 }
 
 // Hide will set this rectangle to not be visible

+ 17 - 14
vendor/fyne.io/fyne/v2/container/apptabs.go

@@ -221,7 +221,6 @@ func (t *AppTabs) SetTabLocation(l TabLocation) {
 func (t *AppTabs) Show() {
 	t.BaseWidget.Show()
 	t.SelectIndex(t.current)
-	t.Refresh()
 }
 
 func (t *AppTabs) onUnselected() func(*TabItem) {
@@ -277,18 +276,22 @@ type appTabsRenderer struct {
 
 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
+	if len(r.appTabs.Items) == 0 {
+		r.updateTabs(0)
+	} else {
+		for i := len(r.appTabs.Items); i > 0; i-- {
+			r.updateTabs(i)
+			barMin := r.bar.MinSize()
+			if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
+				if barMin.Height <= size.Height {
+					// Tab bar is short enough to fit
+					break
+				}
+			} else {
+				if barMin.Width <= size.Width {
+					// Tab bar is thin enough to fit
+					break
+				}
 			}
 		}
 	}
@@ -435,7 +438,7 @@ func (r *appTabsRenderer) updateTabs(max int) {
 	// Set overflow action
 	if tabCount <= max {
 		r.action.Hide()
-		r.bar.Layout = layout.NewMaxLayout()
+		r.bar.Layout = layout.NewStackLayout()
 	} else {
 		tabCount = max
 		r.action.Show()

+ 2 - 2
vendor/fyne.io/fyne/v2/container/container.go

@@ -9,12 +9,12 @@ import (
 //
 // Since: 2.0
 func New(layout fyne.Layout, objects ...fyne.CanvasObject) *fyne.Container {
-	return fyne.NewContainerWithLayout(layout, objects...)
+	return &fyne.Container{Layout: layout, Objects: objects}
 }
 
 // NewWithoutLayout returns a new Container instance holding the specified CanvasObjects that are manually arranged.
 //
 // Since: 2.0
 func NewWithoutLayout(objects ...fyne.CanvasObject) *fyne.Container {
-	return fyne.NewContainerWithoutLayout(objects...)
+	return &fyne.Container{Objects: objects}
 }

+ 5 - 1
vendor/fyne.io/fyne/v2/container/doctabs.go

@@ -178,7 +178,6 @@ func (t *DocTabs) SetTabLocation(l TabLocation) {
 func (t *DocTabs) Show() {
 	t.BaseWidget.Show()
 	t.SelectIndex(t.current)
-	t.Refresh()
 }
 
 func (t *DocTabs) close(item *TabItem) {
@@ -245,7 +244,12 @@ func (r *docTabsRenderer) Layout(size fyne.Size) {
 	r.updateCreateTab()
 	r.updateTabs()
 	r.layout(r.docTabs, size)
+
+	// lay out buttons before updating indicator, which is relative to their position
+	buttons := r.scroller.Content.(*fyne.Container)
+	buttons.Layout.Layout(buttons.Objects, buttons.Size())
 	r.updateIndicator(r.docTabs.transitioning())
+
 	if r.docTabs.transitioning() {
 		r.docTabs.setTransitioning(false)
 	}

+ 13 - 1
vendor/fyne.io/fyne/v2/container/layouts.go

@@ -87,8 +87,10 @@ func NewHBox(objects ...fyne.CanvasObject) *fyne.Container {
 // NewMax creates a new container with the specified objects filling the available space.
 //
 // Since: 1.4
+//
+// Deprecated: Use container.NewStack() instead.
 func NewMax(objects ...fyne.CanvasObject) *fyne.Container {
-	return New(layout.NewMaxLayout(), objects...)
+	return NewStack(objects...)
 }
 
 // NewPadded creates a new container with the specified objects inset by standard padding size.
@@ -98,6 +100,16 @@ func NewPadded(objects ...fyne.CanvasObject) *fyne.Container {
 	return New(layout.NewPaddedLayout(), objects...)
 }
 
+// NewStack returns a new container that stacks objects on top of each other.
+// Objects at the end of the container will be stacked on top of objects before.
+// Having only a single object has no impact as CanvasObjects will
+// fill the available space even without a Stack.
+//
+// Since: 2.4
+func NewStack(objects ...fyne.CanvasObject) *fyne.Container {
+	return New(layout.NewStackLayout(), objects...)
+}
+
 // NewVBox creates a new container with the specified objects and using the VBox layout.
 // The objects will be stacked in the container from top to bottom and always displayed
 // at their vertical MinSize. Use a different layout if the objects are intended

+ 31 - 18
vendor/fyne.io/fyne/v2/container/split.go

@@ -217,8 +217,10 @@ var _ desktop.Hoverable = (*divider)(nil)
 
 type divider struct {
 	widget.BaseWidget
-	split   *Split
-	hovered bool
+	split          *Split
+	hovered        bool
+	startDragOff   *fyne.Position
+	currentDragPos fyne.Position
 }
 
 func newDivider(split *Split) *divider {
@@ -250,26 +252,37 @@ func (d *divider) Cursor() desktop.Cursor {
 }
 
 func (d *divider) DragEnd() {
+	d.startDragOff = nil
 }
 
-func (d *divider) Dragged(event *fyne.DragEvent) {
-	offset := d.split.Offset
+func (d *divider) Dragged(e *fyne.DragEvent) {
+	if d.startDragOff == nil {
+		d.currentDragPos = d.Position().Add(e.Position)
+		start := e.Position.Subtract(e.Dragged)
+		d.startDragOff = &start
+	} else {
+		d.currentDragPos = d.currentDragPos.Add(e.Dragged)
+	}
+
+	x, y := d.currentDragPos.Components()
+	var offset, leadingRatio, trailingRatio float64
 	if d.split.Horizontal {
-		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)
+		widthFree := float64(d.split.Size().Width - dividerThickness())
+		leadingRatio = float64(d.split.Leading.MinSize().Width) / widthFree
+		trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Width) / widthFree)
+		offset = float64(x-d.startDragOff.X) / widthFree
 	} else {
-		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)
+		heightFree := float64(d.split.Size().Height - dividerThickness())
+		leadingRatio = float64(d.split.Leading.MinSize().Height) / heightFree
+		trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Height) / heightFree)
+		offset = float64(y-d.startDragOff.Y) / heightFree
+	}
+
+	if offset < leadingRatio {
+		offset = leadingRatio
+	}
+	if offset > trailingRatio {
+		offset = trailingRatio
 	}
 	d.split.SetOffset(offset)
 }

+ 47 - 36
vendor/fyne.io/fyne/v2/container/tabs.go

@@ -209,7 +209,7 @@ func selectItem(t baseTabs, item *TabItem) {
 }
 
 func setItems(t baseTabs, items []*TabItem) {
-	if mismatchedTabItems(items) {
+	if internal.HintsEnabled && mismatchedTabItems(items) {
 		internal.LogHint("Tab items should all have the same type of content (text, icons or both)")
 	}
 	t.setItems(items)
@@ -303,6 +303,7 @@ func (r *baseTabsRenderer) applyTheme(t baseTabs) {
 	}
 	r.divider.FillColor = theme.ShadowColor()
 	r.indicator.FillColor = theme.PrimaryColor()
+	r.indicator.CornerRadius = theme.SelectionRadiusSize()
 }
 
 func (r *baseTabsRenderer) layout(t baseTabs, size fyne.Size) {
@@ -313,39 +314,40 @@ func (r *baseTabsRenderer) layout(t baseTabs, size fyne.Size) {
 
 	barMin := r.bar.MinSize()
 
+	padding := theme.Padding()
 	switch t.tabLocation() {
 	case TabLocationTop:
 		barHeight := barMin.Height
 		barPos = fyne.NewPos(0, 0)
 		barSize = fyne.NewSize(size.Width, barHeight)
 		dividerPos = fyne.NewPos(0, barHeight)
-		dividerSize = fyne.NewSize(size.Width, theme.Padding())
-		contentPos = fyne.NewPos(0, barHeight+theme.Padding())
-		contentSize = fyne.NewSize(size.Width, size.Height-barHeight-theme.Padding())
+		dividerSize = fyne.NewSize(size.Width, padding)
+		contentPos = fyne.NewPos(0, barHeight+padding)
+		contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding)
 	case TabLocationLeading:
 		barWidth := barMin.Width
 		barPos = fyne.NewPos(0, 0)
 		barSize = fyne.NewSize(barWidth, size.Height)
 		dividerPos = fyne.NewPos(barWidth, 0)
-		dividerSize = fyne.NewSize(theme.Padding(), size.Height)
+		dividerSize = fyne.NewSize(padding, size.Height)
 		contentPos = fyne.NewPos(barWidth+theme.Padding(), 0)
-		contentSize = fyne.NewSize(size.Width-barWidth-theme.Padding(), size.Height)
+		contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height)
 	case TabLocationBottom:
 		barHeight := barMin.Height
 		barPos = fyne.NewPos(0, size.Height-barHeight)
 		barSize = fyne.NewSize(size.Width, barHeight)
-		dividerPos = fyne.NewPos(0, size.Height-barHeight-theme.Padding())
-		dividerSize = fyne.NewSize(size.Width, theme.Padding())
+		dividerPos = fyne.NewPos(0, size.Height-barHeight-padding)
+		dividerSize = fyne.NewSize(size.Width, padding)
 		contentPos = fyne.NewPos(0, 0)
-		contentSize = fyne.NewSize(size.Width, size.Height-barHeight-theme.Padding())
+		contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding)
 	case TabLocationTrailing:
 		barWidth := barMin.Width
 		barPos = fyne.NewPos(size.Width-barWidth, 0)
 		barSize = fyne.NewSize(barWidth, size.Height)
-		dividerPos = fyne.NewPos(size.Width-barWidth-theme.Padding(), 0)
-		dividerSize = fyne.NewSize(theme.Padding(), size.Height)
+		dividerPos = fyne.NewPos(size.Width-barWidth-padding, 0)
+		dividerSize = fyne.NewSize(padding, size.Height)
 		contentPos = fyne.NewPos(0, 0)
-		contentSize = fyne.NewSize(size.Width-barWidth-theme.Padding(), size.Height)
+		contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height)
 	}
 
 	r.bar.Move(barPos)
@@ -430,7 +432,7 @@ func (r *baseTabsRenderer) moveIndicator(pos fyne.Position, siz fyne.Size, anima
 	r.lastIndicatorHidden = r.indicator.Hidden
 	r.lastIndicatorMutex.Unlock()
 
-	if animate {
+	if animate && fyne.CurrentApp().Settings().ShowAnimations() {
 		r.positionAnimation = canvas.NewPositionAnimation(r.indicator.Position(), pos, canvas.DurationShort, func(p fyne.Position) {
 			r.indicator.Move(p)
 			r.indicator.Refresh()
@@ -489,7 +491,7 @@ type tabButton struct {
 	hovered       bool
 	icon          fyne.Resource
 	iconPosition  buttonIconPosition
-	importance    widget.ButtonImportance
+	importance    widget.Importance
 	onTapped      func()
 	onClosed      func()
 	text          string
@@ -499,6 +501,7 @@ type tabButton struct {
 func (b *tabButton) CreateRenderer() fyne.WidgetRenderer {
 	b.ExtendBaseWidget(b)
 	background := canvas.NewRectangle(theme.HoverColor())
+	background.CornerRadius = theme.SelectionRadiusSize()
 	background.Hide()
 	icon := canvas.NewImageFromResource(b.icon)
 	if b.icon == nil {
@@ -577,15 +580,16 @@ func (r *tabButtonRenderer) Layout(size fyne.Size) {
 	innerOffset := fyne.NewPos(padding.Width/2, padding.Height/2)
 	labelShift := float32(0)
 	if r.icon.Visible() {
+		iconSize := r.iconSize()
 		var iconOffset fyne.Position
 		if r.button.iconPosition == buttonIconTop {
-			iconOffset = fyne.NewPos((innerSize.Width-r.iconSize())/2, 0)
+			iconOffset = fyne.NewPos((innerSize.Width-iconSize)/2, 0)
 		} else {
-			iconOffset = fyne.NewPos(0, (innerSize.Height-r.iconSize())/2)
+			iconOffset = fyne.NewPos(0, (innerSize.Height-iconSize)/2)
 		}
-		r.icon.Resize(fyne.NewSize(r.iconSize(), r.iconSize()))
+		r.icon.Resize(fyne.NewSquareSize(iconSize))
 		r.icon.Move(innerOffset.Add(iconOffset))
-		labelShift = r.iconSize() + theme.Padding()
+		labelShift = iconSize + theme.Padding()
 	}
 	if r.label.Text != "" {
 		var labelOffset fyne.Position
@@ -600,39 +604,43 @@ func (r *tabButtonRenderer) Layout(size fyne.Size) {
 		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()))
+	inlineIconSize := theme.IconInlineSize()
+	r.close.Move(fyne.NewPos(size.Width-inlineIconSize-theme.Padding(), (size.Height-inlineIconSize)/2))
+	r.close.Resize(fyne.NewSquareSize(inlineIconSize))
 }
 
 func (r *tabButtonRenderer) MinSize() fyne.Size {
 	var contentWidth, contentHeight float32
 	textSize := r.label.MinSize()
+	iconSize := r.iconSize()
+	padding := theme.Padding()
 	if r.button.iconPosition == buttonIconTop {
-		contentWidth = fyne.Max(textSize.Width, r.iconSize())
+		contentWidth = fyne.Max(textSize.Width, iconSize)
 		if r.icon.Visible() {
-			contentHeight += r.iconSize()
+			contentHeight += iconSize
 		}
 		if r.label.Text != "" {
 			if r.icon.Visible() {
-				contentHeight += theme.Padding()
+				contentHeight += padding
 			}
 			contentHeight += textSize.Height
 		}
 	} else {
-		contentHeight = fyne.Max(textSize.Height, r.iconSize())
+		contentHeight = fyne.Max(textSize.Height, iconSize)
 		if r.icon.Visible() {
-			contentWidth += r.iconSize()
+			contentWidth += iconSize
 		}
 		if r.label.Text != "" {
 			if r.icon.Visible() {
-				contentWidth += theme.Padding()
+				contentWidth += padding
 			}
 			contentWidth += textSize.Width
 		}
 	}
 	if r.button.onClosed != nil {
-		contentWidth += theme.IconInlineSize() + theme.Padding()
-		contentHeight = fyne.Max(contentHeight, theme.IconInlineSize())
+		inlineIconSize := theme.IconInlineSize()
+		contentWidth += inlineIconSize + padding
+		contentHeight = fyne.Max(contentHeight, inlineIconSize)
 	}
 	return fyne.NewSize(contentWidth, contentHeight).Add(r.padding())
 }
@@ -644,6 +652,7 @@ func (r *tabButtonRenderer) Objects() []fyne.CanvasObject {
 func (r *tabButtonRenderer) Refresh() {
 	if r.button.hovered && !r.button.Disabled() {
 		r.background.FillColor = theme.HoverColor()
+		r.background.CornerRadius = theme.SelectionRadiusSize()
 		r.background.Show()
 	} else {
 		r.background.Hide()
@@ -659,7 +668,7 @@ func (r *tabButtonRenderer) Refresh() {
 			r.label.Color = theme.ForegroundColor()
 		}
 	} else {
-		r.label.Color = theme.DisabledTextColor()
+		r.label.Color = theme.DisabledColor()
 	}
 	r.label.TextSize = theme.TextSize()
 	if r.button.text == "" {
@@ -698,19 +707,19 @@ func (r *tabButtonRenderer) Refresh() {
 }
 
 func (r *tabButtonRenderer) iconSize() float32 {
-	switch r.button.iconPosition {
-	case buttonIconTop:
+	if r.button.iconPosition == buttonIconTop {
 		return 2 * theme.IconInlineSize()
-	default:
-		return theme.IconInlineSize()
 	}
+
+	return theme.IconInlineSize()
 }
 
 func (r *tabButtonRenderer) padding() fyne.Size {
+	padding := theme.InnerPadding()
 	if r.label.Text != "" && r.button.iconPosition == buttonIconInline {
-		return fyne.NewSize(theme.InnerPadding()*2, theme.InnerPadding()*2)
+		return fyne.NewSquareSize(padding * 2)
 	}
-	return fyne.NewSize(theme.InnerPadding(), theme.InnerPadding()*2)
+	return fyne.NewSize(padding, padding*2)
 }
 
 var _ fyne.Widget = (*tabCloseButton)(nil)
@@ -727,6 +736,7 @@ type tabCloseButton struct {
 func (b *tabCloseButton) CreateRenderer() fyne.WidgetRenderer {
 	b.ExtendBaseWidget(b)
 	background := canvas.NewRectangle(theme.HoverColor())
+	background.CornerRadius = theme.SelectionRadiusSize()
 	background.Hide()
 	icon := canvas.NewImageFromResource(theme.CancelIcon())
 
@@ -778,7 +788,7 @@ func (r *tabCloseButtonRenderer) Layout(size fyne.Size) {
 }
 
 func (r *tabCloseButtonRenderer) MinSize() fyne.Size {
-	return fyne.NewSize(theme.IconInlineSize(), theme.IconInlineSize())
+	return fyne.NewSquareSize(theme.IconInlineSize())
 }
 
 func (r *tabCloseButtonRenderer) Objects() []fyne.CanvasObject {
@@ -788,6 +798,7 @@ func (r *tabCloseButtonRenderer) Objects() []fyne.CanvasObject {
 func (r *tabCloseButtonRenderer) Refresh() {
 	if r.button.hovered {
 		r.background.FillColor = theme.HoverColor()
+		r.background.CornerRadius = theme.SelectionRadiusSize()
 		r.background.Show()
 	} else {
 		r.background.Hide()

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

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

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

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

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

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

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

@@ -7,4 +7,9 @@ import "fyne.io/fyne/v2"
 type Driver interface {
 	// Create a new borderless window that is centered on screen
 	CreateSplashWindow() fyne.Window
+
+	// Gets the set of key modifiers that are currently active
+	//
+	// Since: 2.4
+	CurrentKeyModifiers() fyne.KeyModifier
 }

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

@@ -0,0 +1,10 @@
+// Package mobile provides desktop specific mobile functionality.
+package mobile
+
+// Driver represents the extended capabilities of a mobile driver
+//
+// Since: 2.4
+type Driver interface {
+	// GoBack asks the OS to go to the previous app / activity, where supported
+	GoBack()
+}

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

@@ -0,0 +1,10 @@
+package mobile
+
+import (
+	"fyne.io/fyne/v2"
+)
+
+const (
+	// KeyBack represents the back button which may be hardware or software
+	KeyBack fyne.KeyName = "Back"
+)

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

@@ -42,9 +42,17 @@ func NewPos(x float32, y float32) Position {
 	return Position{x, y}
 }
 
+// NewSquareOffsetPos returns a newly allocated Position with the same x and y position.
+//
+// Since: 2.4
+func NewSquareOffsetPos(length float32) Position {
+	return Position{length, length}
+}
+
 // 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 {
+	// NOTE: Do not simplify to `return p.AddXY(v.Components())`, it prevents inlining.
 	x, y := v.Components()
 	return Position{p.X + x, p.Y + y}
 }
@@ -67,6 +75,7 @@ func (p Position) IsZero() bool {
 // 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 {
+	// NOTE: Do not simplify to `return p.SubtractXY(v.Components())`, it prevents inlining.
 	x, y := v.Components()
 	return Position{p.X - x, p.Y - y}
 }
@@ -87,9 +96,17 @@ func NewSize(w float32, h float32) Size {
 	return Size{w, h}
 }
 
+// NewSquareSize returns a newly allocated Size with the same width and height.
+//
+// Since: 2.4
+func NewSquareSize(side float32) Size {
+	return Size{side, side}
+}
+
 // 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 {
+	// NOTE: Do not simplify to `return s.AddXY(v.Components())`, it prevents inlining.
 	w, h := v.Components()
 	return Size{s.Width + w, s.Height + h}
 }
@@ -132,6 +149,7 @@ func (s Size) Components() (float32, float32) {
 // 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 {
+	// NOTE: Do not simplify to `return s.SubtractXY(v.Components())`, it prevents inlining.
 	w, h := v.Components()
 	return Size{s.Width - w, s.Height - h}
 }

+ 2 - 2
vendor/fyne.io/fyne/v2/internal/app/focus_manager.go

@@ -39,7 +39,7 @@ func (f *FocusManager) Focus(obj fyne.Focusable) bool {
 				}
 				return false
 			},
-			func(object, _ fyne.CanvasObject) {
+			func(object fyne.CanvasObject, pos fyne.Position, _ fyne.CanvasObject) {
 				if hiddenAncestor == object {
 					hiddenAncestor = nil
 				}
@@ -158,5 +158,5 @@ func (f *FocusManager) previousInChain(current fyne.Focusable) fyne.Focusable {
 type walkerFunc func(
 	fyne.CanvasObject,
 	func(fyne.CanvasObject, fyne.Position, fyne.Position, fyne.Size) bool,
-	func(fyne.CanvasObject, fyne.CanvasObject),
+	func(fyne.CanvasObject, fyne.Position, fyne.CanvasObject),
 ) bool

+ 13 - 1
vendor/fyne.io/fyne/v2/internal/app/lifecycle.go

@@ -16,6 +16,14 @@ type Lifecycle struct {
 	onBackground atomic.Value // func()
 	onStarted    atomic.Value // func()
 	onStopped    atomic.Value // func()
+
+	onStoppedHookExecuted func()
+}
+
+// SetOnStoppedHookExecuted is an internal function that lets Fyne schedule a clean-up after
+// the user-provided stopped hook. It should only be called once during an application start-up.
+func (l *Lifecycle) SetOnStoppedHookExecuted(f func()) {
+	l.onStoppedHookExecuted = f
 }
 
 // SetOnEnteredForeground hooks into the the app becoming foreground.
@@ -64,10 +72,14 @@ func (l *Lifecycle) TriggerStarted() {
 	}
 }
 
-// TriggerStopped will call the stopped hook, if one is registered.
+// TriggerStopped will call the stopped hook, if one is registered,
+// and an internal stopped hook after that.
 func (l *Lifecycle) TriggerStopped() {
 	f := l.onStopped.Load()
 	if ff, ok := f.(func()); ok && ff != nil {
 		ff()
 	}
+	if l.onStoppedHookExecuted != nil {
+		l.onStoppedHookExecuted()
+	}
 }

+ 1 - 0
vendor/fyne.io/fyne/v2/internal/cache/base.go

@@ -45,6 +45,7 @@ func Clean(canvasRefreshed bool) {
 		return
 	}
 	destroyExpiredSvgs(now)
+	destroyExpiredFontMetrics(now)
 	if canvasRefreshed {
 		// Destroy renderers on canvas refresh to avoid flickering screen.
 		destroyExpiredRenderers(now)

+ 8 - 2
vendor/fyne.io/fyne/v2/internal/cache/canvases.go

@@ -22,12 +22,18 @@ func GetCanvasForObject(obj fyne.CanvasObject) fyne.Canvas {
 }
 
 // SetCanvasForObject sets the canvas for the specified object.
-func SetCanvasForObject(obj fyne.CanvasObject, canvas fyne.Canvas) {
-	cinfo := &canvasInfo{canvas: canvas}
+// The passed function will be called if the item was not previously attached to this canvas
+func SetCanvasForObject(obj fyne.CanvasObject, c fyne.Canvas, setup func()) {
+	cinfo := &canvasInfo{canvas: c}
 	cinfo.setAlive()
 	canvasesLock.Lock()
+	old, found := canvases[obj]
 	canvases[obj] = cinfo
 	canvasesLock.Unlock()
+
+	if (!found || old.canvas != c) && setup != nil {
+		setup()
+	}
 }
 
 type canvasInfo struct {

+ 3 - 1
vendor/fyne.io/fyne/v2/internal/cache/svg.go

@@ -35,7 +35,9 @@ func SetSvg(name string, pix *image.NRGBA, w int, h int) {
 }
 
 type svgInfo struct {
-	expiringCacheNoLock
+	// An svgInfo can be accessed from different goroutines, e.g., systray.
+	// Use expiringCache instead of expiringCacheNoLock.
+	expiringCache
 	pix  *image.NRGBA
 	w, h int
 }

+ 24 - 1
vendor/fyne.io/fyne/v2/internal/cache/text.go

@@ -2,6 +2,7 @@ package cache
 
 import (
 	"sync"
+	"time"
 
 	"fyne.io/fyne/v2"
 )
@@ -12,6 +13,7 @@ var (
 )
 
 type fontMetric struct {
+	expiringCache
 	size     fyne.Size
 	baseLine float32
 }
@@ -31,13 +33,34 @@ func GetFontMetrics(text string, fontSize float32, style fyne.TextStyle) (size f
 	if !ok {
 		return fyne.Size{Width: 0, Height: 0}, 0
 	}
+	ret.setAlive()
 	return ret.size, ret.baseLine
 }
 
 // SetFontMetrics stores a calculated font size and baseline for parameters that were missing from the cache.
 func SetFontMetrics(text string, fontSize float32, style fyne.TextStyle, size fyne.Size, base float32) {
 	ent := fontSizeEntry{text, fontSize, style}
+	metric := fontMetric{size: size, baseLine: base}
+	metric.setAlive()
 	fontSizeLock.Lock()
-	fontSizeCache[ent] = fontMetric{size: size, baseLine: base}
+	fontSizeCache[ent] = metric
+	fontSizeLock.Unlock()
+}
+
+// destroyExpiredFontMetrics destroys expired fontSizeCache entries
+func destroyExpiredFontMetrics(now time.Time) {
+	expiredObjs := make([]fontSizeEntry, 0, 50)
+	fontSizeLock.RLock()
+	for k, v := range fontSizeCache {
+		if v.isExpired(now) {
+			expiredObjs = append(expiredObjs, k)
+		}
+	}
+	fontSizeLock.RUnlock()
+
+	fontSizeLock.Lock()
+	for _, k := range expiredObjs {
+		delete(fontSizeCache, k)
+	}
 	fontSizeLock.Unlock()
 }

+ 4 - 2
vendor/fyne.io/fyne/v2/internal/clip.go

@@ -15,8 +15,10 @@ func (c *ClipStack) Pop() *ClipItem {
 		return nil
 	}
 
-	ret := c.clips[len(c.clips)-1]
-	c.clips = c.clips[:len(c.clips)-1]
+	top := len(c.clips) - 1
+	ret := c.clips[top]
+	c.clips[top] = nil // release memory reference
+	c.clips = c.clips[:top]
 	return ret
 }
 

+ 100 - 25
vendor/fyne.io/fyne/v2/internal/driver/common/canvas.go

@@ -1,10 +1,13 @@
 package common
 
 import (
+	"image/color"
+	"reflect"
 	"sync"
 	"sync/atomic"
 
 	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/canvas"
 	"fyne.io/fyne/v2/internal"
 	"fyne.io/fyne/v2/internal/app"
 	"fyne.io/fyne/v2/internal/async"
@@ -56,6 +59,27 @@ func (c *Canvas) AddShortcut(shortcut fyne.Shortcut, handler func(shortcut fyne.
 	c.shortcut.AddShortcut(shortcut, handler)
 }
 
+func (c *Canvas) DrawDebugOverlay(obj fyne.CanvasObject, pos fyne.Position, size fyne.Size) {
+	switch obj.(type) {
+	case fyne.Widget:
+		r := canvas.NewRectangle(color.Transparent)
+		r.StrokeColor = color.NRGBA{R: 0xcc, G: 0x33, B: 0x33, A: 0xff}
+		r.StrokeWidth = 1
+		r.Resize(obj.Size())
+		c.Painter().Paint(r, pos, size)
+
+		t := canvas.NewText(reflect.ValueOf(obj).Elem().Type().Name(), r.StrokeColor)
+		t.TextSize = 10
+		c.Painter().Paint(t, pos.AddXY(2, 2), size)
+	case *fyne.Container:
+		r := canvas.NewRectangle(color.Transparent)
+		r.StrokeColor = color.NRGBA{R: 0x33, G: 0x33, B: 0xcc, A: 0xff}
+		r.StrokeWidth = 1
+		r.Resize(obj.Size())
+		c.Painter().Paint(r, pos, size)
+	}
+}
+
 // EnsureMinSize ensure canvas min size.
 //
 // This function uses lock.
@@ -63,53 +87,66 @@ func (c *Canvas) EnsureMinSize() bool {
 	if c.impl.Content() == nil {
 		return false
 	}
-	var lastParent fyne.CanvasObject
-
 	windowNeedsMinSizeUpdate := false
 	csize := c.impl.Size()
 	min := c.impl.MinSize()
 
-	ensureMinSize := func(node *RenderCacheNode) {
+	c.RLock()
+	defer c.RUnlock()
+
+	var parentNeedingUpdate *RenderCacheNode
+
+	ensureMinSize := func(node *RenderCacheNode, pos fyne.Position) {
 		obj := node.obj
-		cache.SetCanvasForObject(obj, c.impl)
+		cache.SetCanvasForObject(obj, c.impl, func() {
+			if img, ok := obj.(*canvas.Image); ok {
+				c.RUnlock()
+				img.Refresh() // this may now have a different texScale
+				c.RLock()
+			}
+		})
+
+		if parentNeedingUpdate == node {
+			c.updateLayout(obj)
+			parentNeedingUpdate = nil
+		}
 
+		c.RUnlock()
 		if !obj.Visible() {
+			c.RLock()
 			return
 		}
 		minSize := obj.MinSize()
+		c.RLock()
+
 		minSizeChanged := node.minSize != minSize
 		if minSizeChanged {
-			objToLayout := obj
 			node.minSize = minSize
 			if node.parent != nil {
-				objToLayout = node.parent.obj
+				parentNeedingUpdate = node.parent
 			} else {
 				windowNeedsMinSizeUpdate = true
+				c.RUnlock()
 				size := obj.Size()
+				c.RLock()
 				expectedSize := minSize.Max(size)
 				if expectedSize != size && size != csize {
-					objToLayout = nil
+					c.RUnlock()
 					obj.Resize(expectedSize)
+					c.RLock()
+				} else {
+					c.updateLayout(obj)
 				}
 			}
-
-			if objToLayout != lastParent {
-				updateLayout(lastParent)
-				lastParent = objToLayout
-			}
 		}
 	}
 	c.WalkTrees(nil, ensureMinSize)
 
 	shouldResize := windowNeedsMinSizeUpdate && (csize.Width < min.Width || csize.Height < min.Height)
 	if shouldResize {
+		c.RUnlock()
 		c.impl.Resize(csize.Max(min))
-	}
-
-	if lastParent != nil {
 		c.RLock()
-		updateLayout(lastParent)
-		c.RUnlock()
 	}
 	return windowNeedsMinSizeUpdate
 }
@@ -196,12 +233,25 @@ func (c *Canvas) FocusPrevious() {
 func (c *Canvas) FreeDirtyTextures() (freed uint64) {
 	freeObject := func(object fyne.CanvasObject) {
 		freeWalked := func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool {
+			// No image refresh while recursing to avoid double texture upload.
+			if _, ok := obj.(*canvas.Image); ok {
+				return false
+			}
 			if c.painter != nil {
 				c.painter.Free(obj)
 			}
 			return false
 		}
-		driver.WalkCompleteObjectTree(object, freeWalked, nil)
+
+		// Image.Refresh will trigger a refresh specific to the object, while recursing on parent widget would just lead to
+		// a double texture upload.
+		if img, ok := object.(*canvas.Image); ok {
+			if c.painter != nil {
+				c.painter.Free(img)
+			}
+		} else {
+			driver.WalkCompleteObjectTree(object, freeWalked, nil)
+		}
 	}
 
 	// Within a frame, refresh tasks are requested from the Refresh method,
@@ -288,6 +338,23 @@ func (c *Canvas) Painter() gl.Painter {
 
 // Refresh refreshes a canvas object.
 func (c *Canvas) Refresh(obj fyne.CanvasObject) {
+	walkNeeded := false
+	switch obj.(type) {
+	case *fyne.Container:
+		walkNeeded = true
+	case fyne.Widget:
+		walkNeeded = true
+	}
+
+	if walkNeeded {
+		driver.WalkCompleteObjectTree(obj, func(co fyne.CanvasObject, p1, p2 fyne.Position, s fyne.Size) bool {
+			if i, ok := co.(*canvas.Image); ok {
+				i.Refresh()
+			}
+			return false
+		}, nil)
+	}
+
 	c.refreshQueue.In(obj)
 	c.SetDirty()
 }
@@ -366,7 +433,7 @@ func (c *Canvas) Unfocus() {
 // WalkTrees walks over the trees.
 func (c *Canvas) WalkTrees(
 	beforeChildren func(*RenderCacheNode, fyne.Position),
-	afterChildren func(*RenderCacheNode),
+	afterChildren func(*RenderCacheNode, fyne.Position),
 ) {
 	c.walkTree(c.contentTree, beforeChildren, afterChildren)
 	if c.mWindowHeadTree != nil && c.mWindowHeadTree.root.obj != nil {
@@ -408,7 +475,7 @@ func (c *Canvas) isMenuActive() bool {
 func (c *Canvas) walkTree(
 	tree *renderCacheTree,
 	beforeChildren func(*RenderCacheNode, fyne.Position),
-	afterChildren func(*RenderCacheNode),
+	afterChildren func(*RenderCacheNode, fyne.Position),
 ) {
 	tree.Lock()
 	defer tree.Unlock()
@@ -442,7 +509,7 @@ func (c *Canvas) walkTree(
 		node = parent.firstChild
 		return false
 	}
-	ac := func(obj fyne.CanvasObject, _ fyne.CanvasObject) {
+	ac := func(obj fyne.CanvasObject, pos fyne.Position, _ fyne.CanvasObject) {
 		node = parent
 		parent = node.parent
 		if prev != nil && prev.parent != parent {
@@ -450,7 +517,7 @@ func (c *Canvas) walkTree(
 		}
 
 		if afterChildren != nil {
-			afterChildren(node)
+			afterChildren(node, pos)
 		}
 
 		prev = node
@@ -517,6 +584,7 @@ func (o *overlayStack) add(overlay fyne.CanvasObject) {
 func (o *overlayStack) remove(overlay fyne.CanvasObject) {
 	o.OverlayStack.Remove(overlay)
 	overlayCount := len(o.List())
+	o.renderCaches[overlayCount] = nil // release memory reference to removed element
 	o.renderCaches = o.renderCaches[:overlayCount]
 }
 
@@ -525,13 +593,20 @@ type renderCacheTree struct {
 	root *RenderCacheNode
 }
 
-func updateLayout(objToLayout fyne.CanvasObject) {
+func (c *Canvas) updateLayout(objToLayout fyne.CanvasObject) {
 	switch cont := objToLayout.(type) {
 	case *fyne.Container:
 		if cont.Layout != nil {
-			cont.Layout.Layout(cont.Objects, cont.Size())
+			layout := cont.Layout
+			objects := cont.Objects
+			c.RUnlock()
+			layout.Layout(objects, cont.Size())
+			c.RLock()
 		}
 	case fyne.Widget:
-		cache.Renderer(cont).Layout(cont.Size())
+		renderer := cache.Renderer(cont)
+		c.RUnlock()
+		renderer.Layout(cont.Size())
+		c.RLock()
 	}
 }

+ 3 - 3
vendor/fyne.io/fyne/v2/internal/driver/common/window.go

@@ -38,14 +38,14 @@ func (w *Window) RunEventQueue() {
 
 // WaitForEvents wait for all the events.
 func (w *Window) WaitForEvents() {
-	done := donePool.Get().(chan struct{})
-	defer donePool.Put(done)
+	done := DonePool.Get().(chan struct{})
+	defer DonePool.Put(done)
 
 	w.eventQueue.In() <- func() { done <- struct{}{} }
 	<-done
 }
 
-var donePool = sync.Pool{
+var DonePool = sync.Pool{
 	New: func() interface{} {
 		return make(chan struct{})
 	},

+ 31 - 25
vendor/fyne.io/fyne/v2/internal/driver/glfw/canvas.go

@@ -20,10 +20,10 @@ var _ fyne.Canvas = (*glCanvas)(nil)
 type glCanvas struct {
 	common.Canvas
 
-	content fyne.CanvasObject
-	menu    fyne.CanvasObject
-	padded  bool
-	size    fyne.Size
+	content       fyne.CanvasObject
+	menu          fyne.CanvasObject
+	padded, debug bool
+	size          fyne.Size
 
 	onTypedRune func(rune)
 	onTypedKey  func(*fyne.KeyEvent)
@@ -125,14 +125,20 @@ func (c *glCanvas) Resize(size fyne.Size) {
 	}
 
 	c.RLock()
-	c.content.Resize(c.contentSize(nearestSize))
-	c.content.Move(c.contentPos())
+	content := c.content
+	contentSize := c.contentSize(nearestSize)
+	contentPos := c.contentPos()
+	menu := c.menu
+	menuHeight := c.menuHeight()
+	c.RUnlock()
 
-	if c.menu != nil {
-		c.menu.Refresh()
-		c.menu.Resize(fyne.NewSize(nearestSize.Width, c.menu.MinSize().Height))
+	content.Resize(contentSize)
+	content.Move(contentPos)
+
+	if menu != nil {
+		menu.Refresh()
+		menu.Resize(fyne.NewSize(nearestSize.Width, menuHeight))
 	}
-	c.RUnlock()
 }
 
 func (c *glCanvas) Scale() float32 {
@@ -191,7 +197,7 @@ func (c *glCanvas) reloadScale() {
 	}
 
 	c.Lock()
-	c.scale = c.context.(*window).calculatedScale()
+	c.scale = w.calculatedScale()
 	c.Unlock()
 	c.SetDirty()
 
@@ -231,8 +237,7 @@ func (c *glCanvas) buildMenu(w *window, m *fyne.MainMenu) {
 func (c *glCanvas) canvasSize(contentSize fyne.Size) fyne.Size {
 	canvasSize := contentSize.Add(fyne.NewSize(0, c.menuHeight()))
 	if c.Padded() {
-		pad := theme.Padding() * 2
-		canvasSize = canvasSize.Add(fyne.NewSize(pad, pad))
+		return canvasSize.Add(fyne.NewSquareSize(theme.Padding() * 2))
 	}
 	return canvasSize
 }
@@ -240,7 +245,7 @@ func (c *glCanvas) canvasSize(contentSize fyne.Size) fyne.Size {
 func (c *glCanvas) contentPos() fyne.Position {
 	contentPos := fyne.NewPos(0, c.menuHeight())
 	if c.Padded() {
-		contentPos = contentPos.Add(fyne.NewPos(theme.Padding(), theme.Padding()))
+		return contentPos.Add(fyne.NewSquareOffsetPos(theme.Padding()))
 	}
 	return contentPos
 }
@@ -248,20 +253,17 @@ func (c *glCanvas) contentPos() fyne.Position {
 func (c *glCanvas) contentSize(canvasSize fyne.Size) fyne.Size {
 	contentSize := fyne.NewSize(canvasSize.Width, canvasSize.Height-c.menuHeight())
 	if c.Padded() {
-		pad := theme.Padding() * 2
-		contentSize = contentSize.Subtract(fyne.NewSize(pad, pad))
+		return contentSize.Subtract(fyne.NewSquareSize(theme.Padding() * 2))
 	}
 	return contentSize
 }
 
 func (c *glCanvas) menuHeight() float32 {
-	switch c.menu {
-	case nil:
-		// no menu or native menu -> does not consume space on the canvas
-		return 0
-	default:
-		return c.menu.MinSize().Height
+	if c.menu == nil {
+		return 0 // no menu or native menu -> does not consume space on the canvas
 	}
+
+	return c.menu.MinSize().Height
 }
 
 func (c *glCanvas) overlayChanged() {
@@ -286,7 +288,7 @@ func (c *glCanvas) paint(size fyne.Size) {
 		}
 		c.Painter().Paint(obj, pos, size)
 	}
-	afterPaint := func(node *common.RenderCacheNode) {
+	afterPaint := func(node *common.RenderCacheNode, pos fyne.Position) {
 		if _, ok := node.Obj().(fyne.Scrollable); ok {
 			clips.Pop()
 			if top := clips.Top(); top != nil {
@@ -295,6 +297,10 @@ func (c *glCanvas) paint(size fyne.Size) {
 				c.Painter().StopClipping()
 			}
 		}
+
+		if c.debug {
+			c.DrawDebugOverlay(node.Obj(), pos, size)
+		}
 	}
 	c.WalkTrees(paint, afterPaint)
 }
@@ -330,9 +336,9 @@ func (c *glCanvas) applyThemeOutOfTreeObjects() {
 }
 
 func newCanvas() *glCanvas {
-	c := &glCanvas{scale: 1.0, texScale: 1.0}
+	c := &glCanvas{scale: 1.0, texScale: 1.0, padded: true}
 	c.Initialize(c, c.overlayChanged)
 	c.setContent(&canvas.Rectangle{FillColor: theme.BackgroundColor()})
-	c.padded = true
+	c.debug = fyne.CurrentApp().Settings().BuildType() == fyne.BuildDebug
 	return c
 }

+ 15 - 27
vendor/fyne.io/fyne/v2/internal/driver/glfw/driver.go

@@ -6,10 +6,8 @@ import (
 	"bytes"
 	"image"
 	"os"
-	"os/signal"
 	"runtime"
 	"sync"
-	"syscall"
 
 	"github.com/fyne-io/image/ico"
 
@@ -29,15 +27,13 @@ import (
 // influence of a garbage collector.
 var mainGoroutineID uint64
 
-var (
-	curWindow *window
-	isWayland = false
-)
+var curWindow *window
 
 // Declare conformity with Driver
 var _ fyne.Driver = (*gLDriver)(nil)
 
-var drawOnMainThread bool // A workaround on Apple M1, just use 1 thread until fixed upstream
+// A workaround on Apple M1/M2, just use 1 thread until fixed upstream.
+const drawOnMainThread bool = runtime.GOOS == "darwin" && runtime.GOARCH == "arm64"
 
 type gLDriver struct {
 	windowLock sync.RWMutex
@@ -48,6 +44,8 @@ type gLDriver struct {
 
 	animation *animation.Runner
 
+	currentKeyModifiers fyne.KeyModifier // desktop driver only
+
 	trayStart, trayStop func()     // shut down the system tray, if used
 	systrayMenu         *fyne.Menu // cache the menu set so we know when to refresh
 }
@@ -150,12 +148,13 @@ func (d *gLDriver) windowList() []fyne.Window {
 func (d *gLDriver) initFailed(msg string, err error) {
 	logError(msg, err)
 
-	run.Lock()
-	if !run.flag {
-		run.Unlock()
+	run.L.Lock()
+	running := !run.flag
+	run.L.Unlock()
+
+	if running {
 		d.Quit()
 	} else {
-		run.Unlock()
 		os.Exit(1)
 	}
 }
@@ -165,28 +164,17 @@ func (d *gLDriver) Run() {
 		panic("Run() or ShowAndRun() must be called from main goroutine")
 	}
 
-	go catchTerm(d)
+	go d.catchTerm()
 	d.runGL()
 }
 
 // NewGLDriver sets up a new Driver instance implemented using the GLFW Go library and OpenGL bindings.
 func NewGLDriver() fyne.Driver {
-	d := new(gLDriver)
-	d.done = make(chan interface{})
-	d.drawDone = make(chan interface{})
-	d.animation = &animation.Runner{}
-
 	repository.Register("file", intRepo.NewFileRepository())
 
-	return d
-}
-
-func catchTerm(d *gLDriver) {
-	terminateSignals := make(chan os.Signal, 1)
-	signal.Notify(terminateSignals, syscall.SIGINT, syscall.SIGTERM)
-
-	for range terminateSignals {
-		d.Quit()
-		break
+	return &gLDriver{
+		done:      make(chan interface{}),
+		drawDone:  make(chan interface{}),
+		animation: &animation.Runner{},
 	}
 }

+ 21 - 11
vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_desktop.go

@@ -6,11 +6,15 @@ package glfw
 import (
 	"bytes"
 	"image/png"
+	"os"
+	"os/signal"
 	"runtime"
 	"sync"
+	"syscall"
 
 	"fyne.io/fyne/v2/canvas"
 	"fyne.io/fyne/v2/internal/painter"
+	"fyne.io/fyne/v2/internal/svg"
 	"fyne.io/systray"
 
 	"fyne.io/fyne/v2"
@@ -39,7 +43,7 @@ func (d *gLDriver) SetSystemTrayMenu(m *fyne.Menu) {
 			} else if fyne.CurrentApp().Icon() != nil {
 				d.SetSystemTrayIcon(fyne.CurrentApp().Icon())
 			} else {
-				d.SetSystemTrayIcon(theme.FyneLogo())
+				d.SetSystemTrayIcon(theme.BrokenImageIcon())
 			}
 
 			// it must be refreshed after init, so an earlier call would have been ineffective
@@ -51,12 +55,8 @@ func (d *gLDriver) SetSystemTrayMenu(m *fyne.Menu) {
 		// the only way we know the app was asked to quit is if this window is asked to close...
 		w := d.CreateWindow("SystrayMonitor")
 		w.(*window).create()
-		w.SetCloseIntercept(func() {
-			d.Quit()
-		})
-		w.SetOnClosed(func() {
-			systray.Quit()
-		})
+		w.SetCloseIntercept(d.Quit)
+		w.SetOnClosed(systray.Quit)
 	})
 
 	d.refreshSystray(m)
@@ -91,7 +91,7 @@ func itemForMenuItem(i *fyne.MenuItem, parent *systray.MenuItem) *systray.MenuIt
 	}
 	if i.Icon != nil {
 		data := i.Icon.Content()
-		if painter.IsResourceSVG(i.Icon) {
+		if svg.IsResourceSVG(i.Icon) {
 			b := &bytes.Buffer{}
 			res := i.Icon
 			if runtime.GOOS == "windows" && isDark() { // windows menus don't match dark mode so invert icons
@@ -161,6 +161,18 @@ func (d *gLDriver) SystemTrayMenu() *fyne.Menu {
 	return d.systrayMenu
 }
 
+func (d *gLDriver) CurrentKeyModifiers() fyne.KeyModifier {
+	return d.currentKeyModifiers
+}
+
+func (d *gLDriver) catchTerm() {
+	terminateSignal := make(chan os.Signal, 1)
+	signal.Notify(terminateSignal, syscall.SIGINT, syscall.SIGTERM)
+
+	<-terminateSignal
+	d.Quit()
+}
+
 func addMissingQuitForMenu(menu *fyne.Menu, d *gLDriver) {
 	var lastItem *fyne.MenuItem
 	if len(menu.Items) > 0 {
@@ -176,9 +188,7 @@ func addMissingQuitForMenu(menu *fyne.Menu, d *gLDriver) {
 	}
 	for _, item := range menu.Items {
 		if item.IsQuit && item.Action == nil {
-			item.Action = func() {
-				d.Quit()
-			}
+			item.Action = d.Quit
 		}
 	}
 }

+ 6 - 0
vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_notwayland.go

@@ -0,0 +1,6 @@
+//go:build !wayland
+// +build !wayland
+
+package glfw
+
+const isWayland = false

+ 1 - 3
vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_wayland.go

@@ -3,6 +3,4 @@
 
 package glfw
 
-func init() {
-	isWayland = true
-}
+const isWayland = true

+ 3 - 0
vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_mobile.go → vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_web.go

@@ -8,3 +8,6 @@ import "fyne.io/fyne/v2"
 func (d *gLDriver) SetSystemTrayMenu(m *fyne.Menu) {
 	// no-op for mobile apps using this driver
 }
+
+func (d *gLDriver) catchTerm() {
+}

+ 20 - 30
vendor/fyne.io/fyne/v2/internal/driver/glfw/loop.go

@@ -6,10 +6,11 @@ import (
 	"time"
 
 	"fyne.io/fyne/v2"
-	"fyne.io/fyne/v2/internal"
 	"fyne.io/fyne/v2/internal/app"
 	"fyne.io/fyne/v2/internal/cache"
+	"fyne.io/fyne/v2/internal/driver/common"
 	"fyne.io/fyne/v2/internal/painter"
+	"fyne.io/fyne/v2/internal/scale"
 )
 
 type funcData struct {
@@ -24,47 +25,35 @@ type drawData struct {
 }
 
 type runFlag struct {
-	sync.Mutex
+	sync.Cond
 	flag bool
-	cond *sync.Cond
 }
 
 // channel for queuing functions on the main thread
 var funcQueue = make(chan funcData)
 var drawFuncQueue = make(chan drawData)
-var run *runFlag
+var run = &runFlag{Cond: sync.Cond{L: &sync.Mutex{}}}
 var initOnce = &sync.Once{}
-var donePool = &sync.Pool{New: func() interface{} {
-	return make(chan struct{})
-}}
-
-func newRun() *runFlag {
-	r := runFlag{}
-	r.cond = sync.NewCond(&r)
-	return &r
-}
 
 // Arrange that main.main runs on main thread.
 func init() {
 	runtime.LockOSThread()
 	mainGoroutineID = goroutineID()
-
-	run = newRun()
 }
 
 // force a function f to run on the main thread
 func runOnMain(f func()) {
 	// If we are on main just execute - otherwise add it to the main queue and wait.
 	// The "running" variable is normally false when we are on the main thread.
-	run.Lock()
-	if !run.flag {
+	run.L.Lock()
+	running := !run.flag
+	run.L.Unlock()
+
+	if running {
 		f()
-		run.Unlock()
 	} else {
-		run.Unlock()
-
-		done := donePool.Get().(chan struct{})
-		defer donePool.Put(done)
+		done := common.DonePool.Get().(chan struct{})
+		defer common.DonePool.Put(done)
 
 		funcQueue <- funcData{f: f, done: done}
 
@@ -78,8 +67,8 @@ func runOnDraw(w *window, f func()) {
 		runOnMain(func() { w.RunWithContext(f) })
 		return
 	}
-	done := donePool.Get().(chan struct{})
-	defer donePool.Put(done)
+	done := common.DonePool.Get().(chan struct{})
+	defer common.DonePool.Put(done)
 
 	drawFuncQueue <- drawData{f: f, win: w, done: done}
 	<-done
@@ -111,10 +100,11 @@ func (d *gLDriver) drawSingleFrame() {
 
 func (d *gLDriver) runGL() {
 	eventTick := time.NewTicker(time.Second / 60)
-	run.Lock()
+
+	run.L.Lock()
 	run.flag = true
-	run.Unlock()
-	run.cond.Broadcast()
+	run.L.Unlock()
+	run.Broadcast()
 
 	d.initGLFW()
 	if d.trayStart != nil {
@@ -196,7 +186,7 @@ func (d *gLDriver) runGL() {
 func (d *gLDriver) repaintWindow(w *window) {
 	canvas := w.canvas
 	w.RunWithContext(func() {
-		if w.canvas.EnsureMinSize() {
+		if canvas.EnsureMinSize() {
 			w.viewLock.Lock()
 			w.shouldExpand = true
 			w.viewLock.Unlock()
@@ -267,8 +257,8 @@ func updateGLContext(w *window) {
 	size := canvas.Size()
 
 	// w.width and w.height are not correct if we are maximised, so figure from canvas
-	winWidth := float32(internal.ScaleInt(canvas, size.Width)) * canvas.texScale
-	winHeight := float32(internal.ScaleInt(canvas, size.Height)) * canvas.texScale
+	winWidth := float32(scale.ToScreenCoordinate(canvas, size.Width)) * canvas.texScale
+	winHeight := float32(scale.ToScreenCoordinate(canvas, size.Height)) * canvas.texScale
 
 	canvas.Painter().SetFrameBufferScale(canvas.texScale)
 	w.canvas.Painter().SetOutputSize(int(winWidth), int(winHeight))

+ 0 - 5
vendor/fyne.io/fyne/v2/internal/driver/glfw/loop_desktop.go

@@ -5,7 +5,6 @@ package glfw
 
 import (
 	"fmt"
-	"runtime"
 
 	"fyne.io/fyne/v2"
 
@@ -14,10 +13,6 @@ import (
 
 func (d *gLDriver) initGLFW() {
 	initOnce.Do(func() {
-		if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
-			drawOnMainThread = true
-		}
-
 		err := glfw.Init()
 		if err != nil {
 			fyne.LogError("failed to initialise GLFW", err)

+ 5 - 6
vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_bar.go

@@ -68,9 +68,7 @@ func (b *MenuBar) Toggle() {
 }
 
 func (b *MenuBar) activateChild(item *menuBarItem) {
-	if !b.active {
-		b.active = true
-	}
+	b.active = true
 	if item.Child() != nil {
 		item.Child().DeactivateChild()
 	}
@@ -142,13 +140,14 @@ func (r *menuBarRenderer) Layout(size fyne.Size) {
 	} else {
 		r.underlay.Resize(fyne.NewSize(0, 0))
 	}
-	r.cont.Resize(fyne.NewSize(size.Width-2*theme.InnerPadding(), size.Height))
-	r.cont.Move(fyne.NewPos(theme.InnerPadding(), 0))
+	innerPadding := theme.InnerPadding()
+	r.cont.Resize(fyne.NewSize(size.Width-2*innerPadding, size.Height))
+	r.cont.Move(fyne.NewPos(innerPadding, 0))
 	if item := r.b.activeItem; item != nil {
 		if item.Child().Size().IsZero() {
 			item.Child().Resize(item.Child().MinSize())
 		}
-		item.Child().Move(fyne.NewPos(item.Position().X+theme.InnerPadding(), item.Size().Height))
+		item.Child().Move(fyne.NewPos(item.Position().X+innerPadding, item.Size().Height))
 	}
 	r.background.Resize(size)
 }

+ 2 - 0
vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_bar_item.go

@@ -39,6 +39,7 @@ func (i *menuBarItem) Child() *publicWidget.Menu {
 // Implements: fyne.Widget
 func (i *menuBarItem) CreateRenderer() fyne.WidgetRenderer {
 	background := canvas.NewRectangle(theme.HoverColor())
+	background.CornerRadius = theme.SelectionRadiusSize()
 	background.Hide()
 	text := canvas.NewText(i.Menu.Label, theme.ForegroundColor())
 	objects := []fyne.CanvasObject{background, text}
@@ -160,6 +161,7 @@ func (r *menuBarItemRenderer) MinSize() fyne.Size {
 }
 
 func (r *menuBarItemRenderer) Refresh() {
+	r.background.CornerRadius = theme.SelectionRadiusSize()
 	if r.i.active && r.i.Parent.active {
 		r.background.FillColor = theme.FocusColor()
 		r.background.Show()

+ 1 - 1
vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_darwin.go

@@ -172,7 +172,7 @@ func insertNativeMenuItem(nsMenu unsafe.Pointer, item *fyne.MenuItem, nextItemID
 	var imgData unsafe.Pointer
 	var imgDataLength uint
 	if item.Icon != nil {
-		if painter.IsResourceSVG(item.Icon) {
+		if svg.IsResourceSVG(item.Icon) {
 			rsc := item.Icon
 			if _, isThemed := rsc.(*theme.ThemedResource); isThemed {
 				var r, g, b, a C.int

+ 10 - 0
vendor/fyne.io/fyne/v2/internal/driver/glfw/scroll_speed_darwin.go

@@ -0,0 +1,10 @@
+//go:build darwin
+// +build darwin
+
+package glfw
+
+const (
+	scrollAccelerateRate   = float64(5)
+	scrollAccelerateCutoff = float64(5)
+	scrollSpeed            = float32(10)
+)

+ 10 - 0
vendor/fyne.io/fyne/v2/internal/driver/glfw/scroll_speed_default.go

@@ -0,0 +1,10 @@
+//go:build !darwin
+// +build !darwin
+
+package glfw
+
+const (
+	scrollAccelerateRate   = float64(125)
+	scrollAccelerateCutoff = float64(10)
+	scrollSpeed            = float32(25)
+)

+ 45 - 32
vendor/fyne.io/fyne/v2/internal/driver/glfw/window.go

@@ -9,20 +9,17 @@ import (
 
 	"fyne.io/fyne/v2"
 	"fyne.io/fyne/v2/driver/desktop"
-	"fyne.io/fyne/v2/internal"
 	"fyne.io/fyne/v2/internal/app"
 	"fyne.io/fyne/v2/internal/cache"
 	"fyne.io/fyne/v2/internal/driver"
 	"fyne.io/fyne/v2/internal/driver/common"
+	"fyne.io/fyne/v2/internal/scale"
 )
 
 const (
-	scrollAccelerateRate   = float64(5)
-	scrollAccelerateCutoff = float64(5)
-	scrollSpeed            = float32(10)
-	doubleClickDelay       = 300 // ms (maximum interval between clicks for double click detection)
-	dragMoveThreshold      = 2   // how far can we move before it is a drag
-	windowIconSize         = 256
+	doubleClickDelay  = 300 // ms (maximum interval between clicks for double click detection)
+	dragMoveThreshold = 2   // how far can we move before it is a drag
+	windowIconSize    = 256
 )
 
 func (w *window) Title() string {
@@ -49,7 +46,7 @@ func (w *window) minSizeOnScreen() (int, int) {
 
 // screenSize computes the actual output size of the given content size in screen pixels
 func (w *window) screenSize(canvasSize fyne.Size) (int, int) {
-	return internal.ScaleInt(w.canvas, canvasSize.Width), internal.ScaleInt(w.canvas, canvasSize.Height)
+	return scale.ToScreenCoordinate(w.canvas, canvasSize.Width), scale.ToScreenCoordinate(w.canvas, canvasSize.Height)
 }
 
 func (w *window) Resize(size fyne.Size) {
@@ -58,7 +55,7 @@ func (w *window) Resize(size fyne.Size) {
 	w.runOnMainWhenCreated(func() {
 		w.viewLock.Lock()
 
-		width, height := internal.ScaleInt(w.canvas, bigEnough.Width), internal.ScaleInt(w.canvas, bigEnough.Height)
+		width, height := scale.ToScreenCoordinate(w.canvas, bigEnough.Width), scale.ToScreenCoordinate(w.canvas, bigEnough.Height)
 		if w.fixedSize || !w.visible { // fixed size ignores future `resized` and if not visible we may not get the event
 			w.shouldWidth, w.shouldHeight = width, height
 			w.width, w.height = width, height
@@ -137,11 +134,11 @@ func (w *window) doShow() {
 		return
 	}
 
-	run.Lock()
+	run.L.Lock()
 	for !run.flag {
-		run.cond.Wait()
+		run.Wait()
 	}
-	run.Unlock()
+	run.L.Unlock()
 
 	w.createLock.Do(w.create)
 	if w.view() == nil {
@@ -173,6 +170,10 @@ func (w *window) doShow() {
 	// show top canvas element
 	if w.canvas.Content() != nil {
 		w.canvas.Content().Show()
+
+		runOnDraw(w, func() {
+			w.driver.repaintWindow(w)
+		})
 	}
 }
 
@@ -218,7 +219,7 @@ func (w *window) Close() {
 		})
 	})
 
-	w.canvas.WalkTrees(nil, func(node *common.RenderCacheNode) {
+	w.canvas.WalkTrees(nil, func(node *common.RenderCacheNode, _ fyne.Position) {
 		if wid, ok := node.Obj().(fyne.Widget); ok {
 			cache.DestroyRenderer(wid)
 		}
@@ -306,8 +307,8 @@ func (w *window) processMoved(x, y int) {
 func (w *window) processResized(width, height int) {
 	canvasSize := w.computeCanvasSize(width, height)
 	if !w.fullScreen {
-		w.width = internal.ScaleInt(w.canvas, canvasSize.Width)
-		w.height = internal.ScaleInt(w.canvas, canvasSize.Height)
+		w.width = scale.ToScreenCoordinate(w.canvas, canvasSize.Width)
+		w.height = scale.ToScreenCoordinate(w.canvas, canvasSize.Height)
 	}
 
 	if !w.visible { // don't redraw if hidden
@@ -353,7 +354,7 @@ func (w *window) findObjectAtPositionMatching(canvas *glCanvas, mouse fyne.Posit
 func (w *window) processMouseMoved(xpos float64, ypos float64) {
 	w.mouseLock.Lock()
 	previousPos := w.mousePos
-	w.mousePos = fyne.NewPos(internal.UnscaleInt(w.canvas, int(xpos)), internal.UnscaleInt(w.canvas, int(ypos)))
+	w.mousePos = fyne.NewPos(scale.ToFyneCoordinate(w.canvas, int(xpos)), scale.ToFyneCoordinate(w.canvas, int(ypos)))
 	mousePos := w.mousePos
 	mouseButton := w.mouseButton
 	mouseDragPos := w.mouseDragPos
@@ -410,10 +411,9 @@ func (w *window) processMouseMoved(xpos float64, ypos float64) {
 	isMouseOverDragged := w.objIsDragged(mouseOver)
 	w.mouseLock.RUnlock()
 	if obj != nil && !isObjDragged {
-		ev := new(desktop.MouseEvent)
+		ev := &desktop.MouseEvent{Button: mouseButton}
 		ev.AbsolutePosition = mousePos
 		ev.Position = pos
-		ev.Button = mouseButton
 
 		if hovered, ok := obj.(desktop.Hoverable); ok {
 			if hovered == mouseOver {
@@ -450,7 +450,7 @@ func (w *window) processMouseMoved(xpos float64, ypos float64) {
 	if mouseDragged != nil && mouseButton != desktop.MouseButtonSecondary {
 		if w.mouseButton > 0 {
 			draggedObjDelta := mouseDraggedObjStart.Subtract(mouseDragged.(fyne.CanvasObject).Position())
-			ev := new(fyne.DragEvent)
+			ev := &fyne.DragEvent{}
 			ev.AbsolutePosition = mousePos
 			ev.Position = mousePos.Subtract(mouseDraggedOffset).Add(draggedObjDelta)
 			ev.Dragged = fyne.NewDelta(mousePos.X-mouseDragPos.X, mousePos.Y-mouseDragPos.Y)
@@ -507,7 +507,7 @@ func (w *window) processMouseClicked(button desktop.MouseButton, action action,
 	if mousePos.IsZero() { // window may not be focused (darwin mostly) and so position callbacks not happening
 		xpos, ypos := w.view().GetCursorPos()
 		w.mouseLock.Lock()
-		w.mousePos = fyne.NewPos(internal.UnscaleInt(w.canvas, int(xpos)), internal.UnscaleInt(w.canvas, int(ypos)))
+		w.mousePos = fyne.NewPos(scale.ToFyneCoordinate(w.canvas, int(xpos)), scale.ToFyneCoordinate(w.canvas, int(ypos)))
 		mousePos = w.mousePos
 		w.mouseLock.Unlock()
 	}
@@ -524,22 +524,37 @@ func (w *window) processMouseClicked(button desktop.MouseButton, action action,
 
 		return false
 	})
-	ev := new(fyne.PointEvent)
-	ev.Position = pos
-	ev.AbsolutePosition = mousePos
+	ev := &fyne.PointEvent{
+		Position:         pos,
+		AbsolutePosition: mousePos,
+	}
 
 	coMouse := co
 	if wid, ok := co.(desktop.Mouseable); ok {
-		mev := new(desktop.MouseEvent)
+		mev := &desktop.MouseEvent{
+			Button:   button,
+			Modifier: modifiers,
+		}
 		mev.Position = ev.Position
 		mev.AbsolutePosition = mousePos
-		mev.Button = button
-		mev.Modifier = modifiers
 		w.mouseClickedHandleMouseable(mev, action, wid)
 	}
 
 	if wid, ok := co.(fyne.Focusable); !ok || wid != w.canvas.Focused() {
-		w.canvas.Unfocus()
+		ignore := false
+		_, _, _ = w.findObjectAtPositionMatching(w.canvas, mousePos, func(object fyne.CanvasObject) bool {
+			switch object.(type) {
+			case fyne.Focusable:
+				ignore = true
+				return true
+			}
+
+			return false
+		})
+
+		if !ignore { // if a parent item under the mouse has focus then ignore this tap unfocus
+			w.canvas.Unfocus()
+		}
 	}
 
 	w.mouseLock.Lock()
@@ -572,7 +587,7 @@ func (w *window) processMouseClicked(button desktop.MouseButton, action action,
 	}
 
 	_, tap := co.(fyne.Tappable)
-	_, altTap := co.(fyne.SecondaryTappable)
+	secondary, altTap := co.(fyne.SecondaryTappable)
 	if tap || altTap {
 		if action == press {
 			w.mouseLock.Lock()
@@ -581,7 +596,7 @@ func (w *window) processMouseClicked(button desktop.MouseButton, action action,
 		} else if action == release {
 			if co == mousePressed {
 				if button == desktop.MouseButtonSecondary && altTap {
-					w.QueueEvent(func() { co.(fyne.SecondaryTappable).TappedSecondary(ev) })
+					w.QueueEvent(func() { secondary.TappedSecondary(ev) })
 				}
 			}
 		}
@@ -910,9 +925,7 @@ func (w *window) RunWithContext(f func()) {
 }
 
 func (w *window) RescaleContext() {
-	runOnMain(func() {
-		w.rescaleOnMain()
-	})
+	runOnMain(w.rescaleOnMain)
 }
 
 func (w *window) Context() interface{} {

+ 51 - 5
vendor/fyne.io/fyne/v2/internal/driver/glfw/window_desktop.go

@@ -14,10 +14,12 @@ import (
 	"fyne.io/fyne/v2"
 	"fyne.io/fyne/v2/canvas"
 	"fyne.io/fyne/v2/driver/desktop"
-	"fyne.io/fyne/v2/internal"
 	"fyne.io/fyne/v2/internal/driver/common"
 	"fyne.io/fyne/v2/internal/painter"
 	"fyne.io/fyne/v2/internal/painter/gl"
+	"fyne.io/fyne/v2/internal/scale"
+	"fyne.io/fyne/v2/internal/svg"
+	"fyne.io/fyne/v2/storage"
 
 	"github.com/go-gl/glfw/v3.3/glfw"
 )
@@ -140,6 +142,23 @@ func (w *window) CenterOnScreen() {
 	}
 }
 
+func (w *window) SetOnDropped(dropped func(pos fyne.Position, items []fyne.URI)) {
+	w.runOnMainWhenCreated(func() {
+		w.viewport.SetDropCallback(func(win *glfw.Window, names []string) {
+			if dropped == nil {
+				return
+			}
+
+			uris := make([]fyne.URI, len(names))
+			for i, name := range names {
+				uris[i] = storage.NewFileURI(name)
+			}
+
+			dropped(w.mousePos, uris)
+		})
+	})
+}
+
 func (w *window) doCenterOnScreen() {
 	viewWidth, viewHeight := w.screenSize(w.canvas.size)
 	if w.width > viewWidth { // in case our window has not called back to canvas size yet
@@ -189,7 +208,7 @@ func (w *window) SetIcon(icon fyne.Resource) {
 		}
 
 		var img image.Image
-		if painter.IsResourceSVG(w.icon) {
+		if svg.IsResourceSVG(w.icon) {
 			img = painter.PaintImage(&canvas.Image{Resource: w.icon}, nil, windowIconSize, windowIconSize)
 		} else {
 			pix, _, err := image.Decode(bytes.NewReader(w.icon.Content()))
@@ -600,6 +619,7 @@ func convertASCII(key glfw.Key) fyne.KeyName {
 func (w *window) keyPressed(_ *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
 	keyName := keyToName(key, scancode)
 	keyDesktopModifier := desktopModifier(mods)
+	w.driver.currentKeyModifiers = desktopModifierCorrected(mods, key, action)
 	keyAction := convertAction(action)
 	keyASCII := convertASCII(key)
 
@@ -623,6 +643,32 @@ func desktopModifier(mods glfw.ModifierKey) fyne.KeyModifier {
 	return m
 }
 
+func desktopModifierCorrected(mods glfw.ModifierKey, key glfw.Key, action glfw.Action) fyne.KeyModifier {
+	// On X11, pressing/releasing modifier keys does not include newly pressed/released keys in 'mod' mask.
+	// https://github.com/glfw/glfw/issues/1630
+	if action == glfw.Press {
+		mods |= glfwKeyToModifier(key)
+	} else {
+		mods &= ^glfwKeyToModifier(key)
+	}
+	return desktopModifier(mods)
+}
+
+func glfwKeyToModifier(key glfw.Key) glfw.ModifierKey {
+	var m glfw.ModifierKey
+	switch key {
+	case glfw.KeyLeftControl, glfw.KeyRightControl:
+		m = glfw.ModControl
+	case glfw.KeyLeftAlt, glfw.KeyRightAlt:
+		m = glfw.ModAlt
+	case glfw.KeyLeftShift, glfw.KeyRightShift:
+		m = glfw.ModShift
+	case glfw.KeyLeftSuper, glfw.KeyRightSuper:
+		m = glfw.ModSuper
+	}
+	return m
+}
+
 // charInput defines the character with modifiers callback which is called when a
 // Unicode character is input.
 //
@@ -648,8 +694,8 @@ func (w *window) rescaleOnMain() {
 	if w.fullScreen {
 		w.width, w.height = w.viewport.GetSize()
 		scaledFull := fyne.NewSize(
-			internal.UnscaleInt(w.canvas, w.width),
-			internal.UnscaleInt(w.canvas, w.height))
+			scale.ToFyneCoordinate(w.canvas, w.width),
+			scale.ToFyneCoordinate(w.canvas, w.height))
 		w.canvas.Resize(scaledFull)
 		return
 	}
@@ -737,7 +783,7 @@ func (w *window) create() {
 
 		if w.FixedSize() && (w.requestedWidth == 0 || w.requestedHeight == 0) {
 			bigEnough := w.canvas.canvasSize(w.canvas.Content().MinSize())
-			w.width, w.height = internal.ScaleInt(w.canvas, bigEnough.Width), internal.ScaleInt(w.canvas, bigEnough.Height)
+			w.width, w.height = scale.ToScreenCoordinate(w.canvas, bigEnough.Width), scale.ToScreenCoordinate(w.canvas, bigEnough.Height)
 			w.shouldWidth, w.shouldHeight = w.width, w.height
 		}
 

+ 7 - 3
vendor/fyne.io/fyne/v2/internal/driver/glfw/window_goxjs.go

@@ -11,9 +11,9 @@ import (
 
 	"fyne.io/fyne/v2"
 	"fyne.io/fyne/v2/driver/desktop"
-	"fyne.io/fyne/v2/internal"
 	"fyne.io/fyne/v2/internal/driver/common"
 	"fyne.io/fyne/v2/internal/painter/gl"
+	"fyne.io/fyne/v2/internal/scale"
 
 	"github.com/fyne-io/glfw-js"
 )
@@ -107,6 +107,10 @@ func (w *window) CenterOnScreen() {
 	w.centered = true
 }
 
+func (w *window) SetOnDropped(dropped func(pos fyne.Position, items []fyne.URI)) {
+	// FIXME: not implemented yet
+}
+
 func (w *window) doCenterOnScreen() {
 	// FIXME: no meaning for defining center on screen in WebGL
 }
@@ -474,8 +478,8 @@ func (w *window) rescaleOnMain() {
 	//	if w.fullScreen {
 	w.width, w.height = w.viewport.GetSize()
 	scaledFull := fyne.NewSize(
-		internal.UnscaleInt(w.canvas, w.width),
-		internal.UnscaleInt(w.canvas, w.height))
+		scale.ToFyneCoordinate(w.canvas, w.width),
+		scale.ToFyneCoordinate(w.canvas, w.height))
 	w.canvas.Resize(scaledFull)
 	return
 	//	}

+ 2 - 2
vendor/fyne.io/fyne/v2/internal/driver/glfw/window_notwindows.go

@@ -5,12 +5,12 @@ package glfw
 
 import (
 	"fyne.io/fyne/v2"
-	"fyne.io/fyne/v2/internal"
+	"fyne.io/fyne/v2/internal/scale"
 )
 
 func (w *window) setDarkMode() {
 }
 
 func (w *window) computeCanvasSize(width, height int) fyne.Size {
-	return fyne.NewSize(internal.UnscaleInt(w.canvas, width), internal.UnscaleInt(w.canvas, height))
+	return fyne.NewSize(scale.ToFyneCoordinate(w.canvas, width), scale.ToFyneCoordinate(w.canvas, height))
 }

+ 4 - 3
vendor/fyne.io/fyne/v2/internal/driver/glfw/window_windows.go

@@ -6,7 +6,8 @@ import (
 	"unsafe"
 
 	"fyne.io/fyne/v2"
-	"fyne.io/fyne/v2/internal"
+	"fyne.io/fyne/v2/internal/scale"
+
 	"golang.org/x/sys/windows/registry"
 )
 
@@ -45,7 +46,7 @@ func isDark() bool {
 
 func (w *window) computeCanvasSize(width, height int) fyne.Size {
 	if w.fixedSize {
-		return fyne.NewSize(internal.UnscaleInt(w.canvas, w.width), internal.UnscaleInt(w.canvas, w.height))
+		return fyne.NewSize(scale.ToFyneCoordinate(w.canvas, w.width), scale.ToFyneCoordinate(w.canvas, w.height))
 	}
-	return fyne.NewSize(internal.UnscaleInt(w.canvas, width), internal.UnscaleInt(w.canvas, height))
+	return fyne.NewSize(scale.ToFyneCoordinate(w.canvas, width), scale.ToFyneCoordinate(w.canvas, height))
 }

+ 16 - 0
vendor/fyne.io/fyne/v2/internal/driver/mobile/app/GoNativeActivity.java

@@ -43,6 +43,7 @@ public class GoNativeActivity extends NativeActivity {
     private native void insetsChanged(int top, int bottom, int left, int right);
     private native void keyboardTyped(String str);
     private native void keyboardDelete();
+    private native void backPressed();
     private native void setDarkMode(boolean dark);
 
 	private EditText mTextEdit;
@@ -313,6 +314,21 @@ public class GoNativeActivity extends NativeActivity {
         filePickerReturned(uri.toString());
     }
 
+    @Override
+    public void onBackPressed() {
+        // skip the default behaviour - we can call finishActivity if we want to go back
+        backPressed();
+    }
+
+    public void finishActivity() {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                GoNativeActivity.super.onBackPressed();
+            }
+        });
+    }
+
     @Override
     public void onConfigurationChanged(Configuration config) {
         super.onConfigurationChanged(config);

+ 22 - 1
vendor/fyne.io/fyne/v2/internal/driver/mobile/app/android.c

@@ -53,6 +53,7 @@ static jmethodID show_keyboard_method;
 static jmethodID hide_keyboard_method;
 static jmethodID show_file_open_method;
 static jmethodID show_file_save_method;
+static jmethodID finish_method;
 
 jint JNI_OnLoad(JavaVM* vm, void* reserved) {
 	JNIEnv* env;
@@ -65,6 +66,14 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
 
 static int main_running = 0;
 
+// ensure we refresh context on resume in case something has changed...
+void processOnResume(ANativeActivity *activity) {
+	JNIEnv* env = activity->env;
+	setCurrentContext(activity->vm, (*env)->NewGlobalRef(env, activity->clazz));
+
+    onResume(activity);
+}
+
 // Entry point from our subclassed NativeActivity.
 //
 // By here, the Go runtime has been initialized (as we are running in
@@ -85,6 +94,7 @@ void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_
 		hide_keyboard_method = find_static_method(env, current_class, "hideKeyboard", "()V");
 		show_file_open_method = find_static_method(env, current_class, "showFileOpen", "(Ljava/lang/String;)V");
 		show_file_save_method = find_static_method(env, current_class, "showFileSave", "(Ljava/lang/String;Ljava/lang/String;)V");
+		finish_method = find_method(env, current_class, "finishActivity", "()V");
 
 		setCurrentContext(activity->vm, (*env)->NewGlobalRef(env, activity->clazz));
 
@@ -117,7 +127,7 @@ void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_
 	// Note that onNativeWindowResized is not called on resize. Avoid it.
 	// https://code.google.com/p/android/issues/detail?id=180645
 	activity->callbacks->onStart = onStart;
-	activity->callbacks->onResume = onResume;
+	activity->callbacks->onResume = processOnResume;
 	activity->callbacks->onSaveInstanceState = onSaveInstanceState;
 	activity->callbacks->onPause = onPause;
 	activity->callbacks->onStop = onStop;
@@ -204,6 +214,13 @@ char* destroyEGLSurface() {
 	return NULL;
 }
 
+void finish(JNIEnv* env, jobject ctx) {
+    (*env)->CallVoidMethod(
+        env,
+        ctx,
+        finish_method);
+}
+
 int32_t getKeyRune(JNIEnv* env, AInputEvent* e) {
 	return (int32_t)(*env)->CallStaticIntMethod(
 		env,
@@ -272,6 +289,10 @@ void Java_org_golang_app_GoNativeActivity_keyboardDelete(JNIEnv *env, jclass cla
     keyboardDelete();
 }
 
+void Java_org_golang_app_GoNativeActivity_backPressed(JNIEnv *env, jclass clazz) {
+    onBackPressed();
+}
+
 void Java_org_golang_app_GoNativeActivity_setDarkMode(JNIEnv *env, jclass clazz, jboolean dark) {
     setDarkMode((bool)dark);
 }

+ 26 - 0
vendor/fyne.io/fyne/v2/internal/driver/mobile/app/android.go

@@ -48,6 +48,7 @@ void showKeyboard(JNIEnv* env, int keyboardType);
 void hideKeyboard(JNIEnv* env);
 void showFileOpen(JNIEnv* env, char* mimes);
 void showFileSave(JNIEnv* env, char* mimes, char* filename);
+void finish(JNIEnv* env, jobject ctx);
 
 void Java_org_golang_app_GoNativeActivity_filePickerReturned(JNIEnv *env, jclass clazz, jstring str);
 */
@@ -77,6 +78,18 @@ var mimeMap = map[string]string{
 	".txt": "text/plain",
 }
 
+// GoBack asks the OS to go to the previous app / activity
+func GoBack() {
+	err := RunOnJVM(func(_, jniEnv, ctx uintptr) error {
+		env := (*C.JNIEnv)(unsafe.Pointer(jniEnv))
+		C.finish(env, C.jobject(ctx))
+		return nil
+	})
+	if err != nil {
+		log.Fatalf("app: %v", err)
+	}
+}
+
 // RunOnJVM runs fn on a new goroutine locked to an OS thread with a JNIEnv.
 //
 // RunOnJVM blocks until the call to fn is complete. Any Java
@@ -139,6 +152,19 @@ func onPause(activity *C.ANativeActivity) {
 func onStop(activity *C.ANativeActivity) {
 }
 
+//export onBackPressed
+func onBackPressed() {
+	k := key.Event{
+		Code:      key.CodeBackButton,
+		Direction: key.DirPress,
+	}
+	log.Println("Logging key event back")
+	theApp.events.In() <- k
+
+	k.Direction = key.DirRelease
+	theApp.events.In() <- k
+}
+
 //export onCreate
 func onCreate(activity *C.ANativeActivity) {
 	// Set the initial configuration.

+ 2 - 2
vendor/fyne.io/fyne/v2/internal/driver/mobile/app/app.go

@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build linux || darwin || windows
-// +build linux darwin windows
+//go:build freebsd || linux || darwin || windows
+// +build freebsd linux darwin windows
 
 package app
 

+ 4 - 0
vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_desktop.go

@@ -63,6 +63,10 @@ func main(f func(App)) {
 	C.runApp()
 }
 
+func GoBack() {
+	// When simulating mobile there are no other activities open (and we can't just force background)
+}
+
 // loop is the primary drawing loop.
 //
 // After Cocoa has captured the initial OS thread for processing Cocoa

+ 4 - 0
vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_ios.go

@@ -82,6 +82,10 @@ var DisplayMetrics struct {
 	HeightPx int
 }
 
+func GoBack() {
+	// Apple do not permit apps to exit in any way other than user pressing home button / gesture
+}
+
 //export setDisplayMetrics
 func setDisplayMetrics(width, height int, scale int) {
 	DisplayMetrics.WidthPx = width

+ 4 - 0
vendor/fyne.io/fyne/v2/internal/driver/mobile/app/shiny.go

@@ -15,6 +15,10 @@ func main(f func(a App)) {
 	fmt.Errorf("Running mobile simulation mode does not currently work on Windows.")
 }
 
+func GoBack() {
+	// When simulating mobile there are no other activities open (and we can't just force background)
+}
+
 // driverShowVirtualKeyboard does nothing on desktop
 func driverShowVirtualKeyboard(KeyboardType) {
 }

+ 2 - 2
vendor/fyne.io/fyne/v2/internal/driver/mobile/app/x11.c

@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build linux && !android
-// +build linux,!android
+//go:build (linux && !android) || freebsd
+// +build linux,!android freebsd
 
 #include "_cgo_export.h"
 #include <EGL/egl.h>

+ 7 - 2
vendor/fyne.io/fyne/v2/internal/driver/mobile/app/x11.go

@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build linux && !android
-// +build linux,!android
+//go:build (linux && !android) || freebsd
+// +build linux,!android freebsd
 
 package app
 
@@ -15,6 +15,7 @@ than screens with touch panels.
 
 /*
 #cgo LDFLAGS: -lEGL -lGLESv2 -lX11
+#cgo freebsd CFLAGS: -I/usr/local/include/
 
 void createWindow(void);
 void processEvents(void);
@@ -79,6 +80,10 @@ func main(f func(App)) {
 	}
 }
 
+func GoBack() {
+	// When simulating mobile there are no other activities open (and we can't just force background)
+}
+
 //export onResize
 func onResize(w, h int) {
 	// TODO(nigeltao): don't assume 72 DPI. DisplayWidth and DisplayWidthMM

+ 8 - 6
vendor/fyne.io/fyne/v2/internal/driver/mobile/canvas.go

@@ -29,8 +29,8 @@ type mobileCanvas struct {
 	scale            float32
 	size             fyne.Size
 
-	touched map[int]mobile.Touchable
-	padded  bool
+	touched       map[int]mobile.Touchable
+	padded, debug bool
 
 	onTypedRune func(rune)
 	onTypedKey  func(event *fyne.KeyEvent)
@@ -49,6 +49,7 @@ type mobileCanvas struct {
 // NewCanvas creates a new gomobile mobileCanvas. This is a mobileCanvas that will render on a mobile device using OpenGL.
 func NewCanvas() fyne.Canvas {
 	ret := &mobileCanvas{padded: true}
+	ret.debug = fyne.CurrentApp().Settings().BuildType() == fyne.BuildDebug
 	ret.scale = fyne.CurrentDevice().SystemScaleForWindow(nil) // we don't need a window parameter on mobile
 	ret.touched = make(map[int]mobile.Touchable)
 	ret.lastTapDownPos = make(map[int]fyne.Position)
@@ -292,7 +293,7 @@ func (c *mobileCanvas) tapMove(pos fyne.Position, tapID int,
 		}
 	}
 
-	ev := new(fyne.DragEvent)
+	ev := &fyne.DragEvent{}
 	draggedObjDelta := c.dragStart.Subtract(c.dragging.(fyne.CanvasObject).Position())
 	ev.Position = pos.Subtract(c.dragOffset).Add(draggedObjDelta)
 	ev.Dragged = fyne.Delta{DX: deltaX, DY: deltaY}
@@ -344,9 +345,10 @@ func (c *mobileCanvas) tapUp(pos fyne.Position, tapID int,
 		c.touched[tapID] = nil
 	}
 
-	ev := new(fyne.PointEvent)
-	ev.Position = objPos
-	ev.AbsolutePosition = pos
+	ev := &fyne.PointEvent{
+		Position:         objPos,
+		AbsolutePosition: pos,
+	}
 
 	if duration < tapSecondaryDelay {
 		_, doubleTap := co.(fyne.DoubleTappable)

+ 29 - 12
vendor/fyne.io/fyne/v2/internal/driver/mobile/driver.go

@@ -7,6 +7,7 @@ import (
 
 	"fyne.io/fyne/v2"
 	"fyne.io/fyne/v2/canvas"
+	"fyne.io/fyne/v2/driver/mobile"
 	"fyne.io/fyne/v2/internal"
 	"fyne.io/fyne/v2/internal/animation"
 	intapp "fyne.io/fyne/v2/internal/app"
@@ -22,6 +23,7 @@ import (
 	"fyne.io/fyne/v2/internal/driver/mobile/gl"
 	"fyne.io/fyne/v2/internal/painter"
 	pgl "fyne.io/fyne/v2/internal/painter/gl"
+	"fyne.io/fyne/v2/internal/scale"
 	"fyne.io/fyne/v2/theme"
 )
 
@@ -126,6 +128,10 @@ func (d *mobileDriver) AbsolutePositionForObject(co fyne.CanvasObject) fyne.Posi
 	return pos.Subtract(inset)
 }
 
+func (d *mobileDriver) GoBack() {
+	app.GoBack()
+}
+
 func (d *mobileDriver) Quit() {
 	// Android and iOS guidelines say this should not be allowed!
 }
@@ -294,7 +300,7 @@ func (d *mobileDriver) paintWindow(window fyne.Window, size fyne.Size) {
 		}
 		c.Painter().Paint(obj, pos, size)
 	}
-	afterDraw := func(node *common.RenderCacheNode) {
+	afterDraw := func(node *common.RenderCacheNode, pos fyne.Position) {
 		if _, ok := node.Obj().(fyne.Scrollable); ok {
 			c.Painter().StopClipping()
 			clips.Pop()
@@ -302,6 +308,10 @@ func (d *mobileDriver) paintWindow(window fyne.Window, size fyne.Size) {
 				c.Painter().StartClipping(top.Rect())
 			}
 		}
+
+		if c.debug {
+			c.DrawDebugOverlay(node.Obj(), pos, size)
+		}
 	}
 
 	c.WalkTrees(draw, afterDraw)
@@ -330,16 +340,16 @@ func (d *mobileDriver) setTheme(dark bool) {
 }
 
 func (d *mobileDriver) tapDownCanvas(w *window, x, y float32, tapID touch.Sequence) {
-	tapX := internal.UnscaleInt(w.canvas, int(x))
-	tapY := internal.UnscaleInt(w.canvas, int(y))
+	tapX := scale.ToFyneCoordinate(w.canvas, int(x))
+	tapY := scale.ToFyneCoordinate(w.canvas, int(y))
 	pos := fyne.NewPos(tapX, tapY+tapYOffset)
 
 	w.canvas.tapDown(pos, int(tapID))
 }
 
 func (d *mobileDriver) tapMoveCanvas(w *window, x, y float32, tapID touch.Sequence) {
-	tapX := internal.UnscaleInt(w.canvas, int(x))
-	tapY := internal.UnscaleInt(w.canvas, int(y))
+	tapX := scale.ToFyneCoordinate(w.canvas, int(x))
+	tapY := scale.ToFyneCoordinate(w.canvas, int(y))
 	pos := fyne.NewPos(tapX, tapY+tapYOffset)
 
 	w.canvas.tapMove(pos, int(tapID), func(wid fyne.Draggable, ev *fyne.DragEvent) {
@@ -348,8 +358,8 @@ func (d *mobileDriver) tapMoveCanvas(w *window, x, y float32, tapID touch.Sequen
 }
 
 func (d *mobileDriver) tapUpCanvas(w *window, x, y float32, tapID touch.Sequence) {
-	tapX := internal.UnscaleInt(w.canvas, int(x))
-	tapY := internal.UnscaleInt(w.canvas, int(y))
+	tapX := scale.ToFyneCoordinate(w.canvas, int(x))
+	tapY := scale.ToFyneCoordinate(w.canvas, int(y))
 	pos := fyne.NewPos(tapX, tapY+tapYOffset)
 
 	w.canvas.tapUp(pos, int(tapID), func(wid fyne.Tappable, ev *fyne.PointEvent) {
@@ -453,6 +463,8 @@ var keyCodeMap = map[key.Code]fyne.KeyName{
 	key.CodeBackslash:          fyne.KeyBackslash,
 	key.CodeRightSquareBracket: fyne.KeyRightBracket,
 	key.CodeGraveAccent:        fyne.KeyBackTick,
+
+	key.CodeBackButton: mobile.KeyBack,
 }
 
 func keyToName(code key.Code) fyne.KeyName {
@@ -503,8 +515,12 @@ func (d *mobileDriver) typeDownCanvas(canvas *mobileCanvas, r rune, code key.Cod
 			canvas.Focused().TypedRune(r)
 		}
 	} else {
-		if keyName != "" && canvas.onTypedKey != nil {
-			canvas.onTypedKey(keyEvent)
+		if keyName != "" {
+			if canvas.onTypedKey != nil {
+				canvas.onTypedKey(keyEvent)
+			} else if keyName == mobile.KeyBack {
+				d.GoBack()
+			}
 		}
 		if r > 0 && canvas.onTypedRune != nil {
 			canvas.onTypedRune(r)
@@ -530,9 +546,10 @@ func (d *mobileDriver) SetOnConfigurationChanged(f func(*Configuration)) {
 // NewGoMobileDriver sets up a new Driver instance implemented using the Go
 // Mobile extension and OpenGL bindings.
 func NewGoMobileDriver() fyne.Driver {
-	d := new(mobileDriver)
-	d.theme = fyne.ThemeVariant(2) // unspecified
-	d.animation = &animation.Runner{}
+	d := &mobileDriver{
+		theme:     fyne.ThemeVariant(2), // unspecified
+		animation: &animation.Runner{},
+	}
 
 	registerRepository(d)
 	return d

+ 2 - 0
vendor/fyne.io/fyne/v2/internal/driver/mobile/event/key/key.go

@@ -221,6 +221,8 @@ const (
 	CodeRightAlt     Code = 230
 	CodeRightGUI     Code = 231
 
+	CodeBackButton Code = 301 // anything above 255 is not used in the USB spec
+
 	// The following codes are not part of the standard USB HID Usage IDs for
 	// keyboards. See http://www.usb.org/developers/hidpage/Hut1_12v2.pdf
 	//

+ 0 - 2
vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/consts.go

@@ -44,14 +44,12 @@ const (
 	DepthTest        = 0x0B71
 	Blend            = 0x0BE2
 	ScissorTest      = 0x0C11
-	UnpackAlignment  = 0x0CF5
 	Texture2D        = 0x0DE1
 
 	UnsignedByte = 0x1401
 	Float        = 0x1406
 	RED          = 0x1903
 	RGBA         = 0x1908
-	LUMINANCE    = 0x1909
 
 	Nearest          = 0x2600
 	Linear           = 0x2601

+ 8 - 9
vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/dll_windows.go

@@ -10,7 +10,6 @@ import (
 	"debug/pe"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"log"
 	"net/http"
 	"os"
@@ -18,7 +17,7 @@ import (
 	"runtime"
 )
 
-var debug = log.New(ioutil.Discard, "gl: ", log.LstdFlags)
+var debug = log.New(io.Discard, "gl: ", log.LstdFlags)
 
 func downloadDLLs() (path string, err error) {
 	url := "https://dl.google.com/go/mobile/angle-bd3f8780b-" + runtime.GOARCH + ".tgz"
@@ -54,11 +53,11 @@ func downloadDLLs() (path string, err error) {
 		}
 		switch header.Name {
 		case "angle-" + runtime.GOARCH + "/libglesv2.dll":
-			bytesGLESv2, err = ioutil.ReadAll(tr)
+			bytesGLESv2, err = io.ReadAll(tr)
 		case "angle-" + runtime.GOARCH + "/libegl.dll":
-			bytesEGL, err = ioutil.ReadAll(tr)
+			bytesEGL, err = io.ReadAll(tr)
 		case "angle-" + runtime.GOARCH + "/d3dcompiler_47.dll":
-			bytesD3DCompiler, err = ioutil.ReadAll(tr)
+			bytesD3DCompiler, err = io.ReadAll(tr)
 		default: // skip
 		}
 		if err != nil {
@@ -70,13 +69,13 @@ func downloadDLLs() (path string, err error) {
 	}
 
 	writeDLLs := func(path string) error {
-		if err := ioutil.WriteFile(filepath.Join(path, "libglesv2.dll"), bytesGLESv2, 0755); err != nil {
+		if err := os.WriteFile(filepath.Join(path, "libglesv2.dll"), bytesGLESv2, 0755); err != nil {
 			return fmt.Errorf("gl: cannot install ANGLE: %v", err)
 		}
-		if err := ioutil.WriteFile(filepath.Join(path, "libegl.dll"), bytesEGL, 0755); err != nil {
+		if err := os.WriteFile(filepath.Join(path, "libegl.dll"), bytesEGL, 0755); err != nil {
 			return fmt.Errorf("gl: cannot install ANGLE: %v", err)
 		}
-		if err := ioutil.WriteFile(filepath.Join(path, "d3dcompiler_47.dll"), bytesD3DCompiler, 0755); err != nil {
+		if err := os.WriteFile(filepath.Join(path, "d3dcompiler_47.dll"), bytesD3DCompiler, 0755); err != nil {
 			return fmt.Errorf("gl: cannot install ANGLE: %v", err)
 		}
 		return nil
@@ -152,7 +151,7 @@ func chromePath() string {
 	}
 
 	for _, installdir := range installdirs {
-		versiondirs, err := ioutil.ReadDir(installdir)
+		versiondirs, err := os.ReadDir(installdir)
 		if err != nil {
 			continue
 		}

+ 1 - 1
vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/fn.go

@@ -63,7 +63,6 @@ const (
 	glfnGetShaderiv
 	glfnGetTexParameteriv
 	glfnGetUniformLocation
-	glfnPixelStorei
 	glfnLinkProgram
 	glfnReadPixels
 	glfnScissor
@@ -71,6 +70,7 @@ const (
 	glfnTexImage2D
 	glfnTexParameteri
 	glfnUniform1f
+	glfnUniform2f
 	glfnUniform4f
 	glfnUniform4fv
 	glfnUseProgram

+ 16 - 10
vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/gl.go

@@ -45,6 +45,7 @@ func (ctx *context) BindBuffer(target Enum, b Buffer) {
 		},
 	})
 }
+
 func (ctx *context) BindTexture(target Enum, t Texture) {
 	ctx.enqueue(call{
 		args: fnargs{
@@ -181,6 +182,7 @@ func (ctx *context) CreateVertexArray() VertexArray {
 		blocking: true,
 	}))}
 }
+
 func (ctx *context) DeleteBuffer(v Buffer) {
 	ctx.enqueue(call{
 		args: fnargs{
@@ -218,6 +220,7 @@ func (ctx *context) DrawArrays(mode Enum, first, count int) {
 		},
 	})
 }
+
 func (ctx *context) Enable(cap Enum) {
 	ctx.enqueue(call{
 		args: fnargs{
@@ -382,16 +385,6 @@ func (ctx *context) LinkProgram(p Program) {
 	})
 }
 
-func (ctx *context) PixelStorei(pname Enum, param int32) {
-	ctx.enqueue(call{
-		args: fnargs{
-			fn: glfnPixelStorei,
-			a0: pname.c(),
-			a1: uintptr(param),
-		},
-	})
-}
-
 func (ctx *context) ReadPixels(dst []byte, x, y, width, height int, format, ty Enum) {
 	ctx.enqueue(call{
 		args: fnargs{
@@ -434,6 +427,7 @@ func (ctx *context) ShaderSource(s Shader, src string) {
 		blocking: true,
 	})
 }
+
 func (ctx *context) TexImage2D(target Enum, level int, internalFormat int, width, height int, format Enum, ty Enum, data []byte) {
 	// It is common to pass TexImage2D a nil data, indicating that a
 	// bound GL buffer is being used as the source. In that case, it
@@ -480,6 +474,18 @@ func (ctx *context) Uniform1f(dst Uniform, v float32) {
 		},
 	})
 }
+
+func (ctx *context) Uniform2f(dst Uniform, v0, v1 float32) {
+	ctx.enqueue(call{
+		args: fnargs{
+			fn: glfnUniform2f,
+			a0: dst.c(),
+			a1: uintptr(math.Float32bits(v0)),
+			a2: uintptr(math.Float32bits(v1)),
+		},
+	})
+}
+
 func (ctx *context) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) {
 	ctx.enqueue(call{
 		args: fnargs{

+ 6 - 5
vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/interface.go

@@ -172,11 +172,6 @@ type Context interface {
 	// http://www.khronos.org/opengles/sdk/docs/man3/html/glLinkProgram.xhtml
 	LinkProgram(p Program)
 
-	// PixelStorei set pixel storage modes
-	//
-	// https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glPixelStorei.xhtml
-	PixelStorei(pname Enum, param int32)
-
 	// ReadPixels returns pixel data from a buffer.
 	//
 	// In GLES 3, the source buffer is controlled with ReadBuffer.
@@ -208,6 +203,11 @@ type Context interface {
 	// http://www.khronos.org/opengles/sdk/docs/man3/html/glUniform.xhtml
 	Uniform1f(dst Uniform, v float32)
 
+	// Uniform2f writes a vec2 uniform variable.
+	//
+	// http://www.khronos.org/opengles/sdk/docs/man3/html/glUniform.xhtml
+	Uniform2f(dst Uniform, v0, v1 float32)
+
 	// Uniform4f writes a vec4 uniform variable.
 	//
 	// http://www.khronos.org/opengles/sdk/docs/man3/html/glUniform.xhtml
@@ -217,6 +217,7 @@ type Context interface {
 	//
 	// http://www.khronos.org/opengles/sdk/docs/man3/html/glUniform.xhtml
 	Uniform4fv(dst Uniform, src []float32)
+
 	// UseProgram sets the active program.
 	//
 	// http://www.khronos.org/opengles/sdk/docs/man3/html/glUseProgram.xhtml

+ 3 - 3
vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.c

@@ -125,9 +125,6 @@ uintptr_t processFn(struct fnargs* args, char* parg) {
 	case glfnLinkProgram:
 		glLinkProgram((GLint)args->a0);
 		break;
-	case glfnPixelStorei:
-		glPixelStorei((GLenum)args->a0, (GLint)args->a1);
-		break;
 	case glfnReadPixels:
 		glReadPixels((GLint)args->a0, (GLint)args->a1, (GLsizei)args->a2, (GLsizei)args->a3, (GLenum)args->a4, (GLenum)args->a5, (void*)parg);
 		break;
@@ -159,6 +156,9 @@ uintptr_t processFn(struct fnargs* args, char* parg) {
 	case glfnUniform1f:
 		glUniform1f((GLint)args->a0, *(GLfloat*)&args->a1);
 		break;
+	case glfnUniform2f:
+		glUniform2f((GLint)args->a0, *(GLfloat*)&args->a1, *(GLfloat*)&args->a2);
+		break;
 	case glfnUniform4f:
 		glUniform4f((GLint)args->a0, *(GLfloat*)&args->a1, *(GLfloat*)&args->a2, *(GLfloat*)&args->a3, *(GLfloat*)&args->a4);
 		break;

+ 3 - 4
vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.go

@@ -2,9 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build (darwin || linux || openbsd || freebsd) && go1.15
+//go:build darwin || linux || openbsd || freebsd
 // +build darwin linux openbsd freebsd
-// +build go1.15
 
 package gl
 
@@ -13,7 +12,7 @@ package gl
 #cgo darwin,!ios  LDFLAGS: -framework OpenGL
 #cgo linux        LDFLAGS: -lGLESv2
 #cgo openbsd      LDFLAGS: -L/usr/X11R6/lib/ -lGLESv2
-#cgo freebsd      LDFLAGS: -L/usr/local/X11R6/lib/ -lGLESv2
+#cgo freebsd      LDFLAGS: -L/usr/local/lib/ -lGLESv2
 
 #cgo android      CFLAGS: -Dos_android
 #cgo ios          CFLAGS: -Dos_ios
@@ -23,7 +22,7 @@ package gl
 #cgo openbsd      CFLAGS: -Dos_openbsd
 #cgo freebsd      CFLAGS: -Dos_freebsd
 #cgo openbsd      CFLAGS: -I/usr/X11R6/include/
-#cgo freebsd      CFLAGS: -I/usr/local/X11R6/include/
+#cgo freebsd      CFLAGS: -I/usr/local/include/
 
 #include <stdint.h>
 #include "work.h"

+ 1 - 1
vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.h

@@ -69,7 +69,6 @@ typedef enum {
 	glfnGetShaderiv,
 	glfnGetTexParameteriv,
 	glfnGetUniformLocation,
-	glfnPixelStorei,
 	glfnLinkProgram,
 	glfnReadPixels,
 	glfnScissor,
@@ -77,6 +76,7 @@ typedef enum {
 	glfnTexImage2D,
 	glfnTexParameteri,
 	glfnUniform1f,
+	glfnUniform2f,
 	glfnUniform4f,
 	glfnUniform4fv,
 	glfnUseProgram,

+ 0 - 181
vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work114.go

@@ -1,181 +0,0 @@
-// Copyright 2015 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build (darwin || linux || openbsd || freebsd) && !go1.15
-// +build darwin linux openbsd freebsd
-// +build !go1.15
-
-package gl
-
-/*
-#cgo ios                LDFLAGS: -framework OpenGLES
-#cgo darwin,amd64,!ios  LDFLAGS: -framework OpenGL
-#cgo darwin,arm         LDFLAGS: -framework OpenGLES
-#cgo darwin,arm64       LDFLAGS: -framework OpenGLES
-#cgo linux              LDFLAGS: -lGLESv2
-#cgo openbsd            LDFLAGS: -L/usr/X11R6/lib/ -lGLESv2
-#cgo freebsd            LDFLAGS: -L/usr/local/X11R6/lib/ -lGLESv2
-
-#cgo android            CFLAGS: -Dos_android
-#cgo ios                CFLAGS: -Dos_ios
-#cgo darwin,amd64,!ios  CFLAGS: -Dos_macos
-#cgo darwin,arm         CFLAGS: -Dos_ios
-#cgo darwin,arm64       CFLAGS: -Dos_ios
-#cgo darwin             CFLAGS: -DGL_SILENCE_DEPRECATION
-#cgo linux        CFLAGS: -Dos_linux
-#cgo openbsd      CFLAGS: -Dos_openbsd
-#cgo freebsd      CFLAGS: -Dos_freebsd
-#cgo openbsd      CFLAGS: -I/usr/X11R6/include/
-#cgo freebsd      CFLAGS: -I/usr/local/X11R6/include/
-
-#include <stdint.h>
-#include "work.h"
-
-uintptr_t process(struct fnargs* cargs, char* parg0, char* parg1, char* parg2, int count) {
-	uintptr_t ret;
-
-	ret = processFn(&cargs[0], parg0);
-	if (count > 1) {
-		ret = processFn(&cargs[1], parg1);
-	}
-	if (count > 2) {
-		ret = processFn(&cargs[2], parg2);
-	}
-
-	return ret;
-}
-*/
-import "C"
-
-import (
-	"unsafe"
-
-	"fyne.io/fyne/v2/internal/async"
-)
-
-const workbufLen = 3
-
-type context struct {
-	cptr  uintptr
-	debug int32
-
-	workAvailable *async.UnboundedStructChan
-
-	// work is a queue of calls to execute.
-	work chan call
-
-	// retvalue is sent a return value when blocking calls complete.
-	// It is safe to use a global unbuffered channel here as calls
-	// cannot currently be made concurrently.
-	//
-	// TODO: the comment above about concurrent calls isn't actually true: package
-	// app calls package gl, but it has to do so in a separate goroutine, which
-	// means that its gl calls (which may be blocking) can race with other gl calls
-	// in the main program. We should make it safe to issue blocking gl calls
-	// concurrently, or get the gl calls out of package app, or both.
-	retvalue chan C.uintptr_t
-
-	cargs [workbufLen]C.struct_fnargs
-	parg  [workbufLen]*C.char
-}
-
-func (ctx *context) WorkAvailable() <-chan struct{} { return ctx.workAvailable.Out() }
-
-type context3 struct {
-	*context
-}
-
-// NewContext creates a cgo OpenGL context.
-//
-// See the Worker interface for more details on how it is used.
-func NewContext() (Context, Worker) {
-	glctx := &context{
-		workAvailable: async.NewUnboundedStructChan(),
-		work:          make(chan call, workbufLen*4),
-		retvalue:      make(chan C.uintptr_t),
-	}
-	if C.GLES_VERSION == "GL_ES_2_0" {
-		return glctx, glctx
-	}
-	return context3{glctx}, glctx
-}
-
-// Version returns a GL ES version string, either "GL_ES_2_0" or "GL_ES_3_0".
-// Future versions of the gl package may return "GL_ES_3_1".
-func Version() string {
-	return C.GLES_VERSION
-}
-
-func (ctx *context) enqueue(c call) uintptr {
-	ctx.work <- c
-	ctx.workAvailable.In() <- struct{}{}
-
-	if c.blocking {
-		return uintptr(<-ctx.retvalue)
-	}
-	return 0
-}
-
-func (ctx *context) DoWork() {
-	queue := make([]call, 0, workbufLen)
-	for {
-		// Wait until at least one piece of work is ready.
-		// Accumulate work until a piece is marked as blocking.
-		select {
-		case w := <-ctx.work:
-			queue = append(queue, w)
-		default:
-			return
-		}
-		blocking := queue[len(queue)-1].blocking
-	enqueue:
-		for len(queue) < cap(queue) && !blocking {
-			select {
-			case w := <-ctx.work:
-				queue = append(queue, w)
-				blocking = queue[len(queue)-1].blocking
-			default:
-				break enqueue
-			}
-		}
-
-		// Process the queued GL functions.
-		for i, q := range queue {
-			ctx.cargs[i] = *(*C.struct_fnargs)(unsafe.Pointer(&q.args))
-			ctx.parg[i] = (*C.char)(q.parg)
-		}
-		ret := C.process(&ctx.cargs[0], ctx.parg[0], ctx.parg[1], ctx.parg[2], C.int(len(queue)))
-
-		// Cleanup and signal.
-		queue = queue[:0]
-		if blocking {
-			ctx.retvalue <- ret
-		}
-	}
-}
-
-func init() {
-	if unsafe.Sizeof(C.GLint(0)) != unsafe.Sizeof(int32(0)) {
-		panic("GLint is not an int32")
-	}
-}
-
-// cString creates C string off the Go heap.
-// ret is a *char.
-func (ctx *context) cString(str string) (uintptr, func()) {
-	ptr := unsafe.Pointer(C.CString(str))
-	return uintptr(ptr), func() { C.free(ptr) }
-}
-
-// cString creates a pointer to a C string off the Go heap.
-// ret is a **char.
-func (ctx *context) cStringPtr(str string) (uintptr, func()) {
-	s, free := ctx.cString(str)
-	ptr := C.malloc(C.size_t(unsafe.Sizeof((*int)(nil))))
-	*(*uintptr)(ptr) = s
-	return uintptr(ptr), func() {
-		free()
-		C.free(ptr)
-	}
-}

+ 5 - 4
vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work_windows.go

@@ -237,10 +237,6 @@ var glfnMap = map[glfn]func(c call) (ret uintptr){
 		ret, _, _ = syscall.Syscall(glGetUniformLocation.Addr(), 2, c.args.a0, c.args.a1, 0)
 		return ret
 	},
-	glfnPixelStorei: func(c call) (ret uintptr) {
-		syscall.Syscall(glPixelStorei.Addr(), 2, c.args.a0, c.args.a1, 0)
-		return
-	},
 	glfnLinkProgram: func(c call) (ret uintptr) {
 		syscall.Syscall(glLinkProgram.Addr(), 1, c.args.a0, 0, 0)
 		return
@@ -269,6 +265,10 @@ var glfnMap = map[glfn]func(c call) (ret uintptr){
 		syscall.Syscall6(glUniform1f.Addr(), 2, c.args.a0, c.args.a1, c.args.a2, c.args.a3, c.args.a4, c.args.a5)
 		return
 	},
+	glfnUniform2f: func(c call) (ret uintptr) {
+		syscall.Syscall6(glUniform2f.Addr(), 3, c.args.a0, c.args.a1, c.args.a2, c.args.a3, c.args.a4, c.args.a5)
+		return
+	},
 	glfnUniform4f: func(c call) (ret uintptr) {
 		syscall.Syscall6(glUniform4f.Addr(), 5, c.args.a0, c.args.a1, c.args.a2, c.args.a3, c.args.a4, c.args.a5)
 		return
@@ -356,6 +356,7 @@ var (
 	glTexImage2D              = libGLESv2.NewProc("glTexImage2D")
 	glTexParameteri           = libGLESv2.NewProc("glTexParameteri")
 	glUniform1f               = libGLESv2.NewProc("glUniform1f")
+	glUniform2f               = libGLESv2.NewProc("glUniform2f")
 	glUniform4f               = libGLESv2.NewProc("glUniform4f")
 	glUniform4fv              = libGLESv2.NewProc("glUniform4fv")
 	glUseProgram              = libGLESv2.NewProc("glUseProgram")

+ 5 - 1
vendor/fyne.io/fyne/v2/internal/driver/mobile/window.go

@@ -101,6 +101,10 @@ func (w *window) SetCloseIntercept(callback func()) {
 	w.onCloseIntercepted = callback
 }
 
+func (w *window) SetOnDropped(dropped func(fyne.Position, []fyne.URI)) {
+	// not implemented yet
+}
+
 func (w *window) Show() {
 	menu := fyne.CurrentApp().Driver().(*mobileDriver).findMenu(w)
 	menuButton := w.newMenuButton(menu)
@@ -161,7 +165,7 @@ func (w *window) Close() {
 		w.canvas.Painter().Free(obj)
 	})
 
-	w.canvas.WalkTrees(nil, func(node *common.RenderCacheNode) {
+	w.canvas.WalkTrees(nil, func(node *common.RenderCacheNode, _ fyne.Position) {
 		if wid, ok := node.Obj().(fyne.Widget); ok {
 			cache.DestroyRenderer(wid)
 		}

+ 8 - 6
vendor/fyne.io/fyne/v2/internal/driver/util.go

@@ -92,7 +92,7 @@ func FindObjectAtPositionMatching(mouse fyne.Position, matches func(object fyne.
 func ReverseWalkVisibleObjectTree(
 	obj fyne.CanvasObject,
 	beforeChildren func(fyne.CanvasObject, fyne.Position, fyne.Position, fyne.Size) bool,
-	afterChildren func(fyne.CanvasObject, fyne.CanvasObject),
+	afterChildren func(fyne.CanvasObject, fyne.Position, fyne.CanvasObject),
 ) bool {
 	clipSize := fyne.NewSize(math.MaxInt32, math.MaxInt32)
 	return walkObjectTree(obj, true, nil, fyne.NewPos(0, 0), fyne.NewPos(0, 0), clipSize, beforeChildren, afterChildren, true)
@@ -110,7 +110,7 @@ func ReverseWalkVisibleObjectTree(
 func WalkCompleteObjectTree(
 	obj fyne.CanvasObject,
 	beforeChildren func(fyne.CanvasObject, fyne.Position, fyne.Position, fyne.Size) bool,
-	afterChildren func(fyne.CanvasObject, fyne.CanvasObject),
+	afterChildren func(fyne.CanvasObject, fyne.Position, fyne.CanvasObject),
 ) bool {
 	clipSize := fyne.NewSize(math.MaxInt32, math.MaxInt32)
 	return walkObjectTree(obj, false, nil, fyne.NewPos(0, 0), fyne.NewPos(0, 0), clipSize, beforeChildren, afterChildren, false)
@@ -128,7 +128,7 @@ func WalkCompleteObjectTree(
 func WalkVisibleObjectTree(
 	obj fyne.CanvasObject,
 	beforeChildren func(fyne.CanvasObject, fyne.Position, fyne.Position, fyne.Size) bool,
-	afterChildren func(fyne.CanvasObject, fyne.CanvasObject),
+	afterChildren func(fyne.CanvasObject, fyne.Position, fyne.CanvasObject),
 ) bool {
 	clipSize := fyne.NewSize(math.MaxInt32, math.MaxInt32)
 	return walkObjectTree(obj, false, nil, fyne.NewPos(0, 0), fyne.NewPos(0, 0), clipSize, beforeChildren, afterChildren, true)
@@ -141,7 +141,7 @@ func walkObjectTree(
 	offset, clipPos fyne.Position,
 	clipSize fyne.Size,
 	beforeChildren func(fyne.CanvasObject, fyne.Position, fyne.Position, fyne.Size) bool,
-	afterChildren func(fyne.CanvasObject, fyne.CanvasObject),
+	afterChildren func(fyne.CanvasObject, fyne.Position, fyne.CanvasObject),
 	requireVisible bool,
 ) bool {
 	if obj == nil {
@@ -157,7 +157,9 @@ func walkObjectTree(
 	case *fyne.Container:
 		children = co.Objects
 	case fyne.Widget:
-		children = cache.Renderer(co).Objects()
+		if cache.IsRendered(co) || requireVisible {
+			children = cache.Renderer(co).Objects()
+		}
 	}
 
 	if _, ok := obj.(fyne.Scrollable); ok {
@@ -194,7 +196,7 @@ func walkObjectTree(
 	}
 
 	if afterChildren != nil {
-		afterChildren(obj, parent)
+		afterChildren(obj, pos, parent)
 	}
 	return cancelled
 }

+ 12 - 2
vendor/fyne.io/fyne/v2/internal/overlay_stack.go

@@ -75,13 +75,23 @@ func (s *OverlayStack) Remove(overlay fyne.CanvasObject) {
 	s.propertyLock.Lock()
 	defer s.propertyLock.Unlock()
 
+	overlayIdx := -1
 	for i, o := range s.overlays {
 		if o == overlay {
-			s.overlays = s.overlays[:i]
-			s.focusManagers = s.focusManagers[:i]
+			overlayIdx = i
 			break
 		}
 	}
+	if overlayIdx == -1 {
+		return
+	}
+	// set removed elements in backing array to nil to release memory references
+	for i := overlayIdx; i < len(s.overlays); i++ {
+		s.overlays[i] = nil
+		s.focusManagers[i] = nil
+	}
+	s.overlays = s.overlays[:overlayIdx]
+	s.focusManagers = s.focusManagers[:overlayIdx]
 }
 
 // Top returns the top-most overlay of the stack.

+ 28 - 5
vendor/fyne.io/fyne/v2/internal/painter/draw.go

@@ -10,6 +10,8 @@ import (
 	"golang.org/x/image/math/fixed"
 )
 
+const quarterCircleControl = 1 - 0.55228
+
 // DrawCircle rasterizes the given circle object into an image.
 // The bounds of the output image will be increased by vectorPad to allow for stroke overflow at the edges.
 // The scale function is used to understand how many pixels are required per unit of size.
@@ -98,18 +100,39 @@ func DrawRectangle(rect *canvas.Rectangle, vectorPad float32, scale func(float32
 	if rect.FillColor != nil {
 		filler := rasterx.NewFiller(width, height, scanner)
 		filler.SetColor(rect.FillColor)
-		rasterx.AddRect(float64(p1x), float64(p1y), float64(p3x), float64(p3y), 0, filler)
+		if rect.CornerRadius == 0 {
+			rasterx.AddRect(float64(p1x), float64(p1y), float64(p3x), float64(p3y), 0, filler)
+		} else {
+			r := float64(scale(rect.CornerRadius))
+			rasterx.AddRoundRect(float64(p1x), float64(p1y), float64(p3x), float64(p3y), r, r, 0, rasterx.RoundGap, filler)
+		}
 		filler.Draw()
 	}
 
 	if rect.StrokeColor != nil && rect.StrokeWidth > 0 {
+		r := scale(rect.CornerRadius)
+		c := quarterCircleControl * r
 		dasher := rasterx.NewDasher(width, height, scanner)
 		dasher.SetColor(rect.StrokeColor)
 		dasher.SetStroke(fixed.Int26_6(float64(stroke)*64), 0, nil, nil, nil, 0, nil, 0)
-		dasher.Start(rasterx.ToFixedP(float64(p1x), float64(p1y)))
-		dasher.Line(rasterx.ToFixedP(float64(p2x), float64(p2y)))
-		dasher.Line(rasterx.ToFixedP(float64(p3x), float64(p3y)))
-		dasher.Line(rasterx.ToFixedP(float64(p4x), float64(p4y)))
+		if c != 0 {
+			dasher.Start(rasterx.ToFixedP(float64(p1x), float64(p1y+r)))
+			dasher.CubeBezier(rasterx.ToFixedP(float64(p1x), float64(p1y+c)), rasterx.ToFixedP(float64(p1x+c), float64(p1y)), rasterx.ToFixedP(float64(p1x+r), float64(p2y)))
+		} else {
+			dasher.Start(rasterx.ToFixedP(float64(p1x), float64(p1y)))
+		}
+		dasher.Line(rasterx.ToFixedP(float64(p2x-r), float64(p2y)))
+		if c != 0 {
+			dasher.CubeBezier(rasterx.ToFixedP(float64(p2x-c), float64(p2y)), rasterx.ToFixedP(float64(p2x), float64(p2y+c)), rasterx.ToFixedP(float64(p2x), float64(p2y+r)))
+		}
+		dasher.Line(rasterx.ToFixedP(float64(p3x), float64(p3y-r)))
+		if c != 0 {
+			dasher.CubeBezier(rasterx.ToFixedP(float64(p3x), float64(p3y-c)), rasterx.ToFixedP(float64(p3x-c), float64(p3y)), rasterx.ToFixedP(float64(p3x-r), float64(p3y)))
+		}
+		dasher.Line(rasterx.ToFixedP(float64(p4x+r), float64(p4y)))
+		if c != 0 {
+			dasher.CubeBezier(rasterx.ToFixedP(float64(p4x+c), float64(p4y)), rasterx.ToFixedP(float64(p4x), float64(p4y-c)), rasterx.ToFixedP(float64(p4x), float64(p4y-r)))
+		}
 		dasher.Stop(true)
 		dasher.Draw()
 	}

+ 121 - 268
vendor/fyne.io/fyne/v2/internal/painter/font.go

@@ -2,18 +2,16 @@ package painter
 
 import (
 	"bytes"
-	"image"
 	"image/color"
 	"image/draw"
 	"math"
+	"strings"
 	"sync"
 
+	"github.com/go-text/render"
 	"github.com/go-text/typesetting/di"
-	gotext "github.com/go-text/typesetting/font"
+	"github.com/go-text/typesetting/font"
 	"github.com/go-text/typesetting/shaping"
-	"github.com/goki/freetype"
-	"github.com/goki/freetype/truetype"
-	"golang.org/x/image/font"
 	"golang.org/x/image/math/fixed"
 
 	"fyne.io/fyne/v2"
@@ -25,142 +23,91 @@ const (
 	// DefaultTabWidth is the default width in spaces
 	DefaultTabWidth = 4
 
-	// TextDPI is a global constant that determines how text scales to interface sizes
-	TextDPI = 78
-
 	fontTabSpaceSize = 10
 )
 
-// CachedFontFace returns a font face held in memory. These are loaded from the current theme.
-func CachedFontFace(style fyne.TextStyle, fontDP float32, texScale float32) (font.Face, gotext.Face) {
-	key := faceCacheKey{float32ToFixed266(fontDP), float32ToFixed266(texScale)}
+// CachedFontFace returns a Font face held in memory. These are loaded from the current theme.
+func CachedFontFace(style fyne.TextStyle, fontDP float32, texScale float32) *FontCacheItem {
 	val, ok := fontCache.Load(style)
 	if !ok {
-		var f1, f2 *truetype.Font
+		var f1, f2 font.Face
 		switch {
 		case style.Monospace:
-			f1 = loadFont(theme.TextMonospaceFont())
-			f2 = loadFont(theme.DefaultTextMonospaceFont())
+			f1 = loadMeasureFont(theme.TextMonospaceFont())
+			f2 = loadMeasureFont(theme.DefaultTextMonospaceFont())
 		case style.Bold:
 			if style.Italic {
-				f1 = loadFont(theme.TextBoldItalicFont())
-				f2 = loadFont(theme.DefaultTextBoldItalicFont())
+				f1 = loadMeasureFont(theme.TextBoldItalicFont())
+				f2 = loadMeasureFont(theme.DefaultTextBoldItalicFont())
 			} else {
-				f1 = loadFont(theme.TextBoldFont())
-				f2 = loadFont(theme.DefaultTextBoldFont())
+				f1 = loadMeasureFont(theme.TextBoldFont())
+				f2 = loadMeasureFont(theme.DefaultTextBoldFont())
 			}
 		case style.Italic:
-			f1 = loadFont(theme.TextItalicFont())
-			f2 = loadFont(theme.DefaultTextItalicFont())
+			f1 = loadMeasureFont(theme.TextItalicFont())
+			f2 = loadMeasureFont(theme.DefaultTextItalicFont())
 		case style.Symbol:
-			f2 = loadFont(theme.DefaultSymbolFont())
+			f1 = loadMeasureFont(theme.SymbolFont())
+			f2 = loadMeasureFont(theme.DefaultSymbolFont())
 		default:
-			f1 = loadFont(theme.TextFont())
-			f2 = loadFont(theme.DefaultTextFont())
+			f1 = loadMeasureFont(theme.TextFont())
+			f2 = loadMeasureFont(theme.DefaultTextFont())
 		}
 
 		if f1 == nil {
 			f1 = f2
 		}
-		val = &fontCacheItem{font: f1, fallback: f2, faces: make(map[faceCacheKey]font.Face),
-			measureFaces: make(map[faceCacheKey]gotext.Face)}
-		fontCache.Store(style, val)
-	}
-
-	comp := val.(*fontCacheItem)
-	comp.facesMutex.RLock()
-	face := comp.faces[key]
-	measureFace := comp.measureFaces[key]
-	comp.facesMutex.RUnlock()
-	if face == nil {
-		var opts truetype.Options
-		opts.Size = float64(fontDP)
-		opts.DPI = float64(TextDPI * texScale)
-
-		f1 := truetype.NewFace(comp.font, &opts)
-		f2 := truetype.NewFace(comp.fallback, &opts)
-		face = newFontWithFallback(f1, f2, comp.font, comp.fallback)
-
-		switch {
-		case style.Monospace:
-			measureFace = loadMeasureFont(theme.TextMonospaceFont())
-			if measureFace == nil {
-				measureFace = loadMeasureFont(theme.DefaultTextMonospaceFont())
-			}
-		case style.Bold:
-			if style.Italic {
-				measureFace = loadMeasureFont(theme.TextBoldItalicFont())
-				if measureFace == nil {
-					measureFace = loadMeasureFont(theme.DefaultTextBoldItalicFont())
-				}
-			} else {
-				measureFace = loadMeasureFont(theme.TextBoldFont())
-				if measureFace == nil {
-					measureFace = loadMeasureFont(theme.DefaultTextBoldFont())
-				}
-			}
-		case style.Italic:
-			measureFace = loadMeasureFont(theme.TextItalicFont())
-			if measureFace == nil {
-				measureFace = loadMeasureFont(theme.DefaultTextItalicFont())
-			}
-		case style.Symbol:
-			measureFace = loadMeasureFont(theme.DefaultSymbolFont())
-		default:
-			measureFace = loadMeasureFont(theme.TextFont())
-			if measureFace == nil {
-				measureFace = loadMeasureFont(theme.DefaultTextFont())
-			}
+		faces := []font.Face{f1, f2}
+		if emoji := theme.DefaultEmojiFont(); emoji != nil {
+			faces = append(faces, loadMeasureFont(emoji))
 		}
-
-		comp.facesMutex.Lock()
-		comp.faces[key] = face
-		comp.measureFaces[key] = measureFace
-		comp.facesMutex.Unlock()
+		val = &FontCacheItem{Fonts: faces}
+		fontCache.Store(style, val)
 	}
 
-	return face, measureFace
+	return val.(*FontCacheItem)
 }
 
-// ClearFontCache is used to remove cached fonts in the case that we wish to re-load font faces
+// ClearFontCache is used to remove cached fonts in the case that we wish to re-load Font faces
 func ClearFontCache() {
-	fontCache.Range(func(_, val interface{}) bool {
-		item := val.(*fontCacheItem)
-		for _, face := range item.faces {
-			if face == nil {
-				continue
-			}
-			err := face.Close()
-
-			if err != nil {
-				fyne.LogError("failed to close font face", err)
-				return false
-			}
-		}
-		return true
-	})
 
 	fontCache = &sync.Map{}
 }
 
 // DrawString draws a string into an image.
-func DrawString(dst draw.Image, s string, color color.Color, f font.Face, face gotext.Face, fontSize, scale float32,
-	height int, tabWidth int) {
-	src := &image.Uniform{C: color}
-	dot := freetype.Pt(0, height-f.Metrics().Descent.Ceil())
-	walkString(face, s, float32ToFixed266(fontSize), tabWidth, &dot.X, scale, func(g gotext.GID) {
-		dr, mask, maskp, _, ok := f.(truetype.IndexableFace).GlyphAtIndex(dot, truetype.Index(g))
-		if !ok {
-			dr, mask, maskp, _, ok = f.Glyph(dot, 0xfffd)
-		}
-		if ok {
-			draw.DrawMask(dst, dr, src, image.Point{}, mask, maskp, draw.Over)
+func DrawString(dst draw.Image, s string, color color.Color, f []font.Face, fontSize, scale float32, tabWidth int) {
+	r := render.Renderer{
+		FontSize: fontSize,
+		PixScale: scale,
+		Color:    color,
+	}
+
+	// TODO avoid shaping twice!
+	sh := &shaping.HarfbuzzShaper{}
+	out := sh.Shape(shaping.Input{
+		Text:     []rune(s),
+		RunStart: 0,
+		RunEnd:   len(s),
+		Face:     f[0],
+		Size:     fixed.I(int(fontSize * r.PixScale)),
+	})
+
+	advance := float32(0)
+	y := int(math.Ceil(float64(fixed266ToFloat32(out.LineBounds.Ascent))))
+	walkString(f, s, float32ToFixed266(fontSize), tabWidth, &advance, scale, func(run shaping.Output, x float32) {
+		if len(run.Glyphs) == 1 {
+			if run.Glyphs[0].GlyphID == 0 {
+				r.DrawStringAt(string([]rune{0xfffd}), dst, int(x), y, f[0])
+				return
+			}
 		}
+
+		r.DrawShapedRunAt(run, dst, int(x), y)
 	})
 }
 
-func loadMeasureFont(data fyne.Resource) gotext.Face {
-	loaded, err := gotext.ParseTTF(bytes.NewReader(data.Content()))
+func loadMeasureFont(data fyne.Resource) font.Face {
+	loaded, err := font.ParseTTF(bytes.NewReader(data.Content()))
 	if err != nil {
 		fyne.LogError("font load error", err)
 		return nil
@@ -171,8 +118,8 @@ func loadMeasureFont(data fyne.Resource) gotext.Face {
 
 // MeasureString returns how far dot would advance by drawing s with f.
 // Tabs are translated into a dot location change.
-func MeasureString(f gotext.Face, s string, textSize float32, tabWidth int) (size fyne.Size, advance fixed.Int26_6) {
-	return walkString(f, s, float32ToFixed266(textSize), tabWidth, &advance, 1, func(gotext.GID) {})
+func MeasureString(f []font.Face, s string, textSize float32, tabWidth int) (size fyne.Size, advance float32) {
+	return walkString(f, s, float32ToFixed266(textSize), tabWidth, &advance, 1, func(shaping.Output, float32) {})
 }
 
 // RenderedTextSize looks up how big a string would be if drawn on screen.
@@ -196,206 +143,112 @@ func float32ToFixed266(f float32) fixed.Int26_6 {
 	return fixed.Int26_6(float64(f) * (1 << 6))
 }
 
-func loadFont(data fyne.Resource) *truetype.Font {
-	loaded, err := truetype.Parse(data.Content())
-	if err != nil {
-		fyne.LogError("font load error", err)
-	}
-
-	return loaded
-}
-
 func measureText(text string, fontSize float32, style fyne.TextStyle) (fyne.Size, float32) {
-	_, face := CachedFontFace(style, fontSize, 1)
-	size, base := MeasureString(face, text, fontSize, style.TabWidth)
-	return size, fixed266ToFloat32(base)
-}
-
-func newFontWithFallback(chosen, fallback font.Face, chosenFont, fallbackFont ttfFont) font.Face {
-	return &compositeFace{chosen: chosen, fallback: fallback, chosenFont: chosenFont, fallbackFont: fallbackFont}
+	face := CachedFontFace(style, fontSize, 1)
+	return MeasureString(face.Fonts, text, fontSize, style.TabWidth)
 }
 
-func tabStop(spacew, x fixed.Int26_6, tabWidth int) fixed.Int26_6 {
+func tabStop(spacew, x float32, tabWidth int) float32 {
 	if tabWidth <= 0 {
 		tabWidth = DefaultTabWidth
 	}
 
-	tabw := spacew * fixed.Int26_6(tabWidth)
+	tabw := spacew * float32(tabWidth)
 	tabs, _ := math.Modf(float64((x + tabw) / tabw))
-	return tabw * fixed.Int26_6(tabs)
+	return tabw * float32(tabs)
 }
 
-func walkString(f gotext.Face, s string, textSize fixed.Int26_6, tabWidth int, advance *fixed.Int26_6, scale float32, cb func(g gotext.GID)) (size fyne.Size, base fixed.Int26_6) {
+func walkString(faces []font.Face, s string, textSize fixed.Int26_6, tabWidth int, advance *float32, scale float32,
+	cb func(run shaping.Output, x float32)) (size fyne.Size, base float32) {
+	s = strings.ReplaceAll(s, "\r", "")
+
 	runes := []rune(s)
 	in := shaping.Input{
 		Text:      []rune{' '},
 		RunStart:  0,
 		RunEnd:    1,
 		Direction: di.DirectionLTR,
-		Face:      f,
+		Face:      faces[0],
 		Size:      textSize,
 	}
 	shaper := &shaping.HarfbuzzShaper{}
 	out := shaper.Shape(in)
-	spacew := float32ToFixed266(scale) * fontTabSpaceSize
 
 	in.Text = runes
 	in.RunStart = 0
 	in.RunEnd = len(runes)
 
-	ins := shaping.SplitByFontGlyphs(in, []gotext.Face{f}) // TODO provide fallback...
+	x := float32(0)
+	spacew := scale * fontTabSpaceSize
+	ins := shaping.SplitByFontGlyphs(in, faces)
 	for _, in := range ins {
-		out = shaper.Shape(in)
-
-		var c rune
-		nextRuneIndex := 0
-		last := -1
-		for _, g := range out.Glyphs {
-			if g.ClusterIndex != last {
-				c = in.Text[nextRuneIndex]
-				nextRuneIndex += g.RuneCount
-				last = g.ClusterIndex
-			}
+		inEnd := in.RunEnd
+
+		pending := false
+		for i, r := range in.Text[in.RunStart:in.RunEnd] {
+			if r == '\t' {
+				if pending {
+					in.RunEnd = i
+					out = shaper.Shape(in)
+					x = shapeCallback(shaper, in, out, x, scale, cb)
+				}
+				x = tabStop(spacew, x, tabWidth)
 
-			if c == '\r' {
-				continue
-			}
-			if c == '\t' {
-				*advance = tabStop(spacew, *advance, tabWidth)
+				in.RunStart = i + 1
+				in.RunEnd = inEnd
+				pending = false
 			} else {
-				cb(g.GlyphID)
-				*advance += float32ToFixed266(fixed266ToFloat32(g.XAdvance) * scale)
+				pending = true
 			}
 		}
-	}
 
-	return fyne.NewSize(fixed266ToFloat32(*advance), fixed266ToFloat32(out.LineBounds.LineHeight())),
-		out.LineBounds.Ascent
-}
-
-var _ truetype.IndexableFace = (*compositeFace)(nil)
-
-type compositeFace struct {
-	sync.Mutex
-
-	chosen, fallback         font.Face
-	chosenFont, fallbackFont ttfFont
-}
-
-func (c *compositeFace) Close() (err error) {
-	c.Lock()
-	defer c.Unlock()
-
-	if c.chosen != nil {
-		err = c.chosen.Close()
-	}
-
-	err2 := c.fallback.Close()
-	if err2 != nil {
-		return err2
-	}
-
-	return
-}
-
-func (c *compositeFace) Glyph(dot fixed.Point26_6, r rune) (
-	dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
-	c.Lock()
-	defer c.Unlock()
-
-	if c.containsGlyph(c.chosenFont, r) {
-		return c.chosen.Glyph(dot, r)
-	}
-
-	if c.containsGlyph(c.fallbackFont, r) {
-		return c.fallback.Glyph(dot, r)
-	}
-
-	return
-}
-
-func (c *compositeFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
-	c.Lock()
-	defer c.Unlock()
-
-	if c.containsGlyph(c.chosenFont, r) {
-		return c.chosen.GlyphAdvance(r)
-	}
-
-	if c.containsGlyph(c.fallbackFont, r) {
-		return c.fallback.GlyphAdvance(r)
+		x = shapeCallback(shaper, in, out, x, scale, cb)
 	}
 
-	return
+	*advance = x
+	return fyne.NewSize(*advance, fixed266ToFloat32(out.LineBounds.LineHeight())),
+		fixed266ToFloat32(out.LineBounds.Ascent)
 }
 
-func (c *compositeFace) GlyphAtIndex(dot fixed.Point26_6, g truetype.Index) (dr image.Rectangle, mask image.Image, maskp image.Point,
-	advance fixed.Int26_6, ok bool) {
-	if g == 0 {
-		return image.Rectangle{}, nil, image.Point{}, 0, false
-	}
-
-	c.Lock()
-	defer c.Unlock()
-
-	dr, mask, maskp, advance, ok = c.chosen.(truetype.IndexableFace).GlyphAtIndex(dot, g)
-	if ok {
-		return
-	}
-
-	return c.fallback.(truetype.IndexableFace).GlyphAtIndex(dot, g)
-}
+func shapeCallback(shaper shaping.Shaper, in shaping.Input, out shaping.Output, x, scale float32, cb func(shaping.Output, float32)) float32 {
+	out = shaper.Shape(in)
+	glyphs := out.Glyphs
+	start := 0
+	pending := false
+	adv := fixed.I(0)
+	for i, g := range out.Glyphs {
+		if g.GlyphID == 0 {
+			if pending {
+				out.Glyphs = glyphs[start:i]
+				cb(out, x)
+				x += fixed266ToFloat32(adv) * scale
+				adv = 0
+			}
 
-func (c *compositeFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
-	c.Lock()
-	defer c.Unlock()
+			out.Glyphs = glyphs[i : i+1]
+			cb(out, x)
+			x += fixed266ToFloat32(glyphs[i].XAdvance) * scale
+			adv = 0
 
-	if c.containsGlyph(c.chosenFont, r) {
-		return c.chosen.GlyphBounds(r)
+			start = i + 1
+			pending = false
+		} else {
+			pending = true
+		}
+		adv += g.XAdvance
 	}
 
-	if c.containsGlyph(c.fallbackFont, r) {
-		return c.fallback.GlyphBounds(r)
+	if pending {
+		out.Glyphs = glyphs[start:]
+		cb(out, x)
+		x += fixed266ToFloat32(adv) * scale
+		adv = 0
 	}
-
-	return
-}
-
-func (c *compositeFace) Kern(r0, r1 rune) fixed.Int26_6 {
-	c.Lock()
-	defer c.Unlock()
-
-	if c.containsGlyph(c.chosenFont, r0) && c.containsGlyph(c.chosenFont, r1) {
-		return c.chosen.Kern(r0, r1)
-	}
-
-	return c.fallback.Kern(r0, r1)
-}
-
-func (c *compositeFace) Metrics() font.Metrics {
-	c.Lock()
-	defer c.Unlock()
-
-	return c.chosen.Metrics()
-}
-
-func (c *compositeFace) containsGlyph(font ttfFont, r rune) bool {
-	return font != nil && font.Index(r) != 0
-}
-
-type ttfFont interface {
-	Index(rune) truetype.Index
-}
-
-type faceCacheKey struct {
-	size, scale fixed.Int26_6
+	return x + fixed266ToFloat32(adv)*scale
 }
 
-type fontCacheItem struct {
-	font, fallback *truetype.Font
-	faces          map[faceCacheKey]font.Face
-	measureFaces   map[faceCacheKey]gotext.Face
-	facesMutex     sync.RWMutex
+type FontCacheItem struct {
+	Fonts []font.Face
 }
 
-var fontCache = &sync.Map{} // map[fyne.TextStyle]*fontCacheItem
+var fontCache = &sync.Map{} // map[fyne.TextStyle]*FontCacheItem

+ 1 - 1
vendor/fyne.io/fyne/v2/internal/painter/gl/context.go

@@ -29,7 +29,6 @@ type context interface {
 	GetShaderInfoLog(shader Shader) string
 	GetUniformLocation(program Program, name string) Uniform
 	LinkProgram(program Program)
-	PixelStorei(pname uint32, param int32)
 	ReadBuffer(src uint32)
 	ReadPixels(x, y, width, height int, colorFormat, typ uint32, pixels []uint8)
 	Scissor(x, y, w, h int32)
@@ -37,6 +36,7 @@ type context interface {
 	TexImage2D(target uint32, level, width, height int, colorFormat, typ uint32, data []uint8)
 	TexParameteri(target, param uint32, value int32)
 	Uniform1f(uniform Uniform, v float32)
+	Uniform2f(uniform Uniform, v0, v1 float32)
 	Uniform4f(uniform Uniform, v0, v1, v2, v3 float32)
 	UseProgram(program Program)
 	VertexAttribPointerWithOffset(attribute Attribute, size int, typ uint32, normalized bool, stride, offset int)

+ 118 - 48
vendor/fyne.io/fyne/v2/internal/painter/gl/draw.go

@@ -7,7 +7,6 @@ import (
 	"fyne.io/fyne/v2"
 	"fyne.io/fyne/v2/canvas"
 	paint "fyne.io/fyne/v2/internal/painter"
-	"fyne.io/fyne/v2/theme"
 )
 
 func (p *painter) createBuffer(points []float32) Buffer {
@@ -44,7 +43,6 @@ func (p *painter) drawLine(line *canvas.Line, pos fyne.Position, frame fyne.Size
 	if line.StrokeColor == color.Transparent || line.StrokeColor == nil || line.StrokeWidth == 0 {
 		return
 	}
-
 	points, halfWidth, feather := p.lineCoords(pos, line.Position1, line.Position2, line.StrokeWidth, 0.5, frame)
 	p.ctx.UseProgram(p.lineProgram)
 	vbo := p.createBuffer(points)
@@ -55,13 +53,9 @@ func (p *painter) drawLine(line *canvas.Line, pos fyne.Position, frame fyne.Size
 	p.logError()
 
 	colorUniform := p.ctx.GetUniformLocation(p.lineProgram, "color")
-	r, g, b, a := line.StrokeColor.RGBA()
-	if a == 0 {
-		p.ctx.Uniform4f(colorUniform, 0, 0, 0, 0)
-	} else {
-		alpha := float32(a)
-		p.ctx.Uniform4f(colorUniform, float32(r)/alpha, float32(g)/alpha, float32(b)/alpha, alpha/0xffff)
-	}
+	r, g, b, a := getFragmentColor(line.StrokeColor)
+	p.ctx.Uniform4f(colorUniform, r, g, b, a)
+
 	lineWidthUniform := p.ctx.GetUniformLocation(p.lineProgram, "lineWidth")
 	p.ctx.Uniform1f(lineWidthUniform, halfWidth)
 
@@ -102,8 +96,71 @@ func (p *painter) drawRectangle(rect *canvas.Rectangle, pos fyne.Position, frame
 	if (rect.FillColor == color.Transparent || rect.FillColor == nil) && (rect.StrokeColor == color.Transparent || rect.StrokeColor == nil || rect.StrokeWidth == 0) {
 		return
 	}
-	p.drawTextureWithDetails(rect, p.newGlRectTexture, pos, rect.Size(), frame, canvas.ImageFillStretch,
-		1.0, paint.VectorPad(rect))
+
+	roundedCorners := rect.CornerRadius != 0
+	var program Program
+	if roundedCorners {
+		program = p.roundRectangleProgram
+	} else {
+		program = p.rectangleProgram
+	}
+
+	// Vertex: BEG
+	bounds, points := p.vecRectCoords(pos, rect, frame)
+	p.ctx.UseProgram(program)
+	vbo := p.createBuffer(points)
+	p.defineVertexArray(program, "vert", 2, 4, 0)
+	p.defineVertexArray(program, "normal", 2, 4, 2)
+
+	p.ctx.BlendFunc(srcAlpha, oneMinusSrcAlpha)
+	p.logError()
+	// Vertex: END
+
+	// Fragment: BEG
+	frameSizeUniform := p.ctx.GetUniformLocation(program, "frame_size")
+	frameWidthScaled, frameHeightScaled := p.scaleFrameSize(frame)
+	p.ctx.Uniform2f(frameSizeUniform, frameWidthScaled, frameHeightScaled)
+
+	rectCoordsUniform := p.ctx.GetUniformLocation(program, "rect_coords")
+	x1Scaled, x2Scaled, y1Scaled, y2Scaled := p.scaleRectCoords(bounds[0], bounds[2], bounds[1], bounds[3])
+	p.ctx.Uniform4f(rectCoordsUniform, x1Scaled, x2Scaled, y1Scaled, y2Scaled)
+
+	strokeWidthScaled := roundToPixel(rect.StrokeWidth*p.pixScale, 1.0)
+	if roundedCorners {
+		strokeUniform := p.ctx.GetUniformLocation(program, "stroke_width_half")
+		p.ctx.Uniform1f(strokeUniform, strokeWidthScaled*0.5)
+
+		rectSizeUniform := p.ctx.GetUniformLocation(program, "rect_size_half")
+		rectSizeWidthScaled := x2Scaled - x1Scaled - strokeWidthScaled
+		rectSizeHeightScaled := y2Scaled - y1Scaled - strokeWidthScaled
+		p.ctx.Uniform2f(rectSizeUniform, rectSizeWidthScaled*0.5, rectSizeHeightScaled*0.5)
+
+		radiusUniform := p.ctx.GetUniformLocation(program, "radius")
+		radiusScaled := roundToPixel(rect.CornerRadius*p.pixScale, 1.0)
+		p.ctx.Uniform1f(radiusUniform, radiusScaled)
+	} else {
+		strokeUniform := p.ctx.GetUniformLocation(program, "stroke_width")
+		p.ctx.Uniform1f(strokeUniform, strokeWidthScaled)
+	}
+
+	var r, g, b, a float32
+	fillColorUniform := p.ctx.GetUniformLocation(program, "fill_color")
+	r, g, b, a = getFragmentColor(rect.FillColor)
+	p.ctx.Uniform4f(fillColorUniform, r, g, b, a)
+
+	strokeColorUniform := p.ctx.GetUniformLocation(program, "stroke_color")
+	strokeColor := rect.StrokeColor
+	if strokeColor == nil {
+		strokeColor = color.Transparent
+	}
+	r, g, b, a = getFragmentColor(strokeColor)
+	p.ctx.Uniform4f(strokeColorUniform, r, g, b, a)
+	p.logError()
+	// Fragment: END
+
+	p.ctx.DrawArrays(triangleStrip, 0, 4)
+	p.logError()
+	p.freeBuffer(vbo)
 }
 
 func (p *painter) drawText(text *canvas.Text, pos fyne.Position, frame fyne.Size) {
@@ -124,45 +181,10 @@ func (p *painter) drawText(text *canvas.Text, pos fyne.Position, frame fyne.Size
 		pos = fyne.NewPos(pos.X, pos.Y+(containerSize.Height-size.Height)/2)
 	}
 
-	color := text.Color
-	if color == nil {
-		color = theme.ForegroundColor()
-	}
-
 	// text size is sensitive to position on screen
 	size, _ = roundToPixelCoords(size, text.Position(), p.pixScale)
 	size.Width += roundToPixel(paint.VectorPad(text), p.pixScale)
-	p.drawSingleChannelTexture(text, p.newGlTextTexture, pos, size, frame, color, 0)
-}
-
-func (p *painter) drawSingleChannelTexture(o fyne.CanvasObject, creator func(canvasObject fyne.CanvasObject) Texture,
-	pos fyne.Position, size, frame fyne.Size, c color.Color, pad float32) {
-	texture, err := p.getTexture(o, creator)
-	if err != nil {
-		return
-	}
-
-	points := p.rectCoords(size, pos, frame, canvas.ImageFillStretch, 0, pad)
-	p.ctx.UseProgram(p.singleChannelProgram)
-	vbo := p.createBuffer(points)
-	p.defineVertexArray(p.singleChannelProgram, "vert", 3, 5, 0)
-	p.defineVertexArray(p.singleChannelProgram, "vertTexCoord", 2, 5, 3)
-
-	p.ctx.BlendFunc(srcAlpha, oneMinusSrcAlpha)
-	p.logError()
-
-	shaderColor := p.ctx.GetUniformLocation(p.singleChannelProgram, "color")
-	r, g, b, a := getFragmentColor(c)
-	p.ctx.Uniform4f(shaderColor, r, g, b, a)
-
-	p.ctx.ActiveTexture(texture0)
-	p.ctx.BindTexture(texture2D, texture)
-	p.logError()
-
-	p.ctx.DrawArrays(triangleStrip, 0, 4)
-	p.logError()
-	p.freeBuffer(vbo)
-
+	p.drawTextureWithDetails(text, p.newGlTextTexture, pos, size, frame, canvas.ImageFillStretch, 1.0, 0)
 }
 
 func (p *painter) drawTextureWithDetails(o fyne.CanvasObject, creator func(canvasObject fyne.CanvasObject) Texture,
@@ -175,7 +197,7 @@ func (p *painter) drawTextureWithDetails(o fyne.CanvasObject, creator func(canva
 
 	aspect := float32(0)
 	if img, ok := o.(*canvas.Image); ok {
-		aspect = paint.GetAspect(img)
+		aspect = img.Aspect()
 		if aspect == 0 {
 			aspect = 1 // fallback, should not occur - normally an image load error
 		}
@@ -315,6 +337,36 @@ func rectInnerCoords(size fyne.Size, pos fyne.Position, fill canvas.ImageFill, a
 	return size, pos
 }
 
+func (p *painter) vecRectCoords(pos fyne.Position, rect *canvas.Rectangle, frame fyne.Size) ([4]float32, []float32) {
+	size := rect.Size()
+	pos1 := rect.Position()
+
+	xPosDiff := pos.X - pos1.X
+	yPosDiff := pos.Y - pos1.Y
+	pos1.X = roundToPixel(pos1.X+xPosDiff, p.pixScale)
+	pos1.Y = roundToPixel(pos1.Y+yPosDiff, p.pixScale)
+	size.Width = roundToPixel(size.Width, p.pixScale)
+	size.Height = roundToPixel(size.Height, p.pixScale)
+
+	x1Pos := pos1.X
+	x1Norm := -1 + x1Pos*2/frame.Width
+	x2Pos := pos1.X + size.Width
+	x2Norm := -1 + x2Pos*2/frame.Width
+	y1Pos := pos1.Y
+	y1Norm := 1 - y1Pos*2/frame.Height
+	y2Pos := pos1.Y + size.Height
+	y2Norm := 1 - y2Pos*2/frame.Height
+
+	// output a norm for the fill and the vert is unused, but we pass 0 to avoid optimisation issues
+	coords := []float32{
+		0, 0, x1Norm, y1Norm, // first triangle
+		0, 0, x2Norm, y1Norm, // second triangle
+		0, 0, x1Norm, y2Norm,
+		0, 0, x2Norm, y2Norm}
+
+	return [4]float32{x1Pos, y1Pos, x2Pos, y2Pos}, coords
+}
+
 func roundToPixel(v float32, pixScale float32) float32 {
 	if pixScale == 1.0 {
 		return float32(math.Round(float64(v)))
@@ -337,6 +389,9 @@ func roundToPixelCoords(size fyne.Size, pos fyne.Position, pixScale float32) (fy
 
 // Returns FragmentColor(red,green,blue,alpha) from fyne.Color
 func getFragmentColor(col color.Color) (float32, float32, float32, float32) {
+	if col == nil {
+		return 0, 0, 0, 0
+	}
 	r, g, b, a := col.RGBA()
 	if a == 0 {
 		return 0, 0, 0, 0
@@ -344,3 +399,18 @@ func getFragmentColor(col color.Color) (float32, float32, float32, float32) {
 	alpha := float32(a)
 	return float32(r) / alpha, float32(g) / alpha, float32(b) / alpha, alpha / 0xffff
 }
+
+func (p *painter) scaleFrameSize(frame fyne.Size) (float32, float32) {
+	frameWidthScaled := roundToPixel(frame.Width*p.pixScale, 1.0)
+	frameHeightScaled := roundToPixel(frame.Height*p.pixScale, 1.0)
+	return frameWidthScaled, frameHeightScaled
+}
+
+// Returns scaled RectCoords(x1,x2,y1,y2) in same order
+func (p *painter) scaleRectCoords(x1, x2, y1, y2 float32) (float32, float32, float32, float32) {
+	x1Scaled := roundToPixel(x1*p.pixScale, 1.0)
+	x2Scaled := roundToPixel(x2*p.pixScale, 1.0)
+	y1Scaled := roundToPixel(y1*p.pixScale, 1.0)
+	y2Scaled := roundToPixel(y2*p.pixScale, 1.0)
+	return x1Scaled, x2Scaled, y1Scaled, y2Scaled
+}

+ 6 - 1
vendor/fyne.io/fyne/v2/internal/painter/gl/gl.go

@@ -12,11 +12,16 @@ import (
 const floatSize = 4
 const max16bit = float32(255 * 255)
 
-func logGLError(err uint32) {
+// logGLError logs error in the GL renderer.
+//
+// Receives a function as parameter, to lazily get the error code only when
+// needed, avoiding unneeded overhead.
+func logGLError(getError func() uint32) {
 	if fyne.CurrentApp().Settings().BuildType() != fyne.BuildDebug {
 		return
 	}
 
+	err := getError()
 	if err == 0 {
 		return
 	}

+ 0 - 9
vendor/fyne.io/fyne/v2/internal/painter/gl/gl_const_darwin.go

@@ -1,9 +0,0 @@
-package gl
-
-import (
-	"fyne.io/fyne/v2/internal/driver/mobile/gl"
-)
-
-const (
-	singleChannelColorFormat = gl.RED
-)

+ 0 - 17
vendor/fyne.io/fyne/v2/internal/painter/gl/gl_const_mobile.go

@@ -1,17 +0,0 @@
-//go:build !darwin && !js && !wasm && (android || ios || mobile)
-// +build !darwin
-// +build !js
-// +build !wasm
-// +build android ios mobile
-
-package gl
-
-import (
-	"fyne.io/fyne/v2/internal/driver/mobile/gl"
-)
-
-const (
-	singleChannelColorFormat = gl.LUMINANCE
-)
-
-var _ = singleChannelColorFormat

+ 6 - 7
vendor/fyne.io/fyne/v2/internal/painter/gl/gl_core.go

@@ -17,7 +17,6 @@ const (
 	bitDepthBuffer        = gl.DEPTH_BUFFER_BIT
 	clampToEdge           = gl.CLAMP_TO_EDGE
 	colorFormatRGBA       = gl.RGBA
-	colorFormatR          = gl.RED
 	compileStatus         = gl.COMPILE_STATUS
 	constantAlpha         = gl.CONSTANT_ALPHA
 	float                 = gl.FLOAT
@@ -39,7 +38,6 @@ const (
 	textureWrapT          = gl.TEXTURE_WRAP_T
 	triangles             = gl.TRIANGLES
 	triangleStrip         = gl.TRIANGLE_STRIP
-	unpackAlignment       = gl.UNPACK_ALIGNMENT
 	unsignedByte          = gl.UNSIGNED_BYTE
 	vertexShader          = gl.VERTEX_SHADER
 )
@@ -74,8 +72,9 @@ func (p *painter) Init() {
 	gl.Enable(gl.BLEND)
 	p.logError()
 	p.program = p.createProgram("simple")
-	p.singleChannelProgram = p.createProgram("single_channel")
 	p.lineProgram = p.createProgram("line")
+	p.rectangleProgram = p.createProgram("rectangle")
+	p.roundRectangleProgram = p.createProgram("round_rectangle")
 }
 
 type coreContext struct{}
@@ -211,10 +210,6 @@ func (c *coreContext) LinkProgram(program Program) {
 	gl.LinkProgram(uint32(program))
 }
 
-func (c *coreContext) PixelStorei(pname uint32, param int32) {
-	gl.PixelStorei(pname, param)
-}
-
 func (c *coreContext) ReadBuffer(src uint32) {
 	gl.ReadBuffer(src)
 }
@@ -255,6 +250,10 @@ func (c *coreContext) Uniform1f(uniform Uniform, v float32) {
 	gl.Uniform1f(int32(uniform), v)
 }
 
+func (c *coreContext) Uniform2f(uniform Uniform, v0, v1 float32) {
+	gl.Uniform2f(int32(uniform), v0, v1)
+}
+
 func (c *coreContext) Uniform4f(uniform Uniform, v0, v1, v2, v3 float32) {
 	gl.Uniform4f(int32(uniform), v0, v1, v2, v3)
 }

+ 6 - 7
vendor/fyne.io/fyne/v2/internal/painter/gl/gl_es.go

@@ -24,7 +24,6 @@ const (
 	bitDepthBuffer        = gl.DEPTH_BUFFER_BIT
 	clampToEdge           = gl.CLAMP_TO_EDGE
 	colorFormatRGBA       = gl.RGBA
-	colorFormatR          = gl.LUMINANCE
 	compileStatus         = gl.COMPILE_STATUS
 	constantAlpha         = gl.CONSTANT_ALPHA
 	float                 = gl.FLOAT
@@ -46,7 +45,6 @@ const (
 	textureWrapT          = gl.TEXTURE_WRAP_T
 	triangles             = gl.TRIANGLES
 	triangleStrip         = gl.TRIANGLE_STRIP
-	unpackAlignment       = gl.UNPACK_ALIGNMENT
 	unsignedByte          = gl.UNSIGNED_BYTE
 	vertexShader          = gl.VERTEX_SHADER
 )
@@ -81,8 +79,9 @@ func (p *painter) Init() {
 	gl.Enable(gl.BLEND)
 	p.logError()
 	p.program = p.createProgram("simple_es")
-	p.singleChannelProgram = p.createProgram("single_channel_es")
 	p.lineProgram = p.createProgram("line_es")
+	p.rectangleProgram = p.createProgram("rectangle_es")
+	p.roundRectangleProgram = p.createProgram("round_rectangle_es")
 }
 
 type esContext struct{}
@@ -218,10 +217,6 @@ func (c *esContext) LinkProgram(program Program) {
 	gl.LinkProgram(uint32(program))
 }
 
-func (c *esContext) PixelStorei(pname uint32, param int32) {
-	gl.PixelStorei(pname, param)
-}
-
 func (c *esContext) ReadBuffer(src uint32) {
 	gl.ReadBuffer(src)
 }
@@ -262,6 +257,10 @@ func (c *esContext) Uniform1f(uniform Uniform, v float32) {
 	gl.Uniform1f(int32(uniform), v)
 }
 
+func (c *esContext) Uniform2f(uniform Uniform, v0, v1 float32) {
+	gl.Uniform2f(int32(uniform), v0, v1)
+}
+
 func (c *esContext) Uniform4f(uniform Uniform, v0, v1, v2, v3 float32) {
 	gl.Uniform4f(int32(uniform), v0, v1, v2, v3)
 }

+ 16 - 9
vendor/fyne.io/fyne/v2/internal/painter/gl/gl_gomobile.go

@@ -18,7 +18,6 @@ const (
 	bitDepthBuffer        = gl.DepthBufferBit
 	clampToEdge           = gl.ClampToEdge
 	colorFormatRGBA       = gl.RGBA
-	colorFormatR          = singleChannelColorFormat
 	compileStatus         = gl.CompileStatus
 	constantAlpha         = gl.ConstantAlpha
 	float                 = gl.Float
@@ -40,7 +39,6 @@ const (
 	textureWrapT          = gl.TextureWrapT
 	triangles             = gl.Triangles
 	triangleStrip         = gl.TriangleStrip
-	unpackAlignment       = gl.UnpackAlignment
 	unsignedByte          = gl.UnsignedByte
 	vertexShader          = gl.VertexShader
 )
@@ -58,6 +56,7 @@ type (
 	Uniform gl.Uniform
 )
 
+var compiled []Program // avoid multiple compilations with the re-used mobile GUI context
 var noBuffer = Buffer{}
 var noShader = Shader{}
 var textureFilterToGL = []int32{gl.Linear, gl.Nearest}
@@ -70,9 +69,17 @@ func (p *painter) Init() {
 	p.ctx = &mobileContext{glContext: p.contextProvider.Context().(gl.Context)}
 	p.glctx().Disable(gl.DepthTest)
 	p.glctx().Enable(gl.Blend)
-	p.program = p.createProgram("simple_es")
-	p.singleChannelProgram = p.createProgram("single_channel_es")
-	p.lineProgram = p.createProgram("line_es")
+	if compiled == nil {
+		compiled = []Program{
+			p.createProgram("simple_es"),
+			p.createProgram("line_es"),
+			p.createProgram("rectangle_es"),
+			p.createProgram("round_rectangle_es")}
+	}
+	p.program = compiled[0]
+	p.lineProgram = compiled[1]
+	p.rectangleProgram = compiled[2]
+	p.roundRectangleProgram = compiled[3]
 }
 
 // f32Bytes returns the byte representation of float32 values in the given byte
@@ -224,10 +231,6 @@ func (c *mobileContext) LinkProgram(program Program) {
 	c.glContext.LinkProgram(gl.Program(program))
 }
 
-func (c *mobileContext) PixelStorei(pname uint32, param int32) {
-	c.glContext.PixelStorei(gl.Enum(pname), param)
-}
-
 func (c *mobileContext) ReadBuffer(_ uint32) {
 }
 
@@ -264,6 +267,10 @@ func (c *mobileContext) Uniform1f(uniform Uniform, v float32) {
 	c.glContext.Uniform1f(gl.Uniform(uniform), v)
 }
 
+func (c *mobileContext) Uniform2f(uniform Uniform, v0, v1 float32) {
+	c.glContext.Uniform2f(gl.Uniform(uniform), v0, v1)
+}
+
 func (c *mobileContext) Uniform4f(uniform Uniform, v0, v1, v2, v3 float32) {
 	c.glContext.Uniform4f(gl.Uniform(uniform), v0, v1, v2, v3)
 }

+ 6 - 7
vendor/fyne.io/fyne/v2/internal/painter/gl/gl_goxjs.go

@@ -16,7 +16,6 @@ const (
 	bitDepthBuffer        = gl.DEPTH_BUFFER_BIT
 	clampToEdge           = gl.CLAMP_TO_EDGE
 	colorFormatRGBA       = gl.RGBA
-	colorFormatR          = gl.LUMINANCE
 	compileStatus         = gl.COMPILE_STATUS
 	constantAlpha         = gl.CONSTANT_ALPHA
 	float                 = gl.FLOAT
@@ -38,7 +37,6 @@ const (
 	textureWrapT          = gl.TEXTURE_WRAP_T
 	triangles             = gl.TRIANGLES
 	triangleStrip         = gl.TRIANGLE_STRIP
-	unpackAlignment       = gl.UNPACK_ALIGNMENT
 	unsignedByte          = gl.UNSIGNED_BYTE
 	vertexShader          = gl.VERTEX_SHADER
 )
@@ -66,8 +64,9 @@ func (p *painter) Init() {
 	gl.Enable(gl.BLEND)
 	p.logError()
 	p.program = p.createProgram("simple_es")
-	p.singleChannelProgram = p.createProgram("single_channel_es")
 	p.lineProgram = p.createProgram("line_es")
+	p.rectangleProgram = p.createProgram("rectangle_es")
+	p.roundRectangleProgram = p.createProgram("round_rectangle_es")
 }
 
 type xjsContext struct{}
@@ -186,10 +185,6 @@ func (c *xjsContext) LinkProgram(program Program) {
 	gl.LinkProgram(gl.Program(program))
 }
 
-func (c *xjsContext) PixelStorei(pname uint32, param int32) {
-	gl.PixelStorei(gl.Enum(pname), param)
-}
-
 func (c *xjsContext) ReadBuffer(_ uint32) {
 }
 
@@ -225,6 +220,10 @@ func (c *xjsContext) Uniform1f(uniform Uniform, v float32) {
 	gl.Uniform1f(gl.Uniform(uniform), v)
 }
 
+func (c *xjsContext) Uniform2f(uniform Uniform, v0, v1 float32) {
+	gl.Uniform2f(gl.Uniform(uniform), v0, v1)
+}
+
 func (c *xjsContext) Uniform4f(uniform Uniform, v0, v1, v2, v3 float32) {
 	gl.Uniform4f(gl.Uniform(uniform), v0, v1, v2, v3)
 }

+ 18 - 14
vendor/fyne.io/fyne/v2/internal/painter/gl/painter.go

@@ -20,12 +20,15 @@ func shaderSourceNamed(name string) ([]byte, []byte) {
 		return shaderSimpleVert.StaticContent, shaderSimpleFrag.StaticContent
 	case "simple_es":
 		return shaderSimpleesVert.StaticContent, shaderSimpleesFrag.StaticContent
-	case "single_channel":
-		return shaderSimpleVert.StaticContent, shaderSinglechannelFrag.StaticContent
-	case "single_channel_es":
-		return shaderSimpleesVert.StaticContent, shaderSinglechannelesFrag.StaticContent
+	case "rectangle":
+		return shaderRectangleVert.StaticContent, shaderRectangleFrag.StaticContent
+	case "round_rectangle":
+		return shaderRectangleVert.StaticContent, shaderRoundrectangleFrag.StaticContent
+	case "rectangle_es":
+		return shaderRectangleesVert.StaticContent, shaderRectangleesFrag.StaticContent
+	case "round_rectangle_es":
+		return shaderRectangleesVert.StaticContent, shaderRoundrectangleesFrag.StaticContent
 	}
-
 	return nil, nil
 }
 
@@ -60,14 +63,15 @@ func NewPainter(c fyne.Canvas, ctx driver.WithContext) Painter {
 }
 
 type painter struct {
-	canvas               fyne.Canvas
-	ctx                  context
-	contextProvider      driver.WithContext
-	program              Program
-	singleChannelProgram Program
-	lineProgram          Program
-	texScale             float32
-	pixScale             float32 // pre-calculate scale*texScale for each draw
+	canvas                fyne.Canvas
+	ctx                   context
+	contextProvider       driver.WithContext
+	program               Program
+	lineProgram           Program
+	rectangleProgram      Program
+	roundRectangleProgram Program
+	texScale              float32
+	pixScale              float32 // pre-calculate scale*texScale for each draw
 }
 
 // Declare conformity to Painter interface
@@ -179,5 +183,5 @@ func (p *painter) createProgram(shaderFilename string) Program {
 }
 
 func (p *painter) logError() {
-	logGLError(p.ctx.GetError())
+	logGLError(p.ctx.GetError)
 }

+ 30 - 10
vendor/fyne.io/fyne/v2/internal/painter/gl/shaders.go

@@ -25,6 +25,36 @@ var shaderLineesVert = &fyne.StaticResource{
 	StaticContent: []byte(
 		"#version 100\n\n#ifdef GL_ES\n# ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n# else\nprecision mediump float;\n#endif\nprecision mediump int;\nprecision lowp sampler2D;\n#endif\n\nattribute vec2 vert;\nattribute vec2 normal;\n    \nuniform float lineWidth;\n\nvarying vec2 delta;\n\nvoid main() {\n    delta = normal * lineWidth;\n\n    gl_Position = vec4(vert + delta, 0, 1);\n}\n"),
 }
+var shaderRectangleFrag = &fyne.StaticResource{
+	StaticName: "rectangle.frag",
+	StaticContent: []byte(
+		"#version 110\n\n/* scaled params */\nuniform vec2 frame_size;\nuniform vec4 rect_coords; //x1 [0], x2 [1], y1 [2], y2 [3]; coords of the rect_frame\nuniform float stroke_width;\n/* colors params*/\nuniform vec4 fill_color;\nuniform vec4 stroke_color;\n\n\nvoid main() {\n\n    vec4 color = fill_color;\n    \n    if (gl_FragCoord.x >= rect_coords[1] - stroke_width ){\n        color = stroke_color;\n    } else if (gl_FragCoord.x <= rect_coords[0] + stroke_width){\n        color = stroke_color;\n    } else if (gl_FragCoord.y <= frame_size.y - rect_coords[3] + stroke_width ){\n        color = stroke_color;\n    } else if (gl_FragCoord.y >= frame_size.y - rect_coords[2] - stroke_width ){\n        color = stroke_color;\n    }\n\n    gl_FragColor = color;\n}\n"),
+}
+var shaderRectangleVert = &fyne.StaticResource{
+	StaticName: "rectangle.vert",
+	StaticContent: []byte(
+		"#version 110\n\nattribute vec2 vert;\nattribute vec2 normal;\n\nvoid main() {\n    gl_Position = vec4(vert+normal, 0, 1);\n}\n"),
+}
+var shaderRectangleesFrag = &fyne.StaticResource{
+	StaticName: "rectangle_es.frag",
+	StaticContent: []byte(
+		"#version 100\n\n#ifdef GL_ES\n# ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n# else\nprecision mediump float;\n#endif\nprecision mediump int;\nprecision lowp sampler2D;\n#endif\n\n/* scaled params */\nuniform vec2 frame_size;\nuniform vec4 rect_coords; //x1 [0], x2 [1], y1 [2], y2 [3]; coords of the rect_frame\nuniform float stroke_width;\n/* colors params*/\nuniform vec4 fill_color;\nuniform vec4 stroke_color;\n\n\nvoid main() {\n\n    vec4 color = fill_color;\n    \n    if (gl_FragCoord.x >= rect_coords[1] - stroke_width ){\n        color = stroke_color;\n    } else if (gl_FragCoord.x <= rect_coords[0] + stroke_width){\n        color = stroke_color;\n    } else if (gl_FragCoord.y <= frame_size.y - rect_coords[3] + stroke_width ){\n        color = stroke_color;\n    } else if (gl_FragCoord.y >= frame_size.y - rect_coords[2] - stroke_width ){\n        color = stroke_color;\n    }\n\n    gl_FragColor = color;\n}\n"),
+}
+var shaderRectangleesVert = &fyne.StaticResource{
+	StaticName: "rectangle_es.vert",
+	StaticContent: []byte(
+		"#version 100\n\n#ifdef GL_ES\n# ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n# else\nprecision mediump float;\n#endif\nprecision mediump int;\nprecision lowp sampler2D;\n#endif\n\nattribute vec2 vert;\nattribute vec2 normal;\n\nvoid main() {\n    gl_Position = vec4(vert+normal, 0, 1);\n}\n"),
+}
+var shaderRoundrectangleFrag = &fyne.StaticResource{
+	StaticName: "round_rectangle.frag",
+	StaticContent: []byte(
+		"#version 110\n\n/* scaled params */\nuniform vec2 frame_size;\nuniform vec4 rect_coords; //x1 [0], x2 [1], y1 [2], y2 [3]; coords of the rect_frame\nuniform float stroke_width_half;\nuniform vec2 rect_size_half;\nuniform float radius;\n/* colors params*/\nuniform vec4 fill_color;\nuniform vec4 stroke_color;\n\nfloat calc_distance(vec2 p, vec2 b, float r)\n{\n    vec2 d = abs(p) - b + vec2(r);\n\treturn min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;   \n}\n\nvoid main() {\n\n    vec4 frag_rect_coords = vec4(rect_coords[0], rect_coords[1], frame_size.y - rect_coords[3], frame_size.y - rect_coords[2]);\n    vec2 vec_centered_pos = (gl_FragCoord.xy - vec2(frag_rect_coords[0] + frag_rect_coords[1], frag_rect_coords[2] + frag_rect_coords[3]) * 0.5);\n\n    float distance = calc_distance(vec_centered_pos, rect_size_half, radius - stroke_width_half);\n\n    vec4 from_color = stroke_color; //Always the border color. If no border, this still should be set\n    vec4 to_color = stroke_color; //Outside color\n\n    if (stroke_width_half == 0.0)\n    {\n        from_color = fill_color;\n        to_color = fill_color;\n    }\n    to_color[3] = 0.0; // blend the fill colour to alpha\n\n    if (distance < 0.0)\n    {\n        to_color = fill_color;\n    } \n\n    distance = abs(distance) - stroke_width_half;\n\n    float blend_amount = smoothstep(-1.0, 1.0, distance);\n\n    // final color\n    gl_FragColor = mix(from_color, to_color, blend_amount);\n}\n"),
+}
+var shaderRoundrectangleesFrag = &fyne.StaticResource{
+	StaticName: "round_rectangle_es.frag",
+	StaticContent: []byte(
+		"#version 100\n\n#ifdef GL_ES\n# ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n# else\nprecision mediump float;\n#endif\nprecision mediump int;\nprecision lowp sampler2D;\n#endif\n\n/* scaled params */\nuniform vec2 frame_size;\nuniform vec4 rect_coords; //x1 [0], x2 [1], y1 [2], y2 [3]; coords of the rect_frame\nuniform float stroke_width_half;\nuniform vec2 rect_size_half;\nuniform float radius;\n/* colors params*/\nuniform vec4 fill_color;\nuniform vec4 stroke_color;\n\nfloat calc_distance(vec2 p, vec2 b, float r)\n{\n    vec2 d = abs(p) - b + vec2(r);\n\treturn min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;   \n}\n\nvoid main() {\n\n    vec4 frag_rect_coords = vec4(rect_coords[0], rect_coords[1], frame_size.y - rect_coords[3], frame_size.y - rect_coords[2]);\n    vec2 vec_centered_pos = (gl_FragCoord.xy - vec2(frag_rect_coords[0] + frag_rect_coords[1], frag_rect_coords[2] + frag_rect_coords[3]) * 0.5);\n\n    float distance = calc_distance(vec_centered_pos, rect_size_half, radius - stroke_width_half);\n\n    vec4 from_color = stroke_color; //Always the border color. If no border, this still should be set\n    vec4 to_color = stroke_color; //Outside color\n\n    if (stroke_width_half == 0.0)\n    {\n        from_color = fill_color;\n        to_color = fill_color;\n    }\n    to_color[3] = 0.0; // blend the fill colour to alpha\n\n    if (distance < 0.0)\n    {\n        to_color = fill_color;\n    } \n\n    distance = abs(distance) - stroke_width_half;\n\n    float blend_amount = smoothstep(-1.0, 1.0, distance);\n\n    // final color\n    gl_FragColor = mix(from_color, to_color, blend_amount);\n}\n"),
+}
 var shaderSimpleFrag = &fyne.StaticResource{
 	StaticName: "simple.frag",
 	StaticContent: []byte(
@@ -45,13 +75,3 @@ var shaderSimpleesVert = &fyne.StaticResource{
 	StaticContent: []byte(
 		"#version 100\n\n#ifdef GL_ES\n# ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n# else\nprecision mediump float;\n#endif\nprecision mediump int;\nprecision lowp sampler2D;\n#endif\n\nattribute vec3 vert;\nattribute vec2 vertTexCoord;\nvarying vec2 fragTexCoord;\n\nvoid main() {\n    fragTexCoord = vertTexCoord;\n\n    gl_Position = vec4(vert, 1);\n}"),
 }
-var shaderSinglechannelFrag = &fyne.StaticResource{
-	StaticName: "single_channel.frag",
-	StaticContent: []byte(
-		"#version 110\n\nuniform vec4 color;\n\nuniform sampler2D tex;\nvarying vec2 fragTexCoord;\n\nvoid main()\n{\n    gl_FragColor = vec4(color.r, color.g, color.b, texture2D(tex, fragTexCoord).r*color.a);\n}\n"),
-}
-var shaderSinglechannelesFrag = &fyne.StaticResource{
-	StaticName: "single_channel_es.frag",
-	StaticContent: []byte(
-		"#version 100\n\n#ifdef GL_ES\n# ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n# else\nprecision mediump float;\n#endif\nprecision mediump int;\nprecision lowp sampler2D;\n#endif\n\nuniform vec4 color;\n\nuniform sampler2D tex;\nvarying vec2 fragTexCoord;\n\nvoid main()\n{\n    gl_FragColor = vec4(color.r, color.g, color.b, texture2D(tex, fragTexCoord).r*color.a);\n}\n"),
-}

+ 8 - 41
vendor/fyne.io/fyne/v2/internal/painter/gl/texture.go

@@ -3,7 +3,6 @@ package gl
 import (
 	"fmt"
 	"image"
-	"image/color"
 	"image/draw"
 	"math"
 
@@ -11,6 +10,7 @@ import (
 	"fyne.io/fyne/v2/canvas"
 	"fyne.io/fyne/v2/internal/cache"
 	paint "fyne.io/fyne/v2/internal/painter"
+	"fyne.io/fyne/v2/theme"
 )
 
 var noTexture = Texture(cache.NoTexture)
@@ -77,25 +77,6 @@ func (p *painter) imgToTexture(img image.Image, textureFilter canvas.ImageScale)
 		)
 		p.logError()
 		return texture
-	case *image.Gray:
-		if len(i.Pix) == 0 { // image is empty
-			return noTexture
-		}
-
-		p.ctx.PixelStorei(unpackAlignment, 1) // OpenGL expects 4 byte alignment for images which is not guaranteed for image.Gray
-		texture := p.newTexture(textureFilter)
-		p.ctx.TexImage2D(
-			texture2D,
-			0,
-			i.Bounds().Dx(),
-			i.Bounds().Dy(),
-			colorFormatR,
-			unsignedByte,
-			i.Pix,
-		)
-		p.ctx.PixelStorei(unpackAlignment, 4) // Reset to default for performance reasons
-		p.logError()
-		return texture
 	default:
 		rgba := image.NewRGBA(image.Rect(0, 0, img.Bounds().Dx(), img.Bounds().Dy()))
 		draw.Draw(rgba, rgba.Rect, img, image.Point{}, draw.Over)
@@ -159,34 +140,20 @@ func (p *painter) newGlRasterTexture(obj fyne.CanvasObject) Texture {
 	return p.imgToTexture(rast.Generator(int(width), int(height)), rast.ScaleMode)
 }
 
-func (p *painter) newGlRectTexture(obj fyne.CanvasObject) Texture {
-	rect := obj.(*canvas.Rectangle)
-	if rect.StrokeColor != nil && rect.StrokeWidth > 0 {
-		return p.newGlStrokedRectTexture(rect)
-	}
-	if rect.FillColor == nil {
-		return noTexture
-	}
-	return p.imgToTexture(image.NewUniform(rect.FillColor), canvas.ImageScaleSmooth)
-}
-
-func (p *painter) newGlStrokedRectTexture(obj fyne.CanvasObject) Texture {
-	rect := obj.(*canvas.Rectangle)
-	raw := paint.DrawRectangle(rect, paint.VectorPad(rect), p.textureScale)
-
-	return p.imgToTexture(raw, canvas.ImageScaleSmooth)
-}
-
 func (p *painter) newGlTextTexture(obj fyne.CanvasObject) Texture {
 	text := obj.(*canvas.Text)
+	color := text.Color
+	if color == nil {
+		color = theme.ForegroundColor()
+	}
 
 	bounds := text.MinSize()
 	width := int(math.Ceil(float64(p.textureScale(bounds.Width) + paint.VectorPad(text)))) // potentially italic overspill
 	height := int(math.Ceil(float64(p.textureScale(bounds.Height))))
-	img := image.NewGray(image.Rect(0, 0, width, height))
+	img := image.NewNRGBA(image.Rect(0, 0, width, height))
 
-	face, measureFace := paint.CachedFontFace(text.TextStyle, text.TextSize*p.canvas.Scale(), p.texScale)
-	paint.DrawString(img, text.Text, color.White, face, measureFace, text.TextSize, p.pixScale, height, text.TextStyle.TabWidth)
+	face := paint.CachedFontFace(text.TextStyle, text.TextSize*p.canvas.Scale(), p.texScale)
+	paint.DrawString(img, text.Text, color, face.Fonts, text.TextSize, p.pixScale, text.TextStyle.TabWidth)
 	return p.imgToTexture(img, canvas.ImageScaleSmooth)
 }
 

+ 11 - 136
vendor/fyne.io/fyne/v2/internal/painter/image.go

@@ -1,149 +1,45 @@
 package painter
 
 import (
-	"bytes"
-	"fmt"
 	"image"
 	_ "image/jpeg" // avoid users having to import when using image widget
 	_ "image/png"  // avoid the same for PNG images
-	"io"
-	"os"
-	"path/filepath"
-	"strings"
 
 	"golang.org/x/image/draw"
 
 	"fyne.io/fyne/v2"
 	"fyne.io/fyne/v2/canvas"
-	"fyne.io/fyne/v2/internal"
-	"fyne.io/fyne/v2/internal/cache"
-	"fyne.io/fyne/v2/internal/svg"
 )
 
-var aspects = make(map[interface{}]float32, 16)
-
-// GetAspect looks up an aspect ratio of an image
-func GetAspect(img *canvas.Image) float32 {
-	aspect := float32(0.0)
-	if img.Resource != nil {
-		aspect = aspects[img.Resource.Name()]
-	} else if img.File != "" {
-		aspect = aspects[img.File]
-	} else if img.Image != nil {
-		// HOTFIX until Fyne 2.4 proper fix:
-		// we are not storing the aspect ratio in the map for the image.Image case
-		size := img.Image.Bounds().Size()
-		return float32(size.X) / float32(size.Y)
-	}
-
-	if aspect == 0 {
-		aspect = aspects[img]
-	}
-
-	return aspect
-}
-
 // PaintImage renders a given fyne Image to a Go standard image
 // If a fyne.Canvas is given and the image’s fill mode is “fill original” the image’s min size has
 // to fit its original size. If it doesn’t, PaintImage does not paint the image but adjusts its min size.
 // The image will then be painted on the next frame because of the min size change.
 func PaintImage(img *canvas.Image, c fyne.Canvas, width, height int) image.Image {
-	var wantOrigW, wantOrigH int
-	wantOrigSize := false
-	if img.FillMode == canvas.ImageFillOriginal && c != nil {
-		wantOrigW = internal.ScaleInt(c, img.MinSize().Width)
-		wantOrigH = internal.ScaleInt(c, img.MinSize().Height)
-		wantOrigSize = true
+	if img.Size().IsZero() && c == nil { // an image without size or canvas won't get rendered unless we setup
+		img.Resize(fyne.NewSize(float32(width), float32(height)))
 	}
-
-	dst, origW, origH, err := paintImage(img, width, height, wantOrigSize, wantOrigW, wantOrigH)
+	dst, err := paintImage(img, width, height)
 	if err != nil {
 		fyne.LogError("failed to paint image", err)
-		return nil
 	}
 
-	if wantOrigSize && dst == nil {
-		dpSize := fyne.NewSize(internal.UnscaleInt(c, origW), internal.UnscaleInt(c, origH))
-		img.SetMinSize(dpSize)
-		canvas.Refresh(img) // force the initial size to be respected
-	}
 	return dst
 }
 
-func paintImage(img *canvas.Image, width, height int, wantOrigSize bool, wantOrigW, wantOrigH int) (dst image.Image, origW, origH int, err error) {
-	if (width <= 0 || height <= 0) && !wantOrigSize {
+func paintImage(img *canvas.Image, width, height int) (dst image.Image, err error) {
+	if width <= 0 || height <= 0 {
 		return
 	}
 
-	var aspectCacheKey interface{} = img
-	checkSize := func(origW, origH int) bool {
-		aspect := float32(origW) / float32(origH)
-		// this is used by our render code, so let's set it to the file aspect
-		aspects[aspectCacheKey] = aspect
-		return !wantOrigSize || (wantOrigW == origW && wantOrigH == origH)
+	dst = img.Image
+	if dst == nil {
+		dst = image.NewNRGBA(image.Rect(0, 0, width, height))
 	}
 
-	switch {
-	case img.File != "" || img.Resource != nil:
-		var (
-			file  io.Reader
-			name  string
-			isSVG bool
-		)
-		if img.Resource != nil {
-			name = img.Resource.Name()
-			file = bytes.NewReader(img.Resource.Content())
-			isSVG = IsResourceSVG(img.Resource)
-		} else {
-			name = img.File
-			var handle *os.File
-			handle, err = os.Open(img.File)
-			if err != nil {
-				err = fmt.Errorf("image load error: %w", err)
-				return
-			}
-			defer handle.Close()
-			file = handle
-			isSVG = isFileSVG(img.File)
-		}
-		aspectCacheKey = name
-
-		if isSVG {
-			tex := cache.GetSvg(name, width, height)
-			if tex == nil {
-				// Not in cache, so load the item and add to cache
-				tex, err = svg.ToImage(file, width, height, checkSize)
-				if err != nil {
-					return
-				}
-
-				cache.SetSvg(name, tex, width, height)
-			}
-			dst = tex
-		} else {
-			var pixels image.Image
-			pixels, _, err = image.Decode(file)
-			if err != nil {
-				err = fmt.Errorf("failed to decode image: %w", err)
-				return
-			}
-
-			origSize := pixels.Bounds().Size()
-			origW, origH = origSize.X, origSize.Y
-			if checkSize(origSize.X, origSize.Y) {
-				dst = scaleImage(pixels, width, height, img.ScaleMode)
-			}
-		}
-	case img.Image != nil:
-		origSize := img.Image.Bounds().Size()
-		origW, origH = origSize.X, origSize.Y
-		// HOTFIX until Fyne 2.4: don't store aspect ratio in map, as checkSize(x, y) does.
-		// Doing so leaks a reference to the image.Image data
-		if !wantOrigSize || (wantOrigW == origW && wantOrigH == origH) {
-			dst = scaleImage(img.Image, width, height, img.ScaleMode)
-		}
-	default:
-		dst = image.NewNRGBA(image.Rect(0, 0, 1, 1))
+	size := dst.Bounds().Size()
+	if width != size.X || height != size.Y {
+		dst = scaleImage(dst, width, height, img.ScaleMode)
 	}
 	return
 }
@@ -169,24 +65,3 @@ func scaleImage(pixels image.Image, scaledW, scaledH int, scale canvas.ImageScal
 	}
 	return tex
 }
-
-func isFileSVG(path string) bool {
-	return strings.ToLower(filepath.Ext(path)) == ".svg"
-}
-
-// IsResourceSVG checks if the resource is an SVG or not.
-func IsResourceSVG(res fyne.Resource) bool {
-	if strings.ToLower(filepath.Ext(res.Name())) == ".svg" {
-		return true
-	}
-
-	if len(res.Content()) < 5 {
-		return false
-	}
-
-	switch strings.ToLower(string(res.Content()[:5])) {
-	case "<!doc", "<?xml", "<svg ":
-		return true
-	}
-	return false
-}

+ 30 - 30
vendor/fyne.io/fyne/v2/internal/painter/software/draw.go

@@ -7,8 +7,8 @@ import (
 
 	"fyne.io/fyne/v2"
 	"fyne.io/fyne/v2/canvas"
-	"fyne.io/fyne/v2/internal"
 	"fyne.io/fyne/v2/internal/painter"
+	"fyne.io/fyne/v2/internal/scale"
 	"fyne.io/fyne/v2/theme"
 
 	"golang.org/x/image/draw"
@@ -21,9 +21,9 @@ type gradient interface {
 
 func drawCircle(c fyne.Canvas, circle *canvas.Circle, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) {
 	pad := painter.VectorPad(circle)
-	scaledWidth := internal.ScaleInt(c, circle.Size().Width+pad*2)
-	scaledHeight := internal.ScaleInt(c, circle.Size().Height+pad*2)
-	scaledX, scaledY := internal.ScaleInt(c, pos.X-pad), internal.ScaleInt(c, pos.Y-pad)
+	scaledWidth := scale.ToScreenCoordinate(c, circle.Size().Width+pad*2)
+	scaledHeight := scale.ToScreenCoordinate(c, circle.Size().Height+pad*2)
+	scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X-pad), scale.ToScreenCoordinate(c, pos.Y-pad)
 	bounds := clip.Intersect(image.Rect(scaledX, scaledY, scaledX+scaledWidth, scaledY+scaledHeight))
 
 	raw := painter.DrawCircle(circle, pad, func(in float32) float32 {
@@ -43,10 +43,10 @@ func drawCircle(c fyne.Canvas, circle *canvas.Circle, pos fyne.Position, base *i
 
 func drawGradient(c fyne.Canvas, g gradient, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) {
 	bounds := g.Size()
-	width := internal.ScaleInt(c, bounds.Width)
-	height := internal.ScaleInt(c, bounds.Height)
+	width := scale.ToScreenCoordinate(c, bounds.Width)
+	height := scale.ToScreenCoordinate(c, bounds.Height)
 	tex := g.Generate(width, height)
-	drawTex(internal.ScaleInt(c, pos.X), internal.ScaleInt(c, pos.Y), width, height, base, tex, clip)
+	drawTex(scale.ToScreenCoordinate(c, pos.X), scale.ToScreenCoordinate(c, pos.Y), width, height, base, tex, clip)
 }
 
 func drawImage(c fyne.Canvas, img *canvas.Image, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) {
@@ -54,14 +54,14 @@ func drawImage(c fyne.Canvas, img *canvas.Image, pos fyne.Position, base *image.
 	if bounds.IsZero() {
 		return
 	}
-	width := internal.ScaleInt(c, bounds.Width)
-	height := internal.ScaleInt(c, bounds.Height)
-	scaledX, scaledY := internal.ScaleInt(c, pos.X), internal.ScaleInt(c, pos.Y)
+	width := scale.ToScreenCoordinate(c, bounds.Width)
+	height := scale.ToScreenCoordinate(c, bounds.Height)
+	scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X), scale.ToScreenCoordinate(c, pos.Y)
 
 	origImg := painter.PaintImage(img, c, width, height)
 
 	if img.FillMode == canvas.ImageFillContain {
-		imgAspect := painter.GetAspect(img)
+		imgAspect := img.Aspect()
 		objAspect := float32(width) / float32(height)
 
 		if objAspect > imgAspect {
@@ -104,9 +104,9 @@ func drawPixels(x, y, width, height int, mode canvas.ImageScale, base *image.NRG
 
 func drawLine(c fyne.Canvas, line *canvas.Line, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) {
 	pad := painter.VectorPad(line)
-	scaledWidth := internal.ScaleInt(c, line.Size().Width+pad*2)
-	scaledHeight := internal.ScaleInt(c, line.Size().Height+pad*2)
-	scaledX, scaledY := internal.ScaleInt(c, pos.X-pad), internal.ScaleInt(c, pos.Y-pad)
+	scaledWidth := scale.ToScreenCoordinate(c, line.Size().Width+pad*2)
+	scaledHeight := scale.ToScreenCoordinate(c, line.Size().Height+pad*2)
+	scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X-pad), scale.ToScreenCoordinate(c, pos.Y-pad)
 	bounds := clip.Intersect(image.Rect(scaledX, scaledY, scaledX+scaledWidth, scaledY+scaledHeight))
 
 	raw := painter.DrawLine(line, pad, func(in float32) float32 {
@@ -133,8 +133,8 @@ func drawTex(x, y, width, height int, base *image.NRGBA, tex image.Image, clip i
 
 func drawText(c fyne.Canvas, text *canvas.Text, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) {
 	bounds := text.MinSize()
-	width := internal.ScaleInt(c, bounds.Width+painter.VectorPad(text))
-	height := internal.ScaleInt(c, bounds.Height)
+	width := scale.ToScreenCoordinate(c, bounds.Width+painter.VectorPad(text))
+	height := scale.ToScreenCoordinate(c, bounds.Height)
 	txtImg := image.NewRGBA(image.Rect(0, 0, width, height))
 
 	color := text.Color
@@ -142,8 +142,8 @@ func drawText(c fyne.Canvas, text *canvas.Text, pos fyne.Position, base *image.N
 		color = theme.ForegroundColor()
 	}
 
-	face, measureFace := painter.CachedFontFace(text.TextStyle, text.TextSize*c.Scale(), 1)
-	painter.DrawString(txtImg, text.Text, color, face, measureFace, text.TextSize, c.Scale(), height, text.TextStyle.TabWidth)
+	face := painter.CachedFontFace(text.TextStyle, text.TextSize*c.Scale(), 1)
+	painter.DrawString(txtImg, text.Text, color, face.Fonts, text.TextSize, c.Scale(), text.TextStyle.TabWidth)
 
 	size := text.Size()
 	offsetX := float32(0)
@@ -157,8 +157,8 @@ func drawText(c fyne.Canvas, text *canvas.Text, pos fyne.Position, base *image.N
 	if size.Height > bounds.Height {
 		offsetY = (size.Height - bounds.Height) / 2
 	}
-	scaledX := internal.ScaleInt(c, pos.X+offsetX)
-	scaledY := internal.ScaleInt(c, pos.Y+offsetY)
+	scaledX := scale.ToScreenCoordinate(c, pos.X+offsetX)
+	scaledY := scale.ToScreenCoordinate(c, pos.Y+offsetY)
 	imgBounds := image.Rect(scaledX, scaledY, scaledX+width, scaledY+height)
 	clippedBounds := clip.Intersect(imgBounds)
 	srcPt := image.Point{X: clippedBounds.Min.X - imgBounds.Min.X, Y: clippedBounds.Min.Y - imgBounds.Min.Y}
@@ -170,9 +170,9 @@ func drawRaster(c fyne.Canvas, rast *canvas.Raster, pos fyne.Position, base *ima
 	if bounds.IsZero() {
 		return
 	}
-	width := internal.ScaleInt(c, bounds.Width)
-	height := internal.ScaleInt(c, bounds.Height)
-	scaledX, scaledY := internal.ScaleInt(c, pos.X), internal.ScaleInt(c, pos.Y)
+	width := scale.ToScreenCoordinate(c, bounds.Width)
+	height := scale.ToScreenCoordinate(c, bounds.Height)
+	scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X), scale.ToScreenCoordinate(c, pos.Y)
 
 	pix := rast.Generator(width, height)
 	if pix.Bounds().Bounds().Dx() != width || pix.Bounds().Dy() != height {
@@ -184,9 +184,9 @@ func drawRaster(c fyne.Canvas, rast *canvas.Raster, pos fyne.Position, base *ima
 
 func drawRectangleStroke(c fyne.Canvas, rect *canvas.Rectangle, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) {
 	pad := painter.VectorPad(rect)
-	scaledWidth := internal.ScaleInt(c, rect.Size().Width+pad*2)
-	scaledHeight := internal.ScaleInt(c, rect.Size().Height+pad*2)
-	scaledX, scaledY := internal.ScaleInt(c, pos.X-pad), internal.ScaleInt(c, pos.Y-pad)
+	scaledWidth := scale.ToScreenCoordinate(c, rect.Size().Width+pad*2)
+	scaledHeight := scale.ToScreenCoordinate(c, rect.Size().Height+pad*2)
+	scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X-pad), scale.ToScreenCoordinate(c, pos.Y-pad)
 	bounds := clip.Intersect(image.Rect(scaledX, scaledY, scaledX+scaledWidth, scaledY+scaledHeight))
 
 	raw := painter.DrawRectangle(rect, pad, func(in float32) float32 {
@@ -205,14 +205,14 @@ func drawRectangleStroke(c fyne.Canvas, rect *canvas.Rectangle, pos fyne.Positio
 }
 
 func drawRectangle(c fyne.Canvas, rect *canvas.Rectangle, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) {
-	if rect.StrokeColor != nil && rect.StrokeWidth > 0 { // use a rasterizer if there is a stroke
+	if (rect.StrokeColor != nil && rect.StrokeWidth > 0) || rect.CornerRadius != 0 { // use a rasterizer if there is a stroke or radius
 		drawRectangleStroke(c, rect, pos, base, clip)
 		return
 	}
 
-	scaledWidth := internal.ScaleInt(c, rect.Size().Width)
-	scaledHeight := internal.ScaleInt(c, rect.Size().Height)
-	scaledX, scaledY := internal.ScaleInt(c, pos.X), internal.ScaleInt(c, pos.Y)
+	scaledWidth := scale.ToScreenCoordinate(c, rect.Size().Width)
+	scaledHeight := scale.ToScreenCoordinate(c, rect.Size().Height)
+	scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X), scale.ToScreenCoordinate(c, pos.Y)
 	bounds := clip.Intersect(image.Rect(scaledX, scaledY, scaledX+scaledWidth, scaledY+scaledHeight))
 	draw.Draw(base, bounds, image.NewUniform(rect.FillColor), image.Point{}, draw.Over)
 }

+ 6 - 6
vendor/fyne.io/fyne/v2/internal/painter/software/painter.go

@@ -5,8 +5,8 @@ import (
 
 	"fyne.io/fyne/v2"
 	"fyne.io/fyne/v2/canvas"
-	"fyne.io/fyne/v2/internal"
 	"fyne.io/fyne/v2/internal/driver"
+	"fyne.io/fyne/v2/internal/scale"
 )
 
 // Painter is a simple software painter that can paint a canvas in memory.
@@ -22,17 +22,17 @@ func NewPainter() *Painter {
 // The canvas to be drawn is passed in as a parameter and the return is an
 // image containing the result of rendering.
 func (*Painter) Paint(c fyne.Canvas) image.Image {
-	bounds := image.Rect(0, 0, internal.ScaleInt(c, c.Size().Width), internal.ScaleInt(c, c.Size().Height))
+	bounds := image.Rect(0, 0, scale.ToScreenCoordinate(c, c.Size().Width), scale.ToScreenCoordinate(c, c.Size().Height))
 	base := image.NewNRGBA(bounds)
 
 	paint := func(obj fyne.CanvasObject, pos, clipPos fyne.Position, clipSize fyne.Size) bool {
 		w := fyne.Min(clipPos.X+clipSize.Width, c.Size().Width)
 		h := fyne.Min(clipPos.Y+clipSize.Height, c.Size().Height)
 		clip := image.Rect(
-			internal.ScaleInt(c, clipPos.X),
-			internal.ScaleInt(c, clipPos.Y),
-			internal.ScaleInt(c, w),
-			internal.ScaleInt(c, h),
+			scale.ToScreenCoordinate(c, clipPos.X),
+			scale.ToScreenCoordinate(c, clipPos.Y),
+			scale.ToScreenCoordinate(c, w),
+			scale.ToScreenCoordinate(c, h),
 		)
 		switch o := obj.(type) {
 		case *canvas.Image:

+ 0 - 1
vendor/fyne.io/fyne/v2/internal/painter/vector.go

@@ -27,7 +27,6 @@ func VectorPad(obj fyne.CanvasObject) float32 {
 		if co.TextStyle.Italic {
 			return co.TextSize / 5 // make sure that even a 20% lean does not overflow
 		}
-		return co.TextSize / 5 // TODO remove after we get our new text rendering all sorted in 2.4 - #3500
 	}
 
 	return 0

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff