From 081b005679ab73e2042bbaedf1b1e93b4b70b275 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 11 Mar 2023 18:00:29 -0500 Subject: [PATCH] Added a somewhat buggy DocumentContainer --- elements/basic/documentContainer.go | 342 ++++++++++++++++++++++++++++ elements/basic/scrollcontainer.go | 1 - examples/documentContainer/main.go | 41 ++++ 3 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 elements/basic/documentContainer.go create mode 100644 examples/documentContainer/main.go diff --git a/elements/basic/documentContainer.go b/elements/basic/documentContainer.go new file mode 100644 index 0000000..c525cde --- /dev/null +++ b/elements/basic/documentContainer.go @@ -0,0 +1,342 @@ +package basicElements + +import "image" +import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/layouts" +import "git.tebibyte.media/sashakoshka/tomo/elements" +import "git.tebibyte.media/sashakoshka/tomo/elements/core" + +type DocumentContainer struct { + *core.Core + *core.Propagator + core core.CoreControl + + children []layouts.LayoutEntry + scroll image.Point + warping bool + contentBounds image.Rectangle + + config config.Wrapped + theme theme.Wrapped + + onFocusRequest func () (granted bool) + onFocusMotionRequest func (input.KeynavDirection) (granted bool) + onScrollBoundsChange func () +} + +// NewDocumentContainer creates a new document container. +func NewDocumentContainer () (element *DocumentContainer) { + element = &DocumentContainer { } + element.theme.Case = theme.C("basic", "documentContainer") + element.Core, element.core = core.NewCore(element.redoAll) + element.Propagator = core.NewPropagator(element) + return +} + +// Adopt adds a new child element to the container. +func (element *DocumentContainer) Adopt (child elements.Element) { + // set event handlers + if child0, ok := child.(elements.Themeable); ok { + child0.SetTheme(element.theme.Theme) + } + if child0, ok := child.(elements.Configurable); ok { + child0.SetConfig(element.config.Config) + } + child.OnDamage (func (region canvas.Canvas) { + element.core.DamageRegion(region.Bounds()) + }) + child.OnMinimumSizeChange (func () { + element.redoAll() + element.core.DamageAll() + }) + if child0, ok := child.(elements.Flexible); ok { + child0.OnFlexibleHeightChange (func () { + element.redoAll() + element.core.DamageAll() + }) + } + if child0, ok := child.(elements.Focusable); ok { + child0.OnFocusRequest (func () (granted bool) { + return element.childFocusRequestCallback(child0) + }) + child0.OnFocusMotionRequest ( + func (direction input.KeynavDirection) (granted bool) { + if element.onFocusMotionRequest == nil { return } + return element.onFocusMotionRequest(direction) + }) + } + + // add child + element.children = append (element.children, layouts.LayoutEntry { + Element: child, + }) + + // refresh stale data + element.reflectChildProperties() + if element.core.HasImage() && !element.warping { + element.redoAll() + element.core.DamageAll() + } +} + +// Warp runs the specified callback, deferring all layout and rendering updates +// until the callback has finished executing. This allows for aplications to +// perform batch gui updates without flickering and stuff. +func (element *DocumentContainer) Warp (callback func ()) { + if element.warping { + callback() + return + } + + element.warping = true + callback() + element.warping = false + + if element.core.HasImage() { + element.redoAll() + element.core.DamageAll() + } +} + +// Disown removes the given child from the container if it is contained within +// it. +func (element *DocumentContainer) Disown (child elements.Element) { + for index, entry := range element.children { + if entry.Element == child { + element.clearChildEventHandlers(entry.Element) + element.children = append ( + element.children[:index], + element.children[index + 1:]...) + break + } + } + + element.reflectChildProperties() + if element.core.HasImage() && !element.warping { + element.redoAll() + element.core.DamageAll() + } +} + +func (element *DocumentContainer) clearChildEventHandlers (child elements.Element) { + child.DrawTo(nil) + child.OnDamage(nil) + child.OnMinimumSizeChange(nil) + if child0, ok := child.(elements.Focusable); ok { + child0.OnFocusRequest(nil) + child0.OnFocusMotionRequest(nil) + if child0.Focused() { + child0.HandleUnfocus() + } + } +} + +// DisownAll removes all child elements from the container at once. +func (element *DocumentContainer) DisownAll () { + for _, entry := range element.children { + element.clearChildEventHandlers(entry.Element) + } + element.children = nil + + element.reflectChildProperties() + if element.core.HasImage() && !element.warping { + element.redoAll() + element.core.DamageAll() + } +} + +// Children returns a slice containing this element's children. +func (element *DocumentContainer) Children () (children []elements.Element) { + children = make([]elements.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 *DocumentContainer) 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 *DocumentContainer) Child (index int) (child elements.Element) { + if index < 0 || index > len(element.children) { return } + return element.children[index].Element +} + +// ChildAt returns the child that contains the specified x and y coordinates. If +// there are no children at the coordinates, this method will return nil. +func (element *DocumentContainer) ChildAt (point image.Point) (child elements.Element) { + for _, entry := range element.children { + if point.In(entry.Bounds) { + child = entry.Element + } + } + return +} + +func (element *DocumentContainer) redoAll () { + if !element.core.HasImage() { return } + + // do a layout + 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.PatternBackground, + theme.State { }) + artist.DrawShatter ( + element.core, pattern, rocks...) + + element.partition() + if element.onScrollBoundsChange != nil { + element.onScrollBoundsChange() + } +} + +func (element *DocumentContainer) partition () { + for _, entry := range element.children { + entry.DrawTo(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)) + } + } +} + +// SetTheme sets the element's theme. +func (element *DocumentContainer) SetTheme (new theme.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + element.Propagator.SetTheme(new) + element.core.SetMinimumSize ( + element.theme.Padding(theme.PatternBackground).Horizontal(), + element.theme.Padding(theme.PatternBackground).Vertical(),) + element.redoAll() +} + +// SetConfig sets the element's configuration. +func (element *DocumentContainer) SetConfig (new config.Config) { + if new == element.config.Config { return } + element.Propagator.SetConfig(new) + element.redoAll() +} + +func (element *DocumentContainer) OnFocusRequest (callback func () (granted bool)) { + element.onFocusRequest = callback + element.Propagator.OnFocusRequest(callback) +} + +func (element *DocumentContainer) OnFocusMotionRequest ( + callback func (direction input.KeynavDirection) (granted bool), +) { + element.onFocusMotionRequest = callback + element.Propagator.OnFocusMotionRequest(callback) +} + +// ScrollContentBounds returns the full content size of the element. +func (element *DocumentContainer) ScrollContentBounds () image.Rectangle { + return element.contentBounds +} + +// ScrollViewportBounds returns the size and position of the element's +// viewport relative to ScrollBounds. +func (element *DocumentContainer) ScrollViewportBounds () image.Rectangle { + padding := element.theme.Padding(theme.PatternBackground) + 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 *DocumentContainer) ScrollTo (position image.Point) { + if position.Y < 0 { + position.Y = 0 + } + element.scroll = position + if element.core.HasImage() && !element.warping { + element.redoAll() + element.core.DamageAll() + } +} + +// ScrollAxes returns the supported axes for scrolling. +func (element *DocumentContainer) ScrollAxes () (horizontal, vertical bool) { + return false, true +} + +// OnScrollBoundsChange sets a function to be called when the element's +// ScrollContentBounds, ScrollViewportBounds, or ScrollAxes are changed. +func (element *DocumentContainer) OnScrollBoundsChange(callback func()) { + element.onScrollBoundsChange = callback +} + +func (element *DocumentContainer) reflectChildProperties () { + focusable := false + for _, entry := range element.children { + _, focusable := entry.Element.(elements.Focusable) + if focusable { + focusable = true + break + } + } + if !focusable && element.Focused() { + element.Propagator.HandleUnfocus() + } +} + +func (element *DocumentContainer) childFocusRequestCallback ( + child elements.Focusable, +) ( + granted bool, +) { + if element.onFocusRequest != nil && element.onFocusRequest() { + element.Propagator.HandleUnfocus() + element.Propagator.HandleFocus(input.KeynavDirectionNeutral) + return true + } else { + return false + } +} + +func (element *DocumentContainer) doLayout () { + margin := element.theme.Margin(theme.PatternBackground) + padding := element.theme.Padding(theme.PatternBackground) + bounds := padding.Apply(element.Bounds()) + element.contentBounds = image.Rectangle { } + + dot := bounds.Min.Sub(element.scroll) + for index, entry := range element.children { + if index > 0 { + dot.Y += margin.Y + } + + width, height := entry.MinimumSize() + if width < bounds.Dx() { + width = bounds.Dx() + } + if typedChild, ok := entry.Element.(elements.Flexible); ok { + height = typedChild.FlexibleHeightFor(width) + } + + entry.Bounds.Min = dot + entry.Bounds.Max = image.Pt(dot.X + width, dot.Y + height) + element.children[index] = entry + element.contentBounds = element.contentBounds.Union(entry.Bounds) + dot.Y += height + } +} diff --git a/elements/basic/scrollcontainer.go b/elements/basic/scrollcontainer.go index 2d5cf20..b990f1d 100644 --- a/elements/basic/scrollcontainer.go +++ b/elements/basic/scrollcontainer.go @@ -267,7 +267,6 @@ func (element *ScrollContainer) layout () ( } func (element *ScrollContainer) draw () { - // XOR if element.horizontal != nil && element.vertical != nil { bounds := element.Bounds() bounds.Min = image.Pt ( diff --git a/examples/documentContainer/main.go b/examples/documentContainer/main.go new file mode 100644 index 0000000..3035399 --- /dev/null +++ b/examples/documentContainer/main.go @@ -0,0 +1,41 @@ +package main + +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/elements/basic" +import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" + +func main () { + tomo.Run(run) +} + +func run () { + window, _ := tomo.NewWindow(480, 360) + window.SetTitle("Scroll") + + scrollContainer := basicElements.NewScrollContainer(false, true) + document := basicElements.NewDocumentContainer() + + document.Adopt (basicElements.NewLabel ( + "A document container is a vertically stacked container " + + "capable of properly laying out flexible elements such as " + + "text-wrapped labels.", true)) + document.Adopt (basicElements.NewButton ( + "You can also include normal elements like buttons,")) + document.Adopt (basicElements.NewCheckbox ( + "checkboxes,", true)) + document.Adopt(basicElements.NewTextBox("", "And text boxes.")) + document.Adopt (basicElements.NewSpacer(true)) + document.Adopt (basicElements.NewLabel ( + "Document containers are meant to be placed inside of a " + + "ScrollContainer, like this one.", true)) + document.Adopt (basicElements.NewLabel ( + "You could use document containers to do things like display various " + + "forms of hypertext (like HTML, gemtext, markdown, etc.), " + + "lay out a settings menu with descriptive label text between " + + "control groups like in iOS, or list comment or chat histories.", true)) + + scrollContainer.Adopt(document) + window.Adopt(scrollContainer) + window.OnClose(tomo.Stop) + window.Show() +}