Boxes that need their minimum size to be updated now use a map like for layout and drawing. Size set with MinimumSize is now treated as separate from the content size and the larger size is used.
		
			
				
	
	
		
			302 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			302 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package x
 | |
| 
 | |
| import "image"
 | |
| import "image/color"
 | |
| import "golang.org/x/image/font"
 | |
| 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
 | |
| 
 | |
| 	selectable  bool
 | |
| 	selecting   bool
 | |
| 	selectStart int
 | |
| 	dot         text.Dot
 | |
| 	dotColor    color.Color
 | |
| 	
 | |
| 	drawer typeset.Drawer
 | |
| 	
 | |
| 	on struct {
 | |
| 		contentBoundsChange event.FuncBroadcaster
 | |
| 		dotChange           event.FuncBroadcaster
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (backend *Backend) NewTextBox() tomo.TextBox {
 | |
| 	this := &textBox {
 | |
| 		textColor: color.Black,
 | |
| 		dotColor:  color.RGBA { B: 255, G: 255, A: 255 },
 | |
| 	}
 | |
| 	this.box = backend.newBox(this)
 | |
| 	return this
 | |
| }
 | |
| 
 | |
| 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) {
 | |
| 	// TODO: constrain scroll
 | |
| 	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.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 { return }
 | |
| 	this.hAlign = x
 | |
| 
 | |
| 	switch x {
 | |
| 	case tomo.AlignStart:  this.drawer.SetAlign(typeset.AlignLeft)
 | |
| 	case tomo.AlignMiddle: this.drawer.SetAlign(typeset.AlignCenter)
 | |
| 	case tomo.AlignEnd:    this.drawer.SetAlign(typeset.AlignRight)
 | |
| 	case tomo.AlignEven:   this.drawer.SetAlign(typeset.AlignJustify)
 | |
| 	}
 | |
| 	
 | |
| 	this.invalidateDraw()
 | |
| }
 | |
| 
 | |
| func (this *textBox) Draw (can canvas.Canvas) {
 | |
| 	if can == nil { return }
 | |
| 	this.drawBorders(can)
 | |
| 	pen := can.Pen()
 | |
| 	pen.Fill(this.color)
 | |
| 	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)
 | |
| 	pen.StrokeWeight(1)
 | |
| 
 | |
| 	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.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.
 | |
| 		Sub(this.scroll).
 | |
| 		Sub(this.drawer.LayoutBoundsSpace().Min)
 | |
| }
 | |
| 
 | |
| func (this *textBox) handleFocusLeave () {
 | |
| 	this.dot = text.EmptyDot(0)
 | |
| 	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 := image.Pt (
 | |
| 		this.drawer.Em().Round(),
 | |
| 		this.drawer.LineHeight().Round())
 | |
| 
 | |
| 	textSize := this.drawer.MinimumSize()
 | |
| 	if !this.hOverflow && !this.wrap {
 | |
| 		minimum.X = textSize.X
 | |
| 	}
 | |
| 	if !this.vOverflow {
 | |
| 		 minimum.Y = textSize.Y
 | |
| 	}
 | |
| 
 | |
| 	return minimum.Add(this.box.contentMinimum())
 | |
| }
 | |
| 
 | |
| func (this *textBox) doLayout () {
 | |
| 	this.box.doLayout()
 | |
| 	previousContentBounds := this.contentBounds
 | |
| 
 | |
| 	innerBounds := this.InnerBounds()
 | |
| 	this.drawer.SetMaxWidth(innerBounds.Dx())
 | |
| 	this.drawer.SetMaxHeight(innerBounds.Dy())
 | |
| 	
 | |
| 	this.contentBounds = this.normalizedLayoutBoundsSpace().Sub(this.scroll)
 | |
| 	
 | |
| 	if previousContentBounds != this.contentBounds {
 | |
| 		this.on.contentBoundsChange.Broadcast()
 | |
| 	}
 | |
| }
 |