From ae1e62c1f29452e269a790bebbbfa80139cef890 Mon Sep 17 00:00:00 2001 From: "sashakoshka@tebibyte.media" Date: Sat, 22 Jun 2024 15:44:37 -0400 Subject: [PATCH] Add swatch --- labelswatch.go | 62 +++++++++++++++++++++ swatch.go | 143 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 labelswatch.go create mode 100644 swatch.go diff --git a/labelswatch.go b/labelswatch.go new file mode 100644 index 0000000..b7fdab2 --- /dev/null +++ b/labelswatch.go @@ -0,0 +1,62 @@ +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.label.SetAlign(tomo.AlignStart, tomo.AlignMiddle) + box.Add(box.swatch) + box.Add(box.label) + box.SetLayout(layouts.NewGrid([]bool { false, true }, []bool { false })) + + 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 +// changes. +func (this *LabelSwatch) OnValueChange (callback func ()) event.Cookie { + return this.swatch.OnValueChange(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() + } +} diff --git a/swatch.go b/swatch.go new file mode 100644 index 0000000..ce7681b --- /dev/null +++ b/swatch.go @@ -0,0 +1,143 @@ +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 + on struct { + valueChange 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) { + return this.value.RGBA() +} + +// OnValueChange specifies a function to be called when the swatch's color +// changes. +func (this *Swatch) OnValueChange (callback func ()) event.Cookie { + return this.on.valueChange.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.NewModal(image.Rectangle { }) + } else { + window, err = tomo.NewWindow(image.Rectangle { }) + } + if err != nil { + log.Println("objects: could not create swatch modal:", err) + return + } + window.SetTitle("Select Color") + + colorPicker := NewColorPicker(this.Value()) + colorPicker.OnValueChange(func () { + this.userSetValue(colorPicker.Value()) + }) + + colorMemory := this.value + cancelButton := NewButton("Cancel") + cancelButton.SetIcon(tomo.IconDialogCancel) + cancelButton.OnClick(func () { + this.userSetValue(colorMemory) + window.Close() + }) + okButton := NewButton("OK") + okButton.SetFocused(true) + okButton.SetIcon(tomo.IconDialogOkay) + okButton.OnClick(func () { + window.Close() + }) + + controlRow := NewInnerContainer ( + layouts.ContractHorizontal, + cancelButton, + okButton) + controlRow.SetAlign(tomo.AlignEnd, tomo.AlignMiddle) + window.SetRoot(NewOuterContainer ( + layouts.Column { true, false }, + colorPicker, + controlRow)) + window.OnClose(func () { + 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() + } +}