Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions content/buttons.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright (c) 2025, 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 content

import (
"slices"

"cogentcore.org/core/colors"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/styles"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
)

func (ct *Content) MakeToolbar(p *tree.Plan) {
if false && ct.SizeClass() == core.SizeCompact { // TODO: implement hamburger menu for compact
tree.Add(p, func(w *core.Button) {
w.SetIcon(icons.Menu)
w.SetTooltip("Navigate pages and headings")
w.OnClick(func(e events.Event) {
d := core.NewBody("Navigate")
// tree.MoveToParent(ct.leftFrame, d)
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar)
})
d.RunDialog(w)
})
})
}
tree.Add(p, func(w *core.Button) {
w.SetIcon(icons.Icon(core.AppIcon))
w.SetTooltip("Home")
w.OnClick(func(e events.Event) {
ct.Open("")
})
})
// Superseded by browser navigation on web.
if core.TheApp.Platform() != system.Web {
tree.Add(p, func(w *core.Button) {
w.SetIcon(icons.ArrowBack).SetKey(keymap.HistPrev)
w.SetTooltip("Back")
w.Updater(func() {
w.SetEnabled(ct.historyIndex > 0)
})
w.OnClick(func(e events.Event) {
ct.historyIndex--
ct.open(ct.history[ct.historyIndex].URL, false) // do not add to history while navigating history
})
})
tree.Add(p, func(w *core.Button) {
w.SetIcon(icons.ArrowForward).SetKey(keymap.HistNext)
w.SetTooltip("Forward")
w.Updater(func() {
w.SetEnabled(ct.historyIndex < len(ct.history)-1)
})
w.OnClick(func(e events.Event) {
ct.historyIndex++
ct.open(ct.history[ct.historyIndex].URL, false) // do not add to history while navigating history
})
})
}
tree.Add(p, func(w *core.Button) {
w.SetText("Search").SetIcon(icons.Search).SetKey(keymap.Menu)
w.Styler(func(s *styles.Style) {
s.Background = colors.Scheme.SurfaceVariant
s.Padding.Right.Em(5)
})
w.OnClick(func(e events.Event) {
ct.Scene.MenuSearchDialog("Search", "Search "+core.TheApp.Name())
})
})
}

func (ct *Content) MenuSearch(items *[]core.ChooserItem) {
newItems := make([]core.ChooserItem, len(ct.pages))
for i, pg := range ct.pages {
newItems[i] = core.ChooserItem{
Value: pg,
Text: pg.Name,
Icon: icons.Article,
Func: func() {
ct.Open(pg.URL)
},
}
}
*items = append(newItems, *items...)
}

// makeBottomButtons makes the previous and next buttons if relevant.
func (ct *Content) makeBottomButtons(p *tree.Plan) {
if len(ct.currentPage.Categories) == 0 {
return
}
cat := ct.currentPage.Categories[0]
pages := ct.pagesByCategory[cat]
idx := slices.Index(pages, ct.currentPage)

ct.prevPage, ct.nextPage = nil, nil

if idx > 0 {
ct.prevPage = pages[idx-1]
}
if idx < len(pages)-1 {
ct.nextPage = pages[idx+1]
}

if ct.prevPage == nil && ct.nextPage == nil {
return
}

tree.Add(p, func(w *core.Frame) {
w.Styler(func(s *styles.Style) {
s.Align.Items = styles.Center
s.Grow.Set(1, 0)
})
w.Maker(func(p *tree.Plan) {
if ct.prevPage != nil {
tree.Add(p, func(w *core.Button) {
w.SetText("Previous").SetIcon(icons.ArrowBack).SetType(core.ButtonTonal)
ct.Context.LinkButtonUpdating(w, func() string { // needed to prevent stale URL variable
return ct.prevPage.URL
})
})
}
if ct.nextPage != nil {
tree.Add(p, func(w *core.Stretch) {})
tree.Add(p, func(w *core.Button) {
w.SetText("Next").SetIcon(icons.ArrowForward).SetType(core.ButtonTonal)
ct.Context.LinkButtonUpdating(w, func() string { // needed to prevent stale URL variable
return ct.nextPage.URL
})
})
}
})
})
}
112 changes: 46 additions & 66 deletions content/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,24 @@ package content

