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