Browse Source

SVI Добавление кода, уборка старого

SVI 1 year ago
parent
commit
f0de518f93
100 changed files with 11395 additions and 228 deletions
  1. 8 1
      Makefile
  2. 0 52
      cmd/desc_lorca/main.go
  3. 0 43
      desc_lorca/desc_lorca.go
  4. 0 36
      desc_lorca/lorca_gui/lorca_gui.go
  5. 19 0
      desktop_cogent/desktop_cogent.go
  6. 38 20
      go.mod
  7. 83 49
      go.sum
  8. 0 9
      pkg/types/igui.go
  9. 2 2
      server/serv_bots/serv_bots.go
  10. 1 1
      server/serv_bots/warbot/angar/base/arsenal/arsenal.go
  11. 3 3
      server/serv_bots/warbot/angar/base/bank/bank.go
  12. 5 5
      server/serv_bots/warbot/angar/tank_params/tank_params.go
  13. 1 1
      server/serv_bots/warbot/warbot.go
  14. 2 2
      server/serv_bots/warbot/warbot_config/warbot_config.go
  15. 2 2
      server/serv_bots/warbot/warbot_net/bot_cookie/bot_cookie.go
  16. 2 2
      server/serv_bots/warbot/warbot_net/warbot_net.go
  17. 28 0
      vendor/cogentcore.org/core/LICENSE
  18. 24 0
      vendor/cogentcore.org/core/base/bools/booler.go
  19. 246 0
      vendor/cogentcore.org/core/base/datasize/datasize.go
  20. 46 0
      vendor/cogentcore.org/core/base/elide/elide.go
  21. 107 0
      vendor/cogentcore.org/core/base/errors/errors.go
  22. 84 0
      vendor/cogentcore.org/core/base/errors/stdlib.go
  23. 89 0
      vendor/cogentcore.org/core/base/fileinfo/enumgen.go
  24. 99 0
      vendor/cogentcore.org/core/base/fileinfo/filecat.go
  25. 393 0
      vendor/cogentcore.org/core/base/fileinfo/fileinfo.go
  26. 153 0
      vendor/cogentcore.org/core/base/fileinfo/icons.go
  27. 301 0
      vendor/cogentcore.org/core/base/fileinfo/known.go
  28. 254 0
      vendor/cogentcore.org/core/base/fileinfo/mimedata/mimedata.go
  29. 1223 0
      vendor/cogentcore.org/core/base/fileinfo/mimetype.go
  30. 9 0
      vendor/cogentcore.org/core/base/fileinfo/typegen.go
  31. 79 0
      vendor/cogentcore.org/core/base/fsx/fs.go
  32. 206 0
      vendor/cogentcore.org/core/base/fsx/fsx.go
  33. 50 0
      vendor/cogentcore.org/core/base/indent/enumgen.go
  34. 68 0
      vendor/cogentcore.org/core/base/indent/indent.go
  35. 9 0
      vendor/cogentcore.org/core/base/iox/README.md
  36. 88 0
      vendor/cogentcore.org/core/base/iox/decoder.go
  37. 59 0
      vendor/cogentcore.org/core/base/iox/encoder.go
  38. 101 0
      vendor/cogentcore.org/core/base/iox/imagex/base64.go
  39. 48 0
      vendor/cogentcore.org/core/base/iox/imagex/enumgen.go
  40. 153 0
      vendor/cogentcore.org/core/base/iox/imagex/imagex.go
  41. 142 0
      vendor/cogentcore.org/core/base/iox/imagex/testing.go
  42. 9 0
      vendor/cogentcore.org/core/base/iox/imagex/testing_noupdate.go
  43. 9 0
      vendor/cogentcore.org/core/base/iox/imagex/testing_update.go
  44. 86 0
      vendor/cogentcore.org/core/base/iox/jsonx/jsonx.go
  45. 83 0
      vendor/cogentcore.org/core/base/iox/tomlx/tomlx.go
  46. 122 0
      vendor/cogentcore.org/core/base/labels/friendly.go
  47. 43 0
      vendor/cogentcore.org/core/base/labels/labeler.go
  48. 46 0
      vendor/cogentcore.org/core/base/nptime/nptime.go
  49. 13 0
      vendor/cogentcore.org/core/base/num/abs.go
  50. 36 0
      vendor/cogentcore.org/core/base/num/bool.go
  51. 54 0
      vendor/cogentcore.org/core/base/num/constraints.go
  52. 54 0
      vendor/cogentcore.org/core/base/option/option.go
  53. 9 0
      vendor/cogentcore.org/core/base/ordmap/README.md
  54. 256 0
      vendor/cogentcore.org/core/base/ordmap/ordmap.go
  55. 87 0
      vendor/cogentcore.org/core/base/plan/update.go
  56. 185 0
      vendor/cogentcore.org/core/base/profile/profile.go
  57. 25 0
      vendor/cogentcore.org/core/base/reflectx/interfaces.go
  58. 239 0
      vendor/cogentcore.org/core/base/reflectx/maps.go
  59. 127 0
      vendor/cogentcore.org/core/base/reflectx/pointers.go
  60. 372 0
      vendor/cogentcore.org/core/base/reflectx/slices.go
  61. 231 0
      vendor/cogentcore.org/core/base/reflectx/structs.go
  62. 57 0
      vendor/cogentcore.org/core/base/reflectx/types.go
  63. 1114 0
      vendor/cogentcore.org/core/base/reflectx/values.go
  64. 139 0
      vendor/cogentcore.org/core/base/slicesx/slicesx.go
  65. 4 0
      vendor/cogentcore.org/core/base/strcase/README.md
  66. 74 0
      vendor/cogentcore.org/core/base/strcase/cases.go
  67. 150 0
      vendor/cogentcore.org/core/base/strcase/convert.go
  68. 89 0
      vendor/cogentcore.org/core/base/strcase/enumgen.go
  69. 34 0
      vendor/cogentcore.org/core/base/strcase/list.go
  70. 67 0
      vendor/cogentcore.org/core/base/strcase/split.go
  71. 60 0
      vendor/cogentcore.org/core/base/strcase/strcase.go
  72. 55 0
      vendor/cogentcore.org/core/base/strcase/unicode.go
  73. 90 0
      vendor/cogentcore.org/core/base/stringsx/stringsx.go
  74. 41 0
      vendor/cogentcore.org/core/base/tiered/tiered.go
  75. 17 0
      vendor/cogentcore.org/core/base/vcs/README.md
  76. 89 0
      vendor/cogentcore.org/core/base/vcs/enumgen.go
  77. 65 0
      vendor/cogentcore.org/core/base/vcs/files.go
  78. 355 0
      vendor/cogentcore.org/core/base/vcs/git.go
  79. 34 0
      vendor/cogentcore.org/core/base/vcs/log.go
  80. 279 0
      vendor/cogentcore.org/core/base/vcs/svn.go
  81. 139 0
      vendor/cogentcore.org/core/base/vcs/vcs.go
  82. 22 0
      vendor/cogentcore.org/core/colors/README.md
  83. 54 0
      vendor/cogentcore.org/core/colors/accent.go
  84. 85 0
      vendor/cogentcore.org/core/colors/blend.go
  85. 229 0
      vendor/cogentcore.org/core/colors/cam/cam16/cam16.go
  86. 74 0
      vendor/cogentcore.org/core/colors/cam/cam16/lms16.go
  87. 47 0
      vendor/cogentcore.org/core/colors/cam/cam16/sanitize.go
  88. 37 0
      vendor/cogentcore.org/core/colors/cam/cam16/transform.go
  89. 178 0
      vendor/cogentcore.org/core/colors/cam/cam16/view.go
  90. 29 0
      vendor/cogentcore.org/core/colors/cam/cie/README.md
  91. 70 0
      vendor/cogentcore.org/core/colors/cam/cie/lab.go
  92. 105 0
      vendor/cogentcore.org/core/colors/cam/cie/srgb.go
  93. 15 0
      vendor/cogentcore.org/core/colors/cam/cie/std.go
  94. 67 0
      vendor/cogentcore.org/core/colors/cam/cie/xyz.go
  95. 16 0
      vendor/cogentcore.org/core/colors/cam/hct/README.md
  96. 382 0
      vendor/cogentcore.org/core/colors/cam/hct/bisect.go
  97. 227 0
      vendor/cogentcore.org/core/colors/cam/hct/contrast.go
  98. 239 0
      vendor/cogentcore.org/core/colors/cam/hct/hct.go
  99. 118 0
      vendor/cogentcore.org/core/colors/cam/hct/solver.go
  100. 138 0
      vendor/cogentcore.org/core/colors/cam/hct/transform.go

+ 8 - 1
Makefile

@@ -39,6 +39,13 @@ debug.run:
 	# go build -race -o ./bin_dev/wartank_dev ./cmd/server/main.go
 	go build -ldflags "-w -s -X main.GoVersion=$(GO_VERS) -X main.Version=${TAG} -X main.Date=${BUILD_DATE}" -o ./bin_dev/wartank_dev ./cmd/server/main.go
 	./debug.sh
+cogent.run:
+	clear
+	cp -r ./web ./bin_dev
+	go fmt ./...
+	# go build -race -o ./bin_dev/wartank_dev ./cmd/desc_core/main.go
+	go build -ldflags "-w -s -X main.GoVersion=$(GO_VERS) -X main.Version=${TAG} -X main.Date=${BUILD_DATE}" -o ./bin_dev/wartank_dev ./cmd/desc_core/main.go
+	./debug.sh
 prod.run:
 	clear
 	go fmt ./...
@@ -52,7 +59,7 @@ test.run:
 mod:
 	clear
 	go get -u ./...
-	go mod tidy -compat=1.22.4
+	go mod tidy -compat=1.23.4
 	go mod vendor
 	go fmt ./...
 lint:

+ 0 - 52
cmd/desc_lorca/main.go

@@ -1,52 +0,0 @@
-// package main -- пускач для десктопа на лорке
-//
-// Профилирование:
-//
-//	go tool pprof http://localhost:29080/debug/pprof/profile?seconds=30
-package main
-
-import (
-	"net/http"
-	_ "net/http/pprof"
-	"os"
-	"time"
-	"wartank/desc_lorca"
-	"wartank/pkg/components/kernel/logger"
-)
-
-func профилировать() {
-	лог := logger.НовЛоггер("Профиль")
-	порт := "29081"
-	стенд := os.Getenv("STAGE")
-	if стенд == "prod" {
-		порт = "29080"
-	}
-	for {
-		ош := http.ListenAndServe("0.0.0.0:"+порт, nil)
-		if ош != nil {
-			лог.Ошибка("Профиль(): ошибка при запуске профилировщика, ош=\n\t%v\n", ош)
-		}
-		time.Sleep(time.Second * 1)
-	}
-}
-
-var (
-	// Version -- версия тега хранилища
-	Version = ""
-	// Date -- дата релиза
-	Date = ""
-	// GoVersion -- версия компилятора
-	GoVersion = ""
-)
-
-func main() {
-	лог := logger.НовЛоггер("main")
-	лог.Инфо("server:\n\tgo = %v\n\tvers = %v\n\tdate = %v\n", GoVersion, Version, Date)
-	go профилировать()
-	десктоп := desc_lorca.НовДесктопЛорка()
-	go func() {
-		time.Sleep(time.Minute * 20)
-		десктоп.Отменить()
-	}()
-	десктоп.Пуск()
-}

+ 0 - 43
desc_lorca/desc_lorca.go

@@ -1,43 +0,0 @@
-// package desc_lorca -- главный тип десктопного приложения для lorca
-package desc_lorca
-
-import (
-	"wartank/desc_lorca/lorca_gui"
-	"wartank/pkg/components/kernel/logger"
-	"wartank/server"
-)
-
-// ДесктопЛорка -- главный тип десктопного приложения для lorca
-type ДесктопЛорка struct {
-	сервер   *server.Сервер      // Сервер для взаимодействия с лоркой
-	гипЛорка *lorca_gui.ЛоркаГуи // ГИП десктопа на Лорке
-	лог      *logger.Логгер
-}
-
-// НовДесктопЛорка -- возвращает новый десктоп на Лорке
-func НовДесктопЛорка() *ДесктопЛорка {
-	лог := logger.НовЛоггер("ДесктопЛорка")
-	лог.Инфо("НовДесктопЛорка()\n")
-	гип, ош := lorca_gui.НовЛоркаГуи()
-	лог.Паника(ош != nil, "НовДесктопЛорка(): при создании ЛоркаГуи, ош=\n\t%v\n", ош)
-	сам := &ДесктопЛорка{
-		гипЛорка: гип,
-		лог:      лог,
-	}
-	сам.сервер = server.НовСервер()
-	return сам
-}
-
-// Отменить -- отменяет работу десктопа
-func (сам *ДесктопЛорка) Отменить() {
-	сам.сервер.Отменить()
-}
-
-// Пуск -- запускает работу десктопа
-func (сам *ДесктопЛорка) Пуск() {
-	go func() {
-		ош := сам.сервер.Пуск()
-		сам.лог.Паника(ош != nil, "Пуск(): при запуске сервера, ош=\n\t%v\n", ош)
-	}()
-	сам.сервер.Wg().Wait()
-}

+ 0 - 36
desc_lorca/lorca_gui/lorca_gui.go

@@ -1,36 +0,0 @@
-// package lorca_gui -- тип десктопного приложения для lorca
-package lorca_gui
-
-import (
-	"fmt"
-
-	"github.com/zserge/lorca"
-)
-
-// ЛоркаГуи -- тип десктопного приложения для lorca
-type ЛоркаГуи struct{}
-
-// НовЛоркаГуи -- создание нового типа десктопного приложения для lorca
-func НовЛоркаГуи() (*ЛоркаГуи, error) {
-	ui, _ := lorca.New("", "", 480, 320)
-	defer ui.Close()
-
-	// Bind Go function to be available in JS. Go function may be long-running and
-	// blocking - in JS it's represented with a Promise.
-	ош := ui.Bind("add", func(a, b int) int { return a + b })
-	if ош != nil {
-		return nil, fmt.Errorf("НовЛоркаГуи(): ош=\n\t%w", ош)
-	}
-
-	// Call JS function from Go. Functions may be asynchronous, i.e. return promises
-	n := ui.Eval(`Math.random()`).Float()
-	fmt.Println(n)
-
-	// Call JS that calls Go and so on and so on...
-	m := ui.Eval(`add(2, 3)`).Int()
-	fmt.Println(m)
-
-	// Wait for the browser window to be closed
-	<-ui.Done()
-	return &ЛоркаГуи{}, nil
-}

+ 19 - 0
desktop_cogent/desktop_cogent.go

@@ -0,0 +1,19 @@
+// package desktop_cogent -- десктоп для работы с вартанком
+package desktop_cogent
+
+import (
+	"cogentcore.org/core/core"
+)
+
+type ДекстопКогент struct {
+	окноГлав *core.Body
+}
+
+// НовДесктопКогент -- возвращает новый десктоп на когенте
+func НовДесктопКогент() *ДекстопКогент {
+	сам := &ДекстопКогент{
+		окноГлав: core.NewBody("Вартанк"),
+	}
+	return сам
+}
+

+ 38 - 20
go.mod

@@ -1,43 +1,61 @@
 module wartank
 
-go 1.22.4
+go 1.23.4
 
 require (
-	fyne.io/fyne/v2 v2.5.0
-	github.com/charmbracelet/bubbletea v0.26.6
-	github.com/gofiber/fiber/v2 v2.52.5
-	github.com/gofiber/template/html/v2 v2.1.2
+	cogentcore.org/core v0.3.7
+	github.com/charmbracelet/bubbletea v1.2.4
+	github.com/gofiber/fiber/v2 v2.52.6
+	github.com/gofiber/template/html/v2 v2.1.3
 	github.com/sirupsen/logrus v1.9.3
 	github.com/syndtr/goleveldb v1.0.0
-	github.com/zserge/lorca v0.1.10
 )
 
 require (
-	github.com/andybalholm/brotli v1.1.0 // indirect
-	github.com/charmbracelet/x/ansi v0.1.4 // indirect
-	github.com/charmbracelet/x/input v0.1.3 // indirect
-	github.com/charmbracelet/x/term v0.1.1 // indirect
-	github.com/charmbracelet/x/windows v0.1.2 // indirect
+	github.com/Bios-Marcel/wastebasket v0.0.4-0.20240213135800-f26f1ae0a7c4 // indirect
+	github.com/Masterminds/vcs v1.13.3 // indirect
+	github.com/andybalholm/brotli v1.1.1 // indirect
+	github.com/anthonynsimon/bild v0.14.0 // indirect
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+	github.com/aymerick/douceur v0.2.0 // indirect
+	github.com/charmbracelet/lipgloss v1.0.0 // indirect
+	github.com/charmbracelet/x/ansi v0.6.0 // indirect
+	github.com/charmbracelet/x/term v0.2.1 // indirect
+	github.com/chewxy/math32 v1.11.1 // indirect
+	github.com/cogentcore/webgpu v0.0.0-20241212004832-ad7475f3b4dd // indirect
 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+	github.com/fsnotify/fsnotify v1.8.0 // indirect
+	github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
 	github.com/gofiber/template v1.8.3 // indirect
 	github.com/gofiber/utils v1.1.0 // indirect
+	github.com/goki/freetype v1.0.5 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/uuid v1.6.0 // indirect
-	github.com/klauspost/compress v1.17.9 // indirect
-	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/gorilla/css v1.0.1 // indirect
+	github.com/h2non/filetype v1.1.3 // indirect
+	github.com/hack-pad/go-indexeddb v0.3.2 // indirect
+	github.com/hack-pad/hackpadfs v0.2.4 // indirect
+	github.com/hack-pad/safejs v0.1.1 // indirect
+	github.com/jinzhu/copier v0.4.0 // indirect
+	github.com/klauspost/compress v1.17.11 // indirect
+	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-localereader v0.0.1 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
+	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
 	github.com/muesli/cancelreader v0.2.2 // indirect
+	github.com/muesli/termenv v0.15.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
-	github.com/valyala/fasthttp v1.55.0 // indirect
+	github.com/valyala/fasthttp v1.58.0 // indirect
 	github.com/valyala/tcplisten v1.0.0 // indirect
-	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
-	golang.org/x/net v0.27.0 // indirect
-	golang.org/x/sync v0.7.0 // indirect
-	golang.org/x/sys v0.22.0 // indirect
-	golang.org/x/text v0.16.0 // indirect
-	gopkg.in/yaml.v2 v2.4.0 // indirect
+	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
+	golang.org/x/image v0.23.0 // indirect
+	golang.org/x/net v0.34.0 // indirect
+	golang.org/x/sync v0.10.0 // indirect
+	golang.org/x/sys v0.29.0 // indirect
+	golang.org/x/text v0.21.0 // indirect
 )

+ 83 - 49
go.sum

@@ -1,61 +1,100 @@
-fyne.io/fyne/v2 v2.5.0 h1:lEjEIso0Vi4sJXYngIMoXOM6aUjqnPjK7pBpxRxG9aI=
-fyne.io/fyne/v2 v2.5.0/go.mod h1:9D4oT3NWeG+MLi/lP7ItZZyujHC/qqMJpoGTAYX5Uqc=
-github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
-github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
-github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s=
-github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk=
-github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
-github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
-github.com/charmbracelet/x/input v0.1.3 h1:oy4TMhyGQsYs/WWJwu1ELUMFnjiUAXwtDf048fHbCkg=
-github.com/charmbracelet/x/input v0.1.3/go.mod h1:1gaCOyw1KI9e2j00j/BBZ4ErzRZqa05w0Ghn83yIhKU=
-github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
-github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
-github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg=
-github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
+cogentcore.org/core v0.3.7 h1:oRc9o1rH31dtVWk6PGJGPvkKDAvmDe6bak7lu3VwCno=
+cogentcore.org/core v0.3.7/go.mod h1:P5sOFH1GF4z7Sir+cVo8AmpSp7TKCehAmIsZ2w4DNro=
+github.com/Bios-Marcel/wastebasket v0.0.4-0.20240213135800-f26f1ae0a7c4 h1:6lx9xzJAhdjq0LvVfbITeC3IH9Fzvo1aBahyPu2FuG8=
+github.com/Bios-Marcel/wastebasket v0.0.4-0.20240213135800-f26f1ae0a7c4/go.mod h1:FChzXi1izqzdPb6BiNZmcZLGyTYiT61iGx9Rxx9GNeI=
+github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE=
+github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8=
+github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
+github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
+github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
+github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE=
+github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM=
+github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
+github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
+github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA=
+github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/chewxy/math32 v1.11.1 h1:b7PGHlp8KjylDoU8RrcEsRuGZhJuz8haxnKfuMMRqy8=
+github.com/chewxy/math32 v1.11.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs=
+github.com/cogentcore/webgpu v0.0.0-20241212004832-ad7475f3b4dd h1:wmOdOGOfQDY/hmiQTWzoM59SskQSjrMz91jWv0gt6Yg=
+github.com/cogentcore/webgpu v0.0.0-20241212004832-ad7475f3b4dd/go.mod h1:ciqaxChrmRRMU1SnI5OE12Cn3QWvOKO+e5nSy+N9S1o=
 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=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
-github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
+github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
+github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
 github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
 github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
-github.com/gofiber/template/html/v2 v2.1.2 h1:wkK/mYJ3nIhongTkG3t0QgV4ADdgOYJYVSAF2AHnh8Y=
-github.com/gofiber/template/html/v2 v2.1.2/go.mod h1:E98Z/FzvpaSib06aWEgYk6GXNf3ctoyaJH8yW5ay5ak=
+github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
+github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
 github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
 github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
+github.com/goki/freetype v1.0.5 h1:yi2lQeUhXnBgSMqYd0vVmPw6RnnfIeTP3N4uvaJXd7A=
+github.com/goki/freetype v1.0.5/go.mod h1:wKmKxddbzKmeci9K96Wknn5kjTWLyfC8tKOqAFbEX8E=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
+github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
+github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
+github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
+github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
+github.com/hack-pad/hackpadfs v0.2.4 h1:7pmzQGR6JsGq/uB0JWxd3wTBi7I85f46CHGvcfrJsiE=
+github.com/hack-pad/hackpadfs v0.2.4/go.mod h1:2XDioLb2NwaQzRYo+cpgNx1iMALzBQ4bQoLhHpArQZM=
+github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8=
+github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
-github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
+github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+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/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
 github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
 github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
 github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
 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=
 github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
+github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -63,49 +102,44 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
 github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
-github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
+github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
+github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
 github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
 github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
-github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
-github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
-github.com/zserge/lorca v0.1.10 h1:f/xBJ3D3ipcVRCcvN8XqZnpoKcOXV8I4vwqlFyw7ruc=
-github.com/zserge/lorca v0.1.10/go.mod h1:bVmnIbIRlOcoV285KIRSe4bUABKi7R7384Ycuum6e4A=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
-golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
+github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
+github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
+golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
+golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
+golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
+golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
-golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
-golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
-golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 0 - 9
pkg/types/igui.go

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

+ 2 - 2
server/serv_bots/serv_bots.go

@@ -73,7 +73,7 @@ func (сам *БотоФерма) НовБот(логин, пароль string,
 		номер++
 	}
 	// Нет такого бота, надо его создать
-		bot := warbot.НовВарБот(сам.серв, номер, логин, пароль, еслиАвто)
-		сам.словБот.Add(bot)
+	bot := warbot.НовВарБот(сам.серв, номер, логин, пароль, еслиАвто)
+	сам.словБот.Add(bot)
 	return nil
 }

+ 1 - 1
server/serv_bots/warbot/angar/base/arsenal/arsenal.go

@@ -658,7 +658,7 @@ func (сам *Арсенал) сделатьРемку() bool {
 	// https://wartank.ru/production/Armory?37-1.ILinkListener-productions-3-production-startProduceLink
 	ссылка := "https://wartank.ru/production/" + _ссылка
 	time.Sleep(time.Millisecond * 50)
-	 _= сам.сеть.ВебВоркер().Получ(ссылка)
+	_ = сам.сеть.ВебВоркер().Получ(ссылка)
 	сам.АренаСостояние().СостояниеУст(стрРемки)
 	return true
 }

+ 3 - 3
server/serv_bots/warbot/angar/base/bank/bank.go

@@ -109,7 +109,7 @@ func (сам *Банк) проверитьУскорить() bool {
 	_ссылка := strings.TrimPrefix(стрСсылка, `<td style="width:50%;padding-left:1px;"><a class="simple-but border" href="`)
 	_ссылка = strings.TrimSuffix(_ссылка, `"><span><span>Ускорение</span></span></a>`)
 	ссылка := "https://wartank.ru/" + _ссылка
-	_= сам.сеть.ВебВоркер().Получ(ссылка)
+	_ = сам.сеть.ВебВоркер().Получ(ссылка)
 	return true
 }
 
@@ -139,8 +139,8 @@ func (сам *Банк) забрать() {
 
 // Проверяет необходимость постройки полигона
 func (сам *Банк) построитьУлучшить() bool {
-	var 		списБанк []string
-	
+	var списБанк []string
+
 	{ // Зайти на страницу постройки
 		// https://wartank.ru/building-upgrade/Bank
 		списБанк = сам.сеть.ВебВоркер().Получ("https://wartank.ru/building-upgrade/Bank")

+ 5 - 5
server/serv_bots/warbot/angar/tank_params/tank_params.go

@@ -15,14 +15,14 @@ import (
 type ТанкПараметры struct {
 	бот   types.ИБот
 	номер string // Номер танка в игре
-	лог types.ИЛоггер
+	лог   types.ИЛоггер
 }
 
 // НовТанкПараметры -- возвращает новые параметры танка
 func НовТанкПараметры(бот types.ИБот) *ТанкПараметры {
-	лог:=logger.НовЛоггер("ТанкПараметры")
+	лог := logger.НовЛоггер("ТанкПараметры")
 	лог.Инфо("НовТанкПараметры()\n")
-	лог.Паника(бот==nil, "НовТанкПараметры(): ИБот == nil")
+	лог.Паника(бот == nil, "НовТанкПараметры(): ИБот == nil")
 	сам := &ТанкПараметры{
 		бот: бот,
 		лог: лог,
@@ -64,7 +64,7 @@ func (сам *ТанкПараметры) работать() {
 func (сам *ТанкПараметры) улучшить() {
 	// https://wartank.ru/pimp/34479487
 	клиент := сам.бот.Сеть().ВебВоркер()
-	фнУлучшить := func() bool{
+	фнУлучшить := func() bool {
 		лстСтр := клиент.Получ("https://wartank.ru/pimp/" + сам.номер)
 		var (
 			стрВых    string
@@ -87,7 +87,7 @@ func (сам *ТанкПараметры) улучшить() {
 	}
 	счётОш := 5
 	for счётОш > 0 {
-		if фнУлучшить(){
+		if фнУлучшить() {
 			break
 		}
 		счётОш--

+ 1 - 1
server/serv_bots/warbot/warbot.go

@@ -88,7 +88,7 @@ func НовВарБот(сервер types.ИСервер, номер alias.Бо
 func создатьЯдроВарБот(серв types.ИСервер, конфиг *warbot_config.ВарБотКонфиг) *ВарБот {
 	лог := logger.НовЛоггер("ВарБот")
 	лог.Инфо("создатьЯдроВарБот()\n")
-	лог.Паника(серв==nil, "создатьЯдроВарБот(): ИСервер == nil")
+	лог.Паника(серв == nil, "создатьЯдроВарБот(): ИСервер == nil")
 	лог.Паника(конфиг == nil, "создатьЯдроВарБот(): ВарБотКонфиг==nil")
 	ctx, fnCancel := context.WithCancel(серв.Контекст())
 	сам := &ВарБот{

+ 2 - 2
server/serv_bots/warbot/warbot_config/warbot_config.go

@@ -25,10 +25,10 @@ func (сам *ВарБотКонфиг) Marshall() []byte {
 
 // Unmarshal -- десериализует себя из байтового потока
 func (сам *ВарБотКонфиг) Unmarshal(binData []byte) {
-	лог:=logger.НовЛоггер("ВарБотКонфиг")
+	лог := logger.НовЛоггер("ВарБотКонфиг")
 	лог.Отладка("Unmarshal()")
 	err := json.Unmarshal(binData, сам)
-	лог.Паника(err!=nil, "Unmarshal(): err=\n\t%v\n", err)
+	лог.Паника(err != nil, "Unmarshal(): err=\n\t%v\n", err)
 }
 
 // Логин -- возвращает логин

+ 2 - 2
server/serv_bots/warbot/warbot_net/bot_cookie/bot_cookie.go

@@ -15,12 +15,12 @@ import (
 type BotCookie struct {
 	cookie []*http.Cookie
 	block  sync.RWMutex
-	лог types.ИЛоггер
+	лог    types.ИЛоггер
 }
 
 // NewBotCookie -- возвращает новый *NetCookie
 func NewBotCookie() BotCookie {
-	лог:=logger.НовЛоггер("NetCookie")
+	лог := logger.НовЛоггер("NetCookie")
 	лог.Инфо("NetCookie()\n")
 	return BotCookie{
 		лог: лог,

+ 2 - 2
server/serv_bots/warbot/warbot_net/warbot_net.go

@@ -23,12 +23,12 @@ type ВарБотСеть struct {
 	вебВоркер  types.ИХттпВоркер
 	ctx        context.Context
 	фнОтмена   func()
-	лог types.ИЛоггер
+	лог        types.ИЛоггер
 }
 
 // НовВарБотСеть -- возвращает новый коннект к сети бота
 func НовВарБотСеть(бот types.ИБот) *ВарБотСеть {
-	лог:=logger.НовЛоггер("ВарБотСеть")
+	лог := logger.НовЛоггер("ВарБотСеть")
 	лог.Инфо("НовВарБотСеть()\n")
 	лог.Паника(бот == nil, "НовВарБотСеть(): ИБот == nil")
 

+ 28 - 0
vendor/cogentcore.org/core/LICENSE

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

+ 24 - 0
vendor/cogentcore.org/core/base/bools/booler.go

@@ -0,0 +1,24 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package bools provides functions and interfaces for
+// interacting with bool values.
+package bools
+
+// A Booler is a type that can return
+// its value as a boolean value
+type Booler interface {
+	// Bool returns the boolean
+	// representation of the value
+	Bool() bool
+}
+
+// A BoolSetter is a Booler that can also
+// set its value from a bool value
+type BoolSetter interface {
+	Booler
+	// SetBool sets the value from the
+	// boolean representation of the value
+	SetBool(val bool)
+}

+ 246 - 0
vendor/cogentcore.org/core/base/datasize/datasize.go

@@ -0,0 +1,246 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Based on https://github.com/c2h5oh/datasize
+// Copyright (c) 2016 Maciej Lisiewski
+
+// Package datasize provides a data size type and constants.
+package datasize
+
+import (
+	"errors"
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+// Size represents a data size.
+type Size uint64
+
+const (
+	B  Size = 1
+	KB      = B << 10
+	MB      = KB << 10
+	GB      = MB << 10
+	TB      = GB << 10
+	PB      = TB << 10
+	EB      = PB << 10
+
+	fnUnmarshalText string = "UnmarshalText"
+	maxUint64       uint64 = (1 << 64) - 1
+	cutoff          uint64 = maxUint64 / 10
+)
+
+var ErrBits = errors.New("unit with capital unit prefix and lower case unit (b) - bits, not bytes")
+
+func (b Size) Bytes() uint64 {
+	return uint64(b)
+}
+
+func (b Size) KBytes() float64 {
+	v := b / KB
+	r := b % KB
+	return float64(v) + float64(r)/float64(KB)
+}
+
+func (b Size) MBytes() float64 {
+	v := b / MB
+	r := b % MB
+	return float64(v) + float64(r)/float64(MB)
+}
+
+func (b Size) GBytes() float64 {
+	v := b / GB
+	r := b % GB
+	return float64(v) + float64(r)/float64(GB)
+}
+
+func (b Size) TBytes() float64 {
+	v := b / TB
+	r := b % TB
+	return float64(v) + float64(r)/float64(TB)
+}
+
+func (b Size) PBytes() float64 {
+	v := b / PB
+	r := b % PB
+	return float64(v) + float64(r)/float64(PB)
+}
+
+func (b Size) EBytes() float64 {
+	v := b / EB
+	r := b % EB
+	return float64(v) + float64(r)/float64(EB)
+}
+
+// String returns a human-readable representation of the data size.
+func (b Size) String() string {
+	switch {
+	case b > EB:
+		return fmt.Sprintf("%.1f EB", b.EBytes())
+	case b > PB:
+		return fmt.Sprintf("%.1f PB", b.PBytes())
+	case b > TB:
+		return fmt.Sprintf("%.1f TB", b.TBytes())
+	case b > GB:
+		return fmt.Sprintf("%.1f GB", b.GBytes())
+	case b > MB:
+		return fmt.Sprintf("%.1f MB", b.MBytes())
+	case b > KB:
+		return fmt.Sprintf("%.1f KB", b.KBytes())
+	default:
+		return fmt.Sprintf("%d B", b)
+	}
+}
+
+// MachineString returns a machine-friendly representation of the data size.
+func (b Size) MachineString() string {
+	switch {
+	case b == 0:
+		return "0B"
+	case b%EB == 0:
+		return fmt.Sprintf("%dEB", b/EB)
+	case b%PB == 0:
+		return fmt.Sprintf("%dPB", b/PB)
+	case b%TB == 0:
+		return fmt.Sprintf("%dTB", b/TB)
+	case b%GB == 0:
+		return fmt.Sprintf("%dGB", b/GB)
+	case b%MB == 0:
+		return fmt.Sprintf("%dMB", b/MB)
+	case b%KB == 0:
+		return fmt.Sprintf("%dKB", b/KB)
+	default:
+		return fmt.Sprintf("%dB", b)
+	}
+}
+
+func (b Size) MarshalText() ([]byte, error) {
+	return []byte(b.MachineString()), nil
+}
+
+func (b *Size) UnmarshalText(t []byte) error {
+	var val uint64
+	var unit string
+
+	// copy for error message
+	t0 := t
+
+	var c byte
+	var i int
+
+ParseLoop:
+	for i < len(t) {
+		c = t[i]
+		switch {
+		case '0' <= c && c <= '9':
+			if val > cutoff {
+				goto Overflow
+			}
+
+			c = c - '0'
+			val *= 10
+
+			if val > val+uint64(c) {
+				// val+v overflows
+				goto Overflow
+			}
+			val += uint64(c)
+			i++
+
+		default:
+			if i == 0 {
+				goto SyntaxError
+			}
+			break ParseLoop
+		}
+	}
+
+	unit = strings.TrimSpace(string(t[i:]))
+	switch unit {
+	case "Kb", "Mb", "Gb", "Tb", "Pb", "Eb":
+		goto BitsError
+	}
+	unit = strings.ToLower(unit)
+	switch unit {
+	case "", "b", "byte":
+		// do nothing - already in bytes
+
+	case "k", "kb", "kilo", "kilobyte", "kilobytes":
+		if val > maxUint64/uint64(KB) {
+			goto Overflow
+		}
+		val *= uint64(KB)
+
+	case "m", "mb", "mega", "megabyte", "megabytes":
+		if val > maxUint64/uint64(MB) {
+			goto Overflow
+		}
+		val *= uint64(MB)
+
+	case "g", "gb", "giga", "gigabyte", "gigabytes":
+		if val > maxUint64/uint64(GB) {
+			goto Overflow
+		}
+		val *= uint64(GB)
+
+	case "t", "tb", "tera", "terabyte", "terabytes":
+		if val > maxUint64/uint64(TB) {
+			goto Overflow
+		}
+		val *= uint64(TB)
+
+	case "p", "pb", "peta", "petabyte", "petabytes":
+		if val > maxUint64/uint64(PB) {
+			goto Overflow
+		}
+		val *= uint64(PB)
+
+	case "E", "EB", "e", "eb", "eB":
+		if val > maxUint64/uint64(EB) {
+			goto Overflow
+		}
+		val *= uint64(EB)
+
+	default:
+		goto SyntaxError
+	}
+
+	*b = Size(val)
+	return nil
+
+Overflow:
+	*b = Size(maxUint64)
+	return &strconv.NumError{fnUnmarshalText, string(t0), strconv.ErrRange}
+
+SyntaxError:
+	*b = 0
+	return &strconv.NumError{fnUnmarshalText, string(t0), strconv.ErrSyntax}
+
+BitsError:
+	*b = 0
+	return &strconv.NumError{fnUnmarshalText, string(t0), ErrBits}
+}
+
+func Parse(t []byte) (Size, error) {
+	var v Size
+	err := v.UnmarshalText(t)
+	return v, err
+}
+
+func MustParse(t []byte) Size {
+	v, err := Parse(t)
+	if err != nil {
+		panic(err)
+	}
+	return v
+}
+
+func ParseString(s string) (Size, error) {
+	return Parse([]byte(s))
+}
+
+func MustParseString(s string) Size {
+	return MustParse([]byte(s))
+}

+ 46 - 0
vendor/cogentcore.org/core/base/elide/elide.go

@@ -0,0 +1,46 @@
+// Copyright (c) 2018, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package elide provides basic text eliding functions.
+package elide
+
+import "strings"
+
+// End elides from the end of the string if it is longer than given
+// size parameter.  The resulting string will not exceed sz in length,
+// with space reserved for … at the end.
+func End(s string, sz int) string {
+	n := len(s)
+	if n <= sz {
+		return s
+	}
+	return s[:sz-1] + "…"
+}
+
+// Middle elides from the middle of the string if it is longer than given
+// size parameter.  The resulting string will not exceed sz in length,
+// with space reserved for … in the middle
+func Middle(s string, sz int) string {
+	n := len(s)
+	if n <= sz {
+		return s
+	}
+	en := sz - 1
+	mid := en / 2
+	rest := en - mid
+	return s[:mid] + "…" + s[n-rest:]
+}
+
+// AppName elides the given app name to be twelve characters or less
+// by removing word(s) from the middle of the string if necessary and possible.
+func AppName(s string) string {
+	if len(s) <= 12 {
+		return s
+	}
+	words := strings.Fields(s)
+	if len(words) < 3 {
+		return s
+	}
+	return words[0] + " " + words[len(words)-1]
+}

+ 107 - 0
vendor/cogentcore.org/core/base/errors/errors.go

@@ -0,0 +1,107 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package errors provides a set of error handling helpers,
+// extending the standard library errors package.
+package errors
+
+import (
+	"log/slog"
+	"runtime"
+	"strconv"
+)
+
+// Log takes the given error and logs it if it is non-nil.
+// The intended usage is:
+//
+//	errors.Log(MyFunc(v))
+//	// or
+//	return errors.Log(MyFunc(v))
+func Log(err error) error {
+	if err != nil {
+		slog.Error(err.Error() + " | " + CallerInfo())
+	}
+	return err
+}
+
+// Log1 takes the given value and error and returns the value if
+// the error is nil, and logs the error and returns a zero value
+// if the error is non-nil. The intended usage is:
+//
+//	a := errors.Log1(MyFunc(v))
+func Log1[T any](v T, err error) T { //yaegi:add
+	if err != nil {
+		slog.Error(err.Error() + " | " + CallerInfo())
+	}
+	return v
+}
+
+// Log2 takes the given two values and error and returns the values if
+// the error is nil, and logs the error and returns zero values
+// if the error is non-nil. The intended usage is:
+//
+//	a, b := errors.Log2(MyFunc(v))
+func Log2[T1, T2 any](v1 T1, v2 T2, err error) (T1, T2) {
+	if err != nil {
+		slog.Error(err.Error() + " | " + CallerInfo())
+	}
+	return v1, v2
+}
+
+// Must takes the given error and panics if it is non-nil.
+// The intended usage is:
+//
+//	errors.Must(MyFunc(v))
+func Must(err error) {
+	if err != nil {
+		panic(err)
+	}
+}
+
+// Must1 takes the given value and error and returns the value if
+// the error is nil, and panics if the error is non-nil. The intended usage is:
+//
+//	a := errors.Must1(MyFunc(v))
+func Must1[T any](v T, err error) T {
+	if err != nil {
+		panic(err)
+	}
+	return v
+}
+
+// Must2 takes the given two values and error and returns the values if
+// the error is nil, and panics if the error is non-nil. The intended usage is:
+//
+//	a, b := errors.Must2(MyFunc(v))
+func Must2[T1, T2 any](v1 T1, v2 T2, err error) (T1, T2) {
+	if err != nil {
+		panic(err)
+	}
+	return v1, v2
+}
+
+// Ignore1 ignores an error return value for a function returning
+// a value and an error, allowing direct usage of the value.
+// The intended usage is:
+//
+//	a := errors.Ignore1(MyFunc(v))
+func Ignore1[T any](v T, err error) T {
+	return v
+}
+
+// Ignore2 ignores an error return value for a function returning
+// two values and an error, allowing direct usage of the values.
+// The intended usage is:
+//
+//	a, b := errors.Ignore2(MyFunc(v))
+func Ignore2[T1, T2 any](v1 T1, v2 T2, err error) (T1, T2) {
+	return v1, v2
+}
+
+// CallerInfo returns string information about the caller
+// of the function that called CallerInfo.
+func CallerInfo() string {
+	pc, file, line, _ := runtime.Caller(2)
+	return runtime.FuncForPC(pc).Name() + " " + file + ":" + strconv.Itoa(line)
+}

+ 84 - 0
vendor/cogentcore.org/core/base/errors/stdlib.go

@@ -0,0 +1,84 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package errors
+
+import "errors"
+
+// Aliases for standard library errors package:
+
+// ErrUnsupported indicates that a requested operation cannot be performed,
+// because it is unsupported. For example, a call to [os.Link] when using a
+// file system that does not support hard links.
+//
+// Functions and methods should not return this error but should instead
+// return an error including appropriate context that satisfies
+//
+//	errors.Is(err, errors.ErrUnsupported)
+//
+// either by directly wrapping ErrUnsupported or by implementing an [Is] method.
+//
+// Functions and methods should document the cases in which an error
+// wrapping this will be returned.
+var ErrUnsupported = errors.ErrUnsupported
+
+// As finds the first error in err's tree that matches target, and if one is found, sets
+// target to that error value and returns true. Otherwise, it returns false.
+//
+// The tree consists of err itself, followed by the errors obtained by repeatedly
+// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple
+// errors, As examines err followed by a depth-first traversal of its children.
+//
+// An error matches target if the error's concrete value is assignable to the value
+// pointed to by target, or if the error has a method As(interface{}) bool such that
+// As(target) returns true. In the latter case, the As method is responsible for
+// setting target.
+//
+// An error type might provide an As method so it can be treated as if it were a
+// different error type.
+//
+// As panics if target is not a non-nil pointer to either a type that implements
+// error, or to any interface type.
+func As(err error, target any) bool { return errors.As(err, target) }
+
+// Is reports whether any error in err's tree matches target.
+//
+// The tree consists of err itself, followed by the errors obtained by repeatedly
+// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple
+// errors, Is examines err followed by a depth-first traversal of its children.
+//
+// An error is considered to match a target if it is equal to that target or if
+// it implements a method Is(error) bool such that Is(target) returns true.
+//
+// An error type might provide an Is method so it can be treated as equivalent
+// to an existing error. For example, if MyError defines
+//
+//	func (m MyError) Is(target error) bool { return target == fs.ErrExist }
+//
+// then Is(MyError{}, fs.ErrExist) returns true. See [syscall.Errno.Is] for
+// an example in the standard library. An Is method should only shallowly
+// compare err and the target and not call [Unwrap] on either.
+func Is(err, target error) bool { return errors.Is(err, target) }
+
+// Join returns an error that wraps the given errors.
+// Any nil error values are discarded.
+// Join returns nil if every value in errs is nil.
+// The error formats as the concatenation of the strings obtained
+// by calling the Error method of each element of errs, with a newline
+// between each string.
+//
+// A non-nil error returned by Join implements the Unwrap() []error method.
+func Join(errs ...error) error { return errors.Join(errs...) }
+
+// New returns an error that formats as the given text.
+// Each call to New returns a distinct error value even if the text is identical.
+func New(text string) error { return errors.New(text) }
+
+// Unwrap returns the result of calling the Unwrap method on err, if err's
+// type contains an Unwrap method returning error.
+// Otherwise, Unwrap returns nil.
+//
+// Unwrap only calls a method of the form "Unwrap() error".
+// In particular Unwrap does not unwrap errors returned by [Join].
+func Unwrap(err error) error { return errors.Unwrap(err) }

+ 89 - 0
vendor/cogentcore.org/core/base/fileinfo/enumgen.go

@@ -0,0 +1,89 @@
+// Code generated by "core generate"; DO NOT EDIT.
+
+package fileinfo
+
+import (
+	"cogentcore.org/core/enums"
+)
+
+var _CategoriesValues = []Categories{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
+
+// CategoriesN is the highest valid value for type Categories, plus one.
+const CategoriesN Categories = 16
+
+var _CategoriesValueMap = map[string]Categories{`UnknownCategory`: 0, `Folder`: 1, `Archive`: 2, `Backup`: 3, `Code`: 4, `Doc`: 5, `Sheet`: 6, `Data`: 7, `Text`: 8, `Image`: 9, `Model`: 10, `Audio`: 11, `Video`: 12, `Font`: 13, `Exe`: 14, `Bin`: 15}
+
+var _CategoriesDescMap = map[Categories]string{0: `UnknownCategory is an unknown file category`, 1: `Folder is a folder / directory`, 2: `Archive is a collection of files, e.g., zip tar`, 3: `Backup is a backup file (# ~ etc)`, 4: `Code is a programming language file`, 5: `Doc is an editable word processing file including latex, markdown, html, css, etc`, 6: `Sheet is a spreadsheet file (.xls etc)`, 7: `Data is some kind of data format (csv, json, database, etc)`, 8: `Text is some other kind of text file`, 9: `Image is an image (jpeg, png, svg, etc) *including* PDF`, 10: `Model is a 3D model`, 11: `Audio is an audio file`, 12: `Video is a video file`, 13: `Font is a font file`, 14: `Exe is a binary executable file (scripts go in Code)`, 15: `Bin is some other type of binary (object files, libraries, etc)`}
+
+var _CategoriesMap = map[Categories]string{0: `UnknownCategory`, 1: `Folder`, 2: `Archive`, 3: `Backup`, 4: `Code`, 5: `Doc`, 6: `Sheet`, 7: `Data`, 8: `Text`, 9: `Image`, 10: `Model`, 11: `Audio`, 12: `Video`, 13: `Font`, 14: `Exe`, 15: `Bin`}
+
+// String returns the string representation of this Categories value.
+func (i Categories) String() string { return enums.String(i, _CategoriesMap) }
+
+// SetString sets the Categories value from its string representation,
+// and returns an error if the string is invalid.
+func (i *Categories) SetString(s string) error {
+	return enums.SetString(i, s, _CategoriesValueMap, "Categories")
+}
+
+// Int64 returns the Categories value as an int64.
+func (i Categories) Int64() int64 { return int64(i) }
+
+// SetInt64 sets the Categories value from an int64.
+func (i *Categories) SetInt64(in int64) { *i = Categories(in) }
+
+// Desc returns the description of the Categories value.
+func (i Categories) Desc() string { return enums.Desc(i, _CategoriesDescMap) }
+
+// CategoriesValues returns all possible values for the type Categories.
+func CategoriesValues() []Categories { return _CategoriesValues }
+
+// Values returns all possible values for the type Categories.
+func (i Categories) Values() []enums.Enum { return enums.Values(_CategoriesValues) }
+
+// MarshalText implements the [encoding.TextMarshaler] interface.
+func (i Categories) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
+
+// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
+func (i *Categories) UnmarshalText(text []byte) error {
+	return enums.UnmarshalText(i, text, "Categories")
+}
+
+var _KnownValues = []Known{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130}
+
+// KnownN is the highest valid value for type Known, plus one.
+const KnownN Known = 131
+
+var _KnownValueMap = map[string]Known{`Unknown`: 0, `Any`: 1, `AnyKnown`: 2, `AnyFolder`: 3, `AnyArchive`: 4, `Multipart`: 5, `Tar`: 6, `Zip`: 7, `GZip`: 8, `SevenZ`: 9, `Xz`: 10, `BZip`: 11, `Dmg`: 12, `Shar`: 13, `AnyBackup`: 14, `Trash`: 15, `AnyCode`: 16, `Ada`: 17, `Bash`: 18, `Cosh`: 19, `Csh`: 20, `C`: 21, `CSharp`: 22, `D`: 23, `Diff`: 24, `Eiffel`: 25, `Erlang`: 26, `Forth`: 27, `Fortran`: 28, `FSharp`: 29, `Go`: 30, `Haskell`: 31, `Java`: 32, `JavaScript`: 33, `Lisp`: 34, `Lua`: 35, `Makefile`: 36, `Mathematica`: 37, `Matlab`: 38, `ObjC`: 39, `OCaml`: 40, `Pascal`: 41, `Perl`: 42, `Php`: 43, `Prolog`: 44, `Python`: 45, `R`: 46, `Ruby`: 47, `Rust`: 48, `Scala`: 49, `Tcl`: 50, `AnyDoc`: 51, `BibTeX`: 52, `TeX`: 53, `Texinfo`: 54, `Troff`: 55, `Html`: 56, `Css`: 57, `Markdown`: 58, `Rtf`: 59, `MSWord`: 60, `OpenText`: 61, `OpenPres`: 62, `MSPowerpoint`: 63, `EBook`: 64, `EPub`: 65, `AnySheet`: 66, `MSExcel`: 67, `OpenSheet`: 68, `AnyData`: 69, `Csv`: 70, `Json`: 71, `Xml`: 72, `Protobuf`: 73, `Ini`: 74, `Tsv`: 75, `Uri`: 76, `Color`: 77, `Yaml`: 78, `Toml`: 79, `Number`: 80, `String`: 81, `Tensor`: 82, `Table`: 83, `AnyText`: 84, `PlainText`: 85, `ICal`: 86, `VCal`: 87, `VCard`: 88, `AnyImage`: 89, `Pdf`: 90, `Postscript`: 91, `Gimp`: 92, `GraphVis`: 93, `Gif`: 94, `Jpeg`: 95, `Png`: 96, `Svg`: 97, `Tiff`: 98, `Pnm`: 99, `Pbm`: 100, `Pgm`: 101, `Ppm`: 102, `Xbm`: 103, `Xpm`: 104, `Bmp`: 105, `Heic`: 106, `Heif`: 107, `AnyModel`: 108, `Vrml`: 109, `X3d`: 110, `Obj`: 111, `AnyAudio`: 112, `Aac`: 113, `Flac`: 114, `Mp3`: 115, `Ogg`: 116, `Midi`: 117, `Wav`: 118, `AnyVideo`: 119, `Mpeg`: 120, `Mp4`: 121, `Mov`: 122, `Ogv`: 123, `Wmv`: 124, `Avi`: 125, `AnyFont`: 126, `TrueType`: 127, `WebOpenFont`: 128, `AnyExe`: 129, `AnyBin`: 130}
+
+var _KnownDescMap = map[Known]string{0: `Unknown = a non-known file type`, 1: `Any is used when selecting a file type, if any type is OK (including Unknown) see also AnyKnown and the Any options for each category`, 2: `AnyKnown is used when selecting a file type, if any Known file type is OK (excludes Unknown) -- see Any and Any options for each category`, 3: `Folder is a folder / directory`, 4: `Archive is a collection of files, e.g., zip tar`, 5: ``, 6: ``, 7: ``, 8: ``, 9: ``, 10: ``, 11: ``, 12: ``, 13: ``, 14: `Backup files`, 15: ``, 16: `Code is a programming language file`, 17: ``, 18: ``, 19: ``, 20: ``, 21: ``, 22: ``, 23: ``, 24: ``, 25: ``, 26: ``, 27: ``, 28: ``, 29: ``, 30: ``, 31: ``, 32: ``, 33: ``, 34: ``, 35: ``, 36: ``, 37: ``, 38: ``, 39: ``, 40: ``, 41: ``, 42: ``, 43: ``, 44: ``, 45: ``, 46: ``, 47: ``, 48: ``, 49: ``, 50: ``, 51: `Doc is an editable word processing file including latex, markdown, html, css, etc`, 52: ``, 53: ``, 54: ``, 55: ``, 56: ``, 57: ``, 58: ``, 59: ``, 60: ``, 61: ``, 62: ``, 63: ``, 64: ``, 65: ``, 66: `Sheet is a spreadsheet file (.xls etc)`, 67: ``, 68: ``, 69: `Data is some kind of data format (csv, json, database, etc)`, 70: ``, 71: ``, 72: ``, 73: ``, 74: ``, 75: ``, 76: ``, 77: ``, 78: ``, 79: ``, 80: `special support for data fs`, 81: ``, 82: ``, 83: ``, 84: `Text is some other kind of text file`, 85: ``, 86: ``, 87: ``, 88: ``, 89: `Image is an image (jpeg, png, svg, etc) *including* PDF`, 90: ``, 91: ``, 92: ``, 93: ``, 94: ``, 95: ``, 96: ``, 97: ``, 98: ``, 99: ``, 100: ``, 101: ``, 102: ``, 103: ``, 104: ``, 105: ``, 106: ``, 107: ``, 108: `Model is a 3D model`, 109: ``, 110: ``, 111: ``, 112: `Audio is an audio file`, 113: ``, 114: ``, 115: ``, 116: ``, 117: ``, 118: ``, 119: `Video is a video file`, 120: ``, 121: ``, 122: ``, 123: ``, 124: ``, 125: ``, 126: `Font is a font file`, 127: ``, 128: ``, 129: `Exe is a binary executable file`, 130: `Bin is some other unrecognized binary type`}
+
+var _KnownMap = map[Known]string{0: `Unknown`, 1: `Any`, 2: `AnyKnown`, 3: `AnyFolder`, 4: `AnyArchive`, 5: `Multipart`, 6: `Tar`, 7: `Zip`, 8: `GZip`, 9: `SevenZ`, 10: `Xz`, 11: `BZip`, 12: `Dmg`, 13: `Shar`, 14: `AnyBackup`, 15: `Trash`, 16: `AnyCode`, 17: `Ada`, 18: `Bash`, 19: `Cosh`, 20: `Csh`, 21: `C`, 22: `CSharp`, 23: `D`, 24: `Diff`, 25: `Eiffel`, 26: `Erlang`, 27: `Forth`, 28: `Fortran`, 29: `FSharp`, 30: `Go`, 31: `Haskell`, 32: `Java`, 33: `JavaScript`, 34: `Lisp`, 35: `Lua`, 36: `Makefile`, 37: `Mathematica`, 38: `Matlab`, 39: `ObjC`, 40: `OCaml`, 41: `Pascal`, 42: `Perl`, 43: `Php`, 44: `Prolog`, 45: `Python`, 46: `R`, 47: `Ruby`, 48: `Rust`, 49: `Scala`, 50: `Tcl`, 51: `AnyDoc`, 52: `BibTeX`, 53: `TeX`, 54: `Texinfo`, 55: `Troff`, 56: `Html`, 57: `Css`, 58: `Markdown`, 59: `Rtf`, 60: `MSWord`, 61: `OpenText`, 62: `OpenPres`, 63: `MSPowerpoint`, 64: `EBook`, 65: `EPub`, 66: `AnySheet`, 67: `MSExcel`, 68: `OpenSheet`, 69: `AnyData`, 70: `Csv`, 71: `Json`, 72: `Xml`, 73: `Protobuf`, 74: `Ini`, 75: `Tsv`, 76: `Uri`, 77: `Color`, 78: `Yaml`, 79: `Toml`, 80: `Number`, 81: `String`, 82: `Tensor`, 83: `Table`, 84: `AnyText`, 85: `PlainText`, 86: `ICal`, 87: `VCal`, 88: `VCard`, 89: `AnyImage`, 90: `Pdf`, 91: `Postscript`, 92: `Gimp`, 93: `GraphVis`, 94: `Gif`, 95: `Jpeg`, 96: `Png`, 97: `Svg`, 98: `Tiff`, 99: `Pnm`, 100: `Pbm`, 101: `Pgm`, 102: `Ppm`, 103: `Xbm`, 104: `Xpm`, 105: `Bmp`, 106: `Heic`, 107: `Heif`, 108: `AnyModel`, 109: `Vrml`, 110: `X3d`, 111: `Obj`, 112: `AnyAudio`, 113: `Aac`, 114: `Flac`, 115: `Mp3`, 116: `Ogg`, 117: `Midi`, 118: `Wav`, 119: `AnyVideo`, 120: `Mpeg`, 121: `Mp4`, 122: `Mov`, 123: `Ogv`, 124: `Wmv`, 125: `Avi`, 126: `AnyFont`, 127: `TrueType`, 128: `WebOpenFont`, 129: `AnyExe`, 130: `AnyBin`}
+
+// String returns the string representation of this Known value.
+func (i Known) String() string { return enums.String(i, _KnownMap) }
+
+// SetString sets the Known value from its string representation,
+// and returns an error if the string is invalid.
+func (i *Known) SetString(s string) error { return enums.SetString(i, s, _KnownValueMap, "Known") }
+
+// Int64 returns the Known value as an int64.
+func (i Known) Int64() int64 { return int64(i) }
+
+// SetInt64 sets the Known value from an int64.
+func (i *Known) SetInt64(in int64) { *i = Known(in) }
+
+// Desc returns the description of the Known value.
+func (i Known) Desc() string { return enums.Desc(i, _KnownDescMap) }
+
+// KnownValues returns all possible values for the type Known.
+func KnownValues() []Known { return _KnownValues }
+
+// Values returns all possible values for the type Known.
+func (i Known) Values() []enums.Enum { return enums.Values(_KnownValues) }
+
+// MarshalText implements the [encoding.TextMarshaler] interface.
+func (i Known) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
+
+// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
+func (i *Known) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Known") }

+ 99 - 0
vendor/cogentcore.org/core/base/fileinfo/filecat.go

@@ -0,0 +1,99 @@
+// Copyright (c) 2018, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fileinfo
+
+// Categories is a functional category for files; a broad functional
+// categorization that can help decide what to do with the file.
+//
+// It is computed in part from the mime type, but some types require
+// other information.
+//
+// No single categorization scheme is perfect, so any given use
+// may require examination of the full mime type etc, but this
+// provides a useful broad-scope categorization of file types.
+type Categories int32 //enums:enum
+
+const (
+	// UnknownCategory is an unknown file category
+	UnknownCategory Categories = iota
+
+	// Folder is a folder / directory
+	Folder
+
+	// Archive is a collection of files, e.g., zip tar
+	Archive
+
+	// Backup is a backup file (# ~ etc)
+	Backup
+
+	// Code is a programming language file
+	Code
+
+	// Doc is an editable word processing file including latex, markdown, html, css, etc
+	Doc
+
+	// Sheet is a spreadsheet file (.xls etc)
+	Sheet
+
+	// Data is some kind of data format (csv, json, database, etc)
+	Data
+
+	// Text is some other kind of text file
+	Text
+
+	// Image is an image (jpeg, png, svg, etc) *including* PDF
+	Image
+
+	// Model is a 3D model
+	Model
+
+	// Audio is an audio file
+	Audio
+
+	// Video is a video file
+	Video
+
+	// Font is a font file
+	Font
+
+	// Exe is a binary executable file (scripts go in Code)
+	Exe
+
+	// Bin is some other type of binary (object files, libraries, etc)
+	Bin
+)
+
+// CategoryFromMime returns the file category based on the mime type;
+// not all Categories can be inferred from file types
+func CategoryFromMime(mime string) Categories {
+	if mime == "" {
+		return UnknownCategory
+	}
+	mime = MimeNoChar(mime)
+	if mt, has := AvailableMimes[mime]; has {
+		return mt.Cat // must be set!
+	}
+	// try from type:
+	ms := MimeTop(mime)
+	if ms == "" {
+		return UnknownCategory
+	}
+	switch ms {
+	case "image":
+		return Image
+	case "audio":
+		return Audio
+	case "video":
+		return Video
+	case "font":
+		return Font
+	case "model":
+		return Model
+	}
+	if ms == "text" {
+		return Text
+	}
+	return UnknownCategory
+}

+ 393 - 0
vendor/cogentcore.org/core/base/fileinfo/fileinfo.go

@@ -0,0 +1,393 @@
+// Copyright (c) 2018, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package fileinfo manages file information and categorizes file types;
+// it is the single, consolidated place where file info, mimetypes, and
+// filetypes are managed in Cogent Core.
+//
+// This whole space is a bit of a heterogenous mess; most file types
+// we care about are not registered on the official iana registry, which
+// seems mainly to have paid registrations in application/ category,
+// and doesn't have any of the main programming languages etc.
+//
+// The official Go std library support depends on different platform
+// libraries and mac doesn't have one, so it has very limited support
+//
+// So we sucked it up and made a full list of all the major file types
+// that we really care about and also provide a broader category-level organization
+// that is useful for functionally organizing / operating on files.
+//
+// As fallback, we are this Go package:
+// github.com/h2non/filetype
+package fileinfo
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"io/fs"
+	"log"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"cogentcore.org/core/base/datasize"
+	"cogentcore.org/core/base/vcs"
+	"cogentcore.org/core/icons"
+	"github.com/Bios-Marcel/wastebasket"
+)
+
+// FileInfo represents the information about a given file / directory,
+// including icon, mimetype, etc
+type FileInfo struct { //types:add
+
+	// icon for file
+	Ic icons.Icon `table:"no-header"`
+
+	// name of the file, without any path
+	Name string `width:"40"`
+
+	// size of the file
+	Size datasize.Size
+
+	// type of file / directory; shorter, more user-friendly
+	// version of mime type, based on category
+	Kind string `width:"20" max-width:"20"`
+
+	// full official mime type of the contents
+	Mime string `table:"-"`
+
+	// functional category of the file, based on mime data etc
+	Cat Categories `table:"-"`
+
+	// known file type
+	Known Known `table:"-"`
+
+	// file mode bits
+	Mode fs.FileMode `table:"-"`
+
+	// time that contents (only) were last modified
+	ModTime time.Time `label:"Last modified"`
+
+	// version control system status, when enabled
+	VCS vcs.FileStatus `table:"-"`
+
+	// full path to file, including name; for file functions
+	Path string `table:"-"`
+}
+
+// NewFileInfo returns a new FileInfo for given file.
+func NewFileInfo(fname string) (*FileInfo, error) {
+	fi := &FileInfo{}
+	err := fi.InitFile(fname)
+	return fi, err
+}
+
+// NewFileInfoType returns a new FileInfo representing the given file type.
+func NewFileInfoType(ftyp Known) *FileInfo {
+	fi := &FileInfo{}
+	fi.SetType(ftyp)
+	return fi
+}
+
+// InitFile initializes a FileInfo for os file based on a filename,
+// which is updated to full path using filepath.Abs.
+// Returns error from filepath.Abs and / or fs.Stat error on the given file,
+// but file info will be updated based on the filename even if
+// the file does not exist.
+func (fi *FileInfo) InitFile(fname string) error {
+	var errs []error
+	path, err := filepath.Abs(fname)
+	if err == nil {
+		fi.Path = path
+	} else {
+		fi.Path = fname
+	}
+	_, fi.Name = filepath.Split(path)
+	fi.SetMimeInfo()
+	info, err := os.Stat(fi.Path)
+	if err != nil {
+		errs = append(errs, err)
+	} else {
+		fi.SetFileInfo(info)
+	}
+	return errors.Join(errs...)
+}
+
+// InitFileFS initializes a FileInfo based on filename in given fs.FS.
+// Returns error from fs.Stat error on the given file,
+// but file info will be updated based on the filename even if
+// the file does not exist.
+func (fi *FileInfo) InitFileFS(fsys fs.FS, fname string) error {
+	var errs []error
+	fi.Path = fname
+	_, fi.Name = path.Split(fname)
+	fi.SetMimeInfo()
+	info, err := fs.Stat(fsys, fi.Path)
+	if err != nil {
+		errs = append(errs, err)
+	} else {
+		fi.SetFileInfo(info)
+	}
+	return errors.Join(errs...)
+}
+
+// SetMimeInfo parses the file name to set mime type,
+// which then drives Kind and Icon.
+func (fi *FileInfo) SetMimeInfo() error {
+	if fi.Path == "" || fi.Path == "." || fi.IsDir() {
+		return nil
+	}
+	fi.Cat = UnknownCategory
+	fi.Known = Unknown
+	fi.Kind = ""
+	mtyp, _, err := MimeFromFile(fi.Path)
+	if err != nil {
+		return err
+	}
+	fi.Mime = mtyp
+	fi.Cat = CategoryFromMime(fi.Mime)
+	fi.Known = MimeKnown(fi.Mime)
+	if fi.Cat != UnknownCategory {
+		fi.Kind = fi.Cat.String() + ": "
+	}
+	if fi.Known != Unknown {
+		fi.Kind += fi.Known.String()
+	} else {
+		fi.Kind += MimeSub(fi.Mime)
+	}
+	return nil
+}
+
+// SetFileInfo updates from given [fs.FileInfo]
+func (fi *FileInfo) SetFileInfo(info fs.FileInfo) {
+	fi.Size = datasize.Size(info.Size())
+	fi.Mode = info.Mode()
+	fi.ModTime = info.ModTime()
+	if info.IsDir() {
+		fi.Kind = "Folder"
+		fi.Cat = Folder
+		fi.Known = AnyFolder
+	} else {
+		if fi.Cat == UnknownCategory {
+			if fi.IsExec() {
+				fi.Cat = Exe
+				fi.Known = AnyExe
+			}
+		}
+	}
+	icn, _ := fi.FindIcon()
+	fi.Ic = icn
+}
+
+// SetType sets file type information for given Known file type
+func (fi *FileInfo) SetType(ftyp Known) {
+	mt := MimeFromKnown(ftyp)
+	fi.Mime = mt.Mime
+	fi.Cat = mt.Cat
+	fi.Known = mt.Known
+	if fi.Name == "" && len(mt.Exts) > 0 {
+		fi.Name = "_fake" + mt.Exts[0]
+		fi.Path = fi.Name
+	}
+	fi.Kind = fi.Cat.String() + ": "
+	if fi.Known != Unknown {
+		fi.Kind += fi.Known.String()
+	}
+}
+
+// IsDir returns true if file is a directory (folder)
+func (fi *FileInfo) IsDir() bool {
+	return fi.Mode.IsDir()
+}
+
+// IsExec returns true if file is an executable file
+func (fi *FileInfo) IsExec() bool {
+	if fi.Mode&0111 != 0 {
+		return true
+	}
+	ext := filepath.Ext(fi.Path)
+	return ext == ".exe"
+}
+
+// IsSymLink returns true if file is a symbolic link
+func (fi *FileInfo) IsSymlink() bool {
+	return fi.Mode&os.ModeSymlink != 0
+}
+
+// IsHidden returns true if file name starts with . or _ which are typically hidden
+func (fi *FileInfo) IsHidden() bool {
+	return fi.Name == "" || fi.Name[0] == '.' || fi.Name[0] == '_'
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//    File ops
+
+// Duplicate creates a copy of given file -- only works for regular files, not
+// directories.
+func (fi *FileInfo) Duplicate() (string, error) { //types:add
+	if fi.IsDir() {
+		err := fmt.Errorf("core.Duplicate: cannot copy directory: %v", fi.Path)
+		log.Println(err)
+		return "", err
+	}
+	ext := filepath.Ext(fi.Path)
+	noext := strings.TrimSuffix(fi.Path, ext)
+	dst := noext + "_Copy" + ext
+	cpcnt := 0
+	for {
+		if _, err := os.Stat(dst); !os.IsNotExist(err) {
+			cpcnt++
+			dst = noext + fmt.Sprintf("_Copy%d", cpcnt) + ext
+		} else {
+			break
+		}
+	}
+	return dst, CopyFile(dst, fi.Path, fi.Mode)
+}
+
+// Delete moves the file to the trash / recycling bin.
+// On mobile and web, it deletes it directly.
+func (fi *FileInfo) Delete() error { //types:add
+	err := wastebasket.Trash(fi.Path)
+	if errors.Is(err, wastebasket.ErrPlatformNotSupported) {
+		return os.RemoveAll(fi.Path)
+	}
+	return err
+}
+
+// Filenames recursively adds fullpath filenames within the starting directory to the "names" slice.
+// Directory names within the starting directory are not added.
+func Filenames(d os.File, names *[]string) (err error) {
+	nms, err := d.Readdirnames(-1)
+	if err != nil {
+		return err
+	}
+	for _, n := range nms {
+		fp := filepath.Join(d.Name(), n)
+		ffi, ferr := os.Stat(fp)
+		if ferr != nil {
+			return ferr
+		}
+		if ffi.IsDir() {
+			dd, err := os.Open(fp)
+			if err != nil {
+				return err
+			}
+			defer dd.Close()
+			Filenames(*dd, names)
+		} else {
+			*names = append(*names, fp)
+		}
+	}
+	return nil
+}
+
+// Filenames returns a slice of file names from the starting directory and its subdirectories
+func (fi *FileInfo) Filenames(names *[]string) (err error) {
+	if !fi.IsDir() {
+		err = errors.New("not a directory: Filenames returns a list of files within a directory")
+		return err
+	}
+	path := fi.Path
+	d, err := os.Open(path)
+	if err != nil {
+		return err
+	}
+	defer d.Close()
+
+	err = Filenames(*d, names)
+	return err
+}
+
+// RenamePath returns the proposed path or the new full path.
+// Does not actually do the renaming -- see Rename method.
+func (fi *FileInfo) RenamePath(path string) (newpath string, err error) {
+	if path == "" {
+		err = fmt.Errorf("core.Rename: new name is empty")
+		log.Println(err)
+		return path, err
+	}
+	if path == fi.Path {
+		return "", nil
+	}
+	ndir, np := filepath.Split(path)
+	if ndir == "" {
+		if np == fi.Name {
+			return path, nil
+		}
+		dir, _ := filepath.Split(fi.Path)
+		newpath = filepath.Join(dir, np)
+	}
+	return newpath, nil
+}
+
+// Rename renames (moves) this file to given new path name.
+// Updates the FileInfo setting to the new name, although it might
+// be out of scope if it moved into a new path
+func (fi *FileInfo) Rename(path string) (newpath string, err error) { //types:add
+	orgpath := fi.Path
+	newpath, err = fi.RenamePath(path)
+	if err != nil {
+		return
+	}
+	err = os.Rename(string(orgpath), newpath)
+	if err == nil {
+		fi.Path = newpath
+		_, fi.Name = filepath.Split(newpath)
+	}
+	return
+}
+
+// FindIcon uses file info to find an appropriate icon for this file -- uses
+// Kind string first to find a correspondingly named icon, and then tries the
+// extension.  Returns true on success.
+func (fi *FileInfo) FindIcon() (icons.Icon, bool) {
+	if fi.IsDir() {
+		return icons.Folder, true
+	}
+	return Icons[fi.Known], true
+}
+
+// Note: can get all the detailed birth, access, change times from this package
+// 	"github.com/djherbis/times"
+
+//////////////////////////////////////////////////////////////////////////////
+//    CopyFile
+
+// here's all the discussion about why CopyFile is not in std lib:
+// https://old.reddit.com/r/golang/comments/3lfqoh/why_golang_does_not_provide_a_copy_file_func/
+// https://github.com/golang/go/issues/8868
+
+// CopyFile copies the contents from src to dst atomically.
+// If dst does not exist, CopyFile creates it with permissions perm.
+// If the copy fails, CopyFile aborts and dst is preserved.
+func CopyFile(dst, src string, perm os.FileMode) error {
+	in, err := os.Open(src)
+	if err != nil {
+		return err
+	}
+	defer in.Close()
+	tmp, err := os.CreateTemp(filepath.Dir(dst), "")
+	if err != nil {
+		return err
+	}
+	_, err = io.Copy(tmp, in)
+	if err != nil {
+		tmp.Close()
+		os.Remove(tmp.Name())
+		return err
+	}
+	if err = tmp.Close(); err != nil {
+		os.Remove(tmp.Name())
+		return err
+	}
+	if err = os.Chmod(tmp.Name(), perm); err != nil {
+		os.Remove(tmp.Name())
+		return err
+	}
+	return os.Rename(tmp.Name(), dst)
+}

+ 153 - 0
vendor/cogentcore.org/core/base/fileinfo/icons.go

@@ -0,0 +1,153 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fileinfo
+
+import "cogentcore.org/core/icons"
+
+var Icons = map[Known]icons.Icon{
+	Unknown:   icons.QuestionMark,
+	Any:       icons.QuestionMark,
+	AnyKnown:  icons.File,
+	AnyFolder: icons.Folder,
+
+	AnyArchive: icons.Archive,
+	Multipart:  icons.Mail,
+	Tar:        icons.Archive,
+	Zip:        icons.Archive,
+	GZip:       icons.Archive,
+	SevenZ:     icons.Archive,
+	Xz:         icons.Archive,
+	BZip:       icons.Archive,
+	Dmg:        icons.Archive,
+	Shar:       icons.Archive,
+
+	AnyBackup: icons.Backup,
+	Trash:     icons.Delete,
+
+	AnyCode:     icons.Code,
+	Ada:         icons.Code,
+	Bash:        icons.Terminal,
+	Cosh:        icons.Terminal,
+	Csh:         icons.Terminal,
+	C:           icons.Code,
+	CSharp:      icons.Code,
+	D:           icons.Code,
+	Diff:        icons.Difference,
+	Eiffel:      icons.Code,
+	Erlang:      icons.Code,
+	Forth:       icons.Code,
+	Fortran:     icons.Calculate,
+	FSharp:      icons.Code,
+	Go:          icons.Go,
+	Haskell:     icons.Code,
+	Java:        icons.Coffee,
+	JavaScript:  icons.Javascript,
+	Lisp:        icons.Code,
+	Lua:         icons.Code,
+	Makefile:    icons.Makefile,
+	Mathematica: icons.Calculate,
+	Matlab:      icons.Calculate,
+	ObjC:        icons.Code,
+	OCaml:       icons.Code,
+	Pascal:      icons.Code,
+	Perl:        icons.Code,
+	Php:         icons.Code,
+	Prolog:      icons.Code,
+	Python:      icons.Code,
+	R:           icons.Calculate,
+	Ruby:        icons.Code,
+	Rust:        icons.Code,
+	Scala:       icons.Code,
+	Tcl:         icons.Code,
+
+	AnyDoc:  icons.Document,
+	BibTeX:  icons.Latex,
+	TeX:     icons.Latex,
+	Texinfo: icons.Latex,
+	Troff:   icons.Document,
+
+	Html:         icons.Html,
+	Css:          icons.Css,
+	Markdown:     icons.FileMarkdown,
+	Rtf:          icons.Document,
+	MSWord:       icons.Document,
+	OpenText:     icons.Document,
+	OpenPres:     icons.Document,
+	MSPowerpoint: icons.PresentToAll,
+
+	EBook: icons.Book,
+	EPub:  icons.Book,
+
+	AnySheet:  icons.BorderAll,
+	MSExcel:   icons.BorderAll,
+	OpenSheet: icons.BorderAll,
+
+	AnyData:  icons.Dataset,
+	Csv:      icons.Csv,
+	Json:     icons.Json,
+	Xml:      icons.Code,
+	Protobuf: icons.Memory,
+	Ini:      icons.Code,
+	Tsv:      icons.Tsv,
+	Uri:      icons.Link,
+	Color:    icons.Colors,
+	Yaml:     icons.Code,
+	Toml:     icons.Toml,
+
+	AnyText:   icons.TextSnippet,
+	PlainText: icons.TextSnippet,
+	ICal:      icons.CalendarMonth,
+	VCal:      icons.CalendarMonth,
+	VCard:     icons.ContactPage,
+
+	AnyImage:   icons.Image,
+	Pdf:        icons.PictureAsPdf,
+	Postscript: icons.Image,
+	Gimp:       icons.Image,
+	GraphVis:   icons.Schema,
+	Gif:        icons.Image,
+	Jpeg:       icons.Image,
+	Png:        icons.Image,
+	Svg:        icons.Shapes,
+	Tiff:       icons.Image,
+	Pnm:        icons.Image,
+	Pbm:        icons.Image,
+	Pgm:        icons.Image,
+	Ppm:        icons.Image,
+	Xbm:        icons.Image,
+	Xpm:        icons.Image,
+	Bmp:        icons.Image,
+	Heic:       icons.Image,
+	Heif:       icons.Image,
+
+	AnyModel: icons.Shapes,
+	Vrml:     icons.Shapes,
+	X3d:      icons.Shapes,
+	Obj:      icons.Shapes,
+
+	AnyAudio: icons.LibraryMusic,
+	Aac:      icons.LibraryMusic,
+	Flac:     icons.LibraryMusic,
+	Mp3:      icons.LibraryMusic,
+	Ogg:      icons.LibraryMusic,
+	Midi:     icons.LibraryMusic,
+	Wav:      icons.LibraryMusic,
+
+	AnyVideo: icons.Videocam,
+	Mpeg:     icons.Videocam,
+	Mp4:      icons.Videocam,
+	Mov:      icons.Videocam,
+	Ogv:      icons.Videocam,
+	Wmv:      icons.Videocam,
+	Avi:      icons.Videocam,
+
+	AnyFont:     icons.FontDownload,
+	TrueType:    icons.FontDownload,
+	WebOpenFont: icons.FontDownload,
+
+	AnyExe: icons.SmartDisplay,
+
+	AnyBin: icons.DeveloperBoard,
+}

+ 301 - 0
vendor/cogentcore.org/core/base/fileinfo/known.go

@@ -0,0 +1,301 @@
+// Copyright (c) 2018, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fileinfo
+
+//go:generate core generate
+
+import (
+	"fmt"
+)
+
+// Known is an enumerated list of known file types, for which
+// appropriate actions can be taken etc.
+type Known int32 //enums:enum
+
+// KnownMimes maps from the known type into the MimeType info for each
+// known file type; the known MimeType may be just one of
+// multiple that correspond to the known type; it should be first in list
+// and have extensions defined
+var KnownMimes map[Known]MimeType
+
+// MimeString gives the string representation of the canonical mime type
+// associated with given known mime type.
+func MimeString(kn Known) string {
+	mt, has := KnownMimes[kn]
+	if !has {
+		// log.Printf("fileinfo.MimeString called with unrecognized 'Known' type: %v\n", sup)
+		return ""
+	}
+	return mt.Mime
+}
+
+// Cat returns the Cat category for given known file type
+func (kn Known) Cat() Categories {
+	if kn == Unknown {
+		return UnknownCategory
+	}
+	mt, has := KnownMimes[kn]
+	if !has {
+		// log.Printf("fileinfo.KnownCat called with unrecognized 'Known' type: %v\n", sup)
+		return UnknownCategory
+	}
+	return mt.Cat
+}
+
+// IsMatch returns true if given file type matches target type,
+// which could be any of the Any options
+func IsMatch(targ, typ Known) bool {
+	if targ == Any {
+		return true
+	}
+	if targ == AnyKnown {
+		return typ != Unknown
+	}
+	if targ == typ {
+		return true
+	}
+	cat := typ.Cat()
+	switch targ {
+	case AnyFolder:
+		return cat == Folder
+	case AnyArchive:
+		return cat == Archive
+	case AnyBackup:
+		return cat == Backup
+	case AnyCode:
+		return cat == Code
+	case AnyDoc:
+		return cat == Doc
+	case AnySheet:
+		return cat == Sheet
+	case AnyData:
+		return cat == Data
+	case AnyText:
+		return cat == Text
+	case AnyImage:
+		return cat == Image
+	case AnyModel:
+		return cat == Model
+	case AnyAudio:
+		return cat == Audio
+	case AnyVideo:
+		return cat == Video
+	case AnyFont:
+		return cat == Font
+	case AnyExe:
+		return cat == Exe
+	case AnyBin:
+		return cat == Bin
+	}
+	return false
+}
+
+// IsMatchList returns true if given file type matches any of a list of targets
+// if list is empty, then always returns true
+func IsMatchList(targs []Known, typ Known) bool {
+	if len(targs) == 0 {
+		return true
+	}
+	for _, trg := range targs {
+		if IsMatch(trg, typ) {
+			return true
+		}
+	}
+	return false
+}
+
+// KnownByName looks up known file type by caps or lowercase name
+func KnownByName(name string) (Known, error) {
+	var kn Known
+	err := kn.SetString(name)
+	if err != nil {
+		err = fmt.Errorf("fileinfo.KnownByName: doesn't look like that is a known file type: %v", name)
+		return kn, err
+	}
+	return kn, nil
+}
+
+// These are the super high-frequency used mime types, to have very quick const level support
+const (
+	TextPlain = "text/plain"
+	DataJson  = "application/json"
+	DataXml   = "application/xml"
+	DataCsv   = "text/csv"
+)
+
+// These are the known file types, organized by category
+const (
+	// Unknown = a non-known file type
+	Unknown Known = iota
+
+	// Any is used when selecting a file type, if any type is OK (including Unknown)
+	// see also AnyKnown and the Any options for each category
+	Any
+
+	// AnyKnown is used when selecting a file type, if any Known
+	// file type is OK (excludes Unknown) -- see Any and Any options for each category
+	AnyKnown
+
+	// Folder is a folder / directory
+	AnyFolder
+
+	// Archive is a collection of files, e.g., zip tar
+	AnyArchive
+	Multipart
+	Tar
+	Zip
+	GZip
+	SevenZ
+	Xz
+	BZip
+	Dmg
+	Shar
+
+	// Backup files
+	AnyBackup
+	Trash
+
+	// Code is a programming language file
+	AnyCode
+	Ada
+	Bash
+	Cosh
+	Csh
+	C // includes C++
+	CSharp
+	D
+	Diff
+	Eiffel
+	Erlang
+	Forth
+	Fortran
+	FSharp
+	Go
+	Haskell
+	Java
+	JavaScript
+	Lisp
+	Lua
+	Makefile
+	Mathematica
+	Matlab
+	ObjC
+	OCaml
+	Pascal
+	Perl
+	Php
+	Prolog
+	Python
+	R
+	Ruby
+	Rust
+	Scala
+	Tcl
+
+	// Doc is an editable word processing file including latex, markdown, html, css, etc
+	AnyDoc
+	BibTeX
+	TeX
+	Texinfo
+	Troff
+
+	Html
+	Css
+	Markdown
+	Rtf
+	MSWord
+	OpenText
+	OpenPres
+	MSPowerpoint
+
+	EBook
+	EPub
+
+	// Sheet is a spreadsheet file (.xls etc)
+	AnySheet
+	MSExcel
+	OpenSheet
+
+	// Data is some kind of data format (csv, json, database, etc)
+	AnyData
+	Csv
+	Json
+	Xml
+	Protobuf
+	Ini
+	Tsv
+	Uri
+	Color
+	Yaml
+	Toml
+	// special support for data fs
+	Number
+	String
+	Tensor
+	Table
+
+	// Text is some other kind of text file
+	AnyText
+	PlainText // text/plain mimetype -- used for clipboard
+	ICal
+	VCal
+	VCard
+
+	// Image is an image (jpeg, png, svg, etc) *including* PDF
+	AnyImage
+	Pdf
+	Postscript
+	Gimp
+	GraphVis
+	Gif
+	Jpeg
+	Png
+	Svg
+	Tiff
+	Pnm
+	Pbm
+	Pgm
+	Ppm
+	Xbm
+	Xpm
+	Bmp
+	Heic
+	Heif
+
+	// Model is a 3D model
+	AnyModel
+	Vrml
+	X3d
+	Obj
+
+	// Audio is an audio file
+	AnyAudio
+	Aac
+	Flac
+	Mp3
+	Ogg
+	Midi
+	Wav
+
+	// Video is a video file
+	AnyVideo
+	Mpeg
+	Mp4
+	Mov
+	Ogv
+	Wmv
+	Avi
+
+	// Font is a font file
+	AnyFont
+	TrueType
+	WebOpenFont
+
+	// Exe is a binary executable file
+	AnyExe
+
+	// Bin is some other unrecognized binary type
+	AnyBin
+)

+ 254 - 0
vendor/cogentcore.org/core/base/fileinfo/mimedata/mimedata.go

@@ -0,0 +1,254 @@
+// Copyright (c) 2018, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package mimedata defines MIME data support used in clipboard and
+// drag-and-drop functions in the Cogent Core GUI.  mimedata.Data struct contains
+// format and []byte data, and multiple representations of the same data are
+// encoded in mimedata.Mimes which is just a []mimedata.Data slice -- it can
+// be encoded / decoded from mime multipart.
+//
+// See the fileinfo package for known mimetypes.
+package mimedata
+
+import (
+	"bytes"
+	"encoding/base64"
+	"fmt"
+	"io"
+	"log"
+	"mime"
+	"mime/multipart"
+	"net/textproto"
+	"strings"
+)
+
+const (
+	MIMEVersion1            = "MIME-Version: 1.0"
+	ContentType             = "Content-Type"
+	ContentTransferEncoding = "Content-Transfer-Encoding"
+
+	TextPlain = "text/plain"
+	DataJson  = "application/json"
+	DataXml   = "application/xml"
+)
+
+var (
+	MIMEVersion1B            = ([]byte)(MIMEVersion1)
+	ContentTypeB             = ([]byte)(ContentType)
+	ContentTransferEncodingB = ([]byte)(ContentTransferEncoding)
+)
+
+// Data represents one element of MIME data as a type string and byte slice
+type Data struct {
+	// MIME Type string representing the data, e.g., text/plain, text/html, text/xml, text/uri-list, image/jpg, png etc
+	Type string
+
+	// Data for the item
+	Data []byte
+}
+
+// NewTextData returns a Data representation of the string -- good idea to
+// always have a text/plain representation of everything on clipboard /
+// drag-n-drop
+func NewTextData(text string) *Data {
+	return &Data{TextPlain, []byte(text)}
+}
+
+// NewTextDataBytes returns a Data representation of the bytes string
+func NewTextDataBytes(text []byte) *Data {
+	return &Data{TextPlain, text}
+}
+
+// IsText returns true if type is any of the text/ types (literally looks for that
+// at start of Type) or is another known text type (e.g., AppJSON, XML)
+func IsText(typ string) bool {
+	if strings.HasPrefix(typ, "text/") {
+		return true
+	}
+	return typ == DataJson || typ == DataXml
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//    Mimes
+
+// Mimes is a slice of mime data, potentially encoding the same data in
+// different formats -- this is used for all system APIs for maximum
+// flexibility
+type Mimes []*Data
+
+// NewMimes returns a new Mimes slice of given length and capacity
+func NewMimes(ln, cp int) Mimes {
+	return make(Mimes, ln, cp)
+}
+
+// NewText returns a Mimes representation of the string as a single text/plain Data
+func NewText(text string) Mimes {
+	md := NewTextData(text)
+	mi := make(Mimes, 1)
+	mi[0] = md
+	return mi
+}
+
+// NewTextBytes returns a Mimes representation of the bytes string as a single text/plain Data
+func NewTextBytes(text []byte) Mimes {
+	md := NewTextDataBytes(text)
+	mi := make(Mimes, 1)
+	mi[0] = md
+	return mi
+}
+
+// NewTextPlus returns a Mimes representation of an item as a text string plus
+// a more specific type
+func NewTextPlus(text, typ string, data []byte) Mimes {
+	md := NewTextData(text)
+	mi := make(Mimes, 2)
+	mi[0] = md
+	mi[1] = &Data{typ, data}
+	return mi
+}
+
+// NewMime returns a Mimes representation of one element
+func NewMime(typ string, data []byte) Mimes {
+	mi := make(Mimes, 1)
+	mi[0] = &Data{typ, data}
+	return mi
+}
+
+// HasType returns true if Mimes has given type of data available
+func (mi Mimes) HasType(typ string) bool {
+	for _, d := range mi {
+		if d.Type == typ {
+			return true
+		}
+	}
+	return false
+}
+
+// TypeData returns data associated with given MIME type
+func (mi Mimes) TypeData(typ string) []byte {
+	for _, d := range mi {
+		if d.Type == typ {
+			return d.Data
+		}
+	}
+	return nil
+}
+
+// Text extracts all the text elements of given type as a string
+func (mi Mimes) Text(typ string) string {
+	str := ""
+	for _, d := range mi {
+		if d.Type == typ {
+			str = str + string(d.Data)
+		}
+	}
+	return str
+}
+
+// ToMultipart produces a MIME multipart representation of multiple data
+// elements present in the stream -- this should be used in system.Clipboard
+// whenever there are multiple elements to be pasted, because windows doesn't
+// support multiple clip elements, and linux isn't very convenient either
+func (mi Mimes) ToMultipart() []byte {
+	var b bytes.Buffer
+	mpw := multipart.NewWriter(io.Writer(&b))
+	hdr := fmt.Sprintf("%v\n%v: multipart/mixed; boundary=%v\n", MIMEVersion1, ContentType, mpw.Boundary())
+	b.Write(([]byte)(hdr))
+	for _, d := range mi {
+		mh := textproto.MIMEHeader{ContentType: {d.Type}}
+		bin := false
+		if !IsText(d.Type) {
+			mh.Add(ContentTransferEncoding, "base64")
+			bin = true
+		}
+		wp, _ := mpw.CreatePart(mh)
+		if bin {
+			eb := make([]byte, base64.StdEncoding.EncodedLen(len(d.Data)))
+			base64.StdEncoding.Encode(eb, d.Data)
+			wp.Write(eb)
+		} else {
+			wp.Write(d.Data)
+		}
+	}
+	mpw.Close()
+	return b.Bytes()
+}
+
+// IsMultipart examines data bytes to see if it has a MIME-Version: 1.0
+// ContentType: multipart/* header -- returns the actual multipart media type,
+// body of the data string after the header (assumed to be a single \n
+// terminated line at start of string, and the boundary separating multipart
+// elements (all from mime.ParseMediaType) -- mediaType is the mediaType if it
+// is another MIME type -- can check that for non-empty string
+func IsMultipart(str []byte) (isMulti bool, mediaType, boundary string, body []byte) {
+	isMulti = false
+	mediaType = ""
+	boundary = ""
+	body = ([]byte)("")
+	var pars map[string]string
+	var err error
+	if bytes.HasPrefix(str, MIMEVersion1B) {
+		cri := bytes.IndexByte(str, '\n')
+		if cri < 0 { // shouldn't happen
+			return
+		}
+		ctln := str[cri+1:]
+		if bytes.HasPrefix(ctln, ContentTypeB) { // should
+			cri2 := bytes.IndexByte(ctln, '\n')
+			if cri2 < 0 { // shouldn't happen
+				return
+			}
+			hdr := ctln[len(ContentTypeB)+1 : cri2]
+			mediaType, pars, err = mime.ParseMediaType(string(hdr))
+			if err != nil { // shouldn't happen
+				log.Printf("mimedata.IsMultipart: malformed MIME header: %v\n", err)
+				return
+			}
+			if strings.HasPrefix(mediaType, "multipart/") {
+				isMulti = true
+				body = str[cri2+1:]
+				boundary = pars["boundary"]
+			}
+		}
+	}
+	return
+}
+
+// FromMultipart parses a MIME multipart representation of multiple data
+// elements into corresponding mime data components
+func FromMultipart(body []byte, boundary string) Mimes {
+	mi := make(Mimes, 0, 10)
+	sr := bytes.NewReader(body)
+	mr := multipart.NewReader(sr, boundary)
+	for {
+		p, err := mr.NextPart()
+		if err == io.EOF {
+			return mi
+		}
+		if err != nil {
+			log.Printf("mimedata.IsMultipart: malformed multipart MIME: %v\n", err)
+			return mi
+		}
+		b, err := io.ReadAll(p)
+		if err != nil {
+			log.Printf("mimedata.IsMultipart: bad ReadAll of multipart MIME: %v\n", err)
+			return mi
+		}
+		d := Data{}
+		d.Type = p.Header.Get(ContentType)
+		cte := p.Header.Get(ContentTransferEncoding)
+		if cte != "" {
+			switch cte {
+			case "base64":
+				eb := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
+				base64.StdEncoding.Decode(eb, b)
+				b = eb
+			}
+		}
+		d.Data = b
+		mi = append(mi, &d)
+	}
+}
+
+// todo: image, etc extractors

+ 1223 - 0
vendor/cogentcore.org/core/base/fileinfo/mimetype.go

@@ -0,0 +1,1223 @@
+// Copyright (c) 2018, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fileinfo
+
+import (
+	"fmt"
+	"mime"
+	"path/filepath"
+	"strings"
+
+	"github.com/h2non/filetype"
+)
+
+// MimeNoChar returns the mime string without any charset
+// encoding information, or anything else after a ;
+func MimeNoChar(mime string) string {
+	if sidx := strings.Index(mime, ";"); sidx > 0 {
+		return strings.TrimSpace(mime[:sidx])
+	}
+	return mime
+}
+
+// MimeTop returns the top-level main type category from mime type
+// i.e., the thing before the /  -- returns empty if no /
+func MimeTop(mime string) string {
+	if sidx := strings.Index(mime, "/"); sidx > 0 {
+		return mime[:sidx]
+	}
+	return ""
+}
+
+// MimeSub returns the sub-level subtype category from mime type
+// i.e., the thing after the /  -- returns empty if no /
+// also trims off any charset encoding stuff
+func MimeSub(mime string) string {
+	if sidx := strings.Index(MimeNoChar(mime), "/"); sidx > 0 {
+		return mime[sidx+1:]
+	}
+	return ""
+}
+
+// MimeFromFile gets mime type from file, using Gabriel Vasile's mimetype
+// package, mime.TypeByExtension, the chroma syntax highlighter,
+// CustomExtMimeMap, and finally FileExtMimeMap.  Use the mimetype package's
+// extension mechanism to add further content-based matchers as needed, and
+// set CustomExtMimeMap to your own map or call AddCustomExtMime for
+// extension-based ones.
+func MimeFromFile(fname string) (mtype, ext string, err error) {
+	ext = strings.ToLower(filepath.Ext(fname))
+	if mtyp, has := ExtMimeMap[ext]; has { // use our map first: very fast!
+		return mtyp, ext, nil
+	}
+	_, fn := filepath.Split(fname)
+	var fc, lc byte
+	if len(fn) > 0 {
+		fc = fn[0]
+		lc = fn[len(fn)-1]
+	}
+	if fc == '~' || fc == '%' || fc == '#' || lc == '~' || lc == '%' || lc == '#' {
+		return MimeString(Trash), ext, nil
+	}
+	mtypt, err := filetype.MatchFile(fn) // h2non next -- has good coverage
+	ptyp := ""
+	isplain := false
+	if err == nil {
+		mtyp := mtypt.MIME.Value
+		ext = mtypt.Extension
+		if strings.HasPrefix(mtyp, "text/plain") {
+			isplain = true
+			ptyp = mtyp
+		} else {
+			return mtyp, ext, nil
+		}
+	}
+	mtyp := mime.TypeByExtension(ext)
+	if mtyp != "" {
+		return mtyp, ext, nil
+	}
+	// TODO(kai/binsize): figure out how to do this without dragging in chroma dependency
+	// lexer := lexers.Match(fn) // todo: could get start of file and pass to
+	// // Analyze, but might be too slow..
+	// if lexer != nil {
+	// 	config := lexer.Config()
+	// 	if len(config.MimeTypes) > 0 {
+	// 		mtyp = config.MimeTypes[0]
+	// 		return mtyp, ext, nil
+	// 	}
+	// 	mtyp := "application/" + strings.ToLower(config.Name)
+	// 	return mtyp, ext, nil
+	// }
+	if isplain {
+		return ptyp, ext, nil
+	}
+	if strings.ToLower(fn) == "makefile" {
+		return MimeString(Makefile), ext, nil
+	}
+	return "", ext, fmt.Errorf("fileinfo.MimeFromFile could not find mime type for ext: %v file: %v", ext, fn)
+}
+
+// todo: use this to check against mime types!
+
+// MimeToKindMapInit makes sure the MimeToKindMap is initialized from
+// InitMimeToKindMap plus chroma lexer types.
+// func MimeToKindMapInit() {
+// 	if MimeToKindMap != nil {
+// 		return
+// 	}
+// 	MimeToKindMap = InitMimeToKindMap
+// 	for _, l := range lexers.Registry.Lexers {
+// 		config := l.Config()
+// 		nm := strings.ToLower(config.Name)
+// 		if len(config.MimeTypes) > 0 {
+// 			mtyp := config.MimeTypes[0]
+// 			MimeToKindMap[mtyp] = nm
+// 		} else {
+// 			MimeToKindMap["application/"+nm] = nm
+// 		}
+// 	}
+// }
+
+//////////////////////////////////////////////////////////////////////////////
+//    Mime types
+
+// ExtMimeMap is the map from extension to mime type, built from AvailMimes
+var ExtMimeMap = map[string]string{}
+
+// MimeType contains all the information associated with a given mime type
+type MimeType struct {
+
+	// mimetype string: type/subtype
+	Mime string
+
+	// file extensions associated with this file type
+	Exts []string
+
+	// category of file
+	Cat Categories
+
+	// if known, the name of the known file type, else NoSupporUnknown
+	Known Known
+}
+
+// CustomMimes can be set by other apps to contain custom mime types that
+// go beyond what is in the standard ones, and can also redefine and
+// override the standard one
+var CustomMimes []MimeType
+
+// AvailableMimes is the full list (as a map from mimetype) of available defined mime types
+// built from StdMimes (compiled in) and then CustomMimes can override
+var AvailableMimes map[string]MimeType
+
+// MimeKnown returns the known type for given mime key,
+// or Unknown if not found or not a known file type
+func MimeKnown(mime string) Known {
+	mt, has := AvailableMimes[MimeNoChar(mime)]
+	if !has {
+		return Unknown
+	}
+	return mt.Known
+}
+
+// ExtKnown returns the known type for given file extension,
+// or Unknown if not found or not a known file type
+func ExtKnown(ext string) Known {
+	mime, has := ExtMimeMap[ext]
+	if !has {
+		return Unknown
+	}
+	mt, has := AvailableMimes[mime]
+	if !has {
+		return Unknown
+	}
+	return mt.Known
+}
+
+// KnownFromFile returns the known type for given file,
+// or Unknown if not found or not a known file type
+func KnownFromFile(fname string) Known {
+	mtyp, _, err := MimeFromFile(fname)
+	if err != nil {
+		return Unknown
+	}
+	return MimeKnown(mtyp)
+}
+
+// MimeFromKnown returns MimeType info for given known file type.
+func MimeFromKnown(ftyp Known) MimeType {
+	for _, mt := range AvailableMimes {
+		if mt.Known == ftyp {
+			return mt
+		}
+	}
+	return MimeType{}
+}
+
+// MergeAvailableMimes merges the StdMimes and CustomMimes into AvailMimes
+// if CustomMimes is updated, then this should be called -- initially
+// it just has StdMimes.
+// It also builds the ExtMimeMap to map from extension to mime type
+// and KnownMimes map of known file types onto their full
+// mime type entry
+func MergeAvailableMimes() {
+	AvailableMimes = make(map[string]MimeType, len(StandardMimes)+len(CustomMimes))
+	for _, mt := range StandardMimes {
+		AvailableMimes[mt.Mime] = mt
+	}
+	for _, mt := range CustomMimes {
+		AvailableMimes[mt.Mime] = mt // overwrite automatically
+	}
+	ExtMimeMap = make(map[string]string) // start over
+	KnownMimes = make(map[Known]MimeType)
+	for _, mt := range AvailableMimes {
+		if len(mt.Exts) > 0 { // first pass add only ext guys to support
+			for _, ex := range mt.Exts {
+				if ex[0] != '.' {
+					fmt.Printf("fileinfo.MergeAvailMimes: ext: %v does not start with a . in type: %v\n", ex, mt.Mime)
+				}
+				if pmt, has := ExtMimeMap[ex]; has {
+					fmt.Printf("fileinfo.MergeAvailMimes: non-unique ext: %v assigned to mime type: %v AND %v\n", ex, pmt, mt.Mime)
+				} else {
+					ExtMimeMap[ex] = mt.Mime
+				}
+			}
+			if mt.Known != Unknown {
+				if hsp, has := KnownMimes[mt.Known]; has {
+					fmt.Printf("fileinfo.MergeAvailMimes: more-than-one mimetype has extensions for same known file type: %v -- one: %v other %v\n", mt.Known, hsp.Mime, mt.Mime)
+				} else {
+					KnownMimes[mt.Known] = mt
+				}
+			}
+		}
+	}
+	// second pass to get any known guys that don't have exts
+	for _, mt := range AvailableMimes {
+		if mt.Known != Unknown {
+			if _, has := KnownMimes[mt.Known]; !has {
+				KnownMimes[mt.Known] = mt
+			}
+		}
+	}
+}
+
+func init() {
+	MergeAvailableMimes()
+}
+
+// http://www.iana.org/assignments/media-types/media-types.xhtml
+// https://github.com/mirage/ocaml-magic-mime/blob/master/x-mime.types
+// https://www.apt-browse.org/browse/debian/stretch/main/all/mime-support/3.60/file/etc/mime.types
+// https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html
+// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types
+
+// StandardMimes is the full list of standard mime types compiled into our code
+// various other maps etc are constructed from it.
+// When there are multiple types associated with the same real type, pick one
+// to be the canonical one and give it, and *only* it, the extensions!
+var StandardMimes = []MimeType{
+	// Folder
+	{"text/directory", nil, Folder, Unknown},
+
+	// Archive
+	{"multipart/mixed", nil, Archive, Multipart},
+	{"application/tar", []string{".tar", ".tar.gz", ".tgz", ".taz", ".taZ", ".tar.bz2", ".tz2", ".tbz2", ".tbz", ".tar.lz", ".tar.lzma", ".tlz", ".tar.lzop", ".tar.xz"}, Archive, Tar},
+	{"application/x-gtar", nil, Archive, Tar},
+	{"application/x-gtar-compressed", nil, Archive, Tar},
+	{"application/x-tar", nil, Archive, Tar},
+
+	{"application/zip", []string{".zip"}, Archive, Zip},
+	{"application/gzip", []string{".gz"}, Archive, GZip},
+	{"application/x-7z-compressed", []string{".7z"}, Archive, SevenZ},
+	{"application/x-xz", []string{".xz"}, Archive, Xz},
+	{"application/x-bzip", []string{".bz", ".bz2"}, Archive, BZip},
+	{"application/x-bzip2", nil, Archive, BZip},
+
+	{"application/x-apple-diskimage", []string{".dmg"}, Archive, Dmg},
+	{"application/x-shar", []string{".shar"}, Archive, Shar},
+
+	{"application/x-bittorrent", []string{".torrent"}, Archive, Unknown},
+	{"application/rar", []string{".rar"}, Archive, Unknown},
+	{"application/x-stuffit", []string{".sit", ".sitx"}, Archive, Unknown},
+
+	{"application/vnd.android.package-archive", []string{".apk"}, Archive, Unknown},
+	{"application/vnd.debian.binary-package", []string{".deb", ".ddeb", ".udeb"}, Archive, Unknown},
+	{"application/x-debian-package", nil, Archive, Unknown},
+	{"application/x-redhat-package-manager", []string{".rpm"}, Archive, Unknown},
+	{"text/x-rpm-spec", nil, Archive, Unknown},
+
+	// Backup
+	{"application/x-trash", []string{".bak", ".old", ".sik"}, Backup, Trash}, // also "~", "%", "#",
+
+	// Code -- use text/ as main instead of application as there are more text
+	{"text/x-ada", []string{".adb", ".ads", ".ada"}, Code, Ada},
+	{"text/x-asp", []string{".aspx", ".asax", ".ascx", ".ashx", ".asmx", ".axd"}, Code, Unknown},
+
+	{"text/x-sh", []string{".bash", ".sh"}, Code, Bash},
+	{"application/x-sh", nil, Code, Bash},
+
+	{"text/x-csrc", []string{".c", ".C", ".c++", ".cpp", ".cxx", ".cc", ".h", ".h++", ".hpp", ".hxx", ".hh", ".hlsl", ".gsl", ".frag", ".vert", ".mm"}, Code, C}, // this is apparently the main one now
+	{"text/x-chdr", nil, Code, C},
+	{"text/x-c", nil, Code, C},
+	{"text/x-c++hdr", nil, Code, C},
+	{"text/x-c++src", nil, Code, C},
+	{"text/x-chdr", nil, Code, C},
+	{"text/x-cpp", nil, Code, C},
+
+	{"text/x-csh", []string{".csh"}, Code, Csh},
+	{"application/x-csh", nil, Code, Csh},
+
+	{"text/x-csharp", []string{".cs"}, Code, CSharp},
+	{"text/x-dsrc", []string{".d"}, Code, D},
+	{"text/x-diff", []string{".diff", ".patch"}, Code, Diff},
+	{"text/x-eiffel", []string{".e"}, Code, Eiffel},
+	{"text/x-erlang", []string{".erl", ".hrl", ".escript"}, Code, Erlang}, // note: ".es" conflicts with ecmascript
+	{"text/x-forth", []string{".frt"}, Code, Forth},                       // note: ".fs" conflicts with fsharp
+	{"text/x-fortran", []string{".f", ".F"}, Code, Fortran},
+	{"text/x-fsharp", []string{".fs", ".fsi"}, Code, FSharp},
+	{"text/x-gosrc", []string{".go", ".mod", ".work", ".cosh"}, Code, Go},
+	{"text/x-haskell", []string{".hs", ".lhs"}, Code, Haskell},
+	{"text/x-literate-haskell", nil, Code, Haskell}, // todo: not sure if same or not
+
+	{"text/x-java", []string{".java", ".jar"}, Code, Java},
+	{"application/java-archive", nil, Code, Java},
+	{"application/javascript", []string{".js"}, Code, JavaScript},
+	{"application/ecmascript", []string{".es"}, Code, Unknown},
+
+	{"text/x-common-lisp", []string{".lisp", ".cl", ".el"}, Code, Lisp},
+	{"text/elisp", nil, Code, Lisp},
+	{"text/x-elisp", nil, Code, Lisp},
+	{"application/emacs-lisp", nil, Code, Lisp},
+
+	{"text/x-lua", []string{".lua", ".wlua"}, Code, Lua},
+
+	{"text/x-makefile", nil, Code, Makefile},
+	{"text/x-autoconf", nil, Code, Makefile},
+
+	{"text/x-moc", []string{".moc"}, Code, Unknown},
+
+	{"application/mathematica", []string{".nb", ".nbp"}, Code, Mathematica},
+
+	{"text/x-matlab", []string{".m"}, Code, Matlab},
+	{"text/matlab", nil, Code, Matlab},
+	{"text/octave", nil, Code, Matlab},
+	{"text/scilab", []string{".sci", ".sce", ".tst"}, Code, Unknown},
+
+	{"text/x-modelica", []string{".mo"}, Code, Unknown},
+	{"text/x-nemerle", []string{".n"}, Code, Unknown},
+
+	{"text/x-objcsrc", nil, Code, ObjC}, // doesn't have chroma support -- use C instead
+	{"text/x-objective-j", nil, Code, Unknown},
+
+	{"text/x-ocaml", []string{".ml", ".mli", ".mll", ".mly"}, Code, OCaml},
+	{"text/x-pascal", []string{".p", ".pas"}, Code, Pascal},
+	{"text/x-perl", []string{".pl", ".pm"}, Code, Perl},
+	{"text/x-php", []string{".php", ".php3", ".php4", ".php5", ".inc"}, Code, Php},
+	{"text/x-prolog", []string{".ecl", ".prolog", ".pro"}, Code, Prolog}, // note: ".pl" conflicts
+
+	{"text/x-python", []string{".py", ".pyc", ".pyo", ".pyw"}, Code, Python},
+	{"application/x-python-code", nil, Code, Python},
+
+	{"text/x-rust", []string{".rs"}, Code, Rust},
+	{"text/rust", nil, Code, Rust},
+
+	{"text/x-r", []string{".r", ".S", ".R", ".Rhistory", ".Rprofile", ".Renviron"}, Code, R},
+	{"text/x-R", nil, Code, R},
+	{"text/S-Plus", nil, Code, R},
+	{"text/S", nil, Code, R},
+	{"text/x-r-source", nil, Code, R},
+	{"text/x-r-history", nil, Code, R},
+	{"text/x-r-profile", nil, Code, R},
+
+	{"text/x-ruby", []string{".rb"}, Code, Ruby},
+	{"application/x-ruby", nil, Code, Ruby},
+	{"text/x-scala", []string{".scala"}, Code, Scala},
+	{"text/x-tcl", []string{".tcl", ".tk"}, Code, Tcl},
+	{"application/x-tcl", nil, Code, Tcl},
+
+	// Doc
+	{"text/x-bibtex", []string{".bib"}, Doc, BibTeX},
+	{"text/x-tex", []string{".tex", ".ltx", ".sty", ".cls", ".latex"}, Doc, TeX},
+	{"application/x-latex", nil, Doc, TeX},
+
+	{"application/x-texinfo", []string{".texinfo", ".texi"}, Doc, Texinfo},
+
+	{"application/x-troff", []string{".t", ".tr", ".roff", ".man", ".me", ".ms"}, Doc, Troff},
+	{"application/x-troff-man", nil, Doc, Troff},
+	{"application/x-troff-me", nil, Doc, Troff},
+	{"application/x-troff-ms", nil, Doc, Troff},
+
+	{"text/html", []string{".html", ".htm", ".shtml", ".xhtml", ".xht"}, Doc, Html},
+	{"application/xhtml+xml", nil, Doc, Html},
+	{"text/mathml", []string{".mml"}, Doc, Unknown},
+	{"text/css", []string{".css"}, Doc, Css},
+
+	{"text/markdown", []string{".md", ".markdown"}, Doc, Markdown},
+	{"text/x-markdown", nil, Doc, Markdown},
+
+	{"application/rtf", []string{".rtf"}, Doc, Rtf},
+	{"text/richtext", []string{".rtx"}, Doc, Unknown},
+
+	{"application/mbox", []string{".mbox"}, Doc, Unknown},
+	{"application/x-rss+xml", []string{".rss"}, Doc, Unknown},
+
+	{"application/msword", []string{".doc", ".dot", ".docx", ".dotx"}, Doc, MSWord},
+	{"application/vnd.ms-word", nil, Doc, MSWord},
+	{"application/vnd.openxmlformats-officedocument.wordprocessingml.document", nil, Doc, MSWord},
+	{"application/vnd.openxmlformats-officedocument.wordprocessingml.template", nil, Doc, MSWord},
+
+	{"application/vnd.oasis.opendocument.text", []string{".odt", ".odm", ".ott", ".oth", ".sxw", ".sxg", ".stw", ".sxm"}, Doc, OpenText},
+	{"application/vnd.oasis.opendocument.text-master", nil, Doc, OpenText},
+	{"application/vnd.oasis.opendocument.text-template", nil, Doc, OpenText},
+	{"application/vnd.oasis.opendocument.text-web", nil, Doc, OpenText},
+	{"application/vnd.sun.xml.writer", nil, Doc, OpenText},
+	{"application/vnd.sun.xml.writer.global", nil, Doc, OpenText},
+	{"application/vnd.sun.xml.writer.template", nil, Doc, OpenText},
+	{"application/vnd.sun.xml.math", nil, Doc, OpenText},
+
+	{"application/vnd.oasis.opendocument.presentation", []string{".odp", ".otp", ".sxi", ".sti"}, Doc, OpenPres},
+	{"application/vnd.oasis.opendocument.presentation-template", nil, Doc, OpenPres},
+	{"application/vnd.sun.xml.impress", nil, Doc, OpenPres},
+	{"application/vnd.sun.xml.impress.template", nil, Doc, OpenPres},
+
+	{"application/vnd.ms-powerpoint", []string{".ppt", ".pps", ".pptx", ".sldx", ".ppsx", ".potx"}, Doc, MSPowerpoint},
+	{"application/vnd.openxmlformats-officedocument.presentationml.presentation", nil, Doc, MSPowerpoint},
+	{"application/vnd.openxmlformats-officedocument.presentationml.slide", nil, Doc, MSPowerpoint},
+	{"application/vnd.openxmlformats-officedocument.presentationml.slideshow", nil, Doc, MSPowerpoint},
+	{"application/vnd.openxmlformats-officedocument.presentationml.template", nil, Doc, MSPowerpoint},
+
+	{"application/ms-tnef", nil, Doc, Unknown},
+	{"application/vnd.ms-tnef", nil, Doc, Unknown},
+
+	{"application/onenote", []string{".one", ".onetoc2", ".onetmp", ".onepkg"}, Doc, Unknown},
+
+	{"application/pgp-encrypted", []string{".pgp"}, Doc, Unknown},
+	{"application/pgp-keys", []string{".key"}, Doc, Unknown},
+	{"application/pgp-signature", []string{".sig"}, Doc, Unknown},
+
+	{"application/vnd.amazon.ebook", []string{".azw"}, Doc, EBook},
+	{"application/epub+zip", []string{".epub"}, Doc, EPub},
+
+	// Sheet
+	{"application/vnd.ms-excel", []string{".xls", ".xlb", ".xlt", ".xlsx", ".xltx"}, Sheet, MSExcel},
+	{"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", nil, Sheet, MSExcel},
+	{"application/vnd.openxmlformats-officedocument.spreadsheetml.template", nil, Sheet, MSExcel},
+
+	{"application/vnd.oasis.opendocument.spreadsheet", []string{".ods", ".ots", ".sxc", ".stc", ".odf"}, Sheet, OpenSheet},
+	{"application/vnd.oasis.opendocument.spreadsheet-template", nil, Sheet, OpenSheet},
+	{"application/vnd.oasis.opendocument.formula", nil, Sheet, OpenSheet}, // todo: could be separate
+	{"application/vnd.sun.xml.calc", nil, Sheet, OpenSheet},
+	{"application/vnd.sun.xml.calc.template", nil, Sheet, OpenSheet},
+
+	// Data
+	{"text/csv", []string{".csv"}, Data, Csv},
+	{"application/json", []string{".json"}, Data, Json},
+	{"application/xml", []string{".xml", ".xsd"}, Data, Xml},
+	{"text/xml", nil, Data, Xml},
+	{"text/x-protobuf", []string{".proto"}, Data, Protobuf},
+	{"text/x-ini", []string{".ini", ".cfg", ".inf"}, Data, Ini},
+	{"text/x-ini-file", nil, Data, Ini},
+	{"text/uri-list", nil, Data, Uri},
+	{"application/x-color", nil, Data, Color},
+	{"text/toml", []string{".toml"}, Data, Toml},
+	{"application/toml", nil, Data, Toml},
+	{"application/yaml", []string{".yaml"}, Data, Yaml},
+
+	{"application/rdf+xml", []string{".rdf"}, Data, Unknown},
+	{"application/msaccess", []string{".mdb"}, Data, Unknown},
+	{"application/vnd.oasis.opendocument.database", []string{".odb"}, Data, Unknown},
+	{"text/tab-separated-values", []string{".tsv"}, Data, Tsv},
+	{"application/vnd.google-earth.kml+xml", []string{".kml", ".kmz"}, Data, Unknown},
+	{"application/vnd.google-earth.kmz", nil, Data, Unknown},
+	{"application/x-sql", []string{".sql"}, Data, Unknown},
+
+	// Text
+	{"text/plain", []string{".asc", ".txt", ".text", ".pot", ".brf", ".srt"}, Text, PlainText},
+	{"text/cache-manifest", []string{".appcache"}, Text, Unknown},
+	{"text/calendar", []string{".ics", ".icz"}, Text, ICal},
+	{"text/x-vcalendar", []string{".vcs"}, Text, VCal},
+	{"text/vcard", []string{".vcf", ".vcard"}, Text, VCard},
+
+	// Image
+	{"application/pdf", []string{".pdf"}, Image, Pdf},
+	{"application/postscript", []string{".ps", ".ai", ".eps", ".epsi", ".epsf", ".eps2", ".eps3"}, Image, Postscript},
+	{"application/vnd.oasis.opendocument.graphics", []string{".odc", ".odg", ".otg", ".odi", ".sxd", ".std"}, Image, Unknown},
+	{"application/vnd.oasis.opendocument.chart", nil, Image, Unknown},
+	{"application/vnd.oasis.opendocument.graphics-template", nil, Image, Unknown},
+	{"application/vnd.oasis.opendocument.image", nil, Image, Unknown},
+	{"application/vnd.sun.xml.draw", nil, Image, Unknown},
+	{"application/vnd.sun.xml.draw.template", nil, Image, Unknown},
+	{"application/x-xfig", []string{".fig"}, Image, Unknown},
+	{"application/x-xcf", []string{".xcf"}, Image, Gimp},
+	{"text/vnd.graphviz", []string{".gv"}, Image, GraphVis},
+
+	{"image/gif", []string{".gif"}, Image, Gif},
+	{"image/ief", []string{".ief"}, Image, Unknown},
+	{"image/jp2", []string{".jp2", ".jpg2"}, Image, Unknown},
+	{"image/jpeg", []string{".jpeg", ".jpg", ".jpe"}, Image, Jpeg},
+	{"image/jpm", []string{".jpm"}, Image, Unknown},
+	{"image/jpx", []string{".jpx", ".jpf"}, Image, Unknown},
+	{"image/pcx", []string{".pcx"}, Image, Unknown},
+	{"image/png", []string{".png"}, Image, Png},
+	{"image/heic", []string{".heic"}, Image, Heic},
+	{"image/heif", []string{".heif"}, Image, Heif},
+	{"image/svg+xml", []string{".svg", ".svgz"}, Image, Svg},
+	{"image/tiff", []string{".tiff", ".tif"}, Image, Tiff},
+	{"image/vnd.djvu", []string{".djvu", ".djv"}, Image, Unknown},
+	{"image/vnd.microsoft.icon", []string{".ico"}, Image, Unknown},
+	{"image/vnd.wap.wbmp", []string{".wbmp"}, Image, Unknown},
+	{"image/x-canon-cr2", []string{".cr2"}, Image, Unknown},
+	{"image/x-canon-crw", []string{".crw"}, Image, Unknown},
+	{"image/x-cmu-raster", []string{".ras"}, Image, Unknown},
+	{"image/x-coreldraw", []string{".cdr", ".pat", ".cdt", ".cpt"}, Image, Unknown},
+	{"image/x-coreldrawpattern", nil, Image, Unknown},
+	{"image/x-coreldrawtemplate", nil, Image, Unknown},
+	{"image/x-corelphotopaint", nil, Image, Unknown},
+	{"image/x-epson-erf", []string{".erf"}, Image, Unknown},
+	{"image/x-jg", []string{".art"}, Image, Unknown},
+	{"image/x-jng", []string{".jng"}, Image, Unknown},
+	{"image/x-ms-bmp", []string{".bmp"}, Image, Bmp},
+	{"image/x-nikon-nef", []string{".nef"}, Image, Unknown},
+	{"image/x-olympus-orf", []string{".orf"}, Image, Unknown},
+	{"image/x-photoshop", []string{".psd"}, Image, Unknown},
+	{"image/x-portable-anymap", []string{".pnm"}, Image, Pnm},
+	{"image/x-portable-bitmap", []string{".pbm"}, Image, Pbm},
+	{"image/x-portable-graymap", []string{".pgm"}, Image, Pgm},
+	{"image/x-portable-pixmap", []string{".ppm"}, Image, Ppm},
+	{"image/x-rgb", []string{".rgb"}, Image, Unknown},
+	{"image/x-xbitmap", []string{".xbm"}, Image, Xbm},
+	{"image/x-xpixmap", []string{".xpm"}, Image, Xpm},
+	{"image/x-xwindowdump", []string{".xwd"}, Image, Unknown},
+
+	// Model
+	{"model/iges", []string{".igs", ".iges"}, Model, Unknown},
+	{"model/mesh", []string{".msh", ".mesh", ".silo"}, Model, Unknown},
+	{"model/vrml", []string{".wrl", ".vrml", ".vrm"}, Model, Vrml},
+	{"x-world/x-vrml", nil, Model, Vrml},
+	{"model/x3d+xml", []string{".x3dv", ".x3d", ".x3db"}, Model, X3d},
+	{"model/x3d+vrml", nil, Model, X3d},
+	{"model/x3d+binary", nil, Model, X3d},
+	{"application/object", []string{".obj", ".mtl"}, Model, Obj},
+
+	// Audio
+	{"audio/aac", []string{".aac"}, Audio, Aac},
+	{"audio/flac", []string{".flac"}, Audio, Flac},
+	{"audio/mpeg", []string{".mpga", ".mpega", ".mp2", ".mp3", ".m4a"}, Audio, Mp3},
+	{"audio/mpegurl", []string{".m3u"}, Audio, Unknown},
+	{"audio/x-mpegurl", nil, Audio, Unknown},
+	{"audio/ogg", []string{".oga", ".ogg", ".opus", ".spx"}, Audio, Ogg},
+	{"audio/amr", []string{".amr"}, Audio, Unknown},
+	{"audio/amr-wb", []string{".awb"}, Audio, Unknown},
+	{"audio/annodex", []string{".axa"}, Audio, Unknown},
+	{"audio/basic", []string{".au", ".snd"}, Audio, Unknown},
+	{"audio/csound", []string{".csd", ".orc", ".sco"}, Audio, Unknown},
+	{"audio/midi", []string{".mid", ".midi", ".kar"}, Audio, Midi},
+	{"audio/prs.sid", []string{".sid"}, Audio, Unknown},
+	{"audio/x-aiff", []string{".aif", ".aiff", ".aifc"}, Audio, Unknown},
+	{"audio/x-gsm", []string{".gsm"}, Audio, Unknown},
+	{"audio/x-ms-wma", []string{".wma"}, Audio, Unknown},
+	{"audio/x-ms-wax", []string{".wax"}, Audio, Unknown},
+	{"audio/x-pn-realaudio", []string{".ra", ".rm", ".ram"}, Audio, Unknown},
+	{"audio/x-realaudio", nil, Audio, Unknown},
+	{"audio/x-scpls", []string{".pls"}, Audio, Unknown},
+	{"audio/x-sd2", []string{".sd2"}, Audio, Unknown},
+	{"audio/x-wav", []string{".wav"}, Audio, Wav},
+
+	// Video
+	{"video/3gpp", []string{".3gp"}, Video, Unknown},
+	{"video/annodex", []string{".axv"}, Video, Unknown},
+	{"video/dl", []string{".dl"}, Video, Unknown},
+	{"video/dv", []string{".dif", ".dv"}, Video, Unknown},
+	{"video/fli", []string{".fli"}, Video, Unknown},
+	{"video/gl", []string{".gl"}, Video, Unknown},
+	{"video/h264", nil, Video, Unknown},
+	{"video/mpeg", []string{".mpeg", ".mpg", ".mpe"}, Video, Mpeg},
+	{"video/MP2T", []string{".ts"}, Video, Unknown},
+	{"video/mp4", []string{".mp4"}, Video, Mp4},
+	{"video/quicktime", []string{".qt", ".mov"}, Video, Mov},
+	{"video/ogg", []string{".ogv"}, Video, Ogv},
+	{"video/webm", []string{".webm"}, Video, Unknown},
+	{"video/vnd.mpegurl", []string{".mxu"}, Video, Unknown},
+	{"video/x-flv", []string{".flv"}, Video, Unknown},
+	{"video/x-la-asf", []string{".lsf", ".lsx"}, Video, Unknown},
+	{"video/x-mng", []string{".mng"}, Video, Unknown},
+	{"video/x-ms-asf", []string{".asf", ".asx"}, Video, Unknown},
+	{"video/x-ms-wm", []string{".wm"}, Video, Unknown},
+	{"video/x-ms-wmv", []string{".wmv"}, Video, Wmv},
+	{"video/x-ms-wmx", []string{".wmx"}, Video, Unknown},
+	{"video/x-ms-wvx", []string{".wvx"}, Video, Unknown},
+	{"video/x-msvideo", []string{".avi"}, Video, Avi},
+	{"video/x-sgi-movie", []string{".movie"}, Video, Unknown},
+	{"video/x-matroska", []string{".mpv", ".mkv"}, Video, Unknown},
+	{"application/x-shockwave-flash", []string{".swf"}, Video, Unknown},
+
+	// Font
+	{"font/ttf", []string{".otf", ".ttf", ".ttc"}, Font, TrueType},
+	{"font/otf", nil, Font, TrueType},
+	{"application/font-sfnt", nil, Font, TrueType},
+	{"application/x-font-ttf", nil, Font, TrueType},
+
+	{"application/x-font", []string{".pfa", ".pfb", ".gsf", ".pcf", ".pcf.Z"}, Font, Unknown},
+	{"application/x-font-pcf", nil, Font, Unknown},
+	{"application/vnd.ms-fontobject", []string{".eot"}, Font, Unknown},
+
+	{"font/woff", []string{".woff", ".woff2"}, Font, WebOpenFont},
+	{"font/woff2", nil, Font, WebOpenFont},
+	{"application/font-woff", nil, Font, WebOpenFont},
+
+	// Exe
+	{"application/x-executable", nil, Exe, Unknown},
+	{"application/x-msdos-program", []string{".com", ".exe", ".bat", ".dll"}, Exe, Unknown},
+
+	// Binary
+	{"application/octet-stream", []string{".bin"}, Bin, Unknown},
+	{"application/x-object", []string{".o"}, Bin, Unknown},
+	{"text/x-libtool", nil, Bin, Unknown},
+}
+
+// below are entries from official /etc/mime.types that we don't recognize
+// or consider to be old / obsolete / not relevant -- please file an issue
+// or a pull-request to add to main list or add yourself in your own app
+
+// application/activemessage
+// application/andrew-insetez
+// application/annodexanx
+// application/applefile
+// application/atom+xmlatom
+// application/atomcat+xmlatomcat
+// application/atomicmail
+// application/atomserv+xmlatomsrv
+// application/batch-SMTP
+// application/bbolinlin
+// application/beep+xml
+// application/cals-1840
+// application/commonground
+// application/cu-seemecu
+// application/cybercash
+// application/davmount+xmldavmount
+// application/dca-rft
+// application/dec-dx
+// application/dicomdcm
+// application/docbook+xml
+// application/dsptypetsp
+// application/dvcs
+// application/edi-consent
+// application/edi-x12
+// application/edifact
+// application/eshop
+// application/font-tdpfrpfr
+// application/futuresplashspl
+// application/ghostview
+// application/htahta
+// application/http
+// application/hyperstudio
+// application/iges
+// application/index
+// application/index.cmd
+// application/index.obj
+// application/index.response
+// application/index.vnd
+// application/iotp
+// application/ipp
+// application/isup
+// application/java-serialized-objectser
+// application/java-vmclass
+// application/m3gm3g
+// application/mac-binhex40hqx
+// application/mac-compactprocpt
+// application/macwriteii
+// application/marc
+// application/mxfmxf
+// application/news-message-id
+// application/news-transmission
+// application/ocsp-request
+// application/ocsp-response
+// application/octet-streambin deploy msu msp
+// application/odaoda
+// application/oebps-package+xmlopf
+// application/oggogx
+// application/parityfec
+// application/pics-rulesprf
+// application/pkcs10
+// application/pkcs7-mime
+// application/pkcs7-signature
+// application/pkix-cert
+// application/pkix-crl
+// application/pkixcmp
+// application/prs.alvestrand.titrax-sheet
+// application/prs.cww
+// application/prs.nprend
+// application/qsig
+// application/remote-printing
+// application/riscos
+// application/sdp
+// application/set-payment
+// application/set-payment-initiation
+// application/set-registration
+// application/set-registration-initiation
+// application/sgml
+// application/sgml-open-catalog
+// application/sieve
+// application/slastl
+// application/slate
+// application/smil+xmlsmi smil
+// application/timestamp-query
+// application/timestamp-reply
+// application/vemmi
+// application/whoispp-query
+// application/whoispp-response
+// application/wita
+// application/x400-bp
+// application/xml-dtd
+// application/xml-external-parsed-entity
+// application/xslt+xmlxsl xslt
+// application/xspf+xmlxspf
+// application/vnd.3M.Post-it-Notes
+// application/vnd.accpac.simply.aso
+// application/vnd.accpac.simply.imp
+// application/vnd.acucobol
+// application/vnd.aether.imp
+// application/vnd.anser-web-certificate-issue-initiation
+// application/vnd.anser-web-funds-transfer-initiation
+// application/vnd.audiograph
+// application/vnd.bmi
+// application/vnd.businessobjects
+// application/vnd.canon-cpdl
+// application/vnd.canon-lips
+// application/vnd.cinderellacdy
+// application/vnd.claymore
+// application/vnd.commerce-battelle
+// application/vnd.commonspace
+// application/vnd.comsocaller
+// application/vnd.contact.cmsg
+// application/vnd.cosmocaller
+// application/vnd.ctc-posml
+// application/vnd.cups-postscript
+// application/vnd.cups-raster
+// application/vnd.cups-raw
+// application/vnd.cybank
+// application/vnd.dna
+// application/vnd.dpgraph
+// application/vnd.dxr
+// application/vnd.ecdis-update
+// application/vnd.ecowin.chart
+// application/vnd.ecowin.filerequest
+// application/vnd.ecowin.fileupdate
+// application/vnd.ecowin.series
+// application/vnd.ecowin.seriesrequest
+// application/vnd.ecowin.seriesupdate
+// application/vnd.enliven
+// application/vnd.epson.esf
+// application/vnd.epson.msf
+// application/vnd.epson.quickanime
+// application/vnd.epson.salt
+// application/vnd.epson.ssf
+// application/vnd.ericsson.quickcall
+// application/vnd.eudora.data
+// application/vnd.fdf
+// application/vnd.ffsns
+// application/vnd.flographit
+// application/vnd.font-fontforge-sfdsfd
+// application/vnd.framemaker
+// application/vnd.fsc.weblaunch
+// application/vnd.fujitsu.oasys
+// application/vnd.fujitsu.oasys2
+// application/vnd.fujitsu.oasys3
+// application/vnd.fujitsu.oasysgp
+// application/vnd.fujitsu.oasysprs
+// application/vnd.fujixerox.ddd
+// application/vnd.fujixerox.docuworks
+// application/vnd.fujixerox.docuworks.binder
+// application/vnd.fut-misnet
+// application/vnd.grafeq
+// application/vnd.groove-account
+// application/vnd.groove-identity-message
+// application/vnd.groove-injector
+// application/vnd.groove-tool-message
+// application/vnd.groove-tool-template
+// application/vnd.groove-vcard
+// application/vnd.hhe.lesson-player
+// application/vnd.hp-HPGL
+// application/vnd.hp-PCL
+// application/vnd.hp-PCLXL
+// application/vnd.hp-hpid
+// application/vnd.hp-hps
+// application/vnd.httphone
+// application/vnd.hzn-3d-crossword
+// application/vnd.ibm.MiniPay
+// application/vnd.ibm.afplinedata
+// application/vnd.ibm.modcap
+// application/vnd.informix-visionary
+// application/vnd.intercon.formnet
+// application/vnd.intertrust.digibox
+// application/vnd.intertrust.nncp
+// application/vnd.intu.qbo
+// application/vnd.intu.qfx
+// application/vnd.irepository.package+xml
+// application/vnd.is-xpr
+// application/vnd.japannet-directory-service
+// application/vnd.japannet-jpnstore-wakeup
+// application/vnd.japannet-payment-wakeup
+// application/vnd.japannet-registration
+// application/vnd.japannet-registration-wakeup
+// application/vnd.japannet-setstore-wakeup
+// application/vnd.japannet-verification
+// application/vnd.japannet-verification-wakeup
+// application/vnd.koan
+// application/vnd.lotus-1-2-3
+// application/vnd.lotus-approach
+// application/vnd.lotus-freelance
+// application/vnd.lotus-notes
+// application/vnd.lotus-organizer
+// application/vnd.lotus-screencam
+// application/vnd.lotus-wordpro
+// application/vnd.mcd
+// application/vnd.mediastation.cdkey
+// application/vnd.meridian-slingshot
+// application/vnd.mif
+// application/vnd.minisoft-hp3000-save
+// application/vnd.mitsubishi.misty-guard.trustweb
+// application/vnd.mobius.daf
+// application/vnd.mobius.dis
+// application/vnd.mobius.msl
+// application/vnd.mobius.plc
+// application/vnd.mobius.txf
+// application/vnd.motorola.flexsuite
+// application/vnd.motorola.flexsuite.adsi
+// application/vnd.motorola.flexsuite.fis
+// application/vnd.motorola.flexsuite.gotap
+// application/vnd.motorola.flexsuite.kmr
+// application/vnd.motorola.flexsuite.ttc
+// application/vnd.motorola.flexsuite.wem
+// application/vnd.mozilla.xul+xmlxul
+// application/vnd.ms-artgalry
+// application/vnd.ms-asf
+// application/vnd.ms-excel.addin.macroEnabled.12xlam
+// application/vnd.ms-excel.sheet.binary.macroEnabled.12xlsb
+// application/vnd.ms-excel.sheet.macroEnabled.12xlsm
+// application/vnd.ms-excel.template.macroEnabled.12xltm
+// application/vnd.ms-fontobjecteot
+// application/vnd.ms-lrm
+// application/vnd.ms-officethemethmx
+// application/vnd.ms-pki.seccatcat
+// #application/vnd.ms-pki.stlstl
+// application/vnd.ms-powerpoint.addin.macroEnabled.12ppam
+// application/vnd.ms-powerpoint.presentation.macroEnabled.12pptm
+// application/vnd.ms-powerpoint.slide.macroEnabled.12sldm
+// application/vnd.ms-powerpoint.slideshow.macroEnabled.12ppsm
+// application/vnd.ms-powerpoint.template.macroEnabled.12potm
+// application/vnd.ms-project
+// application/vnd.ms-word.document.macroEnabled.12docm
+// application/vnd.ms-word.template.macroEnabled.12dotm
+// application/vnd.ms-works
+// application/vnd.mseq
+// application/vnd.msign
+// application/vnd.music-niff
+// application/vnd.musician
+// application/vnd.netfpx
+// application/vnd.noblenet-directory
+// application/vnd.noblenet-sealer
+// application/vnd.noblenet-web
+// application/vnd.novadigm.EDM
+// application/vnd.novadigm.EDX
+// application/vnd.novadigm.EXT
+// application/vnd.osa.netdeploy
+// application/vnd.palm
+// application/vnd.pg.format
+// application/vnd.pg.osasli
+// application/vnd.powerbuilder6
+// application/vnd.powerbuilder6-s
+// application/vnd.powerbuilder7
+// application/vnd.powerbuilder7-s
+// application/vnd.powerbuilder75
+// application/vnd.powerbuilder75-s
+// application/vnd.previewsystems.box
+// application/vnd.publishare-delta-tree
+// application/vnd.pvi.ptid1
+// application/vnd.pwg-xhtml-print+xml
+// application/vnd.rapid
+// application/vnd.rim.codcod
+// application/vnd.s3sms
+// application/vnd.seemail
+// application/vnd.shana.informed.formdata
+// application/vnd.shana.informed.formtemplate
+// application/vnd.shana.informed.interchange
+// application/vnd.shana.informed.package
+// application/vnd.smafmmf
+// application/vnd.sss-cod
+// application/vnd.sss-dtf
+// application/vnd.sss-ntf
+// application/vnd.stardivision.calcsdc
+// application/vnd.stardivision.chartsds
+// application/vnd.stardivision.drawsda
+// application/vnd.stardivision.impresssdd
+// application/vnd.stardivision.mathsdf
+// application/vnd.stardivision.writersdw
+// application/vnd.stardivision.writer-globalsgl
+// application/vnd.street-stream
+// application/vnd.svd
+// application/vnd.swiftview-ics
+// application/vnd.symbian.installsis
+// application/vnd.tcpdump.pcapcap pcap
+// application/vnd.triscape.mxs
+// application/vnd.trueapp
+// application/vnd.truedoc
+// application/vnd.tve-trigger
+// application/vnd.ufdl
+// application/vnd.uplanet.alert
+// application/vnd.uplanet.alert-wbxml
+// application/vnd.uplanet.bearer-choice
+// application/vnd.uplanet.bearer-choice-wbxml
+// application/vnd.uplanet.cacheop
+// application/vnd.uplanet.cacheop-wbxml
+// application/vnd.uplanet.channel
+// application/vnd.uplanet.channel-wbxml
+// application/vnd.uplanet.list
+// application/vnd.uplanet.list-wbxml
+// application/vnd.uplanet.listcmd
+// application/vnd.uplanet.listcmd-wbxml
+// application/vnd.uplanet.signal
+// application/vnd.vcx
+// application/vnd.vectorworks
+// application/vnd.vidsoft.vidconference
+// application/vnd.visiovsd vst vsw vss
+// application/vnd.vividence.scriptfile
+// application/vnd.wap.sic
+// application/vnd.wap.slc
+// application/vnd.wap.wbxmlwbxml
+// application/vnd.wap.wmlcwmlc
+// application/vnd.wap.wmlscriptcwmlsc
+// application/vnd.webturbo
+// application/vnd.wordperfectwpd
+// application/vnd.wordperfect5.1wp5
+// application/vnd.wrq-hp3000-labelled
+// application/vnd.wt.stf
+// application/vnd.xara
+// application/vnd.xfdl
+// application/vnd.yellowriver-custom-menu
+// application/zlib
+// application/x-123wk
+// application/x-abiwordabw
+// application/x-bcpiobcpio
+// application/x-cabcab
+// application/x-cbrcbr
+// application/x-cbzcbz
+// application/x-cdfcdf cda
+// application/x-cdlinkvcd
+// application/x-chess-pgnpgn
+// application/x-comsolmph
+// application/x-core
+// application/x-cpiocpio
+// application/x-directordcr dir dxr
+// application/x-dmsdms
+// application/x-doomwad
+// application/x-dvidvi
+// application/x-freemindmm
+// application/x-futuresplashspl
+// application/x-ganttprojectgan
+// application/x-gnumericgnumeric
+// application/x-go-sgfsgf
+// application/x-graphing-calculatorgcf
+// application/x-hdfhdf
+// #application/x-httpd-erubyrhtml
+// #application/x-httpd-phpphtml pht php
+// #application/x-httpd-php-sourcephps
+// #application/x-httpd-php3php3
+// #application/x-httpd-php3-preprocessedphp3p
+// #application/x-httpd-php4php4
+// #application/x-httpd-php5php5
+// application/x-hwphwp
+// application/x-icaica
+// application/x-infoinfo
+// application/x-internet-signupins isp
+// application/x-iphoneiii
+// application/x-iso9660-imageiso
+// application/x-jamjam
+// application/x-java-applet
+// application/x-java-bean
+// application/x-java-jnlp-filejnlp
+// application/x-jmoljmz
+// application/x-kchartchrt
+// application/x-kdelnk
+// application/x-killustratorkil
+// application/x-koanskp skd skt skm
+// application/x-kpresenterkpr kpt
+// application/x-kspreadksp
+// application/x-kwordkwd kwt
+// application/x-lhalha
+// application/x-lyxlyx
+// application/x-lzhlzh
+// application/x-lzxlzx
+// application/x-makerfrm maker frame fm fb book fbdoc
+// application/x-mifmif
+// application/x-mpegURLm3u8
+// application/x-ms-applicationapplication
+// application/x-ms-manifestmanifest
+// application/x-ms-wmdwmd
+// application/x-ms-wmzwmz
+// application/x-msimsi
+// application/x-netcdfnc
+// application/x-ns-proxy-autoconfigpac
+// application/x-nwcnwc
+// application/x-oz-applicationoza
+// application/x-pkcs7-certreqrespp7r
+// application/x-pkcs7-crlcrl
+// application/x-qgisqgs shp shx
+// application/x-quicktimeplayerqtl
+// application/x-rdprdp
+// application/x-rx
+// application/x-scilabsci sce
+// application/x-scilab-xcosxcos
+// application/x-shellscript
+// application/x-shockwave-flashswf swfl
+// application/x-silverlightscr
+// application/x-sv4cpiosv4cpio
+// application/x-sv4crcsv4crc
+
+// application/x-tex-gfgf
+// application/x-tex-pkpk
+// application/x-ustarustar
+// application/x-videolan
+// application/x-wais-sourcesrc
+// application/x-wingzwz
+// application/x-x509-ca-certcrt
+// application/x-xpinstallxpi
+
+// audio/32kadpcm
+// audio/3gpp
+// audio/g.722.1
+// audio/l16
+// audio/mp4a-latm
+// audio/mpa-robust
+// audio/parityfec
+// audio/telephone-event
+// audio/tone
+// audio/vnd.cisco.nse
+// audio/vnd.cns.anp1
+// audio/vnd.cns.inf1
+// audio/vnd.digital-winds
+// audio/vnd.everad.plj
+// audio/vnd.lucent.voice
+// audio/vnd.nortel.vbk
+// audio/vnd.nuera.ecelp4800
+// audio/vnd.nuera.ecelp7470
+// audio/vnd.nuera.ecelp9600
+// audio/vnd.octel.sbc
+// audio/vnd.qcelp
+// audio/vnd.rhetorex.32kadpcm
+// audio/vnd.vmx.cvsd
+
+// chemical/x-alchemyalc
+// chemical/x-cachecac cache
+// chemical/x-cache-csfcsf
+// chemical/x-cactvs-binarycbin cascii ctab
+// chemical/x-cdxcdx
+// chemical/x-ceriuscer
+// chemical/x-chem3dc3d
+// chemical/x-chemdrawchm
+// chemical/x-cifcif
+// chemical/x-cmdfcmdf
+// chemical/x-cmlcml
+// chemical/x-compasscpa
+// chemical/x-crossfirebsd
+// chemical/x-csmlcsml csm
+// chemical/x-ctxctx
+// chemical/x-cxfcxf cef
+// #chemical/x-daylight-smilessmi
+// chemical/x-embl-dl-nucleotideemb embl
+// chemical/x-galactic-spcspc
+// chemical/x-gamess-inputinp gam gamin
+// chemical/x-gaussian-checkpointfch fchk
+// chemical/x-gaussian-cubecub
+// chemical/x-gaussian-inputgau gjc gjf
+// chemical/x-gaussian-loggal
+// chemical/x-gcg8-sequencegcg
+// chemical/x-genbankgen
+// chemical/x-hinhin
+// chemical/x-isostaristr ist
+// chemical/x-jcamp-dxjdx dx
+// chemical/x-kinemagekin
+// chemical/x-macmoleculemcm
+// chemical/x-macromodel-inputmmd mmod
+// chemical/x-mdl-molfilemol
+// chemical/x-mdl-rdfilerd
+// chemical/x-mdl-rxnfilerxn
+// chemical/x-mdl-sdfilesd sdf
+// chemical/x-mdl-tgftgf
+// #chemical/x-mifmif
+// chemical/x-mmcifmcif
+// chemical/x-mol2mol2
+// chemical/x-molconn-Zb
+// chemical/x-mopac-graphgpt
+// chemical/x-mopac-inputmop mopcrt mpc zmt
+// chemical/x-mopac-outmoo
+// chemical/x-mopac-vibmvb
+// chemical/x-ncbi-asn1asn
+// chemical/x-ncbi-asn1-asciiprt ent
+// chemical/x-ncbi-asn1-binaryval aso
+// chemical/x-ncbi-asn1-specasn
+// chemical/x-pdbpdb ent
+// chemical/x-rosdalros
+// chemical/x-swissprotsw
+// chemical/x-vamas-iso14976vms
+// chemical/x-vmdvmd
+// chemical/x-xtelxtel
+// chemical/x-xyzxyz
+
+// image/cgm
+// image/g3fax
+// image/naplps
+// image/prs.btif
+// image/prs.pti
+// image/vnd.cns.inf2
+// image/vnd.dwg
+// image/vnd.dxf
+// image/vnd.fastbidsheet
+// image/vnd.fpx
+// image/vnd.fst
+// image/vnd.fujixerox.edmics-mmr
+// image/vnd.fujixerox.edmics-rlc
+// image/vnd.mix
+// image/vnd.net-fpx
+// image/vnd.svf
+// image/vnd.xiff
+// image/x-icon
+
+// inode/chardevice
+// inode/blockdevice
+// inode/directory-locked
+// inode/directory
+// inode/fifo
+// inode/socket
+
+// message/delivery-status
+// message/disposition-notification
+// message/external-body
+// message/http
+// message/s-http
+// message/news
+// message/partial
+// message/rfc822eml
+
+// model/vnd.dwf
+// model/vnd.flatland.3dml
+// model/vnd.gdl
+// model/vnd.gs-gdl
+// model/vnd.gtw
+// model/vnd.mts
+// model/vnd.vtu
+
+// multipart/alternative
+// multipart/appledouble
+// multipart/byteranges
+// multipart/digest
+// multipart/encrypted
+// multipart/form-data
+// multipart/header-set
+// multipart/mixed
+// multipart/parallel
+// multipart/related
+// multipart/report
+// multipart/signed
+// multipart/voice-message
+
+// text/english
+// text/enriched
+// {"text/x-gap",
+// {"text/x-gtkrc",
+// text/h323323
+// text/iulsuls
+//{"text/x-idl",
+//{"text/x-netrexx",
+//{"text/x-ocl",
+//{"text/x-dtd",
+// {"text/x-gettext-translation",
+// {"text/x-gettext-translation-template",
+// text/parityfec
+// text/prs.lines.tag
+// text/rfc822-headers
+// text/scriptletsct wsc
+// text/t140
+// text/texmacstm
+// text/turtlettl
+// text/vnd.abc
+// text/vnd.curl
+// text/vnd.debian.copyright
+// text/vnd.DMClientScript
+// text/vnd.flatland.3dml
+// text/vnd.fly
+// text/vnd.fmi.flexstor
+// text/vnd.in3d.3dml
+// text/vnd.in3d.spot
+// text/vnd.IPTC.NewsML
+// text/vnd.IPTC.NITF
+// text/vnd.latex-z
+// text/vnd.motorola.reflex
+// text/vnd.ms-mediapackage
+// text/vnd.sun.j2me.app-descriptorjad
+// text/vnd.wap.si
+// text/vnd.wap.sl
+// text/vnd.wap.wmlwml
+// text/vnd.wap.wmlscriptwmls
+// text/x-booboo
+// text/x-componenthtc
+// text/x-crontab
+// text/x-lilypondly
+// text/x-pcs-gcdgcd
+// text/x-setextetx
+// text/x-sfvsfv
+
+// video/mp4v-es
+// video/parityfec
+// video/pointer
+// video/vnd.fvt
+// video/vnd.motorola.video
+// video/vnd.motorola.videop
+// video/vnd.mts
+// video/vnd.nokia.interleaved-multimedia
+// video/vnd.vivo
+
+// x-conference/x-cooltalkice
+//
+// x-epoc/x-sisx-appsisx

+ 9 - 0
vendor/cogentcore.org/core/base/fileinfo/typegen.go

@@ -0,0 +1,9 @@
+// Code generated by "core generate"; DO NOT EDIT.
+
+package fileinfo
+
+import (
+	"cogentcore.org/core/types"
+)
+
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/base/fileinfo.FileInfo", IDName: "file-info", Doc: "FileInfo represents the information about a given file / directory,\nincluding icon, mimetype, etc", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Methods: []types.Method{{Name: "Duplicate", Doc: "Duplicate creates a copy of given file -- only works for regular files, not\ndirectories.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"string", "error"}}, {Name: "Delete", Doc: "Delete moves the file to the trash / recycling bin.\nOn mobile and web, it deletes it directly.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}, {Name: "Rename", Doc: "Rename renames (moves) this file to given new path name.\nUpdates the FileInfo setting to the new name, although it might\nbe out of scope if it moved into a new path", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"path"}, Returns: []string{"newpath", "err"}}}, Fields: []types.Field{{Name: "Ic", Doc: "icon for file"}, {Name: "Name", Doc: "name of the file, without any path"}, {Name: "Size", Doc: "size of the file"}, {Name: "Kind", Doc: "type of file / directory; shorter, more user-friendly\nversion of mime type, based on category"}, {Name: "Mime", Doc: "full official mime type of the contents"}, {Name: "Cat", Doc: "functional category of the file, based on mime data etc"}, {Name: "Known", Doc: "known file type"}, {Name: "Mode", Doc: "file mode bits"}, {Name: "ModTime", Doc: "time that contents (only) were last modified"}, {Name: "VCS", Doc: "version control system status, when enabled"}, {Name: "Path", Doc: "full path to file, including name; for file functions"}}})

+ 79 - 0
vendor/cogentcore.org/core/base/fsx/fs.go

@@ -0,0 +1,79 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fsx
+
+import (
+	"io/fs"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"cogentcore.org/core/base/errors"
+)
+
+// Sub returns [fs.Sub] with any error automatically logged
+// for cases where the directory is hardcoded and there is
+// no chance of error.
+func Sub(fsys fs.FS, dir string) fs.FS {
+	return errors.Log1(fs.Sub(fsys, dir))
+}
+
+// DirFS returns the directory part of given file path as an os.DirFS
+// and the filename as a string.  These can then be used to access the file
+// using the FS-based interface, consistent with embed and other use-cases.
+func DirFS(fpath string) (fs.FS, string, error) {
+	fabs, err := filepath.Abs(fpath)
+	if err != nil {
+		return nil, "", err
+	}
+	dir, fname := filepath.Split(fabs)
+	dfs := os.DirFS(dir)
+	return dfs, fname, nil
+}
+
+// FileExistsFS checks whether given file exists, returning true if so,
+// false if not, and error if there is an error in accessing the file.
+func FileExistsFS(fsys fs.FS, filePath string) (bool, error) {
+	if fsys, ok := fsys.(fs.StatFS); ok {
+		fileInfo, err := fsys.Stat(filePath)
+		if err == nil {
+			return !fileInfo.IsDir(), nil
+		}
+		if errors.Is(err, fs.ErrNotExist) {
+			return false, nil
+		}
+		return false, err
+	}
+	fp, err := fsys.Open(filePath)
+	if err == nil {
+		fp.Close()
+		return true, nil
+	}
+	if errors.Is(err, fs.ErrNotExist) {
+		return false, nil
+	}
+	return false, err
+}
+
+// SplitRootPathFS returns a split of the given FS path (only / path separators)
+// into the root element and everything after that point.
+// Examples:
+//   - "/a/b/c" returns "/", "a/b/c"
+//   - "a/b/c" returns "a", "b/c" (note removal of intervening "/")
+//   - "a" returns "a", ""
+//   - "a/" returns "a", "" (note removal of trailing "/")
+func SplitRootPathFS(path string) (root, rest string) {
+	pi := strings.IndexByte(path, '/')
+	if pi < 0 {
+		return path, ""
+	}
+	if pi == 0 {
+		return "/", path[1:]
+	}
+	if pi < len(path)-1 {
+		return path[:pi], path[pi+1:]
+	}
+	return path[:pi], ""
+}

+ 206 - 0
vendor/cogentcore.org/core/base/fsx/fsx.go

@@ -0,0 +1,206 @@
+// Copyright (c) 2018, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package fsx provides various utility functions for dealing with filesystems.
+package fsx
+
+import (
+	"errors"
+	"fmt"
+	"go/build"
+	"io/fs"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+	"time"
+)
+
+// GoSrcDir tries to locate dir in GOPATH/src/ or GOROOT/src/pkg/ and returns its
+// full path. GOPATH may contain a list of paths. From Robin Elkind github.com/mewkiz/pkg.
+func GoSrcDir(dir string) (absDir string, err error) {
+	for _, srcDir := range build.Default.SrcDirs() {
+		absDir = filepath.Join(srcDir, dir)
+		finfo, err := os.Stat(absDir)
+		if err == nil && finfo.IsDir() {
+			return absDir, nil
+		}
+	}
+	return "", fmt.Errorf("fsx.GoSrcDir: unable to locate directory (%q) in GOPATH/src/ (%q) or GOROOT/src/pkg/ (%q)", dir, os.Getenv("GOPATH"), os.Getenv("GOROOT"))
+}
+
+// Files returns all the FileInfo's for files with given extension(s) in directory
+// in sorted order (if extensions are empty then all files are returned).
+// In case of error, returns nil.
+func Files(path string, extensions ...string) []fs.DirEntry {
+	files, err := os.ReadDir(path)
+	if err != nil {
+		return nil
+	}
+	if len(extensions) == 0 {
+		return files
+	}
+	sz := len(files)
+	if sz == 0 {
+		return nil
+	}
+	for i := sz - 1; i >= 0; i-- {
+		fn := files[i]
+		ext := filepath.Ext(fn.Name())
+		keep := false
+		for _, ex := range extensions {
+			if strings.EqualFold(ext, ex) {
+				keep = true
+				break
+			}
+		}
+		if !keep {
+			files = append(files[:i], files[i+1:]...)
+		}
+	}
+	return files
+}
+
+// Filenames returns all the file names with given extension(s) in directory
+// in sorted order (if extensions is empty then all files are returned)
+func Filenames(path string, extensions ...string) []string {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil
+	}
+	files, err := f.Readdirnames(-1)
+	f.Close()
+	if err != nil {
+		return nil
+	}
+	if len(extensions) == 0 {
+		sort.StringSlice(files).Sort()
+		return files
+	}
+	sz := len(files)
+	if sz == 0 {
+		return nil
+	}
+	for i := sz - 1; i >= 0; i-- {
+		fn := files[i]
+		ext := filepath.Ext(fn)
+		keep := false
+		for _, ex := range extensions {
+			if strings.EqualFold(ext, ex) {
+				keep = true
+				break
+			}
+		}
+		if !keep {
+			files = append(files[:i], files[i+1:]...)
+		}
+	}
+	sort.StringSlice(files).Sort()
+	return files
+}
+
+// Dirs returns a slice of all the directories within a given directory
+func Dirs(path string) []string {
+	files, err := os.ReadDir(path)
+	if err != nil {
+		return nil
+	}
+
+	var fnms []string
+	for _, fi := range files {
+		if fi.IsDir() {
+			fnms = append(fnms, fi.Name())
+		}
+	}
+	return fnms
+}
+
+// LatestMod returns the latest (most recent) modification time for any of the
+// files in the directory (optionally filtered by extension(s) if exts != nil)
+// if no files or error, returns zero time value
+func LatestMod(path string, exts ...string) time.Time {
+	tm := time.Time{}
+	files := Files(path, exts...)
+	if len(files) == 0 {
+		return tm
+	}
+	for _, de := range files {
+		fi, err := de.Info()
+		if err == nil {
+			if fi.ModTime().After(tm) {
+				tm = fi.ModTime()
+			}
+		}
+	}
+	return tm
+}
+
+// HasFile returns true if given directory has given file (exact match)
+func HasFile(path, file string) bool {
+	files, err := os.ReadDir(path)
+	if err != nil {
+		return false
+	}
+	for _, fn := range files {
+		if fn.Name() == file {
+			return true
+		}
+	}
+	return false
+}
+
+// FindFilesOnPaths attempts to locate given file(s) on given list of paths,
+// returning the full Abs path to each file found (nil if none)
+func FindFilesOnPaths(paths []string, files ...string) []string {
+	var res []string
+	for _, path := range paths {
+		for _, fn := range files {
+			fp := filepath.Join(path, fn)
+			ok, _ := FileExists(fp)
+			if ok {
+				res = append(res, fp)
+			}
+		}
+	}
+	return res
+}
+
+// FileExists checks whether given file exists, returning true if so,
+// false if not, and error if there is an error in accessing the file.
+func FileExists(filePath string) (bool, error) {
+	fileInfo, err := os.Stat(filePath)
+	if err == nil {
+		return !fileInfo.IsDir(), nil
+	}
+	if errors.Is(err, os.ErrNotExist) {
+		return false, nil
+	}
+	return false, err
+}
+
+// DirAndFile returns the final dir and file name.
+func DirAndFile(file string) string {
+	dir, fnm := filepath.Split(file)
+	return filepath.Join(filepath.Base(dir), fnm)
+}
+
+// RelativeFilePath returns the file name relative to given root file path, if it is
+// under that root; otherwise it returns the final dir and file name.
+func RelativeFilePath(file, root string) string {
+	rp, err := filepath.Rel(root, file)
+	if err == nil && !strings.HasPrefix(rp, "..") {
+		return rp
+	}
+	return DirAndFile(file)
+}
+
+// ExtSplit returns the split between the extension and name before
+// the extension, for the given file name.  Any path elements in the
+// file name are preserved; pass [filepath.Base](file) to extract only the
+// last element of the file path if that is what is desired.
+func ExtSplit(file string) (base, ext string) {
+	ext = filepath.Ext(file)
+	base = strings.TrimSuffix(file, ext)
+	return
+}

+ 50 - 0
vendor/cogentcore.org/core/base/indent/enumgen.go

@@ -0,0 +1,50 @@
+// Code generated by "core generate"; DO NOT EDIT.
+
+package indent
+
+import (
+	"cogentcore.org/core/enums"
+)
+
+var _CharacterValues = []Character{0, 1}
+
+// CharacterN is the highest valid value for type Character, plus one.
+const CharacterN Character = 2
+
+var _CharacterValueMap = map[string]Character{`Tab`: 0, `Space`: 1}
+
+var _CharacterDescMap = map[Character]string{0: `Tab indicates to use tabs for indentation.`, 1: `Space indicates to use spaces for indentation.`}
+
+var _CharacterMap = map[Character]string{0: `Tab`, 1: `Space`}
+
+// String returns the string representation of this Character value.
+func (i Character) String() string { return enums.String(i, _CharacterMap) }
+
+// SetString sets the Character value from its string representation,
+// and returns an error if the string is invalid.
+func (i *Character) SetString(s string) error {
+	return enums.SetString(i, s, _CharacterValueMap, "Character")
+}
+
+// Int64 returns the Character value as an int64.
+func (i Character) Int64() int64 { return int64(i) }
+
+// SetInt64 sets the Character value from an int64.
+func (i *Character) SetInt64(in int64) { *i = Character(in) }
+
+// Desc returns the description of the Character value.
+func (i Character) Desc() string { return enums.Desc(i, _CharacterDescMap) }
+
+// CharacterValues returns all possible values for the type Character.
+func CharacterValues() []Character { return _CharacterValues }
+
+// Values returns all possible values for the type Character.
+func (i Character) Values() []enums.Enum { return enums.Values(_CharacterValues) }
+
+// MarshalText implements the [encoding.TextMarshaler] interface.
+func (i Character) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
+
+// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
+func (i *Character) UnmarshalText(text []byte) error {
+	return enums.UnmarshalText(i, text, "Character")
+}

+ 68 - 0
vendor/cogentcore.org/core/base/indent/indent.go

@@ -0,0 +1,68 @@
+// Copyright (c) 2018, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package indent provides indentation generation methods.
+package indent
+
+//go:generate core generate
+
+import (
+	"bytes"
+	"strings"
+)
+
+// Character is the type of indentation character to use.
+type Character int32 //enums:enum
+
+const (
+	// Tab indicates to use tabs for indentation.
+	Tab Character = iota
+
+	// Space indicates to use spaces for indentation.
+	Space
+)
+
+// Tabs returns a string of n tabs.
+func Tabs(n int) string {
+	return strings.Repeat("\t", n)
+}
+
+// TabBytes returns []byte of n tabs.
+func TabBytes(n int) []byte {
+	return bytes.Repeat([]byte("\t"), n)
+}
+
+// Spaces returns a string of n*width spaces.
+func Spaces(n, width int) string {
+	return strings.Repeat(" ", n*width)
+}
+
+// SpaceBytes returns a []byte of n*width spaces.
+func SpaceBytes(n, width int) []byte {
+	return bytes.Repeat([]byte(" "), n*width)
+}
+
+// String returns a string of n tabs or n*width spaces depending on the indent character.
+func String(ich Character, n, width int) string {
+	if ich == Tab {
+		return Tabs(n)
+	}
+	return Spaces(n, width)
+}
+
+// Bytes returns []byte of n tabs or n*width spaces depending on the indent character.
+func Bytes(ich Character, n, width int) []byte {
+	if ich == Tab {
+		return TabBytes(n)
+	}
+	return SpaceBytes(n, width)
+}
+
+// Len returns the length of the indent string given indent character and indent level.
+func Len(ich Character, n, width int) int {
+	if ich == Tab {
+		return n
+	}
+	return n * width
+}

+ 9 - 0
vendor/cogentcore.org/core/base/iox/README.md

@@ -0,0 +1,9 @@
+# iox
+
+Package iox provides boilerplate wrapper functions for the Go standard io functions to Read, Open, Write, and Save, with implementations for commonly used encoding formats.
+
+The top-level `iox` functions define standard `Encoder` and `Decoder` interfaces, and functions to return these.
+
+The specific encoder format implementations provide these `EncoderFunc` and `DecoderFunc` args.
+
+Buffered io is used, and errors are returned.

+ 88 - 0
vendor/cogentcore.org/core/base/iox/decoder.go

@@ -0,0 +1,88 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package iox provides boilerplate wrapper functions for the Go standard
+// io functions to Read, Open, Write, and Save, with implementations for
+// commonly used encoding formats.
+package iox
+
+import (
+	"bufio"
+	"bytes"
+	"io"
+	"io/fs"
+	"os"
+)
+
+// Decoder is an interface for standard decoder types
+type Decoder interface {
+	// Decode decodes from io.Reader specified at creation
+	Decode(v any) error
+}
+
+// DecoderFunc is a function that creates a new Decoder for given reader
+type DecoderFunc func(r io.Reader) Decoder
+
+// NewDecoderFunc returns a DecoderFunc for a specific Decoder type
+func NewDecoderFunc[T Decoder](f func(r io.Reader) T) DecoderFunc {
+	return func(r io.Reader) Decoder { return f(r) }
+}
+
+// Open reads the given object from the given filename using the given [DecoderFunc]
+func Open(v any, filename string, f DecoderFunc) error {
+	fp, err := os.Open(filename)
+	if err != nil {
+		return err
+	}
+	defer fp.Close()
+	return Read(v, bufio.NewReader(fp), f)
+}
+
+// OpenFiles reads the given object from the given filenames using the given [DecoderFunc]
+func OpenFiles(v any, filenames []string, f DecoderFunc) error {
+	for _, file := range filenames {
+		err := Open(v, file, f)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// OpenFS reads the given object from the given filename using the given [DecoderFunc],
+// using the given [fs.FS] filesystem (e.g., for embed files)
+func OpenFS(v any, fsys fs.FS, filename string, f DecoderFunc) error {
+	fp, err := fsys.Open(filename)
+	if err != nil {
+		return err
+	}
+	defer fp.Close()
+	return Read(v, bufio.NewReader(fp), f)
+}
+
+// OpenFilesFS reads the given object from the given filenames using the given [DecoderFunc],
+// using the given [fs.FS] filesystem (e.g., for embed files)
+func OpenFilesFS(v any, fsys fs.FS, filenames []string, f DecoderFunc) error {
+	for _, file := range filenames {
+		err := OpenFS(v, fsys, file, f)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// Read reads the given object from the given reader,
+// using the given [DecoderFunc]
+func Read(v any, reader io.Reader, f DecoderFunc) error {
+	d := f(reader)
+	return d.Decode(v)
+}
+
+// ReadBytes reads the given object from the given bytes,
+// using the given [DecoderFunc]
+func ReadBytes(v any, data []byte, f DecoderFunc) error {
+	b := bytes.NewBuffer(data)
+	return Read(v, b, f)
+}

+ 59 - 0
vendor/cogentcore.org/core/base/iox/encoder.go

@@ -0,0 +1,59 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package iox
+
+import (
+	"bufio"
+	"bytes"
+	"io"
+	"os"
+)
+
+// Encoder is an interface for standard encoder types
+type Encoder interface {
+	// Encode encodes to io.Writer specified at creation
+	Encode(v any) error
+}
+
+// EncoderFunc is a function that creates a new Encoder for given writer
+type EncoderFunc func(w io.Writer) Encoder
+
+// NewEncoderFunc returns a EncoderFunc for a specific Encoder type
+func NewEncoderFunc[T Encoder](f func(w io.Writer) T) EncoderFunc {
+	return func(w io.Writer) Encoder { return f(w) }
+}
+
+// Save writes the given object to the given filename using the given [EncoderFunc]
+func Save(v any, filename string, f EncoderFunc) error {
+	fp, err := os.Create(filename)
+	if err != nil {
+		return err
+	}
+	defer fp.Close()
+	bw := bufio.NewWriter(fp)
+	err = Write(v, bw, f)
+	if err != nil {
+		return err
+	}
+	return bw.Flush()
+}
+
+// Write writes the given object using the given [EncoderFunc]
+func Write(v any, writer io.Writer, f EncoderFunc) error {
+	e := f(writer)
+	return e.Encode(v)
+}
+
+// WriteBytes writes the given object, returning bytes of the encoding,
+// using the given [EncoderFunc]
+func WriteBytes(v any, f EncoderFunc) ([]byte, error) {
+	var b bytes.Buffer
+	e := f(&b)
+	err := e.Encode(v)
+	if err != nil {
+		return nil, err
+	}
+	return b.Bytes(), nil
+}

+ 101 - 0
vendor/cogentcore.org/core/base/iox/imagex/base64.go

@@ -0,0 +1,101 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package imagex
+
+import (
+	"bytes"
+	"encoding/base64"
+	"errors"
+	"image"
+	"image/jpeg"
+	"image/png"
+	"log"
+	"strings"
+)
+
+// ToBase64PNG returns bytes of image encoded as a PNG in Base64 format
+// with "image/png" mimetype returned
+func ToBase64PNG(img image.Image) ([]byte, string) {
+	ibuf := &bytes.Buffer{}
+	png.Encode(ibuf, img)
+	ib := ibuf.Bytes()
+	eb := make([]byte, base64.StdEncoding.EncodedLen(len(ib)))
+	base64.StdEncoding.Encode(eb, ib)
+	return eb, "image/png"
+}
+
+// ToBase64JPG returns bytes image encoded as a JPG in Base64 format
+// with "image/jpeg" mimetype returned
+func ToBase64JPG(img image.Image) ([]byte, string) {
+	ibuf := &bytes.Buffer{}
+	jpeg.Encode(ibuf, img, &jpeg.Options{Quality: 90})
+	ib := ibuf.Bytes()
+	eb := make([]byte, base64.StdEncoding.EncodedLen(len(ib)))
+	base64.StdEncoding.Encode(eb, ib)
+	return eb, "image/jpeg"
+}
+
+// Base64SplitLines splits the encoded Base64 bytes into standard lines of 76
+// chars each.  The last line also ends in a newline
+func Base64SplitLines(b []byte) []byte {
+	ll := 76
+	sz := len(b)
+	nl := (sz / ll)
+	rb := make([]byte, sz+nl+1)
+	for i := 0; i < nl; i++ {
+		st := ll * i
+		rst := ll*i + i
+		copy(rb[rst:rst+ll], b[st:st+ll])
+		rb[rst+ll] = '\n'
+	}
+	st := ll * nl
+	rst := ll*nl + nl
+	ln := sz - st
+	copy(rb[rst:rst+ln], b[st:st+ln])
+	rb[rst+ln] = '\n'
+	return rb
+}
+
+// FromBase64PNG returns image from Base64-encoded bytes in PNG format
+func FromBase64PNG(eb []byte) (image.Image, error) {
+	if eb[76] == ' ' {
+		eb = bytes.ReplaceAll(eb, []byte(" "), []byte("\n"))
+	}
+	db := make([]byte, base64.StdEncoding.DecodedLen(len(eb)))
+	_, err := base64.StdEncoding.Decode(db, eb)
+	if err != nil {
+		log.Println(err)
+		return nil, err
+	}
+	rb := bytes.NewReader(db)
+	return png.Decode(rb)
+}
+
+// FromBase64JPG returns image from Base64-encoded bytes in PNG format
+func FromBase64JPG(eb []byte) (image.Image, error) {
+	if eb[76] == ' ' {
+		eb = bytes.ReplaceAll(eb, []byte(" "), []byte("\n"))
+	}
+	db := make([]byte, base64.StdEncoding.DecodedLen(len(eb)))
+	_, err := base64.StdEncoding.Decode(db, eb)
+	if err != nil {
+		log.Println(err)
+		return nil, err
+	}
+	rb := bytes.NewReader(db)
+	return jpeg.Decode(rb)
+}
+
+// FromBase64 returns image from Base64-encoded bytes in either PNG or JPEG format
+// based on fmt which must end in either png, jpg, or jpeg
+func FromBase64(fmt string, eb []byte) (image.Image, error) {
+	if strings.HasSuffix(fmt, "png") {
+		return FromBase64PNG(eb)
+	}
+	if strings.HasSuffix(fmt, "jpg") || strings.HasSuffix(fmt, "jpeg") {
+		return FromBase64JPG(eb)
+	}
+	return nil, errors.New("image format must be either png or jpeg")
+}

+ 48 - 0
vendor/cogentcore.org/core/base/iox/imagex/enumgen.go

@@ -0,0 +1,48 @@
+// Code generated by "core generate"; DO NOT EDIT.
+
+package imagex
+
+import (
+	"cogentcore.org/core/enums"
+)
+
+var _FormatsValues = []Formats{0, 1, 2, 3, 4, 5, 6}
+
+// FormatsN is the highest valid value for type Formats, plus one.
+const FormatsN Formats = 7
+
+var _FormatsValueMap = map[string]Formats{`None`: 0, `PNG`: 1, `JPEG`: 2, `GIF`: 3, `TIFF`: 4, `BMP`: 5, `WebP`: 6}
+
+var _FormatsDescMap = map[Formats]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``, 5: ``, 6: ``}
+
+var _FormatsMap = map[Formats]string{0: `None`, 1: `PNG`, 2: `JPEG`, 3: `GIF`, 4: `TIFF`, 5: `BMP`, 6: `WebP`}
+
+// String returns the string representation of this Formats value.
+func (i Formats) String() string { return enums.String(i, _FormatsMap) }
+
+// SetString sets the Formats value from its string representation,
+// and returns an error if the string is invalid.
+func (i *Formats) SetString(s string) error {
+	return enums.SetString(i, s, _FormatsValueMap, "Formats")
+}
+
+// Int64 returns the Formats value as an int64.
+func (i Formats) Int64() int64 { return int64(i) }
+
+// SetInt64 sets the Formats value from an int64.
+func (i *Formats) SetInt64(in int64) { *i = Formats(in) }
+
+// Desc returns the description of the Formats value.
+func (i Formats) Desc() string { return enums.Desc(i, _FormatsDescMap) }
+
+// FormatsValues returns all possible values for the type Formats.
+func FormatsValues() []Formats { return _FormatsValues }
+
+// Values returns all possible values for the type Formats.
+func (i Formats) Values() []enums.Enum { return enums.Values(_FormatsValues) }
+
+// MarshalText implements the [encoding.TextMarshaler] interface.
+func (i Formats) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
+
+// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
+func (i *Formats) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Formats") }

+ 153 - 0
vendor/cogentcore.org/core/base/iox/imagex/imagex.go

@@ -0,0 +1,153 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package imagex
+
+//go:generate core generate
+
+import (
+	"bufio"
+	"fmt"
+	"image"
+	"image/draw"
+	"image/gif"
+	"image/jpeg"
+	"image/png"
+	"io"
+	"io/fs"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"golang.org/x/image/bmp"
+	"golang.org/x/image/tiff"
+	_ "golang.org/x/image/webp"
+)
+
+// Formats are the supported image encoding / decoding formats
+type Formats int32 //enums:enum
+
+// The supported image encoding formats
+const (
+	None Formats = iota
+	PNG
+	JPEG
+	GIF
+	TIFF
+	BMP
+	WebP
+)
+
+// ExtToFormat returns a Format based on a filename extension,
+// which can start with a . or not
+func ExtToFormat(ext string) (Formats, error) {
+	if len(ext) == 0 {
+		return None, fmt.Errorf("ExtToFormat: ext is empty")
+	}
+	if ext[0] == '.' {
+		ext = ext[1:]
+	}
+	ext = strings.ToLower(ext)
+	switch ext {
+	case "png":
+		return PNG, nil
+	case "jpg", "jpeg":
+		return JPEG, nil
+	case "gif":
+		return GIF, nil
+	case "tif", "tiff":
+		return TIFF, nil
+	case "bmp":
+		return BMP, nil
+	case "webp":
+		return WebP, nil
+	}
+	return None, fmt.Errorf("ExtToFormat: extension %q not recognized", ext)
+}
+
+// Open opens an image from the given filename.
+// The format is inferred automatically,
+// and is returned using the Formats enum.
+// png, jpeg, gif, tiff, bmp, and webp are supported.
+func Open(filename string) (image.Image, Formats, error) {
+	file, err := os.Open(filename)
+	if err != nil {
+		return nil, None, err
+	}
+	defer file.Close()
+	return Read(file)
+}
+
+// OpenFS opens an image from the given filename
+// using the given [fs.FS] filesystem (e.g., for embed files).
+// The format is inferred automatically,
+// and is returned using the Formats enum.
+// png, jpeg, gif, tiff, bmp, and webp are supported.
+func OpenFS(fsys fs.FS, filename string) (image.Image, Formats, error) {
+	file, err := fsys.Open(filename)
+	if err != nil {
+		return nil, None, err
+	}
+	defer file.Close()
+	return Read(file)
+}
+
+// Read reads an image to the given reader,
+// The format is inferred automatically,
+// and is returned using the Formats enum.
+// png, jpeg, gif, tiff, bmp, and webp are supported.
+func Read(r io.Reader) (image.Image, Formats, error) {
+	im, ext, err := image.Decode(r)
+	if err != nil {
+		return im, None, err
+	}
+	f, err := ExtToFormat(ext)
+	return im, f, err
+}
+
+// Save saves the image to the given filename,
+// with the format inferred from the filename.
+// png, jpeg, gif, tiff, and bmp are supported.
+func Save(im image.Image, filename string) error {
+	ext := filepath.Ext(filename)
+	f, err := ExtToFormat(ext)
+	if err != nil {
+		return err
+	}
+	file, err := os.Create(filename)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	bw := bufio.NewWriter(file)
+	defer bw.Flush()
+	return Write(im, file, f)
+}
+
+// Write writes the image to the given writer using the given foramt.
+// png, jpeg, gif, tiff, and bmp are supported.
+func Write(im image.Image, w io.Writer, f Formats) error {
+	switch f {
+	case PNG:
+		return png.Encode(w, im)
+	case JPEG:
+		return jpeg.Encode(w, im, &jpeg.Options{Quality: 90})
+	case GIF:
+		return gif.Encode(w, im, nil)
+	case TIFF:
+		return tiff.Encode(w, im, nil)
+	case BMP:
+		return bmp.Encode(w, im)
+	default:
+		return fmt.Errorf("iox/imagex.Save: format %q not valid", f)
+	}
+}
+
+// CloneAsRGBA returns an RGBA copy of the supplied image.
+func CloneAsRGBA(src image.Image) *image.RGBA {
+	bounds := src.Bounds()
+	img := image.NewRGBA(bounds)
+	draw.Draw(img, bounds, src, bounds.Min, draw.Src)
+	return img
+}

+ 142 - 0
vendor/cogentcore.org/core/base/iox/imagex/testing.go

@@ -0,0 +1,142 @@
+// Copyright 2023 Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package imagex
+
+import (
+	"errors"
+	"image"
+	"image/color"
+	"io/fs"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+// TestingT is an interface wrapper around *testing.T
+type TestingT interface {
+	Errorf(format string, args ...any)
+}
+
+// UpdateTestImages indicates whether to update currently saved test
+// images in [AssertImage] instead of comparing against them.
+// It is automatically set if the build tag "update" is specified,
+// and it should typically only be set through that. It should only be
+// set when behavior has been updated that causes test images to change,
+// and it should only be set once and then turned back off.
+var UpdateTestImages = updateTestImages
+
+// CompareUint8 returns true if two numbers are more different than tol
+func CompareUint8(cc, ic uint8, tol int) bool {
+	d := int(cc) - int(ic)
+	if d < -tol {
+		return false
+	}
+	if d > tol {
+		return false
+	}
+	return true
+}
+
+// CompareColors returns true if two colors are more different than tol
+func CompareColors(cc, ic color.RGBA, tol int) bool {
+	if !CompareUint8(cc.R, ic.R, tol) {
+		return false
+	}
+	if !CompareUint8(cc.G, ic.G, tol) {
+		return false
+	}
+	if !CompareUint8(cc.B, ic.B, tol) {
+		return false
+	}
+	if !CompareUint8(cc.A, ic.A, tol) {
+		return false
+	}
+	return true
+}
+
+// Assert asserts that the given image is equivalent
+// to the image stored at the given filename in the testdata directory,
+// with ".png" added to the filename if there is no extension
+// (eg: "button" becomes "testdata/button.png"). Forward slashes are
+// automatically replaced with backslashes on Windows.
+// If it is not, it fails the test with an error, but continues its
+// execution. If there is no image at the given filename in the testdata
+// directory, it creates the image.
+func Assert(t TestingT, img image.Image, filename string) {
+	filename = filepath.Join("testdata", filename)
+	if filepath.Ext(filename) == "" {
+		filename += ".png"
+	}
+
+	err := os.MkdirAll(filepath.Dir(filename), 0750)
+	if err != nil {
+		t.Errorf("error making testdata directory: %v", err)
+	}
+
+	ext := filepath.Ext(filename)
+	failFilename := strings.TrimSuffix(filename, ext) + ".fail" + ext
+
+	if UpdateTestImages {
+		err := Save(img, filename)
+		if err != nil {
+			t.Errorf("AssertImage: error saving updated image: %v", err)
+		}
+		err = os.RemoveAll(failFilename)
+		if err != nil {
+			t.Errorf("AssertImage: error removing old fail image: %v", err)
+		}
+		return
+	}
+
+	fimg, _, err := Open(filename)
+	if err != nil {
+		if !errors.Is(err, fs.ErrNotExist) {
+			t.Errorf("AssertImage: error opening saved image: %v", err)
+			return
+		}
+		// we don't have the file yet, so we make it
+		err := Save(img, filename)
+		if err != nil {
+			t.Errorf("AssertImage: error saving new image: %v", err)
+		}
+		return
+	}
+
+	failed := false
+
+	ibounds := img.Bounds()
+	fbounds := fimg.Bounds()
+	if ibounds != fbounds {
+		t.Errorf("AssertImage: expected bounds %v for image for %s, but got bounds %v; see %s", fbounds, filename, ibounds, failFilename)
+		failed = true
+	} else {
+		for y := ibounds.Min.Y; y < ibounds.Max.Y; y++ {
+			for x := ibounds.Min.X; x < ibounds.Max.X; x++ {
+				cc := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA)
+				ic := color.RGBAModel.Convert(fimg.At(x, y)).(color.RGBA)
+				if !CompareColors(cc, ic, 1) {
+					t.Errorf("AssertImage: image for %s is not the same as expected; see %s; expected color %v at (%d, %d), but got %v", filename, failFilename, ic, x, y, cc)
+					failed = true
+					break
+				}
+			}
+			if failed {
+				break
+			}
+		}
+	}
+
+	if failed {
+		err := Save(img, failFilename)
+		if err != nil {
+			t.Errorf("AssertImage: error saving fail image: %v", err)
+		}
+	} else {
+		err := os.RemoveAll(failFilename)
+		if err != nil {
+			t.Errorf("AssertImage: error removing old fail image: %v", err)
+		}
+	}
+}

+ 9 - 0
vendor/cogentcore.org/core/base/iox/imagex/testing_noupdate.go

@@ -0,0 +1,9 @@
+// Copyright 2023 Cogent Core. 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 !update
+
+package imagex
+
+var updateTestImages = false

+ 9 - 0
vendor/cogentcore.org/core/base/iox/imagex/testing_update.go

@@ -0,0 +1,9 @@
+// Copyright 2023 Cogent Core. 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 update
+
+package imagex
+
+var updateTestImages = true

+ 86 - 0
vendor/cogentcore.org/core/base/iox/jsonx/jsonx.go

@@ -0,0 +1,86 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package jsonx
+
+import (
+	"encoding/json"
+	"io"
+	"io/fs"
+
+	"cogentcore.org/core/base/iox"
+)
+
+// Open reads the given object from the given filename using JSON encoding
+func Open(v any, filename string) error {
+	return iox.Open(v, filename, iox.NewDecoderFunc(json.NewDecoder))
+}
+
+// OpenFiles reads the given object from the given filenames using JSON encoding
+func OpenFiles(v any, filenames ...string) error {
+	return iox.OpenFiles(v, filenames, iox.NewDecoderFunc(json.NewDecoder))
+}
+
+// OpenFS reads the given object from the given filename using JSON encoding,
+// using the given [fs.FS] filesystem (e.g., for embed files)
+func OpenFS(v any, fsys fs.FS, filename string) error {
+	return iox.OpenFS(v, fsys, filename, iox.NewDecoderFunc(json.NewDecoder))
+}
+
+// OpenFilesFS reads the given object from the given filenames using JSON encoding,
+// using the given [fs.FS] filesystem (e.g., for embed files)
+func OpenFilesFS(v any, fsys fs.FS, filenames ...string) error {
+	return iox.OpenFilesFS(v, fsys, filenames, iox.NewDecoderFunc(json.NewDecoder))
+}
+
+// Read reads the given object from the given reader,
+// using JSON encoding
+func Read(v any, reader io.Reader) error {
+	return iox.Read(v, reader, iox.NewDecoderFunc(json.NewDecoder))
+}
+
+// ReadBytes reads the given object from the given bytes,
+// using JSON encoding
+func ReadBytes(v any, data []byte) error {
+	return iox.ReadBytes(v, data, iox.NewDecoderFunc(json.NewDecoder))
+}
+
+// Save writes the given object to the given filename using JSON encoding
+func Save(v any, filename string) error {
+	return iox.Save(v, filename, iox.NewEncoderFunc(json.NewEncoder))
+}
+
+// Write writes the given object using JSON encoding
+func Write(v any, writer io.Writer) error {
+	return iox.Write(v, writer, iox.NewEncoderFunc(json.NewEncoder))
+}
+
+// WriteBytes writes the given object, returning bytes of the encoding,
+// using JSON encoding
+func WriteBytes(v any) ([]byte, error) {
+	return iox.WriteBytes(v, iox.NewEncoderFunc(json.NewEncoder))
+}
+
+// IndentEncoderFunc is a [iox.EncoderFunc] that sets indentation
+var IndentEncoderFunc = func(w io.Writer) iox.Encoder {
+	e := json.NewEncoder(w)
+	e.SetIndent("", "\t")
+	return e
+}
+
+// SaveIndent writes the given object to the given filename using JSON encoding, with indentation
+func SaveIndent(v any, filename string) error {
+	return iox.Save(v, filename, IndentEncoderFunc)
+}
+
+// WriteIndent writes the given object using JSON encoding, with indentation
+func WriteIndent(v any, writer io.Writer) error {
+	return iox.Write(v, writer, IndentEncoderFunc)
+}
+
+// WriteBytesIndent writes the given object, returning bytes of the encoding,
+// using JSON encoding, with indentation
+func WriteBytesIndent(v any) ([]byte, error) {
+	return iox.WriteBytes(v, IndentEncoderFunc)
+}

+ 83 - 0
vendor/cogentcore.org/core/base/iox/tomlx/tomlx.go

@@ -0,0 +1,83 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tomlx
+
+import (
+	"fmt"
+	"io"
+	"io/fs"
+
+	"cogentcore.org/core/base/fsx"
+	"cogentcore.org/core/base/iox"
+	"github.com/pelletier/go-toml/v2"
+)
+
+// NewDecoder returns a new [iox.Decoder]
+func NewDecoder(r io.Reader) iox.Decoder { return toml.NewDecoder(r) }
+
+// Open reads the given object from the given filename using TOML encoding
+func Open(v any, filename string) error {
+	return iox.Open(v, filename, NewDecoder)
+}
+
+// OpenFiles reads the given object from the given filenames using TOML encoding
+func OpenFiles(v any, filenames ...string) error {
+	return iox.OpenFiles(v, filenames, NewDecoder)
+}
+
+// OpenFS reads the given object from the given filename using TOML encoding,
+// using the given [fs.FS] filesystem (e.g., for embed files)
+func OpenFS(v any, fsys fs.FS, filename string) error {
+	return iox.OpenFS(v, fsys, filename, NewDecoder)
+}
+
+// OpenFilesFS reads the given object from the given filenames using TOML encoding,
+// using the given [fs.FS] filesystem (e.g., for embed files)
+func OpenFilesFS(v any, fsys fs.FS, filenames ...string) error {
+	return iox.OpenFilesFS(v, fsys, filenames, NewDecoder)
+}
+
+// Read reads the given object from the given reader,
+// using TOML encoding
+func Read(v any, reader io.Reader) error {
+	return iox.Read(v, reader, NewDecoder)
+}
+
+// ReadBytes reads the given object from the given bytes,
+// using TOML encoding
+func ReadBytes(v any, data []byte) error {
+	return iox.ReadBytes(v, data, NewDecoder)
+}
+
+// NewEncoder returns a new [iox.Encoder]
+func NewEncoder(w io.Writer) iox.Encoder {
+	return toml.NewEncoder(w).SetIndentTables(true).SetArraysMultiline(true)
+}
+
+// Save writes the given object to the given filename using TOML encoding
+func Save(v any, filename string) error {
+	return iox.Save(v, filename, NewEncoder)
+}
+
+// Write writes the given object using TOML encoding
+func Write(v any, writer io.Writer) error {
+	return iox.Write(v, writer, NewEncoder)
+}
+
+// WriteBytes writes the given object, returning bytes of the encoding,
+// using TOML encoding
+func WriteBytes(v any) ([]byte, error) {
+	return iox.WriteBytes(v, NewEncoder)
+}
+
+// OpenFromPaths reads the given object from the given TOML file,
+// looking on paths for the file.
+func OpenFromPaths(v any, file string, paths ...string) error {
+	filenames := fsx.FindFilesOnPaths(paths, file)
+	if len(filenames) == 0 {
+		return fmt.Errorf("OpenFromPaths: no files found")
+	}
+	return Open(v, filenames[0])
+}

+ 122 - 0
vendor/cogentcore.org/core/base/labels/friendly.go

@@ -0,0 +1,122 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package labels
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+
+	"cogentcore.org/core/base/reflectx"
+	"cogentcore.org/core/base/strcase"
+)
+
+// FriendlyTypeName returns a user-friendly version of the name of the given type.
+// It transforms it into sentence case, excludes the package, and converts various
+// builtin types into more friendly forms (eg: "int" to "Number").
+func FriendlyTypeName(typ reflect.Type) string {
+	nptyp := reflectx.NonPointerType(typ)
+	if nptyp == nil {
+		return "None"
+	}
+	nm := nptyp.Name()
+
+	// if it is named, we use that
+	if nm != "" {
+		switch nm {
+		case "string":
+			return "Text"
+		case "float32", "float64", "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr":
+			return "Number"
+		}
+		return strcase.ToSentence(nm)
+	}
+
+	// otherwise, we fall back on Kind
+	switch nptyp.Kind() {
+	case reflect.Slice, reflect.Array, reflect.Map:
+		bnm := FriendlyTypeName(nptyp.Elem())
+		if strings.HasSuffix(bnm, "s") {
+			return "List of " + bnm
+		} else if strings.Contains(bnm, "Function of") {
+			return strings.ReplaceAll(bnm, "Function of", "Functions of") + "s"
+		} else {
+			return bnm + "s"
+		}
+	case reflect.Func:
+		str := "Function"
+		ni := nptyp.NumIn()
+		if ni > 0 {
+			str += " of"
+		}
+		for i := 0; i < ni; i++ {
+			str += " " + FriendlyTypeName(nptyp.In(i))
+			if ni == 2 && i == 0 {
+				str += " and"
+			} else if i == ni-2 {
+				str += ", and"
+			} else if i < ni-1 {
+				str += ","
+			}
+		}
+		return str
+	}
+	if nptyp.String() == "interface {}" {
+		return "Value"
+	}
+	return nptyp.String()
+}
+
+// FriendlyStructLabel returns a user-friendly label for the given struct value.
+func FriendlyStructLabel(v reflect.Value) string {
+	npv := reflectx.NonPointerValue(v)
+	if !v.IsValid() || v.IsZero() {
+		return "None"
+	}
+	opv := reflectx.UnderlyingPointer(v)
+	if lbler, ok := opv.Interface().(Labeler); ok {
+		return lbler.Label()
+	}
+	return FriendlyTypeName(npv.Type())
+}
+
+// FriendlySliceLabel returns a user-friendly label for the given slice value.
+func FriendlySliceLabel(v reflect.Value) string {
+	uv := reflectx.Underlying(v)
+	label := ""
+	if !uv.IsValid() {
+		label = "None"
+	} else {
+		if uv.Kind() == reflect.Array || !uv.IsNil() {
+			bnm := FriendlyTypeName(reflectx.SliceElementType(v.Interface()))
+			if strings.HasSuffix(bnm, "s") {
+				label = strcase.ToSentence(fmt.Sprintf("%d lists of %s", uv.Len(), bnm))
+			} else {
+				label = strcase.ToSentence(fmt.Sprintf("%d %ss", uv.Len(), bnm))
+			}
+		} else {
+			label = "None"
+		}
+	}
+	return label
+}
+
+// FriendlyMapLabel returns a user-friendly label for the given map value.
+func FriendlyMapLabel(v reflect.Value) string {
+	uv := reflectx.Underlying(v)
+	mpi := v.Interface()
+	label := ""
+	if !uv.IsValid() || uv.IsNil() {
+		label = "None"
+	} else {
+		bnm := FriendlyTypeName(reflectx.MapValueType(mpi))
+		if strings.HasSuffix(bnm, "s") {
+			label = strcase.ToSentence(fmt.Sprintf("%d lists of %s", uv.Len(), bnm))
+		} else {
+			label = strcase.ToSentence(fmt.Sprintf("%d %ss", uv.Len(), bnm))
+		}
+	}
+	return label
+}

+ 43 - 0
vendor/cogentcore.org/core/base/labels/labeler.go

@@ -0,0 +1,43 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package labels
+
+import (
+	"cogentcore.org/core/base/reflectx"
+)
+
+// Labeler interface provides a GUI-appropriate label for an item,
+// via a Label string method. See [ToLabel] and [ToLabeler].
+type Labeler interface {
+
+	// Label returns a GUI-appropriate label for item
+	Label() string
+}
+
+// ToLabel returns the GUI-appropriate label for an item, using the Labeler
+// interface if it is defined, and falling back on [reflectx.ToString] converter
+// otherwise.
+func ToLabel(v any) string {
+	if lb, ok := v.(Labeler); ok {
+		return lb.Label()
+	}
+	return reflectx.ToString(v)
+}
+
+// ToLabeler returns the Labeler label, true if it was defined, else "", false
+func ToLabeler(v any) (string, bool) {
+	if lb, ok := v.(Labeler); ok {
+		return lb.Label(), true
+	}
+	return "", false
+}
+
+// SliceLabeler interface provides a GUI-appropriate label
+// for a slice item, given an index into the slice.
+type SliceLabeler interface {
+
+	// ElemLabel returns a GUI-appropriate label for slice element at given index.
+	ElemLabel(idx int) string
+}

+ 46 - 0
vendor/cogentcore.org/core/base/nptime/nptime.go

@@ -0,0 +1,46 @@
+// Copyright (c) 2018, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+Package nptime provides a non-pointer version of the time.Time struct
+that does not have the location pointer information that time.Time has,
+which is more efficient from a memory management perspective, in cases
+where you have a lot of time values being kept: https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/
+*/
+package nptime
+
+import "time"
+
+// Time represents the value of time.Time without using any pointers for the
+// location information, so it is more memory efficient when lots of time
+// values are being stored.
+type Time struct {
+
+	// [time.Time.Unix] seconds since 1970
+	Sec int64
+
+	// [time.Time.Nanosecond]; nanosecond offset within second, *not* UnixNano
+	NSec uint32
+}
+
+// IsZero returns true if the time is zero and has not been initialized.
+func (t Time) IsZero() bool {
+	return t == Time{}
+}
+
+// Time returns the [time.Time] value for this [Time] value.
+func (t Time) Time() time.Time {
+	return time.Unix(t.Sec, int64(t.NSec))
+}
+
+// SetTime sets the [Time] value based on the [time.Time]. value
+func (t *Time) SetTime(tt time.Time) {
+	t.Sec = tt.Unix()
+	t.NSec = uint32(tt.Nanosecond())
+}
+
+// Now sets the time value to [time.Now].
+func (t *Time) Now() {
+	t.SetTime(time.Now())
+}

+ 13 - 0
vendor/cogentcore.org/core/base/num/abs.go

@@ -0,0 +1,13 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package num
+
+// Abs returns the absolute value of the given value.
+func Abs[T Signed | Float](x T) T {
+	if x < 0 {
+		return -x
+	}
+	return x
+}

+ 36 - 0
vendor/cogentcore.org/core/base/num/bool.go

@@ -0,0 +1,36 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package num
+
+// see: https://github.com/golang/go/issues/61915
+
+// ToBool returns a bool true if the given number is not zero,
+// and false if it is zero, providing a direct way to convert
+// numbers to bools as is done automatically in C and other languages.
+func ToBool[T Number](v T) bool {
+	return v != 0
+}
+
+// FromBool returns a 1 if the bool is true and a 0 for false.
+// Typically the type parameter cannot be inferred and must be provided.
+// See SetFromBool for a version that uses a pointer to the destination
+// which avoids the need to specify the type parameter.
+func FromBool[T Number](v bool) T {
+	if v {
+		return 1
+	}
+	return 0
+}
+
+// SetFromBool converts a bool into a number, using generics,
+// setting the pointer to the dst destination value to a 1 if bool is true,
+// and 0 otherwise.
+// This version of FromBool does not require type parameters typically.
+func SetFromBool[T Number](dst *T, b bool) {
+	if b {
+		*dst = 1
+	}
+	*dst = 0
+}

+ 54 - 0
vendor/cogentcore.org/core/base/num/constraints.go

@@ -0,0 +1,54 @@
+// Copyright 2021 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.
+
+package num
+
+// Signed is a constraint that permits any signed integer type.
+// If future releases of Go add new predeclared signed integer types,
+// this constraint will be modified to include them.
+type Signed interface {
+	~int | ~int8 | ~int16 | ~int32 | ~int64
+}
+
+// Unsigned is a constraint that permits any unsigned integer type.
+// If future releases of Go add new predeclared unsigned integer types,
+// this constraint will be modified to include them.
+type Unsigned interface {
+	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
+}
+
+// Integer is a constraint that permits any integer type.
+// If future releases of Go add new predeclared integer types,
+// this constraint will be modified to include them.
+type Integer interface {
+	Signed | Unsigned
+}
+
+// Float is a constraint that permits any floating-point type.
+// If future releases of Go add new predeclared floating-point types,
+// this constraint will be modified to include them.
+type Float interface {
+	~float32 | ~float64
+}
+
+// Complex is a constraint that permits any complex numeric type.
+// If future releases of Go add new predeclared complex numeric types,
+// this constraint will be modified to include them.
+type Complex interface {
+	~complex64 | ~complex128
+}
+
+// Ordered is a constraint that permits any ordered type: any type
+// that supports the operators < <= >= >.
+// If future releases of Go add new ordered types,
+// this constraint will be modified to include them.
+type Ordered interface {
+	Integer | Float | ~string
+}
+
+// Number is a constraint that permits any single-valued numerical type,
+// i.e., Integer or Float.  It excludes complex numbers.
+type Number interface {
+	Integer | Float
+}

+ 54 - 0
vendor/cogentcore.org/core/base/option/option.go

@@ -0,0 +1,54 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package option provides optional (nullable) types.
+package option
+
+// Option represents an optional (nullable) type. If Valid is true, Option
+// represents Value. Otherwise, it represents a null/unset/invalid value.
+type Option[T any] struct {
+	Valid bool `label:"Set"`
+	Value T
+}
+
+// New returns a new [Option] set to the given value.
+func New[T any](v T) *Option[T] {
+	o := &Option[T]{}
+	o.Set(v)
+	return o
+}
+
+// Set sets the value to the given value.
+func (o *Option[T]) Set(v T) *Option[T] {
+	o.Value = v
+	o.Valid = true
+	return o
+}
+
+// Clear marks the value as null/unset/invalid.
+func (o *Option[T]) Clear() *Option[T] {
+	o.Valid = false
+	return o
+}
+
+// Or returns the value of the option if it is not marked
+// as null/unset/invalid, and otherwise it returns the given value.
+func (o *Option[T]) Or(or T) T {
+	if o.Valid {
+		return o.Value
+	}
+	return or
+}
+
+func (o *Option[T]) ShouldSave() bool {
+	return o.Valid
+}
+
+func (o *Option[T]) ShouldDisplay(field string) bool {
+	switch field {
+	case "Value":
+		return o.Valid
+	}
+	return true
+}

+ 9 - 0
vendor/cogentcore.org/core/base/ordmap/README.md

@@ -0,0 +1,9 @@
+# ordmap: ordered map using Go generics
+
+Package `ordmap` implements an ordered map that retains the order of items added to a slice, while also providing fast key-based map lookup of items, using the Go 1.18 generics system.
+
+The implementation is fully visible and the API provides a minimal subset of methods, compared to other implementations that are heavier, so that additional functionality can be added as needed.  Iteration can be performed directly on the `Order` using standard Go `range` function.
+
+The slice structure holds the Key and Value for items as they are added, enabling direct updating of the corresponding map, which holds the index into the slice.
+
+Adding and access are fast, while deleting and inserting are relatively slow, requiring updating of the index map, but these are already slow due to the slice updating.

+ 256 - 0
vendor/cogentcore.org/core/base/ordmap/ordmap.go

@@ -0,0 +1,256 @@
+// Copyright (c) 2022, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+package ordmap implements an ordered map that retains the order of items
+added to a slice, while also providing fast key-based map lookup of items,
+using the Go 1.18 generics system.
+
+The implementation is fully visible and the API provides a minimal
+subset of methods, compared to other implementations that are heavier,
+so that additional functionality can be added as needed.
+
+The slice structure holds the Key and Value for items as they are added,
+enabling direct updating of the corresponding map, which holds the
+index into the slice.  Adding and access are fast, while deleting
+and inserting are relatively slow, requiring updating of the index map,
+but these are already slow due to the slice updating.
+*/
+package ordmap
+
+import (
+	"fmt"
+
+	"slices"
+)
+
+// KeyValue represents a key-value pair.
+type KeyValue[K comparable, V any] struct {
+	Key   K
+	Value V
+}
+
+// Map is a generic ordered map that combines the order of a slice
+// and the fast key lookup of a map. A map stores an index
+// into a slice that has the value and key associated with the value.
+type Map[K comparable, V any] struct {
+
+	// Order is an ordered list of values and associated keys, in the order added.
+	Order []KeyValue[K, V]
+
+	// Map is the key to index mapping.
+	Map map[K]int `display:"-"`
+}
+
+// New returns a new ordered map.
+func New[K comparable, V any]() *Map[K, V] {
+	return &Map[K, V]{
+		Map: make(map[K]int),
+	}
+}
+
+// Make constructs a new ordered map with the given key-value pairs
+func Make[K comparable, V any](vals []KeyValue[K, V]) *Map[K, V] {
+	om := &Map[K, V]{
+		Order: vals,
+		Map:   make(map[K]int, len(vals)),
+	}
+	for i, v := range om.Order {
+		om.Map[v.Key] = i
+	}
+	return om
+}
+
+// Init initializes the map if it isn't already.
+func (om *Map[K, V]) Init() {
+	if om.Map == nil {
+		om.Map = make(map[K]int)
+	}
+}
+
+// Reset resets the map, removing any existing elements.
+func (om *Map[K, V]) Reset() {
+	om.Map = nil
+	om.Order = nil
+}
+
+// Add adds a new value for given key.
+// If key already exists in map, it replaces the item at that existing index,
+// otherwise it is added to the end.
+func (om *Map[K, V]) Add(key K, val V) {
+	om.Init()
+	if idx, has := om.Map[key]; has {
+		om.Map[key] = idx
+		om.Order[idx] = KeyValue[K, V]{Key: key, Value: val}
+	} else {
+		om.Map[key] = len(om.Order)
+		om.Order = append(om.Order, KeyValue[K, V]{Key: key, Value: val})
+	}
+}
+
+// ReplaceIndex replaces the value at the given index
+// with the given new item with the given key.
+func (om *Map[K, V]) ReplaceIndex(idx int, key K, val V) {
+	old := om.Order[idx]
+	if key != old.Key {
+		delete(om.Map, old.Key)
+		om.Map[key] = idx
+	}
+	om.Order[idx] = KeyValue[K, V]{Key: key, Value: val}
+}
+
+// InsertAtIndex inserts the given value with the given key at the given index.
+// This is relatively slow because it needs to renumber the index map above
+// the inserted value.  It will panic if the key already exists because
+// the behavior is undefined in that situation.
+func (om *Map[K, V]) InsertAtIndex(idx int, key K, val V) {
+	if _, has := om.Map[key]; has {
+		panic("key already exists")
+	}
+	om.Init()
+	sz := len(om.Order)
+	for o := idx; o < sz; o++ {
+		om.Map[om.Order[o].Key] = o + 1
+	}
+	om.Map[key] = idx
+	om.Order = slices.Insert(om.Order, idx, KeyValue[K, V]{Key: key, Value: val})
+}
+
+// ValueByKey returns the value corresponding to the given key,
+// with a zero value returned for a missing key. See [Map.ValueByKeyTry]
+// for one that returns a bool for missing keys.
+func (om *Map[K, V]) ValueByKey(key K) V {
+	idx, ok := om.Map[key]
+	if ok {
+		return om.Order[idx].Value
+	}
+	var zv V
+	return zv
+}
+
+// ValueByKeyTry returns the value corresponding to the given key,
+// with false returned for a missing key.
+func (om *Map[K, V]) ValueByKeyTry(key K) (V, bool) {
+	idx, ok := om.Map[key]
+	if ok {
+		return om.Order[idx].Value, ok
+	}
+	var zv V
+	return zv, false
+}
+
+// IndexIsValid returns an error if the given index is invalid
+func (om *Map[K, V]) IndexIsValid(idx int) error {
+	if idx >= len(om.Order) || idx < 0 {
+		return fmt.Errorf("ordmap.Map: IndexIsValid: index %d is out of range of a map of length %d", idx, len(om.Order))
+	}
+	return nil
+}
+
+// IndexByKey returns the index of the given key, with a -1 for missing key.
+// See [Map.IndexByKeyTry] for a version returning a bool for missing key.
+func (om *Map[K, V]) IndexByKey(key K) int {
+	idx, ok := om.Map[key]
+	if !ok {
+		return -1
+	}
+	return idx
+}
+
+// IndexByKeyTry returns the index of the given key, with false for a missing key.
+func (om *Map[K, V]) IndexByKeyTry(key K) (int, bool) {
+	idx, ok := om.Map[key]
+	return idx, ok
+}
+
+// ValueByIndex returns the value at the given index in the ordered slice.
+func (om *Map[K, V]) ValueByIndex(idx int) V {
+	return om.Order[idx].Value
+}
+
+// KeyByIndex returns the key for the given index in the ordered slice.
+func (om *Map[K, V]) KeyByIndex(idx int) K {
+	return om.Order[idx].Key
+}
+
+// Len returns the number of items in the map.
+func (om *Map[K, V]) Len() int {
+	if om == nil {
+		return 0
+	}
+	return len(om.Order)
+}
+
+// DeleteIndex deletes item(s) within the index range [i:j].
+// This is relatively slow because it needs to renumber the
+// index map above the deleted range.
+func (om *Map[K, V]) DeleteIndex(i, j int) {
+	sz := len(om.Order)
+	ndel := j - i
+	if ndel <= 0 {
+		panic("index range is <= 0")
+	}
+	for o := j; o < sz; o++ {
+		om.Map[om.Order[o].Key] = o - ndel
+	}
+	for o := i; o < j; o++ {
+		delete(om.Map, om.Order[o].Key)
+	}
+	om.Order = slices.Delete(om.Order, i, j)
+}
+
+// DeleteKey deletes the item with the given key, returning false if it does not find it.
+func (om *Map[K, V]) DeleteKey(key K) bool {
+	idx, ok := om.Map[key]
+	if !ok {
+		return false
+	}
+	om.DeleteIndex(idx, idx+1)
+	return true
+}
+
+// Keys returns a slice of the keys in order.
+func (om *Map[K, V]) Keys() []K {
+	kl := make([]K, om.Len())
+	for i, kv := range om.Order {
+		kl[i] = kv.Key
+	}
+	return kl
+}
+
+// Values returns a slice of the values in order.
+func (om *Map[K, V]) Values() []V {
+	vl := make([]V, om.Len())
+	for i, kv := range om.Order {
+		vl[i] = kv.Value
+	}
+	return vl
+}
+
+// Copy copies all of the entries from the given ordered map
+// into this ordered map. It keeps existing entries in this
+// map unless they also exist in the given map, in which case
+// they are overwritten.
+func (om *Map[K, V]) Copy(from *Map[K, V]) {
+	for _, kv := range from.Order {
+		om.Add(kv.Key, kv.Value)
+	}
+}
+
+// String returns a string representation of the map.
+func (om *Map[K, V]) String() string {
+	return fmt.Sprintf("%v", om.Order)
+}
+
+// GoString returns the map as Go code.
+func (om *Map[K, V]) GoString() string {
+	var zk K
+	var zv V
+	res := fmt.Sprintf("ordmap.Make([]ordmap.KeyVal[%T, %T]{\n", zk, zv)
+	for _, kv := range om.Order {
+		res += fmt.Sprintf("{%#v, %#v},\n", kv.Key, kv.Value)
+	}
+	res += "})"
+	return res
+}

+ 87 - 0
vendor/cogentcore.org/core/base/plan/update.go

@@ -0,0 +1,87 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package plan provides an efficient mechanism for updating a slice
+// to contain a target list of elements, generating minimal edits to
+// modify the current slice contents to match the target.
+// The mechanism depends on the use of unique name string identifiers
+// to determine whether an element is currently configured correctly.
+// These could be algorithmically generated hash strings or any other
+// such unique identifier.
+package plan
+
+import (
+	"slices"
+
+	"cogentcore.org/core/base/slicesx"
+)
+
+// Namer is an interface that types can implement to specify their name in a plan context.
+type Namer interface {
+
+	// PlanName returns the name of the object in a plan context.
+	PlanName() string
+}
+
+// Update ensures that the elements of the given slice contain
+// the elements according to the plan specified by the given arguments.
+// The argument n specifies the total number of items in the target plan.
+// The elements have unique names specified by the given name function.
+// If a new item is needed, the given new function is called to create it
+// for the given name at the given index position. After a new element is
+// created, it is added to the slice, and if the given optional init function
+// is non-nil, it is called with the new element and its index. If the
+// given destroy function is not-nil, then it is called on any element
+// that is being deleted from the slice. Update returns whether any changes
+// were made. The given slice must be a pointer so that it can be modified
+// live, which is required for init functions to run when the slice is
+// correctly updated to the current state.
+func Update[T Namer](s *[]T, n int, name func(i int) string, new func(name string, i int) T, init func(e T, i int), destroy func(e T)) bool {
+	changed := false
+	// first make a map for looking up the indexes of the target names
+	names := make([]string, n)
+	nmap := make(map[string]int, n)
+	smap := make(map[string]int, n)
+	for i := range n {
+		nm := name(i)
+		names[i] = nm
+		if _, has := nmap[nm]; has {
+			panic("plan.Update: duplicate name: " + nm) // no way to recover
+		}
+		nmap[nm] = i
+	}
+	// first remove anything we don't want
+	sn := len(*s)
+	for i := sn - 1; i >= 0; i-- {
+		nm := (*s)[i].PlanName()
+		if _, ok := nmap[nm]; !ok {
+			changed = true
+			if destroy != nil {
+				destroy((*s)[i])
+			}
+			*s = slices.Delete(*s, i, i+1)
+		}
+		smap[nm] = i
+	}
+	// next add and move items as needed; in order so guaranteed
+	for i, tn := range names {
+		ci := slicesx.Search(*s, func(e T) bool { return e.PlanName() == tn }, smap[tn])
+		if ci < 0 { // item not currently on the list
+			changed = true
+			ne := new(tn, i)
+			*s = slices.Insert(*s, i, ne)
+			if init != nil {
+				init(ne, i)
+			}
+		} else { // on the list; is it in the right place?
+			if ci != i {
+				changed = true
+				e := (*s)[ci]
+				*s = slices.Delete(*s, ci, ci+1)
+				*s = slices.Insert(*s, i, e)
+			}
+		}
+	}
+	return changed
+}

+ 185 - 0
vendor/cogentcore.org/core/base/profile/profile.go

@@ -0,0 +1,185 @@
+// Copyright (c) 2018, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package profile provides basic but effective profiling of targeted
+// functions or code sections, which can often be more informative than
+// generic cpu profiling.
+//
+// Here's how you use it:
+//
+//	// somewhere near start of program (e.g., using flag package)
+//	profileFlag := flag.Bool("profile", false, "turn on targeted profiling")
+//	...
+//	flag.Parse()
+//	profile.Profiling = *profileFlag
+//	...
+//	// surrounding the code of interest:
+//	pr := profile.Start()
+//	... code
+//	pr.End()
+//	...
+//	// at the end or whenever you've got enough data:
+//	profile.Report(time.Millisecond) // or time.Second or whatever
+package profile
+
+import (
+	"cmp"
+	"fmt"
+	"runtime"
+	"slices"
+	"strings"
+	"sync"
+	"time"
+
+	"cogentcore.org/core/base/errors"
+)
+
+// Main User API:
+
+// Start starts profiling and returns a Profile struct that must have [Profile.End]
+// called on it when done timing. It will be nil if not the first to start
+// timing on this function; it assumes nested inner / outer loop structure for
+// calls to the same method. It uses the short, package-qualified name of the
+// calling function as the name of the profile struct. Extra information can be
+// passed to Start, which will be added at the end of the name in a dash-delimited
+// format. See [StartName] for a version that supports a custom name.
+func Start(info ...string) *Profile {
+	if !Profiling {
+		return nil
+	}
+	name := ""
+	pc, _, _, ok := runtime.Caller(1)
+	if ok {
+		name = runtime.FuncForPC(pc).Name()
+		// get rid of everything before the package
+		if li := strings.LastIndex(name, "/"); li >= 0 {
+			name = name[li+1:]
+		}
+	} else {
+		err := "profile.Start: unexpected error: unable to get caller"
+		errors.Log(errors.New(err))
+		name = "!(" + err + ")"
+	}
+	if len(info) > 0 {
+		name += "-" + strings.Join(info, "-")
+	}
+	return TheProfiler.Start(name)
+}
+
+// StartName starts profiling and returns a Profile struct that must have
+// [Profile.End] called on it when done timing. It will be nil if not the first
+// to start timing on this function; it assumes nested inner / outer loop structure
+// for calls to the same method. It uses the given name as the name of the profile
+// struct. Extra information can be passed to StartName, which will be added at
+// the end of the name in a dash-delimited format. See [Start] for a version that
+// automatically determines the name from the name of the calling function.
+func StartName(name string, info ...string) *Profile {
+	if len(info) > 0 {
+		name += "-" + strings.Join(info, "-")
+	}
+	return TheProfiler.Start(name)
+}
+
+// Report generates a report of all the profile data collected.
+func Report(units time.Duration) {
+	TheProfiler.Report(units)
+}
+
+// Reset resets all of the profiling data.
+func Reset() {
+	TheProfiler.Reset()
+}
+
+// Profiling is whether profiling is currently enabled.
+var Profiling = false
+
+// TheProfiler is the global instance of [Profiler].
+var TheProfiler = Profiler{}
+
+// Profile represents one profiled function.
+type Profile struct {
+	Name   string
+	Total  time.Duration
+	N      int64
+	Avg    float64
+	St     time.Time
+	Timing bool
+}
+
+func (p *Profile) Start() *Profile {
+	if !p.Timing {
+		p.St = time.Now()
+		p.Timing = true
+		return p
+	}
+	return nil
+}
+
+func (p *Profile) End() {
+	if p == nil || !Profiling {
+		return
+	}
+	dur := time.Since(p.St)
+	p.Total += dur
+	p.N++
+	p.Avg = float64(p.Total) / float64(p.N)
+	p.Timing = false
+}
+
+func (p *Profile) Report(tot float64, units time.Duration) {
+	us := strings.TrimPrefix(units.String(), "1")
+	fmt.Printf("%-60sTotal:%8.2f %s\tAvg:%6.2f\tN:%6d\tPct:%6.2f\n",
+		p.Name, float64(p.Total)/float64(units), us, p.Avg/float64(units), p.N, 100.0*float64(p.Total)/tot)
+}
+
+// Profiler manages a map of profiled functions.
+type Profiler struct {
+	Profiles map[string]*Profile
+	mu       sync.Mutex
+}
+
+// Start starts profiling and returns a Profile struct that must have .End()
+// called on it when done timing
+func (p *Profiler) Start(name string) *Profile {
+	if !Profiling {
+		return nil
+	}
+	p.mu.Lock()
+	if p.Profiles == nil {
+		p.Profiles = make(map[string]*Profile, 0)
+	}
+	pr, ok := p.Profiles[name]
+	if !ok {
+		pr = &Profile{Name: name}
+		p.Profiles[name] = pr
+	}
+	prval := pr.Start()
+	p.mu.Unlock()
+	return prval
+}
+
+// Report generates a report of all the profile data collected
+func (p *Profiler) Report(units time.Duration) {
+	if !Profiling {
+		return
+	}
+	list := make([]*Profile, len(p.Profiles))
+	tot := 0.0
+	idx := 0
+	for _, pr := range p.Profiles {
+		tot += float64(pr.Total)
+		list[idx] = pr
+		idx++
+	}
+	slices.SortFunc(list, func(a, b *Profile) int {
+		return cmp.Compare(b.Total, a.Total)
+	})
+	for _, pr := range list {
+		pr.Report(tot, units)
+	}
+}
+
+func (p *Profiler) Reset() {
+	p.Profiles = make(map[string]*Profile, 0)
+}

+ 25 - 0
vendor/cogentcore.org/core/base/reflectx/interfaces.go

@@ -0,0 +1,25 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package reflectx
+
+import "image/color"
+
+// SetAnyer represents a type that can be set from any value.
+// It is checked in [SetRobust].
+type SetAnyer interface {
+	SetAny(v any) error
+}
+
+// SetStringer represents a type that can be set from a string
+// value. It is checked in [SetRobust].
+type SetStringer interface {
+	SetString(s string) error
+}
+
+// SetColorer represents a type that can be set from a color value.
+// It is checked in [SetRobust].
+type SetColorer interface {
+	SetColor(c color.Color)
+}

+ 239 - 0
vendor/cogentcore.org/core/base/reflectx/maps.go

@@ -0,0 +1,239 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package reflectx
+
+import (
+	"fmt"
+	"log"
+	"reflect"
+	"sort"
+	"strings"
+	"time"
+
+	"cogentcore.org/core/base/errors"
+)
+
+// This file contains helpful functions for dealing with maps
+// in the reflect system
+
+// MapValueType returns the type of the value for the given map (which can be
+// a pointer to a map or a direct map); just Elem() of map type, but using
+// this function makes it more explicit what is going on.
+func MapValueType(mp any) reflect.Type {
+	return NonPointerType(reflect.TypeOf(mp)).Elem()
+}
+
+// MapKeyType returns the type of the key for the given map (which can be a
+// pointer to a map or a direct map); just Key() of map type, but using
+// this function makes it more explicit what is going on.
+func MapKeyType(mp any) reflect.Type {
+	return NonPointerType(reflect.TypeOf(mp)).Key()
+}
+
+// MapAdd adds a new blank entry to the map.
+func MapAdd(mv any) {
+	mpv := reflect.ValueOf(mv)
+	mpvnp := Underlying(mpv)
+	mvtyp := mpvnp.Type()
+	valtyp := MapValueType(mv)
+	if valtyp.Kind() == reflect.Interface && valtyp.String() == "interface {}" {
+		valtyp = reflect.TypeOf("")
+	}
+	nkey := reflect.New(MapKeyType(mv))
+	nval := reflect.New(valtyp)
+	if mpvnp.IsNil() { // make a new map
+		mpv.Elem().Set(reflect.MakeMap(mvtyp))
+		mpvnp = Underlying(mpv)
+	}
+	mpvnp.SetMapIndex(nkey.Elem(), nval.Elem())
+}
+
+// MapDelete deletes the given key from the given map.
+func MapDelete(mv any, key reflect.Value) {
+	mpv := reflect.ValueOf(mv)
+	mpvnp := Underlying(mpv)
+	mpvnp.SetMapIndex(key, reflect.Value{}) // delete
+}
+
+// MapDeleteAll deletes everything from the given map.
+func MapDeleteAll(mv any) {
+	mpv := reflect.ValueOf(mv)
+	mpvnp := Underlying(mpv)
+	if mpvnp.Len() == 0 {
+		return
+	}
+	itr := mpvnp.MapRange()
+	for itr.Next() {
+		mpvnp.SetMapIndex(itr.Key(), reflect.Value{}) // delete
+	}
+}
+
+// MapSort sorts the keys of the map either by key or by value,
+// and returns those keys as a slice of [reflect.Value]s.
+func MapSort(mp any, byKey, ascending bool) []reflect.Value {
+	mpv := reflect.ValueOf(mp)
+	mpvnp := Underlying(mpv)
+	keys := mpvnp.MapKeys() // note: this is a slice of reflect.Value!
+	if byKey {
+		ValueSliceSort(keys, ascending)
+	} else {
+		MapValueSort(mpvnp, keys, ascending)
+	}
+	return keys
+}
+
+// MapValueSort sorts the keys of the given map by their values.
+func MapValueSort(mpvnp reflect.Value, keys []reflect.Value, ascending bool) error {
+	if len(keys) == 0 {
+		return nil
+	}
+	keyval := keys[0]
+	felval := mpvnp.MapIndex(keyval)
+	eltyp := felval.Type()
+	elnptyp := NonPointerType(eltyp)
+	vk := elnptyp.Kind()
+	elval := OnePointerValue(felval)
+	elif := elval.Interface()
+
+	// try all the numeric types first!
+
+	switch {
+	case vk >= reflect.Int && vk <= reflect.Int64:
+		sort.Slice(keys, func(i, j int) bool {
+			iv := Underlying(mpvnp.MapIndex(keys[i])).Int()
+			jv := Underlying(mpvnp.MapIndex(keys[j])).Int()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	case vk >= reflect.Uint && vk <= reflect.Uint64:
+		sort.Slice(keys, func(i, j int) bool {
+			iv := Underlying(mpvnp.MapIndex(keys[i])).Uint()
+			jv := Underlying(mpvnp.MapIndex(keys[j])).Uint()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	case vk >= reflect.Float32 && vk <= reflect.Float64:
+		sort.Slice(keys, func(i, j int) bool {
+			iv := Underlying(mpvnp.MapIndex(keys[i])).Float()
+			jv := Underlying(mpvnp.MapIndex(keys[j])).Float()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	case vk == reflect.Struct && ShortTypeName(elnptyp) == "time.Time":
+		sort.Slice(keys, func(i, j int) bool {
+			iv := Underlying(mpvnp.MapIndex(keys[i])).Interface().(time.Time)
+			jv := Underlying(mpvnp.MapIndex(keys[j])).Interface().(time.Time)
+			if ascending {
+				return iv.Before(jv)
+			}
+			return jv.Before(iv)
+		})
+	}
+
+	// this stringer case will likely pick up most of the rest
+	switch elif.(type) {
+	case fmt.Stringer:
+		sort.Slice(keys, func(i, j int) bool {
+			iv := Underlying(mpvnp.MapIndex(keys[i])).Interface().(fmt.Stringer).String()
+			jv := Underlying(mpvnp.MapIndex(keys[j])).Interface().(fmt.Stringer).String()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	}
+
+	// last resort!
+	switch {
+	case vk == reflect.String:
+		sort.Slice(keys, func(i, j int) bool {
+			iv := Underlying(mpvnp.MapIndex(keys[i])).String()
+			jv := Underlying(mpvnp.MapIndex(keys[j])).String()
+			if ascending {
+				return strings.ToLower(iv) < strings.ToLower(jv)
+			}
+			return strings.ToLower(iv) > strings.ToLower(jv)
+		})
+		return nil
+	}
+
+	err := fmt.Errorf("MapValueSort: unable to sort elements of type: %v", eltyp.String())
+	log.Println(err)
+	return err
+}
+
+// SetMapRobust robustly sets a map value using [reflect.Value]
+// representations of the map, key, and value elements, ensuring that the
+// proper types are used for the key and value elements using sensible
+// conversions.
+func SetMapRobust(mp, ky, val reflect.Value) bool {
+	mtyp := mp.Type()
+	if mtyp.Kind() != reflect.Map {
+		log.Printf("reflectx.SetMapRobust: map arg is not map, is: %v\n", mtyp.String())
+		return false
+	}
+	if !mp.CanSet() {
+		log.Printf("reflectx.SetMapRobust: map arg is not settable: %v\n", mtyp.String())
+		return false
+	}
+	ktyp := mtyp.Key()
+	etyp := mtyp.Elem()
+	if etyp.Kind() == val.Kind() && ky.Kind() == ktyp.Kind() {
+		mp.SetMapIndex(ky, val)
+		return true
+	}
+	if ky.Kind() == ktyp.Kind() {
+		mp.SetMapIndex(ky, val.Convert(etyp))
+		return true
+	}
+	if etyp.Kind() == val.Kind() {
+		mp.SetMapIndex(ky.Convert(ktyp), val)
+		return true
+	}
+	mp.SetMapIndex(ky.Convert(ktyp), val.Convert(etyp))
+	return true
+}
+
+// CopyMapRobust robustly copies maps.
+func CopyMapRobust(to, from any) error {
+	tov := reflect.ValueOf(to)
+	fmv := reflect.ValueOf(from)
+	tonp := Underlying(tov)
+	fmnp := Underlying(fmv)
+	totyp := tonp.Type()
+	if totyp.Kind() != reflect.Map {
+		err := fmt.Errorf("reflectx.CopyMapRobust: 'to' is not map, is: %v", totyp)
+		return errors.Log(err)
+	}
+	fmtyp := fmnp.Type()
+	if fmtyp.Kind() != reflect.Map {
+		err := fmt.Errorf("reflectx.CopyMapRobust: 'from' is not map, is: %v", fmtyp)
+		return errors.Log(err)
+	}
+	if tonp.IsNil() {
+		OnePointerValue(tov).Elem().Set(reflect.MakeMap(totyp))
+	} else {
+		MapDeleteAll(to)
+	}
+	if fmnp.Len() == 0 {
+		return nil
+	}
+	eltyp := SliceElementType(to)
+	itr := fmnp.MapRange()
+	for itr.Next() {
+		tonp.SetMapIndex(itr.Key(), CloneToType(eltyp, itr.Value().Interface()).Elem())
+	}
+	return nil
+}

+ 127 - 0
vendor/cogentcore.org/core/base/reflectx/pointers.go

@@ -0,0 +1,127 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package reflectx
+
+import (
+	"reflect"
+)
+
+// NonPointerType returns a non-pointer version of the given type.
+func NonPointerType(typ reflect.Type) reflect.Type {
+	if typ == nil {
+		return typ
+	}
+	for typ.Kind() == reflect.Pointer {
+		typ = typ.Elem()
+	}
+	return typ
+}
+
+// NonPointerValue returns a non-pointer version of the given value.
+// If it encounters a nil pointer, it returns the nil pointer instead
+// of an invalid value.
+func NonPointerValue(v reflect.Value) reflect.Value {
+	for v.Kind() == reflect.Pointer {
+		new := v.Elem()
+		if !new.IsValid() {
+			return v
+		}
+		v = new
+	}
+	return v
+}
+
+// PointerValue returns a pointer to the given value if it is not already
+// a pointer.
+func PointerValue(v reflect.Value) reflect.Value {
+	if !v.IsValid() {
+		return v
+	}
+	if v.Kind() == reflect.Pointer {
+		return v
+	}
+	if v.CanAddr() {
+		return v.Addr()
+	}
+	pv := reflect.New(v.Type())
+	pv.Elem().Set(v)
+	return pv
+}
+
+// OnePointerValue returns a value that is exactly one pointer away
+// from a non-pointer value.
+func OnePointerValue(v reflect.Value) reflect.Value {
+	if !v.IsValid() {
+		return v
+	}
+	if v.Kind() != reflect.Pointer {
+		if v.CanAddr() {
+			return v.Addr()
+		}
+		// slog.Error("reflectx.OnePointerValue: cannot take address of value", "value", v)
+		pv := reflect.New(v.Type())
+		pv.Elem().Set(v)
+		return pv
+	} else {
+		for v.Elem().Kind() == reflect.Pointer {
+			v = v.Elem()
+		}
+	}
+	return v
+}
+
+// Underlying returns the actual underlying version of the given value,
+// going through any pointers and interfaces. If it encounters a nil
+// pointer or interface, it returns the nil pointer or interface instead of
+// an invalid value.
+func Underlying(v reflect.Value) reflect.Value {
+	if !v.IsValid() {
+		return v
+	}
+	for v.Type().Kind() == reflect.Interface || v.Type().Kind() == reflect.Pointer {
+		new := v.Elem()
+		if !new.IsValid() {
+			return v
+		}
+		v = new
+	}
+	return v
+}
+
+// UnderlyingPointer returns a pointer to the actual underlying version of the
+// given value, going through any pointers and interfaces. It is equivalent to
+// [OnePointerValue] of [Underlying], so if it encounters a nil pointer or
+// interface, it stops at the nil pointer or interface instead of returning
+// an invalid value.
+func UnderlyingPointer(v reflect.Value) reflect.Value {
+	if !v.IsValid() {
+		return v
+	}
+	uv := Underlying(v)
+	if !uv.IsValid() {
+		return v
+	}
+	return OnePointerValue(uv)
+}
+
+// NonNilNew has the same overall behavior as [reflect.New] except that
+// it traverses through any pointers such that a new zero non-pointer value
+// will be created in the end, so any pointers in the original type will not
+// be nil. For example, in pseudo-code, NonNilNew(**int) will return
+// &(&(&(0))).
+func NonNilNew(typ reflect.Type) reflect.Value {
+	n := 0
+	for typ.Kind() == reflect.Pointer {
+		n++
+		typ = typ.Elem()
+	}
+	v := reflect.New(typ)
+	for range n {
+		pv := reflect.New(v.Type())
+		pv.Elem().Set(v)
+		v = pv
+	}
+	return v
+}

+ 372 - 0
vendor/cogentcore.org/core/base/reflectx/slices.go

@@ -0,0 +1,372 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package reflectx
+
+import (
+	"fmt"
+	"reflect"
+	"sort"
+	"strings"
+	"time"
+
+	"cogentcore.org/core/base/errors"
+)
+
+// This file contains helpful functions for dealing with slices
+// in the reflect system
+
+// SliceElementType returns the type of the elements of the given slice (which can be
+// a pointer to a slice or a direct slice); just [reflect.Type.Elem] of slice type, but
+// using this function bypasses any pointer issues and makes it more explicit what is going on.
+func SliceElementType(sl any) reflect.Type {
+	return Underlying(reflect.ValueOf(sl)).Type().Elem()
+}
+
+// SliceElementValue returns a new [reflect.Value] of the [SliceElementType].
+func SliceElementValue(sl any) reflect.Value {
+	return NonNilNew(SliceElementType(sl)).Elem()
+}
+
+// SliceNewAt inserts a new blank element at the given index in the given slice.
+// -1 means the end.
+func SliceNewAt(sl any, idx int) {
+	up := UnderlyingPointer(reflect.ValueOf(sl))
+	np := up.Elem()
+	val := SliceElementValue(sl)
+	sz := np.Len()
+	np = reflect.Append(np, val)
+	if idx >= 0 && idx < sz {
+		reflect.Copy(np.Slice(idx+1, sz+1), np.Slice(idx, sz))
+		np.Index(idx).Set(val)
+	}
+	up.Elem().Set(np)
+}
+
+// SliceDeleteAt deletes the element at the given index in the given slice.
+func SliceDeleteAt(sl any, idx int) {
+	svl := OnePointerValue(reflect.ValueOf(sl))
+	svnp := NonPointerValue(svl)
+	svtyp := svnp.Type()
+	nval := reflect.New(svtyp.Elem())
+	sz := svnp.Len()
+	reflect.Copy(svnp.Slice(idx, sz-1), svnp.Slice(idx+1, sz))
+	svnp.Index(sz - 1).Set(nval.Elem())
+	svl.Elem().Set(svnp.Slice(0, sz-1))
+}
+
+// SliceSort sorts a slice of basic values (see [StructSliceSort] for sorting a
+// slice-of-struct using a specific field), using float, int, string, and [time.Time]
+// conversions.
+func SliceSort(sl any, ascending bool) error {
+	sv := reflect.ValueOf(sl)
+	svnp := NonPointerValue(sv)
+	if svnp.Len() == 0 {
+		return nil
+	}
+	eltyp := SliceElementType(sl)
+	elnptyp := NonPointerType(eltyp)
+	vk := elnptyp.Kind()
+	elval := OnePointerValue(svnp.Index(0))
+	elif := elval.Interface()
+
+	// try all the numeric types first!
+	switch {
+	case vk >= reflect.Int && vk <= reflect.Int64:
+		sort.Slice(svnp.Interface(), func(i, j int) bool {
+			iv := NonPointerValue(svnp.Index(i)).Int()
+			jv := NonPointerValue(svnp.Index(j)).Int()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	case vk >= reflect.Uint && vk <= reflect.Uint64:
+		sort.Slice(svnp.Interface(), func(i, j int) bool {
+			iv := NonPointerValue(svnp.Index(i)).Uint()
+			jv := NonPointerValue(svnp.Index(j)).Uint()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	case vk >= reflect.Float32 && vk <= reflect.Float64:
+		sort.Slice(svnp.Interface(), func(i, j int) bool {
+			iv := NonPointerValue(svnp.Index(i)).Float()
+			jv := NonPointerValue(svnp.Index(j)).Float()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	case vk == reflect.Struct && ShortTypeName(elnptyp) == "time.Time":
+		sort.Slice(svnp.Interface(), func(i, j int) bool {
+			iv := NonPointerValue(svnp.Index(i)).Interface().(time.Time)
+			jv := NonPointerValue(svnp.Index(j)).Interface().(time.Time)
+			if ascending {
+				return iv.Before(jv)
+			}
+			return jv.Before(iv)
+		})
+	}
+
+	// this stringer case will likely pick up most of the rest
+	switch elif.(type) {
+	case fmt.Stringer:
+		sort.Slice(svnp.Interface(), func(i, j int) bool {
+			iv := NonPointerValue(svnp.Index(i)).Interface().(fmt.Stringer).String()
+			jv := NonPointerValue(svnp.Index(j)).Interface().(fmt.Stringer).String()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	}
+
+	// last resort!
+	switch {
+	case vk == reflect.String:
+		sort.Slice(svnp.Interface(), func(i, j int) bool {
+			iv := NonPointerValue(svnp.Index(i)).String()
+			jv := NonPointerValue(svnp.Index(j)).String()
+			if ascending {
+				return strings.ToLower(iv) < strings.ToLower(jv)
+			}
+			return strings.ToLower(iv) > strings.ToLower(jv)
+		})
+		return nil
+	}
+
+	err := fmt.Errorf("SortSlice: unable to sort elements of type: %v", eltyp.String())
+	return errors.Log(err)
+}
+
+// StructSliceSort sorts the given slice of structs according to the given field
+// indexes and sort direction, using float, int, string, and [time.Time] conversions.
+// It will panic if the field indexes are invalid.
+func StructSliceSort(structSlice any, fieldIndex []int, ascending bool) error {
+	sv := reflect.ValueOf(structSlice)
+	svnp := NonPointerValue(sv)
+	if svnp.Len() == 0 {
+		return nil
+	}
+	structTyp := SliceElementType(structSlice)
+	structNptyp := NonPointerType(structTyp)
+	fld := structNptyp.FieldByIndex(fieldIndex) // not easy to check.
+	vk := fld.Type.Kind()
+	structVal := OnePointerValue(svnp.Index(0))
+	fieldVal := structVal.Elem().FieldByIndex(fieldIndex)
+	fieldIf := fieldVal.Interface()
+
+	// try all the numeric types first!
+	switch {
+	case vk >= reflect.Int && vk <= reflect.Int64:
+		sort.Slice(svnp.Interface(), func(i, j int) bool {
+			ival := OnePointerValue(svnp.Index(i))
+			iv := ival.Elem().FieldByIndex(fieldIndex).Int()
+			jval := OnePointerValue(svnp.Index(j))
+			jv := jval.Elem().FieldByIndex(fieldIndex).Int()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	case vk >= reflect.Uint && vk <= reflect.Uint64:
+		sort.Slice(svnp.Interface(), func(i, j int) bool {
+			ival := OnePointerValue(svnp.Index(i))
+			iv := ival.Elem().FieldByIndex(fieldIndex).Uint()
+			jval := OnePointerValue(svnp.Index(j))
+			jv := jval.Elem().FieldByIndex(fieldIndex).Uint()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	case vk >= reflect.Float32 && vk <= reflect.Float64:
+		sort.Slice(svnp.Interface(), func(i, j int) bool {
+			ival := OnePointerValue(svnp.Index(i))
+			iv := ival.Elem().FieldByIndex(fieldIndex).Float()
+			jval := OnePointerValue(svnp.Index(j))
+			jv := jval.Elem().FieldByIndex(fieldIndex).Float()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	case vk == reflect.Struct && ShortTypeName(fld.Type) == "time.Time":
+		sort.Slice(svnp.Interface(), func(i, j int) bool {
+			ival := OnePointerValue(svnp.Index(i))
+			iv := ival.Elem().FieldByIndex(fieldIndex).Interface().(time.Time)
+			jval := OnePointerValue(svnp.Index(j))
+			jv := jval.Elem().FieldByIndex(fieldIndex).Interface().(time.Time)
+			if ascending {
+				return iv.Before(jv)
+			}
+			return jv.Before(iv)
+		})
+	}
+
+	// this stringer case will likely pick up most of the rest
+	switch fieldIf.(type) {
+	case fmt.Stringer:
+		sort.Slice(svnp.Interface(), func(i, j int) bool {
+			ival := OnePointerValue(svnp.Index(i))
+			iv := ival.Elem().FieldByIndex(fieldIndex).Interface().(fmt.Stringer).String()
+			jval := OnePointerValue(svnp.Index(j))
+			jv := jval.Elem().FieldByIndex(fieldIndex).Interface().(fmt.Stringer).String()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	}
+
+	// last resort!
+	switch {
+	case vk == reflect.String:
+		sort.Slice(svnp.Interface(), func(i, j int) bool {
+			ival := OnePointerValue(svnp.Index(i))
+			iv := ival.Elem().FieldByIndex(fieldIndex).String()
+			jval := OnePointerValue(svnp.Index(j))
+			jv := jval.Elem().FieldByIndex(fieldIndex).String()
+			if ascending {
+				return strings.ToLower(iv) < strings.ToLower(jv)
+			}
+			return strings.ToLower(iv) > strings.ToLower(jv)
+		})
+		return nil
+	}
+
+	err := fmt.Errorf("SortStructSlice: unable to sort on field of type: %v", fld.Type.String())
+	return errors.Log(err)
+}
+
+// ValueSliceSort sorts a slice of [reflect.Value]s using basic types where possible.
+func ValueSliceSort(sl []reflect.Value, ascending bool) error {
+	if len(sl) == 0 {
+		return nil
+	}
+	felval := sl[0] // reflect.Value
+	eltyp := felval.Type()
+	elnptyp := NonPointerType(eltyp)
+	vk := elnptyp.Kind()
+	elval := OnePointerValue(felval)
+	elif := elval.Interface()
+
+	// try all the numeric types first!
+	switch {
+	case vk >= reflect.Int && vk <= reflect.Int64:
+		sort.Slice(sl, func(i, j int) bool {
+			iv := NonPointerValue(sl[i]).Int()
+			jv := NonPointerValue(sl[j]).Int()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	case vk >= reflect.Uint && vk <= reflect.Uint64:
+		sort.Slice(sl, func(i, j int) bool {
+			iv := NonPointerValue(sl[i]).Uint()
+			jv := NonPointerValue(sl[j]).Uint()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	case vk >= reflect.Float32 && vk <= reflect.Float64:
+		sort.Slice(sl, func(i, j int) bool {
+			iv := NonPointerValue(sl[i]).Float()
+			jv := NonPointerValue(sl[j]).Float()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	case vk == reflect.Struct && ShortTypeName(elnptyp) == "time.Time":
+		sort.Slice(sl, func(i, j int) bool {
+			iv := NonPointerValue(sl[i]).Interface().(time.Time)
+			jv := NonPointerValue(sl[j]).Interface().(time.Time)
+			if ascending {
+				return iv.Before(jv)
+			}
+			return jv.Before(iv)
+		})
+	}
+
+	// this stringer case will likely pick up most of the rest
+	switch elif.(type) {
+	case fmt.Stringer:
+		sort.Slice(sl, func(i, j int) bool {
+			iv := NonPointerValue(sl[i]).Interface().(fmt.Stringer).String()
+			jv := NonPointerValue(sl[j]).Interface().(fmt.Stringer).String()
+			if ascending {
+				return iv < jv
+			}
+			return iv > jv
+		})
+		return nil
+	}
+
+	// last resort!
+	switch {
+	case vk == reflect.String:
+		sort.Slice(sl, func(i, j int) bool {
+			iv := NonPointerValue(sl[i]).String()
+			jv := NonPointerValue(sl[j]).String()
+			if ascending {
+				return strings.ToLower(iv) < strings.ToLower(jv)
+			}
+			return strings.ToLower(iv) > strings.ToLower(jv)
+		})
+		return nil
+	}
+
+	err := fmt.Errorf("ValueSliceSort: unable to sort elements of type: %v", eltyp.String())
+	return errors.Log(err)
+}
+
+// CopySliceRobust robustly copies slices.
+func CopySliceRobust(to, from any) error {
+	tov := reflect.ValueOf(to)
+	fmv := reflect.ValueOf(from)
+	tonp := Underlying(tov)
+	fmnp := Underlying(fmv)
+	totyp := tonp.Type()
+	if totyp.Kind() != reflect.Slice {
+		err := fmt.Errorf("reflectx.CopySliceRobust: 'to' is not slice, is: %v", totyp.String())
+		return errors.Log(err)
+	}
+	fmtyp := fmnp.Type()
+	if fmtyp.Kind() != reflect.Slice {
+		err := fmt.Errorf("reflectx.CopySliceRobust: 'from' is not slice, is: %v", fmtyp.String())
+		return errors.Log(err)
+	}
+	fmlen := fmnp.Len()
+	if tonp.IsNil() {
+		tonp.Set(reflect.MakeSlice(totyp, fmlen, fmlen))
+	} else {
+		if tonp.Len() > fmlen {
+			tonp.SetLen(fmlen)
+		}
+	}
+	for i := 0; i < fmlen; i++ {
+		tolen := tonp.Len()
+		if i >= tolen {
+			SliceNewAt(to, i)
+		}
+		SetRobust(PointerValue(tonp.Index(i)).Interface(), fmnp.Index(i).Interface())
+	}
+	return nil
+}

+ 231 - 0
vendor/cogentcore.org/core/base/reflectx/structs.go

@@ -0,0 +1,231 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package reflectx
+
+import (
+	"fmt"
+	"log/slog"
+	"reflect"
+	"strconv"
+	"strings"
+
+	"cogentcore.org/core/base/errors"
+	"cogentcore.org/core/base/iox/jsonx"
+)
+
+// WalkFields calls the given walk function on all the exported primary fields of the
+// given parent struct value, including those on anonymous embedded
+// structs that this struct has. It effectively flattens all of the embedded fields
+// of the struct.
+//
+// It passes the current parent struct, current [reflect.StructField],
+// and current field value to the given should and walk functions.
+//
+// The given should function is called on every struct field (including
+// on embedded structs themselves) to determine whether that field and any fields
+// it has embedded should be handled (a return value of true indicates to continue
+// down and a value of false indicates to not).
+func WalkFields(parent reflect.Value, should func(parent reflect.Value, field reflect.StructField, value reflect.Value) bool, walk func(parent reflect.Value, parentField *reflect.StructField, field reflect.StructField, value reflect.Value)) {
+	walkFields(parent, nil, should, walk)
+}
+
+func walkFields(parent reflect.Value, parentField *reflect.StructField, should func(parent reflect.Value, field reflect.StructField, value reflect.Value) bool, walk func(parent reflect.Value, parentField *reflect.StructField, field reflect.StructField, value reflect.Value)) {
+	typ := parent.Type()
+	for i := 0; i < typ.NumField(); i++ {
+		field := typ.Field(i)
+		if !field.IsExported() {
+			continue
+		}
+		value := parent.Field(i)
+		if !should(parent, field, value) {
+			continue
+		}
+		if field.Type.Kind() == reflect.Struct && field.Anonymous {
+			walkFields(value, &field, should, walk)
+		} else {
+			walk(parent, parentField, field, value)
+		}
+	}
+}
+
+// NumAllFields returns the number of elemental fields in the given struct type
+// using [WalkFields].
+func NumAllFields(parent reflect.Value) int {
+	n := 0
+	WalkFields(parent,
+		func(parent reflect.Value, field reflect.StructField, value reflect.Value) bool {
+			return true
+		}, func(parent reflect.Value, parentField *reflect.StructField, field reflect.StructField, value reflect.Value) {
+			n++
+		})
+	return n
+}
+
+// ValueIsDefault returns whether the given value is equivalent to the
+// given string representation used in a field default tag.
+func ValueIsDefault(fv reflect.Value, def string) bool {
+	kind := fv.Kind()
+	if kind >= reflect.Int && kind <= reflect.Complex128 && strings.Contains(def, ":") {
+		dtags := strings.Split(def, ":")
+		lo, _ := strconv.ParseFloat(dtags[0], 64)
+		hi, _ := strconv.ParseFloat(dtags[1], 64)
+		vf, err := ToFloat(fv.Interface())
+		if err != nil {
+			slog.Error("reflectx.ValueIsDefault: error parsing struct field numerical range def tag", "def", def, "err", err)
+			return true
+		}
+		return lo <= vf && vf <= hi
+	}
+	dtags := strings.Split(def, ",")
+	if strings.ContainsAny(def, "{[") { // complex type, so don't split on commas
+		dtags = []string{def}
+	}
+	for _, df := range dtags {
+		df = FormatDefault(df)
+		if df == "" {
+			return fv.IsZero()
+		}
+		dv := reflect.New(fv.Type())
+		err := SetRobust(dv.Interface(), df)
+		if err != nil {
+			slog.Error("reflectx.ValueIsDefault: error getting value from default struct tag", "defaultStructTag", df, "value", fv, "err", err)
+			return false
+		}
+		if reflect.DeepEqual(fv.Interface(), dv.Elem().Interface()) {
+			return true
+		}
+	}
+	return false
+}
+
+// SetFromDefaultTags sets the values of fields in the given struct based on
+// `default:` default value struct field tags.
+func SetFromDefaultTags(v any) error {
+	ov := reflect.ValueOf(v)
+	if IsNil(ov) {
+		return nil
+	}
+	val := NonPointerValue(ov)
+	typ := val.Type()
+	for i := 0; i < typ.NumField(); i++ {
+		f := typ.Field(i)
+		if !f.IsExported() {
+			continue
+		}
+		fv := val.Field(i)
+		def := f.Tag.Get("default")
+		if NonPointerType(f.Type).Kind() == reflect.Struct && def == "" {
+			SetFromDefaultTags(PointerValue(fv).Interface())
+			continue
+		}
+		err := SetFromDefaultTag(fv, def)
+		if err != nil {
+			return fmt.Errorf("reflectx.SetFromDefaultTags: error setting field %q in object of type %q from val %q: %w", f.Name, typ.Name(), def, err)
+		}
+	}
+	return nil
+}
+
+// SetFromDefaultTag sets the given value from the given default tag.
+func SetFromDefaultTag(v reflect.Value, def string) error {
+	def = FormatDefault(def)
+	if def == "" {
+		return nil
+	}
+	return SetRobust(UnderlyingPointer(v).Interface(), def)
+}
+
+// ShouldSaver is an interface that types can implement to specify
+// whether a value should be included in the result of [NonDefaultFields].
+type ShouldSaver interface {
+
+	// ShouldSave returns whether this value should be included in the
+	// result of [NonDefaultFields].
+	ShouldSave() bool
+}
+
+// TODO: this needs to return an ordmap or struct of the fields
+
+// NonDefaultFields returns a map representing all of the fields of the given
+// struct (or pointer to a struct) that have values different than their default
+// values as specified by the `default:` struct tag. The resulting map is then typically
+// saved using something like JSON or TOML. If a value has no default value, it
+// checks whether its value is non-zero. If a field has a `save:"-"` tag, it wil
+// not be included in the resulting map. If a field implements [ShouldSaver] and
+// returns false, it will not be included in the resulting map.
+func NonDefaultFields(v any) map[string]any {
+	res := map[string]any{}
+
+	rv := Underlying(reflect.ValueOf(v))
+	if IsNil(rv) {
+		return nil
+	}
+	rt := rv.Type()
+	nf := rt.NumField()
+	for i := 0; i < nf; i++ {
+		fv := rv.Field(i)
+		ft := rt.Field(i)
+		if ft.Tag.Get("save") == "-" {
+			continue
+		}
+		if ss, ok := UnderlyingPointer(fv).Interface().(ShouldSaver); ok {
+			if !ss.ShouldSave() {
+				continue
+			}
+		}
+		def := ft.Tag.Get("default")
+		if NonPointerType(ft.Type).Kind() == reflect.Struct && def == "" {
+			sfm := NonDefaultFields(fv.Interface())
+			if len(sfm) > 0 {
+				res[ft.Name] = sfm
+			}
+			continue
+		}
+		if !ValueIsDefault(fv, def) {
+			res[ft.Name] = fv.Interface()
+		}
+	}
+	return res
+}
+
+// FormatDefault converts the given `default:` struct tag string into a format suitable
+// for being used as a value in [SetRobust]. If it returns "", the default value
+// should not be used.
+func FormatDefault(def string) string {
+	if def == "" {
+		return ""
+	}
+	if strings.ContainsAny(def, "{[") { // complex type, so don't split on commas and colons
+		return strings.ReplaceAll(def, `'`, `"`) // allow single quote to work as double quote for JSON format
+	}
+	// we split on commas and colons so we get the first item of lists and ranges
+	def = strings.Split(def, ",")[0]
+	def = strings.Split(def, ":")[0]
+	return def
+}
+
+// StructTags returns a map[string]string of the tag string from a [reflect.StructTag] value.
+func StructTags(tags reflect.StructTag) map[string]string {
+	if len(tags) == 0 {
+		return nil
+	}
+	flds := strings.Fields(string(tags))
+	smap := make(map[string]string, len(flds))
+	for _, fld := range flds {
+		cli := strings.Index(fld, ":")
+		if cli < 0 || len(fld) < cli+3 {
+			continue
+		}
+		vl := strings.TrimSuffix(fld[cli+2:], `"`)
+		smap[fld[:cli]] = vl
+	}
+	return smap
+}
+
+// StringJSON returns an indented JSON string representation
+// of the given value for printing/debugging.
+func StringJSON(v any) string {
+	return string(errors.Log1(jsonx.WriteBytesIndent(v)))
+}

+ 57 - 0
vendor/cogentcore.org/core/base/reflectx/types.go

@@ -0,0 +1,57 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package reflectx
+
+import (
+	"path"
+	"reflect"
+)
+
+// LongTypeName returns the long, full package-path qualified type name.
+// This is guaranteed to be unique and used for internal storage of
+// several maps to avoid any conflicts.  It is also very quick to compute.
+func LongTypeName(typ reflect.Type) string {
+	nptyp := NonPointerType(typ)
+	nm := nptyp.Name()
+	if nm != "" {
+		p := nptyp.PkgPath()
+		if p != "" {
+			return p + "." + nm
+		}
+		return nm
+	}
+	return typ.String()
+}
+
+// ShortTypeName returns the short version of a package-qualified type name
+// which just has the last element of the path.  This is what is used in
+// standard Go programming, and is is used for the key to lookup reflect.Type
+// names -- i.e., this is what you should save in a JSON file.
+// The potential naming conflict is worth the brevity, and typically a given
+// file will only contain mutually compatible, non-conflicting types.
+// This is cached in ShortNames because the path.Base computation is apparently
+// a bit slow.
+func ShortTypeName(typ reflect.Type) string {
+	nptyp := NonPointerType(typ)
+	nm := nptyp.Name()
+	if nm != "" {
+		p := nptyp.PkgPath()
+		if p != "" {
+			return path.Base(p) + "." + nm
+		}
+		return nm
+	}
+	return typ.String()
+}
+
+// CloneToType creates a new pointer to the given type
+// and uses [SetRobust] to copy an existing value
+// (of potentially another type) to it.
+func CloneToType(typ reflect.Type, val any) reflect.Value {
+	vn := reflect.New(typ)
+	evi := vn.Interface()
+	SetRobust(evi, val)
+	return vn
+}

+ 1114 - 0
vendor/cogentcore.org/core/base/reflectx/values.go

@@ -0,0 +1,1114 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package reflectx provides a collection of helpers for the reflect
+// package in the Go standard library.
+package reflectx
+
+import (
+	"encoding/json"
+	"fmt"
+	"image"
+	"image/color"
+	"reflect"
+	"strconv"
+	"time"
+
+	"cogentcore.org/core/base/bools"
+	"cogentcore.org/core/base/elide"
+	"cogentcore.org/core/colors"
+	"cogentcore.org/core/enums"
+)
+
+// IsNil returns whether the given value is nil or invalid.
+// If it is a non-nillable type, it does not check whether
+// it is nil to avoid panics.
+func IsNil(v reflect.Value) bool {
+	if !v.IsValid() {
+		return true
+	}
+	switch v.Kind() {
+	case reflect.Pointer, reflect.Interface, reflect.Map, reflect.Slice, reflect.Func, reflect.Chan:
+		return v.IsNil()
+	}
+	return false
+}
+
+// KindIsBasic returns whether the given [reflect.Kind] is a basic,
+// elemental type such as Int, Float, etc.
+func KindIsBasic(vk reflect.Kind) bool {
+	return vk >= reflect.Bool && vk <= reflect.Complex128
+}
+
+// KindIsNumber returns whether the given [reflect.Kind] is a numeric
+// type such as Int, Float, etc.
+func KindIsNumber(vk reflect.Kind) bool {
+	return vk >= reflect.Int && vk <= reflect.Complex128
+}
+
+// ToBool robustly converts to a bool any basic elemental type
+// (including pointers to such) using a big type switch organized
+// for greatest efficiency. It tries the [bools.Booler]
+// interface if not a bool type. It falls back on reflection when all
+// else fails.
+func ToBool(v any) (bool, error) {
+	switch vt := v.(type) {
+	case bool:
+		return vt, nil
+	case *bool:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *bool")
+		}
+		return *vt, nil
+	}
+
+	if br, ok := v.(bools.Booler); ok {
+		return br.Bool(), nil
+	}
+
+	switch vt := v.(type) {
+	case int:
+		return vt != 0, nil
+	case *int:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *int")
+		}
+		return *vt != 0, nil
+	case int32:
+		return vt != 0, nil
+	case *int32:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *int32")
+		}
+		return *vt != 0, nil
+	case int64:
+		return vt != 0, nil
+	case *int64:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *int64")
+		}
+		return *vt != 0, nil
+	case uint8:
+		return vt != 0, nil
+	case *uint8:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *uint8")
+		}
+		return *vt != 0, nil
+	case float64:
+		return vt != 0, nil
+	case *float64:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *float64")
+		}
+		return *vt != 0, nil
+	case float32:
+		return vt != 0, nil
+	case *float32:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *float32")
+		}
+		return *vt != 0, nil
+	case string:
+		r, err := strconv.ParseBool(vt)
+		if err != nil {
+			return false, err
+		}
+		return r, nil
+	case *string:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *string")
+		}
+		r, err := strconv.ParseBool(*vt)
+		if err != nil {
+			return false, err
+		}
+		return r, nil
+	case int8:
+		return vt != 0, nil
+	case *int8:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *int8")
+		}
+		return *vt != 0, nil
+	case int16:
+		return vt != 0, nil
+	case *int16:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *int16")
+		}
+		return *vt != 0, nil
+	case uint:
+		return vt != 0, nil
+	case *uint:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *uint")
+		}
+		return *vt != 0, nil
+	case uint16:
+		return vt != 0, nil
+	case *uint16:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *uint16")
+		}
+		return *vt != 0, nil
+	case uint32:
+		return vt != 0, nil
+	case *uint32:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *uint32")
+		}
+		return *vt != 0, nil
+	case uint64:
+		return vt != 0, nil
+	case *uint64:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *uint64")
+		}
+		return *vt != 0, nil
+	case uintptr:
+		return vt != 0, nil
+	case *uintptr:
+		if vt == nil {
+			return false, fmt.Errorf("got nil *uintptr")
+		}
+		return *vt != 0, nil
+	}
+
+	// then fall back on reflection
+	uv := Underlying(reflect.ValueOf(v))
+	if IsNil(uv) {
+		return false, fmt.Errorf("got nil value of type %T", v)
+	}
+	vk := uv.Kind()
+	switch {
+	case vk >= reflect.Int && vk <= reflect.Int64:
+		return (uv.Int() != 0), nil
+	case vk >= reflect.Uint && vk <= reflect.Uint64:
+		return (uv.Uint() != 0), nil
+	case vk == reflect.Bool:
+		return uv.Bool(), nil
+	case vk >= reflect.Float32 && vk <= reflect.Float64:
+		return (uv.Float() != 0.0), nil
+	case vk >= reflect.Complex64 && vk <= reflect.Complex128:
+		return (real(uv.Complex()) != 0.0), nil
+	case vk == reflect.String:
+		r, err := strconv.ParseBool(uv.String())
+		if err != nil {
+			return false, err
+		}
+		return r, nil
+	default:
+		return false, fmt.Errorf("got value %v of unsupported type %T", v, v)
+	}
+}
+
+// ToInt robustly converts to an int64 any basic elemental type
+// (including pointers to such) using a big type switch organized
+// for greatest efficiency, only falling back on reflection when all
+// else fails.
+func ToInt(v any) (int64, error) {
+	switch vt := v.(type) {
+	case int:
+		return int64(vt), nil
+	case *int:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int")
+		}
+		return int64(*vt), nil
+	case int32:
+		return int64(vt), nil
+	case *int32:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int32")
+		}
+		return int64(*vt), nil
+	case int64:
+		return vt, nil
+	case *int64:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int64")
+		}
+		return *vt, nil
+	case uint8:
+		return int64(vt), nil
+	case *uint8:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint8")
+		}
+		return int64(*vt), nil
+	case float64:
+		return int64(vt), nil
+	case *float64:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *float64")
+		}
+		return int64(*vt), nil
+	case float32:
+		return int64(vt), nil
+	case *float32:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *float32")
+		}
+		return int64(*vt), nil
+	case bool:
+		if vt {
+			return 1, nil
+		}
+		return 0, nil
+	case *bool:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *bool")
+		}
+		if *vt {
+			return 1, nil
+		}
+		return 0, nil
+	case string:
+		r, err := strconv.ParseInt(vt, 0, 64)
+		if err != nil {
+			return 0, err
+		}
+		return r, nil
+	case *string:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *string")
+		}
+		r, err := strconv.ParseInt(*vt, 0, 64)
+		if err != nil {
+			return 0, err
+		}
+		return r, nil
+	case enums.Enum:
+		return vt.Int64(), nil
+	case int8:
+		return int64(vt), nil
+	case *int8:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int8")
+		}
+		return int64(*vt), nil
+	case int16:
+		return int64(vt), nil
+	case *int16:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int16")
+		}
+		return int64(*vt), nil
+	case uint:
+		return int64(vt), nil
+	case *uint:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint")
+		}
+		return int64(*vt), nil
+	case uint16:
+		return int64(vt), nil
+	case *uint16:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint16")
+		}
+		return int64(*vt), nil
+	case uint32:
+		return int64(vt), nil
+	case *uint32:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint32")
+		}
+		return int64(*vt), nil
+	case uint64:
+		return int64(vt), nil
+	case *uint64:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint64")
+		}
+		return int64(*vt), nil
+	case uintptr:
+		return int64(vt), nil
+	case *uintptr:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uintptr")
+		}
+		return int64(*vt), nil
+	}
+
+	// then fall back on reflection
+	uv := Underlying(reflect.ValueOf(v))
+	if IsNil(uv) {
+		return 0, fmt.Errorf("got nil value of type %T", v)
+	}
+	vk := uv.Kind()
+	switch {
+	case vk >= reflect.Int && vk <= reflect.Int64:
+		return uv.Int(), nil
+	case vk >= reflect.Uint && vk <= reflect.Uint64:
+		return int64(uv.Uint()), nil
+	case vk == reflect.Bool:
+		if uv.Bool() {
+			return 1, nil
+		}
+		return 0, nil
+	case vk >= reflect.Float32 && vk <= reflect.Float64:
+		return int64(uv.Float()), nil
+	case vk >= reflect.Complex64 && vk <= reflect.Complex128:
+		return int64(real(uv.Complex())), nil
+	case vk == reflect.String:
+		r, err := strconv.ParseInt(uv.String(), 0, 64)
+		if err != nil {
+			return 0, err
+		}
+		return r, nil
+	default:
+		return 0, fmt.Errorf("got value %v of unsupported type %T", v, v)
+	}
+}
+
+// ToFloat robustly converts to a float64 any basic elemental type
+// (including pointers to such) using a big type switch organized for
+// greatest efficiency, only falling back on reflection when all else fails.
+func ToFloat(v any) (float64, error) {
+	switch vt := v.(type) {
+	case float64:
+		return vt, nil
+	case *float64:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *float64")
+		}
+		return *vt, nil
+	case float32:
+		return float64(vt), nil
+	case *float32:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *float32")
+		}
+		return float64(*vt), nil
+	case int:
+		return float64(vt), nil
+	case *int:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int")
+		}
+		return float64(*vt), nil
+	case int32:
+		return float64(vt), nil
+	case *int32:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int32")
+		}
+		return float64(*vt), nil
+	case int64:
+		return float64(vt), nil
+	case *int64:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int64")
+		}
+		return float64(*vt), nil
+	case uint8:
+		return float64(vt), nil
+	case *uint8:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint8")
+		}
+		return float64(*vt), nil
+	case bool:
+		if vt {
+			return 1, nil
+		}
+		return 0, nil
+	case *bool:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *bool")
+		}
+		if *vt {
+			return 1, nil
+		}
+		return 0, nil
+	case string:
+		r, err := strconv.ParseFloat(vt, 64)
+		if err != nil {
+			return 0.0, err
+		}
+		return r, nil
+	case *string:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *string")
+		}
+		r, err := strconv.ParseFloat(*vt, 64)
+		if err != nil {
+			return 0.0, err
+		}
+		return r, nil
+	case int8:
+		return float64(vt), nil
+	case *int8:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int8")
+		}
+		return float64(*vt), nil
+	case int16:
+		return float64(vt), nil
+	case *int16:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int16")
+		}
+		return float64(*vt), nil
+	case uint:
+		return float64(vt), nil
+	case *uint:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint")
+		}
+		return float64(*vt), nil
+	case uint16:
+		return float64(vt), nil
+	case *uint16:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint16")
+		}
+		return float64(*vt), nil
+	case uint32:
+		return float64(vt), nil
+	case *uint32:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint32")
+		}
+		return float64(*vt), nil
+	case uint64:
+		return float64(vt), nil
+	case *uint64:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint64")
+		}
+		return float64(*vt), nil
+	case uintptr:
+		return float64(vt), nil
+	case *uintptr:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uintptr")
+		}
+		return float64(*vt), nil
+	}
+
+	// then fall back on reflection
+	uv := Underlying(reflect.ValueOf(v))
+	if IsNil(uv) {
+		return 0, fmt.Errorf("got nil value of type %T", v)
+	}
+	vk := uv.Kind()
+	switch {
+	case vk >= reflect.Int && vk <= reflect.Int64:
+		return float64(uv.Int()), nil
+	case vk >= reflect.Uint && vk <= reflect.Uint64:
+		return float64(uv.Uint()), nil
+	case vk == reflect.Bool:
+		if uv.Bool() {
+			return 1, nil
+		}
+		return 0, nil
+	case vk >= reflect.Float32 && vk <= reflect.Float64:
+		return uv.Float(), nil
+	case vk >= reflect.Complex64 && vk <= reflect.Complex128:
+		return real(uv.Complex()), nil
+	case vk == reflect.String:
+		r, err := strconv.ParseFloat(uv.String(), 64)
+		if err != nil {
+			return 0, err
+		}
+		return r, nil
+	default:
+		return 0, fmt.Errorf("got value %v of unsupported type %T", v, v)
+	}
+}
+
+// ToFloat32 robustly converts to a float32 any basic elemental type
+// (including pointers to such) using a big type switch organized for
+// greatest efficiency, only falling back on reflection when all else fails.
+func ToFloat32(v any) (float32, error) {
+	switch vt := v.(type) {
+	case float32:
+		return vt, nil
+	case *float32:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *float32")
+		}
+		return *vt, nil
+	case float64:
+		return float32(vt), nil
+	case *float64:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *float64")
+		}
+		return float32(*vt), nil
+	case int:
+		return float32(vt), nil
+	case *int:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int")
+		}
+		return float32(*vt), nil
+	case int32:
+		return float32(vt), nil
+	case *int32:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int32")
+		}
+		return float32(*vt), nil
+	case int64:
+		return float32(vt), nil
+	case *int64:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int64")
+		}
+		return float32(*vt), nil
+	case uint8:
+		return float32(vt), nil
+	case *uint8:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint8")
+		}
+		return float32(*vt), nil
+	case bool:
+		if vt {
+			return 1, nil
+		}
+		return 0, nil
+	case *bool:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *bool")
+		}
+		if *vt {
+			return 1, nil
+		}
+		return 0, nil
+	case string:
+		r, err := strconv.ParseFloat(vt, 32)
+		if err != nil {
+			return 0, err
+		}
+		return float32(r), nil
+	case *string:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *string")
+		}
+		r, err := strconv.ParseFloat(*vt, 32)
+		if err != nil {
+			return 0, err
+		}
+		return float32(r), nil
+	case int8:
+		return float32(vt), nil
+	case *int8:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int8")
+		}
+		return float32(*vt), nil
+	case int16:
+		return float32(vt), nil
+	case *int16:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *int8")
+		}
+		return float32(*vt), nil
+	case uint:
+		return float32(vt), nil
+	case *uint:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint")
+		}
+		return float32(*vt), nil
+	case uint16:
+		return float32(vt), nil
+	case *uint16:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint16")
+		}
+		return float32(*vt), nil
+	case uint32:
+		return float32(vt), nil
+	case *uint32:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint32")
+		}
+		return float32(*vt), nil
+	case uint64:
+		return float32(vt), nil
+	case *uint64:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uint64")
+		}
+		return float32(*vt), nil
+	case uintptr:
+		return float32(vt), nil
+	case *uintptr:
+		if vt == nil {
+			return 0, fmt.Errorf("got nil *uintptr")
+		}
+		return float32(*vt), nil
+	}
+
+	// then fall back on reflection
+	uv := Underlying(reflect.ValueOf(v))
+	if IsNil(uv) {
+		return 0, fmt.Errorf("got nil value of type %T", v)
+	}
+	vk := uv.Kind()
+	switch {
+	case vk >= reflect.Int && vk <= reflect.Int64:
+		return float32(uv.Int()), nil
+	case vk >= reflect.Uint && vk <= reflect.Uint64:
+		return float32(uv.Uint()), nil
+	case vk == reflect.Bool:
+		if uv.Bool() {
+			return 1, nil
+		}
+		return 0, nil
+	case vk >= reflect.Float32 && vk <= reflect.Float64:
+		return float32(uv.Float()), nil
+	case vk >= reflect.Complex64 && vk <= reflect.Complex128:
+		return float32(real(uv.Complex())), nil
+	case vk == reflect.String:
+		r, err := strconv.ParseFloat(uv.String(), 32)
+		if err != nil {
+			return 0, err
+		}
+		return float32(r), nil
+	default:
+		return 0, fmt.Errorf("got value %v of unsupported type %T", v, v)
+	}
+}
+
+// ToString robustly converts anything to a String
+// using a big type switch organized for greatest efficiency.
+// First checks for string or []byte and returns that immediately,
+// then checks for the Stringer interface as the preferred conversion
+// (e.g., for enums), and then falls back on strconv calls for numeric types.
+// If everything else fails, it uses fmt.Sprintf("%v") which always works,
+// so there is no need for an error return value. It returns "nil" for any nil
+// pointers, and byte is converted as string(byte), not the decimal representation.
+func ToString(v any) string {
+	nilstr := "nil"
+	// TODO: this reflection is unideal for performance, but we need it to prevent panics,
+	// so this whole "greatest efficiency" type switch is kind of pointless.
+	rv := reflect.ValueOf(v)
+	if IsNil(rv) {
+		return nilstr
+	}
+	switch vt := v.(type) {
+	case string:
+		return vt
+	case *string:
+		if vt == nil {
+			return nilstr
+		}
+		return *vt
+	case []byte:
+		return string(vt)
+	case *[]byte:
+		if vt == nil {
+			return nilstr
+		}
+		return string(*vt)
+	}
+
+	if stringer, ok := v.(fmt.Stringer); ok {
+		return stringer.String()
+	}
+
+	switch vt := v.(type) {
+	case bool:
+		if vt {
+			return "true"
+		}
+		return "false"
+	case *bool:
+		if vt == nil {
+			return nilstr
+		}
+		if *vt {
+			return "true"
+		}
+		return "false"
+	case int:
+		return strconv.FormatInt(int64(vt), 10)
+	case *int:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatInt(int64(*vt), 10)
+	case int32:
+		return strconv.FormatInt(int64(vt), 10)
+	case *int32:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatInt(int64(*vt), 10)
+	case int64:
+		return strconv.FormatInt(vt, 10)
+	case *int64:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatInt(*vt, 10)
+	case uint8: // byte, converts as string char
+		return string(vt)
+	case *uint8:
+		if vt == nil {
+			return nilstr
+		}
+		return string(*vt)
+	case float64:
+		return strconv.FormatFloat(vt, 'G', -1, 64)
+	case *float64:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatFloat(*vt, 'G', -1, 64)
+	case float32:
+		return strconv.FormatFloat(float64(vt), 'G', -1, 32)
+	case *float32:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatFloat(float64(*vt), 'G', -1, 32)
+	case uintptr:
+		return fmt.Sprintf("%#x", uintptr(vt))
+	case *uintptr:
+		if vt == nil {
+			return nilstr
+		}
+		return fmt.Sprintf("%#x", uintptr(*vt))
+
+	case int8:
+		return strconv.FormatInt(int64(vt), 10)
+	case *int8:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatInt(int64(*vt), 10)
+	case int16:
+		return strconv.FormatInt(int64(vt), 10)
+	case *int16:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatInt(int64(*vt), 10)
+	case uint:
+		return strconv.FormatInt(int64(vt), 10)
+	case *uint:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatInt(int64(*vt), 10)
+	case uint16:
+		return strconv.FormatInt(int64(vt), 10)
+	case *uint16:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatInt(int64(*vt), 10)
+	case uint32:
+		return strconv.FormatInt(int64(vt), 10)
+	case *uint32:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatInt(int64(*vt), 10)
+	case uint64:
+		return strconv.FormatInt(int64(vt), 10)
+	case *uint64:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatInt(int64(*vt), 10)
+	case complex64:
+		return strconv.FormatFloat(float64(real(vt)), 'G', -1, 32) + "," + strconv.FormatFloat(float64(imag(vt)), 'G', -1, 32)
+	case *complex64:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatFloat(float64(real(*vt)), 'G', -1, 32) + "," + strconv.FormatFloat(float64(imag(*vt)), 'G', -1, 32)
+	case complex128:
+		return strconv.FormatFloat(real(vt), 'G', -1, 64) + "," + strconv.FormatFloat(imag(vt), 'G', -1, 64)
+	case *complex128:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatFloat(real(*vt), 'G', -1, 64) + "," + strconv.FormatFloat(imag(*vt), 'G', -1, 64)
+	}
+
+	// then fall back on reflection
+	uv := Underlying(rv)
+	if IsNil(uv) {
+		return nilstr
+	}
+	vk := uv.Kind()
+	switch {
+	case vk >= reflect.Int && vk <= reflect.Int64:
+		return strconv.FormatInt(uv.Int(), 10)
+	case vk >= reflect.Uint && vk <= reflect.Uint64:
+		return strconv.FormatUint(uv.Uint(), 10)
+	case vk == reflect.Bool:
+		return strconv.FormatBool(uv.Bool())
+	case vk >= reflect.Float32 && vk <= reflect.Float64:
+		return strconv.FormatFloat(uv.Float(), 'G', -1, 64)
+	case vk >= reflect.Complex64 && vk <= reflect.Complex128:
+		cv := uv.Complex()
+		rv := strconv.FormatFloat(real(cv), 'G', -1, 64) + "," + strconv.FormatFloat(imag(cv), 'G', -1, 64)
+		return rv
+	case vk == reflect.String:
+		return uv.String()
+	case vk == reflect.Slice:
+		eltyp := SliceElementType(v)
+		if eltyp.Kind() == reflect.Uint8 { // []byte
+			return string(v.([]byte))
+		}
+		fallthrough
+	default:
+		return fmt.Sprintf("%v", v)
+	}
+}
+
+// ToStringPrec robustly converts anything to a String using given precision
+// for converting floating values; using a value like 6 truncates the
+// nuisance random imprecision of actual floating point values due to the
+// fact that they are represented with binary bits.
+// Otherwise is identical to ToString for any other cases.
+func ToStringPrec(v any, prec int) string {
+	nilstr := "nil"
+	switch vt := v.(type) {
+	case string:
+		return vt
+	case *string:
+		if vt == nil {
+			return nilstr
+		}
+		return *vt
+	case []byte:
+		return string(vt)
+	case *[]byte:
+		if vt == nil {
+			return nilstr
+		}
+		return string(*vt)
+	}
+
+	if stringer, ok := v.(fmt.Stringer); ok {
+		return stringer.String()
+	}
+
+	switch vt := v.(type) {
+	case float64:
+		return strconv.FormatFloat(vt, 'G', prec, 64)
+	case *float64:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatFloat(*vt, 'G', prec, 64)
+	case float32:
+		return strconv.FormatFloat(float64(vt), 'G', prec, 32)
+	case *float32:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatFloat(float64(*vt), 'G', prec, 32)
+	case complex64:
+		return strconv.FormatFloat(float64(real(vt)), 'G', prec, 32) + "," + strconv.FormatFloat(float64(imag(vt)), 'G', prec, 32)
+	case *complex64:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatFloat(float64(real(*vt)), 'G', prec, 32) + "," + strconv.FormatFloat(float64(imag(*vt)), 'G', prec, 32)
+	case complex128:
+		return strconv.FormatFloat(real(vt), 'G', prec, 64) + "," + strconv.FormatFloat(imag(vt), 'G', prec, 64)
+	case *complex128:
+		if vt == nil {
+			return nilstr
+		}
+		return strconv.FormatFloat(real(*vt), 'G', prec, 64) + "," + strconv.FormatFloat(imag(*vt), 'G', prec, 64)
+	}
+	return ToString(v)
+}
+
+// SetRobust robustly sets the 'to' value from the 'from' value.
+// The 'to' value must be a pointer. It copies slices and maps robustly,
+// and it can set a struct, slice, or map from a JSON-formatted string
+// value. It also handles many other cases, so it is unlikely to fail.
+//
+// Note that maps are not reset prior to setting, whereas slices are
+// set to be fully equivalent to the source slice.
+func SetRobust(to, from any) error {
+	rto := reflect.ValueOf(to)
+	pto := UnderlyingPointer(rto)
+	if IsNil(pto) {
+		return fmt.Errorf("got nil destination value")
+	}
+	pito := pto.Interface()
+
+	totyp := pto.Elem().Type()
+	tokind := totyp.Kind()
+	if !pto.Elem().CanSet() {
+		return fmt.Errorf("destination value cannot be set; it must be a variable or field, not a const or tmp or other value that cannot be set (value: %v of type %T)", pto, pto)
+	}
+
+	// first we do the generic AssignableTo case
+	if rto.Kind() == reflect.Pointer {
+		fv := reflect.ValueOf(from)
+		if fv.IsValid() {
+			if fv.Type().AssignableTo(totyp) {
+				pto.Elem().Set(fv)
+				return nil
+			}
+			ufvp := UnderlyingPointer(fv)
+			if ufvp.IsValid() && ufvp.Type().AssignableTo(totyp) {
+				pto.Elem().Set(ufvp)
+				return nil
+			}
+			ufv := ufvp.Elem()
+			if ufv.IsValid() && ufv.Type().AssignableTo(totyp) {
+				pto.Elem().Set(ufv)
+				return nil
+			}
+		} else {
+			return nil
+		}
+	}
+
+	if sa, ok := pito.(SetAnyer); ok {
+		err := sa.SetAny(from)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+	if ss, ok := pito.(SetStringer); ok {
+		if s, ok := from.(string); ok {
+			err := ss.SetString(s)
+			if err != nil {
+				return err
+			}
+			return nil
+		}
+	}
+	if es, ok := pito.(enums.EnumSetter); ok {
+		if en, ok := from.(enums.Enum); ok {
+			es.SetInt64(en.Int64())
+			return nil
+		}
+		if str, ok := from.(string); ok {
+			return es.SetString(str)
+		}
+		fm, err := ToInt(from)
+		if err != nil {
+			return err
+		}
+		es.SetInt64(fm)
+		return nil
+	}
+
+	if bv, ok := pito.(bools.BoolSetter); ok {
+		fb, err := ToBool(from)
+		if err != nil {
+			return err
+		}
+		bv.SetBool(fb)
+		return nil
+	}
+	if td, ok := pito.(*time.Duration); ok {
+		if fs, ok := from.(string); ok {
+			fd, err := time.ParseDuration(fs)
+			if err != nil {
+				return err
+			}
+			*td = fd
+			return nil
+		}
+	}
+
+	if fc, err := colors.FromAny(from); err == nil {
+		switch c := pito.(type) {
+		case *color.RGBA:
+			*c = fc
+			return nil
+		case *image.Uniform:
+			c.C = fc
+			return nil
+		case SetColorer:
+			c.SetColor(fc)
+			return nil
+		case *image.Image:
+			*c = colors.Uniform(fc)
+			return nil
+		}
+	}
+
+	ftyp := NonPointerType(reflect.TypeOf(from))
+
+	switch {
+	case tokind >= reflect.Int && tokind <= reflect.Int64:
+		fm, err := ToInt(from)
+		if err != nil {
+			return err
+		}
+		pto.Elem().Set(reflect.ValueOf(fm).Convert(totyp))
+		return nil
+	case tokind >= reflect.Uint && tokind <= reflect.Uint64:
+		fm, err := ToInt(from)
+		if err != nil {
+			return err
+		}
+		pto.Elem().Set(reflect.ValueOf(fm).Convert(totyp))
+		return nil
+	case tokind == reflect.Bool:
+		fm, err := ToBool(from)
+		if err != nil {
+			return err
+		}
+		pto.Elem().Set(reflect.ValueOf(fm).Convert(totyp))
+		return nil
+	case tokind >= reflect.Float32 && tokind <= reflect.Float64:
+		fm, err := ToFloat(from)
+		if err != nil {
+			return err
+		}
+		pto.Elem().Set(reflect.ValueOf(fm).Convert(totyp))
+		return nil
+	case tokind == reflect.String:
+		fm := ToString(from)
+		pto.Elem().Set(reflect.ValueOf(fm).Convert(totyp))
+		return nil
+	case tokind == reflect.Struct:
+		if ftyp.Kind() == reflect.String {
+			err := json.Unmarshal([]byte(ToString(from)), to) // todo: this is not working -- see what marshal says, etc
+			if err != nil {
+				marsh, _ := json.Marshal(to)
+				return fmt.Errorf("error setting struct from string: %w (example format for string: %s)", err, string(marsh))
+			}
+			return nil
+		}
+	case tokind == reflect.Slice:
+		if ftyp.Kind() == reflect.String {
+			err := json.Unmarshal([]byte(ToString(from)), to)
+			if err != nil {
+				marsh, _ := json.Marshal(to)
+				return fmt.Errorf("error setting slice from string: %w (example format for string: %s)", err, string(marsh))
+			}
+			return nil
+		}
+		return CopySliceRobust(to, from)
+	case tokind == reflect.Map:
+		if ftyp.Kind() == reflect.String {
+			err := json.Unmarshal([]byte(ToString(from)), to)
+			if err != nil {
+				marsh, _ := json.Marshal(to)
+				return fmt.Errorf("error setting map from string: %w (example format for string: %s)", err, string(marsh))
+			}
+			return nil
+		}
+		return CopyMapRobust(to, from)
+	}
+
+	tos := elide.End(fmt.Sprintf("%v", to), 40)
+	fms := elide.End(fmt.Sprintf("%v", from), 40)
+	return fmt.Errorf("unable to set value %s of type %T (using underlying type: %s) from value %s of type %T (using underlying type: %s): not a supported type pair and direct assigning is not possible", tos, to, totyp.String(), fms, from, LongTypeName(Underlying(reflect.ValueOf(from)).Type()))
+}

+ 139 - 0
vendor/cogentcore.org/core/base/slicesx/slicesx.go

@@ -0,0 +1,139 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package slicesx provides additional slice functions
+// beyond those in the standard [slices] package.
+package slicesx
+
+import (
+	"slices"
+	"unsafe"
+)
+
+// GrowTo increases the slice's capacity, if necessary,
+// so that it can hold at least n elements.
+func GrowTo[S ~[]E, E any](s S, n int) S {
+	if n < 0 {
+		panic("cannot be negative")
+	}
+	if n -= cap(s); n > 0 {
+		s = append(s[:cap(s)], make([]E, n)...)[:len(s)]
+	}
+	return s
+}
+
+// SetLength sets the length of the given slice,
+// re-using and preserving existing values to the extent possible.
+func SetLength[E any](s []E, n int) []E {
+	if len(s) == n {
+		return s
+	}
+	if s == nil {
+		return make([]E, n)
+	}
+	if cap(s) < n {
+		s = GrowTo(s, n)
+	}
+	s = s[:n]
+	return s
+}
+
+// CopyFrom efficiently copies from src into dest, using SetLength
+// to ensure the destination has sufficient capacity, and returns
+// the destination (which may have changed location as a result).
+func CopyFrom[E any](dest []E, src []E) []E {
+	dest = SetLength(dest, len(src))
+	copy(dest, src)
+	return dest
+}
+
+// Move moves the element in the given slice at the given
+// old position to the given new position and returns the
+// resulting slice.
+func Move[E any](s []E, from, to int) []E {
+	temp := s[from]
+	s = slices.Delete(s, from, from+1)
+	s = slices.Insert(s, to, temp)
+	return s
+}
+
+// Swap swaps the elements at the given two indices in the given slice.
+func Swap[E any](s []E, i, j int) {
+	s[i], s[j] = s[j], s[i]
+}
+
+// As converts a slice of the given type to a slice of the other given type.
+// The underlying types of the slice elements must be equivalent.
+func As[F, T any](s []F) []T {
+	as := make([]T, len(s))
+	for i, v := range s {
+		as[i] = any(v).(T)
+	}
+	return as
+}
+
+// Search returns the index of the item in the given slice that matches the target
+// according to the given match function, using the given optional starting index
+// to optimize the search by searching bidirectionally outward from given index.
+// This is much faster when you have some idea about where the item might be.
+// If no start index is given, it starts in the middle, which is a good default.
+// It returns -1 if no item matching the match function is found.
+func Search[E any](slice []E, match func(e E) bool, startIndex ...int) int {
+	n := len(slice)
+	if n == 0 {
+		return -1
+	}
+	si := -1
+	if len(startIndex) > 0 {
+		si = startIndex[0]
+	}
+	if si < 0 {
+		si = n / 2
+	}
+	if si == 0 {
+		for idx, e := range slice {
+			if match(e) {
+				return idx
+			}
+		}
+	} else {
+		if si >= n {
+			si = n - 1
+		}
+		ui := si + 1
+		di := si
+		upo := false
+		for {
+			if !upo && ui < n {
+				if match(slice[ui]) {
+					return ui
+				}
+				ui++
+			} else {
+				upo = true
+			}
+			if di >= 0 {
+				if match(slice[di]) {
+					return di
+				}
+				di--
+			} else if upo {
+				break
+			}
+		}
+	}
+	return -1
+}
+
+// ToBytes returns the underlying bytes of given slice.
+// for items not in a slice, make one of length 1.
+// This is copied from webgpu.
+func ToBytes[E any](src []E) []byte {
+	l := uintptr(len(src))
+	if l == 0 {
+		return nil
+	}
+	elmSize := unsafe.Sizeof(src[0])
+	return unsafe.Slice((*byte)(unsafe.Pointer(&src[0])), l*elmSize)
+}

+ 4 - 0
vendor/cogentcore.org/core/base/strcase/README.md

@@ -0,0 +1,4 @@
+# strcase
+
+Package strcase provides functions for manipulating the case of strings (CamelCase, kebab-case, snake_case, Sentence case, etc). It is based on https://github.com/ettle/strcase, which is Copyright (c) 2020 Liyan David Chang under the MIT License. Its principle difference from other strcase packages is that it preserves acronyms in input text for CamelCase. Therefore, you must call `strings.ToLower` on any SCREAMING_INPUT_STRINGS before passing them to `ToCamel`, `ToLowerCamel`, `ToTitle`, and `ToSentence`.
+

+ 74 - 0
vendor/cogentcore.org/core/base/strcase/cases.go

@@ -0,0 +1,74 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Based on https://github.com/ettle/strcase
+// Copyright (c) 2020 Liyan David Chang under the MIT License
+
+package strcase
+
+import (
+	"strings"
+)
+
+// Cases is an enum with all of the different string cases.
+type Cases int32 //enums:enum
+
+const (
+	// LowerCase is all lower case
+	LowerCase Cases = iota
+
+	// UpperCase is all UPPER CASE
+	UpperCase
+
+	// SnakeCase is lower_case_words_with_underscores
+	SnakeCase
+
+	// SNAKECase is UPPER_CASE_WORDS_WITH_UNDERSCORES
+	SNAKECase
+
+	// KebabCase is lower-case-words-with-dashes
+	KebabCase
+
+	// KEBABCase is UPPER-CASE-WORDS-WITH-DASHES
+	KEBABCase
+
+	// CamelCase is CapitalizedWordsConcatenatedTogether
+	CamelCase
+
+	// LowerCamelCase is capitalizedWordsConcatenatedTogether, with the first word lower case
+	LowerCamelCase
+
+	// TitleCase is Captitalized Words With Spaces
+	TitleCase
+
+	// SentenceCase is Lower case words with spaces, with the first word capitalized
+	SentenceCase
+)
+
+// To converts the given string to the given case.
+func To(s string, c Cases) string {
+	switch c {
+	case LowerCase:
+		return strings.ToLower(s)
+	case UpperCase:
+		return strings.ToUpper(s)
+	case SnakeCase:
+		return ToSnake(s)
+	case SNAKECase:
+		return ToSNAKE(s)
+	case KebabCase:
+		return ToKebab(s)
+	case KEBABCase:
+		return ToKEBAB(s)
+	case CamelCase:
+		return ToCamel(s)
+	case LowerCamelCase:
+		return ToLowerCamel(s)
+	case TitleCase:
+		return ToTitle(s)
+	case SentenceCase:
+		return ToSentence(s)
+	}
+	return s
+}

+ 150 - 0
vendor/cogentcore.org/core/base/strcase/convert.go

@@ -0,0 +1,150 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Based on https://github.com/ettle/strcase
+// Copyright (c) 2020 Liyan David Chang under the MIT License
+
+package strcase
+
+import (
+	"strings"
+	"unicode"
+)
+
+// WordCases is an enumeration of the ways to format a word.
+type WordCases int32 //enums:enum -trim-prefix Word
+
+const (
+	// WordOriginal indicates to preserve the original input case.
+	WordOriginal WordCases = iota
+
+	// WordLowerCase indicates to make all letters lower case (example).
+	WordLowerCase
+
+	// WordUpperCase indicates to make all letters upper case (EXAMPLE).
+	WordUpperCase
+
+	// WordTitleCase indicates to make only the first letter upper case (Example).
+	WordTitleCase
+
+	// WordCamelCase indicates to make only the first letter upper case, except
+	// in the first word, in which all letters are lower case (exampleText).
+	WordCamelCase
+
+	// WordSentenceCase indicates to make only the first letter upper case, and
+	// only for the first word (all other words have fully lower case letters).
+	WordSentenceCase
+)
+
+// ToWordCase converts the given input string to the given word case with the given delimiter.
+// Pass 0 for delimeter to use no delimiter.
+//
+//nolint:gocyclo
+func ToWordCase(input string, wordCase WordCases, delimiter rune) string {
+	input = strings.TrimSpace(input)
+	runes := []rune(input)
+	if len(runes) == 0 {
+		return ""
+	}
+
+	var b strings.Builder
+	b.Grow(len(input) + 4) // In case we need to write delimiters where they weren't before
+
+	firstWord := true
+	var skipIndexes []int
+
+	addWord := func(start, end int) {
+		// If you have nothing good to say, say nothing at all
+		if start == end || len(skipIndexes) == end-start {
+			skipIndexes = nil
+			return
+		}
+
+		// If you have something to say, start with a delimiter
+		if !firstWord && delimiter != 0 {
+			b.WriteRune(delimiter)
+		}
+
+		// Check to see if the entire word is an initialism for preserving initialism.
+		// Note we don't support preserving initialisms if they are followed
+		// by a number and we're not spliting before numbers.
+		if wordCase == WordTitleCase || wordCase == WordSentenceCase || (wordCase == WordCamelCase && !firstWord) {
+			allCaps := true
+			for i := start; i < end; i++ {
+				allCaps = allCaps && (isUpper(runes[i]) || !unicode.IsLetter(runes[i]))
+			}
+			if allCaps {
+				b.WriteString(string(runes[start:end]))
+				firstWord = false
+				return
+			}
+		}
+
+		skipIndex := 0
+		for i := start; i < end; i++ {
+			if len(skipIndexes) > 0 && skipIndex < len(skipIndexes) && i == skipIndexes[skipIndex] {
+				skipIndex++
+				continue
+			}
+			r := runes[i]
+			switch wordCase {
+			case WordUpperCase:
+				b.WriteRune(toUpper(r))
+			case WordLowerCase:
+				b.WriteRune(toLower(r))
+			case WordTitleCase:
+				if i == start {
+					b.WriteRune(toUpper(r))
+				} else {
+					b.WriteRune(toLower(r))
+				}
+			case WordCamelCase:
+				if !firstWord && i == start {
+					b.WriteRune(toUpper(r))
+				} else {
+					b.WriteRune(toLower(r))
+				}
+			case WordSentenceCase:
+				if firstWord && i == start {
+					b.WriteRune(toUpper(r))
+				} else {
+					b.WriteRune(toLower(r))
+				}
+			default:
+				b.WriteRune(r)
+			}
+		}
+		firstWord = false
+		skipIndexes = nil
+	}
+
+	var prev, curr rune
+	next := runes[0] // 0 length will have already returned so safe to index
+	wordStart := 0
+	for i := 0; i < len(runes); i++ {
+		prev = curr
+		curr = next
+		if i+1 == len(runes) {
+			next = 0
+		} else {
+			next = runes[i+1]
+		}
+
+		switch defaultSplitFn(prev, curr, next) {
+		case Skip:
+			skipIndexes = append(skipIndexes, i)
+		case Split:
+			addWord(wordStart, i)
+			wordStart = i
+		case SkipSplit:
+			addWord(wordStart, i)
+			wordStart = i + 1
+		}
+	}
+
+	if wordStart != len(runes) {
+		addWord(wordStart, len(runes))
+	}
+	return b.String()
+}

+ 89 - 0
vendor/cogentcore.org/core/base/strcase/enumgen.go

@@ -0,0 +1,89 @@
+// Code generated by "core generate"; DO NOT EDIT.
+
+package strcase
+
+import (
+	"cogentcore.org/core/enums"
+)
+
+var _CasesValues = []Cases{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
+
+// CasesN is the highest valid value for type Cases, plus one.
+const CasesN Cases = 10
+
+var _CasesValueMap = map[string]Cases{`LowerCase`: 0, `UpperCase`: 1, `SnakeCase`: 2, `SNAKECase`: 3, `KebabCase`: 4, `KEBABCase`: 5, `CamelCase`: 6, `LowerCamelCase`: 7, `TitleCase`: 8, `SentenceCase`: 9}
+
+var _CasesDescMap = map[Cases]string{0: `LowerCase is all lower case`, 1: `UpperCase is all UPPER CASE`, 2: `SnakeCase is lower_case_words_with_underscores`, 3: `SNAKECase is UPPER_CASE_WORDS_WITH_UNDERSCORES`, 4: `KebabCase is lower-case-words-with-dashes`, 5: `KEBABCase is UPPER-CASE-WORDS-WITH-DASHES`, 6: `CamelCase is CapitalizedWordsConcatenatedTogether`, 7: `LowerCamelCase is capitalizedWordsConcatenatedTogether, with the first word lower case`, 8: `TitleCase is Captitalized Words With Spaces`, 9: `SentenceCase is Lower case words with spaces, with the first word capitalized`}
+
+var _CasesMap = map[Cases]string{0: `LowerCase`, 1: `UpperCase`, 2: `SnakeCase`, 3: `SNAKECase`, 4: `KebabCase`, 5: `KEBABCase`, 6: `CamelCase`, 7: `LowerCamelCase`, 8: `TitleCase`, 9: `SentenceCase`}
+
+// String returns the string representation of this Cases value.
+func (i Cases) String() string { return enums.String(i, _CasesMap) }
+
+// SetString sets the Cases value from its string representation,
+// and returns an error if the string is invalid.
+func (i *Cases) SetString(s string) error { return enums.SetString(i, s, _CasesValueMap, "Cases") }
+
+// Int64 returns the Cases value as an int64.
+func (i Cases) Int64() int64 { return int64(i) }
+
+// SetInt64 sets the Cases value from an int64.
+func (i *Cases) SetInt64(in int64) { *i = Cases(in) }
+
+// Desc returns the description of the Cases value.
+func (i Cases) Desc() string { return enums.Desc(i, _CasesDescMap) }
+
+// CasesValues returns all possible values for the type Cases.
+func CasesValues() []Cases { return _CasesValues }
+
+// Values returns all possible values for the type Cases.
+func (i Cases) Values() []enums.Enum { return enums.Values(_CasesValues) }
+
+// MarshalText implements the [encoding.TextMarshaler] interface.
+func (i Cases) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
+
+// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
+func (i *Cases) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Cases") }
+
+var _WordCasesValues = []WordCases{0, 1, 2, 3, 4, 5}
+
+// WordCasesN is the highest valid value for type WordCases, plus one.
+const WordCasesN WordCases = 6
+
+var _WordCasesValueMap = map[string]WordCases{`Original`: 0, `LowerCase`: 1, `UpperCase`: 2, `TitleCase`: 3, `CamelCase`: 4, `SentenceCase`: 5}
+
+var _WordCasesDescMap = map[WordCases]string{0: `WordOriginal indicates to preserve the original input case.`, 1: `WordLowerCase indicates to make all letters lower case (example).`, 2: `WordUpperCase indicates to make all letters upper case (EXAMPLE).`, 3: `WordTitleCase indicates to make only the first letter upper case (Example).`, 4: `WordCamelCase indicates to make only the first letter upper case, except in the first word, in which all letters are lower case (exampleText).`, 5: `WordSentenceCase indicates to make only the first letter upper case, and only for the first word (all other words have fully lower case letters).`}
+
+var _WordCasesMap = map[WordCases]string{0: `Original`, 1: `LowerCase`, 2: `UpperCase`, 3: `TitleCase`, 4: `CamelCase`, 5: `SentenceCase`}
+
+// String returns the string representation of this WordCases value.
+func (i WordCases) String() string { return enums.String(i, _WordCasesMap) }
+
+// SetString sets the WordCases value from its string representation,
+// and returns an error if the string is invalid.
+func (i *WordCases) SetString(s string) error {
+	return enums.SetString(i, s, _WordCasesValueMap, "WordCases")
+}
+
+// Int64 returns the WordCases value as an int64.
+func (i WordCases) Int64() int64 { return int64(i) }
+
+// SetInt64 sets the WordCases value from an int64.
+func (i *WordCases) SetInt64(in int64) { *i = WordCases(in) }
+
+// Desc returns the description of the WordCases value.
+func (i WordCases) Desc() string { return enums.Desc(i, _WordCasesDescMap) }
+
+// WordCasesValues returns all possible values for the type WordCases.
+func WordCasesValues() []WordCases { return _WordCasesValues }
+
+// Values returns all possible values for the type WordCases.
+func (i WordCases) Values() []enums.Enum { return enums.Values(_WordCasesValues) }
+
+// MarshalText implements the [encoding.TextMarshaler] interface.
+func (i WordCases) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
+
+// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
+func (i *WordCases) UnmarshalText(text []byte) error {
+	return enums.UnmarshalText(i, text, "WordCases")
+}

+ 34 - 0
vendor/cogentcore.org/core/base/strcase/list.go

@@ -0,0 +1,34 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package strcase
+
+// FormatList returns a formatted version of the given list of items following these rules:
+//   - nil => ""
+//   - "Go" => "Go"
+//   - "Go", "Python" => "Go and Python"
+//   - "Go", "Python", "JavaScript" => "Go, Python, and JavaScript"
+//   - "Go", "Python", "JavaScript", "C" => "Go, Python, JavaScript, and C"
+func FormatList(items ...string) string {
+	switch len(items) {
+	case 0:
+		return ""
+	case 1:
+		return items[0]
+	case 2:
+		return items[0] + " and " + items[1]
+	}
+	res := ""
+	for i, match := range items {
+		res += match
+		if i == len(items)-1 {
+			// last one, so do nothing
+		} else if i == len(items)-2 {
+			res += ", and "
+		} else {
+			res += ", "
+		}
+	}
+	return res
+}

+ 67 - 0
vendor/cogentcore.org/core/base/strcase/split.go

@@ -0,0 +1,67 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Based on https://github.com/ettle/strcase
+// Copyright (c) 2020 Liyan David Chang under the MIT License
+
+package strcase
+
+import "unicode"
+
+// SplitAction defines if and how to split a string
+type SplitAction int
+
+const (
+	// Noop - Continue to next character
+	Noop SplitAction = iota
+	// Split - Split between words
+	// e.g. to split between wordsWithoutDelimiters
+	Split
+	// SkipSplit - Split the word and drop the character
+	// e.g. to split words with delimiters
+	SkipSplit
+	// Skip - Remove the character completely
+	Skip
+)
+
+//nolint:gocyclo
+func defaultSplitFn(prev, curr, next rune) SplitAction {
+	// The most common case will be that it's just a letter so let lowercase letters return early since we know what they should do
+	if isLower(curr) {
+		return Noop
+	}
+	// Delimiters are _, -, ., and unicode spaces
+	// Handle . lower down as it needs to happen after number exceptions
+	if curr == '_' || curr == '-' || isSpace(curr) {
+		return SkipSplit
+	}
+
+	if isUpper(curr) {
+		if isLower(prev) {
+			// fooBar
+			return Split
+		} else if isUpper(prev) && isLower(next) {
+			// FOOBar
+			return Split
+		}
+	}
+
+	// Do numeric exceptions last to avoid perf penalty
+	if unicode.IsNumber(prev) {
+		// v4.3 is not split
+		if (curr == '.' || curr == ',') && unicode.IsNumber(next) {
+			return Noop
+		}
+		if !unicode.IsNumber(curr) && curr != '.' {
+			return Split
+		}
+	}
+	// While period is a default delimiter, keep it down here to avoid
+	// penalty for other delimiters
+	if curr == '.' {
+		return SkipSplit
+	}
+
+	return Noop
+}

+ 60 - 0
vendor/cogentcore.org/core/base/strcase/strcase.go

@@ -0,0 +1,60 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Based on https://github.com/ettle/strcase
+// Copyright (c) 2020 Liyan David Chang under the MIT License
+
+// Package strcase provides functions for manipulating the case of strings (CamelCase, kebab-case,
+// snake_case, Sentence case, etc). It is based on https://github.com/ettle/strcase, which is Copyright
+// (c) 2020 Liyan David Chang under the MIT License. Its principle difference from other strcase packages
+// is that it preserves acronyms in input text for CamelCase. Therefore, you must call [strings.ToLower]
+// on any SCREAMING_INPUT_STRINGS before passing them to [ToCamel], [ToLowerCamel], [ToTitle], and [ToSentence].
+package strcase
+
+//go:generate core generate
+
+// ToSnake returns words in snake_case (lower case words with underscores).
+func ToSnake(s string) string {
+	return ToWordCase(s, WordLowerCase, '_')
+}
+
+// ToSNAKE returns words in SNAKE_CASE (upper case words with underscores).
+// Also known as SCREAMING_SNAKE_CASE or UPPER_CASE.
+func ToSNAKE(s string) string {
+	return ToWordCase(s, WordUpperCase, '_')
+}
+
+// ToKebab returns words in kebab-case (lower case words with dashes).
+// Also known as dash-case.
+func ToKebab(s string) string {
+	return ToWordCase(s, WordLowerCase, '-')
+}
+
+// ToKEBAB returns words in KEBAB-CASE (upper case words with dashes).
+// Also known as SCREAMING-KEBAB-CASE or SCREAMING-DASH-CASE.
+func ToKEBAB(s string) string {
+	return ToWordCase(s, WordUpperCase, '-')
+}
+
+// ToCamel returns words in CamelCase (capitalized words concatenated together).
+// Also known as UpperCamelCase.
+func ToCamel(s string) string {
+	return ToWordCase(s, WordTitleCase, 0)
+}
+
+// ToLowerCamel returns words in lowerCamelCase (capitalized words concatenated together,
+// with first word lower case). Also known as camelCase or mixedCase.
+func ToLowerCamel(s string) string {
+	return ToWordCase(s, WordCamelCase, 0)
+}
+
+// ToTitle returns words in Title Case (capitalized words with spaces).
+func ToTitle(s string) string {
+	return ToWordCase(s, WordTitleCase, ' ')
+}
+
+// ToSentence returns words in Sentence case (lower case words with spaces, with the first word capitalized).
+func ToSentence(s string) string {
+	return ToWordCase(s, WordSentenceCase, ' ')
+}

+ 55 - 0
vendor/cogentcore.org/core/base/strcase/unicode.go

@@ -0,0 +1,55 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Based on https://github.com/ettle/strcase
+// Copyright (c) 2020 Liyan David Chang under the MIT License
+
+package strcase
+
+import "unicode"
+
+// Unicode functions, optimized for the common case of ascii
+// No performance lost by wrapping since these functions get inlined by the compiler
+
+func isUpper(r rune) bool {
+	return unicode.IsUpper(r)
+}
+
+func isLower(r rune) bool {
+	return unicode.IsLower(r)
+}
+
+func isNumber(r rune) bool {
+	if r >= '0' && r <= '9' {
+		return true
+	}
+	return unicode.IsNumber(r)
+}
+
+func isSpace(r rune) bool {
+	if r == ' ' || r == '\t' || r == '\n' || r == '\r' {
+		return true
+	} else if r < 128 {
+		return false
+	}
+	return unicode.IsSpace(r)
+}
+
+func toUpper(r rune) rune {
+	if r >= 'a' && r <= 'z' {
+		return r - 32
+	} else if r < 128 {
+		return r
+	}
+	return unicode.ToUpper(r)
+}
+
+func toLower(r rune) rune {
+	if r >= 'A' && r <= 'Z' {
+		return r + 32
+	} else if r < 128 {
+		return r
+	}
+	return unicode.ToLower(r)
+}

+ 90 - 0
vendor/cogentcore.org/core/base/stringsx/stringsx.go

@@ -0,0 +1,90 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package stringsx provides additional string functions
+// beyond those in the standard [strings] package.
+package stringsx
+
+import (
+	"bytes"
+	"strings"
+)
+
+// TrimCR returns the string without any trailing \r carriage return
+func TrimCR(s string) string {
+	n := len(s)
+	if n == 0 {
+		return s
+	}
+	if s[n-1] == '\r' {
+		return s[:n-1]
+	}
+	return s
+}
+
+// ByteTrimCR returns the byte string without any trailing \r carriage return
+func ByteTrimCR(s []byte) []byte {
+	n := len(s)
+	if n == 0 {
+		return s
+	}
+	if s[n-1] == '\r' {
+		return s[:n-1]
+	}
+	return s
+}
+
+// SplitLines is a windows-safe version of [strings.Split](s, "\n")
+// that removes any trailing \r carriage returns from the split lines.
+func SplitLines(s string) []string {
+	ls := strings.Split(s, "\n")
+	for i, l := range ls {
+		ls[i] = TrimCR(l)
+	}
+	return ls
+}
+
+// ByteSplitLines is a windows-safe version of [bytes.Split](s, "\n")
+// that removes any trailing \r carriage returns from the split lines.
+func ByteSplitLines(s []byte) [][]byte {
+	ls := bytes.Split(s, []byte("\n"))
+	for i, l := range ls {
+		ls[i] = ByteTrimCR(l)
+	}
+	return ls
+}
+
+// InsertFirstUnique inserts the given string at the start of the given string slice
+// while keeping the overall length to the given max value. If the item is already on
+// the list, then it is moved to the top and not re-added (unique items only). This is
+// useful for a list of recent items.
+func InsertFirstUnique(strs *[]string, str string, max int) {
+	if *strs == nil {
+		*strs = make([]string, 0, max)
+	}
+	sz := len(*strs)
+	if sz > max {
+		*strs = (*strs)[:max]
+	}
+	for i, s := range *strs {
+		if s == str {
+			if i == 0 {
+				return
+			}
+			copy((*strs)[1:i+1], (*strs)[0:i])
+			(*strs)[0] = str
+			return
+		}
+	}
+	if sz >= max {
+		copy((*strs)[1:max], (*strs)[0:max-1])
+		(*strs)[0] = str
+	} else {
+		*strs = append(*strs, "")
+		if sz > 0 {
+			copy((*strs)[1:], (*strs)[0:sz])
+		}
+		(*strs)[0] = str
+	}
+}

+ 41 - 0
vendor/cogentcore.org/core/base/tiered/tiered.go

@@ -0,0 +1,41 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package tiered provides a type for a tiered set of objects.
+package tiered
+
+// Tiered represents a tiered set of objects of the same type.
+// For example, this is frequently used to represent slices of
+// functions that can be run at the first, normal, or final time.
+type Tiered[T any] struct {
+
+	// First is the object that will be used first,
+	// before [Tiered.Normal] and [Tiered.Final].
+	First T
+
+	// Normal is the object that will be used at the normal
+	// time, after [Tiered.First] and before [Tiered.Final].
+	Normal T
+
+	// Final is the object that will be used last,
+	// after [Tiered.First] and [Tiered.Normal].
+	Final T
+}
+
+// Do calls the given function for each tier,
+// going through first, then normal, then final.
+func (t *Tiered[T]) Do(f func(T)) {
+	f(t.First)
+	f(t.Normal)
+	f(t.Final)
+}
+
+// DoWith calls the given function with each tier of this tiered
+// set and the other given tiered set, going through first, then
+// normal, then final.
+func (t *Tiered[T]) DoWith(other *Tiered[T], f func(*T, *T)) {
+	f(&t.First, &other.First)
+	f(&t.Normal, &other.Normal)
+	f(&t.Final, &other.Final)
+}

+ 17 - 0
vendor/cogentcore.org/core/base/vcs/README.md

@@ -0,0 +1,17 @@
+# vcs
+
+Package vcs provides a more complete version control system (ex: git) interface, building on [Masterminds/vcs](https://github.com/Masterminds/vcs).
+
+It adds the following methods on top of what is available in `Masterminds/vcs`:
+
+* `Files`: files in the repository, and their `FileStatus`
+
+* `Add`, `Move`, `Delete`, `CommitFile`, `RevertFile`: manipulate files in the repository.
+
+* `Log`, `CommitDesc`, `Blame`: details on prior commits
+
+The total interface is now sufficient for complete management of a VCS.
+
+# Current Status
+
+Only `git` and `svn` are currently supported in `vcs`; other repositories supported in vcs include Mercurial (Hg) and Bazaar (Bzr); contributions from users of those VCS's are welcome.

+ 89 - 0
vendor/cogentcore.org/core/base/vcs/enumgen.go

@@ -0,0 +1,89 @@
+// Code generated by "core generate"; DO NOT EDIT.
+
+package vcs
+
+import (
+	"cogentcore.org/core/enums"
+)
+
+var _FileStatusValues = []FileStatus{0, 1, 2, 3, 4, 5, 6}
+
+// FileStatusN is the highest valid value for type FileStatus, plus one.
+const FileStatusN FileStatus = 7
+
+var _FileStatusValueMap = map[string]FileStatus{`Untracked`: 0, `Stored`: 1, `Modified`: 2, `Added`: 3, `Deleted`: 4, `Conflicted`: 5, `Updated`: 6}
+
+var _FileStatusDescMap = map[FileStatus]string{0: `Untracked means file is not under VCS control`, 1: `Stored means file is stored under VCS control, and has not been modified in working copy`, 2: `Modified means file is under VCS control, and has been modified in working copy`, 3: `Added means file has just been added to VCS but is not yet committed`, 4: `Deleted means file has been deleted from VCS`, 5: `Conflicted means file is in conflict -- has not been merged`, 6: `Updated means file has been updated in the remote but not locally`}
+
+var _FileStatusMap = map[FileStatus]string{0: `Untracked`, 1: `Stored`, 2: `Modified`, 3: `Added`, 4: `Deleted`, 5: `Conflicted`, 6: `Updated`}
+
+// String returns the string representation of this FileStatus value.
+func (i FileStatus) String() string { return enums.String(i, _FileStatusMap) }
+
+// SetString sets the FileStatus value from its string representation,
+// and returns an error if the string is invalid.
+func (i *FileStatus) SetString(s string) error {
+	return enums.SetString(i, s, _FileStatusValueMap, "FileStatus")
+}
+
+// Int64 returns the FileStatus value as an int64.
+func (i FileStatus) Int64() int64 { return int64(i) }
+
+// SetInt64 sets the FileStatus value from an int64.
+func (i *FileStatus) SetInt64(in int64) { *i = FileStatus(in) }
+
+// Desc returns the description of the FileStatus value.
+func (i FileStatus) Desc() string { return enums.Desc(i, _FileStatusDescMap) }
+
+// FileStatusValues returns all possible values for the type FileStatus.
+func FileStatusValues() []FileStatus { return _FileStatusValues }
+
+// Values returns all possible values for the type FileStatus.
+func (i FileStatus) Values() []enums.Enum { return enums.Values(_FileStatusValues) }
+
+// MarshalText implements the [encoding.TextMarshaler] interface.
+func (i FileStatus) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
+
+// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
+func (i *FileStatus) UnmarshalText(text []byte) error {
+	return enums.UnmarshalText(i, text, "FileStatus")
+}
+
+var _TypesValues = []Types{0, 1, 2, 3, 4}
+
+// TypesN is the highest valid value for type Types, plus one.
+const TypesN Types = 5
+
+var _TypesValueMap = map[string]Types{`NoVCS`: 0, `novcs`: 0, `Git`: 1, `git`: 1, `Svn`: 2, `svn`: 2, `Bzr`: 3, `bzr`: 3, `Hg`: 4, `hg`: 4}
+
+var _TypesDescMap = map[Types]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``}
+
+var _TypesMap = map[Types]string{0: `NoVCS`, 1: `Git`, 2: `Svn`, 3: `Bzr`, 4: `Hg`}
+
+// String returns the string representation of this Types value.
+func (i Types) String() string { return enums.String(i, _TypesMap) }
+
+// SetString sets the Types value from its string representation,
+// and returns an error if the string is invalid.
+func (i *Types) SetString(s string) error { return enums.SetStringLower(i, s, _TypesValueMap, "Types") }
+
+// Int64 returns the Types value as an int64.
+func (i Types) Int64() int64 { return int64(i) }
+
+// SetInt64 sets the Types value from an int64.
+func (i *Types) SetInt64(in int64) { *i = Types(in) }
+
+// Desc returns the description of the Types value.
+func (i Types) Desc() string { return enums.Desc(i, _TypesDescMap) }
+
+// TypesValues returns all possible values for the type Types.
+func TypesValues() []Types { return _TypesValues }
+
+// Values returns all possible values for the type Types.
+func (i Types) Values() []enums.Enum { return enums.Values(_TypesValues) }
+
+// MarshalText implements the [encoding.TextMarshaler] interface.
+func (i Types) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
+
+// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
+func (i *Types) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Types") }

+ 65 - 0
vendor/cogentcore.org/core/base/vcs/files.go

@@ -0,0 +1,65 @@
+// Copyright (c) 2020, The Cogent Core Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vcs
+
+import (
+	"os"
+	"path/filepath"
+)
+
+// Files is a map used for storing files in a repository along with their status
+type Files map[string]FileStatus
+
+// Status returns the VCS file status associated with given filename,
+// returning Untracked if not found and safe to empty map.
+func (fl *Files) Status(repo Repo, fname string) FileStatus {
+	if *fl == nil || len(*fl) == 0 {
+		return Untracked
+	}
+	st, ok := (*fl)[relPath(repo, fname)]
+	if !ok {
+		return Untracked
+	}
+	return st
+}
+
+// allFiles returns a slice of all the files, recursively, within a given directory
+func allFiles(path string) ([]string, error) {
+	var fnms []string
+	er := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		fnms = append(fnms, path)
+		return nil
+	})
+	return fnms, er
+}
+
+// FileStatus indicates the status of files in the repository
+type FileStatus int32 //enums:enum
+
+const (
+	// Untracked means file is not under VCS control
+	Untracked FileStatus = iota
+
+	// Stored means file is stored under VCS control, and has not been modified in working copy
+	Stored
+
+	// Modified means file is under VCS control, and has been modified in working copy
+	Modified
+
+	// Added means file has just been added to VCS but is not yet committed
+	Added
+
+	// Deleted means file has been deleted from VCS
+	Deleted
+
+	// Conflicted means file is in conflict -- has not been merged
+	Conflicted
+
+	// Updated means file has been updated in the remote but not locally
+	Updated
+)

+ 355 - 0
vendor/cogentcore.org/core/base/vcs/git.go

@@ -0,0 +1,355 @@
+// Copyright (c) 2019, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vcs
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"github.com/Masterminds/vcs"
+)
+
+type GitRepo struct {
+	vcs.GitRepo
+}
+
+func (gr *GitRepo) Type() Types {
+	return Git
+}
+
+func (gr *GitRepo) Files() (Files, error) {
+	f := make(Files)
+
+	out, err := gr.RunFromDir("git", "ls-files", "-o") // other -- untracked
+	if err != nil {
+		return nil, err
+	}
+	scan := bufio.NewScanner(bytes.NewReader(out))
+	for scan.Scan() {
+		fn := filepath.FromSlash(string(scan.Bytes()))
+		f[fn] = Untracked
+	}
+
+	out, err = gr.RunFromDir("git", "ls-files", "-c") // cached = all in repo
+	if err != nil {
+		return nil, err
+	}
+	scan = bufio.NewScanner(bytes.NewReader(out))
+	for scan.Scan() {
+		fn := filepath.FromSlash(string(scan.Bytes()))
+		f[fn] = Stored
+	}
+
+	out, err = gr.RunFromDir("git", "ls-files", "-m") // modified
+	if err != nil {
+		return nil, err
+	}
+	scan = bufio.NewScanner(bytes.NewReader(out))
+	for scan.Scan() {
+		fn := filepath.FromSlash(string(scan.Bytes()))
+		f[fn] = Modified
+	}
+
+	out, err = gr.RunFromDir("git", "ls-files", "-d") // deleted
+	if err != nil {
+		return nil, err
+	}
+	scan = bufio.NewScanner(bytes.NewReader(out))
+	for scan.Scan() {
+		fn := filepath.FromSlash(string(scan.Bytes()))
+		f[fn] = Deleted
+	}
+
+	out, err = gr.RunFromDir("git", "ls-files", "-u") // unmerged
+	if err != nil {
+		return nil, err
+	}
+	scan = bufio.NewScanner(bytes.NewReader(out))
+	for scan.Scan() {
+		fn := filepath.FromSlash(string(scan.Bytes()))
+		f[fn] = Conflicted
+	}
+
+	out, err = gr.RunFromDir("git", "diff", "--name-only", "--diff-filter=A", "HEAD") // deleted
+	if err != nil {
+		return nil, err
+	}
+	scan = bufio.NewScanner(bytes.NewReader(out))
+	for scan.Scan() {
+		fn := filepath.FromSlash(string(scan.Bytes()))
+		f[fn] = Added
+	}
+
+	return f, nil
+}
+
+func (gr *GitRepo) charToStat(stat byte) FileStatus {
+	switch stat {
+	case 'M':
+		return Modified
+	case 'A':
+		return Added
+	case 'D':
+		return Deleted
+	case 'U':
+		return Conflicted
+	case '?', '!':
+		return Untracked
+	}
+	return Untracked
+}
+
+// Status returns status of given file; returns Untracked on any error
+func (gr *GitRepo) Status(fname string) (FileStatus, string) {
+	out, err := gr.RunFromDir("git", "status", "--porcelain", relPath(gr, fname))
+	if err != nil {
+		return Untracked, err.Error()
+	}
+	ostr := string(out)
+	if ostr == "" {
+		return Stored, ""
+	}
+	sf := strings.Fields(ostr)
+	if len(sf) < 2 {
+		return Stored, ostr
+	}
+	stat := sf[0][0]
+	return gr.charToStat(stat), ostr
+}
+
+// Add adds the file to the repo
+func (gr *GitRepo) Add(fname string) error {
+	out, err := gr.RunFromDir("git", "add", relPath(gr, fname))
+	if err != nil {
+		log.Println(string(out))
+		return err
+	}
+	return nil
+}
+
+// Move moves updates the repo with the rename
+func (gr *GitRepo) Move(oldpath, newpath string) error {
+	out, err := gr.RunFromDir("git", "mv", relPath(gr, oldpath), relPath(gr, newpath))
+	if err != nil {
+		log.Println(string(out))
+		return err
+	}
+	out, err = gr.RunFromDir("git", "add", relPath(gr, newpath))
+	if err != nil {
+		log.Println(string(out))
+		return err
+	}
+	return nil
+}
+
+// Delete removes the file from the repo; uses "force" option to ensure deletion
+func (gr *GitRepo) Delete(fname string) error {
+	out, err := gr.RunFromDir("git", "rm", "-f", relPath(gr, fname))
+	if err != nil {
+		log.Println(string(out))
+		fmt.Printf("%s\n", out)
+		return err
+	}
+	return nil
+}
+
+// Delete removes the file from the repo
+func (gr *GitRepo) DeleteRemote(fname string) error {
+	out, err := gr.RunFromDir("git", "rm", "--cached", relPath(gr, fname))
+	if err != nil {
+		log.Println(string(out))
+		return err
+	}
+	return nil
+}
+
+// CommitFile commits single file to repo staging
+func (gr *GitRepo) CommitFile(fname string, message string) error {
+	out, err := gr.RunFromDir("git", "commit", relPath(gr, fname), "-m", message)
+	if err != nil {
+		log.Println(string(out))
+		return err
+	}
+	return nil
+}
+
+// RevertFile reverts a single file to last commit of master
+func (gr *GitRepo) RevertFile(fname string) error {
+	out, err := gr.RunFromDir("git", "checkout", relPath(gr, fname))
+	if err != nil {
+		log.Println(string(out))
+		return err
+	}
+	return nil
+}
+
+// UpdateVersion sets the version of a package currently checked out via Git.
+func (s *GitRepo) UpdateVersion(version string) error {
+	out, err := s.RunFromDir("git", "switch", "--detach", version)
+	if err != nil {
+		return vcs.NewLocalError("Unable to update checked out version", err, string(out))
+	}
+	return nil
+}
+
+// FileContents returns the contents of given file, as a []byte array
+// at given revision specifier. -1, -2 etc also work as universal
+// ways of specifying prior revisions.
+func (gr *GitRepo) FileContents(fname string, rev string) ([]byte, error) {
+	if rev == "" {
+		out, err := os.ReadFile(fname)
+		if err != nil {
+			log.Println(err.Error())
+		}
+		return out, err
+	} else if rev[0] == '-' {
+		rsp, err := strconv.Atoi(rev)
+		if err == nil && rsp < 0 {
+			rev = fmt.Sprintf("HEAD~%d:", -rsp)
+		}
+	} else {
+		rev += ":"
+	}
+	fspec := rev + relPath(gr, fname)
+	out, err := gr.RunFromDir("git", "show", fspec)
+	if err != nil {
+		log.Println(string(out))
+		return nil, err
+	}
+	return out, nil
+}
+
+// fieldsThroughDelim gets the concatenated byte through to point where
+// field ends with given delimiter, starting at given index
+func fieldsThroughDelim(flds [][]byte, delim byte, idx int) (int, string) {
+	ln := len(flds)
+	for i := idx; i < ln; i++ {
+		fld := flds[i]
+		fsz := len(fld)
+		if fld[fsz-1] == delim {
+			str := string(bytes.Join(flds[idx:i+1], []byte(" ")))
+			return i + 1, str[:len(str)-1]
+		}
+	}
+	return ln, string(bytes.Join(flds[idx:ln], []byte(" ")))
+}
+
+// Log returns the log history of commits for given filename
+// (or all files if empty).  If since is non-empty, it should be
+// a date-like expression that the VCS will understand, such as
+// 1/1/2020, yesterday, last year, etc
+func (gr *GitRepo) Log(fname string, since string) (Log, error) {
+	args := []string{"log", "--all"}
+	if since != "" {
+		args = append(args, `--since="`+since+`"`)
+	}
+	args = append(args, `--pretty=format:%h %ad} %an} %ae} %s`)
+	if fname != "" {
+		args = append(args, fname)
+	}
+	out, err := gr.RunFromDir("git", args...)
+	if err != nil {
+		return nil, err
+	}
+	var lg Log
+	scan := bufio.NewScanner(bytes.NewReader(out))
+	for scan.Scan() {
+		ln := scan.Bytes()
+		flds := bytes.Fields(ln)
+		if len(flds) < 4 {
+			continue
+		}
+		rev := string(flds[0])
+		ni, date := fieldsThroughDelim(flds, '}', 1)
+		ni, author := fieldsThroughDelim(flds, '}', ni)
+		ni, email := fieldsThroughDelim(flds, '}', ni)
+		msg := string(bytes.Join(flds[ni:], []byte(" ")))
+		lg.Add(rev, date, author, email, msg)
+	}
+	return lg, nil
+}
+
+// CommitDesc returns the full textual description of the given commit,
+// if rev is empty, defaults to current HEAD, -1, -2 etc also work as universal
+// ways of specifying prior revisions.
+// Optionally includes diffs for the changes (otherwise just a list of files
+// with modification status).
+func (gr *GitRepo) CommitDesc(rev string, diffs bool) ([]byte, error) {
+	if rev == "" {
+		rev = "HEAD"
+	} else if rev[0] == '-' {
+		rsp, err := strconv.Atoi(rev)
+		if err == nil && rsp < 0 {
+			rev = fmt.Sprintf("HEAD~%d", -rsp)
+		}
+	}
+	var out []byte
+	var err error
+	if diffs {
+		out, err = gr.RunFromDir("git", "show", rev)
+	} else {
+		out, err = gr.RunFromDir("git", "show", "--name-status", rev)
+	}
+	if err != nil {
+		log.Println(string(out))
+		return nil, err
+	}
+	return out, nil
+}
+
+// FilesChanged returns the list of files changed and their statuses,
+// between two revisions.
+// If revA is empty, defaults to current HEAD; revB defaults to HEAD-1.
+// -1, -2 etc also work as universal ways of specifying prior revisions.
+// Optionally includes diffs for the changes.
+func (gr *GitRepo) FilesChanged(revA, revB string, diffs bool) ([]byte, error) {
+	if revA == "" {
+		revA = "HEAD"
+	} else if revA[0] == '-' {
+		rsp, err := strconv.Atoi(revA)
+		if err == nil && rsp < 0 {
+			revA = fmt.Sprintf("HEAD~%d", -rsp)
+		}
+	}
+	if revB != "" && revB[0] == '-' {
+		rsp, err := strconv.Atoi(revB)
+		if err == nil && rsp < 0 {
+			revB = fmt.Sprintf("HEAD~%d", -rsp)
+		}
+	}
+	var out []byte
+	var err error
+	if diffs {
+		out, err = gr.RunFromDir("git", "diff", "-u", revA, revB)
+	} else {
+		if revB == "" {
+			out, err = gr.RunFromDir("git", "diff", "--name-status", revA)
+		} else {
+			out, err = gr.RunFromDir("git", "diff", "--name-status", revA, revB)
+		}
+	}
+	if err != nil {
+		log.Println(string(out))
+		return nil, err
+	}
+	return out, nil
+}
+
+// Blame returns an annotated report about the file, showing which revision last
+// modified each line.
+func (gr *GitRepo) Blame(fname string) ([]byte, error) {
+	out, err := gr.RunFromDir("git", "blame", fname)
+	if err != nil {
+		log.Println(string(out))
+		return nil, err
+	}
+	return out, nil
+}

+ 34 - 0
vendor/cogentcore.org/core/base/vcs/log.go

@@ -0,0 +1,34 @@
+// Copyright (c) 2018, The Cogent Core Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vcs
+
+// Commit is one VCS commit entry, as returned in a [Log].
+type Commit struct {
+
+	// revision number / hash code / unique id
+	Rev string
+
+	// date (author's time) when committed
+	Date string
+
+	// author's name
+	Author string
+
+	// author's email
+	Email string
+
+	// message / subject line for commit
+	Message string `width:"100"`
+}
+
+// Log is a listing of commits.
+type Log []*Commit
+
+// Add adds a new [Commit] to the [Log], returning the [Commit].
+func (lg *Log) Add(rev, date, author, email, message string) *Commit {
+	cm := &Commit{Rev: rev, Date: date, Author: author, Email: email, Message: message}
+	*lg = append(*lg, cm)
+	return cm
+}

+ 279 - 0
vendor/cogentcore.org/core/base/vcs/svn.go

@@ -0,0 +1,279 @@
+// Copyright (c) 2019, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vcs
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"log"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"github.com/Masterminds/vcs"
+)
+
+type SvnRepo struct {
+	vcs.SvnRepo
+}
+
+func (gr *SvnRepo) Type() Types {
+	return Svn
+}
+
+func (gr *SvnRepo) CharToStat(stat byte) FileStatus {
+	switch stat {
+	case 'M', 'R':
+		return Modified
+	case 'A':
+		return Added
+	case 'D', '!':
+		return Deleted
+	case 'C':
+		return Conflicted
+	case '?', 'I':
+		return Untracked
+	case '*':
+		return Updated
+	default:
+		return Stored
+	}
+}
+
+func (gr *SvnRepo) Files() (Files, error) {
+	f := make(Files)
+
+	lpath := gr.LocalPath()
+	allfs, err := allFiles(lpath) // much faster than svn list --recursive
+	if err != nil {
+		return nil, err
+	}
+	for _, fn := range allfs {
+		rpath, _ := filepath.Rel(lpath, fn)
+		f[rpath] = Stored
+	}
+
+	out, err := gr.RunFromDir("svn", "status", "-u")
+	if err != nil {
+		return nil, err
+	}
+	scan := bufio.NewScanner(bytes.NewReader(out))
+	for scan.Scan() {
+		ln := string(scan.Bytes())
+		flds := strings.Fields(ln)
+		if len(flds) < 2 {
+			continue // shouldn't happend
+		}
+		stat := flds[0][0]
+		fn := flds[len(flds)-1]
+		f[fn] = gr.CharToStat(stat)
+	}
+	return f, nil
+}
+
+// Status returns status of given file; returns Untracked on any error
+func (gr *SvnRepo) Status(fname string) (FileStatus, string) {
+	out, err := gr.RunFromDir("svn", "status", relPath(gr, fname))
+	if err != nil {
+		return Untracked, err.Error()
+	}
+	ostr := string(out)
+	if ostr == "" {
+		return Stored, ""
+	}
+	sf := strings.Fields(ostr)
+	if len(sf) < 2 {
+		return Stored, ostr
+	}
+	stat := sf[0][0]
+	return gr.CharToStat(stat), ostr
+}
+
+// Add adds the file to the repo
+func (gr *SvnRepo) Add(fname string) error {
+	oscmd := exec.Command("svn", "add", relPath(gr, fname))
+	stdoutStderr, err := oscmd.CombinedOutput()
+	if err != nil {
+		log.Println(string(stdoutStderr))
+		return err
+	}
+	return nil
+}
+
+// Move moves updates the repo with the rename
+func (gr *SvnRepo) Move(oldpath, newpath string) error {
+	oscmd := exec.Command("svn", "mv", oldpath, newpath)
+	stdoutStderr, err := oscmd.CombinedOutput()
+	if err != nil {
+		log.Println(string(stdoutStderr))
+		return err
+	}
+	return nil
+}
+
+// Delete removes the file from the repo -- uses "force" option to ensure deletion
+func (gr *SvnRepo) Delete(fname string) error {
+	oscmd := exec.Command("svn", "rm", "-f", relPath(gr, fname))
+	stdoutStderr, err := oscmd.CombinedOutput()
+	if err != nil {
+		log.Println(string(stdoutStderr))
+		return err
+	}
+	return nil
+}
+
+// DeleteRemote removes the file from the repo, but keeps local copy
+func (gr *SvnRepo) DeleteRemote(fname string) error {
+	oscmd := exec.Command("svn", "delete", "--keep-local", relPath(gr, fname))
+	stdoutStderr, err := oscmd.CombinedOutput()
+	if err != nil {
+		log.Println(string(stdoutStderr))
+		return err
+	}
+	return nil
+}
+
+// CommitFile commits single file to repo staging
+func (gr *SvnRepo) CommitFile(fname string, message string) error {
+	oscmd := exec.Command("svn", "commit", relPath(gr, fname), "-m", message)
+	stdoutStderr, err := oscmd.CombinedOutput()
+	if err != nil {
+		log.Println(string(stdoutStderr))
+		return err
+	}
+	return nil
+}
+
+// RevertFile reverts a single file to last commit of master
+func (gr *SvnRepo) RevertFile(fname string) error {
+	oscmd := exec.Command("svn", "revert", relPath(gr, fname))
+	stdoutStderr, err := oscmd.CombinedOutput()
+	if err != nil {
+		log.Println(string(stdoutStderr))
+		return err
+	}
+	return nil
+}
+
+// FileContents returns the contents of given file, as a []byte array
+// at given revision specifier (if empty, defaults to current HEAD).
+// -1, -2 etc also work as universal ways of specifying prior revisions.
+func (gr *SvnRepo) FileContents(fname string, rev string) ([]byte, error) {
+	if rev == "" {
+		rev = "HEAD"
+		// } else if rev[0] == '-' { // no support at this point..
+		// 	rsp, err := strconv.Atoi(rev)
+		// 	if err == nil && rsp < 0 {
+		// 		rev = fmt.Sprintf("HEAD~%d:", rsp)
+		// 	}
+	}
+	out, err := gr.RunFromDir("svn", "-r", "rev", "cat", relPath(gr, fname))
+	if err != nil {
+		log.Println(string(out))
+		return nil, err
+	}
+	return out, nil
+}
+
+// Log returns the log history of commits for given filename
+// (or all files if empty).  If since is non-empty, it is the
+// maximum number of entries to return (a number).
+func (gr *SvnRepo) Log(fname string, since string) (Log, error) {
+	// todo: parse -- requires parsing over multiple lines..
+	args := []string{"log"}
+	if since != "" {
+		args = append(args, `--limit=`+since)
+	}
+	if fname != "" {
+		args = append(args, fname)
+	}
+	out, err := gr.RunFromDir("svn", args...)
+	if err != nil {
+		return nil, err
+	}
+	var lg Log
+	rev := ""
+	date := ""
+	author := ""
+	email := ""
+	msg := ""
+	newStart := false
+	scan := bufio.NewScanner(bytes.NewReader(out))
+	for scan.Scan() {
+		ln := scan.Bytes()
+		if string(ln[:10]) == "----------" {
+			if rev != "" {
+				lg.Add(rev, date, author, email, msg)
+			}
+			newStart = true
+			msg = ""
+			continue
+		}
+		if newStart {
+			flds := bytes.Split(ln, []byte("|"))
+			if len(flds) < 4 {
+				continue
+			}
+			rev = strings.TrimSpace(string(flds[0]))
+			author = strings.TrimSpace(string(flds[1]))
+			date = strings.TrimSpace(string(flds[2]))
+			msg = ""
+			newStart = false
+		} else {
+			nosp := bytes.TrimSpace(ln)
+			if msg == "" && len(nosp) == 0 {
+				continue
+			}
+			msg += string(ln) + "\n"
+		}
+	}
+	return lg, nil
+}
+
+// CommitDesc returns the full textual description of the given commit,
+// if rev is empty, defaults to current HEAD, -1, -2 etc also work as universal
+// ways of specifying prior revisions.
+// Optionally includes diffs for the changes (otherwise just a list of files
+// with modification status).
+func (gr *SvnRepo) CommitDesc(rev string, diffs bool) ([]byte, error) {
+	if rev == "" {
+		rev = "HEAD"
+	} else if rev[0] == '-' {
+		rsp, err := strconv.Atoi(rev)
+		if err == nil && rsp < 0 {
+			rev = fmt.Sprintf("HEAD~%d", -rsp)
+		}
+	}
+	var out []byte
+	var err error
+	if diffs {
+		out, err = gr.RunFromDir("svn", "log", "-v", "--diff", "-r", rev)
+	} else {
+		out, err = gr.RunFromDir("svn", "log", "-v", "-r", rev)
+	}
+	if err != nil {
+		log.Println(string(out))
+		return nil, err
+	}
+
+	return out, err
+}
+
+func (gr *SvnRepo) FilesChanged(revA, revB string, diffs bool) ([]byte, error) {
+	return nil, nil // todo:
+}
+
+// Blame returns an annotated report about the file, showing which revision last
+// modified each line.
+func (gr *SvnRepo) Blame(fname string) ([]byte, error) {
+	out, err := gr.RunFromDir("svn", "blame", fname)
+	if err != nil {
+		log.Println(string(out))
+		return nil, err
+	}
+	return out, nil
+}

+ 139 - 0
vendor/cogentcore.org/core/base/vcs/vcs.go

@@ -0,0 +1,139 @@
+// Copyright (c) 2018, The Cogent Core Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package vcs provides a more complete version control system (ex: git)
+// interface, building on https://github.com/Masterminds/vcs.
+package vcs
+
+//go:generate core generate
+
+import (
+	"fmt"
+	"path/filepath"
+
+	"cogentcore.org/core/base/fsx"
+	"github.com/Masterminds/vcs"
+)
+
+type Types int32 //enums:enum -accept-lower
+
+const (
+	NoVCS Types = iota
+	Git
+	Svn
+	Bzr
+	Hg
+)
+
+// Repo provides an interface extending [vcs.Repo]
+// (https://github.com/Masterminds/vcs)
+// with support for file status information and operations.
+type Repo interface {
+	vcs.Repo
+
+	// Type returns the type of repo we are using
+	Type() Types
+
+	// Files returns a map of the current files and their status.
+	Files() (Files, error)
+
+	// Status returns status of given file -- returns Untracked and error
+	// message on any error. FileStatus is a summary status category,
+	// and string return value is more detailed status information formatted
+	// according to standard conventions of given VCS.
+	Status(fname string) (FileStatus, string)
+
+	// Add adds the file to the repo
+	Add(fname string) error
+
+	// Move moves the file using VCS command to keep it updated
+	Move(oldpath, newpath string) error
+
+	// Delete removes the file from the repo and working copy.
+	// Uses "force" option to ensure deletion.
+	Delete(fname string) error
+
+	// DeleteRemote removes the file from the repo but keeps the local file itself
+	DeleteRemote(fname string) error
+
+	// CommitFile commits a single file
+	CommitFile(fname string, message string) error
+
+	// RevertFile reverts a single file to the version that it was last in VCS,
+	// losing any local changes (destructive!)
+	RevertFile(fname string) error
+
+	// FileContents returns the contents of given file, as a []byte array
+	// at given revision specifier (if empty, defaults to current HEAD).
+	// -1, -2 etc also work as universal ways of specifying prior revisions.
+	FileContents(fname string, rev string) ([]byte, error)
+
+	// Log returns the log history of commits for given filename
+	// (or all files if empty).  If since is non-empty, it should be
+	// a date-like expression that the VCS will understand, such as
+	// 1/1/2020, yesterday, last year, etc.  SVN only understands a
+	// number as a maximum number of items to return.
+	Log(fname string, since string) (Log, error)
+
+	// CommitDesc returns the full textual description of the given commit,
+	// if rev is empty, defaults to current HEAD, -1, -2 etc also work as universal
+	// ways of specifying prior revisions.
+	// Optionally includes diffs for the changes (otherwise just a list of files
+	// with modification status).
+	CommitDesc(rev string, diffs bool) ([]byte, error)
+
+	// FilesChanged returns the list of files changed and their statuses,
+	// between two revisions.
+	// If revA is empty, defaults to current HEAD; revB defaults to HEAD-1.
+	// -1, -2 etc also work as universal ways of specifying prior revisions.
+	// Optionally includes diffs for the changes.
+	FilesChanged(revA, revB string, diffs bool) ([]byte, error)
+
+	// Blame returns an annotated report about the file, showing which revision last
+	// modified each line.
+	Blame(fname string) ([]byte, error)
+}
+
+func NewRepo(remote, local string) (Repo, error) {
+	repo, err := vcs.NewRepo(remote, local)
+	if err == nil {
+		switch repo.Vcs() {
+		case vcs.Git:
+			r := &GitRepo{}
+			r.GitRepo = *(repo.(*vcs.GitRepo))
+			return r, err
+		case vcs.Svn:
+			r := &SvnRepo{}
+			r.SvnRepo = *(repo.(*vcs.SvnRepo))
+			return r, err
+		case vcs.Hg:
+			err = fmt.Errorf("hg version control not yet supported")
+		case vcs.Bzr:
+			err = fmt.Errorf("bzr version control not yet supported")
+		}
+	}
+	return nil, err
+}
+
+// DetectRepo attempts to detect the presence of a repository at the given
+// directory path -- returns type of repository if found, else NoVCS.
+// Very quickly just looks for signature file name:
+// .git for git
+// .svn for svn -- but note that this will find any subdir in svn rep.o
+func DetectRepo(path string) Types {
+	if fsx.HasFile(path, ".git") {
+		return Git
+	}
+	if fsx.HasFile(path, ".svn") {
+		return Svn
+	}
+	// todo: rest later..
+	return NoVCS
+}
+
+// relPath return the path relative to the repository LocalPath()
+func relPath(repo Repo, path string) string {
+	relpath, _ := filepath.Rel(repo.LocalPath(), path)
+	return relpath
+}

+ 22 - 0
vendor/cogentcore.org/core/colors/README.md

@@ -0,0 +1,22 @@
+# colors
+
+Package colors provides named colors, utilities for manipulating colors, and Material Design 3 color schemes, palettes, and keys in Go.
+
+## image.Image as a universal color
+
+The Go standard library defines the `image.Image` interface, which returns a color at a given x,y coordinate via the `At(x,y) color.Color` method.  This provides the most general way of specifying a color, encompassing everything from a single solid color to a pattern to a gradient to an actual image.  Thus, `image.Image` is used to specify colors in most places in the Cogent Core system.
+
+* `image.Uniform` always returns a single uniform color, ignoring the coordinates. Use the `colors.Uniform` helper function to create a new uniform color (it just returns `image.NewUniform(c)`).
+
+* `gradient.Gradient` (from `colors/gradient`) is an `image.Image` interface that specifies an SVG-compatible color gradient using Stops to define specific points of color, with the specific color at each point as a proportional blend between the two nearest stops.  There are `gradient.Linear` and `gradient.Radial` subtypes.
+
+## gradient.Applier
+
+We often need to apply opacity transformations to colors, which have the effect of darkening or lightening colors, for example indicating different states, such as when a Button is hovered vs. clicked.  To do this efficiently and flexibly for the different types of `image.Image` colors, the `gradient` package defines an `ApplyFunc` function that takes a color and returns a modified color.
+
+There is an `gradient.Applier` that implements the `image.Image` interface (so it can be used for any such color), which applies the color transformation via the `At(x,y)` method, so it automatically transforms the color output of any source image (where the `image.Image` is an embedded field) of the struct type.
+
+Finally, there is an `ApplyOpacity` method that is extra efficient for the Uniform and Gradient cases, directly transforming the uniform color or color of the Stops in the gradient, to avoid the extra ApplyFunc call (which is used for the general case of an actual image).
+
+The reason this Apply logic is in `gradient` is so it can manage the Gradient case.
+

+ 54 - 0
vendor/cogentcore.org/core/colors/accent.go

@@ -0,0 +1,54 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package colors
+
+import (
+	"image/color"
+
+	"cogentcore.org/core/colors/cam/hct"
+	"cogentcore.org/core/colors/matcolor"
+)
+
+// Based on matcolor/accent.go
+
+// ToBase returns the base accent color for the given color
+// based on the current scheme (light or dark), which is
+// typically used for high emphasis objects or text.
+func ToBase(c color.Color) color.RGBA {
+	if matcolor.SchemeIsDark {
+		return hct.FromColor(c).WithTone(80).AsRGBA()
+	}
+	return hct.FromColor(c).WithTone(40).AsRGBA()
+}
+
+// ToOn returns the accent color for the given color
+// that should be placed on top of [ToBase] based on
+// the current scheme (light or dark).
+func ToOn(c color.Color) color.RGBA {
+	if matcolor.SchemeIsDark {
+		return hct.FromColor(c).WithTone(20).AsRGBA()
+	}
+	return hct.FromColor(c).WithTone(100).AsRGBA()
+}
+
+// ToContainer returns the container accent color for the given color
+// based on the current scheme (light or dark), which is
+// typically used for lower emphasis content.
+func ToContainer(c color.Color) color.RGBA {
+	if matcolor.SchemeIsDark {
+		return hct.FromColor(c).WithTone(30).AsRGBA()
+	}
+	return hct.FromColor(c).WithTone(90).AsRGBA()
+}
+
+// ToOnContainer returns the accent color for the given color
+// that should be placed on top of [ToContainer] based on
+// the current scheme (light or dark).
+func ToOnContainer(c color.Color) color.RGBA {
+	if matcolor.SchemeIsDark {
+		return hct.FromColor(c).WithTone(90).AsRGBA()
+	}
+	return hct.FromColor(c).WithTone(10).AsRGBA()
+}

+ 85 - 0
vendor/cogentcore.org/core/colors/blend.go

@@ -0,0 +1,85 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package colors
+
+import (
+	"image/color"
+	"log/slog"
+
+	"cogentcore.org/core/colors/cam/cam16"
+	"cogentcore.org/core/colors/cam/hct"
+	"cogentcore.org/core/math32"
+)
+
+// BlendTypes are different algorithms (colorspaces) to use for blending
+// the color stop values in generating the gradients.
+type BlendTypes int32 //enums:enum
+
+const (
+	// HCT uses the hue, chroma, and tone space and generally produces the best results,
+	// but at a slight performance cost.
+	HCT BlendTypes = iota
+
+	// RGB uses raw RGB space, which is the standard space that most other programs use.
+	// It produces decent results with maximum performance.
+	RGB
+
+	// CAM16 is an alternative colorspace, similar to HCT, but not quite as good.
+	CAM16
+)
+
+// Blend returns a color that is the given proportion between the first
+// and second color. For example, 0.1 indicates to blend 10% of the first
+// color and 90% of the second. Blending is done using the given blending
+// algorithm.
+func Blend(bt BlendTypes, p float32, x, y color.Color) color.RGBA {
+	switch bt {
+	case HCT:
+		return hct.Blend(p, x, y)
+	case RGB:
+		return BlendRGB(p, x, y)
+	case CAM16:
+		return cam16.Blend(p, x, y)
+	}
+	slog.Error("got unexpected blend type", "type", bt)
+	return color.RGBA{}
+}
+
+// BlendRGB returns a color that is the given proportion between the first
+// and second color in RGB colorspace. For example, 0.1 indicates to blend
+// 10% of the first color and 90% of the second. Blending is done directly
+// on non-premultiplied
+// RGB values, and a correctly premultiplied color is returned.
+func BlendRGB(pct float32, x, y color.Color) color.RGBA {
+	fx := NRGBAF32Model.Convert(x).(NRGBAF32)
+	fy := NRGBAF32Model.Convert(y).(NRGBAF32)
+	pct = math32.Clamp(pct, 0, 100.0)
+	px := pct / 100
+	py := 1.0 - px
+	fx.R = px*fx.R + py*fy.R
+	fx.G = px*fx.G + py*fy.G
+	fx.B = px*fx.B + py*fy.B
+	fx.A = px*fx.A + py*fy.A
+	return AsRGBA(fx)
+}
+
+// m is the maximum color value returned by [image.Color.RGBA]
+const m = 1<<16 - 1
+
+// AlphaBlend blends the two colors, handling alpha blending correctly.
+// The source color is figuratively placed "on top of" the destination color.
+func AlphaBlend(dst, src color.Color) color.RGBA {
+	res := color.RGBA{}
+
+	dr, dg, db, da := dst.RGBA()
+	sr, sg, sb, sa := src.RGBA()
+	a := (m - sa)
+
+	res.R = uint8((uint32(dr)*a/m + sr) >> 8)
+	res.G = uint8((uint32(dg)*a/m + sg) >> 8)
+	res.B = uint8((uint32(db)*a/m + sb) >> 8)
+	res.A = uint8((uint32(da)*a/m + sa) >> 8)
+	return res
+}

+ 229 - 0
vendor/cogentcore.org/core/colors/cam/cam16/cam16.go

@@ -0,0 +1,229 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Adapted from https://github.com/material-foundation/material-color-utilities
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cam16
+
+import (
+	"image/color"
+
+	"cogentcore.org/core/base/num"
+	"cogentcore.org/core/colors/cam/cie"
+	"cogentcore.org/core/math32"
+)
+
+// CAM represents a point in the cam16 color model along 6 dimensions
+// representing the perceived hue, colorfulness, and brightness,
+// similar to HSL but much more well-calibrated to actual human subjective judgments.
+type CAM struct {
+
+	// hue (h) is the spectral identity of the color (red, green, blue etc) in degrees (0-360)
+	Hue float32
+
+	// chroma (C) is the colorfulness or saturation of the color -- greyscale colors have no chroma, and fully saturated ones have high chroma
+	Chroma float32
+
+	// colorfulness (M) is the absolute chromatic intensity
+	Colorfulness float32
+
+	// saturation (s) is the colorfulness relative to brightness
+	Saturation float32
+
+	// brightness (Q) is the apparent amount of light from the color, which is not a simple function of actual light energy emitted
+	Brightness float32
+
+	// lightness (J) is the brightness relative to a reference white, which varies as a function of chroma and hue
+	Lightness float32
+}
+
+// RGBA implements the color.Color interface.
+func (cam *CAM) RGBA() (r, g, b, a uint32) {
+	x, y, z := cam.XYZ()
+	rf, gf, bf := cie.XYZ100ToSRGB(x, y, z)
+	return cie.SRGBFloatToUint32(rf, gf, bf, 1)
+}
+
+// AsRGBA returns the color as a [color.RGBA].
+func (cam *CAM) AsRGBA() color.RGBA {
+	x, y, z := cam.XYZ()
+	rf, gf, bf := cie.XYZ100ToSRGB(x, y, z)
+	r, g, b, a := cie.SRGBFloatToUint8(rf, gf, bf, 1)
+	return color.RGBA{r, g, b, a}
+}
+
+// UCS returns the CAM16-UCS components based on the the CAM values
+func (cam *CAM) UCS() (j, m, a, b float32) {
+	j = (1 + 100*0.007) * cam.Lightness / (1 + 0.007*cam.Lightness)
+	m = math32.Log(1+0.0228*cam.Colorfulness) / 0.0228
+	hr := math32.DegToRad(cam.Hue)
+	a = m * math32.Cos(hr)
+	b = m * math32.Sin(hr)
+	return
+}
+
+// FromUCS returns CAM values from the given CAM16-UCS coordinates
+// (jstar, astar, and bstar), under standard viewing conditions
+func FromUCS(j, a, b float32) *CAM {
+	return FromUCSView(j, a, b, NewStdView())
+}
+
+// FromUCS returns CAM values from the given CAM16-UCS coordinates
+// (jstar, astar, and bstar), using the given viewing conditions
+func FromUCSView(j, a, b float32, vw *View) *CAM {
+	m := math32.Sqrt(a*a + b*b)
+	M := (math32.Exp(m*0.0228) - 1) / 0.0228
+	c := M / vw.FLRoot
+	h := math32.RadToDeg(math32.Atan2(b, a))
+	if h < 0 {
+		h += 360
+	}
+	j /= 1 - (j-100)*0.007
+
+	return FromJCHView(j, c, h, vw)
+}
+
+// FromJCH returns CAM values from the given lightness (j), chroma (c),
+// and hue (h) values under standard viewing condition
+func FromJCH(j, c, h float32) *CAM {
+	return FromJCHView(j, c, h, NewStdView())
+}
+
+// FromJCHView returns CAM values from the given lightness (j), chroma (c),
+// and hue (h) values under the given viewing conditions
+func FromJCHView(j, c, h float32, vw *View) *CAM {
+	cam := &CAM{Lightness: j, Chroma: c, Hue: h}
+	cam.Brightness = (4 / vw.C) *
+		math32.Sqrt(cam.Lightness/100) *
+		(vw.AW + 4) *
+		(vw.FLRoot)
+	cam.Colorfulness = cam.Chroma * vw.FLRoot
+	alpha := cam.Chroma / math32.Sqrt(cam.Lightness/100)
+	cam.Saturation = 50 * math32.Sqrt((alpha*vw.C)/(vw.AW+4))
+	return cam
+}
+
+// FromSRGB returns CAM values from given SRGB color coordinates,
+// under standard viewing conditions.  The RGB value range is 0-1,
+// and RGB values have gamma correction.
+func FromSRGB(r, g, b float32) *CAM {
+	return FromXYZ(cie.SRGBToXYZ100(r, g, b))
+}
+
+// FromXYZ returns CAM values from given XYZ color coordinate,
+// under standard viewing conditions
+func FromXYZ(x, y, z float32) *CAM {
+	return FromXYZView(x, y, z, NewStdView())
+}
+
+// FromXYZView returns CAM values from given XYZ color coordinate,
+// under given viewing conditions.  Requires 100-base XYZ coordinates.
+func FromXYZView(x, y, z float32, vw *View) *CAM {
+	l, m, s := XYZToLMS(x, y, z)
+	redVgreen, yellowVblue, grey, greyNorm := LMSToOps(l, m, s, vw)
+
+	hue := SanitizeDegrees(math32.RadToDeg(math32.Atan2(yellowVblue, redVgreen)))
+	// achromatic response to color
+	ac := grey * vw.NBB
+
+	// CAM16 lightness and brightness
+	J := 100 * math32.Pow(ac/vw.AW, vw.C*vw.Z)
+	Q := (4 / vw.C) * math32.Sqrt(J/100) * (vw.AW + 4) * (vw.FLRoot)
+
+	huePrime := hue
+	if hue < 20.14 {
+		huePrime += 360
+	}
+	eHue := 0.25 * (math32.Cos(huePrime*math32.Pi/180+2) + 3.8)
+	p1 := 50000 / 13 * eHue * vw.NC * vw.NCB
+	t := p1 * math32.Sqrt(redVgreen*redVgreen+yellowVblue*yellowVblue) / (greyNorm + 0.305)
+	alpha := math32.Pow(t, 0.9) * math32.Pow(1.64-math32.Pow(0.29, vw.BgYToWhiteY), 0.73)
+
+	// CAM16 chroma, colorfulness, chroma
+	C := alpha * math32.Sqrt(J/100)
+	M := C * vw.FLRoot
+	s = 50 * math32.Sqrt((alpha*vw.C)/(vw.AW+4))
+	return &CAM{Hue: hue, Chroma: C, Colorfulness: M, Saturation: s, Brightness: Q, Lightness: J}
+}
+
+// XYZ returns the CAM color as XYZ coordinates
+// under standard viewing conditions.
+// Returns 100-base XYZ coordinates.
+func (cam *CAM) XYZ() (x, y, z float32) {
+	return cam.XYZView(NewStdView())
+}
+
+// XYZ returns the CAM color as XYZ coordinates
+// under the given viewing conditions.
+// Returns 100-base XYZ coordinates.
+func (cam *CAM) XYZView(vw *View) (x, y, z float32) {
+	alpha := float32(0)
+	if cam.Chroma != 0 || cam.Lightness != 0 {
+		alpha = cam.Chroma / math32.Sqrt(cam.Lightness/100)
+	}
+
+	t := math32.Pow(
+		alpha/
+			math32.Pow(
+				1.64-
+					math32.Pow(0.29, vw.BgYToWhiteY),
+				0.73),
+		1.0/0.9)
+
+	hRad := math32.DegToRad(cam.Hue)
+
+	eHue := 0.25 * (math32.Cos(hRad+2) + 3.8)
+	ac := vw.AW * math32.Pow(cam.Lightness/100, 1/vw.C/vw.Z)
+	p1 := eHue * (50000 / 13) * vw.NC * vw.NCB
+
+	p2 := ac / vw.NBB
+
+	hSin := math32.Sin(hRad)
+	hCos := math32.Cos(hRad)
+
+	gamma := 23 *
+		(p2 + 0.305) *
+		t /
+		(23*p1 + 11*t*hCos + 108*t*hSin)
+	a := gamma * hCos
+	b := gamma * hSin
+	rA := (460*p2 + 451*a + 288*b) / 1403
+	gA := (460*p2 - 891*a - 261*b) / 1403
+	bA := (460*p2 - 220*a - 6300*b) / 1403
+
+	rCBase := max(0, (27.13*num.Abs(rA))/(400-num.Abs(rA)))
+	// TODO(kai): their sign function returns 0 for 0, but we return 1, so this might break
+	rC := math32.Sign(rA) *
+		(100 / vw.FL) *
+		math32.Pow(rCBase, 1/0.42)
+	gCBase := max(0, (27.13*num.Abs(gA))/(400-num.Abs(gA)))
+	gC := math32.Sign(gA) *
+		(100 / vw.FL) *
+		math32.Pow(gCBase, 1/0.42)
+	bCBase := max(0, (27.13*num.Abs(bA))/(400-num.Abs(bA)))
+	bC := math32.Sign(bA) *
+		(100 / vw.FL) *
+		math32.Pow(bCBase, 1/0.42)
+	rF := rC / vw.RGBD.X
+	gF := gC / vw.RGBD.Y
+	bF := bC / vw.RGBD.Z
+
+	x = 1.86206786*rF - 1.01125463*gF + 0.14918677*bF
+	y = 0.38752654*rF + 0.62144744*gF - 0.00897398*bF
+	z = -0.01584150*rF - 0.03412294*gF + 1.04996444*bF
+	return
+}

+ 74 - 0
vendor/cogentcore.org/core/colors/cam/cam16/lms16.go

@@ -0,0 +1,74 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cam16
+
+import (
+	"cogentcore.org/core/math32"
+)
+
+// XYZToLMS converts XYZ to Long, Medium, Short cone-based responses,
+// using the CAT16 transform from CIECAM16 color appearance model
+// (LiLiWangEtAl17)
+func XYZToLMS(x, y, z float32) (l, m, s float32) {
+	l = x*0.401288 + y*0.650173 + z*-0.051461
+	m = x*-0.250268 + y*1.204414 + z*0.045854
+	s = x*-0.002079 + y*0.048952 + z*0.953127
+	return
+}
+
+// LMSToXYZ converts Long, Medium, Short cone-based responses to XYZ
+// using the CAT16 transform from CIECAM16 color appearance model
+// (LiLiWangEtAl17)
+func LMSToXYZ(l, m, s float32) (x, y, z float32) {
+	x = l*1.86206787 + m*-1.0112563 + s*0.14918667
+	y = l*0.38752654 + m*0.62144744 + s*-0.00897398
+	z = l*-0.01584150 + m*-0.03412294 + s*1.04996444
+	return
+}
+
+// LuminanceAdaptComp performs luminance adaptation
+// and response compression according to the CAM16 model,
+// on one component, using equations from HuntLiLuo03
+// d = discount factor
+// fl = luminance adaptation factor
+func LuminanceAdaptComp(v, d, fl float32) float32 {
+	vd := v * d
+	f := math32.Pow((fl*math32.Abs(vd))/100, 0.42)
+	return (math32.Sign(vd) * 400 * f) / (f + 27.13)
+}
+
+func InverseChromaticAdapt(adapted float32) float32 {
+	adaptedAbs := math32.Abs(adapted)
+	base := math32.Max(0, 27.13*adaptedAbs/(400.0-adaptedAbs))
+	return math32.Sign(adapted) * math32.Pow(base, 1.0/0.42)
+}
+
+// LuminanceAdapt performs luminance adaptation
+// and response compression according to the CAM16 model,
+// on given r,g,b components, using equations from HuntLiLuo03
+// and parameters on given viewing conditions
+func LuminanceAdapt(l, m, s float32, vw *View) (lA, mA, sA float32) {
+	lA = LuminanceAdaptComp(l, vw.RGBD.X, vw.FL)
+	mA = LuminanceAdaptComp(m, vw.RGBD.Y, vw.FL)
+	sA = LuminanceAdaptComp(s, vw.RGBD.Z, vw.FL)
+	return
+}
+
+// LMSToOps converts Long, Medium, Short cone-based values to
+// opponent redVgreen (a) and yellowVblue (b), and grey (achromatic) values,
+// that more closely reflect neural responses.
+// greyNorm is a normalizing grey factor used in the CAM16 model.
+// l, m, s values must be in 100-base units.
+// Uses the CIECAM16 color appearance model.
+func LMSToOps(l, m, s float32, vw *View) (redVgreen, yellowVblue, grey, greyNorm float32) {
+	// Discount illuminant and adapt
+	lA, mA, sA := LuminanceAdapt(l, m, s, vw)
+	redVgreen = (11*lA + -12*mA + sA) / 11
+	yellowVblue = (lA + mA - 2*sA) / 9
+	// auxiliary components
+	grey = (40*lA + 20*mA + sA) / 20        // achromatic response, multiplied * view.NBB
+	greyNorm = (20*lA + 20*mA + 21*sA) / 20 // normalizing factor
+	return
+}

+ 47 - 0
vendor/cogentcore.org/core/colors/cam/cam16/sanitize.go

@@ -0,0 +1,47 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Adapted from https://github.com/material-foundation/material-color-utilities
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cam16
+
+import "cogentcore.org/core/math32"
+
+// SanitizeDegrees ensures that degrees is in [0-360) range
+func SanitizeDegrees(deg float32) float32 {
+	if deg < 0 {
+		return math32.Mod(deg, 360) + 360
+	} else if deg >= 360 {
+		return math32.Mod(deg, 360)
+	} else {
+		return deg
+	}
+}
+
+// SanitizeRadians sanitizes a small enough angle in radians.
+// Takes an angle in radians; must not deviate too much from 0,
+// and returns a coterminal angle between 0 and 2pi.
+func SanitizeRadians(angle float32) float32 {
+	return math32.Mod(angle+math32.Pi*8, math32.Pi*2)
+}
+
+// InCyclicOrder returns true a, b, c are in order around a circle
+func InCyclicOrder(a, b, c float32) bool {
+	delta_a_b := SanitizeRadians(b - a)
+	delta_a_c := SanitizeRadians(c - a)
+	return delta_a_b < delta_a_c
+}

+ 37 - 0
vendor/cogentcore.org/core/colors/cam/cam16/transform.go

@@ -0,0 +1,37 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cam16
+
+import (
+	"image/color"
+
+	"cogentcore.org/core/colors/cam/cie"
+	"cogentcore.org/core/math32"
+)
+
+// Blend returns a color that is the given percent blend between the first
+// and second color; 10 = 10% of the first and 90% of the second, etc;
+// blending is done directly on non-premultiplied CAM16-UCS values, and
+// a correctly premultiplied color is returned.
+func Blend(pct float32, x, y color.Color) color.RGBA {
+	pct = math32.Clamp(pct, 0, 100)
+	amt := pct / 100
+
+	xsr, xsg, xsb, _ := cie.SRGBUint32ToFloat(x.RGBA())
+	ysr, ysg, ysb, _ := cie.SRGBUint32ToFloat(y.RGBA())
+
+	cx := FromSRGB(xsr, xsg, xsb)
+	cy := FromSRGB(ysr, ysg, ysb)
+
+	xj, _, xa, xb := cx.UCS()
+	yj, _, ya, yb := cy.UCS()
+
+	j := yj + (xj-yj)*amt
+	a := ya + (xa-ya)*amt
+	b := yb + (xb-yb)*amt
+
+	cam := FromUCS(j, a, b)
+	return cam.AsRGBA()
+}

+ 178 - 0
vendor/cogentcore.org/core/colors/cam/cam16/view.go

@@ -0,0 +1,178 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Adapted from https://github.com/material-foundation/material-color-utilities
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cam16
+
+import (
+	"cogentcore.org/core/colors/cam/cie"
+	"cogentcore.org/core/math32"
+)
+
+// View represents viewing conditions under which a color is being perceived,
+// which greatly affects the subjective perception.  Defaults represent the
+// standard defined such conditions, under which the CAM16 computations operate.
+type View struct {
+
+	// white point illumination -- typically cie.WhiteD65
+	WhitePoint math32.Vector3
+
+	// the ambient light strength in lux
+	Luminance float32 `default:"200"`
+
+	// the average luminance of 10 degrees around the color in question
+	BgLuminance float32 `default:"50"`
+
+	// the brightness of the entire environment
+	Surround float32 `default:"2"`
+
+	// whether the person's eyes have adapted to the lighting
+	Adapted bool `default:"false"`
+
+	// computed from Luminance
+	AdaptingLuminance float32 `display:"-"`
+
+	//
+	BgYToWhiteY float32 `display:"-"`
+
+	//
+	AW float32 `display:"-"`
+
+	// luminance level induction factor
+	NBB float32 `display:"-"`
+
+	// luminance level induction factor
+	NCB float32 `display:"-"`
+
+	// exponential nonlinearity
+	C float32 `display:"-"`
+
+	// chromatic induction factor
+	NC float32 `display:"-"`
+
+	// luminance-level adaptation factor, based on the HuntLiLuo03 equations
+	FL float32 `display:"-"`
+
+	// FL to the 1/4 power
+	FLRoot float32 `display:"-"`
+
+	// base exponential nonlinearity
+	Z float32 `display:"-"`
+
+	// inverse of the RGBD factors
+	DRGBInverse math32.Vector3 `display:"-"`
+
+	// cone responses to white point, adjusted for discounting
+	RGBD math32.Vector3 `display:"-"`
+}
+
+// NewView returns a new view with all parameters initialized based on given major params
+func NewView(whitePoint math32.Vector3, lum, bgLum, surround float32, adapt bool) *View {
+	vw := &View{WhitePoint: whitePoint, Luminance: lum, BgLuminance: bgLum, Surround: surround, Adapted: adapt}
+	vw.Update()
+	return vw
+}
+
+// TheStdView is the standard viewing conditions view
+// returned by NewStdView if already created.
+var TheStdView *View
+
+// NewStdView returns a new standard viewing conditions model
+// returns TheStdView if already created
+func NewStdView() *View {
+	if TheStdView != nil {
+		return TheStdView
+	}
+	TheStdView = NewView(cie.WhiteD65, 200, 50, 2, false)
+	return TheStdView
+}
+
+// Update updates all the computed values based on main parameters
+func (vw *View) Update() {
+	vw.AdaptingLuminance = (vw.Luminance / math32.Pi) * (cie.LToY(50) / 100)
+	// A background of pure black is non-physical and leads to infinities that
+	// represent the idea that any color viewed in pure black can't be seen.
+	vw.BgLuminance = math32.Max(0.1, vw.BgLuminance)
+
+	// Transform test illuminant white in XYZ to 'cone'/'rgb' responses
+	rW, gW, bW := XYZToLMS(vw.WhitePoint.X, vw.WhitePoint.Y, vw.WhitePoint.Z)
+
+	// Scale input surround, domain (0, 2), to CAM16 surround, domain (0.8, 1.0)
+	vw.Surround = math32.Clamp(vw.Surround, 0, 2)
+	f := 0.8 + (vw.Surround / 10)
+	// "Exponential non-linearity"
+	if f >= 0.9 {
+		vw.C = math32.Lerp(0.59, 0.69, ((f - 0.9) * 10))
+	} else {
+		vw.C = math32.Lerp(0.525, 0.59, ((f - 0.8) * 10))
+	}
+	// Calculate degree of adaptation to illuminant
+	d := float32(1)
+	if !vw.Adapted {
+		d = f * (1 - ((1 / 3.6) * math32.Exp((-vw.AdaptingLuminance-42)/92)))
+	}
+
+	// Per Li et al, if D is greater than 1 or less than 0, set it to 1 or 0.
+	d = math32.Clamp(d, 0, 1)
+
+	// chromatic induction factor
+	vw.NC = f
+
+	// Cone responses to the whitePoint, r/g/b/W, adjusted for discounting.
+	//
+	// Why use 100 instead of the white point's relative luminance?
+	//
+	// Some papers and implementations, for both CAM02 and CAM16, use the Y
+	// value of the reference white instead of 100. Fairchild's Color Appearance
+	// Models (3rd edition) notes that this is in error: it was included in the
+	// CIE 2004a report on CIECAM02, but, later parts of the conversion process
+	// account for scaling of appearance relative to the white point relative
+	// luminance. This part should simply use 100 as luminance.
+	vw.RGBD.X = d*(100/rW) + 1 - d
+	vw.RGBD.Y = d*(100/gW) + 1 - d
+	vw.RGBD.Z = d*(100/bW) + 1 - d
+
+	// Factor used in calculating meaningful factors
+	k := 1 / (5*vw.AdaptingLuminance + 1)
+	k4 := k * k * k * k
+	k4F := 1 - k4
+
+	// Luminance-level adaptation factor
+	vw.FL = (k4 * vw.AdaptingLuminance) +
+		(0.1 * k4F * k4F * math32.Pow(5*vw.AdaptingLuminance, 1.0/3.0))
+
+	vw.FLRoot = math32.Pow(vw.FL, 0.25)
+
+	// Intermediate factor, ratio of background relative luminance to white relative luminance
+	n := cie.LToY(vw.BgLuminance) / vw.WhitePoint.Y
+	vw.BgYToWhiteY = n
+
+	// Base exponential nonlinearity
+	// note Schlomer 2018 has a typo and uses 1.58, the correct factor is 1.48
+	vw.Z = 1.48 + math32.Sqrt(n)
+
+	// Luminance-level induction factors
+	vw.NBB = 0.725 / math32.Pow(n, 0.2)
+	vw.NCB = vw.NBB
+
+	// Discounted cone responses to the white point, adjusted for post-saturation
+	// adaptation perceptual nonlinearities.
+	rA, gA, bA := LuminanceAdapt(rW, gW, bW, vw)
+
+	vw.AW = ((40*rA + 20*gA + bA) / 20) * vw.NBB
+}

+ 29 - 0
vendor/cogentcore.org/core/colors/cam/cie/README.md

@@ -0,0 +1,29 @@
+# cie
+
+CIE is the International Commission on Illumination which establishes standards for representing color information.
+
+This package contains standard values and routines for converting between standard RGB color space (sRGB), and the the CIE standard color spaces of XYZ and L*a*b* (LAB).
+
+# XYZ color space (1931): standard color basis space
+
+https://en.wikipedia.org/wiki/CIE_1931_color_space
+
+* `Y` is the luminance (overall brightness) = 0.2 red + 0.7 green + 0.07 blue
+* `Z` is purely the short wavelength (blue) = 0.02 red + 0.1 green + 0.95 blue
+* `X` is a mix of the three CIE LMS cone responses chosen to be nonnegative: 1.9 long (red), -1.1 medium (green), and 0.2 short = 0.4 red + 0.36 green + 0.18 blue
+
+The unit of the tristimulus values X, Y, and Z is often arbitrarily chosen so that Y = 1 or Y = 100 is the brightest white that a color display supports. In this case, the Y value is known as the relative luminance. The corresponding whitepoint values for X and Z can then be inferred using the standard illuminants.
+
+# LAB  L*a*b* color space
+
+https://en.wikipedia.org/wiki/CIELAB_color_space
+
+* `lightness L*` defines black at 0 and white at 100.
+* `a*` is relative to the green–magenta opponent colors, with negative values toward green and positive values toward magenta.
+* `b*` represents the blue–yellow opponents, with negative numbers toward blue and positive toward yellow.
+
+The a* and b* axes are unbounded and depending on the reference white they can easily exceed ±150 to cover the human gamut. Nevertheless, software implementations often clamp these values for practical reasons. For instance, if integer math is being used it is common to clamp a* and b* in the range of −128 to 127.
+
+CIELAB is calculated relative to a reference white, for which the CIE recommends the use of CIE Standard illuminant D65.  D65 is used in the vast majority industries and applications, with the notable exception being the printing industry which uses D50.
+
+

+ 70 - 0
vendor/cogentcore.org/core/colors/cam/cie/lab.go

@@ -0,0 +1,70 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cie
+
+import "cogentcore.org/core/math32"
+
+// LABCompress does cube-root compression of the X, Y, Z components
+// prior to performing the LAB conversion
+func LABCompress(t float32) float32 {
+	e := float32(216.0 / 24389.0)
+	if t > e {
+		return math32.Pow(t, 1.0/3.0)
+	}
+	kappa := float32(24389.0 / 27.0)
+	return (kappa*t + 16) / 116
+}
+
+func LABUncompress(ft float32) float32 {
+	e := float32(216.0 / 24389.0)
+	ft3 := ft * ft * ft
+	if ft3 > e {
+		return ft3
+	}
+	kappa := float32(24389.0 / 27.0)
+	return (116*ft - 16) / kappa
+}
+
+// XYZToLAB converts a color from XYZ to L*a*b* coordinates
+// using the standard D65 illuminant
+func XYZToLAB(x, y, z float32) (l, a, b float32) {
+	x, y, z = XYZNormD65(x, y, z)
+	fx := LABCompress(x)
+	fy := LABCompress(y)
+	fz := LABCompress(z)
+	l = 116*fy - 16
+	a = 500 * (fx - fy)
+	b = 200 * (fy - fz)
+	return
+}
+
+// LABToXYZ converts a color from L*a*b* to XYZ coordinates
+// using the standard D65 illuminant
+func LABToXYZ(l, a, b float32) (x, y, z float32) {
+	fy := (l + 16) / 116
+	fx := a/500 + fy
+	fz := fy - b/200
+	x = LABUncompress(fx)
+	y = LABUncompress(fy)
+	z = LABUncompress(fz)
+	x, y, z = XYZDenormD65(x, y, z)
+	return
+}
+
+// LToY Converts an L* value to a Y value.
+// L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
+// L* measures perceptual luminance, a linear scale. Y in XYZ
+// measures relative luminance, a logarithmic scale.
+func LToY(l float32) float32 {
+	return 100 * LABUncompress((l+16)/116)
+}
+
+// YToL Converts a Y value to an L* value.
+// L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
+// L* measures perceptual luminance, a linear scale. Y in XYZ
+// measures relative luminance, a logarithmic scale.
+func YToL(y float32) float32 {
+	return LABCompress(y/100)*116 - 16
+}

+ 105 - 0
vendor/cogentcore.org/core/colors/cam/cie/srgb.go

@@ -0,0 +1,105 @@
+// Copyright (c) 2021, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cie
+
+import "cogentcore.org/core/math32"
+
+// SRGBToLinearComp converts an sRGB rgb component to linear space (removes gamma).
+// Used in converting from sRGB to XYZ colors.
+func SRGBToLinearComp(srgb float32) float32 {
+	if srgb <= 0.04045 {
+		return srgb / 12.92
+	}
+	return math32.Pow((srgb+0.055)/1.055, 2.4)
+}
+
+// SRGBFromLinearComp converts an sRGB rgb linear component
+// to non-linear (gamma corrected) sRGB value
+// Used in converting from XYZ to sRGB.
+func SRGBFromLinearComp(lin float32) float32 {
+	var gv float32
+	if lin <= 0.0031308 {
+		gv = 12.92 * lin
+	} else {
+		gv = (1.055*math32.Pow(lin, 1.0/2.4) - 0.055)
+	}
+	return math32.Clamp(gv, 0, 1)
+}
+
+// SRGBToLinear converts set of sRGB components to linear values,
+// removing gamma correction.
+func SRGBToLinear(r, g, b float32) (rl, gl, bl float32) {
+	rl = SRGBToLinearComp(r)
+	gl = SRGBToLinearComp(g)
+	bl = SRGBToLinearComp(b)
+	return
+}
+
+// SRGB100ToLinear converts set of sRGB components to linear values,
+// removing gamma correction.  returns 100-base RGB values
+func SRGB100ToLinear(r, g, b float32) (rl, gl, bl float32) {
+	rl = 100 * SRGBToLinearComp(r)
+	gl = 100 * SRGBToLinearComp(g)
+	bl = 100 * SRGBToLinearComp(b)
+	return
+}
+
+// SRGBFromLinear converts set of sRGB components from linear values,
+// adding gamma correction.
+func SRGBFromLinear(rl, gl, bl float32) (r, g, b float32) {
+	r = SRGBFromLinearComp(rl)
+	g = SRGBFromLinearComp(gl)
+	b = SRGBFromLinearComp(bl)
+	return
+}
+
+// SRGBFromLinear100 converts set of sRGB components from linear values in 0-100 range,
+// adding gamma correction.
+func SRGBFromLinear100(rl, gl, bl float32) (r, g, b float32) {
+	r = SRGBFromLinearComp(rl / 100)
+	g = SRGBFromLinearComp(gl / 100)
+	b = SRGBFromLinearComp(bl / 100)
+	return
+}
+
+// SRGBFloatToUint8 converts the given non-alpha-premuntiplied sRGB float32
+// values to alpha-premultiplied sRGB uint8 values.
+func SRGBFloatToUint8(rf, gf, bf, af float32) (r, g, b, a uint8) {
+	r = uint8(rf*af*255 + 0.5)
+	g = uint8(gf*af*255 + 0.5)
+	b = uint8(bf*af*255 + 0.5)
+	a = uint8(af*255 + 0.5)
+	return
+}
+
+// SRGBFloatToUint32 converts the given non-alpha-premuntiplied sRGB float32
+// values to alpha-premultiplied sRGB uint32 values.
+func SRGBFloatToUint32(rf, gf, bf, af float32) (r, g, b, a uint32) {
+	r = uint32(rf*af*65535 + 0.5)
+	g = uint32(gf*af*65535 + 0.5)
+	b = uint32(bf*af*65535 + 0.5)
+	a = uint32(af*65535 + 0.5)
+	return
+}
+
+// SRGBUint8ToFloat converts the given alpha-premultiplied sRGB uint8 values
+// to non-alpha-premuntiplied sRGB float32 values.
+func SRGBUint8ToFloat(r, g, b, a uint8) (fr, fg, fb, fa float32) {
+	fa = float32(a) / 255
+	fr = (float32(r) / 255) / fa
+	fg = (float32(g) / 255) / fa
+	fb = (float32(b) / 255) / fa
+	return
+}
+
+// SRGBUint32ToFloat converts the given alpha-premultiplied sRGB uint32 values
+// to non-alpha-premuntiplied sRGB float32 values.
+func SRGBUint32ToFloat(r, g, b, a uint32) (fr, fg, fb, fa float32) {
+	fa = float32(a) / 65535
+	fr = (float32(r) / 65535) / fa
+	fg = (float32(g) / 65535) / fa
+	fb = (float32(b) / 65535) / fa
+	return
+}

+ 15 - 0
vendor/cogentcore.org/core/colors/cam/cie/std.go

@@ -0,0 +1,15 @@
+// Copyright (c) 2021, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cie
+
+import "cogentcore.org/core/math32"
+
+// WhiteD65 is the standard white color for midday sun, D65, in XYZ coordinates.
+// Used as a standard reference illumination condition for most cases.
+var WhiteD65 = math32.Vec3(95.047, 100.0, 108.883)
+
+// WhiteD50 is the standard white color used for printing industry, D50,
+// in XYZ coordinates.
+var WhiteD50 = math32.Vec3(96.4212, 100.0, 82.5188)

+ 67 - 0
vendor/cogentcore.org/core/colors/cam/cie/xyz.go

@@ -0,0 +1,67 @@
+// Copyright (c) 2021, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cie
+
+// SRGBLinToXYZ converts sRGB linear into XYZ CIE standard color space
+func SRGBLinToXYZ(rl, gl, bl float32) (x, y, z float32) {
+	x = 0.41233895*rl + 0.35762064*gl + 0.18051042*bl
+	y = 0.2126*rl + 0.7152*gl + 0.0722*bl
+	z = 0.01932141*rl + 0.11916382*gl + 0.95034478*bl
+	return
+}
+
+// XYZToSRGBLin converts XYZ CIE standard color space to sRGB linear
+func XYZToSRGBLin(x, y, z float32) (rl, gl, bl float32) {
+	rl = 3.2406*x + -1.5372*y + -0.4986*z
+	gl = -0.9689*x + 1.8758*y + 0.0415*z
+	bl = 0.0557*x + -0.2040*y + 1.0570*z
+	return
+}
+
+// SRGBToXYZ converts sRGB into XYZ CIE standard color space
+func SRGBToXYZ(r, g, b float32) (x, y, z float32) {
+	rl, gl, bl := SRGBToLinear(r, g, b)
+	x, y, z = SRGBLinToXYZ(rl, gl, bl)
+	return
+}
+
+// SRGBToXYZ100 converts sRGB into XYZ CIE standard color space
+// with 100-base sRGB values -- used for CAM16 but not CAM02
+func SRGBToXYZ100(r, g, b float32) (x, y, z float32) {
+	rl, gl, bl := SRGB100ToLinear(r, g, b)
+	x, y, z = SRGBLinToXYZ(rl, gl, bl)
+	return
+}
+
+// XYZToSRGB converts XYZ CIE standard color space into sRGB
+func XYZToSRGB(x, y, z float32) (r, g, b float32) {
+	rl, bl, gl := XYZToSRGBLin(x, y, z)
+	r, g, b = SRGBFromLinear(rl, bl, gl)
+	return
+}
+
+// XYZ100ToSRGB converts XYZ CIE standard color space, 100 base units,
+// into sRGB
+func XYZ100ToSRGB(x, y, z float32) (r, g, b float32) {
+	rl, bl, gl := XYZToSRGBLin(x/100, y/100, z/100)
+	r, g, b = SRGBFromLinear(rl, bl, gl)
+	return
+}
+
+// XYZNormD65 normalizes XZY values relative to the D65 outdoor white light values
+func XYZNormD65(x, y, z float32) (xr, yr, zr float32) {
+	xr = x / 0.95047
+	zr = z / 1.08883
+	yr = y
+	return
+}
+
+// XYZDenormD65 de-normalizes XZY values relative to the D65 outdoor white light values
+func XYZDenormD65(x, y, z float32) (xr, yr, zr float32) {
+	xr = x * 0.95047
+	zr = z * 1.08883
+	yr = y
+	return
+}

+ 16 - 0
vendor/cogentcore.org/core/colors/cam/hct/README.md

@@ -0,0 +1,16 @@
+# hct: Hue, Chroma, and Tone
+
+A color system built using CAM16 hue and chroma, and L* (lightness) from the L*a*b* color space, providing a perceptually accurate color measurement system that can also accurately render what colors will appear as in different lighting environments.
+
+Using L* creates a link between the color system, contrast, and thus accessibility. Contrast ratio depends on relative luminance, or Y in the XYZ color space. L*, or perceptual luminance can be calculated from Y.
+
+Unlike Y, L* is linear to human perception, allowing trivial creation of accurate color tones.
+
+Unlike contrast ratio, measuring contrast in L* is linear, and simple to calculate. A difference of 40 in HCT tone guarantees a contrast ratio >= 3.0, and a difference of 50 guarantees a contrast ratio >= 4.5.
+
+## HCT Colorspace
+
+![hct colorspace](testdata/hctspace.png)
+
+
+

+ 382 - 0
vendor/cogentcore.org/core/colors/cam/hct/bisect.go

@@ -0,0 +1,382 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Adapted from https://github.com/material-foundation/material-color-utilities
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hct
+
+import (
+	"cogentcore.org/core/base/num"
+	"cogentcore.org/core/colors/cam/cam16"
+	"cogentcore.org/core/math32"
+)
+
+// double ChromaticAdaptation(double component) {
+//   double af = pow(abs(component), 0.42);
+//   return Signum(component) * 400.0 * af / (af + 27.13);
+// }
+
+func MatMul(v math32.Vector3, mat [3][3]float32) math32.Vector3 {
+	x := v.X*mat[0][0] + v.Y*mat[0][1] + v.Z*mat[0][2]
+	y := v.X*mat[1][0] + v.Y*mat[1][1] + v.Z*mat[1][2]
+	z := v.X*mat[2][0] + v.Y*mat[2][1] + v.Z*mat[2][2]
+	return math32.Vec3(x, y, z)
+}
+
+// HueOf Returns the hue of a linear RGB color in CAM16.
+func HueOf(linrgb math32.Vector3) float32 {
+	sd := MatMul(linrgb, kScaledDiscountFromLinrgb)
+	rA := cam16.LuminanceAdaptComp(sd.X, 1, 1)
+	gA := cam16.LuminanceAdaptComp(sd.Y, 1, 1)
+	bA := cam16.LuminanceAdaptComp(sd.Z, 1, 1)
+
+	// redness-greenness
+	a := (11*rA + -12*gA + bA) / 11
+	// yellowness-blueness
+	b := (rA + gA - 2*bA) / 9
+	return math32.Atan2(b, a)
+}
+
+// Solves the lerp equation.
+// @param source The starting number.
+// @param mid The number in the middle.
+// @param target The ending number.
+// @return A number t such that lerp(source, target, t) = mid.
+func Intercept(source, mid, target float32) float32 {
+	return (mid - source) / (target - source)
+}
+
+// GetAxis returns value along axis 0,1,2 -- result is divided by 100
+// so that resulting numbers are in 0-1 range.
+func GetAxis(v math32.Vector3, axis int) float32 {
+	switch axis {
+	case 0:
+		return v.X
+	case 1:
+		return v.Y
+	case 2:
+		return v.Z
+	default:
+		return -1
+	}
+}
+
+/**
+ * Intersects a segment with a plane.
+ *
+ * @param source The coordinates of point A.
+ * @param coordinate The R-, G-, or B-coordinate of the plane.
+ * @param target The coordinates of point B.
+ * @param axis The axis the plane is perpendicular with. (0: R, 1: G, 2: B)
+ * @return The intersection point of the segment AB with the plane R=coordinate,
+ * G=coordinate, or B=coordinate
+ */
+func SetCoordinate(source, target math32.Vector3, coord float32, axis int) math32.Vector3 {
+	t := Intercept(GetAxis(source, axis), coord, GetAxis(target, axis))
+	return source.Lerp(target, t)
+}
+
+func IsBounded(x float32) bool {
+	return 0 <= x && x <= 100
+}
+
+// Returns the nth possible vertex of the polygonal intersection.
+// @param y The Y value of the plane.
+// @param n The zero-based index of the point. 0 <= n <= 11.
+// @return The nth possible vertex of the polygonal intersection of the y plane
+// and the RGB cube, in linear RGB coordinates, if it exists. If this possible
+// vertex lies outside of the cube,
+//
+//	[-1.0, -1.0, -1.0] is returned.
+func NthVertex(y float32, n int) math32.Vector3 {
+	k_r := kYFromLinrgb[0]
+	k_g := kYFromLinrgb[1]
+	k_b := kYFromLinrgb[2]
+	coord_a := float32(0)
+	if n%4 > 1 {
+		coord_a = 100
+	}
+	coord_b := float32(0)
+	if n%2 != 0 {
+		coord_b = 100
+	}
+	if n < 4 {
+		g := coord_a
+		b := coord_b
+		r := (y - g*k_g - b*k_b) / k_r
+		if IsBounded(r) {
+			return math32.Vec3(r, g, b)
+		} else {
+			return math32.Vec3(-1.0, -1.0, -1.0)
+		}
+	} else if n < 8 {
+		b := coord_a
+		r := coord_b
+		g := (y - r*k_r - b*k_b) / k_g
+		if IsBounded(g) {
+			return math32.Vec3(r, g, b)
+		} else {
+			return math32.Vec3(-1.0, -1.0, -1.0)
+		}
+	} else {
+		r := coord_a
+		g := coord_b
+		b := (y - r*k_r - g*k_g) / k_b
+		if IsBounded(b) {
+			return math32.Vec3(r, g, b)
+		} else {
+			return math32.Vec3(-1.0, -1.0, -1.0)
+		}
+	}
+}
+
+// Finds the segment containing the desired color.
+// @param y The Y value of the color.
+// @param target_hue The hue of the color.
+// @return A list of two sets of linear RGB coordinates, each corresponding to
+// an endpoint of the segment containing the desired color.
+func BisectToSegment(y, target_hue float32) [2]math32.Vector3 {
+	left := math32.Vec3(-1.0, -1.0, -1.0)
+	right := left
+	left_hue := float32(0.0)
+	right_hue := float32(0.0)
+	initialized := false
+	uncut := true
+	for n := 0; n < 12; n++ {
+		mid := NthVertex(y, n)
+		if mid.X < 0 {
+			continue
+		}
+		mid_hue := HueOf(mid)
+		if !initialized {
+			left = mid
+			right = mid
+			left_hue = mid_hue
+			right_hue = mid_hue
+			initialized = true
+			continue
+		}
+		if uncut || cam16.InCyclicOrder(left_hue, mid_hue, right_hue) {
+			uncut = false
+			if cam16.InCyclicOrder(left_hue, target_hue, mid_hue) {
+				right = mid
+				right_hue = mid_hue
+			} else {
+				left = mid
+				left_hue = mid_hue
+			}
+		}
+	}
+	var out [2]math32.Vector3
+	out[0] = left
+	out[1] = right
+	return out
+}
+
+func Midpoint(a, b math32.Vector3) math32.Vector3 {
+	return math32.Vec3((a.X+b.X)/2, (a.Y+b.Y)/2, (a.Z+b.Z)/2)
+}
+
+func CriticalPlaneBelow(x float32) int { return int(math32.Floor(x - 0.5)) }
+
+func CriticalPlaneAbove(x float32) int { return int(math32.Ceil(x - 0.5)) }
+
+// Delinearizes an RGB component, returning a floating-point number.
+// @param rgb_component 0.0 <= rgb_component <= 100.0, represents linear R/G/B
+// channel
+// @return 0.0 <= output <= 255.0, color channel converted to regular RGB space
+func TrueDelinearized(comp float32) float32 {
+	normalized := comp / 100
+	delinearized := float32(0.0)
+	if normalized <= 0.0031308 {
+		delinearized = normalized * 12.92
+	} else {
+		delinearized = 1.055*math32.Pow(normalized, 1.0/2.4) - 0.055
+	}
+	return delinearized * 255
+}
+
+// Finds a color with the given Y and hue on the boundary of the cube.
+// @param y The Y value of the color.
+// @param target_hue The hue of the color.
+// @return The desired color, in linear RGB coordinates.
+func BisectToLimit(y, target_hue float32) math32.Vector3 {
+	segment := BisectToSegment(y, target_hue)
+	left := segment[0]
+	left_hue := HueOf(left)
+	right := segment[1]
+	for axis := 0; axis < 3; axis++ {
+		if GetAxis(left, axis) != GetAxis(right, axis) {
+			l_plane := -1
+			r_plane := 255
+			if GetAxis(left, axis) < GetAxis(right, axis) {
+				l_plane = CriticalPlaneBelow(TrueDelinearized(GetAxis(left, axis)))
+				r_plane = CriticalPlaneAbove(TrueDelinearized(GetAxis(right, axis)))
+			} else {
+				l_plane = CriticalPlaneAbove(TrueDelinearized(GetAxis(left, axis)))
+				r_plane = CriticalPlaneBelow(TrueDelinearized(GetAxis(right, axis)))
+			}
+			for i := 0; i < 8; i++ {
+				if num.Abs(r_plane-l_plane) <= 1 {
+					break
+				} else {
+					m_plane := int(math32.Floor(float32(l_plane+r_plane) / 2.0))
+					mid_plane_coordinate := kCriticalPlanes[m_plane]
+					mid := SetCoordinate(left, right, mid_plane_coordinate, axis)
+					mid_hue := HueOf(mid)
+					if cam16.InCyclicOrder(left_hue, target_hue, mid_hue) {
+						right = mid
+						r_plane = m_plane
+					} else {
+						left = mid
+						left_hue = mid_hue
+						l_plane = m_plane
+					}
+				}
+			}
+		}
+	}
+	return Midpoint(left, right)
+}
+
+/////////////////////////////////////////////
+
+var kScaledDiscountFromLinrgb = [3][3]float32{
+	{
+		0.001200833568784504,
+		0.002389694492170889,
+		0.0002795742885861124,
+	},
+	{
+		0.0005891086651375999,
+		0.0029785502573438758,
+		0.0003270666104008398,
+	},
+	{
+		0.00010146692491640572,
+		0.0005364214359186694,
+		0.0032979401770712076,
+	},
+}
+
+var kLinrgbFromScaledDiscount = [3][3]float32{
+	{
+		1373.2198709594231,
+		-1100.4251190754821,
+		-7.278681089101213,
+	},
+	{
+		-271.815969077903,
+		559.6580465940733,
+		-32.46047482791194,
+	},
+	{
+		1.9622899599665666,
+		-57.173814538844006,
+		308.7233197812385,
+	},
+}
+
+var kYFromLinrgb = [3]float32{0.2126, 0.7152, 0.0722}
+
+var kCriticalPlanes = [255]float32{
+	0.015176349177441876, 0.045529047532325624, 0.07588174588720938,
+	0.10623444424209313, 0.13658714259697685, 0.16693984095186062,
+	0.19729253930674434, 0.2276452376616281, 0.2579979360165119,
+	0.28835063437139563, 0.3188300904430532, 0.350925934958123,
+	0.3848314933096426, 0.42057480301049466, 0.458183274052838,
+	0.4976837250274023, 0.5391024159806381, 0.5824650784040898,
+	0.6277969426914107, 0.6751227633498623, 0.7244668422128921,
+	0.775853049866786, 0.829304845476233, 0.8848452951698498,
+	0.942497089126609, 1.0022825574869039, 1.0642236851973577,
+	1.1283421258858297, 1.1946592148522128, 1.2631959812511864,
+	1.3339731595349034, 1.407011200216447, 1.4823302800086415,
+	1.5599503113873272, 1.6398909516233677, 1.7221716113234105,
+	1.8068114625156377, 1.8938294463134073, 1.9832442801866852,
+	2.075074464868551, 2.1693382909216234, 2.2660538449872063,
+	2.36523901573795, 2.4669114995532007, 2.5710888059345764,
+	2.6777882626779785, 2.7870270208169257, 2.898822059350997,
+	3.0131901897720907, 3.1301480604002863, 3.2497121605402226,
+	3.3718988244681087, 3.4967242352587946, 3.624204428461639,
+	3.754355295633311, 3.887192587735158, 4.022731918402185,
+	4.160988767090289, 4.301978482107941, 4.445716283538092,
+	4.592217266055746, 4.741496401646282, 4.893568542229298,
+	5.048448422192488, 5.20615066083972, 5.3666897647573375,
+	5.5300801301023865, 5.696336044816294, 5.865471690767354,
+	6.037501145825082, 6.212438385869475, 6.390297286737924,
+	6.571091626112461, 6.7548350853498045, 6.941541251256611,
+	7.131223617812143, 7.323895587840543, 7.5195704746346665,
+	7.7182615035334345, 7.919981813454504, 8.124744458384042,
+	8.332562408825165, 8.543448553206703, 8.757415699253682,
+	8.974476575321063, 9.194643831691977, 9.417930041841839,
+	9.644347703669503, 9.873909240696694, 10.106627003236781,
+	10.342513269534024, 10.58158024687427, 10.8238400726681,
+	11.069304815507364, 11.317986476196008, 11.569896988756009,
+	11.825048221409341, 12.083451977536606, 12.345119996613247,
+	12.610063955123938, 12.878295467455942, 13.149826086772048,
+	13.42466730586372, 13.702830557985108, 13.984327217668513,
+	14.269168601521828, 14.55736596900856, 14.848930523210871,
+	15.143873411576273, 15.44220572664832, 15.743938506781891,
+	16.04908273684337, 16.35764934889634, 16.66964922287304,
+	16.985093187232053, 17.30399201960269, 17.62635644741625,
+	17.95219714852476, 18.281524751807332, 18.614349837764564,
+	18.95068293910138, 19.290534541298456, 19.633915083172692,
+	19.98083495742689, 20.331304511189067, 20.685334046541502,
+	21.042933821039977, 21.404114048223256, 21.76888489811322,
+	22.137256497705877, 22.50923893145328, 22.884842241736916,
+	23.264076429332462, 23.6469514538663, 24.033477234264016,
+	24.42366364919083, 24.817520537484558, 25.21505769858089,
+	25.61628489293138, 26.021211842414342, 26.429848230738664,
+	26.842203703840827, 27.258287870275353, 27.678110301598522,
+	28.10168053274597, 28.529008062403893, 28.96010235337422,
+	29.39497283293396, 29.83362889318845, 30.276079891419332,
+	30.722335150426627, 31.172403958865512, 31.62629557157785,
+	32.08401920991837, 32.54558406207592, 33.010999283389665,
+	33.4802739966603, 33.953417292456834, 34.430438229418264,
+	34.911345834551085, 35.39614910352207, 35.88485700094671,
+	36.37747846067349, 36.87402238606382, 37.37449765026789,
+	37.87891309649659, 38.38727753828926, 38.89959975977785,
+	39.41588851594697, 39.93615253289054, 40.460400508064545,
+	40.98864111053629, 41.520882981230194, 42.05713473317016,
+	42.597404951718396, 43.141702194811224, 43.6900349931913,
+	44.24241185063697, 44.798841244188324, 45.35933162437017,
+	45.92389141541209, 46.49252901546552, 47.065252796817916,
+	47.64207110610409, 48.22299226451468, 48.808024568002054,
+	49.3971762874833, 49.9904556690408, 50.587870934119984,
+	51.189430279724725, 51.79514187861014, 52.40501387947288,
+	53.0190544071392, 53.637271562750364, 54.259673423945976,
+	54.88626804504493, 55.517063457223934, 56.15206766869424,
+	56.79128866487574, 57.43473440856916, 58.08241284012621,
+	58.734331877617365, 59.39049941699807, 60.05092333227251,
+	60.715611475655585, 61.38457167773311, 62.057811747619894,
+	62.7353394731159, 63.417162620860914, 64.10328893648692,
+	64.79372614476921, 65.48848194977529, 66.18756403501224,
+	66.89098006357258, 67.59873767827808, 68.31084450182222,
+	69.02730813691093, 69.74813616640164, 70.47333615344107,
+	71.20291564160104, 71.93688215501312, 72.67524319850172,
+	73.41800625771542, 74.16517879925733, 74.9167682708136,
+	75.67278210128072, 76.43322770089146, 77.1981124613393,
+	77.96744375590167, 78.74122893956174, 79.51947534912904,
+	80.30219030335869, 81.08938110306934, 81.88105503125999,
+	82.67721935322541, 83.4778813166706, 84.28304815182372,
+	85.09272707154808, 85.90692527145302, 86.72564993000343,
+	87.54890820862819, 88.3767072518277, 89.2090541872801,
+	90.04595612594655, 90.88742016217518, 91.73345337380438,
+	92.58406282226491, 93.43925555268066, 94.29903859396902,
+	95.16341895893969, 96.03240364439274, 96.9059996312159,
+	97.78421388448044, 98.6670533535366, 99.55452497210776,
+}

+ 227 - 0
vendor/cogentcore.org/core/colors/cam/hct/contrast.go

@@ -0,0 +1,227 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Adapted from https://github.com/material-foundation/material-color-utilities
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hct
+
+import (
+	"image/color"
+
+	"cogentcore.org/core/colors/cam/cie"
+	"cogentcore.org/core/math32"
+)
+
+const (
+	// ContrastAA is the contrast ratio required by WCAG AA for body text
+	ContrastAA float32 = 4.5
+
+	// ContrastLargeAA is the contrast ratio required by WCAG AA for large text
+	// (at least 120-150% larger than the body text)
+	ContrastLargeAA float32 = 3
+
+	// ContrastGraphicsAA is the contrast ratio required by WCAG AA for graphical objects
+	// and active user interface components like graphs, icons, and form input borders
+	ContrastGraphicsAA float32 = 3
+
+	// ContrastAAA is the contrast ratio required by WCAG AAA for body text
+	ContrastAAA float32 = 7
+
+	// ContrastLargeAAA is the contrast ratio required by WCAG AAA for large text
+	// (at least 120-150% larger than the body text)
+	ContrastLargeAAA float32 = 4.5
+)
+
+// ContrastRatio returns the contrast ratio between the given two colors.
+// The contrast ratio will be between 1 and 21.
+func ContrastRatio(a, b color.Color) float32 {
+	ah := FromColor(a)
+	bh := FromColor(b)
+	return ToneContrastRatio(ah.Tone, bh.Tone)
+}
+
+// ToneContrastRatio returns the contrast ratio between the given two tones.
+// The contrast ratio will be between 1 and 21, and the tones should be
+// between 0 and 100 and will be clamped to such.
+func ToneContrastRatio(a, b float32) float32 {
+	a = math32.Clamp(a, 0, 100)
+	b = math32.Clamp(b, 0, 100)
+	return ContrastRatioOfYs(cie.LToY(a), cie.LToY(b))
+}
+
+// ContrastColor returns the color that will ensure that the given contrast ratio
+// between the given color and the resulting color is met. If the given ratio can
+// not be achieved with the given color, it returns the color that would result in
+// the highest contrast ratio. The ratio must be between 1 and 21. If the tone of
+// the given color is greater than 50, it tries darker tones first, and otherwise
+// it tries lighter tones first.
+func ContrastColor(c color.Color, ratio float32) color.RGBA {
+	h := FromColor(c)
+	ct := ContrastTone(h.Tone, ratio)
+	return h.WithTone(ct).AsRGBA()
+}
+
+// ContrastColorTry returns the color that will ensure that the given contrast ratio
+// between the given color and the resulting color is met. It returns color.RGBA{}, false if
+// the given ratio can not be achieved with the given color. The ratio must be between
+// 1 and 21. If the tone of the given color is greater than 50, it tries darker tones first,
+// and otherwise it tries lighter tones first.
+func ContrastColorTry(c color.Color, ratio float32) (color.RGBA, bool) {
+	h := FromColor(c)
+	ct, ok := ContrastToneTry(h.Tone, ratio)
+	if !ok {
+		return color.RGBA{}, false
+	}
+	return h.WithTone(ct).AsRGBA(), true
+}
+
+// ContrastTone returns the tone that will ensure that the given contrast ratio
+// between the given tone and the resulting tone is met. If the given ratio can
+// not be achieved with the given tone, it returns the tone that would result in
+// the highest contrast ratio. The tone must be between 0 and 100 and the ratio must be
+// between 1 and 21. If the given tone is greater than 50, it tries darker tones first,
+// and otherwise it tries lighter tones first.
+func ContrastTone(tone, ratio float32) float32 {
+	ct, ok := ContrastToneTry(tone, ratio)
+	if ok {
+		return ct
+	}
+	dcr := ToneContrastRatio(tone, 0)
+	lcr := ToneContrastRatio(tone, 100)
+	if dcr > lcr {
+		return 0
+	}
+	return 100
+}
+
+// ContrastToneTry returns the tone that will ensure that the given contrast ratio
+// between the given tone and the resulting tone is met. It returns -1, false if
+// the given ratio can not be achieved with the given tone. The tone must be between 0
+// and 100 and the ratio must be between 1 and 21. If the given tone is greater than 50,
+// it tries darker tones first, and otherwise it tries lighter tones first.
+func ContrastToneTry(tone, ratio float32) (float32, bool) {
+	if tone > 50 {
+		d, ok := ContrastToneDarkerTry(tone, ratio)
+		if ok {
+			return d, true
+		}
+		l, ok := ContrastToneLighterTry(tone, ratio)
+		if ok {
+			return l, true
+		}
+		return -1, false
+	}
+
+	l, ok := ContrastToneLighterTry(tone, ratio)
+	if ok {
+		return l, true
+	}
+	d, ok := ContrastToneDarkerTry(tone, ratio)
+	if ok {
+		return d, true
+	}
+	return -1, false
+}
+
+// ContrastToneLighter returns a tone greater than or equal to the given tone
+// that ensures that given contrast ratio between the two tones is met.
+// It returns 100 if the given ratio can not be achieved with the
+// given tone. The tone must be between 0 and 100 and the ratio must be
+// between 1 and 21.
+func ContrastToneLighter(tone, ratio float32) float32 {
+	safe, ok := ContrastToneLighterTry(tone, ratio)
+	if ok {
+		return safe
+	}
+	return 100
+}
+
+// ContrastToneDarker returns a tone less than or equal to the given tone
+// that ensures that given contrast ratio between the two tones is met.
+// It returns 0 if the given ratio can not be achieved with the
+// given tone. The tone must be between 0 and 100 and the ratio must be
+// between 1 and 21.
+func ContrastToneDarker(tone, ratio float32) float32 {
+	safe, ok := ContrastToneDarkerTry(tone, ratio)
+	if ok {
+		return safe
+	}
+	return 0
+}
+
+// ContrastToneLighterTry returns a tone greater than or equal to the given tone
+// that ensures that given contrast ratio between the two tones is met.
+// It returns -1, false if the given ratio can not be achieved with the
+// given tone. The tone must be between 0 and 100 and the ratio must be
+// between 1 and 21.
+func ContrastToneLighterTry(tone, ratio float32) (float32, bool) {
+	if tone < 0 || tone > 100 {
+		return -1, false
+	}
+
+	darkY := cie.LToY(tone)
+	lightY := ratio*(darkY+5) - 5
+	realContrast := ContrastRatioOfYs(lightY, darkY)
+	delta := math32.Abs(realContrast - ratio)
+	if realContrast < ratio && delta > 0.04 {
+		return -1, false
+	}
+
+	// TODO(kai/cam): this +0.4 explained by the comment below only seems to cause problems
+	// Ensure gamut mapping, which requires a 'range' on tone, will still result
+	// the correct ratio by darkening slightly.
+	ret := cie.YToL(lightY) // + 0.4
+	if ret < 0 || ret > 100 {
+		return -1, false
+	}
+	return ret, true
+}
+
+// ContrastToneDarkerTry returns a tone less than or equal to the given tone
+// that ensures that given contrast ratio between the two tones is met.
+// It returns -1, false if the given ratio can not be achieved with the
+// given tone. The tone must be between 0 and 100 and the ratio must be
+// between 1 and 21.
+func ContrastToneDarkerTry(tone, ratio float32) (float32, bool) {
+	if tone < 0 || tone > 100 {
+		return -1, false
+	}
+
+	lightY := cie.LToY(tone)
+	darkY := ((lightY + 5) / ratio) - 5
+	realContrast := ContrastRatioOfYs(lightY, darkY)
+	delta := math32.Abs(realContrast - ratio)
+	if realContrast < ratio && delta > 0.04 {
+		return -1, false
+	}
+
+	// TODO(kai/cam): this -0.4 explained by the comment below only seems to cause problems
+	// Ensure gamut mapping, which requires a 'range' on tone, will still result
+	// the correct ratio by darkening slightly.
+	ret := cie.YToL(darkY) // - 0.4
+	if ret < 0 || ret > 100 {
+		return -1, false
+	}
+	return ret, true
+}
+
+// ContrastRatioOfYs returns the contrast ratio of two XYZ Y values.
+func ContrastRatioOfYs(a, b float32) float32 {
+	lighter := max(a, b)
+	darker := min(a, b)
+	return (lighter + 5) / (darker + 5)
+}

+ 239 - 0
vendor/cogentcore.org/core/colors/cam/hct/hct.go

@@ -0,0 +1,239 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Adapted from https://github.com/material-foundation/material-color-utilities
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hct
+
+import (
+	"fmt"
+	"image/color"
+
+	"cogentcore.org/core/colors/cam/cam16"
+	"cogentcore.org/core/colors/cam/cie"
+)
+
+// HCT represents a color as hue, chroma, and tone. HCT is a color system
+// that provides a perceptually accurate color measurement system that can
+// also accurately render what colors will appear as in different lighting
+// environments. Directly setting the values of the HCT and RGB fields will
+// have no effect on the underlying color; instead, use the Set methods
+// ([HCT.SetHue], etc). The A field (transparency) can be set directly.
+type HCT struct {
+
+	// Hue (h) is the spectral identity of the color
+	// (red, green, blue etc) in degrees (0-360)
+	Hue float32 `min:"0" max:"360"`
+
+	// Chroma (C) is the colorfulness/saturation of the color.
+	// Grayscale colors have no chroma, and fully saturated ones
+	// have high chroma. The maximum varies as a function of hue
+	// and tone, but 120 is a general upper bound (see
+	// [HCT.MaximumChroma] to get a specific value).
+	Chroma float32 `min:"0" max:"120"`
+
+	// Tone is the L* component from the LAB (L*a*b*) color system,
+	// which is linear in human perception of lightness.
+	// It ranges from 0 to 100.
+	Tone float32 `min:"0" max:"100"`
+
+	// sRGB standard gamma-corrected 0-1 normalized RGB representation
+	// of the color. Critically, components are not premultiplied by alpha.
+	R, G, B, A float32
+}
+
+// New returns a new HCT representation for given parameters:
+// hue = 0..360
+// chroma = 0..? depends on other params
+// tone = 0..100
+// also computes and sets the sRGB normalized, gamma corrected RGB values
+// while keeping the sRGB representation within its gamut,
+// which may cause the chroma to decrease until it is inside the gamut.
+func New(hue, chroma, tone float32) HCT {
+	r, g, b := SolveToRGB(hue, chroma, tone)
+	return SRGBToHCT(r, g, b)
+}
+
+// FromColor constructs a new HCT color from a standard [color.Color].
+func FromColor(c color.Color) HCT {
+	return Uint32ToHCT(c.RGBA())
+}
+
+// SetColor sets the HCT color from a standard [color.Color].
+func (h *HCT) SetColor(c color.Color) {
+	*h = FromColor(c)
+}
+
+// Model is the standard [color.Model] that converts colors to HCT.
+var Model = color.ModelFunc(model)
+
+func model(c color.Color) color.Color {
+	if h, ok := c.(HCT); ok {
+		return h
+	}
+	return FromColor(c)
+}
+
+// RGBA implements the color.Color interface.
+// Performs the premultiplication of the RGB components by alpha at this point.
+func (h HCT) RGBA() (r, g, b, a uint32) {
+	return cie.SRGBFloatToUint32(h.R, h.G, h.B, h.A)
+}
+
+// AsRGBA returns a standard color.RGBA type
+func (h HCT) AsRGBA() color.RGBA {
+	r, g, b, a := cie.SRGBFloatToUint8(h.R, h.G, h.B, h.A)
+	return color.RGBA{r, g, b, a}
+}
+
+// SetUint32 sets components from unsigned 32bit integers (alpha-premultiplied)
+func (h *HCT) SetUint32(r, g, b, a uint32) {
+	fr, fg, fb, fa := cie.SRGBUint32ToFloat(r, g, b, a)
+	*h = SRGBAToHCT(fr, fg, fb, fa)
+}
+
+// SetHue sets the hue of this color. Chroma may decrease because chroma has a
+// different maximum for any given hue and tone.
+// 0 <= hue < 360; invalid values are corrected.
+func (h *HCT) SetHue(hue float32) *HCT {
+	r, g, b := SolveToRGB(hue, h.Chroma, h.Tone)
+	*h = SRGBAToHCT(r, g, b, h.A)
+	return h
+}
+
+// WithHue is like [SetHue] except it returns a new color
+// instead of setting the existing one.
+func (h HCT) WithHue(hue float32) HCT {
+	r, g, b := SolveToRGB(hue, h.Chroma, h.Tone)
+	return SRGBAToHCT(r, g, b, h.A)
+}
+
+// SetChroma sets the chroma of this color (0 to max that depends on other params),
+// while keeping the sRGB representation within its gamut,
+// which may cause the chroma to decrease until it is inside the gamut.
+func (h *HCT) SetChroma(chroma float32) *HCT {
+	r, g, b := SolveToRGB(h.Hue, chroma, h.Tone)
+	*h = SRGBAToHCT(r, g, b, h.A)
+	return h
+}
+
+// WithChroma is like [SetChroma] except it returns a new color
+// instead of setting the existing one.
+func (h HCT) WithChroma(chroma float32) HCT {
+	r, g, b := SolveToRGB(h.Hue, chroma, h.Tone)
+	return SRGBAToHCT(r, g, b, h.A)
+}
+
+// SetTone sets the tone of this color (0 < tone < 100),
+// while keeping the sRGB representation within its gamut,
+// which may cause the chroma to decrease until it is inside the gamut.
+func (h *HCT) SetTone(tone float32) *HCT {
+	r, g, b := SolveToRGB(h.Hue, h.Chroma, tone)
+	*h = SRGBAToHCT(r, g, b, h.A)
+	return h
+}
+
+// WithTone is like [SetTone] except it returns a new color
+// instead of setting the existing one.
+func (h HCT) WithTone(tone float32) HCT {
+	r, g, b := SolveToRGB(h.Hue, h.Chroma, tone)
+	return SRGBAToHCT(r, g, b, h.A)
+}
+
+// MaximumChroma returns the maximum [HCT.Chroma] value for the hue
+// and tone of this color. This will always be between 0 and 120.
+func (h HCT) MaximumChroma() float32 {
+	// WithChroma does a round trip, so the resultant chroma will only
+	// be as high as the maximum chroma.
+	return h.WithChroma(120).Chroma
+}
+
+// SRGBAToHCT returns an HCT from the given SRGBA color coordinates
+// under standard viewing conditions. The RGB value range is 0-1,
+// and RGB values have gamma correction. The RGB values must not be
+// premultiplied by the given alpha value. See [SRGBToHCT] for
+// a version that does not take the alpha value.
+func SRGBAToHCT(r, g, b, a float32) HCT {
+	h := SRGBToHCT(r, g, b)
+	h.A = a
+	return h
+}
+
+// SRGBToHCT returns an HCT from the given SRGB color coordinates
+// under standard viewing conditions. The RGB value range is 0-1,
+// and RGB values have gamma correction. Alpha is always 1; see
+// [SRGBAToHCT] for a version that takes the alpha value.
+func SRGBToHCT(r, g, b float32) HCT {
+	x, y, z := cie.SRGBToXYZ(r, g, b)
+	cam := cam16.FromXYZ(100*x, 100*y, 100*z)
+	l, _, _ := cie.XYZToLAB(x, y, z)
+	return HCT{Hue: cam.Hue, Chroma: cam.Chroma, Tone: l, R: r, G: g, B: b, A: 1}
+}
+
+// Uint32ToHCT returns an HCT from given SRGBA uint32 color coordinates,
+// which are used for interchange among image.Color types.
+// Uses standard viewing conditions, and RGB values already have gamma correction
+// (i.e., they are SRGB values).
+func Uint32ToHCT(r, g, b, a uint32) HCT {
+	h := HCT{}
+	h.SetUint32(r, g, b, a)
+	return h
+}
+
+func (h HCT) String() string {
+	return fmt.Sprintf("hct(%g, %g, %g)", h.Hue, h.Chroma, h.Tone)
+}
+
+/*
+  // Translate a color into different [ViewingConditions].
+  //
+  // Colors change appearance. They look different with lights on versus off,
+  // the same color, as in hex code, on white looks different when on black.
+  // This is called color relativity, most famously explicated by Josef Albers
+  // in Interaction of Color.
+  //
+  // In color science, color appearance models can account for this and
+  // calculate the appearance of a color in different settings. HCT is based on
+  // CAM16, a color appearance model, and uses it to make these calculations.
+  //
+  // See [ViewingConditions.make] for parameters affecting color appearance.
+  Hct inViewingConditions(ViewingConditions vc) {
+    // 1. Use CAM16 to find XYZ coordinates of color in specified VC.
+    final cam16 = Cam16.fromInt(toInt());
+    final viewedInVc = cam16.xyzInViewingConditions(vc);
+
+    // 2. Create CAM16 of those XYZ coordinates in default VC.
+    final recastInVc = Cam16.fromXyzInViewingConditions(
+      viewedInVc[0],
+      viewedInVc[1],
+      viewedInVc[2],
+      ViewingConditions.make(),
+    );
+
+    // 3. Create HCT from:
+    // - CAM16 using default VC with XYZ coordinates in specified VC.
+    // - L* converted from Y in XYZ coordinates in specified VC.
+    final recastHct = Hct.from(
+      recastInVc.hue,
+      recastInVc.chroma,
+      ColorUtils.lstarFromY(viewedInVc[1]),
+    );
+    return recastHct;
+  }
+}
+
+*/

+ 118 - 0
vendor/cogentcore.org/core/colors/cam/hct/solver.go

@@ -0,0 +1,118 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Adapted from https://github.com/material-foundation/material-color-utilities
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hct
+
+import (
+	"cogentcore.org/core/colors/cam/cam16"
+	"cogentcore.org/core/colors/cam/cie"
+	"cogentcore.org/core/math32"
+)
+
+// SolveToRGBLin Finds an sRGB linear color (represented by math32.Vector3, 0-100 range)
+// with the given hue, chroma, and tone, if possible.
+// if not possible to represent the target values, the hue and tone will be
+// sufficiently close, and chroma will be maximized.
+func SolveToRGBLin(hue, chroma, tone float32) math32.Vector3 {
+	if chroma < 0.0001 || tone < 0.0001 || tone > 99.9999 {
+		y := cie.LToY(tone)
+		return math32.Vec3(y, y, y)
+	}
+	tone = math32.Clamp(tone, 0, 100)
+	hue_deg := cam16.SanitizeDegrees(hue)
+	hue_rad := math32.DegToRad(hue_deg)
+	y := cie.LToY(tone)
+	exact := FindResultByJ(hue_rad, chroma, y)
+	if exact != nil {
+		return *exact
+	}
+	return BisectToLimit(y, hue_rad)
+}
+
+// SolveToRGB Finds an sRGB (gamma corrected, 0-1 range) color
+// with the given hue, chroma, and tone, if possible.
+// if not possible to represent the target values, the hue and tone will be
+// sufficiently close, and chroma will be maximized.
+func SolveToRGB(hue, chroma, tone float32) (r, g, b float32) {
+	lin := SolveToRGBLin(hue, chroma, tone)
+	r, g, b = cie.SRGBFromLinear100(lin.X, lin.Y, lin.Z)
+	return
+}
+
+// Finds a color with the given hue, chroma, and Y.
+// @param hue_radians The desired hue in radians.
+// @param chroma The desired chroma.
+// @param y The desired Y.
+// @return The desired color as linear sRGB values.
+func FindResultByJ(hue_rad, chroma, y float32) *math32.Vector3 {
+	// Initial estimate of j.
+	j := math32.Sqrt(y) * 11
+
+	// ===========================================================
+	// Operations inlined from Cam16 to avoid repeated calculation
+	// ===========================================================
+	vw := cam16.NewStdView()
+	t_inner_coeff := 1 / math32.Pow(1.64-math32.Pow(0.29, vw.BgYToWhiteY), 0.73)
+	e_hue := 0.25 * (math32.Cos(hue_rad+2) + 3.8)
+	p1 := e_hue * (50000 / 13) * vw.NC * vw.NCB
+	h_sin := math32.Sin(hue_rad)
+	h_cos := math32.Cos(hue_rad)
+	for itr := 0; itr < 5; itr++ {
+		j_norm := j / 100
+		alpha := float32(0)
+		if !(chroma == 0 || j == 0) {
+			alpha = chroma / math32.Sqrt(j_norm)
+		}
+		t := math32.Pow(alpha*t_inner_coeff, 1/0.9)
+		ac := vw.AW * math32.Pow(j_norm, 1/vw.C/vw.Z)
+		p2 := ac / vw.NBB
+		gamma := 23 * (p2 + 0.305) * t / (23*p1 + 11*t*h_cos + 108*t*h_sin)
+		a := gamma * h_cos
+		b := gamma * h_sin
+		r_a := (460*p2 + 451*a + 288*b) / 1403
+		g_a := (460*p2 - 891*a - 261*b) / 1403
+		b_a := (460*p2 - 220*a - 6300*b) / 1403
+		r_c_scaled := cam16.InverseChromaticAdapt(r_a)
+		g_c_scaled := cam16.InverseChromaticAdapt(g_a)
+		b_c_scaled := cam16.InverseChromaticAdapt(b_a)
+		scaled := math32.Vec3(r_c_scaled, g_c_scaled, b_c_scaled)
+		linrgb := MatMul(scaled, kLinrgbFromScaledDiscount)
+
+		if linrgb.X < 0 || linrgb.Y < 0 || linrgb.Z < 0 {
+			return nil
+		}
+		k_r := kYFromLinrgb[0]
+		k_g := kYFromLinrgb[1]
+		k_b := kYFromLinrgb[2]
+		fnj := k_r*linrgb.X + k_g*linrgb.Y + k_b*linrgb.Z
+		if fnj <= 0 {
+			return nil
+		}
+		if itr == 4 || math32.Abs(fnj-y) < 0.002 {
+			if linrgb.X > 100.01 || linrgb.Y > 100.01 || linrgb.Z > 100.01 {
+				return nil
+			}
+			return &linrgb
+		}
+		// Iterates with Newton method,
+		// Using 2 * fn(j) / j as the approximation of fn'(j)
+		j = j - (fnj-y)*j/(2*fnj)
+	}
+	return nil
+}

+ 138 - 0
vendor/cogentcore.org/core/colors/cam/hct/transform.go

@@ -0,0 +1,138 @@
+// Copyright (c) 2023, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package hct
+
+import (
+	"image/color"
+
+	"cogentcore.org/core/math32"
+)
+
+// Lighten returns a color that is lighter by the
+// given absolute HCT tone amount (0-100, ranges enforced)
+func Lighten(c color.Color, amount float32) color.RGBA {
+	h := FromColor(c)
+	h.SetTone(h.Tone + amount)
+	return h.AsRGBA()
+}
+
+// Darken returns a color that is darker by the
+// given absolute HCT tone amount (0-100, ranges enforced)
+func Darken(c color.Color, amount float32) color.RGBA {
+	h := FromColor(c)
+	h.SetTone(h.Tone - amount)
+	return h.AsRGBA()
+}
+
+// Highlight returns a color that is lighter or darker by the
+// given absolute HCT tone amount (0-100, ranges enforced),
+// making the color darker if it is light (tone >= 50) and
+// lighter otherwise. It is the opposite of [Samelight].
+func Highlight(c color.Color, amount float32) color.RGBA {
+	h := FromColor(c)
+	if h.Tone >= 50 {
+		h.SetTone(h.Tone - amount)
+	} else {
+		h.SetTone(h.Tone + amount)
+	}
+	return h.AsRGBA()
+}
+
+// Samelight returns a color that is lighter or darker by the
+// given absolute HCT tone amount (0-100, ranges enforced),
+// making the color lighter if it is light (tone >= 50) and
+// darker otherwise. It is the opposite of [Highlight].
+func Samelight(c color.Color, amount float32) color.RGBA {
+	h := FromColor(c)
+	if h.Tone >= 50 {
+		h.SetTone(h.Tone + amount)
+	} else {
+		h.SetTone(h.Tone - amount)
+	}
+	return h.AsRGBA()
+}
+
+// Saturate returns a color that is more saturated by the
+// given absolute HCT chroma amount (0-max that depends
+// on other params but is around 150, ranges enforced)
+func Saturate(c color.Color, amount float32) color.RGBA {
+	h := FromColor(c)
+	h.SetChroma(h.Chroma + amount)
+	return h.AsRGBA()
+}
+
+// Desaturate returns a color that is less saturated by the
+// given absolute HCT chroma amount (0-max that depends
+// on other params but is around 150, ranges enforced)
+func Desaturate(c color.Color, amount float32) color.RGBA {
+	h := FromColor(c)
+	h.SetChroma(h.Chroma - amount)
+	return h.AsRGBA()
+}
+
+// Spin returns a color that has a different hue by the
+// given absolute HCT hue amount (±0-360, ranges enforced)
+func Spin(c color.Color, amount float32) color.RGBA {
+	h := FromColor(c)
+	h.SetHue(h.Hue + amount)
+	return h.AsRGBA()
+}
+
+// MinHueDistance finds the minimum distance between two hues.
+// A positive number means add to a to get to b.
+// A negative number means subtract from a to get to b.
+func MinHueDistance(a, b float32) float32 {
+	d1 := b - a
+	d2 := (b + 360) - a
+	d3 := (b - (a + 360))
+	d1a := math32.Abs(d1)
+	d2a := math32.Abs(d2)
+	d3a := math32.Abs(d3)
+	if d1a < d2a && d1a < d3a {
+		return d1
+	}
+	if d2a < d1a && d2a < d3a {
+		return d2
+	}
+	return d3
+}
+
+// Blend returns a color that is the given percent blend between the first
+// and second color; 10 = 10% of the first and 90% of the second, etc;
+// blending is done directly on non-premultiplied HCT values, and
+// a correctly premultiplied color is returned.
+func Blend(pct float32, x, y color.Color) color.RGBA {
+	hx := FromColor(x)
+	hy := FromColor(y)
+	pct = math32.Clamp(pct, 0, 100)
+	px := pct / 100
+	py := 1 - px
+
+	dhue := MinHueDistance(hx.Hue, hy.Hue)
+
+	// weight as a function of chroma strength: if near grey, hue is unreliable
+	cpy := py * hy.Chroma / (px*hx.Chroma + py*hy.Chroma)
+	hue := hx.Hue + cpy*dhue
+
+	chroma := px*hx.Chroma + py*hy.Chroma
+	tone := px*hx.Tone + py*hy.Tone
+	hr := New(hue, chroma, tone)
+	hr.A = px*hx.A + py*hy.A
+	return hr.AsRGBA()
+}
+
+// IsLight returns whether the given color is light
+// (has an HCT tone greater than or equal to 50)
+func IsLight(c color.Color) bool {
+	h := FromColor(c)
+	return h.Tone >= 50
+}
+
+// IsDark returns whether the given color is dark
+// (has an HCT tone less than 50)
+func IsDark(c color.Color) bool {
+	h := FromColor(c)
+	return h.Tone < 50
+}

Some files were not shown because too many files changed in this diff