Repository: cogentcore/core
Pull request #854 (2024-01-26 05:56:50)

Only store one gi.App that extends goosi.App
This provides a much nicer structure and developer experience.


Patch:
diff --git a/core/cmd/windowsinstaller.go.tmpl b/core/cmd/windowsinstaller.go.tmpl
index 1aa7621dd..96ae3ded8 100644
--- a/core/cmd/windowsinstaller.go.tmpl
+++ b/core/cmd/windowsinstaller.go.tmpl
@@ -15,7 +15,7 @@ var exe []byte
 var icon []byte
 
 func main() {
-	b := gi.NewAppBody("{{.Name}} Installer")
+	b := gi.NewBody("{{.Name}} Installer")
 	b.App().SetAppBarConfig(nil).SetIconBytes(icon)
 	bt := gi.NewButton(b).SetText("Install {{.Name}}")
 	bt.OnClick(func(e events.Event) {
diff --git a/core/core.go b/core/core.go
index dc24f596d..221b36503 100644
--- a/core/core.go
+++ b/core/core.go
@@ -12,7 +12,7 @@ import (
 )
 
 func main() {
-	opts := grease.DefaultOptions("core", "Cogent Core", "Command line tools for developing apps and libraries using the Cogent Core framework.")
+	opts := grease.DefaultOptions("Cogent Core", "Command line tools for developing apps and libraries using the Cogent Core framework.")
 	opts.DefaultFiles = []string{".core/config.toml"}
 	opts.SearchUp = true
 	grease.Run(opts, &config.Config{}, cmd.Build, cmd.Run, cmd.Pack, cmd.Install, generate.Generate, cmd.Init, cmd.Changed, cmd.Pull, cmd.Log, cmd.VersionRelease, cmd.Release, cmd.GetVersion, cmd.SetVersion, cmd.UpdateVersion)
diff --git a/core/generate/webcore.go b/core/generate/webcore.go
index 2c238514a..1f862863e 100644
--- a/core/generate/webcore.go
+++ b/core/generate/webcore.go
@@ -50,6 +50,7 @@ func GetWebcoreExamples(c *config.Config) (ordmap.Map[string, []byte], error) {
 		sc := bufio.NewScanner(f)
 		var curExample [][]byte
 		inExample := false
+		gotNewBody := false
 		numExamples := 0
 		for sc.Scan() {
 			b := sc.Bytes()
@@ -61,8 +62,9 @@ func GetWebcoreExamples(c *config.Config) (ordmap.Map[string, []byte], error) {
 				continue
 			}
 
-			// gi.NewAppBody counts as a new start so that full examples work
-			if bytes.Contains(b, []byte("gi.NewAppBody(")) {
+			// gi.NewBody counts as a new start so that full examples work
+			if !gotNewBody && bytes.Contains(b, []byte("gi.NewBody(")) {
+				gotNewBody = true
 				curExample = nil
 				curExample = append(curExample, []byte("b := parent"))
 				continue
@@ -85,6 +87,7 @@ func GetWebcoreExamples(c *config.Config) (ordmap.Map[string, []byte], error) {
 				examples.Add(id, bytes.Join(curExample, []byte{'\n'}))
 				curExample = nil
 				inExample = false
+				gotNewBody = false
 				numExamples++
 				continue
 			}
diff --git a/coredom/examples/html/html.go b/coredom/examples/html/html.go
index 5de3458d2..f7449cd91 100644
--- a/coredom/examples/html/html.go
+++ b/coredom/examples/html/html.go
@@ -16,7 +16,7 @@ import (
 var content string
 
 func main() {
-	b := gi.NewAppBody("Coredom HTML")
+	b := gi.NewBody("Coredom HTML")
 	grr.Log(coredom.ReadHTMLString(coredom.NewContext(), b, content))
 	b.RunMainWindow()
 }
diff --git a/coredom/examples/md/md.go b/coredom/examples/md/md.go
index 4e811f2e3..4f774fe43 100644
--- a/coredom/examples/md/md.go
+++ b/coredom/examples/md/md.go
@@ -16,7 +16,7 @@ import (
 var content string
 
 func main() {
-	b := gi.NewAppBody("Coredom MD")
+	b := gi.NewBody("Coredom MD")
 	grr.Log(coredom.ReadMDString(coredom.NewContext(), b, content))
 	b.RunMainWindow()
 }
diff --git a/docs/content/0-gettingstarted/helloworld.md b/docs/content/0-gettingstarted/helloworld.md
index 051752ed6..d1330d935 100644
--- a/docs/content/0-gettingstarted/helloworld.md
+++ b/docs/content/0-gettingstarted/helloworld.md
@@ -12,7 +12,7 @@ package main
 import "cogentcore.org/core/gi"
 
 func main() {
-	b := gi.NewAppBody("Hello")
+	b := gi.NewBody("Hello")
 	gi.NewButton(b).SetText("Hello, World!")
 	b.RunMainWindow()
 }
diff --git a/docs/content/basics/1-apps.md b/docs/content/basics/1-apps.md
index aae3b0f49..8bd1355c6 100644
--- a/docs/content/basics/1-apps.md
+++ b/docs/content/basics/1-apps.md
@@ -1,8 +1,8 @@
 # App creation and structure
 
-The first call in every Cogent Core app is [[gi.NewAppBody]]. This creates a new [[gi.App]] object, which holds information about the app like its name, icon, and description. It also creates and returns a new [[gi.Body]], which is a container in which app content is placed.
+The first call in every Cogent Core app is [[gi.NewBody]]. This creates and returns a new [[gi.Body]], which is a container in which app content is placed.
 
-After calling [[gi.NewAppBody]], you add content to the [[gi.Body]] that was returned, which is typically given the local variable name `b` for body.
+After calling [[gi.NewBody]], you add content to the [[gi.Body]] that was returned, which is typically given the local variable name `b` for body.
 
 Then, after adding content to your body, you can create and start a window from it using [[gi.Body.RunMainWindow]].
 
@@ -14,7 +14,7 @@ package main
 import "cogentcore.org/core/gi"
 
 func main() {
-	b := gi.NewAppBody("App Name")
+	b := gi.NewBody("App Name")
 	// Add app content here
 	b.RunMainWindow()
 }
diff --git a/docs/docs.go b/docs/docs.go
index 684669f4a..60422e1b7 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -19,7 +19,7 @@ import (
 var content embed.FS
 
 func main() {
-	b := gi.NewAppBody("Cogent Core Docs")
+	b := gi.NewBody("Cogent Core Docs")
 	pg := webcore.NewPage(b).SetSource(grr.Log1(fs.Sub(content, "content")))
 	b.AddAppBar(pg.AppBar)
 	pg.OpenURL("/", true)
diff --git a/enums/cmd/enumgen/enumgen.go b/enums/cmd/enumgen/enumgen.go
index e3f99a069..b0cfe676c 100644
--- a/enums/cmd/enumgen/enumgen.go
+++ b/enums/cmd/enumgen/enumgen.go
@@ -12,7 +12,7 @@ import (
 )
 
 func main() {
-	opts := grease.DefaultOptions("enumgen", "Enumgen", "Enumgen generates helpful methods for Go enums.")
+	opts := grease.DefaultOptions("Enumgen", "Enumgen generates helpful methods for Go enums.")
 	opts.DefaultFiles = []string{"enumgen.toml"}
 	grease.Run(opts, &enumgen.Config{}, enumgen.Generate)
 }
diff --git a/examples/demo/demo.go b/examples/demo/demo.go
index d250ecf17..bcb0291cc 100644
--- a/examples/demo/demo.go
+++ b/examples/demo/demo.go
@@ -28,9 +28,7 @@ import (
 var icon []byte
 
 func main() {
-	b := gi.NewAppBody("Cogent Core Demo")
-	b.App().SetIconBytes(icon).SetAbout("The Cogent Core Demo demonstrates the various features of the Cogent Core 2D and 3D Go GUI framework.")
-	b.App().SetSceneConfig(func(sc *gi.Scene) {
+	gi.TheApp.SetIconBytes(icon).SetSceneConfig(func(sc *gi.Scene) {
 		sc.OnWidgetAdded(func(w gi.Widget) {
 			switch w := w.(type) {
 			case *gi.Button, *giv.FuncButton:
@@ -41,6 +39,7 @@ func main() {
 		})
 	})
 
+	b := gi.NewBody("Cogent Core Demo")
 	ts := gi.NewTabs(b)
 
 	home(ts)
diff --git a/examples/filetree/filetree.go b/examples/filetree/filetree.go
index f9b950243..1fc7f993c 100644
--- a/examples/filetree/filetree.go
+++ b/examples/filetree/filetree.go
@@ -401,9 +401,8 @@ func (fb *FileBrowse) FileNodeOpened(fn *filetree.Node) {
 // path, returning the window and the path
 func NewFileBrowser(path string) (*FileBrowse, *gi.Stage) {
 	_, projnm, _, _ := ProjPathParse(path)
-	nm := "browser-" + projnm
 
-	b := gi.NewAppBody(nm).SetTitle("Browser: " + projnm)
+	b := gi.NewBody("Browser: " + projnm)
 	fb := NewFileBrowse(b, "browser")
 	b.AddAppBar(fb.ConfigToolbar)
 	fb.OpenPath(gi.Filename(path))
diff --git a/examples/hello/hello.go b/examples/hello/hello.go
index 3a4ae439d..75d0fa2cc 100644
--- a/examples/hello/hello.go
+++ b/examples/hello/hello.go
@@ -3,7 +3,7 @@ package main
 import "cogentcore.org/core/gi"
 
 func main() {
-	b := gi.NewAppBody("Hello")
+	b := gi.NewBody("Hello")
 	gi.NewButton(b).SetText("Hello, World!")
 	b.RunMainWindow()
 }
diff --git a/examples/icons/icons.go b/examples/icons/icons.go
index fc11d37ed..61da7408a 100644
--- a/examples/icons/icons.go
+++ b/examples/icons/icons.go
@@ -14,8 +14,7 @@ import (
 )
 
 func main() {
-	b := gi.NewAppBody("Cogent Core Icons")
-	b.App().About = `This is a demo of the icons in the <b>Cogent Core</b> graphical interface system, within the <b>Goki</b> tree framework.  See <a href="https://github.com/goki">Cogent Core on GitHub</a>`
+	b := gi.NewBody("Cogent Core Icons")
 
 	// note: can add a path to view other icon sets
 	// svg.CurIconSet.OpenIconsFromPath("/Users/oreilly/github/inkscape/share/icons/multicolor/symbolic/actions")
diff --git a/examples/layout/layout.go b/examples/layout/layout.go
index a2120fcef..a53c3c9e6 100644
--- a/examples/layout/layout.go
+++ b/examples/layout/layout.go
@@ -102,8 +102,7 @@ func BoxFrame(par gi.Widget, nm ...string) *gi.Frame {
 }
 
 func main() {
-	b := gi.NewAppBody("Cogent Core Layout Demo")
-	b.App().About = `This is a demo of the layout functions in the <b>Cogent Core</b> graphical interface system, within the <b>Goki</b> tree framework.  See <a href="https://github.com/goki">Cogent Core on GitHub</a>`
+	b := gi.NewBody("Cogent Core Layout Demo")
 
 	ctrl := &Control{}
 	ctrl.Grow.Set(1, 1)
diff --git a/examples/texteditor/texteditor.go b/examples/texteditor/texteditor.go
index ced4bf3c9..7e217617a 100644
--- a/examples/texteditor/texteditor.go
+++ b/examples/texteditor/texteditor.go
@@ -17,36 +17,35 @@ var samplefile gi.Filename = "texteditor.go"
 // var samplefile gi.Filename = "../../README.md"
 
 func main() {
-	b := gi.NewAppBody("Cogent Core Text Editor Demo")
-	b.App().About = `This is a demo of the texteditor.Editor in the <b>Cogent Core</b> graphical interface system, within the <b>Goki</b> tree framework.  See <a href="https://github.com/goki">Cogent Core on GitHub</a>`
+	b := gi.NewBody("Cogent Core Text Editor Demo")
 
-	splt := gi.NewSplits(b, "split-view")
-	splt.SetSplits(.5, .5)
+	sp := gi.NewSplits(b)
+	sp.SetSplits(.5, .5)
 	// these are all inherited so we can put them at the top "editor panel" level
-	splt.Style(func(s *styles.Style) {
+	sp.Style(func(s *styles.Style) {
 		s.Text.WhiteSpace = styles.WhiteSpacePreWrap
 		s.Text.TabSize = 4
 		s.Font.Family = string(gi.AppearanceSettings.MonoFont)
 	})
 
-	txed1 := texteditor.NewEditor(splt, "texteditor-1")
-	txed1.Style(func(s *styles.Style) {
+	te1 := texteditor.NewEditor(sp)
+	te1.Style(func(s *styles.Style) {
 		s.Min.X.Ch(20)
 		s.Min.Y.Ch(10)
 	})
-	txed2 := texteditor.NewEditor(splt, "texteditor-2")
-	txed2.Style(func(s *styles.Style) {
+	te2 := texteditor.NewEditor(sp)
+	te2.Style(func(s *styles.Style) {
 		s.Min.X.Ch(20)
 		s.Min.Y.Ch(10)
 	})
 
-	txbuf := texteditor.NewBuf()
-	txed1.SetBuf(txbuf)
-	txed2.SetBuf(txbuf)
+	tb := texteditor.NewBuf()
+	te1.SetBuf(tb)
+	te2.SetBuf(tb)
 
 	// txbuf.Hi.Lang = "Markdown" // "Makefile" // "Go" // "Markdown"
-	txbuf.Hi.Lang = "Go"
-	txbuf.Open(samplefile)
+	tb.Hi.Lang = "Go"
+	tb.Open(samplefile)
 	// pr := txbuf.Hi.PiLang.Parser()
 	// giv.InspectorDialog(&pr.Lexer)
 
diff --git a/examples/treeview/treeview.go b/examples/treeview/treeview.go
index c6c233fa4..e9fc61b95 100644
--- a/examples/treeview/treeview.go
+++ b/examples/treeview/treeview.go
@@ -32,9 +32,7 @@ func MakeTree(tv *giv.TreeView, iter, maxIter, maxKids int) {
 }
 
 func main() {
-	b := gi.NewAppBody("Cogent Core Tree View Demo")
-	b.App().About = `This is a demo of the treeview in the <b>Cogent Core</b> graphical interface system, within the <b>Goki</b> tree framework.  See <a href="https://github.com/goki">Cogent Core on GitHub</a>
-<p>Full Drag-and-Drop, Copy / Cut / Paste, and Keyboard Navigation is supported.</p>`
+	b := gi.NewBody("Cogent Core Tree View Demo")
 
 	splits := gi.NewSplits(b)
 
diff --git a/examples/views/views.go b/examples/views/views.go
index 7c7f2f40e..25d0e3e7f 100644
--- a/examples/views/views.go
+++ b/examples/views/views.go
@@ -127,9 +127,7 @@ func main() {
 	stru.Things = make([]*TableStruct, 2)
 	stru.Stuff = make([]float32, 3)
 
-	b := gi.NewAppBody("views").SetTitle("Cogent Core Views Test")
-
-	b.App().About = `This is a demo of the MapView and SliceView views in the <b>Cogent Core</b> graphical interface system, within the <b>Goki</b> tree framework.  See <a href="https://github.com/goki">Cogent Core on GitHub</a>`
+	b := gi.NewBody("Cogent Core Views Demo")
 
 	b.AddAppBar(func(tb *gi.Toolbar) {
 		gi.NewButton(tb, "slice-test").SetText("SliceDialog").
diff --git a/examples/widgets/widgets.go b/examples/widgets/widgets.go
index c7395bec4..e749e5166 100644
--- a/examples/widgets/widgets.go
+++ b/examples/widgets/widgets.go
@@ -18,9 +18,7 @@ import (
 )
 
 func main() {
-	b := gi.NewAppBody("Cogent Core Widgets Demo")
-
-	b.App().About = `This is a demo of the main widgets and general functionality of the <b>Cogent Core</b> graphical interface system, within the <b>Goki</b> tree framework.  See <a href="https://github.com/goki">Cogent Core on GitHub</a>. <p>The <a href="https://cogentcore.org/core/gi/v2/blob/master/examples/widgets/README.md">README</a> page for this example app has lots of further info.</p>`
+	b := gi.NewBody("Cogent Core Widgets Demo")
 
 	b.AddAppBar(func(tb *gi.Toolbar) { // put first in top app bar
 		gi.NewButton(tb).SetText("Button 1").
diff --git a/examples/xyz/xyz_demo.go b/examples/xyz/xyz_demo.go
index b9a9a1259..a6d42bde7 100644
--- a/examples/xyz/xyz_demo.go
+++ b/examples/xyz/xyz_demo.go
@@ -135,13 +135,9 @@ func (an *Anim) Animate() {
 
 func main() {
 	anim := &Anim{}
-	b := gi.NewAppBody("XYZ Demo")
-	b.App().About = `This is a demo of the 3D graphics aspect of the <b>Cogent Core</b> graphical interface system, within the <b>Goki</b> tree framework.  See <a href="https://github.com/goki">Cogent Core on GitHub</a>.
-<p>The <a href="https://github.com/goki/gi/blob/master/examples/xyz/README.md">README</a> page for this example app has further info.</p>`
+	b := gi.NewBody("XYZ Demo")
 
-	gi.NewLabel(b).SetText(`This is a demonstration of <b>XYZ</b>, the
-<a href="https://cogentcore.org/core/gi/v2">Cogent Core</a> <i>3D</i> Framework<br>
-See <a href="https://github.com/goki/gi/blob/master/examples/xyz/README.md">README</a> for detailed info and things to try.`).
+	gi.NewLabel(b).SetText(`This is a demonstration of <b>XYZ</b>, the <a href="https://cogentcore.org/core">Cogent Core</a> <i>3D</i> Framework.`).
 		SetType(gi.LabelHeadlineSmall).
 		Style(func(s *styles.Style) {
 			s.Text.Align = styles.Center
diff --git a/examples/xyz_viewer/xyz_viewer.go b/examples/xyz_viewer/xyz_viewer.go
index 5f8a74598..8d4fcabf5 100644
--- a/examples/xyz_viewer/xyz_viewer.go
+++ b/examples/xyz_viewer/xyz_viewer.go
@@ -20,8 +20,7 @@ import (
 )
 
 func main() {
-	b := gi.NewAppBody("XYZ Object Viewer")
-	b.App().About = `This is a viewer for the 3D graphics aspect of the <b>Cogent Core</b> graphical interface system, within the <b>Goki</b> tree framework.  See <a href="https://github.com/goki">Cogent Core on GitHub</a>. <p>The <a href="https://cogentcore.org/core/gi/v2/blob/master/examples/xyzviewer/README.md">README</a> page for this example app has further info.</p>`
+	b := gi.NewBody("XYZ Object Viewer")
 
 	sv := xyzv.NewSceneView(b)
 	sv.Config()
diff --git a/filetree/file.go b/filetree/file.go
index 2f30c27b1..43c52f441 100644
--- a/filetree/file.go
+++ b/filetree/file.go
@@ -22,7 +22,7 @@ import (
 // OSOpenCommand returns the generic file 'open' command to open file with default app
 // open on Mac, xdg-open on Linux, and start on Windows
 func OSOpenCommand() string {
-	switch gi.Platform() {
+	switch gi.TheApp.Platform() {
 	case goosi.MacOS:
 		return "open"
 	case goosi.Linux:
diff --git a/filetree/tree.go b/filetree/tree.go
index 4289f9263..c99c8976c 100644
--- a/filetree/tree.go
+++ b/filetree/tree.go
@@ -228,7 +228,7 @@ func (ft *Tree) WatchUpdt(path string) {
 // WatchPath adds given path to those watched
 func (ft *Tree) WatchPath(path gi.Filename) error {
 	return nil // TODO: disable for all platforms for now -- getting some issues
-	if gi.Platform() == goosi.MacOS {
+	if gi.TheApp.Platform() == goosi.MacOS {
 		return nil // mac is not supported in a high-capacity fashion at this point
 	}
 	rp := ft.RelPath(path)
diff --git a/gi/app.go b/gi/app.go
index 37de72898..c00f94a25 100644
--- a/gi/app.go
+++ b/gi/app.go
@@ -10,8 +10,6 @@ import (
 	"fmt"
 	"image"
 	"io"
-	"os"
-	"path/filepath"
 	"strings"
 
 	"cogentcore.org/core/colors"
@@ -27,16 +25,14 @@ import (
 	"cogentcore.org/core/units"
 )
 
-// App encapsulates various properties of the overall application,
-// including managing an AppBar and associated elements.
-type App struct { //gti:add -setters
-	// Name can be used in relevant window titles and prompts,
-	// and specifies the default application-specific data directory
-	Name string
+// TheApp is the current [App]; only one is ever in effect.
+var TheApp = &App{App: goosi.TheApp}
 
-	// About sets the 'about' info for the app, which appears as a menu option
-	// in the default app menu.
-	About string
+// App represents a Cogent Core app. It extends [goosi.App] to provide both system-level
+// and high-level data and functions to do with the currently running application. The
+// single instance of it is [TheApp], which embeds [goosi.TheApp].
+type App struct { //gti:add -setters
+	goosi.App `set:"-"`
 
 	// Icon specifies the app icon, which is passed to [goosi.Window.SetIcon].
 	// It should typically be set using [App.SetIconSVG].
@@ -44,10 +40,9 @@ type App struct { //gti:add -setters
 
 	// AppBarConfig is the function that configures the AppBar,
 	// typically put in the [Scene.Bars.Top] (i.e., a TopAppBar).
-	// Set to StdAppBarConfig by default, which makes the standard AppBar behavior.
-	// Most apps will define their own version to add App-specific
-	// functionality, and set this accordingly.
-	// If this is nil, then no TopAppBar will be created by default.
+	// It is set to StdAppBarConfig by default, which makes the
+	// standard AppBar behavior. If this is nil, then no AppBar
+	// will be created by default.
 	AppBarConfig func(pw Widget)
 
 	// SceneConfig is the function called on every newly created [gi.Scene]
@@ -57,27 +52,6 @@ type App struct { //gti:add -setters
 	SceneConfig func(sc *Scene)
 }
 
-// NewApp returns a new App initialized with the given name.
-func NewApp(name string) *App {
-	// we load the settings in NewApp so that people can
-	// add their own settings to AllSettings first
-	grr.Log(LoadAllSettings())
-
-	app := &App{}
-	app.Name = name
-	app.AppBarConfig = StdAppBarConfig
-	app.Config()
-	return app
-}
-
-// NewAppBody returns a new Body with a new App initialized with
-// the given name.
-func NewAppBody(name string) *Body {
-	b := NewBody(name)
-	b.SetApp(NewApp(name))
-	return b
-}
-
 // SetIconSVG sets the icon of the app to the given SVG icon.
 // It automatically logs any errors.
 func (a *App) SetIconSVG(r io.Reader) *App {
@@ -109,55 +83,6 @@ func (a *App) SetIconBytes(b []byte) *App {
 	return a.SetIconSVG(bytes.NewReader(b))
 }
 
-// Config performs one-time configuration steps after setting
-// properties on the App.
-func (a *App) Config() {
-	goosi.TheApp.SetName(a.Name)
-}
-
-// DataDir returns the application-specific data directory:
-// [goosi.DataDir] + [App.Name]. It ensures that the directory exists first.
-// Use this directory to store all app-specific data including settings.
-// DataDir is: Mac: ~/Library, Linux: ~/.config, Windows: ~/AppData/Roaming
-func (a *App) DataDir() string {
-	pdir := filepath.Join(goosi.TheApp.DataDir(), a.Name)
-	os.MkdirAll(pdir, 0755)
-	return pdir
-}
-
-// App returns the [App] this widget is contained in
-func (wb *WidgetBase) App() *App {
-	return wb.Scene.App
-}
-
-// Platform returns the current platform / operating system the app is running on.
-func Platform() goosi.Platforms {
-	return goosi.TheApp.Platform()
-}
-
-// SystemPlatform returns the current underlying platform / operating system the
-// app is running on. On platforms other than [goosi.Web], this is the same as
-// [Platform]. On [goosi.Web], it returns the underlying operating system.
-func SystemPlatform() goosi.Platforms {
-	return goosi.TheApp.SystemPlatform()
-}
-
-// todo: deal with this stuff too:
-
-// SetQuitReqFunc sets the function that is called whenever there is a
-// request to quit the app (via a OS or a call to QuitReq() method).  That
-// function can then adjudicate whether and when to actually call Quit.
-func SetQuitReqFunc(fun func()) {
-	goosi.TheApp.SetQuitReqFunc(fun)
-}
-
-// AddQuitCleanFunc adds the given function to a list that is called whenever
-// app is actually about to quit (irrevocably). Can do any necessary
-// last-minute cleanup here.
-func AddQuitCleanFunc(fun func()) {
-	goosi.TheApp.AddQuitCleanFunc(fun)
-}
-
 // Quit closes all windows and exits the program.
 func Quit() {
 	if !goosi.TheApp.IsQuitting() {
@@ -165,30 +90,6 @@ func Quit() {
 	}
 }
 
-// QuitReq requests to Quit -- calls QuitReqFunc if present
-func QuitReq() {
-	goosi.TheApp.QuitReq()
-}
-
-// OpenURL opens the given URL in the user's default browser.  On Linux
-// this requires that xdg-utils package has been installed -- uses
-// xdg-open command.
-func OpenURL(url string) {
-	goosi.TheApp.OpenURL(url)
-}
-
-// CogentCoreDataDir returns the Cogent Core data directory: [DataDir] + "CogentCoreDataDir".
-// It ensures that the directory exists first.
-func CogentCoreDataDir() string {
-	return goosi.TheApp.CogentCoreDataDir()
-}
-
-// DataDir returns the OS-specific data directory: Mac: ~/Library,
-// Linux: ~/.config, Windows: ~/AppData/Roaming
-func DataDir() string {
-	return goosi.TheApp.DataDir()
-}
-
 //////////////////////////////////////////////////////////////////////////////
 //		AppBar
 
@@ -271,7 +172,7 @@ func (tb *Toolbar) StdOverflowMenu(m *Scene) { //gti:add
 	})
 
 	// no window menu on single-window platforms
-	if Platform().IsMobile() {
+	if TheApp.Platform().IsMobile() {
 		return
 	}
 	NewButton(m).SetText("Window").SetMenu(func(m *Scene) {
@@ -296,7 +197,7 @@ func (tb *Toolbar) StdOverflowMenu(m *Scene) { //gti:add
 			})
 		NewButton(m).SetText("Quit").SetIcon(icons.Close).SetShortcut("Command+Q").
 			OnClick(func(e events.Event) {
-				QuitReq()
+				TheApp.QuitReq()
 			})
 		NewSeparator(m)
 		for _, w := range MainRenderWins {
diff --git a/gi/bars.go b/gi/bars.go
index 386ef0e7d..cc9fa0159 100644
--- a/gi/bars.go
+++ b/gi/bars.go
@@ -54,9 +54,9 @@ func (sc *Scene) AddAppBar(fun func(tb *Toolbar)) {
 // ConfigSceneBars configures the side control bars, for main scenes
 func (sc *Scene) ConfigSceneBars() {
 	// at last possible moment, add app-specific app bar config
-	if sc.App != nil && sc.App.AppBarConfig != nil {
+	if TheApp.AppBarConfig != nil && sc.Stage.Type.IsMain() {
 		if sc.Bars.Top.IsEmpty() {
-			sc.Bars.Top.Add(sc.App.AppBarConfig) // put in the top by default
+			sc.Bars.Top.Add(TheApp.AppBarConfig) // put in the top by default
 		}
 	}
 	if !sc.Bars.Top.IsEmpty() {
diff --git a/gi/body.go b/gi/body.go
index 2415258de..958dc76e2 100644
--- a/gi/body.go
+++ b/gi/body.go
@@ -6,6 +6,7 @@ package gi
 
 import (
 	"cogentcore.org/core/colors"
+	"cogentcore.org/core/grr"
 	"cogentcore.org/core/styles"
 )
 
@@ -23,12 +24,21 @@ type Body struct { //core:no-new
 // It will create its own parent Scene at this point, and has wrapper
 // functions to transparently manage everything that the Scene
 // typically manages during configuration, so you can usually avoid
-// having to access the Scene directly.
+// having to access the Scene directly. If a name is given and
+// the name of [TheApp] is unset, it sets it to the given name.
 func NewBody(name ...string) *Body {
 	bd := &Body{}
 	nm := "body"
 	if len(name) > 0 {
 		nm = name[0]
+		if TheApp.Name() == "" {
+			TheApp.SetName(nm)
+		}
+	}
+	if AppearanceSettings.Zoom == 0 {
+		// we load the settings in NewBody so that people can
+		// add their own settings to AllSettings first
+		grr.Log(LoadAllSettings())
 	}
 	bd.InitName(bd, nm)
 	bd.Title = nm
@@ -68,14 +78,6 @@ func (bd *Body) AddText(text string) *Body {
 	return bd
 }
 
-// SetApp sets the App of the Body's Scene
-func (bd *Body) SetApp(app *App) *Body {
-	bd.Scene.App = app
-	bd.Nm = app.Name
-	bd.Title = app.Name
-	return bd
-}
-
 // SetData sets the Body's [Scene.Data].
 func (bd *Body) SetData(data any) *Body {
 	bd.Scene.SetData(data)
diff --git a/gi/button.go b/gi/button.go
index 776cd2ca2..159820b00 100644
--- a/gi/button.go
+++ b/gi/button.go
@@ -404,7 +404,7 @@ func (bt *Button) ConfigWidget() {
 	}
 
 	sci := -1
-	if bt.Type == ButtonMenu && !SystemPlatform().IsMobile() {
+	if bt.Type == ButtonMenu && !TheApp.SystemPlatform().IsMobile() {
 		if indi < 0 && bt.Shortcut != "" {
 			config.Add(StretchType, "sc-stretch")
 			sci = len(config)
diff --git a/gi/eventmgr.go b/gi/eventmgr.go
index 558eca384..8c67938fd 100644
--- a/gi/eventmgr.go
+++ b/gi/eventmgr.go
@@ -422,7 +422,7 @@ func (em *EventMgr) HandlePosEvent(e events.Event) {
 		// a mouse up event acts also acts as a mouse leave
 		// event on mobile, as that is needed to clear any
 		// hovered state
-		if up != nil && Platform().IsMobile() {
+		if up != nil && TheApp.Platform().IsMobile() {
 			up.Send(events.MouseLeave, e)
 		}
 	case events.Scroll:
diff --git a/gi/gtigen.go b/gi/gtigen.go
index 92a8d2658..09c4ba89b 100644
--- a/gi/gtigen.go
+++ b/gi/gtigen.go
@@ -19,17 +19,7 @@ import (
 	"cogentcore.org/core/units"
 )
 
-var _ = gti.AddType(&gti.Type{Name: "cogentcore.org/core/gi.App", IDName: "app", Doc: "App encapsulates various properties of the overall application,\nincluding managing an AppBar and associated elements.", Directives: []gti.Directive{{Tool: "gti", Directive: "add", Args: []string{"-setters"}}}, Fields: []gti.Field{{Name: "Name", Doc: "Name can be used in relevant window titles and prompts,\nand specifies the default application-specific data directory"}, {Name: "About", Doc: "About sets the 'about' info for the app, which appears as a menu option\nin the default app menu."}, {Name: "Icon", Doc: "Icon specifies the app icon, which is passed to [goosi.Window.SetIcon].\nIt should typically be set using [App.SetIconSVG]."}, {Name: "AppBarConfig", Doc: "AppBarConfig is the function that configures the AppBar,\ntypically put in the [Scene.Bars.Top] (i.e., a TopAppBar).\nSet to StdAppBarConfig by default, which makes the standard AppBar behavior.\nMost apps will define their own version to add App-specific\nfunctionality, and set this accordingly.\nIf this is nil, then no TopAppBar will be created by default."}, {Name: "SceneConfig", Doc: "BodyConfig is the function called on every newly created [gi.Scene]\nto configure it, if it is non-nil. This can be used to set global\nconfiguration and styling for all widgets using the OnWidgetAdded\nmethod."}}})
-
-// SetName sets the [App.Name]:
-// Name can be used in relevant window titles and prompts,
-// and specifies the default application-specific data directory
-func (t *App) SetName(v string) *App { t.Name = v; return t }
-
-// SetAbout sets the [App.About]:
-// About sets the 'about' info for the app, which appears as a menu option
-// in the default app menu.
-func (t *App) SetAbout(v string) *App { t.About = v; return t }
+var _ = gti.AddType(&gti.Type{Name: "cogentcore.org/core/gi.App", IDName: "app", Doc: "App represents a Cogent Core app. It extends [goosi.App] to provide both system-level\nand high-level data and functions to do with the currently running application. The\nsingle instance of it is [TheApp], which embeds [goosi.TheApp].", Directives: []gti.Directive{{Tool: "gti", Directive: "add", Args: []string{"-setters"}}}, Embeds: []gti.Field{{Name: "App"}}, Fields: []gti.Field{{Name: "Icon", Doc: "Icon specifies the app icon, which is passed to [goosi.Window.SetIcon].\nIt should typically be set using [App.SetIconSVG]."}, {Name: "AppBarConfig", Doc: "AppBarConfig is the function that configures the AppBar,\ntypically put in the [Scene.Bars.Top] (i.e., a TopAppBar).\nSet to StdAppBarConfig by default, which makes the standard AppBar behavior.\nMost apps will define their own version to add App-specific\nfunctionality, and set this accordingly.\nIf this is nil, then no TopAppBar will be created by default."}, {Name: "SceneConfig", Doc: "SceneConfig is the function called on every newly created [gi.Scene]\nto configure it, if it is non-nil. This can be used to set global\nconfiguration and styling for all widgets using the OnWidgetAdded\nmethod."}}})
 
 // SetIcon sets the [App.Icon]:
 // Icon specifies the app icon, which is passed to [goosi.Window.SetIcon].
@@ -46,7 +36,7 @@ func (t *App) SetIcon(v ...image.Image) *App { t.Icon = v; return t }
 func (t *App) SetAppBarConfig(v func(pw Widget)) *App { t.AppBarConfig = v; return t }
 
 // SetSceneConfig sets the [App.SceneConfig]:
-// BodyConfig is the function called on every newly created [gi.Scene]
+// SceneConfig is the function called on every newly created [gi.Scene]
 // to configure it, if it is non-nil. This can be used to set global
 // configuration and styling for all widgets using the OnWidgetAdded
 // method.
@@ -609,7 +599,7 @@ func (t *Meter) SetValueColor(v image.Image) *Meter { t.ValueColor = v; return t
 func (t *Meter) SetTooltip(v string) *Meter { t.Tooltip = v; return t }
 
 // SceneType is the [gti.Type] for [Scene]
-var SceneType = gti.AddType(&gti.Type{Name: "cogentcore.org/core/gi.Scene", IDName: "scene", Doc: "Scene contains a Widget tree, rooted in an embedded Frame layout,\nwhich renders into its Pixels image.\nThe Scene is set in a Stage (pointer retained in Scene).\nStage has a StageMgr manager for controlling things like Popups\n(Menus and Dialogs, etc).\n\nEach Scene and Widget tree contains state specific to its particular usage\nwithin a given Stage and overall rendering context, representing the unit\nof rendering in the Cogent Core framework.", Directives: []gti.Directive{{Tool: "core", Directive: "no-new"}, {Tool: "core", Directive: "embedder"}}, Embeds: []gti.Field{{Name: "Frame"}}, Fields: []gti.Field{{Name: "App", Doc: "App is the pointer to the application to which this scene belongs.\nThe first Main Window Scene must set this, and others will automatically\ngrab from there."}, {Name: "Bars", Doc: "Bars contains functions for constructing the control bars for this Scene,\nattached to different sides of a Scene (e.g., TopAppBar at Top,\nNavBar at Bottom, etc).  Functions are called in forward order\nso first added are called first."}, {Name: "BarsInherit", Doc: "BarsInherit determines which of the Bars side functions are inherited\nfrom the context widget, for FullWindow Dialogs"}, {Name: "AppBars", Doc: "AppBars contains functions for configuring a top-level App toolbar,\n(e.g., TopAppBar) for elements contained within this Scene,\nthat should be represented in any app-level toolbar constructed\nfor this Scene."}, {Name: "Body", Doc: "Body provides the main contents of scenes that use control Bars\nto allow the main window contents to be specified separately\nfrom that dynamic control content.  When constructing scenes using\na Body, you can operate directly on the [Body], which has wrappers\nfor most major Scene functions."}, {Name: "Data", Doc: "Data is the optional data value being represented by this scene.\nUsed e.g., for recycling views of a given item instead of creating new one."}, {Name: "SceneGeom", Doc: "Size and position relative to overall rendering context."}, {Name: "PaintContext", Doc: "paint context for rendering"}, {Name: "Pixels", Doc: "live pixels that we render into"}, {Name: "EventMgr", Doc: "event manager for this scene"}, {Name: "Stage", Doc: "current stage in which this Scene is set"}, {Name: "RenderBBoxHue", Doc: "RenderBBoxHue is current hue for rendering bounding box in ScRenderBBoxes mode"}, {Name: "SelectedWidget", Doc: "the currently selected widget through the inspect editor selection mode"}, {Name: "SelectedWidgetChan", Doc: "the channel on which the selected widget through the inspect editor\nselection mode is transmitted to the inspect editor after the user is done selecting"}, {Name: "LastRender", Doc: "LastRender captures key params from last render.\nIf different then a new ApplyStyleScene is needed."}, {Name: "StyleMu", Doc: "StyleMu is RW mutex protecting access to Style-related global vars"}, {Name: "ShowIter", Doc: "ShowIter counts up at start of showing a Scene\nto trigger Show event and other steps at start of first show"}, {Name: "ReRender", Doc: "ReRender items are re-rendered after the current pass"}}, Instance: &Scene{}})
+var SceneType = gti.AddType(&gti.Type{Name: "cogentcore.org/core/gi.Scene", IDName: "scene", Doc: "Scene contains a Widget tree, rooted in an embedded Frame layout,\nwhich renders into its Pixels image.\nThe Scene is set in a Stage (pointer retained in Scene).\nStage has a StageMgr manager for controlling things like Popups\n(Menus and Dialogs, etc).\n\nEach Scene and Widget tree contains state specific to its particular usage\nwithin a given Stage and overall rendering context, representing the unit\nof rendering in the Cogent Core framework.", Directives: []gti.Directive{{Tool: "core", Directive: "no-new"}, {Tool: "core", Directive: "embedder"}}, Embeds: []gti.Field{{Name: "Frame"}}, Fields: []gti.Field{{Name: "Bars", Doc: "Bars contains functions for constructing the control bars for this Scene,\nattached to different sides of a Scene (e.g., TopAppBar at Top,\nNavBar at Bottom, etc).  Functions are called in forward order\nso first added are called first."}, {Name: "BarsInherit", Doc: "BarsInherit determines which of the Bars side functions are inherited\nfrom the context widget, for FullWindow Dialogs"}, {Name: "AppBars", Doc: "AppBars contains functions for configuring a top-level App toolbar,\n(e.g., TopAppBar) for elements contained within this Scene,\nthat should be represented in any app-level toolbar constructed\nfor this Scene."}, {Name: "Body", Doc: "Body provides the main contents of scenes that use control Bars\nto allow the main window contents to be specified separately\nfrom that dynamic control content.  When constructing scenes using\na Body, you can operate directly on the [Body], which has wrappers\nfor most major Scene functions."}, {Name: "Data", Doc: "Data is the optional data value being represented by this scene.\nUsed e.g., for recycling views of a given item instead of creating new one."}, {Name: "SceneGeom", Doc: "Size and position relative to overall rendering context."}, {Name: "PaintContext", Doc: "paint context for rendering"}, {Name: "Pixels", Doc: "live pixels that we render into"}, {Name: "EventMgr", Doc: "event manager for this scene"}, {Name: "Stage", Doc: "current stage in which this Scene is set"}, {Name: "RenderBBoxHue", Doc: "RenderBBoxHue is current hue for rendering bounding box in ScRenderBBoxes mode"}, {Name: "SelectedWidget", Doc: "the currently selected widget through the inspect editor selection mode"}, {Name: "SelectedWidgetChan", Doc: "the channel on which the selected widget through the inspect editor\nselection mode is transmitted to the inspect editor after the user is done selecting"}, {Name: "LastRender", Doc: "LastRender captures key params from last render.\nIf different then a new ApplyStyleScene is needed."}, {Name: "StyleMu", Doc: "StyleMu is RW mutex protecting access to Style-related global vars"}, {Name: "ShowIter", Doc: "ShowIter counts up at start of showing a Scene\nto trigger Show event and other steps at start of first show"}, {Name: "ReRender", Doc: "ReRender items are re-rendered after the current pass"}}, Instance: &Scene{}})
 
 // KiType returns the [*gti.Type] of [Scene]
 func (t *Scene) KiType() *gti.Type { return SceneType }
@@ -637,12 +627,6 @@ func AsScene(k ki.Ki) *Scene {
 // AsScene satisfies the [SceneEmbedder] interface
 func (t *Scene) AsScene() *Scene { return t }
 
-// SetApp sets the [Scene.App]:
-// App is the pointer to the application to which this scene belongs.
-// The first Main Window Scene must set this, and others will automatically
-// grab from there.
-func (t *Scene) SetApp(v *App) *Scene { t.App = v; return t }
-
 // SetBars sets the [Scene.Bars]:
 // Bars contains functions for constructing the control bars for this Scene,
 // attached to different sides of a Scene (e.g., TopAppBar at Top,
diff --git a/gi/init.go b/gi/init.go
index 48af3d5a4..3e429dd21 100644
--- a/gi/init.go
+++ b/gi/init.go
@@ -15,14 +15,14 @@ import (
 func init() {
 	goosi.HandleRecover = HandleRecover
 	goosi.InitScreenLogicalDPIFunc = AppearanceSettings.ApplyDPI // called when screens are initialized
-	CogentCoreDataDir()                                          // ensure it exists
-	WinGeomMgr.NeedToReload()                                    // gets time stamp associated with open, so it doesn't re-open
+	TheApp.AppBarConfig = StdAppBarConfig
+	TheApp.CogentCoreDataDir() // ensure it exists
+	WinGeomMgr.NeedToReload()  // gets time stamp associated with open, so it doesn't re-open
 	WinGeomMgr.Open()
 
-	// needed to prevent app from quitting prematurely
 	if testing.Testing() {
-		b := NewAppBody("__test-base")
-		b.App().AppBarConfig = nil
-		b.NewWindow().Run()
+		TheApp.AppBarConfig = nil
+		// needed to prevent app from quitting prematurely
+		NewBody().NewWindow().Run()
 	}
 }
diff --git a/gi/mainstage.go b/gi/mainstage.go
index 67ad3138f..92ed5171f 100644
--- a/gi/mainstage.go
+++ b/gi/mainstage.go
@@ -96,7 +96,7 @@ func (st *Stage) ConfigMainStage() {
 		st.FullWindow = true
 	}
 	// if we are on mobile, we can never have new windows
-	if Platform().IsMobile() {
+	if TheApp.Platform().IsMobile() {
 		st.NewWindow = false
 	}
 	sc := st.Scene
@@ -117,15 +117,8 @@ func (st *Stage) RunWindow() *Stage {
 		// and we need a *temporary* MainMgr to get initial pref size
 		st.SetMainMgr(st.FirstWinManager())
 	} else {
-		top := CurRenderWin.MainStageMgr.TopOfType(WindowStage)
-		if sc.App == nil && top != nil && top.Scene != nil { // inherit apps
-			sc.App = top.Scene.App
-		}
 		st.SetMainMgr(&CurRenderWin.MainStageMgr)
 	}
-	if sc.App == nil {
-		slog.Warn("Scene is missing App", "scene", sc)
-	}
 	st.ConfigMainStage()
 
 	sz := st.RenderCtx.Geom.Size
@@ -134,15 +127,15 @@ func (st *Stage) RunWindow() *Stage {
 	// non-offscreen mobile windows must take up the whole window
 	// and thus don't consider pref size
 	// desktop new windows and non-full windows can pref size
-	if Platform() == goosi.Offscreen ||
-		(!Platform().IsMobile() &&
+	if TheApp.Platform() == goosi.Offscreen ||
+		(!TheApp.Platform().IsMobile() &&
 			(st.NewWindow || !st.FullWindow || CurRenderWin == nil)) {
 		sz = sc.PrefSize(sz)
 		// on offscreen, we don't want any extra space, as we want the smallest
 		// possible representation of the content
 		// also, on offscreen, if the new size is bigger than the current size,
 		// we need to resize the window
-		if Platform() == goosi.Offscreen {
+		if TheApp.Platform() == goosi.Offscreen {
 			if CurRenderWin != nil {
 				csz := CurRenderWin.GoosiWin.Size()
 				nsz := csz
@@ -218,9 +211,6 @@ func (st *Stage) RunDialog() *Stage {
 	}
 
 	sc := st.Scene
-	if st.FullWindow {
-		sc.App = ctx.Scene.App
-	}
 	st.ConfigMainStage()
 	sc.SceneGeom.Pos = st.Pos
 
@@ -228,7 +218,6 @@ func (st *Stage) RunDialog() *Stage {
 
 	sz := ms.RenderCtx.Geom.Size
 	if !st.FullWindow || st.NewWindow {
-		sc.App = ctx.Scene.App // just for reference
 		sz = sc.PrefSize(sz)
 		sz = sz.Add(image.Point{50, 50})
 		sc.EventMgr.StartFocusFirst = true // popup dialogs always need focus
@@ -273,14 +262,12 @@ func (st *Stage) NewRenderWin() *RenderWin {
 	title := st.Title
 	opts := &goosi.NewWindowOptions{
 		Title:     title,
+		Icon:      TheApp.Icon,
 		Size:      st.Scene.SceneGeom.Size,
 		StdPixels: false,
 	}
-	if st.Scene.App != nil && st.Scene.App.Icon != nil {
-		opts.Icon = st.Scene.App.Icon
-	}
 	wgp := WinGeomMgr.Pref(title, nil)
-	if Platform() != goosi.Offscreen && wgp != nil {
+	if TheApp.Platform() != goosi.Offscreen && wgp != nil {
 		WinGeomMgr.SettingStart()
 		opts.Size = wgp.Size()
 		opts.Pos = wgp.Pos()
diff --git a/gi/recover.go b/gi/recover.go
index 92be9527f..b2ddb9be7 100644
--- a/gi/recover.go
+++ b/gi/recover.go
@@ -35,7 +35,7 @@ func HandleRecover(r any) {
 		AddText("There was an unexpected error and " + goosi.TheApp.Name() + " stopped running.")
 	b.AddBottomBar(func(pw Widget) {
 		NewButton(pw).SetText("Details").SetType(ButtonOutlined).OnClick(func(e events.Event) {
-			clpath := filepath.Join(CogentCoreDataDir(), "crash-logs", goosi.TheApp.Name())
+			clpath := filepath.Join(TheApp.AppDataDir(), "crash-logs")
 			txt := fmt.Sprintf("Crash log saved in %s\n\npanic: %v\n\n%s", clpath, r, stack)
 			d := NewBody("crash-details").AddTitle("Crash details")
 			NewLabel(d).SetText(txt).Style(func(s *styles.Style) {
diff --git a/gi/scene.go b/gi/scene.go
index ed248345e..6d2667bd4 100644
--- a/gi/scene.go
+++ b/gi/scene.go
@@ -42,11 +42,6 @@ import (
 type Scene struct {
 	Frame
 
-	// App is the pointer to the application to which this scene belongs.
-	// The first Main Window Scene must set this, and others will automatically
-	// grab from there.
-	App *App
-
 	// Bars contains functions for constructing the control bars for this Scene,
 	// attached to different sides of a Scene (e.g., TopAppBar at Top,
 	// NavBar at Bottom, etc).  Functions are called in forward order
@@ -143,26 +138,13 @@ func NewScene(name ...string) *Scene {
 	return sc
 }
 
-// TODO: what should we do about sub scenes?
-
-// NewSubScene creates a new [Scene] that will serve as a sub-scene of another [Scene].
-// Scenes can also be added as the content of a [Stage] (without a parent) through the
-// [NewScene] function. If no name is provided, it defaults to "scene".
-// func NewSubScene(par ki.Ki, name ...string) *Scene {
-// 	sc := par.NewChild(SceneType, name...).(*Scene)
-// 	sc.EventMgr.Scene = sc
-// 	return sc
-// }
-
 func (sc *Scene) OnInit() {
 	sc.Scene = sc
 	sc.WidgetBase.OnInit()
 	sc.SetStyles()
 	sc.Layout.HandleEvents()
-	if CurRenderWin != nil {
-		if app := CurRenderWin.MainScene().App; app != nil && app.SceneConfig != nil {
-			app.SceneConfig(sc)
-		}
+	if TheApp.SceneConfig != nil {
+		TheApp.SceneConfig(sc)
 	}
 }
 
@@ -229,7 +211,7 @@ func (sc *Scene) FitInWindow(winGeom mat32.Geom2DInt) {
 	geom := sc.SceneGeom
 	// full offscreen windows ignore any window geometry constraints
 	// because they must be unbounded by any previous window sizes
-	if Platform() != goosi.Offscreen || !sc.Stage.FullWindow {
+	if TheApp.Platform() != goosi.Offscreen || !sc.Stage.FullWindow {
 		geom = geom.FitInWindow(winGeom)
 	}
 	sc.Resize(geom)
@@ -279,7 +261,7 @@ func (sc *Scene) Close() {
 	if mm == nil {
 		return // todo: needed, but not sure why
 	}
-	if sc.Stage.NewWindow && !Platform().IsMobile() {
+	if sc.Stage.NewWindow && !TheApp.Platform().IsMobile() {
 		mm.RenderWin.CloseReq()
 		return
 	}
diff --git a/gi/settings.go b/gi/settings.go
index 09306739a..a27cbe8dc 100644
--- a/gi/settings.go
+++ b/gi/settings.go
@@ -78,7 +78,7 @@ type SettingsBase struct {
 	// Name is the name of the settings.
 	Name string `view:"-" save:"-"`
 
-	// File is the filename/filepath at which the settings are stored relative to [DataDir].
+	// File is the full filename/filepath at which the settings are stored.
 	File string `view:"-" save:"-"`
 }
 
@@ -89,7 +89,7 @@ func (sb *SettingsBase) Label() string {
 
 // Filename returns the full filename/filepath at which the settings are stored.
 func (sb *SettingsBase) Filename() string {
-	return filepath.Join(DataDir(), sb.File)
+	return sb.File
 }
 
 // Defaults does nothing by default and can be extended by other settings data types.
@@ -201,7 +201,7 @@ func UpdateAll() { //gti:add
 var AppearanceSettings = &AppearanceSettingsData{
 	SettingsBase: SettingsBase{
 		Name: "Appearance",
-		File: filepath.Join("CogentCore", "appearance-settings.toml"),
+		File: filepath.Join(TheApp.CogentCoreDataDir(), "appearance-settings.toml"),
 	},
 }
 
@@ -352,7 +352,7 @@ func (as *AppearanceSettingsData) DeleteSavedWindowGeoms() { //gti:add
 var DeviceSettings = &DeviceSettingsData{
 	SettingsBase: SettingsBase{
 		Name: "Device",
-		File: filepath.Join("CogentCore", "device-settings.toml"),
+		File: filepath.Join(TheApp.CogentCoreDataDir(), "device-settings.toml"),
 	},
 }
 
@@ -427,7 +427,7 @@ type ScreenSettings struct { //gti:add
 var SystemSettings = &SystemSettingsData{
 	SettingsBase: SettingsBase{
 		Name: "System",
-		File: filepath.Join("CogentCore", "system-settings.toml"),
+		File: filepath.Join(TheApp.CogentCoreDataDir(), "system-settings.toml"),
 	},
 }
 
@@ -684,21 +684,21 @@ var (
 	SavedPathsExtras = []string{FileViewResetPaths, FileViewEditPaths}
 )
 
-// SavePaths saves the active SavedPaths to prefs dir
+// SavePaths saves the active SavedPaths to data dir
 func SavePaths() {
 	StringsRemoveExtras((*[]string)(&SavedPaths), SavedPathsExtras)
-	pdir := CogentCoreDataDir()
+	pdir := TheApp.CogentCoreDataDir()
 	pnm := filepath.Join(pdir, SavedPathsFilename)
 	SavedPaths.Save(pnm)
 	// add back after save
 	SavedPaths = append(SavedPaths, SavedPathsExtras...)
 }
 
-// OpenPaths loads the active SavedPaths from prefs dir
+// OpenPaths loads the active SavedPaths from data dir
 func OpenPaths() {
 	// remove to be sure we don't have duplicate extras
 	StringsRemoveExtras((*[]string)(&SavedPaths), SavedPathsExtras)
-	pdir := CogentCoreDataDir()
+	pdir := TheApp.CogentCoreDataDir()
 	pnm := filepath.Join(pdir, SavedPathsFilename)
 	SavedPaths.Open(pnm)
 	// add back after save
@@ -712,7 +712,7 @@ func OpenPaths() {
 var DebugSettings = &DebugSettingsData{
 	SettingsBase: SettingsBase{
 		Name: "Debug",
-		File: filepath.Join("CogentCore", "debug-settings.toml"),
+		File: filepath.Join(TheApp.CogentCoreDataDir(), "debug-settings.toml"),
 	},
 }
 
diff --git a/gi/spell.go b/gi/spell.go
index 312c7de56..9d585dd82 100644
--- a/gi/spell.go
+++ b/gi/spell.go
@@ -36,7 +36,7 @@ func InitSpell() error {
 
 // OpenSpellModel loads a saved spelling model
 func OpenSpellModel() error {
-	pdir := CogentCoreDataDir()
+	pdir := TheApp.CogentCoreDataDir()
 	openpath := filepath.Join(pdir, "spell_en_us.json")
 	err := spell.Open(openpath)
 	if err != nil {
@@ -93,7 +93,7 @@ func AddToSpellModel(filepath string) error {
 
 // SaveSpellModel saves the spelling model which includes the data and parameters
 func SaveSpellModel() error {
-	pdir := CogentCoreDataDir()
+	pdir := TheApp.CogentCoreDataDir()
 	path := filepath.Join(pdir, "spell_en_us.json")
 	err := spell.Save(path)
 	if err != nil {
diff --git a/gi/splits.go b/gi/splits.go
index f64bcf2cf..10b6f6263 100644
--- a/gi/splits.go
+++ b/gi/splits.go
@@ -264,7 +264,7 @@ func (sl *Splits) HandleEvents() {
 	sl.OnKeyChord(func(e events.Event) {
 		kc := string(e.KeyChord())
 		mod := "Control+"
-		if Platform() == goosi.MacOS {
+		if TheApp.Platform() == goosi.MacOS {
 			mod = "Meta+"
 		}
 		if !strings.HasPrefix(kc, mod) {
diff --git a/gi/stage.go b/gi/stage.go
index a0b5da605..1f5744d65 100644
--- a/gi/stage.go
+++ b/gi/stage.go
@@ -243,7 +243,7 @@ func (st *Stage) SetType(typ StageTypes) *Stage {
 	st.Type = typ
 	switch st.Type {
 	case WindowStage:
-		if !Platform().IsMobile() {
+		if !TheApp.Platform().IsMobile() {
 			st.NewWindow = true
 		}
 		st.FullWindow = true
diff --git a/gi/textfield.go b/gi/textfield.go
index d642346bf..62e834f7f 100644
--- a/gi/textfield.go
+++ b/gi/textfield.go
@@ -1092,7 +1092,7 @@ var (
 )
 
 func init() {
-	AddQuitCleanFunc(TextFieldBlinker.QuitClean)
+	TheApp.AddQuitCleanFunc(TextFieldBlinker.QuitClean)
 }
 
 // StartCursor starts the cursor blinking and renders it
diff --git a/gi/wingeom.go b/gi/wingeom.go
index 1cf94ee9a..ab3f8ff30 100644
--- a/gi/wingeom.go
+++ b/gi/wingeom.go
@@ -80,7 +80,7 @@ func (mgr *WinGeomPrefsMgr) ResetCache() {
 
 // LockFile attempts to create the win_geom_prefs lock file
 func (mgr *WinGeomPrefsMgr) LockFile() error {
-	pdir := CogentCoreDataDir()
+	pdir := TheApp.CogentCoreDataDir()
 	pnm := filepath.Join(pdir, mgr.Filename+".lck")
 	for rep := 0; rep < 10; rep++ {
 		if _, err := os.Stat(pnm); os.IsNotExist(err) {
@@ -115,7 +115,7 @@ func (mgr *WinGeomPrefsMgr) LockFile() error {
 
 // UnLockFile unlocks the win_geom_prefs lock file (just removes it)
 func (mgr *WinGeomPrefsMgr) UnlockFile() {
-	pdir := CogentCoreDataDir()
+	pdir := TheApp.CogentCoreDataDir()
 	pnm := filepath.Join(pdir, mgr.Filename+".lck")
 	os.Remove(pnm)
 }
@@ -123,7 +123,7 @@ func (mgr *WinGeomPrefsMgr) UnlockFile() {
 // NeedToReload returns true if the last save time of prefs file is more recent than
 // when we last saved.  Called under mutex.
 func (mgr *WinGeomPrefsMgr) NeedToReload() bool {
-	pdir := CogentCoreDataDir()
+	pdir := TheApp.CogentCoreDataDir()
 	pnm := filepath.Join(pdir, mgr.Filename+".lst")
 	if _, err := os.Stat(pnm); os.IsNotExist(err) {
 		return false
@@ -148,7 +148,7 @@ func (mgr *WinGeomPrefsMgr) NeedToReload() bool {
 
 // SaveLastSave saves timestamp (now) of last save to win geom
 func (mgr *WinGeomPrefsMgr) SaveLastSave() {
-	pdir := CogentCoreDataDir()
+	pdir := TheApp.CogentCoreDataDir()
 	pnm := filepath.Join(pdir, mgr.Filename+".lst")
 	mgr.LastSave = time.Now()
 	b, _ := mgr.LastSave.MarshalJSON()
@@ -159,7 +159,7 @@ func (mgr *WinGeomPrefsMgr) SaveLastSave() {
 // called under mutex or at start
 func (mgr *WinGeomPrefsMgr) Open() error {
 	mgr.Init()
-	pdir := CogentCoreDataDir()
+	pdir := TheApp.CogentCoreDataDir()
 	pnm := filepath.Join(pdir, mgr.Filename+".json")
 	b, err := os.ReadFile(pnm)
 	if err != nil {
@@ -194,7 +194,7 @@ func (mgr *WinGeomPrefsMgr) Save() error {
 	if mgr.Geoms == nil {
 		return nil
 	}
-	pdir := CogentCoreDataDir()
+	pdir := TheApp.CogentCoreDataDir()
 	pnm := filepath.Join(pdir, mgr.Filename+".json")
 	b, err := json.MarshalIndent(mgr.Geoms, "", "\t")
 	if err != nil {
@@ -378,7 +378,7 @@ func (mgr *WinGeomPrefsMgr) DeleteAll() {
 	mgr.Mu.Lock()
 	defer mgr.Mu.Unlock()
 
-	pdir := CogentCoreDataDir()
+	pdir := TheApp.CogentCoreDataDir()
 	pnm := filepath.Join(pdir, mgr.Filename+".json")
 	os.Remove(pnm)
 	mgr.Geoms = make(WinGeomPrefs, 1000)
diff --git a/giv/fileview.go b/giv/fileview.go
index c3dc97068..873a2860c 100644
--- a/giv/fileview.go
+++ b/giv/fileview.go
@@ -339,7 +339,7 @@ func (fv *FileView) ConfigFilesRow() {
 		gi.NewButton(m).SetText("Open").SetIcon(icons.Open).
 			SetTooltip("Open the selected file using the default app").
 			OnClick(func(e events.Event) {
-				gi.OpenURL(fv.SelectedFile())
+				gi.TheApp.OpenURL(fv.SelectedFile())
 			})
 		gi.NewSeparator(m)
 		gi.NewButton(m).SetText("Duplicate").SetIcon(icons.FileCopy).
@@ -602,7 +602,7 @@ func (fv *FileView) UpdateFiles() {
 	}
 
 	if fv.PrevPath != fv.DirPath {
-		if gi.Platform() != goosi.MacOS {
+		if gi.TheApp.Platform() != goosi.MacOS {
 			// mac is not supported in a high-capacity fashion at this point
 			if fv.PrevPath == "" {
 				fv.ConfigWatcher()
diff --git a/giv/value.go b/giv/value.go
index 02d95390d..f4f989d6f 100644
--- a/giv/value.go
+++ b/giv/value.go
@@ -898,7 +898,7 @@ func ConfigDialogWidget(vv Value, w gi.Widget, allowReadOnly bool) {
 	doc := vv.Doc()
 	tip := ""
 	// windows are never new on mobile
-	if !gi.Platform().IsMobile() {
+	if !gi.TheApp.Platform().IsMobile() {
 		tip += "[Shift: new window]"
 		if doc != "" {
 			tip += " "
diff --git a/goosi/app.go b/goosi/app.go
index 30c05c552..1f39a52cd 100644
--- a/goosi/app.go
+++ b/goosi/app.go
@@ -94,6 +94,10 @@ type App interface {
 	// Linux: ~/.config, Windows: ~/AppData/Roaming
 	DataDir() string
 
+	// AppDataDir returns the application-specific data directory: [App.DataDir] + [App.Name].
+	// It ensures that the directory exists first.
+	AppDataDir() string
+
 	// CogentCoreDataDir returns the Cogent Core data directory: [App.DataDir] + "CogentCoreDataDir".
 	// It ensures that the directory exists first.
 	CogentCoreDataDir() string
diff --git a/goosi/driver/base/app.go b/goosi/driver/base/app.go
index 2dff38084..5f7b7e51f 100644
--- a/goosi/driver/base/app.go
+++ b/goosi/driver/base/app.go
@@ -125,6 +125,12 @@ func (a *App) OpenFiles() []string {
 	return a.OpenFls
 }
 
+func (a *App) AppDataDir() string {
+	pdir := filepath.Join(goosi.TheApp.DataDir(), a.Name())
+	os.MkdirAll(pdir, 0755)
+	return pdir
+}
+
 func (a *App) CogentCoreDataDir() string {
 	pdir := filepath.Join(a.This.DataDir(), "CogentCore")
 	os.MkdirAll(pdir, 0755)
diff --git a/grease/examples/basic/basic.go b/grease/examples/basic/basic.go
index e2360455d..b0db9f57b 100644
--- a/grease/examples/basic/basic.go
+++ b/grease/examples/basic/basic.go
@@ -113,6 +113,6 @@ func ModTidyRemoteSetURL(c *Config) error {
 
 //gti:skip
 func main() {
-	opts := grease.DefaultOptions("basic", "Basic", "Basic is a basic example application made with Grease.")
+	opts := grease.DefaultOptions("Basic", "Basic is a basic example application made with Grease.")
 	grease.Run(opts, &Config{}, Build, Run, Mod, ModTidy, ModTidyRemote, ModTidyRemoteSetURL)
 }
diff --git a/grease/options.go b/grease/options.go
index 5005a4839..92915a6a0 100644
--- a/grease/options.go
+++ b/grease/options.go
@@ -4,18 +4,15 @@
 
 package grease
 
+import "github.com/iancoleman/strcase"
+
 // Options contains the options passed to Grease
 // that control its behavior.
 type Options struct {
-	// AppName is the internal name of the Grease app
-	// (typically in kebab-case) (see also [AppTitle])
+	// AppName is the name of the Grease app.
 	AppName string
 
-	// AppTitle is the user-visible name of the Grease app
-	// (typically in Title Case) (see also [AppName])
-	AppTitle string
-
-	// AppAbout is the description of the Grease app
+	// AppAbout is the description of the Grease app.
 	AppAbout string
 
 	// Fatal is whether to, if there is an error in [Run],
@@ -57,16 +54,19 @@ type Options struct {
 
 // DefaultOptions returns a new [Options] value
 // with standard default values, based on the given
-// app name, app title, and app about.
-func DefaultOptions(appName, appTitle, appAbout string) *Options {
+// app name and optional app about info.
+func DefaultOptions(name string, about ...string) *Options {
+	abt := ""
+	if len(about) > 0 {
+		abt = about[0]
+	}
 	return &Options{
-		AppName:         appName,
-		AppTitle:        appTitle,
-		AppAbout:        appAbout,
+		AppName:         name,
+		AppAbout:        abt,
 		Fatal:           true,
 		PrintSuccess:    true,
 		DefaultEncoding: "toml",
-		DefaultFiles:    []string{appName + ".toml"},
+		DefaultFiles:    []string{strcase.ToKebab(name) + ".toml"},
 		IncludePaths:    []string{".", "configs"},
 	}
 }
diff --git a/greasi/examples/basic/basic.go b/greasi/examples/basic/basic.go
index c68e90ce4..a34295421 100644
--- a/greasi/examples/basic/basic.go
+++ b/greasi/examples/basic/basic.go
@@ -42,6 +42,6 @@ func Run(c *Config) error {
 
 //gti:skip
 func main() {
-	opts := grease.DefaultOptions("basic", "Basic", "Basic is a basic example application made with Greasi.")
+	opts := grease.DefaultOptions("Basic", "Basic is a basic example application made with Greasi.")
 	greasi.Run(opts, &Config{}, Build, Run)
 }
diff --git a/greasi/greasi.go b/greasi/greasi.go
index 1ab594373..7a9f88b8f 100644
--- a/greasi/greasi.go
+++ b/greasi/greasi.go
@@ -42,7 +42,7 @@ func Run[T any, C grease.CmdOrFunc[T]](opts *grease.Options, cfg T, cmds ...C) e
 			return nil
 		},
 		Name: "gui",
-		Doc:  "gui runs the GUI version of the " + opts.AppTitle + " tool",
+		Doc:  "gui runs the GUI version of the " + opts.AppName + " tool",
 		Root: true, // if root isn't already taken, we take it
 	})
 	return grease.Run(opts, cfg, cs...)
diff --git a/greasi/gui.go b/greasi/gui.go
index a0df51852..09e780c90 100644
--- a/greasi/gui.go
+++ b/greasi/gui.go
@@ -17,8 +17,7 @@ import (
 // GUI starts the GUI for the given Grease app, which must be passed as
 // a pointer. It should typically not be called by end-user code; see [Run].
 func GUI[T any](opts *grease.Options, cfg T, cmds ...*grease.Cmd[T]) {
-	b := gi.NewAppBody(opts.AppName).SetTitle(opts.AppTitle)
-	b.App().About = opts.AppAbout
+	b := gi.NewBody(opts.AppName)
 
 	b.AddAppBar(func(tb *gi.Toolbar) {
 		for _, cmd := range cmds {
diff --git a/texteditor/cursor.go b/texteditor/cursor.go
index 303bdd6cf..508b6e817 100644
--- a/texteditor/cursor.go
+++ b/texteditor/cursor.go
@@ -16,7 +16,7 @@ import (
 )
 
 func init() {
-	gi.AddQuitCleanFunc(EditorBlinker.QuitClean)
+	gi.TheApp.AddQuitCleanFunc(EditorBlinker.QuitClean)
 }
 
 var (
diff --git a/texteditor/histyle/histyles.go b/texteditor/histyle/histyles.go
index 71b94de65..c13a989ab 100644
--- a/texteditor/histyle/histyles.go
+++ b/texteditor/histyle/histyles.go
@@ -113,7 +113,7 @@ var StylesChanged = false
 
 // OpenPrefs opens Styles from Cogent Core standard prefs directory, using PrefsStylesFilename
 func (hs *Styles) OpenPrefs() error {
-	pdir := gi.CogentCoreDataDir()
+	pdir := gi.TheApp.CogentCoreDataDir()
 	pnm := filepath.Join(pdir, PrefsStylesFilename)
 	StylesChanged = false
 	return hs.OpenJSON(gi.Filename(pnm))
@@ -121,7 +121,7 @@ func (hs *Styles) OpenPrefs() error {
 
 // SavePrefs saves Styles to Cogent Core standard prefs directory, using PrefsStylesFilename
 func (hs *Styles) SavePrefs() error {
-	pdir := gi.CogentCoreDataDir()
+	pdir := gi.TheApp.CogentCoreDataDir()
 	pnm := filepath.Join(pdir, PrefsStylesFilename)
 	StylesChanged = false
 	MergeAvailStyles()
diff --git a/webcore/examples/basic/basic.go b/webcore/examples/basic/basic.go
index 1c93c92e5..ec85b32ae 100644
--- a/webcore/examples/basic/basic.go
+++ b/webcore/examples/basic/basic.go
@@ -17,7 +17,7 @@ import (
 var content embed.FS
 
 func main() {
-	b := gi.NewAppBody("Webcore Example")
+	b := gi.NewBody("Webcore Example")
 	pg := webcore.NewPage(b).SetSource(grr.Log1(fs.Sub(content, "content")))
 	b.AddAppBar(pg.AppBar)
 	pg.OpenURL("", true)
diff --git a/webcore/examples/basic/content/docs/gettingstarted.md b/webcore/examples/basic/content/docs/gettingstarted.md
index 16e7cd20d..416bac8f6 100644
--- a/webcore/examples/basic/content/docs/gettingstarted.md
+++ b/webcore/examples/basic/content/docs/gettingstarted.md
@@ -18,7 +18,7 @@ import (
 var content embed.FS
 
 func main() {
-	b := gi.NewAppBody("Webcore Example")
+	b := gi.NewBody("Webcore Example")
 	pg := webcore.NewPage(b).SetSource(grr.Log1(fs.Sub(content, "content")))
 	b.AddAppBar(pg.AppBar)
 	pg.OpenURL("", true)
diff --git a/webcore/page.go b/webcore/page.go
index 88c837101..0d8e979b6 100644
--- a/webcore/page.go
+++ b/webcore/page.go
@@ -152,7 +152,7 @@ func (pg *Page) ConfigWidget() {
 	sp := gi.NewSplits(pg, "splits")
 
 	nfr := gi.NewFrame(sp, "nav-frame")
-	nav := giv.NewTreeView(nfr, "nav").SetText(sentence.Case(strcase.ToCamel(pg.App().Name)))
+	nav := giv.NewTreeView(nfr, "nav").SetText(gi.TheApp.Name())
 	nav.OnSelect(func(e events.Event) {
 		if len(nav.SelectedNodes) == 0 {
 			return


Test patch:
diff --git a/grease/grease_test.go b/grease/grease_test.go
index 6a00e6e53..afce5f703 100644
--- a/grease/grease_test.go
+++ b/grease/grease_test.go
@@ -123,7 +123,7 @@ func TestRun(t *testing.T) {
 		gpu        bool
 		sparseness float32
 	)
-	err := Run(DefaultOptions("myapp", "My App", "My App is an awesome app"), cfg, &Cmd[*TestConfig]{
+	err := Run(DefaultOptions("My App", "My App is an awesome app"), cfg, &Cmd[*TestConfig]{
 		Func: func(tc *TestConfig) error {
 			inCmd = true
 			strSlice = tc.StrSlice
@@ -151,7 +151,7 @@ func TestRun(t *testing.T) {
 func TestConfigFunc(t *testing.T) {
 	cfg := &TestConfig{}
 	os.Args = []string{"myapp", "-no-net-data", "build", "-gpu", "../main", "-Note", "Hello, World", "-v", "-PAT_PARAMS_SPARSENESS=4"}
-	cmd, err := Config(DefaultOptions("myapp", "My App", "My App is an awesome app"), cfg, &Cmd[*TestConfig]{
+	cmd, err := Config(DefaultOptions("My App", "My App is an awesome app"), cfg, &Cmd[*TestConfig]{
 		Func: func(tc *TestConfig) error { return nil },
 		Name: "build",
 		Doc:  "build builds stuff",
@@ -305,7 +305,7 @@ func TestNoErrNotFound(t *testing.T) {
 }
 
 func TestOpen(t *testing.T) {
-	opts := DefaultOptions("test", "Test", "")
+	opts := DefaultOptions("Test")
 	opts.IncludePaths = []string{".", "testdata"}
 	cfg := &TestConfig{}
 	err := OpenWithIncludes(opts, cfg, "testcfg.toml")
@@ -348,13 +348,13 @@ func TestOpen(t *testing.T) {
 func TestUsage(t *testing.T) {
 	t.Skip("prints usage string")
 	cfg := &TestConfig{}
-	us := Usage(DefaultOptions("test", "Test", ""), cfg, "")
+	us := Usage(DefaultOptions("Test"), cfg, "")
 	fmt.Println(us)
 }
 
 func TestSave(t *testing.T) {
 	// t.Skip("prints usage string")
-	opts := DefaultOptions("test", "Test", "")
+	opts := DefaultOptions("Test")
 	opts.IncludePaths = []string{".", "testdata"}
 	cfg := &TestConfig{}
 	OpenWithIncludes(opts, cfg, "testcfg.toml")
@@ -363,7 +363,7 @@ func TestSave(t *testing.T) {
 
 func TestConfigOpen(t *testing.T) {
 	// t.Skip("prints usage string")
-	opts := DefaultOptions("test", "Test", "")
+	opts := DefaultOptions("Test")
 	opts.IncludePaths = []string{".", "testdata"}
 	opts.NeedConfigFile = true
 	cfg := &TestConfig{}