yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
activity_bar.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cctype>
6#include <cstddef>
7#include <cstring>
8#include <functional>
9#include <string>
10#include <unordered_set>
11#include <utility>
12#include <vector>
13
14#include "absl/strings/str_format.h"
20#include "app/gui/core/icons.h"
27#include "core/color.h"
28#include "imgui/imgui.h"
29
30namespace yaze {
31namespace editor {
32
33namespace {
34constexpr const char* kSidebarDragPayload = "YAZE_SIDEBAR_CAT";
35
37 if (!settings)
38 return;
39 (void)settings->Save();
40}
41} // namespace
42
44 std::function<bool()> is_dungeon_workbench_mode,
45 std::function<void(bool)> set_dungeon_workflow_mode)
46 : window_manager_(window_manager),
47 window_browser_(window_manager),
48 window_sidebar_(window_manager, std::move(is_dungeon_workbench_mode),
49 std::move(set_dungeon_workflow_mode),
50 [this]() { return GetBottomReservedHeight(); }),
51 actions_registry_(std::make_unique<MoreActionsRegistry>()) {}
52
54
57 return 0.0f;
58 }
60}
61
62std::vector<std::string> ActivityBar::SortCategories(
63 const std::vector<std::string>& input,
64 const std::vector<std::string>& order,
65 const std::unordered_set<std::string>& pinned,
66 const std::unordered_set<std::string>& hidden) {
67 // visible preserves input order, filters hidden.
68 std::vector<std::string> visible;
69 visible.reserve(input.size());
70 std::unordered_set<std::string> visible_set;
71 visible_set.reserve(input.size());
72 for (const auto& c : input) {
73 if (hidden.count(c))
74 continue;
75 visible.push_back(c);
76 visible_set.insert(c);
77 }
78
79 // Pinned ∩ visible, in input order.
80 std::vector<std::string> pinned_visible;
81 std::unordered_set<std::string> pinned_visible_set;
82 for (const auto& c : input) {
83 if (visible_set.count(c) && pinned.count(c)) {
84 pinned_visible.push_back(c);
85 pinned_visible_set.insert(c);
86 }
87 }
88
89 // When the user has never customized the order, preserve the canonical
90 // input ordering for every non-pinned visible entry. Only once `order` has
91 // content do we split into "explicit order" + "true newcomers alphabetical".
92 std::vector<std::string> ordered;
93 std::vector<std::string> newcomers;
94
95 if (order.empty()) {
96 for (const auto& c : visible) {
97 if (pinned_visible_set.count(c))
98 continue;
99 ordered.push_back(c);
100 }
101 } else {
102 std::unordered_set<std::string> ordered_set;
103 for (const auto& c : order) {
104 if (!visible_set.count(c))
105 continue;
106 if (pinned_visible_set.count(c))
107 continue;
108 ordered.push_back(c);
109 ordered_set.insert(c);
110 }
111 std::unordered_set<std::string> order_set(order.begin(), order.end());
112 for (const auto& c : visible) {
113 if (pinned_visible_set.count(c))
114 continue;
115 if (order_set.count(c))
116 continue;
117 newcomers.push_back(c);
118 }
119 std::sort(newcomers.begin(), newcomers.end());
120 }
121
122 std::vector<std::string> result;
123 result.reserve(pinned_visible.size() + ordered.size() + newcomers.size());
124 result.insert(result.end(), pinned_visible.begin(), pinned_visible.end());
125 result.insert(result.end(), ordered.begin(), ordered.end());
126 result.insert(result.end(), newcomers.begin(), newcomers.end());
127 return result;
128}
129
131 size_t session_id, const std::string& active_category,
132 const std::vector<std::string>& all_categories,
133 const std::unordered_set<std::string>& active_editor_categories,
134 std::function<bool()> has_rom, std::function<bool()> is_rom_dirty,
135 std::function<int()> pending_dungeon_rooms) {
137 return;
138
139 // When the startup dashboard is active there are no meaningful left-panel
140 // cards; keep the activity rail visible but collapse the side panel.
141 const bool dashboard_active =
143 if (dashboard_active && window_manager_.IsSidebarExpanded()) {
145 }
146
148 session_id, active_category, all_categories, active_editor_categories,
149 has_rom, std::move(is_rom_dirty), std::move(pending_dungeon_rooms));
150
151 if (window_manager_.IsSidebarExpanded() && !dashboard_active) {
152 DrawSidePanel(session_id, active_category, has_rom);
153 }
154}
155
156void ActivityBar::DrawCategoryContextMenu(const std::string& category) {
157 if (!user_settings_)
158 return;
159
160 // ImGui generates a stable popup id from the last item by default, but we
161 // pass an explicit id so the popup survives ImGui::PushID changes.
162 std::string popup_id = absl::StrFormat("##SidebarCtx_%s", category);
163 if (!ImGui::BeginPopupContextItem(popup_id.c_str()))
164 return;
165
166 auto& prefs = user_settings_->prefs();
167 const bool is_pinned = prefs.sidebar_pinned.count(category) > 0;
168 const bool is_hidden = prefs.sidebar_hidden.count(category) > 0;
169
170 const char* pin_label = is_pinned ? "Unpin from top" : "Pin to top";
171 if (ImGui::MenuItem(pin_label)) {
172 if (is_pinned) {
173 prefs.sidebar_pinned.erase(category);
174 } else {
175 prefs.sidebar_pinned.insert(category);
176 }
177 PersistSettings(user_settings_);
178 }
179
180 const char* hide_label = is_hidden ? "Show on sidebar" : "Hide from sidebar";
181 if (ImGui::MenuItem(hide_label)) {
182 if (is_hidden) {
183 prefs.sidebar_hidden.erase(category);
184 } else {
185 prefs.sidebar_hidden.insert(category);
186 }
187 PersistSettings(user_settings_);
188 }
189
190 ImGui::Separator();
191 if (ImGui::MenuItem(tr("Reset Sidebar Order"))) {
192 prefs.sidebar_order.clear();
193 PersistSettings(user_settings_);
194 }
195 if (ImGui::MenuItem(tr("Show All Categories"))) {
196 prefs.sidebar_hidden.clear();
197 PersistSettings(user_settings_);
198 }
199
200 ImGui::EndPopup();
201}
202
203void ActivityBar::HandleReorderDragAndDrop(const std::string& category) {
204 if (!user_settings_)
205 return;
206 auto& prefs = user_settings_->prefs();
207
208 // Pinned items participate in pin grouping but not in drag-reorder — the
209 // pin block's order is driven by the registry's canonical order.
210 const bool is_pinned = prefs.sidebar_pinned.count(category) > 0;
211
212 if (!is_pinned &&
213 ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
214 ImGui::SetDragDropPayload(kSidebarDragPayload, category.data(),
215 category.size());
216 ImGui::TextUnformatted(category.c_str());
217 ImGui::EndDragDropSource();
218 }
219
220 if (ImGui::BeginDragDropTarget()) {
221 const ImGuiPayload* payload =
222 ImGui::AcceptDragDropPayload(kSidebarDragPayload);
223 if (payload != nullptr && payload->Data != nullptr) {
224 std::string src(static_cast<const char*>(payload->Data),
225 static_cast<size_t>(payload->DataSize));
226 if (src != category && !prefs.sidebar_pinned.count(src) &&
227 !prefs.sidebar_pinned.count(category)) {
228 auto& order = prefs.sidebar_order;
229 auto rm = std::remove(order.begin(), order.end(), src);
230 if (rm != order.end()) {
231 order.erase(rm, order.end());
232 }
233 auto dst = std::find(order.begin(), order.end(), category);
234 if (dst == order.end()) {
235 // Target wasn't tracked yet; keep moves local by appending.
236 order.push_back(src);
237 } else {
238 order.insert(dst, src);
239 }
240 PersistSettings(user_settings_);
241 }
242 }
243 ImGui::EndDragDropTarget();
244 }
245}
246
248 size_t session_id, const std::string& active_category,
249 const std::vector<std::string>& all_categories,
250 const std::unordered_set<std::string>& active_editor_categories,
251 std::function<bool()> has_rom, std::function<bool()> is_rom_dirty,
252 std::function<int()> pending_dungeon_rooms) {
253
254 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
255 const ImGuiViewport* viewport = ImGui::GetMainViewport();
256 const float top_inset = gui::LayoutHelpers::GetTopInset();
257 const auto safe_area = gui::LayoutHelpers::GetSafeAreaInsets();
258 const float bottom_reserved = GetBottomReservedHeight();
259 const float viewport_height =
260 std::max(0.0f, viewport->WorkSize.y - top_inset - safe_area.bottom -
261 bottom_reserved);
262 const float bar_width = gui::UIConfig::kActivityBarWidth;
263
264 constexpr ImGuiWindowFlags kExtraFlags =
265 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoFocusOnAppearing |
266 ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBringToFrontOnFocus;
267
268 gui::FixedPanel bar(
269 "##ActivityBar",
270 ImVec2(viewport->WorkPos.x, viewport->WorkPos.y + top_inset),
271 ImVec2(bar_width, viewport_height),
272 {.bg = gui::ConvertColorToImVec4(theme.surface),
273 .border = gui::ConvertColorToImVec4(theme.text_disabled),
274 .padding = {0.0f, 8.0f},
275 .spacing = {0.0f, 8.0f},
276 .border_size = 1.0f},
277 kExtraFlags);
278
279 if (bar) {
280
281 // Global Search / Command Palette at top
283 "Global Search (Ctrl+Shift+F)", false,
284 ImVec4(0, 0, 0, 0), "activity_bar",
285 "search")) {
287 }
288
289 // Separator
290 ImGui::Spacing();
291 ImVec2 sep_p1 = ImGui::GetCursorScreenPos();
292 ImVec2 sep_p2 =
293 ImVec2(sep_p1.x + gui::UIConfig::kActivityBarWidth, sep_p1.y);
294 ImGui::GetWindowDrawList()->AddLine(
295 sep_p1, sep_p2,
296 ImGui::ColorConvertFloat4ToU32(gui::ConvertColorToImVec4(theme.border)),
297 1.0f);
298 ImGui::Spacing();
299
300 bool rom_loaded = has_rom ? has_rom() : false;
301
302 // Apply per-user pin/order/hide prefs if available. Dashboard category
303 // always stays excluded regardless of prefs so we strip it first.
304 std::vector<std::string> filtered_input;
305 filtered_input.reserve(all_categories.size());
306 for (const auto& cat : all_categories) {
308 continue;
309 filtered_input.push_back(cat);
310 }
311
312 std::vector<std::string> effective = filtered_input;
313 if (user_settings_) {
314 const auto& prefs = user_settings_->prefs();
315 effective = SortCategories(filtered_input, prefs.sidebar_order,
316 prefs.sidebar_pinned, prefs.sidebar_hidden);
317
318 // Empty-state guard: if the user hid every category, silently reset
319 // the hidden set so the rail stays usable.
320 if (effective.empty() && !filtered_input.empty()) {
322 PersistSettings(user_settings_);
323 effective = filtered_input;
324 }
325 }
326
327 // Draw categories in effective order.
328 for (const auto& cat : effective) {
329 bool is_selected = (cat == active_category);
330 bool panel_expanded = window_manager_.IsSidebarExpanded();
331 bool has_active_editor = active_editor_categories.count(cat) > 0;
332
333 // Emulator is always available, others require ROM
334 bool category_enabled =
335 rom_loaded || (cat == "Emulator") || (cat == "Agent");
336
337 const EditorType editor_type =
339 const bool experimental =
341 const bool allow_experimental =
343 const bool blocked_experimental = experimental && !allow_experimental;
344 if (blocked_experimental) {
345 category_enabled = false;
346 }
347
348 // Get category-specific theme colors for expressive appearance
349 auto cat_theme = WorkspaceWindowManager::GetCategoryTheme(cat);
350 ImVec4 cat_color(cat_theme.r, cat_theme.g, cat_theme.b, cat_theme.a);
351 ImVec4 glow_color(cat_theme.glow_r, cat_theme.glow_g, cat_theme.glow_b,
352 1.0f);
353
354 // Active Indicator with category-specific colors
355 if (is_selected && category_enabled && panel_expanded) {
356 ImVec2 pos = ImGui::GetCursorScreenPos();
357
358 // Outer glow shadow (subtle, category color at 15% opacity)
359 ImVec4 outer_glow = glow_color;
360 outer_glow.w = 0.15f;
361 ImGui::GetWindowDrawList()->AddRectFilled(
362 ImVec2(pos.x - 1.0f, pos.y - 1.0f),
363 ImVec2(pos.x + 49.0f, pos.y + 41.0f),
364 ImGui::ColorConvertFloat4ToU32(outer_glow), 4.0f);
365
366 // Background highlight (category glow at 30% opacity)
367 ImVec4 highlight = glow_color;
368 highlight.w = 0.30f;
369 ImGui::GetWindowDrawList()->AddRectFilled(
370 pos, ImVec2(pos.x + 48.0f, pos.y + 40.0f),
371 ImGui::ColorConvertFloat4ToU32(highlight), 2.0f);
372
373 // Left accent border (4px wide, category-specific color)
374 ImGui::GetWindowDrawList()->AddRectFilled(
375 pos, ImVec2(pos.x + 4.0f, pos.y + 40.0f),
376 ImGui::ColorConvertFloat4ToU32(cat_color));
377 }
378
379 std::string icon = WorkspaceWindowManager::GetCategoryIcon(cat);
380
381 // Subtle indicator even when collapsed
382 if (is_selected && category_enabled && !panel_expanded) {
383 ImVec2 pos = ImGui::GetCursorScreenPos();
384 ImVec4 highlight = glow_color;
385 highlight.w = 0.15f;
386 ImGui::GetWindowDrawList()->AddRectFilled(
387 pos, ImVec2(pos.x + 48.0f, pos.y + 40.0f),
388 ImGui::ColorConvertFloat4ToU32(highlight), 2.0f);
389 ImVec4 accent = cat_color;
390 accent.w = 0.6f;
391 ImGui::GetWindowDrawList()->AddRectFilled(
392 pos, ImVec2(pos.x + 3.0f, pos.y + 40.0f),
393 ImGui::ColorConvertFloat4ToU32(accent));
394 }
395
396 // Dim indicator for categories whose editor is open but not currently
397 // selected. Makes "what's open" readable at a glance without competing
398 // with the full selection glow above.
399 if (!is_selected && category_enabled && has_active_editor) {
400 ImVec2 pos = ImGui::GetCursorScreenPos();
401 ImVec4 dim_accent = cat_color;
402 dim_accent.w = 0.35f;
403 ImGui::GetWindowDrawList()->AddRectFilled(
404 ImVec2(pos.x + 45.0f, pos.y + 8.0f),
405 ImVec2(pos.x + 48.0f, pos.y + 32.0f),
406 ImGui::ColorConvertFloat4ToU32(dim_accent), 1.5f);
407 }
408
409 // Pinned badge — small tick in the top-left corner.
410 bool is_pinned = user_settings_ &&
411 user_settings_->prefs().sidebar_pinned.count(cat) > 0;
412
413 // Always pass category color so inactive icons remain visible
414 ImVec4 icon_color = cat_color;
415 if (!category_enabled) {
416 ImGui::BeginDisabled();
417 }
419 nullptr, is_selected, icon_color,
420 "activity_bar", cat.c_str())) {
421 if (category_enabled) {
422 if (cat == active_category) {
423 // Explicit toggle only — selecting a category must not auto-open
424 // WindowSidebar (ActivityBar-only default).
426 } else {
429 }
430 }
431 }
432 if (!category_enabled) {
433 ImGui::EndDisabled();
434 }
435
436 // Context menu + drag-drop anchor on the icon's last-drawn rect.
439
440 if (is_pinned) {
441 ImVec2 pin_min = ImGui::GetItemRectMin();
442 ImVec4 pin_color = cat_color;
443 pin_color.w = 0.85f;
444 ImGui::GetWindowDrawList()->AddCircleFilled(
445 ImVec2(pin_min.x + 6.0f, pin_min.y + 6.0f), 2.5f,
446 ImGui::ColorConvertFloat4ToU32(pin_color));
447 }
448
449 const int pending_rooms =
450 pending_dungeon_rooms ? pending_dungeon_rooms() : 0;
451 const bool dungeon_pending = cat == "Dungeon" && pending_rooms > 0;
452
453 // Dirty-ROM dot badge on the currently selected category's icon.
454 // We draw after the button so it paints on top.
455 const bool rom_dirty = is_rom_dirty ? is_rom_dirty() : false;
456 if (is_selected && category_enabled && rom_dirty) {
457 ImVec2 last_min = ImGui::GetItemRectMin();
458 ImVec2 last_max = ImGui::GetItemRectMax();
459 ImVec2 dot_center(last_max.x - 7.0f, last_min.y + 7.0f);
460 ImVec4 dot_color = gui::ConvertColorToImVec4(theme.warning);
461 ImGui::GetWindowDrawList()->AddCircleFilled(
462 dot_center, 3.5f, ImGui::ColorConvertFloat4ToU32(dot_color));
463 }
464
465 if (category_enabled && dungeon_pending) {
466 ImVec2 last_min = ImGui::GetItemRectMin();
467 ImVec2 pending_center(last_min.x + 7.0f, last_min.y + 7.0f);
468 ImVec4 pending_color = gui::ConvertColorToImVec4(theme.warning);
469 ImGui::GetWindowDrawList()->AddCircleFilled(
470 pending_center, 3.0f,
471 ImGui::ColorConvertFloat4ToU32(pending_color));
472 }
473
474 // Tooltip with status information
475 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
476 ImGui::BeginTooltip();
477 ImGui::Text("%s %s", icon.c_str(), cat.c_str());
478 if (blocked_experimental) {
480 "In development — enable Experimental Editors in Settings",
481 gui::ConvertColorToImVec4(theme.warning));
482 } else if (!category_enabled) {
483 gui::ColoredText("Open ROM required",
484 gui::ConvertColorToImVec4(theme.warning));
485 } else if (has_active_editor) {
487 is_selected ? "Active editor" : "Editor open (click to focus)",
488 gui::ConvertColorToImVec4(theme.success));
489 } else {
490 gui::ColoredText("Click to view windows",
492 }
493 if (is_pinned) {
494 gui::ColoredText("Pinned (right-click to unpin)",
496 } else if (user_settings_) {
497 gui::ColoredText("Right-click for options • drag to reorder",
499 }
500 if (is_selected && rom_dirty) {
501 gui::ColoredText("ROM has unsaved changes",
502 gui::ConvertColorToImVec4(theme.warning));
503 }
504 if (dungeon_pending) {
506 "%d dungeon room%s pending apply", pending_rooms,
507 pending_rooms == 1 ? "" : "s");
508 }
509 ImGui::EndTooltip();
510 }
511 }
512 }
513
514 // Draw "More Actions" button at the bottom
515 ImGui::SetCursorPosY(viewport_height - 48.0f);
516
518 nullptr, false, ImVec4(0, 0, 0, 0),
519 "activity_bar", "more_actions")) {
520 ImGui::OpenPopup("ActivityBarMoreMenu");
521 }
522
523 if (ImGui::BeginPopup("ActivityBarMoreMenu")) {
524 if (actions_registry_ && !actions_registry_->empty()) {
525 actions_registry_->ForEach([](const MoreAction& action) {
526 std::string label;
527 if (action.icon != nullptr) {
528 label = absl::StrFormat("%s %s", action.icon, action.label);
529 } else {
530 label = action.label;
531 }
532 bool enabled = !action.enabled_fn || action.enabled_fn();
533 if (ImGui::MenuItem(label.c_str(), /*shortcut=*/nullptr,
534 /*selected=*/false, enabled)) {
535 if (action.on_invoke)
536 action.on_invoke();
537 }
538 });
539 } else {
540 ImGui::TextDisabled(tr("No actions available"));
541 }
542 ImGui::EndPopup();
543 }
544 // FixedPanel destructor handles End() + PopStyleVar/PopStyleColor
545}
546
547void ActivityBar::DrawSidePanel(size_t session_id, const std::string& category,
548 std::function<bool()> has_rom) {
549 window_sidebar_.Draw(session_id, category, std::move(has_rom));
550}
551
552void ActivityBar::DrawWindowBrowser(size_t session_id, bool* p_open) {
553 window_browser_.Draw(session_id, p_open);
554}
555
556} // namespace editor
557} // namespace yaze
ActivityBar(WorkspaceWindowManager &window_manager, std::function< bool()> is_dungeon_workbench_mode={}, std::function< void(bool)> set_dungeon_workflow_mode={})
void DrawSidePanel(size_t session_id, const std::string &category, std::function< bool()> has_rom)
void DrawWindowBrowser(size_t session_id, bool *p_open)
WindowBrowser window_browser_
UserSettings * user_settings_
std::unique_ptr< MoreActionsRegistry > actions_registry_
void Render(size_t session_id, const std::string &active_category, const std::vector< std::string > &all_categories, const std::unordered_set< std::string > &active_editor_categories, std::function< bool()> has_rom, std::function< bool()> is_rom_dirty={}, std::function< int()> pending_dungeon_rooms={})
WindowSidebar window_sidebar_
void DrawCategoryContextMenu(const std::string &category)
WorkspaceWindowManager & window_manager_
static std::vector< std::string > SortCategories(const std::vector< std::string > &input, const std::vector< std::string > &order, const std::unordered_set< std::string > &pinned, const std::unordered_set< std::string > &hidden)
void HandleReorderDragAndDrop(const std::string &category)
void DrawActivityBarStrip(size_t session_id, const std::string &active_category, const std::vector< std::string > &all_categories, const std::unordered_set< std::string > &active_editor_categories, std::function< bool()> has_rom, std::function< bool()> is_rom_dirty, std::function< int()> pending_dungeon_rooms)
float GetBottomReservedHeight() const
static EditorType GetEditorTypeFromCategory(const std::string &category)
static bool IsExperimentalEditor(EditorType type)
static constexpr float kStatusBarHeight
Definition status_bar.h:241
Manages user preferences and settings persistence.
void Draw(size_t session_id, bool *p_open)
void Draw(size_t session_id, const std::string &category, std::function< bool()> has_rom)
Central registry for all editor cards with session awareness and dependency injection.
static CategoryTheme GetCategoryTheme(const std::string &category)
void SetActiveCategory(const std::string &category, bool notify=true)
static std::string GetCategoryIcon(const std::string &category)
static constexpr const char * kDashboardCategory
void TriggerCategorySelected(const std::string &category)
void SetSidebarExpanded(bool expanded, bool notify=true)
RAII for fixed-position panels (activity bar, side panel, status bar).
static SafeAreaInsets GetSafeAreaInsets()
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
#define ICON_MD_SEARCH
Definition icons.h:1673
#define ICON_MD_MORE_HORIZ
Definition icons.h:1241
bool TransparentIconButton(const char *icon, const ImVec2 &size, const char *tooltip, bool is_active, const ImVec4 &active_color, const char *panel_id, const char *anim_id)
Draw a transparent icon button (hover effect only).
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:134
void ColoredText(const char *text, const ImVec4 &color)
ImVec4 GetTextSecondaryVec4()
void ColoredTextF(const ImVec4 &color, const char *fmt,...)
std::unordered_set< std::string > sidebar_pinned
std::unordered_set< std::string > sidebar_hidden
static constexpr float kActivityBarWidth
Definition ui_config.h:18