#include "vtext.h" #include #include #include #include #include #include /* Type identifier for vtext */ static int vtext_type_id = -1; /* Events */ uint16_t VTEXT_CHANGED; vtext *vtext_create(jwidget *parent) { if(vtext_type_id < 0) return NULL; vtext *t = malloc(sizeof *t); if(!t) return NULL; jwidget_init(&t->widget, vtext_type_id, parent); t->data = NULL; t->size = 0; t->wrap = VTEXT_WRAP_WORD; t->scroll = 0; t->horiz_scroll = 0; t->virtual_lines = 0; t->vline_offsets = NULL; t->visible_lines = 0; t->line_spacing = 0; t->font = dfont_default(); return t; } static char const *word_boundary(char const *start, char const *cursor, bool look_ahead) { char const *str = cursor; /* Look for a word boundary behind the cursor */ while(1) { /* Current position is end-of-string: suitable */ if(*str == 0) return str; /* Current position is start of string: bad */ if(str <= start) break; /* Look for heteregoneous neighboring characters */ int space_l = (str[-1] == ' ' || str[-1] == '\n'); int space_r = (str[0] == ' ' || str[0] == '\n'); if(!space_l && space_r) return str; str--; } /* If we can't look ahead, return the starting position to force a cut */ if(!look_ahead) return cursor; str++; /* Otherwise, look ahead */ while(*str) { int space_l = (str[-1] == ' ' || str[-1] == '\n'); int space_r = (str[0] == ' ' || str[0] == '\n'); if(!space_l && space_r) return str; str++; } /* If there's really nothing, return end-of-string */ return str; } static char const *next_line(char const *text, int width, font_t const *font, vtext_wrap wrap, int *chars_on_this_line) { char const *endline = text; while(*endline && *endline != '\n') endline++; char const *endscreen = NULL; if(wrap == VTEXT_WRAP_CHAR || wrap == VTEXT_WRAP_WORD) { endscreen = drsize(text, font, width, NULL); if(wrap == VTEXT_WRAP_WORD) { endscreen = word_boundary(text, endscreen, false); while(*endscreen == ' ') endscreen++; } } if(!endscreen || endline <= endscreen) { if(chars_on_this_line) *chars_on_this_line = endline - text; return endline + (*endline != 0); } else { if(chars_on_this_line) *chars_on_this_line = endscreen - text; return endscreen + (*endscreen && endscreen == text); } } static void update_after_layout(vtext *t) { int ch = jwidget_content_height(t); int line_height = t->font->line_height + t->line_spacing; int visible_lines = (ch + t->line_spacing) / line_height; t->visible_lines = visible_lines; } static void update_after_content_change(vtext *t) { if(!t || !t->data) return; char const *data = t->data; t->virtual_lines = 0; int cw = jwidget_content_width(t) - 4; while(*data) { t->virtual_lines++; data = next_line(data, cw, t->font, t->wrap, NULL); } t->vline_offsets = malloc(t->virtual_lines * sizeof *t->vline_offsets); if(!t->vline_offsets) { t->data = NULL; t->size = 0; return; } data = t->data; int i = 0; while(*data) { t->vline_offsets[i] = data - t->data; i++; data = next_line(data, cw, t->font, t->wrap, NULL); } t->max_scroll = max(t->virtual_lines - t->visible_lines, 0); } /* Adjust positioning to make sure we stay on legal values */ static void shake(vtext *t) { vtext_scroll_to(t, t->scroll); if(t->wrap != VTEXT_WRAP_NONE) t->horiz_scroll = 0; } static void emit_changed(vtext *t) { jwidget_emit(t, (jevent){ .type = VTEXT_CHANGED }); } bool vtext_scroll_to(vtext *t, int target_scroll) { target_scroll = max(0, min(target_scroll, t->max_scroll)); if(t->scroll == target_scroll) return false; t->scroll = target_scroll; t->widget.update = true; emit_changed(t); return true; } static void const *memmem(void const *haystack, size_t haystacklen, void const *needle, size_t needlelen) { if(haystacklen < needlelen) return NULL; for(size_t i = 0; i < haystacklen - needlelen; i++) { if(!memcmp(haystack + i, needle, needlelen)) return haystack + i; } return NULL; } static bool scroll_to_substring_within(vtext *t, char const *substring, int start, int length) { length = min(length, t->size - start); char const *occ = memmem(t->data + start, length, substring, strlen(substring)); if(!occ) return false; /* Find nearest line with lazy linear search */ int s = 0; s = 0; while(s + 1 < t->virtual_lines && (int)t->vline_offsets[s + 1] <= occ - t->data) { s++; } vtext_scroll_to(t, s); return true; } bool vtext_scroll_to_substring(vtext *t, char const *key) { int offset = (t->scroll + 1 < t->virtual_lines) ? (int)t->vline_offsets[t->scroll + 1] : t->size; if(scroll_to_substring_within(t, key, offset, t->size - offset)) return true; if(scroll_to_substring_within(t, key, 0, offset + strlen(key)-1)) return true; return false; } void vtext_set_source(vtext *t, char const *data, int size) { t->data = data; t->size = size; t->scroll = 0; t->horiz_scroll = 0; update_after_content_change(t); t->widget.update = true; } // Trivial properties // void vtext_set_font(vtext *t, font_t const *font) { t->font = font ? font : dfont_default(); update_after_layout(t); update_after_content_change(t); shake(t); t->widget.update = true; } void vtext_set_line_spacing(vtext *t, int line_spacing) { t->line_spacing = line_spacing; t->widget.update = true; update_after_layout(t); shake(t); } void vtext_set_word_wrapping(vtext *t, vtext_wrap word_wrapping) { t->wrap = word_wrapping; update_after_content_change(t); shake(t); t->widget.update = true; } // Polymorphic widget operations // static void vtext_poly_csize(void *t0) { vtext *t = t0; jwidget *w = &t->widget; w->w = 128; w->h = 64; } static void vtext_poly_layout(void *t0) { vtext *t = t0; update_after_layout(t); shake(t); } static void vtext_poly_render(void *t0, int x, int y) { vtext *t = t0; font_t const *old_font = dfont(t->font); if(!t->data) { dtext(x, y, C_BLACK, "(No file opened)"); dfont(old_font); return; } int line_height = t->font->line_height + t->line_spacing; struct dwindow w = { .left = x, .top = y, .right = x + jwidget_content_width(t) - 4, .bottom = y + jwidget_content_height(t), }; struct dwindow oldw = dwindow_set(w); for(int i = 0; i < t->visible_lines; i++) { int line = t->scroll + i; int line_y = y + line_height * i; if(line >= t->virtual_lines) continue; char const *curr_line = t->data + t->vline_offsets[line]; char const *end_line = (line + 1 >= t->virtual_lines) ? t->data + t->size : t->data + t->vline_offsets[line + 1]; dtext_opt(x - t->horiz_scroll, line_y, C_BLACK, C_NONE, DTEXT_LEFT, DTEXT_TOP, curr_line, end_line - curr_line); } dwindow_set(oldw); if(t->visible_lines < t->virtual_lines) { int h_total = jwidget_content_height(t); int h_bar = (t->visible_lines * h_total) / t->virtual_lines; int y_bar = (t->scroll * h_total) / t->virtual_lines; int x_bar = jwidget_content_width(t) - 2; drect(x+x_bar, y+y_bar, x+x_bar+1, y+y_bar+h_bar-1, C_BLACK); } dfont(old_font); } static bool vtext_poly_event(void *t0, jevent ev) { vtext *t = t0; if(ev.type != JSCENE_KEY || ev.key.type == KEYEV_UP) return false; // Movement // int key = ev.key.key; int move_speed = (ev.key.alpha ? t->visible_lines : 1); if(key == KEY_UP && ev.key.shift) { vtext_scroll_to(t, 0); return true; } else if(key == KEY_UP) { vtext_scroll_to(t, t->scroll - move_speed); return true; } if(key == KEY_DOWN && ev.key.shift) { vtext_scroll_to(t, t->virtual_lines /* auto adjust */); return true; } else if(key == KEY_DOWN) { vtext_scroll_to(t, t->scroll + move_speed); return true; } if(key == KEY_RIGHT && t->wrap == VTEXT_WRAP_NONE) { t->horiz_scroll += 32; t->widget.update = true; return true; } else if(key == KEY_LEFT && t->wrap == VTEXT_WRAP_NONE) { t->horiz_scroll = max(0, t->horiz_scroll - 32); t->widget.update = true; return true; } return false; } /* vtext type definiton */ static jwidget_poly type_vtext = { .name = "vtext", .csize = vtext_poly_csize, .layout = vtext_poly_layout, .render = vtext_poly_render, .event = vtext_poly_event, }; __attribute__((constructor(1004))) static void j_register_vtext(void) { vtext_type_id = j_register_widget(&type_vtext, "jwidget"); VTEXT_CHANGED = j_register_event(); }