Compare commits
44 Commits
2b354979aa
...
main
| 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 | |||
| 2727972c30 | |||
| e48933385e | |||
| b9c4e3c003 | |||
| 92e4eb970d | |||
| c7887c5ea4 | |||
| 30d4e208b1 | |||
| a688e2dc24 | |||
| e1cef9bb37 | |||
| 6089dd3ff1 | |||
| d4e8847908 | |||
| 82cf822602 |
27
README.md
27
README.md
@@ -1,6 +1,29 @@
|
|||||||
# objects
|
# objects
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
[](https://pkg.go.dev/git.tebibyte.media/tomo/objects)
|
[](https://pkg.go.dev/git.tebibyte.media/tomo/objects)
|
||||||
|
|
||||||
Objects contains a standard collection of re-usable objects. All objects in this
|
Objects contains a standard collection of re-usable objects. It should also be
|
||||||
module visually conform to whatever the theme is set to.
|
viewed as a reference for how to create custom objects in Tomo.
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
All objects in this module have roles of the form:
|
||||||
|
|
||||||
|
```
|
||||||
|
objects.TypeName
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `TypeName` is the exact Go type name of the object in question. Objects
|
||||||
|
may also have different tags to indicate variations, states, etc. If applicable,
|
||||||
|
they are listed and described in the doc comment for the object's type. More
|
||||||
|
complex objects may have sub-components that are not accessible from the API.
|
||||||
|
These are listed alongside the tags.
|
||||||
|
|
||||||
|
## Setting Attributes
|
||||||
|
It is generally not recommended to set attributes on these objects. However, if
|
||||||
|
you must, they can be set by obtaining the object's underlying box through the
|
||||||
|
`GetBox` method. Be aware that the exact type of box that is returned here is
|
||||||
|
not part of the API, and may change unexpectedly even after v1.0. This caveat
|
||||||
|
also applies to boxes/sub-components making up the internal composition of the
|
||||||
|
objects.
|
||||||
|
|||||||
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 |
@@ -12,6 +12,9 @@ var iconButtonLayout = layouts.Row { true }
|
|||||||
var bothButtonLayout = layouts.Row { false, true }
|
var bothButtonLayout = layouts.Row { false, true }
|
||||||
|
|
||||||
// Button is a clickable button.
|
// Button is a clickable button.
|
||||||
|
//
|
||||||
|
// Tags:
|
||||||
|
// - [icon] The button has an icon.
|
||||||
type Button struct {
|
type Button struct {
|
||||||
box tomo.ContainerBox
|
box tomo.ContainerBox
|
||||||
|
|
||||||
|
|||||||
12
calendar.go
12
calendar.go
@@ -10,6 +10,16 @@ var _ tomo.Object = new(Calendar)
|
|||||||
|
|
||||||
// Calendar is an object that can display a date and allow the user to change
|
// Calendar is an object that can display a date and allow the user to change
|
||||||
// it. It can display one month at a time.
|
// it. It can display one month at a time.
|
||||||
|
//
|
||||||
|
// Sub-components:
|
||||||
|
// - CalendarGrid organizes the days into a grid.
|
||||||
|
// - CalendarWeekdayHeader appears at the top of each grid column, and shows
|
||||||
|
// the day of the week that column represents.
|
||||||
|
// - CalendarDay appears within the grid for each day of the current month.
|
||||||
|
//
|
||||||
|
// CalendarDay tags:
|
||||||
|
// - [weekend] The day is a weekend.
|
||||||
|
// - [weekday] The day is a weekday.
|
||||||
type Calendar struct {
|
type Calendar struct {
|
||||||
box tomo.ContainerBox
|
box tomo.ContainerBox
|
||||||
|
|
||||||
@@ -50,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)
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import "git.tebibyte.media/tomo/tomo/event"
|
|||||||
var _ tomo.Object = new(Checkbox)
|
var _ tomo.Object = new(Checkbox)
|
||||||
|
|
||||||
// Checkbox is a control that can be toggled.
|
// Checkbox is a control that can be toggled.
|
||||||
|
//
|
||||||
|
// Tags:
|
||||||
|
// - [checked] The checkbox's value is true.
|
||||||
|
// - [unchecked] The checkbox's value is false.
|
||||||
type Checkbox struct {
|
type Checkbox struct {
|
||||||
box tomo.Box
|
box tomo.Box
|
||||||
value bool
|
value bool
|
||||||
|
|||||||
@@ -6,15 +6,19 @@ 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)
|
||||||
|
|
||||||
// HSVAColorPicker allows the user to pick a color by controlling its HSVA
|
// HSVAColorPicker allows the user to pick a color by controlling its HSVA
|
||||||
// parameters.
|
// parameters.
|
||||||
|
//
|
||||||
|
// Sub-components:
|
||||||
|
// - ColorPickerMap is a rectangular control where the X axis controls
|
||||||
|
// 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
|
||||||
@@ -76,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()
|
||||||
@@ -148,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()),
|
||||||
|
|||||||
123
container.go
123
container.go
@@ -1,128 +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. The container
|
|
||||||
// will have a corresponding object role variation of either "outer", "inner",
|
|
||||||
// or "sunken".
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
129
file.go
Normal file
129
file.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
import "unicode"
|
||||||
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/data"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/input"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/event"
|
||||||
|
import "git.tebibyte.media/tomo/objects/layouts"
|
||||||
|
|
||||||
|
var _ tomo.Object = new(File)
|
||||||
|
|
||||||
|
// File is a representation of a file or directory.
|
||||||
|
type File struct {
|
||||||
|
box tomo.ContainerBox
|
||||||
|
|
||||||
|
label *Label
|
||||||
|
icon *MimeIcon
|
||||||
|
mime data.Mime
|
||||||
|
|
||||||
|
lastClick time.Time
|
||||||
|
|
||||||
|
on struct {
|
||||||
|
doubleClick event.FuncBroadcaster
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFile creates a new file icon with the given name and MIME type.
|
||||||
|
func NewFile (name string, mime data.Mime) *File {
|
||||||
|
file := &File {
|
||||||
|
box: tomo.NewContainerBox(),
|
||||||
|
label: NewLabel(""),
|
||||||
|
icon: NewMimeIcon(mime, tomo.IconSizeLarge),
|
||||||
|
}
|
||||||
|
file.box.SetRole(tomo.R("objects", "File"))
|
||||||
|
file.box.SetAttr(tomo.ALayout(layouts.ContractVertical))
|
||||||
|
|
||||||
|
file.box.Add(file.icon)
|
||||||
|
file.box.Add(file.label)
|
||||||
|
file.box.SetAttr(tomo.AAlign(tomo.AlignMiddle, tomo.AlignStart))
|
||||||
|
file.label.SetAlign(tomo.AlignMiddle, tomo.AlignStart)
|
||||||
|
// file.label.SetOverflow(false, true)
|
||||||
|
// file.label.SetWrap(true)
|
||||||
|
file.SetType(mime)
|
||||||
|
file.SetName(name)
|
||||||
|
|
||||||
|
file.box.SetInputMask(true)
|
||||||
|
file.box.SetFocusable(true)
|
||||||
|
file.box.OnButtonDown(file.handleButtonDown)
|
||||||
|
file.box.OnButtonUp(file.handleButtonUp)
|
||||||
|
file.box.OnKeyDown(file.handleKeyDown)
|
||||||
|
file.box.OnKeyUp(file.handleKeyUp)
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBox returns the underlying box.
|
||||||
|
func (this *File) GetBox () tomo.Box {
|
||||||
|
return this.box
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFocused sets whether or not this file has keyboard focus. If set to
|
||||||
|
// true, this method will steal focus away from whichever object currently has
|
||||||
|
// focus.
|
||||||
|
func (this *File) SetFocused (focused bool) {
|
||||||
|
this.box.SetFocused(focused)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetName sets the text of the file's label.
|
||||||
|
func (this *File) SetName (text string) {
|
||||||
|
this.label.SetText(truncateText(text, 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetType sets the MIME type of the file.
|
||||||
|
func (this *File) SetType (mime data.Mime) {
|
||||||
|
this.mime = mime
|
||||||
|
this.icon.SetIcon(mime, tomo.IconSizeLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnDoubleClick specifies a function to be called when the file is
|
||||||
|
// double-clicked.
|
||||||
|
func (this *File) OnDoubleClick (callback func ()) event.Cookie {
|
||||||
|
return this.on.doubleClick.Connect(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *File) handleKeyDown (key input.Key, numberPad bool) bool {
|
||||||
|
if key != input.KeyEnter && key != input.Key(' ') { return false }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *File) handleKeyUp (key input.Key, numberPad bool) bool {
|
||||||
|
if key != input.KeyEnter && key != input.Key(' ') { return false }
|
||||||
|
this.on.doubleClick.Broadcast()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *File) handleButtonDown (button input.Button) bool {
|
||||||
|
if button != input.ButtonLeft { return false }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *File) handleButtonUp (button input.Button) bool {
|
||||||
|
if button != input.ButtonLeft { return false }
|
||||||
|
if this.box.Window().MousePosition().In(this.box.Bounds()) {
|
||||||
|
// TODO double click delay should be configurable
|
||||||
|
if time.Since(this.lastClick) < time.Second {
|
||||||
|
this.on.doubleClick.Broadcast()
|
||||||
|
}
|
||||||
|
this.lastClick = time.Now()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateText (text string, max int) string {
|
||||||
|
lastChar := -1
|
||||||
|
len := 0
|
||||||
|
for index, char := range text {
|
||||||
|
if !unicode.IsSpace(char) {
|
||||||
|
lastChar = index
|
||||||
|
}
|
||||||
|
len ++
|
||||||
|
if len >= max {
|
||||||
|
if lastChar != -1 {
|
||||||
|
return text[:lastChar] + "..."
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
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=
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ var _ tomo.Object = new(Heading)
|
|||||||
|
|
||||||
// Heading is a label that denotes the start of some section of content. It can
|
// Heading is a label that denotes the start of some section of content. It can
|
||||||
// have a level from 0 to 2, with 0 being the most prominent and 2 being the
|
// have a level from 0 to 2, with 0 being the most prominent and 2 being the
|
||||||
// most subtle. The level is described in the role variation.
|
// most subtle.
|
||||||
|
//
|
||||||
|
// Tags:
|
||||||
|
// - [0] The heading has a level of 0 (most prominent).
|
||||||
|
// - [1] The heading has a level of 1.
|
||||||
|
// - [2] The heading has a level of 2 (least prominent).
|
||||||
type Heading struct {
|
type Heading struct {
|
||||||
box tomo.TextBox
|
box tomo.TextBox
|
||||||
}
|
}
|
||||||
|
|||||||
5
icon.go
5
icon.go
@@ -6,6 +6,11 @@ import "git.tebibyte.media/tomo/tomo/canvas"
|
|||||||
var _ tomo.Object = new(Icon)
|
var _ tomo.Object = new(Icon)
|
||||||
|
|
||||||
// Icon displays a single icon.
|
// Icon displays a single icon.
|
||||||
|
//
|
||||||
|
// Tags:
|
||||||
|
// - [large] The icon is large sized.
|
||||||
|
// - [medium] The icon is medium sized.
|
||||||
|
// - [small] The icon is small sized.
|
||||||
type Icon struct {
|
type Icon struct {
|
||||||
box tomo.Box
|
box tomo.Box
|
||||||
icon tomo.Icon
|
icon tomo.Icon
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
17
label.go
17
label.go
@@ -33,7 +33,7 @@ func (this *Label) SetFocused (focused bool) {
|
|||||||
this.box.SetFocused(focused)
|
this.box.SetFocused(focused)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetText sets the text content of the heading.
|
// SetText sets the text content of the label.
|
||||||
func (this *Label) SetText (text string) {
|
func (this *Label) SetText (text string) {
|
||||||
this.box.SetText(text)
|
this.box.SetText(text)
|
||||||
}
|
}
|
||||||
@@ -48,13 +48,18 @@ func (this *Label) Dot () text.Dot {
|
|||||||
return this.box.Dot()
|
return this.box.Dot()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAlign sets the X and Y alignment of the label.
|
|
||||||
func (this *Label) SetAlign (x, y tomo.Align) {
|
|
||||||
this.box.SetAttr(tomo.AAlign(x, y))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 *Label) OnDotChange (callback func ()) event.Cookie {
|
func (this *Label) OnDotChange (callback func ()) event.Cookie {
|
||||||
return this.box.OnDotChange(callback)
|
return this.box.OnDotChange(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetAlign sets the X and Y alignment of the label.
|
||||||
|
func (this *Label) SetAlign (x, y tomo.Align) {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|||||||
13
menu.go
13
menu.go
@@ -7,6 +7,13 @@ import "git.tebibyte.media/tomo/tomo/input"
|
|||||||
import "git.tebibyte.media/tomo/objects/layouts"
|
import "git.tebibyte.media/tomo/objects/layouts"
|
||||||
|
|
||||||
// Menu is a menu window.
|
// Menu is a menu window.
|
||||||
|
//
|
||||||
|
// 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,
|
||||||
|
// causes the menu to be "torn off" into a movable window.
|
||||||
type Menu struct {
|
type Menu struct {
|
||||||
tomo.Window
|
tomo.Window
|
||||||
|
|
||||||
@@ -36,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
|
||||||
|
|
||||||
@@ -58,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)
|
||||||
@@ -71,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -75,13 +75,13 @@ func (this *NumberInput) Dot () text.Dot {
|
|||||||
|
|
||||||
// Value returns the value of the input.
|
// Value returns the value of the input.
|
||||||
func (this *NumberInput) Value () float64 {
|
func (this *NumberInput) Value () float64 {
|
||||||
value, _ := strconv.ParseFloat(this.input.Text(), 64)
|
value, _ := strconv.ParseFloat(this.input.Value(), 64)
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetValue sets the value of the input.
|
// SetValue sets the value of the input.
|
||||||
func (this *NumberInput) SetValue (value float64) {
|
func (this *NumberInput) SetValue (value float64) {
|
||||||
this.input.SetText(strconv.FormatFloat(value, 'g', -1, 64))
|
this.input.SetValue(strconv.FormatFloat(value, 'g', -1, 64))
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnValueChange specifies a function to be called when the user edits the input
|
// OnValueChange specifies a function to be called when the user edits the input
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
18
scrollbar.go
18
scrollbar.go
@@ -9,6 +9,17 @@ var _ tomo.Object = new(Scrollbar)
|
|||||||
|
|
||||||
// Scrollbar is a special type of slider that can control the scroll of any
|
// Scrollbar is a special type of slider that can control the scroll of any
|
||||||
// overflowing ContainerObject.
|
// overflowing ContainerObject.
|
||||||
|
//
|
||||||
|
// Sub-components:
|
||||||
|
// - ScrollbarHandle is the grabbable handle of the scrollbar.
|
||||||
|
//
|
||||||
|
// Tags:
|
||||||
|
// - [vertical] The scrollbar is oriented vertically.
|
||||||
|
// - [horizontall] The scrollbar is oriented horizontally.
|
||||||
|
//
|
||||||
|
// ScrollbarHandle tags:
|
||||||
|
// - [vertical] The handle is oriented vertically.
|
||||||
|
// - [horizontall] The handle is oriented horizontally.
|
||||||
type Scrollbar struct {
|
type Scrollbar struct {
|
||||||
box tomo.ContainerBox
|
box tomo.ContainerBox
|
||||||
handle *sliderHandle
|
handle *sliderHandle
|
||||||
@@ -161,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())
|
||||||
@@ -309,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
|
||||||
|
}
|
||||||
15
slider.go
15
slider.go
@@ -9,6 +9,17 @@ import "git.tebibyte.media/tomo/tomo/event"
|
|||||||
var _ tomo.Object = new(Slider)
|
var _ tomo.Object = new(Slider)
|
||||||
|
|
||||||
// Slider is a control that selects a numeric value between 0 and 1.
|
// Slider is a control that selects a numeric value between 0 and 1.
|
||||||
|
//
|
||||||
|
// Sub-components:
|
||||||
|
// - SliderHandle is the grabbable handle of the slider.
|
||||||
|
//
|
||||||
|
// Tags:
|
||||||
|
// - [vertical] The slider is oriented vertically.
|
||||||
|
// - [horizontall] The slider is oriented horizontally.
|
||||||
|
//
|
||||||
|
// SliderHandle tags:
|
||||||
|
// - [vertical] The handle is oriented vertically.
|
||||||
|
// - [horizontall] The handle is oriented horizontally.
|
||||||
type Slider struct {
|
type Slider struct {
|
||||||
box tomo.ContainerBox
|
box tomo.ContainerBox
|
||||||
handle *sliderHandle
|
handle *sliderHandle
|
||||||
@@ -116,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)
|
||||||
@@ -124,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,149 +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.
|
|
||||||
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("left", 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
180
textinput.go
180
textinput.go
@@ -1,36 +1,73 @@
|
|||||||
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
|
||||||
|
history *internal.History[textHistoryItem]
|
||||||
on struct {
|
on struct {
|
||||||
|
dotChange event.FuncBroadcaster
|
||||||
valueChange event.FuncBroadcaster
|
valueChange event.FuncBroadcaster
|
||||||
confirm event.FuncBroadcaster
|
confirm event.FuncBroadcaster
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTextInput creates a new text input containing the specified text.
|
func newTextInput (text string, multiline bool) *TextInput {
|
||||||
func NewTextInput (text string) *TextInput {
|
textInput := &TextInput {
|
||||||
textInput := &TextInput { box: tomo.NewTextBox() }
|
box: tomo.NewTextBox(),
|
||||||
|
text: []rune(text),
|
||||||
|
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.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle))
|
textInput.box.SetTag("multiline", multiline)
|
||||||
textInput.box.SetAttr(tomo.AOverflow(true, false))
|
if multiline {
|
||||||
textInput.SetValue(text)
|
textInput.box.SetAttr(tomo.AOverflow(false, true))
|
||||||
|
textInput.box.SetAttr(tomo.AWrap(true))
|
||||||
|
textInput.box.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignStart))
|
||||||
|
} else {
|
||||||
|
textInput.box.SetAttr(tomo.AOverflow(true, false))
|
||||||
|
textInput.box.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle))
|
||||||
|
}
|
||||||
|
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)
|
||||||
return textInput
|
textInput.box.OnDotChange(textInput.handleDotChange)
|
||||||
|
return textInput
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTextInput creates a new text input containing the specified text.
|
||||||
|
func NewTextInput (text string) *TextInput {
|
||||||
|
return newTextInput(text, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMultilineTextInput creates a new multiline text input containing the
|
||||||
|
// specified text.
|
||||||
|
func NewMultilineTextInput (text string) *TextInput {
|
||||||
|
return newTextInput(text, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBox returns the underlying box.
|
// GetBox returns the underlying box.
|
||||||
@@ -48,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.
|
||||||
@@ -55,7 +93,13 @@ func (this *TextInput) Dot () text.Dot {
|
|||||||
return this.box.Dot()
|
return this.box.Dot()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAlign sets the X and Y alignment of the label.
|
// OnDotChange specifies a function to be called when the text cursor or
|
||||||
|
// selection changes.
|
||||||
|
func (this *TextInput) OnDotChange (callback func ()) event.Cookie {
|
||||||
|
return this.on.dotChange.Connect(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAlign sets the X and Y alignment of the text input.
|
||||||
func (this *TextInput) SetAlign (x, y tomo.Align) {
|
func (this *TextInput) SetAlign (x, y tomo.Align) {
|
||||||
this.box.SetAttr(tomo.AAlign(x, y))
|
this.box.SetAttr(tomo.AAlign(x, y))
|
||||||
}
|
}
|
||||||
@@ -82,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.
|
||||||
@@ -101,43 +146,117 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
} ()
|
} ()
|
||||||
|
|
||||||
|
typeRune := func () {
|
||||||
|
txt, dot = text.Type(txt, dot, rune(key))
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if this.multiline && !modifiers.Control() {
|
||||||
|
switch {
|
||||||
|
case key == '\n', key == '\t':
|
||||||
|
typeRune()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case isConfirmationKey(key):
|
case isConfirmationKey(key):
|
||||||
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():
|
||||||
this.text, dot = text.Type(this.text, dot, rune(key))
|
typeRune()
|
||||||
changed = true
|
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
|
||||||
@@ -146,6 +265,14 @@ 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() {
|
||||||
|
switch {
|
||||||
|
case key == '\n', key == '\t':
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case isConfirmationKey(key):
|
case isConfirmationKey(key):
|
||||||
return true
|
return true
|
||||||
@@ -153,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
|
||||||
@@ -165,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()
|
||||||
|
}
|
||||||
|
|||||||
80
textview.go
80
textview.go
@@ -2,23 +2,83 @@ package objects
|
|||||||
|
|
||||||
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/event"
|
||||||
|
|
||||||
|
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 {
|
||||||
tomo.TextBox
|
box tomo.TextBox
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTextView creates a new text view.
|
// NewTextView creates a new text view.
|
||||||
func NewTextView (text string) *TextView {
|
func NewTextView (text string) *TextView {
|
||||||
this := &TextView { TextBox: tomo.NewTextBox() }
|
textView := &TextView { box: tomo.NewTextBox() }
|
||||||
this.SetRole(tomo.R("objects", "TextView"))
|
textView.box.SetRole(tomo.R("objects", "TextView"))
|
||||||
this.SetFocusable(true)
|
textView.box.SetFocusable(true)
|
||||||
this.SetSelectable(true)
|
textView.box.SetSelectable(true)
|
||||||
this.SetText(text)
|
textView.SetText(text)
|
||||||
this.SetAttr(tomo.AOverflow(false, true))
|
textView.box.SetAttr(tomo.AOverflow(false, true))
|
||||||
this.SetAttr(tomo.AWrap(true))
|
textView.box.SetAttr(tomo.AWrap(true))
|
||||||
this.OnScroll(this.handleScroll)
|
textView.box.OnScroll(textView.handleScroll)
|
||||||
return this
|
return textView
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBox returns the underlying box.
|
||||||
|
func (this *TextView) GetBox () tomo.Box {
|
||||||
|
return this.box
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFocused sets whether or not this text view has keyboard focus. If set to
|
||||||
|
// true, this method will steal focus away from whichever object currently has
|
||||||
|
// focus.
|
||||||
|
func (this *TextView) SetFocused (focused bool) {
|
||||||
|
this.box.SetFocused(focused)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select sets the text cursor or selection.
|
||||||
|
func (this *TextView) Select (dot text.Dot) {
|
||||||
|
this.box.Select(dot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dot returns the text cursor or selection.
|
||||||
|
func (this *TextView) Dot () text.Dot {
|
||||||
|
return this.box.Dot()
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnDotChange specifies a function to be called when the text cursor or
|
||||||
|
// selection changes.
|
||||||
|
func (this *TextView) OnDotChange (callback func ()) event.Cookie {
|
||||||
|
return this.box.OnDotChange(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAlign sets the X and Y alignment of the text view.
|
||||||
|
func (this *TextView) SetAlign (x, y tomo.Align) {
|
||||||
|
this.box.SetAttr(tomo.AAlign(x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentBounds returns the bounds of the inner content of the text view
|
||||||
|
// relative to the text view's InnerBounds.
|
||||||
|
func (this *TextView) ContentBounds () image.Rectangle {
|
||||||
|
return this.box.ContentBounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollTo shifts the origin of the text view's content to the origin of the
|
||||||
|
// text view's InnerBounds, offset by the given point.
|
||||||
|
func (this *TextView) ScrollTo (position image.Point) {
|
||||||
|
this.box.ScrollTo(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnContentBoundsChange specifies a function to be called when the text view's
|
||||||
|
// ContentBounds or InnerBounds changes.
|
||||||
|
func (this *TextView) OnContentBoundsChange (callback func ()) event.Cookie {
|
||||||
|
return this.box.OnContentBoundsChange(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetText sets the text content of the view.
|
||||||
|
func (this *TextView) SetText (text string) {
|
||||||
|
this.box.SetText(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *TextView) handleScroll (x, y float64) bool {
|
func (this *TextView) handleScroll (x, y float64) bool {
|
||||||
|
|||||||
Reference in New Issue
Block a user