import (
"bytes"
"cmp"
"fmt"
"io/fs"
"net/http"
"path/filepath"
"slices"
"strconv"
"strings"

"golang.org/x/exp/maps"

"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/content/bcontent"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/htmlcore"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/system"
Expand Down Expand Up @@ -58,6 +60,10 @@ type Content struct {
// pagesByCategory has the [bcontent.Page]s for each of all [bcontent.Page.Categories].
pagesByCategory map[string][]*bcontent.Page

// categories has all unique [bcontent.Page.Categories], sorted such that the categories
// with the most pages are listed first.
categories []string

// history is the history of pages that have been visited.
// The oldest page is first.
history []*bcontent.Page
Expand All @@ -72,16 +78,24 @@ type Content struct {
renderedPage *bcontent.Page

// leftFrame is the frame on the left side of the widget,
// used for displaying the table of contents.
// used for displaying the table of contents and the categories.
leftFrame *core.Frame

// rightFrame is the frame on the right side of the widget,
// used for displaying the page content.
rightFrame *core.Frame

// tocNodes are all of the tree nodes in the table of contents
// by kebab-case heading name.
tocNodes map[string]*core.Tree

// currentHeading is the currently selected heading in the table of contents,
// if any (in kebab-case).
currentHeading string

// The previous and next page, if applicable. They must be stored on this struct
// to avoid stale local closure variables.
prevPage, nextPage *bcontent.Page
}

func init() {
Expand Down Expand Up @@ -137,6 +151,7 @@ func (ct *Content) Init() {
ct.leftFrame = w
})
tree.Add(p, func(w *core.Frame) {
ct.rightFrame = w
w.Maker(func(p *tree.Plan) {
if ct.currentPage.Title != "" {
tree.Add(p, func(w *core.Text) {
Expand All @@ -155,6 +170,7 @@ func (ct *Content) Init() {
errors.Log(ct.loadPage(w))
})
})
ct.makeBottomButtons(p)
})
})
})
Expand Down Expand Up @@ -204,6 +220,11 @@ func (ct *Content) SetSource(source fs.FS) *Content {
}
return nil
}))
ct.categories = maps.Keys(ct.pagesByCategory)
slices.SortFunc(ct.categories, func(a, b string) int {
return cmp.Compare(len(ct.pagesByCategory[b]), len(ct.pagesByCategory[a]))
})

