Compare commits
8 Commits
1efb946953
...
d0ee6c432c
Author | SHA1 | Date | |
---|---|---|---|
d0ee6c432c | |||
b9f980e7fd | |||
b7d1a0abdd | |||
a38cee8437 | |||
48bfa05452 | |||
e8a3a376ea | |||
ae1e62c1f2 | |||
0b7e5392f4 |
@ -47,8 +47,7 @@ func NewCalendar (tm time.Time) *Calendar {
|
||||
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 { }))
|
||||
true, true, true, true, true, true, true)())
|
||||
calendar.Add(NewInnerContainer (
|
||||
layouts.Row { false, true, false },
|
||||
prevButton, calendar.monthLabel, nextButton))
|
||||
|
156
colorpicker.go
Normal file
156
colorpicker.go
Normal file
@ -0,0 +1,156 @@
|
||||
package objects
|
||||
|
||||
import "image/color"
|
||||
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/tomo/canvas"
|
||||
import "git.tebibyte.media/tomo/objects/layouts"
|
||||
import "git.tebibyte.media/tomo/objects/internal"
|
||||
|
||||
// ColorPicker allows the user to pick a color by controlling its HSBA
|
||||
// parameters.
|
||||
type ColorPicker struct {
|
||||
tomo.ContainerBox
|
||||
value internal.HSVA
|
||||
|
||||
pickerMap *colorPickerMap
|
||||
hueSlider *Slider
|
||||
alphaSlider *Slider
|
||||
|
||||
on struct {
|
||||
valueChange event.FuncBroadcaster
|
||||
}
|
||||
}
|
||||
|
||||
// NewColorPicker creates a new color picker with the specified color.
|
||||
func NewColorPicker (value color.Color) *ColorPicker {
|
||||
picker := &ColorPicker {
|
||||
ContainerBox: tomo.NewContainerBox(),
|
||||
}
|
||||
picker.SetRole(tomo.R("objects", "ColorPicker", ""))
|
||||
picker.SetLayout(layouts.Row { true, false, false })
|
||||
picker.pickerMap = newColorPickerMap(picker)
|
||||
picker.Add(picker.pickerMap)
|
||||
|
||||
picker.hueSlider = NewVerticalSlider(0.0)
|
||||
picker.Add(picker.hueSlider)
|
||||
picker.hueSlider.OnSlide(func () {
|
||||
picker.value.H = picker.hueSlider.Value()
|
||||
picker.on.valueChange.Broadcast()
|
||||
picker.pickerMap.Invalidate()
|
||||
})
|
||||
|
||||
picker.alphaSlider = NewVerticalSlider(0.0)
|
||||
picker.Add(picker.alphaSlider)
|
||||
picker.alphaSlider.OnSlide(func () {
|
||||
picker.value.A = uint8(picker.alphaSlider.Value() * 255)
|
||||
picker.on.valueChange.Broadcast()
|
||||
picker.pickerMap.Invalidate()
|
||||
})
|
||||
|
||||
if value == nil { value = color.Transparent }
|
||||
picker.SetValue(value)
|
||||
return picker
|
||||
}
|
||||
|
||||
// SetValue sets the color of the picker.
|
||||
func (this *ColorPicker) SetValue (value color.Color) {
|
||||
if value == nil { value = color.Transparent }
|
||||
this.value = internal.RGBAToHSVA(value.RGBA())
|
||||
this.hueSlider.SetValue(this.value.H)
|
||||
this.alphaSlider.SetValue(float64(this.value.A) / 255)
|
||||
}
|
||||
|
||||
// Value returns the color of the picker.
|
||||
func (this *ColorPicker) Value () color.Color {
|
||||
return this.value
|
||||
}
|
||||
|
||||
// RGBA satisfies the color.Color interface
|
||||
func (this *ColorPicker) RGBA () (r, g, b, a uint32) {
|
||||
return this.value.RGBA()
|
||||
}
|
||||
|
||||
// OnValueChange specifies a function to be called when the swatch's color
|
||||
// changes.
|
||||
func (this *ColorPicker) OnValueChange (callback func ()) event.Cookie {
|
||||
return this.on.valueChange.Connect(callback)
|
||||
}
|
||||
|
||||
type colorPickerMap struct {
|
||||
tomo.CanvasBox
|
||||
dragging bool
|
||||
parent *ColorPicker
|
||||
}
|
||||
|
||||
func newColorPickerMap (parent *ColorPicker) *colorPickerMap {
|
||||
picker := &colorPickerMap {
|
||||
CanvasBox: tomo.NewCanvasBox(),
|
||||
parent: parent,
|
||||
}
|
||||
picker.SetDrawer(picker)
|
||||
picker.SetRole(tomo.R("objects", "ColorPickerMap", ""))
|
||||
picker.OnMouseUp(picker.handleMouseUp)
|
||||
picker.OnMouseDown(picker.handleMouseDown)
|
||||
picker.OnMouseMove(picker.handleMouseMove)
|
||||
return picker
|
||||
}
|
||||
|
||||
func (this *colorPickerMap) handleMouseDown (button input.Button) {
|
||||
if button != input.ButtonLeft { return }
|
||||
this.dragging = true
|
||||
this.drag()
|
||||
}
|
||||
|
||||
func (this *colorPickerMap) handleMouseUp (button input.Button) {
|
||||
if button != input.ButtonLeft || !this.dragging { return }
|
||||
this.dragging = false
|
||||
}
|
||||
|
||||
func (this *colorPickerMap) handleMouseMove () {
|
||||
if !this.dragging { return }
|
||||
this.drag()
|
||||
}
|
||||
|
||||
func (this *colorPickerMap) drag () {
|
||||
pointer := this.MousePosition()
|
||||
bounds := this.InnerBounds()
|
||||
this.parent.value.S = float64(pointer.X - bounds.Min.X) / float64(bounds.Dx())
|
||||
this.parent.value.V = 1 - float64(pointer.Y - bounds.Min.Y) / float64(bounds.Dy())
|
||||
this.parent.value = this.parent.value.Canon()
|
||||
this.parent.on.valueChange.Broadcast()
|
||||
this.Invalidate()
|
||||
}
|
||||
|
||||
func (this *colorPickerMap) Draw (can canvas.Canvas) {
|
||||
bounds := can.Bounds()
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
|
||||
xx := x - bounds.Min.X
|
||||
yy := y - bounds.Min.Y
|
||||
|
||||
pixel := internal.HSVA {
|
||||
H: this.parent.value.H,
|
||||
S: float64(xx) / float64(bounds.Dx()),
|
||||
V: 1 - float64(yy) / float64(bounds.Dy()),
|
||||
A: 255,
|
||||
}
|
||||
|
||||
sPos := int( this.parent.value.S * float64(bounds.Dx()))
|
||||
vPos := int((1 - this.parent.value.V) * float64(bounds.Dy()))
|
||||
sDist := sPos - xx
|
||||
vDist := vPos - yy
|
||||
crosshair :=
|
||||
(sDist == 0 || vDist == 0) &&
|
||||
-8 < sDist && sDist < 8 &&
|
||||
-8 < vDist && vDist < 8
|
||||
if crosshair {
|
||||
pixel.S = 1 - pixel.S
|
||||
pixel.V = 1 - pixel.V
|
||||
}
|
||||
|
||||
can.Set(x, y, pixel)
|
||||
}}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ func NewDialog (kind DialogKind, parent tomo.Window, title, message string, opti
|
||||
dialog.controlRow.SetAlign(tomo.AlignEnd, tomo.AlignEnd)
|
||||
|
||||
dialog.SetRoot(NewOuterContainer (
|
||||
layouts.NewGrid([]bool { true }, []bool { true, false }),
|
||||
layouts.Column { true, false },
|
||||
NewInnerContainer(layouts.ContractHorizontal, icon, messageText),
|
||||
dialog.controlRow))
|
||||
return dialog, nil
|
||||
|
@ -62,8 +62,6 @@ 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)
|
||||
}
|
||||
@ -86,7 +84,7 @@ func RGBAToHSVA (r, g, b, a uint32) HSVA {
|
||||
}
|
||||
|
||||
delta := maxComponent - minComponent
|
||||
if maxComponent == 0 {
|
||||
if delta == 0 {
|
||||
// hsva.S is undefined, so hue doesn't matter
|
||||
return hsva
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ func NewLabelCheckbox (value bool, text string) *LabelCheckbox {
|
||||
box.label.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
|
||||
box.Add(box.checkbox)
|
||||
box.Add(box.label)
|
||||
box.SetLayout(layouts.NewGrid([]bool { false, true }, []bool { false }))
|
||||
box.SetLayout(layouts.Row { false, true })
|
||||
|
||||
box.OnMouseUp(box.handleMouseUp)
|
||||
box.label.OnMouseUp(box.handleMouseUp)
|
||||
|
69
labelswatch.go
Normal file
69
labelswatch.go
Normal file
@ -0,0 +1,69 @@
|
||||
package objects
|
||||
|
||||
import "image/color"
|
||||
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"
|
||||
|
||||
// LabelSwatch is a swatch with a label.
|
||||
type LabelSwatch struct {
|
||||
tomo.ContainerBox
|
||||
swatch *Swatch
|
||||
label *Label
|
||||
}
|
||||
|
||||
// NewLabelSwatch creates a new labeled swatch with the specified color and
|
||||
// label text.
|
||||
func NewLabelSwatch (value color.Color, text string) *LabelSwatch {
|
||||
box := &LabelSwatch {
|
||||
ContainerBox: tomo.NewContainerBox(),
|
||||
swatch: NewSwatch(value),
|
||||
label: NewLabel(text),
|
||||
}
|
||||
box.SetRole(tomo.R("objects", "LabelSwatch", ""))
|
||||
box.swatch.label = text
|
||||
box.label.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
|
||||
box.Add(box.swatch)
|
||||
box.Add(box.label)
|
||||
box.SetLayout(layouts.Row { false, true })
|
||||
|
||||
box.OnMouseUp(box.handleMouseUp)
|
||||
box.label.OnMouseUp(box.handleMouseUp)
|
||||
return box
|
||||
}
|
||||
|
||||
// SetValue sets the color of the swatch.
|
||||
func (this *LabelSwatch) SetValue (value color.Color) {
|
||||
this.swatch.SetValue(value)
|
||||
}
|
||||
|
||||
// Value returns the color of the swatch.
|
||||
func (this *LabelSwatch) Value () color.Color {
|
||||
return this.swatch.Value()
|
||||
}
|
||||
|
||||
// RGBA satisfies the color.Color interface
|
||||
func (this *LabelSwatch) RGBA () (r, g, b, a uint32) {
|
||||
return this.swatch.RGBA()
|
||||
}
|
||||
|
||||
// OnValueChange specifies a function to be called when the swatch's color
|
||||
// is changed by the user.
|
||||
func (this *LabelSwatch) OnValueChange (callback func ()) event.Cookie {
|
||||
return this.swatch.OnValueChange(callback)
|
||||
}
|
||||
|
||||
// OnEnter specifies a function to be called when the user selects "OK" in the
|
||||
// color picker.
|
||||
func (this *LabelSwatch) OnEnter (callback func ()) event.Cookie {
|
||||
return this.swatch.OnEnter(callback)
|
||||
}
|
||||
|
||||
func (this *LabelSwatch) handleMouseUp (button input.Button) {
|
||||
if button != input.ButtonLeft { return }
|
||||
if this.MousePosition().In(this.Bounds()) {
|
||||
this.swatch.SetFocused(true)
|
||||
this.swatch.Choose()
|
||||
}
|
||||
}
|
@ -18,12 +18,18 @@ type Grid struct {
|
||||
// will contract. Boxes are laid out left to right, then top to bottom. Boxes
|
||||
// that go beyond the lengh of rows will be laid out according to columns, but
|
||||
// they will not expand vertically.
|
||||
func NewGrid (columns, rows []bool) *Grid {
|
||||
this := &Grid {
|
||||
xExpand: columns,
|
||||
yExpand: rows,
|
||||
//
|
||||
// If you aren't sure how to use this constructor, here is an example:
|
||||
//
|
||||
// X0 X1 X2 Y0 Y1 Y2
|
||||
// NewGrid(true, false, true)(false, true, true)
|
||||
func NewGrid (columns ...bool) func (rows ...bool) *Grid {
|
||||
return func (rows ...bool) *Grid {
|
||||
return &Grid {
|
||||
xExpand: columns,
|
||||
yExpand: rows,
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Grid) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
|
||||
|
@ -5,7 +5,7 @@ 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.
|
||||
// MenuItem is a selectable memu item.
|
||||
type MenuItem struct {
|
||||
tomo.ContainerBox
|
||||
|
||||
@ -27,7 +27,7 @@ func NewMenuItem (text string) *MenuItem {
|
||||
}
|
||||
box.SetRole(tomo.R("objects", "MenuItem", ""))
|
||||
box.label.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
|
||||
box.SetLayout(layouts.NewGrid([]bool { false, true }, []bool { true }))
|
||||
box.SetLayout(layouts.Row { false, true })
|
||||
|
||||
box.Add(box.icon)
|
||||
box.Add(box.label)
|
||||
|
@ -32,7 +32,7 @@ func NewNumberInput (value float64) *NumberInput {
|
||||
box.Add(box.input)
|
||||
box.Add(box.decrement)
|
||||
box.Add(box.increment)
|
||||
box.SetLayout(layouts.NewGrid([]bool { true, false, false }, []bool { true }))
|
||||
box.SetLayout(layouts.Row { true, false, false })
|
||||
box.increment.SetIcon(tomo.IconValueIncrement)
|
||||
box.decrement.SetIcon(tomo.IconValueDecrement)
|
||||
|
||||
|
13
slider.go
13
slider.go
@ -17,6 +17,7 @@ type Slider struct {
|
||||
|
||||
on struct {
|
||||
slide event.FuncBroadcaster
|
||||
enter event.FuncBroadcaster
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,6 +90,12 @@ func (this *Slider) OnSlide (callback func ()) event.Cookie {
|
||||
return this.on.slide.Connect(callback)
|
||||
}
|
||||
|
||||
// OnEnter specifies a function to be called when the user stops moving the
|
||||
// slider.
|
||||
func (this *Slider) OnEnter (callback func ()) event.Cookie {
|
||||
return this.on.enter.Connect(callback)
|
||||
}
|
||||
|
||||
func (this *Slider) handleKeyDown (key input.Key, numpad bool) {
|
||||
var increment float64; if this.layout.vertical {
|
||||
increment = -0.05
|
||||
@ -148,17 +155,21 @@ func (this *Slider) handleMouseDown (button input.Button) {
|
||||
if above {
|
||||
this.SetValue(0)
|
||||
this.on.slide.Broadcast()
|
||||
this.on.enter.Broadcast()
|
||||
} else {
|
||||
this.SetValue(1)
|
||||
this.on.slide.Broadcast()
|
||||
this.on.enter.Broadcast()
|
||||
}
|
||||
case input.ButtonRight:
|
||||
if above {
|
||||
this.SetValue(this.Value() - this.step)
|
||||
this.on.slide.Broadcast()
|
||||
this.on.enter.Broadcast()
|
||||
} else {
|
||||
this.SetValue(this.Value() + this.step)
|
||||
this.on.slide.Broadcast()
|
||||
this.on.enter.Broadcast()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,6 +177,7 @@ func (this *Slider) handleMouseDown (button input.Button) {
|
||||
func (this *Slider) handleMouseUp (button input.Button) {
|
||||
if button != input.ButtonLeft || !this.dragging { return }
|
||||
this.dragging = false
|
||||
this.on.enter.Broadcast()
|
||||
}
|
||||
|
||||
func (this *Slider) handleMouseMove () {
|
||||
@ -177,6 +189,7 @@ func (this *Slider) handleScroll (x, y float64) {
|
||||
delta := (x + y) * 0.005
|
||||
this.SetValue(this.Value() + delta)
|
||||
this.on.slide.Broadcast()
|
||||
this.on.enter.Broadcast()
|
||||
}
|
||||
|
||||
func (this *Slider) drag () {
|
||||
|
168
swatch.go
Normal file
168
swatch.go
Normal file
@ -0,0 +1,168 @@
|
||||
package objects
|
||||
|
||||
import "log"
|
||||
import "image"
|
||||
import "image/color"
|
||||
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/tomo/canvas"
|
||||
import "git.tebibyte.media/tomo/objects/layouts"
|
||||
|
||||
// Swatch displays a color, allowing the user to edit it by clicking on it.
|
||||
type Swatch struct {
|
||||
tomo.CanvasBox
|
||||
value color.Color
|
||||
editing bool
|
||||
label string
|
||||
on struct {
|
||||
valueChange event.FuncBroadcaster
|
||||
enter event.FuncBroadcaster
|
||||
}
|
||||
}
|
||||
|
||||
// NewSwatch creates a new swatch with the given color.
|
||||
func NewSwatch (value color.Color) *Swatch {
|
||||
swatch := &Swatch {
|
||||
CanvasBox: tomo.NewCanvasBox(),
|
||||
}
|
||||
swatch.SetRole(tomo.R("objects", "Swatch", ""))
|
||||
swatch.SetDrawer(swatch)
|
||||
swatch.SetValue(value)
|
||||
|
||||
swatch.OnMouseUp(swatch.handleMouseUp)
|
||||
swatch.OnKeyUp(swatch.handleKeyUp)
|
||||
swatch.SetFocusable(true)
|
||||
return swatch
|
||||
}
|
||||
|
||||
// SetValue sets the color of the swatch.
|
||||
func (this *Swatch) SetValue (value color.Color) {
|
||||
this.value = value
|
||||
if value == nil { value = color.Transparent }
|
||||
this.Invalidate()
|
||||
}
|
||||
|
||||
// Value returns the color of the swatch.
|
||||
func (this *Swatch) Value () color.Color {
|
||||
return this.value
|
||||
}
|
||||
|
||||
// RGBA satisfies the color.Color interface
|
||||
func (this *Swatch) RGBA () (r, g, b, a uint32) {
|
||||
if this.value == nil { return }
|
||||
return this.value.RGBA()
|
||||
}
|
||||
|
||||
// OnValueChange specifies a function to be called when the swatch's color
|
||||
// is changed by the user.
|
||||
func (this *Swatch) OnValueChange (callback func ()) event.Cookie {
|
||||
return this.on.valueChange.Connect(callback)
|
||||
}
|
||||
|
||||
// OnEnter specifies a function to be called when the user selects "OK" in the
|
||||
// color picker.
|
||||
func (this *Swatch) OnEnter (callback func ()) event.Cookie {
|
||||
return this.on.enter.Connect(callback)
|
||||
}
|
||||
|
||||
// Choose creates a modal that allows the user to edit the color of the swatch.
|
||||
func (this *Swatch) Choose () {
|
||||
if this.editing { return }
|
||||
|
||||
var err error
|
||||
var window tomo.Window
|
||||
if parent := this.Window(); parent != nil {
|
||||
window, err = parent.NewChild(image.Rectangle { })
|
||||
} else {
|
||||
window, err = tomo.NewWindow(image.Rectangle { })
|
||||
}
|
||||
if err != nil {
|
||||
log.Println("objects: could not create swatch modal:", err)
|
||||
return
|
||||
}
|
||||
if this.label == "" {
|
||||
window.SetTitle("Select Color")
|
||||
} else {
|
||||
window.SetTitle(this.label)
|
||||
}
|
||||
|
||||
committed := false
|
||||
|
||||
colorPicker := NewColorPicker(this.Value())
|
||||
colorPicker.OnValueChange(func () {
|
||||
this.userSetValue(colorPicker.Value())
|
||||
})
|
||||
|
||||
hexInput := NewTextInput("TODO")
|
||||
|
||||
colorMemory := this.value
|
||||
cancelButton := NewButton("Cancel")
|
||||
cancelButton.SetIcon(tomo.IconDialogCancel)
|
||||
cancelButton.OnClick(func () {
|
||||
window.Close()
|
||||
})
|
||||
okButton := NewButton("OK")
|
||||
okButton.SetFocused(true)
|
||||
okButton.SetIcon(tomo.IconDialogOkay)
|
||||
okButton.OnClick(func () {
|
||||
committed = true
|
||||
window.Close()
|
||||
})
|
||||
|
||||
controlRow := NewInnerContainer (
|
||||
layouts.ContractHorizontal,
|
||||
cancelButton,
|
||||
okButton)
|
||||
controlRow.SetAlign(tomo.AlignEnd, tomo.AlignMiddle)
|
||||
window.SetRoot(NewOuterContainer (
|
||||
layouts.Column { true, false },
|
||||
colorPicker,
|
||||
NewInnerContainer(layouts.Row { false, true },
|
||||
NewLabel("Hex"),
|
||||
hexInput),
|
||||
controlRow))
|
||||
window.OnClose(func () {
|
||||
if committed {
|
||||
this.on.enter.Broadcast()
|
||||
} else {
|
||||
this.userSetValue(colorMemory)
|
||||
}
|
||||
this.editing = false
|
||||
})
|
||||
this.editing = true
|
||||
window.SetVisible(true)
|
||||
}
|
||||
|
||||
func (this *Swatch) Draw (can canvas.Canvas) {
|
||||
pen := can.Pen()
|
||||
|
||||
// transparency slash
|
||||
pen.Stroke(color.RGBA { R: 255, A: 255 })
|
||||
pen.StrokeWeight(1)
|
||||
pen.Path(this.Bounds().Min, this.Bounds().Max)
|
||||
|
||||
// color
|
||||
if this.value != nil {
|
||||
pen.StrokeWeight(0)
|
||||
pen.Fill(this.value)
|
||||
pen.Rectangle(this.Bounds())
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Swatch) userSetValue (value color.Color) {
|
||||
this.SetValue(value)
|
||||
this.on.valueChange.Broadcast()
|
||||
}
|
||||
|
||||
func (this *Swatch) handleKeyUp (key input.Key, numberPad bool) {
|
||||
if key != input.KeyEnter && key != input.Key(' ') { return }
|
||||
this.Choose()
|
||||
}
|
||||
|
||||
func (this *Swatch) handleMouseUp (button input.Button) {
|
||||
if button != input.ButtonLeft { return }
|
||||
if this.MousePosition().In(this.Bounds()) {
|
||||
this.Choose()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user