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"
12#include "app/gui/core/color.h"
13#include "app/gui/core/icons.h"
14#include "app/gui/core/input.h"
19#include "util/log.h"
20
21#include <cstring>
22#include <string>
23#include <vector>
24
25namespace yaze {
26namespace gui {
27
28namespace {
29
35
36std::optional<DungeonRenderColorTarget> ResolveDungeonRenderColorTarget(
37 int display_index, int dungeon_palette_id) {
38 if (display_index < 0 || display_index >= 128) {
39 return std::nullopt;
40 }
41 if (display_index < 32) {
43 display_index};
44 }
45
46 const int local_index = display_index - 32;
47 const int row = local_index / 16;
48 const int col = local_index % 16;
49 if (row >= 6 || col == 0) {
50 return std::nullopt;
51 }
52
54 dungeon_palette_id, row * 15 + (col - 1)};
55}
56
58 return source == DungeonRenderPaletteSource::kHud ? "hud" : "dungeon_main";
59}
60
62 const zelda3::GameData& game_data, const DungeonRenderColorTarget& target) {
64 ? game_data.palette_groups.hud
65 : game_data.palette_groups.dungeon_main;
66}
67
69 const DungeonRenderColorTarget& target) {
71 return "HUD / floor-ceiling";
72 }
73 return absl::StrFormat("Dungeon row %d", target.color_index / 15 + 2);
74}
75
76const char* RomPaletteGroupNameGetter(void* user_data, int idx) {
77 auto* names = static_cast<std::vector<std::string>*>(user_data);
78 if (!names || idx < 0 || idx >= static_cast<int>(names->size())) {
79 return nullptr;
80 }
81 return (*names)[idx].c_str();
82}
83
84void DrawColorDropTargetOutline(ImVec2 min, ImVec2 max) {
85 const auto& theme = ThemeManager::Get().GetCurrentTheme();
86 ImGui::GetForegroundDrawList()->AddRect(
87 min, max, ImGui::GetColorU32(ConvertColorToImVec4(theme.accent)), 0.0f, 0,
88 2.0f);
89}
90
91bool AcceptImGuiColorDrop(gfx::SnesColor* color, ImVec2 min, ImVec2 max) {
92 if (!color || !ImGui::BeginDragDropTarget()) {
93 return false;
94 }
95
96 bool changed = false;
97 constexpr ImGuiDragDropFlags kFlags =
98 ImGuiDragDropFlags_AcceptBeforeDelivery |
99 ImGuiDragDropFlags_AcceptNoDrawDefaultRect |
100 ImGuiDragDropFlags_AcceptNoPreviewTooltip;
101 auto accept = [&](const char* type, size_t components) {
102 if (changed) {
103 return;
104 }
105 if (const ImGuiPayload* payload =
106 ImGui::AcceptDragDropPayload(type, kFlags)) {
107 DrawColorDropTargetOutline(min, max);
108 if (payload->IsDelivery()) {
109 ImVec4 dropped(0, 0, 0, 1);
110 std::memcpy(&dropped.x, payload->Data, sizeof(float) * components);
111 if (components == 3) {
112 dropped.w = 1.0f;
113 }
114 *color = gfx::SnesColor(dropped);
115 changed = true;
116 }
117 }
118 };
119
120 accept(IMGUI_PAYLOAD_TYPE_COLOR_3F, 3);
121 accept(IMGUI_PAYLOAD_TYPE_COLOR_4F, 4);
122 ImGui::EndDragDropTarget();
123 return changed;
124}
125
126} // namespace
127
128int ResolveDungeonRenderPaletteColumns(float available_width,
129 float min_swatch_size,
130 float item_spacing) {
131 const float safe_width = std::max(available_width, 1.0f);
132 const float safe_swatch = std::max(min_swatch_size, 1.0f);
133 const float safe_spacing = std::max(item_spacing, 0.0f);
134 for (const int columns : {16, 8, 4, 2, 1}) {
135 const float required_width =
136 safe_swatch * columns + safe_spacing * (columns - 1);
137 if (safe_width >= required_width) {
138 return columns;
139 }
140 }
141 return 1;
142}
143
144// Merged implementation from PaletteWidget and PaletteEditorWidget
145
147 game_data_ = game_data;
148 rom_ = nullptr;
149 rom_palettes_loaded_ = false;
150 if (game_data_) {
152 }
155}
156
158 rom_ = rom;
159 game_data_ = nullptr;
160 rom_palettes_loaded_ = false;
161 // Legacy mode - no palette loading without game_data
164}
165
167 int display_index, std::optional<gfx::SnesColor> color) {
168 if (!game_data_) {
169 return absl::FailedPreconditionError("GameData not loaded");
170 }
171
172 const auto target =
173 ResolveDungeonRenderColorTarget(display_index, current_palette_id_);
174 if (!target.has_value()) {
175 return absl::InvalidArgumentError(absl::StrFormat(
176 "Dungeon render palette slot %d is reserved or out of range",
177 display_index));
178 }
179
180 auto& palette_manager = gfx::PaletteManager::Get();
181 if (!palette_manager.IsManaging(game_data_)) {
182 return absl::FailedPreconditionError(
183 "PaletteManager is bound to a different ROM session");
184 }
185 const char* group_name = PaletteManagerGroupName(target->source);
186 absl::Status status =
187 color.has_value()
188 ? palette_manager.SetColor(group_name, target->palette_index,
189 target->color_index, *color)
190 : palette_manager.ResetColor(group_name, target->palette_index,
191 target->color_index);
192 if (!status.ok()) {
193 return status;
194 }
195
197 on_palette_changed_({current_palette_id_, target->source});
198 }
199 return absl::OkStatus();
200}
201
202// --- Embedded Draw Method (from simple editor) ---
204 if (!game_data_) {
205 ImGui::TextColored(ImVec4(1, 0, 0, 1), tr("GameData not loaded"));
206 return;
207 }
208
209 ImGui::BeginGroup();
210
212 ImGui::Separator();
213
214 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
215 if (dungeon_pal_group.empty()) {
216 ImGui::TextDisabled(tr("Dungeon palettes unavailable"));
217 ImGui::EndGroup();
218 return;
219 }
220
221 current_palette_id_ = std::clamp(
222 current_palette_id_, 0, static_cast<int>(dungeon_pal_group.size()) - 1);
223 if (current_palette_id_ >= 0 &&
224 current_palette_id_ < (int)dungeon_pal_group.size()) {
225 auto palette = dungeon_pal_group[current_palette_id_];
228 } else {
229 DrawPaletteGrid(palette, 15);
230 dungeon_pal_group[current_palette_id_] = palette;
231 }
232 }
233
234 ImGui::Separator();
235
236 if (selected_color_index_ >= 0) {
239 } else {
241 }
242 } else {
243 ImGui::TextDisabled(tr("Select a color to edit"));
244 }
245
246 ImGui::EndGroup();
247}
248
250 if (!game_data_)
251 return;
252 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
253 int num_palettes = dungeon_pal_group.size();
254 if (num_palettes <= 0) {
255 ImGui::TextDisabled(tr("No dungeon palettes"));
256 return;
257 }
258
259 current_palette_id_ = std::clamp(current_palette_id_, 0, num_palettes - 1);
260
261 ImGui::Text(tr("Dungeon Palette:"));
262 ImGui::SameLine();
263 ImGui::SetNextItemWidth(std::min(180.0f, ImGui::GetContentRegionAvail().x));
264
265 if (ImGui::BeginCombo(
266 "##PaletteSelect",
267 absl::StrFormat("Palette %d", current_palette_id_).c_str())) {
268 for (int i = 0; i < num_palettes; i++) {
269 bool is_selected = (current_palette_id_ == i);
270 if (ImGui::Selectable(absl::StrFormat("Palette %d", i).c_str(),
271 is_selected)) {
274 }
275 if (is_selected) {
276 ImGui::SetItemDefaultFocus();
277 }
278 }
279 ImGui::EndCombo();
280 }
281
282 int requested_palette_id = current_palette_id_;
283 ImGui::TextUnformatted(tr("Palette ID"));
284 ImGui::SameLine();
285 ImGui::SetNextItemWidth(72.0f);
286 const bool palette_id_changed = gui::InputScalarDeferred(
287 "##DungeonPaletteId", ImGuiDataType_S32, &requested_palette_id, "%d", 0,
288 {reinterpret_cast<uintptr_t>(game_data_)});
289 gui::AutoRegisterLastItem("input_int", "palette_id",
290 "Dungeon main palette ID");
291 if (palette_id_changed) {
292 current_palette_id_ = std::clamp(requested_palette_id, 0, num_palettes - 1);
294 }
295}
296
298 if (!game_data_)
299 return;
300 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
301 if (dungeon_pal_group.empty()) {
302 return;
303 }
304 current_palette_id_ = std::clamp(
305 current_palette_id_, 0, static_cast<int>(dungeon_pal_group.size()) - 1);
306 ImGui::SeparatorText(
307 absl::StrFormat("Edit Color %d", selected_color_index_).c_str());
308
309 auto palette = dungeon_pal_group[current_palette_id_];
310 if (selected_color_index_ < 0 ||
311 selected_color_index_ >= static_cast<int>(palette.size())) {
313 return;
314 }
315 auto original_color = palette[selected_color_index_];
316
317 // Use standardized SnesColorEdit4 for consistency
319 "Color", &palette[selected_color_index_],
320 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_PickerHueWheel)) {
321 // Update the palette group with the modified palette
322 dungeon_pal_group[current_palette_id_] = palette;
323
324 // Update editing_color_ to match (for consistency with other parts of the
325 // widget)
328
332 }
333 }
334
335 ImGui::Text(tr("RGB (0-255): (%d, %d, %d)"),
336 static_cast<int>(editing_color_.x * 255),
337 static_cast<int>(editing_color_.y * 255),
338 static_cast<int>(editing_color_.z * 255));
339 ImGui::Text(tr("SNES BGR555: 0x%04X"), original_color.snes());
340
341 if (gui::ThemedButton("Reset to Original")) {
343 // Also reset the actual palette color
344 palette[selected_color_index_] = original_color;
345 dungeon_pal_group[current_palette_id_] = palette;
349 }
350 }
351}
352
354 ImGui::TextDisabled(tr("Slots 00-1F: HUD / floor / ceiling"));
355 ImGui::TextDisabled(tr("Slots 20-7F: Dungeon main"));
356 constexpr float kSwatchSpacing = 2.0f;
357 const ImVec2 previous_spacing = ImGui::GetStyle().ItemSpacing;
358 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
359 ImVec2(kSwatchSpacing, previous_spacing.y));
360 const int columns = ResolveDungeonRenderPaletteColumns(
361 ImGui::GetContentRegionAvail().x, /*min_swatch_size=*/14.0f,
362 kSwatchSpacing);
363 const float swatch_size = ComputeSwatchSize(columns, 14.0f, 28.0f);
364
365 for (int i = 0; i < 128; ++i) {
366 if (i % columns != 0) {
367 ImGui::SameLine();
368 }
369
370 gfx::SnesColor color(0);
371 bool editable = false;
372 std::string tooltip_label = "Reserved / transparent";
373 int source_index = -1;
374
375 const auto target = ResolveDungeonRenderColorTarget(i, current_palette_id_);
376 if (target.has_value()) {
377 const auto& group = PaletteGroupForTarget(*game_data_, *target);
378 if (target->palette_index >= 0 &&
379 target->palette_index < static_cast<int>(group.size())) {
380 const auto& palette = group.palette_ref(target->palette_index);
381 if (target->color_index >= 0 &&
382 target->color_index < static_cast<int>(palette.size())) {
383 color = palette[target->color_index];
384 editable = true;
385 tooltip_label = DungeonRenderColorSourceLabel(*target);
386 source_index = target->color_index;
387 }
388 }
389 }
390
391 ImVec4 display_color = gui::ConvertSnesColorToImVec4(color);
392 ImGui::PushID(i);
393 if (!editable) {
394 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.25f);
395 }
396 const bool clicked = ImGui::ColorButton("##render_color", display_color,
397 ImGuiColorEditFlags_NoTooltip,
398 ImVec2(swatch_size, swatch_size));
399 gui::AutoRegisterLastItem("color_button",
400 absl::StrFormat("palette_swatch_%03d", i),
401 "Dungeon render palette swatch");
402 const ImVec2 swatch_min = ImGui::GetItemRectMin();
403 const ImVec2 swatch_max = ImGui::GetItemRectMax();
404 const bool double_clicked =
405 editable && ImGui::IsItemHovered() &&
406 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
407 if (!editable) {
408 ImGui::PopStyleVar();
409 }
410
411 if ((clicked || double_clicked) && editable) {
414 temp_color_ = display_color;
415 editing_color_ = display_color;
416 }
417 gfx::SnesColor dropped_color = color;
418 if (editable &&
419 AcceptImGuiColorDrop(&dropped_color, swatch_min, swatch_max)) {
420 const absl::Status status = ApplyDungeonRenderColorEdit(i, dropped_color);
421 if (status.ok()) {
426 } else {
427 LOG_ERROR("PaletteEditorWidget", "Failed to apply dropped color: %s",
428 status.message().data());
429 }
430 }
431
432 if (ImGui::IsItemHovered()) {
433 if (editable) {
434 ImGui::SetTooltip(tr("%s\nSlot %d\nSNES: 0x%04X\nRGB: (%d, %d, %d)"),
435 tooltip_label.c_str(), source_index, color.snes(),
436 static_cast<int>(display_color.x * 255),
437 static_cast<int>(display_color.y * 255),
438 static_cast<int>(display_color.z * 255));
439 } else {
440 ImGui::SetTooltip("%s", tooltip_label.c_str());
441 }
442 }
443 ImGui::PopID();
444 }
445 ImGui::PopStyleVar();
446}
447
448float PaletteEditorWidget::ComputeSwatchSize(int columns, float min_size,
449 float max_size) const {
450 const float available_width =
451 std::max(ImGui::GetContentRegionAvail().x, 1.0f);
452 const float spacing =
453 std::max(ImGui::GetStyle().ItemSpacing.x, 2.0f) * (columns - 1);
454 const float raw = (available_width - spacing) / std::max(columns, 1);
455 return std::clamp(raw, min_size, max_size);
456}
457
459 const auto target = ResolveDungeonRenderColorTarget(selected_color_index_,
461 if (!game_data_ || !target.has_value()) {
464 return;
465 }
466
467 const auto& group = PaletteGroupForTarget(*game_data_, *target);
468 if (target->palette_index < 0 ||
469 target->palette_index >= static_cast<int>(group.size())) {
472 return;
473 }
474 const auto& palette = group.palette_ref(target->palette_index);
475 if (target->color_index < 0 ||
476 target->color_index >= static_cast<int>(palette.size())) {
479 return;
480 }
481
482 ImGui::SeparatorText(absl::StrFormat("%s Color %d",
483 DungeonRenderColorSourceLabel(*target),
484 target->color_index)
485 .c_str());
486
487 const gfx::SnesColor current_color = palette[target->color_index];
488 gfx::SnesColor edited_color = current_color;
490 "Color", &edited_color,
491 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_PickerHueWheel)) {
492 const absl::Status status =
494 if (status.ok()) {
496 } else {
497 LOG_ERROR("PaletteEditorWidget", "Failed to apply palette color: %s",
498 status.message().data());
499 }
500 }
501
502 uint16_t raw_color = current_color.snes();
503 ImGui::AlignTextToFramePadding();
504 ImGui::TextUnformatted(tr("Raw BGR555"));
505 ImGui::SameLine();
506 ImGui::SetNextItemWidth(96.0f);
507 const bool raw_changed = gui::InputScalarDeferred(
508 "##SelectedDungeonPaletteBgr555", ImGuiDataType_U16, &raw_color, "%04X",
509 ImGuiInputTextFlags_CharsHexadecimal,
510 {reinterpret_cast<uintptr_t>(game_data_),
511 static_cast<uint64_t>(target->source),
512 static_cast<uint64_t>(target->palette_index),
513 static_cast<uint64_t>(target->color_index)});
514 gui::AutoRegisterLastItem("input_scalar", "selected_palette_bgr555",
515 "Selected dungeon palette raw BGR555 color");
516 if (raw_changed) {
517 raw_color &= 0x7FFF;
518 const gfx::SnesColor raw_edited_color(raw_color);
519 const absl::Status status =
521 if (status.ok()) {
523 } else {
524 LOG_ERROR("PaletteEditorWidget", "Failed to apply raw palette color: %s",
525 status.message().data());
526 }
527 }
528
529 ImGui::Text(tr("RGB (0-255): (%d, %d, %d)"),
530 static_cast<int>(editing_color_.x * 255),
531 static_cast<int>(editing_color_.y * 255),
532 static_cast<int>(editing_color_.z * 255));
533 ImGui::Text(tr("SNES BGR555: 0x%04X"), current_color.snes());
534
535 if (gui::ThemedButton("Reset to Original")) {
536 const absl::Status status =
538 if (status.ok()) {
540 PaletteManagerGroupName(target->source), target->palette_index,
541 target->color_index);
543 } else {
544 LOG_ERROR("PaletteEditorWidget", "Failed to reset palette color: %s",
545 status.message().data());
546 }
547 }
548}
549
550// --- Modal/Popup Methods (from feature-rich widget) ---
551
553 const std::string& title) {
554 if (ImGui::BeginPopupModal(title.c_str(), nullptr,
555 ImGuiWindowFlags_AlwaysAutoResize)) {
556 ImGui::Text(tr("Enhanced Palette Editor"));
557 ImGui::Separator();
558
559 DrawPaletteGrid(palette);
560 ImGui::Separator();
561
562 if (ImGui::CollapsingHeader(tr("Palette Analysis"))) {
563 DrawPaletteAnalysis(palette);
564 }
565
566 if (ImGui::CollapsingHeader(tr("ROM Palette Manager")) && rom_) {
568
569 if (gui::PrimaryButton("Apply ROM Palette") &&
570 !rom_palette_groups_.empty()) {
572 static_cast<int>(rom_palette_groups_.size())) {
574 }
575 }
576 }
577
578 ImGui::Separator();
579
580 if (gui::ThemedButton("Save Backup")) {
581 SavePaletteBackup(palette);
582 }
583 ImGui::SameLine();
584 if (gui::ThemedButton("Restore Backup")) {
585 RestorePaletteBackup(palette);
586 }
587 ImGui::SameLine();
588 if (gui::ThemedButton("Close")) {
589 ImGui::CloseCurrentPopup();
590 }
591
592 ImGui::EndPopup();
593 }
594}
595
598 return;
599
600 if (ImGui::Begin("ROM Palette Manager", &show_rom_manager_)) {
601 if (!rom_) {
602 ImGui::Text(tr("No ROM loaded"));
603 ImGui::End();
604 return;
605 }
606
609 }
610
612
613 if (current_group_index_ < static_cast<int>(rom_palette_groups_.size())) {
614 ImGui::Separator();
615 ImGui::Text(tr("Preview of %s:"),
617
618 const auto& preview_palette = rom_palette_groups_[current_group_index_];
619 DrawPaletteGrid(const_cast<gfx::SnesPalette&>(preview_palette));
620 DrawPaletteAnalysis(preview_palette);
621 }
622 }
623 ImGui::End();
624}
625
627 const std::string& title) {
629 return;
630
631 if (ImGui::Begin(title.c_str(), &show_color_analysis_)) {
632 ImGui::Text(tr("Bitmap Color Analysis"));
633 ImGui::Separator();
634
635 if (!bitmap.is_active()) {
636 ImGui::Text(tr("Bitmap is not active"));
637 ImGui::End();
638 return;
639 }
640
641 std::map<uint8_t, int> pixel_counts;
642 const auto& data = bitmap.vector();
643
644 for (uint8_t pixel : data) {
645 uint8_t palette_index = pixel & 0x0F;
646 pixel_counts[palette_index]++;
647 }
648
649 ImGui::Text(tr("Bitmap Size: %d x %d (%zu pixels)"), bitmap.width(),
650 bitmap.height(), data.size());
651
652 ImGui::Separator();
653 ImGui::Text(tr("Pixel Distribution:"));
654
655 int total_pixels = static_cast<int>(data.size());
656 plotting::PlotStyleScope plot_style(
657 gui::ThemeManager::Get().GetCurrentTheme());
658 plotting::PlotConfig plot_cfg{.id = "Pixel Distribution",
659 .x_label = "Palette Index",
660 .y_label = "Count",
661 .flags = ImPlotFlags_NoBoxSelect,
662 .x_axis_flags = ImPlotAxisFlags_AutoFit,
663 .y_axis_flags = ImPlotAxisFlags_AutoFit};
664 std::vector<double> x;
665 std::vector<double> y;
666 x.reserve(pixel_counts.size());
667 y.reserve(pixel_counts.size());
668 for (const auto& [index, count] : pixel_counts) {
669 x.push_back(static_cast<double>(index));
670 y.push_back(static_cast<double>(count));
671 }
672 plotting::PlotGuard plot(plot_cfg);
673 if (plot && !x.empty()) {
674 ImPlot::PlotBars("Usage", x.data(), y.data(), static_cast<int>(x.size()),
675 0.67, 0.0, ImPlotBarsFlags_None);
676 }
677 }
678 ImGui::End();
679}
680
682 int palette_index) {
683 if (!bitmap || !rom_palettes_loaded_ || group_index < 0 ||
684 group_index >= static_cast<int>(rom_palette_groups_.size())) {
685 return false;
686 }
687
688 try {
689 const auto& selected_palette = rom_palette_groups_[group_index];
690 SavePaletteBackup(bitmap->palette());
691
692 if (palette_index >= 0 && palette_index < 8) {
693 bitmap->SetPaletteWithTransparent(selected_palette, palette_index);
694 } else {
695 bitmap->SetPalette(selected_palette);
696 }
697
700
701 current_group_index_ = group_index;
702 current_palette_index_ = palette_index;
703 return true;
704 } catch (const std::exception& e) {
705 return false;
706 }
707}
708
711 current_group_index_ >= static_cast<int>(rom_palette_groups_.size())) {
712 return nullptr;
713 }
715}
716
720
722 if (backup_palette_.size() == 0) {
723 return false;
724 }
725 palette = backup_palette_;
726 return true;
727}
728
729// Unified grid drawing function
731 const float swatch_size =
732 ComputeSwatchSize(cols, /*min_size=*/16.0f, /*max_size=*/30.0f);
733 for (int i = 0; i < static_cast<int>(palette.size()); i++) {
734 if (i % cols != 0)
735 ImGui::SameLine();
736
737 auto color = palette[i];
738 ImVec4 display_color = gui::ConvertSnesColorToImVec4(color);
739
740 ImGui::PushID(i);
741 const bool clicked = ImGui::ColorButton("##color", display_color,
742 ImGuiColorEditFlags_NoTooltip,
743 ImVec2(swatch_size, swatch_size));
744 const ImVec2 swatch_min = ImGui::GetItemRectMin();
745 const ImVec2 swatch_max = ImGui::GetItemRectMax();
746 const bool double_clicked =
747 ImGui::IsItemHovered() &&
748 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
749 if (clicked || double_clicked) {
752 temp_color_ = display_color;
753 editing_color_ = display_color;
754 }
755 if (AcceptImGuiColorDrop(&palette[i], swatch_min, swatch_max)) {
763 }
764 }
765
766 if (ImGui::BeginPopupContextItem()) {
767 ImGui::TextDisabled(tr("Color %d (0x%04X)"), i, color.snes());
768 ImGui::Separator();
769 std::vector<MenuItemSpec> items;
770 items.emplace_back(tr("Edit Color..."), ICON_MD_EDIT, [&, i]() {
773 temp_color_ = display_color;
774 editing_color_ = display_color;
775 });
776 items.back().separator_after = true;
777 RenderMenuItems(items);
778 SnesColorCopyMenuItems(palette[i]);
779 ImGui::Separator();
781 tr("Reset to Black"), ICON_MD_FORMAT_COLOR_RESET,
782 [&, i]() {
783 palette[i] = gfx::SnesColor(0);
787 }
788 },
789 /*confirm=*/false));
790 ImGui::EndPopup();
791 }
792
793 if (ImGui::IsItemHovered()) {
794 ImGui::SetTooltip(tr("Color %d\nSNES: 0x%04X\nRGB: (%d, %d, %d)"), i,
795 color.snes(), static_cast<int>(display_color.x * 255),
796 static_cast<int>(display_color.y * 255),
797 static_cast<int>(display_color.z * 255));
798 }
799
800 ImGui::PopID();
801 }
802
803 if (editing_color_index_ >= 0) {
804 // Use a unique ID for the popup to prevent conflicts
805 std::string popup_id =
806 gui::MakePopupIdWithInstance("PaletteEditorWidget", "EditColor", this);
807
808 ImGui::OpenPopup(popup_id.c_str());
809 if (ImGui::BeginPopupModal(popup_id.c_str(), nullptr,
810 ImGuiWindowFlags_AlwaysAutoResize)) {
811 ImGui::Text(tr("Editing Color %d"), editing_color_index_);
812 if (ImGui::ColorEdit4(
813 "Color", &temp_color_.x,
814 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_DisplayRGB)) {
815 auto new_snes_color = gfx::SnesColor(temp_color_);
816 palette[editing_color_index_] = new_snes_color;
819 }
823 }
824 }
825 if (gui::PrimaryButton("Apply")) {
827 ImGui::CloseCurrentPopup();
828 }
829 ImGui::SameLine();
830 if (gui::ThemedButton("Cancel")) {
832 ImGui::CloseCurrentPopup();
833 }
834 ImGui::EndPopup();
835 }
836 }
837}
838
842 }
843
844 if (rom_palette_groups_.empty()) {
845 ImGui::Text(tr("No ROM palettes available"));
846 return;
847 }
848
849 ImGui::Text(tr("Palette Group:"));
850 if (ImGui::Combo("##PaletteGroup", &current_group_index_,
851 RomPaletteGroupNameGetter, &palette_group_names_,
852 static_cast<int>(palette_group_names_.size()))) {}
853
854 ImGui::Text(tr("Palette Index:"));
855 ImGui::SliderInt("##PaletteIndex", &current_palette_index_, 0, 7, "%d");
856
857 if (current_group_index_ < static_cast<int>(rom_palette_groups_.size())) {
858 ImGui::Text(tr("Preview:"));
859 const auto& preview_palette = rom_palette_groups_[current_group_index_];
860 for (int i = 0; i < 8 && i < static_cast<int>(preview_palette.size());
861 i++) {
862 if (i > 0)
863 ImGui::SameLine();
864 auto color = preview_palette[i];
865 ImVec4 display_color = gui::ConvertSnesColorToImVec4(color);
866 ImGui::ColorButton(("##preview" + std::to_string(i)).c_str(),
867 display_color, ImGuiColorEditFlags_NoTooltip,
868 ImVec2(20, 20));
869 }
870 }
871}
872
874 int color_index) {
875 ImVec4 rgba = gui::ConvertSnesColorToImVec4(color);
876
877 ImGui::PushID(color_index);
878
879 if (ImGui::ColorEdit4(
880 "##color_edit", &rgba.x,
881 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_DisplayRGB)) {
882 color = gfx::SnesColor(rgba);
883 }
884
885 ImGui::Text(tr("SNES Color: 0x%04X"), color.snes());
886
887 int r = (color.snes() & 0x1F);
888 int g = (color.snes() >> 5) & 0x1F;
889 int b = (color.snes() >> 10) & 0x1F;
890
891 if (ImGui::SliderInt(tr("Red"), &r, 0, 31)) {
892 uint16_t new_color = (color.snes() & 0xFFE0) | (r & 0x1F);
893 color = gfx::SnesColor(new_color);
894 }
895 if (ImGui::SliderInt(tr("Green"), &g, 0, 31)) {
896 uint16_t new_color = (color.snes() & 0xFC1F) | ((g & 0x1F) << 5);
897 color = gfx::SnesColor(new_color);
898 }
899 if (ImGui::SliderInt(tr("Blue"), &b, 0, 31)) {
900 uint16_t new_color = (color.snes() & 0x83FF) | ((b & 0x1F) << 10);
901 color = gfx::SnesColor(new_color);
902 }
903
904 ImGui::PopID();
905}
906
908 ImGui::Text(tr("Palette Information:"));
909 ImGui::Text(tr("Size: %zu colors"), palette.size());
910
911 std::map<uint16_t, int> color_frequency;
912 for (int i = 0; i < static_cast<int>(palette.size()); i++) {
913 color_frequency[palette[i].snes()]++;
914 }
915
916 ImGui::Text(tr("Unique Colors: %zu"), color_frequency.size());
917
918 if (color_frequency.size() < palette.size()) {
919 ImGui::TextColored(ImVec4(1, 1, 0, 1),
920 tr("Warning: Duplicate colors detected!"));
921 if (ImGui::TreeNode("Duplicate Colors")) {
922 for (const auto& [snes_color, count] : color_frequency) {
923 if (count > 1) {
924 ImVec4 display_color =
926 ImGui::ColorButton(("##dup" + std::to_string(snes_color)).c_str(),
927 display_color, ImGuiColorEditFlags_NoTooltip,
928 ImVec2(16, 16));
929 ImGui::SameLine();
930 ImGui::Text(tr("0x%04X appears %d times"), snes_color, count);
931 }
932 }
933 ImGui::TreePop();
934 }
935 }
936
937 // Visual histogram of color reuse
938 {
939 plotting::PlotStyleScope plot_style(
940 gui::ThemeManager::Get().GetCurrentTheme());
941 plotting::PlotConfig plot_cfg{.id = "Palette Color Frequency",
942 .x_label = "Color Index",
943 .y_label = "Count",
944 .flags = ImPlotFlags_NoBoxSelect,
945 .x_axis_flags = ImPlotAxisFlags_AutoFit,
946 .y_axis_flags = ImPlotAxisFlags_AutoFit};
947 std::vector<double> x;
948 std::vector<double> y;
949 x.reserve(color_frequency.size());
950 y.reserve(color_frequency.size());
951 for (const auto& [snes_color, count] : color_frequency) {
952 x.push_back(static_cast<double>(snes_color));
953 y.push_back(static_cast<double>(count));
954 }
955 plotting::PlotGuard plot(plot_cfg);
956 if (plot && !x.empty()) {
957 ImPlot::PlotBars("Count", x.data(), y.data(), static_cast<int>(x.size()),
958 0.5, 0.0, ImPlotBarsFlags_None);
959 }
960 }
961
962 float total_brightness = 0.0f;
963 float min_brightness = 1.0f;
964 float max_brightness = 0.0f;
965
966 for (int i = 0; i < static_cast<int>(palette.size()); i++) {
967 ImVec4 color = gui::ConvertSnesColorToImVec4(palette[i]);
968 float brightness = (color.x + color.y + color.z) / 3.0f;
969 total_brightness += brightness;
970 min_brightness = std::min(min_brightness, brightness);
971 max_brightness = std::max(max_brightness, brightness);
972 }
973
974 float avg_brightness = total_brightness / palette.size();
975
976 ImGui::Separator();
977 ImGui::Text(tr("Brightness Analysis:"));
978 ImGui::Text(tr("Average: %.2f"), avg_brightness);
979 ImGui::Text(tr("Range: %.2f - %.2f"), min_brightness, max_brightness);
980
981 ImGui::Text(tr("Brightness Distribution:"));
982 ImGui::ProgressBar(avg_brightness, ImVec2(-1, 0), "Avg");
983}
984
987 return;
988
989 try {
990 const auto& palette_groups = game_data_->palette_groups;
992 palette_group_names_.clear();
993
994 if (palette_groups.overworld_main.size() > 0) {
995 rom_palette_groups_.push_back(palette_groups.overworld_main[0]);
996 palette_group_names_.push_back("Overworld Main");
997 }
998 if (palette_groups.overworld_aux.size() > 0) {
999 rom_palette_groups_.push_back(palette_groups.overworld_aux[0]);
1000 palette_group_names_.push_back("Overworld Aux");
1001 }
1002 if (palette_groups.overworld_animated.size() > 0) {
1003 rom_palette_groups_.push_back(palette_groups.overworld_animated[0]);
1004 palette_group_names_.push_back("Overworld Animated");
1005 }
1006 if (palette_groups.dungeon_main.size() > 0) {
1007 rom_palette_groups_.push_back(palette_groups.dungeon_main[0]);
1008 palette_group_names_.push_back("Dungeon Main");
1009 }
1010 if (palette_groups.global_sprites.size() > 0) {
1011 rom_palette_groups_.push_back(palette_groups.global_sprites[0]);
1012 palette_group_names_.push_back("Global Sprites");
1013 }
1014 if (palette_groups.armors.size() > 0) {
1015 rom_palette_groups_.push_back(palette_groups.armors[0]);
1016 palette_group_names_.push_back("Armor");
1017 }
1018 if (palette_groups.swords.size() > 0) {
1019 rom_palette_groups_.push_back(palette_groups.swords[0]);
1020 palette_group_names_.push_back("Swords");
1021 }
1022
1023 rom_palettes_loaded_ = true;
1024 } catch (const std::exception& e) {
1025 LOG_ERROR("Enhanced Palette Editor", "Failed to load ROM palettes");
1026 }
1027}
1028
1029} // namespace gui
1030} // 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:49
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
const SnesPalette & palette() const
Definition bitmap.h:393
const std::vector< uint8_t > & vector() const
Definition bitmap.h:411
bool is_active() const
Definition bitmap.h:414
int height() const
Definition bitmap.h:399
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:452
int width() const
Definition bitmap.h:398
void SetPaletteWithTransparent(const SnesPalette &palette, size_t index, int length=7)
Set the palette with a transparent color.
Definition bitmap.cc:524
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 ICON_MD_FORMAT_COLOR_RESET
Definition icons.h:831
#define ICON_MD_EDIT
Definition icons.h:645
#define LOG_ERROR(category, format,...)
Definition log.h:140
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:140
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
void SnesColorCopyMenuItems(const gfx::SnesColor &color)
Definition color.cc:59
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:24
bool InputScalarDeferred(const char *label, ImGuiDataType data_type, void *data, const char *format, ImGuiInputTextFlags flags, InputScalarTargetIdentity target_identity)
Definition input.cc:392
int ResolveDungeonRenderPaletteColumns(float available_width, float min_swatch_size, float item_spacing)
IMGUI_API bool SnesColorEdit4(absl::string_view label, gfx::SnesColor *color, ImGuiColorEditFlags flags)
Definition color.cc:97
void RenderMenuItem(const CanvasMenuItem &item, std::function< void(const std::string &, std::function< void()>)> popup_opened_callback)
Render a single menu item.
void RenderMenuItems(const std::vector< MenuItemSpec > &items)
SNES color in 15-bit RGB format (BGR555)
Represents a group of palettes.
static CanvasMenuItem Destructive(const std::string &lbl, const std::string &ico, std::function< void()> cb, bool confirm=true, const std::string &sc="")
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95
Automatic widget registration helpers for ImGui Test Engine integration.