text-viewer/src/vtext.c

387 lines
9.2 KiB
C

#include "vtext.h"
#include <justui/jwidget.h>
#include <justui/jwidget-api.h>
#include <justui/jscene.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 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();
}