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