From dbee2ff5a9c92007ece3f3686b3295d52088c689 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 20 Apr 2023 00:15:37 -0400 Subject: [PATCH] Directory view works --- elements/directory.go | 210 ++++++++++++++++++++++++++++++++++-- elements/document.go | 7 +- elements/file.go | 4 + examples/goroutines/main.go | 1 + 4 files changed, 214 insertions(+), 8 deletions(-) diff --git a/elements/directory.go b/elements/directory.go index f5a1221..6859d59 100644 --- a/elements/directory.go +++ b/elements/directory.go @@ -3,10 +3,19 @@ package elements import "image" import "path/filepath" import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/shatter" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" // TODO: base on flow implementation of list. also be able to switch to a table // variant for a more information dense view. +type directoryEntity interface { + tomo.ContainerEntity + tomo.ScrollableEntity +} + type historyEntry struct { location string filesystem ReadDirStatFS @@ -15,10 +24,18 @@ type historyEntry struct { // Directory displays a list of files within a particular directory and // file system. type Directory struct { - *List + container + entity directoryEntity + theme theme.Wrapped + + scroll image.Point + contentBounds image.Rectangle + history []historyEntry historyIndex int - onChoose func (file string) + + onChoose func (file string) + onScrollBoundsChange func () } // NewDirectory creates a new directory view. If within is nil, it will use @@ -30,13 +47,159 @@ func NewDirectory ( element *Directory, err error, ) { - element = &Directory { - List: NewList(8), - } + element = &Directory { } + element.theme.Case = tomo.C("tomo", "list") + element.entity = tomo.NewEntity(element).(directoryEntity) + element.container.entity = element.entity + element.minimumSize = element.updateMinimumSize + element.init() err = element.SetLocation(location, within) return } +func (element *Directory) Draw (destination canvas.Canvas) { + rocks := make([]image.Rectangle, element.entity.CountChildren()) + for index := 0; index < element.entity.CountChildren(); index ++ { + rocks[index] = element.entity.Child(index).Entity().Bounds() + } + + tiles := shatter.Shatter(element.entity.Bounds(), rocks...) + for _, tile := range tiles { + element.DrawBackground(canvas.Cut(destination, tile)) + } +} + +func (element *Directory) Layout () { + if element.scroll.Y > element.maxScrollHeight() { + element.scroll.Y = element.maxScrollHeight() + } + + margin := element.theme.Margin(tomo.PatternPinboard) + padding := element.theme.Padding(tomo.PatternPinboard) + bounds := padding.Apply(element.entity.Bounds()) + element.contentBounds = image.Rectangle { } + + dot := bounds.Min.Sub(element.scroll) + xStart := dot.X + rowHeight := 0 + + nextLine := func () { + dot.X = xStart + dot.Y += margin.Y + dot.Y += rowHeight + rowHeight = 0 + } + + for index := 0; index < element.entity.CountChildren(); index ++ { + child := element.entity.Child(index) + entry := element.scratch[child] + + width := int(entry.minBreadth) + height := int(entry.minSize) + if width + dot.X > bounds.Max.X { + nextLine() + } + if typedChild, ok := child.(tomo.Flexible); ok { + height = typedChild.FlexibleHeightFor(width) + } + if rowHeight < height { + rowHeight = height + } + + childBounds := tomo.Bounds ( + dot.X, dot.Y, + width, height) + element.entity.PlaceChild(index, childBounds) + element.contentBounds = element.contentBounds.Union(childBounds) + + dot.X += width + margin.X + } + + element.contentBounds = + element.contentBounds.Sub(element.contentBounds.Min) + + element.entity.NotifyScrollBoundsChange() + if element.onScrollBoundsChange != nil { + element.onScrollBoundsChange() + } +} + +func (element *Directory) HandleMouseDown (x, y int, button input.Button) { + element.selectNone() +} + +func (element *Directory) HandleMouseUp (x, y int, button input.Button) { } + +func (element *Directory) HandleChildMouseDown (x, y int, button input.Button, child tomo.Element) { + element.selectNone() + if child, ok := child.(tomo.Selectable); ok { + index := element.entity.IndexOf(child) + element.entity.SelectChild(index, true) + } +} + +func (element *Directory) HandleChildMouseUp (int, int, input.Button, tomo.Element) { } + +func (element *Directory) HandleChildFlexibleHeightChange (child tomo.Flexible) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// ScrollContentBounds returns the full content size of the element. +func (element *Directory) ScrollContentBounds () image.Rectangle { + return element.contentBounds +} + +// ScrollViewportBounds returns the size and position of the element's +// viewport relative to ScrollBounds. +func (element *Directory) ScrollViewportBounds () image.Rectangle { + padding := element.theme.Padding(tomo.PatternPinboard) + bounds := padding.Apply(element.entity.Bounds()) + bounds = bounds.Sub(bounds.Min).Add(element.scroll) + return bounds +} + +// ScrollTo scrolls the viewport to the specified point relative to +// ScrollBounds. +func (element *Directory) ScrollTo (position image.Point) { + if position.Y < 0 { + position.Y = 0 + } + maxScrollHeight := element.maxScrollHeight() + if position.Y > maxScrollHeight { + position.Y = maxScrollHeight + } + element.scroll = position + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// OnScrollBoundsChange sets a function to be called when the element's viewport +// bounds, content bounds, or scroll axes change. +func (element *Directory) OnScrollBoundsChange (callback func ()) { + element.onScrollBoundsChange = callback +} + +// ScrollAxes returns the supported axes for scrolling. +func (element *Directory) ScrollAxes () (horizontal, vertical bool) { + return false, true +} + +func (element *Directory) DrawBackground (destination canvas.Canvas) { + element.theme.Pattern(tomo.PatternPinboard, tomo.State { }). + Draw(destination, element.entity.Bounds()) +} + +// SetTheme sets the element's theme. +func (element *Directory) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + // Location returns the directory's location and filesystem. func (element *Directory) Location () (string, ReadDirStatFS) { if len(element.history) < 1 { return "", nil } @@ -101,7 +264,7 @@ func (element *Directory) Update () error { children[index] = file } - + element.DisownAll() element.Adopt(children...) return err @@ -112,3 +275,38 @@ func (element *Directory) Update () error { func (element *Directory) OnChoose (callback func (file string)) { element.onChoose = callback } + +func (element *Directory) selectNone () { + for index := 0; index < element.entity.CountChildren(); index ++ { + element.entity.SelectChild(index, false) + } +} + +func (element *Directory) maxScrollHeight () (height int) { + padding := element.theme.Padding(tomo.PatternSunken) + viewportHeight := element.entity.Bounds().Dy() - padding.Vertical() + height = element.contentBounds.Dy() - viewportHeight + if height < 0 { height = 0 } + return +} + + +func (element *Directory) updateMinimumSize () { + padding := element.theme.Padding(tomo.PatternPinboard) + minimumWidth := 0 + for index := 0; index < element.entity.CountChildren(); index ++ { + width, height := element.entity.ChildMinimumSize(index) + if width > minimumWidth { + minimumWidth = width + } + + key := element.entity.Child(index) + entry := element.scratch[key] + entry.minSize = float64(height) + entry.minBreadth = float64(width) + element.scratch[key] = entry + } + element.entity.SetMinimumSize ( + minimumWidth + padding.Horizontal(), + padding.Vertical()) +} diff --git a/elements/document.go b/elements/document.go index 276b61c..9954322 100644 --- a/elements/document.go +++ b/elements/document.go @@ -47,6 +47,10 @@ func (element *Document) Draw (destination canvas.Canvas) { } func (element *Document) Layout () { + if element.scroll.Y > element.maxScrollHeight() { + element.scroll.Y = element.maxScrollHeight() + } + margin := element.theme.Margin(tomo.PatternBackground) padding := element.theme.Padding(tomo.PatternBackground) bounds := padding.Apply(element.entity.Bounds()) @@ -73,7 +77,7 @@ func (element *Document) Layout () { width := int(entry.minBreadth) height := int(entry.minSize) - if width + dot.X > bounds.Dx() && !entry.expand { + if width + dot.X > bounds.Max.X && !entry.expand { nextLine() } if width < bounds.Dx() && entry.expand { @@ -135,7 +139,6 @@ func (element *Document) SetTheme (theme tomo.Theme) { element.entity.InvalidateLayout() } - // ScrollContentBounds returns the full content size of the element. func (element *Document) ScrollContentBounds () image.Rectangle { return element.contentBounds diff --git a/elements/file.go b/elements/file.go index 6a758ce..6d5835c 100644 --- a/elements/file.go +++ b/elements/file.go @@ -140,6 +140,10 @@ func (element *File) HandleFocusChange () { element.entity.Invalidate() } +func (element *File) HandleSelectionChange () { + element.entity.Invalidate() +} + func (element *File) OnChoose (callback func ()) { element.onChoose = callback } diff --git a/examples/goroutines/main.go b/examples/goroutines/main.go index 5df9198..bfbb508 100644 --- a/examples/goroutines/main.go +++ b/examples/goroutines/main.go @@ -15,6 +15,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 200, 216)) window.SetTitle("Clock") + window.SetApplicationName("TomoClock") container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container)