Initial implementation of System

This commit is contained in:
Sasha Koshka 2024-06-02 13:23:03 -04:00
parent ccf51dfb0f
commit 0a58228773
9 changed files with 1756 additions and 15 deletions

View File

@ -1,7 +1,535 @@
package system package system
import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util"
func (this *System) NewBox () tomo.Box { type textureMode int; const (
textureModeTile textureMode = iota
textureModeCenter
)
type box struct {
system *System
parent parent
outer anyBox
bounds image.Rectangle
minSize image.Point
userMinSize image.Point
innerClippingBounds image.Rectangle
minSizeQueued bool
focusQueued *bool
padding tomo.Inset
border []tomo.Border
color color.Color
texture canvas.Texture
textureMode textureMode
dndData data.Data
dndAccept []data.Mime
focused bool
focusable bool
canvas util.Memo[canvas.Canvas]
drawer canvas.Drawer
on struct {
focusEnter event.FuncBroadcaster
focusLeave event.FuncBroadcaster
dndEnter event.FuncBroadcaster
dndLeave event.FuncBroadcaster
dndDrop event.Broadcaster[func (data.Data)]
mouseEnter event.FuncBroadcaster
mouseLeave event.FuncBroadcaster
mouseMove event.FuncBroadcaster
mouseDown event.Broadcaster[func (input.Button)]
mouseUp event.Broadcaster[func (input.Button)]
scroll event.Broadcaster[func (float64, float64)]
keyDown event.Broadcaster[func (input.Key, bool)]
keyUp event.Broadcaster[func (input.Key, bool)]
}
}
func (this *System) newBox (outer anyBox) *box {
box := &box {
system: this,
color: color.Transparent,
outer: outer,
drawer: outer,
}
box.canvas = util.NewMemo (func () canvas.Canvas {
if box.parent == nil { return nil }
parentCanvas := box.parent.getCanvas()
if parentCanvas == nil { return nil }
return parentCanvas.SubCanvas(box.bounds)
})
if outer == nil {
box.drawer = box
box.outer = box
}
box.invalidateMinimum()
return box
} }
func (this *System) NewBox () tomo.Box {
return this.newBox(nil)
}
func (this *box) GetBox () tomo.Box {
return this.outer
}
func (this *box) Window () tomo.Window {
hierarchy := this.getHierarchy()
if hierarchy == nil { return nil }
return hierarchy.getWindow()
}
func (this *box) Bounds () image.Rectangle {
return this.bounds
}
func (this *box) InnerBounds () image.Rectangle {
return this.padding.Apply(this.innerClippingBounds)
}
func (this *box) MinimumSize () image.Point {
return this.minSize
}
func (this *box) borderSum () tomo.Inset {
sum := tomo.Inset { }
for _, border := range this.border {
sum[0] += border.Width[0]
sum[1] += border.Width[1]
sum[2] += border.Width[2]
sum[3] += border.Width[3]
}
return sum
}
func (this *box) SetBounds (bounds image.Rectangle) {
if this.bounds == bounds { return }
this.bounds = bounds
this.invalidateLayout()
}
func (this *box) SetColor (c color.Color) {
if c == nil { c = color.Transparent }
if this.color == c { return }
this.color = c
this.invalidateDraw()
}
func (this *box) SetTextureTile (texture canvas.Texture) {
if this.texture == texture && this.textureMode == textureModeTile { return }
this.textureMode = textureModeTile
this.texture = texture
this.invalidateDraw()
}
func (this *box) SetTextureCenter (texture canvas.Texture) {
if this.texture == texture && this.textureMode == textureModeCenter { return }
this.texture = texture
this.textureMode = textureModeCenter
this.invalidateDraw()
}
func (this *box) SetBorder (borders ...tomo.Border) {
previousBorderSum := this.borderSum()
previousBorders := this.border
this.border = borders
// only invalidate the layout if the border is sized differently
if this.borderSum() != previousBorderSum {
this.invalidateLayout()
this.invalidateMinimum()
return
}
// if the border takes up the same amount of space, only invalidate the
// drawing if it looks different
for index, newBorder := range this.border {
different :=
index >= len(previousBorders) ||
newBorder != previousBorders[index]
if different {
this.invalidateDraw()
return
}
}
}
func (this *box) SetMinimumSize (size image.Point) {
if this.userMinSize == size { return }
this.userMinSize = size
this.invalidateMinimum()
}
func (this *box) SetPadding (padding tomo.Inset) {
if this.padding == padding { return }
this.padding = padding
this.invalidateLayout()
this.invalidateMinimum()
}
func (this *box) SetDNDData (dat data.Data) {
this.dndData = dat
}
func (this *box) SetDNDAccept (types ...data.Mime) {
this.dndAccept = types
}
func (this *box) SetFocused (focused bool) {
hierarchy := this.parent.getHierarchy()
if hierarchy == nil {
focusedCopy := focused
this.focusQueued = &focusedCopy
return
}
if !this.focusable { return }
if this.Focused () && !focused {
hierarchy.focus(nil)
} else if !this.Focused() && focused {
hierarchy.focus(this.outer)
}
}
func (this *box) SetFocusable (focusable bool) {
if this.focusable == focusable { return }
this.focusable = focusable
if !focusable {
this.SetFocused(false)
}
}
func (this *box) Focused () bool {
hierarchy := this.parent.getHierarchy()
if hierarchy == nil { return false }
return hierarchy.isFocused(this)
}
func (this *box) Modifiers () input.Modifiers {
hierarchy := this.getHierarchy()
if hierarchy == nil { return input.Modifiers { } }
return hierarchy.getModifiers()
}
func (this *box) MousePosition () image.Point {
hierarchy := this.getHierarchy()
if hierarchy == nil { return image.Point { } }
return hierarchy.getMousePosition()
}
// ----- event handler setters ---------------------------------------------- //
func (this *box) OnFocusEnter (callback func()) event.Cookie {
return this.on.focusEnter.Connect(callback)
}
func (this *box) OnFocusLeave (callback func()) event.Cookie {
return this.on.focusLeave.Connect(callback)
}
func (this *box) OnDNDEnter (callback func()) event.Cookie {
return this.on.dndEnter.Connect(callback)
}
func (this *box) OnDNDLeave (callback func()) event.Cookie {
return this.on.dndLeave.Connect(callback)
}
func (this *box) OnDNDDrop (callback func(data.Data)) event.Cookie {
return this.on.dndDrop.Connect(callback)
}
func (this *box) OnMouseEnter (callback func()) event.Cookie {
return this.on.mouseEnter.Connect(callback)
}
func (this *box) OnMouseLeave (callback func()) event.Cookie {
return this.on.mouseLeave.Connect(callback)
}
func (this *box) OnMouseMove (callback func()) event.Cookie {
return this.on.mouseMove.Connect(callback)
}
func (this *box) OnMouseDown (callback func(input.Button)) event.Cookie {
return this.on.mouseDown.Connect(callback)
}
func (this *box) OnMouseUp (callback func(input.Button)) event.Cookie {
return this.on.mouseUp.Connect(callback)
}
func (this *box) OnScroll (callback func(deltaX, deltaY float64)) event.Cookie {
return this.on.scroll.Connect(callback)
}
func (this *box) OnKeyDown (callback func(key input.Key, numberPad bool)) event.Cookie {
return this.on.keyDown.Connect(callback)
}
func (this *box) OnKeyUp (callback func(key input.Key, numberPad bool)) event.Cookie {
return this.on.keyUp.Connect(callback)
}
func (this *box) handleFocusEnter () {
this.on.focusEnter.Broadcast()
}
func (this *box) handleFocusLeave () {
this.on.focusLeave.Broadcast()
}
func (this *box) handleMouseEnter () {
this.on.mouseEnter.Broadcast()
}
func (this *box) handleMouseLeave () {
this.on.mouseLeave.Broadcast()
}
func (this *box) handleMouseMove () {
this.on.mouseMove.Broadcast()
}
func (this *box) handleMouseDown (button input.Button) {
if this.focusable {
this.SetFocused(true)
} else {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
hierarchy.focus(nil)
}
for _, listener := range this.on.mouseDown.Listeners() {
listener(button)
}
}
func (this *box) handleMouseUp (button input.Button) {
for _, listener := range this.on.mouseUp.Listeners() {
listener(button)
}
}
func (this *box) handleScroll (x, y float64) {
for _, listener := range this.on.scroll.Listeners() {
listener(x, y)
}
}
func (this *box) handleKeyDown (key input.Key, numberPad bool) {
for _, listener := range this.on.keyDown.Listeners() {
listener(key, numberPad)
}
}
func (this *box) handleKeyUp (key input.Key, numberPad bool) {
for _, listener := range this.on.keyUp.Listeners() {
listener(key, numberPad)
}
}
// -------------------------------------------------------------------------- //
func (this *box) Draw (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
bounds := this.Bounds()
// background
pen.Fill(this.color)
if this.textureMode == textureModeTile {
pen.Texture(this.texture)
}
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Rectangle(bounds)
// centered texture
if this.textureMode == textureModeCenter && this.texture != nil {
textureBounds := this.texture.Bounds()
textureOrigin :=
bounds.Min.
Add(image.Pt (
bounds.Dx() / 2,
bounds.Dy() / 2)).
Sub(image.Pt (
textureBounds.Dx() / 2,
textureBounds.Dy() / 2))
pen.Fill(color.Transparent)
pen.Texture(this.texture)
pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin))
}
}
func (this *box) drawBorders (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
bounds := this.bounds
rectangle := func (x0, y0, x1, y1 int, c color.Color) {
area := image.Rect(x0, y0, x1, y1)
if util.Transparent(c) && this.parent != nil {
this.parent.drawBackgroundPart(can.SubCanvas(area))
}
pen.Fill(c)
pen.Rectangle(area)
}
for _, border := range this.border {
rectangle (
bounds.Min.X,
bounds.Min.Y,
bounds.Max.X,
bounds.Min.Y + border.Width[tomo.SideTop],
border.Color[tomo.SideTop])
rectangle (
bounds.Min.X,
bounds.Max.Y - border.Width[tomo.SideBottom],
bounds.Max.X,
bounds.Max.Y,
border.Color[tomo.SideBottom])
rectangle (
bounds.Min.X,
bounds.Min.Y + border.Width[tomo.SideTop],
bounds.Min.X + border.Width[tomo.SideLeft],
bounds.Max.Y - border.Width[tomo.SideBottom],
border.Color[tomo.SideLeft])
rectangle (
bounds.Max.X - border.Width[tomo.SideRight],
bounds.Min.Y + border.Width[tomo.SideTop],
bounds.Max.X,
bounds.Max.Y - border.Width[tomo.SideBottom],
border.Color[tomo.SideRight])
bounds = border.Width.Apply(bounds)
}
}
func (this *box) contentMinimum () image.Point {
var minimum image.Point
minimum.X += this.padding.Horizontal()
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
return minimum
}
func (this *box) doMinimumSize () {
this.minSize = this.outer.contentMinimum()
if this.minSize.X < this.userMinSize.X {
this.minSize.X = this.userMinSize.X
}
if this.minSize.Y < this.userMinSize.Y {
this.minSize.Y = this.userMinSize.Y
}
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
}
// var drawcnt int
func (this *box) doDraw () {
// println("DRAW", drawcnt)
// drawcnt ++
canvas := this.canvas.Value()
if canvas == nil { return }
if this.drawer != nil {
this.drawBorders(canvas)
this.drawer.Draw(canvas.SubCanvas(this.innerClippingBounds))
}
}
// var laycnt int
func (this *box) doLayout () {
// println("LAYOUT", laycnt)
// laycnt ++
this.innerClippingBounds = this.borderSum().Apply(this.bounds)
this.loseCanvas()
}
func (this *box) setParent (parent parent) {
if this.parent != parent && this.Focused() {
this.SetFocused(false)
}
this.parent = parent
}
func (this *box) getParent () parent {
return this.parent
}
func (this *box) flushActionQueue () {
if this.getHierarchy() == nil { return }
if this.minSizeQueued {
this.invalidateMinimum()
}
if this.focusQueued != nil {
this.SetFocused(*this.focusQueued)
}
}
func (this *box) recursiveRedo () {
this.doLayout()
this.doDraw()
}
func (this *box) loseCanvas () {
this.canvas.InvalidateTo(nil)
}
func (this *box) invalidateLayout () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
hierarchy.invalidateLayout(this.outer)
}
func (this *box) invalidateDraw () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
hierarchy.invalidateDraw(this.outer)
}
func (this *box) invalidateMinimum () {
hierarchy := this.getHierarchy()
if hierarchy == nil {
this.minSizeQueued = true
} else {
hierarchy.invalidateMinimum(this.outer)
}
}
func (this *box) canBeFocused () bool {
return this.focusable
}
func (this *box) boxUnder (point image.Point, category eventCategory) anyBox {
if point.In(this.bounds) {
return this.outer
} else {
return nil
}
}
func (this *box) propagate (callback func (anyBox) bool) bool {
return callback(this.outer)
}
func (this *box) propagateAlt (callback func (anyBox) bool) bool {
return callback(this.outer)
}
func (this *box) transparent () bool {
// TODO uncomment once we have
// a way to detect texture transparency
return util.Transparent(this.color) /*&&
(this.texture == nil || !this.texture.Opaque())*/
}
func (this *box) getWindow () tomo.Window {
hierarchy := this.getHierarchy()
if hierarchy == nil { return nil }
return hierarchy.getWindow()
}
func (this *box) getHierarchy () *Hierarchy {
if this.parent == nil { return nil }
return this.parent.getHierarchy()
}

