yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
piano_roll_view.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cfloat>
5#include <cmath>
6
7#include "absl/strings/str_format.h"
12#include "imgui/imgui.h"
14
15namespace yaze {
16namespace editor {
17namespace music {
18
19using namespace yaze::zelda3::music;
20
21namespace {
22
24 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
26 p.white_key = ImGui::GetColorU32(gui::ConvertColorToImVec4(theme.surface));
27 p.black_key = ImGui::GetColorU32(gui::ConvertColorToImVec4(theme.child_bg));
28 auto grid = theme.separator;
29 grid.alpha = 0.35f;
30 p.grid_major = ImGui::GetColorU32(gui::ConvertColorToImVec4(grid));
31 grid.alpha = 0.18f;
32 p.grid_minor = ImGui::GetColorU32(gui::ConvertColorToImVec4(grid));
33 p.note = ImGui::GetColorU32(gui::ConvertColorToImVec4(theme.accent));
34 auto hover = theme.accent;
35 hover.alpha = 0.85f;
36 p.note_hover = ImGui::GetColorU32(gui::ConvertColorToImVec4(hover));
37 // Shadow for notes - darker version of background
38 auto shadow = theme.border_shadow;
39 shadow.alpha = 0.4f;
40 p.note_shadow = ImGui::GetColorU32(gui::ConvertColorToImVec4(shadow));
41 p.background =
42 ImGui::GetColorU32(gui::ConvertColorToImVec4(theme.editor_background));
43 auto label = theme.text_secondary;
44 label.alpha = 0.85f;
45 p.key_label = ImGui::GetColorU32(gui::ConvertColorToImVec4(label));
46 // Beat markers - slightly brighter than grid
47 auto beat = theme.accent;
48 beat.alpha = 0.25f;
49 p.beat_marker = ImGui::GetColorU32(gui::ConvertColorToImVec4(beat));
50 // Octave divider lines
51 auto octave = theme.separator;
52 octave.alpha = 0.5f;
53 p.octave_line = ImGui::GetColorU32(gui::ConvertColorToImVec4(octave));
54 return p;
55}
56
57bool IsBlackKey(int semitone) {
58 int s = semitone % 12;
59 return (s == 1 || s == 3 || s == 6 || s == 8 || s == 10);
60}
61
62int CountNotesInTrack(const MusicTrack& track) {
63 int count = 0;
64 for (const auto& evt : track.events) {
65 if (evt.type == TrackEvent::Type::Note)
66 count++;
67 }
68 return count;
69}
70
71int GetChannelInstrument(const MusicTrack& track, int fallback) {
72 int inst = fallback;
73 for (const auto& evt : track.events) {
74 if (evt.type == TrackEvent::Type::Command &&
75 evt.command.opcode ==
76 static_cast<uint8_t>(CommandType::SetInstrument)) {
77 inst = evt.command.params[0];
78 }
79 }
80 return inst;
81}
82
83} // namespace
84
85void PianoRollView::Draw(MusicSong* song, const MusicBank* bank) {
86 if (!song || song->segments.empty()) {
87 ImGui::TextDisabled("No song loaded");
88 return;
89 }
90
91 // Initialize channel colors if needed
92 if (channel_colors_.empty()) {
93 channel_colors_.resize(8);
94 channel_colors_[0] = 0xFFFF6B6B; // Coral Red
95 channel_colors_[1] = 0xFF4ECDC4; // Teal
96 channel_colors_[2] = 0xFF45B7D1; // Sky Blue
97 channel_colors_[3] = 0xFFF7DC6F; // Soft Yellow
98 channel_colors_[4] = 0xFFBB8FCE; // Lavender
99 channel_colors_[5] = 0xFF82E0AA; // Mint Green
100 channel_colors_[6] = 0xFFF8B500; // Amber
101 channel_colors_[7] = 0xFFE59866; // Peach
102 }
103
104 const RollPalette palette = GetPalette();
105 active_segment_index_ = std::clamp(
106 active_segment_index_, 0, static_cast<int>(song->segments.size()) - 1);
108
109 {
110 gui::StyleVarGuard toolbar_padding(ImGuiStyleVar_WindowPadding,
111 ImVec2(8, 4));
112 if (ImGui::BeginChild(
113 "##PianoRollToolbar", ImVec2(0, kToolbarHeight),
114 ImGuiChildFlags_AlwaysUseWindowPadding,
115 ImGuiWindowFlags_NoScrollbar |
116 ImGuiWindowFlags_NoScrollWithMouse)) {
117 DrawToolbar(song, bank);
118 }
119 ImGui::EndChild();
120 }
121
122 ImGui::Separator();
123
124 ImGuiStyle& style = ImGui::GetStyle();
125 float available_height = ImGui::GetContentRegionAvail().y;
126 float reserved_for_status = kStatusBarHeight + style.ItemSpacing.y;
127 float main_height = std::max(0.0f, available_height - reserved_for_status);
128
129 if (ImGui::BeginChild(
130 "PianoRollMain", ImVec2(0, main_height), ImGuiChildFlags_None,
131 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
132 // === MAIN CONTENT ===
133 gui::StyleVarGuard content_style_guard(
134 {{ImGuiStyleVar_CellPadding, ImVec2(0, 0)},
135 {ImGuiStyleVar_FramePadding, ImVec2(0, 0)}});
136 const float layout_height = ImGui::GetContentRegionAvail().y;
137 const ImGuiTableFlags table_flags =
138 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV |
139 ImGuiTableFlags_NoPadOuterX | ImGuiTableFlags_NoPadInnerX;
140 if (ImGui::BeginTable("PianoRollLayout", 2, table_flags,
141 ImVec2(-FLT_MIN, layout_height))) {
142 ImGui::TableSetupColumn(
143 "Channels",
144 ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHide |
145 ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_NoReorder,
147 ImGui::TableSetupColumn("Roll", ImGuiTableColumnFlags_WidthStretch);
148 // Snap row height to a whole number of key rows to avoid partial stretch
149 float snapped_row_height = layout_height;
150 if (key_height_ > 0.0f) {
151 float rows = std::floor(layout_height / key_height_);
152 if (rows >= 1.0f) {
153 snapped_row_height = rows * key_height_;
154 }
155 }
156 ImGui::TableNextRow(ImGuiTableRowFlags_None, snapped_row_height);
157
158 // --- Left Column: Channel List ---
159 ImGui::TableSetColumnIndex(0);
160 const ImGuiChildFlags channel_child_flags =
161 ImGuiChildFlags_Border | ImGuiChildFlags_AlwaysUseWindowPadding;
162 if (ImGui::BeginChild("PianoRollChannelList",
163 ImVec2(-FLT_MIN, layout_height),
164 channel_child_flags,
165 ImGuiWindowFlags_NoScrollbar |
166 ImGuiWindowFlags_NoScrollWithMouse)) {
167 DrawChannelList(song);
168 }
169 ImGui::EndChild();
170
171 // --- Right Column: Piano Roll ---
172 ImGui::TableSetColumnIndex(1);
176
177 ImVec2 roll_area = ImGui::GetContentRegionAvail();
178 DrawRollCanvas(song, palette, roll_area);
179
180 ImGui::EndTable();
181 }
182 }
183 ImGui::EndChild();
184
185 // === STATUS BAR (Fixed Height) ===
186 ImGui::Separator();
187 DrawStatusBar(song);
188
189 // Context menus
190 if (ImGui::BeginPopup("PianoRollNoteContext")) {
191 if (song && context_target_.segment >= 0 && context_target_.channel >= 0 &&
193 context_target_.segment < (int)song->segments.size()) {
194 auto& track = song->segments[context_target_.segment]
195 .tracks[context_target_.channel];
196 if (context_target_.event_index < (int)track.events.size()) {
197 auto& evt = track.events[context_target_.event_index];
198 if (evt.type == TrackEvent::Type::Note) {
199 ImGui::Text(ICON_MD_MUSIC_NOTE " Note %s",
200 evt.note.GetNoteName().c_str());
201 ImGui::Text("Tick: %d", evt.tick);
202 ImGui::Separator();
203
204 // Velocity slider (0-127)
205 ImGui::Text("Velocity:");
206 ImGui::SameLine();
207 int velocity = evt.note.velocity;
208 ImGui::SetNextItemWidth(120);
209 if (ImGui::SliderInt("##velocity", &velocity, 0, 127)) {
210 if (on_edit_) {
211 on_edit_();
212 }
213 evt.note.velocity = static_cast<uint8_t>(velocity);
214 }
215 if (ImGui::IsItemHovered()) {
216 ImGui::SetTooltip("Articulation/velocity (0 = default)");
217 }
218
219 // Duration slider (1-192 ticks, quarter = 72)
220 ImGui::Text("Duration:");
221 ImGui::SameLine();
222 int duration = evt.note.duration;
223 ImGui::SetNextItemWidth(120);
224 if (ImGui::SliderInt("##duration", &duration, 1, 192)) {
225 if (on_edit_) {
226 on_edit_();
227 }
228 evt.note.duration = static_cast<uint8_t>(duration);
229 }
230 if (ImGui::IsItemHovered()) {
231 ImGui::SetTooltip("Duration in ticks (quarter = 72)");
232 }
233
234 ImGui::Separator();
235 ImGui::Text("Quick Duration:");
236 if (ImGui::MenuItem("Whole (288)")) {
237 if (on_edit_) {
238 on_edit_();
239 }
240 evt.note.duration = 0xFE; // Max duration
241 }
242 if (ImGui::MenuItem("Half (144)")) {
243 if (on_edit_) {
244 on_edit_();
245 }
246 evt.note.duration = 144;
247 }
248 if (ImGui::MenuItem("Quarter (72)")) {
249 if (on_edit_) {
250 on_edit_();
251 }
252 evt.note.duration = kDurationQuarter;
253 }
254 if (ImGui::MenuItem("Eighth (36)")) {
255 if (on_edit_) {
256 on_edit_();
257 }
258 evt.note.duration = kDurationEighth;
259 }
260 if (ImGui::MenuItem("Sixteenth (18)")) {
261 if (on_edit_) {
262 on_edit_();
263 }
264 evt.note.duration = kDurationSixteenth;
265 }
266 if (ImGui::MenuItem("32nd (9)")) {
267 if (on_edit_) {
268 on_edit_();
269 }
270 evt.note.duration = kDurationThirtySecond;
271 }
272
273 ImGui::Separator();
274 if (ImGui::MenuItem(ICON_MD_CONTENT_COPY " Duplicate")) {
275 if (on_edit_) {
276 on_edit_();
277 }
278 TrackEvent copy = evt;
279 copy.tick += evt.note.duration;
280 track.InsertEvent(copy);
281 }
282 if (ImGui::MenuItem(ICON_MD_DELETE " Delete", "Del")) {
283 if (on_edit_) {
284 on_edit_();
285 }
286 track.RemoveEvent(context_target_.event_index);
287 }
288 }
289 }
290 }
291 ImGui::EndPopup();
292 }
293
294 if (ImGui::BeginPopup("PianoRollEmptyContext")) {
295 if (song && empty_context_.segment >= 0 &&
296 empty_context_.segment < (int)song->segments.size() &&
298 empty_context_.tick >= 0) {
299 ImGui::Text(ICON_MD_ADD " Add Note");
300 ImGui::Separator();
301 if (ImGui::MenuItem("Quarter note")) {
302 auto& t = song->segments[empty_context_.segment]
303 .tracks[empty_context_.channel];
306 if (on_edit_) {
307 on_edit_();
308 }
309 t.InsertEvent(evt);
312 }
313 if (ImGui::MenuItem("Eighth note")) {
314 auto& t = song->segments[empty_context_.segment]
315 .tracks[empty_context_.channel];
318 if (on_edit_) {
319 on_edit_();
320 }
321 t.InsertEvent(evt);
324 }
325 if (ImGui::MenuItem("Sixteenth note")) {
326 auto& t = song->segments[empty_context_.segment]
327 .tracks[empty_context_.channel];
330 if (on_edit_) {
331 on_edit_();
332 }
333 t.InsertEvent(evt);
336 }
337 }
338 ImGui::EndPopup();
339 }
340}
341
343 const ImVec2& canvas_size_param) {
344 const auto& segment = song->segments[active_segment_index_];
345 const ImGuiStyle& style = ImGui::GetStyle();
346
347 // Normalize zoom to whole pixels to avoid sub-pixel stretching on rows.
348 key_height_ = std::clamp(std::round(key_height_), 6.0f, 24.0f);
349 pixels_per_tick_ = std::clamp(pixels_per_tick_, 0.5f, 10.0f);
350
351 // Reserve layout space and fetch actual rect
352 ImVec2 reserved_size = canvas_size_param;
353 reserved_size.x = std::max(reserved_size.x, 1.0f);
354 reserved_size.y = std::max(reserved_size.y, 1.0f);
355 ImGui::InvisibleButton("##PianoRollCanvasHitbox", reserved_size,
356 ImGuiButtonFlags_MouseButtonLeft |
357 ImGuiButtonFlags_MouseButtonRight |
358 ImGuiButtonFlags_MouseButtonMiddle);
359 ImVec2 canvas_pos = ImGui::GetItemRectMin();
360 canvas_pos.x = std::floor(canvas_pos.x);
361 canvas_pos.y = std::floor(canvas_pos.y);
362 ImVec2 canvas_size = ImGui::GetItemRectSize();
363
364 ImDrawList* draw_list = ImGui::GetWindowDrawList();
365 const bool hovered =
366 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
367 const bool active = ImGui::IsItemActive();
368
369 // Content dimensions
370 float total_height =
373 uint32_t duration = segment.GetDuration();
374 if (duration == 0)
375 duration = 1000;
376 duration += 48; // padding for edits
377 float content_width = duration * pixels_per_tick_;
378
379 // Visible region (account for optional scrollbars)
380 bool show_h_scroll = content_width > (canvas_size.x - key_width_);
381 bool show_v_scroll = total_height > canvas_size.y;
382 float grid_width =
383 std::max(0.0f, canvas_size.x - key_width_ -
384 (show_v_scroll ? style.ScrollbarSize : 0.0f));
385 float grid_height = std::max(
386 0.0f, canvas_size.y - (show_h_scroll ? style.ScrollbarSize : 0.0f));
387 grid_height = std::floor(grid_height);
388 grid_height = std::min(grid_height, total_height);
389 // Snap the visible grid height to whole key rows to avoid partial stretch at the top/bottom.
390 if (grid_height > key_height_) {
391 float snapped_grid = std::floor(grid_height / key_height_) * key_height_;
392 if (snapped_grid > 0.0f)
393 grid_height = snapped_grid;
394 }
395
396 // Zoom/scroll interactions
397 const ImVec2 mouse = ImGui::GetMousePos();
398 if (hovered) {
399 float wheel = ImGui::GetIO().MouseWheel;
400 if (wheel != 0.0f) {
401 bool ctrl = ImGui::GetIO().KeyCtrl;
402 bool shift = ImGui::GetIO().KeyShift;
403 if (ctrl) {
404 float old_ppt = pixels_per_tick_;
406 std::clamp(pixels_per_tick_ + wheel * 0.5f, 0.5f, 10.0f);
407 float rel_x = mouse.x - canvas_pos.x + scroll_x_px_;
408 scroll_x_px_ = std::max(0.0f, rel_x * (pixels_per_tick_ / old_ppt) -
409 (mouse.x - canvas_pos.x));
410 } else if (shift) {
411 float old_kh = key_height_;
412 key_height_ = std::clamp(key_height_ + wheel * 2.0f, 6.0f, 24.0f);
413 float rel_y = mouse.y - canvas_pos.y + scroll_y_px_;
414 scroll_y_px_ = std::max(
415 0.0f, rel_y * (key_height_ / old_kh) - (mouse.y - canvas_pos.y));
416 } else {
417 scroll_y_px_ -= wheel * key_height_ * 3.0f;
418 }
419 }
420
421 float wheel_h = ImGui::GetIO().MouseWheelH;
422 if (wheel_h != 0.0f) {
423 scroll_x_px_ -= wheel_h * pixels_per_tick_ * 10.0f;
424 }
425 }
426
427 if (active && ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) {
428 ImVec2 delta = ImGui::GetIO().MouseDelta;
429 scroll_x_px_ -= delta.x;
430 scroll_y_px_ -= delta.y;
431 }
432
433 // Clamp scroll to content
434 float max_scroll_x = std::max(0.0f, content_width - grid_width);
435 float max_scroll_y = std::max(0.0f, total_height - grid_height);
436 scroll_x_px_ = std::clamp(scroll_x_px_, 0.0f, max_scroll_x);
437 scroll_y_px_ = std::clamp(scroll_y_px_, 0.0f, max_scroll_y);
438
439 // Align key origin so we don't stretch top/bottom rows when partially visible.
440 float key_scroll_step = key_height_;
441 // Snap vertical scroll to full key heights to avoid stretched partial rows.
442 scroll_y_px_ = std::round(scroll_y_px_ / key_scroll_step) * key_scroll_step;
443 scroll_y_px_ = std::clamp(scroll_y_px_, 0.0f, max_scroll_y);
444 float scroll_y_aligned = scroll_y_px_;
445 float fractional = 0.0f;
446
447 // Compute drawing origins
448 ImVec2 key_origin(canvas_pos.x, canvas_pos.y - fractional);
449 ImVec2 grid_origin(key_origin.x + key_width_ - scroll_x_px_, key_origin.y);
450 key_origin.y = std::floor(key_origin.y);
451 grid_origin.y = std::floor(grid_origin.y);
452
453 int num_keys =
455 int start_key_idx = static_cast<int>(scroll_y_aligned / key_height_);
456 start_key_idx = std::clamp(start_key_idx, 0, num_keys - 1);
457 int visible_keys =
458 std::min(num_keys - start_key_idx,
459 std::max(0, static_cast<int>(grid_height / key_height_) + 2));
460 int max_start = std::max(0, num_keys - visible_keys);
461 start_key_idx = std::min(start_key_idx, max_start);
462
463 float clip_bottom =
464 std::min(canvas_pos.y + grid_height, key_origin.y + total_height);
465
466 ImVec2 clip_min = canvas_pos;
467 ImVec2 clip_max = ImVec2(canvas_pos.x + key_width_ + grid_width,
468 canvas_pos.y + grid_height);
469
470 draw_list->AddRectFilled(clip_min, clip_max, palette.background);
471 draw_list->PushClipRect(clip_min, clip_max, true);
472
473 DrawPianoKeys(draw_list, key_origin, total_height, start_key_idx,
474 visible_keys, palette);
475
476 int start_tick =
477 std::max(0, static_cast<int>(scroll_x_px_ / pixels_per_tick_) - 1);
478 int visible_ticks = static_cast<int>(grid_width / pixels_per_tick_) + 2;
479
480 DrawGrid(draw_list, grid_origin, canvas_pos,
481 ImVec2(key_width_ + grid_width, grid_height), total_height,
482 clip_bottom, start_tick, visible_ticks, start_key_idx, visible_keys,
483 content_width, palette);
484
485 DrawNotes(draw_list, song, grid_origin, total_height, start_tick,
486 start_tick + visible_ticks, start_key_idx, visible_keys, palette);
487
489 grid_origin, ImVec2(content_width, total_height), hovered);
490
491 // Draw playback cursor (clipped to visible region) - show even when paused
492 if (is_playing_ || is_paused_) {
493 uint32_t segment_start = 0;
494 for (int i = 0; i < active_segment_index_; ++i) {
495 segment_start += song->segments[i].GetDuration();
496 }
497 DrawPlaybackCursor(draw_list, grid_origin, grid_height, segment_start);
498
500 playback_tick_ >= segment_start) {
501 uint32_t local_tick = playback_tick_ - segment_start;
502 float cursor_x = local_tick * pixels_per_tick_;
503 float visible_width = std::max(grid_width, 1.0f);
504 if (cursor_x > scroll_x_px_ + visible_width - 100 ||
505 cursor_x < scroll_x_px_ + 50) {
507 std::clamp(cursor_x - visible_width / 3.0f, 0.0f, max_scroll_x);
508 }
509 }
510 }
511
512 draw_list->PopClipRect();
513
514 // Custom lightweight scrollbars (overlay, not driving layout)
515 ImU32 scrollbar_bg = ImGui::GetColorU32(ImGuiCol_ScrollbarBg);
516 ImU32 scrollbar_grab = ImGui::GetColorU32(ImGuiCol_ScrollbarGrab);
517 ImU32 scrollbar_grab_active =
518 ImGui::GetColorU32(ImGuiCol_ScrollbarGrabActive);
519
520 if (show_h_scroll && grid_width > 1.0f) {
521 ImVec2 track_min(canvas_pos.x + key_width_, canvas_pos.y + grid_height);
522 ImVec2 track_size(grid_width, style.ScrollbarSize);
523
524 float thumb_ratio = grid_width / content_width;
525 float thumb_w = std::max(style.GrabMinSize, track_size.x * thumb_ratio);
526 float thumb_x =
527 track_min.x + (max_scroll_x > 0.0f ? (scroll_x_px_ / max_scroll_x) *
528 (track_size.x - thumb_w)
529 : 0.0f);
530
531 ImVec2 thumb_min(thumb_x, track_min.y);
532 ImVec2 thumb_max(thumb_x + thumb_w, track_min.y + track_size.y);
533
534 ImVec2 h_rect_max(track_min.x + track_size.x, track_min.y + track_size.y);
535 bool h_hover = ImGui::IsMouseHoveringRect(track_min, h_rect_max);
536 bool h_active = h_hover && ImGui::IsMouseDown(ImGuiMouseButton_Left);
537 if (h_hover && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
538 float rel = (ImGui::GetIO().MousePos.x - track_min.x - thumb_w * 0.5f) /
539 std::max(1.0f, track_size.x - thumb_w);
540 scroll_x_px_ = std::clamp(rel * max_scroll_x, 0.0f, max_scroll_x);
541 }
542
543 draw_list->AddRectFilled(
544 track_min,
545 ImVec2(track_min.x + track_size.x, track_min.y + track_size.y),
546 scrollbar_bg, style.ScrollbarRounding);
547 draw_list->AddRectFilled(thumb_min, thumb_max,
548 h_active ? scrollbar_grab_active : scrollbar_grab,
549 style.ScrollbarRounding);
550 }
551
552 if (show_v_scroll && grid_height > 1.0f) {
553 ImVec2 track_min(canvas_pos.x + key_width_ + grid_width, canvas_pos.y);
554 ImVec2 track_size(style.ScrollbarSize, grid_height);
555
556 float thumb_ratio = grid_height / total_height;
557 float thumb_h = std::max(style.GrabMinSize, track_size.y * thumb_ratio);
558 float thumb_y =
559 track_min.y + (max_scroll_y > 0.0f ? (scroll_y_px_ / max_scroll_y) *
560 (track_size.y - thumb_h)
561 : 0.0f);
562
563 ImVec2 thumb_min(track_min.x, thumb_y);
564 ImVec2 thumb_max(track_min.x + track_size.x, thumb_y + thumb_h);
565
566 ImVec2 v_rect_max(track_min.x + track_size.x, track_min.y + track_size.y);
567 bool v_hover = ImGui::IsMouseHoveringRect(track_min, v_rect_max);
568 bool v_active = v_hover && ImGui::IsMouseDown(ImGuiMouseButton_Left);
569 if (v_hover && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
570 float rel = (ImGui::GetIO().MousePos.y - track_min.y - thumb_h * 0.5f) /
571 std::max(1.0f, track_size.y - thumb_h);
572 scroll_y_px_ = std::clamp(rel * max_scroll_y, 0.0f, max_scroll_y);
573 }
574
575 draw_list->AddRectFilled(
576 track_min,
577 ImVec2(track_min.x + track_size.x, track_min.y + track_size.y),
578 scrollbar_bg, style.ScrollbarRounding);
579 draw_list->AddRectFilled(thumb_min, thumb_max,
580 v_active ? scrollbar_grab_active : scrollbar_grab,
581 style.ScrollbarRounding);
582 }
583
584 // Cursor already advanced by the InvisibleButton reservation.
585}
586
587void PianoRollView::DrawToolbar(const MusicSong* song, const MusicBank* bank) {
588 // --- Transport Group ---
589 if (song) {
590 if (ImGui::Button(ICON_MD_PLAY_ARROW)) {
593 }
594 }
595 if (ImGui::IsItemHovered())
596 ImGui::SetTooltip("Play Segment");
597 }
598
599 ImGui::SameLine();
600 ImGui::TextDisabled("|");
601 ImGui::SameLine();
602
603 // --- Song/Segment Group ---
604 if (song) {
605 ImGui::TextDisabled(ICON_MD_MUSIC_NOTE);
606 ImGui::SameLine();
607 ImGui::Text("%s", song->name.empty() ? "Untitled" : song->name.c_str());
608
609 ImGui::SameLine();
610 ImGui::SetNextItemWidth(100.0f);
611 std::string seg_label = absl::StrFormat(
612 "Seg %d/%d", active_segment_index_ + 1, (int)song->segments.size());
613 if (ImGui::BeginCombo("##SegmentSelect", seg_label.c_str())) {
614 for (int i = 0; i < (int)song->segments.size(); ++i) {
615 bool is_selected = (i == active_segment_index_);
616 std::string label = absl::StrFormat("Segment %d", i + 1);
617 if (ImGui::Selectable(label.c_str(), is_selected)) {
619 }
620 if (is_selected)
621 ImGui::SetItemDefaultFocus();
622 }
623 ImGui::EndCombo();
624 }
625 }
626
627 // --- Instrument Group ---
628 if (bank) {
629 // Sync preview instrument to active channel's last SetInstrument command.
630 const auto& segment = song->segments[active_segment_index_];
631 int channel_inst = GetChannelInstrument(
633 channel_inst = std::clamp(channel_inst, 0,
634 static_cast<int>(bank->GetInstrumentCount() - 1));
635 if (channel_inst != preview_instrument_index_) {
636 preview_instrument_index_ = channel_inst;
637 }
638
639 ImGui::SameLine();
640 ImGui::TextDisabled("|");
641 ImGui::SameLine();
642
643 ImGui::TextDisabled(ICON_MD_PIANO);
644 ImGui::SameLine();
645 ImGui::SetNextItemWidth(120.0f);
646 const auto* inst = bank->GetInstrument(preview_instrument_index_);
647 std::string preview =
648 inst ? absl::StrFormat("%02X: %s", preview_instrument_index_,
649 inst->name.c_str())
650 : absl::StrFormat("%02X", preview_instrument_index_);
651 if (ImGui::BeginCombo("##InstSelect", preview.c_str())) {
652 for (size_t i = 0; i < bank->GetInstrumentCount(); ++i) {
653 const auto* item = bank->GetInstrument(i);
654 bool is_selected = (static_cast<int>(i) == preview_instrument_index_);
655 if (ImGui::Selectable(
656 absl::StrFormat("%02X: %s", i, item->name.c_str()).c_str(),
657 is_selected)) {
658 preview_instrument_index_ = static_cast<int>(i);
659 }
660 if (is_selected)
661 ImGui::SetItemDefaultFocus();
662 }
663 ImGui::EndCombo();
664 }
665 if (ImGui::IsItemHovered()) {
666 ImGui::SetTooltip("Instrument for new notes");
667 }
668 }
669
670 ImGui::SameLine();
671 ImGui::TextDisabled("|");
672 ImGui::SameLine();
673
674 // --- Zoom Group ---
675 ImGui::TextDisabled(ICON_MD_ZOOM_IN);
676 ImGui::SameLine();
677 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4.0f);
678 gui::SliderFloatWheel("##ZoomX", &pixels_per_tick_, 0.5f, 10.0f, "%.1f", 0.2f,
679 ImGuiSliderFlags_AlwaysClamp);
680 if (ImGui::IsItemHovered())
681 ImGui::SetTooltip("Horizontal Zoom (px/tick)");
682
683 ImGui::SameLine();
684 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.0f);
685 gui::SliderFloatWheel("##ZoomY", &key_height_, 6.0f, 24.0f, "%.0f", 0.5f,
686 ImGuiSliderFlags_AlwaysClamp);
687 if (ImGui::IsItemHovered())
688 ImGui::SetTooltip("Vertical Zoom (px/key)");
689
690 ImGui::SameLine();
691 ImGui::TextDisabled("|");
692 ImGui::SameLine();
693
694 // --- Snap/Grid Group ---
695 ImGui::TextDisabled(ICON_MD_GRID_ON);
696 ImGui::SameLine();
697
698 bool snap_active = snap_enabled_;
699 {
700 std::optional<gui::StyleColorGuard> snap_guard;
701 if (snap_active) {
702 snap_guard.emplace(ImGuiCol_Button,
703 ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
704 }
705 if (ImGui::Button("Snap")) {
707 }
708 }
709
710 ImGui::SameLine();
711 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 2.5f);
712 const char* snap_labels[] = {"1/4", "1/8", "1/16"};
713 int snap_idx = 2;
715 snap_idx = 0;
716 else if (snap_ticks_ == kDurationEighth)
717 snap_idx = 1;
718
719 if (ImGui::Combo("##SnapValue", &snap_idx, snap_labels,
720 IM_ARRAYSIZE(snap_labels))) {
721 snap_enabled_ = true;
722 snap_ticks_ = (snap_idx == 0) ? kDurationQuarter
723 : (snap_idx == 1) ? kDurationEighth
725 }
726}
727
729 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
730 gui::StyleVarGuard channel_style_guard(
731 {{ImGuiStyleVar_ItemSpacing, ImVec2(4, 4)},
732 {ImGuiStyleVar_FramePadding, ImVec2(6, 4)}});
733
734 ImGui::TextDisabled(ICON_MD_PIANO " Channels");
735 ImGui::Separator();
736
737 const auto& segment = song->segments[active_segment_index_];
738 ImVec2 button_size(ImGui::GetTextLineHeight() * 1.4f,
739 ImGui::GetTextLineHeight() * 1.4f);
740
741 for (int i = 0; i < 8; ++i) {
742 ImGui::PushID(i);
743
744 bool is_active = (active_channel_index_ == i);
745
746 // Highlight active channel row with theme accent overlay
747 if (is_active) {
748 ImVec2 row_min = ImGui::GetCursorScreenPos();
749 ImVec2 row_max =
750 ImVec2(row_min.x + ImGui::GetContentRegionAvail().x,
751 row_min.y + ImGui::GetTextLineHeightWithSpacing() + 4);
752 ImVec4 active_bg = gui::ConvertColorToImVec4(theme.accent);
753 active_bg.w *= 0.12f;
754 ImGui::GetWindowDrawList()->AddRectFilled(
755 row_min, row_max, ImGui::GetColorU32(active_bg), 4.0f);
756 }
757
758 // Color indicator (clickable to select channel)
759 ImVec4 col_v4 = ImGui::ColorConvertU32ToFloat4(channel_colors_[i]);
760 if (ImGui::ColorButton(
761 "##Col", col_v4,
762 ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoBorder,
763 ImVec2(18, 18))) {
765 channel_visible_[i] = true;
766 }
767
768 // Context menu for color picker
769 if (ImGui::BeginPopupContextItem("ChannelContext")) {
770 if (ImGui::ColorPicker4("##picker", (float*)&col_v4,
771 ImGuiColorEditFlags_NoSidePreview |
772 ImGuiColorEditFlags_NoSmallPreview)) {
773 channel_colors_[i] = ImGui::ColorConvertFloat4ToU32(col_v4);
774 }
775 ImGui::EndPopup();
776 }
777
778 ImGui::SameLine();
779
780 // Mute button (icon, themed)
781 bool muted = channel_muted_[i];
782 ImVec4 base_bg = gui::ConvertColorToImVec4(theme.surface);
783 base_bg.w *= 0.6f;
784 ImVec4 mute_active = gui::ConvertColorToImVec4(theme.accent);
785 mute_active.w = std::min(1.0f, mute_active.w * 0.85f);
786 ImVec4 base_hover = base_bg;
787 base_hover.w = std::min(1.0f, base_bg.w + 0.15f);
788 ImVec4 active_hover = mute_active;
789 active_hover.w = std::min(1.0f, mute_active.w + 0.15f);
790 {
791 gui::StyleColorGuard mute_guard(
792 {{ImGuiCol_Button, muted ? mute_active : base_bg},
793 {ImGuiCol_ButtonHovered, muted ? active_hover : base_hover},
794 {ImGuiCol_ButtonActive, muted ? active_hover : base_hover}});
795 const char* mute_label =
796 muted ? ICON_MD_VOLUME_OFF "##Mute" : ICON_MD_VOLUME_UP "##Mute";
797 if (ImGui::Button(mute_label, button_size)) {
799 }
800 }
801 if (ImGui::IsItemHovered())
802 ImGui::SetTooltip("Mute");
803
804 ImGui::SameLine();
805
806 // Solo button (icon, themed)
807 bool solo = channel_solo_[i];
808 ImVec4 solo_col = gui::ConvertColorToImVec4(theme.accent);
809 solo_col.w = std::min(1.0f, solo_col.w * 0.75f);
810 ImVec4 solo_hover = solo_col;
811 solo_hover.w = std::min(1.0f, solo_col.w + 0.15f);
812 ImVec4 base_hover_solo = base_bg;
813 base_hover_solo.w = std::min(1.0f, base_bg.w + 0.15f);
814 {
815 gui::StyleColorGuard solo_guard(
816 {{ImGuiCol_Button, solo ? solo_col : base_bg},
817 {ImGuiCol_ButtonHovered, solo ? solo_hover : base_hover_solo},
818 {ImGuiCol_ButtonActive, solo ? solo_hover : base_hover_solo}});
819 const char* solo_label = ICON_MD_HEARING "##Solo";
820 if (ImGui::Button(solo_label, button_size)) {
822 }
823 }
824 if (ImGui::IsItemHovered())
825 ImGui::SetTooltip("Solo");
826
827 ImGui::SameLine();
828
829 // Channel number
830 ImGui::TextDisabled("Ch %d", i + 1);
831
832 ImGui::PopID();
833 }
834}
835
837 gui::StyleVarGuard status_padding(ImGuiStyleVar_WindowPadding, ImVec2(8, 4));
838 if (ImGui::BeginChild(
839 "##PianoRollStatusBar", ImVec2(0, kStatusBarHeight),
840 ImGuiChildFlags_AlwaysUseWindowPadding,
841 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
842
843 // Mouse position info
844 if (status_tick_ >= 0 && status_pitch_ >= 0) {
845 ImGui::Text(ICON_MD_MOUSE " Tick: %d | Pitch: %s (%d)", status_tick_,
847 } else {
848 ImGui::TextDisabled(ICON_MD_MOUSE " Hover over grid...");
849 }
850
851 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 420);
852
853 // Keyboard hints
854 ImGui::TextDisabled(
855 "Click: Add | Drag: Move | Ctrl+Wheel: Zoom X | Shift+Wheel: Zoom Y");
856 }
857 ImGui::EndChild();
858}
859
860void PianoRollView::HandleMouseInput(MusicSong* song, int active_channel,
861 int active_segment,
862 const ImVec2& grid_origin,
863 const ImVec2& grid_size, bool is_hovered) {
864 if (!song)
865 return;
866 if (!is_hovered && dragging_event_index_ == -1)
867 return;
868 if (active_segment < 0 ||
869 active_segment >= static_cast<int>(song->segments.size()))
870 return;
871 auto& track = song->segments[active_segment].tracks[active_channel];
872
873 ImVec2 mouse_pos = ImGui::GetMousePos();
874
875 // Mouse to grid conversion
876 float rel_x = mouse_pos.x - grid_origin.x;
877 float rel_y = mouse_pos.y - grid_origin.y;
878 int tick = static_cast<int>(std::lround(rel_x / pixels_per_tick_));
879 int pitch_idx = static_cast<int>(std::lround(rel_y / key_height_));
880 uint8_t pitch =
881 static_cast<uint8_t>(zelda3::music::kNoteMaxPitch - pitch_idx);
882
883 bool in_bounds =
884 rel_x >= 0 && rel_y >= 0 && rel_x <= grid_size.x && rel_y <= grid_size.y;
885
886 if (in_bounds) {
887 status_tick_ = tick;
888 status_pitch_ = pitch;
890 n.pitch = pitch;
892 } else {
893 status_tick_ = -1;
894 status_pitch_ = -1;
895 }
896
897 auto snap_tick = [this](int t) {
898 if (!snap_enabled_)
899 return t;
900 return (t / snap_ticks_) * snap_ticks_;
901 };
902
903 // Drag release
904 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) &&
905 dragging_event_index_ != -1) {
906 drag_moved_ = false;
908 drag_mode_ = 0;
909 }
910
911 // Handle drag update
912 if (dragging_event_index_ != -1 &&
913 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
914 if (drag_segment_index_ < 0 ||
915 drag_segment_index_ >= static_cast<int>(song->segments.size()))
916 return;
917 auto& drag_track =
919 if (dragging_event_index_ >= static_cast<int>(drag_track.events.size()))
920 return;
921
922 int delta_ticks = static_cast<int>(
923 std::lround((mouse_pos.x - drag_start_mouse_.x) / pixels_per_tick_));
924 int delta_pitch = static_cast<int>(
925 std::lround((drag_start_mouse_.y - mouse_pos.y) / key_height_));
926
928 if (drag_mode_ == 1) { // Move
929 updated.tick =
930 snap_tick(std::max(0, drag_original_event_.tick + delta_ticks));
931 int new_pitch = drag_original_event_.note.pitch + delta_pitch;
932 new_pitch =
933 std::clamp(new_pitch, static_cast<int>(zelda3::music::kNoteMinPitch),
934 static_cast<int>(zelda3::music::kNoteMaxPitch));
935 updated.note.pitch = static_cast<uint8_t>(new_pitch);
936 } else if (drag_mode_ == 2) { // Resize left
937 int new_tick =
938 snap_tick(std::max(0, drag_original_event_.tick + delta_ticks));
939 int new_duration = drag_original_event_.note.duration - delta_ticks;
940 updated.tick = new_tick;
941 updated.note.duration = std::max(1, new_duration);
942 } else if (drag_mode_ == 3) { // Resize right
943 int new_duration = drag_original_event_.note.duration + delta_ticks;
944 updated.note.duration = std::max(1, new_duration);
945 }
946
947 const bool changed = (updated.tick != drag_original_event_.tick) ||
950 if (!changed) {
951 return;
952 }
953
954 if (!drag_moved_ && on_edit_) {
955 // Capture undo snapshot before the first mutation in this drag session.
956 on_edit_();
957 }
958
959 drag_track.RemoveEvent(dragging_event_index_);
960 drag_track.InsertEvent(updated);
961
962 // Find updated index
963 for (size_t i = 0; i < drag_track.events.size(); ++i) {
964 const auto& evt = drag_track.events[i];
965 if (evt.type == TrackEvent::Type::Note && evt.tick == updated.tick &&
966 evt.note.pitch == updated.note.pitch &&
967 evt.note.duration == updated.note.duration) {
968 dragging_event_index_ = static_cast<int>(i);
969 break;
970 }
971 }
972
973 drag_moved_ = true;
974 return;
975 }
976
977 // Left click handling (selection / add note)
978 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && in_bounds &&
979 pitch_idx >= 0 &&
980 pitch_idx <=
982 // If clicked on existing note, begin drag
983 if (hovered_event_index_ >= 0 && hovered_segment_index_ == active_segment &&
984 hovered_channel_index_ == active_channel) {
986 drag_segment_index_ = active_segment;
987 drag_channel_index_ = active_channel;
989 drag_start_mouse_ = ImGui::GetMousePos();
990 drag_mode_ = 1;
991
992 // Detect edge hover for resize
994 float right_x =
997 float dist_left = std::fabs((grid_origin.x + left_x) - mouse_pos.x);
998 float dist_right = std::fabs((grid_origin.x + right_x) - mouse_pos.x);
999 const float edge_threshold = 6.0f;
1000 if (dist_left < edge_threshold)
1001 drag_mode_ = 2;
1002 else if (dist_right < edge_threshold)
1003 drag_mode_ = 3;
1004
1005 if (on_note_preview_) {
1006 on_note_preview_(drag_original_event_, active_segment, active_channel);
1007 }
1008 return;
1009 }
1010
1011 // Otherwise add a note with snap
1012 int snapped_tick = snap_enabled_ ? snap_tick(tick) : tick;
1014 snapped_tick, pitch, snap_enabled_ ? snap_ticks_ : kDurationQuarter);
1015 if (on_edit_) {
1016 on_edit_();
1017 }
1018 track.InsertEvent(new_note);
1019 if (on_note_preview_) {
1020 on_note_preview_(new_note, active_segment, active_channel);
1021 }
1022 }
1023}
1024
1025void PianoRollView::DrawPianoKeys(ImDrawList* draw_list,
1026 const ImVec2& key_origin, float total_height,
1027 int start_key_idx, int visible_keys,
1028 const RollPalette& palette) {
1029 int num_keys =
1031
1032 // Key lane background
1033 draw_list->AddRectFilled(
1034 ImVec2(key_origin.x, key_origin.y),
1035 ImVec2(key_origin.x + key_width_, key_origin.y + total_height),
1036 palette.background);
1037
1038 // Draw piano keys
1039 for (int i = start_key_idx;
1040 i < std::min(num_keys, start_key_idx + visible_keys); ++i) {
1041 float y = total_height - (i + 1) * key_height_;
1042 ImVec2 k_min = ImVec2(key_origin.x, key_origin.y + y);
1043 ImVec2 k_max =
1044 ImVec2(key_origin.x + key_width_, key_origin.y + y + key_height_);
1045
1046 int note_val = zelda3::music::kNoteMinPitch + i;
1047 bool is_black = IsBlackKey(note_val);
1048
1049 draw_list->AddRectFilled(k_min, k_max,
1050 is_black ? palette.black_key : palette.white_key);
1051 draw_list->AddRect(k_min, k_max, palette.grid_minor);
1052
1053 // Show labels for all white keys (black keys are too narrow)
1054 if (!is_black) {
1055 Note n;
1056 n.pitch = static_cast<uint8_t>(note_val);
1057 draw_list->AddText(ImVec2(k_min.x + 4, k_min.y + 1), palette.key_label,
1058 n.GetNoteName().c_str());
1059 }
1060 }
1061}
1062
1063void PianoRollView::DrawGrid(ImDrawList* draw_list, const ImVec2& grid_origin,
1064 const ImVec2& canvas_pos,
1065 const ImVec2& canvas_size, float total_height,
1066 float clip_bottom, int start_tick,
1067 int visible_ticks, int start_key_idx,
1068 int visible_keys, float content_width,
1069 const RollPalette& palette) {
1070 // Push clip rect for the entire grid area
1071 draw_list->PushClipRect(ImVec2(canvas_pos.x + key_width_, canvas_pos.y),
1072 ImVec2(canvas_pos.x + canvas_size.x, clip_bottom),
1073 true);
1074
1075 int ticks_per_beat = 72;
1076 int ticks_per_bar = ticks_per_beat * 4; // 4 beats per bar
1077
1078 // Beat markers (major grid lines) - clipped to content height
1079 float grid_clip_bottom = std::min(grid_origin.y + total_height, clip_bottom);
1080 for (int t = start_tick; t < start_tick + visible_ticks; ++t) {
1081 if (t % ticks_per_beat == 0) {
1082 float x = grid_origin.x + t * pixels_per_tick_;
1083 bool is_bar = (t % ticks_per_bar == 0);
1084 draw_list->AddLine(ImVec2(x, std::max(grid_origin.y, canvas_pos.y)),
1085 ImVec2(x, grid_clip_bottom),
1086 is_bar ? palette.beat_marker : palette.grid_major,
1087 is_bar ? 2.0f : 1.0f);
1088
1089 // Draw bar/beat number at top
1090 if (is_bar && x > grid_origin.x) {
1091 int bar_num = t / ticks_per_bar + 1;
1092 std::string bar_label = absl::StrFormat("%d", bar_num);
1093 draw_list->AddText(
1094 ImVec2(x + 2, std::max(grid_origin.y, canvas_pos.y) + 2),
1095 palette.key_label, bar_label.c_str());
1096 }
1097 }
1098 }
1099
1100 // Horizontal key lines with octave emphasis
1101 int num_keys =
1103 for (int i = start_key_idx; i < start_key_idx + visible_keys && i < num_keys;
1104 ++i) {
1105 float y = total_height - (i + 1) * key_height_;
1106 float line_y = grid_origin.y + y;
1107 if (line_y < canvas_pos.y || line_y > clip_bottom)
1108 continue;
1109
1110 int note_val = kNoteMinPitch + i;
1111 bool is_octave = (note_val % 12 == 0);
1112 draw_list->AddLine(ImVec2(grid_origin.x, line_y),
1113 ImVec2(grid_origin.x + content_width, line_y),
1114 is_octave ? palette.octave_line : palette.grid_minor,
1115 is_octave ? 1.5f : 1.0f);
1116 }
1117
1118 draw_list->PopClipRect();
1119}
1120
1121void PianoRollView::DrawNotes(ImDrawList* draw_list, const MusicSong* song,
1122 const ImVec2& grid_origin, float total_height,
1123 int start_tick, int end_tick, int start_key_idx,
1124 int visible_keys, const RollPalette& palette) {
1125 const auto& segment = song->segments[active_segment_index_];
1126
1127 // Check for any solo'd channels
1128 bool any_solo = false;
1129 for (int ch = 0; ch < 8; ++ch) {
1130 if (channel_solo_[ch]) {
1131 any_solo = true;
1132 break;
1133 }
1134 }
1135
1136 // Render channels
1137 // Pass 1: Inactive channels (ghost notes)
1138 for (int ch = 0; ch < 8; ++ch) {
1139 if (ch == active_channel_index_ || !channel_visible_[ch])
1140 continue;
1141 if (channel_muted_[ch])
1142 continue;
1143 if (any_solo && !channel_solo_[ch])
1144 continue;
1145
1146 const auto& track = segment.tracks[ch];
1147 ImU32 base_color = channel_colors_[ch];
1148 ImVec4 c = ImGui::ColorConvertU32ToFloat4(base_color);
1149 c.w = 0.3f; // Reduced opacity for ghost notes
1150 ImU32 ghost_color = ImGui::ColorConvertFloat4ToU32(c);
1151
1152 // Optimization: Only draw visible notes
1153 auto it = std::lower_bound(track.events.begin(), track.events.end(),
1154 start_tick, [](const TrackEvent& e, int tick) {
1155 return e.tick + e.note.duration < tick;
1156 });
1157
1158 for (; it != track.events.end(); ++it) {
1159 const auto& event = *it;
1160 if (event.tick > end_tick)
1161 break; // Stop if we're past the visible area
1162
1163 if (event.type == TrackEvent::Type::Note) {
1164 int key_idx = event.note.pitch - kNoteMinPitch;
1165 // Simple culling for vertical visibility
1166 if (key_idx < start_key_idx || key_idx > start_key_idx + visible_keys)
1167 continue;
1168
1169 float y = total_height - (key_idx + 1) * key_height_;
1170 float x = event.tick * pixels_per_tick_;
1171 float w = std::max(2.0f, event.note.duration * pixels_per_tick_);
1172
1173 ImVec2 p_min = ImVec2(grid_origin.x + x, grid_origin.y + y + 1);
1174 ImVec2 p_max = ImVec2(p_min.x + w, p_min.y + key_height_ - 2);
1175
1176 draw_list->AddRectFilled(p_min, p_max, ghost_color, 2.0f);
1177 }
1178 }
1179 }
1180
1181 // Pass 2: Active channel (interactive)
1184 (!any_solo || channel_solo_[active_channel_index_])) {
1185 const auto& track = segment.tracks[active_channel_index_];
1186 ImU32 active_color = channel_colors_[active_channel_index_];
1187 ImU32 hover_color = palette.note_hover;
1188
1189 // Optimization: Only draw visible notes
1190 auto it = std::lower_bound(track.events.begin(), track.events.end(),
1191 start_tick, [](const TrackEvent& e, int tick) {
1192 return e.tick + e.note.duration < tick;
1193 });
1194
1195 for (size_t idx = std::distance(track.events.begin(), it);
1196 idx < track.events.size(); ++idx) {
1197 const auto& event = track.events[idx];
1198 if (event.tick > end_tick)
1199 break; // Stop if we're past the visible area
1200
1201 if (event.type == TrackEvent::Type::Note) {
1202 int key_idx = event.note.pitch - kNoteMinPitch;
1203 // Simple culling for vertical visibility
1204 if (key_idx < start_key_idx || key_idx > start_key_idx + visible_keys)
1205 continue;
1206
1207 float y = total_height - (key_idx + 1) * key_height_;
1208 float x = event.tick * pixels_per_tick_;
1209 float w = std::max(4.0f, event.note.duration * pixels_per_tick_);
1210
1211 ImVec2 p_min = ImVec2(grid_origin.x + x, grid_origin.y + y + 1);
1212 ImVec2 p_max = ImVec2(p_min.x + w, p_min.y + key_height_ - 2);
1213 bool hovered = ImGui::IsMouseHoveringRect(p_min, p_max);
1214 ImU32 color = hovered ? hover_color : active_color;
1215
1216 // Draw shadow
1217 ImVec2 shadow_offset(2, 2);
1218 draw_list->AddRectFilled(
1219 ImVec2(p_min.x + shadow_offset.x, p_min.y + shadow_offset.y),
1220 ImVec2(p_max.x + shadow_offset.x, p_max.y + shadow_offset.y),
1221 palette.note_shadow, 3.0f);
1222
1223 // Draw note
1224 draw_list->AddRectFilled(p_min, p_max, color, 3.0f);
1225 draw_list->AddRect(p_min, p_max, palette.grid_major, 3.0f);
1226
1227 // Draw resize handles for larger notes
1228 if (w > 10) {
1229 float handle_w = 4.0f;
1230 // Left handle indicator
1231 draw_list->AddRectFilled(ImVec2(p_min.x, p_min.y),
1232 ImVec2(p_min.x + handle_w, p_max.y),
1233 IM_COL32(255, 255, 255, 40), 2.0f);
1234 // Right handle indicator
1235 draw_list->AddRectFilled(ImVec2(p_max.x - handle_w, p_min.y),
1236 ImVec2(p_max.x, p_max.y),
1237 IM_COL32(255, 255, 255, 40), 2.0f);
1238 }
1239
1240 if (hovered) {
1241 hovered_event_index_ = static_cast<int>(idx);
1244 ImGui::SetTooltip("Ch %d | %s\nTick: %d | Dur: %d",
1246 event.note.GetNoteName().c_str(), event.tick,
1247 event.note.duration);
1248 }
1249 }
1250 }
1251 }
1252}
1253
1254void PianoRollView::DrawPlaybackCursor(ImDrawList* draw_list,
1255 const ImVec2& grid_origin,
1256 float grid_height,
1257 uint32_t segment_start_tick) {
1258 // Only draw if playback tick is in or past current segment
1259 if (playback_tick_ < segment_start_tick)
1260 return;
1261
1262 // Calculate cursor position relative to segment start
1263 uint32_t local_tick = playback_tick_ - segment_start_tick;
1264 float cursor_x = grid_origin.x + local_tick * pixels_per_tick_;
1265
1266 // Different colors for playing vs paused state
1267 ImU32 cursor_color, glow_color;
1268 if (is_paused_) {
1269 // Orange/amber for paused state
1270 cursor_color = IM_COL32(255, 180, 50, 255);
1271 glow_color = IM_COL32(255, 180, 50, 80);
1272 } else {
1273 // Bright red for active playback
1274 cursor_color = IM_COL32(255, 100, 100, 255);
1275 glow_color = IM_COL32(255, 100, 100, 80);
1276 }
1277
1278 // Glow layer (thicker, semi-transparent)
1279 draw_list->AddLine(ImVec2(cursor_x, grid_origin.y),
1280 ImVec2(cursor_x, grid_origin.y + grid_height), glow_color,
1281 6.0f);
1282
1283 // Main cursor line
1284 draw_list->AddLine(ImVec2(cursor_x, grid_origin.y),
1285 ImVec2(cursor_x, grid_origin.y + grid_height),
1286 cursor_color, 2.0f);
1287
1288 // Top indicator - triangle when playing, pause bars when paused
1289 const float tri_size = 8.0f;
1290 if (is_paused_) {
1291 // Pause bars indicator
1292 const float bar_width = 3.0f;
1293 const float bar_height = tri_size * 1.5f;
1294 const float bar_gap = 4.0f;
1295 draw_list->AddRectFilled(
1296 ImVec2(cursor_x - bar_gap - bar_width, grid_origin.y - bar_height),
1297 ImVec2(cursor_x - bar_gap, grid_origin.y), cursor_color);
1298 draw_list->AddRectFilled(
1299 ImVec2(cursor_x + bar_gap, grid_origin.y - bar_height),
1300 ImVec2(cursor_x + bar_gap + bar_width, grid_origin.y), cursor_color);
1301 } else {
1302 // Triangle indicator for active playback
1303 draw_list->AddTriangleFilled(
1304 ImVec2(cursor_x, grid_origin.y),
1305 ImVec2(cursor_x - tri_size, grid_origin.y - tri_size),
1306 ImVec2(cursor_x + tri_size, grid_origin.y - tri_size), cursor_color);
1307 }
1308}
1309
1310} // namespace music
1311} // namespace editor
1312} // namespace yaze
void DrawPianoKeys(ImDrawList *draw_list, const ImVec2 &key_origin, float total_height, int start_key_idx, int visible_keys, const RollPalette &palette)
struct yaze::editor::music::PianoRollView::EmptyContextTarget empty_context_
void DrawRollCanvas(zelda3::music::MusicSong *song, const RollPalette &palette, const ImVec2 &canvas_size)
void DrawChannelList(const zelda3::music::MusicSong *song)
void DrawStatusBar(const zelda3::music::MusicSong *song)
struct yaze::editor::music::PianoRollView::ContextTarget context_target_
static constexpr float kStatusBarHeight
std::function< void(const zelda3::music::TrackEvent &, int segment_index, int channel_index) on_note_preview_)
void DrawNotes(ImDrawList *draw_list, const zelda3::music::MusicSong *song, const ImVec2 &grid_origin, float total_height, int start_tick, int end_tick, int start_key_idx, int visible_keys, const RollPalette &palette)
void Draw(zelda3::music::MusicSong *song, const zelda3::music::MusicBank *bank=nullptr)
Draw the piano roll view for the given song.
static constexpr float kChannelListWidth
void DrawPlaybackCursor(ImDrawList *draw_list, const ImVec2 &grid_origin, float grid_height, uint32_t segment_start_tick)
static constexpr float kToolbarHeight
void DrawGrid(ImDrawList *draw_list, const ImVec2 &grid_origin, const ImVec2 &canvas_pos, const ImVec2 &canvas_size, float total_height, float clip_bottom, int start_tick, int visible_ticks, int start_key_idx, int visible_keys, float content_width, const RollPalette &palette)
void HandleMouseInput(zelda3::music::MusicSong *song, int active_channel, int active_segment, const ImVec2 &grid_origin, const ImVec2 &grid_size, bool is_hovered)
void DrawToolbar(const zelda3::music::MusicSong *song, const zelda3::music::MusicBank *bank)
zelda3::music::TrackEvent drag_original_event_
std::function< void(const zelda3::music::MusicSong &, int segment_index) on_segment_preview_)
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui style vars.
Definition style_guard.h:68
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
Manages the collection of songs, instruments, and samples from a ROM.
Definition music_bank.h:27
MusicInstrument * GetInstrument(int index)
Get an instrument by index.
size_t GetInstrumentCount() const
Get the number of instruments.
Definition music_bank.h:176
#define ICON_MD_HEARING
Definition icons.h:928
#define ICON_MD_PIANO
Definition icons.h:1462
#define ICON_MD_VOLUME_UP
Definition icons.h:2111
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_MUSIC_NOTE
Definition icons.h:1264
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_ZOOM_IN
Definition icons.h:2194
#define ICON_MD_MOUSE
Definition icons.h:1251
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_VOLUME_OFF
Definition icons.h:2110
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
int GetChannelInstrument(const MusicTrack &track, int fallback)
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:134
bool SliderFloatWheel(const char *label, float *v, float v_min, float v_max, const char *format, float wheel_step, ImGuiSliderFlags flags)
Definition input.cc:749
Contains classes and functions for handling music data in Zelda 3.
constexpr uint8_t kDurationThirtySecond
Definition song_data.h:70
constexpr uint8_t kDurationSixteenth
Definition song_data.h:68
constexpr uint8_t kDurationEighth
Definition song_data.h:65
constexpr uint8_t kNoteMinPitch
Definition song_data.h:55
constexpr uint8_t kDurationQuarter
Definition song_data.h:62
constexpr uint8_t kNoteMaxPitch
Definition song_data.h:56
A complete song composed of segments.
Definition song_data.h:334
std::vector< MusicSegment > segments
Definition song_data.h:336
One of 8 channels in a music segment.
Definition song_data.h:291
std::vector< TrackEvent > events
Definition song_data.h:292
Represents a single musical note.
Definition song_data.h:192
std::string GetNoteName() const
Definition song_data.h:433
A single event in a music track (note, command, or control).
Definition song_data.h:247
static TrackEvent MakeNote(uint16_t tick, uint8_t pitch, uint8_t duration, uint8_t velocity=0)
Definition song_data.h:259