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