View File

@ -1,7 +1,35 @@
package system package system
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/canvas"
type canvasBox struct {
*box
userDrawer canvas.Drawer
}
func (this *System) NewCanvasBox () tomo.CanvasBox { func (this *System) NewCanvasBox () tomo.CanvasBox {
box := &canvasBox { }
box.box = this.newBox(box)
box.drawer = box
return box
}
func (this *canvasBox) Box () tomo.Box {
return this
}
func (this *canvasBox) SetDrawer (drawer canvas.Drawer) {
this.userDrawer = drawer
this.invalidateDraw()
}
func (this *canvasBox) Invalidate () {
this.invalidateDraw()
}
func (this *canvasBox) Draw (can canvas.Canvas) {
this.box.Draw(can)
this.userDrawer.Draw (
can.SubCanvas(this.padding.Apply(this.innerClippingBounds)))
} }

View File

@ -1,7 +1,373 @@
package system package system
import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util"
type containerBox struct {
*box
hOverflow, vOverflow bool
hAlign, vAlign tomo.Align
contentBounds image.Rectangle
scroll image.Point
capture [4]bool
gap image.Point
children []tomo.Box
layout tomo.Layout
on struct {
contentBoundsChange event.FuncBroadcaster
}
}
func (this *System) NewContainerBox () tomo.ContainerBox { func (this *System) NewContainerBox () tomo.ContainerBox {
box := &containerBox { }
box.box = this.newBox(box)
return box
}
func (this *containerBox) SetColor (c color.Color) {
if this.color == c { return }
this.box.SetColor(c)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetTextureTile (texture canvas.Texture) {
if this.texture == texture { return }
this.box.SetTextureTile(texture)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetTextureCenter (texture canvas.Texture) {
if this.texture == texture { return }
this.box.SetTextureTile(texture)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetOverflow (horizontal, vertical bool) {
if this.hOverflow == horizontal && this.vOverflow == vertical { return }
this.hOverflow = horizontal
this.vOverflow = vertical
this.invalidateLayout()
}
func (this *containerBox) SetAlign (x, y tomo.Align) {
if this.hAlign == x && this.vAlign == y { return }
this.hAlign = x
this.vAlign = y
this.invalidateLayout()
}
func (this *containerBox) ContentBounds () image.Rectangle {
return this.contentBounds
}
func (this *containerBox) ScrollTo (point image.Point) {
if this.scroll == point { return }
this.scroll = point
this.invalidateLayout()
}
func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie {
return this.on.contentBoundsChange.Connect(callback)
}
func (this *containerBox) CaptureDND (capture bool) {
this.capture[eventCategoryDND] = capture
}
func (this *containerBox) CaptureMouse (capture bool) {
this.capture[eventCategoryMouse] = capture
}
func (this *containerBox) CaptureScroll (capture bool) {
this.capture[eventCategoryScroll] = capture
}
func (this *containerBox) CaptureKeyboard (capture bool) {
this.capture[eventCategoryKeyboard] = capture
}
func (this *containerBox) SetGap (gap image.Point) {
if this.gap == gap { return }
this.gap = gap
this.invalidateLayout()
this.invalidateMinimum()
}
func (this *containerBox) Add (child tomo.Object) {
box := assertAnyBox(child.GetBox())
if util.IndexOf(this.children, tomo.Box(box)) > -1 { return }
box.setParent(this)
box.flushActionQueue()
this.children = append(this.children, box)
this.invalidateLayout()
this.invalidateMinimum()
}
func (this *containerBox) Remove (child tomo.Object) {
box := assertAnyBox(child.GetBox())
index := util.IndexOf(this.children, tomo.Box(box))
if index < 0 { return }
box.setParent(nil)
this.children = util.Remove(this.children, index)
this.invalidateLayout()
this.invalidateMinimum()
}
func (this *containerBox) Insert (child, before tomo.Object) {
box := assertAnyBox(child.GetBox())
if util.IndexOf(this.children, tomo.Box(box)) > -1 { return }
beforeBox := assertAnyBox(before.GetBox())
index := util.IndexOf(this.children, tomo.Box(beforeBox))
if index < 0 {
this.children = append(this.children, tomo.Box(box))
} else {
this.children = util.Insert(this.children, index, tomo.Box(box))
}
box.setParent(this)
this.invalidateLayout()
this.invalidateMinimum()
}
func (this *containerBox) Clear () {
for _, box := range this.children {
box.(anyBox).setParent(nil)
}
this.children = nil
this.invalidateLayout()
this.invalidateMinimum()
}
func (this *containerBox) Length () int {
return len(this.children)
}
func (this *containerBox) At (index int) tomo.Object {
if index < 0 || index >= len(this.children) {
return nil
}
return this.children[index]
}
func (this *containerBox) SetLayout (layout tomo.Layout) {
this.layout = layout
this.invalidateLayout()
this.invalidateMinimum()
}
func (this *containerBox) Draw (can canvas.Canvas) {
if can == nil { return }
rocks := make([]image.Rectangle, len(this.children))
for index, box := range this.children {
rocks[index] = box.Bounds()
}
for _, tile := range canvas.Shatter(this.bounds, rocks...) {
clipped := can.SubCanvas(tile)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(clipped)
}
if clipped == nil { continue }
pen := clipped.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
pen.Rectangle(this.innerClippingBounds)
}
}
func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Rectangle(this.innerClippingBounds)
}
func (this *containerBox) invalidateTransparentChildren () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
for _, box := range this.children {
box := assertAnyBox(box)
if box.transparent() {
hierarchy.invalidateDraw(box)
}
}
}
func (this *containerBox) flushActionQueue () {
for _, box := range this.children {
box.(anyBox).flushActionQueue()
}
this.box.flushActionQueue()
}
func (this *containerBox) getHierarchy () *Hierarchy {
if this.parent == nil { return nil }
return this.parent.getHierarchy()
}
func (this *containerBox) getCanvas () canvas.Canvas {
return this.canvas.Value()
}
func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
this.invalidateMinimum()
size := child.MinimumSize()
bounds := child.Bounds()
if bounds.Dx() < size.X || bounds.Dy() < size.Y {
this.invalidateLayout()
}
}
func (this *containerBox) layoutHints () tomo.LayoutHints {
return tomo.LayoutHints {
OverflowX: this.hOverflow,
OverflowY: this.vOverflow,
AlignX: this.hAlign,
AlignY: this.vAlign,
Gap: this.gap,
}
}
func (this *containerBox) contentMinimum () image.Point {
minimum := this.box.contentMinimum()
if this.layout != nil {
layoutMinimum := this.layout.MinimumSize (
this.layoutHints(),
this.children)
if this.hOverflow { layoutMinimum.X = 0 }
if this.vOverflow { layoutMinimum.Y = 0 }
minimum = minimum.Add(layoutMinimum)
}
return minimum
}
func (this *containerBox) doLayout () {
this.box.doLayout()
previousContentBounds := this.contentBounds
// by default, use innerBounds (translated to 0, 0) for contentBounds.
// if a direction overflows, use the layout's minimum size for it.
var minimum image.Point
if this.layout != nil {
minimum = this.layout.MinimumSize (
this.layoutHints(),
this.children)
}
innerBounds := this.InnerBounds()
this.contentBounds = innerBounds.Sub(innerBounds.Min)
if this.hOverflow { this.contentBounds.Max.X = this.contentBounds.Min.X + minimum.X }
if this.vOverflow { this.contentBounds.Max.Y = this.contentBounds.Min.Y + minimum.Y }
// arrange children
if this.layout != nil {
layoutHints := this.layoutHints()
layoutHints.Bounds = this.contentBounds
this.layout.Arrange(layoutHints, this.children)
}
// build an accurate contentBounds by unioning the bounds of all child
// boxes
this.contentBounds = image.Rectangle { }
for _, box := range this.children {
bounds := box.Bounds()
this.contentBounds = this.contentBounds.Union(bounds)
}
// constrain the scroll
this.constrainScroll()
// offset children and contentBounds by scroll
for _, box := range this.children {
box.SetBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
}
this.contentBounds = this.contentBounds.Add(this.scroll)
if previousContentBounds != this.contentBounds {
this.on.contentBoundsChange.Broadcast()
}
}
func (this *containerBox) constrainScroll () {
innerBounds := this.InnerBounds()
width := this.contentBounds.Dx()
height := this.contentBounds.Dy()
// X
if width <= innerBounds.Dx() {
this.scroll.X = 0
} else if this.scroll.X > 0 {
this.scroll.X = 0
} else if this.scroll.X < innerBounds.Dx() - width {
this.scroll.X = innerBounds.Dx() - width
}
// Y
if height <= innerBounds.Dy() {
this.scroll.Y = 0
} else if this.scroll.Y > 0 {
this.scroll.Y = 0
} else if this.scroll.Y < innerBounds.Dy() - height {
this.scroll.Y = innerBounds.Dy() - height
}
}
func (this *containerBox) recursiveRedo () {
this.doLayout()
this.doDraw()
for _, child := range this.children {
child.(anyBox).recursiveRedo()
}
}
func (this *containerBox) boxUnder (point image.Point, category eventCategory) anyBox {
if !point.In(this.bounds) { return nil }
if !this.capture[category] {
for _, box := range this.children {
candidate := box.(anyBox).boxUnder(point, category)
if candidate != nil { return candidate }
}
}
return this
}
func (this *containerBox) propagate (callback func (anyBox) bool) bool {
for _, box := range this.children {
box := box.(anyBox)
if !box.propagate(callback) { return false }
}
return callback(this)
}
func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool {
if !callback(this) { return false}
for _, box := range this.children {
box := box.(anyBox)
if !box.propagateAlt(callback) { return false }
}
return true
}
func (this *containerBox) captures (category eventCategory) bool {
return this.capture[category]
} }

