| 🛟 Updated. 28d5985 k33g 18h ago | 1 | package filetree |
| 2 | |
| 3 | import ( |
| 4 | "codeberg.org/turbo-editors/turbo-core/ui" |
| 5 | ) |
| 6 | |
| 7 | // The markers drawn before a directory, and the indentation of one level. |
| 8 | // |
| 9 | // A file gets a marker's worth of blank so that names line up whatever their |
| 10 | // neighbours are, which is what makes a column of names readable at a glance. |
| 11 | const ( |
| 12 | openMarker = "▼ " |
| 13 | closedMarker = "▶ " |
| 14 | fileMarker = " " |
| 15 | indent = " " |
| 16 | ) |
| 17 | |
| 18 | // wheelStep is how many rows one notch of the mouse wheel moves. |
| 19 | const wheelStep = 3 |
| 20 | |
| 21 | // View shows a project's files in a window and opens the one that is chosen. |
| 22 | // |
| 23 | // It is a ui.Widget, so it goes into an ordinary window and gets the desktop's |
| 24 | // moving, resizing and maximising for free. |
| 25 | // |
| 26 | // view, err := filetree.NewView(".") |
| 27 | // if err != nil { |
| 28 | // return err |
| 29 | // } |
| 30 | // view.OnOpen = func(path string) { editor.Open(path) } |
| 31 | // window := ui.NewWindow(view.Title(), view) |
| 32 | type View struct { |
| 33 | ui.FocusBox |
| 34 | |
| 35 | tree *Tree |
| 36 | selected int |
| 37 | top int |
| 38 | |
| 39 | // OnOpen is called with the absolute path of a file that was chosen. A |
| 40 | // directory never reaches it: choosing one opens or closes the branch. |
| 41 | OnOpen func(path string) |
| 42 | } |
| 43 | |
| 44 | // NewView reads a directory and returns the widget showing it. |
| 45 | // |
| 46 | // The error is the one New gives: a root that is not a directory, or cannot be |
| 47 | // read at all, leaves the caller with nothing to show. |
| 48 | func NewView(root string) (*View, error) { |
| 49 | tree, err := New(root) |
| 50 | if err != nil { |
| 51 | return nil, err |
| 52 | } |
| 53 | return &View{tree: tree}, nil |
| 54 | } |
| 55 | |
| 56 | // Title returns what the window holding this view should be called: the name |
| 57 | // of the directory the tree is rooted at. |
| 58 | func (v *View) Title() string { return v.tree.Root().Name() } |
| 59 | |
| 60 | // Root returns the absolute path the tree is rooted at, which is how a caller |
| 61 | // can tell whether it already has a tree on this project. |
| 62 | func (v *View) Root() string { return v.tree.Root().Path() } |
| 63 | |
| 64 | // Refresh re-reads the project and keeps the highlight on the same entry when |
| 65 | // it is still there. |
| 66 | // |
| 67 | // view.Refresh() // after a build, or when a file is saved |
| 68 | func (v *View) Refresh() { |
| 69 | wanted := v.selectedPath() |
| 70 | v.tree.Refresh() |
| 71 | v.selectPath(wanted) |
| 72 | } |
| 73 | |
| 74 | // selectedPath returns the path of the highlighted row, or "" when there is |
| 75 | // no row to be on. |
| 76 | func (v *View) selectedPath() string { |
| 77 | rows := v.tree.Rows() |
| 78 | if v.selected < 0 || v.selected >= len(rows) { |
| 79 | return "" |
| 80 | } |
| 81 | return rows[v.selected].Node.Path() |
| 82 | } |
| 83 | |
| 84 | // selectPath puts the highlight back on a path, or on the nearest row that |
| 85 | // still exists when it has gone. |
| 86 | func (v *View) selectPath(path string) { |
| 87 | rows := v.tree.Rows() |
| 88 | for i, row := range rows { |
| 89 | if row.Node.Path() == path { |
| 90 | v.selected = i |
| 91 | return |
| 92 | } |
| 93 | } |
| 94 | v.selected = min(max(v.selected, 0), max(len(rows)-1, 0)) |
| 95 | } |
| 96 | |
| 97 | // Selected returns the highlighted node, or nil when the project is empty. |
| 98 | func (v *View) Selected() *Node { |
| 99 | rows := v.tree.Rows() |
| 100 | if v.selected < 0 || v.selected >= len(rows) { |
| 101 | return nil |
| 102 | } |
| 103 | return rows[v.selected].Node |
| 104 | } |
| 105 | |
| 106 | // Choose acts on the highlighted row: a directory opens or closes, and a file |
| 107 | // is handed to OnOpen. |
| 108 | func (v *View) Choose() { |
| 109 | node := v.Selected() |
| 110 | if node == nil { |
| 111 | return |
| 112 | } |
| 113 | if node.IsDir() { |
| 114 | node.Toggle() |
| 115 | v.keepSelectionInRange() |
| 116 | return |
| 117 | } |
| 118 | if v.OnOpen != nil { |
| 119 | v.OnOpen(node.Path()) |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | // keepSelectionInRange holds the highlight inside the rows there are, which |
| 124 | // collapsing a branch above it can otherwise leave past the end. |
| 125 | func (v *View) keepSelectionInRange() { |
| 126 | v.selected = min(max(v.selected, 0), max(len(v.tree.Rows())-1, 0)) |
| 127 | } |