11 Commits

17 changed files with 370 additions and 36 deletions

View File

@@ -2,5 +2,26 @@
[![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. All objects in this
module visually conform to whatever the theme is set to.
Objects contains a standard collection of re-usable objects. It should be viewed
also 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.

View File

@@ -12,6 +12,9 @@ var iconButtonLayout = layouts.Row { true }
var bothButtonLayout = layouts.Row { false, true }
// Button is a clickable button.
//
// Tags:
// - [icon] The button has an icon.
type Button struct {
box tomo.ContainerBox

View File

@@ -10,6 +10,16 @@ var _ tomo.Object = new(Calendar)
// Calendar is an object that can display a date and allow the user to change
// 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 {
box tomo.ContainerBox

View File

@@ -7,6 +7,10 @@ import "git.tebibyte.media/tomo/tomo/event"
var _ tomo.Object = new(Checkbox)
// 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 {
box tomo.Box
value bool

View File

@@ -12,6 +12,10 @@ var _ tomo.Object = new(HSVAColorPicker)
// HSVAColorPicker allows the user to pick a color by controlling its HSVA
// parameters.
//
// Sub-components:
// - ColorPickerMap is a recangular control where the X axis controls
// saturation and the Y axis controls value.
type HSVAColorPicker struct {
box tomo.ContainerBox
value internal.HSVA

View File

@@ -10,9 +10,14 @@ var _ tomo.ContentObject = new(Container)
// primitive for building more complex layouts. It has two main variants: an
// outer container, and an inner container. The outer container has padding
// 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".
// "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 {
box tomo.ContainerBox
}

129
file.go Normal file
View 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
}

View File

@@ -9,7 +9,12 @@ var _ tomo.Object = new(Heading)
// 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
// 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 {
box tomo.TextBox
}

View File

@@ -6,6 +6,11 @@ import "git.tebibyte.media/tomo/tomo/canvas"
var _ tomo.Object = new(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 {
box tomo.Box
icon tomo.Icon

View File

@@ -33,7 +33,7 @@ func (this *Label) SetFocused (focused bool) {
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) {
this.box.SetText(text)
}
@@ -48,13 +48,13 @@ func (this *Label) Dot () text.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
// selection changes.
func (this *Label) OnDotChange (callback func ()) event.Cookie {
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))
}

View File

@@ -7,6 +7,10 @@ import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/objects/layouts"
// Menu is a menu window.
//
// Sub-components:
// - 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 {
tomo.Window

View File

@@ -75,13 +75,13 @@ func (this *NumberInput) Dot () text.Dot {
// Value returns the value of the input.
func (this *NumberInput) Value () float64 {
value, _ := strconv.ParseFloat(this.input.Text(), 64)
value, _ := strconv.ParseFloat(this.input.Value(), 64)
return value
}
// SetValue sets the value of the input.
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

View File

@@ -9,6 +9,17 @@ var _ tomo.Object = new(Scrollbar)
// Scrollbar is a special type of slider that can control the scroll of any
// 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 {
box tomo.ContainerBox
handle *sliderHandle

View File

@@ -9,6 +9,17 @@ import "git.tebibyte.media/tomo/tomo/event"
var _ tomo.Object = new(Slider)
// 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 {
box tomo.ContainerBox
handle *sliderHandle

View File

@@ -8,6 +8,19 @@ 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
@@ -35,7 +48,7 @@ func NewTabbedContainer () *TabbedContainer {
tabbedContainer.leftSpacer.SetTag("left", true)
tabbedContainer.rightSpacer = tomo.NewBox()
tabbedContainer.rightSpacer.SetRole(tomo.R("objects", "TabSpacer"))
tabbedContainer.rightSpacer.SetTag("left", true)
tabbedContainer.rightSpacer.SetTag("right", true)
tabbedContainer.ClearTabs()
tabbedContainer.setTabRowLayout()

View File

@@ -10,27 +10,48 @@ var _ tomo.ContentObject = new(TextInput)
// TextInput is a single-line editable text box.
type TextInput struct {
box tomo.TextBox
text []rune
box tomo.TextBox
text []rune
multiline bool
on struct {
valueChange event.FuncBroadcaster
confirm event.FuncBroadcaster
}
}
// NewTextInput creates a new text input containing the specified text.
func NewTextInput (text string) *TextInput {
textInput := &TextInput { box: tomo.NewTextBox() }
func newTextInput (text string, multiline bool) *TextInput {
textInput := &TextInput {
box: tomo.NewTextBox(),
multiline: multiline,
}
textInput.box.SetRole(tomo.R("objects", "TextInput"))
textInput.box.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle))
textInput.box.SetAttr(tomo.AOverflow(true, false))
textInput.box.SetTag("multiline", multiline)
if multiline {
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.SetValue(text)
textInput.box.SetFocusable(true)
textInput.box.SetSelectable(true)
textInput.box.OnKeyDown(textInput.handleKeyDown)
textInput.box.OnKeyUp(textInput.handleKeyUp)
textInput.box.OnScroll(textInput.handleScroll)
return textInput
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.
@@ -55,7 +76,13 @@ func (this *TextInput) Dot () text.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.box.OnDotChange(callback)
}
// SetAlign sets the X and Y alignment of the text input.
func (this *TextInput) SetAlign (x, y tomo.Align) {
this.box.SetAttr(tomo.AAlign(x, y))
}
@@ -109,6 +136,8 @@ func (this *TextInput) Type (char rune) {
this.box.SetText(string(this.text))
}
// TODO: add up/down controls if this is a multiline input
func (this *TextInput) handleKeyDown (key input.Key, numpad bool) bool {
dot := this.Dot()
modifiers := this.box.Window().Modifiers()
@@ -123,6 +152,19 @@ func (this *TextInput) handleKeyDown (key input.Key, numpad bool) bool {
}
} ()
typ := func () {
this.text, dot = text.Type(this.text, dot, rune(key))
changed = true
}
if this.multiline && !modifiers.Control {
switch {
case key == '\n', key == '\t':
typ()
return true
}
}
switch {
case isConfirmationKey(key):
this.on.confirm.Broadcast()
@@ -136,8 +178,7 @@ func (this *TextInput) handleKeyDown (key input.Key, numpad bool) bool {
changed = true
return true
case key.Printable() && !modifiers.Control:
this.text, dot = text.Type(this.text, dot, rune(key))
changed = true
typ()
return true
default:
return false
@@ -146,6 +187,14 @@ func (this *TextInput) handleKeyDown (key input.Key, numpad bool) bool {
func (this *TextInput) handleKeyUp (key input.Key, numpad bool) bool {
modifiers := this.box.Window().Modifiers()
if this.multiline && !modifiers.Control {
switch {
case key == '\n', key == '\t':
return true
}
}
switch {
case isConfirmationKey(key):
return true

View File

@@ -2,23 +2,83 @@ package objects
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/text"
import "git.tebibyte.media/tomo/tomo/event"
var _ tomo.Object = new(TabbedContainer)
// TextView is an area for displaying a large amount of multi-line text.
type TextView struct {
tomo.TextBox
box tomo.TextBox
}
// NewTextView creates a new text view.
func NewTextView (text string) *TextView {
this := &TextView { TextBox: tomo.NewTextBox() }
this.SetRole(tomo.R("objects", "TextView"))
this.SetFocusable(true)
this.SetSelectable(true)
this.SetText(text)
this.SetAttr(tomo.AOverflow(false, true))
this.SetAttr(tomo.AWrap(true))
this.OnScroll(this.handleScroll)
return this
textView := &TextView { box: tomo.NewTextBox() }
textView.box.SetRole(tomo.R("objects", "TextView"))
textView.box.SetFocusable(true)
textView.box.SetSelectable(true)
textView.SetText(text)
textView.box.SetAttr(tomo.AOverflow(false, true))
textView.box.SetAttr(tomo.AWrap(true))
textView.box.OnScroll(textView.handleScroll)
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 {