97
internal/system/event.go Normal file
View File

@ -0,0 +1,97 @@
package system
import "image"
import "git.tebibyte.media/tomo/tomo/input"
// HandleFocusChange sets whether or not the window containing this Hierarchy
// has input focus.
func (this *Hierarchy) HandleFocusChange (focused bool) {
if this.windowFocused == focused { return }
this.windowFocused = focused
}
// HandleModifiers sets the modifier keys that are currently being pressed.
func (this *Hierarchy) HandleModifiers (modifiers input.Modifiers) {
if this.modifiers == modifiers { return }
this.modifiers = modifiers
}
// HandleKeyDown sends a key down event to the currently focused Box. If the
// event which triggers this comes with modifier key information,
// HandleModifiers must be called *before* HandleKeyDown.
func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) {
if key == input.KeyTab && this.modifiers.Alt {
if this.modifiers.Shift {
this.focusPrevious()
} else {
this.focusNext()
}
} else if this.focused != nil {
this.keyboardTarget().handleKeyDown(key, numberPad)
}
}
// HandleKeyUp sends a key up event to the currently focused Box. If the event
// which triggers this comes with modifier key information, HandleModifiers must
// be called *before* HandleKeyUp.
func (this *Hierarchy) HandleKeyUp (key input.Key, numberPad bool) {
if this.focused == nil {
this.keyboardTarget().handleKeyUp(key, numberPad)
}
}
// HandleMouseDown sends a mouse down event to the Box positioned underneath the
// mouse cursor and marks it as being "dragged" by that mouse button. If the
// event which triggers this comes with mouse position information,
// HandleMouseMove must be called *before* HandleMouseDown.
func (this *Hierarchy) HandleMouseDown (button input.Button) {
underneath := this.boxUnder(this.mousePosition, eventCategoryMouse)
this.drags[button] = underneath
if underneath != nil {
underneath.handleMouseDown(button)
}
}
// HandleMouseUp sends a mouse up event to the Box currently being "dragged" by
// the specified mouse button, and marks it as being "not dragged" by that mouse
// button. If the event which triggers this comes with mouse position
// information, HandleMouseMove must be caleld *before* HandleMouseUp
func (this *Hierarchy) HandleMouseUp (button input.Button) {
dragging := this.drags[button]
this.drags[button] = nil
if dragging != nil {
dragging.handleMouseUp(button)
}
}
// HandleMouseMove sends a mouse move event to any Boxes currently being
// "dragged" by a mouse button. If none are, it sends the event to the Box which
// is underneath the mouse pointer.
func (this *Hierarchy) HandleMouseMove (position image.Point) {
if this.mousePosition == position { return }
this.mousePosition = position
handled := false
for _, child := range this.drags {
if child == nil { continue }
child.handleMouseMove()
handled = true
}
underneath := this.boxUnder(position, eventCategoryMouse)
if underneath != nil {
this.hover(underneath)
if !handled {
underneath.handleMouseMove()
}
}
}
// HandleScroll sends a scroll event to the Box currently underneath the mouse
// cursor.
func (this *Hierarchy) HandleScroll (x, y float64) {
underneath := this.boxUnder(this.mousePosition, eventCategoryScroll)
if underneath != nil {
underneath.handleScroll(x, y)
}
}

