70 Commits

Author SHA1 Message Date
2727972c30 File. Why not. 2024-08-27 13:38:35 -04:00
e48933385e You get an OnDotChange! Everypony gets an OnDotChange! 2024-08-25 18:55:43 -04:00
b9c4e3c003 Forgot to wrap the multiline input text haha 2024-08-25 02:49:08 -04:00
92e4eb970d Add multi-line text inputs 2024-08-25 02:47:23 -04:00
c7887c5ea4 Fix tag on right TabSpacer 2024-08-25 02:37:39 -04:00
30d4e208b1 Document all tags and named sub-components
Closes #9
2024-08-25 02:36:05 -04:00
a688e2dc24 README.md tweaks 2024-08-25 01:59:30 -04:00
e1cef9bb37 Improve README.md 2024-08-25 01:58:03 -04:00
6089dd3ff1 TextView no longer embeds tomo.TextBox 2024-08-25 01:40:40 -04:00
d4e8847908 Fix doc comments on Label, TextInput 2024-08-25 01:38:42 -04:00
82cf822602 Update NumberInput to use new TextInput methods 2024-08-25 01:32:44 -04:00
2b354979aa TextInput no longer embeds tomo.TextBox 2024-08-25 01:31:55 -04:00
1a2449d2b7 TabbedContainer no longer embeds tomo.ContainerBox 2024-08-24 22:26:00 -04:00
889c691c40 TabbedContainer no longer embeds tomo.ContainerBox 2024-08-24 22:15:21 -04:00
32eae0bcca Swatch no longer embeds tomo.CanvasBox 2024-08-24 22:12:43 -04:00
d9d758b5fc Slider no longer embeds tomo.ContainerBox 2024-08-24 22:09:31 -04:00
a11bab452b Ensure Separator fulfils tomo.Object 2024-08-24 22:03:35 -04:00
033e9debf6 Separator no longer embeds tomo.Box 2024-08-24 21:52:37 -04:00
8b79fec1bd ScrollContainer no longer embeds ContainerBox 2024-08-24 21:41:16 -04:00
bc175bb5ae Scrollbar no longer embeds tomo.ContainerBox 2024-08-24 21:35:43 -04:00
02fed8ce48 NumberInput no longer embeds tomo.ContainerBox 2024-08-24 20:19:01 -04:00
b784596b4d Update doc comment for Container 2024-08-24 20:13:38 -04:00
694f9127c0 MimeIcon no longer embeds tomo.Box 2024-08-24 20:10:49 -04:00
ae74c3dbf4 MenuItem no longer embeds tomo.ContainerBox 2024-08-24 20:10:37 -04:00
6f8d5cc426 LabelSwatch, LabelCheckbox changing their labels 2024-08-24 20:03:48 -04:00
f0c334c278 LabelSwatch no longer embeds tomo.ContainerBox 2024-08-24 20:02:14 -04:00
6ee2c5669e Ensure Label satisfies tomo.Object 2024-08-24 19:57:48 -04:00
3aa4b12ffe Update other objects to use new methods of Label 2024-08-24 19:56:58 -04:00
c7caa5bcb6 Label no longer embeds tomo.TextBox 2024-08-24 19:52:47 -04:00
0960fe013d LabelCheckbox no longer embeds tomo.ContainerBox 2024-08-24 19:50:29 -04:00
697229d183 Icon no longer embeds tomo.Box 2024-08-24 19:50:20 -04:00
c043f9bf8d Remove Heading.SetSelectable 2024-08-24 19:49:20 -04:00
14f6e175f0 Heading no longer embeds tomo.TextBox 2024-08-24 19:42:25 -04:00
df2e8f1b07 Dropdown no longer embeds tomo.ContainerBox 2024-08-24 19:33:16 -04:00
0c4e098680 HSVAColorPicker no longer embeds tomo.ContainerBox 2024-08-24 19:28:48 -04:00
fc51e7ab9f Checkbox no longer embeds tomo.ContainerBox 2024-08-24 15:41:47 -04:00
4e8823ef9f Calendar no longer embeds tomo.ContainerBox 2024-08-24 15:20:09 -04:00
8de08a9bdc Button no longer embeds tomo.ContainerBox 2024-08-24 15:11:57 -04:00
04f44cea86 Ensure Container satisfies tomo.ContentObject 2024-08-24 15:04:44 -04:00
c889838c9c Container no longer embeds tomo.ContainerBox
Progress on #7
2024-08-24 15:02:17 -04:00
7bcb4cf823 Add SetLayout to Container 2024-08-24 14:45:31 -04:00
02516bdcce Same as last commit but for TearLine 2024-08-24 14:42:08 -04:00
8432cc70da MenuItem focuses on hover
Styles should remove MenuItem[hover] styling
2024-08-24 14:37:44 -04:00
8469962c90 Use key/button functions for menu 2024-08-24 14:32:19 -04:00
0ccdb609ef Tear off menu windows now have an icon 2024-08-24 01:00:34 -04:00
d1f0786043 Dialog boxes have icons now 2024-08-23 21:39:39 -04:00
73731c6201 Scrollbar has Scrollbar role 2024-08-16 18:36:49 -04:00
7c42b7ad37 Scrollbar has ScrollbarHandle instead of SliderHandle 2024-08-16 18:36:20 -04:00
0fe4979483 Un-export SliderHandle
Closes #8
2024-08-16 18:35:19 -04:00
155752ba78 LabelSwatch uses the new button functions 2024-08-16 18:32:07 -04:00
f4a3cb3c00 LabelSwatch's label is not selectable, to match LabelCheckbox 2024-08-16 18:26:13 -04:00
611705fa0d Change icon on dropdown 2024-08-16 18:01:08 -04:00
16645eeeda Update Tomo API 2024-08-16 18:01:01 -04:00
3219cb712c Remove TODO input value in swatch.go 2024-08-16 16:28:47 -04:00
7d14a25482 Fixed ctrl+key combos on TextInput 2024-08-16 16:17:11 -04:00
e4857da22d Functions to check for common buttons/keys 2024-08-16 16:15:52 -04:00
114cbb346d Keyboard controls activate on key down instead of key up 2024-08-16 15:31:48 -04:00
43ec7a0311 Swatch accepts hex input 2024-08-16 15:17:44 -04:00
3d28c8fea1 Add functions for parsing/formatting NRGBA values 2024-08-16 15:17:15 -04:00
669c638fad Fix transparency in color pickers again 2024-08-16 13:50:22 -04:00
2fe433991d Rename ColorPicker to HSVAColorPicker 2024-08-15 17:05:19 -04:00
acec0f6222 Fix HSV.RGBA sector overflow 2024-08-15 16:51:36 -04:00
0865c28965 Update color picker code in response to HSV color changes 2024-08-15 16:42:31 -04:00
2546c338ad Separate HSVA color into HSV, HSVA, fix alpha premultiplication 2024-08-15 16:41:22 -04:00
b3e7178176 Bring TextInput in line with all the other inputs 2024-08-15 13:17:43 -04:00
080e4511f2 Add Dropdown 2024-08-14 19:06:41 -04:00
f1ac74dcbc Fix TabbedContainer not setting tags correctly 2024-08-14 11:45:30 -04:00
ce0bc5be3b Add MenuHeading 2024-08-14 11:45:10 -04:00
eb0bf58961 Improvements to menus
Major progress on #4
2024-08-14 11:44:47 -04:00
8068036219 Fix icon sizes 2024-08-12 22:06:06 -04:00
30 changed files with 1520 additions and 464 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) [![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 Objects contains a standard collection of re-usable objects. It should be viewed
module visually conform to whatever the theme is set to. 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

@@ -5,13 +5,18 @@ 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/layouts" import "git.tebibyte.media/tomo/objects/layouts"
var _ tomo.Object = new(Button)
var buttonLayout = layouts.Row { true } var buttonLayout = layouts.Row { true }
var iconButtonLayout = layouts.Row { true } 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 {
tomo.ContainerBox box tomo.ContainerBox
label *Label label *Label
icon *Icon icon *Icon
@@ -24,33 +29,45 @@ type Button struct {
// NewButton creates a new button with the specified text. // NewButton creates a new button with the specified text.
func NewButton (text string) *Button { func NewButton (text string) *Button {
box := &Button { button := &Button {
ContainerBox: tomo.NewContainerBox(), box: tomo.NewContainerBox(),
label: NewLabel(text), label: NewLabel(text),
} }
box.SetRole(tomo.R("objects", "Button")) button.box.SetRole(tomo.R("objects", "Button"))
box.label.SetAttr(tomo.AAlign(tomo.AlignMiddle, tomo.AlignMiddle)) button.label.SetAlign(tomo.AlignMiddle, tomo.AlignMiddle)
box.SetAttr(tomo.ALayout(buttonLayout)) button.box.SetAttr(tomo.ALayout(buttonLayout))
box.SetText(text) button.SetText(text)
box.SetInputMask(true) button.box.SetInputMask(true)
box.OnButtonDown(box.handleButtonDown) button.box.OnButtonDown(button.handleButtonDown)
box.OnButtonUp(box.handleButtonUp) button.box.OnButtonUp(button.handleButtonUp)
box.OnKeyDown(box.handleKeyDown) button.box.OnKeyDown(button.handleKeyDown)
box.OnKeyUp(box.handleKeyUp) button.box.OnKeyUp(button.handleKeyUp)
box.SetFocusable(true) button.box.SetFocusable(true)
return box return button
}
// GetBox returns the underlying box.
func (this *Button) GetBox () tomo.Box {
return this.box
}
// SetFocused sets whether or not this button has keyboard focus. If set to
// true, this method will steal focus away from whichever object currently has
// focus.
func (this *Button) SetFocused (focused bool) {
this.box.SetFocused(focused)
} }
// SetText sets the text of the button's label. // SetText sets the text of the button's label.
func (this *Button) SetText (text string) { func (this *Button) SetText (text string) {
this.label.SetText(text) this.label.SetText(text)
if this.labelActive && text == "" { if this.labelActive && text == "" {
this.Remove(this.label) this.box.Remove(this.label)
this.labelActive = false this.labelActive = false
} }
if !this.labelActive && text != "" { if !this.labelActive && text != "" {
this.Add(this.label) this.box.Add(this.label)
this.labelActive = true this.labelActive = true
} }
this.applyLayout() this.applyLayout()
@@ -59,7 +76,7 @@ func (this *Button) SetText (text string) {
// SetIcon sets an icon for this button. Setting the icon to IconUnknown will // SetIcon sets an icon for this button. Setting the icon to IconUnknown will
// remove it. // remove it.
func (this *Button) SetIcon (id tomo.Icon) { func (this *Button) SetIcon (id tomo.Icon) {
if this.icon != nil { this.Remove(this.icon) } if this.icon != nil { this.box.Remove(this.icon) }
var icon *Icon; if id != tomo.IconUnknown { var icon *Icon; if id != tomo.IconUnknown {
icon = NewIcon(id, tomo.IconSizeSmall) icon = NewIcon(id, tomo.IconSizeSmall)
@@ -67,9 +84,9 @@ func (this *Button) SetIcon (id tomo.Icon) {
this.icon = icon this.icon = icon
if this.icon != nil { if this.icon != nil {
this.Insert(this.icon, this.label) this.box.Insert(this.icon, this.label)
} }
this.SetTag("icon", this.icon != nil) this.box.SetTag("icon", this.icon != nil)
this.applyLayout() this.applyLayout()
} }
@@ -80,33 +97,33 @@ func (this *Button) OnClick (callback func ()) event.Cookie {
func (this *Button) applyLayout () { func (this *Button) applyLayout () {
if this.labelActive && this.icon == nil { if this.labelActive && this.icon == nil {
this.SetAttr(tomo.ALayout(buttonLayout)) this.box.SetAttr(tomo.ALayout(buttonLayout))
} else if !this.labelActive && this.icon != nil { } else if !this.labelActive && this.icon != nil {
this.SetAttr(tomo.ALayout(iconButtonLayout)) this.box.SetAttr(tomo.ALayout(iconButtonLayout))
} else { } else {
this.SetAttr(tomo.ALayout(bothButtonLayout)) this.box.SetAttr(tomo.ALayout(bothButtonLayout))
} }
} }
func (this *Button) handleKeyDown (key input.Key, numberPad bool) bool { func (this *Button) handleKeyDown (key input.Key, numberPad bool) bool {
if key != input.KeyEnter && key != input.Key(' ') { return false } if !isClickingKey(key) { return false }
return true
}
func (this *Button) handleKeyUp (key input.Key, numberPad bool) bool {
if key != input.KeyEnter && key != input.Key(' ') { return false }
this.on.click.Broadcast() this.on.click.Broadcast()
return true return true
} }
func (this *Button) handleKeyUp (key input.Key, numberPad bool) bool {
if !isClickingKey(key) { return false }
return true
}
func (this *Button) handleButtonDown (button input.Button) bool { func (this *Button) handleButtonDown (button input.Button) bool {
if button != input.ButtonLeft { return false } if !isClickingButton(button) { return false }
return true return true
} }
func (this *Button) handleButtonUp (button input.Button) bool { func (this *Button) handleButtonUp (button input.Button) bool {
if button != input.ButtonLeft { return false } if !isClickingButton(button) { return false }
if this.Window().MousePosition().In(this.Bounds()) { if this.box.Window().MousePosition().In(this.box.Bounds()) {
this.on.click.Broadcast() this.on.click.Broadcast()
} }
return true return true

View File

@@ -6,10 +6,22 @@ import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/objects/layouts" import "git.tebibyte.media/tomo/objects/layouts"
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 {
tomo.ContainerBox box tomo.ContainerBox
grid tomo.ContainerBox grid tomo.ContainerBox
time time.Time time time.Time
@@ -23,11 +35,11 @@ type Calendar struct {
// NewCalendar creates a new calendar with the specified date. // NewCalendar creates a new calendar with the specified date.
func NewCalendar (tm time.Time) *Calendar { func NewCalendar (tm time.Time) *Calendar {
calendar := &Calendar { calendar := &Calendar {
ContainerBox: tomo.NewContainerBox(), box: tomo.NewContainerBox(),
time: tm, time: tm,
} }
calendar.SetRole(tomo.R("objects", "Calendar")) calendar.box.SetRole(tomo.R("objects", "Calendar"))
calendar.SetAttr(tomo.ALayout(layouts.ContractVertical)) calendar.box.SetAttr(tomo.ALayout(layouts.ContractVertical))
prevButton := NewButton("") prevButton := NewButton("")
prevButton.SetIcon(tomo.IconGoPrevious) prevButton.SetIcon(tomo.IconGoPrevious)
@@ -42,23 +54,28 @@ func NewCalendar (tm time.Time) *Calendar {
calendar.on.valueChange.Broadcast() calendar.on.valueChange.Broadcast()
}) })
calendar.monthLabel = NewLabel("") calendar.monthLabel = NewLabel("")
calendar.monthLabel.SetAttr(tomo.AAlign(tomo.AlignMiddle, tomo.AlignMiddle)) calendar.monthLabel.SetAlign(tomo.AlignMiddle, tomo.AlignMiddle)
calendar.grid = tomo.NewContainerBox() calendar.grid = tomo.NewContainerBox()
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.Add(NewInnerContainer ( calendar.box.Add(NewInnerContainer (
layouts.Row { false, true, false }, layouts.Row { false, true, false },
prevButton, calendar.monthLabel, nextButton)) prevButton, calendar.monthLabel, nextButton))
calendar.Add(calendar.grid) calendar.box.Add(calendar.grid)
calendar.OnScroll(calendar.handleScroll) calendar.box.OnScroll(calendar.handleScroll)
calendar.refresh() calendar.refresh()
return calendar return calendar
} }
// GetBox returns the underlying box.
func (this *Calendar) GetBox () tomo.Box {
return this.box
}
// Value returns the time this calendar is displaying. // Value returns the time this calendar is displaying.
func (this *Calendar) Value () time.Time { func (this *Calendar) Value () time.Time {
return this.time return this.time

View File

@@ -4,9 +4,15 @@ import "git.tebibyte.media/tomo/tomo"
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"
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 {
tomo.Box box tomo.Box
value bool value bool
on struct { on struct {
valueChange event.FuncBroadcaster valueChange event.FuncBroadcaster
@@ -15,18 +21,30 @@ type Checkbox struct {
// NewCheckbox creates a new checkbox with the specified value. // NewCheckbox creates a new checkbox with the specified value.
func NewCheckbox (value bool) *Checkbox { func NewCheckbox (value bool) *Checkbox {
box := &Checkbox { checkbox := &Checkbox {
Box: tomo.NewBox(), box: tomo.NewBox(),
} }
box.SetRole(tomo.R("objects", "Checkbox")) checkbox.box.SetRole(tomo.R("objects", "Checkbox"))
box.SetValue(value) checkbox.SetValue(value)
box.OnButtonDown(box.handleButtonDown) checkbox.box.OnButtonDown(checkbox.handleButtonDown)
box.OnButtonUp(box.handleButtonUp) checkbox.box.OnButtonUp(checkbox.handleButtonUp)
box.OnKeyDown(box.handleKeyDown) checkbox.box.OnKeyDown(checkbox.handleKeyDown)
box.OnKeyUp(box.handleKeyUp) checkbox.box.OnKeyUp(checkbox.handleKeyUp)
box.SetFocusable(true) checkbox.box.SetFocusable(true)
return box return checkbox
}
// GetBox returns the underlying box.
func (this *Checkbox) GetBox () tomo.Box {
return this.box
}
// SetFocused sets whether or not this checkbox has keyboard focus. If set to
// true, this method will steal focus away from whichever object currently has
// focus.
func (this *Checkbox) SetFocused (focused bool) {
this.box.SetFocused(focused)
} }
// Value returns the value of the checkbox. // Value returns the value of the checkbox.
@@ -38,8 +56,8 @@ func (this *Checkbox) Value () bool {
func (this *Checkbox) SetValue (value bool) { func (this *Checkbox) SetValue (value bool) {
this.value = value this.value = value
// the theme shall decide what checked and unchecked states look like // the theme shall decide what checked and unchecked states look like
this.SetTag("checked", value) this.box.SetTag("checked", value)
this.SetTag("unchecked", !value) this.box.SetTag("unchecked", !value)
} }
// Toggle toggles the value of the checkbox between true and false. // Toggle toggles the value of the checkbox between true and false.
@@ -54,24 +72,24 @@ func (this *Checkbox) OnValueChange (callback func ()) event.Cookie {
} }
func (this *Checkbox) handleKeyDown (key input.Key, numberPad bool) bool { func (this *Checkbox) handleKeyDown (key input.Key, numberPad bool) bool {
if key != input.KeyEnter && key != input.Key(' ') { return false } if !isClickingKey(key) { return false }
this.Toggle() this.Toggle()
return true return true
} }
func (this *Checkbox) handleKeyUp (key input.Key, numberPad bool) bool { func (this *Checkbox) handleKeyUp (key input.Key, numberPad bool) bool {
if key != input.KeyEnter && key != input.Key(' ') { return false} if !isClickingKey(key) { return false}
return true return true
} }
func (this *Checkbox) handleButtonDown (button input.Button) bool { func (this *Checkbox) handleButtonDown (button input.Button) bool {
if button != input.ButtonLeft { return false } if !isClickingButton(button) { return false }
return true return true
} }
func (this *Checkbox) handleButtonUp (button input.Button) bool { func (this *Checkbox) handleButtonUp (button input.Button) bool {
if button != input.ButtonLeft { return false } if !isClickingButton(button) { return false }
if this.Window().MousePosition().In(this.Bounds()) { if this.box.Window().MousePosition().In(this.box.Bounds()) {
this.Toggle() this.Toggle()
} }
return true return true

View File

@@ -8,13 +8,19 @@ 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/tomo/objects/internal"
// ColorPicker allows the user to pick a color by controlling its HSBA var _ tomo.Object = new(HSVAColorPicker)
// HSVAColorPicker allows the user to pick a color by controlling its HSVA
// parameters. // parameters.
type ColorPicker struct { //
tomo.ContainerBox // 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 value internal.HSVA
pickerMap *colorPickerMap pickerMap *hsvaColorPickerMap
hueSlider *Slider hueSlider *Slider
alphaSlider *Slider alphaSlider *Slider
@@ -23,18 +29,18 @@ type ColorPicker struct {
} }
} }
// NewColorPicker creates a new color picker with the specified color. // NewHSVAColorPicker creates a new color picker with the specified color.
func NewColorPicker (value color.Color) *ColorPicker { func NewHSVAColorPicker (value color.Color) *HSVAColorPicker {
picker := &ColorPicker { picker := &HSVAColorPicker {
ContainerBox: tomo.NewContainerBox(), box: tomo.NewContainerBox(),
} }
picker.SetRole(tomo.R("objects", "ColorPicker")) picker.box.SetRole(tomo.R("objects", "ColorPicker"))
picker.SetAttr(tomo.ALayout(layouts.Row { true, false, false })) picker.box.SetAttr(tomo.ALayout(layouts.Row { true, false, false }))
picker.pickerMap = newColorPickerMap(picker) picker.pickerMap = newHsvaColorPickerMap(picker)
picker.Add(picker.pickerMap) picker.box.Add(picker.pickerMap)
picker.hueSlider = NewVerticalSlider(0.0) picker.hueSlider = NewVerticalSlider(0.0)
picker.Add(picker.hueSlider) picker.box.Add(picker.hueSlider)
picker.hueSlider.OnValueChange(func () { picker.hueSlider.OnValueChange(func () {
picker.value.H = picker.hueSlider.Value() picker.value.H = picker.hueSlider.Value()
picker.on.valueChange.Broadcast() picker.on.valueChange.Broadcast()
@@ -42,9 +48,9 @@ func NewColorPicker (value color.Color) *ColorPicker {
}) })
picker.alphaSlider = NewVerticalSlider(0.0) picker.alphaSlider = NewVerticalSlider(0.0)
picker.Add(picker.alphaSlider) picker.box.Add(picker.alphaSlider)
picker.alphaSlider.OnValueChange(func () { picker.alphaSlider.OnValueChange(func () {
picker.value.A = uint8(picker.alphaSlider.Value() * 255) picker.value.A = uint16(picker.alphaSlider.Value() * 0xFFFF)
picker.on.valueChange.Broadcast() picker.on.valueChange.Broadcast()
picker.pickerMap.Invalidate() picker.pickerMap.Invalidate()
}) })
@@ -54,38 +60,51 @@ func NewColorPicker (value color.Color) *ColorPicker {
return picker return picker
} }
// GetBox returns the underlying box.
func (this *HSVAColorPicker) GetBox () tomo.Box {
return this.box
}
// SetFocused sets whether or not this color picker has keyboard focus. If set
// to true, this method will steal focus away from whichever object currently
// has focus.
func (this *HSVAColorPicker) SetFocused (focused bool) {
this.box.SetFocused(focused)
}
// Value returns the color of the picker. // Value returns the color of the picker.
func (this *ColorPicker) Value () color.Color { func (this *HSVAColorPicker) Value () color.Color {
return this.value return this.value
} }
// SetValue sets the color of the picker. // SetValue sets the color of the picker.
func (this *ColorPicker) 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.RGBAToHSVA(value.RGBA()) this.value = internal.HSVAModel.Convert(value).(internal.HSVA)
this.hueSlider.SetValue(this.value.H) this.hueSlider.SetValue(this.value.H)
this.alphaSlider.SetValue(float64(this.value.A) / 255) this.alphaSlider.SetValue(float64(this.value.A) / 0xFFFF)
this.pickerMap.Invalidate()
} }
// OnValueChange specifies a function to be called when the user changes the // OnValueChange specifies a function to be called when the user changes the
// swatch's color. // swatch's color.
func (this *ColorPicker) OnValueChange (callback func ()) event.Cookie { func (this *HSVAColorPicker) OnValueChange (callback func ()) event.Cookie {
return this.on.valueChange.Connect(callback) return this.on.valueChange.Connect(callback)
} }
// RGBA satisfies the color.Color interface // RGBA satisfies the color.Color interface
func (this *ColorPicker) RGBA () (r, g, b, a uint32) { func (this *HSVAColorPicker) RGBA () (r, g, b, a uint32) {
return this.value.RGBA() return this.value.RGBA()
} }
type colorPickerMap struct { type hsvaColorPickerMap struct {
tomo.CanvasBox tomo.CanvasBox
dragging bool dragging bool
parent *ColorPicker parent *HSVAColorPicker
} }
func newColorPickerMap (parent *ColorPicker) *colorPickerMap { func newHsvaColorPickerMap (parent *HSVAColorPicker) *hsvaColorPickerMap {
picker := &colorPickerMap { picker := &hsvaColorPickerMap {
CanvasBox: tomo.NewCanvasBox(), CanvasBox: tomo.NewCanvasBox(),
parent: parent, parent: parent,
} }
@@ -97,26 +116,26 @@ func newColorPickerMap (parent *ColorPicker) *colorPickerMap {
return picker return picker
} }
func (this *colorPickerMap) handleButtonDown (button input.Button) bool { func (this *hsvaColorPickerMap) handleButtonDown (button input.Button) bool {
if button != input.ButtonLeft { return false } if button != input.ButtonLeft { return false }
this.dragging = true this.dragging = true
this.drag() this.drag()
return true return true
} }
func (this *colorPickerMap) handleButtonUp (button input.Button) bool { func (this *hsvaColorPickerMap) handleButtonUp (button input.Button) bool {
if button != input.ButtonLeft { return false } if button != input.ButtonLeft { return false }
this.dragging = false this.dragging = false
return true return true
} }
func (this *colorPickerMap) handleMouseMove () bool { func (this *hsvaColorPickerMap) handleMouseMove () bool {
if !this.dragging { return false } if !this.dragging { return false }
this.drag() this.drag()
return true return true
} }
func (this *colorPickerMap) drag () { func (this *hsvaColorPickerMap) drag () {
pointer := this.Window().MousePosition() pointer := this.Window().MousePosition()
bounds := this.InnerBounds() bounds := this.InnerBounds()
this.parent.value.S = float64(pointer.X - bounds.Min.X) / float64(bounds.Dx()) this.parent.value.S = float64(pointer.X - bounds.Min.X) / float64(bounds.Dx())
@@ -126,7 +145,7 @@ func (this *colorPickerMap) drag () {
this.Invalidate() this.Invalidate()
} }
func (this *colorPickerMap) Draw (can canvas.Canvas) { func (this *hsvaColorPickerMap) Draw (can canvas.Canvas) {
bounds := can.Bounds() bounds := can.Bounds()
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ { for x := bounds.Min.X; x < bounds.Max.X; x ++ {
@@ -137,7 +156,7 @@ func (this *colorPickerMap) Draw (can canvas.Canvas) {
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()),
A: 255, A: 0xFFFF,
} }
sPos := int( this.parent.value.S * float64(bounds.Dx())) sPos := int( this.parent.value.S * float64(bounds.Dx()))

View File

@@ -1,21 +1,32 @@
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)
// 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 can be used as a
// primitive for building more complex layouts. It has two variants: an outer // primitive for building more complex layouts. It has two main variants: an
// container, and an inner container. The outer container has padding around // outer container, and an inner container. The outer container has padding
// its edges, whereas the inner container does not. The container will have a // around its edges, whereas the inner container does not. It also has a
// corresponding object role variation of either "outer" or "inner". // "sunken" variation designed to hold a scrolled list of items.
//
// Tags:
// - [outer] The container is the root of a window.
// - [inner] The container is within another container, and is part of a
// larger layout.
// - [sunken] The container holds a visually grouped, usually scrolled, list
// of items.
type Container struct { type Container struct {
tomo.ContainerBox box tomo.ContainerBox
} }
func newContainer (layout tomo.Layout, children ...tomo.Object) *Container { func newContainer (layout tomo.Layout, children ...tomo.Object) *Container {
this := &Container { this := &Container {
ContainerBox: tomo.NewContainerBox(), box: tomo.NewContainerBox(),
} }
this.SetAttr(tomo.ALayout(layout)) this.box.SetAttr(tomo.ALayout(layout))
for _, child := range children { for _, child := range children {
this.Add(child) this.Add(child)
} }
@@ -27,8 +38,8 @@ func newContainer (layout tomo.Layout, children ...tomo.Object) *Container {
// window, tab pane, etc. // window, tab pane, etc.
func NewOuterContainer (layout tomo.Layout, children ...tomo.Object) *Container { func NewOuterContainer (layout tomo.Layout, children ...tomo.Object) *Container {
this := newContainer(layout, children...) this := newContainer(layout, children...)
this.SetRole(tomo.R("objects", "Container")) this.box.SetRole(tomo.R("objects", "Container"))
this.SetTag("outer", true) this.box.SetTag("outer", true)
return this return this
} }
@@ -36,16 +47,87 @@ func NewOuterContainer (layout tomo.Layout, children ...tomo.Object) *Container
// around it. It is meant to be used as a root container for a ScrollContainer. // around it. It is meant to be used as a root container for a ScrollContainer.
func NewSunkenContainer (layout tomo.Layout, children ...tomo.Object) *Container { func NewSunkenContainer (layout tomo.Layout, children ...tomo.Object) *Container {
this := newContainer(layout, children...) this := newContainer(layout, children...)
this.SetRole(tomo.R("objects", "Container")) this.box.SetRole(tomo.R("objects", "Container"))
this.SetTag("sunken", true) this.box.SetTag("sunken", true)
return this return this
} }
// NewInnerContainer creates a new container that has no padding around it. // NewInnerContainer creates a new container that has no padding around it.
func NewInnerContainer (layout tomo.Layout, children ...tomo.Object) *Container { func NewInnerContainer (layout tomo.Layout, children ...tomo.Object) *Container {
this := newContainer(layout, children...) this := newContainer(layout, children...)
this.SetRole(tomo.R("objects", "Container")) this.box.SetRole(tomo.R("objects", "Container"))
this.SetTag("inner", true) this.box.SetTag("inner", true)
return this 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)
}

View File

@@ -16,7 +16,7 @@ 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 tomo.ContainerBox controlRow *Container
} }
type clickable interface { type clickable interface {
@@ -53,9 +53,10 @@ func NewDialog (kind DialogKind, parent tomo.Window, title, message string, opti
case DialogError: iconId = tomo.IconDialogError case DialogError: iconId = tomo.IconDialogError
} }
dialog.SetTitle(title) dialog.SetTitle(title)
dialog.SetIcon(iconId)
icon := NewIcon(iconId, tomo.IconSizeLarge) icon := NewIcon(iconId, tomo.IconSizeLarge)
messageText := NewLabel(message) messageText := NewLabel(message)
messageText.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle)) messageText.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
for _, option := range options { for _, option := range options {
if option, ok := option.(clickable); ok { if option, ok := option.(clickable); ok {
@@ -63,7 +64,7 @@ func NewDialog (kind DialogKind, parent tomo.Window, title, message string, opti
} }
} }
dialog.controlRow = NewInnerContainer(layouts.ContractHorizontal, options...) dialog.controlRow = NewInnerContainer(layouts.ContractHorizontal, options...)
dialog.controlRow.SetAttr(tomo.AAlign(tomo.AlignEnd, tomo.AlignEnd)) dialog.controlRow.SetAlign(tomo.AlignEnd, tomo.AlignEnd)
dialog.SetRoot(NewOuterContainer ( dialog.SetRoot(NewOuterContainer (
layouts.Column { true, false }, layouts.Column { true, false },

128
dropdown.go Normal file
View File

@@ -0,0 +1,128 @@
package objects
// import "image"
import "git.tebibyte.media/tomo/tomo"
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(Dropdown)
// Dropdown is a non-editable text input that allows the user to pick a value
// from a list.
type Dropdown struct {
box tomo.ContainerBox
label *Label
value string
items []string
menu *Menu
on struct {
valueChange event.FuncBroadcaster
}
}
// NewDropdown creates a new dropdown input with the specified items
func NewDropdown (items ...string) *Dropdown {
dropdown := &Dropdown {
box: tomo.NewContainerBox(),
label: NewLabel(""),
}
dropdown.box.SetRole(tomo.R("objects", "Dropdown"))
dropdown.box.SetAttr(tomo.ALayout(layouts.Row { true, false }))
dropdown.box.Add(dropdown.label)
dropdown.box.Add(NewIcon(tomo.IconListChoose, tomo.IconSizeSmall))
dropdown.SetItems(items...)
if len(items) > 0 {
dropdown.SetValue(items[0])
}
dropdown.box.SetInputMask(true)
dropdown.box.OnButtonDown(dropdown.handleButtonDown)
dropdown.box.OnButtonUp(dropdown.handleButtonUp)
dropdown.box.OnKeyDown(dropdown.handleKeyDown)
dropdown.box.OnKeyUp(dropdown.handleKeyUp)
dropdown.box.SetFocusable(true)
return dropdown
}
// GetBox returns the underlying box.
func (this *Dropdown) GetBox () tomo.Box {
return this.box
}
// SetFocused sets whether or not this dropdown has keyboard focus. If set to
// true, this method will steal focus away from whichever object currently has
// focus.
func (this *Dropdown) SetFocused (focused bool) {
this.box.SetFocused(focused)
}
// Value returns the value of the dropdown. This does not necissarily have to be
// in the list of items.
func (this *Dropdown) Value () string {
return this.value
}
// SetValue sets the value of the dropdown. This does not necissarily have to be
// in the list of items.
func (this *Dropdown) SetValue (value string) {
this.value = value
this.label.SetText(value)
}
// SetItems sets the items from which the user is able to pick.
func (this *Dropdown) SetItems (items ...string) {
this.items = items
}
// Choose creates a menu that allows the user to pick a value.
func (this *Dropdown) Choose () {
if this.menu != nil {
this.menu.Close()
}
menu, err := NewAnchoredMenu(this, this.itemList()...)
if err != nil { return }
this.menu = menu
menu.SetVisible(true)
}
func (this *Dropdown) itemList () []tomo.Object {
items := make([]tomo.Object, len(this.items))
for index, value := range this.items {
value := value
item := NewMenuItem(value)
item.OnClick(func () {
this.SetValue(value)
this.on.valueChange.Broadcast()
})
items[index] = item
}
return items
}
func (this *Dropdown) handleKeyDown (key input.Key, numberPad bool) bool {
if !isClickingKey(key) { return false }
this.Choose()
return true
}
func (this *Dropdown) handleKeyUp (key input.Key, numberPad bool) bool {
if !isClickingKey(key) { return false }
return true
}
func (this *Dropdown) handleButtonDown (button input.Button) bool {
if !isClickingButton(button) { return false }
return true
}
func (this *Dropdown) handleButtonUp (button input.Button) bool {
if !isClickingButton(button) { return false }
if this.box.Window().MousePosition().In(this.box.Bounds()) {
this.Choose()
}
return true
}

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
}

2
go.mod
View File

@@ -2,4 +2,4 @@ module git.tebibyte.media/tomo/objects
go 1.20 go 1.20
require git.tebibyte.media/tomo/tomo v0.45.0 require git.tebibyte.media/tomo/tomo v0.46.1

4
go.sum
View File

@@ -1,2 +1,2 @@
git.tebibyte.media/tomo/tomo v0.45.0 h1:fQH0WIPidW275hOq9dE6R7p064xG1RGx2QU68Avlr84= git.tebibyte.media/tomo/tomo v0.46.1 h1:/8fT6I9l4TK529zokrThbNDHGRvUsNgif1Zs++0PBSQ=
git.tebibyte.media/tomo/tomo v0.45.0/go.mod h1:WrtilgKB1y8O2Yu7X4mYcRiqOlPR8NuUnoA/ynkQWrs= git.tebibyte.media/tomo/tomo v0.46.1/go.mod h1:WrtilgKB1y8O2Yu7X4mYcRiqOlPR8NuUnoA/ynkQWrs=

View File

@@ -2,24 +2,72 @@ package objects
import "fmt" import "fmt"
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(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 {
tomo.TextBox box tomo.TextBox
} }
// NewHeading creates a new section heading. The level can be from 0 to 2. // NewHeading creates a new section heading. The level can be from 0 to 2.
func NewHeading (level int, text string) *Heading { func NewHeading (level int, text string) *Heading {
if level < 0 { level = 0 } if level < 0 { level = 0 }
if level > 2 { level = 2 } if level > 2 { level = 2 }
this := &Heading { TextBox: tomo.NewTextBox() } this := &Heading { box: tomo.NewTextBox() }
this.SetRole(tomo.R("objects", "Heading")) this.box.SetRole(tomo.R("objects", "Heading"))
this.SetTag(fmt.Sprint(level), true) this.box.SetTag(fmt.Sprint(level), true)
this.SetText(text) this.SetText(text)
this.SetSelectable(true) this.box.SetSelectable(true)
this.SetFocusable(true) this.box.SetFocusable(true)
return this return this
} }
// GetBox returns the underlying box.
func (this *Heading) GetBox () tomo.Box {
return this.box
}
// SetFocused sets whether or not this heading has keyboard focus. If set to
// true, this method will steal focus away from whichever object currently has
// focus.
func (this *Heading) SetFocused (focused bool) {
this.box.SetFocused(focused)
}
// SetText sets the text content of the heading.
func (this *Heading) SetText (text string) {
this.box.SetText(text)
}
// Select sets the text cursor or selection.
func (this *Heading) Select (dot text.Dot) {
this.box.Select(dot)
}
// Dot returns the text cursor or selection.
func (this *Heading) Dot () text.Dot {
return this.box.Dot()
}
// OnDotChange specifies a function to be called when the text cursor or
// selection changes.
func (this *Heading) OnDotChange (callback func ()) event.Cookie {
return this.box.OnDotChange(callback)
}
// NewMenuHeading creatss a new heading for use in menus.
func NewMenuHeading (text string) *Heading {
heading := NewHeading(0, text)
heading.box.SetTag("menu", true)
return heading
}

29
icon.go
View File

@@ -3,9 +3,16 @@ package objects
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
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 {
tomo.Box box tomo.Box
icon tomo.Icon icon tomo.Icon
size tomo.IconSize size tomo.IconSize
} }
@@ -21,18 +28,24 @@ func iconSizeString (size tomo.IconSize) string {
// NewIcon creates a new icon from an icon ID. // NewIcon creates a new icon from an icon ID.
func NewIcon (icon tomo.Icon, size tomo.IconSize) *Icon { func NewIcon (icon tomo.Icon, size tomo.IconSize) *Icon {
this := &Icon { this := &Icon {
Box: tomo.NewBox(), box: tomo.NewBox(),
} }
this.SetRole(tomo.R("objects", "Icon")) this.box.SetRole(tomo.R("objects", "Icon"))
this.SetIcon(icon, size) this.SetIcon(icon, size)
this.OnIconSetChange(this.handleIconSetChange) this.box.OnIconSetChange(this.handleIconSetChange)
return this return this
} }
// GetBox returns the underlying box.
func (this *Icon) GetBox () tomo.Box {
return this.box
}
// SetIcon sets the icon. // SetIcon sets the icon.
func (this *Icon) SetIcon (icon tomo.Icon, size tomo.IconSize) { func (this *Icon) SetIcon (icon tomo.Icon, size tomo.IconSize) {
if this.icon == icon { return } if this.icon == icon { return }
this.icon = icon this.icon = icon
this.size = size
this.setTexture(icon.Texture(size)) this.setTexture(icon.Texture(size))
} }
@@ -41,12 +54,12 @@ func (this *Icon) handleIconSetChange () {
} }
func (this *Icon) setTexture (texture canvas.Texture) { func (this *Icon) setTexture (texture canvas.Texture) {
this.SetAttr(tomo.ATexture(texture)) this.box.SetAttr(tomo.ATexture(texture))
this.SetAttr(tomo.ATextureMode(tomo.TextureModeCenter)) this.box.SetAttr(tomo.ATextureMode(tomo.TextureModeCenter))
if texture == nil { if texture == nil {
this.SetAttr(tomo.AMinimumSize(0, 0)) this.box.SetAttr(tomo.AMinimumSize(0, 0))
} else { } else {
bounds := texture.Bounds() bounds := texture.Bounds()
this.SetAttr(tomo.AttrMinimumSize(bounds.Max.Sub(bounds.Min))) this.box.SetAttr(tomo.AttrMinimumSize(bounds.Max.Sub(bounds.Min)))
} }
} }

19
input.go Normal file
View File

@@ -0,0 +1,19 @@
package objects
import "git.tebibyte.media/tomo/tomo/input"
func isClickingKey (key input.Key) bool {
return key == input.KeyEnter || key == input.Key(' ')
}
func isConfirmationKey (key input.Key) bool {
return key == input.KeyEnter
}
func isClickingButton (button input.Button) bool {
return button == input.ButtonLeft
}
func isMenuButton (button input.Button) bool {
return button == input.ButtonLeft
}

View File

@@ -1,13 +1,32 @@
package internal package internal
import "fmt"
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 { type HSVA struct {
H float64 H float64
S float64 S float64
V float64 V float64
A uint8 A uint16
} }
func (hsva HSVA) RGBA () (r, g, b, a uint32) { var (
HSVModel color.Model = color.ModelFunc(hsvModel)
HSVAModel color.Model = color.ModelFunc(hsvaModel)
)
func (hsv HSV) RGBA () (r, g, b, a uint32) {
// Adapted from: // Adapted from:
// https://www.cs.rit.edu/~ncs/color/t_convert.html // https://www.cs.rit.edu/~ncs/color/t_convert.html
@@ -15,34 +34,57 @@ func (hsva HSVA) RGBA () (r, g, b, a uint32) {
return uint32(float64(0xFFFF) * x) return uint32(float64(0xFFFF) * x)
} }
ca := uint32(hsva.A) << 8 s := clamp01(hsv.S)
s := clamp01(hsva.S) v := clamp01(hsv.V)
v := clamp01(hsva.V)
if s == 0 { if s == 0 {
light := component(v) light := component(v)
return light, light, light, ca return light, light, light, 0xFFFF
} }
h := clamp01(hsva.H) * 360 h := clamp01(hsv.H) * 360
sector := int(h / 60) 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) offset := (h / 60) - float64(sector)
fac := float64(hsva.A) / 255 p := component(v * (1 - s))
p := component(fac * v * (1 - s)) q := component(v * (1 - s * offset))
q := component(fac * v * (1 - s * offset)) t := component(v * (1 - s * (1 - offset)))
t := component(fac * v * (1 - s * (1 - offset)))
va := component(v) va := component(v)
switch sector { switch sector {
case 0: return va, t, p, ca case 0: return va, t, p, 0xFFFF
case 1: return q, va, p, ca case 1: return q, va, p, 0xFFFF
case 2: return p, va, t, ca case 2: return p, va, t, 0xFFFF
case 3: return p, q, va, ca case 3: return p, q, va, 0xFFFF
case 4: return t, p, va, ca case 4: return t, p, va, 0xFFFF
default: return va, p, q, ca 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 // Canon returns the color but with the H, S, and V fields are constrained to
// the range 0.0-1.0 // the range 0.0-1.0
func (hsva HSVA) Canon () HSVA { func (hsva HSVA) Canon () HSVA {
@@ -58,7 +100,38 @@ func clamp01 (x float64) float64 {
return x return x
} }
func RGBAToHSVA (r, g, b, a uint32) HSVA { 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: // Adapted from:
// https://www.cs.rit.edu/~ncs/color/t_convert.html // https://www.cs.rit.edu/~ncs/color/t_convert.html
@@ -78,27 +151,81 @@ func RGBAToHSVA (r, g, b, a uint32) HSVA {
if cg < minComponent { minComponent = cg } if cg < minComponent { minComponent = cg }
if cb < minComponent { minComponent = cb } if cb < minComponent { minComponent = cb }
hsva := HSVA { hsv := HSV {
V: maxComponent, V: maxComponent,
A: uint8(a >> 8),
} }
delta := maxComponent - minComponent delta := maxComponent - minComponent
if delta == 0 { if delta == 0 {
// hsva.S is undefined, so hue doesn't matter // hsva.S is undefined, so hue doesn't matter
return hsva return hsv
} }
hsva.S = delta / maxComponent hsv.S = delta / maxComponent
switch { switch {
case cr == maxComponent: hsva.H = (cg - cb) / delta case cr == maxComponent: hsv.H = (cg - cb) / delta
case cg == maxComponent: hsva.H = 2 + (cb - cr) / delta case cg == maxComponent: hsv.H = 2 + (cb - cr) / delta
case cb == maxComponent: hsva.H = 4 + (cr - cg) / delta case cb == maxComponent: hsv.H = 4 + (cr - cg) / delta
} }
hsva.H *= 60 hsv.H *= 60
if hsva.H < 0 { hsva.H += 360 } if hsv.H < 0 { hsv.H += 360 }
hsva.H /= 360 hsv.H /= 360
return hsva return hsv
}
// FormatNRGBA formats an NRGBA value into a hex string.
func FormatNRGBA (nrgba color.NRGBA) string {
return fmt.Sprintf("%02X%02X%02X%02X", nrgba.R, nrgba.G, nrgba.B, nrgba.A)
}
// ParseNRGBA parses an NRGBA value from a hex string. It can be of the format:
// - RGB
// - RGBA
// - RRGGBB
// - RRGGBBAA
// If none of these are specified, this function will return an opaque black
// color. Hex digits may either be upper case or lower case.
func ParseNRGBA (str string) color.NRGBA {
runes := []rune(str)
c := color.NRGBA { A: 255 }
switch len(runes) {
case 3:
c.R = fillOctet(hexDigit(runes[0]))
c.G = fillOctet(hexDigit(runes[1]))
c.B = fillOctet(hexDigit(runes[2]))
case 4:
c.R = fillOctet(hexDigit(runes[0]))
c.G = fillOctet(hexDigit(runes[1]))
c.B = fillOctet(hexDigit(runes[2]))
c.A = fillOctet(hexDigit(runes[3]))
case 6:
c.R = hexOctet(runes[0], runes[1])
c.G = hexOctet(runes[2], runes[3])
c.B = hexOctet(runes[4], runes[5])
case 8:
c.R = hexOctet(runes[0], runes[1])
c.G = hexOctet(runes[2], runes[3])
c.B = hexOctet(runes[4], runes[5])
c.A = hexOctet(runes[6], runes[7])
}
return c
}
func hexDigit (r rune) uint8 {
switch {
case r >= '0' && r <= '9': return uint8(r - '0')
case r >= 'A' && r <= 'F': return uint8(r - 'A') + 10
case r >= 'a' && r <= 'f': return uint8(r - 'a') + 10
default: return 0
}
}
func fillOctet (low uint8) uint8 {
return low << 4 | low
}
func hexOctet (high, low rune) uint8 {
return hexDigit(high) << 4 | hexDigit(low)
} }

View File

@@ -1,19 +1,60 @@
package objects package objects
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(Label)
// Label is a simple text label. // Label is a simple text label.
type Label struct { type Label struct {
tomo.TextBox box tomo.TextBox
} }
// NewLabel creates a new text label. // NewLabel creates a new text label.
func NewLabel (text string) *Label { func NewLabel (text string) *Label {
this := &Label { TextBox: tomo.NewTextBox() } this := &Label { box: tomo.NewTextBox() }
this.SetRole(tomo.R("objects", "Label")) this.box.SetRole(tomo.R("objects", "Label"))
this.SetText(text) this.SetText(text)
this.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle)) this.box.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle))
this.SetSelectable(true) this.box.SetSelectable(true)
this.SetFocusable(true) this.box.SetFocusable(true)
return this return this
} }
// GetBox returns the underlying box.
func (this *Label) GetBox () tomo.Box {
return this.box
}
// SetFocused sets whether or not this label has keyboard focus. If set to true,
// this method will steal focus away from whichever object currently has focus.
func (this *Label) SetFocused (focused bool) {
this.box.SetFocused(focused)
}
// SetText sets the text content of the label.
func (this *Label) SetText (text string) {
this.box.SetText(text)
}
// Select sets the text cursor or selection.
func (this *Label) Select (dot text.Dot) {
this.box.Select(dot)
}
// Dot returns the text cursor or selection.
func (this *Label) Dot () text.Dot {
return this.box.Dot()
}
// 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

@@ -5,9 +5,11 @@ 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/layouts" import "git.tebibyte.media/tomo/objects/layouts"
var _ tomo.Object = new(LabelCheckbox)
// LabelCheckbox is a checkbox with a label. // LabelCheckbox is a checkbox with a label.
type LabelCheckbox struct { type LabelCheckbox struct {
tomo.ContainerBox box tomo.ContainerBox
checkbox *Checkbox checkbox *Checkbox
label *Label label *Label
} }
@@ -15,22 +17,39 @@ type LabelCheckbox struct {
// NewLabelCheckbox creates a new labeled checkbox with the specified value and // NewLabelCheckbox creates a new labeled checkbox with the specified value and
// label text. // label text.
func NewLabelCheckbox (value bool, text string) *LabelCheckbox { func NewLabelCheckbox (value bool, text string) *LabelCheckbox {
box := &LabelCheckbox { labelCheckbox := &LabelCheckbox {
ContainerBox: tomo.NewContainerBox(), box: tomo.NewContainerBox(),
checkbox: NewCheckbox(value), checkbox: NewCheckbox(value),
label: NewLabel(text), label: NewLabel(text),
} }
box.SetRole(tomo.R("objects", "LabelCheckbox")) labelCheckbox.box.SetRole(tomo.R("objects", "LabelCheckbox"))
box.label.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle)) labelCheckbox.label.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
box.label.SetSelectable(false) labelCheckbox.label.GetBox().(tomo.TextBox).SetSelectable(false)
box.label.SetFocusable(false) labelCheckbox.label.GetBox().(tomo.TextBox).SetFocusable(false)
box.Add(box.checkbox) labelCheckbox.box.Add(labelCheckbox.checkbox)
box.Add(box.label) labelCheckbox.box.Add(labelCheckbox.label)
box.SetAttr(tomo.ALayout(layouts.Row { false, true })) labelCheckbox.box.SetAttr(tomo.ALayout(layouts.Row { false, true }))
box.OnButtonDown(box.handleButtonDown) labelCheckbox.box.OnButtonDown(labelCheckbox.handleButtonDown)
box.OnButtonUp(box.handleButtonUp) labelCheckbox.box.OnButtonUp(labelCheckbox.handleButtonUp)
return box return labelCheckbox
}
// GetBox returns the underlying box.
func (this *LabelCheckbox) GetBox () tomo.Box {
return this.box
}
// SetFocused sets whether or not this checkbox has keyboard focus. If set to
// true, this method will steal focus away from whichever object currently has
// focus.
func (this *LabelCheckbox) SetFocused (focused bool) {
this.checkbox.SetFocused(focused)
}
// SetText sets the text label of the checkbox.
func (this *LabelCheckbox) SetText (text string) {
this.label.SetText(text)
} }
// Value returns the value of the checkbox. // Value returns the value of the checkbox.
@@ -55,13 +74,13 @@ func (this *LabelCheckbox) OnValueChange (callback func ()) event.Cookie {
} }
func (this *LabelCheckbox) handleButtonDown (button input.Button) bool { func (this *LabelCheckbox) handleButtonDown (button input.Button) bool {
if button != input.ButtonLeft { return false } if !isClickingButton(button) { return false }
return true return true
} }
func (this *LabelCheckbox) handleButtonUp (button input.Button) bool { func (this *LabelCheckbox) handleButtonUp (button input.Button) bool {
if button != input.ButtonLeft { return false } if !isClickingButton(button) { return false }
if this.Window().MousePosition().In(this.Bounds()) { if this.box.Window().MousePosition().In(this.box.Bounds()) {
this.checkbox.SetFocused(true) this.checkbox.SetFocused(true)
this.checkbox.Toggle() this.checkbox.Toggle()
} }

View File

@@ -6,31 +6,52 @@ 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/layouts" import "git.tebibyte.media/tomo/objects/layouts"
var _ tomo.Object = new(LabelSwatch)
// LabelSwatch is a swatch with a label. // LabelSwatch is a swatch with a label.
type LabelSwatch struct { type LabelSwatch struct {
tomo.ContainerBox box tomo.ContainerBox
swatch *Swatch swatch *Swatch
label *Label label *Label
} }
// NewLabelSwatch creates a new labeled swatch with the specified color and // NewLabelSwatch creates a new labeled swatch with the specified color and
// label text. // label text.
func NewLabelSwatch (value color.Color, text string) *LabelSwatch { func NewLabelSwatch (value color.Color, text string) *LabelSwatch {
box := &LabelSwatch { labelSwatch := &LabelSwatch {
ContainerBox: tomo.NewContainerBox(), box: tomo.NewContainerBox(),
swatch: NewSwatch(value), swatch: NewSwatch(value),
label: NewLabel(text), label: NewLabel(text),
} }
box.SetRole(tomo.R("objects", "LabelSwatch")) labelSwatch.box.SetRole(tomo.R("objects", "LabelSwatch"))
box.swatch.label = text labelSwatch.swatch.label = text
box.label.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle)) labelSwatch.label.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
box.Add(box.swatch) labelSwatch.label.GetBox().(tomo.TextBox).SetSelectable(false)
box.Add(box.label) labelSwatch.label.GetBox().(tomo.TextBox).SetFocusable(false)
box.SetAttr(tomo.ALayout(layouts.Row { false, true })) labelSwatch.box.Add(labelSwatch.swatch)
labelSwatch.box.Add(labelSwatch.label)
labelSwatch.box.SetAttr(tomo.ALayout(layouts.Row { false, true }))
box.OnButtonDown(box.handleButtonDown) labelSwatch.box.OnButtonDown(labelSwatch.handleButtonDown)
box.OnButtonUp(box.handleButtonUp) labelSwatch.box.OnButtonUp(labelSwatch.handleButtonUp)
return box return labelSwatch
}
// GetBox returns the underlying box.
func (this *LabelSwatch) GetBox () tomo.Box {
return this.box
}
// SetFocused sets whether or not this swatch has keyboard focus. If set to
// true, this method will steal focus away from whichever object currently has
// focus.
func (this *LabelSwatch) SetFocused (focused bool) {
this.swatch.SetFocused(focused)
}
// SetText sets the text label of the swatch.
func (this *LabelSwatch) SetText (text string) {
this.label.SetText(text)
} }
// Value returns the color of the swatch. // Value returns the color of the swatch.
@@ -61,13 +82,13 @@ func (this *LabelSwatch) OnConfirm (callback func ()) event.Cookie {
} }
func (this *LabelSwatch) handleButtonDown (button input.Button) bool { func (this *LabelSwatch) handleButtonDown (button input.Button) bool {
if button != input.ButtonLeft { return true } if !isClickingButton(button) { return true }
return true return true
} }
func (this *LabelSwatch) handleButtonUp (button input.Button) bool { func (this *LabelSwatch) handleButtonUp (button input.Button) bool {
if button != input.ButtonLeft { return true } if !isClickingButton(button) { return true }
if this.Window().MousePosition().In(this.Bounds()) { if this.box.Window().MousePosition().In(this.box.Bounds()) {
this.swatch.SetFocused(true) this.swatch.SetFocused(true)
this.swatch.Choose() this.swatch.Choose()
} }

57
menu.go
View File

@@ -7,6 +7,10 @@ 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:
// - 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
@@ -18,23 +22,28 @@ type Menu struct {
} }
// NewMenu creates a new menu with the specified items. The menu will appear // NewMenu creates a new menu with the specified items. The menu will appear
// directly under the anchor Object. If the anchor is nil, it will appear // directly under the mouse pointer.
// directly under the mouse pointer instead. func NewMenu (parent tomo.Window, items ...tomo.Object) (*Menu, error) {
func NewMenu (anchor tomo.Object, items ...tomo.Object) (*Menu, error) { bounds := (image.Rectangle { }).Add(parent.MousePosition())
menu := &Menu { } return newMenu(parent, bounds, items...)
if anchor == nil { }
// TODO: *actually* put it under the mouse
window, err := tomo.NewWindow(menu.bounds)
if err != nil { return nil, err }
menu.Window = window
} else {
menu.bounds = menuBoundsFromAnchor(anchor)
menu.parent = anchor.GetBox().Window()
window, err := menu.parent.NewMenu(menu.bounds)
if err != nil { return nil, err }
menu.Window = window
}
// NewAnchoredMenu creates a new menu with the specified items. The menu will
// appear directly under the anchor.
func NewAnchoredMenu (anchor tomo.Object, items ...tomo.Object) (*Menu, error) {
parent := anchor.GetBox().Window()
bounds := menuBoundsFromAnchor(anchor)
return newMenu(parent, bounds, items...)
}
func newMenu (parent tomo.Window, bounds image.Rectangle, items ...tomo.Object) (*Menu, error) {
menu := &Menu { }
menu.bounds = bounds
menu.parent = parent
window, err := menu.parent.NewMenu(menu.bounds)
if err != nil { return nil, err }
menu.Window = window
menu.rootContainer = tomo.NewContainerBox() menu.rootContainer = tomo.NewContainerBox()
menu.rootContainer.SetAttr(tomo.ALayout(layouts.ContractVertical)) menu.rootContainer.SetAttr(tomo.ALayout(layouts.ContractVertical))
@@ -67,6 +76,7 @@ func (this *Menu) TearOff () {
this.torn = true this.torn = true
window, err := this.parent.NewChild(this.bounds) window, err := this.parent.NewChild(this.bounds)
window.SetIcon(tomo.IconListChoose)
if err != nil { return } if err != nil { return }
visible := this.Window.Visible() visible := this.Window.Visible()
@@ -74,6 +84,7 @@ func (this *Menu) TearOff () {
this.Window.Close() this.Window.Close()
this.rootContainer.Remove(this.tearLine) this.rootContainer.Remove(this.tearLine)
this.rootContainer.SetTag("torn", true)
this.Window = window this.Window = window
this.Window.SetRoot(this.rootContainer) this.Window.SetRoot(this.rootContainer)
@@ -84,21 +95,27 @@ func (this *Menu) newTearLine () tomo.Object {
tearLine := tomo.NewBox() tearLine := tomo.NewBox()
tearLine.SetRole(tomo.R("objects", "TearLine")) tearLine.SetRole(tomo.R("objects", "TearLine"))
tearLine.SetFocusable(true) tearLine.SetFocusable(true)
tearLine.OnMouseEnter(func () {
tearLine.SetFocused(true)
})
tearLine.OnMouseLeave(func () {
tearLine.SetFocused(false)
})
tearLine.OnKeyDown(func (key input.Key, numberPad bool) bool { tearLine.OnKeyDown(func (key input.Key, numberPad bool) bool {
if key != input.KeyEnter && key != input.Key(' ') { return false } if !isClickingKey(key) { return false }
return true return true
}) })
tearLine.OnKeyUp(func (key input.Key, numberPad bool) bool { tearLine.OnKeyUp(func (key input.Key, numberPad bool) bool {
if key != input.KeyEnter && key != input.Key(' ') { return false } if !isClickingKey(key) { return false }
this.TearOff() this.TearOff()
return true return true
}) })
tearLine.OnButtonDown(func (button input.Button) bool { tearLine.OnButtonDown(func (button input.Button) bool {
if button != input.ButtonLeft { return false } if !isClickingButton(button) { return false }
return true return true
}) })
tearLine.OnButtonUp(func (button input.Button) bool { tearLine.OnButtonUp(func (button input.Button) bool {
if button != input.ButtonLeft { return false } if !isClickingButton(button) { return false }
if tearLine.Window().MousePosition().In(tearLine.Bounds()) { if tearLine.Window().MousePosition().In(tearLine.Bounds()) {
this.TearOff() this.TearOff()
} }

View File

@@ -5,12 +5,14 @@ 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/layouts" import "git.tebibyte.media/tomo/objects/layouts"
var _ tomo.Object = new(MenuItem)
// MenuItem is a selectable memu item. // MenuItem is a selectable memu item.
type MenuItem struct { type MenuItem struct {
tomo.ContainerBox box tomo.ContainerBox
label *Label label *Label
icon *Icon icon *Icon
labelActive bool labelActive bool
on struct { on struct {
@@ -20,25 +22,44 @@ type MenuItem struct {
// NewMenuItem creates a new menu item with the specified text. // NewMenuItem creates a new menu item with the specified text.
func NewMenuItem (text string) *MenuItem { func NewMenuItem (text string) *MenuItem {
box := &MenuItem { return NewIconMenuItem(tomo.IconUnknown, text)
ContainerBox: tomo.NewContainerBox(), }
label: NewLabel(text),
icon: NewIcon("", tomo.IconSizeSmall), // NewIconMenuItem creates a new menu item with the specified icon and text.
func NewIconMenuItem (icon tomo.Icon, text string) *MenuItem {
menuItem := &MenuItem {
box: tomo.NewContainerBox(),
label: NewLabel(text),
icon: NewIcon(icon, tomo.IconSizeSmall),
} }
box.SetRole(tomo.R("objects", "MenuItem")) menuItem.box.SetRole(tomo.R("objects", "MenuItem"))
box.label.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle)) menuItem.label.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
box.SetAttr(tomo.ALayout(layouts.Row { false, true })) menuItem.box.SetAttr(tomo.ALayout(layouts.Row { false, true }))
box.Add(box.icon) menuItem.box.Add(menuItem.icon)
box.Add(box.label) menuItem.box.Add(menuItem.label)
box.SetInputMask(true) menuItem.box.SetInputMask(true)
box.OnButtonDown(box.handleButtonDown) menuItem.box.OnMouseEnter(menuItem.handleMouseEnter)
box.OnButtonUp(box.handleButtonUp) menuItem.box.OnMouseLeave(menuItem.handleMouseLeave)
box.OnKeyDown(box.handleKeyDown) menuItem.box.OnButtonDown(menuItem.handleButtonDown)
box.OnKeyUp(box.handleKeyUp) menuItem.box.OnButtonUp(menuItem.handleButtonUp)
box.SetFocusable(true) menuItem.box.OnKeyDown(menuItem.handleKeyDown)
return box menuItem.box.OnKeyUp(menuItem.handleKeyUp)
menuItem.box.SetFocusable(true)
return menuItem
}
// GetBox returns the underlying box.
func (this *MenuItem) GetBox () tomo.Box {
return this.box
}
// SetFocused sets whether or not this menu item has keyboard focus. If set to
// true, this method will steal focus away from whichever object currently has
// focus.
func (this *MenuItem) SetFocused (focused bool) {
this.box.SetFocused(focused)
} }
// SetText sets the text of the items's label. // SetText sets the text of the items's label.
@@ -49,8 +70,7 @@ func (this *MenuItem) SetText (text string) {
// SetIcon sets an icon for this item. Setting the icon to IconUnknown will // SetIcon sets an icon for this item. Setting the icon to IconUnknown will
// remove it. // remove it.
func (this *MenuItem) SetIcon (id tomo.Icon) { func (this *MenuItem) SetIcon (id tomo.Icon) {
if this.icon != nil { this.Remove(this.icon) } this.icon.SetIcon(id, tomo.IconSizeSmall)
this.Insert(NewIcon(id, tomo.IconSizeSmall), this.label)
} }
// OnClick specifies a function to be called when the menu item is clicked. // OnClick specifies a function to be called when the menu item is clicked.
@@ -58,6 +78,14 @@ func (this *MenuItem) OnClick (callback func ()) event.Cookie {
return this.on.click.Connect(callback) return this.on.click.Connect(callback)
} }
func (this *MenuItem) handleMouseEnter () {
this.SetFocused(true)
}
func (this *MenuItem) handleMouseLeave () {
this.SetFocused(false)
}
func (this *MenuItem) handleKeyDown (key input.Key, numberPad bool) bool { func (this *MenuItem) handleKeyDown (key input.Key, numberPad bool) bool {
if key != input.KeyEnter && key != input.Key(' ') { return false } if key != input.KeyEnter && key != input.Key(' ') { return false }
return true return true
@@ -76,7 +104,7 @@ func (this *MenuItem) handleButtonDown (button input.Button) bool {
func (this *MenuItem) handleButtonUp (button input.Button) bool { func (this *MenuItem) handleButtonUp (button input.Button) bool {
if button != input.ButtonLeft { return false } if button != input.ButtonLeft { return false }
if this.Window().MousePosition().In(this.Bounds()) { if this.box.Window().MousePosition().In(this.box.Bounds()) {
this.on.click.Broadcast() this.on.click.Broadcast()
} }
return true return true

View File

@@ -4,22 +4,29 @@ import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data" import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
var _ tomo.Object = new(MimeIcon)
// MimeIcon displays an icon of a MIME type. // MimeIcon displays an icon of a MIME type.
type MimeIcon struct { type MimeIcon struct {
tomo.Box box tomo.Box
mime data.Mime mime data.Mime
size tomo.IconSize size tomo.IconSize
} }
// NewMimeIcon creates a new icon from a MIME type. // NewMimeIcon creates a new icon from a MIME type.
func NewMimeIcon (mime data.Mime, size tomo.IconSize) *MimeIcon { func NewMimeIcon (mime data.Mime, size tomo.IconSize) *MimeIcon {
this := &MimeIcon { mimeIcon := &MimeIcon {
Box: tomo.NewBox(), box: tomo.NewBox(),
} }
this.SetRole(tomo.R("objects", "MimeIcon")) mimeIcon.box.SetRole(tomo.R("objects", "MimeIcon"))
this.SetIcon(mime, size) mimeIcon.SetIcon(mime, size)
this.OnIconSetChange(this.handleIconSetChange) mimeIcon.box.OnIconSetChange(mimeIcon.handleIconSetChange)
return this return mimeIcon
}
// GetBox returns the underlying box.
func (this *MimeIcon) GetBox () tomo.Box {
return this.box
} }
// SetIcon sets the MIME type and size of the icon. // SetIcon sets the MIME type and size of the icon.
@@ -35,12 +42,12 @@ func (this *MimeIcon) handleIconSetChange () {
} }
func (this *MimeIcon) setTexture (texture canvas.Texture) { func (this *MimeIcon) setTexture (texture canvas.Texture) {
this.SetAttr(tomo.ATexture(texture)) this.box.SetAttr(tomo.ATexture(texture))
this.SetAttr(tomo.ATextureMode(tomo.TextureModeCenter)) this.box.SetAttr(tomo.ATextureMode(tomo.TextureModeCenter))
if texture == nil { if texture == nil {
this.SetAttr(tomo.AMinimumSize(0, 0)) this.box.SetAttr(tomo.AMinimumSize(0, 0))
} else { } else {
bounds := texture.Bounds() bounds := texture.Bounds()
this.SetAttr(tomo.AttrMinimumSize(bounds.Max.Sub(bounds.Min))) this.box.SetAttr(tomo.AttrMinimumSize(bounds.Max.Sub(bounds.Min)))
} }
} }

View File

@@ -3,14 +3,17 @@ package objects
import "math" import "math"
import "strconv" import "strconv"
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/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/layouts" import "git.tebibyte.media/tomo/objects/layouts"
var _ tomo.Object = new(NumberInput)
// NumberInput is an editable text box which accepts only numbers, and has // NumberInput is an editable text box which accepts only numbers, and has
// controls to increment and decrement its value. // controls to increment and decrement its value.
type NumberInput struct { type NumberInput struct {
tomo.ContainerBox box tomo.ContainerBox
input *TextInput input *TextInput
increment *Button increment *Button
decrement *Button decrement *Button
@@ -22,41 +25,63 @@ type NumberInput struct {
// NewNumberInput creates a new number input with the specified value. // NewNumberInput creates a new number input with the specified value.
func NewNumberInput (value float64) *NumberInput { func NewNumberInput (value float64) *NumberInput {
box := &NumberInput { numberInput := &NumberInput {
ContainerBox: tomo.NewContainerBox(), box: tomo.NewContainerBox(),
input: NewTextInput(""), input: NewTextInput(""),
increment: NewButton(""), increment: NewButton(""),
decrement: NewButton(""), decrement: NewButton(""),
} }
box.SetRole(tomo.R("objects", "NumberInput")) numberInput.box.SetRole(tomo.R("objects", "NumberInput"))
box.Add(box.input) numberInput.box.Add(numberInput.input)
box.Add(box.decrement) numberInput.box.Add(numberInput.decrement)
box.Add(box.increment) numberInput.box.Add(numberInput.increment)
box.SetAttr(tomo.ALayout(layouts.Row { true, false, false })) numberInput.box.SetAttr(tomo.ALayout(layouts.Row { true, false, false }))
box.increment.SetIcon(tomo.IconValueIncrement) numberInput.increment.SetIcon(tomo.IconValueIncrement)
box.decrement.SetIcon(tomo.IconValueDecrement) numberInput.decrement.SetIcon(tomo.IconValueDecrement)
box.SetValue(value) numberInput.SetValue(value)
box.OnScroll(box.handleScroll) numberInput.box.OnScroll(numberInput.handleScroll)
box.OnKeyDown(box.handleKeyDown) numberInput.box.OnKeyDown(numberInput.handleKeyDown)
box.OnKeyUp(box.handleKeyUp) numberInput.box.OnKeyUp(numberInput.handleKeyUp)
box.input.OnConfirm(box.handleConfirm) numberInput.input.OnConfirm(numberInput.handleConfirm)
box.input.OnValueChange(box.on.valueChange.Broadcast) numberInput.input.OnValueChange(numberInput.on.valueChange.Broadcast)
box.increment.OnClick(func () { box.shift(1) }) numberInput.increment.OnClick(func () { numberInput.shift( 1) })
box.decrement.OnClick(func () { box.shift(-1) }) numberInput.decrement.OnClick(func () { numberInput.shift(-1) })
return box return numberInput
}
// GetBox returns the underlying box.
func (this *NumberInput) GetBox () tomo.Box {
return this.box
}
// SetFocused sets whether or not this number input has keyboard focus. If set
// to true, this method will steal focus away from whichever object currently
// has focus.
func (this *NumberInput) SetFocused (focused bool) {
this.input.SetFocused(focused)
}
// Select sets the text cursor or selection.
func (this *NumberInput) Select (dot text.Dot) {
this.input.Select(dot)
}
// Dot returns the text cursor or selection.
func (this *NumberInput) Dot () text.Dot {
return this.input.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

View File

@@ -5,11 +5,24 @@ import "git.tebibyte.media/tomo/tomo"
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"
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 ContainerBox. // 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 {
tomo.ContainerBox box tomo.ContainerBox
handle *SliderHandle handle *sliderHandle
layout scrollbarLayout layout scrollbarLayout
dragging bool dragging bool
dragOffset image.Point dragOffset image.Point
@@ -23,8 +36,8 @@ type Scrollbar struct {
func newScrollbar (orient string) *Scrollbar { func newScrollbar (orient string) *Scrollbar {
this := &Scrollbar { this := &Scrollbar {
ContainerBox: tomo.NewContainerBox(), box: tomo.NewContainerBox(),
handle: &SliderHandle { handle: &sliderHandle {
Box: tomo.NewBox(), Box: tomo.NewBox(),
}, },
layout: scrollbarLayout { layout: scrollbarLayout {
@@ -32,21 +45,21 @@ func newScrollbar (orient string) *Scrollbar {
}, },
} }
this.Add(this.handle) this.box.Add(this.handle)
this.SetFocusable(true) this.box.SetFocusable(true)
this.SetInputMask(true) this.box.SetInputMask(true)
this.OnKeyUp(this.handleKeyUp) this.box.OnKeyUp(this.handleKeyUp)
this.OnKeyDown(this.handleKeyDown) this.box.OnKeyDown(this.handleKeyDown)
this.OnButtonDown(this.handleButtonDown) this.box.OnButtonDown(this.handleButtonDown)
this.OnButtonUp(this.handleButtonUp) this.box.OnButtonUp(this.handleButtonUp)
this.OnMouseMove(this.handleMouseMove) this.box.OnMouseMove(this.handleMouseMove)
this.OnScroll(this.handleScroll) this.box.OnScroll(this.handleScroll)
this.handle.SetRole(tomo.R("objects", "SliderHandle")) this.handle.SetRole(tomo.R("objects", "ScrollbarHandle"))
this.handle.SetTag(orient, true) this.handle.SetTag(orient, true)
this.SetRole(tomo.R("objects", "Slider")) this.box.SetRole(tomo.R("objects", "Scrollbar"))
this.SetTag(orient, true) this.box.SetTag(orient, true)
return this return this
} }
@@ -60,13 +73,18 @@ func NewHorizontalScrollbar () *Scrollbar {
return newScrollbar("horizontal") return newScrollbar("horizontal")
} }
// GetBox returns the underlying box.
func (this *Scrollbar) GetBox () tomo.Box {
return this.box
}
// Link assigns this scrollbar to a ContentObject. Closing the returned cookie // Link assigns this scrollbar to a ContentObject. Closing the returned cookie
// will unlink it. // will unlink it.
func (this *Scrollbar) Link (box tomo.ContentObject) event.Cookie { func (this *Scrollbar) Link (box tomo.ContentObject) event.Cookie {
this.layout.linked = box this.layout.linked = box
this.linkCookie = this.newLinkCookie ( this.linkCookie = this.newLinkCookie (
box.OnContentBoundsChange(this.handleLinkedContentBoundsChange)) box.OnContentBoundsChange(this.handleLinkedContentBoundsChange))
this.SetAttr(tomo.ALayout(this.layout)) this.box.SetAttr(tomo.ALayout(this.layout))
return this.linkCookie return this.linkCookie
} }
@@ -79,7 +97,7 @@ func (this *Scrollbar) handleLinkedContentBoundsChange () {
} else { } else {
this.layout.value = this.layout.contentPos() / trackLength this.layout.value = this.layout.contentPos() / trackLength
} }
this.SetAttr(tomo.ALayout(this.layout)) this.box.SetAttr(tomo.ALayout(this.layout))
if this.layout.value != previousValue { if this.layout.value != previousValue {
this.on.valueChange.Broadcast() this.on.valueChange.Broadcast()
} }
@@ -150,7 +168,7 @@ func (this *Scrollbar) handleKeyUp (key input.Key, numpad bool) bool {
} }
func (this *Scrollbar) handleKeyDown (key input.Key, numpad bool) bool { func (this *Scrollbar) handleKeyDown (key input.Key, numpad bool) bool {
modifiers := this.Window().Modifiers() modifiers := this.box.Window().Modifiers()
switch key { switch key {
case input.KeyUp, input.KeyLeft: case input.KeyUp, input.KeyLeft:
@@ -183,7 +201,7 @@ func (this *Scrollbar) handleKeyDown (key input.Key, numpad bool) bool {
} }
func (this *Scrollbar) handleButtonDown (button input.Button) bool { func (this *Scrollbar) handleButtonDown (button input.Button) bool {
pointer := this.Window().MousePosition() pointer := this.box.Window().MousePosition()
handle := this.handle.Bounds() handle := this.handle.Bounds()
within := pointer.In(handle) within := pointer.In(handle)
@@ -199,7 +217,7 @@ func (this *Scrollbar) handleButtonDown (button input.Button) bool {
this.dragging = true this.dragging = true
this.dragOffset = this.dragOffset =
pointer.Sub(this.handle.Bounds().Min). pointer.Sub(this.handle.Bounds().Min).
Add(this.InnerBounds().Min) Add(this.box.InnerBounds().Min)
this.drag() this.drag()
} else { } else {
this.dragOffset = this.fallbackDragOffset() this.dragOffset = this.fallbackDragOffset()
@@ -253,8 +271,8 @@ func (this *Scrollbar) handleScroll (x, y float64) bool {
} }
func (this *Scrollbar) drag () { func (this *Scrollbar) drag () {
pointer := this.Window().MousePosition().Sub(this.dragOffset) pointer := this.box.Window().MousePosition().Sub(this.dragOffset)
gutter := this.InnerBounds() gutter := this.box.InnerBounds()
handle := this.handle.Bounds() handle := this.handle.Bounds()
if this.layout.vertical { if this.layout.vertical {
@@ -270,10 +288,10 @@ func (this *Scrollbar) drag () {
func (this *Scrollbar) fallbackDragOffset () image.Point { func (this *Scrollbar) fallbackDragOffset () image.Point {
if this.layout.vertical { if this.layout.vertical {
return this.InnerBounds().Min. return this.box.InnerBounds().Min.
Add(image.Pt(0, this.handle.Bounds().Dy() / 2)) Add(image.Pt(0, this.handle.Bounds().Dy() / 2))
} else { } else {
return this.InnerBounds().Min. return this.box.InnerBounds().Min.
Add(image.Pt(this.handle.Bounds().Dx() / 2, 0)) Add(image.Pt(this.handle.Bounds().Dx() / 2, 0))
} }
} }
@@ -307,7 +325,7 @@ func (this *scrollbarCookie) Close () {
cookie.Close() cookie.Close()
} }
this.owner.layout.linked = nil this.owner.layout.linked = nil
this.owner.SetAttr(tomo.ALayout(this.owner.layout)) this.owner.box.SetAttr(tomo.ALayout(this.owner.layout))
} }
type scrollbarLayout struct { type scrollbarLayout struct {

View File

@@ -37,9 +37,11 @@ func (sides ScrollSide) String () string {
} }
} }
var _ tomo.Object = new(ScrollContainer)
// ScrollContainer couples a ContentBox with one or two Scrollbars. // ScrollContainer couples a ContentBox with one or two Scrollbars.
type ScrollContainer struct { type ScrollContainer struct {
tomo.ContainerBox box tomo.ContainerBox
root tomo.ContentObject root tomo.ContentObject
horizontal *Scrollbar horizontal *Scrollbar
@@ -55,30 +57,35 @@ type ScrollContainer struct {
// NewScrollContainer creates a new scroll container. // NewScrollContainer creates a new scroll container.
func NewScrollContainer (sides ScrollSide) *ScrollContainer { func NewScrollContainer (sides ScrollSide) *ScrollContainer {
this := &ScrollContainer { scrollContainer := &ScrollContainer {
ContainerBox: tomo.NewContainerBox(), box: tomo.NewContainerBox(),
} }
if sides.Vertical() { if sides.Vertical() {
this.vertical = NewVerticalScrollbar() scrollContainer.vertical = NewVerticalScrollbar()
this.vertical.OnValueChange(this.handleValueChange) scrollContainer.vertical.OnValueChange(scrollContainer.handleValueChange)
this.Add(this.vertical) scrollContainer.box.Add(scrollContainer.vertical)
} }
if sides.Horizontal() { if sides.Horizontal() {
this.horizontal = NewHorizontalScrollbar() scrollContainer.horizontal = NewHorizontalScrollbar()
this.horizontal.OnValueChange(this.handleValueChange) scrollContainer.horizontal.OnValueChange(scrollContainer.handleValueChange)
this.Add(this.horizontal) scrollContainer.box.Add(scrollContainer.horizontal)
} }
this.OnScroll(this.handleScroll) scrollContainer.box.OnScroll(scrollContainer.handleScroll)
this.OnKeyDown(this.handleKeyDown) scrollContainer.box.OnKeyDown(scrollContainer.handleKeyDown)
this.OnKeyUp(this.handleKeyUp) scrollContainer.box.OnKeyUp(scrollContainer.handleKeyUp)
this.SetRole(tomo.R("objects", "ScrollContainer")) scrollContainer.box.SetRole(tomo.R("objects", "ScrollContainer"))
this.SetTag(sides.String(), true) scrollContainer.box.SetTag(sides.String(), true)
if sides == ScrollHorizontal { if sides == ScrollHorizontal {
this.SetAttr(tomo.ALayout(layouts.NewGrid(true)(true, false))) scrollContainer.box.SetAttr(tomo.ALayout(layouts.NewGrid(true)(true, false)))
} else { } else {
this.SetAttr(tomo.ALayout(layouts.NewGrid(true, false)(true, false))) scrollContainer.box.SetAttr(tomo.ALayout(layouts.NewGrid(true, false)(true, false)))
} }
return this return scrollContainer
}
// GetBox returns the underlying box.
func (this *ScrollContainer) GetBox () tomo.Box {
return this.box
} }
// SetRoot sets the root child of the ScrollContainer. There can only be one at // SetRoot sets the root child of the ScrollContainer. There can only be one at
@@ -87,7 +94,7 @@ func NewScrollContainer (sides ScrollSide) *ScrollContainer {
func (this *ScrollContainer) SetRoot (root tomo.ContentObject) { func (this *ScrollContainer) SetRoot (root tomo.ContentObject) {
if this.root != nil { if this.root != nil {
// remove root and close cookies // remove root and close cookies
this.Remove(this.root) this.box.Remove(this.root)
if this.horizontalCookie != nil { if this.horizontalCookie != nil {
this.horizontalCookie.Close() this.horizontalCookie.Close()
this.horizontalCookie = nil this.horizontalCookie = nil
@@ -102,11 +109,11 @@ func (this *ScrollContainer) SetRoot (root tomo.ContentObject) {
// insert root at the beginning (for keynav) // insert root at the beginning (for keynav)
switch { switch {
case this.vertical != nil: case this.vertical != nil:
this.Insert(root, this.vertical) this.box.Insert(root, this.vertical)
case this.horizontal != nil: case this.horizontal != nil:
this.Insert(root, this.horizontal) this.box.Insert(root, this.horizontal)
default: default:
this.Add(root) this.box.Add(root)
} }
// link root and remember cookies // link root and remember cookies
@@ -191,7 +198,7 @@ func (this *ScrollContainer) handleScroll (x, y float64) bool {
} }
func (this *ScrollContainer) handleKeyDown (key input.Key, numpad bool) bool { func (this *ScrollContainer) handleKeyDown (key input.Key, numpad bool) bool {
modifiers := this.Window().Modifiers() modifiers := this.box.Window().Modifiers()
vector := image.Point { } vector := image.Point { }
switch key { switch key {
case input.KeyPageUp: case input.KeyPageUp:

View File

@@ -2,16 +2,23 @@ package objects
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
var _ tomo.Object = new(Separator)
// Separator is a line for visually separating elements. // Separator is a line for visually separating elements.
type Separator struct { type Separator struct {
tomo.Box box tomo.Box
} }
// NewSeparator creates a new separator line. // NewSeparator creates a new separator line.
func NewSeparator () *Separator { func NewSeparator () *Separator {
this := &Separator { this := &Separator {
Box: tomo.NewBox(), box: tomo.NewBox(),
} }
this.SetRole(tomo.R("objects", "Separator")) this.box.SetRole(tomo.R("objects", "Separator"))
return this return this
} }
// GetBox returns the underlying box.
func (this *Separator) GetBox () tomo.Box {
return this.box
}

View File

@@ -6,10 +6,23 @@ import "git.tebibyte.media/tomo/tomo"
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"
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 {
tomo.ContainerBox box tomo.ContainerBox
handle *SliderHandle handle *sliderHandle
layout sliderLayout layout sliderLayout
dragging bool dragging bool
dragOffset image.Point dragOffset image.Point
@@ -21,16 +34,14 @@ type Slider struct {
} }
} }
// SliderHandle is a simple object that serves as a handle for sliders and type sliderHandle struct {
// scrollbars. It is completely inert.
type SliderHandle struct {
tomo.Box tomo.Box
} }
func newSlider (orient string, value float64) *Slider { func newSlider (orient string, value float64) *Slider {
this := &Slider { slider := &Slider {
ContainerBox: tomo.NewContainerBox(), box: tomo.NewContainerBox(),
handle: &SliderHandle { handle: &sliderHandle {
Box: tomo.NewBox(), Box: tomo.NewBox(),
}, },
layout: sliderLayout { layout: sliderLayout {
@@ -40,23 +51,23 @@ func newSlider (orient string, value float64) *Slider {
step: 0.05, step: 0.05,
} }
this.Add(this.handle) slider.handle.SetRole(tomo.R("objects", "SliderHandle"))
this.SetFocusable(true) slider.handle.SetTag(orient, true)
this.SetValue(value) slider.box.SetRole(tomo.R("objects", "Slider"))
slider.box.SetTag(orient, true)
this.SetInputMask(true)
this.OnKeyUp(this.handleKeyUp)
this.OnKeyDown(this.handleKeyDown)
this.OnButtonDown(this.handleButtonDown)
this.OnButtonUp(this.handleButtonUp)
this.OnMouseMove(this.handleMouseMove)
this.OnScroll(this.handleScroll)
this.handle.SetRole(tomo.R("objects", "SliderHandle")) slider.box.Add(slider.handle)
this.handle.SetTag(orient, true) slider.box.SetFocusable(true)
this.SetRole(tomo.R("objects", "Slider")) slider.SetValue(value)
this.SetTag(orient, true)
return this slider.box.SetInputMask(true)
slider.box.OnKeyUp(slider.handleKeyUp)
slider.box.OnKeyDown(slider.handleKeyDown)
slider.box.OnButtonDown(slider.handleButtonDown)
slider.box.OnButtonUp(slider.handleButtonUp)
slider.box.OnMouseMove(slider.handleMouseMove)
slider.box.OnScroll(slider.handleScroll)
return slider
} }
// NewVerticalSlider creates a new vertical slider with the specified value. // NewVerticalSlider creates a new vertical slider with the specified value.
@@ -69,13 +80,25 @@ func NewHorizontalSlider (value float64) *Slider {
return newSlider("horizontal", value) return newSlider("horizontal", value)
} }
// GetBox returns the underlying box.
func (this *Slider) GetBox () tomo.Box {
return this.box
}
// SetFocused sets whether or not this slider has keyboard focus. If set to
// true, this method will steal focus away from whichever object currently has
// focus.
func (this *Slider) SetFocused (focused bool) {
this.box.SetFocused(focused)
}
// SetValue sets the value of the slider between 0 and 1. // SetValue sets the value of the slider between 0 and 1.
func (this *Slider) SetValue (value float64) { func (this *Slider) SetValue (value float64) {
if value < 0 { value = 0 } if value < 0 { value = 0 }
if value > 1 { value = 1 } if value > 1 { value = 1 }
if value == this.layout.value { return } if value == this.layout.value { return }
this.layout.value = value this.layout.value = value
this.SetAttr(tomo.ALayout(this.layout)) this.box.SetAttr(tomo.ALayout(this.layout))
} }
// Value returns the value of the slider between 0 and 1. // Value returns the value of the slider between 0 and 1.
@@ -104,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.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)
@@ -112,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.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)
@@ -142,7 +165,7 @@ func (this *Slider) handleKeyUp (key input.Key, numpad bool) bool {
} }
func (this *Slider) handleButtonDown (button input.Button) bool { func (this *Slider) handleButtonDown (button input.Button) bool {
pointer := this.Window().MousePosition() pointer := this.box.Window().MousePosition()
handle := this.handle.Bounds() handle := this.handle.Bounds()
within := pointer.In(handle) within := pointer.In(handle)
@@ -158,7 +181,7 @@ func (this *Slider) handleButtonDown (button input.Button) bool {
this.dragging = true this.dragging = true
this.dragOffset = this.dragOffset =
pointer.Sub(this.handle.Bounds().Min). pointer.Sub(this.handle.Bounds().Min).
Add(this.InnerBounds().Min) Add(this.box.InnerBounds().Min)
this.drag() this.drag()
} else { } else {
this.dragOffset = this.fallbackDragOffset() this.dragOffset = this.fallbackDragOffset()
@@ -211,8 +234,8 @@ func (this *Slider) handleScroll (x, y float64) bool {
} }
func (this *Slider) drag () { func (this *Slider) drag () {
pointer := this.Window().MousePosition().Sub(this.dragOffset) pointer := this.box.Window().MousePosition().Sub(this.dragOffset)
gutter := this.InnerBounds() gutter := this.box.InnerBounds()
handle := this.handle.Bounds() handle := this.handle.Bounds()
if this.layout.vertical { if this.layout.vertical {
@@ -230,10 +253,10 @@ func (this *Slider) drag () {
func (this *Slider) fallbackDragOffset () image.Point { func (this *Slider) fallbackDragOffset () image.Point {
if this.layout.vertical { if this.layout.vertical {
return this.InnerBounds().Min. return this.box.InnerBounds().Min.
Add(image.Pt(0, this.handle.Bounds().Dy() / 2)) Add(image.Pt(0, this.handle.Bounds().Dy() / 2))
} else { } else {
return this.InnerBounds().Min. return this.box.InnerBounds().Min.
Add(image.Pt(this.handle.Bounds().Dx() / 2, 0)) Add(image.Pt(this.handle.Bounds().Dx() / 2, 0))
} }
} }

105
swatch.go
View File

@@ -8,13 +8,16 @@ 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"
var _ tomo.Object = new(Swatch)
// Swatch displays a color, allowing the user to edit it by clicking on it. // Swatch displays a color, allowing the user to edit it by clicking on it.
type Swatch struct { type Swatch struct {
tomo.CanvasBox box tomo.CanvasBox
value color.Color value color.Color
editing bool editing bool
label string label string
on struct { on struct {
valueChange event.FuncBroadcaster valueChange event.FuncBroadcaster
confirm event.FuncBroadcaster confirm event.FuncBroadcaster
@@ -24,20 +27,32 @@ type Swatch struct {
// NewSwatch creates a new swatch with the given color. // NewSwatch creates a new swatch with the given color.
func NewSwatch (value color.Color) *Swatch { func NewSwatch (value color.Color) *Swatch {
swatch := &Swatch { swatch := &Swatch {
CanvasBox: tomo.NewCanvasBox(), box: tomo.NewCanvasBox(),
} }
swatch.SetRole(tomo.R("objects", "Swatch")) swatch.box.SetRole(tomo.R("objects", "Swatch"))
swatch.SetDrawer(swatch) swatch.box.SetDrawer(swatch)
swatch.SetValue(value) swatch.SetValue(value)
swatch.OnButtonDown(swatch.handleButtonDown) swatch.box.OnButtonDown(swatch.handleButtonDown)
swatch.OnButtonUp(swatch.handleButtonUp) swatch.box.OnButtonUp(swatch.handleButtonUp)
swatch.OnKeyDown(swatch.handleKeyDown) swatch.box.OnKeyDown(swatch.handleKeyDown)
swatch.OnKeyUp(swatch.handleKeyUp) swatch.box.OnKeyUp(swatch.handleKeyUp)
swatch.SetFocusable(true) swatch.box.SetFocusable(true)
return swatch return swatch
} }
// GetBox returns the underlying box.
func (this *Swatch) GetBox () tomo.Box {
return this.box
}
// SetFocused sets whether or not this swatch has keyboard focus. If set to
// true, this method will steal focus away from whichever object currently has
// focus.
func (this *Swatch) SetFocused (focused bool) {
this.box.SetFocused(focused)
}
// Value returns the color of the swatch. // Value returns the color of the swatch.
func (this *Swatch) Value () color.Color { func (this *Swatch) Value () color.Color {
return this.value return this.value
@@ -47,7 +62,7 @@ func (this *Swatch) Value () color.Color {
func (this *Swatch) SetValue (value color.Color) { func (this *Swatch) SetValue (value color.Color) {
this.value = value this.value = value
if value == nil { value = color.Transparent } if value == nil { value = color.Transparent }
this.Invalidate() this.box.Invalidate()
} }
// OnValueChange specifies a function to be called when the swatch's color // OnValueChange specifies a function to be called when the swatch's color
@@ -74,7 +89,7 @@ func (this *Swatch) Choose () {
var err error var err error
var window tomo.Window var window tomo.Window
if parent := this.Window(); parent != nil { if parent := this.box.Window(); parent != nil {
window, err = parent.NewChild(image.Rectangle { }) window, err = parent.NewChild(image.Rectangle { })
} else { } else {
window, err = tomo.NewWindow(image.Rectangle { }) window, err = tomo.NewWindow(image.Rectangle { })
@@ -91,32 +106,44 @@ func (this *Swatch) Choose () {
committed := false committed := false
colorPicker := NewColorPicker(this.Value()) colorPicker := NewHSVAColorPicker(this.Value())
colorPicker.OnValueChange(func () {
this.userSetValue(colorPicker.Value())
})
hexInput := NewTextInput("TODO")
colorMemory := this.value colorMemory := this.value
hexInput := NewTextInput("")
hexInput.SetFocused(true)
cancelButton := NewButton("Cancel") cancelButton := NewButton("Cancel")
cancelButton.SetIcon(tomo.IconDialogCancel) cancelButton.SetIcon(tomo.IconDialogCancel)
okButton := NewButton("OK")
okButton.SetIcon(tomo.IconDialogOkay)
updateHexInput := func () {
nrgba := color.NRGBAModel.Convert(colorPicker.Value()).(color.NRGBA)
hexInput.SetValue(internal.FormatNRGBA(nrgba))
}
updateHexInput()
commit := func () {
committed = true
window.Close()
}
colorPicker.OnValueChange(func () {
this.userSetValue(colorPicker.Value())
updateHexInput()
})
hexInput.OnConfirm(commit)
hexInput.OnValueChange(func () {
nrgba := internal.ParseNRGBA(hexInput.Value())
this.userSetValue(nrgba)
colorPicker.SetValue(nrgba)
})
cancelButton.OnClick(func () { cancelButton.OnClick(func () {
window.Close() window.Close()
}) })
okButton := NewButton("OK") okButton.OnClick(commit)
okButton.SetFocused(true)
okButton.SetIcon(tomo.IconDialogOkay)
okButton.OnClick(func () {
committed = true
window.Close()
})
controlRow := NewInnerContainer ( controlRow := NewInnerContainer (
layouts.ContractHorizontal, layouts.ContractHorizontal,
cancelButton, cancelButton,
okButton) okButton)
controlRow.SetAttr(tomo.AAlign(tomo.AlignEnd, tomo.AlignMiddle)) controlRow.SetAlign(tomo.AlignEnd, tomo.AlignMiddle)
window.SetRoot(NewOuterContainer ( window.SetRoot(NewOuterContainer (
layouts.Column { true, false }, layouts.Column { true, false },
colorPicker, colorPicker,
@@ -142,13 +169,13 @@ func (this *Swatch) Draw (can canvas.Canvas) {
// transparency slash // transparency slash
pen.Stroke(color.RGBA { R: 255, A: 255 }) pen.Stroke(color.RGBA { R: 255, A: 255 })
pen.StrokeWeight(1) pen.StrokeWeight(1)
pen.Path(this.Bounds().Min, this.Bounds().Max) pen.Path(this.box.Bounds().Min, this.box.Bounds().Max)
// color // color
if this.value != nil { if this.value != nil {
pen.StrokeWeight(0) pen.StrokeWeight(0)
pen.Fill(this.value) pen.Fill(this.value)
pen.Rectangle(this.Bounds()) pen.Rectangle(this.box.Bounds())
} }
} }
@@ -158,24 +185,24 @@ func (this *Swatch) userSetValue (value color.Color) {
} }
func (this *Swatch) handleKeyDown (key input.Key, numberPad bool) bool { func (this *Swatch) handleKeyDown (key input.Key, numberPad bool) bool {
if key != input.KeyEnter && key != input.Key(' ') { return false } if !isClickingKey(key) { return false }
return true
}
func (this *Swatch) handleKeyUp (key input.Key, numberPad bool) bool {
if key != input.KeyEnter && key != input.Key(' ') { return false }
this.Choose() this.Choose()
return true return true
} }
func (this *Swatch) handleKeyUp (key input.Key, numberPad bool) bool {
if !isClickingKey(key) { return false }
return true
}
func (this *Swatch) handleButtonDown (button input.Button) bool { func (this *Swatch) handleButtonDown (button input.Button) bool {
if button != input.ButtonLeft { return false } if !isClickingButton(button) { return false }
return true return true
} }
func (this *Swatch) handleButtonUp (button input.Button) bool { func (this *Swatch) handleButtonUp (button input.Button) bool {
if button != input.ButtonLeft { return false } if !isClickingButton(button) { return false }
if this.Window().MousePosition().In(this.Bounds()) { if this.box.Window().MousePosition().In(this.box.Bounds()) {
this.Choose() this.Choose()
} }
return true return true

View File

@@ -4,8 +4,25 @@ import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/objects/layouts" import "git.tebibyte.media/tomo/objects/layouts"
var _ tomo.Object = new(TabbedContainer)
// TabbedContainer holds multiple objects, each in their own tab. The user can
// click the tab bar at the top to choose which one is activated.
//
// Sub-components:
// - TabRow sits at the top of the container and contains a row of tabs.
// - TabSpacer sits at either end of the tab row, bookending the list of tabs.
// - Tab appears in the tab row for each tab in the container. The user can
// click on the tab to switch to it.
//
// TabSpacer tags:
// - [left] The spacer is on the left.
// - [right] The spacer is on the right.
//
// Tab tags:
// - [active] The tab is currently active and its contents are visible.
type TabbedContainer struct { type TabbedContainer struct {
tomo.ContainerBox box tomo.ContainerBox
leftSpacer tomo.Box leftSpacer tomo.Box
rightSpacer tomo.Box rightSpacer tomo.Box
@@ -14,43 +31,51 @@ type TabbedContainer struct {
tabs []*tab tabs []*tab
} }
// NewTabbedContainer creates a new tabbed container.
func NewTabbedContainer () *TabbedContainer { func NewTabbedContainer () *TabbedContainer {
container := &TabbedContainer { tabbedContainer := &TabbedContainer {
ContainerBox: tomo.NewContainerBox(), box: tomo.NewContainerBox(),
} }
container.SetRole(tomo.R("objects", "TabbedContainer")) tabbedContainer.box.SetRole(tomo.R("objects", "TabbedContainer"))
container.SetAttr(tomo.ALayout(layouts.Column { false, true })) tabbedContainer.box.SetAttr(tomo.ALayout(layouts.Column { false, true }))
container.tabsRow = tomo.NewContainerBox() tabbedContainer.tabsRow = tomo.NewContainerBox()
container.tabsRow.SetRole(tomo.R("objects", "TabRow")) tabbedContainer.tabsRow.SetRole(tomo.R("objects", "TabRow"))
container.Add(container.tabsRow) tabbedContainer.box.Add(tabbedContainer.tabsRow)
container.leftSpacer = tomo.NewBox() tabbedContainer.leftSpacer = tomo.NewBox()
container.leftSpacer.SetRole(tomo.R("objects", "TabSpacer")) tabbedContainer.leftSpacer.SetRole(tomo.R("objects", "TabSpacer"))
container.leftSpacer.SetTag("left", true) tabbedContainer.leftSpacer.SetTag("left", true)
container.rightSpacer = tomo.NewBox() tabbedContainer.rightSpacer = tomo.NewBox()
container.rightSpacer.SetRole(tomo.R("objects", "TabSpacer")) tabbedContainer.rightSpacer.SetRole(tomo.R("objects", "TabSpacer"))
container.rightSpacer.SetTag("left", true) tabbedContainer.rightSpacer.SetTag("right", true)
container.ClearTabs() tabbedContainer.ClearTabs()
container.setTabRowLayout() tabbedContainer.setTabRowLayout()
return container 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) { func (this *TabbedContainer) Activate (name string) {
if _, tab := this.findTab(this.active); tab != nil { if _, tab := this.findTab(this.active); tab != nil {
tab.setActive(false) tab.setActive(false)
this.Remove(tab.root) this.box.Remove(tab.root)
} }
if _, tab := this.findTab(name); tab != nil { if _, tab := this.findTab(name); tab != nil {
tab.setActive(true) tab.setActive(true)
this.Add(tab.root) this.box.Add(tab.root)
} else { } else {
name = "" name = ""
} }
this.active = name this.active = name
} }
// AddTab adds an object as a tab with the specified name.
func (this *TabbedContainer) AddTab (name string, root tomo.Object) { func (this *TabbedContainer) AddTab (name string, root tomo.Object) {
tab := &tab { tab := &tab {
TextBox: tomo.NewTextBox(), TextBox: tomo.NewTextBox(),
@@ -79,6 +104,7 @@ func (this *TabbedContainer) AddTab (name string, root tomo.Object) {
} }
} }
// RemoveTab removes the named tab.
func (this *TabbedContainer) RemoveTab (name string) { func (this *TabbedContainer) RemoveTab (name string) {
index, tab := this.findTab(name) index, tab := this.findTab(name)
if index < 0 { return } if index < 0 { return }
@@ -97,6 +123,7 @@ func (this *TabbedContainer) RemoveTab (name string) {
} }
} }
// ClearTabs removes all tabs.
func (this *TabbedContainer) ClearTabs () { func (this *TabbedContainer) ClearTabs () {
this.tabs = nil this.tabs = nil
this.tabsRow.Clear() this.tabsRow.Clear()
@@ -126,10 +153,10 @@ type tab struct {
func (this *tab) setActive (active bool) { func (this *tab) setActive (active bool) {
if active { if active {
this.SetRole(tomo.R("objects", "Tab")) this.SetRole(tomo.R("objects", "Tab"))
this.SetTag("active", false) this.SetTag("active", true)
} else { } else {
this.SetRole(tomo.R("objects", "Tab")) this.SetRole(tomo.R("objects", "Tab"))
this.SetTag("active", true) this.SetTag("active", false)
} }
} }

View File

@@ -6,39 +6,113 @@ 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"
var _ tomo.ContentObject = new(TextInput)
// TextInput is a single-line editable text box. // TextInput is a single-line editable text box.
type TextInput struct { type TextInput struct {
tomo.TextBox box tomo.TextBox
text []rune text []rune
multiline bool
on struct { on struct {
valueChange event.FuncBroadcaster valueChange event.FuncBroadcaster
confirm event.FuncBroadcaster confirm event.FuncBroadcaster
} }
} }
func newTextInput (text string, multiline bool) *TextInput {
textInput := &TextInput {
box: tomo.NewTextBox(),
multiline: multiline,
}
textInput.box.SetRole(tomo.R("objects", "TextInput"))
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
}
// NewTextInput creates a new text input containing the specified text. // NewTextInput creates a new text input containing the specified text.
func NewTextInput (text string) *TextInput { func NewTextInput (text string) *TextInput {
this := &TextInput { TextBox: tomo.NewTextBox() } return newTextInput(text, false)
this.SetRole(tomo.R("objects", "TextInput"))
this.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle))
this.SetAttr(tomo.AOverflow(true, false))
this.SetText(text)
this.SetFocusable(true)
this.SetSelectable(true)
this.OnKeyDown(this.handleKeyDown)
this.OnKeyUp(this.handleKeyUp)
this.OnScroll(this.handleScroll)
return this
} }
// SetText sets the text content of the input. // NewMultilineTextInput creates a new multiline text input containing the
func (this *TextInput) SetText (text string) { // specified text.
func NewMultilineTextInput (text string) *TextInput {
return newTextInput(text, true)
}
// GetBox returns the underlying box.
func (this *TextInput) GetBox () tomo.Box {
return this.box
}
// SetFocused sets whether or not this text input has keyboard focus. If set to
// true, this method will steal focus away from whichever object currently has
// focus.
func (this *TextInput) SetFocused (focused bool) {
this.box.SetFocused(focused)
}
// Select sets the text cursor or selection.
func (this *TextInput) Select (dot text.Dot) {
this.box.Select(dot)
}
// Dot returns the text cursor or selection.
func (this *TextInput) Dot () text.Dot {
return this.box.Dot()
}
// 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))
}
// ContentBounds returns the bounds of the inner content of the text input
// relative to the input's InnerBounds.
func (this *TextInput) ContentBounds () image.Rectangle {
return this.box.ContentBounds()
}
// ScrollTo shifts the origin of the text input's content to the origin of the
// inputs's InnerBounds, offset by the given point.
func (this *TextInput) ScrollTo (position image.Point) {
this.box.ScrollTo(position)
}
// OnContentBoundsChange specifies a function to be called when the text input's
// ContentBounds or InnerBounds changes.
func (this *TextInput) OnContentBoundsChange (callback func ()) event.Cookie {
return this.box.OnContentBoundsChange(callback)
}
// SetValue sets the text content of the input.
func (this *TextInput) SetValue (text string) {
this.text = []rune(text) this.text = []rune(text)
this.TextBox.SetText(text) this.box.SetText(text)
} }
// Text returns the text content of the input. // Value returns the text content of the input.
func (this *TextInput) Text () string { func (this *TextInput) Value () string {
return string(this.text) return string(this.text)
} }
@@ -59,25 +133,40 @@ func (this *TextInput) Type (char rune) {
dot := this.Dot() dot := this.Dot()
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.SetText(string(this.text)) 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 { func (this *TextInput) handleKeyDown (key input.Key, numpad bool) bool {
dot := this.Dot() dot := this.Dot()
modifiers := this.Window().Modifiers() modifiers := this.box.Window().Modifiers()
word := modifiers.Control word := modifiers.Control
changed := false changed := false
defer func () { defer func () {
this.Select(dot) this.Select(dot)
if changed { if changed {
this.SetText(string(this.text)) this.box.SetText(string(this.text))
this.on.valueChange.Broadcast() this.on.valueChange.Broadcast()
} }
} () } ()
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 { switch {
case key == input.KeyEnter: case isConfirmationKey(key):
this.on.confirm.Broadcast() this.on.confirm.Broadcast()
return true return true
case key == input.KeyBackspace: case key == input.KeyBackspace:
@@ -88,13 +177,8 @@ func (this *TextInput) handleKeyDown (key input.Key, numpad bool) bool {
this.text, dot = text.Delete(this.text, dot, word) this.text, dot = text.Delete(this.text, dot, word)
changed = true changed = true
return true return true
case key == input.Key('a') && modifiers.Control: case key.Printable() && !modifiers.Control:
dot.Start = 0 typ()
dot.End = len(this.text)
return true
case key.Printable():
this.text, dot = text.Type(this.text, dot, rune(key))
changed = true
return true return true
default: default:
return false return false
@@ -102,17 +186,23 @@ 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.Window().Modifiers() modifiers := this.box.Window().Modifiers()
if this.multiline && !modifiers.Control {
switch {
case key == '\n', key == '\t':
return true
}
}
switch { switch {
case key == input.KeyEnter: case isConfirmationKey(key):
return true return true
case key == input.KeyBackspace: case key == input.KeyBackspace:
return true return true
case key == input.KeyDelete: case key == input.KeyDelete:
return true return true
case key == input.Key('a') && modifiers.Control: case key.Printable() && !modifiers.Control:
return true
case key.Printable():
return true return true
default: default:
return false return false

View File

@@ -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(TabbedContainer)
// 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 {