27 Commits

Author SHA1 Message Date
e62afcd667 Fix calendar prev/forward month buttons 2024-06-19 00:38:54 -04:00
f778ef5c95 Add calendar widget 2024-06-18 19:37:37 -04:00
c06f10c193 Fix flow layout getting stuck in a loop 2024-06-15 18:12:08 -04:00
23fb28ce5c Some internal layouts return recommended sizes 2024-06-15 09:27:35 -04:00
3533ce3726 Row/Column uses correct axis for determining free space 2024-06-15 07:58:51 -04:00
6d157eb9af Fix Column layout not allocating space to expanding boxes 2024-06-15 07:54:49 -04:00
da346f2f12 Row, Column have recommended size support 2024-06-12 03:35:38 -04:00
71a41d390f Row, Column handle expansion properly 2024-06-12 03:32:32 -04:00
9ce7f8b8f3 Contract layouts are now based on new Row and Column layouts 2024-06-12 03:15:38 -04:00
1596d54834 Fix up objects code 2024-06-11 17:17:11 -04:00
95d3dc3288 Add placeholder methods for recommended sizes 2024-06-11 17:12:18 -04:00
1069ae6455 Removed cut layout
Not a very good implementation
2024-06-11 16:58:38 -04:00
5c8358fc4a Add blank vars to ensure layouts satisfy tomo.Layout 2024-06-11 16:46:04 -04:00
6a8aaca18d Rename layouts.go -> contract.go 2024-06-11 16:44:15 -04:00
460733c8f3 Update code for objects 2024-06-11 16:40:35 -04:00
5d2a366a62 Update Tomo API 2024-06-11 16:40:06 -04:00
2c7c77d8da Add menus 2024-06-07 01:59:29 -04:00
8139d871cc Text-less buttons place their icon in the middle 2024-06-07 01:58:29 -04:00
bb320dfcc9 Update Tomo API 2024-06-07 01:58:09 -04:00
2ab920eb26 Store role in Boxes 2024-06-03 21:13:18 -04:00
d8ae20d739 Update Tomo API 2024-06-03 21:10:20 -04:00
06561bb671 Scrollbar, ScrollContainer use ContentObject now 2024-05-27 16:28:48 -04:00
a71d81af48 Checkbox uses CheckboxUnchecked icon when unchecked 2024-05-27 15:59:24 -04:00
bd9dbb762d Update Tomo API 2024-05-27 15:22:18 -04:00
6389556199 Updated all objects to new API 2024-05-26 17:21:58 -04:00
06d99b2790 Update Tomo API 2024-05-26 17:13:40 -04:00
6ab689b5c1 Renamed input.go to textinput.go 2024-05-20 02:23:07 -04:00
26 changed files with 744 additions and 351 deletions

View File

