package filetree import ( "codeberg.org/turbo-editors/turbo-core/ui" ) // The markers drawn before a directory, and the indentation of one level. // // A file gets a marker's worth of blank so that names line up whatever their // neighbours are, which is what makes a column of names readable at a glance. const ( openMarker = "▼ " closedMarker = "▶ " fileMarker = " " indent = " " ) // wheelStep is how many rows one notch of the mouse wheel moves. const wheelStep = 3 // View shows a project's files in a window and opens the one that is chosen. // // It is a ui.Widget, so it goes into an ordinary window and gets the desktop's // moving, resizing and maximising for free. // // view, err := filetree.NewView(".") // if err != nil { // return err // } // view.OnOpen = func(path string) { editor.Open(path) } // window := ui.NewWindow(view.Title(), view) type View struct { ui.FocusBox tree *Tree selected int top int // OnOpen is called with the absolute path of a file that was chosen. A // directory never reaches it: choosing one opens or closes the branch. OnOpen func(path string) } // NewView reads a directory and returns the widget showing it. // // The error is the one New gives: a root that is not a directory, or cannot be // read at all, leaves the caller with nothing to show. func NewView(root string) (*View, error) { tree, err := New(root) if err != nil { return nil, err } return &View{tree: tree}, nil } // Title returns what the window holding this view should be called: the name // of the directory the tree is rooted at. func (v *View) Title() string { return v.tree.Root().Name() } // Root returns the absolute path the tree is rooted at, which is how a caller // can tell whether it already has a tree on this project. func (v *View) Root() string { return v.tree.Root().Path() } // Refresh re-reads the project and keeps the highlight on the same entry when // it is still there. // // view.Refresh() // after a build, or when a file is saved func (v *View) Refresh() { wanted := v.selectedPath() v.tree.Refresh() v.selectPath(wanted) } // selectedPath returns the path of the highlighted row, or "" when there is // no row to be on. func (v *View) selectedPath() string { rows := v.tree.Rows() if v.selected < 0 || v.selected >= len(rows) { return "" } return rows[v.selected].Node.Path() } // selectPath puts the highlight back on a path, or on the nearest row that // still exists when it has gone. func (v *View) selectPath(path string) { rows := v.tree.Rows() for i, row := range rows { if row.Node.Path() == path { v.selected = i return } } v.selected = min(max(v.selected, 0), max(len(rows)-1, 0)) } // Selected returns the highlighted node, or nil when the project is empty. func (v *View) Selected() *Node { rows := v.tree.Rows() if v.selected < 0 || v.selected >= len(rows) { return nil } return rows[v.selected].Node } // Choose acts on the highlighted row: a directory opens or closes, and a file // is handed to OnOpen. func (v *View) Choose() { node := v.Selected() if node == nil { return } if node.IsDir() { node.Toggle() v.keepSelectionInRange() return } if v.OnOpen != nil { v.OnOpen(node.Path()) } } // keepSelectionInRange holds the highlight inside the rows there are, which // collapsing a branch above it can otherwise leave past the end. func (v *View) keepSelectionInRange() { v.selected = min(max(v.selected, 0), max(len(v.tree.Rows())-1, 0)) }