View File

@ -1,15 +1,283 @@
package system package system
import "image"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util"
// Hierarchy is coupled to a tomo.Window implementation, and manages a tree of
// Boxes.
type Hierarchy struct { type Hierarchy struct {
link WindowLink link WindowLink
system *System
canvas canvas.Canvas
root anyBox
focused anyBox
hovered anyBox
windowFocused bool
modifiers input.Modifiers
mousePosition image.Point
drags [10]anyBox
minimumSize image.Point
needMinimum util.Set[anyBox]
needLayout util.Set[anyBox]
needDraw util.Set[anyBox]
needRedo bool
minimumClean bool
} }
// WindowLink allows the Hierarchy to call up into the backend implementation
// which contains it.
type WindowLink interface { type WindowLink interface {
GetWindow () tomo.Window GetWindow () tomo.Window
PushRegion (image.Rectangle)
PushAll ()
NotifyMinimumSizeChange ()
} }
// NewHierarchy creates a new Hierarchy.
func (this *System) NewHierarchy (link WindowLink) *Hierarchy { func (this *System) NewHierarchy (link WindowLink) *Hierarchy {
hierarchy := &Hierarchy {
system: this,
link: link,
}
return hierarchy
}
// SetRoot sets the root Box of the hierarchy.
func (this *Hierarchy) SetRoot (root tomo.Box) {
if this.root != nil {
this.root.setParent(nil)
}
if root == nil {
this.root = nil
} else {
box := assertAnyBox(root.GetBox())
box.setParent(this)
box.flushActionQueue()
this.invalidateLayout(box)
this.root = box
}
this.minimumClean = false
}
// SetCanvas sets the held canvas of the Hierarchy that all boxes within it will
// draw to. The Hierarchy will take on the canvas's bounds to lay itself out.
func (this *Hierarchy) SetCanvas (can canvas.Canvas) {
this.canvas = can
if this.root != nil { this.root.loseCanvas() }
this.needRedo = true
}
// MinimumSize returns the minimum size of the Hierarchy.
func (this *Hierarchy) MinimumSize () image.Point {
return this.minimumSize
}
func (this *Hierarchy) getHierarchy () *Hierarchy {
return this
}
func (this *Hierarchy) getWindow () tomo.Window {
return this.link.GetWindow()
}
func (this *Hierarchy) getCanvas () canvas.Canvas {
return this.canvas
}
func (this *Hierarchy) getModifiers () input.Modifiers {
return this.modifiers
}
func (this *Hierarchy) getMousePosition () image.Point {
return this.mousePosition
}
func (this *Hierarchy) notifyMinimumSizeChange (anyBox) {
this.minimumClean = false
}
func (this *Hierarchy) invalidateMinimum (box anyBox) {
this.needMinimum.Add(box)
}
func (this *Hierarchy) invalidateDraw (box anyBox) {
this.needDraw.Add(box)
}
func (this *Hierarchy) invalidateLayout (box anyBox) {
this.needLayout.Add(box)
this.invalidateDraw(box)
}
func (this *Hierarchy) focus (box anyBox) {
if this.focused == box { return }
previous := this.focused
this.focused = box
if previous != nil {
previous.handleFocusLeave()
}
if box != nil && box.canBeFocused() {
box.handleFocusEnter()
}
}
func (this *Hierarchy) isFocused (box anyBox) bool {
return this.focused == box
}
func (this *Hierarchy) hover (box anyBox) {
if this.hovered == box { return }
previous := this.hovered
this.hovered = box
if previous != nil {
previous.handleMouseLeave()
}
if box != nil {
box.handleMouseEnter()
}
}
func (this *Hierarchy) anyFocused () bool {
return this.focused != nil
}
func (this *Hierarchy) boxUnder (point image.Point, category eventCategory) anyBox {
if this.root == nil { return nil }
return this.root.boxUnder(point, category)
}
func (this *Hierarchy) captures (eventCategory) bool {
return false
}
func (this *Hierarchy) keyboardTarget () anyBox {
focused := this.focused
if focused == nil { return nil }
parent := focused.getParent()
for {
parentBox, ok := parent.(anyBox)
if !ok { break }
if parent.captures(eventCategoryKeyboard) {
return parentBox
}
parent = parentBox.getParent()
}
return focused
}
func (this *Hierarchy) focusNext () {
found := !this.anyFocused()
focused := false
this.propagateAlt (func (box anyBox) bool {
if found {
// looking for the next box to select
if box.canBeFocused() {
// found it
this.focus(box)
focused = true
return false
}
} else {
// looking for the current focused element
if box == this.focused {
// found it
found = true
}
}
return true
})
if !focused { this.focus(nil) }
}
func (this *Hierarchy) focusPrevious () {
var behind anyBox
this.propagate (func (box anyBox) bool {
if box == this.focused {
return false
}
if box.canBeFocused() { behind = box }
return true
})
this.focus(behind)
}
func (this *Hierarchy) propagate (callback func (box anyBox) bool) {
if this.root == nil { return }
this.root.propagate(callback)
}
func (this *Hierarchy) propagateAlt (callback func (box anyBox) bool) {
if this.root == nil { return }
this.root.propagateAlt(callback)
}
func (this *Hierarchy) afterEvent () {
if this.canvas == nil { return }
if this.needRedo {
// set child bounds
childBounds := this.canvas.Bounds()
childBounds = childBounds.Sub(childBounds.Min)
if this.root != nil {
this.root.SetBounds(childBounds)
}
// full relayout/redraw
if this.root != nil {
this.root.recursiveRedo()
}
this.link.PushAll()
this.needRedo = false
return
}
for len(this.needMinimum) > 0 {
this.needMinimum.Pop().doMinimumSize()
}
if !this.minimumClean {
this.doMinimumSize()
}
for len(this.needLayout) > 0 {
this.needLayout.Pop().doLayout()
}
var toPush image.Rectangle
for len(this.needDraw) > 0 {
box := this.needDraw.Pop()
box.doDraw()
toPush = toPush.Union(box.Bounds())
}
if !toPush.Empty() {
this.link.PushRegion(toPush)
}
}
func (this *Hierarchy) drawBackgroundPart (canvas.Canvas) {
// TODO
// no-op for now? maybe eventually windows will be able to have a
// background
// if so, windows should be transparent if the color has transparency
}
func (this *Hierarchy) doMinimumSize () {
this.minimumClean = true
this.minimumSize = image.Point { }
if this.root != nil {
this.minimumSize = this.root.MinimumSize()
}
this.link.NotifyMinimumSizeChange()
} }

