85 if (!song || song->
segments.empty()) {
86 ImGui::TextDisabled(
"No song loaded");
108 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 4));
109 if (ImGui::BeginChild(
111 ImGuiChildFlags_AlwaysUseWindowPadding,
112 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
116 ImGui::PopStyleVar();
120 ImGuiStyle& style = ImGui::GetStyle();
121 float available_height = ImGui::GetContentRegionAvail().y;
123 float main_height = std::max(0.0f, available_height - reserved_for_status);
125 if (ImGui::BeginChild(
126 "PianoRollMain", ImVec2(0, main_height), ImGuiChildFlags_None,
127 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
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(
139 ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHide |
140 ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_NoReorder,
142 ImGui::TableSetupColumn(
"Roll", ImGuiTableColumnFlags_WidthStretch);
144 float snapped_row_height = layout_height;
146 float rows = std::floor(layout_height /
key_height_);
151 ImGui::TableNextRow(ImGuiTableRowFlags_None, snapped_row_height);
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),
160 ImGuiWindowFlags_NoScrollbar |
161 ImGuiWindowFlags_NoScrollWithMouse)) {
167 ImGui::TableSetColumnIndex(1);
172 ImVec2 roll_area = ImGui::GetContentRegionAvail();
177 ImGui::PopStyleVar(2);
186 if (ImGui::BeginPopup(
"PianoRollNoteContext")) {
194 if (evt.type == TrackEvent::Type::Note) {
196 evt.note.GetNoteName().c_str());
197 ImGui::Text(
"Tick: %d", evt.tick);
201 ImGui::Text(
"Velocity:");
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);
210 if (ImGui::IsItemHovered()) {
211 ImGui::SetTooltip(
"Articulation/velocity (0 = default)");
215 ImGui::Text(
"Duration:");
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);
224 if (ImGui::IsItemHovered()) {
225 ImGui::SetTooltip(
"Duration in ticks (quarter = 72)");
229 ImGui::Text(
"Quick Duration:");
230 if (ImGui::MenuItem(
"Whole (288)")) {
231 evt.note.duration = 0xFE;
235 if (ImGui::MenuItem(
"Half (144)")) {
236 evt.note.duration = 144;
240 if (ImGui::MenuItem(
"Quarter (72)")) {
245 if (ImGui::MenuItem(
"Eighth (36)")) {
250 if (ImGui::MenuItem(
"Sixteenth (18)")) {
255 if (ImGui::MenuItem(
"32nd (9)")) {
264 copy.
tick += evt.note.duration;
265 track.InsertEvent(copy);
280 if (ImGui::BeginPopup(
"PianoRollEmptyContext")) {
287 if (ImGui::MenuItem(
"Quarter note")) {
298 if (ImGui::MenuItem(
"Eighth note")) {
309 if (ImGui::MenuItem(
"Sixteenth note")) {
326 const ImVec2& canvas_size_param) {
328 const ImGuiStyle& style = ImGui::GetStyle();
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();
347 ImDrawList* draw_list = ImGui::GetWindowDrawList();
349 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
350 const bool active = ImGui::IsItemActive();
356 uint32_t duration = segment.GetDuration();
363 bool show_h_scroll = content_width > (canvas_size.x -
key_width_);
364 bool show_v_scroll = total_height > canvas_size.y;
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);
375 if (snapped_grid > 0.0f)
376 grid_height = snapped_grid;
380 const ImVec2 mouse = ImGui::GetMousePos();
382 float wheel = ImGui::GetIO().MouseWheel;
384 bool ctrl = ImGui::GetIO().KeyCtrl;
385 bool shift = ImGui::GetIO().KeyShift;
392 (mouse.x - canvas_pos.x));
398 0.0f, rel_y * (
key_height_ / old_kh) - (mouse.y - canvas_pos.y));
404 float wheel_h = ImGui::GetIO().MouseWheelH;
405 if (wheel_h != 0.0f) {
410 if (active && ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) {
411 ImVec2 delta = ImGui::GetIO().MouseDelta;
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);
428 float fractional = 0.0f;
431 ImVec2 key_origin(canvas_pos.x, canvas_pos.y - fractional);
433 key_origin.y = std::floor(key_origin.y);
434 grid_origin.y = std::floor(grid_origin.y);
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);
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);
447 std::min(canvas_pos.y + grid_height, key_origin.y + total_height);
449 ImVec2 clip_min = canvas_pos;
450 ImVec2 clip_max = ImVec2(canvas_pos.x +
key_width_ + grid_width,
451 canvas_pos.y + grid_height);
453 draw_list->AddRectFilled(clip_min, clip_max, palette.
background);
454 draw_list->PushClipRect(clip_min, clip_max,
true);
456 DrawPianoKeys(draw_list, key_origin, total_height, start_key_idx,
457 visible_keys, palette);
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);
468 DrawNotes(draw_list, song, grid_origin, total_height, start_tick,
469 start_tick + visible_ticks, start_key_idx, visible_keys, palette);
472 grid_origin, ImVec2(content_width, total_height), hovered);
476 uint32_t segment_start = 0;
478 segment_start += song->
segments[i].GetDuration();
486 float visible_width = std::max(grid_width, 1.0f);
490 std::clamp(cursor_x - visible_width / 3.0f, 0.0f, max_scroll_x);
495 draw_list->PopClipRect();
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);
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);
507 float thumb_ratio = grid_width / content_width;
508 float thumb_w = std::max(style.GrabMinSize, track_size.x * thumb_ratio);
510 track_min.x + (max_scroll_x > 0.0f ? (
scroll_x_px_ / max_scroll_x) *
511 (track_size.x - thumb_w)
514 ImVec2 thumb_min(thumb_x, track_min.y);
515 ImVec2 thumb_max(thumb_x + thumb_w, track_min.y + track_size.y);
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);
526 draw_list->AddRectFilled(
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);
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);
539 float thumb_ratio = grid_height / total_height;
540 float thumb_h = std::max(style.GrabMinSize, track_size.y * thumb_ratio);
542 track_min.y + (max_scroll_y > 0.0f ? (
scroll_y_px_ / max_scroll_y) *
543 (track_size.y - thumb_h)
546 ImVec2 thumb_min(track_min.x, thumb_y);
547 ImVec2 thumb_max(track_min.x + track_size.x, thumb_y + thumb_h);
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);
558 draw_list->AddRectFilled(
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);
713 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 4));
714 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6, 4));
720 ImVec2 button_size(ImGui::GetTextLineHeight() * 1.4f,
721 ImGui::GetTextLineHeight() * 1.4f);
723 for (
int i = 0; i < 8; ++i) {
730 ImVec2 row_min = ImGui::GetCursorScreenPos();
732 ImVec2(row_min.x + ImGui::GetContentRegionAvail().x,
733 row_min.y + ImGui::GetTextLineHeightWithSpacing() + 4);
735 active_bg.w *= 0.12f;
736 ImGui::GetWindowDrawList()->AddRectFilled(
737 row_min, row_max, ImGui::GetColorU32(active_bg), 4.0f);
742 if (ImGui::ColorButton(
744 ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoBorder,
751 if (ImGui::BeginPopupContextItem(
"ChannelContext")) {
752 if (ImGui::ColorPicker4(
"##picker", (
float*)&col_v4,
753 ImGuiColorEditFlags_NoSidePreview |
754 ImGuiColorEditFlags_NoSmallPreview)) {
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 =
779 if (ImGui::Button(mute_label, button_size)) {
782 ImGui::PopStyleColor(3);
783 if (ImGui::IsItemHovered())
784 ImGui::SetTooltip(
"Mute");
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);
802 if (ImGui::Button(solo_label, button_size)) {
805 ImGui::PopStyleColor(3);
806 if (ImGui::IsItemHovered())
807 ImGui::SetTooltip(
"Solo");
812 ImGui::TextDisabled(
"Ch %d", i + 1);
816 ImGui::PopStyleVar(2);
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,
1045 draw_list->PushClipRect(ImVec2(canvas_pos.x +
key_width_, canvas_pos.y),
1046 ImVec2(canvas_pos.x + canvas_size.x, clip_bottom),
1049 int ticks_per_beat = 72;
1050 int ticks_per_bar = ticks_per_beat * 4;
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) {
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),
1061 is_bar ? 2.0f : 1.0f);
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);
1068 ImVec2(x + 2, std::max(grid_origin.y, canvas_pos.y) + 2),
1077 for (
int i = start_key_idx; i < start_key_idx + visible_keys && i < num_keys;
1080 float line_y = grid_origin.y + y;
1081 if (line_y < canvas_pos.y || line_y > clip_bottom)
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),
1089 is_octave ? 1.5f : 1.0f);
1092 draw_list->PopClipRect();
1096 const ImVec2& grid_origin,
float total_height,
1097 int start_tick,
int end_tick,
int start_key_idx,
1102 bool any_solo =
false;
1103 for (
int ch = 0; ch < 8; ++ch) {
1112 for (
int ch = 0; ch < 8; ++ch) {
1120 const auto& track = segment.tracks[ch];
1122 ImVec4 c = ImGui::ColorConvertU32ToFloat4(base_color);
1124 ImU32 ghost_color = ImGui::ColorConvertFloat4ToU32(c);
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;
1132 for (; it != track.events.end(); ++it) {
1133 const auto&
event = *it;
1134 if (event.tick > end_tick)
1137 if (event.type == TrackEvent::Type::Note) {
1140 if (key_idx < start_key_idx || key_idx > start_key_idx + visible_keys)
1143 float y = total_height - (key_idx + 1) *
key_height_;
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);
1150 draw_list->AddRectFilled(p_min, p_max, ghost_color, 2.0f);
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;
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)
1175 if (event.type == TrackEvent::Type::Note) {
1178 if (key_idx < start_key_idx || key_idx > start_key_idx + visible_keys)
1181 float y = total_height - (key_idx + 1) *
key_height_;
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;
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),
1198 draw_list->AddRectFilled(p_min, p_max, color, 3.0f);
1199 draw_list->AddRect(p_min, p_max, palette.
grid_major, 3.0f);
1203 float handle_w = 4.0f;
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);
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);
1218 ImGui::SetTooltip(
"Ch %d | %s\nTick: %d | Dur: %d",
1220 event.note.GetNoteName().c_str(), event.tick,
1221 event.note.duration);
1229 const ImVec2& grid_origin,
1231 uint32_t segment_start_tick) {
1241 ImU32 cursor_color, glow_color;
1244 cursor_color = IM_COL32(255, 180, 50, 255);
1245 glow_color = IM_COL32(255, 180, 50, 80);
1248 cursor_color = IM_COL32(255, 100, 100, 255);
1249 glow_color = IM_COL32(255, 100, 100, 80);
1253 draw_list->AddLine(ImVec2(cursor_x, grid_origin.y),
1254 ImVec2(cursor_x, grid_origin.y + grid_height), glow_color,
1258 draw_list->AddLine(ImVec2(cursor_x, grid_origin.y),
1259 ImVec2(cursor_x, grid_origin.y + grid_height),
1260 cursor_color, 2.0f);
1263 const float tri_size = 8.0f;
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);
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);