The typesetter will need to be reworked to properly break lines in the middle of words for this to function properly.
374 lines
10 KiB
Go
374 lines
10 KiB
Go
package fileElements
|
|
|
|
import "io/fs"
|
|
import "image"
|
|
import "path/filepath"
|
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
|
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
|
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
|
|
|
type fileLayoutEntry struct {
|
|
*File
|
|
fs.DirEntry
|
|
Bounds image.Rectangle
|
|
Drawer textdraw.Drawer
|
|
TextPoint image.Point
|
|
}
|
|
|
|
type historyEntry struct {
|
|
location string
|
|
filesystem ReadDirStatFS
|
|
}
|
|
|
|
// DirectoryView displays a list of files within a particular directory and
|
|
// file system.
|
|
type DirectoryView struct {
|
|
*core.Core
|
|
*core.Propagator
|
|
core core.CoreControl
|
|
|
|
children []fileLayoutEntry
|
|
scroll image.Point
|
|
contentBounds image.Rectangle
|
|
|
|
config config.Wrapped
|
|
theme theme.Wrapped
|
|
|
|
onScrollBoundsChange func ()
|
|
|
|
history []historyEntry
|
|
historyIndex int
|
|
onChoose func (file string)
|
|
}
|
|
|
|
// NewDirectoryView creates a new directory view. If within is nil, it will use
|
|
// the OS file system.
|
|
func NewDirectoryView (
|
|
location string,
|
|
within ReadDirStatFS,
|
|
) (
|
|
element *DirectoryView,
|
|
err error,
|
|
) {
|
|
element = &DirectoryView { }
|
|
element.theme.Case = theme.C("files", "directoryView")
|
|
element.Core, element.core = core.NewCore(element, element.redoAll)
|
|
element.Propagator = core.NewPropagator(element, element.core)
|
|
err = element.SetLocation(location, within)
|
|
return
|
|
}
|
|
|
|
// Location returns the directory's location and filesystem.
|
|
func (element *DirectoryView) Location () (string, ReadDirStatFS) {
|
|
if len(element.history) < 1 { return "", nil }
|
|
current := element.history[element.historyIndex]
|
|
return current.location, current.filesystem
|
|
}
|
|
|
|
// SetLocation sets the directory's location and filesystem. If within is nil,
|
|
// it will use the OS file system.
|
|
func (element *DirectoryView) SetLocation (
|
|
location string,
|
|
within ReadDirStatFS,
|
|
) error {
|
|
if within == nil {
|
|
within = defaultFS { }
|
|
}
|
|
element.scroll = image.Point { }
|
|
|
|
if element.history != nil {
|
|
element.historyIndex ++
|
|
}
|
|
element.history = append (
|
|
element.history[:element.historyIndex],
|
|
historyEntry { location, within })
|
|
return element.Update()
|
|
}
|
|
|
|
// Backward goes back a directory in history
|
|
func (element *DirectoryView) Backward () (bool, error) {
|
|
if element.historyIndex > 1 {
|
|
element.historyIndex --
|
|
return true, element.Update()
|
|
} else {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
// Forward goes forward a directory in history
|
|
func (element *DirectoryView) Forward () (bool, error) {
|
|
if element.historyIndex < len(element.history) - 1 {
|
|
element.historyIndex ++
|
|
return true, element.Update()
|
|
} else {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
// Update refreshes the directory's contents.
|
|
func (element *DirectoryView) Update () error {
|
|
location, filesystem := element.Location()
|
|
entries, err := filesystem.ReadDir(location)
|
|
|
|
// disown all entries
|
|
for _, file := range element.children {
|
|
file.DrawTo(nil, image.Rectangle { }, nil)
|
|
file.SetParent(nil)
|
|
|
|
if file.Focused() {
|
|
file.HandleUnfocus()
|
|
}
|
|
}
|
|
|
|
element.children = make([]fileLayoutEntry, len(entries))
|
|
for index, entry := range entries {
|
|
filePath := filepath.Join(location, entry.Name())
|
|
file, err := NewFile(filePath, filesystem)
|
|
if err != nil { continue }
|
|
file.SetParent(element)
|
|
file.OnChoose (func () {
|
|
if element.onChoose != nil {
|
|
element.onChoose(filePath)
|
|
}
|
|
})
|
|
element.children[index].File = file
|
|
element.children[index].DirEntry = entry
|
|
element.children[index].Drawer.SetFace (element.theme.FontFace(
|
|
theme.FontStyleRegular,
|
|
theme.FontSizeNormal))
|
|
element.children[index].Drawer.SetText([]rune(entry.Name()))
|
|
element.children[index].Drawer.SetAlign(textdraw.AlignCenter)
|
|
}
|
|
|
|
if element.core.HasImage() {
|
|
element.redoAll()
|
|
element.core.DamageAll()
|
|
}
|
|
return err
|
|
}
|
|
|
|
// OnChoose sets a function to be called when the user double-clicks a file or
|
|
// sub-directory within the directory view.
|
|
func (element *DirectoryView) OnChoose (callback func (file string)) {
|
|
element.onChoose = callback
|
|
}
|
|
|
|
// CountChildren returns the amount of children contained within this element.
|
|
func (element *DirectoryView) CountChildren () (count int) {
|
|
return len(element.children)
|
|
}
|
|
|
|
// Child returns the child at the specified index. If the index is out of
|
|
// bounds, this method will return nil.
|
|
func (element *DirectoryView) Child (index int) (child elements.Element) {
|
|
if index < 0 || index > len(element.children) { return }
|
|
return element.children[index].File
|
|
}
|
|
|
|
func (element *DirectoryView) HandleMouseDown (x, y int, button input.Button) {
|
|
var file *File
|
|
for _, entry := range element.children {
|
|
if image.Pt(x, y).In(entry.Bounds) {
|
|
file = entry.File
|
|
}
|
|
}
|
|
if file != nil {
|
|
file.SetSelected(!file.Selected())
|
|
}
|
|
element.Propagator.HandleMouseDown(x, y, button)
|
|
}
|
|
|
|
func (element *DirectoryView) redoAll () {
|
|
if !element.core.HasImage() { return }
|
|
|
|
// do a layout
|
|
element.doLayout()
|
|
|
|
maxScrollHeight := element.maxScrollHeight()
|
|
if element.scroll.Y > maxScrollHeight {
|
|
element.scroll.Y = maxScrollHeight
|
|
element.doLayout()
|
|
}
|
|
|
|
// draw a background
|
|
rocks := make([]image.Rectangle, len(element.children))
|
|
for index, entry := range element.children {
|
|
rocks[index] = entry.Bounds
|
|
}
|
|
pattern := element.theme.Pattern (
|
|
theme.PatternPinboard,
|
|
theme.State { })
|
|
artist.DrawShatter(element.core, pattern, element.Bounds(), rocks...)
|
|
|
|
element.partition()
|
|
if parent, ok := element.core.Parent().(elements.ScrollableParent); ok {
|
|
parent.NotifyScrollBoundsChange(element)
|
|
}
|
|
if element.onScrollBoundsChange != nil {
|
|
element.onScrollBoundsChange()
|
|
}
|
|
|
|
// draw labels
|
|
foreground := element.theme.Color(theme.ColorForeground, theme.State { })
|
|
for _, entry := range element.children {
|
|
entry.Drawer.Draw(element.core, foreground, entry.TextPoint)
|
|
}
|
|
}
|
|
|
|
func (element *DirectoryView) partition () {
|
|
for _, entry := range element.children {
|
|
entry.DrawTo(nil, entry.Bounds, nil)
|
|
}
|
|
|
|
// cut our canvas up and give peices to child elements
|
|
for _, entry := range element.children {
|
|
if entry.Bounds.Overlaps(element.Bounds()) {
|
|
entry.DrawTo (
|
|
canvas.Cut(element.core, entry.Bounds),
|
|
entry.Bounds, func (region image.Rectangle) {
|
|
element.core.DamageRegion(region)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// NotifyMinimumSizeChange notifies the container that the minimum size of a
|
|
// child element has changed.
|
|
func (element *DirectoryView) NotifyMinimumSizeChange (child elements.Element) {
|
|
element.redoAll()
|
|
element.core.DamageAll()
|
|
}
|
|
|
|
// SetTheme sets the element's theme.
|
|
func (element *DirectoryView) SetTheme (new theme.Theme) {
|
|
if new == element.theme.Theme { return }
|
|
element.theme.Theme = new
|
|
element.Propagator.SetTheme(new)
|
|
element.redoAll()
|
|
}
|
|
|
|
// SetConfig sets the element's configuration.
|
|
func (element *DirectoryView) SetConfig (new config.Config) {
|
|
if new == element.config.Config { return }
|
|
element.Propagator.SetConfig(new)
|
|
element.redoAll()
|
|
}
|
|
// ScrollContentBounds returns the full content size of the element.
|
|
func (element *DirectoryView) ScrollContentBounds () image.Rectangle {
|
|
return element.contentBounds
|
|
}
|
|
|
|
// ScrollViewportBounds returns the size and position of the element's
|
|
// viewport relative to ScrollBounds.
|
|
func (element *DirectoryView) ScrollViewportBounds () image.Rectangle {
|
|
padding := element.theme.Padding(theme.PatternPinboard)
|
|
bounds := padding.Apply(element.Bounds())
|
|
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
|
|
return bounds
|
|
}
|
|
|
|
// ScrollTo scrolls the viewport to the specified point relative to
|
|
// ScrollBounds.
|
|
func (element *DirectoryView) ScrollTo (position image.Point) {
|
|
if position.Y < 0 {
|
|
position.Y = 0
|
|
}
|
|
maxScrollHeight := element.maxScrollHeight()
|
|
if position.Y > maxScrollHeight {
|
|
position.Y = maxScrollHeight
|
|
}
|
|
element.scroll = position
|
|
if element.core.HasImage() {
|
|
element.redoAll()
|
|
element.core.DamageAll()
|
|
}
|
|
}
|
|
|
|
// OnScrollBoundsChange sets a function to be called when the element's viewport
|
|
// bounds, content bounds, or scroll axes change.
|
|
func (element *DirectoryView) OnScrollBoundsChange (callback func ()) {
|
|
element.onScrollBoundsChange = callback
|
|
}
|
|
|
|
// ScrollAxes returns the supported axes for scrolling.
|
|
func (element *DirectoryView) ScrollAxes () (horizontal, vertical bool) {
|
|
return false, true
|
|
}
|
|
|
|
func (element *DirectoryView) maxScrollHeight () (height int) {
|
|
padding := element.theme.Padding(theme.PatternSunken)
|
|
viewportHeight := element.Bounds().Dy() - padding.Vertical()
|
|
height = element.contentBounds.Dy() - viewportHeight
|
|
if height < 0 { height = 0 }
|
|
return
|
|
}
|
|
|
|
func (element *DirectoryView) doLayout () {
|
|
margin := element.theme.Margin(theme.PatternPinboard)
|
|
padding := element.theme.Padding(theme.PatternPinboard)
|
|
bounds := padding.Apply(element.Bounds())
|
|
element.contentBounds = image.Rectangle { }
|
|
|
|
beginningOfRow := true
|
|
dot := bounds.Min.Sub(element.scroll)
|
|
rowHeight := 0
|
|
for index, entry := range element.children {
|
|
width, height := entry.MinimumSize()
|
|
|
|
if dot.X + width > bounds.Max.X {
|
|
dot.X = bounds.Min.Sub(element.scroll).X
|
|
dot.Y += rowHeight
|
|
if index > 1 {
|
|
dot.Y += margin.Y
|
|
}
|
|
beginningOfRow = true
|
|
}
|
|
|
|
if beginningOfRow {
|
|
beginningOfRow = false
|
|
} else {
|
|
dot.X += margin.X
|
|
}
|
|
|
|
entry.Drawer.SetMaxWidth(width)
|
|
bounds := image.Rect(dot.X, dot.Y, dot.X + width, dot.Y + height)
|
|
entry.Bounds = bounds
|
|
|
|
drawerHeight := entry.Drawer.ReccomendedHeightFor(width)
|
|
entry.TextPoint =
|
|
image.Pt(bounds.Min.X, bounds.Max.Y + margin.Y).
|
|
Sub(entry.Drawer.LayoutBounds().Min)
|
|
bounds.Max.Y += margin.Y + drawerHeight
|
|
height += margin.Y + drawerHeight
|
|
if rowHeight < height {
|
|
rowHeight = height
|
|
}
|
|
|
|
element.contentBounds = element.contentBounds.Union(bounds)
|
|
element.children[index] = entry
|
|
dot.X += width
|
|
}
|
|
|
|
element.contentBounds =
|
|
element.contentBounds.Sub(element.contentBounds.Min)
|
|
}
|
|
|
|
func (element *DirectoryView) updateMinimumSize () {
|
|
padding := element.theme.Padding(theme.PatternPinboard)
|
|
minimumWidth := 0
|
|
for _, entry := range element.children {
|
|
width, _ := entry.MinimumSize()
|
|
if width > minimumWidth {
|
|
minimumWidth = width
|
|
}
|
|
}
|
|
element.core.SetMinimumSize (
|
|
minimumWidth + padding.Horizontal(),
|
|
padding.Vertical())
|
|
}
|