View File

@ -0,0 +1,105 @@
package system
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/canvas"
// eventCategory lists kinds of Tomo events.
type eventCategory int; const (
eventCategoryDND eventCategory = iota
eventCategoryMouse
eventCategoryScroll
eventCategoryKeyboard
)
// parent is any hierarchical type which contains other boxes. This can be a
// Hierarchy, containerBox, etc.
type parent interface {
// hierarchy returns the hierarchy the parent is apart of.
getHierarchy () *Hierarchy
// canvas returns the canvas held by the parent.
getCanvas () canvas.Canvas
// notifyMinimumSizeChange informs the parent that the minimum size of
// one of its children has changed.
notifyMinimumSizeChange (anyBox)
// drawBackgroundPart draws a part of the parent's background to the
// given Canvas, filling the Canvas's entire bounds. The origin (0, 0)
// of the given Canvas is assumed to be the same as the parent's canvas.
drawBackgroundPart (canvas.Canvas)
// captures returns whether or not this parent captures the given event
// category.
captures (eventCategory) bool
}
// anyBox is any tomo.Box type that is implemented by this package.
type anyBox interface {
tomo.Box
canvas.Drawer
// setParent sets this anyBox's parent.
// getParent returns this anyBox's parent as set by setParent.
setParent (parent)
getParent () parent
// doDraw re-paints the anyBox onto its currently held Canvas non-recursively
// doLayout re-calculates the layout of the anyBox non-recursively
// doMinimumSize re-calculates the minimum size of the anyBox non-recursively
doDraw ()
doLayout ()
doMinimumSize ()
// flushActionQueue performs any queued actions, like invalidating the
// minimum size or grabbing input focus.
flushActionQueue ()
// recursiveRedo recursively recalculates the minimum size, layout, and
// re-paints this anyBox and all of its children.
recursiveRedo ()
// loseCanvas causes this anyBox and its children (if applicable) to
// lose their canvases and re-cut them as needed.
loseCanvas ()
// contentMinimum returns the minimum dimensions of this box's content
contentMinimum () image.Point
// canBeFocused returns whether or not this anyBox is capable of holding
// input focus.
canBeFocused () bool
// boxUnder returns the anyBox under the mouse pointer. It can be this
// anyBox, one of its children (if applicable). It must return nil if
// the mouse pointer is outside of this anyBox's bounds.
boxUnder (image.Point, eventCategory) anyBox
// transparent returns whether or not this anyBox contains transparent
// pixels or not, and thus needs its parent's backround to be painted
// underneath it.
transparent () bool
// propagate recursively calls a function on this anyBox, and all of its
// children (if applicable) The normal propagate behavior calls the
// callback on all children before calling it on this anyBox, and
// propagateAlt calls the callback on this anyBox before calling it on
// its children.
propagate (func (anyBox) bool) bool
propagateAlt (func (anyBox) bool) bool
handleFocusEnter ()
handleFocusLeave ()
// handleDndEnter ()
// handleDndLeave ()
// handleDndDrop (data.Data)
handleMouseEnter ()
handleMouseLeave ()
handleMouseMove ()
handleMouseDown (input.Button)
handleMouseUp (input.Button)
handleScroll (float64, float64)
handleKeyDown (input.Key, bool)
handleKeyUp (input.Key, bool)
}
func assertAnyBox (unknown tomo.Box) anyBox {
if box, ok := unknown.(anyBox); ok {
return box
} else {
panic("system: foregin box implementation, i did not make this!")
}
}

