yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
palette_editor_widget.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <map>
6
7#include "absl/strings/str_format.h"
11#include "app/gui/core/color.h"
12#include "app/gui/core/input.h"
17#include "util/log.h"
18
19#include <cstring>
20#include <string>
21#include <vector>
22
23namespace yaze {
24namespace gui {
25
26namespace {
27
33
34std::optional<DungeonRenderColorTarget> ResolveDungeonRenderColorTarget(
35 int display_index, int dungeon_palette_id) {
36 if (display_index < 0 || display_index >= 128) {
37 return std::nullopt;
38 }
39 if (display_index < 32) {
41 display_index};
42 }
43
44 const int local_index = display_index - 32;
45 const int row = local_index / 16;
46 const int col = local_index % 16;
47 if (row >= 6 || col == 0) {
48 return std::nullopt;
49 }
50
52 dungeon_palette_id, row * 15 + (col - 1)};
53}
54
56 return source == DungeonRenderPaletteSource::kHud ? "hud" : "dungeon_main";
57}
58
60 const zelda3::GameData& game_data, const DungeonRenderColorTarget& target) {
62 ? game_data.palette_groups.hud
63 : game_data.palette_groups.dungeon_main;
64}
65
67 const DungeonRenderColorTarget& target) {
69 return "HUD / floor-ceiling";
70 }
71 return absl::StrFormat("Dungeon row %d", target.color_index / 15 + 2);
72}
73
74const char* RomPaletteGroupNameGetter(void* user_data, int idx) {
75 auto* names = static_cast<std::vector<std::string>*>(user_data);
76 if (!names || idx < 0 || idx >= static_cast<int>(names->size())) {
77 return nullptr;
78 }
79 return (*names)[idx].c_str();
80}
81
82void DrawColorDropTargetOutline(ImVec2 min, ImVec2 max) {
83 const auto& theme = ThemeManager::Get().GetCurrentTheme();
84 ImGui::GetForegroundDrawList()->AddRect(
85 min, max, ImGui::GetColorU32(ConvertColorToImVec4(theme.accent)), 0.0f, 0,
86 2.0f);
87}
88
89bool AcceptImGuiColorDrop(gfx::SnesColor* color, ImVec2 min, ImVec2 max) {
90 if (!color || !ImGui::BeginDragDropTarget()) {
91 return false;
92 }
93
94 bool changed = false;
95 constexpr ImGuiDragDropFlags kFlags =
96 ImGuiDragDropFlags_AcceptBeforeDelivery |
97 ImGuiDragDropFlags_AcceptNoDrawDefaultRect |
98 ImGuiDragDropFlags_AcceptNoPreviewTooltip;
99 auto accept = [&](const char* type, size_t components) {
100 if (changed) {
101 return;
102 }
103 if (const ImGuiPayload* payload =
104 ImGui::AcceptDragDropPayload(type, kFlags)) {
105 DrawColorDropTargetOutline(min, max);
106 if (payload->IsDelivery()) {
107 ImVec4 dropped(0, 0, 0, 1);
108 std::memcpy(&dropped.x, payload->Data, sizeof(float) * components);
109 if (components == 3) {
110 dropped.w = 1.0f;
111 }
112 *color = gfx::SnesColor(dropped);
113 changed = true;
114 }
115 }
116 };
117
118 accept(IMGUI_PAYLOAD_TYPE_COLOR_3F, 3);
119 accept(IMGUI_PAYLOAD_TYPE_COLOR_4F, 4);
120 ImGui::EndDragDropTarget();
121 return changed;
122}
123
124} // namespace
125
126// Merged implementation from PaletteWidget and PaletteEditorWidget
127
129 game_data_ = game_data;
130 rom_ = nullptr;
131 rom_palettes_loaded_ = false;
132 if (game_data_) {
134 }
137}
138
140 rom_ = rom;
141 game_data_ = nullptr;
142 rom_palettes_loaded_ = false;
143 // Legacy mode - no palette loading without game_data
146}
147
149 int display_index, std::optional<gfx::SnesColor> color) {
150 if (!game_data_) {
151 return absl::FailedPreconditionError("GameData not loaded");
152 }
153
154 const auto target =
155 ResolveDungeonRenderColorTarget(display_index, current_palette_id_);
156 if (!target.has_value()) {
157 return absl::InvalidArgumentError(absl::StrFormat(
158 "Dungeon render palette slot %d is reserved or out of range",
159 display_index));
160 }
161
162 auto& palette_manager = gfx::PaletteManager::Get();
163 if (!palette_manager.IsManaging(game_data_)) {
164 return absl::FailedPreconditionError(
165 "PaletteManager is bound to a different ROM session");
166 }
167 const char* group_name = PaletteManagerGroupName(target->source);
168 absl::Status status =
169 color.has_value()
170 ? palette_manager.SetColor(group_name, target->palette_index,
171 target->color_index, *color)
172 : palette_manager.ResetColor(group_name, target->palette_index,
173 target->color_index);
174 if (!status.ok()) {
175 return status;
176 }
177
179 on_palette_changed_({current_palette_id_, target->source});
180 }
181 return absl::OkStatus();
182}
183
184// --- Embedded Draw Method (from simple editor) ---
186 if (!game_data_) {
187 ImGui::TextColored(ImVec4(1, 0, 0, 1), tr("GameData not loaded"));
188 return;
189 }
190
191 ImGui::BeginGroup();
192
194 ImGui::Separator();
195
196 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
197 if (dungeon_pal_group.empty()) {
198 ImGui::TextDisabled(tr("Dungeon palettes unavailable"));
199 ImGui::EndGroup();
200 return;
201 }
202
203 current_palette_id_ = std::clamp(
204 current_palette_id_, 0, static_cast<int>(dungeon_pal_group.size()) - 1);
205 if (current_palette_id_ >= 0 &&
206 current_palette_id_ < (int)dungeon_pal_group.size()) {
207 auto palette = dungeon_pal_group[current_palette_id_];
210 } else {
211 DrawPaletteGrid(palette, 15);
212 dungeon_pal_group[current_palette_id_] = palette;
213 }
214 }
215
216 ImGui::Separator();
217
218 if (selected_color_index_ >= 0) {
221 } else {
223 }
224 } else {
225 ImGui::TextDisabled(tr("Select a color to edit"));
226 }
227
228 ImGui::EndGroup();
229}
230
232 if (!game_data_)
233 return;
234 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
235 int num_palettes = dungeon_pal_group.size();
236 if (num_palettes <= 0) {
237 ImGui::TextDisabled(tr("No dungeon palettes"));
238 return;
239 }
240
241 current_palette_id_ = std::clamp(current_palette_id_, 0, num_palettes - 1);
242
243 ImGui::Text(tr("Dungeon Palette:"));
244 ImGui::SameLine();
245 ImGui::SetNextItemWidth(std::min(180.0f, ImGui::GetContentRegionAvail().x));
246
247 if (ImGui::BeginCombo(
248 "##PaletteSelect",
249 absl::StrFormat("Palette %d", current_palette_id_).c_str())) {
250 for (int i = 0; i < num_palettes; i++) {
251 bool is_selected = (current_palette_id_ == i);
252 if (ImGui::Selectable(absl::StrFormat("Palette %d", i).c_str(),
253 is_selected)) {
256 }
257 if (is_selected) {
258 ImGui::SetItemDefaultFocus();
259 }
260 }
261 ImGui::EndCombo();
262 }
263
264 int requested_palette_id = current_palette_id_;
265 ImGui::TextUnformatted(tr("Palette ID"));
266 ImGui::SameLine();
267 ImGui::SetNextItemWidth(72.0f);
268 const bool palette_id_changed = gui::InputScalarDeferred(
269 "##DungeonPaletteId", ImGuiDataType_S32, &requested_palette_id, "%d", 0,
270 {reinterpret_cast<uintptr_t>(game_data_)});
271 gui::AutoRegisterLastItem("input_int", "palette_id",
272 "Dungeon main palette ID");
273 if (palette_id_changed) {
274 current_palette_id_ = std::clamp(requested_palette_id, 0, num_palettes - 1);
276 }
277}
278
280 if (!game_data_)
281 return;
282 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
283 if (dungeon_pal_group.empty()) {
284 return;
285 }
286 current_palette_id_ = std::clamp(
287 current_palette_id_, 0, static_cast<int>(dungeon_pal_group.size()) - 1);
288 ImGui::SeparatorText(
289 absl::StrFormat("Edit Color %d", selected_color_index_).c_str());
290
291 auto palette = dungeon_pal_group[current_palette_id_];
292 if (selected_color_index_ < 0 ||
293 selected_color_index_ >= static_cast<int>(palette.size())) {
295 return;
296 }
297 auto original_color = palette[selected_color_index_];
298
299 // Use standardized SnesColorEdit4 for consistency
301 "Color", &palette[selected_color_index_],
302 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_PickerHueWheel)) {
303 // Update the palette group with the modified palette
304 dungeon_pal_group[current_palette_id_] = palette;
305
306 // Update editing_color_ to match (for consistency with other parts of the
307 // widget)
310
314 }
315 }
316
317 ImGui::Text(tr("RGB (0-255): (%d, %d, %d)"),
318 static_cast<int>(editing_color_.x * 255),
319 static_cast<int>(editing_color_.y * 255),
320 static_cast<int>(editing_color_.z * 255));
321 ImGui::Text(tr("SNES BGR555: 0x%04X"), original_color.snes());
322
323 if (gui::ThemedButton("Reset to Original")) {
325 // Also reset the actual palette color
326 palette[selected_color_index_] = original_color;
327 dungeon_pal_group[current_palette_id_] = palette;
331 }
332 }
333}
334
336 ImGui::TextDisabled(
337 tr("Rows 0-1: HUD / floor / ceiling | Rows 2-7: Dungeon main"));
338 const float swatch_size = ComputeSwatchSize(/*columns=*/16, 14.0f, 28.0f);
339
340 for (int i = 0; i < 128; ++i) {
341 if (i % 16 != 0) {
342 ImGui::SameLine();
343 }
344
345 gfx::SnesColor color(0);
346 bool editable = false;
347 std::string tooltip_label = "Reserved / transparent";
348 int source_index = -1;
349
350 const auto target = ResolveDungeonRenderColorTarget(i, current_palette_id_);
351 if (target.has_value()) {
352 const auto& group = PaletteGroupForTarget(*game_data_, *target);
353 if (target->palette_index >= 0 &&
354 target->palette_index < static_cast<int>(group.size())) {
355 const auto& palette = group.palette_ref(target->palette_index);
356 if (target->color_index >= 0 &&
357 target->color_index < static_cast<int>(palette.size())) {
358 color = palette[target->color_index];
359 editable = true;
360 tooltip_label = DungeonRenderColorSourceLabel(*target);
361 source_index = target->color_index;
362 }
363 }
364 }
365
366 ImVec4 display_color = gui::ConvertSnesColorToImVec4(color);
367 ImGui::PushID(i);
368 if (!editable) {
369 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.25f);
370 }
371 const bool clicked = ImGui::ColorButton("##render_color", display_color,
372 ImGuiColorEditFlags_NoTooltip,
373 ImVec2(swatch_size, swatch_size));
374 gui::AutoRegisterLastItem("color_button",
375 absl::StrFormat("palette_swatch_%03d", i),
376 "Dungeon render palette swatch");
377 const ImVec2 swatch_min = ImGui::GetItemRectMin();
378 const ImVec2 swatch_max = ImGui::GetItemRectMax();
379 const bool double_clicked =
380 editable && ImGui::IsItemHovered() &&
381 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
382 if (!editable) {
383 ImGui::PopStyleVar();
384 }
385
386 if ((clicked || double_clicked) && editable) {
389 temp_color_ = display_color;
390 editing_color_ = display_color;
391 }
392 gfx::SnesColor dropped_color = color;
393 if (editable &&
394 AcceptImGuiColorDrop(&dropped_color, swatch_min, swatch_max)) {
395 const absl::Status status = ApplyDungeonRenderColorEdit(i, dropped_color);
396 if (status.ok()) {
401 } else {
402 LOG_ERROR("PaletteEditorWidget", "Failed to apply dropped color: %s",
403 status.message().data());
404 }
405 }
406
407 if (ImGui::IsItemHovered()) {
408 if (editable) {
409 ImGui::SetTooltip(tr("%s\nSlot %d\nSNES: 0x%04X\nRGB: (%d, %d, %d)"),
410 tooltip_label.c_str(), source_index, color.snes(),
411 static_cast<int>(display_color.x * 255),
412 static_cast<int>(display_color.y * 255),
413 static_cast<int>(display_color.z * 255));
414 } else {
415 ImGui::SetTooltip("%s", tooltip_label.c_str());
416 }
417 }
418 ImGui::PopID();
419 }
420}
421
422float PaletteEditorWidget::ComputeSwatchSize(int columns, float min_size,
423 float max_size) const {
424 const float available_width =
425 std::max(ImGui::GetContentRegionAvail().x, 1.0f);
426 const float spacing =
427 std::max(ImGui::GetStyle().ItemSpacing.x, 2.0f) * (columns - 1);
428 const float raw = (available_width - spacing) / std::max(columns, 1);
429 return std::clamp(raw, min_size, max_size);
430}
431
433 const auto target = ResolveDungeonRenderColorTarget(selected_color_index_,
435 if (!game_data_ || !target.has_value()) {
438 return;
439 }
440
441 const auto& group = PaletteGroupForTarget(*game_data_, *target);
442 if (target->palette_index < 0 ||
443 target->palette_index >= static_cast<int>(group.size())) {
446 return;
447 }
448 const auto& palette = group.palette_ref(target->palette_index);
449 if (target->color_index < 0 ||
450 target->color_index >= static_cast<int>(palette.size())) {
453 return;
454 }
455
456 ImGui::SeparatorText(absl::StrFormat("%s Color %d",
457 DungeonRenderColorSourceLabel(*target),
458 target->color_index)
459 .c_str());
460
461 const gfx::SnesColor current_color = palette[target->color_index];
462 gfx::SnesColor edited_color = current_color;
464 "Color", &edited_color,
465 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_PickerHueWheel)) {
466 const absl::Status status =
468 if (status.ok()) {
470 } else {
471 LOG_ERROR("PaletteEditorWidget", "Failed to apply palette color: %s",
472 status.message().data());
473 }
474 }
475
476 uint16_t raw_color = current_color.snes();
477 ImGui::AlignTextToFramePadding();
478 ImGui::TextUnformatted(tr("Raw BGR555"));
479 ImGui::SameLine();
480 ImGui::SetNextItemWidth(96.0f);
481 const bool raw_changed = gui::InputScalarDeferred(
482 "##SelectedDungeonPaletteBgr555", ImGuiDataType_U16, &raw_color, "%04X",
483 ImGuiInputTextFlags_CharsHexadecimal,
484 {reinterpret_cast<uintptr_t>(game_data_),
485 static_cast<uint64_t>(target->source),
486 static_cast<uint64_t>(target->palette_index),
487 static_cast<uint64_t>(target->color_index)});
488 gui::AutoRegisterLastItem("input_scalar", "selected_palette_bgr555",
489 "Selected dungeon palette raw BGR555 color");
490 if (raw_changed) {
491 raw_color &= 0x7FFF;
492 const gfx::SnesColor raw_edited_color(raw_color);
493 const absl::Status status =
495 if (status.ok()) {
497 } else {
498 LOG_ERROR("PaletteEditorWidget", "Failed to apply raw palette color: %s",
499 status.message().data());
500 }
501 }
502
503 ImGui::Text(tr("RGB (0-255): (%d, %d, %d)"),
504 static_cast<int>(editing_color_.x * 255),
505 static_cast<int>(editing_color_.y * 255),
506 static_cast<int>(editing_color_.z * 255));
507 ImGui::Text(tr("SNES BGR555: 0x%04X"), current_color.snes());
508
509 if (gui::ThemedButton("Reset to Original")) {
510 const absl::Status status =
512 if (status.ok()) {
514 PaletteManagerGroupName(target->source), target->palette_index,
515 target->color_index);
517 } else {
518 LOG_ERROR("PaletteEditorWidget", "Failed to reset palette color: %s",
519 status.message().data());
520 }
521 }
522}
523
524// --- Modal/Popup Methods (from feature-rich widget) ---
525
527 const std::string& title) {
528 if (ImGui::BeginPopupModal(title.c_str(), nullptr,
529 ImGuiWindowFlags_AlwaysAutoResize)) {
530 ImGui::Text(tr("Enhanced Palette Editor"));
531 ImGui::Separator();
532
533 DrawPaletteGrid(palette);
534 ImGui::Separator();
535
536 if (ImGui::CollapsingHeader(tr("Palette Analysis"))) {
537 DrawPaletteAnalysis(palette);
538 }
539
540 if (ImGui::CollapsingHeader(tr("ROM Palette Manager")) && rom_) {
542
543 if (gui::PrimaryButton("Apply ROM Palette") &&
544 !rom_palette_groups_.empty()) {
546 static_cast<int>(rom_palette_groups_.size())) {
548 }
549 }
550 }
551
552 ImGui::Separator();
553
554 if (gui::ThemedButton("Save Backup")) {
555 SavePaletteBackup(palette);
556 }
557 ImGui::SameLine();
558 if (gui::ThemedButton("Restore Backup")) {
559 RestorePaletteBackup(palette);
560 }
561 ImGui::SameLine();
562 if (gui::ThemedButton("Close")) {
563 ImGui::CloseCurrentPopup();
564 }
565
566 ImGui::EndPopup();
567 }
568}
569
572 return;
573
574 if (ImGui::Begin("ROM Palette Manager", &show_rom_manager_)) {
575 if (!rom_) {
576 ImGui::Text(tr("No ROM loaded"));
577 ImGui::End();
578 return;
579 }
580
583 }
584
586
587 if (current_group_index_ < static_cast<int>(rom_palette_groups_.size())) {
588 ImGui::Separator();
589 ImGui::Text(tr("Preview of %s:"),
591
592 const auto& preview_palette = rom_palette_groups_[current_group_index_];
593 DrawPaletteGrid(const_cast<gfx::SnesPalette&>(preview_palette));
594 DrawPaletteAnalysis(preview_palette);
595 }
596 }
597 ImGui::End();
598}
599
601 const std::string& title) {
603 return;
604
605 if (ImGui::Begin(title.c_str(), &show_color_analysis_)) {
606 ImGui::Text(tr("Bitmap Color Analysis"));
607 ImGui::Separator();
608
609 if (!bitmap.is_active()) {
610 ImGui::Text(tr("Bitmap is not active"));
611 ImGui::End();
612 return;
613 }
614
615 std::map<uint8_t, int> pixel_counts;
616 const auto& data = bitmap.vector();
617
618 for (uint8_t pixel : data) {
619 uint8_t palette_index = pixel & 0x0F;
620 pixel_counts[palette_index]++;
621 }
622
623 ImGui::Text(tr("Bitmap Size: %d x %d (%zu pixels)"), bitmap.width(),
624 bitmap.height(), data.size());
625
626 ImGui::Separator();
627 ImGui::Text(tr("Pixel Distribution:"));
628
629 int total_pixels = static_cast<int>(data.size());
630 plotting::PlotStyleScope plot_style(
631 gui::ThemeManager::Get().GetCurrentTheme());
632 plotting::PlotConfig plot_cfg{.id = "Pixel Distribution",
633 .x_label = "Palette Index",
634 .y_label = "Count",
635 .flags = ImPlotFlags_NoBoxSelect,
636 .x_axis_flags = ImPlotAxisFlags_AutoFit,
637 .y_axis_flags = ImPlotAxisFlags_AutoFit};
638 std::vector<double> x;
639 std::vector<double> y;
640 x.reserve(pixel_counts.size());
641 y.reserve(pixel_counts.size());
642 for (const auto& [index, count] : pixel_counts) {
643 x.push_back(static_cast<double>(index));
644 y.push_back(static_cast<double>(count));
645 }
646 plotting::PlotGuard plot(plot_cfg);
647 if (plot && !x.empty()) {
648 ImPlot::PlotBars("Usage", x.data(), y.data(), static_cast<int>(x.size()),
649 0.67, 0.0, ImPlotBarsFlags_None);
650 }
651 }
652 ImGui::End();
653}
654
656 int palette_index) {
657 if (!bitmap || !rom_palettes_loaded_ || group_index < 0 ||
658 group_index >= static_cast<int>(rom_palette_groups_.size())) {
659 return false;
660 }
661
662 try {
663 const auto& selected_palette = rom_palette_groups_[group_index];
664 SavePaletteBackup(bitmap->palette());
665
666 if (palette_index >= 0 && palette_index < 8) {
667 bitmap->SetPaletteWithTransparent(selected_palette, palette_index);
668 } else {
669 bitmap->SetPalette(selected_palette);
670 }
671
674
675 current_group_index_ = group_index;
676 current_palette_index_ = palette_index;
677 return true;
678 } catch (const std::exception& e) {
679 return false;
680 }
681}
682
685 current_group_index_ >= static_cast<int>(rom_palette_groups_.size())) {
686 return nullptr;
687 }
689}
690
694
696 if (backup_palette_.size() == 0) {
697 return false;
698 }
699 palette = backup_palette_;
700 return true;
701}
702
703// Unified grid drawing function
705 const float swatch_size =
706 ComputeSwatchSize(cols, /*min_size=*/16.0f, /*max_size=*/30.0f);
707 for (int i = 0; i < static_cast<int>(palette.size()); i++) {
708 if (i % cols != 0)
709 ImGui::SameLine();
710
711 auto color = palette[i];
712 ImVec4 display_color = gui::ConvertSnesColorToImVec4(color);
713
714 ImGui::PushID(i);
715 const bool clicked = ImGui::ColorButton("##color", display_color,
716 ImGuiColorEditFlags_NoTooltip,
717 ImVec2(swatch_size, swatch_size));
718 const ImVec2 swatch_min = ImGui::GetItemRectMin();
719 const ImVec2 swatch_max = ImGui::GetItemRectMax();
720 const bool double_clicked =
721 ImGui::IsItemHovered() &&
722 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
723 if (clicked || double_clicked) {
726 temp_color_ = display_color;
727 editing_color_ = display_color;
728 }
729 if (AcceptImGuiColorDrop(&palette[i], swatch_min, swatch_max)) {
737 }
738 }
739
740 if (ImGui::BeginPopupContextItem()) {
741 ImGui::Text(tr("Color %d (0x%04X)"), i, color.snes());
742 ImGui::Separator();
743 if (ImGui::MenuItem(tr("Edit Color"))) {
746 temp_color_ = display_color;
747 editing_color_ = display_color;
748 }
749 if (ImGui::MenuItem(tr("Reset to Black"))) {
750 palette[i] = gfx::SnesColor(0);
754 }
755 }
756 ImGui::EndPopup();
757 }
758
759 if (ImGui::IsItemHovered()) {
760 ImGui::SetTooltip(tr("Color %d\nSNES: 0x%04X\nRGB: (%d, %d, %d)"), i,
761 color.snes(), static_cast<int>(display_color.x * 255),
762 static_cast<int>(display_color.y * 255),
763 static_cast<int>(display_color.z * 255));
764 }
765
766 ImGui::PopID();
767 }
768
769 if (editing_color_index_ >= 0) {
770 // Use a unique ID for the popup to prevent conflicts
771 std::string popup_id =
772 gui::MakePopupIdWithInstance("PaletteEditorWidget", "EditColor", this);
773
774 ImGui::OpenPopup(popup_id.c_str());
775 if (ImGui::BeginPopupModal(popup_id.c_str(), nullptr,
776 ImGuiWindowFlags_AlwaysAutoResize)) {
777 ImGui::Text(tr("Editing Color %d"), editing_color_index_);
778 if (ImGui::ColorEdit4(
779 "Color", &temp_color_.x,
780 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_DisplayRGB)) {
781 auto new_snes_color = gfx::SnesColor(temp_color_);
782 palette[editing_color_index_] = new_snes_color;
785 }
789 }
790 }
791 if (gui::PrimaryButton("Apply")) {
793 ImGui::CloseCurrentPopup();
794 }
795 ImGui::SameLine();
796 if (gui::ThemedButton("Cancel")) {
798 ImGui::CloseCurrentPopup();
799 }
800 ImGui::EndPopup();
801 }
802 }
803}
804
808 }
809
810 if (rom_palette_groups_.empty()) {
811 ImGui::Text(tr("No ROM palettes available"));
812 return;
813 }
814
815 ImGui::Text(tr("Palette Group:"));
816 if (ImGui::Combo("##PaletteGroup", &current_group_index_,
817 RomPaletteGroupNameGetter, &palette_group_names_,
818 static_cast<int>(palette_group_names_.size()))) {}
819
820 ImGui::Text(tr("Palette Index:"));
821 ImGui::SliderInt("##PaletteIndex", &current_palette_index_, 0, 7, "%d");
822
823 if (current_group_index_ < static_cast<int>(rom_palette_groups_.size())) {
824 ImGui::Text(tr("Preview:"));
825 const auto& preview_palette = rom_palette_groups_[current_group_index_];
826 for (int i = 0; i < 8 && i < static_cast<int>(preview_palette.size());
827 i++) {
828 if (i > 0)
829 ImGui::SameLine();
830 auto color = preview_palette[i];
831 ImVec4 display_color = gui::ConvertSnesColorToImVec4(color);
832 ImGui::ColorButton(("##preview" + std::to_string(i)).c_str(),
833 display_color, ImGuiColorEditFlags_NoTooltip,
834 ImVec2(20, 20));
835 }
836 }
837}
838
840 int color_index) {
841 ImVec4 rgba = gui::ConvertSnesColorToImVec4(color);
842
843 ImGui::PushID(color_index);
844
845 if (ImGui::ColorEdit4(
846 "##color_edit", &rgba.x,
847 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_DisplayRGB)) {
848 color = gfx::SnesColor(rgba);
849 }
850
851 ImGui::Text(tr("SNES Color: 0x%04X"), color.snes());
852
853 int r = (color.snes() & 0x1F);
854 int g = (color.snes() >> 5) & 0x1F;
855 int b = (color.snes() >> 10) & 0x1F;
856
857 if (ImGui::SliderInt(tr("Red"), &r, 0, 31)) {
858 uint16_t new_color = (color.snes() & 0xFFE0) | (r & 0x1F);
859 color = gfx::SnesColor(new_color);
860 }
861 if (ImGui::SliderInt(tr("Green"), &g, 0, 31)) {
862 uint16_t new_color = (color.snes() & 0xFC1F) | ((g & 0x1F) << 5);
863 color = gfx::SnesColor(new_color);
864 }
865 if (ImGui::SliderInt(tr("Blue"), &b, 0, 31)) {
866 uint16_t new_color = (color.snes() & 0x83FF) | ((b & 0x1F) << 10);
867 color = gfx::SnesColor(new_color);
868 }
869
870 ImGui::PopID();
871}
872
874 ImGui::Text(tr("Palette Information:"));
875 ImGui::Text(tr("Size: %zu colors"), palette.size());
876
877 std::map<uint16_t, int> color_frequency;
878 for (int i = 0; i < static_cast<int>(palette.size()); i++) {
879 color_frequency[palette[i].snes()]++;
880 }
881
882 ImGui::Text(tr("Unique Colors: %zu"), color_frequency.size());
883
884 if (color_frequency.size() < palette.size()) {
885 ImGui::TextColored(ImVec4(1, 1, 0, 1),
886 tr("Warning: Duplicate colors detected!"));
887 if (ImGui::TreeNode("Duplicate Colors")) {
888 for (const auto& [snes_color, count] : color_frequency) {
889 if (count > 1) {
890 ImVec4 display_color =
892 ImGui::ColorButton(("##dup" + std::to_string(snes_color)).c_str(),
893 display_color, ImGuiColorEditFlags_NoTooltip,
894 ImVec2(16, 16));
895 ImGui::SameLine();
896 ImGui::Text(tr("0x%04X appears %d times"), snes_color, count);
897 }
898 }
899 ImGui::TreePop();
900 }
901 }
902
903 // Visual histogram of color reuse
904 {
905 plotting::PlotStyleScope plot_style(
906 gui::ThemeManager::Get().GetCurrentTheme());
907 plotting::PlotConfig plot_cfg{.id = "Palette Color Frequency",
908 .x_label = "Color Index",
909 .y_label = "Count",
910 .flags = ImPlotFlags_NoBoxSelect,
911 .x_axis_flags = ImPlotAxisFlags_AutoFit,
912 .y_axis_flags = ImPlotAxisFlags_AutoFit};
913 std::vector<double> x;
914 std::vector<double> y;
915 x.reserve(color_frequency.size());
916 y.reserve(color_frequency.size());
917 for (const auto& [snes_color, count] : color_frequency) {
918 x.push_back(static_cast<double>(snes_color));
919 y.push_back(static_cast<double>(count));
920 }
921 plotting::PlotGuard plot(plot_cfg);
922 if (plot && !x.empty()) {
923 ImPlot::PlotBars("Count", x.data(), y.data(), static_cast<int>(x.size()),
924 0.5, 0.0, ImPlotBarsFlags_None);
925 }
926 }
927
928 float total_brightness = 0.0f;
929 float min_brightness = 1.0f;
930 float max_brightness = 0.0f;
931
932 for (int i = 0; i < static_cast<int>(palette.size()); i++) {
933 ImVec4 color = gui::ConvertSnesColorToImVec4(palette[i]);
934 float brightness = (color.x + color.y + color.z) / 3.0f;
935 total_brightness += brightness;
936 min_brightness = std::min(min_brightness, brightness);
937 max_brightness = std::max(max_brightness, brightness);
938 }
939
940 float avg_brightness = total_brightness / palette.size();
941
942 ImGui::Separator();
943 ImGui::Text(tr("Brightness Analysis:"));
944 ImGui::Text(tr("Average: %.2f"), avg_brightness);
945 ImGui::Text(tr("Range: %.2f - %.2f"), min_brightness, max_brightness);
946
947 ImGui::Text(tr("Brightness Distribution:"));
948 ImGui::ProgressBar(avg_brightness, ImVec2(-1, 0), "Avg");
949}
950
953 return;
954
955 try {
956 const auto& palette_groups = game_data_->palette_groups;
958 palette_group_names_.clear();
959
960 if (palette_groups.overworld_main.size() > 0) {
961 rom_palette_groups_.push_back(palette_groups.overworld_main[0]);
962 palette_group_names_.push_back("Overworld Main");
963 }
964 if (palette_groups.overworld_aux.size() > 0) {
965 rom_palette_groups_.push_back(palette_groups.overworld_aux[0]);
966 palette_group_names_.push_back("Overworld Aux");
967 }
968 if (palette_groups.overworld_animated.size() > 0) {
969 rom_palette_groups_.push_back(palette_groups.overworld_animated[0]);
970 palette_group_names_.push_back("Overworld Animated");
971 }
972 if (palette_groups.dungeon_main.size() > 0) {
973 rom_palette_groups_.push_back(palette_groups.dungeon_main[0]);
974 palette_group_names_.push_back("Dungeon Main");
975 }
976 if (palette_groups.global_sprites.size() > 0) {
977 rom_palette_groups_.push_back(palette_groups.global_sprites[0]);
978 palette_group_names_.push_back("Global Sprites");
979 }
980 if (palette_groups.armors.size() > 0) {
981 rom_palette_groups_.push_back(palette_groups.armors[0]);
982 palette_group_names_.push_back("Armor");
983 }
984 if (palette_groups.swords.size() > 0) {
985 rom_palette_groups_.push_back(palette_groups.swords[0]);
986 palette_group_names_.push_back("Swords");
987 }
988
990 } catch (const std::exception& e) {
991 LOG_ERROR("Enhanced Palette Editor", "Failed to load ROM palettes");
992 }
993}
994
995} // namespace gui
996} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
static Arena & Get()
Definition arena.cc:21
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
const SnesPalette & palette() const
Definition bitmap.h:389
const std::vector< uint8_t > & vector() const
Definition bitmap.h:402
bool is_active() const
Definition bitmap.h:405
int height() const
Definition bitmap.h:395
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:394
int width() const
Definition bitmap.h:394
void SetPaletteWithTransparent(const SnesPalette &palette, size_t index, int length=7)
Set the palette with a transparent color.
Definition bitmap.cc:466
static PaletteManager & Get()
Get the singleton instance.
SnesColor GetColor(const std::string &group_name, int palette_index, int color_index) const
Get a color from a palette.
SNES Color container.
Definition snes_color.h:110
constexpr uint16_t snes() const
Get SNES 15-bit color.
Definition snes_color.h:193
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
std::vector< gfx::SnesPalette > rom_palette_groups_
bool RestorePaletteBackup(gfx::SnesPalette &palette)
void ShowPaletteEditor(gfx::SnesPalette &palette, const std::string &title="Palette Editor")
bool ApplyROMPalette(gfx::Bitmap *bitmap, int group_index, int palette_index)
const gfx::SnesPalette * GetSelectedROMPalette() const
void SavePaletteBackup(const gfx::SnesPalette &palette)
std::function< void(DungeonPaletteChange)> on_palette_changed_
void ShowColorAnalysis(const gfx::Bitmap &bitmap, const std::string &title="Color Analysis")
void Initialize(zelda3::GameData *game_data)
absl::Status ApplyDungeonRenderColorEdit(int display_index, std::optional< gfx::SnesColor > color)
void DrawColorEditControls(gfx::SnesColor &color, int color_index)
void DrawPaletteAnalysis(const gfx::SnesPalette &palette)
float ComputeSwatchSize(int columns, float min_size, float max_size) const
std::vector< std::string > palette_group_names_
void DrawPaletteGrid(gfx::SnesPalette &palette, int cols=15)
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
#define LOG_ERROR(category, format,...)
Definition log.h:109
const char * RomPaletteGroupNameGetter(void *user_data, int idx)
std::optional< DungeonRenderColorTarget > ResolveDungeonRenderColorTarget(int display_index, int dungeon_palette_id)
const gfx::PaletteGroup & PaletteGroupForTarget(const zelda3::GameData &game_data, const DungeonRenderColorTarget &target)
const char * PaletteManagerGroupName(DungeonRenderPaletteSource source)
std::string DungeonRenderColorSourceLabel(const DungeonRenderColorTarget &target)
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:134
bool PrimaryButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a primary action button (accented color).
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
std::string MakePopupIdWithInstance(const std::string &editor_name, const std::string &popup_name, const void *instance)
Generate popup ID with instance pointer for guaranteed uniqueness.
Definition popup_id.h:45
bool ThemedButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a standard text button with theme colors.
ImVec4 ConvertSnesColorToImVec4(const gfx::SnesColor &color)
Convert SnesColor to standard ImVec4 for display.
Definition color.cc:23
bool InputScalarDeferred(const char *label, ImGuiDataType data_type, void *data, const char *format, ImGuiInputTextFlags flags, InputScalarTargetIdentity target_identity)
Definition input.cc:388
IMGUI_API bool SnesColorEdit4(absl::string_view label, gfx::SnesColor *color, ImGuiColorEditFlags flags)
Definition color.cc:58
SNES color in 15-bit RGB format (BGR555)
Represents a group of palettes.
gfx::PaletteGroupMap palette_groups
Definition game_data.h:92
Automatic widget registration helpers for ImGui Test Engine integration.