@@ -1,14 +1,13 @@
package objects
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/objects/layouts"
var buttonLayout = layouts.NewGrid([]bool { true }, []bool { true })
var iconButtonLayout = layouts.NewGrid([]bool { false }, []bool { true })
var bothButtonLayout = layouts.NewGrid([]bool { false, true }, []bool { true })
var buttonLayout = layouts.Row { true }
var iconButtonLayout = layouts.Row { true }
var bothButtonLayout = layouts.Row { false, true }
// Button is a clickable button.
type Button struct {
@@ -29,7 +28,7 @@ func NewButton (text string) *Button {
ContainerBox: tomo.NewContainerBox(),
label: NewLabel(text),
}
theme.Apply(box, theme.R("objects", "Button", ""))
box.SetRole(tomo.R("objects", "Button", ""))
box.label.SetAlign(tomo.AlignMiddle, tomo.AlignMiddle)
box.SetLayout(buttonLayout)
box.SetText(text)
@@ -49,7 +48,7 @@ func NewButton (text string) *Button {
func (this *Button) SetText (text string) {
this.label.SetText(text)
if this.labelActive && text == "" {
this.Delete(this.label)
this.Remove(this.label)
this.labelActive = false
}
if !this.labelActive && text != "" {
@@ -61,11 +60,11 @@ func (this *Button) SetText (text string) {
// SetIcon sets an icon for this button. Setting the icon to IconUnknown will
// remove it.
func (this *Button) SetIcon (id theme.Icon) {
if this.icon != nil { this.Delete(this.icon) }
func (this *Button) SetIcon (id tomo.Icon) {
if this.icon != nil { this.Remove(this.icon) }
var icon *Icon; if id != theme.IconUnknown {
icon = NewIcon(id, theme.IconSizeSmall)
var icon *Icon; if id != tomo.IconUnknown {
icon = NewIcon(id, tomo.IconSizeSmall)
}
this.icon = icon

167
calendar.go Normal file
View File

@@ -0,0 +1,167 @@
package objects
import "fmt"
import "time"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/objects/layouts"
// Calendar is an object that can display a date and allow the user to change
// it. It can display one month at a time.
type Calendar struct {
tomo.ContainerBox
grid tomo.ContainerBox
time time.Time
monthLabel *Label
on struct {
edit event.FuncBroadcaster
}
}
// NewCalendar creates a new calendar with the specified date.
func NewCalendar (tm time.Time) *Calendar {
calendar := &Calendar {
ContainerBox: tomo.NewContainerBox(),
time: tm,
}
calendar.SetRole(tomo.R("objects", "Calendar", ""))
calendar.SetLayout(layouts.ContractVertical)
prevButton := NewButton("")
prevButton.SetIcon(tomo.IconGoPrevious)
prevButton.OnClick(func () {
calendar.prevMonth()
calendar.on.edit.Broadcast()
})
nextButton := NewButton("")
nextButton.SetIcon(tomo.IconGoNext)
nextButton.OnClick(func () {
calendar.nextMonth()
calendar.on.edit.Broadcast()
})
calendar.monthLabel = NewLabel("")
calendar.monthLabel.SetAlign(tomo.AlignMiddle, tomo.AlignMiddle)
calendar.grid = tomo.NewContainerBox()
calendar.grid.SetRole(tomo.R("objects", "CalendarGrid", ""))
calendar.grid.SetLayout(layouts.NewGrid (
[]bool { true, true, true, true, true, true, true },
[]bool { }))
calendar.Add(NewInnerContainer (
layouts.Row { false, true, false },
prevButton, calendar.monthLabel, nextButton))
calendar.Add(calendar.grid)
calendar.OnScroll(calendar.handleScroll)
calendar.CaptureScroll(true)
calendar.refresh()
return calendar
}
// SetTime sets the date the calendar will display.
func (this *Calendar) SetTime (tm time.Time) {
if this.time == tm { return }
this.time = tm
this.refresh()
}
// OnEdit sets a function to be called when the user changes the date on the
// calendar.
func (this *Calendar) OnEdit (callback func ()) {
this.on.edit.Connect(callback)
}
func (this *Calendar) prevMonth () {
this.time = firstOfMonth(this.time.Add(24 * time.Hour * -20))
this.refresh()
}
func (this *Calendar) nextMonth () {
this.time = firstOfMonth(this.time.Add(24 * time.Hour * 40))
this.refresh()
}
var weekdayAbbreviations = []string {
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
}
func (this *Calendar) refresh () {
this.monthLabel.SetText(this.time.Format("2006 January"))
this.grid.Clear()
for _, day := range weekdayAbbreviations {
dayLabel := tomo.NewTextBox()
dayLabel.SetRole(tomo.R("objects", "CalendarWeekdayHeader", ""))
dayLabel.SetText(day)
dayLabel.SetAlign(tomo.AlignMiddle, tomo.AlignMiddle)
this.grid.Add(dayLabel)
}
dayIter := 0 - int(firstOfMonth(this.time).Weekday())
if dayIter <= -6 {
dayIter = 1
}
weekday := 0
totalDays := daysInMonth(this.time)
for ; dayIter <= totalDays; dayIter ++ {
weekday = (weekday + 1) % 7
if dayIter > 0 {
day := tomo.NewTextBox()
day.SetText(fmt.Sprint(dayIter))
if weekday == 1 || weekday == 0 {
day.SetRole(tomo.R("objects", "CalendarDay", "weekend"))
} else {
day.SetRole(tomo.R("objects", "CalendarDay", "weekday"))
}
this.grid.Add(day)
} else {
this.grid.Add(tomo.NewBox())
}
}
}
func (this *Calendar) handleScroll (x, y float64) {
if y < 0 {
this.prevMonth()
} else {
this.nextMonth()
}
}
func firstOfMonth (tm time.Time) time.Time {
return time.Date(tm.Year(), tm.Month(), 0, 0, 0, 0, 0, time.Local)
}
func daysInMonth (tm time.Time) (days int) {
year := tm.Year()
month := tm.Month()
switch month {
case 1: days = 31
case 2:
// betcha didn't know this about leap years
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
days = 29
} else {
days = 28
}
case 3: days = 31
case 4: days = 30
case 5: days = 31
case 6: days = 30
case 7: days = 31
case 8: days = 31
case 9: days = 30
case 10: days = 31
case 11: days = 30
case 12: days = 31
}
return
}
func canonMonth (tm time.Time) int {
return int(tm.Month() - 1) + tm.Year() * 12
}

View File

@@ -1,7 +1,6 @@
package objects
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
@@ -19,7 +18,7 @@ func NewCheckbox (value bool) *Checkbox {
box := &Checkbox {
Box: tomo.NewBox(),
}
theme.Apply(box, theme.R("objects", "Checkbox", ""))
box.SetRole(tomo.R("objects", "Checkbox", ""))
box.SetValue(false)
box.OnMouseUp(box.handleMouseUp)
@@ -32,11 +31,9 @@ func NewCheckbox (value bool) *Checkbox {
func (this *Checkbox) SetValue (value bool) {
this.value = value
if this.value {
// TODO perhaps have IconStatusOkay/Cancel in actions, and have
// a status icon for checked checkboxes.
this.SetTexture(theme.IconStatusOkay.Texture(theme.IconSizeSmall))
this.SetTextureCenter(tomo.IconCheckboxChecked.Texture(tomo.IconSizeSmall))
} else {
this.SetTexture(nil)
this.SetTextureCenter(tomo.IconCheckboxUnchecked.Texture(tomo.IconSizeSmall))
}
}

View File

@@ -1,7 +1,6 @@
package objects
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/theme"
// 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
@@ -28,7 +27,7 @@ func newContainer (layout tomo.Layout, children ...tomo.Object) *Container {
// window, tab pane, etc.
func NewOuterContainer (layout tomo.Layout, children ...tomo.Object) *Container {
this := newContainer(layout, children...)
theme.Apply(this, theme.R("objects", "Container", "outer"))
this.SetRole(tomo.R("objects", "Container", "outer"))
return this
}
@@ -36,14 +35,14 @@ func NewOuterContainer (layout tomo.Layout, children ...tomo.Object) *Container
// around it. It is meant to be used as a root container for a ScrollContainer.
func NewSunkenContainer (layout tomo.Layout, children ...tomo.Object) *Container {
this := newContainer(layout, children...)
theme.Apply(this, theme.R("objects", "Container", "sunken"))
this.SetRole(tomo.R("objects", "Container", "sunken"))
return this
}
// NewInnerContainer creates a new container that has no padding around it.
func NewInnerContainer (layout tomo.Layout, children ...tomo.Object) *Container {
this := newContainer(layout, children...)
theme.Apply(this, theme.R("objects", "Container", "inner"))
this.SetRole(tomo.R("objects", "Container", "inner"))
return this
}

View File

@@ -3,7 +3,6 @@ package objects
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/objects/layouts"
// DialogKind defines the semantic role of a dialog window.
@@ -46,15 +45,15 @@ func NewDialog (kind DialogKind, parent tomo.Window, title, message string, opti
dialog.Window = window
}
var iconId theme.Icon
var iconId tomo.Icon
switch kind {
case DialogInformation: iconId = theme.IconStatusInformation
case DialogQuestion: iconId = theme.IconStatusQuestion
case DialogWarning: iconId = theme.IconStatusWarning
case DialogError: iconId = theme.IconStatusError
case DialogInformation: iconId = tomo.IconDialogInformation
case DialogQuestion: iconId = tomo.IconDialogQuestion
case DialogWarning: iconId = tomo.IconDialogWarning
case DialogError: iconId = tomo.IconDialogError
}
dialog.SetTitle(title)
icon := NewIcon(iconId, theme.IconSizeLarge)
icon := NewIcon(iconId, tomo.IconSizeLarge)
messageText := NewLabel(message)
messageText.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
@@ -76,7 +75,7 @@ func NewDialog (kind DialogKind, parent tomo.Window, title, message string, opti
// NewDialogOk creates a new dialog window with an OK option.
func NewDialogOk (kind DialogKind, parent tomo.Window, title, message string, onOk func ()) (*Dialog, error) {
okButton := NewButton("OK")
okButton.SetIcon(theme.IconStatusOkay)
okButton.SetIcon(tomo.IconDialogOkay)
okButton.OnClick(func () {
if onOk != nil { onOk() }
})
@@ -88,10 +87,10 @@ func NewDialogOk (kind DialogKind, parent tomo.Window, title, message string, on
// NewDialogOkCancel creates a new dialog window with OK and Cancel options.
func NewDialogOkCancel (kind DialogKind, parent tomo.Window, title, message string, onOk func ()) (*Dialog, error) {
cancelButton := NewButton("Cancel")
cancelButton.SetIcon(theme.IconStatusCancel)
cancelButton.SetIcon(tomo.IconDialogCancel)
okButton := NewButton("OK")
okButton.SetIcon(theme.IconStatusOkay)
okButton.SetIcon(tomo.IconDialogOkay)
okButton.OnClick(func () {
if onOk != nil { onOk() }
})

2
go.mod
View File

@@ -2,6 +2,6 @@ module git.tebibyte.media/tomo/objects
go 1.20
require git.tebibyte.media/tomo/tomo v0.31.0
require git.tebibyte.media/tomo/tomo v0.38.0
require golang.org/x/image v0.11.0 // indirect

4
go.sum
View File

@@ -1,5 +1,5 @@
git.tebibyte.media/tomo/tomo v0.31.0 h1:LHPpj3AWycochnC8F441aaRNS6Tq6w6WnBrp/LGjyhM=
git.tebibyte.media/tomo/tomo v0.31.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/tomo v0.38.0 h1:K5TP67RxnszudeNfmGZiU5cFTRjFueXiI3NCsgw+05U=
git.tebibyte.media/tomo/tomo v0.38.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=

View File

@@ -2,7 +2,6 @@ package objects
import "fmt"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/theme"
// 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
@@ -16,7 +15,7 @@ func NewHeading (level int, text string) *Heading {
if level < 0 { level = 0 }
if level > 2 { level = 2 }
this := &Heading { TextBox: tomo.NewTextBox() }
theme.Apply(this, theme.R("objects", "Heading", fmt.Sprint(level)))
this.SetRole(tomo.R("objects", "Heading", fmt.Sprint(level)))
this.SetText(text)
return this
}

13
icon.go
View File

@@ -3,7 +3,6 @@ package objects
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/tomo/canvas"
// Icon displays a single icon.
@@ -12,27 +11,27 @@ type Icon struct {
}
// NewIcon creates a new icon from an icon ID.
func NewIcon (id theme.Icon, size theme.IconSize) *Icon {
func NewIcon (id tomo.Icon, size tomo.IconSize) *Icon {
this := &Icon {
Box: tomo.NewBox(),
}
theme.Apply(this, theme.R("objects", "Icon", size.String()))
this.SetRole(tomo.R("objects", "Icon", size.String()))
this.SetTexture(id.Texture(size))
return this
}
// NewMimeIcon creates a new icon from a MIME type.
func NewMimeIcon (mime data.Mime, size theme.IconSize) *Icon {
func NewMimeIcon (mime data.Mime, size tomo.IconSize) *Icon {
this := &Icon {
Box: tomo.NewBox(),
}
theme.Apply(this, theme.R("objects", "Icon", size.String()))
this.SetTexture(theme.MimeIcon(mime, size))
this.SetRole(tomo.R("objects", "Icon", size.String()))
this.SetTexture(tomo.MimeIcon(mime, size))
return this
}
func (this *Icon) SetTexture (texture canvas.Texture) {
this.Box.SetTexture(texture)
this.Box.SetTextureCenter(texture)
if texture == nil {
this.SetMinimumSize(image.Pt(0, 0))
} else {

View File

@@ -1,7 +1,6 @@
package objects
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/theme"
// Label is a simple text label.
type Label struct {
@@ -11,7 +10,7 @@ type Label struct {
// NewLabel creates a new text label.
func NewLabel (text string) *Label {
this := &Label { TextBox: tomo.NewTextBox() }
theme.Apply(this, theme.R("objects", "Label", ""))
this.SetRole(tomo.R("objects", "Label", ""))
this.SetText(text)
return this
}

View File

@@ -1,7 +1,6 @@
package objects
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/objects/layouts"
@@ -21,7 +20,7 @@ func NewLabelCheckbox (value bool, text string) *LabelCheckbox {
checkbox: NewCheckbox(value),
label: NewLabel(text),
}
theme.Apply(box, theme.R("objects", "LabelCheckbox", ""))
box.SetRole(tomo.R("objects", "LabelCheckbox", ""))
box.label.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
box.Add(box.checkbox)
box.Add(box.label)

40
layouts/contract.go Normal file
View File

@@ -0,0 +1,40 @@
package layouts
import "image"
import "git.tebibyte.media/tomo/tomo"
var _ tomo.Layout = ContractVertical
// Contract is a layout that arranges boxes in a simple row or column according
// to their minimum sizes.
type Contract bool
// ContractVertical is a vertical contracted layout.
const ContractVertical Contract = true
// ContractHorizontal is a horizontal contracted layout.
const ContractHorizontal Contract = false
func (contract Contract) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
return contract.fallback().MinimumSize(hints, boxes)
}
func (contract Contract) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
contract.fallback().Arrange(hints, boxes)
}
func (contract Contract) RecommendedHeight (hints tomo.LayoutHints, boxes []tomo.Box, width int) int {
return contract.fallback().RecommendedHeight(hints, boxes, width)
}
func (contract Contract) RecommendedWidth (hints tomo.LayoutHints, boxes []tomo.Box, height int) int {
return contract.fallback().RecommendedWidth(hints, boxes, height)
}
func (contract Contract) fallback () tomo.Layout {
if contract == ContractVertical {
return Column { }
} else {
return Row { }
}
}

View File

@@ -1,173 +0,0 @@
package layouts
import "image"
import "git.tebibyte.media/tomo/tomo"
// Cut is a layout that can be divided into smaller and smaller sections.
type Cut struct {
branches []*Cut
expand []bool
vertical bool
}
// NewCut creates and returns a new Cut layout.
func NewCut () *Cut {
return new(Cut)
}
// Vertical divides the layout vertically. Sections are specified using
// booleans. If a section is true, it will expand. If false, it will contract.
func (this *Cut) Vertical (expand ...bool) {
this.expand = expand
this.vertical = true
this.fill()
}
// Horizontal divides the layout horizontally. Sections are specified using
// booleans. If a section is true, it will expand. If false, it will contract.
func (this *Cut) Horizontal (expand ...bool) {
this.expand = expand
this.vertical = false
this.fill()
}
// At returns the section of this layout at the specified index.
func (this *Cut) At (index int) *Cut {
return this.branches[index]
}
func (this *Cut) real () bool {
return this != nil && this.branches != nil
}
func (this *Cut) fill () {
this.branches = make([]*Cut, len(this.expand))
for index := range this.branches {
this.branches[index] = new(Cut)
}
}
func (this *Cut) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
size, _ := this.minimumSize(hints, boxes)
return size
}
func (this *Cut) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
this.arrange(hints, boxes)
}
func (this *Cut) minimumSize (hints tomo.LayoutHints, boxes []tomo.Box) (image.Point, []tomo.Box) {
size := image.Point { }
for index, branch := range this.branches {
if len(boxes) == 0 { break }
var point image.Point
if branch.real() {
point, boxes = branch.minimumSize(hints, boxes)
} else {
point = boxes[0].MinimumSize()
boxes = boxes[1:]
}
if this.vertical {
if point.X > size.X { size.X = point.X }
if index > 0 { size.Y += hints.Gap.Y }
size.Y += point.Y
} else {
if point.Y > size.Y { size.Y = point.Y }
if index > 0 { size.X += hints.Gap.X }
size.X += point.X
}
}
return size, boxes
}
func (this *Cut) arrange (hints tomo.LayoutHints, boxes []tomo.Box) []tomo.Box {
nChildren := len(this.branches)
// collect minimum sizes and physical endpoints
var minimums = make([]image.Point, nChildren)
var leaves = make([]tomo.Box, nChildren)
var nBranches int
remaining := boxes
for index, branch := range this.branches {
if branch.real() {
minimums[index], remaining = branch.minimumSize(hints, remaining)
} else {
if len(remaining) == 0 { break }
leaves[index] = remaining[0]
minimums[index] = remaining[0].MinimumSize()
remaining = remaining[1:]
}
nBranches ++
}
// determine the amount of space to divide among expanding branches
gaps := nBranches - 1
var freeSpace float64; if this.vertical {
freeSpace = float64(hints.Bounds.Dy() - hints.Gap.Y * gaps)
} else {
freeSpace = float64(hints.Bounds.Dx() - hints.Gap.X * gaps)
}
var nExpanding float64
for index, minimum := range minimums {
if this.expand[index] {
nExpanding ++
} else if this.vertical {
freeSpace -= float64(minimum.Y)
} else {
freeSpace -= float64(minimum.X)
}
}
expandingSize := freeSpace / nExpanding
// calculate the size and position of branches
var bounds = make([]image.Rectangle, nChildren)
x := float64(hints.Bounds.Min.X)
y := float64(hints.Bounds.Min.Y)
for index, minimum := range minimums {
// get size along significant axis
var size float64; if this.expand[index] {
size = expandingSize
} else if this.vertical {
size = float64(minimum.Y)
} else {
size = float64(minimum.X)
}
// figure out bounds from size
if this.vertical {
bounds[index].Max = image.Pt (
int(hints.Bounds.Dx()),
int(size))
} else {
bounds[index].Max = image.Pt (
int(size),
int(hints.Bounds.Dy()))
}
bounds[index] = bounds[index].Add(image.Pt(int(x), int(y)))
// move along
if this.vertical {
y += float64(hints.Gap.Y) + size
} else {
x += float64(hints.Gap.X) + size
}
}
// apply the size and position
for index, bound := range bounds {
if leaves[index] != nil {
leaves[index].SetBounds(bound)
boxes = boxes[1:]
} else if this.branches[index] != nil {
newHints := hints
newHints.Bounds = bound
boxes = this.branches[index].arrange(newHints, boxes)
}
}
return boxes
}

View File

@@ -3,6 +3,8 @@ package layouts
import "image"
import "git.tebibyte.media/tomo/tomo"
var _ tomo.Layout = FlowVertical
// Flow is a grid layout where the number of rows and columns changes depending
// on the size of the container. It is designed to be used with an overflowing
// container. If the container does not overflow in the correct direction, the
@@ -36,6 +38,7 @@ func (flow Flow) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
minorSteps :=
(flow.deltaMinor(hints.Bounds) + flow.minor(hints.Gap)) /
(minorSize + flow.minor(hints.Gap))
if minorSteps < 1 { minorSteps = 1 }
// arrange
point := hints.Bounds.Min
@@ -121,3 +124,13 @@ func (flow Flow) deltaMinor (rectangle image.Rectangle) int {
func (flow Flow) fallback () tomo.Layout {
return Contract(flow)
}
func (flow Flow) RecommendedHeight (hints tomo.LayoutHints, boxes []tomo.Box, width int) int {
// TODO
return 0
}
func (flow Flow) RecommendedWidth (hints tomo.LayoutHints, boxes []tomo.Box, height int) int {
// TODO
return 0
}

View File

@@ -4,6 +4,8 @@ import "math"
import "image"
import "git.tebibyte.media/tomo/tomo"
var _ tomo.Layout = new(Grid)
// Grid is a layout that arranges boxes in a grid formation with distinct rows
// and columns. It is great for creating forms.
type Grid struct {
@@ -106,3 +108,13 @@ func expand (hints tomo.LayoutHints, sizes []int, space int, expands func (int)
func ceilDiv (x, y int) int {
return int(math.Ceil(float64(x) / float64(y)))
}
func (this *Grid) RecommendedHeight (hints tomo.LayoutHints, boxes []tomo.Box, width int) int {
// TODO
return 0
}
func (this *Grid) RecommendedWidth (hints tomo.LayoutHints, boxes []tomo.Box, height int) int {
// TODO
return 0
}

View File

@@ -1,95 +0,0 @@
package layouts
import "image"
import "git.tebibyte.media/tomo/tomo"
// Contract is a layout that arranges boxes in a simple row or column according
// to their minimum sizes.
type Contract bool
// ContractVertical is a vertical contracted layout.
const ContractVertical Contract = true
// ContractHorizontal is a horizontal contracted layout.
const ContractHorizontal Contract = false
func (contract Contract) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
if contract.v() {
dot := image.Point { }
for _, box := range boxes {
minimum := box.MinimumSize()
dot.Y += minimum.Y
if dot.X < minimum.X {
dot.X = minimum.X
}
}
dot.Y += hints.Gap.Y * (len(boxes) - 1)
return dot
} else {
dot := image.Point { }
for _, box := range boxes {
minimum := box.MinimumSize()
dot.X += minimum.X
if dot.Y < minimum.Y {
dot.Y = minimum.Y
}
}
dot.X += hints.Gap.X * (len(boxes) - 1)
return dot
}
}
func (contract Contract) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
if contract.v() {
dot := hints.Bounds.Min
for index, box := range boxes {
if index > 0 { dot.Y += hints.Gap.Y }
minimum := box.MinimumSize()
box.SetBounds(image.Rectangle {
Min: dot,
Max: dot.Add(image.Pt(hints.Bounds.Dx(), minimum.Y)),
})
dot.Y += minimum.Y
}
height := dot.Y - hints.Bounds.Min.Y
offset := 0
switch hints.AlignY {
case tomo.AlignMiddle:
offset = (hints.Bounds.Dy() - height) / 2
case tomo.AlignEnd:
offset = hints.Bounds.Dy() - height
}
for _, box := range boxes {
box.SetBounds(box.Bounds().Add(image.Pt(0, offset)))
}
} else {
dot := hints.Bounds.Min
for index, box := range boxes {
if index > 0 { dot.X += hints.Gap.X }
minimum := box.MinimumSize()
box.SetBounds(image.Rectangle {
Min: dot,
Max: dot.Add(image.Pt(minimum.X, hints.Bounds.Dy())),
})
dot.X += minimum.X
}
width := dot.X - hints.Bounds.Min.X
offset := 0
switch hints.AlignX {
case tomo.AlignMiddle:
offset = (hints.Bounds.Dx() - width) / 2
case tomo.AlignEnd:
offset = hints.Bounds.Dx() - width
}
for _, box := range boxes {
box.SetBounds(box.Bounds().Add(image.Pt(offset, 0)))
}
}
}
func (contract Contract) v () bool { return contract == ContractVertical }
func (contract Contract) h () bool { return contract == ContractHorizontal }

238
layouts/rowcol.go Normal file
View File

@@ -0,0 +1,238 @@
package layouts
import "image"
import "git.tebibyte.media/tomo/tomo"
var _ tomo.Layout = ContractVertical
// Row arranges boxes in a row. Boxes that share an index with a true value will
// expand, and others will contract.
type Row []bool
// Column arranges boxes in a column. Boxes that share an index with a true
// value will expand, and others will contract.
type Column []bool
func (column Column) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
dot := image.Point { }
for _, box := range boxes {
minimum := box.MinimumSize()
dot.Y += minimum.Y
if dot.X < minimum.X {
dot.X = minimum.X
}
}
dot.Y += hints.Gap.Y * (len(boxes) - 1)
return dot
}
func (row Row) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
dot := image.Point { }
for _, box := range boxes {
minimum := box.MinimumSize()
dot.X += minimum.X
if dot.Y < minimum.Y {
dot.Y = minimum.Y
}
}
dot.X += hints.Gap.X * (len(boxes) - 1)
return dot
}
func (column Column) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
expands := func (index int) bool {
if index >= len(column) { return false }
return column[index]
}
// determine expanding box size
expandingSize := 0.0
if !hints.OverflowY {
gaps := len(boxes) - 1
freeSpace := float64(hints.Bounds.Dy() - hints.Gap.Y * gaps)
nExpanding := 0; for index, box := range boxes {
if expands(index) {
nExpanding ++
} else {
freeSpace -= float64(box.MinimumSize().Y)
}
}
expandingSize = freeSpace / float64(nExpanding)
}
// determine width
width := 0
if hints.OverflowX {
for _, box := range boxes {
minimum := box.MinimumSize()
if width < minimum.X { width = minimum.X }
}
} else {
width = hints.Bounds.Dx()
}
// arrange
dot := hints.Bounds.Min
for index, box := range boxes {
if index > 0 { dot.Y += hints.Gap.Y }
// determine height
height := box.MinimumSize().Y
if hints.OverflowY {
if box, ok := box.(tomo.ContentBox); ok {
height = box.RecommendedHeight(width)
}
} else {
if expands(index) {
height = int(expandingSize)
}
}
// set bounds
box.SetBounds(image.Rectangle {
Min: dot,
Max: dot.Add(image.Pt(width, height)),
})
dot.Y += height
}
height := dot.Y - hints.Bounds.Min.Y
offset := 0
switch hints.AlignY {
case tomo.AlignMiddle:
offset = (hints.Bounds.Dy() - height) / 2
case tomo.AlignEnd:
offset = hints.Bounds.Dy() - height
}
for _, box := range boxes {
box.SetBounds(box.Bounds().Add(image.Pt(0, offset)))
}
}
func (row Row) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
expands := func (index int) bool {
if index >= len(row) { return false }
return row[index]
}
// determine expanding box size
expandingSize := 0.0
if !hints.OverflowY {
gaps := len(boxes) - 1
freeSpace := float64(hints.Bounds.Dx() - hints.Gap.X * gaps)
nExpanding := 0; for index, box := range boxes {
if expands(index) {
nExpanding ++
} else {
freeSpace -= float64(box.MinimumSize().X)
}
}
expandingSize = freeSpace / float64(nExpanding)
}
// determine height
height := 0
if hints.OverflowY {
for _, box := range boxes {
minimum := box.MinimumSize()
if height < minimum.Y { height = minimum.Y }
}
} else {
height = hints.Bounds.Dy()
}
// arrange
dot := hints.Bounds.Min
for index, box := range boxes {
if index > 0 { dot.X += hints.Gap.X }
// determine width
width := box.MinimumSize().X
if hints.OverflowY {
if box, ok := box.(tomo.ContentBox); ok {
width = box.RecommendedHeight(height)
}
} else {
if expands(index) {
width = int(expandingSize)
}
}
// set bounds
box.SetBounds(image.Rectangle {
Min: dot,
Max: dot.Add(image.Pt(width, height)),
})
dot.X += width
}
width := dot.X - hints.Bounds.Min.X
offset := 0
switch hints.AlignX {
case tomo.AlignMiddle:
offset = (hints.Bounds.Dx() - width) / 2
case tomo.AlignEnd:
offset = hints.Bounds.Dx() - width
}
for _, box := range boxes {
box.SetBounds(box.Bounds().Add(image.Pt(offset, 0)))
}
}
func (column Column) RecommendedHeight (hints tomo.LayoutHints, boxes []tomo.Box, width int) int {
height := 0
for _, box := range boxes {
if box, ok := box.(tomo.ContentBox); ok {
height += box.RecommendedHeight(width)
} else {
height += box.MinimumSize().Y
}
}
height += hints.Gap.Y * (len(boxes) - 1)
return height
}
func (row Row) RecommendedHeight (hints tomo.LayoutHints, boxes []tomo.Box, width int) int {
height := 0
for _, box := range boxes {
minimum := box.MinimumSize()
boxHeight := 0
if box, ok := box.(tomo.ContentBox); ok {
boxHeight = box.RecommendedHeight(minimum.X)
} else {
boxHeight = minimum.Y
}
if boxHeight > height { height = boxHeight }
}
return height
}
func (column Column) RecommendedWidth (hints tomo.LayoutHints, boxes []tomo.Box, height int) int {
width := 0
for _, box := range boxes {
minimum := box.MinimumSize()
boxWidth := 0
if box, ok := box.(tomo.ContentBox); ok {
boxWidth = box.RecommendedHeight(minimum.Y)
} else {
boxWidth = minimum.X
}
if boxWidth > width { width = boxWidth }
}
return width
}
func (row Row) RecommendedWidth (hints tomo.LayoutHints, boxes []tomo.Box, height int) int {
width := 0
for _, box := range boxes {
if box, ok := box.(tomo.ContentBox); ok {
width += box.RecommendedWidth(height)
} else {
width += box.MinimumSize().X
}
}
width += hints.Gap.X * (len(boxes) - 1)
return width
}

99
menu.go Normal file
View File

@@ -0,0 +1,99 @@
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"
// Menu is a menu window.
type Menu struct {
tomo.Window
parent tomo.Window
bounds image.Rectangle
rootContainer tomo.ContainerBox
tearLine tomo.Box
torn bool
}
// 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 instead.
func NewMenu (anchor tomo.Object, items ...tomo.Object) (*Menu, error) {
menu := &Menu { }
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
}
menu.rootContainer = tomo.NewContainerBox()
menu.rootContainer.SetLayout(layouts.ContractVertical)
if !menu.torn {
menu.tearLine = tomo.NewBox()
menu.tearLine.SetRole(tomo.R("objects", "TearLine", ""))
menu.tearLine.SetFocusable(true)
menu.tearLine.OnKeyUp(func (key input.Key, numberPad bool) {
if key != input.KeyEnter && key != input.Key(' ') { return }
menu.TearOff()
})
menu.tearLine.OnMouseUp(func (button input.Button) {
if button != input.ButtonLeft { return }
if menu.tearLine.MousePosition().In(menu.tearLine.Bounds()) {
menu.TearOff()
}
})
menu.rootContainer.Add(menu.tearLine)
}
for _, item := range items {
menu.rootContainer.Add(item)
if item, ok := item.(*MenuItem); ok {
item.OnClick(func () {
if !menu.torn {
menu.Close()
}
})
}
}
menu.rootContainer.SetRole(tomo.R("objects", "Container", "menu"))
menu.Window.SetRoot(menu.rootContainer)
return menu, nil
}
// TearOff converts this menu into a tear-off menu.
func (this *Menu) TearOff () {
if this.torn { return }
if this.parent == nil { return }
this.torn = true
window, err := this.parent.NewChild(this.bounds)
if err != nil { return }
visible := this.Window.Visible()
this.Window.SetRoot(nil)
this.Window.Close()
this.rootContainer.Remove(this.tearLine)
this.Window = window
this.Window.SetRoot(this.rootContainer)
this.Window.SetVisible(visible)
}
func menuBoundsFromAnchor (anchor tomo.Object) image.Rectangle {
bounds := anchor.GetBox().Bounds()
return image.Rect (
bounds.Min.X, bounds.Max.Y,
bounds.Max.X, bounds.Max.Y)//.Add(windowBounds.Min)
}

73
menuitem.go Normal file
View File

@@ -0,0 +1,73 @@
package objects
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"
// MenuItem is a clickable button.
type MenuItem struct {
tomo.ContainerBox
label *Label
icon *Icon
labelActive bool
on struct {
click event.FuncBroadcaster
}
}
// NewMenuItem creates a new menu item with the specified text.
func NewMenuItem (text string) *MenuItem {
box := &MenuItem {
ContainerBox: tomo.NewContainerBox(),
label: NewLabel(text),
icon: NewIcon("", tomo.IconSizeSmall),
}
box.SetRole(tomo.R("objects", "MenuItem", ""))
box.label.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
box.SetLayout(layouts.NewGrid([]bool { false, true }, []bool { true }))
box.Add(box.icon)
box.Add(box.label)
box.CaptureDND(true)
box.CaptureMouse(true)
box.CaptureScroll(true)
box.CaptureKeyboard(true)
box.OnMouseUp(box.handleMouseUp)
box.OnKeyUp(box.handleKeyUp)
box.SetFocusable(true)
return box
}
// SetText sets the text of the items's label.
func (this *MenuItem) SetText (text string) {
this.label.SetText(text)
}
// SetIcon sets an icon for this item. Setting the icon to IconUnknown will
// remove it.
func (this *MenuItem) SetIcon (id tomo.Icon) {
if this.icon != nil { this.Remove(this.icon) }
this.Insert(NewIcon(id, tomo.IconSizeSmall), this.label)
}
// OnClick specifies a function to be called when the menu item is clicked.
func (this *MenuItem) OnClick (callback func ()) event.Cookie {
return this.on.click.Connect(callback)
}
func (this *MenuItem) handleKeyUp (key input.Key, numberPad bool) {
if key != input.KeyEnter && key != input.Key(' ') { return }
this.on.click.Broadcast()
}
func (this *MenuItem) handleMouseUp (button input.Button) {
if button != input.ButtonLeft { return }
if this.MousePosition().In(this.Bounds()) {
this.on.click.Broadcast()
}
}

View File

@@ -3,7 +3,6 @@ package objects
import "math"
import "strconv"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/objects/layouts"
@@ -29,13 +28,13 @@ func NewNumberInput (value float64) *NumberInput {
increment: NewButton(""),
decrement: NewButton(""),
}
theme.Apply(box, theme.R("objects", "NumberInput", ""))
box.SetRole(tomo.R("objects", "NumberInput", ""))
box.Add(box.input)
box.Add(box.decrement)
box.Add(box.increment)
box.SetLayout(layouts.NewGrid([]bool { true, false, false }, []bool { true }))
box.increment.SetIcon(theme.IconActionIncrement)
box.decrement.SetIcon(theme.IconActionDecrement)
box.increment.SetIcon(tomo.IconValueIncrement)
box.decrement.SetIcon(tomo.IconValueDecrement)
box.SetValue(value)

View File

@@ -2,7 +2,6 @@ package objects
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
@@ -46,8 +45,9 @@ func newScrollbar (orient string) *Scrollbar {
this.OnMouseUp(this.handleMouseUp)
this.OnMouseMove(this.handleMouseMove)
this.OnScroll(this.handleScroll)
theme.Apply(this.handle, theme.R("objects", "SliderHandle", orient))
theme.Apply(this, theme.R("objects", "Slider", orient))
this.handle.SetRole(tomo.R("objects", "SliderHandle", orient))
this.SetRole(tomo.R("objects", "Slider", orient))
return this
}
@@ -61,9 +61,9 @@ func NewHorizontalScrollbar () *Scrollbar {
return newScrollbar("horizontal")
}
// Link assigns this scrollbar to a ContentBox. Closing the returned cookie will
// unlink it.
func (this *Scrollbar) Link (box tomo.ContentBox) event.Cookie {
// Link assigns this scrollbar to a ContentObject. Closing the returned cookie
// will unlink it.
func (this *Scrollbar) Link (box tomo.ContentObject) event.Cookie {
this.layout.linked = box
this.linkCookie = this.newLinkCookie (
box.OnContentBoundsChange(this.handleLinkedContentBoundsChange))
@@ -231,7 +231,7 @@ func (this *Scrollbar) fallbackDragOffset () image.Point {
func (this *Scrollbar) pageSize () int {
if this.layout.linked == nil { return 0 }
viewport := this.layout.linked.InnerBounds()
viewport := this.layout.linked.GetBox().InnerBounds()
if this.layout.vertical {
return viewport.Dy()
} else {
@@ -241,7 +241,7 @@ func (this *Scrollbar) pageSize () int {
func (this *Scrollbar) stepSize () int {
// FIXME: this should not be hardcoded, need to get base font metrics
// from theme somehow. should be (emspace, lineheight)
// from tomo.somehow. should be (emspace, lineheight)
return 16
}
@@ -280,7 +280,7 @@ func (this *scrollbarCookie) Close () {
type scrollbarLayout struct {
vertical bool
value float64
linked tomo.ContentBox
linked tomo.ContentObject
}
func (scrollbarLayout) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
@@ -307,9 +307,10 @@ func (this scrollbarLayout) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
handleLength := gutterLength * this.viewportContentRatio()
if handleLength < handleMin { handleLength = handleMin }
if handleLength >= gutterLength {
// just hide the handle if it isn't needed.
// TODO: we need a way to hide and show boxes, this is janky as
// fuck
// just hide the handle if it isn't needed. we are the layout
// and we shouldn't be adding and removing boxes, so this is
// really the only good way to hide things.
// TODO perhaps have a "Hidden" rectangle in the Tomo API?
boxes[0].SetBounds(image.Rect(-16, -16, 0, 0))
return
}
@@ -334,6 +335,14 @@ func (this scrollbarLayout) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
}
func (this scrollbarLayout) RecommendedHeight (hints tomo.LayoutHints, boxes []tomo.Box, width int) int {
return this.MinimumSize(hints, boxes).X
}
func (this scrollbarLayout) RecommendedWidth (hints tomo.LayoutHints, boxes []tomo.Box, height int) int {
return this.MinimumSize(hints, boxes).Y
}
func (this scrollbarLayout) viewportContentRatio () float64 {
if this.linked == nil { return 0 }
return this.viewportLength() / this.contentLength()
@@ -341,9 +350,9 @@ func (this scrollbarLayout) viewportContentRatio () float64 {
func (this scrollbarLayout) viewportLength () float64 {
if this.vertical {
return float64(this.linked.InnerBounds().Dy())
return float64(this.linked.GetBox().InnerBounds().Dy())
} else {
return float64(this.linked.InnerBounds().Dx())
return float64(this.linked.GetBox().InnerBounds().Dx())
}
}

View File

@@ -3,7 +3,6 @@ package objects
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/theme"
// ScrollSide determines which Scrollbars are active in a ScrollContainer.
type ScrollSide int; const (
@@ -61,7 +60,7 @@ func NewScrollContainer (sides ScrollSide) *ScrollContainer {
}
this.CaptureScroll(true)
this.OnScroll(this.handleScroll)
theme.Apply(this, theme.R("objects", "ScrollContainer", sides.String()))
this.SetRole(tomo.R("objects", "ScrollContainer", sides.String()))
this.SetLayout(this.layout)
return this
}
@@ -69,10 +68,10 @@ func NewScrollContainer (sides ScrollSide) *ScrollContainer {
// SetRoot sets the root child of the ScrollContainer. There can only be one at
// a time, and setting it will remove and unlink the current child if there is
// one.
func (this *ScrollContainer) SetRoot (root tomo.ContentBox) {
func (this *ScrollContainer) SetRoot (root tomo.ContentObject) {
if this.layout.root != nil {
// delete root and close cookies
this.Delete(this.layout.root)
// remove root and close cookies
this.Remove(this.layout.root)
if this.horizontalCookie != nil {
this.horizontalCookie.Close()
this.horizontalCookie = nil
@@ -134,15 +133,16 @@ func (this *ScrollContainer) handleScroll (x, y float64) {
Sub(image.Pt(int(x), int(y))))
}
// TODO: remove this and replace it with something from the layouts package
type scrollContainerLayout struct {
root tomo.ContentBox
root tomo.ContentObject
horizontal *Scrollbar
vertical *Scrollbar
}
func (this *scrollContainerLayout) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
var minimum image.Point; if this.root != nil {
minimum = this.root.MinimumSize()
minimum = this.root.GetBox().MinimumSize()
}
if this.horizontal != nil {
minimum.Y += this.horizontal.MinimumSize().Y
@@ -166,7 +166,7 @@ func (this *scrollContainerLayout) Arrange (hints tomo.LayoutHints, boxes []tomo
rootBounds.Max.X -= this.vertical.MinimumSize().X
}
if this.root != nil {
this.root.SetBounds(rootBounds)
this.root.GetBox().SetBounds(rootBounds)
}
if this.horizontal != nil {
this.horizontal.SetBounds(image.Rect (
@@ -184,6 +184,14 @@ func (this *scrollContainerLayout) Arrange (hints tomo.LayoutHints, boxes []tomo
}
}
func (this *scrollContainerLayout) RecommendedHeight (hints tomo.LayoutHints, boxes []tomo.Box, width int) int {
return this.MinimumSize(hints, boxes).X
}
func (this *scrollContainerLayout) RecommendedWidth (hints tomo.LayoutHints, boxes []tomo.Box, height int) int {
return this.MinimumSize(hints, boxes).Y
}
func max (x, y int) int {
if x > y { return x }
return y

View File

@@ -1,7 +1,6 @@
package objects
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/theme"
// Separator is a line for visually separating elements.
type Separator struct {
@@ -13,6 +12,6 @@ func NewSeparator () *Separator {
this := &Separator {
Box: tomo.NewBox(),
}
theme.Apply(this, theme.R("objects", "Separator", ""))
this.SetRole(tomo.R("objects", "Separator", ""))
return this
}

View File

@@ -2,7 +2,6 @@ package objects
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
@@ -52,8 +51,9 @@ func newSlider (orient string, value float64) *Slider {
this.OnMouseUp(this.handleMouseUp)
this.OnMouseMove(this.handleMouseMove)
this.OnScroll(this.handleScroll)
theme.Apply(this.handle, theme.R("objects", "SliderHandle", orient))
theme.Apply(this, theme.R("objects", "Slider", orient))
this.handle.SetRole(tomo.R("objects", "SliderHandle", orient))
this.SetRole(tomo.R("objects", "Slider", orient))
return this
}
@@ -238,3 +238,11 @@ func (this sliderLayout) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
Add(gutter.Min))
}
}
func (this sliderLayout) RecommendedHeight (tomo.LayoutHints, []tomo.Box, int) int {
return 0
}
func (this sliderLayout) RecommendedWidth (tomo.LayoutHints, []tomo.Box, int) int {
return 0
}

View File

@@ -3,7 +3,6 @@ package objects
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/text"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
@@ -20,7 +19,7 @@ type TextInput struct {
// NewTextInput creates a new text input containing the specified text.
func NewTextInput (text string) *TextInput {
this := &TextInput { TextBox: tomo.NewTextBox() }
theme.Apply(this, theme.R("objects", "TextInput", ""))
this.SetRole(tomo.R("objects", "TextInput", ""))
this.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
this.SetText(text)
this.SetFocusable(true)
@@ -60,8 +59,8 @@ func (this *TextInput) handleKeyDown (key input.Key, numpad bool) {
sel := modifiers.Shift
changed := false
// TODO all this (except editing stuff) really should be moved into the
// backend
// TODO all dot control (movement, selection, etc) should be done in the
// backend. (editing should be done here, though)
switch {
case key == input.KeyEnter:
@@ -105,6 +104,14 @@ func (this *TextInput) handleKeyDown (key input.Key, numpad bool) {
}
}
// Type types a character at the current dot position.
func (this *TextInput) Type (char rune) {
dot := this.Dot()
this.text, dot = text.Type(this.text, dot, rune(char))
this.Select(dot)
this.SetText(string(this.text))
}
func (this *TextInput) handleScroll (x, y float64) {
this.ScrollTo(this.ContentBounds().Min.Add(image.Pt(int(x), int(y))))
}

View File

@@ -2,7 +2,6 @@ package objects
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/theme"
// TextView is an area for displaying a large amount of multi-line text.
type TextView struct {
@@ -12,7 +11,7 @@ type TextView struct {
// NewTextView creates a new text view.
func NewTextView (text string) *TextView {
this := &TextView { TextBox: tomo.NewTextBox() }
theme.Apply(this, theme.R("objects", "TextView", ""))
this.SetRole(tomo.R("objects", "TextView", ""))
this.SetFocusable(true)
this.SetSelectable(true)
this.SetText(text)