View File

@ -1,7 +1,9 @@
package system package system
import "errors"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
func (this *System) NewSurfaceBox () (tomo.SurfaceBox, error) { func (this *System) NewSurfaceBox () (tomo.SurfaceBox, error) {
// TODO
return nil, errors.New("system: not implemented yet")
} }

View File

@ -1,25 +1,30 @@
package system package system
import "io"
import "image" import "image"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
// System is coupled to a tomo.Backend implementation, and manages Hierarchies
// and Boxes.
type System struct { type System struct {
link BackendLink link BackendLink
} }
// BackendLink allows the System to call up into the backend implementation
// which contains it in order to do things such as create new textures.
type BackendLink interface { type BackendLink interface {
NewTexture (image.Image) canvas.TextureCloser NewTexture (image.Image) canvas.TextureCloser
NewCanvas (image.Rectangle) canvas.Canvas NewCanvas (image.Rectangle) canvas.Canvas
NewSurface (image.Rectangle) SurfaceLink NewSurface (image.Rectangle) SurfaceLink
} }
// SurfaceLink wraps a Surface created by the backend implementation, allowing
// the System a higher level of control over it.
type SurfaceLink interface { type SurfaceLink interface {
io.Closer
GetSurface () any GetSurface () any
SetSize (image.Rectangle) SetSize (image.Rectangle)
} }
// New creates a new System.
func New (link BackendLink) *System { func New (link BackendLink) *System {
return &System { return &System {
link: link, link: link,

View File

@ -1,7 +1,349 @@
package system package system
import "image"
import "image/color"
import "golang.org/x/image/font"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/tomo/typeset"
import "git.tebibyte.media/tomo/tomo/text"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
type textBox struct {
*box
hOverflow, vOverflow bool
contentBounds image.Rectangle
scroll image.Point
text string
textColor color.Color
face font.Face
wrap bool
hAlign tomo.Align
vAlign tomo.Align
selectable bool
selecting bool
selectStart int
dot text.Dot
dotColor color.Color
drawer typeset.Drawer
on struct {
contentBoundsChange event.FuncBroadcaster
dotChange event.FuncBroadcaster
}
}
func (this *System) NewTextBox () tomo.TextBox { func (this *System) NewTextBox () tomo.TextBox {
box := &textBox {
textColor: color.Black,
dotColor: color.RGBA { B: 255, G: 255, A: 255 },
}
box.box = this.newBox(box)
return box
}
func (this *textBox) SetOverflow (horizontal, vertical bool) {
if this.hOverflow == horizontal && this.vOverflow == vertical { return }
this.hOverflow = horizontal
this.vOverflow = vertical
this.invalidateLayout()
}
func (this *textBox) ContentBounds () image.Rectangle {
return this.contentBounds
}
func (this *textBox) ScrollTo (point image.Point) {
if this.scroll == point { return }
this.scroll = point
this.invalidateLayout()
}
func (this *textBox) OnContentBoundsChange (callback func()) event.Cookie {
return this.on.contentBoundsChange.Connect(callback)
}
func (this *textBox) SetText (text string) {
if this.text == text { return }
this.text = text
this.drawer.SetText([]rune(text))
this.invalidateMinimum()
this.invalidateLayout()
}
func (this *textBox) SetTextColor (c color.Color) {
if this.textColor == c { return }
this.textColor = c
this.invalidateDraw()
}
func (this *textBox) SetFace (face font.Face) {
if this.face == face { return }
this.face = face
this.drawer.SetFace(face)
this.invalidateMinimum()
this.invalidateLayout()
}
func (this *textBox) SetWrap (wrap bool) {
if this.wrap == wrap { return }
this.drawer.SetWrap(wrap)
this.invalidateMinimum()
this.invalidateLayout()
}
func (this *textBox) SetSelectable (selectable bool) {
if this.selectable == selectable { return }
this.selectable = selectable
}
func (this *textBox) SetDotColor (c color.Color) {
if this.dotColor == c { return }
this.dotColor = c
if !this.dot.Empty() {
this.invalidateDraw()
}
}
func (this *textBox) Select (dot text.Dot) {
if !this.selectable { return }
if this.dot == dot { return }
this.SetFocused(true)
this.dot = dot
this.scrollToDot()
this.on.dotChange.Broadcast()
this.invalidateDraw()
}
func (this *textBox) Dot () text.Dot {
return this.dot
}
func (this *textBox) OnDotChange (callback func ()) event.Cookie {
return this.on.dotChange.Connect(callback)
}
func (this *textBox) SetAlign (x, y tomo.Align) {
if this.hAlign == x && this.vAlign == y { return }
this.hAlign = x
this.vAlign = y
this.drawer.SetAlign(typeset.Align(x), typeset.Align(y))
this.invalidateDraw()
}
func (this *textBox) Draw (can canvas.Canvas) {
if can == nil { return }
this.drawBorders(can)
pen := can.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Rectangle(can.Bounds())
if this.selectable && this.Focused() {
this.drawDot(can)
}
if this.face == nil { return }
this.drawer.Draw(can, this.textColor, this.textOffset())
}
func roundPt (point fixed.Point26_6) image.Point {
return image.Pt(point.X.Round(), point.Y.Round())
}
func fixPt (point image.Point) fixed.Point26_6 {
return fixed.P(point.X, point.Y)
}
func (this *textBox) drawDot (can canvas.Canvas) {
if this.face == nil { return }
pen := can.Pen()
pen.Fill(color.Transparent)
pen.Stroke(this.textColor)
bounds := this.InnerBounds()
metrics := this.face.Metrics()
dot := this.dot.Canon()
start := this.drawer.PositionAt(dot.Start).Add(fixPt(this.textOffset()))
end := this.drawer.PositionAt(dot.End ).Add(fixPt(this.textOffset()))
height := this.drawer.LineHeight().Round()
ascent := fixed.Point26_6 { Y: metrics.Descent }
descent := fixed.Point26_6 { Y: metrics.Ascent }
switch {
case dot.Empty():
pen.StrokeWeight(1)
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
case start.Y == end.Y:
pen.Fill(this.dotColor)
pen.StrokeWeight(0)
pen.Rectangle(image.Rectangle {
Min: roundPt(start.Add(ascent)),
Max: roundPt(end.Sub(descent)),
})
default:
pen.Fill(this.dotColor)
pen.StrokeWeight(0)
rect := image.Rectangle {
Min: roundPt(start.Add(ascent)),
Max: roundPt(start.Sub(descent)),
}
rect.Max.X = bounds.Max.X
pen.Rectangle(rect)
if end.Y - start.Y > fixed.I(height) {
rect.Min.X = bounds.Min.X
rect.Min.Y = roundPt(start.Sub(descent)).Y + height
rect.Max.X = bounds.Max.X
rect.Max.Y = roundPt(end.Add(ascent)).Y - height
pen.Rectangle(rect)
}
rect = image.Rectangle {
Min: roundPt(end.Add(ascent)),
Max: roundPt(end.Sub(descent)),
}
rect.Min.X = bounds.Min.X
pen.Rectangle(rect)
}
}
func (this *textBox) textOffset () image.Point {
return this.InnerBounds().Min.
Add(this.scroll).
Sub(this.drawer.LayoutBoundsSpace().Min)
}
func (this *textBox) handleFocusLeave () {
this.on.dotChange.Broadcast()
this.invalidateDraw()
this.box.handleFocusLeave()
}
func (this *textBox) handleMouseDown (button input.Button) {
if button == input.ButtonLeft {
index := this.runeUnderMouse()
this.selectStart = index
this.selecting = true
this.Select(text.Dot { Start: this.selectStart, End: index })
}
this.box.handleMouseDown(button)
}
func (this *textBox) handleMouseUp (button input.Button) {
if button == input.ButtonLeft && this.selecting {
index := this.runeUnderMouse()
this.selecting = false
this.Select(text.Dot { Start: this.selectStart, End: index })
}
this.box.handleMouseUp(button)
}
func (this *textBox) handleMouseMove () {
if this.selecting {
index := this.runeUnderMouse()
this.Select(text.Dot { Start: this.selectStart, End: index })
}
this.box.handleMouseMove()
}
func (this *textBox) runeUnderMouse () int {
position := this.MousePosition().Sub(this.textOffset())
return this.drawer.AtPosition(fixPt(position))
}
func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
bounds := this.drawer.LayoutBoundsSpace()
return bounds.Sub(bounds.Min)
}
func (this *textBox) contentMinimum () image.Point {
minimum := this.drawer.MinimumSize()
if this.hOverflow || this.wrap {
minimum.X = this.drawer.Em().Round()
}
if this.vOverflow {
minimum.Y = this.drawer.LineHeight().Round()
}
return minimum.Add(this.box.contentMinimum())
}
func (this *textBox) doLayout () {
this.box.doLayout()
previousContentBounds := this.contentBounds
innerBounds := this.InnerBounds()
this.drawer.SetWidth(innerBounds.Dx())
this.drawer.SetHeight(innerBounds.Dy())
this.contentBounds = this.normalizedLayoutBoundsSpace()
this.constrainScroll()
this.contentBounds = this.contentBounds.Add(this.scroll)
// println(this.InnerBounds().String(), this.contentBounds.String())
if previousContentBounds != this.contentBounds {
this.on.contentBoundsChange.Broadcast()
}
}
func (this *textBox) constrainScroll () {
innerBounds := this.InnerBounds()
width := this.contentBounds.Dx()
height := this.contentBounds.Dy()
// X
if width <= innerBounds.Dx() {
this.scroll.X = 0
} else if this.scroll.X > 0 {
this.scroll.X = 0
} else if this.scroll.X < innerBounds.Dx() - width {
this.scroll.X = innerBounds.Dx() - width
}
// Y
if height <= innerBounds.Dy() {
this.scroll.Y = 0
} else if this.scroll.Y > 0 {
this.scroll.Y = 0
} else if this.scroll.Y < innerBounds.Dy() - height {
this.scroll.Y = innerBounds.Dy() - height
}
}
func (this *textBox) scrollToDot () {
dot := roundPt(this.drawer.PositionAt(this.dot.End)).Add(this.textOffset())
innerBounds := this.InnerBounds()
scroll := this.scroll
em := this.drawer.Em().Round()
lineHeight := this.drawer.LineHeight().Round()
// X
if dot.X < innerBounds.Min.X + em {
scroll.X += innerBounds.Min.X - dot.X + em
} else if dot.X > innerBounds.Max.X - em {
scroll.X -= dot.X - innerBounds.Max.X + em
}
// Y
if dot.Y < innerBounds.Min.Y + lineHeight {
scroll.Y += innerBounds.Min.Y - dot.Y + lineHeight
} else if dot.Y > innerBounds.Max.Y - lineHeight {
scroll.Y -= dot.Y - innerBounds.Max.Y + lineHeight
}
this.ScrollTo(scroll)
} }