summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 11:40:51 -0400
committerkj_sh6042026-03-15 11:40:51 -0400
commitef712cc8c545044b469fe843c5a3fc190fab4e42 (patch)
tree7b9df2b3a796591cff9d9ec18df0788647f65da2 /src
parent9ceac108893fe2ce2b2abc441aaf9294d58ee4ed (diff)
refactor: boilerplate re-write
Diffstat (limited to 'src')
-rw-r--r--src/main.go1047
1 files changed, 800 insertions, 247 deletions
diff --git a/src/main.go b/src/main.go
index bab289c..910aef8 100644
--- a/src/main.go
+++ b/src/main.go
@@ -4,10 +4,14 @@ import (
4 "encoding/json" 4 "encoding/json"
5 "fmt" 5 "fmt"
6 "log" 6 "log"
7 "math"
8 "math/rand"
7 "os" 9 "os"
8 "os/exec" 10 "os/exec"
9 "path/filepath" 11 "path/filepath"
12 "sort"
10 "strings" 13 "strings"
14 "time"
11 15
12 "github.com/gotk3/gotk3/gdk" 16 "github.com/gotk3/gotk3/gdk"
13 "github.com/gotk3/gotk3/glib" 17 "github.com/gotk3/gotk3/glib"
@@ -15,8 +19,9 @@ import (
15) 19)
16 20
17const ( 21const (
18 appTitle = "kjagave" 22 appTitle = "kjagave"
19 appVersion = "20251221-0050" 23 appVersion = "20260315-0200"
24 maxHistoryLen = 250
20) 25)
21 26
22type SavedColor struct { 27type SavedColor struct {
@@ -24,316 +29,613 @@ type SavedColor struct {
24 Name string `json:"name"` 29 Name string `json:"name"`
25} 30}
26 31
32type AppConfig struct {
33 Favorites []SavedColor `json:"favorites"`
34 LastColor string `json:"lastColor"`
35 LastScheme string `json:"lastScheme"`
36 Palette string `json:"palette"`
37}
38
39type SwatchCard struct {
40 button *gtk.Button
41 image *gtk.Image
42 label *gtk.Label
43 hex string
44}
45
27type App struct { 46type App struct {
28 window *gtk.Window 47 window *gtk.Window
48
29 colorButton *gtk.ColorButton 49 colorButton *gtk.ColorButton
50 hexEntry *gtk.Entry
51 schemeCombo *gtk.ComboBoxText
52 paletteCombo *gtk.ComboBoxText
53
54 historyBackBtn *gtk.Button
55 historyFwdBtn *gtk.Button
56 lightenBtn *gtk.Button
57 darkenBtn *gtk.Button
58 saturateBtn *gtk.Button
59 desaturateBtn *gtk.Button
60
61 swatchCards []SwatchCard
62
63 paletteStore *gtk.ListStore
64 paletteView *gtk.TreeView
65
66 favoritesStore *gtk.ListStore
67 favoritesView *gtk.TreeView
68 removeFavBtn *gtk.Button
69 clearFavBtn *gtk.Button
70 selectedIter *gtk.TreeIter
71
30 currentColor *gdk.RGBA 72 currentColor *gdk.RGBA
31 listStore *gtk.ListStore 73 currentHex string
32 treeView *gtk.TreeView 74
33 deleteBtn *gtk.Button 75 savedColors []SavedColor
34 savedColors []SavedColor 76 history []string
35 configFile string 77 historyPos int
36 selectedIter *gtk.TreeIter 78
79 configFile string
80 config AppConfig
81
82 suppressColorSet bool
83 rng *rand.Rand
84}
85
86var schemeNames = []string{
87 "Triads",
88 "Complements",
89 "Split Complements",
90 "Tetrads",
91 "Analogous",
92 "Monochromatic",
93}
94
95var paletteNames = []string{
96 "Web-safe colors",
97 "Tango",
98 "Visibone Core",
37} 99}
38 100
39func main() { 101func main() {
40 gtk.Init(nil) 102 gtk.Init(nil)
41 103
42 configDir := filepath.Join(os.Getenv("HOME"), ".config") 104 configDir := filepath.Join(os.Getenv("HOME"), ".config")
43 os.MkdirAll(configDir, 0755) 105 _ = os.MkdirAll(configDir, 0755)
44 106
45 app := &App{ 107 app := &App{
46 configFile: filepath.Join(configDir, "kjagave.json"), 108 configFile: filepath.Join(configDir, "kjagave.json"),
109 rng: rand.New(rand.NewSource(time.Now().UnixNano())),
110 historyPos: -1,
47 } 111 }
48 112
49 app.loadColors() 113 app.loadConfig()
50 app.createUI() 114 app.createUI()
51 app.populateList() 115 app.refreshFavoritesView()
116 app.restoreStartupState()
117 app.populatePaletteList()
118 app.updateSchemePreview()
119 app.updateActionStates()
52 120
53 gtk.Main() 121 gtk.Main()
54} 122}
55 123
56func (app *App) createUI() { 124func (app *App) createUI() {
57 var err error 125 var err error
58
59 // main window
60 app.window, err = gtk.WindowNew(gtk.WINDOW_TOPLEVEL) 126 app.window, err = gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
61 if err != nil { 127 if err != nil {
62 log.Fatal("Unable to create window:", err) 128 log.Fatal("Unable to create window:", err)
63 } 129 }
64 app.window.SetTitle(appTitle) 130 app.window.SetTitle(appTitle)
65 app.window.SetDefaultSize(550, 450) 131 app.window.SetDefaultSize(980, 680)
66 app.window.SetResizable(false) 132 app.window.Connect("destroy", func() {
67 app.window.Connect("destroy", gtk.MainQuit) 133 app.saveConfig()
134 gtk.MainQuit()
135 })
136
137 root, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 8)
138 root.SetMarginTop(8)
139 root.SetMarginBottom(8)
140 root.SetMarginStart(8)
141 root.SetMarginEnd(8)
142
143 toolbar, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 4)
144 app.historyBackBtn, _ = gtk.ButtonNewWithLabel("Back")
145 app.historyBackBtn.Connect("clicked", func() { app.navigateHistory(-1) })
146 toolbar.PackStart(app.historyBackBtn, false, false, 0)
68 147
69 // vertical box 148 app.historyFwdBtn, _ = gtk.ButtonNewWithLabel("Forward")
70 mainBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 10) 149 app.historyFwdBtn.Connect("clicked", func() { app.navigateHistory(1) })
71 mainBox.SetMarginTop(15) 150 toolbar.PackStart(app.historyFwdBtn, false, false, 0)
72 mainBox.SetMarginBottom(15)
73 mainBox.SetMarginStart(15)
74 mainBox.SetMarginEnd(15)
75 151
76 // color selection area 152 randomBtn, _ := gtk.ButtonNewWithLabel("Random")
77 colorBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 10) 153 randomBtn.Connect("clicked", func() { app.randomizeColor() })
78 colorBox.SetHAlign(gtk.ALIGN_CENTER) 154 toolbar.PackStart(randomBtn, false, false, 0)
79 155
80 label, _ := gtk.LabelNew("Select Color:") 156 app.lightenBtn, _ = gtk.ButtonNewWithLabel("Lighten")
81 colorBox.PackStart(label, false, false, 0) 157 app.lightenBtn.Connect("clicked", func() { app.adjustSV(0, 5) })
158 toolbar.PackStart(app.lightenBtn, false, false, 0)
82 159
83 // initialize color 160 app.darkenBtn, _ = gtk.ButtonNewWithLabel("Darken")
161 app.darkenBtn.Connect("clicked", func() { app.adjustSV(0, -5) })
162 toolbar.PackStart(app.darkenBtn, false, false, 0)
163
164 app.saturateBtn, _ = gtk.ButtonNewWithLabel("Saturate")
165 app.saturateBtn.Connect("clicked", func() { app.adjustSV(5, 0) })
166 toolbar.PackStart(app.saturateBtn, false, false, 0)
167
168 app.desaturateBtn, _ = gtk.ButtonNewWithLabel("Desaturate")
169 app.desaturateBtn.Connect("clicked", func() { app.adjustSV(-5, 0) })
170 toolbar.PackStart(app.desaturateBtn, false, false, 0)
171
172 pasteBtn, _ := gtk.ButtonNewWithLabel("Paste")
173 pasteBtn.Connect("clicked", func() { app.pasteColorFromClipboard() })
174 toolbar.PackStart(pasteBtn, false, false, 0)
175
176 aboutBtn, _ := gtk.ButtonNewWithLabel("About")
177 aboutBtn.Connect("clicked", func() { app.onAboutClicked() })
178 toolbar.PackStart(aboutBtn, false, false, 0)
179
180 root.PackStart(toolbar, false, false, 0)
181
182 schemeRow, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 8)
183 app.swatchCards = make([]SwatchCard, 0, 4)
184 for i := 0; i < 4; i++ {
185 card := app.newSwatchCard()
186 cardIdx := i
187 card.button.Connect("clicked", func() {
188 hex := app.swatchCards[cardIdx].hex
189 if hex == "" {
190 return
191 }
192 rgba := gdk.NewRGBA()
193 if !rgba.Parse(hex) {
194 return
195 }
196 app.setCurrentColor(rgba, true)
197 })
198 app.swatchCards = append(app.swatchCards, card)
199 schemeRow.PackStart(card.button, true, true, 0)
200 }
201 root.PackStart(schemeRow, false, false, 0)
202
203 controlRow, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 8)
84 app.currentColor = gdk.NewRGBA() 204 app.currentColor = gdk.NewRGBA()
85 app.currentColor.Parse("#69BAA7") 205 app.currentColor.Parse("#0066FF")
86 206
87 app.colorButton, err = gtk.ColorButtonNewWithRGBA(app.currentColor) 207 app.colorButton, err = gtk.ColorButtonNewWithRGBA(app.currentColor)
88 if err != nil { 208 if err != nil {
89 log.Fatal("Unable to create color button:", err) 209 log.Fatal("Unable to create color button:", err)
90 } 210 }
91 app.colorButton.SetUseAlpha(true) 211 app.colorButton.SetUseAlpha(false)
92 app.colorButton.SetTitle("Choose a Color") 212 app.colorButton.SetTitle("Pick a Color")
93 app.colorButton.Connect("color-set", func() { 213 app.colorButton.Connect("color-set", func() {
94 app.currentColor = app.colorButton.GetRGBA() 214 if app.suppressColorSet {
215 return
216 }
217 app.setCurrentColor(app.colorButton.GetRGBA(), true)
95 }) 218 })
219 controlRow.PackStart(app.colorButton, false, false, 0)
96 220
97 colorBox.PackStart(app.colorButton, false, false, 0) 221 app.schemeCombo, _ = gtk.ComboBoxTextNew()
98 222 for _, schemeName := range schemeNames {
99 hexEntry, _ := gtk.EntryNew() 223 app.schemeCombo.AppendText(schemeName)
100 hexEntry.SetEditable(false) 224 }
101 hexEntry.SetWidthChars(10) 225 app.schemeCombo.SetActive(0)
102 hexEntry.SetText(rgbaToHex(app.currentColor)) 226 app.schemeCombo.Connect("changed", func() {
103 colorBox.PackStart(hexEntry, false, false, 0) 227 app.updateSchemePreview()
104 228 app.saveConfig()
105 // color picker button 229 })
106 pickerBtn, _ := gtk.ButtonNewWithLabel("Pick from Screen") 230 controlRow.PackStart(app.schemeCombo, false, false, 0)
107 pickerBtn.Connect("clicked", func() { 231
108 if color, err := app.pickColorFromScreen(); err == nil { 232 app.hexEntry, _ = gtk.EntryNew()
109 app.colorButton.SetRGBA(color) 233 app.hexEntry.SetWidthChars(11)
110 app.currentColor = color 234 app.hexEntry.Connect("activate", func() { app.applyHexEntry() })
111 hexEntry.SetText(rgbaToHex(color)) 235 controlRow.PackStart(app.hexEntry, false, false, 0)
236
237 useHexBtn, _ := gtk.ButtonNewWithLabel("Use")
238 useHexBtn.Connect("clicked", func() { app.applyHexEntry() })
239 controlRow.PackStart(useHexBtn, false, false, 0)
240
241 pickBtn, _ := gtk.ButtonNewWithLabel("Pick from Screen")
242 pickBtn.Connect("clicked", func() {
243 clr, err := app.pickColorFromScreen()
244 if err == nil {
245 app.setCurrentColor(clr, true)
112 } 246 }
113 }) 247 })
114 colorBox.PackStart(pickerBtn, false, false, 0) 248 controlRow.PackStart(pickBtn, false, false, 0)
115 249
116 // bump hex entry when color changes 250 copyBtn, _ := gtk.ButtonNewWithLabel("Copy")
117 app.colorButton.Connect("color-set", func() { 251 copyBtn.Connect("clicked", func() {
118 app.currentColor = app.colorButton.GetRGBA() 252 clipboard, _ := gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD)
119 hexEntry.SetText(rgbaToHex(app.currentColor)) 253 clipboard.SetText(app.currentHex)
120 }) 254 })
255 controlRow.PackStart(copyBtn, false, false, 0)
121 256
122 mainBox.PackStart(colorBox, false, false, 0) 257 root.PackStart(controlRow, false, false, 0)
123
124 expander, _ := gtk.ExpanderNew("Saved Colors")
125 expander.SetExpanded(true)
126 expanderBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 5)
127 expanderBox.SetMarginTop(5)
128 expanderBox.SetMarginBottom(5)
129 btnBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
130 btnBox.SetHAlign(gtk.ALIGN_END)
131 app.deleteBtn, _ = gtk.ButtonNewWithLabel("Delete")
132 app.deleteBtn.SetSensitive(false)
133 app.deleteBtn.Connect("clicked", app.onDeleteClicked)
134 btnBox.PackStart(app.deleteBtn, false, false, 0)
135 saveBtn, _ := gtk.ButtonNewWithLabel("Save...")
136 saveBtn.Connect("clicked", app.onSaveClicked)
137 btnBox.PackStart(saveBtn, false, false, 0)
138 expanderBox.PackStart(btnBox, false, false, 0)
139
140 // treeview
141 scrolled, _ := gtk.ScrolledWindowNew(nil, nil)
142 scrolled.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
143 scrolled.SetSizeRequest(-1, 180)
144
145 app.listStore, _ = gtk.ListStoreNew(gdk.PixbufGetType(), glib.TYPE_STRING, glib.TYPE_STRING)
146 app.treeView, _ = gtk.TreeViewNew()
147 app.treeView.SetModel(app.listStore)
148 app.treeView.SetHeadersVisible(true)
149
150 // color column with swatch
151 colorCol, _ := gtk.TreeViewColumnNew()
152 colorCol.SetTitle("Color")
153 colorCol.SetSortColumnID(1)
154
155 pixbufRenderer, _ := gtk.CellRendererPixbufNew()
156 colorCol.PackStart(pixbufRenderer, false)
157 colorCol.AddAttribute(pixbufRenderer, "pixbuf", 0)
158
159 textRenderer, _ := gtk.CellRendererTextNew()
160 colorCol.PackStart(textRenderer, true)
161 colorCol.AddAttribute(textRenderer, "text", 1)
162
163 app.treeView.AppendColumn(colorCol)
164
165 // name column
166 nameRenderer, _ := gtk.CellRendererTextNew()
167 nameCol, _ := gtk.TreeViewColumnNewWithAttribute("Name", nameRenderer, "text", 2)
168 nameCol.SetSortColumnID(2)
169 app.treeView.AppendColumn(nameCol)
170
171 selection, _ := app.treeView.GetSelection()
172 selection.SetMode(gtk.SELECTION_SINGLE)
173 selection.Connect("changed", app.onSelectionChanged)
174
175 scrolled.Add(app.treeView)
176 expanderBox.PackStart(scrolled, true, true, 0)
177 expander.Add(expanderBox)
178 mainBox.PackStart(expander, true, true, 0)
179
180 // bottom button box
181 bottomBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
182 bottomBox.SetHAlign(gtk.ALIGN_END)
183
184 copyBtn, _ := gtk.ButtonNewWithLabel("Copy to Clipboard")
185 copyBtn.Connect("clicked", app.onCopyClicked)
186 bottomBox.PackStart(copyBtn, false, false, 0)
187 258
188 aboutBtn, _ := gtk.ButtonNewWithLabel("About") 259 lower, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 8)
189 aboutBtn.Connect("clicked", app.onAboutClicked) 260
190 bottomBox.PackStart(aboutBtn, false, false, 0) 261 paletteFrame, _ := gtk.FrameNew("Palette")
262 paletteBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 6)
263 paletteBox.SetMarginTop(8)
264 paletteBox.SetMarginBottom(8)
265 paletteBox.SetMarginStart(8)
266 paletteBox.SetMarginEnd(8)
267
268 app.paletteCombo, _ = gtk.ComboBoxTextNew()
269 for _, name := range paletteNames {
270 app.paletteCombo.AppendText(name)
271 }
272 app.paletteCombo.SetActive(0)
273 app.paletteCombo.Connect("changed", func() {
274 app.populatePaletteList()
275 app.saveConfig()
276 })
277 paletteBox.PackStart(app.paletteCombo, false, false, 0)
278
279 paletteScroll, _ := gtk.ScrolledWindowNew(nil, nil)
280 paletteScroll.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
281 paletteScroll.SetSizeRequest(430, 260)
282
283 app.paletteStore, _ = gtk.ListStoreNew(gdk.PixbufGetType(), glib.TYPE_STRING)
284 app.paletteView, _ = gtk.TreeViewNew()
285 app.paletteView.SetModel(app.paletteStore)
286 app.paletteView.SetHeadersVisible(false)
287 paletteCol, _ := gtk.TreeViewColumnNew()
288 palettePix, _ := gtk.CellRendererPixbufNew()
289 paletteCol.PackStart(palettePix, false)
290 paletteCol.AddAttribute(palettePix, "pixbuf", 0)
291 paletteText, _ := gtk.CellRendererTextNew()
292 paletteCol.PackStart(paletteText, true)
293 paletteCol.AddAttribute(paletteText, "text", 1)
294 app.paletteView.AppendColumn(paletteCol)
295 palSel, _ := app.paletteView.GetSelection()
296 palSel.SetMode(gtk.SELECTION_SINGLE)
297 palSel.Connect("changed", app.onPaletteSelectionChanged)
298 paletteScroll.Add(app.paletteView)
299 paletteBox.PackStart(paletteScroll, true, true, 0)
300 paletteFrame.Add(paletteBox)
301 lower.PackStart(paletteFrame, true, true, 0)
302
303 favFrame, _ := gtk.FrameNew("Favorites")
304 favBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 6)
305 favBox.SetMarginTop(8)
306 favBox.SetMarginBottom(8)
307 favBox.SetMarginStart(8)
308 favBox.SetMarginEnd(8)
309
310 favScroll, _ := gtk.ScrolledWindowNew(nil, nil)
311 favScroll.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
312 favScroll.SetSizeRequest(260, 260)
313
314 app.favoritesStore, _ = gtk.ListStoreNew(gdk.PixbufGetType(), glib.TYPE_STRING, glib.TYPE_STRING)
315 app.favoritesView, _ = gtk.TreeViewNew()
316 app.favoritesView.SetModel(app.favoritesStore)
317 app.favoritesView.SetHeadersVisible(false)
318
319 favCol, _ := gtk.TreeViewColumnNew()
320 favPix, _ := gtk.CellRendererPixbufNew()
321 favCol.PackStart(favPix, false)
322 favCol.AddAttribute(favPix, "pixbuf", 0)
323 favHexText, _ := gtk.CellRendererTextNew()
324 favCol.PackStart(favHexText, true)
325 favCol.AddAttribute(favHexText, "text", 1)
326 app.favoritesView.AppendColumn(favCol)
327
328 favNameText, _ := gtk.CellRendererTextNew()
329 favNameCol, _ := gtk.TreeViewColumnNewWithAttribute("", favNameText, "text", 2)
330 app.favoritesView.AppendColumn(favNameCol)
331
332 favSel, _ := app.favoritesView.GetSelection()
333 favSel.SetMode(gtk.SELECTION_SINGLE)
334 favSel.Connect("changed", app.onFavoriteSelectionChanged)
335
336 favScroll.Add(app.favoritesView)
337 favBox.PackStart(favScroll, true, true, 0)
338
339 favBtns, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 4)
340 addFavBtn, _ := gtk.ButtonNewWithLabel("+")
341 addFavBtn.Connect("clicked", func() { app.addCurrentToFavorites() })
342 favBtns.PackStart(addFavBtn, true, true, 0)
343
344 app.removeFavBtn, _ = gtk.ButtonNewWithLabel("-")
345 app.removeFavBtn.Connect("clicked", func() { app.removeSelectedFavorite() })
346 favBtns.PackStart(app.removeFavBtn, true, true, 0)
347
348 app.clearFavBtn, _ = gtk.ButtonNewWithLabel("Clear")
349 app.clearFavBtn.Connect("clicked", func() {
350 app.savedColors = nil
351 app.refreshFavoritesView()
352 app.saveConfig()
353 })
354 favBtns.PackStart(app.clearFavBtn, true, true, 0)
355
356 exportBtn, _ := gtk.ButtonNewWithLabel("Export GPL")
357 exportBtn.Connect("clicked", func() { app.exportFavoritesGPLToDefaultPath() })
358 favBtns.PackStart(exportBtn, true, true, 0)
359
360 favBox.PackStart(favBtns, false, false, 0)
361 favFrame.Add(favBox)
362 lower.PackStart(favFrame, false, false, 0)
191 363
192 mainBox.PackStart(bottomBox, false, false, 0) 364 root.PackStart(lower, true, true, 0)
193 365
194 app.window.Add(mainBox) 366 status, _ := gtk.LabelNew("Choose a color and a scheme type")
367 status.SetHAlign(gtk.ALIGN_START)
368 root.PackStart(status, false, false, 0)
369
370 app.window.Add(root)
195 app.window.ShowAll() 371 app.window.ShowAll()
196} 372}
197 373
198func (app *App) populateList() { 374func (app *App) newSwatchCard() SwatchCard {
199 app.listStore.Clear() 375 button, _ := gtk.ButtonNew()
376 button.SetSizeRequest(220, 240)
200 377
201 for _, color := range app.savedColors { 378 vbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 4)
202 pixbuf := app.createColorSwatch(color.Hex) 379 vbox.SetMarginTop(8)
203 iter := app.listStore.Append() 380 vbox.SetMarginBottom(8)
204 app.listStore.Set(iter, []int{0, 1, 2}, []interface{}{pixbuf, color.Hex, color.Name}) 381 vbox.SetMarginStart(8)
205 } 382 vbox.SetMarginEnd(8)
383
384 image, _ := gtk.ImageNew()
385 image.SetFromPixbuf(solidPixbuf("#000000", 220, 150))
386 vbox.PackStart(image, false, false, 0)
387
388 label, _ := gtk.LabelNew("")
389 label.SetJustify(gtk.JUSTIFY_CENTER)
390 label.SetHAlign(gtk.ALIGN_CENTER)
391 vbox.PackStart(label, false, false, 0)
392
393 button.Add(vbox)
394
395 return SwatchCard{button: button, image: image, label: label}
206} 396}
207 397
208func (app *App) createColorSwatch(hexColor string) *gdk.Pixbuf { 398func (app *App) setCurrentColor(rgba *gdk.RGBA, pushHistory bool) {
209 pixbuf, err := gdk.PixbufNew(gdk.COLORSPACE_RGB, false, 8, 16, 14) 399 hex := rgbaToHex(rgba)
210 if err != nil { 400 app.currentColor = rgba
211 return nil 401 app.currentHex = hex
402 app.hexEntry.SetText(hex)
403
404 app.suppressColorSet = true
405 app.colorButton.SetRGBA(rgba)
406 app.suppressColorSet = false
407
408 if pushHistory {
409 app.pushHistory(hex)
212 } 410 }
213 411
214 rgba := gdk.NewRGBA() 412 app.config.LastColor = hex
215 rgba.Parse(hexColor) 413 app.config.LastScheme = app.activeSchemeName()
216 414 app.config.Palette = app.activePaletteName()
217 r := uint32(rgba.GetRed() * 255) 415 app.updateSchemePreview()
218 g := uint32(rgba.GetGreen() * 255) 416 app.updateActionStates()
219 b := uint32(rgba.GetBlue() * 255) 417 app.saveConfig()
220 418}
221 pixels := pixbuf.GetPixels() 419
222 rowstride := pixbuf.GetRowstride() 420func (app *App) updateSchemePreview() {
223 nChannels := pixbuf.GetNChannels() 421 colors := generateScheme(app.currentColor, app.activeSchemeName())
224 422 for i := 0; i < len(app.swatchCards); i++ {
225 for y := 0; y < 14; y++ { 423 if i >= len(colors) {
226 for x := 0; x < 16; x++ { 424 app.swatchCards[i].button.Hide()
227 offset := y*rowstride + x*nChannels 425 continue
228 pixels[offset] = byte(r)
229 pixels[offset+1] = byte(g)
230 pixels[offset+2] = byte(b)
231 } 426 }
427 app.swatchCards[i].button.Show()
428 hex := rgbaToHex(colors[i])
429 h, s, v := rgbToHSV(colors[i])
430 r := int(math.Round(colors[i].GetRed() * 255))
431 g := int(math.Round(colors[i].GetGreen() * 255))
432 b := int(math.Round(colors[i].GetBlue() * 255))
433 app.swatchCards[i].hex = hex
434 app.swatchCards[i].image.SetFromPixbuf(solidPixbuf(hex, 220, 150))
435 app.swatchCards[i].label.SetText(fmt.Sprintf("%s\nrgb(%d, %d, %d)\nhsv(%d, %d, %d)", hex, r, g, b, int(h), int(s), int(v)))
232 } 436 }
233
234 return pixbuf
235} 437}
236 438
237func (app *App) onSelectionChanged(selection *gtk.TreeSelection) { 439func (app *App) applyHexEntry() {
238 model, iter, ok := selection.GetSelected() 440 text, _ := app.hexEntry.GetText()
239 if !ok { 441 text = strings.TrimSpace(text)
240 app.deleteBtn.SetSensitive(false) 442 if text == "" {
241 app.selectedIter = nil
242 return 443 return
243 } 444 }
445 if !strings.HasPrefix(text, "#") {
446 text = "#" + text
447 }
448 rgba := gdk.NewRGBA()
449 if !rgba.Parse(text) {
450 app.hexEntry.SetText(app.currentHex)
451 return
452 }
453 app.setCurrentColor(rgba, true)
454}
244 455
245 app.selectedIter = iter 456func (app *App) randomizeColor() {
246 app.deleteBtn.SetSensitive(true) 457 r := app.rng.Intn(256)
458 g := app.rng.Intn(256)
459 b := app.rng.Intn(256)
460 rgba := gdk.NewRGBA()
461 rgba.SetRed(float64(r) / 255.0)
462 rgba.SetGreen(float64(g) / 255.0)
463 rgba.SetBlue(float64(b) / 255.0)
464 rgba.SetAlpha(1)
465 app.setCurrentColor(rgba, true)
466}
247 467
248 value, _ := model.ToTreeModel().GetValue(iter, 1) 468func (app *App) adjustSV(ds, dv float64) {
249 hexColor, _ := value.GetString() 469 h, s, v := rgbToHSV(app.currentColor)
470 s = clamp(s+ds, 0, 100)
471 v = clamp(v+dv, 0, 100)
472 app.setCurrentColor(hsvToRGBA(h, s, v), true)
473}
250 474
475func (app *App) pasteColorFromClipboard() {
476 clipboard, _ := gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD)
477 text, err := clipboard.WaitForText()
478 if err != nil {
479 return
480 }
481 text = strings.TrimSpace(text)
482 if text == "" {
483 return
484 }
485 if !strings.HasPrefix(text, "#") {
486 text = "#" + text
487 }
251 rgba := gdk.NewRGBA() 488 rgba := gdk.NewRGBA()
252 rgba.Parse(hexColor) 489 if rgba.Parse(text) {
253 app.colorButton.SetRGBA(rgba) 490 app.setCurrentColor(rgba, true)
254 app.currentColor = rgba 491 }
255} 492}
256 493
257func (app *App) onSaveClicked() { 494func (app *App) pushHistory(hex string) {
258 dialog, _ := gtk.DialogNew() 495 if app.historyPos >= 0 && app.historyPos < len(app.history) && app.history[app.historyPos] == hex {
259 dialog.SetTitle("Save Color") 496 return
260 dialog.SetTransientFor(app.window) 497 }
261 dialog.SetModal(true) 498 if app.historyPos+1 < len(app.history) {
262 dialog.SetDefaultSize(300, -1) 499 app.history = app.history[:app.historyPos+1]
263 500 }
264 box, _ := dialog.GetContentArea() 501 app.history = append(app.history, hex)
265 box.SetSpacing(10) 502 if len(app.history) > maxHistoryLen {
266 box.SetMarginTop(10) 503 over := len(app.history) - maxHistoryLen
267 box.SetMarginBottom(10) 504 app.history = app.history[over:]
268 box.SetMarginStart(10) 505 }
269 box.SetMarginEnd(10) 506 app.historyPos = len(app.history) - 1
507}
270 508
271 // get current color 509func (app *App) navigateHistory(step int) {
272 hexColor := rgbaToHex(app.currentColor) 510 if len(app.history) == 0 {
511 return
512 }
513 next := app.historyPos + step
514 if next < 0 || next >= len(app.history) {
515 return
516 }
517 rgba := gdk.NewRGBA()
518 if !rgba.Parse(app.history[next]) {
519 return
520 }
521 app.historyPos = next
522 app.setCurrentColor(rgba, false)
523}
273 524
274 label, _ := gtk.LabelNew(fmt.Sprintf("Color: %s", hexColor)) 525func (app *App) updateActionStates() {
275 box.PackStart(label, false, false, 0) 526 _, s, v := rgbToHSV(app.currentColor)
527 app.historyBackBtn.SetSensitive(app.historyPos > 0)
528 app.historyFwdBtn.SetSensitive(app.historyPos >= 0 && app.historyPos < len(app.history)-1)
529 app.lightenBtn.SetSensitive(v < 100)
530 app.darkenBtn.SetSensitive(v > 5)
531 app.saturateBtn.SetSensitive(s < 100)
532 app.desaturateBtn.SetSensitive(s > 5)
533 app.removeFavBtn.SetSensitive(app.selectedIter != nil)
534 app.clearFavBtn.SetSensitive(len(app.savedColors) > 0)
535}
276 536
277 entryLabel, _ := gtk.LabelNew("Color Name:") 537func (app *App) activeSchemeName() string {
278 entryLabel.SetHAlign(gtk.ALIGN_START) 538 idx := app.schemeCombo.GetActive()
279 box.PackStart(entryLabel, false, false, 0) 539 if idx < 0 || idx >= len(schemeNames) {
540 return schemeNames[0]
541 }
542 return schemeNames[idx]
543}
280 544
281 entry, _ := gtk.EntryNew() 545func (app *App) activePaletteName() string {
282 entry.SetText("Untitled") 546 idx := app.paletteCombo.GetActive()
283 entry.SetActivatesDefault(true) 547 if idx < 0 || idx >= len(paletteNames) {
284 box.PackStart(entry, false, false, 0) 548 return paletteNames[0]
549 }
550 return paletteNames[idx]
551}
285 552
286 dialog.AddButton("Cancel", gtk.RESPONSE_CANCEL) 553func (app *App) onPaletteSelectionChanged(selection *gtk.TreeSelection) {
287 okBtn, _ := dialog.AddButton("OK", gtk.RESPONSE_OK) 554 model, iter, ok := selection.GetSelected()
288 okBtn.SetCanDefault(true) 555 if !ok {
289 okBtn.GrabDefault() 556 return
557 }
558 value, _ := model.ToTreeModel().GetValue(iter, 1)
559 hex, _ := value.GetString()
560 rgba := gdk.NewRGBA()
561 if rgba.Parse(hex) {
562 app.setCurrentColor(rgba, true)
563 }
564}
290 565
291 dialog.ShowAll() 566func (app *App) onFavoriteSelectionChanged(selection *gtk.TreeSelection) {
567 model, iter, ok := selection.GetSelected()
568 if !ok {
569 app.selectedIter = nil
570 app.updateActionStates()
571 return
572 }
573 app.selectedIter = iter
574 value, _ := model.ToTreeModel().GetValue(iter, 1)
575 hex, _ := value.GetString()
576 rgba := gdk.NewRGBA()
577 if rgba.Parse(hex) {
578 app.setCurrentColor(rgba, true)
579 }
580 app.updateActionStates()
581}
292 582
293 response := dialog.Run() 583func (app *App) populatePaletteList() {
294 if response == gtk.RESPONSE_OK { 584 app.paletteStore.Clear()
295 text, _ := entry.GetText() 585 for _, hex := range paletteByName(app.activePaletteName()) {
296 app.savedColors = append([]SavedColor{{Hex: hexColor, Name: text}}, app.savedColors...) 586 iter := app.paletteStore.Append()
297 app.saveColors() 587 app.paletteStore.Set(iter, []int{0, 1}, []interface{}{solidPixbuf(hex, 36, 14), hex})
298 app.populateList()
299 } 588 }
589}
300 590
301 dialog.Destroy() 591func (app *App) addCurrentToFavorites() {
592 for _, item := range app.savedColors {
593 if strings.EqualFold(item.Hex, app.currentHex) {
594 return
595 }
596 }
597 app.savedColors = append([]SavedColor{{Hex: app.currentHex, Name: app.currentHex}}, app.savedColors...)
598 app.refreshFavoritesView()
599 app.saveConfig()
302} 600}
303 601
304func (app *App) onDeleteClicked() { 602func (app *App) removeSelectedFavorite() {
305 if app.selectedIter == nil { 603 if app.selectedIter == nil {
306 return 604 return
307 } 605 }
308 606 value, _ := app.favoritesStore.GetValue(app.selectedIter, 1)
309 model := app.listStore.ToTreeModel() 607 hex, _ := value.GetString()
310 value, _ := model.GetValue(app.selectedIter, 1) 608 for i, c := range app.savedColors {
311 hexColor, _ := value.GetString() 609 if strings.EqualFold(c.Hex, hex) {
312
313 // remove from saved colors
314 for i, color := range app.savedColors {
315 if color.Hex == hexColor {
316 app.savedColors = append(app.savedColors[:i], app.savedColors[i+1:]...) 610 app.savedColors = append(app.savedColors[:i], app.savedColors[i+1:]...)
317 break 611 break
318 } 612 }
319 } 613 }
320
321 app.saveColors()
322 app.populateList()
323 app.deleteBtn.SetSensitive(false)
324 app.selectedIter = nil 614 app.selectedIter = nil
615 app.refreshFavoritesView()
616 app.saveConfig()
325} 617}
326 618
327func (app *App) onCopyClicked() { 619func (app *App) refreshFavoritesView() {
328 hexColor := rgbaToHex(app.currentColor) 620 app.favoritesStore.Clear()
329 621 for _, color := range app.savedColors {
330 clipboard, _ := gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD) 622 iter := app.favoritesStore.Append()
331 clipboard.SetText(hexColor) 623 app.favoritesStore.Set(iter, []int{0, 1, 2}, []interface{}{solidPixbuf(color.Hex, 16, 14), strings.ToUpper(color.Hex), color.Name})
624 }
625 app.updateActionStates()
626}
332 627
333 dialog := gtk.MessageDialogNew(app.window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, 628func (app *App) exportFavoritesGPLToDefaultPath() {
334 gtk.BUTTONS_OK, fmt.Sprintf("Color %s copied to clipboard!", hexColor)) 629 if len(app.savedColors) == 0 {
335 dialog.Run() 630 return
336 dialog.Destroy() 631 }
632 outPath := filepath.Join(filepath.Dir(app.configFile), "kjagave-favorites.gpl")
633 lines := []string{"GIMP Palette", "Name: kjagave Favorites", "#"}
634 for _, item := range app.savedColors {
635 r, g, b := hexToRGB(item.Hex)
636 lines = append(lines, fmt.Sprintf("%3d %3d %3d\t%s", r, g, b, item.Name))
637 }
638 _ = os.WriteFile(outPath, []byte(strings.Join(lines, "\n")+"\n"), 0644)
337} 639}
338 640
339func (app *App) onAboutClicked() { 641func (app *App) onAboutClicked() {
@@ -341,8 +643,8 @@ func (app *App) onAboutClicked() {
341 dialog.SetTransientFor(app.window) 643 dialog.SetTransientFor(app.window)
342 dialog.SetProgramName(appTitle) 644 dialog.SetProgramName(appTitle)
343 dialog.SetVersion(appVersion) 645 dialog.SetVersion(appVersion)
344 dialog.SetComments("a color picker inspired by agave, but only with the features kj_sh604 actually used") 646 dialog.SetComments("Agave-inspired GTK color scheme tool")
345 dialog.SetAuthors([]string{"kj_sh604"}) 647 dialog.SetAuthors([]string{"kj_sh604", "Agave inspiration: Jonathon Jongsma"})
346 dialog.SetLicense("BSD Zero Clause License (0-clause BSD)") 648 dialog.SetLicense("BSD Zero Clause License (0-clause BSD)")
347 dialog.SetLogoIconName("applications-graphics") 649 dialog.SetLogoIconName("applications-graphics")
348 dialog.Run() 650 dialog.Run()
@@ -350,63 +652,314 @@ func (app *App) onAboutClicked() {
350} 652}
351 653
352func (app *App) pickColorFromScreen() (*gdk.RGBA, error) { 654func (app *App) pickColorFromScreen() (*gdk.RGBA, error) {
353 // use xcolor for x11 color picking
354 cmd := exec.Command("xcolor", "--format", "hex") 655 cmd := exec.Command("xcolor", "--format", "hex")
355 output, err := cmd.Output() 656 output, err := cmd.Output()
356 if err != nil { 657 if err != nil {
357 // fallback to grabc
358 cmd = exec.Command("grabc") 658 cmd = exec.Command("grabc")
359 output, err = cmd.Output() 659 output, err = cmd.Output()
360 if err != nil { 660 if err != nil {
361 dialog := gtk.MessageDialogNew(app.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, 661 dialog := gtk.MessageDialogNew(app.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
362 gtk.BUTTONS_OK, "Color picker not found. Please install 'xcolor'") 662 gtk.BUTTONS_OK, "Color picker not found. Please install xcolor or grabc")
363 dialog.Run() 663 dialog.Run()
364 dialog.Destroy() 664 dialog.Destroy()
365 return nil, err 665 return nil, err
366 } 666 }
367 } 667 }
368 668
369 hexColor := strings.TrimSpace(string(output)) 669 hex := strings.TrimSpace(string(output))
370 if !strings.HasPrefix(hexColor, "#") { 670 if !strings.HasPrefix(hex, "#") {
371 hexColor = "#" + hexColor 671 hex = "#" + hex
372 } 672 }
373
374 rgba := gdk.NewRGBA() 673 rgba := gdk.NewRGBA()
375 if !rgba.Parse(hexColor) { 674 if !rgba.Parse(hex) {
376 return nil, fmt.Errorf("invalid color format: %s", hexColor) 675 return nil, fmt.Errorf("invalid color format: %s", hex)
377 } 676 }
378
379 return rgba, nil 677 return rgba, nil
380} 678}
381 679
382func (app *App) loadColors() { 680func (app *App) loadConfig() {
383 data, err := os.ReadFile(app.configFile) 681 data, err := os.ReadFile(app.configFile)
384 if err != nil { 682 if err != nil {
385 app.savedColors = []SavedColor{} 683 app.savedColors = []SavedColor{}
684 app.config = AppConfig{Favorites: []SavedColor{}, LastColor: "#0066FF", LastScheme: "Triads", Palette: "Web-safe colors"}
386 return 685 return
387 } 686 }
388 687
389 if err := json.Unmarshal(data, &app.savedColors); err != nil { 688 var cfg AppConfig
390 log.Printf("Error loading colors: %v", err) 689 if err := json.Unmarshal(data, &cfg); err == nil && (cfg.LastColor != "" || len(cfg.Favorites) > 0) {
391 app.savedColors = []SavedColor{} 690 app.config = cfg
691 app.savedColors = append([]SavedColor(nil), cfg.Favorites...)
692 if app.savedColors == nil {
693 app.savedColors = []SavedColor{}
694 }
695 if app.config.LastColor == "" {
696 app.config.LastColor = "#0066FF"
697 }
698 if app.config.LastScheme == "" {
699 app.config.LastScheme = "Triads"
700 }
701 if app.config.Palette == "" {
702 app.config.Palette = "Web-safe colors"
703 }
704 return
705 }
706
707 var legacy []SavedColor
708 if err := json.Unmarshal(data, &legacy); err == nil {
709 app.savedColors = legacy
710 app.config = AppConfig{Favorites: legacy, LastColor: "#0066FF", LastScheme: "Triads", Palette: "Web-safe colors"}
711 return
392 } 712 }
713
714 app.savedColors = []SavedColor{}
715 app.config = AppConfig{Favorites: []SavedColor{}, LastColor: "#0066FF", LastScheme: "Triads", Palette: "Web-safe colors"}
393} 716}
394 717
395func (app *App) saveColors() { 718func (app *App) saveConfig() {
396 data, err := json.MarshalIndent(app.savedColors, "", " ") 719 app.config.Favorites = app.savedColors
720 if app.currentHex != "" {
721 app.config.LastColor = app.currentHex
722 }
723 if app.schemeCombo != nil {
724 app.config.LastScheme = app.activeSchemeName()
725 }
726 if app.paletteCombo != nil {
727 app.config.Palette = app.activePaletteName()
728 }
729
730 data, err := json.MarshalIndent(app.config, "", " ")
397 if err != nil { 731 if err != nil {
398 log.Printf("Error marshaling colors: %v", err) 732 log.Printf("Error marshaling config: %v", err)
399 return 733 return
400 } 734 }
401
402 if err := os.WriteFile(app.configFile, data, 0644); err != nil { 735 if err := os.WriteFile(app.configFile, data, 0644); err != nil {
403 log.Printf("Error saving colors: %v", err) 736 log.Printf("Error saving config: %v", err)
404 } 737 }
405} 738}
406 739
740func (app *App) restoreStartupState() {
741 rgba := gdk.NewRGBA()
742 if app.config.LastColor != "" && rgba.Parse(app.config.LastColor) {
743 app.currentColor = rgba
744 }
745
746 for i, name := range schemeNames {
747 if name == app.config.LastScheme {
748 app.schemeCombo.SetActive(i)
749 break
750 }
751 }
752 for i, name := range paletteNames {
753 if name == app.config.Palette {
754 app.paletteCombo.SetActive(i)
755 break
756 }
757 }
758
759 app.setCurrentColor(app.currentColor, true)
760}
761
762func generateScheme(base *gdk.RGBA, schemeName string) []*gdk.RGBA {
763 h, s, v := rgbToHSV(base)
764 mk := func(hue float64) *gdk.RGBA {
765 return hsvToRGBA(wrapHue(hue), s, v)
766 }
767
768 switch schemeName {
769 case "Complements":
770 return []*gdk.RGBA{hsvToRGBA(h, s, v), mk(h + 180)}
771 case "Split Complements":
772 offset := 360.0 / 15.0
773 return []*gdk.RGBA{hsvToRGBA(h, s, v), mk(h + 180 - offset), mk(h + 180 + offset)}
774 case "Tetrads":
775 offset := 90.0
776 return []*gdk.RGBA{hsvToRGBA(h, s, v), mk(h + offset), mk(h + 180), mk(h + 180 + offset)}
777 case "Analogous":
778 offset := 360.0 / 12.0
779 return []*gdk.RGBA{mk(h - offset), hsvToRGBA(h, s, v), mk(h + offset)}
780 case "Monochromatic":
781 c0 := hsvToRGBA(h, s, v)
782 c1 := hsvToRGBA(h, s, v)
783 c2 := hsvToRGBA(h, s, v)
784 if s < 10 {
785 c1 = hsvToRGBA(h, math.Mod(s+33, 100), v)
786 c2 = hsvToRGBA(h, math.Mod(s+66, 100), v)
787 } else {
788 c1 = hsvToRGBA(h, s, math.Mod(v+33, 100))
789 c2 = hsvToRGBA(h, s, math.Mod(v+66, 100))
790 }
791 out := []*gdk.RGBA{c0, c1, c2}
792 sort.Slice(out, func(i, j int) bool { return luminance(out[i]) < luminance(out[j]) })
793 return out
794 case "Triads":
795 fallthrough
796 default:
797 offset := 120.0
798 return []*gdk.RGBA{hsvToRGBA(h, s, v), mk(h + offset), mk(h - offset)}
799 }
800}
801
802func paletteByName(name string) []string {
803 switch name {
804 case "Tango":
805 return []string{
806 "#2E3436", "#555753", "#888A85", "#BABDB6", "#D3D7CF", "#EEEEEC",
807 "#FCE94F", "#EDD400", "#C4A000", "#8AE234", "#73D216", "#4E9A06",
808 "#729FCF", "#3465A4", "#204A87", "#AD7FA8", "#75507B", "#5C3566",
809 "#EF2929", "#CC0000", "#A40000", "#FCAF3E", "#F57900", "#CE5C00",
810 }
811 case "Visibone Core":
812 return []string{
813 "#000000", "#333333", "#666666", "#999999", "#CCCCCC", "#FFFFFF",
814 "#FF0000", "#FF9900", "#FFFF00", "#00FF00", "#00FFFF", "#0000FF",
815 "#9900FF", "#FF00FF", "#FF0066", "#663300", "#CC6633", "#99CC33",
816 "#6699CC", "#CC33CC", "#CC9999", "#33CCCC", "#336699", "#9966CC",
817 }
818 default:
819 vals := []int{0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF}
820 colors := make([]string, 0, 216)
821 for _, r := range vals {
822 for _, g := range vals {
823 for _, b := range vals {
824 colors = append(colors, fmt.Sprintf("#%02X%02X%02X", r, g, b))
825 }
826 }
827 }
828 return colors
829 }
830}
831
832func solidPixbuf(hex string, width, height int) *gdk.Pixbuf {
833 pb, err := gdk.PixbufNew(gdk.COLORSPACE_RGB, false, 8, width, height)
834 if err != nil {
835 return nil
836 }
837 rgba := gdk.NewRGBA()
838 rgba.Parse(hex)
839 r := byte(rgba.GetRed() * 255)
840 g := byte(rgba.GetGreen() * 255)
841 b := byte(rgba.GetBlue() * 255)
842
843 pixels := pb.GetPixels()
844 rowstride := pb.GetRowstride()
845 channels := pb.GetNChannels()
846 for y := 0; y < height; y++ {
847 for x := 0; x < width; x++ {
848 off := y*rowstride + x*channels
849 pixels[off] = r
850 pixels[off+1] = g
851 pixels[off+2] = b
852 }
853 }
854 return pb
855}
856
857func rgbToHSV(rgba *gdk.RGBA) (float64, float64, float64) {
858 r := rgba.GetRed()
859 g := rgba.GetGreen()
860 b := rgba.GetBlue()
861
862 mx := math.Max(r, math.Max(g, b))
863 mn := math.Min(r, math.Min(g, b))
864 delta := mx - mn
865
866 h := 0.0
867 if delta > 0 {
868 switch mx {
869 case r:
870 h = 60 * math.Mod((g-b)/delta, 6)
871 case g:
872 h = 60 * ((b-r)/delta + 2)
873 case b:
874 h = 60 * ((r-g)/delta + 4)
875 }
876 }
877 if h < 0 {
878 h += 360
879 }
880
881 s := 0.0
882 if mx > 0 {
883 s = (delta / mx) * 100
884 }
885 v := mx * 100
886 return wrapHue(h), clamp(s, 0, 100), clamp(v, 0, 100)
887}
888
889func hsvToRGBA(h, s, v float64) *gdk.RGBA {
890 h = wrapHue(h)
891 s = clamp(s, 0, 100) / 100
892 v = clamp(v, 0, 100) / 100
893
894 c := v * s
895 x := c * (1 - math.Abs(math.Mod(h/60, 2)-1))
896 m := v - c
897
898 var r1, g1, b1 float64
899 switch {
900 case h < 60:
901 r1, g1, b1 = c, x, 0
902 case h < 120:
903 r1, g1, b1 = x, c, 0
904 case h < 180:
905 r1, g1, b1 = 0, c, x
906 case h < 240:
907 r1, g1, b1 = 0, x, c
908 case h < 300:
909 r1, g1, b1 = x, 0, c
910 default:
911 r1, g1, b1 = c, 0, x
912 }
913
914 rgba := gdk.NewRGBA()
915 rgba.SetRed(r1 + m)
916 rgba.SetGreen(g1 + m)
917 rgba.SetBlue(b1 + m)
918 rgba.SetAlpha(1)
919 return rgba
920}
921
407func rgbaToHex(rgba *gdk.RGBA) string { 922func rgbaToHex(rgba *gdk.RGBA) string {
408 r := uint8(rgba.GetRed() * 255) 923 r := int(math.Round(rgba.GetRed() * 255))
409 g := uint8(rgba.GetGreen() * 255) 924 g := int(math.Round(rgba.GetGreen() * 255))
410 b := uint8(rgba.GetBlue() * 255) 925 b := int(math.Round(rgba.GetBlue() * 255))
411 return fmt.Sprintf("#%02X%02X%02X", r, g, b) 926 return fmt.Sprintf("#%02X%02X%02X", r, g, b)
412} 927}
928
929func hexToRGB(hex string) (int, int, int) {
930 text := strings.TrimPrefix(strings.TrimSpace(hex), "#")
931 if len(text) != 6 {
932 return 0, 0, 0
933 }
934 var r, g, b int
935 _, err := fmt.Sscanf(text, "%02x%02x%02x", &r, &g, &b)
936 if err != nil {
937 return 0, 0, 0
938 }
939 return r, g, b
940}
941
942func wrapHue(h float64) float64 {
943 v := math.Mod(h, 360)
944 if v < 0 {
945 v += 360
946 }
947 return v
948}
949
950func clamp(v, lo, hi float64) float64 {
951 if v < lo {
952 return lo
953 }
954 if v > hi {
955 return hi
956 }
957 return v
958}
959
960func luminance(rgba *gdk.RGBA) float64 {
961 r := rgba.GetRed()
962 g := rgba.GetGreen()
963 b := rgba.GetBlue()
964 return 0.2126*r + 0.7152*g + 0.0722*b
965}