86 if (!song || song->
segments.empty()) {
87 ImGui::TextDisabled(
"No song loaded");
112 if (ImGui::BeginChild(
114 ImGuiChildFlags_AlwaysUseWindowPadding,
115 ImGuiWindowFlags_NoScrollbar |
116 ImGuiWindowFlags_NoScrollWithMouse)) {
124 ImGuiStyle& style = ImGui::GetStyle();
125 float available_height = ImGui::GetContentRegionAvail().y;
127 float main_height = std::max(0.0f, available_height - reserved_for_status);
129 if (ImGui::BeginChild(
130 "PianoRollMain", ImVec2(0, main_height), ImGuiChildFlags_None,
131 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
134 {{ImGuiStyleVar_CellPadding, ImVec2(0, 0)},
135 {ImGuiStyleVar_FramePadding, ImVec2(0, 0)}});
136 const float layout_height = ImGui::GetContentRegionAvail().y;
137 const ImGuiTableFlags table_flags =
138 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV |
139 ImGuiTableFlags_NoPadOuterX | ImGuiTableFlags_NoPadInnerX;
140 if (ImGui::BeginTable(
"PianoRollLayout", 2, table_flags,
141 ImVec2(-FLT_MIN, layout_height))) {
142 ImGui::TableSetupColumn(
144 ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHide |
145 ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_NoReorder,
147 ImGui::TableSetupColumn(
"Roll", ImGuiTableColumnFlags_WidthStretch);
149 float snapped_row_height = layout_height;
151 float rows = std::floor(layout_height /
key_height_);
156 ImGui::TableNextRow(ImGuiTableRowFlags_None, snapped_row_height);
159 ImGui::TableSetColumnIndex(0);
160 const ImGuiChildFlags channel_child_flags =
161 ImGuiChildFlags_Border | ImGuiChildFlags_AlwaysUseWindowPadding;
162 if (ImGui::BeginChild(
"PianoRollChannelList",
163 ImVec2(-FLT_MIN, layout_height),
165 ImGuiWindowFlags_NoScrollbar |
166 ImGuiWindowFlags_NoScrollWithMouse)) {
172 ImGui::TableSetColumnIndex(1);
177 ImVec2 roll_area = ImGui::GetContentRegionAvail();
190 if (ImGui::BeginPopup(
"PianoRollNoteContext")) {
198 if (evt.type == TrackEvent::Type::Note) {
200 evt.note.GetNoteName().c_str());
201 ImGui::Text(
"Tick: %d", evt.tick);
205 ImGui::Text(
"Velocity:");
207 int velocity = evt.note.velocity;
208 ImGui::SetNextItemWidth(120);
209 if (ImGui::SliderInt(
"##velocity", &velocity, 0, 127)) {
213 evt.note.velocity =
static_cast<uint8_t
>(velocity);
215 if (ImGui::IsItemHovered()) {
216 ImGui::SetTooltip(
"Articulation/velocity (0 = default)");
220 ImGui::Text(
"Duration:");
222 int duration = evt.note.duration;
223 ImGui::SetNextItemWidth(120);
224 if (ImGui::SliderInt(
"##duration", &duration, 1, 192)) {
228 evt.note.duration =
static_cast<uint8_t
>(duration);
230 if (ImGui::IsItemHovered()) {
231 ImGui::SetTooltip(
"Duration in ticks (quarter = 72)");
235 ImGui::Text(
"Quick Duration:");
236 if (ImGui::MenuItem(
"Whole (288)")) {
240 evt.note.duration = 0xFE;
242 if (ImGui::MenuItem(
"Half (144)")) {
246 evt.note.duration = 144;
248 if (ImGui::MenuItem(
"Quarter (72)")) {
254 if (ImGui::MenuItem(
"Eighth (36)")) {
260 if (ImGui::MenuItem(
"Sixteenth (18)")) {
266 if (ImGui::MenuItem(
"32nd (9)")) {
279 copy.
tick += evt.note.duration;
280 track.InsertEvent(copy);
294 if (ImGui::BeginPopup(
"PianoRollEmptyContext")) {
301 if (ImGui::MenuItem(
"Quarter note")) {
313 if (ImGui::MenuItem(
"Eighth note")) {
325 if (ImGui::MenuItem(
"Sixteenth note")) {
343 const ImVec2& canvas_size_param) {
345 const ImGuiStyle& style = ImGui::GetStyle();
352 ImVec2 reserved_size = canvas_size_param;
353 reserved_size.x = std::max(reserved_size.x, 1.0f);
354 reserved_size.y = std::max(reserved_size.y, 1.0f);
355 ImGui::InvisibleButton(
"##PianoRollCanvasHitbox", reserved_size,
356 ImGuiButtonFlags_MouseButtonLeft |
357 ImGuiButtonFlags_MouseButtonRight |
358 ImGuiButtonFlags_MouseButtonMiddle);
359 ImVec2 canvas_pos = ImGui::GetItemRectMin();
360 canvas_pos.x = std::floor(canvas_pos.x);
361 canvas_pos.y = std::floor(canvas_pos.y);
362 ImVec2 canvas_size = ImGui::GetItemRectSize();
364 ImDrawList* draw_list = ImGui::GetWindowDrawList();
366 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
367 const bool active = ImGui::IsItemActive();
373 uint32_t duration = segment.GetDuration();
380 bool show_h_scroll = content_width > (canvas_size.x -
key_width_);
381 bool show_v_scroll = total_height > canvas_size.y;
384 (show_v_scroll ? style.ScrollbarSize : 0.0f));
385 float grid_height = std::max(
386 0.0f, canvas_size.y - (show_h_scroll ? style.ScrollbarSize : 0.0f));
387 grid_height = std::floor(grid_height);
388 grid_height = std::min(grid_height, total_height);
392 if (snapped_grid > 0.0f)
393 grid_height = snapped_grid;
397 const ImVec2 mouse = ImGui::GetMousePos();
399 float wheel = ImGui::GetIO().MouseWheel;
401 bool ctrl = ImGui::GetIO().KeyCtrl;
402 bool shift = ImGui::GetIO().KeyShift;
409 (mouse.x - canvas_pos.x));
415 0.0f, rel_y * (
key_height_ / old_kh) - (mouse.y - canvas_pos.y));
421 float wheel_h = ImGui::GetIO().MouseWheelH;
422 if (wheel_h != 0.0f) {
427 if (active && ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) {
428 ImVec2 delta = ImGui::GetIO().MouseDelta;
434 float max_scroll_x = std::max(0.0f, content_width - grid_width);
435 float max_scroll_y = std::max(0.0f, total_height - grid_height);
445 float fractional = 0.0f;
448 ImVec2 key_origin(canvas_pos.x, canvas_pos.y - fractional);
450 key_origin.y = std::floor(key_origin.y);
451 grid_origin.y = std::floor(grid_origin.y);
455 int start_key_idx =
static_cast<int>(scroll_y_aligned /
key_height_);
456 start_key_idx = std::clamp(start_key_idx, 0, num_keys - 1);
458 std::min(num_keys - start_key_idx,
459 std::max(0,
static_cast<int>(grid_height /
key_height_) + 2));
460 int max_start = std::max(0, num_keys - visible_keys);
461 start_key_idx = std::min(start_key_idx, max_start);
464 std::min(canvas_pos.y + grid_height, key_origin.y + total_height);
466 ImVec2 clip_min = canvas_pos;
467 ImVec2 clip_max = ImVec2(canvas_pos.x +
key_width_ + grid_width,
468 canvas_pos.y + grid_height);
470 draw_list->AddRectFilled(clip_min, clip_max, palette.
background);
471 draw_list->PushClipRect(clip_min, clip_max,
true);
473 DrawPianoKeys(draw_list, key_origin, total_height, start_key_idx,
474 visible_keys, palette);
480 DrawGrid(draw_list, grid_origin, canvas_pos,
481 ImVec2(
key_width_ + grid_width, grid_height), total_height,
482 clip_bottom, start_tick, visible_ticks, start_key_idx, visible_keys,
483 content_width, palette);
485 DrawNotes(draw_list, song, grid_origin, total_height, start_tick,
486 start_tick + visible_ticks, start_key_idx, visible_keys, palette);
489 grid_origin, ImVec2(content_width, total_height), hovered);
493 uint32_t segment_start = 0;
495 segment_start += song->
segments[i].GetDuration();
503 float visible_width = std::max(grid_width, 1.0f);
507 std::clamp(cursor_x - visible_width / 3.0f, 0.0f, max_scroll_x);
512 draw_list->PopClipRect();
515 ImU32 scrollbar_bg = ImGui::GetColorU32(ImGuiCol_ScrollbarBg);
516 ImU32 scrollbar_grab = ImGui::GetColorU32(ImGuiCol_ScrollbarGrab);
517 ImU32 scrollbar_grab_active =
518 ImGui::GetColorU32(ImGuiCol_ScrollbarGrabActive);
520 if (show_h_scroll && grid_width > 1.0f) {
521 ImVec2 track_min(canvas_pos.x +
key_width_, canvas_pos.y + grid_height);
522 ImVec2 track_size(grid_width, style.ScrollbarSize);
524 float thumb_ratio = grid_width / content_width;
525 float thumb_w = std::max(style.GrabMinSize, track_size.x * thumb_ratio);
527 track_min.x + (max_scroll_x > 0.0f ? (
scroll_x_px_ / max_scroll_x) *
528 (track_size.x - thumb_w)
531 ImVec2 thumb_min(thumb_x, track_min.y);
532 ImVec2 thumb_max(thumb_x + thumb_w, track_min.y + track_size.y);
534 ImVec2 h_rect_max(track_min.x + track_size.x, track_min.y + track_size.y);
535 bool h_hover = ImGui::IsMouseHoveringRect(track_min, h_rect_max);
536 bool h_active = h_hover && ImGui::IsMouseDown(ImGuiMouseButton_Left);
537 if (h_hover && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
538 float rel = (ImGui::GetIO().MousePos.x - track_min.x - thumb_w * 0.5f) /
539 std::max(1.0f, track_size.x - thumb_w);
540 scroll_x_px_ = std::clamp(rel * max_scroll_x, 0.0f, max_scroll_x);
543 draw_list->AddRectFilled(
545 ImVec2(track_min.x + track_size.x, track_min.y + track_size.y),
546 scrollbar_bg, style.ScrollbarRounding);
547 draw_list->AddRectFilled(thumb_min, thumb_max,
548 h_active ? scrollbar_grab_active : scrollbar_grab,
549 style.ScrollbarRounding);
552 if (show_v_scroll && grid_height > 1.0f) {
553 ImVec2 track_min(canvas_pos.x +
key_width_ + grid_width, canvas_pos.y);
554 ImVec2 track_size(style.ScrollbarSize, grid_height);
556 float thumb_ratio = grid_height / total_height;
557 float thumb_h = std::max(style.GrabMinSize, track_size.y * thumb_ratio);
559 track_min.y + (max_scroll_y > 0.0f ? (
scroll_y_px_ / max_scroll_y) *
560 (track_size.y - thumb_h)
563 ImVec2 thumb_min(track_min.x, thumb_y);
564 ImVec2 thumb_max(track_min.x + track_size.x, thumb_y + thumb_h);
566 ImVec2 v_rect_max(track_min.x + track_size.x, track_min.y + track_size.y);
567 bool v_hover = ImGui::IsMouseHoveringRect(track_min, v_rect_max);
568 bool v_active = v_hover && ImGui::IsMouseDown(ImGuiMouseButton_Left);
569 if (v_hover && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
570 float rel = (ImGui::GetIO().MousePos.y - track_min.y - thumb_h * 0.5f) /
571 std::max(1.0f, track_size.y - thumb_h);
572 scroll_y_px_ = std::clamp(rel * max_scroll_y, 0.0f, max_scroll_y);
575 draw_list->AddRectFilled(
577 ImVec2(track_min.x + track_size.x, track_min.y + track_size.y),
578 scrollbar_bg, style.ScrollbarRounding);
579 draw_list->AddRectFilled(thumb_min, thumb_max,
580 v_active ? scrollbar_grab_active : scrollbar_grab,
581 style.ScrollbarRounding);
731 {{ImGuiStyleVar_ItemSpacing, ImVec2(4, 4)},
732 {ImGuiStyleVar_FramePadding, ImVec2(6, 4)}});
738 ImVec2 button_size(ImGui::GetTextLineHeight() * 1.4f,
739 ImGui::GetTextLineHeight() * 1.4f);
741 for (
int i = 0; i < 8; ++i) {
748 ImVec2 row_min = ImGui::GetCursorScreenPos();
750 ImVec2(row_min.x + ImGui::GetContentRegionAvail().x,
751 row_min.y + ImGui::GetTextLineHeightWithSpacing() + 4);
753 active_bg.w *= 0.12f;
754 ImGui::GetWindowDrawList()->AddRectFilled(
755 row_min, row_max, ImGui::GetColorU32(active_bg), 4.0f);
760 if (ImGui::ColorButton(
762 ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoBorder,
769 if (ImGui::BeginPopupContextItem(
"ChannelContext")) {
770 if (ImGui::ColorPicker4(
"##picker", (
float*)&col_v4,
771 ImGuiColorEditFlags_NoSidePreview |
772 ImGuiColorEditFlags_NoSmallPreview)) {
785 mute_active.w = std::min(1.0f, mute_active.w * 0.85f);
786 ImVec4 base_hover = base_bg;
787 base_hover.w = std::min(1.0f, base_bg.w + 0.15f);
788 ImVec4 active_hover = mute_active;
789 active_hover.w = std::min(1.0f, mute_active.w + 0.15f);
792 {{ImGuiCol_Button, muted ? mute_active : base_bg},
793 {ImGuiCol_ButtonHovered, muted ? active_hover : base_hover},
794 {ImGuiCol_ButtonActive, muted ? active_hover : base_hover}});
795 const char* mute_label =
797 if (ImGui::Button(mute_label, button_size)) {
801 if (ImGui::IsItemHovered())
802 ImGui::SetTooltip(
"Mute");
809 solo_col.w = std::min(1.0f, solo_col.w * 0.75f);
810 ImVec4 solo_hover = solo_col;
811 solo_hover.w = std::min(1.0f, solo_col.w + 0.15f);
812 ImVec4 base_hover_solo = base_bg;
813 base_hover_solo.w = std::min(1.0f, base_bg.w + 0.15f);
816 {{ImGuiCol_Button, solo ? solo_col : base_bg},
817 {ImGuiCol_ButtonHovered, solo ? solo_hover : base_hover_solo},
818 {ImGuiCol_ButtonActive, solo ? solo_hover : base_hover_solo}});
820 if (ImGui::Button(solo_label, button_size)) {
824 if (ImGui::IsItemHovered())
825 ImGui::SetTooltip(
"Solo");
830 ImGui::TextDisabled(
"Ch %d", i + 1);
1064 const ImVec2& canvas_pos,
1065 const ImVec2& canvas_size,
float total_height,
1066 float clip_bottom,
int start_tick,
1067 int visible_ticks,
int start_key_idx,
1068 int visible_keys,
float content_width,
1071 draw_list->PushClipRect(ImVec2(canvas_pos.x +
key_width_, canvas_pos.y),
1072 ImVec2(canvas_pos.x + canvas_size.x, clip_bottom),
1075 int ticks_per_beat = 72;
1076 int ticks_per_bar = ticks_per_beat * 4;
1079 float grid_clip_bottom = std::min(grid_origin.y + total_height, clip_bottom);
1080 for (
int t = start_tick; t < start_tick + visible_ticks; ++t) {
1081 if (t % ticks_per_beat == 0) {
1083 bool is_bar = (t % ticks_per_bar == 0);
1084 draw_list->AddLine(ImVec2(x, std::max(grid_origin.y, canvas_pos.y)),
1085 ImVec2(x, grid_clip_bottom),
1087 is_bar ? 2.0f : 1.0f);
1090 if (is_bar && x > grid_origin.x) {
1091 int bar_num = t / ticks_per_bar + 1;
1092 std::string bar_label = absl::StrFormat(
"%d", bar_num);
1094 ImVec2(x + 2, std::max(grid_origin.y, canvas_pos.y) + 2),
1103 for (
int i = start_key_idx; i < start_key_idx + visible_keys && i < num_keys;
1106 float line_y = grid_origin.y + y;
1107 if (line_y < canvas_pos.y || line_y > clip_bottom)
1111 bool is_octave = (note_val % 12 == 0);
1112 draw_list->AddLine(ImVec2(grid_origin.x, line_y),
1113 ImVec2(grid_origin.x + content_width, line_y),
1115 is_octave ? 1.5f : 1.0f);
1118 draw_list->PopClipRect();
1122 const ImVec2& grid_origin,
float total_height,
1123 int start_tick,
int end_tick,
int start_key_idx,
1128 bool any_solo =
false;
1129 for (
int ch = 0; ch < 8; ++ch) {
1138 for (
int ch = 0; ch < 8; ++ch) {
1146 const auto& track = segment.tracks[ch];
1148 ImVec4 c = ImGui::ColorConvertU32ToFloat4(base_color);
1150 ImU32 ghost_color = ImGui::ColorConvertFloat4ToU32(c);
1153 auto it = std::lower_bound(track.events.begin(), track.events.end(),
1154 start_tick, [](
const TrackEvent& e,
int tick) {
1155 return e.tick + e.note.duration < tick;
1158 for (; it != track.events.end(); ++it) {
1159 const auto&
event = *it;
1160 if (event.tick > end_tick)
1163 if (event.type == TrackEvent::Type::Note) {
1166 if (key_idx < start_key_idx || key_idx > start_key_idx + visible_keys)
1169 float y = total_height - (key_idx + 1) *
key_height_;
1173 ImVec2 p_min = ImVec2(grid_origin.x + x, grid_origin.y + y + 1);
1174 ImVec2 p_max = ImVec2(p_min.x + w, p_min.y +
key_height_ - 2);
1176 draw_list->AddRectFilled(p_min, p_max, ghost_color, 2.0f);
1190 auto it = std::lower_bound(track.events.begin(), track.events.end(),
1191 start_tick, [](
const TrackEvent& e,
int tick) {
1192 return e.tick + e.note.duration < tick;
1195 for (
size_t idx = std::distance(track.events.begin(), it);
1196 idx < track.events.size(); ++idx) {
1197 const auto&
event = track.events[idx];
1198 if (event.tick > end_tick)
1201 if (event.type == TrackEvent::Type::Note) {
1204 if (key_idx < start_key_idx || key_idx > start_key_idx + visible_keys)
1207 float y = total_height - (key_idx + 1) *
key_height_;
1211 ImVec2 p_min = ImVec2(grid_origin.x + x, grid_origin.y + y + 1);
1212 ImVec2 p_max = ImVec2(p_min.x + w, p_min.y +
key_height_ - 2);
1213 bool hovered = ImGui::IsMouseHoveringRect(p_min, p_max);
1214 ImU32 color = hovered ? hover_color : active_color;
1217 ImVec2 shadow_offset(2, 2);
1218 draw_list->AddRectFilled(
1219 ImVec2(p_min.x + shadow_offset.x, p_min.y + shadow_offset.y),
1220 ImVec2(p_max.x + shadow_offset.x, p_max.y + shadow_offset.y),
1224 draw_list->AddRectFilled(p_min, p_max, color, 3.0f);
1225 draw_list->AddRect(p_min, p_max, palette.
grid_major, 3.0f);
1229 float handle_w = 4.0f;
1231 draw_list->AddRectFilled(ImVec2(p_min.x, p_min.y),
1232 ImVec2(p_min.x + handle_w, p_max.y),
1233 IM_COL32(255, 255, 255, 40), 2.0f);
1235 draw_list->AddRectFilled(ImVec2(p_max.x - handle_w, p_min.y),
1236 ImVec2(p_max.x, p_max.y),
1237 IM_COL32(255, 255, 255, 40), 2.0f);
1244 ImGui::SetTooltip(
"Ch %d | %s\nTick: %d | Dur: %d",
1246 event.note.GetNoteName().c_str(), event.tick,
1247 event.note.duration);
1255 const ImVec2& grid_origin,
1257 uint32_t segment_start_tick) {
1267 ImU32 cursor_color, glow_color;
1270 cursor_color = IM_COL32(255, 180, 50, 255);
1271 glow_color = IM_COL32(255, 180, 50, 80);
1274 cursor_color = IM_COL32(255, 100, 100, 255);
1275 glow_color = IM_COL32(255, 100, 100, 80);
1279 draw_list->AddLine(ImVec2(cursor_x, grid_origin.y),
1280 ImVec2(cursor_x, grid_origin.y + grid_height), glow_color,
1284 draw_list->AddLine(ImVec2(cursor_x, grid_origin.y),
1285 ImVec2(cursor_x, grid_origin.y + grid_height),
1286 cursor_color, 2.0f);
1289 const float tri_size = 8.0f;
1292 const float bar_width = 3.0f;
1293 const float bar_height = tri_size * 1.5f;
1294 const float bar_gap = 4.0f;
1295 draw_list->AddRectFilled(
1296 ImVec2(cursor_x - bar_gap - bar_width, grid_origin.y - bar_height),
1297 ImVec2(cursor_x - bar_gap, grid_origin.y), cursor_color);
1298 draw_list->AddRectFilled(
1299 ImVec2(cursor_x + bar_gap, grid_origin.y - bar_height),
1300 ImVec2(cursor_x + bar_gap + bar_width, grid_origin.y), cursor_color);
1303 draw_list->AddTriangleFilled(
1304 ImVec2(cursor_x, grid_origin.y),
1305 ImVec2(cursor_x - tri_size, grid_origin.y - tri_size),
1306 ImVec2(cursor_x + tri_size, grid_origin.y - tri_size), cursor_color);