diff --git a/example/basic/home.go b/example/basic/home.go index 0595a53..59ed753 100644 --- a/example/basic/home.go +++ b/example/basic/home.go @@ -17,7 +17,7 @@ import ( type HomeView struct { view.ViewManager - sidebar *navi.NavDrawer + sidebar *NavDrawer tabbar *navi.Tabbar currentModal *view.ModalView } @@ -41,7 +41,7 @@ func (hv *HomeView) LayoutMain(gtx C, th *theme.Theme) layout.Dimensions { }.Layout(gtx, // navdrawer layout.Rigid(func(gtx C) D { - return navi.NaviDrawerStyle{ + return NaviDrawerStyle{ NavDrawer: hv.sidebar, Inset: layout.Inset{ Top: unit.Dp(20), @@ -109,13 +109,31 @@ func newHome(window *app.Window) *HomeView { fileChooser, _ = explorer.NewFileChooser(vm) - sidebar := navi.NewNavDrawer(vm) - sidebar.AddSection(navi.SimpleItemSection(viewIcon, "Tabviews & Image", ExampleViewID, false)) - sidebar.AddSection(navi.SimpleItemSection(viewIcon, "Editor Example", EditorExampleViewID, false)) - sidebar.AddSection(navi.SimpleItemSection(viewIcon, "File Explorer", ExplorerViewID, false)) + sidebar := NewNavDrawer(vm) + sidebar.AddSection(SimpleItemSection(viewIcon, "Tabviews & Image", func(item *navi.NavTree) { + sidebar.OnItemSelected(item) + intent := view.Intent{Target: ExampleViewID, ShowAsModal: false} + _ = vm.RequestSwitch(intent) + })) + + sidebar.AddSection(SimpleItemSection(viewIcon, "Editor Example", func(item *navi.NavTree) { + sidebar.OnItemSelected(item) + intent := view.Intent{Target: EditorExampleViewID, ShowAsModal: false} + _ = vm.RequestSwitch(intent) + })) + + sidebar.AddSection(SimpleItemSection(viewIcon, "File Explorer", func(item *navi.NavTree) { + sidebar.OnItemSelected(item) + intent := view.Intent{Target: ExplorerViewID, ShowAsModal: false} + _ = vm.RequestSwitch(intent) + })) fileTree, _ := explorer.NewEntryNavItem("../../", nil, nil) - sidebar.AddSection(explorer.NewFileTreeNav(sidebar, "File Explorer", fileTree)) + sidebar.AddSection(NewFileTreeNav("File Explorer", fileTree, func(item *navi.NavTree) { + sidebar.OnItemSelected(item) + //intent := view.Intent{Target: EditorExampleViewID, ShowAsModal: false} + //_ = vm.RequestSwitch(intent) + })) vm.Register(ExampleViewID, func() view.View { return NewExampleView(vm) }) vm.Register(EditorExampleViewID, NewEditorExample) diff --git a/navi/nav_drawer.go b/example/basic/navi.go similarity index 60% rename from navi/nav_drawer.go rename to example/basic/navi.go index 735d6b2..21d4283 100644 --- a/navi/nav_drawer.go +++ b/example/basic/navi.go @@ -1,14 +1,9 @@ -package navi +package main import ( "image/color" - "log" "slices" - "github.com/oligo/gioview/misc" - "github.com/oligo/gioview/theme" - "github.com/oligo/gioview/view" - "gioui.org/font" "gioui.org/layout" "gioui.org/op/clip" @@ -16,16 +11,22 @@ import ( "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" + "github.com/oligo/gioview/explorer" + "github.com/oligo/gioview/menu" + "github.com/oligo/gioview/misc" + "github.com/oligo/gioview/navi" + "github.com/oligo/gioview/theme" + "github.com/oligo/gioview/view" ) -type ( - C = layout.Context - D = layout.Dimensions -) +type NavSection interface { + Title() string + Layout(gtx C, th *theme.Theme) D +} type NavDrawer struct { vm view.ViewManager - selectedItem *NavItemStyle + selectedItem *navi.NavTree listItems []NavSection listState *widget.List @@ -40,6 +41,20 @@ type NaviDrawerStyle struct { Width unit.Dp } +type FileTreeNav struct { + title string + root *navi.NavTree +} + +type simpleItemSection struct { + item *navi.NavTree +} + +type simpleNavItem struct { + icon *widget.Icon + name string +} + func NewNavDrawer(vm view.ViewManager) *NavDrawer { return &NavDrawer{ vm: vm, @@ -52,13 +67,11 @@ func NewNavDrawer(vm view.ViewManager) *NavDrawer { } func (nv *NavDrawer) AddSection(item NavSection) { - item.Attach(nv) nv.listItems = append(nv.listItems, item) } func (nv *NavDrawer) InsertAt(index int, item NavSection) { nv.listItems = slices.Insert(nv.listItems, index, []NavSection{item}...) - item.Attach(nv) } func (nv *NavDrawer) RemoveSection(index int) { @@ -103,21 +116,13 @@ func (nv *NavDrawer) Layout(gtx C, th *theme.Theme) D { }) } -func (nv *NavDrawer) OnItemSelected(gtx C, item *NavItemStyle) { +func (nv *NavDrawer) OnItemSelected(item *navi.NavTree) { if item != nv.selectedItem { if nv.selectedItem != nil { nv.selectedItem.Unselect() } nv.selectedItem = item } - - if item != nil { - intent := item.item.OnSelect(gtx) - // An empty also refresh the UI so do not drop it. - if err := nv.vm.RequestSwitch(intent); err != nil { - log.Printf("switching to view %s error: %v", intent.Target, err) - } - } } func (ns NaviDrawerStyle) Layout(gtx C, th *theme.Theme) D { @@ -144,3 +149,47 @@ func (ns NaviDrawerStyle) Layout(gtx C, th *theme.Theme) D { }) } + +func (item simpleNavItem) Layout(gtx C, th *theme.Theme, textColor color.NRGBA) D { + label := material.Label(th.Theme, th.TextSize, item.name) + label.Color = textColor + return label.Layout(gtx) +} + +func (item simpleNavItem) ContextMenuOptions(gtx C) ([][]menu.MenuOption, bool) { + return nil, false +} + +func (item simpleNavItem) Children() []navi.NavItem { + return nil +} + +func (ss simpleItemSection) Title() string { + return "" +} + +func (ss simpleItemSection) Layout(gtx C, th *theme.Theme) D { + return ss.item.Layout(gtx, th) +} + +func SimpleItemSection(icon *widget.Icon, name string, onSelect func(item *navi.NavTree)) NavSection { + item := navi.NewNavItem(simpleNavItem{icon: icon, name: name}, onSelect) + return simpleItemSection{item: item} +} + +// Construct a FileTreeNav object that loads files and folders from rootDir. The skipFolders +// parameter allows you to specify folder name prefixes to exclude from the navigation. +func NewFileTreeNav(title string, navRoot *explorer.EntryNavItem, onClick func(item *navi.NavTree)) *FileTreeNav { + return &FileTreeNav{ + title: title, + root: navi.NewNavItem(navRoot, onClick), + } +} + +func (tn *FileTreeNav) Title() string { + return tn.title +} + +func (tn *FileTreeNav) Layout(gtx C, th *theme.Theme) D { + return tn.root.Layout(gtx, th) +} diff --git a/example/basic/view.go b/example/basic/view.go index 5dbeb66..2b8f721 100644 --- a/example/basic/view.go +++ b/example/basic/view.go @@ -85,21 +85,21 @@ func (vw *ExampleView) Layout(gtx layout.Context, th *theme.Theme) layout.Dimens layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - - if vw.img == nil { - vw.img = loadImg(vw.vm) - } - - //sz := 480 - //gtx.Constraints = layout.Exact(image.Pt(sz, sz)) - // gtx.Constraints.Max.X = 500 - // gtx.Constraints.Min = gtx.Constraints.Max - return gioimg.ImageStyle{ - Src: vw.img, - Radius: unit.Dp(12), - Fit: widget.Contain, - Position: layout.N, - }.Layout(gtx) + return D{} + // if vw.img == nil { + // vw.img = loadImg(vw.vm) + // } + + // //sz := 480 + // //gtx.Constraints = layout.Exact(image.Pt(sz, sz)) + // // gtx.Constraints.Max.X = 500 + // // gtx.Constraints.Min = gtx.Constraints.Max + // return gioimg.ImageStyle{ + // Src: vw.img, + // Radius: unit.Dp(12), + // Fit: widget.Contain, + // Position: layout.N, + // }.Layout(gtx) }), layout.Rigid(layout.Spacer{Height: unit.Dp(25)}.Layout), diff --git a/explorer/tree_style.go b/explorer/tree_style.go index f3d9327..5fdc1a3 100644 --- a/explorer/tree_style.go +++ b/explorer/tree_style.go @@ -11,6 +11,7 @@ import ( "slices" "strings" + "gioui.org/gesture" "gioui.org/io/clipboard" "gioui.org/io/event" "gioui.org/io/key" @@ -20,11 +21,12 @@ import ( "gioui.org/op" "gioui.org/op/clip" "gioui.org/op/paint" + "gioui.org/unit" "gioui.org/widget" "github.com/oligo/gioview/menu" + "github.com/oligo/gioview/misc" "github.com/oligo/gioview/navi" "github.com/oligo/gioview/theme" - "github.com/oligo/gioview/view" gv "github.com/oligo/gioview/widget" "golang.org/x/exp/shiny/materialdesign/icons" ) @@ -40,16 +42,11 @@ var ( fileIcon, _ = widget.NewIcon(icons.ActionDescription) ) -var _ navi.NavSection = (*FileTreeNav)(nil) var _ navi.NavItem = (*EntryNavItem)(nil) -type FileTreeNav struct { - title string - root *navi.NavItemStyle -} - type EntryNavItem struct { state *EntryNode + click gesture.Click menuOptionFunc MenuOptionFunc onSelectFunc OnSelectFunc @@ -62,28 +59,7 @@ type EntryNavItem struct { } type MenuOptionFunc func(gtx C, item *EntryNavItem) [][]menu.MenuOption -type OnSelectFunc func(item *EntryNode) view.Intent - -// Construct a FileTreeNav object that loads files and folders from rootDir. The skipFolders -// parameter allows you to specify folder name prefixes to exclude from the navigation. -func NewFileTreeNav(drawer *navi.NavDrawer, title string, navRoot *EntryNavItem) *FileTreeNav { - return &FileTreeNav{ - title: title, - root: navi.NewNavItem(navRoot, drawer), - } -} - -func (tn *FileTreeNav) Attach(drawer *navi.NavDrawer) { - // NOOP -} - -func (tn *FileTreeNav) Title() string { - return tn.title -} - -func (tn *FileTreeNav) Layout(gtx C, th *theme.Theme) D { - return tn.root.Layout(gtx, th) -} +type OnSelectFunc func(item *EntryNode) // Construct a file tree object that loads files and folders from rootDir. // `menuOptionFunc` is used to define the operations allowed by context menu(use right click to active it). @@ -104,7 +80,7 @@ func NewEntryNavItem(rootDir string, menuOptionFunc MenuOptionFunc, onSelectFunc } -func (eitem *EntryNavItem) Icon() *widget.Icon { +func (eitem *EntryNavItem) icon() *widget.Icon { if eitem.state.Kind() == FolderNode { if eitem.expaned { return folderOpenIcon @@ -115,18 +91,15 @@ func (eitem *EntryNavItem) Icon() *widget.Icon { return fileIcon } -func (eitem *EntryNavItem) OnSelect(gtx C) view.Intent { +func (eitem *EntryNavItem) OnSelect() { eitem.expaned = !eitem.expaned if eitem.expaned { eitem.needSync = true } if eitem.state.Kind() == FileNode && eitem.onSelectFunc != nil { - return eitem.onSelectFunc(eitem.state) + eitem.onSelectFunc(eitem.state) } - - return view.Intent{} - } func (eitem *EntryNavItem) Layout(gtx layout.Context, th *theme.Theme, textColor color.NRGBA) D { @@ -159,7 +132,24 @@ func (eitem *EntryNavItem) layout(gtx layout.Context, th *theme.Theme, textColor eitem.label.Color = textColor eitem.label.TextSize = th.TextSize - return eitem.label.Layout(gtx, th) + + return layout.Flex{Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(func(gtx C) D { + if eitem.icon() == nil { + return layout.Dimensions{} + } + return layout.Inset{Right: unit.Dp(6)}.Layout(gtx, func(gtx C) D { + iconColor := th.ContrastBg + return misc.Icon{Icon: eitem.icon(), Color: iconColor, Size: unit.Dp(th.TextSize)}.Layout(gtx, th) + }) + }), + layout.Flexed(1, func(gtx C) D { + return layout.W.Layout(gtx, func(gtx C) D { + return eitem.label.Layout(gtx, th) + }) + }), + ) + } func (eitem *EntryNavItem) IsDir() bool { @@ -334,6 +324,7 @@ func (eitem *EntryNavItem) Update(gtx C) error { for { ke, ok := gtx.Event( // focus conflicts with editable. so subscribe editable's key events here. + pointer.Filter{Target: eitem, Kinds: pointer.Press | pointer.Release}, key.Filter{Focus: eitem.label, Name: "C", Required: key.ModShortcut}, key.Filter{Focus: eitem.label, Name: "V", Required: key.ModShortcut}, key.Filter{Focus: eitem.label, Name: "X", Required: key.ModShortcut}, @@ -381,6 +372,10 @@ func (eitem *EntryNavItem) Update(gtx C) error { } } } + case pointer.Event: + if event.Buttons.Contain(pointer.ButtonPrimary) && event.Kind == pointer.Press { + eitem.OnSelect() + } } } diff --git a/navi/navitem.go b/navi/navitem.go index db1738a..f14d002 100644 --- a/navi/navitem.go +++ b/navi/navitem.go @@ -16,7 +16,11 @@ import ( "github.com/oligo/gioview/menu" "github.com/oligo/gioview/misc" "github.com/oligo/gioview/theme" - "github.com/oligo/gioview/view" +) + +type ( + C = layout.Context + D = layout.Dimensions ) var ( @@ -30,25 +34,16 @@ var NavItemPadding = layout.Inset{ Bottom: unit.Dp(1), } -type NavSection interface { - Title() string - Layout(gtx C, th *theme.Theme) D - Attach(d *NavDrawer) -} - type NavItem interface { - OnSelect(gtx layout.Context) view.Intent - Icon() *widget.Icon Layout(gtx layout.Context, th *theme.Theme, textColor color.NRGBA) D // when there's menu options, a context menu should be attached to this navItem. // The returned boolean value suggest the position of the popup menu should be at - // fixed position or not. NavItemStyle should place a clickable icon to guide user interactions. + // fixed position or not. NavTree should place a clickable icon to guide user interactions. ContextMenuOptions(gtx layout.Context) ([][]menu.MenuOption, bool) Children() []NavItem } -type NavItemStyle struct { - drawer *NavDrawer +type NavTree struct { item NavItem label *list.InteractiveLabel menu *menu.ContextMenu @@ -56,18 +51,19 @@ type NavItemStyle struct { showMenuBtn widget.Clickable childList layout.List - children []*NavItemStyle + children []*NavTree + OnClicked func(item *NavTree) } -func (n *NavItemStyle) IsSelected() bool { +func (n *NavTree) IsSelected() bool { return n.label.IsSelected() } -func (n *NavItemStyle) Unselect() { +func (n *NavTree) Unselect() { n.label.Unselect() } -func (n *NavItemStyle) Update(gtx C) bool { +func (n *NavTree) Update(gtx C) bool { if n.menu == nil { menuOpts, fixPos := n.item.ContextMenuOptions(gtx) if len(menuOpts) > 0 { @@ -78,32 +74,20 @@ func (n *NavItemStyle) Update(gtx C) bool { } // handle naviitem events - if n.label.Update(gtx) { - n.drawer.OnItemSelected(gtx, n) + if n.label.Update(gtx) && n.OnClicked != nil { + n.OnClicked(n) return true } return false } -func (n *NavItemStyle) layoutRoot(gtx layout.Context, th *theme.Theme) layout.Dimensions { +func (n *NavTree) layoutRoot(gtx layout.Context, th *theme.Theme) layout.Dimensions { macro := op.Record(gtx.Ops) dims := layout.Inset{Bottom: unit.Dp(2)}.Layout(gtx, func(gtx C) D { return n.label.Layout(gtx, th, func(gtx C, color color.NRGBA) D { return NavItemPadding.Layout(gtx, func(gtx C) D { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(func(gtx C) D { - if n.item.Icon() == nil { - return layout.Dimensions{} - } - return layout.Inset{Right: unit.Dp(6)}.Layout(gtx, func(gtx C) D { - iconColor := th.ContrastBg - if n.label.IsSelected() { - iconColor = th.ContrastFg - } - return misc.Icon{Icon: n.item.Icon(), Color: iconColor, Size: unit.Dp(th.TextSize)}.Layout(gtx, th) - }) - }), layout.Flexed(1, func(gtx C) D { return layout.W.Layout(gtx, func(gtx C) D { return n.item.Layout(gtx, th, color) @@ -143,7 +127,7 @@ func (n *NavItemStyle) layoutRoot(gtx layout.Context, th *theme.Theme) layout.Di return dims } -func (n *NavItemStyle) Layout(gtx C, th *theme.Theme) D { +func (n *NavTree) Layout(gtx C, th *theme.Theme) D { if n.label == nil { n.label = &list.InteractiveLabel{} } @@ -158,7 +142,7 @@ func (n *NavItemStyle) Layout(gtx C, th *theme.Theme) D { if len(n.children) != len(itemChildren) { n.children = n.children[:0] for _, child := range itemChildren { - n.children = append(n.children, NewNavItem(child, n.drawer)) + n.children = append(n.children, NewNavItem(child, n.OnClicked)) } } @@ -187,63 +171,13 @@ func (n *NavItemStyle) Layout(gtx C, th *theme.Theme) D { } -func NewNavItem(item NavItem, drawer *NavDrawer) *NavItemStyle { - style := &NavItemStyle{ +func NewNavItem(item NavItem, onClicked func(item *NavTree)) *NavTree { + style := &NavTree{ item: item, label: &list.InteractiveLabel{}, - drawer: drawer, fixMenuPos: false, + OnClicked: onClicked, } return style } - -type simpleItemSection struct { - item *NavItemStyle -} - -type simpleNavItem struct { - icon *widget.Icon - name string - targetView view.ViewID - openAsModal bool -} - -func (item simpleNavItem) OnSelect(gtx C) view.Intent { - return view.Intent{Target: item.targetView, ShowAsModal: item.openAsModal} -} - -func (item simpleNavItem) Icon() *widget.Icon { - return item.icon -} - -func (item simpleNavItem) Layout(gtx C, th *theme.Theme, textColor color.NRGBA) D { - label := material.Label(th.Theme, th.TextSize, item.name) - label.Color = textColor - return label.Layout(gtx) -} - -func (item simpleNavItem) ContextMenuOptions(gtx C) ([][]menu.MenuOption, bool) { - return nil, false -} - -func (item simpleNavItem) Children() []NavItem { - return nil -} - -func (ss simpleItemSection) Title() string { - return "" -} - -func (ss simpleItemSection) Layout(gtx C, th *theme.Theme) D { - return ss.item.Layout(gtx, th) -} - -func (ss simpleItemSection) Attach(d *NavDrawer) { - ss.item.drawer = d -} - -func SimpleItemSection(icon *widget.Icon, name string, targetView view.ViewID, openAsModal bool) NavSection { - item := NewNavItem(simpleNavItem{icon: icon, name: name, targetView: targetView, openAsModal: openAsModal}, nil) - return simpleItemSection{item: item} -}