diff --git a/tui_main.c b/tui_main.c index 83ec7c0..45cac7e 100644 --- a/tui_main.c +++ b/tui_main.c @@ -196,30 +196,119 @@ void tui_cursor_move_relative(Tui_cursor* tc, Usz field_h, Usz field_w, tc->x = (Usz)x0; } -void tdraw_tui_cursor(WINDOW* win, int win_h, int win_w, Glyph const* gbuffer, - Usz field_h, Usz field_w, Usz ruler_spacing_y, - Usz ruler_spacing_x, Usz cursor_y, Usz cursor_x, - Usz cursor_h, Usz cursor_w, Tui_input_mode input_mode, - bool is_playing) { - (void)ruler_spacing_y; - (void)ruler_spacing_x; - (void)cursor_h; - (void)cursor_w; +void tdraw_grid_cursor(WINDOW* win, int draw_y, int draw_x, int draw_h, + int draw_w, Glyph const* gbuffer, Usz field_h, + Usz field_w, int scroll_y, int scroll_x, Usz cursor_y, + Usz cursor_x, Usz cursor_h, Usz cursor_w, + Tui_input_mode input_mode, bool is_playing) { (void)input_mode; - if (cursor_y >= field_h || cursor_x >= field_w || (int)cursor_y >= win_h || - (int)cursor_x >= win_w) + if (cursor_y >= field_h || cursor_x >= field_w) return; - Glyph beneath = gbuffer[cursor_y * field_w + cursor_x]; - char displayed; - if (beneath == '.') { - displayed = is_playing ? '@' : '~'; + if (scroll_y < 0) { + draw_y += -scroll_y; + scroll_y = 0; + } + if (scroll_x < 0) { + draw_x += -scroll_x; + scroll_x = 0; + } + Usz offset_y = (Usz)scroll_y; + Usz offset_x = (Usz)scroll_x; + if (offset_y >= field_h || offset_x >= field_w) + return; + if (draw_y >= draw_h || draw_x >= draw_w) + return; + int const curs_attr = A_reverse | A_bold | fg_bg(C_yellow, C_natural); + if (offset_y <= cursor_y && offset_x <= cursor_x) { + Usz cdraw_y = cursor_y - offset_y + (Usz)draw_y; + Usz cdraw_x = cursor_x - offset_x + (Usz)draw_x; + if (cdraw_y < (Usz)draw_h && cdraw_x < (Usz)draw_w) { + Glyph beneath = gbuffer[cursor_y * field_w + cursor_x]; + char displayed; + if (beneath == '.') { + displayed = is_playing ? '@' : '~'; + } else { + displayed = beneath; + } + chtype ch = (chtype)(displayed | curs_attr); + wmove(win, (int)cdraw_y, (int)cdraw_x); + waddchnstr(win, &ch, 1); + } + } + + // Early out for selection area that won't have any visual effect + if (cursor_h <= 1 && cursor_w <= 1) + return; + + // Now mutate visually selected area under grid to have the selection color + // attributes. (This will rewrite the attributes on the cursor character we + // wrote above, but if it was the only character that would have been + // changed, we already early-outed.) + // + // We'll do this by reading back the characters on the grid from the curses + // window buffer, changing the attributes, then writing it back. This is + // easier than pulling the glyphs from the gbuffer, since we already did the + // ruler calculations to turn . into +, and we don't need special behavior + // for any other attributes (e.g. we don't show a special state for selected + // uppercase characters.) + // + // First, confine cursor selection to the grid field/gbuffer that actually + // exists, in case the cursor selection exceeds the area of the field. + Usz sel_rows = field_h - cursor_y; + if (cursor_h < sel_rows) + sel_rows = cursor_h; + Usz sel_cols = field_w - cursor_x; + if (cursor_w < sel_cols) + sel_cols = cursor_w; + // Now, confine the selection area to what's visible on screen. Kind of + // tricky since we have to handle it being partially visible from any edge on + // any axis, and we have to be mindful overflow. + Usz vis_sel_y; + Usz vis_sel_x; + if (offset_y > cursor_y) { + vis_sel_y = 0; + Usz sub_y = offset_y - cursor_y; + if (sub_y > sel_rows) + sel_rows = 0; + else + sel_rows -= sub_y; } else { - displayed = beneath; + vis_sel_y = cursor_y - offset_y; + } + if (offset_x > cursor_x) { + vis_sel_x = 0; + Usz sub_x = offset_x - cursor_x; + if (sub_x > sel_cols) + sel_cols = 0; + else + sel_cols -= sub_x; + } else { + vis_sel_x = cursor_x - offset_x; + } + vis_sel_y += (Usz)draw_y; + vis_sel_x += (Usz)draw_x; + if (vis_sel_y >= (Usz)draw_h || vis_sel_x >= (Usz)draw_w) + return; + Usz vis_sel_h = (Usz)draw_h - vis_sel_y; + Usz vis_sel_w = (Usz)draw_w - vis_sel_x; + if (sel_rows < vis_sel_h) + vis_sel_h = sel_rows; + if (sel_cols < vis_sel_w) + vis_sel_w = sel_cols; + if (vis_sel_w == 0 || vis_sel_h == 0) + return; + enum { Bufcount = 4096 }; + chtype chbuffer[Bufcount]; + if (Bufcount < vis_sel_w) + vis_sel_w = Bufcount; + for (Usz iy = 0; iy < vis_sel_h; ++iy) { + int at_y = (int)(vis_sel_y + iy); + int num = mvwinchnstr(win, at_y, (int)vis_sel_x, chbuffer, (int)vis_sel_w); + for (int ix = 0; ix < num; ++ix) { + chbuffer[ix] = (chtype)((int)(chbuffer[ix] & A_CHARTEXT) | curs_attr); + } + waddchnstr(win, chbuffer, (int)num); } - chtype ch = - (chtype)(displayed | (A_reverse | A_bold | fg_bg(C_yellow, C_natural))); - wmove(win, (int)cursor_y, (int)cursor_x); - waddchnstr(win, &ch, 1); } typedef struct Undo_node { @@ -335,45 +424,66 @@ void tdraw_hud(WINDOW* win, int win_y, int win_x, int height, int width, wclrtoeol(win); } -void tdraw_field(WINDOW* win, int term_h, int term_w, int pos_y, int pos_x, - Glyph const* gbuffer, Mark const* mbuffer, Usz field_h, - Usz field_w, Usz ruler_spacing_y, Usz ruler_spacing_x) { +void tdraw_glyphs_grid(WINDOW* win, int draw_y, int draw_x, int draw_h, + int draw_w, Glyph const* restrict gbuffer, + Mark const* restrict mbuffer, Usz field_h, Usz field_w, + Usz offset_y, Usz offset_x, Usz ruler_spacing_y, + Usz ruler_spacing_x) { + assert(draw_y >= 0 && draw_x >= 0); + assert(draw_h >= 0 && draw_w >= 0); enum { Bufcount = 4096 }; - if (field_w > Bufcount) + chtype chbuffer[Bufcount]; + // todo buffer limit + if (offset_y >= field_h || offset_x >= field_w) + return; + if (draw_y >= draw_h || draw_x >= draw_w) return; - if (pos_y >= term_h || pos_x >= term_w) + Usz rows = (Usz)(draw_h - draw_y); + if (field_h - offset_y < rows) + rows = field_h - offset_y; + Usz cols = (Usz)(draw_w - draw_x); + if (field_w - offset_x < cols) + cols = field_w - offset_x; + if (rows == 0 || cols == 0) return; - Usz num_y = (Usz)term_h - (Usz)pos_y; - Usz num_x = (Usz)term_w - (Usz)pos_x; - if (field_h < num_y) - num_y = field_h; - if (field_w < num_x) - num_x = field_w; - chtype buffer[Bufcount]; bool use_rulers = ruler_spacing_y != 0 && ruler_spacing_x != 0; - for (Usz y = 0; y < num_y; ++y) { - Glyph const* gline = gbuffer + y * field_w; - Mark const* mline = mbuffer + y * field_w; - bool use_y_ruler = use_rulers && y % ruler_spacing_y == 0; - for (Usz x = 0; x < num_x; ++x) { - Glyph g = gline[x]; - Mark m = mline[x]; + (void)use_rulers; + for (Usz iy = 0; iy < rows; ++iy) { + Usz line_offset = (offset_y + iy) * field_w + offset_x; + Glyph const* g_row = gbuffer + line_offset; + Mark const* m_row = mbuffer + line_offset; + bool use_y_ruler = use_rulers && (iy + offset_y) % ruler_spacing_y == 0; + for (Usz ix = 0; ix < cols; ++ix) { + Glyph g = g_row[ix]; + Mark m = m_row[ix]; int attrs = term_attrs_of_cell(g, m); if (g == '.') { - if (use_y_ruler && x % ruler_spacing_x == 0) + if (use_y_ruler && (ix + offset_x) % ruler_spacing_x == 0) g = '+'; } - buffer[x] = (chtype)((int)g | attrs); - } - wmove(win, pos_y + (int)y, pos_x); - waddchnstr(win, buffer, (int)num_x); - // Trying to clear to eol with 0 chars remaining on line will clear whole - // line from start - if (pos_x + (int)num_x != term_w) { - wmove(win, pos_y + (int)y, pos_x + (int)num_x); - wclrtoeol(win); + chbuffer[ix] = (chtype)((int)g | attrs); } + wmove(win, draw_y + (int)iy, draw_x); + waddchnstr(win, chbuffer, (int)cols); + } +} + +void tdraw_glyphs_grid_scrolled(WINDOW* win, int draw_y, int draw_x, int draw_h, + int draw_w, Glyph const* restrict gbuffer, + Mark const* restrict mbuffer, Usz field_h, + Usz field_w, int scroll_y, int scroll_x, + Usz ruler_spacing_y, Usz ruler_spacing_x) { + if (scroll_y < 0) { + draw_y += -scroll_y; + scroll_y = 0; } + if (scroll_x < 0) { + draw_x += -scroll_x; + scroll_x = 0; + } + tdraw_glyphs_grid(win, draw_y, draw_x, draw_h, draw_w, gbuffer, mbuffer, + field_h, field_w, (Usz)scroll_y, (Usz)scroll_x, + ruler_spacing_y, ruler_spacing_x); } void tui_cursor_confine(Tui_cursor* tc, Usz height, Usz width) { @@ -518,6 +628,8 @@ typedef struct { char const* filename; Oosc_dev* oosc_dev; Midi_mode const* midi_mode; + int grid_scroll_y; + int grid_scroll_x; bool needs_remarking; bool is_draw_dirty; bool is_playing; @@ -546,6 +658,8 @@ void app_init(App_state* a) { a->filename = NULL; a->oosc_dev = NULL; a->midi_mode = NULL; + a->grid_scroll_y = 0; + a->grid_scroll_x = 0; a->needs_remarking = true; a->is_draw_dirty = false; a->is_playing = false; @@ -775,6 +889,7 @@ static double ms_to_sec(double ms) { return ms / 1000.0; } void app_force_draw_dirty(App_state* a) { a->is_draw_dirty = true; } void app_draw(App_state* a, WINDOW* win) { + werase(win); // We can predictavely step the next simulation tick and then use the // resulting markmap buffer for better UI visualization. If we don't do // this, after loading a fresh file or after the user performs some edit @@ -804,17 +919,14 @@ void app_draw(App_state* a, WINDOW* win) { int hud_height = 2; bool draw_hud = win_h > hud_height + 1; int grid_h = draw_hud ? win_h - 2 : win_h; - tdraw_field(win, grid_h, win_w, 0, 0, a->field.buffer, a->markmap_r.buffer, - a->field.height, a->field.width, a->ruler_spacing_y, - a->ruler_spacing_x); - for (int y = a->field.height; y < win_h - 1; ++y) { - wmove(win, y, 0); - wclrtoeol(win); - } - tdraw_tui_cursor(win, grid_h, win_w, a->field.buffer, a->field.height, - a->field.width, a->ruler_spacing_y, a->ruler_spacing_x, - a->tui_cursor.y, a->tui_cursor.x, a->tui_cursor.h, - a->tui_cursor.w, a->input_mode, a->is_playing); + tdraw_glyphs_grid_scrolled(win, 0, 0, grid_h, win_w, a->field.buffer, + a->markmap_r.buffer, a->field.height, + a->field.width, a->grid_scroll_y, a->grid_scroll_x, + a->ruler_spacing_y, a->ruler_spacing_x); + tdraw_grid_cursor(win, 0, 0, grid_h, win_w, a->field.buffer, a->field.height, + a->field.width, a->grid_scroll_y, a->grid_scroll_x, + a->tui_cursor.y, a->tui_cursor.x, a->tui_cursor.h, + a->tui_cursor.w, a->input_mode, a->is_playing); if (draw_hud) { char const* filename = a->filename ? a->filename : ""; tdraw_hud(win, win_h - hud_height, 0, hud_height, win_w, filename, @@ -841,6 +953,41 @@ void app_adjust_bpm(App_state* a, Isz delta_bpm) { } } +#if 0 +int scroll_offset_on_axis_for_visible_index(int win_len, int cont_len, int pos, + int pad) { + assert(win_len >= 1 && cont_len >= 1 && pos >= 0 && pad >= 0); + if (win_len < 1 || cont_len < 1 || pos < 0 || pad < 0) + return 0; + if (cont_len <= win_len) return 0; + if (pad * 2 >= win_len) + pad = (win_len - 1) / 2; + //if (pos + pad > +} + +int padded_scrollguy(int win_len, int cont_len, int vis_target, int cur_scroll, int pad) { +} + +void scroll_offset_for_visible_cell(int win_h, int win_w, int cont_h, + int cont_w, int pos_y, int pos_x, int pad_y, + int pad_x, int* out_y, int* out_x) { + assert(win_h >= 1 && win_w >= 1 && cont_h >= 1 && cont_w >= 1 && pad_y >= 0 && + pad_x >= 0 && pos_y >= 0 && pos_x >= 0); + if (win_h < 1 || win_w < 1 || cont_h < 1 || cont_w < 1 || pad_y < 0 || + pad_x < 0 || pos_x < 0 || pos_y < 0) { + *out_y = 0; + *out_x = 0; + return; + } + if (pad_y * 2 >= win_h) { + pad_y = (win_h - 1) / 2; + } + if (pad_x * 2 >= win_x) { + pad_x = (win_x - 1) / 2; + } +} +#endif + void app_move_cursor_relative(App_state* a, Isz delta_y, Isz delta_x) { tui_cursor_move_relative(&a->tui_cursor, a->field.height, a->field.width, delta_y, delta_x); @@ -1338,6 +1485,42 @@ int main(int argc, char** argv) { // to play being applied as actual playback time stm_laptime(&last_time); break; + case KEY_F(1): + app_state.grid_scroll_x -= 1; + app_state.is_draw_dirty = true; + break; + case KEY_F(2): + app_state.grid_scroll_x += 1; + app_state.is_draw_dirty = true; + break; + case KEY_F(3): + app_state.grid_scroll_y -= 1; + app_state.is_draw_dirty = true; + break; + case KEY_F(4): + app_state.grid_scroll_y += 1; + app_state.is_draw_dirty = true; + break; + case KEY_F(5): + if (app_state.tui_cursor.w > 0) { + --app_state.tui_cursor.w; + app_state.is_draw_dirty = true; + } + break; + case KEY_F(6): + ++app_state.tui_cursor.w; + app_state.is_draw_dirty = true; + break; + case KEY_F(7): + if (app_state.tui_cursor.h > 0) { + --app_state.tui_cursor.h; + app_state.is_draw_dirty = true; + } + break; + case KEY_F(8): + ++app_state.tui_cursor.h; + app_state.is_draw_dirty = true; + break; default: if (key >= '!' && key <= '~') { app_input_character(&app_state, (char)key);