@@ -7,13 +7,24 @@ package egui
77//go:generate core generate -add-types
88
99import (
10+ "embed"
11+ "fmt"
1012 "io/fs"
13+ "net/http"
14+ "strings"
1115 "sync"
1216
17+ "cogentcore.org/core/base/errors"
18+ "cogentcore.org/core/base/fileinfo/mimedata"
19+ "cogentcore.org/core/base/labels"
1320 "cogentcore.org/core/core"
1421 "cogentcore.org/core/enums"
1522 "cogentcore.org/core/events"
23+ "cogentcore.org/core/htmlcore"
1624 "cogentcore.org/core/styles"
25+ "cogentcore.org/core/styles/abilities"
26+ "cogentcore.org/core/system"
27+ "cogentcore.org/core/text/textcore"
1728 "cogentcore.org/core/tree"
1829 _ "cogentcore.org/lab/gosl/slbool/slboolcore" // include to get gui views
1930 "cogentcore.org/lab/lab"
@@ -40,6 +51,9 @@ type GUI struct {
4051 // Body is the entire content of the sim window.
4152 Body * core.Body `display:"-"`
4253
54+ // Readme is the sim readme frame
55+ Readme * core.Frame `display:"-"`
56+
4357 // OnStop is called when running is stopped through the GUI,
4458 // via the Stopped method. It should update the network view for example.
4559 OnStop func (mode , level enums.Enum )
@@ -136,8 +150,8 @@ func NewGUIBody(b tree.Node, sim any, fsroot fs.FS, appname, title, about string
136150// a [core.Form] editor of the given sim object, and a filetree for the data filesystem
137151// rooted at fsroot, and with given app name, title, and about information.
138152// The first arg is an optional existing [core.Body] to make into: if nil then
139- // a new body is made first.
140- func (gui * GUI ) MakeBody (b tree.Node , sim any , fsroot fs.FS , appname , title , about string ) {
153+ // a new body is made first. It takes an optional fs with a README.md file.
154+ func (gui * GUI ) MakeBody (b tree.Node , sim any , fsroot fs.FS , appname , title , about string , readme ... embed. FS ) {
141155 gui .StopLevel = etime .NoTime // corresponds to the first level typically
142156 core .NoSentenceCaseFor = append (core .NoSentenceCaseFor , "github.com/emer" )
143157 if b == nil {
@@ -183,8 +197,121 @@ func (gui *GUI) MakeBody(b tree.Node, sim any, fsroot fs.FS, appname, title, abo
183197 gui .CycleUpdateInterval = 10
184198 gui .UpdateFiles ()
185199 gui .Files .Tabber = tabs
186- split .SetTiles (core .TileSplit , core .TileSpan )
187- split .SetSplits (.2 , .5 , .8 )
200+
201+ if len (readme ) > 0 {
202+ gui .addReadme (readme [0 ], split )
203+ } else {
204+ split .SetTiles (core .TileSplit , core .TileSpan )
205+ split .SetSplits (.2 , .5 , .8 )
206+ }
207+ }
208+
209+ func (gui * GUI ) addReadme (readmefs embed.FS , split * core.Splits ) {
210+ gui .Readme = core .NewFrame (split )
211+ gui .Readme .Name = "readme"
212+
213+ split .SetTiles (core .TileSplit , core .TileSpan , core .TileSpan )
214+ split .SetSplits (.2 , .5 , .5 , .3 )
215+
216+ ctx := htmlcore .NewContext ()
217+
218+ ctx .GetURL = func (rawURL string ) (* http.Response , error ) {
219+ return htmlcore .GetURLFromFS (readmefs , rawURL )
220+ }
221+
222+ ctx .AddWikilinkHandler (gui .readmeWikilink ("sim" ))
223+
224+ ctx .OpenURL = gui .readmeOpenURL
225+
226+ eds := []* textcore.Editor {}
227+
228+ ctx .ElementHandlers ["sim-question" ] = func (ctx * htmlcore.Context ) bool {
229+ ed := textcore .NewEditor (ctx .BlockParent )
230+ ed .Lines .Settings .LineNumbers = false
231+ eds = append (eds , ed )
232+ id := htmlcore .GetAttr (ctx .Node , "id" )
233+ ed .SetName (id )
234+ return true
235+ }
236+
237+ core .NewButton (gui .Readme ).SetText ("Copy answers" ).OnClick (func (e events.Event ) {
238+ clipboard := gui .Readme .Clipboard ()
239+ var ab strings.Builder
240+ for _ , ed := range eds {
241+ ab .WriteString ("## Question " + ed .Name + "\n " + ed .Lines .String () + "\n " )
242+ }
243+ answers := ab .String ()
244+ md := mimedata .NewText (answers )
245+ clipboard .Write (md )
246+ core .MessageSnackbar (gui .Body , "Answers copied to clipboard" )
247+ })
248+
249+ readme , err := readmefs .ReadFile ("README.md" )
250+
251+ if errors .Log (err ) == nil {
252+ htmlcore .ReadMDString (ctx , gui .Readme , string (readme ))
253+ }
254+ }
255+
256+ func (gui * GUI ) readmeWikilink (prefix string ) htmlcore.WikilinkHandler {
257+ return func (text string ) (url string , label string ) {
258+ if ! strings .HasPrefix (text , prefix + ":" ) {
259+ return "" , ""
260+ }
261+ text = strings .TrimPrefix (text , prefix + ":" )
262+ url = prefix + "://" + text
263+ if strings .Contains (text , "/" ) {
264+ _ , text , _ = strings .Cut (text , "/" )
265+ }
266+ return url , text
267+ }
268+ }
269+
270+ // readmeOpenURL Parses URL, highlights linked button or opens URL
271+ func (gui * GUI ) readmeOpenURL (url string ) {
272+ focusSet := false
273+ if ! strings .HasPrefix (url , "sim://" ) {
274+ system .TheApp .OpenURL (url )
275+ return
276+ }
277+
278+ text := strings .TrimPrefix (url , "sim://" )
279+ var pathPrefix string = ""
280+ hasPath := false
281+ if strings .Contains (text , "/" ) {
282+ pathPrefix , text , hasPath = strings .Cut (text , "/" )
283+ }
284+
285+ gui .Body .Scene .WidgetWalkDown (func (cw core.Widget , cwb * core.WidgetBase ) bool {
286+ if focusSet {
287+ return tree .Break
288+ }
289+ if ! hasPath && ! cwb .IsDisplayable () {
290+ return tree .Break
291+ }
292+ if hasPath && ! strings .Contains (cw .AsTree ().Path (), pathPrefix ) {
293+ return tree .Continue
294+ }
295+ label := labels .ToLabel (cw )
296+ if ! strings .EqualFold (label , text ) {
297+ return tree .Continue
298+ }
299+ if cwb .AbilityIs (abilities .Focusable ) {
300+ cwb .SetFocus ()
301+ focusSet = true
302+ return tree .Break
303+ }
304+ next := core .AsWidget (tree .Next (cwb ))
305+ if next .AbilityIs (abilities .Focusable ) {
306+ next .SetFocus ()
307+ focusSet = true
308+ return tree .Break
309+ }
310+ return tree .Continue
311+ })
312+ if ! focusSet {
313+ core .ErrorSnackbar (gui .Body , fmt .Errorf ("invalid sim url %q" , url ))
314+ }
188315}
189316
190317// AddNetView adds NetView in tab with given name
0 commit comments