if url := ct.getWebURL(); url != "" {
ct.Open(url)
return ct
Expand Down Expand Up @@ -275,6 +296,7 @@ func (ct *Content) open(url string, history bool) {

func (ct *Content) openHeading(heading string) {
if heading == "" {
ct.rightFrame.ScrollDimToContentStart(math32.Y)
return
}
tr := ct.tocNodes[strcase.ToKebab(heading)]
Expand All @@ -300,7 +322,6 @@ func (ct *Content) loadPage(w *core.Frame) error {
return err
}

w.Parent.(*core.Frame).ScrollDimToContentStart(math32.Y) // the parent is the one that scrolls
ct.leftFrame.DeleteChildren()
ct.makeTableOfContents(w)
ct.makeCategories()
Expand All @@ -314,6 +335,13 @@ func (ct *Content) loadPage(w *core.Frame) error {
func (ct *Content) makeTableOfContents(w *core.Frame) {
ct.tocNodes = map[string]*core.Tree{}
contents := core.NewTree(ct.leftFrame).SetText("<b>Contents</b>")
contents.OnSelect(func(e events.Event) {
if contents.IsRootSelected() {
ct.rightFrame.ScrollDimToContentStart(math32.Y)
ct.currentHeading = ""
ct.saveWebURL()
}
})
// last is the most recent tree node for each heading level, used for nesting.
last := map[int]*core.Tree{}
w.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool {
Expand Down Expand Up @@ -353,23 +381,32 @@ func (ct *Content) makeTableOfContents(w *core.Frame) {

// makeCategories makes the categories tree for the current page and adds it to [Content.leftFrame].
func (ct *Content) makeCategories() {
if len(ct.currentPage.Categories) == 0 {
if len(ct.categories) == 0 {
return
}

cats := core.NewTree(ct.leftFrame).SetText("<b>Categories</b>")
for _, cat := range ct.currentPage.Categories {
catTree := core.NewTree(cats).SetText(cat)
cats.OnSelect(func(e events.Event) {
if cats.IsRootSelected() {
ct.Open("")
}
})
for _, cat := range ct.categories {
catTree := core.NewTree(cats).SetText(cat).SetClosed(true)
if ct.currentPage.Name == cat {
catTree.SetSelected(true)
}
catTree.OnSelect(func(e events.Event) {
if catPage := ct.pageByName(cat); catPage != nil {
ct.Open(catPage.URL)
}
})
for _, pg := range ct.pagesByCategory[cat] {
pgTree := core.NewTree(catTree).SetText(pg.Name)
if pg == ct.currentPage {
continue
pgTree.SetSelected(true)
catTree.SetClosed(false)
}
pgTree := core.NewTree(catTree).SetText(pg.Name)
pgTree.OnSelect(func(e events.Event) {
ct.Open(pg.URL)
})
Expand Down Expand Up @@ -409,60 +446,3 @@ func (ct *Content) setStageTitle() {
rw.SetStageTitle(name)
}
}

func (ct *Content) MakeToolbar(p *tree.Plan) {
tree.Add(p, func(w *core.Button) {
w.SetIcon(icons.Icon(core.AppIcon))
w.SetTooltip("Home")
w.OnClick(func(e events.Event) {
ct.Open("")
})
})
// Superseded by browser navigation on web.
if core.TheApp.Platform() != system.Web {
tree.Add(p, func(w *core.Button) {
w.SetIcon(icons.ArrowBack).SetKey(keymap.HistPrev)
w.SetTooltip("Back")
w.Updater(func() {
w.SetEnabled(ct.historyIndex > 0)
})
w.OnClick(func(e events.Event) {
ct.historyIndex--
ct.open(ct.history[ct.historyIndex].URL, false) // do not add to history while navigating history
})
})
tree.Add(p, func(w *core.Button) {
w.SetIcon(icons.ArrowForward).SetKey(keymap.HistNext)
w.SetTooltip("Forward")
w.Updater(func() {
w.SetEnabled(ct.historyIndex < len(ct.history)-1)
})
w.OnClick(func(e events.Event) {
ct.historyIndex++
ct.open(ct.history[ct.historyIndex].URL, false) // do not add to history while navigating history
})
})
}
tree.Add(p, func(w *core.Button) {
w.SetIcon(icons.Search).SetKey(keymap.Menu)
w.SetTooltip("Search")
w.OnClick(func(e events.Event) {
ct.Scene.MenuSearchDialog("Search", "Search "+core.TheApp.Name())
})
})
}

func (ct *Content) MenuSearch(items *[]core.ChooserItem) {
newItems := make([]core.ChooserItem, len(ct.pages))
for i, pg := range ct.pages {
newItems[i] = core.ChooserItem{
Value: pg,
Text: pg.Name,
Icon: icons.Article,
Func: func() {
ct.Open(pg.URL)
},
}
}
*items = append(newItems, *items...)
}
14 changes: 13 additions & 1 deletion core/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ func AsTree(n tree.Node) *Tree {
//
// Standard [events.Event]s are sent to any listeners, including
// [events.Select], [events.Change], and [events.DoubleClick].
// The selected nodes are in the root [Tree.SelectedNodes] list.
// The selected nodes are in the root [Tree.SelectedNodes] list;
// select events are sent to both selected nodes and the root node.
// See [Tree.IsRootSelected] to check whether a select event on the root
// node corresponds to the root node or another node.
type Tree struct {
WidgetBase

Expand Down Expand Up @@ -670,6 +673,15 @@ func (tr *Tree) RenderWidget() {

//////// Selection

// IsRootSelected returns whether the root node is the only node selected.
// This can be used in [events.Select] event handlers to check whether a
// select event on the root node truly corresponds to the root node or whether
// it is for another node, as select events are sent to the root when any node
// is selected.
func (tr *Tree) IsRootSelected() bool {
return len(tr.SelectedNodes) == 1 && tr.SelectedNodes[0] == tr.Root
}

// GetSelectedNodes returns a slice of the currently selected
// Trees within the entire tree, using a list maintained
// by the root node.
Expand Down
Loading