Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39541e1b78 | |||
| c8bcb9e428 | |||
| 858129ec33 | |||
| 02551987a4 | |||
| e45e391f6d |
2
go.mod
2
go.mod
@@ -2,6 +2,6 @@ module git.tebibyte.media/tomo/objects
|
|||||||
|
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require git.tebibyte.media/tomo/tomo v0.29.0
|
require git.tebibyte.media/tomo/tomo v0.31.0
|
||||||
|
|
||||||
require golang.org/x/image v0.11.0 // indirect
|
require golang.org/x/image v0.11.0 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -1,5 +1,5 @@
|
|||||||
git.tebibyte.media/tomo/tomo v0.29.0 h1:uvdPaEQYcWH965y85SjIKwhLklnTbs+x6MRwLfdRvfo=
|
git.tebibyte.media/tomo/tomo v0.31.0 h1:LHPpj3AWycochnC8F441aaRNS6Tq6w6WnBrp/LGjyhM=
|
||||||
git.tebibyte.media/tomo/tomo v0.29.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
|
git.tebibyte.media/tomo/tomo v0.31.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
|||||||
10
icon.go
10
icon.go
@@ -31,16 +31,6 @@ func NewMimeIcon (mime data.Mime, size theme.IconSize) *Icon {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApplicationIcon creates a new icon from an application description.
|
|
||||||
func NewApplicationIcon (id theme.ApplicationIcon, size theme.IconSize) *Icon {
|
|
||||||
this := &Icon {
|
|
||||||
Box: tomo.NewBox(),
|
|
||||||
}
|
|
||||||
theme.Apply(this, theme.R("objects", "Icon", size.String()))
|
|
||||||
this.SetTexture(id.Texture(size))
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this *Icon) SetTexture (texture canvas.Texture) {
|
func (this *Icon) SetTexture (texture canvas.Texture) {
|
||||||
this.Box.SetTexture(texture)
|
this.Box.SetTexture(texture)
|
||||||
if texture == nil {
|
if texture == nil {
|
||||||
|
|||||||
4
input.go
4
input.go
@@ -20,6 +20,7 @@ type TextInput struct {
|
|||||||
func NewTextInput (text string) *TextInput {
|
func NewTextInput (text string) *TextInput {
|
||||||
this := &TextInput { TextBox: tomo.NewTextBox() }
|
this := &TextInput { TextBox: tomo.NewTextBox() }
|
||||||
theme.Apply(this, theme.R("objects", "TextInput", ""))
|
theme.Apply(this, theme.R("objects", "TextInput", ""))
|
||||||
|
this.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
|
||||||
this.SetText(text)
|
this.SetText(text)
|
||||||
this.SetFocusable(true)
|
this.SetFocusable(true)
|
||||||
this.SetSelectable(true)
|
this.SetSelectable(true)
|
||||||
@@ -53,6 +54,9 @@ func (this *TextInput) handleKeyDown (key input.Key, numpad bool) {
|
|||||||
sel := modifiers.Shift
|
sel := modifiers.Shift
|
||||||
changed := false
|
changed := false
|
||||||
|
|
||||||
|
// TODO all this (except editing stuff) really should be moved into the
|
||||||
|
// backend
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case key == input.KeyEnter:
|
case key == input.KeyEnter:
|
||||||
this.on.enter.Broadcast()
|
this.on.enter.Broadcast()
|
||||||
|
|||||||
1
label.go
1
label.go
@@ -15,4 +15,3 @@ func NewLabel (text string) *Label {
|
|||||||
this.SetText(text)
|
this.SetText(text)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
scrollbar.go
44
scrollbar.go
@@ -173,15 +173,15 @@ func (this *Scrollbar) handleMouseDown (button input.Button) {
|
|||||||
}
|
}
|
||||||
case input.ButtonMiddle:
|
case input.ButtonMiddle:
|
||||||
if above {
|
if above {
|
||||||
this.SetValue(0)
|
this.scrollBy(-this.pageSize())
|
||||||
} else {
|
} else {
|
||||||
this.SetValue(1)
|
this.scrollBy(this.pageSize())
|
||||||
}
|
}
|
||||||
case input.ButtonRight:
|
case input.ButtonRight:
|
||||||
if above {
|
if above {
|
||||||
this.SetValue(this.Value() - 0.05)
|
this.scrollBy(-this.stepSize())
|
||||||
} else {
|
} else {
|
||||||
this.SetValue(this.Value() + 0.05)
|
this.scrollBy(this.stepSize())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,6 +229,34 @@ func (this *Scrollbar) fallbackDragOffset () image.Point {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (this *Scrollbar) pageSize () int {
|
||||||
|
if this.layout.linked == nil { return 0 }
|
||||||
|
viewport := this.layout.linked.InnerBounds()
|
||||||
|
if this.layout.vertical {
|
||||||
|
return viewport.Dy()
|
||||||
|
} else {
|
||||||
|
return viewport.Dx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Scrollbar) stepSize () int {
|
||||||
|
// FIXME: this should not be hardcoded, need to get base font metrics
|
||||||
|
// from theme somehow. should be (emspace, lineheight)
|
||||||
|
return 16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Scrollbar) scrollBy (distance int) {
|
||||||
|
if this.layout.linked == nil { return }
|
||||||
|
var vector image.Point; if this.layout.vertical {
|
||||||
|
vector.Y = distance
|
||||||
|
} else {
|
||||||
|
vector.X = distance
|
||||||
|
}
|
||||||
|
this.layout.linked.ScrollTo (
|
||||||
|
this.layout.linked.ContentBounds().Min.
|
||||||
|
Add(vector))
|
||||||
|
}
|
||||||
|
|
||||||
type scrollbarCookie struct {
|
type scrollbarCookie struct {
|
||||||
owner *Scrollbar
|
owner *Scrollbar
|
||||||
subCookies []event.Cookie
|
subCookies []event.Cookie
|
||||||
@@ -278,7 +306,13 @@ func (this scrollbarLayout) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
|
|||||||
// calculate handle length
|
// calculate handle length
|
||||||
handleLength := gutterLength * this.viewportContentRatio()
|
handleLength := gutterLength * this.viewportContentRatio()
|
||||||
if handleLength < handleMin { handleLength = handleMin }
|
if handleLength < handleMin { handleLength = handleMin }
|
||||||
if handleLength > gutterLength { handleLength = gutterLength }
|
if handleLength >= gutterLength {
|
||||||
|
// just hide the handle if it isn't needed.
|
||||||
|
// TODO: we need a way to hide and show boxes, this is janky as
|
||||||
|
// fuck
|
||||||
|
boxes[0].SetBounds(image.Rect(-16, -16, 0, 0))
|
||||||
|
return
|
||||||
|
}
|
||||||
if this.vertical {
|
if this.vertical {
|
||||||
handle.Max.Y = int(handleLength)
|
handle.Max.Y = int(handleLength)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
167
scrollcontainer.go
Normal file
167
scrollcontainer.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/event"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/theme"
|
||||||
|
|
||||||
|
// ScrollSide determines which Scrollbars are active in a ScrollContainer.
|
||||||
|
type ScrollSide int; const (
|
||||||
|
ScrollVertical ScrollSide = 1 << iota
|
||||||
|
ScrollHorizontal
|
||||||
|
ScrollBoth = ScrollVertical | ScrollHorizontal
|
||||||
|
)
|
||||||
|
|
||||||
|
// Horizontal returns true if the side includes a horizontal bar.
|
||||||
|
func (sides ScrollSide) Horizontal () bool {
|
||||||
|
return sides & ScrollHorizontal > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical returns true if the side includes a vertical bar.
|
||||||
|
func (sides ScrollSide) Vertical () bool {
|
||||||
|
return sides & ScrollVertical > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns one of:
|
||||||
|
// - both
|
||||||
|
// - horizontal
|
||||||
|
// - vertical
|
||||||
|
// - none
|
||||||
|
func (sides ScrollSide) String () string {
|
||||||
|
switch {
|
||||||
|
case sides.Horizontal() && sides.Vertical(): return "both"
|
||||||
|
case sides.Horizontal(): return "horizontal"
|
||||||
|
case sides.Vertical(): return "vertical"
|
||||||
|
default: return "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollContainer couples a ContentBox with one or two Scrollbars.
|
||||||
|
type ScrollContainer struct {
|
||||||
|
tomo.ContainerBox
|
||||||
|
layout *scrollContainerLayout
|
||||||
|
|
||||||
|
horizontalCookie event.Cookie
|
||||||
|
verticalCookie event.Cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScrollContainer creates a new scroll container.
|
||||||
|
func NewScrollContainer (sides ScrollSide) *ScrollContainer {
|
||||||
|
this := &ScrollContainer {
|
||||||
|
ContainerBox: tomo.NewContainerBox(),
|
||||||
|
layout: &scrollContainerLayout { },
|
||||||
|
}
|
||||||
|
if sides.Vertical() {
|
||||||
|
this.layout.vertical = NewVerticalScrollbar()
|
||||||
|
this.Add(this.layout.vertical)
|
||||||
|
}
|
||||||
|
if sides.Horizontal() {
|
||||||
|
this.layout.horizontal = NewHorizontalScrollbar()
|
||||||
|
this.Add(this.layout.horizontal)
|
||||||
|
}
|
||||||
|
this.CaptureScroll(true)
|
||||||
|
this.OnScroll(this.handleScroll)
|
||||||
|
theme.Apply(this, theme.R("objects", "ScrollContainer", sides.String()))
|
||||||
|
this.SetLayout(this.layout)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRoot sets the root child of the ScrollContainer. There can only be one at
|
||||||
|
// a time, and setting it will remove and unlink the current child if there is
|
||||||
|
// one.
|
||||||
|
func (this *ScrollContainer) SetRoot (root tomo.ContentBox) {
|
||||||
|
if this.layout.root != nil {
|
||||||
|
// delete root and close cookies
|
||||||
|
this.Delete(this.layout.root)
|
||||||
|
if this.horizontalCookie != nil {
|
||||||
|
this.horizontalCookie.Close()
|
||||||
|
this.horizontalCookie = nil
|
||||||
|
}
|
||||||
|
if this.verticalCookie != nil {
|
||||||
|
this.verticalCookie.Close()
|
||||||
|
this.verticalCookie = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.layout.root = root
|
||||||
|
if root != nil {
|
||||||
|
// insert root at the beginning (for keynav)
|
||||||
|
switch {
|
||||||
|
case this.layout.vertical != nil:
|
||||||
|
this.Insert(root, this.layout.vertical)
|
||||||
|
case this.layout.horizontal != nil:
|
||||||
|
this.Insert(root, this.layout.horizontal)
|
||||||
|
default:
|
||||||
|
this.Add(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
// link root and remember cookies
|
||||||
|
if this.layout.horizontal != nil {
|
||||||
|
this.horizontalCookie = this.layout.horizontal.Link(root)
|
||||||
|
}
|
||||||
|
if this.layout.vertical != nil {
|
||||||
|
this.verticalCookie = this.layout.vertical.Link(root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *ScrollContainer) handleScroll (x, y float64) {
|
||||||
|
if this.layout.root == nil { return }
|
||||||
|
this.layout.root.ScrollTo (
|
||||||
|
this.layout.root.ContentBounds().Min.
|
||||||
|
Add(image.Pt(int(x), int(y))))
|
||||||
|
}
|
||||||
|
|
||||||
|
type scrollContainerLayout struct {
|
||||||
|
root tomo.ContentBox
|
||||||
|
horizontal *Scrollbar
|
||||||
|
vertical *Scrollbar
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *scrollContainerLayout) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
|
||||||
|
var minimum image.Point; if this.root != nil {
|
||||||
|
minimum = this.root.MinimumSize()
|
||||||
|
}
|
||||||
|
if this.horizontal != nil {
|
||||||
|
minimum.Y += this.horizontal.MinimumSize().Y
|
||||||
|
}
|
||||||
|
if this.vertical != nil {
|
||||||
|
minimum.X += this.vertical.MinimumSize().X
|
||||||
|
minimum.Y = max(minimum.Y, this.vertical.MinimumSize().Y)
|
||||||
|
}
|
||||||
|
if this.horizontal != nil {
|
||||||
|
minimum.X = max(minimum.X, this.horizontal.MinimumSize().X)
|
||||||
|
}
|
||||||
|
return minimum
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *scrollContainerLayout) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
|
||||||
|
rootBounds := hints.Bounds
|
||||||
|
if this.horizontal != nil {
|
||||||
|
rootBounds.Max.Y -= this.horizontal.MinimumSize().Y
|
||||||
|
}
|
||||||
|
if this.vertical != nil {
|
||||||
|
rootBounds.Max.X -= this.vertical.MinimumSize().X
|
||||||
|
}
|
||||||
|
if this.root != nil {
|
||||||
|
this.root.SetBounds(rootBounds)
|
||||||
|
}
|
||||||
|
if this.horizontal != nil {
|
||||||
|
this.horizontal.SetBounds(image.Rect (
|
||||||
|
hints.Bounds.Min.X,
|
||||||
|
rootBounds.Max.Y,
|
||||||
|
rootBounds.Max.X,
|
||||||
|
hints.Bounds.Max.Y))
|
||||||
|
}
|
||||||
|
if this.vertical != nil {
|
||||||
|
this.vertical.SetBounds(image.Rect (
|
||||||
|
rootBounds.Max.X,
|
||||||
|
hints.Bounds.Min.Y,
|
||||||
|
hints.Bounds.Max.X,
|
||||||
|
rootBounds.Max.Y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func max (x, y int) int {
|
||||||
|
if x > y { return x }
|
||||||
|
return y
|
||||||
|
}
|
||||||
27
textview.go
Normal file
27
textview.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/theme"
|
||||||
|
|
||||||
|
// TextView is an area for displaying a large amount of multi-line text.
|
||||||
|
type TextView struct {
|
||||||
|
tomo.TextBox
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTextView creates a new text view.
|
||||||
|
func NewTextView (text string) *TextView {
|
||||||
|
this := &TextView { TextBox: tomo.NewTextBox() }
|
||||||
|
theme.Apply(this, theme.R("objects", "TextView", ""))
|
||||||
|
this.SetFocusable(true)
|
||||||
|
this.SetSelectable(true)
|
||||||
|
this.SetText(text)
|
||||||
|
this.SetOverflow(false, true)
|
||||||
|
this.SetWrap(true)
|
||||||
|
this.OnScroll(this.handleScroll)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *TextView) handleScroll (x, y float64) {
|
||||||
|
this.ScrollTo(this.ContentBounds().Min.Add(image.Pt(int(x), int(y))))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user