From 50692503702b9d419de99c97cff59a970627de01 Mon Sep 17 00:00:00 2001 From: pommicket Date: Sun, 16 Jul 2023 15:07:41 -0400 Subject: initial draft of variable-width font handling --- buffer.c | 239 +++++++++++++++++++++++++++++++++++++-------------------------- config.c | 1 + ted.cfg | 2 + ted.h | 9 +-- text.c | 17 +++-- text.h | 7 +- util.c | 6 ++ util.h | 1 + 8 files changed, 171 insertions(+), 111 deletions(-) diff --git a/buffer.c b/buffer.c index 4ec789b..3ca57f9 100644 --- a/buffer.c +++ b/buffer.c @@ -778,65 +778,123 @@ static void buffer_print(TextBuffer const *buffer) { fflush(stdout); } -static u32 buffer_index_to_column(TextBuffer *buffer, u32 line, u32 index) { - char32_t *str = buffer->lines[line].str; - u32 col = 0; - uint tab_width = buffer_tab_width(buffer); - for (u32 i = 0; i < index && i < buffer->lines[line].len; ++i) { - switch (str[i]) { - case '\t': { - do +// convert line character index to offset in pixels +static double buffer_index_to_xoff(TextBuffer *buffer, u32 line_number, u32 index) { + if (line_number >= buffer->nlines) { + assert(0); + return 0; + } + Line *line = &buffer->lines[line_number]; + char32_t *str = line->str; + const Settings *settings = buffer_settings(buffer); + Font *font = buffer_font(buffer); + u8 tab_width = settings->tab_width; + if (index > line->len) + index = line->len; + + if (settings->force_monospace) { + u32 col = 0; + for (u32 i = 0; i < index; ++i) { + switch (str[i]) { + case '\t': { + do + ++col; + while (col % tab_width); + } break; + default: ++col; - while (col % tab_width); - } break; - default: - ++col; - break; + break; + } } + return col * (double)text_font_char_width(font, ' '); + } else { + double x = 0; + for (u32 i = 0; i < index; ++i) { + switch (str[i]) { + case '\t': { + x += tab_width * text_font_char_width(font, ' '); + } break; + default: + x += text_font_char_width(font, str[i]); + break; + } + } + return x; } - return col; } -static u32 buffer_column_to_index(TextBuffer *buffer, u32 line, u32 column) { - if (line >= buffer->nlines) { +// convert line x offset in pixels to character index +static u32 buffer_xoff_to_index(TextBuffer *buffer, u32 line_number, double xoff) { + if (line_number >= buffer->nlines) { assert(0); return 0; } - char32_t *str = buffer->lines[line].str; - u32 len = buffer->lines[line].len; - u32 col = 0; - uint tab_width = buffer_tab_width(buffer); - for (u32 i = 0; i < len; ++i) { - switch (str[i]) { - case '\t': { - do { - if (col == column) + if (xoff <= 0) { + return 0; + } + Line *line = &buffer->lines[line_number]; + char32_t *str = line->str; + const Settings *settings = buffer_settings(buffer); + Font *font = buffer_font(buffer); + u8 tab_width = settings->tab_width; + + if (settings->force_monospace) { + u32 target_col = (u32)(xoff / text_font_char_width(font, ' ')); + u32 col = 0; + for (u32 i = 0; i < line->len; ++i) { + switch (str[i]) { + case '\t': { + do { + if (col == target_col) + return i; + ++col; + } while (col % tab_width); + } break; + default: + if (col == target_col) return i; ++col; - } while (col % tab_width); - } break; - default: - if (col == column) - return i; - ++col; - break; + break; + } + } + return line->len; + } else { + double x = 0; + for (u32 i = 0; i < line->len; ++i) { + double x_prev = x; + switch (str[i]) { + case '\t': { + x += tab_width * text_font_char_width(font, ' '); + } break; + default: + x += text_font_char_width(font, str[i]); + break; + } + if (x > xoff) { + if (xoff - x_prev < x - xoff) { + return i; + } else { + return i + 1; + } + } } + return line->len; } - return len; } + void buffer_text_dimensions(TextBuffer *buffer, u32 *lines, u32 *columns) { if (lines) { *lines = buffer->nlines; } if (columns) { - u32 longest_line = 0; + double longest_line = 0; // which line on screen is the longest? for (u32 l = buffer->first_line_on_screen; l <= buffer->last_line_on_screen && l < buffer->nlines; ++l) { Line *line = &buffer->lines[l]; - longest_line = max_u32(longest_line, buffer_index_to_column(buffer, l, line->len)); + longest_line = maxd(longest_line, buffer_index_to_xoff(buffer, l, line->len)); } - *columns = longest_line; + *columns = (u32)(longest_line / text_font_char_width(buffer_font(buffer), ' ')); } } @@ -846,7 +904,7 @@ float buffer_display_lines(TextBuffer *buffer) { } float buffer_display_cols(TextBuffer *buffer) { - return (buffer->x2 - buffer->x1) / text_font_char_width(buffer_font(buffer)); + return (buffer->x2 - buffer->x1) / text_font_char_width(buffer_font(buffer), ' '); } // make sure we don't scroll too far @@ -882,48 +940,30 @@ void buffer_scroll(TextBuffer *buffer, double dx, double dy) { vec2 buffer_pos_to_pixels(TextBuffer *buffer, BufferPos pos) { buffer_pos_validate(buffer, &pos); u32 line = pos.line, index = pos.index; - // we need to convert the index to a column - u32 col = buffer_index_to_column(buffer, line, index); + double xoff = buffer_index_to_xoff(buffer, line, index); Font *font = buffer_font(buffer); - float x = (float)((double)col - buffer->scroll_x) * text_font_char_width(font) + buffer->x1; + float x = (float)((double)xoff - buffer->scroll_x * text_font_char_width(font, ' ')) + buffer->x1; float y = (float)((double)line - buffer->scroll_y) * text_font_char_height(font) + buffer->y1; return Vec2(x, y); } bool buffer_pixels_to_pos(TextBuffer *buffer, vec2 pixel_coords, BufferPos *pos) { bool ret = true; - float x = pixel_coords.x, y = pixel_coords.y; + double x = pixel_coords.x, y = pixel_coords.y; Font *font = buffer_font(buffer); pos->line = pos->index = 0; x -= buffer->x1; y -= buffer->y1; - x /= text_font_char_width(font); - y /= text_font_char_height(font); - double display_col = (double)x; - if (display_col < 0) { - display_col = 0; - ret = false; - } - double display_cols = buffer_display_cols(buffer), display_lines = buffer_display_lines(buffer); - if (display_col >= display_cols) { - display_col = display_cols - 1; - ret = false; - } - double display_line = (double)y; - if (display_line < 0) { - display_line = 0; - ret = false; - } - if (display_line >= display_lines) { - display_line = display_lines - 1; - ret = false; - } - u32 line = (u32)floor(display_line + buffer->scroll_y); + x = clampd(x, 0, buffer->x2 - buffer->x1); + y = clampd(y, 0, buffer->y2 - buffer->y1); + + double xoff = x + buffer->scroll_x * text_font_char_width(font, ' '); + + u32 line = (u32)floor(y / text_font_char_height(font) + buffer->scroll_y); if (line >= buffer->nlines) line = buffer->nlines - 1; - u32 column = (u32)round(display_col + buffer->scroll_x); - u32 index = buffer_column_to_index(buffer, line, column); + u32 index = buffer_xoff_to_index(buffer, line, xoff); pos->line = line; pos->index = index; @@ -948,10 +988,13 @@ bool buffer_clip_rect(TextBuffer *buffer, Rect *r) { void buffer_scroll_to_pos(TextBuffer *buffer, BufferPos pos) { const Settings *settings = buffer_settings(buffer); + Font *font = buffer_font(buffer); double line = pos.line; - double col = buffer_index_to_column(buffer, pos.line, pos.index); - double display_lines = buffer_display_lines(buffer); - double display_cols = buffer_display_cols(buffer); + double space_width = text_font_char_width(font, ' '); + double char_height = text_font_char_height(font); + double col = buffer_index_to_xoff(buffer, pos.line, pos.index) / space_width; + double display_lines = (buffer->y2 - buffer->y1) / char_height; + double display_cols = (buffer->x2 - buffer->x1) / space_width; double scroll_x = buffer->scroll_x, scroll_y = buffer->scroll_y; double scrolloff = settings->scrolloff; @@ -978,12 +1021,12 @@ void buffer_scroll_to_pos(TextBuffer *buffer, BufferPos pos) { void buffer_scroll_center_pos(TextBuffer *buffer, BufferPos pos) { double line = pos.line; - double col = buffer_index_to_column(buffer, pos.line, pos.index); - - double display_lines = buffer_display_lines(buffer); - double display_cols = buffer_display_cols(buffer); - buffer->scroll_x = col - display_cols * 0.5; - buffer->scroll_y = line - display_lines * 0.5; + Font *font = buffer_font(buffer); + float space_width = text_font_char_width(font, ' '); + float char_height = text_font_char_height(font); + double xoff = buffer_index_to_xoff(buffer, pos.line, pos.index); + buffer->scroll_x = (xoff - (buffer->x1 - buffer->x1) * 0.5) / space_width; + buffer->scroll_y = line - (buffer->y2 - buffer->y1) / char_height * 0.5; buffer_correct_scroll(buffer); } @@ -995,7 +1038,8 @@ void buffer_scroll_to_cursor(TextBuffer *buffer) { // scroll so that the cursor is in the center of the screen void buffer_center_cursor(TextBuffer *buffer) { double cursor_line = buffer->cursor_pos.line; - double cursor_col = buffer_index_to_column(buffer, (u32)cursor_line, buffer->cursor_pos.index); + double cursor_col = buffer_index_to_xoff(buffer, (u32)cursor_line, buffer->cursor_pos.index) + / text_font_char_width(buffer_font(buffer), ' '); double display_lines = buffer_display_lines(buffer); double display_cols = buffer_display_cols(buffer); @@ -1061,33 +1105,33 @@ static i64 buffer_pos_move_horizontally(TextBuffer *buffer, BufferPos *p, i64 by // same as buffer_pos_move_horizontally, but for up and down. static i64 buffer_pos_move_vertically(TextBuffer *buffer, BufferPos *pos, i64 by) { buffer_pos_validate(buffer, pos); - // moving up/down should preserve the column, not the index. + // moving up/down should preserve the x offset, not the index. // consider: // tab|hello world // tab|tab|more text // the character above the 'm' is the 'o', not the 'e' if (by < 0) { by = -by; - u32 column = buffer_index_to_column(buffer, pos->line, pos->index); + double xoff = buffer_index_to_xoff(buffer, pos->line, pos->index); if (pos->line < by) { i64 ret = pos->line; pos->line = 0; return -ret; } pos->line -= (u32)by; - pos->index = buffer_column_to_index(buffer, pos->line, column); + pos->index = buffer_xoff_to_index(buffer, pos->line, xoff); u32 line_len = buffer->lines[pos->line].len; if (pos->index >= line_len) pos->index = line_len; return -by; } else if (by > 0) { - u32 column = buffer_index_to_column(buffer, pos->line, pos->index); + double xoff = buffer_index_to_xoff(buffer, pos->line, pos->index); if (pos->line + by >= buffer->nlines) { i64 ret = buffer->nlines-1 - pos->line; pos->line = buffer->nlines-1; return ret; } pos->line += (u32)by; - pos->index = buffer_column_to_index(buffer, pos->line, column); + pos->index = buffer_xoff_to_index(buffer, pos->line, xoff); u32 line_len = buffer->lines[pos->line].len; if (pos->index >= line_len) pos->index = line_len; return by; @@ -2865,7 +2909,7 @@ bool buffer_handle_click(Ted *ted, TextBuffer *buffer, vec2 click, u8 times) { return false; } -bool buffer_pos_move_to_matching_bracket(TextBuffer *buffer, BufferPos *pos) { +char32_t buffer_pos_move_to_matching_bracket(TextBuffer *buffer, BufferPos *pos) { Language language = buffer_language(buffer); char32_t bracket_char = buffer_char_at_pos(buffer, *pos); char32_t matching_char = syntax_matching_bracket(language, bracket_char); @@ -2882,10 +2926,10 @@ bool buffer_pos_move_to_matching_bracket(TextBuffer *buffer, BufferPos *pos) { break; } } - return found_bracket; - } else { - return false; + if (found_bracket) + return matching_char; } + return 0; } bool buffer_cursor_move_to_matching_bracket(TextBuffer *buffer) { @@ -2922,15 +2966,14 @@ void buffer_render(TextBuffer *buffer, Rect r) { Font *font = buffer_font(buffer); u32 nlines = buffer->nlines; Line *lines = buffer->lines; - float char_width = text_font_char_width(font), - char_height = text_font_char_height(font); + float char_height = text_font_char_height(font); Ted *ted = buffer->ted; const Settings *settings = buffer_settings(buffer); const u32 *colors = settings->colors; const float padding = settings->padding; const float border_thickness = settings->border_thickness; - + u32 start_line = buffer_first_rendered_line(buffer); // line to start rendering from @@ -2949,7 +2992,8 @@ void buffer_render(TextBuffer *buffer, Rect r) { // line numbering if (!buffer->is_line_buffer && settings->line_numbers) { - float line_number_width = ndigits_u64(buffer->nlines) * char_width + padding; + // TODO: compute max digit width + float line_number_width = ndigits_u64(buffer->nlines) * text_font_char_width(font, '8') + padding; TextRenderState text_state = text_render_state_default; text_state.min_x = x1; @@ -2962,7 +3006,7 @@ void buffer_render(TextBuffer *buffer, Rect r) { for (u32 line = start_line; line < nlines; ++line) { char str[32] = {0}; strbuf_printf(str, U32_FMT, line + 1); // convert line number to string - float x = x1 + line_number_width - (float)strlen(str) * char_width; // right justify + float x = x1 + line_number_width - text_get_size_vec2(font, str).x; // right justify // set color rgba_u32_to_floats(colors[line == cursor_line ? COLOR_CURSOR_LINE_NUMBER : COLOR_LINE_NUMBERS], text_state.color); @@ -3022,7 +3066,7 @@ void buffer_render(TextBuffer *buffer, Rect r) { // what x coordinate to start rendering the text from - double render_start_x = x1 - (double)buffer->scroll_x * char_width; + double render_start_x = x1 - (double)buffer->scroll_x * text_font_char_width(font, ' '); u32 column = 0; if (buffer->selection) { // draw selection @@ -3045,18 +3089,18 @@ void buffer_render(TextBuffer *buffer, Rect r) { assert(index2 >= index1); // highlight everything from index1 to index2 - u32 n_columns_highlighted = buffer_index_to_column(buffer, line_idx, index2) - - buffer_index_to_column(buffer, line_idx, index1); + double highlight_width = buffer_index_to_xoff(buffer, line_idx, index2) + - buffer_index_to_xoff(buffer, line_idx, index1); if (line_idx != sel_end.line) { - ++n_columns_highlighted; // highlight the newline (otherwise empty higlighted lines wouldn't be highlighted at all). + highlight_width += text_font_char_width(font, ' '); // highlight the newline (otherwise empty higlighted lines wouldn't be highlighted at all). } - if (n_columns_highlighted) { + if (highlight_width > 0) { BufferPos p1 = {.line = line_idx, .index = index1}; vec2 hl_p1 = buffer_pos_to_pixels(buffer, p1); Rect hl_rect = rect( hl_p1, - Vec2((float)n_columns_highlighted * char_width, char_height) + Vec2((float)highlight_width, char_height) ); buffer_clip_rect(buffer, &hl_rect); gl_geometry_rect(hl_rect, colors[buffer->view_only ? COLOR_VIEW_ONLY_SELECTION_BG : COLOR_SELECTION_BG]); @@ -3164,9 +3208,10 @@ void buffer_render(TextBuffer *buffer, Rect r) { if (pos.index > 0) { // it's more natural to consider the bracket to the left of the cursor buffer_pos_move_left(buffer, &pos, 1); - if (buffer_pos_move_to_matching_bracket(buffer, &pos)) { + char32_t c = buffer_pos_move_to_matching_bracket(buffer, &pos); + if (c) { vec2 gl_pos = buffer_pos_to_pixels(buffer, pos); - Rect hl_rect = rect(gl_pos, Vec2(char_width, char_height)); + Rect hl_rect = rect(gl_pos, Vec2(text_font_char_width(font, c), char_height)); if (buffer_clip_rect(buffer, &hl_rect)) { gl_geometry_rect(hl_rect, colors[COLOR_MATCHING_BRACKET_HL]); } diff --git a/config.c b/config.c index 0da3a19..e70bf98 100644 --- a/config.c +++ b/config.c @@ -100,6 +100,7 @@ static const SettingBool settings_bool[] = { {"save-backup", &settings_zero.save_backup, true}, {"crlf-windows", &settings_zero.crlf_windows, true}, {"jump-to-build-error", &settings_zero.jump_to_build_error, true}, + {"force-monospace", &settings_zero.force_monospace, true}, }; static const SettingU8 settings_u8[] = { {"tab-width", &settings_zero.tab_width, 1, 100, true}, diff --git a/ted.cfg b/ted.cfg index 1c46b52..268f09b 100644 --- a/ted.cfg +++ b/ted.cfg @@ -78,6 +78,8 @@ max-file-size = 20000000 max-file-size-view-only = 100000000 # how much ctrl+scroll wheel changes the text size (0 for no change, negative to invert change) ctrl-scroll-adjust-text-size = 1.0 +# force every letter to get its width from space +force-monospace = no # whether to use vsync or not. you probably want this on. vsync = on diff --git a/ted.h b/ted.h index c9ba32f..968b2ee 100644 --- a/ted.h +++ b/ted.h @@ -275,6 +275,7 @@ typedef struct { bool save_backup; bool crlf_windows; bool jump_to_build_error; + bool force_monospace; KeyCombo hover_key; KeyCombo highlight_key; u8 tab_width; @@ -360,7 +361,7 @@ typedef struct { char *path; /// we keep a back-pointer to the ted instance so we don't have to pass it in to every buffer function struct Ted *ted; - /// number of characters scrolled in the x direction + /// number of characters scrolled in the x direction (multiply by space width to get pixels) double scroll_x; /// number of characters scrolled in the y direction double scroll_y; @@ -930,8 +931,8 @@ char32_t buffer_char_before_cursor(TextBuffer *buffer); BufferPos buffer_pos_start_of_file(TextBuffer *buffer); /// buffer position of end of file BufferPos buffer_pos_end_of_file(TextBuffer *buffer); -/// move position to matching bracket. returns true if there was a bracket at the position, otherwise returns false and does nothing. -bool buffer_pos_move_to_matching_bracket(TextBuffer *buffer, BufferPos *pos); +/// move position to matching bracket. returns the matching bracket character if there is one, otherwise returns 0 and does nothing. +char32_t buffer_pos_move_to_matching_bracket(TextBuffer *buffer, BufferPos *pos); /// move cursor to matching bracket. returns true if cursor was to the right of a bracket. bool buffer_cursor_move_to_matching_bracket(TextBuffer *buffer); /// ensures that `p` refers to a valid position, moving it if needed. @@ -981,7 +982,7 @@ void buffer_clear(TextBuffer *buffer); /// or 0 if `line_number` is out of range. u32 buffer_line_len(TextBuffer *buffer, u32 line_number); /// returns the number of lines of text in the buffer into *lines (if not NULL), -/// and the number of columns of text, i.e. the number of columns in the longest line displayed, into *cols (if not NULL) +/// and the number of columns of text, i.e. the width of the longest column divided by the width of the space character, into *cols (if not NULL) void buffer_text_dimensions(TextBuffer *buffer, u32 *lines, u32 *columns); /// returns the number of rows of text that can fit in the buffer float buffer_display_lines(TextBuffer *buffer); diff --git a/text.c b/text.c index 398bb3f..093c5ec 100644 --- a/text.c +++ b/text.c @@ -39,7 +39,7 @@ typedef struct { struct Font { bool force_monospace; - float char_width; // width of the character 'a'. calculated when font is loaded. + float space_width; // width of the character ' '. calculated when font is loaded. float char_height; GLuint textures[CHAR_PAGE_COUNT]; int tex_widths[CHAR_PAGE_COUNT], tex_heights[CHAR_PAGE_COUNT]; @@ -200,7 +200,7 @@ Font *text_font_load(const char *ttf_filename, float font_size) { float x = 0, y = 0; stbtt_GetBakedQuad(font->char_pages[0], font->tex_widths[0], font->tex_heights[0], 'a', &x, &y, &q, 1); - font->char_width = x; + font->space_width = x; } } else { text_set_err("Couldn't read font file."); @@ -231,8 +231,14 @@ float text_font_char_height(Font *font) { return font->char_height; } -float text_font_char_width(Font *font) { - return font->char_width; +float text_font_char_width(Font *font, char32_t c) { + if (c == ' ') return font->space_width; + uint page = c / CHAR_PAGE_SIZE; + uint index = c % CHAR_PAGE_SIZE; + if (!font->char_pages[page]) + if (!text_load_char_page(font, (int)page)) + return font->space_width; + return font->char_pages[page][index].xadvance; } void text_render(Font *font) { @@ -282,7 +288,6 @@ top: } stbtt_bakedchar *char_data = font->char_pages[page]; const float char_height = font->char_height; - const float char_width = font->char_width; if (char_data) { // if page was successfully loaded stbtt_aligned_quad q = {0}; @@ -308,7 +313,7 @@ top: q.y1 += (float)floor(state->y); if (font->force_monospace) { - state->x += char_width; // ignore actual character width + state->x += font->space_width; // ignore actual character width } else { state->x = x + floor(state->x); state->y = y + floor(state->y); diff --git a/text.h b/text.h index 31997d7..c0cb260 100644 --- a/text.h +++ b/text.h @@ -70,10 +70,9 @@ void text_clear_err(void); Font *text_font_load(const char *ttf_filename, float font_size); /// Height of a character of this font in pixels. float text_font_char_height(Font *font); -/// Width of the character 'a' of this font in pixels. -/// This is meant to be only used for monospace fonts. -float text_font_char_width(Font *font); -/// Force text to advance by text_font_char_width(font) pixels per character (actually, per code point). +/// Width of the given character in pixels. +float text_font_char_width(Font *font, char32_t c); +/// Force text to advance by text_font_char_width(font, ' ') pixels per character (actually, per code point). void text_font_set_force_monospace(Font *font, bool force); /// Get the dimensions of some text. void text_get_size(Font *font, const char *text, float *width, float *height); diff --git a/util.c b/util.c index f441479..02c5fc7 100644 --- a/util.c +++ b/util.c @@ -514,6 +514,12 @@ float clampf(float x, float a, float b) { return x; } +double clampd(double x, double a, double b) { + if (x < a) return a; + if (x > b) return b; + return x; +} + int clampi(int x, int a, int b) { if (x < a) return a; if (x > b) return b; diff --git a/util.h b/util.h index 9fb078c..15181bc 100644 --- a/util.h +++ b/util.h @@ -138,6 +138,7 @@ float radians(float r); float lerpf(float x, float a, float b); float normf(float x, float a, float b); float clampf(float x, float a, float b); +double clampd(double x, double a, double b); int clampi(int x, int a, int b); i16 clamp_i16(i16 x, i16 a, i16 b); u16 clamp_u16(u16 x, u16 a, u16 b); -- cgit v1.2.3