Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
a3bb4098fb | |||
c30ac90577 | |||
7628903e59 | |||
ce08487eff | |||
6130b84a27 | |||
8b80520f8c | |||
38434db75c | |||
ffb6e9fb95 | |||
12b855ba24 | |||
5b8e401e60 | |||
7144900d31 | |||
51ce2a84f2 | |||
f1f71208f2 | |||
cdf23c9b13 | |||
b8b80f8862 | |||
2224d2e73e | |||
c2245ec304 | |||
dca3880a87 | |||
f0573bf551 | |||
ba2eeeba74 | |||
b37d5398d8 | |||
f761da8cdc | |||
039e0da646 | |||
6f2a31cd60 | |||
177167510b | |||
3077249a13 | |||
38d950f44a | |||
8b1b2e4199 | |||
63ad06e214 | |||
ac1a952b40 | |||
45a6634e73 | |||
ead7d493d7 | |||
c1cf6edd8e |
@ -1,9 +1,11 @@
|
|||||||
# objects
|
# objects
|
||||||
|
|
||||||
|
![Some of the objects in this package](assets/preview.png)
|
||||||
|
|
||||||
[![Go Reference](https://pkg.go.dev/badge/git.tebibyte.media/tomo/objects.svg)](https://pkg.go.dev/git.tebibyte.media/tomo/objects)
|
[![Go Reference](https://pkg.go.dev/badge/git.tebibyte.media/tomo/objects.svg)](https://pkg.go.dev/git.tebibyte.media/tomo/objects)
|
||||||
|
|
||||||
Objects contains a standard collection of re-usable objects. It should be viewed
|
Objects contains a standard collection of re-usable objects. It should also be
|
||||||
also as a reference for how to create custom objects in Tomo.
|
viewed as a reference for how to create custom objects in Tomo.
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
All objects in this module have roles of the form:
|
All objects in this module have roles of the form:
|
||||||
|
93
abstractcontainer.go
Normal file
93
abstractcontainer.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/event"
|
||||||
|
|
||||||
|
type abstractContainer struct {
|
||||||
|
box tomo.ContainerBox
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *abstractContainer) init (layout tomo.Layout, children ...tomo.Object) {
|
||||||
|
this.box = tomo.NewContainerBox()
|
||||||
|
this.SetLayout(layout)
|
||||||
|
for _, child := range children {
|
||||||
|
this.Add(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBox returns the underlying box.
|
||||||
|
func (this *abstractContainer) GetBox () tomo.Box {
|
||||||
|
return this.box
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentBounds returns the bounds of the inner content of the container
|
||||||
|
// relative to the container's InnerBounds.
|
||||||
|
func (this *abstractContainer) ContentBounds () image.Rectangle {
|
||||||
|
return this.box.ContentBounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollTo shifts the origin of the container's content to the origin of the
|
||||||
|
// container's InnerBounds, offset by the given point.
|
||||||
|
func (this *abstractContainer) ScrollTo (position image.Point) {
|
||||||
|
this.box.ScrollTo(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnContentBoundsChange specifies a function to be called when the container's
|
||||||
|
// ContentBounds or InnerBounds changes.
|
||||||
|
func (this *abstractContainer) OnContentBoundsChange (callback func ()) event.Cookie {
|
||||||
|
return this.box.OnContentBoundsChange(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLayout sets the layout of the container.
|
||||||
|
func (this *abstractContainer) SetLayout (layout tomo.Layout) {
|
||||||
|
if layout == nil {
|
||||||
|
this.box.UnsetAttr(tomo.AttrKindLayout)
|
||||||
|
} else {
|
||||||
|
this.box.SetAttr(tomo.ALayout(layout))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAlign sets the X and Y alignment of the container.
|
||||||
|
func (this *abstractContainer) SetAlign (x, y tomo.Align) {
|
||||||
|
this.box.SetAttr(tomo.AAlign(x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOverflow sets the X and Y overflow of the container.
|
||||||
|
func (this *abstractContainer) SetOverflow (x, y bool) {
|
||||||
|
this.box.SetAttr(tomo.AOverflow(x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add appends a child object. If the object is already a child of another
|
||||||
|
// object, it will be removed from that object first.
|
||||||
|
func (this *abstractContainer) Add (object tomo.Object) {
|
||||||
|
this.box.Add(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes a child object, if it is a child of this container.
|
||||||
|
func (this *abstractContainer) Remove (object tomo.Object) {
|
||||||
|
this.box.Remove(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert inserts a child object before a specified object. If the before object
|
||||||
|
// is nil or is not contained within this container, the inserted object is
|
||||||
|
// appended. If the inserted object is already a child of another object, it
|
||||||
|
// will be removed from that object first.
|
||||||
|
func (this *abstractContainer) Insert (child tomo.Object, before tomo.Object) {
|
||||||
|
this.box.Insert(child, before)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear removes all child objects.
|
||||||
|
func (this *abstractContainer) Clear () {
|
||||||
|
this.box.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns hte amount of child objects.
|
||||||
|
func (this *abstractContainer) Len () int {
|
||||||
|
return this.box.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// At returns the child object at the specified index.
|
||||||
|
func (this *abstractContainer) At (index int) tomo.Object {
|
||||||
|
return this.box.At(index)
|
||||||
|
}
|
BIN
assets/preview.png
Normal file
BIN
assets/preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
@ -60,7 +60,7 @@ func NewCalendar (tm time.Time) *Calendar {
|
|||||||
calendar.grid.SetRole(tomo.R("objects", "CalendarGrid"))
|
calendar.grid.SetRole(tomo.R("objects", "CalendarGrid"))
|
||||||
calendar.grid.SetAttr(tomo.ALayout(layouts.NewGrid (
|
calendar.grid.SetAttr(tomo.ALayout(layouts.NewGrid (
|
||||||
true, true, true, true, true, true, true)()))
|
true, true, true, true, true, true, true)()))
|
||||||
calendar.box.Add(NewInnerContainer (
|
calendar.box.Add(NewContainer (
|
||||||
layouts.Row { false, true, false },
|
layouts.Row { false, true, false },
|
||||||
prevButton, calendar.monthLabel, nextButton))
|
prevButton, calendar.monthLabel, nextButton))
|
||||||
calendar.box.Add(calendar.grid)
|
calendar.box.Add(calendar.grid)
|
||||||
|
@ -6,7 +6,7 @@ import "git.tebibyte.media/tomo/tomo/input"
|
|||||||
import "git.tebibyte.media/tomo/tomo/event"
|
import "git.tebibyte.media/tomo/tomo/event"
|
||||||
import "git.tebibyte.media/tomo/tomo/canvas"
|
import "git.tebibyte.media/tomo/tomo/canvas"
|
||||||
import "git.tebibyte.media/tomo/objects/layouts"
|
import "git.tebibyte.media/tomo/objects/layouts"
|
||||||
import "git.tebibyte.media/tomo/objects/internal"
|
import "git.tebibyte.media/sashakoshka/goutil/image/color"
|
||||||
|
|
||||||
var _ tomo.Object = new(HSVAColorPicker)
|
var _ tomo.Object = new(HSVAColorPicker)
|
||||||
|
|
||||||
@ -14,11 +14,11 @@ var _ tomo.Object = new(HSVAColorPicker)
|
|||||||
// parameters.
|
// parameters.
|
||||||
//
|
//
|
||||||
// Sub-components:
|
// Sub-components:
|
||||||
// - ColorPickerMap is a recangular control where the X axis controls
|
// - ColorPickerMap is a rectangular control where the X axis controls
|
||||||
// saturation and the Y axis controls value.
|
// saturation and the Y axis controls value.
|
||||||
type HSVAColorPicker struct {
|
type HSVAColorPicker struct {
|
||||||
box tomo.ContainerBox
|
box tomo.ContainerBox
|
||||||
value internal.HSVA
|
value ucolor.HSVA
|
||||||
|
|
||||||
pickerMap *hsvaColorPickerMap
|
pickerMap *hsvaColorPickerMap
|
||||||
hueSlider *Slider
|
hueSlider *Slider
|
||||||
@ -80,7 +80,7 @@ func (this *HSVAColorPicker) Value () color.Color {
|
|||||||
// SetValue sets the color of the picker.
|
// SetValue sets the color of the picker.
|
||||||
func (this *HSVAColorPicker) SetValue (value color.Color) {
|
func (this *HSVAColorPicker) SetValue (value color.Color) {
|
||||||
if value == nil { value = color.Transparent }
|
if value == nil { value = color.Transparent }
|
||||||
this.value = internal.HSVAModel.Convert(value).(internal.HSVA)
|
this.value = ucolor.HSVAModel.Convert(value).(ucolor.HSVA)
|
||||||
this.hueSlider.SetValue(this.value.H)
|
this.hueSlider.SetValue(this.value.H)
|
||||||
this.alphaSlider.SetValue(float64(this.value.A) / 0xFFFF)
|
this.alphaSlider.SetValue(float64(this.value.A) / 0xFFFF)
|
||||||
this.pickerMap.Invalidate()
|
this.pickerMap.Invalidate()
|
||||||
@ -152,7 +152,7 @@ func (this *hsvaColorPickerMap) Draw (can canvas.Canvas) {
|
|||||||
xx := x - bounds.Min.X
|
xx := x - bounds.Min.X
|
||||||
yy := y - bounds.Min.Y
|
yy := y - bounds.Min.Y
|
||||||
|
|
||||||
pixel := internal.HSVA {
|
pixel := ucolor.HSVA {
|
||||||
H: this.parent.value.H,
|
H: this.parent.value.H,
|
||||||
S: float64(xx) / float64(bounds.Dx()),
|
S: float64(xx) / float64(bounds.Dx()),
|
||||||
V: 1 - float64(yy) / float64(bounds.Dy()),
|
V: 1 - float64(yy) / float64(bounds.Dy()),
|
||||||
|
128
container.go
128
container.go
@ -1,133 +1,21 @@
|
|||||||
package objects
|
package objects
|
||||||
|
|
||||||
import "image"
|
|
||||||
import "git.tebibyte.media/tomo/tomo"
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
import "git.tebibyte.media/tomo/tomo/event"
|
|
||||||
|
|
||||||
var _ tomo.ContentObject = new(Container)
|
var _ tomo.ContentObject = new(Container)
|
||||||
|
|
||||||
// Container is an object that can contain other objects. It can be used as a
|
// Container is an object that can contain other objects. It is plain looking,
|
||||||
// primitive for building more complex layouts. It has two main variants: an
|
// and is intended to be used within other containers as a primitive for
|
||||||
// outer container, and an inner container. The outer container has padding
|
// building more complex layouts.
|
||||||
// around its edges, whereas the inner container does not. It also has a
|
|
||||||
// "sunken" variation designed to hold a scrolled list of items.
|
|
||||||
//
|
|
||||||
// Tags:
|
|
||||||
// - [outer] The container is the root of a window.
|
|
||||||
// - [inner] The container is within another container, and is part of a
|
|
||||||
// larger layout.
|
|
||||||
// - [sunken] The container holds a visually grouped, usually scrolled, list
|
|
||||||
// of items.
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
box tomo.ContainerBox
|
abstractContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
func newContainer (layout tomo.Layout, children ...tomo.Object) *Container {
|
// NewContainer creates a new container.
|
||||||
this := &Container {
|
func NewContainer (layout tomo.Layout, children ...tomo.Object) *Container {
|
||||||
box: tomo.NewContainerBox(),
|
this := &Container { }
|
||||||
}
|
this.init(layout, children...)
|
||||||
this.box.SetAttr(tomo.ALayout(layout))
|
|
||||||
for _, child := range children {
|
|
||||||
this.Add(child)
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewOuterContainer creates a new container that has padding around it, as well
|
|
||||||
// as a solid background color. It is meant to be used as a root container for a
|
|
||||||
// window, tab pane, etc.
|
|
||||||
func NewOuterContainer (layout tomo.Layout, children ...tomo.Object) *Container {
|
|
||||||
this := newContainer(layout, children...)
|
|
||||||
this.box.SetRole(tomo.R("objects", "Container"))
|
this.box.SetRole(tomo.R("objects", "Container"))
|
||||||
this.box.SetTag("outer", true)
|
this.box.SetTag("outer", true)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSunkenContainer creates a new container with a sunken style and padding
|
|
||||||
// around it. It is meant to be used as a root container for a ScrollContainer.
|
|
||||||
func NewSunkenContainer (layout tomo.Layout, children ...tomo.Object) *Container {
|
|
||||||
this := newContainer(layout, children...)
|
|
||||||
this.box.SetRole(tomo.R("objects", "Container"))
|
|
||||||
this.box.SetTag("sunken", true)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewInnerContainer creates a new container that has no padding around it.
|
|
||||||
func NewInnerContainer (layout tomo.Layout, children ...tomo.Object) *Container {
|
|
||||||
this := newContainer(layout, children...)
|
|
||||||
this.box.SetRole(tomo.R("objects", "Container"))
|
|
||||||
this.box.SetTag("inner", true)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBox returns the underlying box.
|
|
||||||
func (this *Container) GetBox () tomo.Box {
|
|
||||||
return this.box
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContentBounds returns the bounds of the inner content of the container
|
|
||||||
// relative to the container's InnerBounds.
|
|
||||||
func (this *Container) ContentBounds () image.Rectangle {
|
|
||||||
return this.box.ContentBounds()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScrollTo shifts the origin of the container's content to the origin of the
|
|
||||||
// container's InnerBounds, offset by the given point.
|
|
||||||
func (this *Container) ScrollTo (position image.Point) {
|
|
||||||
this.box.ScrollTo(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnContentBoundsChange specifies a function to be called when the container's
|
|
||||||
// ContentBounds or InnerBounds changes.
|
|
||||||
func (this *Container) OnContentBoundsChange (callback func ()) event.Cookie {
|
|
||||||
return this.box.OnContentBoundsChange(callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLayout sets the layout of the container.
|
|
||||||
func (this *Container) SetLayout (layout tomo.Layout) {
|
|
||||||
this.box.SetAttr(tomo.ALayout(layout))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAlign sets the X and Y alignment of the container.
|
|
||||||
func (this *Container) SetAlign (x, y tomo.Align) {
|
|
||||||
this.box.SetAttr(tomo.AAlign(x, y))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOverflow sets the X and Y overflow of the container.
|
|
||||||
func (this *Container) SetOverflow (x, y bool) {
|
|
||||||
this.box.SetAttr(tomo.AOverflow(x, y))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add appends a child object. If the object is already a child of another
|
|
||||||
// object, it will be removed from that object first.
|
|
||||||
func (this *Container) Add (object tomo.Object) {
|
|
||||||
this.box.Add(object)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes a child object, if it is a child of this container.
|
|
||||||
func (this *Container) Remove (object tomo.Object) {
|
|
||||||
this.box.Remove(object)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert inserts a child object before a specified object. If the before object
|
|
||||||
// is nil or is not contained within this container, the inserted object is
|
|
||||||
// appended. If the inserted object is already a child of another object, it
|
|
||||||
// will be removed from that object first.
|
|
||||||
func (this *Container) Insert (child tomo.Object, before tomo.Object) {
|
|
||||||
this.box.Insert(child, before)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear removes all child objects.
|
|
||||||
func (this *Container) Clear () {
|
|
||||||
this.box.Clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns hte amount of child objects.
|
|
||||||
func (this *Container) Len () int {
|
|
||||||
return this.box.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
// At returns the child object at the specified index.
|
|
||||||
func (this *Container) At (index int) tomo.Object {
|
|
||||||
return this.box.At(index)
|
|
||||||
}
|
|
||||||
|
19
dialog.go
19
dialog.go
@ -16,7 +16,6 @@ type DialogKind int; const (
|
|||||||
// Dialog is a modal dialog window.
|
// Dialog is a modal dialog window.
|
||||||
type Dialog struct {
|
type Dialog struct {
|
||||||
tomo.Window
|
tomo.Window
|
||||||
controlRow *Container
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type clickable interface {
|
type clickable interface {
|
||||||
@ -36,11 +35,11 @@ func NewDialog (kind DialogKind, parent tomo.Window, title, message string, opti
|
|||||||
|
|
||||||
dialog := &Dialog { }
|
dialog := &Dialog { }
|
||||||
if parent == nil {
|
if parent == nil {
|
||||||
window, err := tomo.NewWindow(image.Rectangle { })
|
window, err := tomo.NewWindow(tomo.WindowKindNormal, image.Rectangle { })
|
||||||
if err != nil { return nil, err }
|
if err != nil { return nil, err }
|
||||||
dialog.Window = window
|
dialog.Window = window
|
||||||
} else {
|
} else {
|
||||||
window, err := parent.NewModal(image.Rectangle { })
|
window, err := parent.NewChild(tomo.WindowKindModal, image.Rectangle { })
|
||||||
if err != nil { return nil, err }
|
if err != nil { return nil, err }
|
||||||
dialog.Window = window
|
dialog.Window = window
|
||||||
}
|
}
|
||||||
@ -60,16 +59,16 @@ func NewDialog (kind DialogKind, parent tomo.Window, title, message string, opti
|
|||||||
|
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
if option, ok := option.(clickable); ok {
|
if option, ok := option.(clickable); ok {
|
||||||
option.OnClick(dialog.Close)
|
option.OnClick(func () {
|
||||||
|
dialog.Close()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialog.controlRow = NewInnerContainer(layouts.ContractHorizontal, options...)
|
|
||||||
dialog.controlRow.SetAlign(tomo.AlignEnd, tomo.AlignEnd)
|
dialog.SetRoot(NewRoot (
|
||||||
|
|
||||||
dialog.SetRoot(NewOuterContainer (
|
|
||||||
layouts.Column { true, false },
|
layouts.Column { true, false },
|
||||||
NewInnerContainer(layouts.ContractHorizontal, icon, messageText),
|
NewContentSegment(layouts.ContractHorizontal, icon, messageText),
|
||||||
dialog.controlRow))
|
NewOptionSegment(nil, options...)))
|
||||||
return dialog, nil
|
return dialog, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7
go.mod
7
go.mod
@ -1,5 +1,8 @@
|
|||||||
module git.tebibyte.media/tomo/objects
|
module git.tebibyte.media/tomo/objects
|
||||||
|
|
||||||
go 1.20
|
go 1.21.0
|
||||||
|
|
||||||
require git.tebibyte.media/tomo/tomo v0.46.1
|
require (
|
||||||
|
git.tebibyte.media/sashakoshka/goutil v0.3.1
|
||||||
|
git.tebibyte.media/tomo/tomo v0.48.0
|
||||||
|
)
|
||||||
|
6
go.sum
6
go.sum
@ -1,2 +1,4 @@
|
|||||||
git.tebibyte.media/tomo/tomo v0.46.1 h1:/8fT6I9l4TK529zokrThbNDHGRvUsNgif1Zs++0PBSQ=
|
git.tebibyte.media/sashakoshka/goutil v0.3.1 h1:zvAMKS+aea96q6oTttCWfNLXqOHisI3IKAwX6BWKfY0=
|
||||||
git.tebibyte.media/tomo/tomo v0.46.1/go.mod h1:WrtilgKB1y8O2Yu7X4mYcRiqOlPR8NuUnoA/ynkQWrs=
|
git.tebibyte.media/sashakoshka/goutil v0.3.1/go.mod h1:Yo/M2sbi9IbzZCFsEj8/Fg7sNwHkDaJ6saTHOha+Dow=
|
||||||
|
git.tebibyte.media/tomo/tomo v0.48.0 h1:AE21ElHwUSPsX82ZWCnoNxJFi9Oswyd3dPDPMbxTueQ=
|
||||||
|
git.tebibyte.media/tomo/tomo v0.48.0/go.mod h1:WrtilgKB1y8O2Yu7X4mYcRiqOlPR8NuUnoA/ynkQWrs=
|
||||||
|
@ -3,178 +3,6 @@ package internal
|
|||||||
import "fmt"
|
import "fmt"
|
||||||
import "image/color"
|
import "image/color"
|
||||||
|
|
||||||
// HSV represents a color with hue, saturation, and value components. Each
|
|
||||||
// component C is in range 0 <= C <= 1.
|
|
||||||
type HSV struct {
|
|
||||||
H float64
|
|
||||||
S float64
|
|
||||||
V float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// HSVA is an HSV color with an added 8-bit alpha component. The alpha component
|
|
||||||
// ranges from 0x0000 (fully transparent) to 0xFFFF (opaque), and has no bearing
|
|
||||||
// on the other components.
|
|
||||||
type HSVA struct {
|
|
||||||
H float64
|
|
||||||
S float64
|
|
||||||
V float64
|
|
||||||
A uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
HSVModel color.Model = color.ModelFunc(hsvModel)
|
|
||||||
HSVAModel color.Model = color.ModelFunc(hsvaModel)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (hsv HSV) RGBA () (r, g, b, a uint32) {
|
|
||||||
// Adapted from:
|
|
||||||
// https://www.cs.rit.edu/~ncs/color/t_convert.html
|
|
||||||
|
|
||||||
component := func (x float64) uint32 {
|
|
||||||
return uint32(float64(0xFFFF) * x)
|
|
||||||
}
|
|
||||||
|
|
||||||
s := clamp01(hsv.S)
|
|
||||||
v := clamp01(hsv.V)
|
|
||||||
if s == 0 {
|
|
||||||
light := component(v)
|
|
||||||
return light, light, light, 0xFFFF
|
|
||||||
}
|
|
||||||
|
|
||||||
h := clamp01(hsv.H) * 360
|
|
||||||
sector := int(h / 60)
|
|
||||||
// otherwise when given 1.0 for H, sector would overflow to 6
|
|
||||||
if sector > 5 { sector = 5 }
|
|
||||||
offset := (h / 60) - float64(sector)
|
|
||||||
|
|
||||||
p := component(v * (1 - s))
|
|
||||||
q := component(v * (1 - s * offset))
|
|
||||||
t := component(v * (1 - s * (1 - offset)))
|
|
||||||
va := component(v)
|
|
||||||
|
|
||||||
switch sector {
|
|
||||||
case 0: return va, t, p, 0xFFFF
|
|
||||||
case 1: return q, va, p, 0xFFFF
|
|
||||||
case 2: return p, va, t, 0xFFFF
|
|
||||||
case 3: return p, q, va, 0xFFFF
|
|
||||||
case 4: return t, p, va, 0xFFFF
|
|
||||||
default: return va, p, q, 0xFFFF
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hsva HSVA) RGBA () (r, g, b, a uint32) {
|
|
||||||
r, g, b, a = HSV {
|
|
||||||
H: hsva.H,
|
|
||||||
S: hsva.S,
|
|
||||||
V: hsva.V,
|
|
||||||
}.RGBA()
|
|
||||||
a = uint32(hsva.A)
|
|
||||||
// alpha premultiplication
|
|
||||||
r = (r * a) / 0xFFFF
|
|
||||||
g = (g * a) / 0xFFFF
|
|
||||||
b = (b * a) / 0xFFFF
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Canon returns the color but with the H, S, and V fields are constrained to
|
|
||||||
// the range 0.0-1.0
|
|
||||||
func (hsv HSV) Canon () HSV {
|
|
||||||
hsv.H = clamp01(hsv.H)
|
|
||||||
hsv.S = clamp01(hsv.S)
|
|
||||||
hsv.V = clamp01(hsv.V)
|
|
||||||
return hsv
|
|
||||||
}
|
|
||||||
|
|
||||||
// Canon returns the color but with the H, S, and V fields are constrained to
|
|
||||||
// the range 0.0-1.0
|
|
||||||
func (hsva HSVA) Canon () HSVA {
|
|
||||||
hsva.H = clamp01(hsva.H)
|
|
||||||
hsva.S = clamp01(hsva.S)
|
|
||||||
hsva.V = clamp01(hsva.V)
|
|
||||||
return hsva
|
|
||||||
}
|
|
||||||
|
|
||||||
func clamp01 (x float64) float64 {
|
|
||||||
if x > 1.0 { return 1.0 }
|
|
||||||
if x < 0.0 { return 0.0 }
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|
||||||
func hsvModel (c color.Color) color.Color {
|
|
||||||
switch c := c.(type) {
|
|
||||||
case HSV: return c
|
|
||||||
case HSVA: return HSV { H: c.H, S: c.S, V: c.V }
|
|
||||||
default:
|
|
||||||
r, g, b, a := c.RGBA()
|
|
||||||
// alpha unpremultiplication
|
|
||||||
r = (r / a) * 0xFFFF
|
|
||||||
g = (g / a) * 0xFFFF
|
|
||||||
b = (b / a) * 0xFFFF
|
|
||||||
return rgbToHSV(r, g, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hsvaModel (c color.Color) color.Color {
|
|
||||||
switch c := c.(type) {
|
|
||||||
case HSV: return HSVA { H: c.H, S: c.S, V: c.V, A: 0xFFFF }
|
|
||||||
case HSVA: return c
|
|
||||||
default:
|
|
||||||
r, g, b, a := c.RGBA()
|
|
||||||
hsv := rgbToHSV(r, g, b)
|
|
||||||
|
|
||||||
return HSVA {
|
|
||||||
H: hsv.H,
|
|
||||||
S: hsv.S,
|
|
||||||
V: hsv.V,
|
|
||||||
A: uint16(a),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func rgbToHSV (r, g, b uint32) HSV {
|
|
||||||
// Adapted from:
|
|
||||||
// https://www.cs.rit.edu/~ncs/color/t_convert.html
|
|
||||||
|
|
||||||
component := func (x uint32) float64 {
|
|
||||||
return clamp01(float64(x) / 0xFFFF)
|
|
||||||
}
|
|
||||||
cr := component(r)
|
|
||||||
cg := component(g)
|
|
||||||
cb := component(b)
|
|
||||||
|
|
||||||
var maxComponent float64
|
|
||||||
if cr > maxComponent { maxComponent = cr }
|
|
||||||
if cg > maxComponent { maxComponent = cg }
|
|
||||||
if cb > maxComponent { maxComponent = cb }
|
|
||||||
var minComponent = 1.0
|
|
||||||
if cr < minComponent { minComponent = cr }
|
|
||||||
if cg < minComponent { minComponent = cg }
|
|
||||||
if cb < minComponent { minComponent = cb }
|
|
||||||
|
|
||||||
hsv := HSV {
|
|
||||||
V: maxComponent,
|
|
||||||
}
|
|
||||||
|
|
||||||
delta := maxComponent - minComponent
|
|
||||||
if delta == 0 {
|
|
||||||
// hsva.S is undefined, so hue doesn't matter
|
|
||||||
return hsv
|
|
||||||
}
|
|
||||||
hsv.S = delta / maxComponent
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case cr == maxComponent: hsv.H = (cg - cb) / delta
|
|
||||||
case cg == maxComponent: hsv.H = 2 + (cb - cr) / delta
|
|
||||||
case cb == maxComponent: hsv.H = 4 + (cr - cg) / delta
|
|
||||||
}
|
|
||||||
|
|
||||||
hsv.H *= 60
|
|
||||||
if hsv.H < 0 { hsv.H += 360 }
|
|
||||||
hsv.H /= 360
|
|
||||||
|
|
||||||
return hsv
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatNRGBA formats an NRGBA value into a hex string.
|
// FormatNRGBA formats an NRGBA value into a hex string.
|
||||||
func FormatNRGBA (nrgba color.NRGBA) string {
|
func FormatNRGBA (nrgba color.NRGBA) string {
|
||||||
return fmt.Sprintf("%02X%02X%02X%02X", nrgba.R, nrgba.G, nrgba.B, nrgba.A)
|
return fmt.Sprintf("%02X%02X%02X%02X", nrgba.R, nrgba.G, nrgba.B, nrgba.A)
|
||||||
|
92
internal/history.go
Normal file
92
internal/history.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// History stores a stack of items, always keeping the bottom-most one. It must
|
||||||
|
// be created using the NewHistory constructor, otherwise it will be invalid.
|
||||||
|
type History[T comparable] struct {
|
||||||
|
max int
|
||||||
|
stack []T
|
||||||
|
topIndex int
|
||||||
|
topTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHistory creates a new History. The initial item will be on the bottom, and
|
||||||
|
// it will remain there until the History overflows and chooses the item after
|
||||||
|
// it to be the initial item.
|
||||||
|
func NewHistory[T comparable] (initial T, max int) *History[T] {
|
||||||
|
return &History[T] {
|
||||||
|
max: max,
|
||||||
|
stack: []T { initial },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top returns the most recent item.
|
||||||
|
func (this *History[T]) Top () T {
|
||||||
|
return this.stack[this.topIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap replaces the most recent item with another.
|
||||||
|
func (this *History[T]) Swap (item T) {
|
||||||
|
this.topTime = time.Now()
|
||||||
|
this.SwapSilently(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwapSilently replaces the most recent item with another without updating the
|
||||||
|
// time.
|
||||||
|
func (this *History[T]) SwapSilently (item T) {
|
||||||
|
this.stack[this.topIndex] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push pushes a new item onto the stack. If the stack overflows (becomes bigger
|
||||||
|
// than the specified max value), the initial item is removed and the one on top
|
||||||
|
// of it takes its place.
|
||||||
|
func (this *History[T]) Push (item T) {
|
||||||
|
this.topTime = time.Now()
|
||||||
|
if this.Top() != item {
|
||||||
|
this.topIndex ++
|
||||||
|
this.stack = append(this.stack[:this.topIndex], item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(this.stack) > this.max {
|
||||||
|
this.topIndex --
|
||||||
|
this.stack = this.stack[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushWeak replaces the most recent item if it was added recently (sooner than
|
||||||
|
// specified by minAge), and will otherwise push the item normally. If the
|
||||||
|
// history was popped or cleared beforehand, the item will always be pushed
|
||||||
|
// normally. This is intended to be used for things such as keystrokes.
|
||||||
|
func (this *History[T]) PushWeak (item T, minAge time.Duration) {
|
||||||
|
if time.Since(this.topTime) > minAge {
|
||||||
|
this.Push(item)
|
||||||
|
} else {
|
||||||
|
this.Swap(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redo undoes an Undo operation and returns the resulting top of the stack.
|
||||||
|
func (this *History[T]) Redo () T {
|
||||||
|
if this.topIndex < len(this.stack) - 1 {
|
||||||
|
this.topIndex ++
|
||||||
|
}
|
||||||
|
return this.Top()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo removes the most recent item and returns what was under it. If there is
|
||||||
|
// only one item (the initial item), it will kept and returned.
|
||||||
|
func (this *History[T]) Undo () T {
|
||||||
|
this.topTime = time.Time { }
|
||||||
|
if this.topIndex > 0 {
|
||||||
|
this.topIndex --
|
||||||
|
}
|
||||||
|
return this.Top()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear removes all items except for the initial one.
|
||||||
|
func (this *History[T]) Clear () {
|
||||||
|
this.topTime = time.Time { }
|
||||||
|
this.stack = this.stack[:1]
|
||||||
|
this.topIndex = 0
|
||||||
|
}
|
5
label.go
5
label.go
@ -58,3 +58,8 @@ func (this *Label) OnDotChange (callback func ()) event.Cookie {
|
|||||||
func (this *Label) SetAlign (x, y tomo.Align) {
|
func (this *Label) SetAlign (x, y tomo.Align) {
|
||||||
this.box.SetAttr(tomo.AAlign(x, y))
|
this.box.SetAttr(tomo.AAlign(x, y))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetOverflow sets the X and Y overflow of the label.
|
||||||
|
func (this *Label) SetOverflow (x, y bool) {
|
||||||
|
this.box.SetAttr(tomo.AOverflow(x, y))
|
||||||
|
}
|
||||||
|
9
menu.go
9
menu.go
@ -9,6 +9,9 @@ import "git.tebibyte.media/tomo/objects/layouts"
|
|||||||
// Menu is a menu window.
|
// Menu is a menu window.
|
||||||
//
|
//
|
||||||
// Sub-components:
|
// Sub-components:
|
||||||
|
// - Root is the root of the window. It is differentiated from a normal Root
|
||||||
|
// object in that it has the [menu] tag. If the menu has been torn off, it
|
||||||
|
// will have the [torn] tag.
|
||||||
// - TearLine is a horizontal line at the top of the menu that, when clicked,
|
// - TearLine is a horizontal line at the top of the menu that, when clicked,
|
||||||
// causes the menu to be "torn off" into a movable window.
|
// causes the menu to be "torn off" into a movable window.
|
||||||
type Menu struct {
|
type Menu struct {
|
||||||
@ -40,7 +43,7 @@ func newMenu (parent tomo.Window, bounds image.Rectangle, items ...tomo.Object)
|
|||||||
menu := &Menu { }
|
menu := &Menu { }
|
||||||
menu.bounds = bounds
|
menu.bounds = bounds
|
||||||
menu.parent = parent
|
menu.parent = parent
|
||||||
window, err := menu.parent.NewMenu(menu.bounds)
|
window, err := menu.parent.NewChild(tomo.WindowKindMenu, menu.bounds)
|
||||||
if err != nil { return nil, err }
|
if err != nil { return nil, err }
|
||||||
menu.Window = window
|
menu.Window = window
|
||||||
|
|
||||||
@ -62,7 +65,7 @@ func newMenu (parent tomo.Window, bounds image.Rectangle, items ...tomo.Object)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
menu.rootContainer.SetRole(tomo.R("objects", "Container"))
|
menu.rootContainer.SetRole(tomo.R("objects", "Root"))
|
||||||
menu.rootContainer.SetTag("menu", true)
|
menu.rootContainer.SetTag("menu", true)
|
||||||
|
|
||||||
menu.Window.SetRoot(menu.rootContainer)
|
menu.Window.SetRoot(menu.rootContainer)
|
||||||
@ -75,7 +78,7 @@ func (this *Menu) TearOff () {
|
|||||||
if this.parent == nil { return }
|
if this.parent == nil { return }
|
||||||
this.torn = true
|
this.torn = true
|
||||||
|
|
||||||
window, err := this.parent.NewChild(this.bounds)
|
window, err := this.parent.NewChild(tomo.WindowKindToolbar, this.bounds)
|
||||||
window.SetIcon(tomo.IconListChoose)
|
window.SetIcon(tomo.IconListChoose)
|
||||||
if err != nil { return }
|
if err != nil { return }
|
||||||
|
|
||||||
|
170
notebook.go
Normal file
170
notebook.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/input"
|
||||||
|
import "git.tebibyte.media/tomo/objects/layouts"
|
||||||
|
|
||||||
|
var _ tomo.Object = new(Notebook)
|
||||||
|
|
||||||
|
// Notebook holds multiple objects, each in their own page. The user can click
|
||||||
|
// the tab bar at the top to choose which one is activated.
|
||||||
|
//
|
||||||
|
// Sub-components:
|
||||||
|
// - TabRow sits at the top of the container and contains a row of tabs.
|
||||||
|
// - TabSpacer sits at either end of the tab row, bookending the list of tabs.
|
||||||
|
// - Tab appears in the tab row for each tab in the notebook. The user can
|
||||||
|
// click on the tab to switch to it.
|
||||||
|
// - PageWrapper sits underneath the TabRow and contains the active page.
|
||||||
|
//
|
||||||
|
// TabSpacer tags:
|
||||||
|
// - [left] The spacer is on the left.
|
||||||
|
// - [right] The spacer is on the right.
|
||||||
|
//
|
||||||
|
// Tab tags:
|
||||||
|
// - [active] The tab is currently active and its page is visible.
|
||||||
|
type Notebook struct {
|
||||||
|
box tomo.ContainerBox
|
||||||
|
|
||||||
|
leftSpacer tomo.Box
|
||||||
|
rightSpacer tomo.Box
|
||||||
|
tabsRow tomo.ContainerBox
|
||||||
|
pageWrapper tomo.ContainerBox
|
||||||
|
active string
|
||||||
|
pages []*page
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNotebook creates a new tabbed notebook.
|
||||||
|
func NewNotebook () *Notebook {
|
||||||
|
notebook := &Notebook {
|
||||||
|
box: tomo.NewContainerBox(),
|
||||||
|
}
|
||||||
|
notebook.box.SetRole(tomo.R("objects", "Notebook"))
|
||||||
|
notebook.box.SetAttr(tomo.ALayout(layouts.Column { false, true }))
|
||||||
|
|
||||||
|
notebook.leftSpacer = tomo.NewBox()
|
||||||
|
notebook.leftSpacer.SetRole(tomo.R("objects", "TabSpacer"))
|
||||||
|
notebook.leftSpacer.SetTag("left", true)
|
||||||
|
notebook.rightSpacer = tomo.NewBox()
|
||||||
|
notebook.rightSpacer.SetRole(tomo.R("objects", "TabSpacer"))
|
||||||
|
notebook.rightSpacer.SetTag("right", true)
|
||||||
|
|
||||||
|
notebook.tabsRow = tomo.NewContainerBox()
|
||||||
|
notebook.tabsRow.SetRole(tomo.R("objects", "TabRow"))
|
||||||
|
notebook.box.Add(notebook.tabsRow)
|
||||||
|
|
||||||
|
notebook.pageWrapper = tomo.NewContainerBox()
|
||||||
|
notebook.pageWrapper.SetRole(tomo.R("objects", "PageWrapper"))
|
||||||
|
notebook.pageWrapper.SetAttr(tomo.ALayout(layouts.Column { true }))
|
||||||
|
notebook.box.Add(notebook.pageWrapper)
|
||||||
|
|
||||||
|
notebook.Clear()
|
||||||
|
notebook.setTabRowLayout()
|
||||||
|
return notebook
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBox returns the underlying box.
|
||||||
|
func (this *Notebook) GetBox () tomo.Box {
|
||||||
|
return this.box
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate switches to a named page.
|
||||||
|
func (this *Notebook) Activate (name string) {
|
||||||
|
if _, tab := this.findTab(this.active); tab != nil {
|
||||||
|
tab.setActive(false)
|
||||||
|
this.pageWrapper.Remove(tab.root)
|
||||||
|
}
|
||||||
|
if _, tab := this.findTab(name); tab != nil {
|
||||||
|
tab.setActive(true)
|
||||||
|
this.pageWrapper.Add(tab.root)
|
||||||
|
} else {
|
||||||
|
name = ""
|
||||||
|
}
|
||||||
|
this.active = name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds an object as a page with the specified name.
|
||||||
|
func (this *Notebook) Add (name string, root tomo.Object) {
|
||||||
|
pag := &page {
|
||||||
|
TextBox: tomo.NewTextBox(),
|
||||||
|
name: name,
|
||||||
|
root: root,
|
||||||
|
}
|
||||||
|
pag.SetRole(tomo.R("objects", "Tab"))
|
||||||
|
pag.SetText(name)
|
||||||
|
pag.OnButtonDown(func (button input.Button) bool {
|
||||||
|
if button != input.ButtonLeft { return false }
|
||||||
|
this.Activate(name)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
pag.OnButtonUp(func (button input.Button) bool {
|
||||||
|
if button != input.ButtonLeft { return false }
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
this.pages = append(this.pages, pag)
|
||||||
|
this.tabsRow.Insert(pag, this.rightSpacer)
|
||||||
|
this.setTabRowLayout()
|
||||||
|
|
||||||
|
// if the row was empty before, activate this tab
|
||||||
|
if len(this.pages) == 1 {
|
||||||
|
this.Activate(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes the named page.
|
||||||
|
func (this *Notebook) Remove (name string) {
|
||||||
|
index, tab := this.findTab(name)
|
||||||
|
if index < 0 { return }
|
||||||
|
nextIndex := index - 1
|
||||||
|
|
||||||
|
this.tabsRow.Remove(tab)
|
||||||
|
this.pages = append(this.pages[:index], this.pages[index - 1:]...)
|
||||||
|
this.setTabRowLayout()
|
||||||
|
|
||||||
|
if nextIndex < 0 { nextIndex = 0 }
|
||||||
|
if nextIndex >= len(this.pages) { nextIndex = len(this.pages) - 1 }
|
||||||
|
if nextIndex < 0 {
|
||||||
|
this.Activate("")
|
||||||
|
} else {
|
||||||
|
this.Activate(this.pages[nextIndex].name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear removes all tabs.
|
||||||
|
func (this *Notebook) Clear () {
|
||||||
|
this.pages = nil
|
||||||
|
this.tabsRow.Clear()
|
||||||
|
this.tabsRow.Add(this.leftSpacer)
|
||||||
|
this.tabsRow.Add(this.rightSpacer)
|
||||||
|
this.pageWrapper.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Notebook) setTabRowLayout () {
|
||||||
|
row := make(layouts.Row, 1 + len(this.pages) + 1)
|
||||||
|
row[len(row) - 1] = true
|
||||||
|
this.tabsRow.SetAttr(tomo.ALayout(row))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Notebook) findTab (name string) (int, *page) {
|
||||||
|
for index, pag := range this.pages {
|
||||||
|
if pag.name == name { return index, pag }
|
||||||
|
}
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type page struct {
|
||||||
|
tomo.TextBox
|
||||||
|
name string
|
||||||
|
root tomo.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *page) setActive (active bool) {
|
||||||
|
if active {
|
||||||
|
this.SetRole(tomo.R("objects", "Tab"))
|
||||||
|
this.SetTag("active", true)
|
||||||
|
} else {
|
||||||
|
this.SetRole(tomo.R("objects", "Tab"))
|
||||||
|
this.SetTag("active", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
22
pegboard.go
Normal file
22
pegboard.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
import "git.tebibyte.media/tomo/objects/layouts"
|
||||||
|
|
||||||
|
var _ tomo.ContentObject = new(Pegboard)
|
||||||
|
|
||||||
|
// Pegboard is an object that can contain other objects. It is intended to
|
||||||
|
// contain a flowed list of objects which represent some data, such as files.
|
||||||
|
type Pegboard struct {
|
||||||
|
abstractContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPegboard creates a new pegboard. If the provided layout is nil, it will
|
||||||
|
// use a FlowVertical layout.
|
||||||
|
func NewPegboard (layout tomo.Layout, children ...tomo.Object) *Pegboard {
|
||||||
|
if layout == nil { layout = layouts.FlowVertical }
|
||||||
|
pegboard := &Pegboard { }
|
||||||
|
pegboard.init(layout, children...)
|
||||||
|
pegboard.box.SetRole(tomo.R("objects", "Pegboard"))
|
||||||
|
return pegboard
|
||||||
|
}
|
20
root.go
Normal file
20
root.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
|
||||||
|
var _ tomo.ContentObject = new(Root)
|
||||||
|
|
||||||
|
// Root is an object that can contain other objects. It is intended to be used
|
||||||
|
// as the root of a window in order to contain its segments.
|
||||||
|
type Root struct {
|
||||||
|
abstractContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoot creates a new container.
|
||||||
|
func NewRoot (layout tomo.Layout, children ...tomo.Object) *Root {
|
||||||
|
this := &Root { }
|
||||||
|
this.init(layout, children...)
|
||||||
|
this.box.SetRole(tomo.R("objects", "Root"))
|
||||||
|
this.box.SetTag("outer", true)
|
||||||
|
return this
|
||||||
|
}
|
@ -172,14 +172,14 @@ func (this *Scrollbar) handleKeyDown (key input.Key, numpad bool) bool {
|
|||||||
|
|
||||||
switch key {
|
switch key {
|
||||||
case input.KeyUp, input.KeyLeft:
|
case input.KeyUp, input.KeyLeft:
|
||||||
if modifiers.Alt {
|
if modifiers.Alt() {
|
||||||
this.SetValue(0)
|
this.SetValue(0)
|
||||||
} else {
|
} else {
|
||||||
this.scrollBy(this.StepSize())
|
this.scrollBy(this.StepSize())
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
case input.KeyDown, input.KeyRight:
|
case input.KeyDown, input.KeyRight:
|
||||||
if modifiers.Alt {
|
if modifiers.Alt() {
|
||||||
this.SetValue(1)
|
this.SetValue(1)
|
||||||
} else {
|
} else {
|
||||||
this.scrollBy(-this.StepSize())
|
this.scrollBy(-this.StepSize())
|
||||||
@ -320,12 +320,13 @@ func (this *Scrollbar) newLinkCookie (subCookies ...event.Cookie) *scrollbarCook
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *scrollbarCookie) Close () {
|
func (this *scrollbarCookie) Close () error {
|
||||||
for _, cookie := range this.subCookies {
|
for _, cookie := range this.subCookies {
|
||||||
cookie.Close()
|
cookie.Close()
|
||||||
}
|
}
|
||||||
this.owner.layout.linked = nil
|
this.owner.layout.linked = nil
|
||||||
this.owner.box.SetAttr(tomo.ALayout(this.owner.layout))
|
this.owner.box.SetAttr(tomo.ALayout(this.owner.layout))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type scrollbarLayout struct {
|
type scrollbarLayout struct {
|
||||||
|
@ -202,7 +202,7 @@ func (this *ScrollContainer) handleKeyDown (key input.Key, numpad bool) bool {
|
|||||||
vector := image.Point { }
|
vector := image.Point { }
|
||||||
switch key {
|
switch key {
|
||||||
case input.KeyPageUp:
|
case input.KeyPageUp:
|
||||||
if modifiers.Shift {
|
if modifiers.Shift() {
|
||||||
vector.X -= this.PageSize().X
|
vector.X -= this.PageSize().X
|
||||||
} else {
|
} else {
|
||||||
vector.Y -= this.PageSize().Y
|
vector.Y -= this.PageSize().Y
|
||||||
@ -210,7 +210,7 @@ func (this *ScrollContainer) handleKeyDown (key input.Key, numpad bool) bool {
|
|||||||
this.scrollBy(vector)
|
this.scrollBy(vector)
|
||||||
return true
|
return true
|
||||||
case input.KeyPageDown:
|
case input.KeyPageDown:
|
||||||
if modifiers.Shift {
|
if modifiers.Shift() {
|
||||||
vector.X += this.PageSize().X
|
vector.X += this.PageSize().X
|
||||||
} else {
|
} else {
|
||||||
vector.Y += this.PageSize().Y
|
vector.Y += this.PageSize().Y
|
||||||
@ -218,7 +218,7 @@ func (this *ScrollContainer) handleKeyDown (key input.Key, numpad bool) bool {
|
|||||||
this.scrollBy(vector)
|
this.scrollBy(vector)
|
||||||
return true
|
return true
|
||||||
case input.KeyUp:
|
case input.KeyUp:
|
||||||
if modifiers.Shift {
|
if modifiers.Shift() {
|
||||||
vector.X -= this.StepSize().X
|
vector.X -= this.StepSize().X
|
||||||
} else {
|
} else {
|
||||||
vector.Y -= this.StepSize().Y
|
vector.Y -= this.StepSize().Y
|
||||||
@ -226,7 +226,7 @@ func (this *ScrollContainer) handleKeyDown (key input.Key, numpad bool) bool {
|
|||||||
this.scrollBy(vector)
|
this.scrollBy(vector)
|
||||||
return true
|
return true
|
||||||
case input.KeyDown:
|
case input.KeyDown:
|
||||||
if modifiers.Shift {
|
if modifiers.Shift() {
|
||||||
vector.X += this.StepSize().X
|
vector.X += this.StepSize().X
|
||||||
} else {
|
} else {
|
||||||
vector.Y += this.StepSize().Y
|
vector.Y += this.StepSize().Y
|
||||||
@ -241,6 +241,8 @@ func (this *ScrollContainer) handleKeyUp (key input.Key, numpad bool) bool {
|
|||||||
switch key {
|
switch key {
|
||||||
case input.KeyPageUp: return true
|
case input.KeyPageUp: return true
|
||||||
case input.KeyPageDown: return true
|
case input.KeyPageDown: return true
|
||||||
|
case input.KeyUp: return true
|
||||||
|
case input.KeyDown: return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
89
segment.go
Normal file
89
segment.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
import "git.tebibyte.media/tomo/objects/layouts"
|
||||||
|
|
||||||
|
var _ tomo.ContentObject = new(Segment)
|
||||||
|
|
||||||
|
// Segment is an object that can contain other objects, and provides a way to
|
||||||
|
// categorize the functionality of a window. Segments are typically laid out
|
||||||
|
// vertically in a window. There are several variants, the main one being
|
||||||
|
// the content segment. They can all be created using specialized constructors.
|
||||||
|
//
|
||||||
|
// The following is a brief visual discription of how they are typically used,
|
||||||
|
// and in what order:
|
||||||
|
//
|
||||||
|
// ┌──────────────────────────┐
|
||||||
|
// │ ┌──┐ ┌──┐ ┌────────────┐ ├─ command
|
||||||
|
// │ │◄─│ │─►│ │/foo/bar/baz│ │
|
||||||
|
// │ └──┘ └──┘ └────────────┘ │
|
||||||
|
// ├──────────────────────────┤
|
||||||
|
// │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ├─ content
|
||||||
|
// │ │ │ │ │ │ │ │ │ │
|
||||||
|
// │ └───┘ └───┘ └───┘ └───┘ │
|
||||||
|
// │ ┌───┐ │
|
||||||
|
// │ │ │ │
|
||||||
|
// │ └───┘ │
|
||||||
|
// ├──────────────────────────┤
|
||||||
|
// │ 5 items, 4KiB ├─ status
|
||||||
|
// └──────────────────────────┘
|
||||||
|
// ┌─────────────────┐
|
||||||
|
// │ ┌───┐ │
|
||||||
|
// │ │ ! │ Continue? │
|
||||||
|
// │ └───┘ │
|
||||||
|
// ├─────────────────┤
|
||||||
|
// │ ┌──┐ ┌───┐ ├────────── option
|
||||||
|
// │ │No│ │Yes│ │
|
||||||
|
// │ └──┘ └───┘ │
|
||||||
|
// └─────────────────┘
|
||||||
|
//
|
||||||
|
// Tags:
|
||||||
|
// - [command] The segment is a command segment.
|
||||||
|
// - [content] The segment is a content segment.
|
||||||
|
// - [status] The segment is a status segment.
|
||||||
|
// - [option] The segment is an option segment.
|
||||||
|
type Segment struct {
|
||||||
|
abstractContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSegment (kind string, layout tomo.Layout, children ...tomo.Object) *Segment {
|
||||||
|
segment := &Segment { }
|
||||||
|
segment.init(layout, children...)
|
||||||
|
segment.box.SetRole(tomo.R("objects", "Segment"))
|
||||||
|
segment.box.SetTag(kind, true)
|
||||||
|
return segment
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommandSegment creates a new segment intended to hold the window's
|
||||||
|
// navigational controls and command functionality. If the provided layout is
|
||||||
|
// nil, it will use a ContractHorizontal layout.
|
||||||
|
func NewCommandSegment (layout tomo.Layout, children ...tomo.Object) *Segment {
|
||||||
|
if layout == nil { layout = layouts.ContractHorizontal }
|
||||||
|
return newSegment("command", layout, children...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContentSegment creates a new segment intended to hold the window's main
|
||||||
|
// content. If the provided layout is nil, it will use a ContractVertical
|
||||||
|
// layout.
|
||||||
|
func NewContentSegment (layout tomo.Layout, children ...tomo.Object) *Segment {
|
||||||
|
if layout == nil { layout = layouts.ContractVertical }
|
||||||
|
return newSegment("content", layout, children...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatusSegment creates a new segment intended to display the window's
|
||||||
|
// status. If the provided layout is nil, it will use a ContractHorizontal
|
||||||
|
// layout.
|
||||||
|
func NewStatusSegment (layout tomo.Layout, children ...tomo.Object) *Segment {
|
||||||
|
if layout == nil { layout = layouts.ContractHorizontal }
|
||||||
|
return newSegment("status", layout, children...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptionSegment creates a new segment intended to hold the window's options.
|
||||||
|
// This is typically used for dialog boxes. If the provided layout is nil, it
|
||||||
|
// will use a ContractHorizontal layout. By default, it is end-aligned.
|
||||||
|
func NewOptionSegment (layout tomo.Layout, children ...tomo.Object) *Segment {
|
||||||
|
if layout == nil { layout = layouts.ContractHorizontal }
|
||||||
|
segment := newSegment("option", layout, children...)
|
||||||
|
segment.GetBox().SetAttr(tomo.AAlign(tomo.AlignEnd, tomo.AlignMiddle))
|
||||||
|
return segment
|
||||||
|
}
|
@ -127,7 +127,7 @@ func (this *Slider) handleKeyDown (key input.Key, numpad bool) bool {
|
|||||||
|
|
||||||
switch key {
|
switch key {
|
||||||
case input.KeyUp, input.KeyLeft:
|
case input.KeyUp, input.KeyLeft:
|
||||||
if this.box.Window().Modifiers().Alt {
|
if this.box.Window().Modifiers().Alt() {
|
||||||
this.SetValue(0)
|
this.SetValue(0)
|
||||||
} else {
|
} else {
|
||||||
this.SetValue(this.Value() - increment)
|
this.SetValue(this.Value() - increment)
|
||||||
@ -135,7 +135,7 @@ func (this *Slider) handleKeyDown (key input.Key, numpad bool) bool {
|
|||||||
this.on.valueChange.Broadcast()
|
this.on.valueChange.Broadcast()
|
||||||
return true
|
return true
|
||||||
case input.KeyDown, input.KeyRight:
|
case input.KeyDown, input.KeyRight:
|
||||||
if this.box.Window().Modifiers().Alt {
|
if this.box.Window().Modifiers().Alt() {
|
||||||
this.SetValue(1)
|
this.SetValue(1)
|
||||||
} else {
|
} else {
|
||||||
this.SetValue(this.Value() + increment)
|
this.SetValue(this.Value() + increment)
|
||||||
|
24
swatch.go
24
swatch.go
@ -90,9 +90,9 @@ func (this *Swatch) Choose () {
|
|||||||
var err error
|
var err error
|
||||||
var window tomo.Window
|
var window tomo.Window
|
||||||
if parent := this.box.Window(); parent != nil {
|
if parent := this.box.Window(); parent != nil {
|
||||||
window, err = parent.NewChild(image.Rectangle { })
|
window, err = parent.NewChild(tomo.WindowKindNormal, image.Rectangle { })
|
||||||
} else {
|
} else {
|
||||||
window, err = tomo.NewWindow(image.Rectangle { })
|
window, err = tomo.NewWindow(tomo.WindowKindNormal, image.Rectangle { })
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("objects: could not create swatch modal:", err)
|
log.Println("objects: could not create swatch modal:", err)
|
||||||
@ -139,18 +139,16 @@ func (this *Swatch) Choose () {
|
|||||||
})
|
})
|
||||||
okButton.OnClick(commit)
|
okButton.OnClick(commit)
|
||||||
|
|
||||||
controlRow := NewInnerContainer (
|
window.SetRoot(NewRoot (
|
||||||
layouts.ContractHorizontal,
|
|
||||||
cancelButton,
|
|
||||||
okButton)
|
|
||||||
controlRow.SetAlign(tomo.AlignEnd, tomo.AlignMiddle)
|
|
||||||
window.SetRoot(NewOuterContainer (
|
|
||||||
layouts.Column { true, false },
|
layouts.Column { true, false },
|
||||||
colorPicker,
|
NewContentSegment (
|
||||||
NewInnerContainer(layouts.Row { false, true },
|
layouts.Column { true, false },
|
||||||
NewLabel("Hex"),
|
colorPicker,
|
||||||
hexInput),
|
NewContainer (
|
||||||
controlRow))
|
layouts.Row { false, true },
|
||||||
|
NewLabel("Hex"),
|
||||||
|
hexInput)),
|
||||||
|
NewOptionSegment(nil, cancelButton, okButton)))
|
||||||
window.OnClose(func () {
|
window.OnClose(func () {
|
||||||
if committed {
|
if committed {
|
||||||
this.on.confirm.Broadcast()
|
this.on.confirm.Broadcast()
|
||||||
|
@ -1,162 +0,0 @@
|
|||||||
package objects
|
|
||||||
|
|
||||||
import "git.tebibyte.media/tomo/tomo"
|
|
||||||
import "git.tebibyte.media/tomo/tomo/input"
|
|
||||||
import "git.tebibyte.media/tomo/objects/layouts"
|
|
||||||
|
|
||||||
var _ tomo.Object = new(TabbedContainer)
|
|
||||||
|
|
||||||
// TabbedContainer holds multiple objects, each in their own tab. The user can
|
|
||||||
// click the tab bar at the top to choose which one is activated.
|
|
||||||
//
|
|
||||||
// Sub-components:
|
|
||||||
// - TabRow sits at the top of the container and contains a row of tabs.
|
|
||||||
// - TabSpacer sits at either end of the tab row, bookending the list of tabs.
|
|
||||||
// - Tab appears in the tab row for each tab in the container. The user can
|
|
||||||
// click on the tab to switch to it.
|
|
||||||
//
|
|
||||||
// TabSpacer tags:
|
|
||||||
// - [left] The spacer is on the left.
|
|
||||||
// - [right] The spacer is on the right.
|
|
||||||
//
|
|
||||||
// Tab tags:
|
|
||||||
// - [active] The tab is currently active and its contents are visible.
|
|
||||||
type TabbedContainer struct {
|
|
||||||
box tomo.ContainerBox
|
|
||||||
|
|
||||||
leftSpacer tomo.Box
|
|
||||||
rightSpacer tomo.Box
|
|
||||||
tabsRow tomo.ContainerBox
|
|
||||||
active string
|
|
||||||
tabs []*tab
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTabbedContainer creates a new tabbed container.
|
|
||||||
func NewTabbedContainer () *TabbedContainer {
|
|
||||||
tabbedContainer := &TabbedContainer {
|
|
||||||
box: tomo.NewContainerBox(),
|
|
||||||
}
|
|
||||||
tabbedContainer.box.SetRole(tomo.R("objects", "TabbedContainer"))
|
|
||||||
tabbedContainer.box.SetAttr(tomo.ALayout(layouts.Column { false, true }))
|
|
||||||
|
|
||||||
tabbedContainer.tabsRow = tomo.NewContainerBox()
|
|
||||||
tabbedContainer.tabsRow.SetRole(tomo.R("objects", "TabRow"))
|
|
||||||
tabbedContainer.box.Add(tabbedContainer.tabsRow)
|
|
||||||
|
|
||||||
tabbedContainer.leftSpacer = tomo.NewBox()
|
|
||||||
tabbedContainer.leftSpacer.SetRole(tomo.R("objects", "TabSpacer"))
|
|
||||||
tabbedContainer.leftSpacer.SetTag("left", true)
|
|
||||||
tabbedContainer.rightSpacer = tomo.NewBox()
|
|
||||||
tabbedContainer.rightSpacer.SetRole(tomo.R("objects", "TabSpacer"))
|
|
||||||
tabbedContainer.rightSpacer.SetTag("right", true)
|
|
||||||
|
|
||||||
tabbedContainer.ClearTabs()
|
|
||||||
tabbedContainer.setTabRowLayout()
|
|
||||||
return tabbedContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBox returns the underlying box.
|
|
||||||
func (this *TabbedContainer) GetBox () tomo.Box {
|
|
||||||
return this.box
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activate switches to a named tab.
|
|
||||||
func (this *TabbedContainer) Activate (name string) {
|
|
||||||
if _, tab := this.findTab(this.active); tab != nil {
|
|
||||||
tab.setActive(false)
|
|
||||||
this.box.Remove(tab.root)
|
|
||||||
}
|
|
||||||
if _, tab := this.findTab(name); tab != nil {
|
|
||||||
tab.setActive(true)
|
|
||||||
this.box.Add(tab.root)
|
|
||||||
} else {
|
|
||||||
name = ""
|
|
||||||
}
|
|
||||||
this.active = name
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddTab adds an object as a tab with the specified name.
|
|
||||||
func (this *TabbedContainer) AddTab (name string, root tomo.Object) {
|
|
||||||
tab := &tab {
|
|
||||||
TextBox: tomo.NewTextBox(),
|
|
||||||
name: name,
|
|
||||||
root: root,
|
|
||||||
}
|
|
||||||
tab.SetRole(tomo.R("objects", "Tab"))
|
|
||||||
tab.SetText(name)
|
|
||||||
tab.OnButtonDown(func (button input.Button) bool {
|
|
||||||
if button != input.ButtonLeft { return false }
|
|
||||||
this.Activate(name)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
tab.OnButtonUp(func (button input.Button) bool {
|
|
||||||
if button != input.ButtonLeft { return false }
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
this.tabs = append(this.tabs, tab)
|
|
||||||
this.tabsRow.Insert(tab, this.rightSpacer)
|
|
||||||
this.setTabRowLayout()
|
|
||||||
|
|
||||||
// if the row was empty before, activate this tab
|
|
||||||
if len(this.tabs) == 1 {
|
|
||||||
this.Activate(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveTab removes the named tab.
|
|
||||||
func (this *TabbedContainer) RemoveTab (name string) {
|
|
||||||
index, tab := this.findTab(name)
|
|
||||||
if index < 0 { return }
|
|
||||||
nextIndex := index - 1
|
|
||||||
|
|
||||||
this.tabsRow.Remove(tab)
|
|
||||||
this.tabs = append(this.tabs[:index], this.tabs[index - 1:]...)
|
|
||||||
this.setTabRowLayout()
|
|
||||||
|
|
||||||
if nextIndex < 0 { nextIndex = 0 }
|
|
||||||
if nextIndex >= len(this.tabs) { nextIndex = len(this.tabs) - 1 }
|
|
||||||
if nextIndex < 0 {
|
|
||||||
this.Activate("")
|
|
||||||
} else {
|
|
||||||
this.Activate(this.tabs[nextIndex].name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearTabs removes all tabs.
|
|
||||||
func (this *TabbedContainer) ClearTabs () {
|
|
||||||
this.tabs = nil
|
|
||||||
this.tabsRow.Clear()
|
|
||||||
this.tabsRow.Add(this.leftSpacer)
|
|
||||||
this.tabsRow.Add(this.rightSpacer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this *TabbedContainer) setTabRowLayout () {
|
|
||||||
row := make(layouts.Row, 1 + len(this.tabs) + 1)
|
|
||||||
row[len(row) - 1] = true
|
|
||||||
this.tabsRow.SetAttr(tomo.ALayout(row))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this *TabbedContainer) findTab (name string) (int, *tab) {
|
|
||||||
for index, tab := range this.tabs {
|
|
||||||
if tab.name == name { return index, tab }
|
|
||||||
}
|
|
||||||
return -1, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type tab struct {
|
|
||||||
tomo.TextBox
|
|
||||||
name string
|
|
||||||
root tomo.Object
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this *tab) setActive (active bool) {
|
|
||||||
if active {
|
|
||||||
this.SetRole(tomo.R("objects", "Tab"))
|
|
||||||
this.SetTag("active", true)
|
|
||||||
} else {
|
|
||||||
this.SetRole(tomo.R("objects", "Tab"))
|
|
||||||
this.SetTag("active", false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
125
textinput.go
125
textinput.go
@ -1,19 +1,30 @@
|
|||||||
package objects
|
package objects
|
||||||
|
|
||||||
|
import "time"
|
||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/tomo/tomo"
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
import "git.tebibyte.media/tomo/tomo/text"
|
import "git.tebibyte.media/tomo/tomo/text"
|
||||||
import "git.tebibyte.media/tomo/tomo/input"
|
import "git.tebibyte.media/tomo/tomo/input"
|
||||||
import "git.tebibyte.media/tomo/tomo/event"
|
import "git.tebibyte.media/tomo/tomo/event"
|
||||||
|
import "git.tebibyte.media/tomo/objects/internal"
|
||||||
|
|
||||||
|
const textInputHistoryMaximum = 64
|
||||||
|
const textInputHistoryMaxAge = time.Second / 4
|
||||||
var _ tomo.ContentObject = new(TextInput)
|
var _ tomo.ContentObject = new(TextInput)
|
||||||
|
|
||||||
|
type textHistoryItem struct {
|
||||||
|
text string
|
||||||
|
dot text.Dot
|
||||||
|
}
|
||||||
|
|
||||||
// TextInput is a single-line editable text box.
|
// TextInput is a single-line editable text box.
|
||||||
type TextInput struct {
|
type TextInput struct {
|
||||||
box tomo.TextBox
|
box tomo.TextBox
|
||||||
text []rune
|
text []rune
|
||||||
multiline bool
|
multiline bool
|
||||||
|
history *internal.History[textHistoryItem]
|
||||||
on struct {
|
on struct {
|
||||||
|
dotChange event.FuncBroadcaster
|
||||||
valueChange event.FuncBroadcaster
|
valueChange event.FuncBroadcaster
|
||||||
confirm event.FuncBroadcaster
|
confirm event.FuncBroadcaster
|
||||||
}
|
}
|
||||||
@ -22,7 +33,11 @@ type TextInput struct {
|
|||||||
func newTextInput (text string, multiline bool) *TextInput {
|
func newTextInput (text string, multiline bool) *TextInput {
|
||||||
textInput := &TextInput {
|
textInput := &TextInput {
|
||||||
box: tomo.NewTextBox(),
|
box: tomo.NewTextBox(),
|
||||||
|
text: []rune(text),
|
||||||
multiline: multiline,
|
multiline: multiline,
|
||||||
|
history: internal.NewHistory[textHistoryItem] (
|
||||||
|
textHistoryItem { text: text },
|
||||||
|
textInputHistoryMaximum),
|
||||||
}
|
}
|
||||||
textInput.box.SetRole(tomo.R("objects", "TextInput"))
|
textInput.box.SetRole(tomo.R("objects", "TextInput"))
|
||||||
textInput.box.SetTag("multiline", multiline)
|
textInput.box.SetTag("multiline", multiline)
|
||||||
@ -34,12 +49,13 @@ func newTextInput (text string, multiline bool) *TextInput {
|
|||||||
textInput.box.SetAttr(tomo.AOverflow(true, false))
|
textInput.box.SetAttr(tomo.AOverflow(true, false))
|
||||||
textInput.box.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle))
|
textInput.box.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle))
|
||||||
}
|
}
|
||||||
textInput.SetValue(text)
|
textInput.box.SetText(text)
|
||||||
textInput.box.SetFocusable(true)
|
textInput.box.SetFocusable(true)
|
||||||
textInput.box.SetSelectable(true)
|
textInput.box.SetSelectable(true)
|
||||||
textInput.box.OnKeyDown(textInput.handleKeyDown)
|
textInput.box.OnKeyDown(textInput.handleKeyDown)
|
||||||
textInput.box.OnKeyUp(textInput.handleKeyUp)
|
textInput.box.OnKeyUp(textInput.handleKeyUp)
|
||||||
textInput.box.OnScroll(textInput.handleScroll)
|
textInput.box.OnScroll(textInput.handleScroll)
|
||||||
|
textInput.box.OnDotChange(textInput.handleDotChange)
|
||||||
return textInput
|
return textInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +85,7 @@ func (this *TextInput) SetFocused (focused bool) {
|
|||||||
// Select sets the text cursor or selection.
|
// Select sets the text cursor or selection.
|
||||||
func (this *TextInput) Select (dot text.Dot) {
|
func (this *TextInput) Select (dot text.Dot) {
|
||||||
this.box.Select(dot)
|
this.box.Select(dot)
|
||||||
|
this.historySwapDot()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dot returns the text cursor or selection.
|
// Dot returns the text cursor or selection.
|
||||||
@ -79,7 +96,7 @@ func (this *TextInput) Dot () text.Dot {
|
|||||||
// OnDotChange specifies a function to be called when the text cursor or
|
// OnDotChange specifies a function to be called when the text cursor or
|
||||||
// selection changes.
|
// selection changes.
|
||||||
func (this *TextInput) OnDotChange (callback func ()) event.Cookie {
|
func (this *TextInput) OnDotChange (callback func ()) event.Cookie {
|
||||||
return this.box.OnDotChange(callback)
|
return this.on.dotChange.Connect(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAlign sets the X and Y alignment of the text input.
|
// SetAlign sets the X and Y alignment of the text input.
|
||||||
@ -109,6 +126,7 @@ func (this *TextInput) OnContentBoundsChange (callback func ()) event.Cookie {
|
|||||||
func (this *TextInput) SetValue (text string) {
|
func (this *TextInput) SetValue (text string) {
|
||||||
this.text = []rune(text)
|
this.text = []rune(text)
|
||||||
this.box.SetText(text)
|
this.box.SetText(text)
|
||||||
|
this.logLargeAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Value returns the text content of the input.
|
// Value returns the text content of the input.
|
||||||
@ -128,39 +146,89 @@ func (this *TextInput) OnValueChange (callback func ()) event.Cookie {
|
|||||||
return this.on.valueChange.Connect(callback)
|
return this.on.valueChange.Connect(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Undo undoes the last action.
|
||||||
|
func (this *TextInput) Undo () {
|
||||||
|
this.recoverHistoryItem(this.history.Undo())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redo redoes the last previously undone action.
|
||||||
|
func (this *TextInput) Redo () {
|
||||||
|
this.recoverHistoryItem(this.history.Redo())
|
||||||
|
}
|
||||||
|
|
||||||
// Type types a character at the current dot position.
|
// Type types a character at the current dot position.
|
||||||
func (this *TextInput) Type (char rune) {
|
func (this *TextInput) Type (char rune) {
|
||||||
dot := this.Dot()
|
dot := this.Dot()
|
||||||
|
this.historySwapDot()
|
||||||
this.text, dot = text.Type(this.text, dot, rune(char))
|
this.text, dot = text.Type(this.text, dot, rune(char))
|
||||||
this.Select(dot)
|
this.Select(dot)
|
||||||
this.box.SetText(string(this.text))
|
this.box.SetText(string(this.text))
|
||||||
|
this.logKeystroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add up/down controls if this is a multiline input
|
func (this *TextInput) logKeystroke () {
|
||||||
|
if this.Dot().Empty() {
|
||||||
|
this.history.PushWeak (
|
||||||
|
this.currentHistoryState(),
|
||||||
|
textInputHistoryMaxAge)
|
||||||
|
} else {
|
||||||
|
this.logLargeAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *TextInput) logLargeAction () {
|
||||||
|
this.history.Push(this.currentHistoryState())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *TextInput) historySwapDot () {
|
||||||
|
top := this.history.Top()
|
||||||
|
top.dot = this.Dot()
|
||||||
|
this.history.SwapSilently(top)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *TextInput) currentHistoryState () textHistoryItem {
|
||||||
|
return textHistoryItem {
|
||||||
|
text: string(this.text),
|
||||||
|
dot: this.Dot(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *TextInput) recoverHistoryItem (item textHistoryItem) {
|
||||||
|
this.box.SetText(item.text)
|
||||||
|
this.text = []rune(item.text)
|
||||||
|
this.box.Select(item.dot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add things like alt+up/down to move lines
|
||||||
|
|
||||||
func (this *TextInput) handleKeyDown (key input.Key, numpad bool) bool {
|
func (this *TextInput) handleKeyDown (key input.Key, numpad bool) bool {
|
||||||
dot := this.Dot()
|
dot := this.Dot()
|
||||||
modifiers := this.box.Window().Modifiers()
|
txt := this.text
|
||||||
word := modifiers.Control
|
modifiers := this.box.Window().Modifiers()
|
||||||
changed := false
|
word := modifiers.Control()
|
||||||
|
changed := false
|
||||||
|
|
||||||
defer func () {
|
defer func () {
|
||||||
this.Select(dot)
|
|
||||||
if changed {
|
if changed {
|
||||||
this.box.SetText(string(this.text))
|
this.historySwapDot()
|
||||||
|
this.text = txt
|
||||||
|
this.box.SetText(string(txt))
|
||||||
|
this.box.Select(dot)
|
||||||
this.on.valueChange.Broadcast()
|
this.on.valueChange.Broadcast()
|
||||||
|
this.on.dotChange.Broadcast()
|
||||||
|
this.logKeystroke()
|
||||||
}
|
}
|
||||||
} ()
|
} ()
|
||||||
|
|
||||||
typ := func () {
|
typeRune := func () {
|
||||||
this.text, dot = text.Type(this.text, dot, rune(key))
|
txt, dot = text.Type(txt, dot, rune(key))
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if this.multiline && !modifiers.Control {
|
if this.multiline && !modifiers.Control() {
|
||||||
switch {
|
switch {
|
||||||
case key == '\n', key == '\t':
|
case key == '\n', key == '\t':
|
||||||
typ()
|
typeRune()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,15 +238,25 @@ func (this *TextInput) handleKeyDown (key input.Key, numpad bool) bool {
|
|||||||
this.on.confirm.Broadcast()
|
this.on.confirm.Broadcast()
|
||||||
return true
|
return true
|
||||||
case key == input.KeyBackspace:
|
case key == input.KeyBackspace:
|
||||||
this.text, dot = text.Backspace(this.text, dot, word)
|
txt, dot = text.Backspace(txt, dot, word)
|
||||||
changed = true
|
changed = true
|
||||||
return true
|
return true
|
||||||
case key == input.KeyDelete:
|
case key == input.KeyDelete:
|
||||||
this.text, dot = text.Delete(this.text, dot, word)
|
txt, dot = text.Delete(txt, dot, word)
|
||||||
changed = true
|
changed = true
|
||||||
return true
|
return true
|
||||||
case key.Printable() && !modifiers.Control:
|
case key.Printable() && !modifiers.Control():
|
||||||
typ()
|
typeRune()
|
||||||
|
return true
|
||||||
|
case key == 'z' || key == 'Z' && modifiers.Control():
|
||||||
|
if modifiers.Shift() {
|
||||||
|
this.Redo()
|
||||||
|
} else {
|
||||||
|
this.Undo()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case key == 'y' && modifiers.Control():
|
||||||
|
this.Redo()
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@ -188,7 +266,7 @@ func (this *TextInput) handleKeyDown (key input.Key, numpad bool) bool {
|
|||||||
func (this *TextInput) handleKeyUp (key input.Key, numpad bool) bool {
|
func (this *TextInput) handleKeyUp (key input.Key, numpad bool) bool {
|
||||||
modifiers := this.box.Window().Modifiers()
|
modifiers := this.box.Window().Modifiers()
|
||||||
|
|
||||||
if this.multiline && !modifiers.Control {
|
if this.multiline && !modifiers.Control() {
|
||||||
switch {
|
switch {
|
||||||
case key == '\n', key == '\t':
|
case key == '\n', key == '\t':
|
||||||
return true
|
return true
|
||||||
@ -202,7 +280,11 @@ func (this *TextInput) handleKeyUp (key input.Key, numpad bool) bool {
|
|||||||
return true
|
return true
|
||||||
case key == input.KeyDelete:
|
case key == input.KeyDelete:
|
||||||
return true
|
return true
|
||||||
case key.Printable() && !modifiers.Control:
|
case key.Printable() && !modifiers.Control():
|
||||||
|
return true
|
||||||
|
case key == 'z' && modifiers.Control():
|
||||||
|
return true
|
||||||
|
case key == 'y' && modifiers.Control():
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@ -214,3 +296,8 @@ func (this *TextInput) handleScroll (x, y float64) bool {
|
|||||||
this.ScrollTo(this.ContentBounds().Min.Sub(image.Pt(int(x), int(y))))
|
this.ScrollTo(this.ContentBounds().Min.Sub(image.Pt(int(x), int(y))))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (this *TextInput) handleDotChange () {
|
||||||
|
this.historySwapDot()
|
||||||
|
this.on.dotChange.Broadcast()
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@ import "git.tebibyte.media/tomo/tomo"
|
|||||||
import "git.tebibyte.media/tomo/tomo/text"
|
import "git.tebibyte.media/tomo/tomo/text"
|
||||||
import "git.tebibyte.media/tomo/tomo/event"
|
import "git.tebibyte.media/tomo/tomo/event"
|
||||||
|
|
||||||
var _ tomo.Object = new(TabbedContainer)
|
var _ tomo.Object = new(TextView)
|
||||||
|
|
||||||
// TextView is an area for displaying a large amount of multi-line text.
|
// TextView is an area for displaying a large amount of multi-line text.
|
||||||
type TextView struct {
|
type TextView struct {
|
||||||
|
Loading…
Reference in New Issue
Block a user