84 if (!song || song->
segments.empty()) {
85 ImGui::TextDisabled(
"No song loaded");
105 static_cast<int>(song->
segments.size()) - 1);
108 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 4));
109 if (ImGui::BeginChild(
"##PianoRollToolbar", ImVec2(0,
kToolbarHeight),
110 ImGuiChildFlags_AlwaysUseWindowPadding,
111 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
115 ImGui::PopStyleVar();
119 ImGuiStyle& style = ImGui::GetStyle();
120 float available_height = ImGui::GetContentRegionAvail().y;
122 float main_height = std::max(0.0f, available_height - reserved_for_status);
124 if (ImGui::BeginChild(
"PianoRollMain", ImVec2(0, main_height), ImGuiChildFlags_None,
125 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
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);
143 float snapped_row_height = layout_height;
145 float rows = std::floor(layout_height /
key_height_);
150 ImGui::TableNextRow(ImGuiTableRowFlags_None, snapped_row_height);
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),
158 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
164 ImGui::TableSetColumnIndex(1);
169 ImVec2 roll_area = ImGui::GetContentRegionAvail();
174 ImGui::PopStyleVar(2);
183 if (ImGui::BeginPopup(
"PianoRollNoteContext")) {
190 if (evt.type == TrackEvent::Type::Note) {
192 ImGui::Text(
"Tick: %d", evt.tick);
196 ImGui::Text(
"Velocity:");
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);
204 if (ImGui::IsItemHovered()) {
205 ImGui::SetTooltip(
"Articulation/velocity (0 = default)");
209 ImGui::Text(
"Duration:");
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);
217 if (ImGui::IsItemHovered()) {
218 ImGui::SetTooltip(
"Duration in ticks (quarter = 72)");
222 ImGui::Text(
"Quick Duration:");
223 if (ImGui::MenuItem(
"Whole (288)")) {
224 evt.note.duration = 0xFE;
227 if (ImGui::MenuItem(
"Half (144)")) {
228 evt.note.duration = 144;
231 if (ImGui::MenuItem(
"Quarter (72)")) {
235 if (ImGui::MenuItem(
"Eighth (36)")) {
239 if (ImGui::MenuItem(
"Sixteenth (18)")) {
243 if (ImGui::MenuItem(
"32nd (9)")) {
251 copy.
tick += evt.note.duration;
252 track.InsertEvent(copy);
265 if (ImGui::BeginPopup(
"PianoRollEmptyContext")) {
272 if (ImGui::MenuItem(
"Quarter note")) {
283 if (ImGui::MenuItem(
"Eighth note")) {
294 if (ImGui::MenuItem(
"Sixteenth note")) {
311 const ImVec2& canvas_size_param) {
313 const ImGuiStyle& style = ImGui::GetStyle();
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();
332 ImDrawList* draw_list = ImGui::GetWindowDrawList();
334 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
335 const bool active = ImGui::IsItemActive();
341 uint32_t duration = segment.GetDuration();
342 if (duration == 0) duration = 1000;
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(
351 canvas_size.x -
key_width_ - (show_v_scroll ? style.ScrollbarSize : 0.0f));
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);
359 if (snapped_grid > 0.0f) grid_height = snapped_grid;
363 const ImVec2 mouse = ImGui::GetMousePos();
365 float wheel = ImGui::GetIO().MouseWheel;
367 bool ctrl = ImGui::GetIO().KeyCtrl;
368 bool shift = ImGui::GetIO().KeyShift;
381 std::max(0.0f, rel_y * (
key_height_ / old_kh) - (mouse.y - canvas_pos.y));
387 float wheel_h = ImGui::GetIO().MouseWheelH;
388 if (wheel_h != 0.0f) {
393 if (active && ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) {
394 ImVec2 delta = ImGui::GetIO().MouseDelta;
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);
411 float fractional = 0.0f;
414 ImVec2 key_origin(canvas_pos.x, canvas_pos.y - fractional);
416 key_origin.y = std::floor(key_origin.y);
417 grid_origin.y = std::floor(grid_origin.y);
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);
429 std::min(canvas_pos.y + grid_height, key_origin.y + total_height);
431 ImVec2 clip_min = canvas_pos;
432 ImVec2 clip_max = ImVec2(canvas_pos.x +
key_width_ + grid_width,
433 canvas_pos.y + grid_height);
435 draw_list->AddRectFilled(clip_min, clip_max, palette.
background);
436 draw_list->PushClipRect(clip_min, clip_max,
true);
438 DrawPianoKeys(draw_list, key_origin, total_height, start_key_idx, visible_keys,
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,
451 DrawNotes(draw_list, song, grid_origin, total_height, start_tick,
452 start_tick + visible_ticks, start_key_idx, visible_keys, palette);
455 ImVec2(content_width, total_height), hovered);
459 uint32_t segment_start = 0;
461 segment_start += song->
segments[i].GetDuration();
469 float visible_width = std::max(grid_width, 1.0f);
473 std::clamp(cursor_x - visible_width / 3.0f, 0.0f, max_scroll_x);
478 draw_list->PopClipRect();
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);
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);
490 float thumb_ratio = grid_width / content_width;
492 std::max(style.GrabMinSize, track_size.x * thumb_ratio);
493 float thumb_x = track_min.x +
496 (track_size.x - thumb_w)
499 ImVec2 thumb_min(thumb_x, track_min.y);
500 ImVec2 thumb_max(thumb_x + thumb_w, track_min.y + track_size.y);
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);
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);
521 if (show_v_scroll && grid_height > 1.0f) {
522 ImVec2 track_min(canvas_pos.x +
key_width_ + grid_width,
524 ImVec2 track_size(style.ScrollbarSize, grid_height);
526 float thumb_ratio = grid_height / total_height;
528 std::max(style.GrabMinSize, track_size.y * thumb_ratio);
529 float thumb_y = track_min.y +
532 (track_size.y - thumb_h)
535 ImVec2 thumb_min(track_min.x, thumb_y);
536 ImVec2 thumb_max(track_min.x + track_size.x, thumb_y + thumb_h);
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);
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);
692 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 4));
693 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6, 4));
699 ImVec2 button_size(ImGui::GetTextLineHeight() * 1.4f,
700 ImGui::GetTextLineHeight() * 1.4f);
702 for (
int i = 0; i < 8; ++i) {
709 ImVec2 row_min = ImGui::GetCursorScreenPos();
710 ImVec2 row_max = ImVec2(row_min.x + ImGui::GetContentRegionAvail().x,
711 row_min.y + ImGui::GetTextLineHeightWithSpacing() + 4);
713 active_bg.w *= 0.12f;
714 ImGui::GetWindowDrawList()->AddRectFilled(row_min, row_max,
715 ImGui::GetColorU32(active_bg), 4.0f);
720 if (ImGui::ColorButton(
"##Col", col_v4,
721 ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoBorder,
728 if (ImGui::BeginPopupContextItem(
"ChannelContext")) {
729 if (ImGui::ColorPicker4(
"##picker", (
float*)&col_v4,
730 ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_NoSmallPreview)) {
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);
750 if (ImGui::Button(mute_label, button_size)) {
753 ImGui::PopStyleColor(3);
754 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Mute");
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);
768 if (ImGui::Button(solo_label, button_size)) {
771 ImGui::PopStyleColor(3);
772 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Solo");
777 ImGui::TextDisabled(
"Ch %d", i + 1);
781 ImGui::PopStyleVar(2);
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,
984 draw_list->PushClipRect(
985 ImVec2(canvas_pos.x +
key_width_, canvas_pos.y),
986 ImVec2(canvas_pos.x + canvas_size.x, clip_bottom),
989 int ticks_per_beat = 72;
990 int ticks_per_bar = ticks_per_beat * 4;
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) {
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),
1001 is_bar ? 2.0f : 1.0f);
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),
1015 for (
int i = start_key_idx; i < start_key_idx + visible_keys && i < num_keys; ++i) {
1017 float line_y = grid_origin.y + y;
1018 if (line_y < canvas_pos.y || line_y > clip_bottom)
continue;
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),
1025 is_octave ? 1.5f : 1.0f);
1028 draw_list->PopClipRect();
1032 const ImVec2& grid_origin,
float total_height,
1033 int start_tick,
int end_tick,
int start_key_idx,
int visible_keys,
1038 bool any_solo =
false;
1039 for (
int ch = 0; ch < 8; ++ch) {
1045 for (
int ch = 0; ch < 8; ++ch) {
1050 const auto& track = segment.tracks[ch];
1052 ImVec4 c = ImGui::ColorConvertU32ToFloat4(base_color);
1054 ImU32 ghost_color = ImGui::ColorConvertFloat4ToU32(c);
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; });
1060 for (; it != track.events.end(); ++it) {
1061 const auto&
event = *it;
1062 if (event.tick > end_tick)
break;
1064 if (event.type == TrackEvent::Type::Note) {
1067 if (key_idx < start_key_idx || key_idx > start_key_idx + visible_keys)
continue;
1069 float y = total_height - (key_idx + 1) *
key_height_;
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);
1076 draw_list->AddRectFilled(p_min, p_max, ghost_color, 2.0f);
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; });
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;
1097 if (event.type == TrackEvent::Type::Note) {
1100 if (key_idx < start_key_idx || key_idx > start_key_idx + visible_keys)
continue;
1102 float y = total_height - (key_idx + 1) *
key_height_;
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;
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),
1119 draw_list->AddRectFilled(p_min, p_max, color, 3.0f);
1120 draw_list->AddRect(p_min, p_max, palette.
grid_major, 3.0f);
1124 float handle_w = 4.0f;
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);
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);
1141 ImGui::SetTooltip(
"Ch %d | %s\nTick: %d | Dur: %d",
1143 event.note.GetNoteName().c_str(),
1145 event.note.duration);
1153 const ImVec2& grid_origin,
1155 uint32_t segment_start_tick) {
1164 ImU32 cursor_color, glow_color;
1167 cursor_color = IM_COL32(255, 180, 50, 255);
1168 glow_color = IM_COL32(255, 180, 50, 80);
1171 cursor_color = IM_COL32(255, 100, 100, 255);
1172 glow_color = IM_COL32(255, 100, 100, 80);
1176 draw_list->AddLine(ImVec2(cursor_x, grid_origin.y),
1177 ImVec2(cursor_x, grid_origin.y + grid_height),
1181 draw_list->AddLine(ImVec2(cursor_x, grid_origin.y),
1182 ImVec2(cursor_x, grid_origin.y + grid_height),
1183 cursor_color, 2.0f);
1186 const float tri_size = 8.0f;
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),
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),
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),