387 lines
9.2 KiB
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();
|
|
}
|