flexible-elements-were-a-mistake #11

Merged
sashakoshka merged 17 commits from flexible-elements-were-a-mistake into main 2023-03-13 21:37:58 -06: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 // Pattern is capable of drawing to a canvas within the bounds of a given
// clipping rectangle. // clipping rectangle.
type Pattern interface { type Pattern interface {
// Draw draws to destination, using the bounds of destination as a width // Draw draws the pattern onto the destination canvas, using the
// and height for things like gradients, bevels, etc. The pattern may // specified bounds. The given bounds can be smaller or larger than the
// not draw outside the union of destination.Bounds() and clip. The // bounds of the destination canvas. The destination canvas can be cut
// clipping rectangle effectively takes a subset of the pattern. To // using canvas.Cut() to draw only a specific subset of a pattern.
// change the bounds of the pattern itself, use canvas.Cut() on the Draw (destination canvas.Canvas, bounds image.Rectangle)
// destination before passing it to Draw().
Draw (destination canvas.Canvas, clip image.Rectangle)
} }
// Draw lets you use several clipping rectangles to draw a pattern. // Fill fills the destination canvas with the given pattern.
func Draw ( 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, destination canvas.Canvas,
source Pattern, source Pattern,
clips ...image.Rectangle, bounds image.Rectangle,
subsets ...image.Rectangle,
) ( ) (
updatedRegion image.Rectangle, updatedRegion image.Rectangle,
) { ) {
for _, clip := range clips { for _, subset := range subsets {
source.Draw(destination, clip) source.Draw(canvas.Cut(destination, subset), bounds)
updatedRegion = updatedRegion.Union(clip) updatedRegion = updatedRegion.Union(subset)
} }
return return
} }
// DrawBounds lets you specify an overall bounding rectangle for drawing a // DrawShatter is like an inverse of DrawClip, drawing nothing in the areas
// pattern. The destination is cut to this rectangle. // specified by "rocks".
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".
func DrawShatter ( func DrawShatter (
destination canvas.Canvas, destination canvas.Canvas,
source Pattern, source Pattern,
bounds image.Rectangle,
rocks ...image.Rectangle, rocks ...image.Rectangle,
) ( ) (
updatedRegion image.Rectangle, updatedRegion image.Rectangle,
) { ) {
tiles := shatter.Shatter(destination.Bounds(), rocks...) tiles := shatter.Shatter(bounds, rocks...)
return Draw(destination, source, tiles...) return DrawClip(destination, source, bounds, tiles...)
} }
// AllocateSample returns a new canvas containing the result of a pattern. The // 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 // 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 // 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. // it because that is horrible and cruel to the computer.
func AllocateSample (source Pattern, width, height int) (allocated canvas.Canvas) { func AllocateSample (source Pattern, width, height int) canvas.Canvas {
allocated = canvas.NewBasicCanvas(width, height) allocated := canvas.NewBasicCanvas(width, height)
source.Draw(allocated, allocated.Bounds()) Fill(allocated, source)
return return allocated
} }

View File

