mirror of https://github.com/buserror/mii_emu.git
1108 lines
33 KiB
C
1108 lines
33 KiB
C
/*
|
|
* mui_cdef_textedit.c
|
|
*
|
|
* Copyright (C) 2023 Michel Pollet <buserror@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
/*
|
|
* This is a simple textedit control, it's not meant to be a full fledged
|
|
* text editor, but more a simple text input field.
|
|
*
|
|
* One obvious low hanging fruit would be to split the drawing code
|
|
* to be able to draw line-by-line, allowing skipping lines that are
|
|
* not visible. Currently the whole text is drawn every time, and relies
|
|
* on clipping to avoid drawing outside the control.
|
|
*
|
|
* System is based on mui_font_measure() returning a mui_glyph_line_array_t
|
|
* that contains the position of each glyph in the text, and the width of
|
|
* each line.
|
|
* The text itself is a UTF8 array, so we need to be aware of multi-byte
|
|
* glyphs. The 'selection' is kept as a start and end glyph index, and
|
|
* the drawing code calculates the rectangles for the selection.
|
|
*
|
|
* There is a text_content rectangle that deals with the scrolling, and
|
|
* the text (and selection) is drawn offset by the top left of this rectangle.
|
|
*
|
|
* There is a carret timer that makes the carret blink, and a carret is
|
|
* drawn when the selection is empty.
|
|
*
|
|
* There can only one 'carret' blinking at a time, the one in the control
|
|
* that has the focus, so the carret timer is a global timer that is reset
|
|
* every time a control gets the focus.
|
|
*
|
|
* The control has a margin, and a frame, and the text is drawn inside the
|
|
* frame, and the margin is used to inset the text_content rectangle.
|
|
* Margin is optional, and frame is optional too.
|
|
*
|
|
* The control deals with switching focus as well, so clicking in a textedit
|
|
* will deactivate the previously focused control, and activate the new one.
|
|
* TAB will do the same, for the current window.
|
|
*/
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
#include "mui.h"
|
|
#include "cg.h"
|
|
|
|
enum {
|
|
MUI_CONTROL_TEXTEDIT = FCC('T','e','a','c'),
|
|
};
|
|
|
|
enum {
|
|
MUI_TE_SELECTING_GLYPHS = 0,
|
|
MUI_TE_SELECTING_WORDS,
|
|
// MUI_TE_SELECTING_LINES, // TODO?
|
|
};
|
|
|
|
typedef struct mui_sel_t {
|
|
uint carret: 1; // carret is visible (if sel.start == end)
|
|
uint start, end; // glyph index in text
|
|
// rectangles for the first partial line, the body,
|
|
// and the last partial line. All of them can be empty
|
|
union {
|
|
struct {
|
|
c2_rect_t first, body, last;
|
|
};
|
|
c2_rect_t e[3];
|
|
};
|
|
} mui_sel_t;
|
|
|
|
typedef struct mui_textedit_control_t {
|
|
mui_control_t control;
|
|
uint trace : 1; // debug trace
|
|
uint32_t flags; // display flags
|
|
mui_sel_t sel;
|
|
mui_font_t * font;
|
|
mui_utf8_t text;
|
|
mui_glyph_line_array_t measure;
|
|
c2_pt_t margin;
|
|
c2_rect_t text_content;
|
|
struct {
|
|
uint start, end;
|
|
} click;
|
|
uint selecting_mode;
|
|
} mui_textedit_control_t;
|
|
|
|
extern const mui_control_color_t mui_control_color[MUI_CONTROL_STATE_COUNT];
|
|
|
|
|
|
static void
|
|
_mui_textedit_select_signed(
|
|
mui_textedit_control_t * te,
|
|
int glyph_start,
|
|
int glyph_end);
|
|
static void
|
|
_mui_textedit_refresh_sel(
|
|
mui_textedit_control_t * te,
|
|
mui_sel_t * sel);
|
|
static bool
|
|
mui_cdef_textedit(
|
|
struct mui_control_t * c,
|
|
uint8_t what,
|
|
void * param);
|
|
|
|
/*
|
|
* Rectangles passed here are in TEXT coordinates.
|
|
* which means they are already offset by margin.x, margin.y
|
|
* and the text_content.tl.x, text_content.tl.y
|
|
*/
|
|
static void
|
|
_mui_textedit_inval(
|
|
mui_textedit_control_t * te,
|
|
c2_rect_t r)
|
|
{
|
|
c2_rect_offset(&r, te->text_content.tl.x, te->text_content.tl.y);
|
|
if (!c2_rect_isempty(&r))
|
|
mui_window_inval(te->control.win, &r);
|
|
}
|
|
|
|
/* this is the timer used to make the carret blink *for all windows* */
|
|
static mui_time_t
|
|
_mui_textedit_carret_timer(
|
|
struct mui_t * mui,
|
|
mui_time_t now,
|
|
void * param)
|
|
{
|
|
mui_window_t *win = mui_window_front(mui);
|
|
|
|
// printf("carret timer win %p focus %p\n", win, win->control_focus);
|
|
if (win && win->control_focus.control) {
|
|
mui_textedit_control_t *te =
|
|
(mui_textedit_control_t *)win->control_focus.control;
|
|
te->sel.carret = !te->sel.carret;
|
|
if (te->sel.start == te->sel.end)
|
|
_mui_textedit_refresh_sel(te, NULL);
|
|
}
|
|
return 500 * MUI_TIME_MS;
|
|
}
|
|
|
|
/* this 'forces' the carret to be visible, used when typing */
|
|
static void
|
|
_mui_textedit_show_carret(
|
|
mui_textedit_control_t * te)
|
|
{
|
|
mui_t * mui = te->control.win->ui;
|
|
mui_window_t *win = mui_window_front(mui);
|
|
if (win && win->control_focus.control == &te->control) {
|
|
mui_timer_reset(mui,
|
|
mui->carret_timer,
|
|
_mui_textedit_carret_timer,
|
|
500 * MUI_TIME_MS);
|
|
}
|
|
te->sel.carret = 1;
|
|
_mui_textedit_refresh_sel(te, NULL);
|
|
|
|
}
|
|
|
|
/* Return the line number, and glyph position in line a glyph index */
|
|
static int
|
|
_mui_glyph_to_line_index(
|
|
mui_glyph_line_array_t * measure,
|
|
uint glyph_pos,
|
|
uint * out_line,
|
|
uint * out_line_index)
|
|
{
|
|
*out_line = 0;
|
|
*out_line_index = 0;
|
|
if (!measure->count)
|
|
return -1;
|
|
for (uint i = 0; i < measure->count; i++) {
|
|
mui_glyph_array_t * line = &measure->e[i];
|
|
if (glyph_pos > line->count) {
|
|
glyph_pos -= line->count;
|
|
continue;
|
|
}
|
|
*out_line = i;
|
|
*out_line_index = glyph_pos;
|
|
return i;
|
|
}
|
|
// return last glyph last line
|
|
*out_line = measure->count - 1;
|
|
*out_line_index = measure->e[*out_line].count - 1;
|
|
return measure->count - 1;
|
|
}
|
|
|
|
/* Return the line number and glyph index in that line for a point x,y */
|
|
static int
|
|
_mui_point_to_line_index(
|
|
mui_textedit_control_t * te,
|
|
mui_font_t * font,
|
|
c2_rect_t frame,
|
|
c2_pt_t where,
|
|
uint * out_line,
|
|
uint * out_line_index)
|
|
{
|
|
mui_glyph_line_array_t * measure = &te->measure;
|
|
if (!measure->count)
|
|
return -1;
|
|
*out_line = 0;
|
|
*out_line_index = 0;
|
|
for (uint i = 0; i < measure->count; i++) {
|
|
mui_glyph_array_t * line = &measure->e[i];
|
|
c2_rect_t line_r = {
|
|
.l = frame.l + te->text_content.l,
|
|
.t = frame.t + line->t + te->text_content.t,
|
|
.r = frame.r + te->text_content.l,
|
|
.b = frame.t + line->b + te->text_content.t,
|
|
};
|
|
if (!((where.y >= line_r.t) && (where.y < line_r.b)))
|
|
continue;
|
|
*out_line = i;
|
|
*out_line_index = line->count;
|
|
// printf(" last x: %d where.x: %d\n",
|
|
// frame.l + (int)line->e[line->count-1].x, where.x);
|
|
if (where.x > (line_r.l + (int)line->e[line->count].x)) {
|
|
*out_line_index = line->count;
|
|
return 0;
|
|
} else if (where.x < (line_r.l + (int)line->e[0].x)) {
|
|
*out_line_index = 0;
|
|
return 0;
|
|
}
|
|
for (uint j = 0; j < line->count; j++) {
|
|
if (where.x < (line_r.l + (int)line->e[j].x))
|
|
return 0;
|
|
*out_line_index = j;
|
|
}
|
|
// printf("point_to_line_index: line %d:%d / %d\n",
|
|
// *out_line, *out_line_index, line->count);
|
|
return 0;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* Return the glyph position in the text for line number and index in line */
|
|
static uint
|
|
_mui_line_index_to_glyph(
|
|
mui_glyph_line_array_t * measure,
|
|
uint line,
|
|
uint index)
|
|
{
|
|
uint pos = 0;
|
|
for (uint i = 0; i < line; i++)
|
|
pos += measure->e[i].count;
|
|
pos += index;
|
|
return pos;
|
|
}
|
|
|
|
/* Return the beginning and end glyphs for the line/index in line */
|
|
static void
|
|
_mui_line_index_to_glyph_word(
|
|
mui_glyph_line_array_t * measure,
|
|
uint line,
|
|
uint index,
|
|
uint *word_start,
|
|
uint *word_end)
|
|
{
|
|
*word_start = 0;
|
|
*word_end = 0;
|
|
uint start = index;
|
|
uint end = index;
|
|
mui_glyph_array_t * l = &measure->e[line];
|
|
while (start > 0 && l->e[start-1].glyph > 32)
|
|
start--;
|
|
while (end < l->count && l->e[end].glyph > 32)
|
|
end++;
|
|
*word_start = _mui_line_index_to_glyph(measure, line, start);
|
|
*word_end = _mui_line_index_to_glyph(measure, line, end);
|
|
}
|
|
|
|
/* Convert a glyph index to a byte index (used to manipulate text array) */
|
|
static uint
|
|
_mui_glyph_to_byte_offset(
|
|
mui_glyph_line_array_t * measure,
|
|
uint glyph_pos)
|
|
{
|
|
uint pos = 0;
|
|
for (uint i = 0; i < measure->count; i++) {
|
|
mui_glyph_array_t * line = &measure->e[i];
|
|
if (glyph_pos > pos + line->count) {
|
|
pos += line->count;
|
|
continue;
|
|
}
|
|
uint idx = glyph_pos - pos;
|
|
// printf("glyph_to_byte_offset: glyph_pos %d line %d:%2d\n",
|
|
// glyph_pos, i, idx);
|
|
return line->e[idx].pos;
|
|
}
|
|
// printf("glyph_to_byte_offset: glyph_pos %d out of range\n", glyph_pos);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Calculate the 3 rectangles that represent the graphical selection.
|
|
* The 'start' is the first line of the selection, or the position of the
|
|
* carret if the selection is empty.
|
|
* The other two are 'optional' (they can be empty), and represent the last
|
|
* line of the selection, and the body of the selection that is the rectangle
|
|
* between the first and last line.
|
|
*/
|
|
static int
|
|
_mui_make_sel_rects(
|
|
mui_glyph_line_array_t * measure,
|
|
mui_font_t * font,
|
|
mui_sel_t * sel,
|
|
c2_rect_t frame)
|
|
{
|
|
if (!measure->count)
|
|
return -1;
|
|
sel->last = sel->first = sel->body = (c2_rect_t) {};
|
|
uint start_line, start_index;
|
|
uint end_line, end_index;
|
|
_mui_glyph_to_line_index(measure, sel->start, &start_line, &start_index);
|
|
_mui_glyph_to_line_index(measure, sel->end, &end_line, &end_index);
|
|
mui_glyph_array_t * line = &measure->e[start_line];
|
|
|
|
if (start_line == end_line) {
|
|
// single line selection
|
|
sel->first = (c2_rect_t) {
|
|
.l = frame.l + line->e[start_index].x,
|
|
.t = frame.t + line->t,
|
|
.r = frame.l + line->e[end_index].x,
|
|
.b = frame.t + line->b,
|
|
};
|
|
return 0;
|
|
}
|
|
// first line
|
|
sel->first = (c2_rect_t) {
|
|
.l = frame.l + line->e[start_index].x, .t = frame.t + line->t,
|
|
.r = frame.r, .b = frame.t + line->b,
|
|
};
|
|
// last line
|
|
line = &measure->e[end_line];
|
|
sel->last = (c2_rect_t) {
|
|
.l = frame.l, .t = frame.t + line->t,
|
|
.r = frame.l + line->e[end_index].x, .b = frame.t + line->b,
|
|
};
|
|
// body
|
|
sel->body = (c2_rect_t) {
|
|
.l = frame.l, .t = sel->first.b,
|
|
.r = frame.r, .b = sel->last.t,
|
|
};
|
|
return 0;
|
|
}
|
|
|
|
/* Refresh the whole selection (or around the carret selection) */
|
|
static void
|
|
_mui_textedit_refresh_sel(
|
|
mui_textedit_control_t * te,
|
|
mui_sel_t * sel)
|
|
{
|
|
if (!sel)
|
|
sel = &te->sel;
|
|
for (int i = 0; i < 3; i++) {
|
|
c2_rect_t r = te->sel.e[i];
|
|
if (i == 0 && te->sel.start == te->sel.end)
|
|
c2_rect_inset(&r, -1, -1);
|
|
_mui_textedit_inval(te, r);
|
|
}
|
|
}
|
|
|
|
/* this makes sure the text is always visible in the frame */
|
|
static void
|
|
_mui_textedit_clamp_text_frame(
|
|
mui_textedit_control_t * te)
|
|
{
|
|
c2_rect_t f = te->control.frame;
|
|
c2_rect_offset(&f, -f.l, -f.t);
|
|
if (te->flags & MUI_CONTROL_TEXTBOX_FRAME)
|
|
c2_rect_inset(&f, te->margin.x, te->margin.y);
|
|
c2_rect_t old = te->text_content;
|
|
te->text_content.r = te->text_content.l + te->measure.margin_right;
|
|
te->text_content.b = te->text_content.t + te->measure.height;
|
|
printf(" %s %s / %3dx%3d\n", __func__,
|
|
c2_rect_as_str(&te->text_content),
|
|
c2_rect_width(&f), c2_rect_height(&f));
|
|
if (te->text_content.b < c2_rect_height(&f))
|
|
c2_rect_offset(&te->text_content, 0,
|
|
c2_rect_height(&f) - te->text_content.b);
|
|
if (te->text_content.t > f.t)
|
|
c2_rect_offset(&te->text_content, 0, f.t - te->text_content.t);
|
|
if (te->text_content.r < c2_rect_width(&f))
|
|
c2_rect_offset(&te->text_content,
|
|
c2_rect_width(&f) - te->text_content.r, 0);
|
|
if (te->text_content.l > f.l)
|
|
c2_rect_offset(&te->text_content, f.l - te->text_content.l, 0);
|
|
if (c2_rect_equal(&te->text_content, &old))
|
|
return;
|
|
printf(" clamped TE from %s to %s\n", c2_rect_as_str(&old),
|
|
c2_rect_as_str(&te->text_content));
|
|
mui_control_inval(&te->control);
|
|
}
|
|
|
|
/* This scrolls the view following the carret, used when typing.
|
|
* This doesn't check for out of bounds, but the clamping should
|
|
* have made sure the text is always visible. */
|
|
static void
|
|
_mui_textedit_ensure_carret_visible(
|
|
mui_textedit_control_t * te)
|
|
{
|
|
c2_rect_t f = te->control.frame;
|
|
// c2_rect_offset(&f, -f.l, -f.t);
|
|
if (te->flags & MUI_CONTROL_TEXTBOX_FRAME)
|
|
c2_rect_inset(&f, te->margin.x, te->margin.y);
|
|
if (te->sel.start != te->sel.end)
|
|
return;
|
|
c2_rect_t old = te->text_content;
|
|
c2_rect_t r = te->sel.first;
|
|
printf("%s carret %s frame %s\n", __func__,
|
|
c2_rect_as_str(&r), c2_rect_as_str(&f));
|
|
c2_rect_offset(&r, -te->text_content.l, -te->text_content.t);
|
|
if (r.r < f.l) {
|
|
printf(" moved TE LEFT %d\n", -(f.l - r.r));
|
|
c2_rect_offset(&te->text_content, -(f.l - r.l), 0);
|
|
}
|
|
if (r.l > f.r) {
|
|
printf(" moved TE RIGHT %d\n", -(r.l - f.r));
|
|
c2_rect_offset(&te->text_content, -(r.l - f.r), 0);
|
|
}
|
|
if (r.t < f.t)
|
|
c2_rect_offset(&te->text_content, 0, r.t - f.t);
|
|
if (r.b > f.b)
|
|
c2_rect_offset(&te->text_content, 0, r.b - f.b);
|
|
if (c2_rect_equal(&te->text_content, &old))
|
|
return;
|
|
printf(" moved TE from %s to %s\n", c2_rect_as_str(&old),
|
|
c2_rect_as_str(&te->text_content));
|
|
_mui_textedit_clamp_text_frame(te);
|
|
}
|
|
|
|
/*
|
|
* This is to be called when the text changes, or the frame (width) changes
|
|
*/
|
|
static void
|
|
_mui_textedit_refresh_measure(
|
|
mui_textedit_control_t * te)
|
|
{
|
|
c2_rect_t f = te->control.frame;
|
|
c2_rect_offset(&f, -f.l, -f.t);
|
|
if (te->flags & MUI_CONTROL_TEXTBOX_FRAME)
|
|
c2_rect_inset(&f, te->margin.x, te->margin.y);
|
|
if (!(te->flags & MUI_CONTROL_TEXTEDIT_VERTICAL))
|
|
f.r = 0x7fff; // make it very large, we don't want wrapping.
|
|
|
|
mui_glyph_line_array_t new_measure = {};
|
|
|
|
mui_font_measure(te->font, f,
|
|
(const char*)te->text.e, te->text.count-1,
|
|
&new_measure, te->flags);
|
|
|
|
f = te->control.frame;
|
|
if (te->flags & MUI_CONTROL_TEXTBOX_FRAME)
|
|
c2_rect_inset(&f, te->margin.x, te->margin.y);
|
|
// Refresh the lines that have changed. Perhaps all of them did,
|
|
// doesn't matter, but it's nice to avoid redrawing the whole text
|
|
// when someone is typing.
|
|
for (uint i = 0; i < new_measure.count && i < te->measure.count; i++) {
|
|
if (i >= te->measure.count) {
|
|
c2_rect_t r = f;
|
|
r.t += new_measure.e[i].t;
|
|
r.b = r.t + new_measure.e[i].b;
|
|
r.r = new_measure.e[i].x + new_measure.e[i].w;
|
|
_mui_textedit_inval(te, r);
|
|
} else if (i >= new_measure.count) {
|
|
c2_rect_t r = f;
|
|
r.t += te->measure.e[i].t;
|
|
r.b = r.t + te->measure.e[i].b;
|
|
r.r = te->measure.e[i].x + te->measure.e[i].w;
|
|
_mui_textedit_inval(te, r);
|
|
} else {
|
|
int dirty = 0;
|
|
// unsure if this could happen, but let's be safe --
|
|
// technically we should refresh BOTH rectangles (old, new)
|
|
if (new_measure.e[i].t != te->measure.e[i].t ||
|
|
new_measure.e[i].b != te->measure.e[i].b) {
|
|
dirty = 1;
|
|
} else if (new_measure.e[i].x != te->measure.e[i].x ||
|
|
new_measure.e[i].count != te->measure.e[i].count ||
|
|
new_measure.e[i].w != te->measure.e[i].w)
|
|
dirty = 1;
|
|
else {
|
|
for (uint x = 0; x < new_measure.e[i].count; x++) {
|
|
if (new_measure.e[i].e[x].glyph != te->measure.e[i].e[x].glyph ||
|
|
new_measure.e[i].e[x].x != te->measure.e[i].e[x].x ||
|
|
new_measure.e[i].e[x].w != te->measure.e[i].e[x].w) {
|
|
dirty = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (dirty) {
|
|
c2_rect_t r = f;
|
|
r.t += new_measure.e[i].t;
|
|
r.b = r.t + new_measure.e[i].b;
|
|
r.r = new_measure.e[i].x + new_measure.e[i].w;
|
|
_mui_textedit_inval(te, r);
|
|
}
|
|
}
|
|
}
|
|
mui_font_measure_clear(&te->measure);
|
|
te->measure = new_measure;
|
|
_mui_textedit_clamp_text_frame(te);
|
|
}
|
|
|
|
static void
|
|
_mui_textedit_sel_delete(
|
|
mui_textedit_control_t * te,
|
|
bool re_measure,
|
|
bool reset_sel)
|
|
{
|
|
if (te->sel.start == te->sel.end)
|
|
return;
|
|
mui_utf8_delete(&te->text,
|
|
_mui_glyph_to_byte_offset(&te->measure, te->sel.start),
|
|
_mui_glyph_to_byte_offset(&te->measure, te->sel.end) -
|
|
_mui_glyph_to_byte_offset(&te->measure, te->sel.start));
|
|
if (re_measure)
|
|
_mui_textedit_refresh_measure(te);
|
|
if (reset_sel)
|
|
_mui_textedit_select_signed(te,
|
|
te->sel.start, te->sel.start);
|
|
}
|
|
|
|
void
|
|
mui_textedit_set_text(
|
|
mui_control_t * c,
|
|
const char * text)
|
|
{
|
|
mui_textedit_control_t *te = (mui_textedit_control_t *)c;
|
|
mui_utf8_clear(&te->text);
|
|
int tl = strlen(text);
|
|
mui_utf8_realloc(&te->text, tl + 1);
|
|
memcpy(te->text.e, text, tl + 1);
|
|
/*
|
|
* Note, the text.count *counts the terminating zero*
|
|
*/
|
|
te->text.count = tl + 1;
|
|
if (!te->font)
|
|
te->font = mui_font_find(c->win->ui, "main");
|
|
_mui_textedit_refresh_measure(te);
|
|
}
|
|
|
|
/* this one allows passing -1 etc, which is handy of cursor movement */
|
|
static void
|
|
_mui_textedit_select_signed(
|
|
mui_textedit_control_t * te,
|
|
int glyph_start,
|
|
int glyph_end)
|
|
{
|
|
if (glyph_start < 0)
|
|
glyph_start = 0;
|
|
if (glyph_end < 0)
|
|
glyph_end = 0;
|
|
if (glyph_end > (int)te->text.count)
|
|
glyph_end = te->text.count;
|
|
if (glyph_start > (int)te->text.count)
|
|
glyph_start = te->text.count;
|
|
if (glyph_start > glyph_end) {
|
|
uint t = glyph_start;
|
|
glyph_start = glyph_end;
|
|
glyph_end = t;
|
|
}
|
|
|
|
printf("%s %d:%d\n", __func__, glyph_start, glyph_end);
|
|
c2_rect_t f = te->control.frame;
|
|
if (te->flags & MUI_CONTROL_TEXTBOX_FRAME)
|
|
c2_rect_inset(&f, te->margin.x, te->margin.y);
|
|
|
|
mui_glyph_line_array_t * measure = &te->measure;
|
|
_mui_textedit_refresh_sel(te, NULL);
|
|
mui_sel_t newone = { .start = glyph_start, .end = glyph_end };
|
|
_mui_make_sel_rects(measure, te->font, &newone, f);
|
|
te->sel = newone;
|
|
_mui_textedit_ensure_carret_visible(te);
|
|
_mui_textedit_refresh_sel(te, NULL);
|
|
}
|
|
|
|
/*
|
|
* Mark old selection as invalid, and set the new one,
|
|
* and make sure it's visible
|
|
*/
|
|
void
|
|
mui_textedit_set_selection(
|
|
mui_control_t * c,
|
|
uint glyph_start,
|
|
uint glyph_end)
|
|
{
|
|
mui_textedit_control_t *te = (mui_textedit_control_t *)c;
|
|
_mui_textedit_select_signed(te, glyph_start, glyph_end);
|
|
}
|
|
|
|
static void
|
|
mui_textedit_draw(
|
|
mui_window_t * win,
|
|
mui_control_t * c,
|
|
mui_drawable_t *dr )
|
|
{
|
|
c2_rect_t f = c->frame;
|
|
c2_rect_offset(&f, win->content.l, win->content.t);
|
|
|
|
mui_textedit_control_t *te = (mui_textedit_control_t *)c;
|
|
|
|
mui_drawable_clip_push(dr, &f);
|
|
struct cg_ctx_t * cg = mui_drawable_get_cg(dr);
|
|
if (te->flags & MUI_CONTROL_TEXTBOX_FRAME) {
|
|
cg_set_line_width(cg, 1);
|
|
cg_set_source_color(cg, &CG_COLOR(mui_control_color[c->state].frame));
|
|
cg_rectangle(cg, f.l, f.t,
|
|
c2_rect_width(&f), c2_rect_height(&f));
|
|
cg_stroke(cg);
|
|
}
|
|
if (te->text.count <= 1)
|
|
goto done;
|
|
if (te->flags & MUI_CONTROL_TEXTBOX_FRAME)
|
|
c2_rect_inset(&f, te->margin.x, te->margin.y);
|
|
mui_drawable_clip_push(dr, &f);
|
|
cg = mui_drawable_get_cg(dr); // this updates the cg clip too
|
|
bool is_active = c == c->win->control_focus.control;
|
|
if (te->sel.start == te->sel.end) {
|
|
if (te->sel.carret && is_active) {
|
|
c2_rect_t carret = te->sel.first;
|
|
c2_rect_offset(&carret,
|
|
c->win->content.l + te->text_content.tl.x,
|
|
c->win->content.t + te->text_content.tl.y);
|
|
// rect is empty, but it's a carret!
|
|
// draw a line at the current position
|
|
cg_set_line_width(cg, 1);
|
|
cg_set_source_color(cg, &CG_COLOR(mui_control_color[c->state].text));
|
|
cg_move_to(cg, carret.l, carret.t);
|
|
cg_line_to(cg, carret.l, carret.b);
|
|
cg_stroke(cg);
|
|
}
|
|
} else {
|
|
if (is_active) {
|
|
for (int i = 0; i < 3; i++) {
|
|
if (!c2_rect_isempty(&te->sel.e[i])) {
|
|
c2_rect_t sr = te->sel.e[i];
|
|
// c2_rect_clip_rect(&sr, &f, &sr);
|
|
cg_set_source_color(cg, &CG_COLOR(c->win->ui->color.highlight));
|
|
c2_rect_offset(&sr,
|
|
c->win->content.l + te->text_content.tl.x,
|
|
c->win->content.t + te->text_content.tl.y);
|
|
cg_rectangle(cg,
|
|
sr.l, sr.t, c2_rect_width(&sr), c2_rect_height(&sr));
|
|
cg_fill(cg);
|
|
}
|
|
}
|
|
} else { // draw a path around the selection
|
|
cg_set_line_width(cg, 2);
|
|
cg_set_source_color(cg, &CG_COLOR(c->win->ui->color.highlight));
|
|
mui_sel_t o = te->sel;
|
|
for (int i = 0; i < 3; i++)
|
|
c2_rect_offset(&o.e[i],
|
|
c->win->content.l + te->text_content.tl.x,
|
|
c->win->content.t + te->text_content.tl.y);
|
|
cg_move_to(cg, o.first.l, o.first.t);
|
|
cg_line_to(cg, o.first.r, o.first.t);
|
|
cg_line_to(cg, o.first.r, o.first.b);
|
|
if (c2_rect_isempty(&o.last))
|
|
cg_line_to(cg, o.first.l, o.first.b);
|
|
else {
|
|
cg_line_to(cg, o.first.r, o.first.b);
|
|
cg_line_to(cg, o.first.r, o.last.t);
|
|
cg_line_to(cg, o.last.r, o.last.t);
|
|
cg_line_to(cg, o.last.r, o.last.b);
|
|
cg_line_to(cg, o.last.l, o.last.b);
|
|
cg_line_to(cg, o.last.l, o.first.b);
|
|
}
|
|
cg_line_to(cg, o.first.l, o.first.b);
|
|
cg_line_to(cg, o.first.l, o.first.t);
|
|
cg_stroke(cg);
|
|
}
|
|
}
|
|
c2_rect_t tf = f;
|
|
c2_rect_offset(&tf, te->text_content.tl.x, te->text_content.tl.y);
|
|
mui_font_measure_draw(te->font, dr, tf,
|
|
&te->measure, mui_control_color[c->state].text, te->flags);
|
|
mui_drawable_clip_pop(dr);
|
|
cg = mui_drawable_get_cg(dr); // this updates the cg clip too
|
|
if (te->flags & MUI_CONTROL_TEXTBOX_FRAME) {
|
|
if (c2_rect_width(&f) < c2_rect_width(&te->text_content)) {
|
|
// draw a line-like mini scroll bar to show scroll position
|
|
int fsize = c2_rect_width(&f);
|
|
int tsize = c2_rect_width(&te->text_content);
|
|
float ratio = fsize / (float)tsize;
|
|
float dsize = fsize * ratio;
|
|
c2_rect_t r = C2_RECT_WH(f.l, f.b + 1, dsize, 1);
|
|
float pos = -te->text_content.tl.x / (float)(tsize - fsize);
|
|
c2_rect_offset(&r, (fsize - dsize) * pos, 0);
|
|
cg_set_source_color(cg,
|
|
&CG_COLOR(mui_control_color[c->state].frame));
|
|
cg_move_to(cg, r.l, r.t);
|
|
cg_line_to(cg, r.r, r.t);
|
|
cg_stroke(cg);
|
|
}
|
|
// same for vertical
|
|
if (c2_rect_height(&f) < c2_rect_height(&te->text_content)) {
|
|
int fsize = c2_rect_height(&f);
|
|
int tsize = c2_rect_height(&te->text_content);
|
|
float ratio = fsize / (float)tsize;
|
|
float dsize = fsize * ratio;
|
|
c2_rect_t r = C2_RECT_WH(f.r +1, f.t, 1, dsize);
|
|
float pos = -te->text_content.tl.y / (float)(tsize - fsize);
|
|
c2_rect_offset(&r, 0, (fsize - dsize) * pos);
|
|
cg_set_source_color(cg,
|
|
&CG_COLOR(mui_control_color[c->state].frame));
|
|
cg_move_to(cg, r.l, r.t);
|
|
cg_line_to(cg, r.l, r.b);
|
|
cg_stroke(cg);
|
|
}
|
|
}
|
|
done:
|
|
mui_drawable_clip_pop(dr);
|
|
}
|
|
|
|
static bool
|
|
mui_textedit_mouse(
|
|
struct mui_control_t * c,
|
|
mui_event_t * ev)
|
|
{
|
|
mui_textedit_control_t *te = (mui_textedit_control_t *)c;
|
|
|
|
c2_rect_t f = c->frame;
|
|
c2_rect_offset(&f, c->win->content.l, c->win->content.t);
|
|
uint line = 0, index = 0;
|
|
bool res = false;
|
|
switch (ev->type) {
|
|
case MUI_EVENT_BUTTONDOWN: {
|
|
if (!c2_rect_contains_pt(&f, &ev->mouse.where))
|
|
break;
|
|
// if we aren't the focus, make us the focus
|
|
if (c != c->win->control_focus.control) {
|
|
mui_control_t * prev = c->win->control_focus.control;
|
|
|
|
mui_cdef_textedit(c, MUI_CDEF_ACTIVATE, &(int){0});
|
|
mui_control_inval(c);
|
|
mui_cdef_textedit(prev, MUI_CDEF_ACTIVATE, &(int){1});
|
|
mui_control_inval(prev);
|
|
mui_control_deref(&c->win->control_focus);
|
|
mui_control_ref(&c->win->control_focus, c,
|
|
FCC('T','e','a','c'));
|
|
}
|
|
if (_mui_point_to_line_index(te, te->font, f,
|
|
ev->mouse.where, &line, &index) == 0) {
|
|
uint pos = _mui_line_index_to_glyph(
|
|
&te->measure, line, index);
|
|
te->selecting_mode = MUI_TE_SELECTING_GLYPHS;
|
|
if (ev->mouse.count == 2) {
|
|
// double click, select word
|
|
uint32_t start,end;
|
|
_mui_line_index_to_glyph_word(&te->measure, line, index,
|
|
&start, &end);
|
|
_mui_textedit_select_signed(te, start, end);
|
|
te->selecting_mode = MUI_TE_SELECTING_WORDS;
|
|
} else if (ev->modifiers & MUI_MODIFIER_SHIFT) {
|
|
// shift click, extend selection
|
|
if (pos < te->sel.start) {
|
|
_mui_textedit_select_signed(te, pos, te->sel.end);
|
|
} else {
|
|
_mui_textedit_select_signed(te, te->sel.start, pos);
|
|
}
|
|
} else {
|
|
// single click, set carret (and start selection
|
|
_mui_textedit_select_signed(te, pos, pos);
|
|
}
|
|
te->click.start = te->sel.start;
|
|
te->click.end = te->sel.end;
|
|
printf("DOWN line %2d index %3d pos:%3d\n",
|
|
line, index, pos);
|
|
res = true;
|
|
};
|
|
te->sel.carret = 0;
|
|
} break;
|
|
case MUI_EVENT_BUTTONUP: {
|
|
res = true;
|
|
if (_mui_point_to_line_index(te, te->font, f,
|
|
ev->mouse.where, &line, &index) == 0) {
|
|
printf("UP line %d index %d\n", line, index);
|
|
}
|
|
te->sel.carret = 1;
|
|
_mui_textedit_refresh_sel(te, NULL);
|
|
} break;
|
|
case MUI_EVENT_DRAG: {
|
|
res = true;
|
|
if (!c2_rect_contains_pt(&f, &ev->mouse.where)) {
|
|
if (te->flags & MUI_CONTROL_TEXTEDIT_VERTICAL) {
|
|
if (ev->mouse.where.y > f.b) {
|
|
te->text_content.tl.y -= ev->mouse.where.y - f.b;
|
|
printf("scroll down %3d\n", te->text_content.tl.y);
|
|
_mui_textedit_clamp_text_frame(te);
|
|
mui_control_inval(c);
|
|
} else if (ev->mouse.where.y < f.t) {
|
|
te->text_content.tl.y += f.t - ev->mouse.where.y;
|
|
printf("scroll up %3d\n", te->text_content.tl.y);
|
|
_mui_textedit_clamp_text_frame(te);
|
|
mui_control_inval(c);
|
|
}
|
|
} else {
|
|
if (ev->mouse.where.x > f.r) {
|
|
te->text_content.tl.x -= ev->mouse.where.x - f.r;
|
|
printf("scroll right %3d\n", te->text_content.tl.x);
|
|
_mui_textedit_clamp_text_frame(te);
|
|
mui_control_inval(c);
|
|
} else if (ev->mouse.where.x < f.l) {
|
|
te->text_content.tl.x += f.l - ev->mouse.where.x;
|
|
printf("scroll left %3d\n", te->text_content.tl.x);
|
|
_mui_textedit_clamp_text_frame(te);
|
|
mui_control_inval(c);
|
|
}
|
|
}
|
|
}
|
|
if (_mui_point_to_line_index(te, te->font, f,
|
|
ev->mouse.where, &line, &index) == 0) {
|
|
// printf(" line %d index %d\n", line, index);
|
|
uint pos = _mui_line_index_to_glyph(
|
|
&te->measure, line, index);
|
|
if (te->selecting_mode == MUI_TE_SELECTING_WORDS) {
|
|
uint32_t start,end;
|
|
_mui_line_index_to_glyph_word(&te->measure, line, index,
|
|
&start, &end);
|
|
_mui_line_index_to_glyph_word(&te->measure,
|
|
line, index, &start, &end);
|
|
if (pos < te->click.start)
|
|
_mui_textedit_select_signed(te, start, te->click.end);
|
|
else
|
|
_mui_textedit_select_signed(te, te->click.start, end);
|
|
} else {
|
|
if (pos < te->click.start)
|
|
_mui_textedit_select_signed(te, pos, te->click.start);
|
|
else
|
|
_mui_textedit_select_signed(te, te->click.start, pos);
|
|
}
|
|
}
|
|
} break;
|
|
case MUI_EVENT_WHEEL: {
|
|
if (te->flags & MUI_CONTROL_TEXTEDIT_VERTICAL) {
|
|
te->text_content.tl.y -= ev->wheel.delta * 10;
|
|
_mui_textedit_clamp_text_frame(te);
|
|
mui_control_inval(c);
|
|
} else {
|
|
te->text_content.tl.x -= ev->wheel.delta * 10;
|
|
_mui_textedit_clamp_text_frame(te);
|
|
mui_control_inval(c);
|
|
}
|
|
res = true;
|
|
} break;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
static bool
|
|
mui_textedit_key(
|
|
struct mui_control_t * c,
|
|
mui_event_t * ev)
|
|
{
|
|
mui_textedit_control_t *te = (mui_textedit_control_t *)c;
|
|
|
|
_mui_textedit_show_carret(te);
|
|
mui_glyph_line_array_t * me = &te->measure;
|
|
if (ev->modifiers & MUI_MODIFIER_CTRL) {
|
|
switch (ev->key.key) {
|
|
case 'T': {
|
|
te->trace = !te->trace;
|
|
printf("TRACE %s\n", te->trace ? "ON" : "OFF");
|
|
} break;
|
|
case 'D': {// dump text status and measures lines
|
|
printf("Text:\n'%s'\n", te->text.e);
|
|
printf("Text count: %d\n", te->text.count);
|
|
printf("Text measure: %d\n", me->count);
|
|
for (uint i = 0; i < me->count; i++) {
|
|
mui_glyph_array_t * line = &me->e[i];
|
|
printf(" line %d: %d\n", i, line->count);
|
|
for (uint j = 0; j < line->count; j++) {
|
|
mui_glyph_t * g = &line->e[j];
|
|
printf(" %3d: %04x:%c x:%3f w:%3d\n",
|
|
j, te->text.e[g->pos],
|
|
te->text.e[g->pos] < ' ' ?
|
|
'.' : te->text.e[g->pos],
|
|
g->x, g->w);
|
|
}
|
|
}
|
|
te->flags |= MUI_TEXT_DEBUG;
|
|
} break;
|
|
case 'a': {
|
|
_mui_textedit_select_signed(te, 0, te->text.count-1);
|
|
} break;
|
|
case 'c': {
|
|
if (te->sel.start != te->sel.end) {
|
|
uint32_t start = _mui_glyph_to_byte_offset(me, te->sel.start);
|
|
uint32_t end = _mui_glyph_to_byte_offset(me, te->sel.end);
|
|
mui_clipboard_set(c->win->ui,
|
|
te->text.e + start, end - start);
|
|
}
|
|
} break;
|
|
case 'x': {
|
|
if (te->sel.start != te->sel.end) {
|
|
uint32_t start = _mui_glyph_to_byte_offset(me, te->sel.start);
|
|
uint32_t end = _mui_glyph_to_byte_offset(me, te->sel.end);
|
|
mui_clipboard_set(c->win->ui,
|
|
te->text.e + start, end - start);
|
|
_mui_textedit_sel_delete(te, true, true);
|
|
}
|
|
} break;
|
|
case 'v': {
|
|
uint32_t len;
|
|
const uint8_t * clip = mui_clipboard_get(c->win->ui, &len);
|
|
if (clip) {
|
|
if (te->sel.start != te->sel.end)
|
|
_mui_textedit_sel_delete(te, true, true);
|
|
mui_utf8_insert(&te->text,
|
|
_mui_glyph_to_byte_offset(me, te->sel.start),
|
|
clip, len);
|
|
_mui_textedit_refresh_measure(te);
|
|
_mui_textedit_select_signed(te,
|
|
te->sel.start + len, te->sel.start + len);
|
|
}
|
|
} break;
|
|
}
|
|
return true;
|
|
}
|
|
switch (ev->key.key) {
|
|
case MUI_KEY_UP: {
|
|
uint line, index;
|
|
_mui_glyph_to_line_index(me, te->sel.start, &line, &index);
|
|
if (line > 0) {
|
|
uint pos = _mui_line_index_to_glyph(me, line-1, index);
|
|
if (ev->modifiers & MUI_MODIFIER_SHIFT) {
|
|
_mui_textedit_select_signed(te, te->sel.start, pos);
|
|
} else {
|
|
_mui_textedit_select_signed(te, pos, pos);
|
|
}
|
|
}
|
|
} break;
|
|
case MUI_KEY_DOWN: {
|
|
uint line, index;
|
|
_mui_glyph_to_line_index(me, te->sel.start, &line, &index);
|
|
if (line < me->count-1) {
|
|
uint pos = _mui_line_index_to_glyph(me, line+1, index);
|
|
if (ev->modifiers & MUI_MODIFIER_SHIFT) {
|
|
_mui_textedit_select_signed(te, te->sel.start, pos);
|
|
} else {
|
|
_mui_textedit_select_signed(te, pos, pos);
|
|
}
|
|
}
|
|
} break;
|
|
case MUI_KEY_LEFT: {
|
|
if (ev->modifiers & MUI_MODIFIER_SHIFT) {
|
|
_mui_textedit_select_signed(te, te->sel.start - 1, te->sel.end);
|
|
} else {
|
|
if (te->sel.start == te->sel.end)
|
|
_mui_textedit_select_signed(te, te->sel.start - 1, te->sel.start - 1);
|
|
else
|
|
_mui_textedit_select_signed(te, te->sel.start, te->sel.start);
|
|
}
|
|
} break;
|
|
case MUI_KEY_RIGHT: {
|
|
if (ev->modifiers & MUI_MODIFIER_SHIFT) {
|
|
_mui_textedit_select_signed(te, te->sel.start, te->sel.end + 1);
|
|
} else {
|
|
if (te->sel.start == te->sel.end)
|
|
_mui_textedit_select_signed(te, te->sel.start + 1, te->sel.start + 1);
|
|
else
|
|
_mui_textedit_select_signed(te, te->sel.end, te->sel.end);
|
|
}
|
|
} break;
|
|
case MUI_KEY_BACKSPACE: {
|
|
if (te->sel.start == te->sel.end) {
|
|
if (te->sel.start > 0) {
|
|
mui_utf8_delete(&te->text,
|
|
_mui_glyph_to_byte_offset(me, te->sel.start - 1),
|
|
1);
|
|
_mui_textedit_refresh_measure(te);
|
|
_mui_textedit_select_signed(te, te->sel.start - 1, te->sel.start - 1);
|
|
}
|
|
} else {
|
|
_mui_textedit_sel_delete(te, true, true);
|
|
}
|
|
} break;
|
|
case MUI_KEY_DELETE: {
|
|
if (te->sel.start == te->sel.end) {
|
|
if (te->sel.start < te->text.count-1) {
|
|
mui_utf8_delete(&te->text,
|
|
_mui_glyph_to_byte_offset(me, te->sel.start), 1);
|
|
_mui_textedit_refresh_measure(te);
|
|
_mui_textedit_select_signed(te, te->sel.start, te->sel.start);
|
|
}
|
|
} else {
|
|
_mui_textedit_sel_delete(te, true, true);
|
|
}
|
|
} break;
|
|
case '\t': {
|
|
// look for the next window control that is a text-edit (loop to
|
|
// start of necessary, and set it as the focus -- deactivate this one)
|
|
mui_control_t * next = c;
|
|
do {
|
|
next = TAILQ_NEXT(next, self);
|
|
if (!next)
|
|
next = TAILQ_FIRST(&c->win->controls);
|
|
if (next->cdef == mui_cdef_textedit) {
|
|
mui_cdef_textedit(c, MUI_CDEF_ACTIVATE, &(int){0});
|
|
mui_control_inval(c);
|
|
mui_cdef_textedit(next, MUI_CDEF_ACTIVATE, &(int){1});
|
|
mui_control_inval(next);
|
|
mui_control_deref(&c->win->control_focus);
|
|
mui_control_ref(&c->win->control_focus, next,
|
|
FCC('T','e','a','c'));
|
|
break;
|
|
}
|
|
} while (next != c);
|
|
} break;
|
|
default:
|
|
printf("%s key 0x%x\n", __func__, ev->key.key);
|
|
if (ev->key.key == 13 ||
|
|
(ev->key.key >= 32 && ev->key.key < 127)) {
|
|
if (te->sel.start != te->sel.end) {
|
|
_mui_textedit_sel_delete(te, false, false);
|
|
_mui_textedit_select_signed(te, te->sel.start, te->sel.start);
|
|
}
|
|
uint8_t k = ev->key.key;
|
|
mui_utf8_insert(&te->text,
|
|
_mui_glyph_to_byte_offset(me, te->sel.start), &k, 1);
|
|
_mui_textedit_refresh_measure(te);
|
|
_mui_textedit_select_signed(te,
|
|
te->sel.start + 1, te->sel.start + 1);
|
|
}
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
mui_cdef_textedit(
|
|
struct mui_control_t * c,
|
|
uint8_t what,
|
|
void * param)
|
|
{
|
|
if (!c)
|
|
return false;
|
|
mui_textedit_control_t *te = (mui_textedit_control_t *)c;
|
|
switch (what) {
|
|
case MUI_CDEF_INIT: {
|
|
if (!c->win->control_focus.control)
|
|
mui_control_ref(&c->win->control_focus, c,
|
|
FCC('T','e','a','c'));
|
|
/* If we are the first text-edit created, register the timer */
|
|
if (c->win->ui->carret_timer == 0xff)
|
|
c->win->ui->carret_timer = mui_timer_register(c->win->ui,
|
|
_mui_textedit_carret_timer, NULL,
|
|
500 * MUI_TIME_MS);
|
|
if (mui_window_isfront(c->win)) {
|
|
int activate = 1;
|
|
mui_cdef_textedit(c, MUI_CDEF_ACTIVATE, &activate);
|
|
}
|
|
} break;
|
|
case MUI_CDEF_DRAW: {
|
|
mui_drawable_t * dr = param;
|
|
mui_textedit_draw(c->win, c, dr);
|
|
} break;
|
|
case MUI_CDEF_DISPOSE: {
|
|
mui_font_measure_clear(&te->measure);
|
|
mui_utf8_clear(&te->text);
|
|
/*
|
|
* If we are the focus, and we are being disposed, we need to
|
|
* find another control to focus on, if there is one.
|
|
* This is a bit tricky, as the control isn't attached to the
|
|
* window anymore, so we might have to devise another plan.
|
|
*/
|
|
if (c->win->control_focus.control == c) {
|
|
mui_control_deref(&c->win->control_focus);
|
|
}
|
|
} break;
|
|
case MUI_CDEF_EVENT: {
|
|
// printf("%s event\n", __func__);
|
|
mui_event_t *ev = param;
|
|
switch (ev->type) {
|
|
case MUI_EVENT_WHEEL:
|
|
case MUI_EVENT_BUTTONUP:
|
|
case MUI_EVENT_DRAG:
|
|
case MUI_EVENT_BUTTONDOWN: {
|
|
return mui_textedit_mouse(c, ev);
|
|
} break;
|
|
case MUI_EVENT_KEYDOWN: {
|
|
return mui_textedit_key(c, ev);
|
|
} break;
|
|
}
|
|
} break;
|
|
case MUI_CDEF_ACTIVATE: {
|
|
// int activate = *(int*)param;
|
|
// printf("%s activate %d\n", __func__, activate);
|
|
// mui_textedit_control_t *te = (mui_textedit_control_t *)c;
|
|
} break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
mui_control_t *
|
|
mui_textedit_control_new(
|
|
mui_window_t * win,
|
|
c2_rect_t frame,
|
|
uint32_t flags)
|
|
{
|
|
mui_textedit_control_t *te = (mui_textedit_control_t *)mui_control_new(
|
|
win, MUI_CONTROL_TEXTEDIT, mui_cdef_textedit,
|
|
frame, NULL, 0, sizeof(mui_textedit_control_t));
|
|
te->flags = flags;
|
|
te->margin = (c2_pt_t){ .x = 4, .y = 2 };
|
|
return &te->control;
|
|
}
|