diff --git a/colorpicker.go b/colorpicker.go new file mode 100644 index 0000000..26f1273 --- /dev/null +++ b/colorpicker.go @@ -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) + }} +} +