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
126int ResolveDungeonRenderPaletteColumns(float available_width,
127 float min_swatch_size,
128 float item_spacing) {
129 const float safe_width = std::max(available_width, 1.0f);
130 const float safe_swatch = std::max(min_swatch_size, 1.0f);
131 const float safe_spacing = std::max(item_spacing, 0.0f);
132 for (const int columns : {16, 8, 4, 2, 1}) {
133 const float required_width =
134 safe_swatch * columns + safe_spacing * (columns - 1);
135 if (safe_width >= required_width) {
136 return columns;
137 }
138 }
139 return 1;
140}
141
142// Merged implementation from PaletteWidget and PaletteEditorWidget
143
145 game_data_ = game_data;
146 rom_ = nullptr;
147 rom_palettes_loaded_ = false;
148 if (game_data_) {
150 }
153}
154
156 rom_ = rom;
157 game_data_ = nullptr;
158 rom_palettes_loaded_ = false;
159 // Legacy mode - no palette loading without game_data
162}
163
165 int display_index, std::optional<gfx::SnesColor> color) {
166 if (!game_data_) {
167 return absl::FailedPreconditionError("GameData not loaded");
168 }
169
170 const auto target =
171 ResolveDungeonRenderColorTarget(display_index, current_palette_id_);
172 if (!target.has_value()) {
173 return absl::InvalidArgumentError(absl::StrFormat(
174 "Dungeon render palette slot %d is reserved or out of range",
175 display_index));
176 }
177
178 auto& palette_manager = gfx::PaletteManager::Get();
179 if (!palette_manager.IsManaging(game_data_)) {
180 return absl::FailedPreconditionError(
181 "PaletteManager is bound to a different ROM session");
182 }
183 const char* group_name = PaletteManagerGroupName(target->source);
184 absl::Status status =
185 color.has_value()
186 ? palette_manager.SetColor(group_name, target->palette_index,
187 target->color_index, *color)
188 : palette_manager.ResetColor(group_name, target->palette_index,
189 target->color_index);
190 if (!status.ok()) {
191 return status;
192 }
193
195 on_palette_changed_({current_palette_id_, target->source});
196 }
197 return absl::OkStatus();
198}
199
200// --- Embedded Draw Method (from simple editor) ---
202 if (!game_data_) {
203 ImGui::TextColored(ImVec4(1, 0, 0, 1), tr("GameData not loaded"));
204 return;
205 }
206
207 ImGui::BeginGroup();
208
210 ImGui::Separator();
211
212 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
213 if (dungeon_pal_group.empty()) {
214 ImGui::TextDisabled(tr("Dungeon palettes unavailable"));
215 ImGui::EndGroup();
216 return;
217 }
218
219 current_palette_id_ = std::clamp(
220 current_palette_id_, 0, static_cast<int>(dungeon_pal_group.size()) - 1);
221 if (current_palette_id_ >= 0 &&
222 current_palette_id_ < (int)dungeon_pal_group.size()) {
223 auto palette = dungeon_pal_group[current_palette_id_];
226 } else {
227 DrawPaletteGrid(palette, 15);
228 dungeon_pal_group[current_palette_id_] = palette;
229 }
230 }
231
232 ImGui::Separator();
233
234 if (selected_color_index_ >= 0) {
237 } else {
239 }
240 } else {
241 ImGui::TextDisabled(tr("Select a color to edit"));
242 }
243
244 ImGui::EndGroup();
245}
246
248 if (!game_data_)
249 return;
250 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
251 int num_palettes = dungeon_pal_group.size();
252 if (num_palettes <= 0) {
253 ImGui::TextDisabled(tr("No dungeon palettes"));
254 return;
255 }
256
257 current_palette_id_ = std::clamp(current_palette_id_, 0, num_palettes - 1);
258
259 ImGui::Text(tr("Dungeon Palette:"));
260 ImGui::SameLine();
261 ImGui::SetNextItemWidth(std::min(180.0f, ImGui::GetContentRegionAvail().x));
262
263 if (ImGui::BeginCombo(
264 "##PaletteSelect",
265 absl::StrFormat("Palette %d", current_palette_id_).c_str())) {
266 for (int i = 0; i < num_palettes; i++) {
267 bool is_selected = (current_palette_id_ == i);
268 if (ImGui::Selectable(absl::StrFormat("Palette %d", i).c_str(),
269 is_selected)) {
272 }
273 if (is_selected) {
274 ImGui::SetItemDefaultFocus();
275 }
276 }
277 ImGui::EndCombo();
278 }
279
280 int requested_palette_id = current_palette_id_;
281 ImGui::TextUnformatted(tr("Palette ID"));
282 ImGui::SameLine();
283 ImGui::SetNextItemWidth(72.0f);
284 const bool palette_id_changed = gui::InputScalarDeferred(
285 "##DungeonPaletteId", ImGuiDataType_S32, &requested_palette_id, "%d", 0,
286 {reinterpret_cast<uintptr_t>(game_data_)});
287 gui::AutoRegisterLastItem("input_int", "palette_id",
288 "Dungeon main palette ID");
289 if (palette_id_changed) {
290 current_palette_id_ = std::clamp(requested_palette_id, 0, num_palettes - 1);
292 }
293}
294
296 if (!game_data_)
297 return;
298 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
299 if (dungeon_pal_group.empty()) {
300 return;
301 }
302 current_palette_id_ = std::clamp(
303 current_palette_id_, 0, static_cast<int>(dungeon_pal_group.size()) - 1);
304 ImGui::SeparatorText(
305 absl::StrFormat("Edit Color %d", selected_color_index_).c_str());
306
307 auto palette = dungeon_pal_group[current_palette_id_];
308 if (selected_color_index_ < 0 ||
309 selected_color_index_ >= static_cast<int>(palette.size())) {
311 return;
312 }
313 auto original_color = palette[selected_color_index_];
314
315 // Use standardized SnesColorEdit4 for consistency
317 "Color", &palette[selected_color_index_],
318 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_PickerHueWheel)) {
319 // Update the palette group with the modified palette
320 dungeon_pal_group[current_palette_id_] = palette;
321
322 // Update editing_color_ to match (for consistency with other parts of the
323 // widget)
326
330 }
331 }
332
333 ImGui::Text(tr("RGB (0-255): (%d, %d, %d)"),
334 static_cast<int>(editing_color_.x * 255),
335 static_cast<int>(editing_color_.y * 255),
336 static_cast<int>(editing_color_.z * 255));
337 ImGui::Text(tr("SNES BGR555: 0x%04X"), original_color.snes());
338
339 if (gui::ThemedButton("Reset to Original")) {
341 // Also reset the actual palette color
342 palette[selected_color_index_] = original_color;
343 dungeon_pal_group[current_palette_id_] = palette;
347 }
348 }
349}
350
352 ImGui::TextDisabled(tr("Slots 00-1F: HUD / floor / ceiling"));
353 ImGui::TextDisabled(tr("Slots 20-7F: Dungeon main"));
354 constexpr float kSwatchSpacing = 2.0f;
355 const ImVec2 previous_spacing = ImGui::GetStyle().ItemSpacing;
356 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
357 ImVec2(kSwatchSpacing, previous_spacing.y));
358 const int columns = ResolveDungeonRenderPaletteColumns(
359 ImGui::GetContentRegionAvail().x, /*min_swatch_size=*/14.0f,
360 kSwatchSpacing);
361 const float swatch_size = ComputeSwatchSize(columns, 14.0f, 28.0f);
362
363 for (int i = 0; i < 128; ++i) {
364 if (i % columns != 0) {
365 ImGui::SameLine();
366 }
367
368 gfx::SnesColor color(0);
369 bool editable = false;
370 std::string tooltip_label = "Reserved / transparent";
371 int source_index = -1;
372
373 const auto target = ResolveDungeonRenderColorTarget(i, current_palette_id_);
374 if (target.has_value()) {
375 const auto& group = PaletteGroupForTarget(*game_data_, *target);
376 if (target->palette_index >= 0 &&
377 target->palette_index < static_cast<int>(group.size())) {
378 const auto& palette = group.palette_ref(target->palette_index);
379 if (target->color_index >= 0 &&
380 target->color_index < static_cast<int>(palette.size())) {
381 color = palette[target->color_index];
382 editable = true;
383 tooltip_label = DungeonRenderColorSourceLabel(*target);
384 source_index = target->color_index;
385 }
386 }
387 }
388
389 ImVec4 display_color = gui::ConvertSnesColorToImVec4(color);
390 ImGui::PushID(i);
391 if (!editable) {
392 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.25f);
393 }
394 const bool clicked = ImGui::ColorButton("##render_color", display_color,
395 ImGuiColorEditFlags_NoTooltip,
396 ImVec2(swatch_size, swatch_size));
397 gui::AutoRegisterLastItem("color_button",
398 absl::StrFormat("palette_swatch_%03d", i),
399 "Dungeon render palette swatch");
400 const ImVec2 swatch_min = ImGui::GetItemRectMin();
401 const ImVec2 swatch_max = ImGui::GetItemRectMax();
402 const bool double_clicked =
403 editable && ImGui::IsItemHovered() &&
404 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
405 if (!editable) {
406 ImGui::PopStyleVar();
407 }
408
409 if ((clicked || double_clicked) && editable) {
412 temp_color_ = display_color;
413 editing_color_ = display_color;
414 }
415 gfx::SnesColor dropped_color = color;
416 if (editable &&
417 AcceptImGuiColorDrop(&dropped_color, swatch_min, swatch_max)) {
418 const absl::Status status = ApplyDungeonRenderColorEdit(i, dropped_color);
419 if (status.ok()) {
424 } else {
425 LOG_ERROR("PaletteEditorWidget", "Failed to apply dropped color: %s",
426 status.message().data());
427 }
428 }
429
430 if (ImGui::IsItemHovered()) {
431 if (editable) {
432 ImGui::SetTooltip(tr("%s\nSlot %d\nSNES: 0x%04X\nRGB: (%d, %d, %d)"),
433 tooltip_label.c_str(), source_index, color.snes(),
434 static_cast<int>(display_color.x * 255),
435 static_cast<int>(display_color.y * 255),
436 static_cast<int>(display_color.z * 255));
437 } else {
438 ImGui::SetTooltip("%s", tooltip_label.c_str());
439 }
440 }
441 ImGui::PopID();
442 }
443 ImGui::PopStyleVar();
444}
445
446float PaletteEditorWidget::ComputeSwatchSize(int columns, float min_size,
447 float max_size) const {
448 const float available_width =
449 std::max(ImGui::GetContentRegionAvail().x, 1.0f);
450 const float spacing =
451 std::max(ImGui::GetStyle().ItemSpacing.x, 2.0f) * (columns - 1);
452 const float raw = (available_width - spacing) / std::max(columns, 1);
453 return std::clamp(raw, min_size, max_size);
454}
455
457 const auto target = ResolveDungeonRenderColorTarget(selected_color_index_,
459 if (!game_data_ || !target.has_value()) {
462 return;
463 }
464
465 const auto& group = PaletteGroupForTarget(*game_data_, *target);
466 if (target->palette_index < 0 ||
467 target->palette_index >= static_cast<int>(group.size())) {
470 return;
471 }
472 const auto& palette = group.palette_ref(target->palette_index);
473 if (target->color_index < 0 ||
474 target->color_index >= static_cast<int>(palette.size())) {
477 return;
478 }
479
480 ImGui::SeparatorText(absl::StrFormat("%s Color %d",
481 DungeonRenderColorSourceLabel(*target),
482 target->color_index)
483 .c_str());
484
485 const gfx::SnesColor current_color = palette[target->color_index];
486 gfx::SnesColor edited_color = current_color;
488 "Color", &edited_color,
489 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_PickerHueWheel)) {
490 const absl::Status status =
492 if (status.ok()) {
494 } else {
495 LOG_ERROR("PaletteEditorWidget", "Failed to apply palette color: %s",
496 status.message().data());
497 }
498 }
499
500 uint16_t raw_color = current_color.snes();
501 ImGui::AlignTextToFramePadding();
502 ImGui::TextUnformatted(tr("Raw BGR555"));
503 ImGui::SameLine();
504 ImGui::SetNextItemWidth(96.0f);
505 const bool raw_changed = gui::InputScalarDeferred(
506 "##SelectedDungeonPaletteBgr555", ImGuiDataType_U16, &raw_color, "%04X",
507 ImGuiInputTextFlags_CharsHexadecimal,
508 {reinterpret_cast<uintptr_t>(game_data_),
509 static_cast<uint64_t>(target->source),
510 static_cast<uint64_t>(target->palette_index),
511 static_cast<uint64_t>(target->color_index)});
512 gui::AutoRegisterLastItem("input_scalar", "selected_palette_bgr555",
513 "Selected dungeon palette raw BGR555 color");
514 if (raw_changed) {
515 raw_color &= 0x7FFF;
516 const gfx::SnesColor raw_edited_color(raw_color);
517 const absl::Status status =
519 if (status.ok()) {
521 } else {
522 LOG_ERROR("PaletteEditorWidget", "Failed to apply raw palette color: %s",
523 status.message().data());
524 }
525 }
526
527 ImGui::Text(tr("RGB (0-255): (%d, %d, %d)"),
528 static_cast<int>(editing_color_.x * 255),
529 static_cast<int>(editing_color_.y * 255),
530 static_cast<int>(editing_color_.z * 255));
531 ImGui::Text(tr("SNES BGR555: 0x%04X"), current_color.snes());
532
533 if (gui::ThemedButton("Reset to Original")) {
534 const absl::Status status =
536 if (status.ok()) {
538 PaletteManagerGroupName(target->source), target->palette_index,
539 target->color_index);
541 } else {
542 LOG_ERROR("PaletteEditorWidget", "Failed to reset palette color: %s",
543 status.message().data());
544 }
545 }
546}
547
548// --- Modal/Popup Methods (from feature-rich widget) ---
549
551 const std::string& title) {
552 if (ImGui::BeginPopupModal(title.c_str(), nullptr,
553 ImGuiWindowFlags_AlwaysAutoResize)) {
554 ImGui::Text(tr("Enhanced Palette Editor"));
555 ImGui::Separator();
556
557 DrawPaletteGrid(palette);
558 ImGui::Separator();
559
560 if (ImGui::CollapsingHeader(tr("Palette Analysis"))) {
561 DrawPaletteAnalysis(palette);
562 }
563
564 if (ImGui::CollapsingHeader(tr("ROM Palette Manager")) && rom_) {
566
567 if (gui::PrimaryButton("Apply ROM Palette") &&
568 !rom_palette_groups_.empty()) {
570 static_cast<int>(rom_palette_groups_.size())) {
572 }
573 }
574 }
575
576 ImGui::Separator();
577
578 if (gui::ThemedButton("Save Backup")) {
579 SavePaletteBackup(palette);
580 }
581 ImGui::SameLine();
582 if (gui::ThemedButton("Restore Backup")) {
583 RestorePaletteBackup(palette);
584 }
585 ImGui::SameLine();
586 if (gui::ThemedButton("Close")) {
587 ImGui::CloseCurrentPopup();
588 }
589
590 ImGui::EndPopup();
591 }
592}
593
596 return;
597
598 if (ImGui::Begin("ROM Palette Manager", &show_rom_manager_)) {
599 if (!rom_) {
600 ImGui::Text(tr("No ROM loaded"));
601 ImGui::End();
602 return;
603 }
604
607 }
608
610
611 if (current_group_index_ < static_cast<int>(rom_palette_groups_.size())) {
612 ImGui::Separator();
613 ImGui::Text(tr("Preview of %s:"),
615
616 const auto& preview_palette = rom_palette_groups_[current_group_index_];
617 DrawPaletteGrid(const_cast<gfx::SnesPalette&>(preview_palette));
618 DrawPaletteAnalysis(preview_palette);
619 }
620 }
621 ImGui::End();
622}
623
625 const std::string& title) {
627 return;
628
629 if (ImGui::Begin(title.c_str(), &show_color_analysis_)) {
630 ImGui::Text(tr("Bitmap Color Analysis"));
631 ImGui::Separator();
632
633 if (!bitmap.is_active()) {
634 ImGui::Text(tr("Bitmap is not active"));
635 ImGui::End();
636 return;
637 }
638
639 std::map<uint8_t, int> pixel_counts;
640 const auto& data = bitmap.vector();
641
642 for (uint8_t pixel : data) {
643 uint8_t palette_index = pixel & 0x0F;
644 pixel_counts[palette_index]++;
645 }
646
647 ImGui::Text(tr("Bitmap Size: %d x %d (%zu pixels)"), bitmap.width(),
648 bitmap.height(), data.size());
649
650 ImGui::Separator();
651 ImGui::Text(tr("Pixel Distribution:"));
652
653 int total_pixels = static_cast<int>(data.size());
654 plotting::PlotStyleScope plot_style(
655 gui::ThemeManager::Get().GetCurrentTheme());
656 plotting::PlotConfig plot_cfg{.id = "Pixel Distribution",
657 .x_label = "Palette Index",
658 .y_label = "Count",
659 .flags = ImPlotFlags_NoBoxSelect,
660 .x_axis_flags = ImPlotAxisFlags_AutoFit,
661 .y_axis_flags = ImPlotAxisFlags_AutoFit};
662 std::vector<double> x;
663 std::vector<double> y;
664 x.reserve(pixel_counts.size());
665 y.reserve(pixel_counts.size());
666 for (const auto& [index, count] : pixel_counts) {
667 x.push_back(static_cast<double>(index));
668 y.push_back(static_cast<double>(count));
669 }
670 plotting::PlotGuard plot(plot_cfg);
671 if (plot && !x.empty()) {
672 ImPlot::PlotBars("Usage", x.data(), y.data(), static_cast<int>(x.size()),
673 0.67, 0.0, ImPlotBarsFlags_None);
674 }
675 }
676 ImGui::End();
677}
678
680 int palette_index) {
681 if (!bitmap || !rom_palettes_loaded_ || group_index < 0 ||
682 group_index >= static_cast<int>(rom_palette_groups_.size())) {
683 return false;
684 }
685
686 try {
687 const auto& selected_palette = rom_palette_groups_[group_index];
688 SavePaletteBackup(bitmap->palette());
689
690 if (palette_index >= 0 && palette_index < 8) {
691 bitmap->SetPaletteWithTransparent(selected_palette, palette_index);
692 } else {
693 bitmap->SetPalette(selected_palette);
694 }
695
698
699 current_group_index_ = group_index;
700 current_palette_index_ = palette_index;
701 return true;
702 } catch (const std::exception& e) {
703 return false;
704 }
705}
706
709 current_group_index_ >= static_cast<int>(rom_palette_groups_.size())) {
710 return nullptr;
711 }
713}
714
718
720 if (backup_palette_.size() == 0) {
721 return false;
722 }
723 palette = backup_palette_;
724 return true;
725}
726
727// Unified grid drawing function
729 const float swatch_size =
730 ComputeSwatchSize(cols, /*min_size=*/16.0f, /*max_size=*/30.0f);
731 for (int i = 0; i < static_cast<int>(palette.size()); i++) {
732 if (i % cols != 0)
733 ImGui::SameLine();
734
735 auto color = palette[i];
736 ImVec4 display_color = gui::ConvertSnesColorToImVec4(color);
737
738 ImGui::PushID(i);
739 const bool clicked = ImGui::ColorButton("##color", display_color,
740 ImGuiColorEditFlags_NoTooltip,
741 ImVec2(swatch_size, swatch_size));
742 const ImVec2 swatch_min = ImGui::GetItemRectMin();
743 const ImVec2 swatch_max = ImGui::GetItemRectMax();
744 const bool double_clicked =
745 ImGui::IsItemHovered() &&
746 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
747 if (clicked || double_clicked) {
750 temp_color_ = display_color;
751 editing_color_ = display_color;
752 }
753 if (AcceptImGuiColorDrop(&palette[i], swatch_min, swatch_max)) {
761 }
762 }
763
764 if (ImGui::BeginPopupContextItem()) {
765 ImGui::Text(tr("Color %d (0x%04X)"), i, color.snes());
766 ImGui::Separator();
767 if (ImGui::MenuItem(tr("Edit Color"))) {
770 temp_color_ = display_color;
771 editing_color_ = display_color;
772 }
773 if (ImGui::MenuItem(tr("Reset to Black"))) {
774 palette[i] = gfx::SnesColor(0);
778 }
779 }
780 ImGui::EndPopup();
781 }
782
783 if (ImGui::IsItemHovered()) {
784 ImGui::SetTooltip(tr("Color %d\nSNES: 0x%04X\nRGB: (%d, %d, %d)"), i,
785 color.snes(), static_cast<int>(display_color.x * 255),
786 static_cast<int>(display_color.y * 255),
787 static_cast<int>(display_color.z * 255));
788 }
789
790 ImGui::PopID();
791 }
792
793 if (editing_color_index_ >= 0) {
794 // Use a unique ID for the popup to prevent conflicts
795 std::string popup_id =
796 gui::MakePopupIdWithInstance("PaletteEditorWidget", "EditColor", this);
797
798 ImGui::OpenPopup(popup_id.c_str());
799 if (ImGui::BeginPopupModal(popup_id.c_str(), nullptr,
800 ImGuiWindowFlags_AlwaysAutoResize)) {
801 ImGui::Text(tr("Editing Color %d"), editing_color_index_);
802 if (ImGui::ColorEdit4(
803 "Color", &temp_color_.x,
804 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_DisplayRGB)) {
805 auto new_snes_color = gfx::SnesColor(temp_color_);
806 palette[editing_color_index_] = new_snes_color;
809 }
813 }
814 }
815 if (gui::PrimaryButton("Apply")) {
817 ImGui::CloseCurrentPopup();
818 }
819 ImGui::SameLine();
820 if (gui::ThemedButton("Cancel")) {
822 ImGui::CloseCurrentPopup();
823 }
824 ImGui::EndPopup();
825 }
826 }
827}
828
832 }
833
834 if (rom_palette_groups_.empty()) {
835 ImGui::Text(tr("No ROM palettes available"));
836 return;
837 }
838
839 ImGui::Text(tr("Palette Group:"));
840 if (ImGui::Combo("##PaletteGroup", &current_group_index_,
841 RomPaletteGroupNameGetter, &palette_group_names_,
842 static_cast<int>(palette_group_names_.size()))) {}
843
844 ImGui::Text(tr("Palette Index:"));
845 ImGui::SliderInt("##PaletteIndex", &current_palette_index_, 0, 7, "%d");
846
847 if (current_group_index_ < static_cast<int>(rom_palette_groups_.size())) {
848 ImGui::Text(tr("Preview:"));
849 const auto& preview_palette = rom_palette_groups_[current_group_index_];
850 for (int i = 0; i < 8 && i < static_cast<int>(preview_palette.size());
851 i++) {
852 if (i > 0)
853 ImGui::SameLine();
854 auto color = preview_palette[i];
855 ImVec4 display_color = gui::ConvertSnesColorToImVec4(color);
856 ImGui::ColorButton(("##preview" + std::to_string(i)).c_str(),
857 display_color, ImGuiColorEditFlags_NoTooltip,
858 ImVec2(20, 20));
859 }
860 }
861}
862
864 int color_index) {
865 ImVec4 rgba = gui::ConvertSnesColorToImVec4(color);
866
867 ImGui::PushID(color_index);
868
869 if (ImGui::ColorEdit4(
870 "##color_edit", &rgba.x,
871 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_DisplayRGB)) {
872 color = gfx::SnesColor(rgba);
873 }
874
875 ImGui::Text(tr("SNES Color: 0x%04X"), color.snes());
876
877 int r = (color.snes() & 0x1F);
878 int g = (color.snes() >> 5) & 0x1F;
879 int b = (color.snes() >> 10) & 0x1F;
880
881 if (ImGui::SliderInt(tr("Red"), &r, 0, 31)) {
882 uint16_t new_color = (color.snes() & 0xFFE0) | (r & 0x1F);
883 color = gfx::SnesColor(new_color);
884 }
885 if (ImGui::SliderInt(tr("Green"), &g, 0, 31)) {
886 uint16_t new_color = (color.snes() & 0xFC1F) | ((g & 0x1F) << 5);
887 color = gfx::SnesColor(new_color);
888 }
889 if (ImGui::SliderInt(tr("Blue"), &b, 0, 31)) {
890 uint16_t new_color = (color.snes() & 0x83FF) | ((b & 0x1F) << 10);
891 color = gfx::SnesColor(new_color);
892 }
893
894 ImGui::PopID();
895}
896
898 ImGui::Text(tr("Palette Information:"));
899 ImGui::Text(tr("Size: %zu colors"), palette.size());
900
901 std::map<uint16_t, int> color_frequency;
902 for (int i = 0; i < static_cast<int>(palette.size()); i++) {
903 color_frequency[palette[i].snes()]++;
904 }
905
906 ImGui::Text(tr("Unique Colors: %zu"), color_frequency.size());
907
908 if (color_frequency.size() < palette.size()) {
909 ImGui::TextColored(ImVec4(1, 1, 0, 1),
910 tr("Warning: Duplicate colors detected!"));
911 if (ImGui::TreeNode("Duplicate Colors")) {
912 for (const auto& [snes_color, count] : color_frequency) {
913 if (count > 1) {
914 ImVec4 display_color =
916 ImGui::ColorButton(("##dup" + std::to_string(snes_color)).c_str(),
917 display_color, ImGuiColorEditFlags_NoTooltip,
918 ImVec2(16, 16));
919 ImGui::SameLine();
920 ImGui::Text(tr("0x%04X appears %d times"), snes_color, count);
921 }
922 }
923 ImGui::TreePop();
924 }
925 }
926
927 // Visual histogram of color reuse
928 {
929 plotting::PlotStyleScope plot_style(
930 gui::ThemeManager::Get().GetCurrentTheme());
931 plotting::PlotConfig plot_cfg{.id = "Palette Color Frequency",
932 .x_label = "Color Index",
933 .y_label = "Count",
934 .flags = ImPlotFlags_NoBoxSelect,
935 .x_axis_flags = ImPlotAxisFlags_AutoFit,
936 .y_axis_flags = ImPlotAxisFlags_AutoFit};
937 std::vector<double> x;
938 std::vector<double> y;
939 x.reserve(color_frequency.size());
940 y.reserve(color_frequency.size());
941 for (const auto& [snes_color, count] : color_frequency) {
942 x.push_back(static_cast<double>(snes_color));
943 y.push_back(static_cast<double>(count));
944 }
945 plotting::PlotGuard plot(plot_cfg);
946 if (plot && !x.empty()) {
947 ImPlot::PlotBars("Count", x.data(), y.data(), static_cast<int>(x.size()),
948 0.5, 0.0, ImPlotBarsFlags_None);
949 }
950 }
951
952 float total_brightness = 0.0f;
953 float min_brightness = 1.0f;
954 float max_brightness = 0.0f;
955
956 for (int i = 0; i < static_cast<int>(palette.size()); i++) {
957 ImVec4 color = gui::ConvertSnesColorToImVec4(palette[i]);
958 float brightness = (color.x + color.y + color.z) / 3.0f;
959 total_brightness += brightness;
960 min_brightness = std::min(min_brightness, brightness);
961 max_brightness = std::max(max_brightness, brightness);
962 }
963
964 float avg_brightness = total_brightness / palette.size();
965
966 ImGui::Separator();
967 ImGui::Text(tr("Brightness Analysis:"));
968 ImGui::Text(tr("Average: %.2f"), avg_brightness);
969 ImGui::Text(tr("Range: %.2f - %.2f"), min_brightness, max_brightness);
970
971 ImGui::Text(tr("Brightness Distribution:"));
972 ImGui::ProgressBar(avg_brightness, ImVec2(-1, 0), "Avg");
973}
974
977 return;
978
979 try {
980 const auto& palette_groups = game_data_->palette_groups;
982 palette_group_names_.clear();
983
984 if (palette_groups.overworld_main.size() > 0) {
985 rom_palette_groups_.push_back(palette_groups.overworld_main[0]);
986 palette_group_names_.push_back("Overworld Main");
987 }
988 if (palette_groups.overworld_aux.size() > 0) {
989 rom_palette_groups_.push_back(palette_groups.overworld_aux[0]);
990 palette_group_names_.push_back("Overworld Aux");
991 }
992 if (palette_groups.overworld_animated.size() > 0) {
993 rom_palette_groups_.push_back(palette_groups.overworld_animated[0]);
994 palette_group_names_.push_back("Overworld Animated");
995 }
996 if (palette_groups.dungeon_main.size() > 0) {
997 rom_palette_groups_.push_back(palette_groups.dungeon_main[0]);
998 palette_group_names_.push_back("Dungeon Main");
999 }
1000 if (palette_groups.global_sprites.size() > 0) {
1001 rom_palette_groups_.push_back(palette_groups.global_sprites[0]);
1002 palette_group_names_.push_back("Global Sprites");
1003 }
1004 if (palette_groups.armors.size() > 0) {
1005 rom_palette_groups_.push_back(palette_groups.armors[0]);
1006 palette_group_names_.push_back("Armor");
1007 }
1008 if (palette_groups.swords.size() > 0) {
1009 rom_palette_groups_.push_back(palette_groups.swords[0]);
1010 palette_group_names_.push_back("Swords");
1011 }
1012
1013 rom_palettes_loaded_ = true;
1014 } catch (const std::exception& e) {
1015 LOG_ERROR("Enhanced Palette Editor", "Failed to load ROM palettes");
1016 }
1017}
1018
1019} // namespace gui
1020} // 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:39
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:69
const SnesPalette & palette() const
Definition bitmap.h:391
const std::vector< uint8_t > & vector() const
Definition bitmap.h:404
bool is_active() const
Definition bitmap.h:407
int height() const
Definition bitmap.h:397
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:396
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:110
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: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: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.