@ -37,9 +37,9 @@ type Border struct {
// Draw draws the border pattern onto the destination canvas within the clipping // Draw draws the border pattern onto the destination canvas within the clipping
// bounds. // bounds.
func (pattern Border) Draw (destination canvas.Canvas, clip image.Rectangle) { func (pattern Border) Draw (destination canvas.Canvas, bounds image.Rectangle) {
bounds := clip.Canon().Intersect(destination.Bounds()) drawBounds := bounds.Canon().Intersect(destination.Bounds())
if bounds.Empty() { return } if drawBounds.Empty() { return }
srcSections := nonasect(pattern.Bounds(), pattern.Inset) srcSections := nonasect(pattern.Bounds(), pattern.Inset)
srcTextures := [9]Texture { } srcTextures := [9]Texture { }
@ -47,9 +47,9 @@ func (pattern Border) Draw (destination canvas.Canvas, clip image.Rectangle) {
srcTextures[index].Canvas = canvas.Cut(pattern, section) srcTextures[index].Canvas = canvas.Cut(pattern, section)
} }
dstSections := nonasect(destination.Bounds(), pattern.Inset) dstSections := nonasect(bounds, pattern.Inset)
for index, section := range dstSections { 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 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. // points of the pattern's canvas and the destination canvas will be lined up.
func (pattern Texture) Draw (destination canvas.Canvas, clip image.Rectangle) { func (pattern Texture) Draw (destination canvas.Canvas, bounds image.Rectangle) {
realBounds := destination.Bounds() drawBounds := bounds.Canon().Intersect(destination.Bounds())
bounds := clip.Canon().Intersect(realBounds) if drawBounds.Empty() { return }
if bounds.Empty() { return }
dstData, dstStride := destination.Buffer() dstData, dstStride := destination.Buffer()
srcData, srcStride := pattern.Buffer() srcData, srcStride := pattern.Buffer()
srcBounds := pattern.Bounds() srcBounds := pattern.Bounds()
dstPoint := image.Point { } 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.X = wrap(srcPoint.X, srcBounds.Min.X, srcBounds.Max.X)
srcPoint.Y = wrap(srcPoint.Y, srcBounds.Min.Y, srcBounds.Max.Y) 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 srcPoint.X = srcBounds.Min.X
dstPoint.X = bounds.Min.X dstPoint.X = drawBounds.Min.X
dstYComponent := dstPoint.Y * dstStride dstYComponent := dstPoint.Y * dstStride
srcYComponent := srcPoint.Y * srcStride srcYComponent := srcPoint.Y * srcStride
@ -42,7 +41,7 @@ func (pattern Texture) Draw (destination canvas.Canvas, clip image.Rectangle) {
} }
dstPoint.X ++ dstPoint.X ++
if dstPoint.X >= bounds.Max.X { if dstPoint.X >= drawBounds.Max.X {
break break
} }
} }

View File

@ -9,9 +9,9 @@ import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
// Uniform is a pattern that draws a solid color. // Uniform is a pattern that draws a solid color.
type Uniform color.RGBA type Uniform color.RGBA
// Draw fills the clipping rectangle with the pattern's color. // Draw fills the bounding rectangle with the pattern's color.
func (pattern Uniform) Draw (destination canvas.Canvas, clip image.Rectangle) { func (pattern Uniform) Draw (destination canvas.Canvas, bounds image.Rectangle) {
shapes.FillColorRectangle(destination, color.RGBA(pattern), clip) shapes.FillColorRectangle(destination, color.RGBA(pattern), bounds)
} }
// Uhex creates a new Uniform pattern from an RGBA integer value. // 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 ( func FillEllipse (
destination canvas.Canvas, destination canvas.Canvas,
source canvas.Canvas, source canvas.Canvas,
bounds image.Rectangle,
) ( ) (
updatedRegion image.Rectangle, updatedRegion image.Rectangle,
) { ) {
@ -20,15 +21,17 @@ func FillEllipse (
srcData, srcStride := source.Buffer() srcData, srcStride := source.Buffer()
offset := source.Bounds().Min.Sub(destination.Bounds().Min) offset := source.Bounds().Min.Sub(destination.Bounds().Min)
bounds := source.Bounds().Sub(offset).Intersect(destination.Bounds()) drawBounds :=
realBounds := destination.Bounds() source.Bounds().Sub(offset).
Intersect(destination.Bounds()).
Intersect(bounds)
if bounds.Empty() { return } if bounds.Empty() { return }
updatedRegion = bounds updatedRegion = bounds
point := image.Point { } point := image.Point { }
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ { for point.Y = drawBounds.Min.Y; point.Y < drawBounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ { for point.X = drawBounds.Min.X; point.X < drawBounds.Max.X; point.X ++ {
if inEllipse(point, realBounds) { if inEllipse(point, bounds) {
offsetPoint := point.Add(offset) offsetPoint := point.Add(offset)
dstIndex := point.X + point.Y * dstStride dstIndex := point.X + point.Y * dstStride
srcIndex := offsetPoint.X + offsetPoint.Y * srcStride srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
@ -41,6 +44,7 @@ func FillEllipse (
func StrokeEllipse ( func StrokeEllipse (
destination canvas.Canvas, destination canvas.Canvas,
source canvas.Canvas, source canvas.Canvas,
bounds image.Rectangle,
weight int, weight int,
) { ) {
if weight < 1 { return } if weight < 1 { return }
@ -48,10 +52,9 @@ func StrokeEllipse (
dstData, dstStride := destination.Buffer() dstData, dstStride := destination.Buffer()
srcData, srcStride := source.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) offset := source.Bounds().Min.Sub(destination.Bounds().Min)
realBounds := destination.Bounds() if drawBounds.Empty() { return }
if bounds.Empty() { return }
context := ellipsePlottingContext { context := ellipsePlottingContext {
plottingContext: plottingContext { plottingContext: plottingContext {
@ -61,11 +64,11 @@ func StrokeEllipse (
srcStride: srcStride, srcStride: srcStride,
weight: weight, weight: weight,
offset: offset, 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() context.plotEllipse()
} }

View File

@ -10,20 +10,24 @@ import "git.tebibyte.media/sashakoshka/tomo/shatter"
func FillRectangle ( func FillRectangle (
destination canvas.Canvas, destination canvas.Canvas,
source canvas.Canvas, source canvas.Canvas,
bounds image.Rectangle,
) ( ) (
updatedRegion image.Rectangle, updatedRegion image.Rectangle,
) { ) {
dstData, dstStride := destination.Buffer() dstData, dstStride := destination.Buffer()
srcData, srcStride := source.Buffer() srcData, srcStride := source.Buffer()
offset := source.Bounds().Min.Sub(destination.Bounds().Min) offset := source.Bounds().Min.Sub(destination.Bounds().Min)
bounds := source.Bounds().Sub(offset).Intersect(destination.Bounds()) drawBounds :=
if bounds.Empty() { return } source.Bounds().Sub(offset).
updatedRegion = bounds Intersect(destination.Bounds()).
Intersect(bounds)
if drawBounds.Empty() { return }
updatedRegion = drawBounds
point := image.Point { } point := image.Point { }
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ { for point.Y = drawBounds.Min.Y; point.Y < drawBounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ { for point.X = drawBounds.Min.X; point.X < drawBounds.Max.X; point.X ++ {
offsetPoint := point.Add(offset) offsetPoint := point.Add(offset)
dstIndex := point.X + point.Y * dstStride dstIndex := point.X + point.Y * dstStride
srcIndex := offsetPoint.X + offsetPoint.Y * srcStride srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
@ -36,15 +40,16 @@ func FillRectangle (
func StrokeRectangle ( func StrokeRectangle (
destination canvas.Canvas, destination canvas.Canvas,
source canvas.Canvas, source canvas.Canvas,
bounds image.Rectangle,
weight int, weight int,
) (
updatedRegion image.Rectangle,
) { ) {
bounds := destination.Bounds()
insetBounds := bounds.Inset(weight) insetBounds := bounds.Inset(weight)
if insetBounds.Empty() { if insetBounds.Empty() {
FillRectangle(destination, source) return FillRectangle(destination, source, bounds)
return
} }
FillRectangleShatter(destination, source, insetBounds) return FillRectangleShatter(destination, source, bounds, insetBounds)
} }
// FillRectangleShatter is like FillRectangle, but it does not draw in areas // FillRectangleShatter is like FillRectangle, but it does not draw in areas
@ -52,15 +57,19 @@ func StrokeRectangle (
func FillRectangleShatter ( func FillRectangleShatter (
destination canvas.Canvas, destination canvas.Canvas,
source canvas.Canvas, source canvas.Canvas,
bounds image.Rectangle,
rocks ...image.Rectangle, rocks ...image.Rectangle,
) (
updatedRegion image.Rectangle,
) { ) {
tiles := shatter.Shatter(destination.Bounds(), rocks...) tiles := shatter.Shatter(bounds, rocks...)
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
for _, tile := range tiles { for _, tile := range tiles {
FillRectangle ( FillRectangle (
canvas.Cut(destination, tile), 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 // FillColorRectangle fills a rectangle within the destination canvas with a
@ -92,11 +101,15 @@ func FillColorRectangleShatter (
color color.RGBA, color color.RGBA,
bounds image.Rectangle, bounds image.Rectangle,
rocks ...image.Rectangle, rocks ...image.Rectangle,
) (
updatedRegion image.Rectangle,
) { ) {
tiles := shatter.Shatter(bounds, rocks...) tiles := shatter.Shatter(bounds, rocks...)
for _, tile := range tiles { for _, tile := range tiles {
FillColorRectangle(destination, color, tile) FillColorRectangle(destination, color, tile)
updatedRegion = updatedRegion.Union(tile)
} }
return
} }
// StrokeColorRectangle is similar to FillColorRectangle, but it draws an inset // StrokeColorRectangle is similar to FillColorRectangle, but it draws an inset
@ -106,11 +119,12 @@ func StrokeColorRectangle (
color color.RGBA, color color.RGBA,
bounds image.Rectangle, bounds image.Rectangle,
weight int, weight int,
) (
updatedRegion image.Rectangle,
) { ) {
insetBounds := bounds.Inset(weight) insetBounds := bounds.Inset(weight)
if insetBounds.Empty() { if insetBounds.Empty() {
FillColorRectangle(destination, color, bounds) return FillColorRectangle(destination, color, bounds)
return
} }
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.OnDamage(nil)
window.child.OnMinimumSizeChange(nil) window.child.OnMinimumSizeChange(nil)
} }
if previousChild, ok := window.child.(elements.Flexible); ok {
previousChild.OnFlexibleHeightChange(nil)
}
if previousChild, ok := window.child.(elements.Focusable); ok { if previousChild, ok := window.child.(elements.Focusable); ok {
previousChild.OnFocusRequest(nil) previousChild.OnFocusRequest(nil)
previousChild.OnFocusMotionRequest(nil) previousChild.OnFocusMotionRequest(nil)
@ -115,9 +112,6 @@ func (window *Window) Adopt (child elements.Element) {
if newChild, ok := child.(elements.Configurable); ok { if newChild, ok := child.(elements.Configurable); ok {
newChild.SetConfig(window.config) newChild.SetConfig(window.config)
} }
if newChild, ok := child.(elements.Flexible); ok {
newChild.OnFlexibleHeightChange(window.resizeChildToFit)
}
if newChild, ok := child.(elements.Focusable); ok { if newChild, ok := child.(elements.Focusable); ok {
newChild.OnFocusRequest(window.childSelectionRequestCallback) newChild.OnFocusRequest(window.childSelectionRequestCallback)
} }
@ -263,26 +257,7 @@ func (window *Window) redrawChildEntirely () {
func (window *Window) resizeChildToFit () { func (window *Window) resizeChildToFit () {
window.skipChildDrawCallback = true window.skipChildDrawCallback = true
if child, ok := window.child.(elements.Flexible); ok { window.child.DrawTo(window.canvas, window.canvas.Bounds())
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.skipChildDrawCallback = false window.skipChildDrawCallback = false
} }

View File

@ -4,7 +4,6 @@ import "image"
import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config" 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/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
@ -176,7 +175,7 @@ func (element *Checkbox) draw () {
backgroundPattern.Draw(element.core, bounds) backgroundPattern.Draw(element.core, bounds)
pattern := element.theme.Pattern(theme.PatternButton, state) pattern := element.theme.Pattern(theme.PatternButton, state)
artist.DrawBounds(element.core, pattern, boxBounds) pattern.Draw(element.core, boxBounds)
textBounds := element.drawer.LayoutBounds() textBounds := element.drawer.LayoutBounds()
margin := element.theme.Margin(theme.PatternBackground) margin := element.theme.Margin(theme.PatternBackground)

View File

@ -20,12 +20,10 @@ type Container struct {
layout layouts.Layout layout layouts.Layout
children []layouts.LayoutEntry children []layouts.LayoutEntry
warping bool warping bool
flexible bool
config config.Wrapped config config.Wrapped
theme theme.Wrapped theme theme.Wrapped
onFlexibleHeightChange func ()
onFocusRequest func () (granted bool) onFocusRequest func () (granted bool)
onFocusMotionRequest func (input.KeynavDirection) (granted bool) onFocusMotionRequest func (input.KeynavDirection) (granted bool)
} }
@ -70,9 +68,6 @@ func (element *Container) Adopt (child elements.Element, expand bool) {
element.redoAll() element.redoAll()
element.core.DamageAll() element.core.DamageAll()
}) })
if child0, ok := child.(elements.Flexible); ok {
child0.OnFlexibleHeightChange(element.notifyFlexibleChange)
}
if child0, ok := child.(elements.Focusable); ok { if child0, ok := child.(elements.Focusable); ok {
child0.OnFocusRequest (func () (granted bool) { child0.OnFocusRequest (func () (granted bool) {
return element.childFocusRequestCallback(child0) return element.childFocusRequestCallback(child0)
@ -92,7 +87,6 @@ func (element *Container) Adopt (child elements.Element, expand bool) {
// refresh stale data // refresh stale data
element.updateMinimumSize() element.updateMinimumSize()
element.reflectChildProperties()
if element.core.HasImage() && !element.warping { if element.core.HasImage() && !element.warping {
element.redoAll() element.redoAll()
element.core.DamageAll() element.core.DamageAll()
@ -135,7 +129,6 @@ func (element *Container) Disown (child elements.Element) {
} }
element.updateMinimumSize() element.updateMinimumSize()
element.reflectChildProperties()
if element.core.HasImage() && !element.warping { if element.core.HasImage() && !element.warping {
element.redoAll() element.redoAll()
element.core.DamageAll() element.core.DamageAll()
@ -143,7 +136,7 @@ func (element *Container) Disown (child elements.Element) {
} }
func (element *Container) clearChildEventHandlers (child elements.Element) { func (element *Container) clearChildEventHandlers (child elements.Element) {
child.DrawTo(nil) child.DrawTo(nil, image.Rectangle { })
child.OnDamage(nil) child.OnDamage(nil)
child.OnMinimumSizeChange(nil) child.OnMinimumSizeChange(nil)
if child0, ok := child.(elements.Focusable); ok { if child0, ok := child.(elements.Focusable); ok {
@ -153,9 +146,6 @@ func (element *Container) clearChildEventHandlers (child elements.Element) {
child0.HandleUnfocus() child0.HandleUnfocus()
} }
} }
if child0, ok := child.(elements.Flexible); ok {
child0.OnFlexibleHeightChange(nil)
}
} }
// DisownAll removes all child elements from the container at once. // DisownAll removes all child elements from the container at once.
@ -166,7 +156,6 @@ func (element *Container) DisownAll () {
element.children = nil element.children = nil
element.updateMinimumSize() element.updateMinimumSize()
element.reflectChildProperties()
if element.core.HasImage() && !element.warping { if element.core.HasImage() && !element.warping {
element.redoAll() element.redoAll()
element.core.DamageAll() element.core.DamageAll()
@ -211,7 +200,7 @@ func (element *Container) redoAll () {
// remove child canvasses so that any operations done in here will not // remove child canvasses so that any operations done in here will not
// cause a child to draw to a wack ass canvas. // cause a child to draw to a wack ass canvas.
for _, entry := range element.children { for _, entry := range element.children {
entry.DrawTo(nil) entry.DrawTo(nil, entry.Bounds)
} }
// do a layout // do a layout
@ -225,12 +214,13 @@ func (element *Container) redoAll () {
pattern := element.theme.Pattern ( pattern := element.theme.Pattern (
theme.PatternBackground, theme.PatternBackground,
theme.State { }) theme.State { })
artist.DrawShatter ( artist.DrawShatter(element.core, pattern, element.Bounds(), rocks...)
element.core, pattern, rocks...)
// cut our canvas up and give peices to child elements // cut our canvas up and give peices to child elements
for _, entry := range element.children { 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() 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)) { func (element *Container) OnFocusRequest (callback func () (granted bool)) {
element.onFocusRequest = callback element.onFocusRequest = callback
element.Propagator.OnFocusRequest(callback) element.Propagator.OnFocusRequest(callback)
@ -275,35 +253,6 @@ func (element *Container) OnFocusMotionRequest (
element.Propagator.OnFocusMotionRequest(callback) 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 ( func (element *Container) childFocusRequestCallback (
child elements.Focusable, child elements.Focusable,
) ( ) (
@ -326,12 +275,6 @@ func (element *Container) updateMinimumSize () {
element.core.SetMinimumSize(width, height) element.core.SetMinimumSize(width, height)
} }
func (element *Container) notifyFlexibleChange () {
if element.onFlexibleHeightChange != nil {
element.onFlexibleHeightChange()
}
}
func (element *Container) doLayout () { func (element *Container) doLayout () {
margin := element.theme.Margin(theme.PatternBackground) margin := element.theme.Margin(theme.PatternBackground)
padding := element.theme.Padding(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 package basicElements
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/textdraw"
@ -14,6 +15,9 @@ type Label struct {
text string text string
drawer textdraw.Drawer drawer textdraw.Drawer
forcedColumns int
forcedRows int
config config.Wrapped config config.Wrapped
theme theme.Wrapped theme theme.Wrapped
@ -56,6 +60,17 @@ func (element *Label) handleResize () {
return 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 // FlexibleHeightFor returns the reccomended height for this element based on
// the given width in order to allow the text to wrap properly. // the given width in order to allow the text to wrap properly.
func (element *Label) FlexibleHeightFor (width int) (height int) { func (element *Label) FlexibleHeightFor (width int) (height int) {
@ -134,20 +149,35 @@ func (element *Label) SetConfig (new config.Config) {
} }
func (element *Label) updateMinimumSize () { func (element *Label) updateMinimumSize () {
var width, height int
if element.wrap { if element.wrap {
em := element.drawer.Em().Round() em := element.drawer.Em().Round()
if em < 1 { if em < 1 {
em = element.theme.Padding(theme.PatternBackground)[0] em = element.theme.Padding(theme.PatternBackground)[0]
} }
element.core.SetMinimumSize ( width, height = em, element.drawer.LineHeight().Round()
em, element.drawer.LineHeight().Round())
if element.onFlexibleHeightChange != nil { if element.onFlexibleHeightChange != nil {
element.onFlexibleHeightChange() element.onFlexibleHeightChange()
} }
} else { } else {
bounds := element.drawer.LayoutBounds() 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 () { func (element *Label) draw () {
@ -160,7 +190,7 @@ func (element *Label) draw () {
textBounds := element.drawer.LayoutBounds() textBounds := element.drawer.LayoutBounds()
foreground := element.theme.Color ( foreground := element.theme.Color (
theme.ColorForeground, theme.ColorForeground,
theme.State { }) theme.State { })
element.drawer.Draw(element.core, foreground, bounds.Min.Sub(textBounds.Min)) 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) ).Add(innerBounds.Min).Intersect(innerBounds)
pattern := element.theme.Pattern(theme.PatternSunken, state) pattern := element.theme.Pattern(theme.PatternSunken, state)
artist.DrawShatter ( 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) pattern := entry.theme.Pattern(theme.PatternRaised, state)
padding := entry.theme.Padding(theme.PatternRaised) padding := entry.theme.Padding(theme.PatternRaised)
bounds := entry.Bounds().Add(offset) bounds := entry.Bounds().Add(offset)
artist.DrawBounds(destination, pattern, bounds) pattern.Draw(destination, bounds)
foreground := entry.theme.Color (theme.ColorForeground, state) foreground := entry.theme.Color (theme.ColorForeground, state)
return entry.drawer.Draw ( return entry.drawer.Draw (

View File

@ -3,7 +3,6 @@ package basicElements
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// ProgressBar displays a visual indication of how far along a task is. // 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.Min.X + int(float64(bounds.Dx()) * element.progress),
bounds.Max.Y) bounds.Max.Y)
mercury := element.theme.Pattern(theme.PatternMercury, theme.State { }) 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/input"
import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// ScrollBar is an element similar to Slider, but it has special behavior that // ScrollBar is an element similar to Slider, but it has special behavior that
@ -315,12 +314,10 @@ func (element *ScrollBar) draw () {
Disabled: !element.Enabled(), Disabled: !element.Enabled(),
Pressed: element.dragging, Pressed: element.dragging,
} }
artist.DrawBounds ( element.theme.Pattern(theme.PatternGutter, state).Draw (
element.core, element.core,
element.theme.Pattern(theme.PatternGutter, state),
bounds) bounds)
artist.DrawBounds ( element.theme.Pattern(theme.PatternHandle, state).Draw (
element.core, element.core,
element.theme.Pattern(theme.PatternHandle, state),
element.bar) element.bar)
} }

View File

@ -120,7 +120,7 @@ func (element *ScrollContainer) setChildEventHandlers (child elements.Element) {
} }
func (element *ScrollContainer) clearChildEventHandlers (child elements.Scrollable) { func (element *ScrollContainer) clearChildEventHandlers (child elements.Scrollable) {
child.DrawTo(nil) child.DrawTo(nil, image.Rectangle { })
child.OnDamage(nil) child.OnDamage(nil)
child.OnMinimumSizeChange(nil) child.OnMinimumSizeChange(nil)
child.OnScrollBoundsChange(nil) child.OnScrollBoundsChange(nil)
@ -131,9 +131,6 @@ func (element *ScrollContainer) clearChildEventHandlers (child elements.Scrollab
child0.HandleUnfocus() child0.HandleUnfocus()
} }
} }
if child0, ok := child.(elements.Flexible); ok {
child0.OnFlexibleHeightChange(nil)
}
} }
// SetTheme sets the element's theme. // SetTheme sets the element's theme.
@ -201,19 +198,26 @@ func (element *ScrollContainer) Child (index int) (child elements.Element) {
func (element *ScrollContainer) redoAll () { func (element *ScrollContainer) redoAll () {
if !element.core.HasImage() { return } if !element.core.HasImage() { return }
if element.child != nil { element.child.DrawTo(nil) } zr := image.Rectangle { }
if element.horizontal != nil { element.horizontal.DrawTo(nil) } if element.child != nil { element.child.DrawTo(nil, zr) }
if element.vertical != nil { element.vertical.DrawTo(nil) } if element.horizontal != nil { element.horizontal.DrawTo(nil, zr) }
if element.vertical != nil { element.vertical.DrawTo(nil, zr) }
childBounds, horizontalBounds, verticalBounds := element.layout() childBounds, horizontalBounds, verticalBounds := element.layout()
if element.child != nil { 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 { 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 { if element.vertical != nil {
element.vertical.DrawTo(canvas.Cut(element.core, verticalBounds)) element.vertical.DrawTo (
canvas.Cut(element.core, verticalBounds),
verticalBounds)
} }
element.draw() element.draw()
} }
@ -270,7 +274,6 @@ func (element *ScrollContainer) layout () (
} }
func (element *ScrollContainer) draw () { func (element *ScrollContainer) draw () {
// XOR
if element.horizontal != nil && element.vertical != nil { if element.horizontal != nil && element.vertical != nil {
bounds := element.Bounds() bounds := element.Bounds()
bounds.Min = image.Pt ( 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/input"
import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// Slider is a slider control with a floating point value between zero and one. // 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(), Disabled: !element.Enabled(),
Pressed: element.dragging, Pressed: element.dragging,
} }
artist.DrawBounds ( element.theme.Pattern(theme.PatternGutter, state).Draw (
element.core, element.core,
element.theme.Pattern(theme.PatternGutter, state),
bounds) bounds)
artist.DrawBounds ( element.theme.Pattern(theme.PatternHandle, state).Draw (
element.core, element.core,
element.theme.Pattern(theme.PatternHandle, state),
element.bar) element.bar)
} }

View File

@ -4,7 +4,6 @@ import "image"
import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config" 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/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
@ -185,11 +184,11 @@ func (element *Switch) draw () {
gutterPattern := element.theme.Pattern ( gutterPattern := element.theme.Pattern (
theme.PatternGutter, state) theme.PatternGutter, state)
artist.DrawBounds(element.core, gutterPattern, gutterBounds) gutterPattern.Draw(element.core, gutterBounds)
handlePattern := element.theme.Pattern ( handlePattern := element.theme.Pattern (
theme.PatternHandle, state) theme.PatternHandle, state)
artist.DrawBounds(element.core, handlePattern, handleBounds) handlePattern.Draw(element.core, handleBounds)
textBounds := element.drawer.LayoutBounds() textBounds := element.drawer.LayoutBounds()
offset := bounds.Min.Add(image.Point { 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) { func (element *TextBox) HandleMouseMove (x, y int) {
if !element.Enabled() { return } if !element.Enabled() { return }
if !element.Focused() { element.Focus() }
if element.dragging { if element.dragging {
runeIndex := element.atPosition(image.Pt(x, y)) 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. // widgets. It is meant to be embedded directly into a struct.
type Core struct { type Core struct {
canvas canvas.Canvas canvas canvas.Canvas
bounds image.Rectangle
metrics struct { metrics struct {
minimumWidth int minimumWidth int
@ -37,7 +38,7 @@ func NewCore (
// overridden. // overridden.
func (core *Core) Bounds () (bounds image.Rectangle) { func (core *Core) Bounds () (bounds image.Rectangle) {
if core.canvas == nil { return } if core.canvas == nil { return }
return core.canvas.Bounds() return core.bounds
} }
// MinimumSize fulfils the tomo.Element interface. This should not need to be // 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 // DrawTo fulfills the tomo.Element interface. This should not need to be
// overridden. // overridden.
func (core *Core) DrawTo (canvas canvas.Canvas) { func (core *Core) DrawTo (canvas canvas.Canvas, bounds image.Rectangle) {
core.canvas = canvas core.canvas = canvas
core.bounds = bounds
if core.drawSizeChange != nil && core.canvas != nil { if core.drawSizeChange != nil && core.canvas != nil {
core.drawSizeChange() 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 { func (propagator *Propagator) firstFocused () int {
for index := 0; index < propagator.parent.CountChildren(); index ++ { for index := 0; index < propagator.parent.CountChildren(); index ++ {
child, focusable := propagator.parent.Child(index).(elements.Focusable) child, focusable := propagator.parent.Child(index).(elements.Focusable)

View File

@ -1,5 +1,6 @@
package core package core
// import "runtime/debug"
import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/input"
// FocusableCore is a struct that can be embedded into objects to make them // 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. // HandleUnfocus causes this element to mark itself as unfocused.
func (core *FocusableCore) HandleUnfocus () { func (core *FocusableCore) HandleUnfocus () {
core.focused = false core.focused = false
// debug.PrintStack()
if core.drawFocusChange != nil { core.drawFocusChange() } 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. // Element represents a basic on-screen object.
type Element interface { type Element interface {
// Bounds reports the element's bounding box. This must reflect the // 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) Bounds () (bounds image.Rectangle)
// DrawTo sets this element's canvas. This should only be called by the // DrawTo gives the element a canvas to draw on, along with a bounding
// parent element. This is typically a region of the parent element's // box to be used for laying out the element. This should only be called
// canvas. // by the parent element. This is typically a region of the parent
DrawTo (canvas canvas.Canvas) // element's canvas.
DrawTo (canvas canvas.Canvas, bounds image.Rectangle)
// OnDamage sets a function to be called when an area of the element is // OnDamage sets a function to be called when an area of the element is
// drawn on and should be pushed to the screen. // 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) pattern := element.theme.Pattern(theme.PatternPinboard, state)
artist.DrawShatter ( artist.DrawShatter (
element.core, pattern, element.contentBounds) element.core, pattern, element.Bounds(), element.contentBounds)
} }
func (element *Piano) drawFlat ( func (element *Piano) drawFlat (
@ -316,7 +316,7 @@ func (element *Piano) drawFlat (
state.Pressed = pressed state.Pressed = pressed
pattern := element.theme.Theme.Pattern ( pattern := element.theme.Theme.Pattern (
theme.PatternButton, state, theme.C("fun", "flatKey")) theme.PatternButton, state, theme.C("fun", "flatKey"))
artist.DrawBounds(element.core, pattern, bounds) pattern.Draw(element.core, bounds)
} }
func (element *Piano) drawSharp ( func (element *Piano) drawSharp (
@ -327,5 +327,5 @@ func (element *Piano) drawSharp (
state.Pressed = pressed state.Pressed = pressed
pattern := element.theme.Theme.Pattern ( pattern := element.theme.Theme.Pattern (
theme.PatternButton, state, theme.C("fun", "sharpKey")) 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...) tiles := shatter.Shatter(c41.Bounds(), rocks...)
for index, tile := range tiles { for index, tile := range tiles {
artist.DrawBounds ( []artist.Pattern {
element.core, patterns.Uhex(0xFF0000FF),
[]artist.Pattern { patterns.Uhex(0x00FF00FF),
patterns.Uhex(0xFF0000FF), patterns.Uhex(0xFF00FFFF),
patterns.Uhex(0x00FF00FF), patterns.Uhex(0xFFFF00FF),
patterns.Uhex(0xFF00FFFF), patterns.Uhex(0x00FFFFFF),
patterns.Uhex(0xFFFF00FF), } [index % 5].Draw(element.core, tile)
patterns.Uhex(0x00FFFFFF),
} [index % 5], tile)
} }
// 0, 2 // 0, 2
c02 := element.cellAt(0, 2) c02 := element.cellAt(0, 2)
shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1) shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1)
shapes.FillEllipse(c02, c41) shapes.FillEllipse(c02, c41, c02.Bounds())
// 1, 2 // 1, 2
c12 := element.cellAt(1, 2) c12 := element.cellAt(1, 2)
shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1) shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1)
shapes.StrokeEllipse(c12, c41, 5) shapes.StrokeEllipse(c12, c41, c12.Bounds(), 5)
// 2, 2 // 2, 2
c22 := element.cellAt(2, 2) c22 := element.cellAt(2, 2)
shapes.FillRectangle(c22, c41) shapes.FillRectangle(c22, c41, c22.Bounds())
// 3, 2 // 3, 2
c32 := element.cellAt(3, 2) c32 := element.cellAt(3, 2)
shapes.StrokeRectangle(c32, c41, 5) shapes.StrokeRectangle(c32, c41, c32.Bounds(), 5)
// 4, 2 // 4, 2
c42 := element.cellAt(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 package main
import "time" import "time"
import "image"
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/canvas"
@ -30,14 +31,17 @@ func NewGame (world World, textures Textures) (game *Game) {
return return
} }
func (game *Game) DrawTo (canvas canvas.Canvas) { func (game *Game) DrawTo (canvas canvas.Canvas, bounds image.Rectangle) {
if canvas == nil { if canvas == nil {
game.stopChan <- true select {
case game.stopChan <- true:
default:
}
} else if !game.running { } else if !game.running {
game.running = true game.running = true
go game.run() go game.run()
} }
game.Raycaster.DrawTo(canvas) game.Raycaster.DrawTo(canvas, bounds)
} }
func (game *Game) Stamina () float64 { func (game *Game) Stamina () float64 {

View File

@ -3,7 +3,6 @@ package basicLayouts
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/layouts" 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 // 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 // positioned above as the main focus of the dialog, and is set to expand
@ -132,45 +131,6 @@ func (layout Dialog) MinimumSize (
return 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 ( func (layout Dialog) minimumSizeOfControlRow (
entries []layouts.LayoutEntry, entries []layouts.LayoutEntry,
margin image.Point, margin image.Point,

View File

@ -3,7 +3,6 @@ package basicLayouts
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/layouts" 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 // 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 // list will be positioned on the left, and elements at the end of the entry
@ -76,49 +75,6 @@ func (layout Horizontal) MinimumSize (
return 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 ( func (layout Horizontal) expandingElementWidth (
entries []layouts.LayoutEntry, entries []layouts.LayoutEntry,
margin image.Point, margin image.Point,

View File

@ -3,7 +3,6 @@ package basicLayouts
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/layouts" 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 // 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 // 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)) minimumHeights := make([]int, len(entries))
expandingElements := 0 expandingElements := 0
for index, entry := range entries { for index, entry := range entries {
var entryMinHeight int _, entryMinHeight := entry.MinimumSize()
if child, flexible := entry.Element.(elements.Flexible); flexible {
entryMinHeight = child.FlexibleHeightFor(bounds.Dx())
} else {
_, entryMinHeight = entry.MinimumSize()
}
minimumHeights[index] = entryMinHeight minimumHeights[index] = entryMinHeight
if entry.Expand { if entry.Expand {
@ -101,34 +94,3 @@ func (layout Vertical) MinimumSize (
} }
return 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 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 // 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. // to determine the minimum amount of room it needs to do so.
type Layout interface { type Layout interface {
@ -39,16 +35,4 @@ type Layout interface {
) ( ) (
width, height int, 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() metrics := setter.face.Metrics()
remaining := setter.text remaining := setter.text
y := fixed.Int26_6(0) y := fixed.Int26_6(0)
maxY := fixed.I(setter.maxHeight) + metrics.Height for len(remaining) > 0 {
for len(remaining) > 0 && (y < maxY || setter.maxHeight == 0) {
// process one line // process one line
line, remainingFromLine := DoLine ( line, remainingFromLine := DoLine (
remaining, setter.face, fixed.I(setter.maxWidth)) 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