Merge pull request 'direct-draw' (#6) from direct-draw into main

Reviewed-on: sashakoshka/tomo#6
This commit is contained in:
Sasha Koshka 2023-02-01 04:05:25 +00:00
commit b4a5bc7d03
21 changed files with 206 additions and 304 deletions

View File

@ -13,6 +13,7 @@ type Window struct {
backend *Backend
xWindow *xwindow.Window
xCanvas *xgraphics.Image
canvas tomo.BasicCanvas
child tomo.Element
onClose func ()
skipChildDrawCallback bool
@ -194,6 +195,9 @@ func (window *Window) OnClose (callback func ()) {
}
func (window *Window) reallocateCanvas () {
window.canvas = tomo.NewBasicCanvas (
window.metrics.width,
window.metrics.height)
if window.xCanvas != nil {
window.xCanvas.Destroy()
}
@ -203,12 +207,12 @@ func (window *Window) reallocateCanvas () {
0, 0,
window.metrics.width,
window.metrics.height))
window.xCanvas.CreatePixmap()
}
func (window *Window) redrawChildEntirely () {
window.pushRegion(window.paste(window.child))
}
func (window *Window) resizeChildToFit () {
@ -228,15 +232,10 @@ func (window *Window) resizeChildToFit () {
if window.metrics.height >= minimumHeight &&
window.metrics.width >= minimumWidth {
window.child.Resize (
window.metrics.width,
window.metrics.height)
window.child.DrawTo(window.canvas)
}
} else {
window.child.Resize (
window.metrics.width,
window.metrics.height)
window.child.DrawTo(window.canvas)
}
window.skipChildDrawCallback = false
}

View File

@ -63,6 +63,7 @@ func (canvas BasicCanvas) Buffer () (data []color.RGBA, stride int) {
// Cut returns a sub-canvas of a given canvas.
func Cut (canvas Canvas, bounds image.Rectangle) (reduced BasicCanvas) {
// println(canvas.Bounds().String(), bounds.String())
bounds = bounds.Intersect(canvas.Bounds())
if bounds.Empty() { return }
reduced.rect = bounds

View File

@ -15,9 +15,10 @@ type Element interface {
// instead of the offending dimension(s).
MinimumSize () (width, height int)
// Resize resizes the element. This should only be called by the
// element's parent.
Resize (width, height int)
// 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)
// 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

@ -25,7 +25,7 @@ type Button struct {
// NewButton creates a new button with the specified label text.
func NewButton (text string) (element *Button) {
element = &Button { }
element.Core, element.core = core.NewCore(element)
element.Core, element.core = core.NewCore(element.draw)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
@ -38,11 +38,6 @@ func NewButton (text string) (element *Button) {
return
}
func (element *Button) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
element.draw()
}
func (element *Button) HandleMouseDown (x, y int, button tomo.Button) {
if !element.Enabled() { return }
if !element.Focused() { element.Focus() }
@ -126,7 +121,7 @@ func (element *Button) SetText (text string) {
}
func (element *Button) draw () {
bounds := element.core.Bounds()
bounds := element.Bounds()
pattern, inset := theme.ButtonPattern(theme.PatternState {
Case: buttonCase,
@ -135,14 +130,14 @@ func (element *Button) draw () {
Pressed: element.pressed,
})
artist.FillRectangle(element.core, pattern, bounds)
artist.FillRectangle(element, pattern, bounds)
innerBounds := inset.Apply(bounds)
textBounds := element.drawer.LayoutBounds()
offset := image.Point {
X: innerBounds.Min.X + (innerBounds.Dx() - textBounds.Dx()) / 2,
Y: innerBounds.Min.X + (innerBounds.Dy() - textBounds.Dy()) / 2,
Y: innerBounds.Min.Y + (innerBounds.Dy() - textBounds.Dy()) / 2,
}
// account for the fact that the bounding rectangle will be shifted over
@ -154,5 +149,5 @@ func (element *Button) draw () {
Case: buttonCase,
Disabled: !element.Enabled(),
})
element.drawer.Draw(element.core, foreground, offset)
element.drawer.Draw(element, foreground, offset)
}

View File

@ -26,7 +26,7 @@ type Checkbox struct {
// NewCheckbox creates a new cbeckbox with the specified label text.
func NewCheckbox (text string, checked bool) (element *Checkbox) {
element = &Checkbox { checked: checked }
element.Core, element.core = core.NewCore(element)
element.Core, element.core = core.NewCore(element.draw)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
@ -39,12 +39,6 @@ func NewCheckbox (text string, checked bool) (element *Checkbox) {
return
}
// Resize changes this element's size.
func (element *Checkbox) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
element.draw()
}
func (element *Checkbox) HandleMouseDown (x, y int, button tomo.Button) {
if !element.Enabled() { return }
element.Focus()
@ -139,13 +133,13 @@ func (element *Checkbox) SetText (text string) {
}
func (element *Checkbox) draw () {
bounds := element.core.Bounds()
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy())
bounds := element.Bounds()
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState {
Case: checkboxCase,
})
artist.FillRectangle ( element.core, backgroundPattern, bounds)
artist.FillRectangle(element, backgroundPattern, bounds)
pattern, inset := theme.ButtonPattern(theme.PatternState {
Case: checkboxCase,
@ -153,12 +147,12 @@ func (element *Checkbox) draw () {
Focused: element.Focused(),
Pressed: element.pressed,
})
artist.FillRectangle(element.core, pattern, boxBounds)
artist.FillRectangle(element, pattern, boxBounds)
textBounds := element.drawer.LayoutBounds()
offset := image.Point {
offset := bounds.Min.Add(image.Point {
X: bounds.Dy() + theme.Padding(),
}
})
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
@ -167,10 +161,10 @@ func (element *Checkbox) draw () {
Case: checkboxCase,
Disabled: !element.Enabled(),
})
element.drawer.Draw(element.core, foreground, offset)
element.drawer.Draw(element, foreground, offset)
if element.checked {
checkBounds := inset.Apply(boxBounds).Inset(2)
artist.FillRectangle(element.core, foreground, checkBounds)
artist.FillRectangle(element, foreground, checkBounds)
}
}

View File

@ -30,7 +30,7 @@ type Container struct {
// NewContainer creates a new container.
func NewContainer (layout tomo.Layout) (element *Container) {
element = &Container { }
element.Core, element.core = core.NewCore(element)
element.Core, element.core = core.NewCore(element.redoAll)
element.SetLayout(layout)
return
}
@ -39,8 +39,7 @@ func NewContainer (layout tomo.Layout) (element *Container) {
func (element *Container) SetLayout (layout tomo.Layout) {
element.layout = layout
if element.core.HasImage() {
element.recalculate()
element.draw()
element.redoAll()
element.core.DamageAll()
}
}
@ -51,7 +50,7 @@ func (element *Container) SetLayout (layout tomo.Layout) {
func (element *Container) Adopt (child tomo.Element, expand bool) {
// set event handlers
child.OnDamage (func (region tomo.Canvas) {
element.drawChildRegion(child, region)
element.core.DamageRegion(region.Bounds())
})
child.OnMinimumSizeChange(element.updateMinimumSize)
if child0, ok := child.(tomo.Flexible); ok {
@ -78,8 +77,7 @@ func (element *Container) Adopt (child tomo.Element, expand bool) {
element.updateMinimumSize()
element.reflectChildProperties()
if element.core.HasImage() && !element.warping {
element.recalculate()
element.draw()
element.redoAll()
element.core.DamageAll()
}
}
@ -101,8 +99,7 @@ func (element *Container) Warp (callback func ()) {
// and redraw every time, because although that is the most likely use
// case, it is not the only one.
if element.core.HasImage() {
element.recalculate()
element.draw()
element.redoAll()
element.core.DamageAll()
}
}
@ -123,13 +120,13 @@ func (element *Container) Disown (child tomo.Element) {
element.updateMinimumSize()
element.reflectChildProperties()
if element.core.HasImage() && !element.warping {
element.recalculate()
element.draw()
element.redoAll()
element.core.DamageAll()
}
}
func (element *Container) clearChildEventHandlers (child tomo.Element) {
child.DrawTo(nil)
child.OnDamage(nil)
child.OnMinimumSizeChange(nil)
if child0, ok := child.(tomo.Focusable); ok {
@ -151,8 +148,7 @@ func (element *Container) DisownAll () {
element.updateMinimumSize()
element.reflectChildProperties()
if element.core.HasImage() && !element.warping {
element.recalculate()
element.draw()
element.redoAll()
element.core.DamageAll()
}
}
@ -182,7 +178,7 @@ func (element *Container) Child (index int) (child tomo.Element) {
// there are no children at the coordinates, this method will return nil.
func (element *Container) ChildAt (point image.Point) (child tomo.Element) {
for _, entry := range element.children {
if point.In(entry.Bounds().Add(entry.Position)) {
if point.In(entry.Bounds) {
child = entry.Element
}
}
@ -192,7 +188,7 @@ func (element *Container) ChildAt (point image.Point) (child tomo.Element) {
func (element *Container) childPosition (child tomo.Element) (position image.Point) {
for _, entry := range element.children {
if entry.Element == child {
position = entry.Position
position = entry.Bounds.Min
break
}
}
@ -200,41 +196,48 @@ func (element *Container) childPosition (child tomo.Element) (position image.Poi
return
}
func (element *Container) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
func (element *Container) redoAll () {
// do a layout
element.recalculate()
element.draw()
// draw a background
bounds := element.Bounds()
pattern, _ := theme.BackgroundPattern (theme.PatternState {
Case: containerCase,
})
artist.FillRectangle(element, pattern, bounds)
// cut our canvas up and give peices to child elements
for _, entry := range element.children {
entry.DrawTo(tomo.Cut(element, entry.Bounds))
}
}
func (element *Container) HandleMouseDown (x, y int, button tomo.Button) {
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(tomo.MouseTarget)
if !handlesMouse { return }
element.drags[button] = child
childPosition := element.childPosition(child)
child.HandleMouseDown(x - childPosition.X, y - childPosition.Y, button)
child.HandleMouseDown(x, y, button)
}
func (element *Container) HandleMouseUp (x, y int, button tomo.Button) {
child := element.drags[button]
if child == nil { return }
element.drags[button] = nil
childPosition := element.childPosition(child)
child.HandleMouseUp(x - childPosition.X, y - childPosition.Y, button)
child.HandleMouseUp(x, y, button)
}
func (element *Container) HandleMouseMove (x, y int) {
for _, child := range element.drags {
if child == nil { continue }
childPosition := element.childPosition(child)
child.HandleMouseMove(x - childPosition.X, y - childPosition.Y)
child.HandleMouseMove(x, y)
}
}
func (element *Container) HandleMouseScroll (x, y int, deltaX, deltaY float64) {
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(tomo.MouseTarget)
if !handlesMouse { return }
childPosition := element.childPosition(child)
child.HandleMouseScroll(x - childPosition.X, y - childPosition.Y, deltaX, deltaY)
child.HandleMouseScroll(x, y, deltaX, deltaY)
}
func (element *Container) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
@ -468,31 +471,5 @@ func (element *Container) updateMinimumSize () {
}
func (element *Container) recalculate () {
bounds := element.Bounds()
element.layout.Arrange(element.children, bounds.Dx(), bounds.Dy())
}
func (element *Container) draw () {
bounds := element.core.Bounds()
pattern, _ := theme.BackgroundPattern (theme.PatternState {
Case: containerCase,
})
artist.FillRectangle(element.core, pattern, bounds)
for _, entry := range element.children {
artist.Paste(element.core, entry, entry.Position)
}
}
func (element *Container) drawChildRegion (child tomo.Element, region tomo.Canvas) {
if element.warping { return }
for _, entry := range element.children {
if entry.Element == child {
artist.Paste(element.core, region, entry.Position)
element.core.DamageRegion (
region.Bounds().Add(entry.Position))
break
}
}
element.layout.Arrange(element.children, element.Bounds())
}

View File

@ -1,6 +1,5 @@
package basic
import "image"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
@ -23,7 +22,7 @@ type Label struct {
// wrapped.
func NewLabel (text string, wrap bool) (element *Label) {
element = &Label { }
element.Core, element.core = core.NewCore(element)
element.Core, element.core = core.NewCore(element.handleResize)
face := theme.FontFaceRegular()
element.drawer.SetFace(face)
element.SetWrap(wrap)
@ -31,12 +30,11 @@ func NewLabel (text string, wrap bool) (element *Label) {
return
}
// Resize resizes the label and re-wraps the text if wrapping is enabled.
func (element *Label) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
func (element *Label) handleResize () {
bounds := element.Bounds()
if element.wrap {
element.drawer.SetMaxWidth(width)
element.drawer.SetMaxHeight(height)
element.drawer.SetMaxWidth(bounds.Dx())
element.drawer.SetMaxHeight(bounds.Dy())
}
element.draw()
return
@ -108,20 +106,17 @@ func (element *Label) updateMinimumSize () {
}
func (element *Label) draw () {
bounds := element.core.Bounds()
bounds := element.Bounds()
pattern, _ := theme.BackgroundPattern(theme.PatternState {
Case: labelCase,
})
artist.FillRectangle(element.core, pattern, bounds)
artist.FillRectangle(element, pattern, bounds)
textBounds := element.drawer.LayoutBounds()
foreground, _ := theme.ForegroundPattern (theme.PatternState {
Case: labelCase,
})
element.drawer.Draw (element.core, foreground, image.Point {
X: 0 - textBounds.Min.X,
Y: 0 - textBounds.Min.Y,
})
element.drawer.Draw (element, foreground, bounds.Min.Sub(textBounds.Min))
}

View File

@ -33,7 +33,7 @@ type List struct {
// NewList creates a new list element with the specified entries.
func NewList (entries ...ListEntry) (element *List) {
element = &List { selectedEntry: -1 }
element.Core, element.core = core.NewCore(element)
element.Core, element.core = core.NewCore(element.handleResize)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
@ -51,10 +51,7 @@ func NewList (entries ...ListEntry) (element *List) {
return
}
// Resize changes the element's size.
func (element *List) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
func (element *List) handleResize () {
for index, entry := range element.entries {
element.entries[index] = element.resizeEntryToFit(entry)
}
@ -379,7 +376,7 @@ func (element *List) draw () {
Disabled: !element.Enabled(),
Focused: element.Focused(),
})
artist.FillRectangle(element.core, pattern, bounds)
artist.FillRectangle(element, pattern, bounds)
bounds = inset.Apply(bounds)
dot := image.Point {

View File

@ -16,18 +16,11 @@ type ProgressBar struct {
// level.
func NewProgressBar (progress float64) (element *ProgressBar) {
element = &ProgressBar { progress: progress }
element.Core, element.core = core.NewCore(element)
element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(theme.Padding() * 2, theme.Padding() * 2)
return
}
// Resize resizes the progress bar.
func (element *ProgressBar) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
element.draw()
return
}
// SetProgress sets the progress level of the bar.
func (element *ProgressBar) SetProgress (progress float64) {
if progress == element.progress { return }
@ -39,15 +32,15 @@ func (element *ProgressBar) SetProgress (progress float64) {
}
func (element *ProgressBar) draw () {
bounds := element.core.Bounds()
bounds := element.Bounds()
pattern, inset := theme.SunkenPattern(theme.PatternState { })
artist.FillRectangle(element.core, pattern, bounds)
artist.FillRectangle(element, pattern, bounds)
bounds = inset.Apply(bounds)
meterBounds := image.Rect (
bounds.Min.X, bounds.Min.Y,
bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
bounds.Max.Y)
accent, _ := theme.AccentPattern(theme.PatternState { })
artist.FillRectangle(element.core, accent, meterBounds)
artist.FillRectangle(element, accent, meterBounds)
}

View File

@ -48,20 +48,16 @@ type ScrollContainer struct {
// bars.
func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) {
element = &ScrollContainer { }
element.Core, element.core = core.NewCore(element)
element.Core, element.core = core.NewCore(element.handleResize)
element.updateMinimumSize()
element.horizontal.exists = horizontal
element.vertical.exists = vertical
return
}
// Resize resizes the scroll box.
func (element *ScrollContainer) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
func (element *ScrollContainer) handleResize () {
element.recalculate()
element.child.Resize (
element.childWidth,
element.childHeight)
element.resizeChildToFit()
element.draw()
}
@ -95,10 +91,7 @@ func (element *ScrollContainer) Adopt (child tomo.Scrollable) {
element.vertical.enabled = element.child.ScrollAxes()
if element.core.HasImage() {
element.child.Resize (
element.childWidth,
element.childHeight)
element.core.DamageAll()
element.resizeChildToFit()
}
}
}
@ -120,7 +113,8 @@ func (element *ScrollContainer) HandleMouseDown (x, y int, button tomo.Button) {
if point.In(element.horizontal.bar) {
element.horizontal.dragging = true
element.horizontal.dragOffset =
point.Sub(element.horizontal.bar.Min).X
x - element.horizontal.bar.Min.X +
element.Bounds().Min.X
element.dragHorizontalBar(point)
} else if point.In(element.horizontal.gutter) {
@ -135,7 +129,8 @@ func (element *ScrollContainer) HandleMouseDown (x, y int, button tomo.Button) {
} else if point.In(element.vertical.bar) {
element.vertical.dragging = true
element.vertical.dragOffset =
point.Sub(element.vertical.bar.Min).Y
y - element.vertical.bar.Min.Y +
element.Bounds().Min.Y
element.dragVerticalBar(point)
} else if point.In(element.vertical.gutter) {
@ -259,6 +254,7 @@ func (element *ScrollContainer) childFocusMotionRequestCallback (
}
func (element *ScrollContainer) clearChildEventHandlers (child tomo.Scrollable) {
child.DrawTo(nil)
child.OnDamage(nil)
child.OnMinimumSizeChange(nil)
child.OnScrollBoundsChange(nil)
@ -274,6 +270,14 @@ func (element *ScrollContainer) clearChildEventHandlers (child tomo.Scrollable)
}
}
func (element *ScrollContainer) resizeChildToFit () {
childBounds := image.Rect (
0, 0,
element.childWidth,
element.childHeight).Add(element.Bounds().Min)
element.child.DrawTo(tomo.Cut(element, childBounds))
}
func (element *ScrollContainer) recalculate () {
_, gutterInsetHorizontal := theme.GutterPattern(theme.PatternState {
Case: scrollBarHorizontalCase,
@ -306,6 +310,7 @@ func (element *ScrollContainer) recalculate () {
// if enabled, give substance to the gutters
if horizontal.exists {
horizontal.gutter.Min.X = bounds.Min.X
horizontal.gutter.Min.Y = bounds.Max.Y - thicknessHorizontal
horizontal.gutter.Max.X = bounds.Max.X
horizontal.gutter.Max.Y = bounds.Max.Y
@ -318,6 +323,7 @@ func (element *ScrollContainer) recalculate () {
if vertical.exists {
vertical.gutter.Min.X = bounds.Max.X - thicknessVertical
vertical.gutter.Max.X = bounds.Max.X
vertical.gutter.Min.Y = bounds.Min.Y
vertical.gutter.Max.Y = bounds.Max.Y
if horizontal.exists {
vertical.gutter.Max.Y -= thicknessHorizontal
@ -364,7 +370,7 @@ func (element *ScrollContainer) recalculate () {
}
func (element *ScrollContainer) draw () {
artist.Paste(element.core, element.child, image.Point { })
artist.Paste(element, element.child, image.Point { })
deadPattern, _ := theme.DeadPattern(theme.PatternState {
Case: scrollContainerCase,
})

View File

@ -18,18 +18,11 @@ type Spacer struct {
// will appear as a line.
func NewSpacer (line bool) (element *Spacer) {
element = &Spacer { line: line }
element.Core, element.core = core.NewCore(element)
element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(1, 1)
return
}
// Resize resizes the label and re-wraps the text if wrapping is enabled.
func (element *Spacer) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
element.draw()
return
}
/// SetLine sets whether or not the spacer will appear as a colored line.
func (element *Spacer) SetLine (line bool) {
if element.line == line { return }
@ -41,19 +34,19 @@ func (element *Spacer) SetLine (line bool) {
}
func (element *Spacer) draw () {
bounds := element.core.Bounds()
bounds := element.Bounds()
if element.line {
pattern, _ := theme.ForegroundPattern(theme.PatternState {
Case: spacerCase,
Disabled: true,
})
artist.FillRectangle(element.core, pattern, bounds)
artist.FillRectangle(element, pattern, bounds)
} else {
pattern, _ := theme.BackgroundPattern(theme.PatternState {
Case: spacerCase,
Disabled: true,
})
artist.FillRectangle(element.core, pattern, bounds)
artist.FillRectangle(element, pattern, bounds)
}
}

View File

@ -27,7 +27,7 @@ type Switch struct {
// NewSwitch creates a new switch with the specified label text.
func NewSwitch (text string, on bool) (element *Switch) {
element = &Switch { checked: on, text: text }
element.Core, element.core = core.NewCore(element)
element.Core, element.core = core.NewCore(element.draw)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
@ -41,12 +41,6 @@ func NewSwitch (text string, on bool) (element *Switch) {
return
}
// Resize changes this element's size.
func (element *Switch) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
element.draw()
}
func (element *Switch) HandleMouseDown (x, y int, button tomo.Button) {
if !element.Enabled() { return }
element.Focus()
@ -146,13 +140,13 @@ func (element *Switch) calculateMinimumSize () {
}
func (element *Switch) draw () {
bounds := element.core.Bounds()
handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy())
gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy())
bounds := element.Bounds()
handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min)
backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState {
Case: switchCase,
})
artist.FillRectangle ( element.core, backgroundPattern, bounds)
artist.FillRectangle (element, backgroundPattern, bounds)
if element.checked {
handleBounds.Min.X += bounds.Dy()
@ -174,7 +168,7 @@ func (element *Switch) draw () {
Focused: element.Focused(),
Pressed: element.pressed,
})
artist.FillRectangle(element.core, gutterPattern, gutterBounds)
artist.FillRectangle(element, gutterPattern, gutterBounds)
handlePattern, _ := theme.HandlePattern(theme.PatternState {
Case: switchCase,
@ -182,12 +176,12 @@ func (element *Switch) draw () {
Focused: element.Focused(),
Pressed: element.pressed,
})
artist.FillRectangle(element.core, handlePattern, handleBounds)
artist.FillRectangle(element, handlePattern, handleBounds)
textBounds := element.drawer.LayoutBounds()
offset := image.Point {
offset := bounds.Min.Add(image.Point {
X: bounds.Dy() * 2 + theme.Padding(),
}
})
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
@ -196,5 +190,5 @@ func (element *Switch) draw () {
Case: switchCase,
Disabled: !element.Enabled(),
})
element.drawer.Draw(element.core, foreground, offset)
element.drawer.Draw(element, foreground, offset)
}

View File

@ -34,7 +34,7 @@ type TextBox struct {
// text.
func NewTextBox (placeholder, value string) (element *TextBox) {
element = &TextBox { }
element.Core, element.core = core.NewCore(element)
element.Core, element.core = core.NewCore(element.handleResize)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
@ -51,8 +51,7 @@ func NewTextBox (placeholder, value string) (element *TextBox) {
return
}
func (element *TextBox) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
func (element *TextBox) handleResize () {
element.scrollToCursor()
element.draw()
if element.onScrollBoundsChange != nil {
@ -258,7 +257,8 @@ func (element *TextBox) runOnChange () {
func (element *TextBox) scrollToCursor () {
if !element.core.HasImage() { return }
bounds := element.core.Bounds().Inset(theme.Padding())
bounds := element.Bounds().Inset(theme.Padding())
bounds = bounds.Sub(bounds.Min)
bounds.Max.X -= element.valueDrawer.Em().Round()
cursorPosition := element.valueDrawer.PositionOf(element.cursor)
cursorPosition.X -= element.scroll
@ -273,7 +273,7 @@ func (element *TextBox) scrollToCursor () {
}
func (element *TextBox) draw () {
bounds := element.core.Bounds()
bounds := element.Bounds()
// FIXME: take index into account
pattern, inset := theme.InputPattern(theme.PatternState {
@ -281,36 +281,36 @@ func (element *TextBox) draw () {
Disabled: !element.Enabled(),
Focused: element.Focused(),
})
artist.FillRectangle(element.core, pattern, bounds)
artist.FillRectangle(element, pattern, bounds)
if len(element.text) == 0 && !element.Focused() {
// draw placeholder
textBounds := element.placeholderDrawer.LayoutBounds()
offset := image.Point {
offset := bounds.Min.Add (image.Point {
X: theme.Padding() + inset[3],
Y: theme.Padding() + inset[0],
}
})
foreground, _ := theme.ForegroundPattern(theme.PatternState {
Case: textBoxCase,
Disabled: true,
})
element.placeholderDrawer.Draw (
element.core,
element,
foreground,
offset.Sub(textBounds.Min))
} else {
// draw input value
textBounds := element.valueDrawer.LayoutBounds()
offset := image.Point {
offset := bounds.Min.Add (image.Point {
X: theme.Padding() + inset[3] - element.scroll,
Y: theme.Padding() + inset[0],
}
})
foreground, _ := theme.ForegroundPattern(theme.PatternState {
Case: textBoxCase,
Disabled: !element.Enabled(),
})
element.valueDrawer.Draw (
element.core,
element,
foreground,
offset.Sub(textBounds.Min))
@ -322,7 +322,7 @@ func (element *TextBox) draw () {
Case: textBoxCase,
})
artist.Line (
element.core,
element,
foreground, 1,
cursorPosition.Add(offset),
image.Pt (

View File

@ -7,21 +7,21 @@ import "git.tebibyte.media/sashakoshka/tomo"
// Core is a struct that implements some core functionality common to most
// widgets. It is meant to be embedded directly into a struct.
type Core struct {
canvas tomo.BasicCanvas
parent tomo.Element
canvas tomo.Canvas
metrics struct {
minimumWidth int
minimumHeight int
}
drawSizeChange func ()
onMinimumSizeChange func ()
onDamage func (region tomo.Canvas)
}
// NewCore creates a new element core and its corresponding control.
func NewCore (parent tomo.Element) (core *Core, control CoreControl) {
core = &Core { parent: parent }
func NewCore (drawSizeChange func ()) (core *Core, control CoreControl) {
core = &Core { drawSizeChange: drawSizeChange }
control = CoreControl { core: core }
return
}
@ -33,21 +33,25 @@ func (core *Core) ColorModel () (model color.Model) {
// ColorModel fulfills the draw.Image interface.
func (core *Core) At (x, y int) (pixel color.Color) {
if core.canvas == nil { return }
return core.canvas.At(x, y)
}
// ColorModel fulfills the draw.Image interface.
func (core *Core) Bounds () (bounds image.Rectangle) {
if core.canvas == nil { return }
return core.canvas.Bounds()
}
// ColorModel fulfills the draw.Image interface.
func (core *Core) Set (x, y int, c color.Color) () {
if core.canvas == nil { return }
core.canvas.Set(x, y, c)
}
// Buffer fulfills the tomo.Canvas interface.
func (core *Core) Buffer () (data []color.RGBA, stride int) {
if core.canvas == nil { return }
return core.canvas.Buffer()
}
@ -57,6 +61,15 @@ func (core *Core) MinimumSize () (width, height int) {
return core.metrics.minimumWidth, core.metrics.minimumHeight
}
// DrawTo fulfills the tomo.Element interface. This should not need to be
// overridden.
func (core *Core) DrawTo (canvas tomo.Canvas) {
core.canvas = canvas
if core.drawSizeChange != nil {
core.drawSizeChange()
}
}
// OnDamage fulfils the tomo.Element interface. This should not need to be
// overridden.
func (core *Core) OnDamage (callback func (region tomo.Canvas)) {
@ -74,35 +87,27 @@ func (core *Core) OnMinimumSizeChange (callback func ()) {
// instead kept as a private member. When a Core struct is created, a
// corresponding CoreControl struct is linked to it and returned alongside it.
type CoreControl struct {
tomo.BasicCanvas
core *Core
}
// HasImage returns true if the core has an allocated image buffer, and false if
// it doesn't.
func (control CoreControl) HasImage () (has bool) {
return !control.Bounds().Empty()
return control.core.canvas != nil && !control.core.canvas.Bounds().Empty()
}
// DamageRegion pushes the selected region of pixels to the parent element. This
// does not need to be called when responding to a resize event.
func (control CoreControl) DamageRegion (bounds image.Rectangle) {
if control.core.onDamage != nil {
control.core.onDamage(tomo.Cut(control, bounds))
control.core.onDamage(tomo.Cut(control.core, bounds))
}
}
// DamageAll pushes all pixels to the parent element. This does not need to be
// called when responding to a resize event.
// called when redrawing in response to a change in size.
func (control CoreControl) DamageAll () {
control.DamageRegion(control.Bounds())
}
// AllocateCanvas resizes the canvas, constraining the width and height so that
// they are not less than the specified minimum width and height.
func (control *CoreControl) AllocateCanvas (width, height int) {
control.core.canvas = tomo.NewBasicCanvas(width, height)
control.BasicCanvas = control.core.canvas
control.DamageRegion(control.core.Bounds())
}
// SetMinimumSize sets the minimum size of this element, notifying the parent
@ -119,18 +124,6 @@ func (control CoreControl) SetMinimumSize (width, height int) {
if control.core.onMinimumSizeChange != nil {
control.core.onMinimumSizeChange()
}
// if there is an image buffer, and the current size is less
// than this new minimum size, send core.parent a resize event.
if control.HasImage() {
bounds := control.Bounds()
imageWidth,
imageHeight,
constrained := control.ConstrainSize(bounds.Dx(), bounds.Dy())
if constrained {
core.parent.Resize(imageWidth, imageHeight)
}
}
}
// ConstrainSize contstrains the specified width and height to the minimum width

View File

@ -19,17 +19,11 @@ type AnalogClock struct {
// NewAnalogClock creates a new analog clock that displays the specified time.
func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
element = &AnalogClock { }
element.Core, element.core = core.NewCore(element)
element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(64, 64)
return
}
// Resize changes the size of the clock.
func (element *AnalogClock) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
element.draw()
}
// SetTime changes the time that the clock displays.
func (element *AnalogClock) SetTime (newTime time.Time) {
if newTime == element.time { return }
@ -41,7 +35,7 @@ func (element *AnalogClock) SetTime (newTime time.Time) {
}
func (element *AnalogClock) draw () {
bounds := element.core.Bounds()
bounds := element.Bounds()
pattern, inset := theme.SunkenPattern(theme.PatternState {
Case: clockCase,
@ -87,15 +81,15 @@ func (element *AnalogClock) radialLine (
outer float64,
radian float64,
) {
bounds := element.core.Bounds()
bounds := element.Bounds()
width := float64(bounds.Dx()) / 2
height := float64(bounds.Dy()) / 2
min := image.Pt (
min := element.Bounds().Min.Add(image.Pt (
int(math.Cos(radian) * inner * width + width),
int(math.Sin(radian) * inner * height + height))
max := image.Pt (
int(math.Sin(radian) * inner * height + height)))
max := element.Bounds().Min.Add(image.Pt (
int(math.Cos(radian) * outer * width + width),
int(math.Sin(radian) * outer * height + height))
int(math.Sin(radian) * outer * height + height)))
// println(min.String(), max.String())
artist.Line(element.core, source, 1, min, max)
artist.Line(element, source, 1, min, max)
}

View File

@ -19,16 +19,15 @@ type Artist struct {
// NewArtist creates a new artist test element.
func NewArtist () (element *Artist) {
element = &Artist { }
element.Core, element.core = core.NewCore(element)
element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(480, 600)
return
}
func (element *Artist) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
func (element *Artist) draw () {
bounds := element.Bounds()
element.cellBounds.Max.X = bounds.Dx() / 5
element.cellBounds.Max.Y = (bounds.Dy() - 48) / 8
element.cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
element.cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 8
drawStart := time.Now()

View File

@ -20,29 +20,28 @@ type Mouse struct {
// NewMouse creates a new mouse test element.
func NewMouse () (element *Mouse) {
element = &Mouse { }
element.Core, element.core = core.NewCore(element)
element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(32, 32)
element.color = artist.NewUniform(color.Black)
return
}
func (element *Mouse) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
func (element *Mouse) draw () {
bounds := element.Bounds()
pattern, _ := theme.AccentPattern(theme.PatternState { })
artist.FillRectangle(element.core, pattern, bounds)
artist.FillRectangle(element, pattern, bounds)
artist.StrokeRectangle (
element.core,
element,
artist.NewUniform(color.Black), 1,
bounds)
artist.Line (
element.core, artist.NewUniform(color.White), 1,
image.Pt(1, 1),
image.Pt(bounds.Dx() - 2, bounds.Dy() - 2))
element, artist.NewUniform(color.White), 1,
bounds.Min.Add(image.Pt(1, 1)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2)))
artist.Line (
element.core, artist.NewUniform(color.White), 1,
image.Pt(1, bounds.Dy() - 2),
image.Pt(bounds.Dx() - 2, 1))
element, artist.NewUniform(color.White), 1,
bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1)))
}
func (element *Mouse) HandleMouseDown (x, y int, button tomo.Button) {
@ -54,7 +53,7 @@ func (element *Mouse) HandleMouseUp (x, y int, button tomo.Button) {
element.drawing = false
mousePos := image.Pt(x, y)
element.core.DamageRegion (artist.Line (
element.core, element.color, 1,
element, element.color, 1,
element.lastMousePos, mousePos))
element.lastMousePos = mousePos
}
@ -63,7 +62,7 @@ func (element *Mouse) HandleMouseMove (x, y int) {
if !element.drawing { return }
mousePos := image.Pt(x, y)
element.core.DamageRegion (artist.Line (
element.core, element.color, 1,
element, element.color, 1,
element.lastMousePos, mousePos))
element.lastMousePos = mousePos
}

View File

@ -6,7 +6,7 @@ import "image"
// it can be arranged by a Layout.
type LayoutEntry struct {
Element
Position image.Point
Bounds image.Rectangle
Expand bool
}
@ -17,7 +17,7 @@ type Layout interface {
// and changes the position of the entiries in the slice so that they
// are properly laid out. The given width and height should not be less
// than what is returned by MinimumSize.
Arrange (entries []LayoutEntry, width, height int)
Arrange (entries []LayoutEntry, bounds image.Rectangle)
// MinimumSize returns the minimum width and height that the layout
// needs to properly arrange the given slice of layout entries.

View File

@ -19,12 +19,9 @@ type Dialog struct {
}
// Arrange arranges a list of entries into a dialog.
func (layout Dialog) Arrange (entries []tomo.LayoutEntry, width, height int) {
if layout.Pad {
width -= theme.Margin() * 2
height -= theme.Margin() * 2
}
func (layout Dialog) Arrange (entries []tomo.LayoutEntry, bounds image.Rectangle) {
if layout.Pad { bounds = bounds.Inset(theme.Margin()) }
controlRowWidth, controlRowHeight := 0, 0
if len(entries) > 1 {
controlRowWidth,
@ -32,24 +29,18 @@ func (layout Dialog) Arrange (entries []tomo.LayoutEntry, width, height int) {
}
if len(entries) > 0 {
entries[0].Position = image.Point { }
if layout.Pad {
entries[0].Position.X += theme.Margin()
entries[0].Position.Y += theme.Margin()
}
mainHeight := height - controlRowHeight
main := entries[0]
main.Bounds.Min = bounds.Min
mainHeight := bounds.Dy() - controlRowHeight
if layout.Gap {
mainHeight -= theme.Margin()
}
mainBounds := entries[0].Bounds()
if mainBounds.Dy() != mainHeight ||
mainBounds.Dx() != width {
entries[0].Resize(width, mainHeight)
}
main.Bounds.Max = main.Bounds.Min.Add(image.Pt(bounds.Dx(), mainHeight))
entries[0] = main
}
if len(entries) > 1 {
freeSpace := width
freeSpace := bounds.Dx()
expandingElements := 0
// count the number of expanding elements and the amount of free
@ -71,33 +62,30 @@ func (layout Dialog) Arrange (entries []tomo.LayoutEntry, width, height int) {
}
// determine starting position and dimensions for control row
x, y := 0, height - controlRowHeight
dot := image.Pt(bounds.Min.X, bounds.Max.Y - controlRowHeight)
if expandingElements == 0 {
x = width - controlRowWidth
dot.X = bounds.Max.X - controlRowWidth
}
if layout.Pad {
x += theme.Margin()
y += theme.Margin()
}
height -= controlRowHeight
// set the size and position of each element in the control row
for index, entry := range entries[1:] {
if index > 0 && layout.Gap { x += theme.Margin() }
if index > 0 && layout.Gap { dot.X += theme.Margin() }
entries[index + 1].Position = image.Pt(x, y)
entry.Bounds.Min = dot
entryWidth := 0
if entry.Expand {
entryWidth = expandingElementWidth
} else {
entryWidth, _ = entry.MinimumSize()
}
x += entryWidth
entryBounds := entry.Bounds()
dot.X += entryWidth
entryBounds := entry.Bounds
if entryBounds.Dy() != controlRowHeight ||
entryBounds.Dx() != entryWidth {
entry.Resize(entryWidth, controlRowHeight)
entry.Bounds.Max = entryBounds.Min.Add (
image.Pt(entryWidth, controlRowHeight))
}
entries[index + 1] = entry
}
}

View File

@ -17,36 +17,28 @@ type Horizontal struct {
}
// Arrange arranges a list of entries horizontally.
func (layout Horizontal) Arrange (entries []tomo.LayoutEntry, width, height int) {
if layout.Pad {
width -= theme.Margin() * 2
height -= theme.Margin() * 2
}
// get width of expanding elements
expandingElementWidth := layout.expandingElementWidth(entries, width)
func (layout Horizontal) Arrange (entries []tomo.LayoutEntry, bounds image.Rectangle) {
if layout.Pad { bounds = bounds.Inset(theme.Margin()) }
x, y := 0, 0
if layout.Pad {
x += theme.Margin()
y += theme.Margin()
}
// get width of expanding elements
expandingElementWidth := layout.expandingElementWidth(entries, bounds.Dx())
// set the size and position of each element
dot := bounds.Min
for index, entry := range entries {
if index > 0 && layout.Gap { x += theme.Margin() }
if index > 0 && layout.Gap { dot.X += theme.Margin() }
entries[index].Position = image.Pt(x, y)
entry.Bounds.Min = dot
entryWidth := 0
if entry.Expand {
entryWidth = expandingElementWidth
} else {
entryWidth, _ = entry.MinimumSize()
}
x += entryWidth
entryBounds := entry.Bounds()
if entryBounds.Dy() != height || entryBounds.Dx() != entryWidth {
entry.Resize(entryWidth, height)
}
dot.X += entryWidth
entry.Bounds.Max = entry.Bounds.Min.Add(image.Pt(entryWidth, bounds.Dy()))
entries[index] = entry
}
}

View File

@ -17,22 +17,19 @@ type Vertical struct {
}
// Arrange arranges a list of entries vertically.
func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) {
if layout.Pad {
width -= theme.Margin() * 2
height -= theme.Margin() * 2
}
freeSpace := height
expandingElements := 0
func (layout Vertical) Arrange (entries []tomo.LayoutEntry, bounds image.Rectangle) {
if layout.Pad { bounds = bounds.Inset(theme.Margin()) }
// count the number of expanding elements and the amount of free space
// for them to collectively occupy, while gathering minimum heights.
freeSpace := bounds.Dy()
minimumHeights := make([]int, len(entries))
expandingElements := 0
for index, entry := range entries {
var entryMinHeight int
if child, flexible := entry.Element.(tomo.Flexible); flexible {
entryMinHeight = child.FlexibleHeightFor(width)
entryMinHeight = child.FlexibleHeightFor(bounds.Dx())
} else {
_, entryMinHeight = entry.MinimumSize()
}
@ -47,33 +44,28 @@ func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) {
freeSpace -= theme.Margin()
}
}
expandingElementHeight := 0
if expandingElements > 0 {
expandingElementHeight = freeSpace / expandingElements
}
x, y := 0, 0
if layout.Pad {
x += theme.Margin()
y += theme.Margin()
}
// set the size and position of each element
dot := bounds.Min
for index, entry := range entries {
if index > 0 && layout.Gap { y += theme.Margin() }
if index > 0 && layout.Gap { dot.Y += theme.Margin() }
entries[index].Position = image.Pt(x, y)
entry.Bounds.Min = dot
entryHeight := 0
if entry.Expand {
entryHeight = expandingElementHeight
} else {
entryHeight = minimumHeights[index]
}
y += entryHeight
entryBounds := entry.Bounds()
if entryBounds.Dx() != width || entryBounds.Dy() != entryHeight {
entry.Resize(width, entryHeight)
}
dot.Y += entryHeight
entryBounds := entry.Bounds
entry.Bounds.Max = entryBounds.Min.Add(image.Pt(bounds.Dx(), entryHeight))
entries[index] = entry
}
}