From 5d64788b684cbd36c1c1d167610b015786085dfe Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 17 Jan 2023 16:38:57 -0500 Subject: [PATCH] Checkboxes! --- elements/basic/checkbox.go | 211 +++++++++++++++++++++++++++++++++++++ examples/checkbox/main.go | 34 ++++++ theme/button.go | 54 ++++++++++ theme/theme.go | 50 --------- 4 files changed, 299 insertions(+), 50 deletions(-) create mode 100644 elements/basic/checkbox.go create mode 100644 examples/checkbox/main.go create mode 100644 theme/button.go diff --git a/elements/basic/checkbox.go b/elements/basic/checkbox.go new file mode 100644 index 0000000..35830fd --- /dev/null +++ b/elements/basic/checkbox.go @@ -0,0 +1,211 @@ +package basic + +import "image" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/elements/core" + +// Checkbox is a toggle-able checkbox with a label. +type Checkbox struct { + *core.Core + core core.CoreControl + + pressed bool + checked bool + enabled bool + selected bool + onClick func () + + text string + drawer artist.TextDrawer +} + +// NewCheckbox creates a new cbeckbox with the specified label text. +func NewCheckbox (text string, checked bool) (element *Checkbox) { + element = &Checkbox { enabled: true } + element.Core, element.core = core.NewCore(element) + element.drawer.SetFace(theme.FontFaceRegular()) + element.SetText(text) + return +} + +// Resize changes this element's size. +func (element *Checkbox) Resize (width, height int) { + element.core.AllocateCanvas(width, height) + element.draw() +} + +func (element *Checkbox) HandleMouseDown (x, y int, button tomo.Button) { + element.Select() + element.pressed = true + if element.core.HasImage() { + element.draw() + element.core.PushAll() + } +} + +func (element *Checkbox) HandleMouseUp (x, y int, button tomo.Button) { + if button != tomo.ButtonLeft { return } + + element.pressed = false + within := image.Point { x, y }. + In(element.Bounds()) + if within { + element.checked = !element.checked + } + + if element.core.HasImage() { + element.draw() + element.core.PushAll() + } + if within && element.onClick != nil { + element.onClick() + } +} + +func (element *Checkbox) HandleMouseMove (x, y int) { } +func (element *Checkbox) HandleScroll (x, y int, deltaX, deltaY float64) { } + +func (element *Checkbox) HandleKeyDown ( + key tomo.Key, + modifiers tomo.Modifiers, + repeated bool, +) { + if key == tomo.KeyEnter { + element.pressed = true + if element.core.HasImage() { + element.draw() + element.core.PushAll() + } + } +} + +func (element *Checkbox) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) { + if key == tomo.KeyEnter && element.pressed { + element.pressed = false + element.checked = !element.checked + if element.core.HasImage() { + element.draw() + element.core.PushAll() + } + if element.onClick != nil { + element.onClick() + } + } +} + +// Selected returns whether or not this element is selected. +func (element *Checkbox) Selected () (selected bool) { + return element.selected +} + +// Select requests that this element be selected. +func (element *Checkbox) Select () { + element.core.RequestSelection() +} + +func (element *Checkbox) HandleSelection ( + direction tomo.SelectionDirection, +) ( + accepted bool, +) { + direction = direction.Canon() + if !element.enabled { return false } + if element.selected && direction != tomo.SelectionDirectionNeutral { + return false + } + + element.selected = true + if element.core.HasImage() { + element.draw() + element.core.PushAll() + } + return true +} + +func (element *Checkbox) HandleDeselection () { + element.selected = false + if element.core.HasImage() { + element.draw() + element.core.PushAll() + } +} + +// OnClick sets the function to be called when the checkbox is toggled. +func (element *Checkbox) OnClick (callback func ()) { + element.onClick = callback +} + +// Value reports whether or not the checkbox is currently checked. +func (element *Checkbox) Value () (checked bool) { + return element.checked +} + +// SetEnabled sets whether this checkbox can be toggled or not. +func (element *Checkbox) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + if element.core.HasImage () { + element.draw() + element.core.PushAll() + } +} + +// SetText sets the checkbox's label text. +func (element *Checkbox) SetText (text string) { + if element.text == text { return } + + element.text = text + element.drawer.SetText(text) + textBounds := element.drawer.LayoutBounds() + element.core.SetMinimumSize ( + textBounds.Dy() + theme.Padding() + textBounds.Dx(), + textBounds.Dy()) + if element.core.HasImage () { + element.draw() + element.core.PushAll() + } +} + +func (element *Checkbox) draw () { + bounds := element.core.Bounds() + boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()) + + artist.FillRectangle ( element.core, theme.BackgroundPattern(), bounds) + artist.FillRectangle ( + element.core, + theme.ButtonPattern ( + element.enabled, + element.Selected(), + element.pressed), + boxBounds) + + innerBounds := bounds + innerBounds.Min.X += theme.Padding() + innerBounds.Min.Y += theme.Padding() + innerBounds.Max.X -= theme.Padding() + innerBounds.Max.Y -= theme.Padding() + + textBounds := element.drawer.LayoutBounds() + offset := image.Point { + X: bounds.Dy() + theme.Padding(), + } + + offset.Y -= textBounds.Min.Y + offset.X -= textBounds.Min.X + + foreground := theme.ForegroundPattern(element.enabled) + element.drawer.Draw(element.core, foreground, offset) + + if element.checked { + checkBounds := boxBounds.Inset(4) + if element.pressed { + checkBounds = checkBounds.Add(theme.SinkOffsetVector()) + } + artist.FillRectangle ( + element.core, + theme.ForegroundPattern(element.enabled), + checkBounds) + } +} diff --git a/examples/checkbox/main.go b/examples/checkbox/main.go new file mode 100644 index 0000000..92e92de --- /dev/null +++ b/examples/checkbox/main.go @@ -0,0 +1,34 @@ +package main + +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/layouts" +import "git.tebibyte.media/sashakoshka/tomo/elements/basic" +import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" + +func main () { + tomo.Run(run) +} + +func run () { + window, _ := tomo.NewWindow(2, 2) + window.SetTitle("Checkboxes") + + container := basic.NewContainer(layouts.Vertical { true, true }) + window.Adopt(container) + + container.Adopt(basic.NewCheckbox("Oh god", false), false) + container.Adopt(basic.NewCheckbox("Can you hear them", true), false) + container.Adopt(basic.NewCheckbox("They are in the walls", false), false) + container.Adopt(basic.NewCheckbox("They are coming for us", false), false) + disabledCheckbox := basic.NewCheckbox("We are but their helpless prey", false) + disabledCheckbox.SetEnabled(false) + container.Adopt(disabledCheckbox, false) + container.Adopt(basic.NewCheckbox("Enable vsync", true), false) + button := basic.NewButton("What") + button.OnClick(tomo.Stop) + container.Adopt(button, false) + button.Select() + + window.OnClose(tomo.Stop) + window.Show() +} diff --git a/theme/button.go b/theme/button.go new file mode 100644 index 0000000..6601e4b --- /dev/null +++ b/theme/button.go @@ -0,0 +1,54 @@ +package theme + +import "git.tebibyte.media/sashakoshka/tomo/artist" + +var buttonPattern = artist.NewMultiBorder ( + artist.Border { Weight: 1, Stroke: strokePattern }, + artist.Border { + Weight: 1, + Stroke: artist.Chiseled { + Highlight: artist.NewUniform(hex(0xCCD5D2FF)), + Shadow: artist.NewUniform(hex(0x4B5B59FF)), + }, + }, + artist.Border { Stroke: artist.NewUniform(hex(0x8D9894FF)) }) +var selectedButtonPattern = artist.NewMultiBorder ( + artist.Border { Weight: 1, Stroke: strokePattern }, + artist.Border { + Weight: 1, + Stroke: artist.Chiseled { + Highlight: artist.NewUniform(hex(0xCCD5D2FF)), + Shadow: artist.NewUniform(hex(0x4B5B59FF)), + }, + }, + artist.Border { Weight: 1, Stroke: accentPattern }, + artist.Border { Stroke: artist.NewUniform(hex(0x8D9894FF)) }) +var pressedButtonPattern = artist.NewMultiBorder ( + artist.Border { Weight: 1, Stroke: strokePattern }, + artist.Border { + Weight: 1, + Stroke: artist.Chiseled { + Highlight: artist.NewUniform(hex(0x4B5B59FF)), + Shadow: artist.NewUniform(hex(0x8D9894FF)), + }, + }, + artist.Border { Stroke: artist.NewUniform(hex(0x8D9894FF)) }) +var disabledButtonPattern = artist.NewMultiBorder ( + artist.Border { Weight: 1, Stroke: weakForegroundPattern }, + artist.Border { Stroke: backgroundPattern }) + +func ButtonPattern (enabled, selected, pressed bool) (artist.Pattern) { + if enabled { + if pressed { + return pressedButtonPattern + } else { + if selected { + return selectedButtonPattern + } else { + return buttonPattern + } + } + } else { + return disabledButtonPattern + } +} diff --git a/theme/theme.go b/theme/theme.go index 1305ac3..087cf1b 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -23,41 +23,6 @@ var foregroundPattern = artist.NewUniform(color.Gray16 { 0x0000 }) var weakForegroundPattern = artist.NewUniform(color.Gray16 { 0x4444 }) var strokePattern = artist.NewUniform(color.Gray16 { 0x0000 }) -var buttonPattern = artist.NewMultiBorder ( - artist.Border { Weight: 1, Stroke: strokePattern }, - artist.Border { - Weight: 1, - Stroke: artist.Chiseled { - Highlight: artist.NewUniform(hex(0xCCD5D2FF)), - Shadow: artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Border { Stroke: artist.NewUniform(hex(0x8D9894FF)) }) -var selectedButtonPattern = artist.NewMultiBorder ( - artist.Border { Weight: 1, Stroke: strokePattern }, - artist.Border { - Weight: 1, - Stroke: artist.Chiseled { - Highlight: artist.NewUniform(hex(0xCCD5D2FF)), - Shadow: artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Border { Weight: 1, Stroke: accentPattern }, - artist.Border { Stroke: artist.NewUniform(hex(0x8D9894FF)) }) -var pressedButtonPattern = artist.NewMultiBorder ( - artist.Border { Weight: 1, Stroke: strokePattern }, - artist.Border { - Weight: 1, - Stroke: artist.Chiseled { - Highlight: artist.NewUniform(hex(0x4B5B59FF)), - Shadow: artist.NewUniform(hex(0x8D9894FF)), - }, - }, - artist.Border { Stroke: artist.NewUniform(hex(0x8D9894FF)) }) -var disabledButtonPattern = artist.NewMultiBorder ( - artist.Border { Weight: 1, Stroke: weakForegroundPattern }, - artist.Border { Stroke: backgroundPattern }) - var sunkenPattern = artist.NewMultiBorder ( artist.Border { Weight: 1, Stroke: strokePattern }, artist.Border { @@ -79,21 +44,6 @@ func ForegroundPattern (enabled bool) (artist.Pattern) { return weakForegroundPattern } } -func ButtonPattern (enabled, selected, pressed bool) (artist.Pattern) { - if enabled { - if pressed { - return pressedButtonPattern - } else { - if selected { - return selectedButtonPattern - } else { - return buttonPattern - } - } - } else { - return disabledButtonPattern - } -} // TODO: load fonts from an actual source instead of using defaultfont