diff --git a/artist/pattern.go b/artist/pattern.go index 558bbf6..c2caa5c 100644 --- a/artist/pattern.go +++ b/artist/pattern.go @@ -7,61 +7,55 @@ import "git.tebibyte.media/sashakoshka/tomo/shatter" // Pattern is capable of drawing to a canvas within the bounds of a given // clipping rectangle. type Pattern interface { - // Draw draws to destination, using the bounds of destination as a width - // and height for things like gradients, bevels, etc. The pattern may - // not draw outside the union of destination.Bounds() and clip. The - // clipping rectangle effectively takes a subset of the pattern. To - // change the bounds of the pattern itself, use canvas.Cut() on the - // destination before passing it to Draw(). - Draw (destination canvas.Canvas, clip image.Rectangle) + // Draw draws the pattern onto the destination canvas, using the + // specified bounds. The given bounds can be smaller or larger than the + // bounds of the destination canvas. The destination canvas can be cut + // using canvas.Cut() to draw only a specific subset of a pattern. + Draw (destination canvas.Canvas, bounds image.Rectangle) } -// Draw lets you use several clipping rectangles to draw a pattern. -func Draw ( +// Fill fills the destination canvas with the given pattern. +func Fill (destination canvas.Canvas, source Pattern) (updated image.Rectangle) { + source.Draw(destination, destination.Bounds()) + return destination.Bounds() +} + +// DrawClip lets you draw several subsets of a pattern at once. +func DrawClip ( destination canvas.Canvas, source Pattern, - clips ...image.Rectangle, + bounds image.Rectangle, + subsets ...image.Rectangle, ) ( updatedRegion image.Rectangle, ) { - for _, clip := range clips { - source.Draw(destination, clip) - updatedRegion = updatedRegion.Union(clip) + for _, subset := range subsets { + source.Draw(canvas.Cut(destination, subset), bounds) + updatedRegion = updatedRegion.Union(subset) } return } -// DrawBounds lets you specify an overall bounding rectangle for drawing a -// pattern. The destination is cut to this rectangle. -func DrawBounds ( - destination canvas.Canvas, - source Pattern, - bounds image.Rectangle, -) ( - updatedRegion image.Rectangle, -) { - return Draw(canvas.Cut(destination, bounds), source, bounds) -} - -// DrawShatter is like an inverse of Draw, drawing nothing in the areas -// specified in "rocks". +// DrawShatter is like an inverse of DrawClip, drawing nothing in the areas +// specified by "rocks". func DrawShatter ( destination canvas.Canvas, source Pattern, + bounds image.Rectangle, rocks ...image.Rectangle, ) ( updatedRegion image.Rectangle, ) { - tiles := shatter.Shatter(destination.Bounds(), rocks...) - return Draw(destination, source, tiles...) + tiles := shatter.Shatter(bounds, rocks...) + return DrawClip(destination, source, bounds, tiles...) } // AllocateSample returns a new canvas containing the result of a pattern. The // resulting canvas can be sourced from shape drawing functions. I beg of you // please do not call this every time you need to draw a shape with a pattern on // it because that is horrible and cruel to the computer. -func AllocateSample (source Pattern, width, height int) (allocated canvas.Canvas) { - allocated = canvas.NewBasicCanvas(width, height) - source.Draw(allocated, allocated.Bounds()) - return +func AllocateSample (source Pattern, width, height int) canvas.Canvas { + allocated := canvas.NewBasicCanvas(width, height) + Fill(allocated, source) + return allocated } diff --git a/artist/patterns/border.go b/artist/patterns/border.go index 3202a0b..0ce56c0 100644 --- a/artist/patterns/border.go +++ b/artist/patterns/border.go @@ -37,9 +37,9 @@ type Border struct { // Draw draws the border pattern onto the destination canvas within the clipping // bounds. -func (pattern Border) Draw (destination canvas.Canvas, clip image.Rectangle) { - bounds := clip.Canon().Intersect(destination.Bounds()) - if bounds.Empty() { return } +func (pattern Border) Draw (destination canvas.Canvas, bounds image.Rectangle) { + drawBounds := bounds.Canon().Intersect(destination.Bounds()) + if drawBounds.Empty() { return } srcSections := nonasect(pattern.Bounds(), pattern.Inset) srcTextures := [9]Texture { } @@ -47,9 +47,9 @@ func (pattern Border) Draw (destination canvas.Canvas, clip image.Rectangle) { srcTextures[index].Canvas = canvas.Cut(pattern, section) } - dstSections := nonasect(destination.Bounds(), pattern.Inset) + dstSections := nonasect(bounds, pattern.Inset) for index, section := range dstSections { - srcTextures[index].Draw(canvas.Cut(destination, section), clip) + srcTextures[index].Draw(destination, section) } } diff --git a/artist/patterns/texture.go b/artist/patterns/texture.go index 47f0596..91e3e40 100644 --- a/artist/patterns/texture.go +++ b/artist/patterns/texture.go @@ -9,25 +9,24 @@ type Texture struct { canvas.Canvas } -// Draw tiles the pattern's canvas within the clipping bounds. The minimum +// Draw tiles the pattern's canvas within the given bounds. The minimum // points of the pattern's canvas and the destination canvas will be lined up. -func (pattern Texture) Draw (destination canvas.Canvas, clip image.Rectangle) { - realBounds := destination.Bounds() - bounds := clip.Canon().Intersect(realBounds) - if bounds.Empty() { return } +func (pattern Texture) Draw (destination canvas.Canvas, bounds image.Rectangle) { + drawBounds := bounds.Canon().Intersect(destination.Bounds()) + if drawBounds.Empty() { return } dstData, dstStride := destination.Buffer() srcData, srcStride := pattern.Buffer() srcBounds := pattern.Bounds() dstPoint := image.Point { } - srcPoint := bounds.Min.Sub(realBounds.Min).Add(srcBounds.Min) + srcPoint := drawBounds.Min.Sub(bounds.Min).Add(srcBounds.Min) srcPoint.X = wrap(srcPoint.X, srcBounds.Min.X, srcBounds.Max.X) srcPoint.Y = wrap(srcPoint.Y, srcBounds.Min.Y, srcBounds.Max.Y) - for dstPoint.Y = bounds.Min.Y; dstPoint.Y < bounds.Max.Y; dstPoint.Y ++ { + for dstPoint.Y = drawBounds.Min.Y; dstPoint.Y < drawBounds.Max.Y; dstPoint.Y ++ { srcPoint.X = srcBounds.Min.X - dstPoint.X = bounds.Min.X + dstPoint.X = drawBounds.Min.X dstYComponent := dstPoint.Y * dstStride srcYComponent := srcPoint.Y * srcStride @@ -42,7 +41,7 @@ func (pattern Texture) Draw (destination canvas.Canvas, clip image.Rectangle) { } dstPoint.X ++ - if dstPoint.X >= bounds.Max.X { + if dstPoint.X >= drawBounds.Max.X { break } } diff --git a/artist/patterns/uniform.go b/artist/patterns/uniform.go index 8efb8be..173367a 100644 --- a/artist/patterns/uniform.go +++ b/artist/patterns/uniform.go @@ -9,9 +9,9 @@ import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" // Uniform is a pattern that draws a solid color. type Uniform color.RGBA -// Draw fills the clipping rectangle with the pattern's color. -func (pattern Uniform) Draw (destination canvas.Canvas, clip image.Rectangle) { - shapes.FillColorRectangle(destination, color.RGBA(pattern), clip) +// Draw fills the bounding rectangle with the pattern's color. +func (pattern Uniform) Draw (destination canvas.Canvas, bounds image.Rectangle) { + shapes.FillColorRectangle(destination, color.RGBA(pattern), bounds) } // Uhex creates a new Uniform pattern from an RGBA integer value. diff --git a/artist/shapes/ellipse.go b/artist/shapes/ellipse.go index 0e5f0c5..c30041d 100644 --- a/artist/shapes/ellipse.go +++ b/artist/shapes/ellipse.go @@ -13,6 +13,7 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" func FillEllipse ( destination canvas.Canvas, source canvas.Canvas, + bounds image.Rectangle, ) ( updatedRegion image.Rectangle, ) { @@ -20,15 +21,17 @@ func FillEllipse ( srcData, srcStride := source.Buffer() offset := source.Bounds().Min.Sub(destination.Bounds().Min) - bounds := source.Bounds().Sub(offset).Intersect(destination.Bounds()) - realBounds := destination.Bounds() + drawBounds := + source.Bounds().Sub(offset). + Intersect(destination.Bounds()). + Intersect(bounds) if bounds.Empty() { return } updatedRegion = bounds point := image.Point { } - for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ { - for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ { - if inEllipse(point, realBounds) { + for point.Y = drawBounds.Min.Y; point.Y < drawBounds.Max.Y; point.Y ++ { + for point.X = drawBounds.Min.X; point.X < drawBounds.Max.X; point.X ++ { + if inEllipse(point, bounds) { offsetPoint := point.Add(offset) dstIndex := point.X + point.Y * dstStride srcIndex := offsetPoint.X + offsetPoint.Y * srcStride @@ -41,6 +44,7 @@ func FillEllipse ( func StrokeEllipse ( destination canvas.Canvas, source canvas.Canvas, + bounds image.Rectangle, weight int, ) { if weight < 1 { return } @@ -48,10 +52,9 @@ func StrokeEllipse ( dstData, dstStride := destination.Buffer() srcData, srcStride := source.Buffer() - bounds := destination.Bounds().Inset(weight - 1) + drawBounds := destination.Bounds().Inset(weight - 1) offset := source.Bounds().Min.Sub(destination.Bounds().Min) - realBounds := destination.Bounds() - if bounds.Empty() { return } + if drawBounds.Empty() { return } context := ellipsePlottingContext { plottingContext: plottingContext { @@ -61,11 +64,11 @@ func StrokeEllipse ( srcStride: srcStride, weight: weight, offset: offset, - bounds: realBounds, + bounds: bounds, }, - radii: image.Pt(bounds.Dx() / 2, bounds.Dy() / 2), + radii: image.Pt(drawBounds.Dx() / 2, drawBounds.Dy() / 2), } - context.center = bounds.Min.Add(context.radii) + context.center = drawBounds.Min.Add(context.radii) context.plotEllipse() } diff --git a/artist/shapes/rectangle.go b/artist/shapes/rectangle.go index 8912ade..968e00c 100644 --- a/artist/shapes/rectangle.go +++ b/artist/shapes/rectangle.go @@ -10,20 +10,24 @@ import "git.tebibyte.media/sashakoshka/tomo/shatter" func FillRectangle ( destination canvas.Canvas, source canvas.Canvas, + bounds image.Rectangle, ) ( updatedRegion image.Rectangle, ) { dstData, dstStride := destination.Buffer() srcData, srcStride := source.Buffer() - offset := source.Bounds().Min.Sub(destination.Bounds().Min) - bounds := source.Bounds().Sub(offset).Intersect(destination.Bounds()) - if bounds.Empty() { return } - updatedRegion = bounds + offset := source.Bounds().Min.Sub(destination.Bounds().Min) + drawBounds := + source.Bounds().Sub(offset). + Intersect(destination.Bounds()). + Intersect(bounds) + if drawBounds.Empty() { return } + updatedRegion = drawBounds point := image.Point { } - for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ { - for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ { + for point.Y = drawBounds.Min.Y; point.Y < drawBounds.Max.Y; point.Y ++ { + for point.X = drawBounds.Min.X; point.X < drawBounds.Max.X; point.X ++ { offsetPoint := point.Add(offset) dstIndex := point.X + point.Y * dstStride srcIndex := offsetPoint.X + offsetPoint.Y * srcStride @@ -36,15 +40,16 @@ func FillRectangle ( func StrokeRectangle ( destination canvas.Canvas, source canvas.Canvas, + bounds image.Rectangle, weight int, +) ( + updatedRegion image.Rectangle, ) { - bounds := destination.Bounds() insetBounds := bounds.Inset(weight) if insetBounds.Empty() { - FillRectangle(destination, source) - return + return FillRectangle(destination, source, bounds) } - FillRectangleShatter(destination, source, insetBounds) + return FillRectangleShatter(destination, source, bounds, insetBounds) } // FillRectangleShatter is like FillRectangle, but it does not draw in areas @@ -52,15 +57,19 @@ func StrokeRectangle ( func FillRectangleShatter ( destination canvas.Canvas, source canvas.Canvas, + bounds image.Rectangle, rocks ...image.Rectangle, +) ( + updatedRegion image.Rectangle, ) { - tiles := shatter.Shatter(destination.Bounds(), rocks...) - offset := source.Bounds().Min.Sub(destination.Bounds().Min) + tiles := shatter.Shatter(bounds, rocks...) for _, tile := range tiles { FillRectangle ( canvas.Cut(destination, tile), - canvas.Cut(source, tile.Add(offset))) + source, tile) + updatedRegion = updatedRegion.Union(tile) } + return } // FillColorRectangle fills a rectangle within the destination canvas with a @@ -92,11 +101,15 @@ func FillColorRectangleShatter ( color color.RGBA, bounds image.Rectangle, rocks ...image.Rectangle, +) ( + updatedRegion image.Rectangle, ) { tiles := shatter.Shatter(bounds, rocks...) for _, tile := range tiles { FillColorRectangle(destination, color, tile) + updatedRegion = updatedRegion.Union(tile) } + return } // StrokeColorRectangle is similar to FillColorRectangle, but it draws an inset @@ -106,11 +119,12 @@ func StrokeColorRectangle ( color color.RGBA, bounds image.Rectangle, weight int, +) ( + updatedRegion image.Rectangle, ) { insetBounds := bounds.Inset(weight) if insetBounds.Empty() { - FillColorRectangle(destination, color, bounds) - return + return FillColorRectangle(destination, color, bounds) } - FillColorRectangleShatter(destination, color, bounds, insetBounds) + return FillColorRectangleShatter(destination, color, bounds, insetBounds) } diff --git a/backends/x/window.go b/backends/x/window.go index 8150076..67d352a 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -96,9 +96,6 @@ func (window *Window) Adopt (child elements.Element) { window.child.OnDamage(nil) window.child.OnMinimumSizeChange(nil) } - if previousChild, ok := window.child.(elements.Flexible); ok { - previousChild.OnFlexibleHeightChange(nil) - } if previousChild, ok := window.child.(elements.Focusable); ok { previousChild.OnFocusRequest(nil) previousChild.OnFocusMotionRequest(nil) @@ -115,9 +112,6 @@ func (window *Window) Adopt (child elements.Element) { if newChild, ok := child.(elements.Configurable); ok { newChild.SetConfig(window.config) } - if newChild, ok := child.(elements.Flexible); ok { - newChild.OnFlexibleHeightChange(window.resizeChildToFit) - } if newChild, ok := child.(elements.Focusable); ok { newChild.OnFocusRequest(window.childSelectionRequestCallback) } @@ -263,26 +257,7 @@ func (window *Window) redrawChildEntirely () { func (window *Window) resizeChildToFit () { window.skipChildDrawCallback = true - if child, ok := window.child.(elements.Flexible); ok { - minimumHeight := child.FlexibleHeightFor(window.metrics.width) - minimumWidth, _ := child.MinimumSize() - - icccm.WmNormalHintsSet ( - window.backend.connection, - window.xWindow.Id, - &icccm.NormalHints { - Flags: icccm.SizeHintPMinSize, - MinWidth: uint(minimumWidth), - MinHeight: uint(minimumHeight), - }) - - if window.metrics.height >= minimumHeight && - window.metrics.width >= minimumWidth { - window.child.DrawTo(window.canvas) - } - } else { - window.child.DrawTo(window.canvas) - } + window.child.DrawTo(window.canvas, window.canvas.Bounds()) window.skipChildDrawCallback = false } diff --git a/elements/basic/checkbox.go b/elements/basic/checkbox.go index ab2217e..14690bb 100644 --- a/elements/basic/checkbox.go +++ b/elements/basic/checkbox.go @@ -4,7 +4,6 @@ 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/artist" import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -176,7 +175,7 @@ func (element *Checkbox) draw () { backgroundPattern.Draw(element.core, bounds) pattern := element.theme.Pattern(theme.PatternButton, state) - artist.DrawBounds(element.core, pattern, boxBounds) + pattern.Draw(element.core, boxBounds) textBounds := element.drawer.LayoutBounds() margin := element.theme.Margin(theme.PatternBackground) diff --git a/elements/basic/container.go b/elements/basic/container.go index 6728f31..ec8a024 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -20,12 +20,10 @@ type Container struct { layout layouts.Layout children []layouts.LayoutEntry warping bool - flexible bool config config.Wrapped theme theme.Wrapped - onFlexibleHeightChange func () onFocusRequest func () (granted bool) onFocusMotionRequest func (input.KeynavDirection) (granted bool) } @@ -70,9 +68,6 @@ func (element *Container) Adopt (child elements.Element, expand bool) { element.redoAll() element.core.DamageAll() }) - if child0, ok := child.(elements.Flexible); ok { - child0.OnFlexibleHeightChange(element.notifyFlexibleChange) - } if child0, ok := child.(elements.Focusable); ok { child0.OnFocusRequest (func () (granted bool) { return element.childFocusRequestCallback(child0) @@ -92,7 +87,6 @@ func (element *Container) Adopt (child elements.Element, expand bool) { // refresh stale data element.updateMinimumSize() - element.reflectChildProperties() if element.core.HasImage() && !element.warping { element.redoAll() element.core.DamageAll() @@ -135,7 +129,6 @@ func (element *Container) Disown (child elements.Element) { } element.updateMinimumSize() - element.reflectChildProperties() if element.core.HasImage() && !element.warping { element.redoAll() element.core.DamageAll() @@ -143,7 +136,7 @@ func (element *Container) Disown (child elements.Element) { } func (element *Container) clearChildEventHandlers (child elements.Element) { - child.DrawTo(nil) + child.DrawTo(nil, image.Rectangle { }) child.OnDamage(nil) child.OnMinimumSizeChange(nil) if child0, ok := child.(elements.Focusable); ok { @@ -153,9 +146,6 @@ func (element *Container) clearChildEventHandlers (child elements.Element) { child0.HandleUnfocus() } } - if child0, ok := child.(elements.Flexible); ok { - child0.OnFlexibleHeightChange(nil) - } } // DisownAll removes all child elements from the container at once. @@ -166,7 +156,6 @@ func (element *Container) DisownAll () { element.children = nil element.updateMinimumSize() - element.reflectChildProperties() if element.core.HasImage() && !element.warping { element.redoAll() element.core.DamageAll() @@ -211,7 +200,7 @@ func (element *Container) redoAll () { // remove child canvasses so that any operations done in here will not // cause a child to draw to a wack ass canvas. for _, entry := range element.children { - entry.DrawTo(nil) + entry.DrawTo(nil, entry.Bounds) } // do a layout @@ -225,12 +214,13 @@ func (element *Container) redoAll () { pattern := element.theme.Pattern ( theme.PatternBackground, theme.State { }) - artist.DrawShatter ( - element.core, pattern, rocks...) + artist.DrawShatter(element.core, pattern, element.Bounds(), rocks...) // cut our canvas up and give peices to child elements for _, entry := range element.children { - entry.DrawTo(canvas.Cut(element.core, entry.Bounds)) + entry.DrawTo ( + canvas.Cut(element.core, entry.Bounds), + entry.Bounds) } } @@ -251,18 +241,6 @@ func (element *Container) SetConfig (new config.Config) { element.redoAll() } -func (element *Container) FlexibleHeightFor (width int) (height int) { - margin := element.theme.Margin(theme.PatternBackground) - padding := element.theme.Padding(theme.PatternBackground) - return element.layout.FlexibleHeightFor ( - element.children, - margin, padding, width) -} - -func (element *Container) OnFlexibleHeightChange (callback func ()) { - element.onFlexibleHeightChange = callback -} - func (element *Container) OnFocusRequest (callback func () (granted bool)) { element.onFocusRequest = callback element.Propagator.OnFocusRequest(callback) @@ -275,35 +253,6 @@ func (element *Container) OnFocusMotionRequest ( element.Propagator.OnFocusMotionRequest(callback) } -func (element *Container) forFlexible (callback func (child elements.Flexible) bool) { - for _, entry := range element.children { - child, flexible := entry.Element.(elements.Flexible) - if flexible { - if !callback(child) { break } - } - } -} - -func (element *Container) 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() - } - - element.flexible = false - element.forFlexible (func (elements.Flexible) bool { - element.flexible = true - return false - }) -} - func (element *Container) childFocusRequestCallback ( child elements.Focusable, ) ( @@ -326,12 +275,6 @@ func (element *Container) updateMinimumSize () { element.core.SetMinimumSize(width, height) } -func (element *Container) notifyFlexibleChange () { - if element.onFlexibleHeightChange != nil { - element.onFlexibleHeightChange() - } -} - func (element *Container) doLayout () { margin := element.theme.Margin(theme.PatternBackground) padding := element.theme.Padding(theme.PatternBackground) diff --git a/elements/basic/documentContainer.go b/elements/basic/documentContainer.go new file mode 100644 index 0000000..81e8c60 --- /dev/null +++ b/elements/basic/documentContainer.go @@ -0,0 +1,368 @@ +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, image.Rectangle { }) + 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() + + 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.PatternBackground, + theme.State { }) + artist.DrawShatter(element.core, pattern, element.Bounds(), rocks...) + + element.partition() + if element.onScrollBoundsChange != nil { + element.onScrollBoundsChange() + } +} + +func (element *DocumentContainer) partition () { + for _, entry := range element.children { + entry.DrawTo(nil, entry.Bounds) + } + + // 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) + } + } +} + +// 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.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 + } + maxScrollHeight := element.maxScrollHeight() + if position.Y > maxScrollHeight { + position.Y = maxScrollHeight + } + element.scroll = position + if element.core.HasImage() && !element.warping { + element.redoAll() + element.core.DamageAll() + } +} + +func (element *DocumentContainer) 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 +} + +// 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 { } + + minimumWidth := 0 + 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 > minimumWidth { + minimumWidth = width + } + 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 + } + + element.contentBounds = + element.contentBounds.Sub(element.contentBounds.Min) + element.core.SetMinimumSize ( + minimumWidth + padding.Horizontal(), + padding.Vertical()) +} diff --git a/elements/basic/label.go b/elements/basic/label.go index 2674715..c14674d 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -1,5 +1,6 @@ package basicElements +import "golang.org/x/image/math/fixed" import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/textdraw" @@ -13,6 +14,9 @@ type Label struct { wrap bool text string drawer textdraw.Drawer + + forcedColumns int + forcedRows int config config.Wrapped theme theme.Wrapped @@ -56,6 +60,17 @@ func (element *Label) handleResize () { return } +// EmCollapse forces a minimum width and height upon the label. The width is +// measured in emspaces, and the height is measured in lines. If a zero value is +// given for a dimension, its minimum will be determined by the label's content. +// If the label's content is greater than these dimensions, it will be truncated +// to fit. +func (element *Label) EmCollapse (columns int, rows int) { + element.forcedColumns = columns + element.forcedRows = rows + element.updateMinimumSize() +} + // FlexibleHeightFor returns the reccomended height for this element based on // the given width in order to allow the text to wrap properly. func (element *Label) FlexibleHeightFor (width int) (height int) { @@ -134,20 +149,35 @@ func (element *Label) SetConfig (new config.Config) { } func (element *Label) updateMinimumSize () { + var width, height int + if element.wrap { em := element.drawer.Em().Round() if em < 1 { em = element.theme.Padding(theme.PatternBackground)[0] } - element.core.SetMinimumSize ( - em, element.drawer.LineHeight().Round()) + width, height = em, element.drawer.LineHeight().Round() if element.onFlexibleHeightChange != nil { element.onFlexibleHeightChange() } } else { bounds := element.drawer.LayoutBounds() - element.core.SetMinimumSize(bounds.Dx(), bounds.Dy()) + width, height = bounds.Dx(), bounds.Dy() } + + if element.forcedColumns > 0 { + width = int ( + element.drawer.Em(). + Mul(fixed.I(element.forcedColumns))) + } + + if element.forcedRows > 0 { + height = int ( + element.drawer.LineHeight(). + Mul(fixed.I(element.forcedRows))) + } + + element.core.SetMinimumSize(width, height) } func (element *Label) draw () { @@ -160,7 +190,7 @@ func (element *Label) draw () { textBounds := element.drawer.LayoutBounds() - foreground := element.theme.Color ( + foreground := element.theme.Color ( theme.ColorForeground, theme.State { }) element.drawer.Draw(element.core, foreground, bounds.Min.Sub(textBounds.Min)) diff --git a/elements/basic/list.go b/elements/basic/list.go index f6386b1..0f4c009 100644 --- a/elements/basic/list.go +++ b/elements/basic/list.go @@ -461,5 +461,5 @@ func (element *List) draw () { ).Add(innerBounds.Min).Intersect(innerBounds) pattern := element.theme.Pattern(theme.PatternSunken, state) artist.DrawShatter ( - element.core, pattern, covered) + element.core, pattern, bounds, covered) } diff --git a/elements/basic/listentry.go b/elements/basic/listentry.go index d1020c2..2d5267a 100644 --- a/elements/basic/listentry.go +++ b/elements/basic/listentry.go @@ -70,7 +70,7 @@ func (entry *ListEntry) Draw ( pattern := entry.theme.Pattern(theme.PatternRaised, state) padding := entry.theme.Padding(theme.PatternRaised) bounds := entry.Bounds().Add(offset) - artist.DrawBounds(destination, pattern, bounds) + pattern.Draw(destination, bounds) foreground := entry.theme.Color (theme.ColorForeground, state) return entry.drawer.Draw ( diff --git a/elements/basic/progressbar.go b/elements/basic/progressbar.go index 8e5ce32..9178e3e 100644 --- a/elements/basic/progressbar.go +++ b/elements/basic/progressbar.go @@ -3,7 +3,6 @@ package basicElements import "image" import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/config" -import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" // ProgressBar displays a visual indication of how far along a task is. @@ -78,5 +77,5 @@ func (element *ProgressBar) draw () { bounds.Min.X + int(float64(bounds.Dx()) * element.progress), bounds.Max.Y) mercury := element.theme.Pattern(theme.PatternMercury, theme.State { }) - artist.DrawBounds(element.core, mercury, meterBounds) + mercury.Draw(element.core, meterBounds) } diff --git a/elements/basic/scrollbar.go b/elements/basic/scrollbar.go index 3c2c16d..4f9af08 100644 --- a/elements/basic/scrollbar.go +++ b/elements/basic/scrollbar.go @@ -4,7 +4,6 @@ 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/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" // ScrollBar is an element similar to Slider, but it has special behavior that @@ -315,12 +314,10 @@ func (element *ScrollBar) draw () { Disabled: !element.Enabled(), Pressed: element.dragging, } - artist.DrawBounds ( + element.theme.Pattern(theme.PatternGutter, state).Draw ( element.core, - element.theme.Pattern(theme.PatternGutter, state), bounds) - artist.DrawBounds ( + element.theme.Pattern(theme.PatternHandle, state).Draw ( element.core, - element.theme.Pattern(theme.PatternHandle, state), element.bar) } diff --git a/elements/basic/scrollcontainer.go b/elements/basic/scrollcontainer.go index cbc53ce..e333dbb 100644 --- a/elements/basic/scrollcontainer.go +++ b/elements/basic/scrollcontainer.go @@ -120,7 +120,7 @@ func (element *ScrollContainer) setChildEventHandlers (child elements.Element) { } func (element *ScrollContainer) clearChildEventHandlers (child elements.Scrollable) { - child.DrawTo(nil) + child.DrawTo(nil, image.Rectangle { }) child.OnDamage(nil) child.OnMinimumSizeChange(nil) child.OnScrollBoundsChange(nil) @@ -131,9 +131,6 @@ func (element *ScrollContainer) clearChildEventHandlers (child elements.Scrollab child0.HandleUnfocus() } } - if child0, ok := child.(elements.Flexible); ok { - child0.OnFlexibleHeightChange(nil) - } } // SetTheme sets the element's theme. @@ -201,19 +198,26 @@ func (element *ScrollContainer) Child (index int) (child elements.Element) { func (element *ScrollContainer) redoAll () { if !element.core.HasImage() { return } - if element.child != nil { element.child.DrawTo(nil) } - if element.horizontal != nil { element.horizontal.DrawTo(nil) } - if element.vertical != nil { element.vertical.DrawTo(nil) } + zr := image.Rectangle { } + if element.child != nil { element.child.DrawTo(nil, zr) } + if element.horizontal != nil { element.horizontal.DrawTo(nil, zr) } + if element.vertical != nil { element.vertical.DrawTo(nil, zr) } childBounds, horizontalBounds, verticalBounds := element.layout() if element.child != nil { - element.child.DrawTo(canvas.Cut(element.core, childBounds)) + element.child.DrawTo ( + canvas.Cut(element.core, childBounds), + childBounds) } if element.horizontal != nil { - element.horizontal.DrawTo(canvas.Cut(element.core, horizontalBounds)) + element.horizontal.DrawTo ( + canvas.Cut(element.core, horizontalBounds), + horizontalBounds) } if element.vertical != nil { - element.vertical.DrawTo(canvas.Cut(element.core, verticalBounds)) + element.vertical.DrawTo ( + canvas.Cut(element.core, verticalBounds), + verticalBounds) } element.draw() } @@ -270,7 +274,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/elements/basic/slider.go b/elements/basic/slider.go index 67b6f65..6ad8ff1 100644 --- a/elements/basic/slider.go +++ b/elements/basic/slider.go @@ -4,7 +4,6 @@ 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/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" // Slider is a slider control with a floating point value between zero and one. @@ -229,12 +228,10 @@ func (element *Slider) draw () { Disabled: !element.Enabled(), Pressed: element.dragging, } - artist.DrawBounds ( + element.theme.Pattern(theme.PatternGutter, state).Draw ( element.core, - element.theme.Pattern(theme.PatternGutter, state), bounds) - artist.DrawBounds ( + element.theme.Pattern(theme.PatternHandle, state).Draw ( element.core, - element.theme.Pattern(theme.PatternHandle, state), element.bar) } diff --git a/elements/basic/switch.go b/elements/basic/switch.go index 4eeb9ad..80491de 100644 --- a/elements/basic/switch.go +++ b/elements/basic/switch.go @@ -4,7 +4,6 @@ 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/artist" import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -185,11 +184,11 @@ func (element *Switch) draw () { gutterPattern := element.theme.Pattern ( theme.PatternGutter, state) - artist.DrawBounds(element.core, gutterPattern, gutterBounds) + gutterPattern.Draw(element.core, gutterBounds) handlePattern := element.theme.Pattern ( theme.PatternHandle, state) - artist.DrawBounds(element.core, handlePattern, handleBounds) + handlePattern.Draw(element.core, handleBounds) textBounds := element.drawer.LayoutBounds() offset := bounds.Min.Add(image.Point { diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index 98296dc..a76358f 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -81,7 +81,6 @@ func (element *TextBox) HandleMouseDown (x, y int, button input.Button) { func (element *TextBox) HandleMouseMove (x, y int) { if !element.Enabled() { return } - if !element.Focused() { element.Focus() } if element.dragging { runeIndex := element.atPosition(image.Pt(x, y)) diff --git a/elements/core/core.go b/elements/core/core.go index 4b98dca..b5c6259 100644 --- a/elements/core/core.go +++ b/elements/core/core.go @@ -8,6 +8,7 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" // widgets. It is meant to be embedded directly into a struct. type Core struct { canvas canvas.Canvas + bounds image.Rectangle metrics struct { minimumWidth int @@ -37,7 +38,7 @@ func NewCore ( // overridden. func (core *Core) Bounds () (bounds image.Rectangle) { if core.canvas == nil { return } - return core.canvas.Bounds() + return core.bounds } // MinimumSize fulfils the tomo.Element interface. This should not need to be @@ -48,8 +49,9 @@ func (core *Core) MinimumSize () (width, height int) { // DrawTo fulfills the tomo.Element interface. This should not need to be // overridden. -func (core *Core) DrawTo (canvas canvas.Canvas) { +func (core *Core) DrawTo (canvas canvas.Canvas, bounds image.Rectangle) { core.canvas = canvas + core.bounds = bounds if core.drawSizeChange != nil && core.canvas != nil { core.drawSizeChange() } diff --git a/elements/core/propagator.go b/elements/core/propagator.go index 291d406..2375f5b 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -326,16 +326,6 @@ func (propagator *Propagator) forFocusable (callback func (child elements.Focusa }) } -func (propagator *Propagator) forFlexible (callback func (child elements.Flexible) bool) { - propagator.forChildren (func (child elements.Element) bool { - typedChild, flexible := child.(elements.Flexible) - if flexible { - if !callback(typedChild) { return false } - } - return true - }) -} - func (propagator *Propagator) firstFocused () int { for index := 0; index < propagator.parent.CountChildren(); index ++ { child, focusable := propagator.parent.Child(index).(elements.Focusable) diff --git a/elements/core/selectable.go b/elements/core/selectable.go index 11d100f..824dcae 100644 --- a/elements/core/selectable.go +++ b/elements/core/selectable.go @@ -1,5 +1,6 @@ package core +// import "runtime/debug" import "git.tebibyte.media/sashakoshka/tomo/input" // FocusableCore is a struct that can be embedded into objects to make them @@ -71,6 +72,7 @@ func (core *FocusableCore) HandleFocus ( // HandleUnfocus causes this element to mark itself as unfocused. func (core *FocusableCore) HandleUnfocus () { core.focused = false + // debug.PrintStack() if core.drawFocusChange != nil { core.drawFocusChange() } } diff --git a/elements/element.go b/elements/element.go index 8881970..3df2466 100644 --- a/elements/element.go +++ b/elements/element.go @@ -9,13 +9,14 @@ import "git.tebibyte.media/sashakoshka/tomo/config" // Element represents a basic on-screen object. type Element interface { // Bounds reports the element's bounding box. This must reflect the - // bounding box of the last canvas given to the element by DrawTo. + // bounding last given to the element by DrawTo. Bounds () (bounds image.Rectangle) - // DrawTo sets this element's canvas. This should only be called by the - // parent element. This is typically a region of the parent element's - // canvas. - DrawTo (canvas canvas.Canvas) + // DrawTo gives the element a canvas to draw on, along with a bounding + // box to be used for laying out the element. This should only be called + // by the parent element. This is typically a region of the parent + // element's canvas. + DrawTo (canvas canvas.Canvas, bounds image.Rectangle) // OnDamage sets a function to be called when an area of the element is // drawn on and should be pushed to the screen. diff --git a/elements/fun/piano.go b/elements/fun/piano.go index 4cba308..e18cac8 100644 --- a/elements/fun/piano.go +++ b/elements/fun/piano.go @@ -305,7 +305,7 @@ func (element *Piano) draw () { pattern := element.theme.Pattern(theme.PatternPinboard, state) artist.DrawShatter ( - element.core, pattern, element.contentBounds) + element.core, pattern, element.Bounds(), element.contentBounds) } func (element *Piano) drawFlat ( @@ -316,7 +316,7 @@ func (element *Piano) drawFlat ( state.Pressed = pressed pattern := element.theme.Theme.Pattern ( theme.PatternButton, state, theme.C("fun", "flatKey")) - artist.DrawBounds(element.core, pattern, bounds) + pattern.Draw(element.core, bounds) } func (element *Piano) drawSharp ( @@ -327,5 +327,5 @@ func (element *Piano) drawSharp ( state.Pressed = pressed pattern := element.theme.Theme.Pattern ( theme.PatternButton, state, theme.C("fun", "sharpKey")) - artist.DrawBounds(element.core, pattern, bounds) + pattern.Draw(element.core, bounds) } diff --git a/elements/testing/artist.go b/elements/testing/artist.go index bb42bfa..fe5c162 100644 --- a/elements/testing/artist.go +++ b/elements/testing/artist.go @@ -79,34 +79,32 @@ func (element *Artist) draw () { } tiles := shatter.Shatter(c41.Bounds(), rocks...) for index, tile := range tiles { - artist.DrawBounds ( - element.core, - []artist.Pattern { - patterns.Uhex(0xFF0000FF), - patterns.Uhex(0x00FF00FF), - patterns.Uhex(0xFF00FFFF), - patterns.Uhex(0xFFFF00FF), - patterns.Uhex(0x00FFFFFF), - } [index % 5], tile) + []artist.Pattern { + patterns.Uhex(0xFF0000FF), + patterns.Uhex(0x00FF00FF), + patterns.Uhex(0xFF00FFFF), + patterns.Uhex(0xFFFF00FF), + patterns.Uhex(0x00FFFFFF), + } [index % 5].Draw(element.core, tile) } // 0, 2 c02 := element.cellAt(0, 2) shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1) - shapes.FillEllipse(c02, c41) + shapes.FillEllipse(c02, c41, c02.Bounds()) // 1, 2 c12 := element.cellAt(1, 2) shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1) - shapes.StrokeEllipse(c12, c41, 5) + shapes.StrokeEllipse(c12, c41, c12.Bounds(), 5) // 2, 2 c22 := element.cellAt(2, 2) - shapes.FillRectangle(c22, c41) + shapes.FillRectangle(c22, c41, c22.Bounds()) // 3, 2 c32 := element.cellAt(3, 2) - shapes.StrokeRectangle(c32, c41, 5) + shapes.StrokeRectangle(c32, c41, c32.Bounds(), 5) // 4, 2 c42 := element.cellAt(4, 2) diff --git a/examples/documentContainer/main.go b/examples/documentContainer/main.go new file mode 100644 index 0000000..7326302 --- /dev/null +++ b/examples/documentContainer/main.go @@ -0,0 +1,64 @@ +package main + +import "os" +import "image" +import _ "image/png" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/layouts/basic" +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(383, 360) + window.SetTitle("Scroll") + + file, err := os.Open("assets/banner.png") + if err != nil { panic(err.Error()); return } + logo, _, err := image.Decode(file) + file.Close() + if err != nil { panic(err.Error()); return } + + 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. You can also include normal elements " + + "like:", true)) + document.Adopt (basicElements.NewButton ( + "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)) + document.Adopt(basicElements.NewImage(logo)) + document.Adopt (basicElements.NewLabel ( + "Oh, you're a switch? Then name all of these switches:", true)) + for i := 0; i < 3; i ++ { + switchContainer := basicElements.NewContainer (basicLayouts.Horizontal { + Gap: true, + }) + for i := 0; i < 10; i ++ { + switchContainer.Adopt(basicElements.NewSwitch("", false), true) + } + document.Adopt(switchContainer) + } + + scrollContainer.Adopt(document) + window.Adopt(scrollContainer) + window.OnClose(tomo.Stop) + window.Show() +} diff --git a/examples/raycaster/game.go b/examples/raycaster/game.go index 25a05c4..e0413c1 100644 --- a/examples/raycaster/game.go +++ b/examples/raycaster/game.go @@ -1,6 +1,7 @@ package main import "time" +import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/canvas" @@ -30,14 +31,17 @@ func NewGame (world World, textures Textures) (game *Game) { return } -func (game *Game) DrawTo (canvas canvas.Canvas) { +func (game *Game) DrawTo (canvas canvas.Canvas, bounds image.Rectangle) { if canvas == nil { - game.stopChan <- true + select { + case game.stopChan <- true: + default: + } } else if !game.running { game.running = true go game.run() } - game.Raycaster.DrawTo(canvas) + game.Raycaster.DrawTo(canvas, bounds) } func (game *Game) Stamina () float64 { diff --git a/layouts/basic/dialog.go b/layouts/basic/dialog.go index ed3b45e..b73c987 100644 --- a/layouts/basic/dialog.go +++ b/layouts/basic/dialog.go @@ -3,7 +3,6 @@ package basicLayouts import "image" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/layouts" -import "git.tebibyte.media/sashakoshka/tomo/elements" // Dialog arranges elements in the form of a dialog box. The first element is // positioned above as the main focus of the dialog, and is set to expand @@ -132,45 +131,6 @@ func (layout Dialog) MinimumSize ( return } -// FlexibleHeightFor Returns the minimum height the layout needs to lay out the -// specified elements at the given width, taking into account flexible elements. -func (layout Dialog) FlexibleHeightFor ( - entries []layouts.LayoutEntry, - margin image.Point, - padding artist.Inset, - width int, -) ( - height int, -) { - if layout.Pad { - width -= padding.Horizontal() - } - - if len(entries) > 0 { - mainChildHeight := 0 - if child, flexible := entries[0].Element.(elements.Flexible); flexible { - mainChildHeight = child.FlexibleHeightFor(width) - } else { - _, mainChildHeight = entries[0].MinimumSize() - } - height += mainChildHeight - } - - if len(entries) > 1 { - if layout.Gap { height += margin.Y } - _, additionalHeight := layout.minimumSizeOfControlRow ( - entries[1:], margin, padding) - height += additionalHeight - } - - if layout.Pad { - height += padding.Vertical() - } - return -} - -// TODO: possibly flatten this method to account for flexible elements within -// the control row. func (layout Dialog) minimumSizeOfControlRow ( entries []layouts.LayoutEntry, margin image.Point, diff --git a/layouts/basic/horizontal.go b/layouts/basic/horizontal.go index e27c3fe..0819241 100644 --- a/layouts/basic/horizontal.go +++ b/layouts/basic/horizontal.go @@ -3,7 +3,6 @@ package basicLayouts import "image" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/layouts" -import "git.tebibyte.media/sashakoshka/tomo/elements" // Horizontal arranges elements horizontally. Elements at the start of the entry // list will be positioned on the left, and elements at the end of the entry @@ -76,49 +75,6 @@ func (layout Horizontal) MinimumSize ( return } -// FlexibleHeightFor Returns the minimum height the layout needs to lay out the -// specified elements at the given width, taking into account flexible elements. -func (layout Horizontal) FlexibleHeightFor ( - entries []layouts.LayoutEntry, - margin image.Point, - padding artist.Inset, - width int, -) ( - height int, -) { - if layout.Pad { width -= padding.Horizontal() } - - // get width of expanding elements - expandingElementWidth := layout.expandingElementWidth ( - entries, margin, padding, width) - - x, y := 0, 0 - if layout.Pad { - x += padding.Horizontal() - y += padding.Vertical() - } - - // set the size and position of each element - for index, entry := range entries { - entryWidth, entryHeight := entry.MinimumSize() - if entry.Expand { - entryWidth = expandingElementWidth - } - if child, flexible := entry.Element.(elements.Flexible); flexible { - entryHeight = child.FlexibleHeightFor(entryWidth) - } - if entryHeight > height { height = entryHeight } - - x += entryWidth - if index > 0 && layout.Gap { x += margin.X } - } - - if layout.Pad { - height += padding.Vertical() - } - return -} - func (layout Horizontal) expandingElementWidth ( entries []layouts.LayoutEntry, margin image.Point, diff --git a/layouts/basic/vertical.go b/layouts/basic/vertical.go index a66c648..bbcfb9a 100644 --- a/layouts/basic/vertical.go +++ b/layouts/basic/vertical.go @@ -3,7 +3,6 @@ package basicLayouts import "image" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/layouts" -import "git.tebibyte.media/sashakoshka/tomo/elements" // 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 @@ -32,13 +31,7 @@ func (layout Vertical) Arrange ( minimumHeights := make([]int, len(entries)) expandingElements := 0 for index, entry := range entries { - var entryMinHeight int - - if child, flexible := entry.Element.(elements.Flexible); flexible { - entryMinHeight = child.FlexibleHeightFor(bounds.Dx()) - } else { - _, entryMinHeight = entry.MinimumSize() - } + _, entryMinHeight := entry.MinimumSize() minimumHeights[index] = entryMinHeight if entry.Expand { @@ -101,34 +94,3 @@ func (layout Vertical) MinimumSize ( } return } - -// FlexibleHeightFor Returns the minimum height the layout needs to lay out the -// specified elements at the given width, taking into account flexible elements. -func (layout Vertical) FlexibleHeightFor ( - entries []layouts.LayoutEntry, - margin image.Point, - padding artist.Inset, - width int, -) ( - height int, -) { - if layout.Pad { - width -= padding.Horizontal() - height += padding.Vertical() - } - - for index, entry := range entries { - child, flexible := entry.Element.(elements.Flexible) - if flexible { - height += child.FlexibleHeightFor(width) - } else { - _, entryHeight := entry.MinimumSize() - height += entryHeight - } - - if layout.Gap && index > 0 { - height += margin.Y - } - } - return -} diff --git a/layouts/layout.go b/layouts/layout.go index 786a2b3..d538e5f 100644 --- a/layouts/layout.go +++ b/layouts/layout.go @@ -12,10 +12,6 @@ type LayoutEntry struct { Expand bool } -// TODO: have layouts take in artist.Inset for margin and padding -// TODO: create a layout that only displays the first element and full screen. -// basically a blank layout for containers that only ever have one element. - // 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 { @@ -39,16 +35,4 @@ type Layout interface { ) ( width, height int, ) - - // FlexibleHeightFor Returns the minimum height the layout needs to lay - // out the specified elements at the given width, taking into account - // flexible elements. - FlexibleHeightFor ( - entries []LayoutEntry, - margin image.Point, - padding artist.Inset, - squeeze int, - ) ( - height int, - ) } diff --git a/textdraw/setter.go b/textdraw/setter.go index 83b9f90..b43da89 100644 --- a/textdraw/setter.go +++ b/textdraw/setter.go @@ -41,8 +41,7 @@ func (setter *TypeSetter) needLayout () { metrics := setter.face.Metrics() remaining := setter.text y := fixed.Int26_6(0) - maxY := fixed.I(setter.maxHeight) + metrics.Height - for len(remaining) > 0 && (y < maxY || setter.maxHeight == 0) { + for len(remaining) > 0 { // process one line line, remainingFromLine := DoLine ( remaining, setter.face, fixed.I(setter.maxWidth)) diff --git a/theme/assets/wintergreen.png b/theme/assets/wintergreen.png index 154a6d8..a0cde5e 100644 Binary files a/theme/assets/wintergreen.png and b/theme/assets/wintergreen.png differ