Compare commits
9 Commits
23fb28ce5c
...
v0.19.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 1efb946953 | |||
| 1e8df2392d | |||
| 83dca60257 | |||
| 5b60717b8f | |||
| d01d39569b | |||
| 55637e36db | |||
| e62afcd667 | |||
| f778ef5c95 | |||
| c06f10c193 |
167
calendar.go
Normal file
167
calendar.go
Normal 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
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ func NewCheckbox (value bool) *Checkbox {
|
|||||||
Box: tomo.NewBox(),
|
Box: tomo.NewBox(),
|
||||||
}
|
}
|
||||||
box.SetRole(tomo.R("objects", "Checkbox", ""))
|
box.SetRole(tomo.R("objects", "Checkbox", ""))
|
||||||
box.SetValue(false)
|
box.SetValue(value)
|
||||||
|
|
||||||
box.OnMouseUp(box.handleMouseUp)
|
box.OnMouseUp(box.handleMouseUp)
|
||||||
box.OnKeyUp(box.handleKeyUp)
|
box.OnKeyUp(box.handleKeyUp)
|
||||||
|
|||||||
106
internal/color.go
Normal file
106
internal/color.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
type HSVA struct {
|
||||||
|
H float64
|
||||||
|
S float64
|
||||||
|
V float64
|
||||||
|
A uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hsva HSVA) RGBA () (r, g, b, a uint32) {
|
||||||
|
// Adapted from:
|
||||||
|
// https://www.cs.rit.edu/~ncs/color/t_convert.html
|
||||||
|
|
||||||
|
component := func (x float64) uint32 {
|
||||||
|
return uint32(float64(0xFFFF) * x)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca := uint32(hsva.A) << 8
|
||||||
|
s := clamp01(hsva.S)
|
||||||
|
v := clamp01(hsva.V)
|
||||||
|
if s == 0 {
|
||||||
|
light := component(v)
|
||||||
|
return light, light, light, ca
|
||||||
|
}
|
||||||
|
|
||||||
|
h := clamp01(hsva.H) * 360
|
||||||
|
sector := int(h / 60)
|
||||||
|
offset := (h / 60) - float64(sector)
|
||||||
|
|
||||||
|
fac := float64(hsva.A) / 255
|
||||||
|
p := component(fac * v * (1 - s))
|
||||||
|
q := component(fac * v * (1 - s * offset))
|
||||||
|
t := component(fac * v * (1 - s * (1 - offset)))
|
||||||
|
va := component(v)
|
||||||
|
|
||||||
|
switch sector {
|
||||||
|
case 0: return va, t, p, ca
|
||||||
|
case 1: return q, va, p, ca
|
||||||
|
case 2: return p, va, t, ca
|
||||||
|
case 3: return p, q, va, ca
|
||||||
|
case 4: return t, p, va, ca
|
||||||
|
default: return va, p, q, ca
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canon returns the color but with the H, S, and V fields are constrained to
|
||||||
|
// the range 0.0-1.0
|
||||||
|
func (hsva HSVA) Canon () HSVA {
|
||||||
|
hsva.H = clamp01(hsva.H)
|
||||||
|
hsva.S = clamp01(hsva.S)
|
||||||
|
hsva.V = clamp01(hsva.V)
|
||||||
|
return hsva
|
||||||
|
}
|
||||||
|
|
||||||
|
func clamp01 (x float64) float64 {
|
||||||
|
if x > 1.0 { return 1.0 }
|
||||||
|
if x < 0.0 { return 0.0 }
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
func RGBAToHSVA (r, g, b, a uint32) HSVA {
|
||||||
|
// Adapted from:
|
||||||
|
// https://www.cs.rit.edu/~ncs/color/t_convert.html
|
||||||
|
|
||||||
|
// FIXME: this does not always work!
|
||||||
|
|
||||||
|
component := func (x uint32) float64 {
|
||||||
|
return clamp01(float64(x) / 0xFFFF)
|
||||||
|
}
|
||||||
|
cr := component(r)
|
||||||
|
cg := component(g)
|
||||||
|
cb := component(b)
|
||||||
|
|
||||||
|
var maxComponent float64
|
||||||
|
if cr > maxComponent { maxComponent = cr }
|
||||||
|
if cg > maxComponent { maxComponent = cg }
|
||||||
|
if cb > maxComponent { maxComponent = cb }
|
||||||
|
var minComponent = 1.0
|
||||||
|
if cr < minComponent { minComponent = cr }
|
||||||
|
if cg < minComponent { minComponent = cg }
|
||||||
|
if cb < minComponent { minComponent = cb }
|
||||||
|
|
||||||
|
hsva := HSVA {
|
||||||
|
V: maxComponent,
|
||||||
|
A: uint8(a >> 8),
|
||||||
|
}
|
||||||
|
|
||||||
|
delta := maxComponent - minComponent
|
||||||
|
if maxComponent == 0 {
|
||||||
|
// hsva.S is undefined, so hue doesn't matter
|
||||||
|
return hsva
|
||||||
|
}
|
||||||
|
hsva.S = delta / maxComponent
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cr == maxComponent: hsva.H = (cg - cb) / delta
|
||||||
|
case cg == maxComponent: hsva.H = 2 + (cb - cr) / delta
|
||||||
|
case cb == maxComponent: hsva.H = 4 + (cr - cg) / delta
|
||||||
|
}
|
||||||
|
|
||||||
|
hsva.H *= 60
|
||||||
|
if hsva.H < 0 { hsva.H += 360 }
|
||||||
|
hsva.H /= 360
|
||||||
|
|
||||||
|
return hsva
|
||||||
|
}
|
||||||
1
label.go
1
label.go
@@ -12,5 +12,6 @@ func NewLabel (text string) *Label {
|
|||||||
this := &Label { TextBox: tomo.NewTextBox() }
|
this := &Label { TextBox: tomo.NewTextBox() }
|
||||||
this.SetRole(tomo.R("objects", "Label", ""))
|
this.SetRole(tomo.R("objects", "Label", ""))
|
||||||
this.SetText(text)
|
this.SetText(text)
|
||||||
|
this.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ func (flow Flow) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
|
|||||||
minorSteps :=
|
minorSteps :=
|
||||||
(flow.deltaMinor(hints.Bounds) + flow.minor(hints.Gap)) /
|
(flow.deltaMinor(hints.Bounds) + flow.minor(hints.Gap)) /
|
||||||
(minorSize + flow.minor(hints.Gap))
|
(minorSize + flow.minor(hints.Gap))
|
||||||
|
if minorSteps < 1 { minorSteps = 1 }
|
||||||
|
|
||||||
// arrange
|
// arrange
|
||||||
point := hints.Bounds.Min
|
point := hints.Bounds.Min
|
||||||
|
|||||||
@@ -106,15 +106,14 @@ func expand (hints tomo.LayoutHints, sizes []int, space int, expands func (int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ceilDiv (x, y int) int {
|
func ceilDiv (x, y int) int {
|
||||||
|
if y == 0 { return 0 }
|
||||||
return int(math.Ceil(float64(x) / float64(y)))
|
return int(math.Ceil(float64(x) / float64(y)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *Grid) RecommendedHeight (hints tomo.LayoutHints, boxes []tomo.Box, width int) int {
|
func (this *Grid) RecommendedHeight (hints tomo.LayoutHints, boxes []tomo.Box, width int) int {
|
||||||
// TODO
|
return this.MinimumSize(hints, boxes).Y
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *Grid) RecommendedWidth (hints tomo.LayoutHints, boxes []tomo.Box, height int) int {
|
func (this *Grid) RecommendedWidth (hints tomo.LayoutHints, boxes []tomo.Box, height int) int {
|
||||||
// TODO
|
return this.MinimumSize(hints, boxes).X
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package objects
|
package objects
|
||||||
|
|
||||||
|
import "math"
|
||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/tomo/tomo"
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
import "git.tebibyte.media/tomo/tomo/input"
|
import "git.tebibyte.media/tomo/tomo/input"
|
||||||
@@ -33,6 +34,7 @@ func newSlider (orient string, value float64) *Slider {
|
|||||||
},
|
},
|
||||||
layout: sliderLayout {
|
layout: sliderLayout {
|
||||||
vertical: orient == "vertical",
|
vertical: orient == "vertical",
|
||||||
|
value: math.NaN(),
|
||||||
},
|
},
|
||||||
step: 0.05,
|
step: 0.05,
|
||||||
}
|
}
|
||||||
|
|||||||
126
tabbedcontainer.go
Normal file
126
tabbedcontainer.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/input"
|
||||||
|
import "git.tebibyte.media/tomo/objects/layouts"
|
||||||
|
|
||||||
|
type TabbedContainer struct {
|
||||||
|
tomo.ContainerBox
|
||||||
|
|
||||||
|
leftSpacer tomo.Box
|
||||||
|
rightSpacer tomo.Box
|
||||||
|
tabsRow tomo.ContainerBox
|
||||||
|
active string
|
||||||
|
tabs []*tab
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTabbedContainer () *TabbedContainer {
|
||||||
|
container := &TabbedContainer {
|
||||||
|
ContainerBox: tomo.NewContainerBox(),
|
||||||
|
}
|
||||||
|
container.SetRole(tomo.R("objects", "TabbedContainer", ""))
|
||||||
|
container.SetLayout(layouts.Column { false, true } )
|
||||||
|
|
||||||
|
container.tabsRow = tomo.NewContainerBox()
|
||||||
|
container.tabsRow.SetRole(tomo.R("objects", "TabRow", ""))
|
||||||
|
container.Add(container.tabsRow)
|
||||||
|
|
||||||
|
container.leftSpacer = tomo.NewBox()
|
||||||
|
container.leftSpacer.SetRole(tomo.R("objects", "TabSpacer", "left"))
|
||||||
|
container.rightSpacer = tomo.NewBox()
|
||||||
|
container.rightSpacer.SetRole(tomo.R("objects", "TabSpacer", "right"))
|
||||||
|
|
||||||
|
container.ClearTabs()
|
||||||
|
container.setTabRowLayout()
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *TabbedContainer) Activate (name string) {
|
||||||
|
if _, tab := this.findTab(this.active); tab != nil {
|
||||||
|
tab.setActive(false)
|
||||||
|
this.Remove(tab.root)
|
||||||
|
}
|
||||||
|
if _, tab := this.findTab(name); tab != nil {
|
||||||
|
tab.setActive(true)
|
||||||
|
this.Add(tab.root)
|
||||||
|
} else {
|
||||||
|
name = ""
|
||||||
|
}
|
||||||
|
this.active = name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *TabbedContainer) AddTab (name string, root tomo.Object) {
|
||||||
|
tab := &tab {
|
||||||
|
TextBox: tomo.NewTextBox(),
|
||||||
|
name: name,
|
||||||
|
root: root,
|
||||||
|
}
|
||||||
|
tab.SetRole(tomo.R("objects", "Tab", ""))
|
||||||
|
tab.SetText(name)
|
||||||
|
tab.OnMouseDown(func (button input.Button) {
|
||||||
|
if button != input.ButtonLeft { return }
|
||||||
|
this.Activate(name)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.tabs = append(this.tabs, tab)
|
||||||
|
this.tabsRow.Insert(tab, this.rightSpacer)
|
||||||
|
this.setTabRowLayout()
|
||||||
|
|
||||||
|
// if the row was empty before, activate this tab
|
||||||
|
if len(this.tabs) == 1 {
|
||||||
|
this.Activate(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *TabbedContainer) RemoveTab (name string) {
|
||||||
|
index, tab := this.findTab(name)
|
||||||
|
if index < 0 { return }
|
||||||
|
nextIndex := index - 1
|
||||||
|
|
||||||
|
this.tabsRow.Remove(tab)
|
||||||
|
this.tabs = append(this.tabs[:index], this.tabs[index - 1:]...)
|
||||||
|
this.setTabRowLayout()
|
||||||
|
|
||||||
|
if nextIndex < 0 { nextIndex = 0 }
|
||||||
|
if nextIndex >= len(this.tabs) { nextIndex = len(this.tabs) - 1 }
|
||||||
|
if nextIndex < 0 {
|
||||||
|
this.Activate("")
|
||||||
|
} else {
|
||||||
|
this.Activate(this.tabs[nextIndex].name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *TabbedContainer) ClearTabs () {
|
||||||
|
this.tabs = nil
|
||||||
|
this.tabsRow.Clear()
|
||||||
|
this.tabsRow.Add(this.leftSpacer)
|
||||||
|
this.tabsRow.Add(this.rightSpacer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *TabbedContainer) setTabRowLayout () {
|
||||||
|
row := make(layouts.Row, 1 + len(this.tabs) + 1)
|
||||||
|
row[len(row) - 1] = true
|
||||||
|
this.tabsRow.SetLayout(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *TabbedContainer) findTab (name string) (int, *tab) {
|
||||||
|
for index, tab := range this.tabs {
|
||||||
|
if tab.name == name { return index, tab }
|
||||||
|
}
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type tab struct {
|
||||||
|
tomo.TextBox
|
||||||
|
name string
|
||||||
|
root tomo.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *tab) setActive (active bool) {
|
||||||
|
if active {
|
||||||
|
this.SetRole(tomo.R("objects", "Tab", "active"))
|
||||||
|
} else {
|
||||||
|
this.SetRole(tomo.R("objects", "Tab", ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user