15 Commits

10 changed files with 173 additions and 59 deletions

View File

@@ -5,13 +5,15 @@ 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.
type Button struct { type Button struct {
tomo.ContainerBox box tomo.ContainerBox
label *Label label *Label
icon *Icon icon *Icon
@@ -24,33 +26,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.SetAttr(tomo.AAlign(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 +73,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 +81,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,11 +94,11 @@ 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))
} }
} }
@@ -106,7 +120,7 @@ func (this *Button) handleButtonDown (button input.Button) bool {
func (this *Button) handleButtonUp (button input.Button) bool { func (this *Button) handleButtonUp (button input.Button) bool {
if !isClickingButton(button) { 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,12 @@ 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.
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 +25,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)
@@ -48,17 +50,22 @@ func NewCalendar (tm time.Time) *Calendar {
calendar.grid.SetRole(tomo.R("objects", "CalendarGrid")) calendar.grid.SetRole(tomo.R("objects", "CalendarGrid"))
calendar.grid.SetAttr(tomo.ALayout(layouts.NewGrid ( calendar.grid.SetAttr(tomo.ALayout(layouts.NewGrid (
true, true, true, true, true, true, true)())) true, true, true, true, true, true, true)()))
calendar.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

@@ -1,6 +1,10 @@
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 variants: an outer
@@ -8,14 +12,14 @@ import "git.tebibyte.media/tomo/tomo"
// its edges, whereas the inner container does not. The container will have a // its edges, whereas the inner container does not. The container will have a
// corresponding object role variation of either "outer" or "inner". // corresponding object role variation of either "outer" or "inner".
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 +31,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 +40,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,6 +53,7 @@ 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.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle))
@@ -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 },

View File

@@ -24,6 +24,8 @@ func NewLabelSwatch (value color.Color, text string) *LabelSwatch {
box.SetRole(tomo.R("objects", "LabelSwatch")) box.SetRole(tomo.R("objects", "LabelSwatch"))
box.swatch.label = text box.swatch.label = text
box.label.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle)) box.label.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle))
box.label.SetSelectable(false)
box.label.SetFocusable(false)
box.Add(box.swatch) box.Add(box.swatch)
box.Add(box.label) box.Add(box.label)
box.SetAttr(tomo.ALayout(layouts.Row { false, true })) box.SetAttr(tomo.ALayout(layouts.Row { false, true }))
@@ -61,12 +63,12 @@ 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.Window().MousePosition().In(this.Bounds()) {
this.swatch.SetFocused(true) this.swatch.SetFocused(true)
this.swatch.Choose() this.swatch.Choose()

15
menu.go
View File

@@ -72,6 +72,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()
@@ -90,21 +91,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

@@ -38,6 +38,8 @@ func NewIconMenuItem (icon tomo.Icon, text string) *MenuItem {
box.Add(box.label) box.Add(box.label)
box.SetInputMask(true) box.SetInputMask(true)
box.OnMouseEnter(box.handleMouseEnter)
box.OnMouseLeave(box.handleMouseLeave)
box.OnButtonDown(box.handleButtonDown) box.OnButtonDown(box.handleButtonDown)
box.OnButtonUp(box.handleButtonUp) box.OnButtonUp(box.handleButtonUp)
box.OnKeyDown(box.handleKeyDown) box.OnKeyDown(box.handleKeyDown)
@@ -62,6 +64,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

View File

@@ -9,7 +9,7 @@ import "git.tebibyte.media/tomo/tomo/event"
// overflowing ContainerBox. // overflowing ContainerBox.
type Scrollbar struct { type Scrollbar struct {
tomo.ContainerBox tomo.ContainerBox
handle *SliderHandle handle *sliderHandle
layout scrollbarLayout layout scrollbarLayout
dragging bool dragging bool
dragOffset image.Point dragOffset image.Point
@@ -24,7 +24,7 @@ type Scrollbar struct {
func newScrollbar (orient string) *Scrollbar { func newScrollbar (orient string) *Scrollbar {
this := &Scrollbar { this := &Scrollbar {
ContainerBox: tomo.NewContainerBox(), ContainerBox: tomo.NewContainerBox(),
handle: &SliderHandle { handle: &sliderHandle {
Box: tomo.NewBox(), Box: tomo.NewBox(),
}, },
layout: scrollbarLayout { layout: scrollbarLayout {
@@ -43,9 +43,9 @@ func newScrollbar (orient string) *Scrollbar {
this.OnMouseMove(this.handleMouseMove) this.OnMouseMove(this.handleMouseMove)
this.OnScroll(this.handleScroll) this.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.SetRole(tomo.R("objects", "Scrollbar"))
this.SetTag(orient, true) this.SetTag(orient, true)
return this return this
} }

View File

@@ -9,7 +9,7 @@ import "git.tebibyte.media/tomo/tomo/event"
// 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.
type Slider struct { type Slider struct {
tomo.ContainerBox tomo.ContainerBox
handle *SliderHandle handle *sliderHandle
layout sliderLayout layout sliderLayout
dragging bool dragging bool
dragOffset image.Point dragOffset image.Point
@@ -21,16 +21,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 { this := &Slider {
ContainerBox: tomo.NewContainerBox(), ContainerBox: tomo.NewContainerBox(),
handle: &SliderHandle { handle: &sliderHandle {
Box: tomo.NewBox(), Box: tomo.NewBox(),
}, },
layout: sliderLayout { layout: sliderLayout {

View File

@@ -129,7 +129,7 @@ func (this *Swatch) Choose () {
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,