flexible-elements-were-a-mistake #11

Merged
sashakoshka merged 17 commits from flexible-elements-were-a-mistake into main 2023-03-14 03:37:58 +00:00
33 changed files with 619 additions and 378 deletions

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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.

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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())
}

View File

@ -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))

View File

@ -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)
}

View File

@ -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 (

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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 (

View File

@ -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)
}

View File

@ -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 {

View File

@ -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))

View File

@ -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()
}

View File

@ -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)

View File

@ -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() }
}

View File

@ -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.

View File

@ -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)
}

View File

@ -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)

View File

@ -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()
}

View File

@ -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 {

View File

@ -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,

View File

@ -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,

View File

@ -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
}

View File

@ -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,
)
}

View File

@ -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))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB