From 6eed70e79e75f9196df36f7a79608f90882f62f9 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 10 Jan 2023 16:39:37 -0500 Subject: [PATCH] The beginnings of a layout system --- elements/basic/container.go | 136 +++++++++++++++++++++++ elements/layouts/vertical.go | 190 ++++++++++++-------------------- examples/verticalLayout/main.go | 19 +++- tomo.go | 22 ++++ 4 files changed, 244 insertions(+), 123 deletions(-) create mode 100644 elements/basic/container.go diff --git a/elements/basic/container.go b/elements/basic/container.go new file mode 100644 index 0000000..e130c62 --- /dev/null +++ b/elements/basic/container.go @@ -0,0 +1,136 @@ +package basic + +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/elements/core" + +type Container struct { + *core.Core + core core.CoreControl + + layout tomo.Layout + children []tomo.LayoutEntry + selectable bool +} + +func NewContainer (layout tomo.Layout) (element *Container) { + element = &Container { } + element.Core, element.core = core.NewCore(element) + element.SetLayout(layout) + return +} + +func (element *Container) SetLayout (layout tomo.Layout) { + element.layout = layout + element.recalculate() +} + +func (element *Container) Adopt (child tomo.Element, expand bool) { + child.SetParentHooks (tomo.ParentHooks { + MinimumSizeChange: + func (int, int) { element.updateMinimumSize() }, + SelectabilityChange: + func (bool) { element.updateSelectable() }, + }) + element.children = append (element.children, tomo.LayoutEntry { + Element: child, + }) + + element.updateMinimumSize() + element.updateSelectable() + element.recalculate() + if element.core.HasImage() { element.draw() } +} + +// Disown removes the given child from the container if it is contained within +// it. +func (element *Container) Disown (child tomo.Element) { + for index, entry := range element.children { + if entry.Element == child { + entry.SetParentHooks(tomo.ParentHooks { }) + element.children = append ( + element.children[:index], + element.children[index + 1:]...) + break + } + } + + element.updateMinimumSize() + element.updateSelectable() + element.recalculate() + if element.core.HasImage() { element.draw() } +} + +// Children returns a slice containing this element's children. +func (element *Container) Children () (children []tomo.Element) { + children = make([]tomo.Element, len(element.children)) + for index, entry := range element.children { + children[index] = entry.Element + } + return +} + +// CountChildren returns the amount of children contained within this element. +func (element *Container) 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 *Container) Child (index int) (child tomo.Element) { + if index < 0 || index > len(element.children) { return } + return element.children[index].Element +} + +func (element *Container) Handle (event tomo.Event) { + switch event.(type) { + case tomo.EventResize: + resizeEvent := event.(tomo.EventResize) + element.core.AllocateCanvas ( + resizeEvent.Width, + resizeEvent.Height) + element.recalculate() + element.draw() + + // TODO: + } + return +} + +func (element *Container) AdvanceSelection (direction int) (ok bool) { + // TODO: + return +} + +func (element *Container) updateSelectable () { + selectable := false + for _, entry := range element.children { + if entry.Selectable() { selectable = true } + } + element.core.SetSelectable(selectable) +} + +func (element *Container) updateMinimumSize () { + element.core.SetMinimumSize(element.layout.MinimumSize(element.children)) +} + +func (element *Container) recalculate () { + bounds := element.Bounds() + element.layout.Arrange(element.children, bounds.Dx(), bounds.Dy()) +} + +func (element *Container) draw () { + bounds := element.core.Bounds() + + artist.Rectangle ( + element.core, + theme.BackgroundImage(), + nil, 0, + bounds) + + // TODO + for _, entry := range element.children { + artist.Paste(element.core, entry, entry.Position) + } +} diff --git a/elements/layouts/vertical.go b/elements/layouts/vertical.go index 4b53fdf..2ebc7a8 100644 --- a/elements/layouts/vertical.go +++ b/elements/layouts/vertical.go @@ -1,136 +1,90 @@ package layouts +import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/theme" -// import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" -type verticalEntry struct { - y int - minHeight int - element tomo.Element -} - -// Vertical lays its children out vertically. It can contain any number of -// children. When an child is added to the layout, it can either be set to -// contract to its minimum height or expand to fill the remaining space (space -// that is not taken up by other children or padding is divided equally among -// these). Child elements will all have the same width. +// Vertical arranges elements vertically. Elements at the start of the entry +// list will be positioned at the top, and elements at the end of the entry list +// will positioned at the bottom. All elements have the same width. type Vertical struct { - *core.Core - core core.CoreControl + // If Gap is true, a gap will be placed between each element. + Gap bool - gap, pad bool - children []verticalEntry - selectable bool + // If Pad is true, there will be padding running along the inside of the + // layout's border. + Pad bool } -// NewVertical creates a new vertical layout. If gap is set to true, a gap will -// be placed between each child element. If pad is set to true, padding will be -// be placed around the inside of this element's border. Usually, you will want -// these to be true. -func NewVertical (gap, pad bool) (element *Vertical) { - element = &Vertical { } - element.Core, element.core = core.NewCore(element) - element.gap = gap - element.pad = pad - element.recalculate() - return -} +// Arrange arranges a list of entries vertically. +func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) { + if layout.Pad { + width -= theme.Padding() * 2 + height -= theme.Padding() * 2 + } + freeSpace := height + expandingElements := 0 -// SetPad sets whether or not padding will be placed around the inside of this -// element's border. -func (element *Vertical) SetPad (pad bool) { - changed := element.pad != pad - element.pad = pad - if changed { element.recalculate() } -} - -// SetGap sets whether or not a gap will be placed in between child elements. -func (element *Vertical) SetGap (gap bool) { - changed := element.gap != gap - element.gap = gap - if changed { element.recalculate() } -} - -// Adopt adds a child element to the vertical layout. If expand is set to true, -// the element will be expanded to fill a portion of the remaining space in the -// layout. -func (element *Vertical) Adopt (child tomo.Element, expand bool) { - _, minHeight := child.MinimumSize() - child.SetParentHooks (tomo.ParentHooks { - // TODO - }) - element.children = append (element.children, verticalEntry { - element: child, - minHeight: minHeight, - }) - if child.Selectable() { element.core.SetSelectable(true) } - - element.recalculate() -} - -// Disown removes the given child from the layout if it is contained within it. -func (element *Vertical) Disown (child tomo.Element) { - for index, entry := range element.children { - if entry.element == child { - entry.element.SetParentHooks(tomo.ParentHooks { }) - element.children = append ( - element.children[:index], - element.children[index + 1:]...) - break + // count the number of expanding elements and the amount of free space + // for them to collectively occupy + for _, entry := range entries { + if entry.Expand { + expandingElements ++ + } else { + _, entryMinHeight := entry.MinimumSize() + freeSpace -= entryMinHeight } } - - selectable := false - for _, entry := range element.children { - if entry.element.Selectable() { selectable = true } + expandingElementHeight := 0 + if expandingElements > 0 { + expandingElementHeight = freeSpace / expandingElements } - element.core.SetSelectable(selectable) -} - -// Children returns a slice containing this element's children. -func (element *Vertical) Children () (children []tomo.Element) { - children = make([]tomo.Element, len(element.children)) - for index, entry := range element.children { - children[index] = entry.element - } - return -} - -// CountChildren returns the amount of children contained within this element. -func (element *Vertical) 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 *Vertical) Child (index int) (child tomo.Element) { - if index < 0 || index > len(element.children) { return } - return element.children[index].element -} - -func (element *Vertical) Handle (event tomo.Event) { - switch event.(type) { - case tomo.EventResize: - element.recalculate() - // TODO: - // TODO: - } - return -} - -func (element *Vertical) AdvanceSelection (direction int) (ok bool) { - // TODO: - return -} - -func (element *Vertical) recalculate () { - var x, y int - if element.pad { + x, y := 0, 0 + if layout.Pad { x += theme.Padding() y += theme.Padding() } - // TODO + + // set the size and position of each element + for index, entry := range entries { + if index > 0 && layout.Gap { y += theme.Padding() } + + entries[index].Position = image.Pt(x, y) + entryHeight := 0 + if entry.Expand { + entryHeight = expandingElementHeight + } else { + _, entryHeight = entry.MinimumSize() + } + y += entryHeight + entryBounds := entry.Bounds() + if entryBounds.Dx() != width || entryBounds.Dy() != entryHeight { + entry.Handle (tomo.EventResize { + Width: width, + Height: entryHeight, + }) + } + } +} + +// MinimumSize returns the minimum width and height will be needed to arrange +// the given list of entries. +func (layout Vertical) MinimumSize (entries []tomo.LayoutEntry) (width, height int) { + for index, entry := range entries { + entryWidth, entryHeight := entry.MinimumSize() + if entryWidth > width { + width = entryWidth + } + height += entryHeight + if layout.Gap && index > 0 { + height += theme.Padding() + } + } + + if layout.Pad { + width += theme.Padding() * 2 + height += theme.Padding() * 2 + } + return } diff --git a/examples/verticalLayout/main.go b/examples/verticalLayout/main.go index 2c5b3d7..dd7d152 100644 --- a/examples/verticalLayout/main.go +++ b/examples/verticalLayout/main.go @@ -13,12 +13,21 @@ func run () { window, _ := tomo.NewWindow(2, 2) window.SetTitle("vertical stack") - layout := layouts.NewVertical(true, true) - window.Adopt(layout) + container := basic.NewContainer(layouts.Vertical { true, true }) + window.Adopt(container) - layout.Adopt(basic.NewLabel("it is a label hehe")) - layout.Adopt(basic.NewButton("yeah"), false) - layout.Adopt(button := basic.NewButton("wow"), false) + label := basic.NewLabel("it is a label hehe") + button := basic.NewButton("press me") + button.OnClick (func () { + label.SetText ( + "woah, this button changes the label text! since the " + + "size of this text box has changed, the window " + + "should expand (unless you resized it already).") + }) + + container.Adopt(label, true) + container.Adopt(basic.NewButton("yeah"), false) + container.Adopt(button, false) window.OnClose(tomo.Stop) window.Show() diff --git a/tomo.go b/tomo.go index 3aaf5bd..14ac3c6 100644 --- a/tomo.go +++ b/tomo.go @@ -150,6 +150,28 @@ type Window interface { OnClose (func ()) } +// LayoutEntry associates an element with layout and positioning information so +// it can be arranged by a Layout. +type LayoutEntry struct { + Element + Position image.Point + Expand bool +} + +// Layout is capable of arranging elements within a container. It is also able +// to determine the minimum amount of room it needs to do so. +type Layout interface { + // Arrange takes in a slice of entries and a bounding width and height, + // and changes the position of the entiries in the slice so that they + // are properly laid out. The given width and height should not be less + // than what is returned by MinimumSize. + Arrange (entries []LayoutEntry, width, height int) + + // MinimumSize returns the minimum width and height that the layout + // needs to properly arrange the given slice of layout entries. + MinimumSize (entries []LayoutEntry) (width, height int) +} + var backend Backend // Run initializes a backend, calls the callback function, and begins the event