diff options
Diffstat (limited to 'src/main.go')
| -rw-r--r-- | src/main.go | 1344 |
1 files changed, 1103 insertions, 241 deletions
diff --git a/src/main.go b/src/main.go index bab289c..df5ecef 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,11 @@ import ( | |||
| 15 | ) | 19 | ) |
| 16 | 20 | ||
| 17 | const ( | 21 | const ( |
| 18 | appTitle = "kjagave" | 22 | appTitle = "kjagave" |
| 19 | appVersion = "20251221-0050" | 23 | appVersion = "20260315-0200" |
| 24 | maxHistoryLen = 250 | ||
| 25 | cardImageW = 160 | ||
| 26 | cardImageH = 132 | ||
| 20 | ) | 27 | ) |
| 21 | 28 | ||
| 22 | type SavedColor struct { | 29 | type SavedColor struct { |
| @@ -24,325 +31,859 @@ type SavedColor struct { | |||
| 24 | Name string `json:"name"` | 31 | Name string `json:"name"` |
| 25 | } | 32 | } |
| 26 | 33 | ||
| 34 | type AppConfig struct { | ||
| 35 | Favorites []SavedColor `json:"favorites"` | ||
| 36 | LastColor string `json:"lastColor"` | ||
| 37 | LastScheme string `json:"lastScheme"` | ||
| 38 | Palette string `json:"palette"` | ||
| 39 | } | ||
| 40 | |||
| 41 | type SwatchCard struct { | ||
| 42 | button *gtk.Button | ||
| 43 | image *gtk.Image | ||
| 44 | label *gtk.Label | ||
| 45 | hex string | ||
| 46 | rgb string | ||
| 47 | hsv string | ||
| 48 | } | ||
| 49 | |||
| 27 | type App struct { | 50 | type App struct { |
| 28 | window *gtk.Window | 51 | window *gtk.Window |
| 52 | css *gtk.CssProvider | ||
| 53 | |||
| 29 | colorButton *gtk.ColorButton | 54 | colorButton *gtk.ColorButton |
| 55 | hexEntry *gtk.Entry | ||
| 56 | schemeCombo *gtk.ComboBoxText | ||
| 57 | paletteCombo *gtk.ComboBoxText | ||
| 58 | |||
| 59 | historyBackBtn *gtk.Button | ||
| 60 | historyFwdBtn *gtk.Button | ||
| 61 | lightenBtn *gtk.Button | ||
| 62 | darkenBtn *gtk.Button | ||
| 63 | saturateBtn *gtk.Button | ||
| 64 | desaturateBtn *gtk.Button | ||
| 65 | |||
| 66 | swatchCards []SwatchCard | ||
| 67 | |||
| 68 | paletteGrid *gtk.Grid | ||
| 69 | paletteScroll *gtk.ScrolledWindow | ||
| 70 | |||
| 71 | favoritesStore *gtk.ListStore | ||
| 72 | favoritesView *gtk.TreeView | ||
| 73 | renameFavBtn *gtk.Button | ||
| 74 | removeFavBtn *gtk.Button | ||
| 75 | clearFavBtn *gtk.Button | ||
| 76 | selectedIter *gtk.TreeIter | ||
| 77 | |||
| 30 | currentColor *gdk.RGBA | 78 | currentColor *gdk.RGBA |
| 31 | listStore *gtk.ListStore | 79 | currentHex string |
| 32 | treeView *gtk.TreeView | 80 | |
| 33 | deleteBtn *gtk.Button | 81 | savedColors []SavedColor |
| 34 | savedColors []SavedColor | 82 | history []string |
| 35 | configFile string | 83 | historyPos int |
| 36 | selectedIter *gtk.TreeIter | 84 | |
| 85 | configFile string | ||
| 86 | config AppConfig | ||
| 87 | |||
| 88 | suppressColorSet bool | ||
| 89 | rng *rand.Rand | ||
| 90 | } | ||
| 91 | |||
| 92 | var schemeNames = []string{ | ||
| 93 | "Triads", | ||
| 94 | "Complements", | ||
| 95 | "Split Complements", | ||
| 96 | "Tetrads", | ||
| 97 | "Analogous", | ||
| 98 | "Monochromatic", | ||
| 99 | } | ||
| 100 | |||
| 101 | var paletteNames = []string{ | ||
| 102 | "Web-safe (legacy)", | ||
| 103 | "Material Design", | ||
| 104 | "Tailwind CSS", | ||
| 105 | "Flat UI", | ||
| 106 | "Pastel", | ||
| 107 | "Nord", | ||
| 108 | "Dracula", | ||
| 109 | "Solarized", | ||
| 110 | "Gruvbox", | ||
| 111 | "One Dark", | ||
| 112 | "Monokai", | ||
| 113 | "KiJiSH Dark Pastel Terminal", | ||
| 37 | } | 114 | } |
| 38 | 115 | ||
| 39 | func main() { | 116 | func main() { |
| 40 | gtk.Init(nil) | 117 | gtk.Init(nil) |
| 41 | 118 | ||
| 42 | configDir := filepath.Join(os.Getenv("HOME"), ".config") | 119 | configDir := filepath.Join(os.Getenv("HOME"), ".config") |
| 43 | os.MkdirAll(configDir, 0755) | 120 | _ = os.MkdirAll(configDir, 0755) |
| 44 | 121 | ||
| 45 | app := &App{ | 122 | app := &App{ |
| 46 | configFile: filepath.Join(configDir, "kjagave.json"), | 123 | configFile: filepath.Join(configDir, "kjagave.json"), |
| 124 | rng: rand.New(rand.NewSource(time.Now().UnixNano())), | ||
| 125 | historyPos: -1, | ||
| 47 | } | 126 | } |
| 48 | 127 | ||
| 49 | app.loadColors() | 128 | app.loadConfig() |
| 50 | app.createUI() | 129 | app.createUI() |
| 51 | app.populateList() | 130 | app.refreshFavoritesView() |
| 131 | app.restoreStartupState() | ||
| 132 | app.populatePaletteGrid() | ||
| 133 | app.updateSchemePreview() | ||
| 134 | app.updateActionStates() | ||
| 52 | 135 | ||
| 53 | gtk.Main() | 136 | gtk.Main() |
| 54 | } | 137 | } |
| 55 | 138 | ||
| 56 | func (app *App) createUI() { | 139 | func (app *App) createUI() { |
| 57 | var err error | 140 | var err error |
| 58 | |||
| 59 | // main window | ||
| 60 | app.window, err = gtk.WindowNew(gtk.WINDOW_TOPLEVEL) | 141 | app.window, err = gtk.WindowNew(gtk.WINDOW_TOPLEVEL) |
| 61 | if err != nil { | 142 | if err != nil { |
| 62 | log.Fatal("Unable to create window:", err) | 143 | log.Fatal("Unable to create window:", err) |
| 63 | } | 144 | } |
| 64 | app.window.SetTitle(appTitle) | 145 | app.window.SetTitle(appTitle) |
| 65 | app.window.SetDefaultSize(550, 450) | 146 | app.window.SetDefaultSize(640, 480) |
| 66 | app.window.SetResizable(false) | 147 | app.window.Connect("destroy", func() { |
| 67 | app.window.Connect("destroy", gtk.MainQuit) | 148 | app.saveConfig() |
| 68 | 149 | gtk.MainQuit() | |
| 69 | // vertical box | 150 | }) |
| 70 | mainBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 10) | ||
| 71 | mainBox.SetMarginTop(15) | ||
| 72 | mainBox.SetMarginBottom(15) | ||
| 73 | mainBox.SetMarginStart(15) | ||
| 74 | mainBox.SetMarginEnd(15) | ||
| 75 | 151 | ||
| 76 | // color selection area | 152 | root, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2) |
| 77 | colorBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 10) | 153 | root.SetMarginTop(2) |
| 78 | colorBox.SetHAlign(gtk.ALIGN_CENTER) | 154 | root.SetMarginBottom(2) |
| 155 | root.SetMarginStart(2) | ||
| 156 | root.SetMarginEnd(2) | ||
| 157 | |||
| 158 | menuBar := app.buildMenuBar() | ||
| 159 | root.PackStart(menuBar, false, false, 0) | ||
| 160 | app.initCompactButtonCSS() | ||
| 161 | |||
| 162 | toolbar, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 3) | ||
| 163 | app.historyBackBtn, _ = gtk.ButtonNewWithLabel("Back") | ||
| 164 | app.setButtonIcon(app.historyBackBtn, "go-previous") | ||
| 165 | app.historyBackBtn.Connect("clicked", func() { app.navigateHistory(-1) }) | ||
| 166 | toolbar.PackStart(app.historyBackBtn, false, false, 0) | ||
| 167 | |||
| 168 | app.historyFwdBtn, _ = gtk.ButtonNewWithLabel("Forward") | ||
| 169 | app.setButtonIcon(app.historyFwdBtn, "go-next") | ||
| 170 | app.historyFwdBtn.Connect("clicked", func() { app.navigateHistory(1) }) | ||
| 171 | toolbar.PackStart(app.historyFwdBtn, false, false, 0) | ||
| 172 | |||
| 173 | randomBtn, _ := gtk.ButtonNewWithLabel("Random") | ||
| 174 | app.setButtonIcon(randomBtn, "view-refresh") | ||
| 175 | randomBtn.Connect("clicked", func() { app.randomizeColor() }) | ||
| 176 | toolbar.PackStart(randomBtn, false, false, 0) | ||
| 177 | |||
| 178 | app.lightenBtn, _ = gtk.ButtonNewWithLabel("Lighten") | ||
| 179 | app.setButtonIcon(app.lightenBtn, "go-up") | ||
| 180 | app.lightenBtn.Connect("clicked", func() { app.adjustSV(0, 5) }) | ||
| 181 | toolbar.PackStart(app.lightenBtn, false, false, 0) | ||
| 182 | |||
| 183 | app.darkenBtn, _ = gtk.ButtonNewWithLabel("Darken") | ||
| 184 | app.setButtonIcon(app.darkenBtn, "go-down") | ||
| 185 | app.darkenBtn.Connect("clicked", func() { app.adjustSV(0, -5) }) | ||
| 186 | toolbar.PackStart(app.darkenBtn, false, false, 0) | ||
| 187 | |||
| 188 | app.saturateBtn, _ = gtk.ButtonNewWithLabel("Saturate") | ||
| 189 | app.setButtonIcon(app.saturateBtn, "list-add") | ||
| 190 | app.saturateBtn.Connect("clicked", func() { app.adjustSV(5, 0) }) | ||
| 191 | toolbar.PackStart(app.saturateBtn, false, false, 0) | ||
| 192 | |||
| 193 | app.desaturateBtn, _ = gtk.ButtonNewWithLabel("Desaturate") | ||
| 194 | app.setButtonIcon(app.desaturateBtn, "list-remove") | ||
| 195 | app.desaturateBtn.Connect("clicked", func() { app.adjustSV(-5, 0) }) | ||
| 196 | toolbar.PackStart(app.desaturateBtn, false, false, 0) | ||
| 197 | |||
| 198 | pasteBtn, _ := gtk.ButtonNewWithLabel("Paste") | ||
| 199 | app.setButtonIcon(pasteBtn, "edit-paste") | ||
| 200 | pasteBtn.Connect("clicked", func() { app.pasteColorFromClipboard() }) | ||
| 201 | toolbar.PackStart(pasteBtn, false, false, 0) | ||
| 79 | 202 | ||
| 80 | label, _ := gtk.LabelNew("Select Color:") | 203 | aboutBtn, _ := gtk.ButtonNewWithLabel("About") |
| 81 | colorBox.PackStart(label, false, false, 0) | 204 | app.setButtonIcon(aboutBtn, "help-about") |
| 205 | aboutBtn.Connect("clicked", func() { app.onAboutClicked() }) | ||
| 206 | toolbar.PackStart(aboutBtn, false, false, 0) | ||
| 207 | |||
| 208 | root.PackStart(toolbar, false, false, 0) | ||
| 209 | |||
| 210 | schemeRow, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 2) | ||
| 211 | app.swatchCards = make([]SwatchCard, 0, 4) | ||
| 212 | for i := 0; i < 4; i++ { | ||
| 213 | card := app.newSwatchCard() | ||
| 214 | cardIdx := i | ||
| 215 | card.button.Connect("button-press-event", func(_ *gtk.Button, ev *gdk.Event) bool { | ||
| 216 | if ev == nil { | ||
| 217 | return false | ||
| 218 | } | ||
| 219 | evBtn := gdk.EventButtonNewFromEvent(ev) | ||
| 220 | if evBtn == nil || evBtn.Button() != 3 { | ||
| 221 | return false | ||
| 222 | } | ||
| 223 | app.showSwatchContextMenu(cardIdx, ev) | ||
| 224 | return true | ||
| 225 | }) | ||
| 226 | card.button.Connect("clicked", func() { | ||
| 227 | hex := app.swatchCards[cardIdx].hex | ||
| 228 | if hex == "" { | ||
| 229 | return | ||
| 230 | } | ||
| 231 | rgba := gdk.NewRGBA() | ||
| 232 | if !rgba.Parse(hex) { | ||
| 233 | return | ||
| 234 | } | ||
| 235 | app.setCurrentColor(rgba, true) | ||
| 236 | }) | ||
| 237 | app.swatchCards = append(app.swatchCards, card) | ||
| 238 | schemeRow.PackStart(card.button, true, true, 0) | ||
| 239 | } | ||
| 240 | root.PackStart(schemeRow, false, false, 0) | ||
| 82 | 241 | ||
| 83 | // initialize color | 242 | controlRow, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6) |
| 84 | app.currentColor = gdk.NewRGBA() | 243 | app.currentColor = gdk.NewRGBA() |
| 85 | app.currentColor.Parse("#69BAA7") | 244 | app.currentColor.Parse("#0066FF") |
| 86 | 245 | ||
| 87 | app.colorButton, err = gtk.ColorButtonNewWithRGBA(app.currentColor) | 246 | app.colorButton, err = gtk.ColorButtonNewWithRGBA(app.currentColor) |
| 88 | if err != nil { | 247 | if err != nil { |
| 89 | log.Fatal("Unable to create color button:", err) | 248 | log.Fatal("Unable to create color button:", err) |
| 90 | } | 249 | } |
| 91 | app.colorButton.SetUseAlpha(true) | 250 | app.colorButton.SetUseAlpha(false) |
| 92 | app.colorButton.SetTitle("Choose a Color") | 251 | app.colorButton.SetTitle("Pick a Color") |
| 93 | app.colorButton.Connect("color-set", func() { | 252 | app.colorButton.Connect("color-set", func() { |
| 94 | app.currentColor = app.colorButton.GetRGBA() | 253 | if app.suppressColorSet { |
| 254 | return | ||
| 255 | } | ||
| 256 | app.setCurrentColor(app.colorButton.GetRGBA(), true) | ||
| 95 | }) | 257 | }) |
| 258 | controlRow.PackStart(app.colorButton, false, false, 0) | ||
| 96 | 259 | ||
| 97 | colorBox.PackStart(app.colorButton, false, false, 0) | 260 | app.schemeCombo, _ = gtk.ComboBoxTextNew() |
| 98 | 261 | for _, schemeName := range schemeNames { | |
| 99 | hexEntry, _ := gtk.EntryNew() | 262 | app.schemeCombo.AppendText(schemeName) |
| 100 | hexEntry.SetEditable(false) | 263 | } |
| 101 | hexEntry.SetWidthChars(10) | 264 | app.schemeCombo.SetActive(0) |
| 102 | hexEntry.SetText(rgbaToHex(app.currentColor)) | 265 | app.schemeCombo.Connect("changed", func() { |
| 103 | colorBox.PackStart(hexEntry, false, false, 0) | 266 | app.updateSchemePreview() |
| 104 | 267 | app.saveConfig() | |
| 105 | // color picker button | 268 | }) |
| 106 | pickerBtn, _ := gtk.ButtonNewWithLabel("Pick from Screen") | 269 | controlRow.PackStart(app.schemeCombo, false, false, 0) |
| 107 | pickerBtn.Connect("clicked", func() { | 270 | |
| 108 | if color, err := app.pickColorFromScreen(); err == nil { | 271 | app.hexEntry, _ = gtk.EntryNew() |
| 109 | app.colorButton.SetRGBA(color) | 272 | app.hexEntry.SetWidthChars(11) |
| 110 | app.currentColor = color | 273 | app.hexEntry.Connect("activate", func() { app.applyHexEntry() }) |
| 111 | hexEntry.SetText(rgbaToHex(color)) | 274 | controlRow.PackStart(app.hexEntry, false, false, 0) |
| 275 | |||
| 276 | pickBtn, _ := gtk.ButtonNewWithLabel("Pick from Screen") | ||
| 277 | app.setButtonIcon(pickBtn, "color-select") | ||
| 278 | pickBtn.Connect("clicked", func() { | ||
| 279 | clr, err := app.pickColorFromScreen() | ||
| 280 | if err == nil { | ||
| 281 | app.setCurrentColor(clr, true) | ||
| 112 | } | 282 | } |
| 113 | }) | 283 | }) |
| 114 | colorBox.PackStart(pickerBtn, false, false, 0) | 284 | controlRow.PackStart(pickBtn, false, false, 0) |
| 115 | 285 | ||
| 116 | // bump hex entry when color changes | 286 | copyBtn, _ := gtk.ButtonNewWithLabel("Copy") |
| 117 | app.colorButton.Connect("color-set", func() { | 287 | app.setButtonIcon(copyBtn, "edit-copy") |
| 118 | app.currentColor = app.colorButton.GetRGBA() | 288 | copyBtn.Connect("clicked", func() { |
| 119 | hexEntry.SetText(rgbaToHex(app.currentColor)) | 289 | clipboard, _ := gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD) |
| 290 | clipboard.SetText(app.currentHex) | ||
| 291 | }) | ||
| 292 | controlRow.PackStart(copyBtn, false, false, 0) | ||
| 293 | |||
| 294 | root.PackStart(controlRow, false, false, 0) | ||
| 295 | |||
| 296 | lower, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 4) | ||
| 297 | |||
| 298 | paletteFrame, _ := gtk.FrameNew("Palette") | ||
| 299 | paletteBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2) | ||
| 300 | paletteBox.SetMarginTop(2) | ||
| 301 | paletteBox.SetMarginBottom(2) | ||
| 302 | paletteBox.SetMarginStart(2) | ||
| 303 | paletteBox.SetMarginEnd(2) | ||
| 304 | |||
| 305 | app.paletteScroll, _ = gtk.ScrolledWindowNew(nil, nil) | ||
| 306 | app.paletteScroll.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) | ||
| 307 | app.paletteScroll.SetSizeRequest(430, 150) | ||
| 308 | app.paletteGrid, _ = gtk.GridNew() | ||
| 309 | app.paletteGrid.SetRowSpacing(0) | ||
| 310 | app.paletteGrid.SetColumnSpacing(0) | ||
| 311 | app.paletteScroll.Add(app.paletteGrid) | ||
| 312 | paletteBox.PackStart(app.paletteScroll, true, true, 0) | ||
| 313 | |||
| 314 | app.paletteCombo, _ = gtk.ComboBoxTextNew() | ||
| 315 | for _, name := range paletteNames { | ||
| 316 | app.paletteCombo.AppendText(name) | ||
| 317 | } | ||
| 318 | app.paletteCombo.SetActive(0) | ||
| 319 | app.paletteCombo.Connect("changed", func() { | ||
| 320 | app.populatePaletteGrid() | ||
| 321 | app.saveConfig() | ||
| 120 | }) | 322 | }) |
| 323 | paletteBox.PackStart(app.paletteCombo, false, false, 0) | ||
| 324 | paletteFrame.Add(paletteBox) | ||
| 325 | lower.PackStart(paletteFrame, true, true, 0) | ||
| 326 | |||
| 327 | favFrame, _ := gtk.FrameNew("Favorites") | ||
| 328 | favBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2) | ||
| 329 | favBox.SetMarginTop(2) | ||
| 330 | favBox.SetMarginBottom(2) | ||
| 331 | favBox.SetMarginStart(2) | ||
| 332 | favBox.SetMarginEnd(2) | ||
| 333 | |||
| 334 | favScroll, _ := gtk.ScrolledWindowNew(nil, nil) | ||
| 335 | favScroll.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) | ||
| 336 | favScroll.SetSizeRequest(230, 180) | ||
| 337 | |||
| 338 | app.favoritesStore, _ = gtk.ListStoreNew(gdk.PixbufGetType(), glib.TYPE_STRING, glib.TYPE_STRING) | ||
| 339 | app.favoritesView, _ = gtk.TreeViewNew() | ||
| 340 | app.favoritesView.SetModel(app.favoritesStore) | ||
| 341 | app.favoritesView.SetHeadersVisible(false) | ||
| 342 | |||
| 343 | favCol, _ := gtk.TreeViewColumnNew() | ||
| 344 | favPix, _ := gtk.CellRendererPixbufNew() | ||
| 345 | favCol.PackStart(favPix, false) | ||
| 346 | favCol.AddAttribute(favPix, "pixbuf", 0) | ||
| 347 | favHexText, _ := gtk.CellRendererTextNew() | ||
| 348 | favCol.PackStart(favHexText, true) | ||
| 349 | favCol.AddAttribute(favHexText, "text", 1) | ||
| 350 | app.favoritesView.AppendColumn(favCol) | ||
| 351 | |||
| 352 | favNameText, _ := gtk.CellRendererTextNew() | ||
| 353 | favNameCol, _ := gtk.TreeViewColumnNewWithAttribute("", favNameText, "text", 2) | ||
| 354 | app.favoritesView.AppendColumn(favNameCol) | ||
| 355 | |||
| 356 | favSel, _ := app.favoritesView.GetSelection() | ||
| 357 | favSel.SetMode(gtk.SELECTION_SINGLE) | ||
| 358 | favSel.Connect("changed", app.onFavoriteSelectionChanged) | ||
| 359 | app.favoritesView.Connect("row-activated", func() { | ||
| 360 | app.renameSelectedFavorite() | ||
| 361 | }) | ||
| 362 | |||
| 363 | favScroll.Add(app.favoritesView) | ||
| 364 | favBox.PackStart(favScroll, true, true, 0) | ||
| 365 | |||
| 366 | favBtns, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 4) | ||
| 367 | addFavBtn, _ := gtk.ButtonNewWithLabel("+") | ||
| 368 | app.setButtonIcon(addFavBtn, "list-add") | ||
| 369 | addFavBtn.Connect("clicked", func() { app.addCurrentToFavorites() }) | ||
| 370 | favBtns.PackStart(addFavBtn, true, true, 0) | ||
| 371 | |||
| 372 | app.removeFavBtn, _ = gtk.ButtonNewWithLabel("-") | ||
| 373 | app.setButtonIcon(app.removeFavBtn, "list-remove") | ||
| 374 | app.removeFavBtn.Connect("clicked", func() { app.removeSelectedFavorite() }) | ||
| 375 | favBtns.PackStart(app.removeFavBtn, true, true, 0) | ||
| 376 | |||
| 377 | app.renameFavBtn, _ = gtk.ButtonNewWithLabel("Rename") | ||
| 378 | app.setButtonIcon(app.renameFavBtn, "document-edit") | ||
| 379 | app.renameFavBtn.Connect("clicked", func() { app.renameSelectedFavorite() }) | ||
| 380 | favBtns.PackStart(app.renameFavBtn, true, true, 0) | ||
| 381 | |||
| 382 | app.clearFavBtn, _ = gtk.ButtonNewWithLabel("Clear") | ||
| 383 | app.setButtonIcon(app.clearFavBtn, "edit-clear") | ||
| 384 | app.clearFavBtn.Connect("clicked", func() { | ||
| 385 | app.savedColors = nil | ||
| 386 | app.refreshFavoritesView() | ||
| 387 | app.saveConfig() | ||
| 388 | }) | ||
| 389 | favBtns.PackStart(app.clearFavBtn, true, true, 0) | ||
| 121 | 390 | ||
| 122 | mainBox.PackStart(colorBox, false, false, 0) | 391 | favBox.PackStart(favBtns, false, false, 0) |
| 123 | 392 | favFrame.Add(favBox) | |
| 124 | expander, _ := gtk.ExpanderNew("Saved Colors") | 393 | lower.PackStart(favFrame, false, false, 0) |
| 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 | 394 | ||
| 188 | aboutBtn, _ := gtk.ButtonNewWithLabel("About") | 395 | root.PackStart(lower, true, true, 0) |
| 189 | aboutBtn.Connect("clicked", app.onAboutClicked) | ||
| 190 | bottomBox.PackStart(aboutBtn, false, false, 0) | ||
| 191 | 396 | ||
| 192 | mainBox.PackStart(bottomBox, false, false, 0) | 397 | status, _ := gtk.LabelNew("Choose a color and a scheme type") |
| 398 | status.SetHAlign(gtk.ALIGN_START) | ||
| 399 | root.PackStart(status, false, false, 0) | ||
| 193 | 400 | ||
| 194 | app.window.Add(mainBox) | 401 | app.window.Add(root) |
| 195 | app.window.ShowAll() | 402 | app.window.ShowAll() |
| 196 | } | 403 | } |
| 197 | 404 | ||
| 198 | func (app *App) populateList() { | 405 | func (app *App) buildMenuBar() *gtk.MenuBar { |
| 199 | app.listStore.Clear() | 406 | menuBar, _ := gtk.MenuBarNew() |
| 407 | |||
| 408 | fileTop, _ := gtk.MenuItemNewWithLabel("File") | ||
| 409 | fileMenu, _ := gtk.MenuNew() | ||
| 410 | random, _ := gtk.MenuItemNewWithLabel("Random") | ||
| 411 | random.Connect("activate", func() { app.randomizeColor() }) | ||
| 412 | fileMenu.Append(random) | ||
| 413 | quit, _ := gtk.MenuItemNewWithLabel("Quit") | ||
| 414 | quit.Connect("activate", func() { | ||
| 415 | app.saveConfig() | ||
| 416 | gtk.MainQuit() | ||
| 417 | }) | ||
| 418 | fileMenu.Append(quit) | ||
| 419 | fileTop.SetSubmenu(fileMenu) | ||
| 420 | menuBar.Append(fileTop) | ||
| 421 | |||
| 422 | editTop, _ := gtk.MenuItemNewWithLabel("Edit") | ||
| 423 | editMenu, _ := gtk.MenuNew() | ||
| 424 | paste, _ := gtk.MenuItemNewWithLabel("Paste") | ||
| 425 | paste.Connect("activate", func() { app.pasteColorFromClipboard() }) | ||
| 426 | editMenu.Append(paste) | ||
| 427 | editTop.SetSubmenu(editMenu) | ||
| 428 | menuBar.Append(editTop) | ||
| 429 | |||
| 430 | favTop, _ := gtk.MenuItemNewWithLabel("Favorites") | ||
| 431 | favMenu, _ := gtk.MenuNew() | ||
| 432 | add, _ := gtk.MenuItemNewWithLabel("Add Current") | ||
| 433 | add.Connect("activate", func() { app.addCurrentToFavorites() }) | ||
| 434 | favMenu.Append(add) | ||
| 435 | rename, _ := gtk.MenuItemNewWithLabel("Rename Selected") | ||
| 436 | rename.Connect("activate", func() { app.renameSelectedFavorite() }) | ||
| 437 | favMenu.Append(rename) | ||
| 438 | favTop.SetSubmenu(favMenu) | ||
| 439 | menuBar.Append(favTop) | ||
| 440 | |||
| 441 | helpTop, _ := gtk.MenuItemNewWithLabel("Help") | ||
| 442 | helpMenu, _ := gtk.MenuNew() | ||
| 443 | about, _ := gtk.MenuItemNewWithLabel("About") | ||
| 444 | about.Connect("activate", func() { app.onAboutClicked() }) | ||
| 445 | helpMenu.Append(about) | ||
| 446 | helpTop.SetSubmenu(helpMenu) | ||
| 447 | menuBar.Append(helpTop) | ||
| 448 | |||
| 449 | return menuBar | ||
| 450 | } | ||
| 200 | 451 | ||
| 201 | for _, color := range app.savedColors { | 452 | func (app *App) initCompactButtonCSS() { |
| 202 | pixbuf := app.createColorSwatch(color.Hex) | 453 | app.css, _ = gtk.CssProviderNew() |
| 203 | iter := app.listStore.Append() | 454 | css := "button { padding: 1px 4px; min-height: 0; min-width: 0; } .palette-swatch { padding: 0; border-width: 0; border-radius: 0; } .swatch-overlay-label { text-shadow: none; }" |
| 204 | app.listStore.Set(iter, []int{0, 1, 2}, []interface{}{pixbuf, color.Hex, color.Name}) | 455 | _ = app.css.LoadFromData(css) |
| 456 | if screen, err := gdk.ScreenGetDefault(); err == nil && screen != nil { | ||
| 457 | gtk.AddProviderForScreen(screen, app.css, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) | ||
| 205 | } | 458 | } |
| 206 | } | 459 | } |
| 207 | 460 | ||
| 208 | func (app *App) createColorSwatch(hexColor string) *gdk.Pixbuf { | 461 | func (app *App) setButtonIcon(btn *gtk.Button, iconName string) { |
| 209 | pixbuf, err := gdk.PixbufNew(gdk.COLORSPACE_RGB, false, 8, 16, 14) | 462 | img, err := gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_BUTTON) |
| 210 | if err != nil { | 463 | if err != nil || img == nil { |
| 211 | return nil | 464 | return |
| 465 | } | ||
| 466 | if label, err := btn.GetLabel(); err == nil && strings.TrimSpace(label) != "" { | ||
| 467 | btn.SetTooltipText(label) | ||
| 468 | btn.SetLabel("") | ||
| 212 | } | 469 | } |
| 470 | btn.SetImage(img) | ||
| 471 | btn.SetAlwaysShowImage(true) | ||
| 472 | } | ||
| 213 | 473 | ||
| 214 | rgba := gdk.NewRGBA() | 474 | func (app *App) newSwatchCard() SwatchCard { |
| 215 | rgba.Parse(hexColor) | 475 | button, _ := gtk.ButtonNew() |
| 476 | button.SetSizeRequest(166, 138) | ||
| 477 | |||
| 478 | vbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) | ||
| 479 | vbox.SetMarginTop(1) | ||
| 480 | vbox.SetMarginBottom(1) | ||
| 481 | vbox.SetMarginStart(1) | ||
| 482 | vbox.SetMarginEnd(1) | ||
| 483 | |||
| 484 | overlay, _ := gtk.OverlayNew() | ||
| 485 | overlay.SetHExpand(true) | ||
| 486 | overlay.SetVExpand(true) | ||
| 487 | |||
| 488 | image, _ := gtk.ImageNew() | ||
| 489 | image.SetFromPixbuf(solidPixbuf("#000000", cardImageW, cardImageH)) | ||
| 490 | overlay.Add(image) | ||
| 491 | |||
| 492 | label, _ := gtk.LabelNew("") | ||
| 493 | label.SetJustify(gtk.JUSTIFY_CENTER) | ||
| 494 | label.SetHAlign(gtk.ALIGN_CENTER) | ||
| 495 | label.SetVAlign(gtk.ALIGN_CENTER) | ||
| 496 | if ctx, err := label.GetStyleContext(); err == nil { | ||
| 497 | ctx.AddClass("swatch-overlay-label") | ||
| 498 | } | ||
| 499 | overlay.AddOverlay(label) | ||
| 500 | vbox.PackStart(overlay, true, true, 0) | ||
| 501 | |||
| 502 | button.Add(vbox) | ||
| 503 | |||
| 504 | return SwatchCard{button: button, image: image, label: label} | ||
| 505 | } | ||
| 216 | 506 | ||
| 217 | r := uint32(rgba.GetRed() * 255) | 507 | func (app *App) setCurrentColor(rgba *gdk.RGBA, pushHistory bool) { |
| 218 | g := uint32(rgba.GetGreen() * 255) | 508 | hex := rgbaToHex(rgba) |
| 219 | b := uint32(rgba.GetBlue() * 255) | 509 | app.currentColor = rgba |
| 510 | app.currentHex = hex | ||
| 511 | app.hexEntry.SetText(hex) | ||
| 512 | |||
| 513 | app.suppressColorSet = true | ||
| 514 | app.colorButton.SetRGBA(rgba) | ||
| 515 | app.suppressColorSet = false | ||
| 220 | 516 | ||
| 221 | pixels := pixbuf.GetPixels() | 517 | if pushHistory { |
| 222 | rowstride := pixbuf.GetRowstride() | 518 | app.pushHistory(hex) |
| 223 | nChannels := pixbuf.GetNChannels() | 519 | } |
| 224 | 520 | ||
| 225 | for y := 0; y < 14; y++ { | 521 | app.config.LastColor = hex |
| 226 | for x := 0; x < 16; x++ { | 522 | app.config.LastScheme = app.activeSchemeName() |
| 227 | offset := y*rowstride + x*nChannels | 523 | app.config.Palette = app.activePaletteName() |
| 228 | pixels[offset] = byte(r) | 524 | app.updateSchemePreview() |
| 229 | pixels[offset+1] = byte(g) | 525 | app.updateActionStates() |
| 230 | pixels[offset+2] = byte(b) | 526 | app.saveConfig() |
| 527 | } | ||
| 528 | |||
| 529 | func (app *App) updateSchemePreview() { | ||
| 530 | colors := generateScheme(app.currentColor, app.activeSchemeName()) | ||
| 531 | for i := 0; i < len(app.swatchCards); i++ { | ||
| 532 | if i >= len(colors) { | ||
| 533 | app.swatchCards[i].hex = "" | ||
| 534 | app.swatchCards[i].rgb = "" | ||
| 535 | app.swatchCards[i].hsv = "" | ||
| 536 | app.swatchCards[i].button.Hide() | ||
| 537 | continue | ||
| 231 | } | 538 | } |
| 539 | app.swatchCards[i].button.Show() | ||
| 540 | app.swatchCards[i].button.SetSensitive(true) | ||
| 541 | hex := rgbaToHex(colors[i]) | ||
| 542 | h, s, v := rgbToHSV(colors[i]) | ||
| 543 | r := int(math.Round(colors[i].GetRed() * 255)) | ||
| 544 | g := int(math.Round(colors[i].GetGreen() * 255)) | ||
| 545 | b := int(math.Round(colors[i].GetBlue() * 255)) | ||
| 546 | textColor := "#F5F5F5" | ||
| 547 | if luminance(colors[i]) > 0.53 { | ||
| 548 | textColor = "#111111" | ||
| 549 | } | ||
| 550 | rgbText := fmt.Sprintf("rgb(%d, %d, %d)", r, g, b) | ||
| 551 | hsvText := fmt.Sprintf("hsv(%d, %d, %d)", int(h), int(s), int(v)) | ||
| 552 | app.swatchCards[i].hex = hex | ||
| 553 | app.swatchCards[i].rgb = rgbText | ||
| 554 | app.swatchCards[i].hsv = hsvText | ||
| 555 | app.swatchCards[i].image.SetFromPixbuf(solidPixbuf(hex, cardImageW, cardImageH)) | ||
| 556 | app.swatchCards[i].label.SetMarkup(fmt.Sprintf("<span foreground=\"%s\" size=\"9000\"><b>%s</b>\n%s\n%s</span>", textColor, hex, rgbText, hsvText)) | ||
| 232 | } | 557 | } |
| 558 | } | ||
| 233 | 559 | ||
| 234 | return pixbuf | 560 | func (app *App) copyTextToClipboard(text string) { |
| 561 | if strings.TrimSpace(text) == "" { | ||
| 562 | return | ||
| 563 | } | ||
| 564 | clipboard, _ := gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD) | ||
| 565 | clipboard.SetText(text) | ||
| 235 | } | 566 | } |
| 236 | 567 | ||
| 237 | func (app *App) onSelectionChanged(selection *gtk.TreeSelection) { | 568 | func (app *App) showSwatchContextMenu(cardIdx int, ev *gdk.Event) { |
| 238 | model, iter, ok := selection.GetSelected() | 569 | if cardIdx < 0 || cardIdx >= len(app.swatchCards) { |
| 239 | if !ok { | 570 | return |
| 240 | app.deleteBtn.SetSensitive(false) | 571 | } |
| 241 | app.selectedIter = nil | 572 | card := app.swatchCards[cardIdx] |
| 573 | if card.hex == "" { | ||
| 242 | return | 574 | return |
| 243 | } | 575 | } |
| 576 | app.showColorContextMenu(card.hex, card.rgb, card.hsv, ev) | ||
| 577 | } | ||
| 244 | 578 | ||
| 245 | app.selectedIter = iter | 579 | func (app *App) showColorContextMenu(hex, rgbText, hsvText string, ev *gdk.Event) { |
| 246 | app.deleteBtn.SetSensitive(true) | 580 | if strings.TrimSpace(hex) == "" { |
| 581 | return | ||
| 582 | } | ||
| 247 | 583 | ||
| 248 | value, _ := model.ToTreeModel().GetValue(iter, 1) | 584 | menu, _ := gtk.MenuNew() |
| 249 | hexColor, _ := value.GetString() | 585 | copyHex, _ := gtk.MenuItemNewWithLabel("Copy HEX") |
| 586 | copyHex.Connect("activate", func() { | ||
| 587 | app.copyTextToClipboard(hex) | ||
| 588 | }) | ||
| 589 | menu.Append(copyHex) | ||
| 250 | 590 | ||
| 251 | rgba := gdk.NewRGBA() | 591 | copyHSV, _ := gtk.MenuItemNewWithLabel("Copy HSV") |
| 252 | rgba.Parse(hexColor) | 592 | copyHSV.Connect("activate", func() { |
| 253 | app.colorButton.SetRGBA(rgba) | 593 | app.copyTextToClipboard(hsvText) |
| 254 | app.currentColor = rgba | 594 | }) |
| 595 | menu.Append(copyHSV) | ||
| 596 | |||
| 597 | copyRGB, _ := gtk.MenuItemNewWithLabel("Copy RGB (RGV)") | ||
| 598 | copyRGB.Connect("activate", func() { | ||
| 599 | app.copyTextToClipboard(rgbText) | ||
| 600 | }) | ||
| 601 | menu.Append(copyRGB) | ||
| 602 | |||
| 603 | menu.ShowAll() | ||
| 604 | menu.PopupAtPointer(ev) | ||
| 255 | } | 605 | } |
| 256 | 606 | ||
| 257 | func (app *App) onSaveClicked() { | 607 | func colorStringsFromHex(hex string) (string, string) { |
| 258 | dialog, _ := gtk.DialogNew() | 608 | rgba := gdk.NewRGBA() |
| 259 | dialog.SetTitle("Save Color") | 609 | if !rgba.Parse(hex) { |
| 260 | dialog.SetTransientFor(app.window) | 610 | return "", "" |
| 261 | dialog.SetModal(true) | 611 | } |
| 262 | dialog.SetDefaultSize(300, -1) | 612 | r := int(math.Round(rgba.GetRed() * 255)) |
| 613 | g := int(math.Round(rgba.GetGreen() * 255)) | ||
| 614 | b := int(math.Round(rgba.GetBlue() * 255)) | ||
| 615 | h, s, v := rgbToHSV(rgba) | ||
| 616 | return fmt.Sprintf("rgb(%d, %d, %d)", r, g, b), fmt.Sprintf("hsv(%d, %d, %d)", int(h), int(s), int(v)) | ||
| 617 | } | ||
| 263 | 618 | ||
| 264 | box, _ := dialog.GetContentArea() | 619 | func (app *App) applyHexEntry() { |
| 265 | box.SetSpacing(10) | 620 | text, _ := app.hexEntry.GetText() |
| 266 | box.SetMarginTop(10) | 621 | text = strings.TrimSpace(text) |
| 267 | box.SetMarginBottom(10) | 622 | if text == "" { |
| 268 | box.SetMarginStart(10) | 623 | return |
| 269 | box.SetMarginEnd(10) | 624 | } |
| 625 | if !strings.HasPrefix(text, "#") { | ||
| 626 | text = "#" + text | ||
| 627 | } | ||
| 628 | rgba := gdk.NewRGBA() | ||
| 629 | if !rgba.Parse(text) { | ||
| 630 | app.hexEntry.SetText(app.currentHex) | ||
| 631 | return | ||
| 632 | } | ||
| 633 | app.setCurrentColor(rgba, true) | ||
| 634 | } | ||
| 270 | 635 | ||
| 271 | // get current color | 636 | func (app *App) randomizeColor() { |
| 272 | hexColor := rgbaToHex(app.currentColor) | 637 | r := app.rng.Intn(256) |
| 638 | g := app.rng.Intn(256) | ||
| 639 | b := app.rng.Intn(256) | ||
| 640 | rgba := gdk.NewRGBA() | ||
| 641 | rgba.SetRed(float64(r) / 255.0) | ||
| 642 | rgba.SetGreen(float64(g) / 255.0) | ||
| 643 | rgba.SetBlue(float64(b) / 255.0) | ||
| 644 | rgba.SetAlpha(1) | ||
| 645 | app.setCurrentColor(rgba, true) | ||
| 646 | } | ||
| 273 | 647 | ||
| 274 | label, _ := gtk.LabelNew(fmt.Sprintf("Color: %s", hexColor)) | 648 | func (app *App) adjustSV(ds, dv float64) { |
| 275 | box.PackStart(label, false, false, 0) | 649 | h, s, v := rgbToHSV(app.currentColor) |
| 650 | s = clamp(s+ds, 0, 100) | ||
| 651 | v = clamp(v+dv, 0, 100) | ||
| 652 | app.setCurrentColor(hsvToRGBA(h, s, v), true) | ||
| 653 | } | ||
| 276 | 654 | ||
| 277 | entryLabel, _ := gtk.LabelNew("Color Name:") | 655 | func (app *App) pasteColorFromClipboard() { |
| 278 | entryLabel.SetHAlign(gtk.ALIGN_START) | 656 | clipboard, _ := gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD) |
| 279 | box.PackStart(entryLabel, false, false, 0) | 657 | text, err := clipboard.WaitForText() |
| 658 | if err != nil { | ||
| 659 | return | ||
| 660 | } | ||
| 661 | text = strings.TrimSpace(text) | ||
| 662 | if text == "" { | ||
| 663 | return | ||
| 664 | } | ||
| 665 | if !strings.HasPrefix(text, "#") { | ||
| 666 | text = "#" + text | ||
| 667 | } | ||
| 668 | rgba := gdk.NewRGBA() | ||
| 669 | if rgba.Parse(text) { | ||
| 670 | app.setCurrentColor(rgba, true) | ||
| 671 | } | ||
| 672 | } | ||
| 280 | 673 | ||
| 281 | entry, _ := gtk.EntryNew() | 674 | func (app *App) pushHistory(hex string) { |
| 282 | entry.SetText("Untitled") | 675 | if app.historyPos >= 0 && app.historyPos < len(app.history) && app.history[app.historyPos] == hex { |
| 283 | entry.SetActivatesDefault(true) | 676 | return |
| 284 | box.PackStart(entry, false, false, 0) | 677 | } |
| 678 | if app.historyPos+1 < len(app.history) { | ||
| 679 | app.history = app.history[:app.historyPos+1] | ||
| 680 | } | ||
| 681 | app.history = append(app.history, hex) | ||
| 682 | if len(app.history) > maxHistoryLen { | ||
| 683 | over := len(app.history) - maxHistoryLen | ||
| 684 | app.history = app.history[over:] | ||
| 685 | } | ||
| 686 | app.historyPos = len(app.history) - 1 | ||
| 687 | } | ||
| 285 | 688 | ||
| 286 | dialog.AddButton("Cancel", gtk.RESPONSE_CANCEL) | 689 | func (app *App) navigateHistory(step int) { |
| 287 | okBtn, _ := dialog.AddButton("OK", gtk.RESPONSE_OK) | 690 | if len(app.history) == 0 { |
| 288 | okBtn.SetCanDefault(true) | 691 | return |
| 289 | okBtn.GrabDefault() | 692 | } |
| 693 | next := app.historyPos + step | ||
| 694 | if next < 0 || next >= len(app.history) { | ||
| 695 | return | ||
| 696 | } | ||
| 697 | rgba := gdk.NewRGBA() | ||
| 698 | if !rgba.Parse(app.history[next]) { | ||
| 699 | return | ||
| 700 | } | ||
| 701 | app.historyPos = next | ||
| 702 | app.setCurrentColor(rgba, false) | ||
| 703 | } | ||
| 290 | 704 | ||
| 291 | dialog.ShowAll() | 705 | func (app *App) updateActionStates() { |
| 706 | _, s, v := rgbToHSV(app.currentColor) | ||
| 707 | app.historyBackBtn.SetSensitive(app.historyPos > 0) | ||
| 708 | app.historyFwdBtn.SetSensitive(app.historyPos >= 0 && app.historyPos < len(app.history)-1) | ||
| 709 | app.lightenBtn.SetSensitive(v < 100) | ||
| 710 | app.darkenBtn.SetSensitive(v > 5) | ||
| 711 | app.saturateBtn.SetSensitive(s < 100) | ||
| 712 | app.desaturateBtn.SetSensitive(s > 5) | ||
| 713 | app.renameFavBtn.SetSensitive(app.selectedIter != nil) | ||
| 714 | app.removeFavBtn.SetSensitive(app.selectedIter != nil) | ||
| 715 | app.clearFavBtn.SetSensitive(len(app.savedColors) > 0) | ||
| 716 | } | ||
| 292 | 717 | ||
| 293 | response := dialog.Run() | 718 | func (app *App) activeSchemeName() string { |
| 294 | if response == gtk.RESPONSE_OK { | 719 | idx := app.schemeCombo.GetActive() |
| 295 | text, _ := entry.GetText() | 720 | if idx < 0 || idx >= len(schemeNames) { |
| 296 | app.savedColors = append([]SavedColor{{Hex: hexColor, Name: text}}, app.savedColors...) | 721 | return schemeNames[0] |
| 297 | app.saveColors() | ||
| 298 | app.populateList() | ||
| 299 | } | 722 | } |
| 723 | return schemeNames[idx] | ||
| 724 | } | ||
| 300 | 725 | ||
| 301 | dialog.Destroy() | 726 | func (app *App) activePaletteName() string { |
| 727 | idx := app.paletteCombo.GetActive() | ||
| 728 | if idx < 0 || idx >= len(paletteNames) { | ||
| 729 | return paletteNames[0] | ||
| 730 | } | ||
| 731 | return paletteNames[idx] | ||
| 302 | } | 732 | } |
| 303 | 733 | ||
| 304 | func (app *App) onDeleteClicked() { | 734 | func (app *App) onFavoriteSelectionChanged(selection *gtk.TreeSelection) { |
| 305 | if app.selectedIter == nil { | 735 | model, iter, ok := selection.GetSelected() |
| 736 | if !ok { | ||
| 737 | app.selectedIter = nil | ||
| 738 | app.updateActionStates() | ||
| 306 | return | 739 | return |
| 307 | } | 740 | } |
| 741 | app.selectedIter = iter | ||
| 742 | value, _ := model.ToTreeModel().GetValue(iter, 1) | ||
| 743 | hex, _ := value.GetString() | ||
| 744 | rgba := gdk.NewRGBA() | ||
| 745 | if rgba.Parse(hex) { | ||
| 746 | app.setCurrentColor(rgba, true) | ||
| 747 | } | ||
| 748 | app.updateActionStates() | ||
| 749 | } | ||
| 308 | 750 | ||
| 309 | model := app.listStore.ToTreeModel() | 751 | func (app *App) populatePaletteGrid() { |
| 310 | value, _ := model.GetValue(app.selectedIter, 1) | 752 | children := app.paletteGrid.GetChildren() |
| 311 | hexColor, _ := value.GetString() | 753 | children.Foreach(func(item interface{}) { |
| 754 | if widget, ok := item.(*gtk.Widget); ok { | ||
| 755 | app.paletteGrid.Remove(widget) | ||
| 756 | } | ||
| 757 | }) | ||
| 758 | |||
| 759 | colors := paletteByName(app.activePaletteName()) | ||
| 760 | cols := 24 | ||
| 761 | for i, hex := range colors { | ||
| 762 | btn, _ := gtk.ButtonNew() | ||
| 763 | btn.SetRelief(gtk.RELIEF_NONE) | ||
| 764 | btn.SetCanFocus(false) | ||
| 765 | btn.SetSizeRequest(16, 11) | ||
| 766 | if ctx, err := btn.GetStyleContext(); err == nil { | ||
| 767 | ctx.AddClass("palette-swatch") | ||
| 768 | } | ||
| 769 | img, _ := gtk.ImageNewFromPixbuf(solidPixbuf(hex, 16, 11)) | ||
| 770 | btn.Add(img) | ||
| 771 | h := hex | ||
| 772 | btn.Connect("button-press-event", func(_ *gtk.Button, ev *gdk.Event) bool { | ||
| 773 | if ev == nil { | ||
| 774 | return false | ||
| 775 | } | ||
| 776 | evBtn := gdk.EventButtonNewFromEvent(ev) | ||
| 777 | if evBtn == nil || evBtn.Button() != 3 { | ||
| 778 | return false | ||
| 779 | } | ||
| 780 | rgbText, hsvText := colorStringsFromHex(h) | ||
| 781 | app.showColorContextMenu(h, rgbText, hsvText, ev) | ||
| 782 | return true | ||
| 783 | }) | ||
| 784 | btn.Connect("clicked", func() { | ||
| 785 | rgba := gdk.NewRGBA() | ||
| 786 | if rgba.Parse(h) { | ||
| 787 | app.setCurrentColor(rgba, true) | ||
| 788 | } | ||
| 789 | }) | ||
| 790 | app.paletteGrid.Attach(btn, i%cols, i/cols, 1, 1) | ||
| 791 | } | ||
| 792 | app.paletteGrid.ShowAll() | ||
| 793 | } | ||
| 312 | 794 | ||
| 313 | // remove from saved colors | 795 | func (app *App) addCurrentToFavorites() { |
| 314 | for i, color := range app.savedColors { | 796 | for _, item := range app.savedColors { |
| 315 | if color.Hex == hexColor { | 797 | if strings.EqualFold(item.Hex, app.currentHex) { |
| 798 | return | ||
| 799 | } | ||
| 800 | } | ||
| 801 | app.savedColors = append([]SavedColor{{Hex: app.currentHex, Name: app.currentHex}}, app.savedColors...) | ||
| 802 | app.refreshFavoritesView() | ||
| 803 | app.saveConfig() | ||
| 804 | } | ||
| 805 | |||
| 806 | func (app *App) removeSelectedFavorite() { | ||
| 807 | if app.selectedIter == nil { | ||
| 808 | return | ||
| 809 | } | ||
| 810 | value, _ := app.favoritesStore.GetValue(app.selectedIter, 1) | ||
| 811 | hex, _ := value.GetString() | ||
| 812 | for i, c := range app.savedColors { | ||
| 813 | if strings.EqualFold(c.Hex, hex) { | ||
| 316 | app.savedColors = append(app.savedColors[:i], app.savedColors[i+1:]...) | 814 | app.savedColors = append(app.savedColors[:i], app.savedColors[i+1:]...) |
| 317 | break | 815 | break |
| 318 | } | 816 | } |
| 319 | } | 817 | } |
| 320 | |||
| 321 | app.saveColors() | ||
| 322 | app.populateList() | ||
| 323 | app.deleteBtn.SetSensitive(false) | ||
| 324 | app.selectedIter = nil | 818 | app.selectedIter = nil |
| 819 | app.refreshFavoritesView() | ||
| 820 | app.saveConfig() | ||
| 325 | } | 821 | } |
| 326 | 822 | ||
| 327 | func (app *App) onCopyClicked() { | 823 | func (app *App) renameSelectedFavorite() { |
| 328 | hexColor := rgbaToHex(app.currentColor) | 824 | if app.selectedIter == nil { |
| 329 | 825 | return | |
| 330 | clipboard, _ := gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD) | 826 | } |
| 331 | clipboard.SetText(hexColor) | 827 | vHex, _ := app.favoritesStore.GetValue(app.selectedIter, 1) |
| 828 | hex, _ := vHex.GetString() | ||
| 829 | vName, _ := app.favoritesStore.GetValue(app.selectedIter, 2) | ||
| 830 | currentName, _ := vName.GetString() | ||
| 332 | 831 | ||
| 333 | dialog := gtk.MessageDialogNew(app.window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, | 832 | dialog, _ := gtk.DialogNew() |
| 334 | gtk.BUTTONS_OK, fmt.Sprintf("Color %s copied to clipboard!", hexColor)) | 833 | dialog.SetTitle("Rename Color") |
| 335 | dialog.Run() | 834 | dialog.SetTransientFor(app.window) |
| 835 | dialog.SetModal(true) | ||
| 836 | box, _ := dialog.GetContentArea() | ||
| 837 | box.SetMarginTop(10) | ||
| 838 | box.SetMarginBottom(10) | ||
| 839 | box.SetMarginStart(10) | ||
| 840 | box.SetMarginEnd(10) | ||
| 841 | box.SetSpacing(6) | ||
| 842 | label, _ := gtk.LabelNew("Enter a new name:") | ||
| 843 | label.SetHAlign(gtk.ALIGN_START) | ||
| 844 | box.PackStart(label, false, false, 0) | ||
| 845 | entry, _ := gtk.EntryNew() | ||
| 846 | entry.SetText(currentName) | ||
| 847 | entry.SetActivatesDefault(true) | ||
| 848 | box.PackStart(entry, false, false, 0) | ||
| 849 | dialog.AddButton("Cancel", gtk.RESPONSE_CANCEL) | ||
| 850 | okBtn, _ := dialog.AddButton("OK", gtk.RESPONSE_OK) | ||
| 851 | okBtn.SetCanDefault(true) | ||
| 852 | okBtn.GrabDefault() | ||
| 853 | dialog.ShowAll() | ||
| 854 | if dialog.Run() == gtk.RESPONSE_OK { | ||
| 855 | newName, _ := entry.GetText() | ||
| 856 | newName = strings.TrimSpace(newName) | ||
| 857 | if newName != "" { | ||
| 858 | for i := range app.savedColors { | ||
| 859 | if strings.EqualFold(app.savedColors[i].Hex, hex) { | ||
| 860 | app.savedColors[i].Name = newName | ||
| 861 | break | ||
| 862 | } | ||
| 863 | } | ||
| 864 | app.refreshFavoritesView() | ||
| 865 | app.saveConfig() | ||
| 866 | } | ||
| 867 | } | ||
| 336 | dialog.Destroy() | 868 | dialog.Destroy() |
| 337 | } | 869 | } |
| 338 | 870 | ||
| 871 | func (app *App) refreshFavoritesView() { | ||
| 872 | app.favoritesStore.Clear() | ||
| 873 | for _, color := range app.savedColors { | ||
| 874 | iter := app.favoritesStore.Append() | ||
| 875 | app.favoritesStore.Set(iter, []int{0, 1, 2}, []interface{}{solidPixbuf(color.Hex, 16, 14), strings.ToUpper(color.Hex), color.Name}) | ||
| 876 | } | ||
| 877 | app.updateActionStates() | ||
| 878 | } | ||
| 879 | |||
| 339 | func (app *App) onAboutClicked() { | 880 | func (app *App) onAboutClicked() { |
| 340 | dialog, _ := gtk.AboutDialogNew() | 881 | dialog, _ := gtk.AboutDialogNew() |
| 341 | dialog.SetTransientFor(app.window) | 882 | dialog.SetTransientFor(app.window) |
| 342 | dialog.SetProgramName(appTitle) | 883 | dialog.SetProgramName(appTitle) |
| 343 | dialog.SetVersion(appVersion) | 884 | dialog.SetVersion(appVersion) |
| 344 | dialog.SetComments("a color picker inspired by agave, but only with the features kj_sh604 actually used") | 885 | dialog.SetComments("Agave-inspired GTK color scheme tool") |
| 345 | dialog.SetAuthors([]string{"kj_sh604"}) | 886 | dialog.SetAuthors([]string{"kj_sh604", "Agave inspiration: Jonathon Jongsma"}) |
| 346 | dialog.SetLicense("BSD Zero Clause License (0-clause BSD)") | 887 | dialog.SetLicense("BSD Zero Clause License (0-clause BSD)") |
| 347 | dialog.SetLogoIconName("applications-graphics") | 888 | dialog.SetLogoIconName("applications-graphics") |
| 348 | dialog.Run() | 889 | dialog.Run() |
| @@ -350,63 +891,384 @@ func (app *App) onAboutClicked() { | |||
| 350 | } | 891 | } |
| 351 | 892 | ||
| 352 | func (app *App) pickColorFromScreen() (*gdk.RGBA, error) { | 893 | func (app *App) pickColorFromScreen() (*gdk.RGBA, error) { |
| 353 | // use xcolor for x11 color picking | ||
| 354 | cmd := exec.Command("xcolor", "--format", "hex") | 894 | cmd := exec.Command("xcolor", "--format", "hex") |
| 355 | output, err := cmd.Output() | 895 | output, err := cmd.Output() |
| 356 | if err != nil { | 896 | if err != nil { |
| 357 | // fallback to grabc | ||
| 358 | cmd = exec.Command("grabc") | 897 | cmd = exec.Command("grabc") |
| 359 | output, err = cmd.Output() | 898 | output, err = cmd.Output() |
| 360 | if err != nil { | 899 | if err != nil { |
| 361 | dialog := gtk.MessageDialogNew(app.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, | 900 | dialog := gtk.MessageDialogNew(app.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, |
| 362 | gtk.BUTTONS_OK, "Color picker not found. Please install 'xcolor'") | 901 | gtk.BUTTONS_OK, "Color picker not found. Please install xcolor or grabc") |
| 363 | dialog.Run() | 902 | dialog.Run() |
| 364 | dialog.Destroy() | 903 | dialog.Destroy() |
| 365 | return nil, err | 904 | return nil, err |
| 366 | } | 905 | } |
| 367 | } | 906 | } |
| 368 | 907 | ||
| 369 | hexColor := strings.TrimSpace(string(output)) | 908 | hex := strings.TrimSpace(string(output)) |
| 370 | if !strings.HasPrefix(hexColor, "#") { | 909 | if !strings.HasPrefix(hex, "#") { |
| 371 | hexColor = "#" + hexColor | 910 | hex = "#" + hex |
| 372 | } | 911 | } |
| 373 | |||
| 374 | rgba := gdk.NewRGBA() | 912 | rgba := gdk.NewRGBA() |
| 375 | if !rgba.Parse(hexColor) { | 913 | if !rgba.Parse(hex) { |
| 376 | return nil, fmt.Errorf("invalid color format: %s", hexColor) | 914 | return nil, fmt.Errorf("invalid color format: %s", hex) |
| 377 | } | 915 | } |
| 378 | |||
| 379 | return rgba, nil | 916 | return rgba, nil |
| 380 | } | 917 | } |
| 381 | 918 | ||
| 382 | func (app *App) loadColors() { | 919 | func (app *App) loadConfig() { |
| 383 | data, err := os.ReadFile(app.configFile) | 920 | data, err := os.ReadFile(app.configFile) |
| 384 | if err != nil { | 921 | if err != nil { |
| 385 | app.savedColors = []SavedColor{} | 922 | app.savedColors = []SavedColor{} |
| 923 | app.config = AppConfig{Favorites: []SavedColor{}, LastColor: "#0066FF", LastScheme: "Triads", Palette: "Web-safe colors"} | ||
| 386 | return | 924 | return |
| 387 | } | 925 | } |
| 388 | 926 | ||
| 389 | if err := json.Unmarshal(data, &app.savedColors); err != nil { | 927 | var cfg AppConfig |
| 390 | log.Printf("Error loading colors: %v", err) | 928 | if err := json.Unmarshal(data, &cfg); err == nil && (cfg.LastColor != "" || len(cfg.Favorites) > 0) { |
| 391 | app.savedColors = []SavedColor{} | 929 | app.config = cfg |
| 930 | app.savedColors = append([]SavedColor(nil), cfg.Favorites...) | ||
| 931 | if app.savedColors == nil { | ||
| 932 | app.savedColors = []SavedColor{} | ||
| 933 | } | ||
| 934 | if app.config.LastColor == "" { | ||
| 935 | app.config.LastColor = "#0066FF" | ||
| 936 | } | ||
| 937 | if app.config.LastScheme == "" { | ||
| 938 | app.config.LastScheme = "Triads" | ||
| 939 | } | ||
| 940 | if app.config.Palette == "" { | ||
| 941 | app.config.Palette = "Web-safe colors" | ||
| 942 | } | ||
| 943 | return | ||
| 944 | } | ||
| 945 | |||
| 946 | var legacy []SavedColor | ||
| 947 | if err := json.Unmarshal(data, &legacy); err == nil { | ||
| 948 | app.savedColors = legacy | ||
| 949 | app.config = AppConfig{Favorites: legacy, LastColor: "#0066FF", LastScheme: "Triads", Palette: "Web-safe colors"} | ||
| 950 | return | ||
| 392 | } | 951 | } |
| 952 | |||
| 953 | app.savedColors = []SavedColor{} | ||
| 954 | app.config = AppConfig{Favorites: []SavedColor{}, LastColor: "#0066FF", LastScheme: "Triads", Palette: "Web-safe colors"} | ||
| 393 | } | 955 | } |
| 394 | 956 | ||
| 395 | func (app *App) saveColors() { | 957 | func (app *App) saveConfig() { |
| 396 | data, err := json.MarshalIndent(app.savedColors, "", " ") | 958 | app.config.Favorites = app.savedColors |
| 959 | if app.currentHex != "" { | ||
| 960 | app.config.LastColor = app.currentHex | ||
| 961 | } | ||
| 962 | if app.schemeCombo != nil { | ||
| 963 | app.config.LastScheme = app.activeSchemeName() | ||
| 964 | } | ||
| 965 | if app.paletteCombo != nil { | ||
| 966 | app.config.Palette = app.activePaletteName() | ||
| 967 | } | ||
| 968 | |||
| 969 | data, err := json.MarshalIndent(app.config, "", " ") | ||
| 397 | if err != nil { | 970 | if err != nil { |
| 398 | log.Printf("Error marshaling colors: %v", err) | 971 | log.Printf("Error marshaling config: %v", err) |
| 399 | return | 972 | return |
| 400 | } | 973 | } |
| 401 | |||
| 402 | if err := os.WriteFile(app.configFile, data, 0644); err != nil { | 974 | if err := os.WriteFile(app.configFile, data, 0644); err != nil { |
| 403 | log.Printf("Error saving colors: %v", err) | 975 | log.Printf("Error saving config: %v", err) |
| 976 | } | ||
| 977 | } | ||
| 978 | |||
| 979 | func (app *App) restoreStartupState() { | ||
| 980 | rgba := gdk.NewRGBA() | ||
| 981 | if app.config.LastColor != "" && rgba.Parse(app.config.LastColor) { | ||
| 982 | app.currentColor = rgba | ||
| 983 | } | ||
| 984 | |||
| 985 | for i, name := range schemeNames { | ||
| 986 | if name == app.config.LastScheme { | ||
| 987 | app.schemeCombo.SetActive(i) | ||
| 988 | break | ||
| 989 | } | ||
| 990 | } | ||
| 991 | for i, name := range paletteNames { | ||
| 992 | if name == app.config.Palette { | ||
| 993 | app.paletteCombo.SetActive(i) | ||
| 994 | break | ||
| 995 | } | ||
| 996 | } | ||
| 997 | |||
| 998 | app.setCurrentColor(app.currentColor, true) | ||
| 999 | } | ||
| 1000 | |||
| 1001 | func generateScheme(base *gdk.RGBA, schemeName string) []*gdk.RGBA { | ||
| 1002 | h, s, v := rgbToHSV(base) | ||
| 1003 | mk := func(hue float64) *gdk.RGBA { | ||
| 1004 | return hsvToRGBA(wrapHue(hue), s, v) | ||
| 1005 | } | ||
| 1006 | |||
| 1007 | switch schemeName { | ||
| 1008 | case "Complements": | ||
| 1009 | return []*gdk.RGBA{hsvToRGBA(h, s, v), mk(h + 180)} | ||
| 1010 | case "Split Complements": | ||
| 1011 | offset := 360.0 / 15.0 | ||
| 1012 | return []*gdk.RGBA{hsvToRGBA(h, s, v), mk(h + 180 - offset), mk(h + 180 + offset)} | ||
| 1013 | case "Tetrads": | ||
| 1014 | offset := 90.0 | ||
| 1015 | return []*gdk.RGBA{hsvToRGBA(h, s, v), mk(h + offset), mk(h + 180), mk(h + 180 + offset)} | ||
| 1016 | case "Analogous": | ||
| 1017 | offset := 360.0 / 12.0 | ||
| 1018 | return []*gdk.RGBA{mk(h - offset), hsvToRGBA(h, s, v), mk(h + offset)} | ||
| 1019 | case "Monochromatic": | ||
| 1020 | c0 := hsvToRGBA(h, s, v) | ||
| 1021 | c1 := hsvToRGBA(h, s, v) | ||
| 1022 | c2 := hsvToRGBA(h, s, v) | ||
| 1023 | if s < 10 { | ||
| 1024 | c1 = hsvToRGBA(h, math.Mod(s+33, 100), v) | ||
| 1025 | c2 = hsvToRGBA(h, math.Mod(s+66, 100), v) | ||
| 1026 | } else { | ||
| 1027 | c1 = hsvToRGBA(h, s, math.Mod(v+33, 100)) | ||
| 1028 | c2 = hsvToRGBA(h, s, math.Mod(v+66, 100)) | ||
| 1029 | } | ||
| 1030 | out := []*gdk.RGBA{c0, c1, c2} | ||
| 1031 | sort.Slice(out, func(i, j int) bool { return luminance(out[i]) < luminance(out[j]) }) | ||
| 1032 | return out | ||
| 1033 | case "Triads": | ||
| 1034 | fallthrough | ||
| 1035 | default: | ||
| 1036 | offset := 120.0 | ||
| 1037 | return []*gdk.RGBA{hsvToRGBA(h, s, v), mk(h + offset), mk(h - offset)} | ||
| 1038 | } | ||
| 1039 | } | ||
| 1040 | |||
| 1041 | func paletteByName(name string) []string { | ||
| 1042 | switch name { | ||
| 1043 | case "Web-safe (legacy)": | ||
| 1044 | vals := []int{0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF} | ||
| 1045 | colors := make([]string, 0, 216) | ||
| 1046 | for _, r := range vals { | ||
| 1047 | for _, g := range vals { | ||
| 1048 | for _, b := range vals { | ||
| 1049 | colors = append(colors, fmt.Sprintf("#%02X%02X%02X", r, g, b)) | ||
| 1050 | } | ||
| 1051 | } | ||
| 1052 | } | ||
| 1053 | return colors | ||
| 1054 | case "Material Design": | ||
| 1055 | return []string{ | ||
| 1056 | "#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", | ||
| 1057 | "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", | ||
| 1058 | "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E", | ||
| 1059 | "#607D8B", "#000000", "#FFFFFF", "#EF5350", "#EC407A", "#AB47BC", | ||
| 1060 | "#7E57C2", "#5C6BC0", "#42A5F5", "#29B6F6", "#26C6DA", "#26A69A", | ||
| 1061 | "#66BB6A", "#9CCC65", "#D4E157", "#FFEE58", "#FFCA28", "#FFA726", | ||
| 1062 | "#FF7043", "#8D6E63", "#BDBDBD", "#78909C", "#212121", "#FAFAFA", | ||
| 1063 | "#C62828", "#AD1457", "#6A1B9A", "#4527A0", "#283593", "#1565C0", | ||
| 1064 | } | ||
| 1065 | case "Tailwind CSS": | ||
| 1066 | return []string{ | ||
| 1067 | "#EF4444", "#F97316", "#F59E0B", "#EAB308", "#84CC16", "#22C55E", | ||
| 1068 | "#10B981", "#14B8A6", "#06B6D4", "#0EA5E9", "#3B82F6", "#6366F1", | ||
| 1069 | "#8B5CF6", "#A855F7", "#D946EF", "#EC4899", "#F43F5E", "#64748B", | ||
| 1070 | "#DC2626", "#EA580C", "#D97706", "#CA8A04", "#65A30D", "#16A34A", | ||
| 1071 | "#059669", "#0D9488", "#0891B2", "#0284C7", "#2563EB", "#4F46E5", | ||
| 1072 | "#7C3AED", "#9333EA", "#C026D3", "#DB2777", "#E11D48", "#475569", | ||
| 1073 | "#991B1B", "#9A3412", "#92400E", "#854D0E", "#3F6212", "#14532D", | ||
| 1074 | "#064E3B", "#134E4A", "#164E63", "#075985", "#1E3A8A", "#312E81", | ||
| 1075 | } | ||
| 1076 | case "Flat UI": | ||
| 1077 | return []string{ | ||
| 1078 | "#1ABC9C", "#16A085", "#2ECC71", "#27AE60", "#3498DB", "#2980B9", | ||
| 1079 | "#9B59B6", "#8E44AD", "#34495E", "#2C3E50", "#F1C40F", "#F39C12", | ||
| 1080 | "#E67E22", "#D35400", "#E74C3C", "#C0392B", "#ECF0F1", "#BDC3C7", | ||
| 1081 | "#95A5A6", "#7F8C8D", "#52B3D9", "#E8F8F5", "#D5F4E6", "#D6EAF8", | ||
| 1082 | "#E8DAEF", "#FADBD8", "#F9E79F", "#FAD7A0", "#F5B7B1", "#D7DBDD", | ||
| 1083 | } | ||
| 1084 | case "Pastel": | ||
| 1085 | return []string{ | ||
| 1086 | "#FFB3BA", "#FFDFBA", "#FFFFBA", "#BAFFC9", "#BAE1FF", "#E0BBE4", | ||
| 1087 | "#FFDFD3", "#FEC8D8", "#D5AAFF", "#B4F8C8", "#A0E7E5", "#FFAEBC", | ||
| 1088 | "#FBE7C6", "#B4F8C8", "#A0C4FF", "#BDB2FF", "#FFC6FF", "#FDFFB6", | ||
| 1089 | "#CAFFBF", "#9BF6FF", "#A0C4FF", "#BDB2FF", "#FFC6FF", "#FFFFFC", | ||
| 1090 | "#FFD6A5", "#FDFFB6", "#CAFFBF", "#A8E6CF", "#FFD3B6", "#FFAAA5", | ||
| 1091 | } | ||
| 1092 | case "Nord": | ||
| 1093 | return []string{ | ||
| 1094 | "#2E3440", "#3B4252", "#434C5E", "#4C566A", "#D8DEE9", "#E5E9F0", | ||
| 1095 | "#ECEFF4", "#8FBCBB", "#88C0D0", "#81A1C1", "#5E81AC", "#BF616A", | ||
| 1096 | "#D08770", "#EBCB8B", "#A3BE8C", "#B48EAD", "#4C566A", "#434C5E", | ||
| 1097 | "#3B4252", "#2E3440", "#ECEFF4", "#E5E9F0", "#D8DEE9", "#88C0D0", | ||
| 1098 | } | ||
| 1099 | case "Dracula": | ||
| 1100 | return []string{ | ||
| 1101 | "#282A36", "#44475A", "#F8F8F2", "#6272A4", "#8BE9FD", "#50FA7B", | ||
| 1102 | "#FFB86C", "#FF79C6", "#BD93F9", "#FF5555", "#F1FA8C", "#21222C", | ||
| 1103 | "#191A21", "#6272A4", "#B45BCF", "#4D4F68", "#626483", "#62D6E8", | ||
| 1104 | "#EA51B2", "#EBFF87", "#00F769", "#B45BCF", "#7081D0", "#A1EFE4", | ||
| 1105 | } | ||
| 1106 | case "Solarized": | ||
| 1107 | return []string{ | ||
| 1108 | "#002B36", "#073642", "#586E75", "#657B83", "#839496", "#93A1A1", | ||
| 1109 | "#EEE8D5", "#FDF6E3", "#B58900", "#CB4B16", "#DC322F", "#D33682", | ||
| 1110 | "#6C71C4", "#268BD2", "#2AA198", "#859900", "#002B36", "#073642", | ||
| 1111 | "#586E75", "#657B83", "#839496", "#93A1A1", "#EEE8D5", "#FDF6E3", | ||
| 1112 | } | ||
| 1113 | case "Gruvbox": | ||
| 1114 | return []string{ | ||
| 1115 | "#282828", "#CC241D", "#98971A", "#D79921", "#458588", "#B16286", | ||
| 1116 | "#689D6A", "#A89984", "#928374", "#FB4934", "#B8BB26", "#FABD2F", | ||
| 1117 | "#83A598", "#D3869B", "#8EC07C", "#EBDBB2", "#FBF1C7", "#3C3836", | ||
| 1118 | "#504945", "#665C54", "#7C6F64", "#D65D0E", "#FE8019", "#BDAE93", | ||
| 1119 | } | ||
| 1120 | case "One Dark": | ||
| 1121 | return []string{ | ||
| 1122 | "#282C34", "#ABB2BF", "#E06C75", "#D19A66", "#E5C07B", "#98C379", | ||
| 1123 | "#56B6C2", "#61AFEF", "#C678DD", "#BE5046", "#3B4048", "#4B5263", | ||
| 1124 | "#545862", "#565C64", "#5C6370", "#636D83", "#828997", "#2C323C", | ||
| 1125 | "#353B45", "#3E4451", "#4F5666", "#5F697A", "#6B7587", "#979EAB", | ||
| 1126 | } | ||
| 1127 | case "Monokai": | ||
| 1128 | return []string{ | ||
| 1129 | "#272822", "#F8F8F2", "#F92672", "#E6DB74", "#A6E22E", "#66D9EF", | ||
| 1130 | "#AE81FF", "#FD971F", "#75715E", "#49483E", "#3E3D32", "#F8F8F0", | ||
| 1131 | "#F5F4F1", "#A59F85", "#FD5FF0", "#F4BF75", "#FFF59D", "#CFCFC2", | ||
| 1132 | "#A1EFE4", "#FFE792", "#CC6633", "#778899", "#9D550F", "#E69F66", | ||
| 1133 | } | ||
| 1134 | case "KiJiSH Dark Pastel Terminal": | ||
| 1135 | return []string{ | ||
| 1136 | "#2C2C2C", "#DCDCDC", "#3F3F3F", "#D67979", "#60B48A", "#DFAF8F", | ||
| 1137 | "#9AB8D7", "#DC8CC3", "#8CD0D3", "#DCDCDC", "#709080", "#DCA3A3", | ||
| 1138 | "#72D5A3", "#F0DFAF", "#94BFF3", "#EC93D3", "#93E0E3", "#FFFFFF", | ||
| 1139 | } | ||
| 1140 | default: | ||
| 1141 | vals := []int{0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF} | ||
| 1142 | colors := make([]string, 0, 216) | ||
| 1143 | for _, r := range vals { | ||
| 1144 | for _, g := range vals { | ||
| 1145 | for _, b := range vals { | ||
| 1146 | colors = append(colors, fmt.Sprintf("#%02X%02X%02X", r, g, b)) | ||
| 1147 | } | ||
| 1148 | } | ||
| 1149 | } | ||
| 1150 | return colors | ||
| 404 | } | 1151 | } |
| 405 | } | 1152 | } |
| 406 | 1153 | ||
| 1154 | func solidPixbuf(hex string, width, height int) *gdk.Pixbuf { | ||
| 1155 | pb, err := gdk.PixbufNew(gdk.COLORSPACE_RGB, false, 8, width, height) | ||
| 1156 | if err != nil { | ||
| 1157 | return nil | ||
| 1158 | } | ||
| 1159 | rgba := gdk.NewRGBA() | ||
| 1160 | rgba.Parse(hex) | ||
| 1161 | r := byte(rgba.GetRed() * 255) | ||
| 1162 | g := byte(rgba.GetGreen() * 255) | ||
| 1163 | b := byte(rgba.GetBlue() * 255) | ||
| 1164 | |||
| 1165 | pixels := pb.GetPixels() | ||
| 1166 | rowstride := pb.GetRowstride() | ||
| 1167 | channels := pb.GetNChannels() | ||
| 1168 | for y := 0; y < height; y++ { | ||
| 1169 | for x := 0; x < width; x++ { | ||
| 1170 | off := y*rowstride + x*channels | ||
| 1171 | pixels[off] = r | ||
| 1172 | pixels[off+1] = g | ||
| 1173 | pixels[off+2] = b | ||
| 1174 | } | ||
| 1175 | } | ||
| 1176 | return pb | ||
| 1177 | } | ||
| 1178 | |||
| 1179 | func rgbToHSV(rgba *gdk.RGBA) (float64, float64, float64) { | ||
| 1180 | r := rgba.GetRed() | ||
| 1181 | g := rgba.GetGreen() | ||
| 1182 | b := rgba.GetBlue() | ||
| 1183 | |||
| 1184 | mx := math.Max(r, math.Max(g, b)) | ||
| 1185 | mn := math.Min(r, math.Min(g, b)) | ||
| 1186 | delta := mx - mn | ||
| 1187 | |||
| 1188 | h := 0.0 | ||
| 1189 | if delta > 0 { | ||
| 1190 | switch mx { | ||
| 1191 | case r: | ||
| 1192 | h = 60 * math.Mod((g-b)/delta, 6) | ||
| 1193 | case g: | ||
| 1194 | h = 60 * ((b-r)/delta + 2) | ||
| 1195 | case b: | ||
| 1196 | h = 60 * ((r-g)/delta + 4) | ||
| 1197 | } | ||
| 1198 | } | ||
| 1199 | if h < 0 { | ||
| 1200 | h += 360 | ||
| 1201 | } | ||
| 1202 | |||
| 1203 | s := 0.0 | ||
| 1204 | if mx > 0 { | ||
| 1205 | s = (delta / mx) * 100 | ||
| 1206 | } | ||
| 1207 | v := mx * 100 | ||
| 1208 | return wrapHue(h), clamp(s, 0, 100), clamp(v, 0, 100) | ||
| 1209 | } | ||
| 1210 | |||
| 1211 | func hsvToRGBA(h, s, v float64) *gdk.RGBA { | ||
| 1212 | h = wrapHue(h) | ||
| 1213 | s = clamp(s, 0, 100) / 100 | ||
| 1214 | v = clamp(v, 0, 100) / 100 | ||
| 1215 | |||
| 1216 | c := v * s | ||
| 1217 | x := c * (1 - math.Abs(math.Mod(h/60, 2)-1)) | ||
| 1218 | m := v - c | ||
| 1219 | |||
| 1220 | var r1, g1, b1 float64 | ||
| 1221 | switch { | ||
| 1222 | case h < 60: | ||
| 1223 | r1, g1, b1 = c, x, 0 | ||
| 1224 | case h < 120: | ||
| 1225 | r1, g1, b1 = x, c, 0 | ||
| 1226 | case h < 180: | ||
| 1227 | r1, g1, b1 = 0, c, x | ||
| 1228 | case h < 240: | ||
| 1229 | r1, g1, b1 = 0, x, c | ||
| 1230 | case h < 300: | ||
| 1231 | r1, g1, b1 = x, 0, c | ||
| 1232 | default: | ||
| 1233 | r1, g1, b1 = c, 0, x | ||
| 1234 | } | ||
| 1235 | |||
| 1236 | rgba := gdk.NewRGBA() | ||
| 1237 | rgba.SetRed(r1 + m) | ||
| 1238 | rgba.SetGreen(g1 + m) | ||
| 1239 | rgba.SetBlue(b1 + m) | ||
| 1240 | rgba.SetAlpha(1) | ||
| 1241 | return rgba | ||
| 1242 | } | ||
| 1243 | |||
| 407 | func rgbaToHex(rgba *gdk.RGBA) string { | 1244 | func rgbaToHex(rgba *gdk.RGBA) string { |
| 408 | r := uint8(rgba.GetRed() * 255) | 1245 | r := int(math.Round(rgba.GetRed() * 255)) |
| 409 | g := uint8(rgba.GetGreen() * 255) | 1246 | g := int(math.Round(rgba.GetGreen() * 255)) |
| 410 | b := uint8(rgba.GetBlue() * 255) | 1247 | b := int(math.Round(rgba.GetBlue() * 255)) |
| 411 | return fmt.Sprintf("#%02X%02X%02X", r, g, b) | 1248 | return fmt.Sprintf("#%02X%02X%02X", r, g, b) |
| 412 | } | 1249 | } |
| 1250 | |||
| 1251 | func wrapHue(h float64) float64 { | ||
| 1252 | v := math.Mod(h, 360) | ||
| 1253 | if v < 0 { | ||
| 1254 | v += 360 | ||
| 1255 | } | ||
| 1256 | return v | ||
| 1257 | } | ||
| 1258 | |||
| 1259 | func clamp(v, lo, hi float64) float64 { | ||
| 1260 | if v < lo { | ||
| 1261 | return lo | ||
| 1262 | } | ||
| 1263 | if v > hi { | ||
| 1264 | return hi | ||
| 1265 | } | ||
| 1266 | return v | ||
| 1267 | } | ||
| 1268 | |||
| 1269 | func luminance(rgba *gdk.RGBA) float64 { | ||
| 1270 | r := rgba.GetRed() | ||
| 1271 | g := rgba.GetGreen() | ||
| 1272 | b := rgba.GetBlue() | ||
| 1273 | return 0.2126*r + 0.7152*g + 0.0722*b | ||
| 1274 | } | ||
