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