Compare commits
No commits in common. "546f3628c755f1bfbf0513b8f229a0259ea5b39e" and "e3c0bac9ca23f03422b6f8f23145b9331dd85fba" have entirely different histories.
546f3628c7
...
e3c0bac9ca
@ -65,9 +65,6 @@ struct Font {
|
|||||||
bool should_update; // should send update_atlas command, resets at frame_end()
|
bool should_update; // should send update_atlas command, resets at frame_end()
|
||||||
}
|
}
|
||||||
|
|
||||||
macro Rect Glyph.bounds(&g) => {.x = g.ox, .y = g.oy, .w = g.w, .h = g.h};
|
|
||||||
macro Rect Glyph.uv(&g) => {.x = g.u, .y = g.v, .w = g.w, .h = g.h};
|
|
||||||
|
|
||||||
<*
|
<*
|
||||||
@param [&inout] font
|
@param [&inout] font
|
||||||
@param [in] name
|
@param [in] name
|
||||||
|
@ -9,8 +9,6 @@ struct LineInfo @local {
|
|||||||
short first_off; // first character offset
|
short first_off; // first character offset
|
||||||
}
|
}
|
||||||
|
|
||||||
macro usz LineInfo.len(li) => li.end-li.start;
|
|
||||||
|
|
||||||
alias LineStack @local = list::List{LineInfo};
|
alias LineStack @local = list::List{LineInfo};
|
||||||
|
|
||||||
fn short Rect.y_off(Rect bounds, short height, Anchor anchor) @local
|
fn short Rect.y_off(Rect bounds, short height, Anchor anchor) @local
|
||||||
@ -62,245 +60,32 @@ fn short Rect.x_off(Rect bounds, short width, Anchor anchor) @local
|
|||||||
// ---------------------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------------------- //
|
||||||
|
|
||||||
|
|
||||||
struct GlyphIterator {
|
|
||||||
short baseline;
|
|
||||||
short line_height;
|
|
||||||
short line_gap;
|
|
||||||
short space_width;
|
|
||||||
short tab_width;
|
|
||||||
Rect bounds;
|
|
||||||
Anchor anchor;
|
|
||||||
bool reflow;
|
|
||||||
Font* font;
|
|
||||||
|
|
||||||
LineStack lines;
|
|
||||||
usz line_off, line_idx;
|
|
||||||
String text;
|
|
||||||
|
|
||||||
Codepoint cp;
|
|
||||||
Glyph* gp;
|
|
||||||
Point o;
|
|
||||||
Rect str_bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
<*
|
|
||||||
@param [&inout] self
|
|
||||||
@param [in] text
|
|
||||||
@param [&inout] font
|
|
||||||
*>
|
|
||||||
fn void? GlyphIterator.init(&self, Allocator allocator, String text, Rect bounds, Font* font, Anchor anchor, bool reflow, uint tab_size)
|
|
||||||
{
|
|
||||||
self.font = font;
|
|
||||||
|
|
||||||
self.line_height = (short)font.line_height();
|
|
||||||
self.baseline = (short)font.ascender;
|
|
||||||
self.line_gap = (short)font.linegap;
|
|
||||||
self.space_width = font.get_glyph(' ').adv!;
|
|
||||||
self.tab_width = self.space_width * (short)tab_size;
|
|
||||||
|
|
||||||
self.bounds = bounds;
|
|
||||||
self.o = bounds.position();
|
|
||||||
self.reflow = reflow;
|
|
||||||
self.text = text;
|
|
||||||
self.anchor = anchor;
|
|
||||||
|
|
||||||
// if the anchor is top_left we can skip dividing the string line by line, in GlyphIterator.next
|
|
||||||
// this has to be accounted for
|
|
||||||
if (anchor != TOP_LEFT) {
|
|
||||||
self.lines.init(allocator, 4);
|
|
||||||
self.populate_lines_stack()!;
|
|
||||||
self.line_off = 0;
|
|
||||||
self.line_idx = 0;
|
|
||||||
|
|
||||||
if (self.lines.len() > 0) {
|
|
||||||
self.o.y += bounds.y_off(self.str_bounds.h, anchor);
|
|
||||||
self.o.x += bounds.x_off(self.lines[0].width, anchor) - self.lines[0].first_off;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn void? GlyphIterator.populate_lines_stack(&self)
|
|
||||||
{
|
|
||||||
usz line_start;
|
|
||||||
LineInfo li;
|
|
||||||
Point o = self.o;
|
|
||||||
StringIterator ti = self.text.iterator();
|
|
||||||
|
|
||||||
usz prev_off;
|
|
||||||
for (Codepoint cp; ti.has_next();) {
|
|
||||||
cp = ti.next()!;
|
|
||||||
usz off = ti.current;
|
|
||||||
bool push = false;
|
|
||||||
|
|
||||||
li.height = self.line_height;
|
|
||||||
switch {
|
|
||||||
case cp == '\n':
|
|
||||||
push = true;
|
|
||||||
case cp == '\t':
|
|
||||||
o.x += self.tab_width;
|
|
||||||
case ascii::is_cntrl((char)cp):
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Glyph* gp = self.font.get_glyph(cp)!;
|
|
||||||
|
|
||||||
if (off == line_start) {
|
|
||||||
li.first_off = gp.ox;
|
|
||||||
o.x -= gp.ox;
|
|
||||||
}
|
|
||||||
|
|
||||||
Rect b = gp.bounds().off(o);
|
|
||||||
b.y += self.baseline;
|
|
||||||
|
|
||||||
if (self.reflow && b.x + b.w > self.bounds.x + self.bounds.w) {
|
|
||||||
push = true;
|
|
||||||
// roll back this character since it is on the next line
|
|
||||||
ti.current = prev_off;
|
|
||||||
off = prev_off;
|
|
||||||
} else {
|
|
||||||
o.x += gp.adv;
|
|
||||||
li.width += gp.adv;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (push) {
|
|
||||||
li.start = line_start;
|
|
||||||
li.end = off;
|
|
||||||
self.lines.push(li);
|
|
||||||
self.str_bounds.w = max(self.str_bounds.w, li.width);
|
|
||||||
self.str_bounds.h += li.height;
|
|
||||||
|
|
||||||
o.x = self.bounds.x;
|
|
||||||
o.y += self.line_height;
|
|
||||||
line_start = off;
|
|
||||||
|
|
||||||
li.height = 0;
|
|
||||||
li.width = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
prev_off = off;
|
|
||||||
}
|
|
||||||
if (line_start != ti.current) {
|
|
||||||
// FIXME: crap, can we not repeat this code?
|
|
||||||
li.start = line_start;
|
|
||||||
li.end = ti.current;
|
|
||||||
self.lines.push(li);
|
|
||||||
self.str_bounds.w = max(self.str_bounds.w, li.width);
|
|
||||||
self.str_bounds.h += li.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.str_bounds.h += (short)(self.lines.len()-1) * self.line_gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn String GlyphIterator.current_line(&self)
|
|
||||||
{
|
|
||||||
LineInfo li = self.lines[self.line_idx];
|
|
||||||
return self.text[li.start:li.len()];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn Rect? GlyphIterator.next(&self)
|
|
||||||
{
|
|
||||||
// check if there is a next glyph and maybe update the line and offset indices
|
|
||||||
if (self.anchor != TOP_LEFT) {
|
|
||||||
if (self.line_idx >= self.lines.len()) {
|
|
||||||
return NO_MORE_ELEMENT?;
|
|
||||||
}
|
|
||||||
|
|
||||||
LineInfo li = self.lines[self.line_idx];
|
|
||||||
if (self.line_off >= li.len()) {
|
|
||||||
self.line_idx++;
|
|
||||||
|
|
||||||
if (self.line_idx >= self.lines.len()) {
|
|
||||||
return NO_MORE_ELEMENT?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.line_off = 0;
|
|
||||||
li = self.lines[self.line_idx];
|
|
||||||
|
|
||||||
self.o.y += self.line_height + self.line_gap;
|
|
||||||
self.o.x = self.bounds.x + self.bounds.x_off(li.width, self.anchor) - li.first_off;
|
|
||||||
}
|
|
||||||
} else if (self.line_off >= self.text.len) {
|
|
||||||
return NO_MORE_ELEMENT?;
|
|
||||||
}
|
|
||||||
|
|
||||||
String t;
|
|
||||||
if (self.anchor != TOP_LEFT) {
|
|
||||||
t = self.current_line()[self.line_off..];
|
|
||||||
} else {
|
|
||||||
t = self.text[self.line_off..];
|
|
||||||
}
|
|
||||||
|
|
||||||
usz read = t.len < 4 ? t.len : 4;
|
|
||||||
self.cp = conv::utf8_to_char32(&t[0], &read)!;
|
|
||||||
self.line_off += read;
|
|
||||||
self.gp = self.font.get_glyph(self.cp)!;
|
|
||||||
|
|
||||||
Rect b = {.x = self.o.x, .y = self.o.y};
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case self.cp == '\n':
|
|
||||||
if (self.anchor == TOP_LEFT) {
|
|
||||||
self.o.x = self.bounds.x;
|
|
||||||
self.o.y += self.line_height + self.line_gap;
|
|
||||||
self.line_idx++;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case self.cp == '\t':
|
|
||||||
self.o.x += self.tab_width;
|
|
||||||
case ascii::is_cntrl((char)self.cp):
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
b = self.gp.bounds().off(self.o);
|
|
||||||
b.y += self.baseline;
|
|
||||||
if (self.anchor == TOP_LEFT) {
|
|
||||||
//if (self.o.x == self.bounds.x) self.bounds.x -= self.gp.ox;
|
|
||||||
if (self.reflow && b.bottom_right().x > self.bounds.bottom_right().x) {
|
|
||||||
self.o.x = self.bounds.x - self.gp.ox;
|
|
||||||
self.o.y += self.line_height + self.line_gap;
|
|
||||||
self.line_idx++;
|
|
||||||
b = self.gp.bounds().off(self.o);
|
|
||||||
b.y += self.baseline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.o.x += self.gp.adv;
|
|
||||||
}
|
|
||||||
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bool GlyphIterator.has_next(&self)
|
|
||||||
{
|
|
||||||
if (self.anchor == TOP_LEFT) {
|
|
||||||
return self.line_off < self.text.len;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.line_idx >= self.lines.len()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LineInfo li = self.lines[self.line_idx];
|
|
||||||
if (self.line_idx == self.lines.len() - 1 && self.line_off >= li.len()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn usz GlyphIterator.current_offset(&self)
|
|
||||||
{
|
|
||||||
if (self.anchor == TOP_LEFT) return self.line_off;
|
|
||||||
return self.lines[self.line_idx].start + self.line_off;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// layout a string inside a bounding box, following the given alignment (anchor).
|
|
||||||
<*
|
<*
|
||||||
@param[&in] ctx
|
@param[&in] ctx
|
||||||
@param[in] text
|
@param[in] text
|
||||||
*>
|
*>
|
||||||
fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, bool reflow = false)
|
fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, bool reflow = false)
|
||||||
|
{
|
||||||
|
if (anchor == TOP_LEFT) {
|
||||||
|
return ctx.layout_string_topleft(text, bounds, z_index, hue, reflow);
|
||||||
|
} else {
|
||||||
|
return ctx.layout_string_aligned(text, bounds, anchor, z_index, hue, reflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// layout a string inside a bounding box, following the given alignment (anchor).
|
||||||
|
// TODO: Following improvements
|
||||||
|
// [ ] implement a macro to fetch and layout each character, this can be used to reduce code
|
||||||
|
// repetition both here and in measure_string
|
||||||
|
// [ ] the cursor position can be determined by a separate function like get_cursor_position()
|
||||||
|
// this way the function can terminate early if the cursor position is found
|
||||||
|
// [ ] implement a function hit_test_string() to get the character position at point, this can
|
||||||
|
// be used to implement mouse interactions, like cursor movement and selection
|
||||||
|
<*
|
||||||
|
@param [&in] ctx
|
||||||
|
@param [in] text
|
||||||
|
*>
|
||||||
|
fn void? Ctx.layout_string_aligned(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, bool reflow = false)
|
||||||
{
|
{
|
||||||
if (text == "") return;
|
if (text == "") return;
|
||||||
if (bounds.w <= 0 || bounds.h <= 0) return;
|
if (bounds.w <= 0 || bounds.h <= 0) return;
|
||||||
@ -308,19 +93,188 @@ fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_
|
|||||||
|
|
||||||
Font* font = &ctx.font;
|
Font* font = &ctx.font;
|
||||||
Id texture_id = font.id;
|
Id texture_id = font.id;
|
||||||
|
short line_height = (short)font.line_height();
|
||||||
|
short baseline = (short)font.ascender;
|
||||||
|
short line_gap = (short)font.linegap;
|
||||||
|
short space_width = font.get_glyph(' ').adv!;
|
||||||
|
short tab_width = space_width * TAB_SIZE;
|
||||||
|
|
||||||
|
Point origin = bounds.position();
|
||||||
|
|
||||||
|
LineStack lines;
|
||||||
|
lines.init(tmem, 1);
|
||||||
|
|
||||||
|
Rect str_bounds;
|
||||||
|
|
||||||
|
usz line_start;
|
||||||
|
LineInfo li;
|
||||||
|
Point o;
|
||||||
|
StringIterator ti = text.iterator();
|
||||||
|
for (Codepoint cp; ti.has_next();) {
|
||||||
|
// FIXME: what if the interface changes?
|
||||||
|
cp = ti.next()!;
|
||||||
|
usz off = ti.current;
|
||||||
|
Glyph* gp = font.get_glyph(cp)!;
|
||||||
|
bool push = false;
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cp == '\n':
|
||||||
|
push = true;
|
||||||
|
case cp == '\t':
|
||||||
|
o.x += tab_width;
|
||||||
|
case ascii::is_cntrl((char)cp):
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (off == line_start) li.first_off = gp.ox;
|
||||||
|
|
||||||
|
Rect b = {
|
||||||
|
.x = o.x + gp.ox,
|
||||||
|
.y = o.y + gp.oy + baseline,
|
||||||
|
.w = gp.w,
|
||||||
|
.h = gp.h,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (reflow && b.x + b.w > bounds.w) {
|
||||||
|
li.width += gp.ox + gp.w;
|
||||||
|
li.height = line_height;
|
||||||
|
push = true;
|
||||||
|
} else {
|
||||||
|
o.x += gp.adv;
|
||||||
|
li.width += gp.adv;
|
||||||
|
li.height = line_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (push) {
|
||||||
|
li.start = line_start;
|
||||||
|
li.end = off;
|
||||||
|
lines.push(li);
|
||||||
|
str_bounds.w = max(str_bounds.w, li.width);
|
||||||
|
str_bounds.h += li.height;
|
||||||
|
|
||||||
|
o.x = 0;
|
||||||
|
o.y += line_height;
|
||||||
|
line_start = off;
|
||||||
|
|
||||||
|
li.height = 0;
|
||||||
|
li.width = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FIXME: crap
|
||||||
|
li.start = line_start;
|
||||||
|
li.end = ti.current;
|
||||||
|
lines.push(li);
|
||||||
|
str_bounds.w = max(str_bounds.w, li.width);
|
||||||
|
str_bounds.h += li.height;
|
||||||
|
|
||||||
|
// account for the line gap
|
||||||
|
str_bounds.h += (short)(lines.len() - 1)*line_gap;
|
||||||
|
|
||||||
|
o = bounds.position();
|
||||||
|
o.y += bounds.y_off(str_bounds.h, anchor);
|
||||||
|
foreach (idx, line : lines) {
|
||||||
|
o.x = bounds.x + bounds.x_off(line.width, anchor) - line.first_off;
|
||||||
|
|
||||||
|
StringIterator s = text[line.start:line.end-line.start].iterator();
|
||||||
|
for (Codepoint cp; s.has_next();) {
|
||||||
|
cp = s.next()!;
|
||||||
|
Glyph* gp = font.get_glyph(cp)!;
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cp == '\n':
|
||||||
|
break;
|
||||||
|
case cp == '\t':
|
||||||
|
o.x += tab_width;
|
||||||
|
case ascii::is_cntrl((char)cp):
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Rect b = {
|
||||||
|
.x = o.x + gp.ox,
|
||||||
|
.y = o.y + gp.oy + baseline,
|
||||||
|
.w = gp.w,
|
||||||
|
.h = gp.h,
|
||||||
|
};
|
||||||
|
Rect uv = {
|
||||||
|
.x = gp.u,
|
||||||
|
.y = gp.v,
|
||||||
|
.w = gp.w,
|
||||||
|
.h = gp.h
|
||||||
|
};
|
||||||
|
|
||||||
GlyphIterator gi;
|
|
||||||
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
|
|
||||||
while (gi.has_next()) {
|
|
||||||
Rect b = gi.next()!;
|
|
||||||
Rect uv = gi.gp.uv();
|
|
||||||
ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
|
ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
|
||||||
|
o.x += gp.adv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.y += line.height + line_gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.reset_scissor(z_index)!;
|
ctx.reset_scissor(z_index)!;
|
||||||
// ctx.dbg_rect(str_bounds.off(bounds.position()));
|
// ctx.dbg_rect(str_bounds.off(bounds.position()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// layout a string inside a bounding box, with TOP_LEFT alignment.
|
||||||
|
<*
|
||||||
|
@param[&in] ctx
|
||||||
|
@param[in] text
|
||||||
|
*>
|
||||||
|
fn void? Ctx.layout_string_topleft(&ctx, String text, Rect bounds, int z_index, Color hue, bool reflow = false)
|
||||||
|
{
|
||||||
|
if (text == "") return;
|
||||||
|
if (bounds.w <= 0 || bounds.h <= 0) return;
|
||||||
|
ctx.push_scissor(bounds, z_index)!;
|
||||||
|
|
||||||
|
Font* font = &ctx.font;
|
||||||
|
Id texture_id = font.id;
|
||||||
|
short line_height = (short)font.line_height();
|
||||||
|
short baseline = (short)font.ascender;
|
||||||
|
short line_gap = (short)font.linegap;
|
||||||
|
short space_width = font.get_glyph(' ').adv!;
|
||||||
|
short tab_width = space_width * TAB_SIZE;
|
||||||
|
|
||||||
|
Point o = bounds.position();
|
||||||
|
StringIterator it = text.iterator();
|
||||||
|
for (Codepoint cp; it.has_next();) {
|
||||||
|
cp = it.next()!;
|
||||||
|
Glyph* gp = font.get_glyph(cp)!;
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cp == '\n':
|
||||||
|
o.y += line_height;
|
||||||
|
o.x = bounds.x;
|
||||||
|
break;
|
||||||
|
case cp == '\t':
|
||||||
|
o.x += tab_width;
|
||||||
|
case ascii::is_cntrl((char)cp):
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Rect b = {
|
||||||
|
.x = o.x + gp.ox,
|
||||||
|
.y = o.y + gp.oy + baseline,
|
||||||
|
.w = gp.w,
|
||||||
|
.h = gp.h,
|
||||||
|
};
|
||||||
|
Rect uv = {
|
||||||
|
.x = gp.u,
|
||||||
|
.y = gp.v,
|
||||||
|
.w = gp.w,
|
||||||
|
.h = gp.h
|
||||||
|
};
|
||||||
|
|
||||||
|
if (reflow && b.x + b.w > bounds.x + bounds.w) {
|
||||||
|
o.y += line_height + line_gap;
|
||||||
|
o.x = bounds.x;
|
||||||
|
b.x = o.x + gp.ox;
|
||||||
|
b.y = o.y + gp.oy + baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
|
||||||
|
o.x += gp.adv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.reset_scissor(z_index)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------------------- //
|
||||||
// CURSOR AND MOUSE //
|
// CURSOR AND MOUSE //
|
||||||
@ -329,168 +283,71 @@ fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_
|
|||||||
|
|
||||||
fn Rect? Ctx.get_cursor_position(&ctx, String text, Rect bounds, Anchor anchor, usz cursor, bool reflow = false)
|
fn Rect? Ctx.get_cursor_position(&ctx, String text, Rect bounds, Anchor anchor, usz cursor, bool reflow = false)
|
||||||
{
|
{
|
||||||
|
if (anchor != TOP_LEFT) {
|
||||||
|
unreachable("TODO: anchor has to be TOP_LEFT");
|
||||||
|
}
|
||||||
|
|
||||||
if (bounds.w <= 0 || bounds.h <= 0) return {};
|
if (bounds.w <= 0 || bounds.h <= 0) return {};
|
||||||
|
if (text == "") text = "\f";
|
||||||
|
|
||||||
Font* font = &ctx.font;
|
Font* font = &ctx.font;
|
||||||
Id texture_id = font.id;
|
Id texture_id = font.id;
|
||||||
|
short line_height = (short)font.line_height();
|
||||||
|
short baseline = (short)font.ascender;
|
||||||
|
short line_gap = (short)font.linegap;
|
||||||
|
short space_width = font.get_glyph(' ').adv!;
|
||||||
|
short tab_width = space_width * TAB_SIZE;
|
||||||
|
|
||||||
if (text == "") text = "\f";
|
|
||||||
|
|
||||||
GlyphIterator gi;
|
|
||||||
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
|
|
||||||
Rect cursor_rect;
|
Rect cursor_rect;
|
||||||
cursor_rect.x = gi.o.x;
|
cursor_rect.x = bounds.x;
|
||||||
cursor_rect.y = gi.o.y;
|
cursor_rect.y = bounds.y;
|
||||||
cursor_rect.h = (short)font.line_height();
|
cursor_rect.h = line_height;
|
||||||
|
|
||||||
if (cursor == 0) return cursor_rect;
|
if (cursor == 0) return cursor_rect;
|
||||||
|
|
||||||
while (gi.has_next()) {
|
Point o = bounds.position();
|
||||||
Rect b = gi.next()!;
|
StringIterator it = text.iterator();
|
||||||
if (gi.current_offset() == cursor) {
|
usz off;
|
||||||
if (gi.cp == '\n') {
|
for (Codepoint cp; it.has_next();) {
|
||||||
if (!gi.has_next()) {
|
cp = it.next()!;
|
||||||
cursor_rect.x = bounds.x + bounds.x_off(0, anchor);
|
off = it.current;
|
||||||
cursor_rect.y = b.y + gi.line_height + gi.line_gap;
|
Glyph* gp = font.get_glyph(cp)!;
|
||||||
} else {
|
|
||||||
gi.next()!;
|
switch {
|
||||||
cursor_rect.x = gi.o.x - gi.gp.adv;
|
case cp == '\n':
|
||||||
cursor_rect.y = gi.o.y;
|
o.y += line_height;
|
||||||
|
o.x = bounds.x;
|
||||||
|
break;
|
||||||
|
case cp == '\t':
|
||||||
|
o.x += tab_width;
|
||||||
|
case ascii::is_cntrl((char)cp):
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Rect b = {
|
||||||
|
.x = o.x + gp.ox,
|
||||||
|
.y = o.y + gp.oy + baseline,
|
||||||
|
.w = gp.w,
|
||||||
|
.h = gp.h,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (reflow && b.x + b.w > bounds.x + bounds.w) {
|
||||||
|
o.y += line_height + line_gap;
|
||||||
|
o.x = bounds.x;
|
||||||
|
b.x = o.x + gp.ox;
|
||||||
|
b.y = o.y + gp.oy + baseline;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Use the updated origin position instead of glyph bounds
|
o.x += gp.adv;
|
||||||
cursor_rect.x = gi.o.x;
|
|
||||||
cursor_rect.y = gi.o.y;
|
|
||||||
}
|
}
|
||||||
|
if (off == cursor) {
|
||||||
|
cursor_rect.x = o.x;
|
||||||
|
cursor_rect.y = o.y;
|
||||||
return cursor_rect;
|
return cursor_rect;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
<*
|
|
||||||
@param [&in] ctx
|
|
||||||
@param [in] text
|
|
||||||
*>
|
|
||||||
fn usz? Ctx.hit_test_string(&ctx, String text, Rect bounds, Anchor anchor, Point p, bool reflow = false)
|
|
||||||
{
|
|
||||||
if (text == "") return 0;
|
|
||||||
if (bounds.w <= 0 || bounds.h <= 0) return 0;
|
|
||||||
|
|
||||||
Font* font = &ctx.font;
|
|
||||||
|
|
||||||
GlyphIterator gi;
|
|
||||||
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
|
|
||||||
|
|
||||||
usz prev_offset = 0;
|
|
||||||
Point prev_o = gi.o;
|
|
||||||
|
|
||||||
while (gi.has_next()) {
|
|
||||||
Point o_before = gi.o;
|
|
||||||
usz offset_before = gi.current_offset();
|
|
||||||
Rect b = gi.next()!;
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case gi.cp == '\n':
|
|
||||||
// Check if point is on this line before the newline
|
|
||||||
Rect line_rect = {
|
|
||||||
.x = prev_o.x,
|
|
||||||
.y = prev_o.y,
|
|
||||||
.w = (short)(o_before.x - prev_o.x),
|
|
||||||
.h = gi.line_height
|
|
||||||
};
|
|
||||||
if (p.in_rect(line_rect)) return offset_before;
|
|
||||||
prev_o = gi.o;
|
|
||||||
break;
|
|
||||||
case gi.cp == '\t':
|
|
||||||
// Check if point is in the tab space
|
|
||||||
Rect tab_rect = {
|
|
||||||
.x = o_before.x,
|
|
||||||
.y = o_before.y,
|
|
||||||
.w = gi.tab_width,
|
|
||||||
.h = gi.line_height
|
|
||||||
};
|
|
||||||
if (p.in_rect(tab_rect)) return offset_before;
|
|
||||||
break;
|
|
||||||
case ascii::is_cntrl((char)gi.cp):
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Create a hit test rect for this character
|
|
||||||
Rect hit_rect = {
|
|
||||||
.x = o_before.x,
|
|
||||||
.y = o_before.y,
|
|
||||||
.w = gi.gp.adv,
|
|
||||||
.h = gi.line_height
|
|
||||||
};
|
|
||||||
|
|
||||||
if (p.in_rect(hit_rect)) {
|
|
||||||
// Check if cursor should be before or after this character
|
|
||||||
// by checking which half of the character was clicked
|
|
||||||
short mid_x = o_before.x + gi.gp.adv / 2;
|
|
||||||
if (p.x < mid_x) {
|
|
||||||
return offset_before;
|
|
||||||
} else {
|
|
||||||
return gi.current_offset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prev_offset = gi.current_offset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Point is after all text
|
|
||||||
return text.len;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: implement a function `layout_string_with_selection` to avoid iterating over the string twice
|
|
||||||
fn void? Ctx.draw_string_selection(&ctx, String text, Rect bounds, Anchor anchor, usz start, usz end, int z_index, Color hue, bool reflow = false)
|
|
||||||
{
|
|
||||||
if (text == "") return;
|
|
||||||
if (bounds.w <= 0 || bounds.h <= 0) return;
|
|
||||||
|
|
||||||
if (start > end) @swap(start, end);
|
|
||||||
|
|
||||||
// Ensure start < end
|
|
||||||
if (start > end) {
|
|
||||||
usz temp = start;
|
|
||||||
start = end;
|
|
||||||
end = temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.push_scissor(bounds, z_index)!;
|
|
||||||
|
|
||||||
Font* font = &ctx.font;
|
|
||||||
|
|
||||||
GlyphIterator gi;
|
|
||||||
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
|
|
||||||
|
|
||||||
Rect sel_rect = { .h = gi.line_height }; // selection rect
|
|
||||||
isz sel_line = -1; // selection line
|
|
||||||
while (gi.has_next()) {
|
|
||||||
Rect b = gi.next()!;
|
|
||||||
usz off = gi.current_offset()-1;
|
|
||||||
isz line = gi.line_idx;
|
|
||||||
bool in_selection = start <= off && off <= end;
|
|
||||||
|
|
||||||
if (in_selection && line != sel_line) {
|
|
||||||
if (sel_line != -1) {
|
|
||||||
ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!;
|
|
||||||
}
|
|
||||||
sel_rect = {.x = gi.o.x - b.w, .y = gi.o.y, .w = 0, .h = gi.line_height};
|
|
||||||
sel_line = line;
|
|
||||||
}
|
|
||||||
if (in_selection) {
|
|
||||||
sel_rect.w = gi.o.x - sel_rect.x;
|
|
||||||
if (gi.cp == '\n') sel_rect.w += gi.space_width;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (off > end) break;
|
|
||||||
}
|
|
||||||
ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!;
|
|
||||||
|
|
||||||
ctx.reset_scissor(z_index)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------------------- //
|
||||||
// TEXT MEASUREMENT //
|
// TEXT MEASUREMENT //
|
||||||
@ -549,8 +406,12 @@ fn TextSize? Ctx.measure_string(&ctx, String text)
|
|||||||
case ascii::is_cntrl((char)cp):
|
case ascii::is_cntrl((char)cp):
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Rect b = gp.bounds().off(origin);
|
Rect b = {
|
||||||
b.y += baseline;
|
.x = origin.x + gp.ox,
|
||||||
|
.y = origin.y + gp.oy + baseline,
|
||||||
|
.w = gp.w,
|
||||||
|
.h = gp.h,
|
||||||
|
};
|
||||||
bounds = containing_rect(bounds, b);
|
bounds = containing_rect(bounds, b);
|
||||||
origin.x += gp.adv;
|
origin.x += gp.adv;
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,6 @@ struct TextEdit {
|
|||||||
char[] buffer;
|
char[] buffer;
|
||||||
usz chars;
|
usz chars;
|
||||||
usz cursor;
|
usz cursor;
|
||||||
usz sel_start, sel_end;
|
|
||||||
bool selection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn String TextEdit.to_string(&te) => (String)te.buffer[:te.chars];
|
fn String TextEdit.to_string(&te) => (String)te.buffer[:te.chars];
|
||||||
@ -40,15 +38,11 @@ fn bool Ctx.text_edit(&ctx, TextEdit* te)
|
|||||||
|
|
||||||
// handle modkeys
|
// handle modkeys
|
||||||
if (te.chars) {
|
if (te.chars) {
|
||||||
// if not already in selection update the selection start/end to the cursor positon
|
|
||||||
if (!te.selection) {
|
|
||||||
te.sel_start = te.sel_end = te.cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle backspace and delete
|
// handle backspace and delete
|
||||||
if (mod.bkspc) {
|
if (mod.bkspc) {
|
||||||
if (te.cursor > 0) {
|
if (te.cursor > 0) {
|
||||||
usz how_many = te.how_many_bw(mod);
|
// TODO: only delete until punctuation
|
||||||
|
usz how_many = mod & KMOD_CTRL ? te.until_cursor().prev_word_off() : te.until_cursor().prev_char_off();
|
||||||
te.buffer[te.cursor-how_many : after] = te.buffer[te.cursor : after];
|
te.buffer[te.cursor-how_many : after] = te.buffer[te.cursor : after];
|
||||||
te.cursor -= how_many;
|
te.cursor -= how_many;
|
||||||
te.chars -= how_many;
|
te.chars -= how_many;
|
||||||
@ -57,7 +51,7 @@ fn bool Ctx.text_edit(&ctx, TextEdit* te)
|
|||||||
}
|
}
|
||||||
if (mod.del) {
|
if (mod.del) {
|
||||||
if (after > 0 && te.cursor < te.chars) {
|
if (after > 0 && te.cursor < te.chars) {
|
||||||
usz how_many = te.how_many_fw(mod);
|
usz how_many = mod & KMOD_CTRL ? te.from_cursor().next_word_off() : te.from_cursor().next_char_off();
|
||||||
te.buffer[te.cursor : after] = te.buffer[te.cursor+how_many : after];
|
te.buffer[te.cursor : after] = te.buffer[te.cursor+how_many : after];
|
||||||
te.chars -= how_many;
|
te.chars -= how_many;
|
||||||
after -= how_many;
|
after -= how_many;
|
||||||
@ -68,31 +62,16 @@ fn bool Ctx.text_edit(&ctx, TextEdit* te)
|
|||||||
// handle arrow keys
|
// handle arrow keys
|
||||||
if (mod.left) {
|
if (mod.left) {
|
||||||
if (te.cursor > 0) {
|
if (te.cursor > 0) {
|
||||||
usz how_many = te.how_many_bw(mod);
|
usz how_many = mod & KMOD_CTRL ? te.until_cursor().prev_word_off() : te.until_cursor().prev_char_off();
|
||||||
te.cursor -= how_many;
|
te.cursor -= how_many;
|
||||||
after += how_many;
|
after += how_many;
|
||||||
|
|
||||||
if (mod & KMOD_SHIFT) {
|
|
||||||
te.selection = true;
|
|
||||||
if (!te.selection) te.sel_start = te.cursor;
|
|
||||||
te.sel_end = te.cursor;
|
|
||||||
} else {
|
|
||||||
te.selection = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mod.right) {
|
if (mod.right) {
|
||||||
if (after > 0) {
|
if (after > 0) {
|
||||||
usz how_many = te.how_many_fw(mod);
|
usz how_many = mod & KMOD_CTRL ? te.from_cursor().next_word_off() : te.from_cursor().next_char_off();
|
||||||
te.cursor += how_many;
|
te.cursor += how_many;
|
||||||
after -= how_many;
|
after -= how_many;
|
||||||
|
|
||||||
if (mod & KMOD_SHIFT) {
|
|
||||||
te.sel_end = te.cursor-1;
|
|
||||||
te.selection = true;
|
|
||||||
} else {
|
|
||||||
te.selection = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// FIXME: this still doesn't work
|
// FIXME: this still doesn't work
|
||||||
@ -124,36 +103,10 @@ fn bool Ctx.text_edit(&ctx, TextEdit* te)
|
|||||||
// TODO: mod.end
|
// TODO: mod.end
|
||||||
// TODO: selection with shift+arrows
|
// TODO: selection with shift+arrows
|
||||||
}
|
}
|
||||||
println("cursor: ", te.cursor, " start: ", te.sel_start, " end: ", te.sel_end);
|
|
||||||
|
|
||||||
return free == 0;
|
return free == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usz TextEdit.how_many_fw(&te, ModKeys mod)
|
|
||||||
{
|
|
||||||
if (mod & KMOD_CTRL) {
|
|
||||||
return te.from_cursor().next_word_off();
|
|
||||||
} else if (te.selection && !(mod & KMOD_SHIFT)) {
|
|
||||||
// FIXME: +1 here is not correct, should use next_char_off()
|
|
||||||
return te.sel_start > te.sel_end ? te.sel_end - te.sel_start + 1 : te.from_cursor().next_char_off();
|
|
||||||
} else {
|
|
||||||
return te.from_cursor().next_char_off();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// how many characters to jump backwards (left arrow)
|
|
||||||
fn usz TextEdit.how_many_bw(&te, ModKeys mod)
|
|
||||||
{
|
|
||||||
if (mod & KMOD_CTRL) {
|
|
||||||
return te.until_cursor().prev_word_off();
|
|
||||||
} else if (te.selection && !(mod & KMOD_SHIFT)){
|
|
||||||
// FIXME: +1 here is not correct, should use prev_char_off()
|
|
||||||
return te.sel_start < te.sel_end ? te.sel_end - te.sel_start + 1 : te.until_cursor().prev_char_off();
|
|
||||||
} else {
|
|
||||||
return te.until_cursor().prev_char_off();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro isz char[].next_char_off(b) => grapheme::next_character_break_utf8(b.ptr, b.len);
|
macro isz char[].next_char_off(b) => grapheme::next_character_break_utf8(b.ptr, b.len);
|
||||||
macro isz char[].next_word_off(b) => grapheme::next_word_break_utf8(b.ptr, b.len);
|
macro isz char[].next_word_off(b) => grapheme::next_word_break_utf8(b.ptr, b.len);
|
||||||
|
|
||||||
|
@ -71,23 +71,16 @@ fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Size w, Size h, TextEdit* te, Anchor
|
|||||||
ctx.text_edit(elem.text.te);
|
ctx.text_edit(elem.text.te);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// draw the box
|
||||||
|
|
||||||
Rect bg_bounds = elem.bounds.pad(style.margin);
|
Rect bg_bounds = elem.bounds.pad(style.margin);
|
||||||
Rect text_bounds = elem.bounds.pad(elem.layout.content_offset);
|
Rect text_bounds = elem.bounds.pad(elem.layout.content_offset);
|
||||||
ctx.push_rect(bg_bounds, parent.z_index, style)!;
|
ctx.push_rect(bg_bounds, parent.z_index, style)!;
|
||||||
String s = elem.text.te.to_string();
|
ctx.layout_string(elem.text.te.to_string(), text_bounds, text_alignment, parent.z_index, style.fg, reflow)!;
|
||||||
if (elem.text.te.selection) {
|
|
||||||
usz start = elem.text.te.sel_start;
|
|
||||||
usz end = elem.text.te.sel_end;
|
|
||||||
ctx.draw_string_selection(s, text_bounds, text_alignment, start, end, parent.z_index, style.accent, reflow)!;
|
|
||||||
}
|
|
||||||
ctx.layout_string(s, text_bounds, text_alignment, parent.z_index, style.fg, reflow)!;
|
|
||||||
|
|
||||||
// draw the cursor if the element has focus
|
// draw the cursor if the element has focus
|
||||||
if (elem.events.has_focus) {
|
if (elem.events.has_focus) {
|
||||||
if (elem.events.mouse_press) {
|
Rect cur = ctx.get_cursor_position(elem.text.te.to_string(), text_bounds, text_alignment, elem.text.te.cursor, reflow)!;
|
||||||
elem.text.te.cursor = ctx.hit_test_string(s, text_bounds, text_alignment, ctx.input.mouse.pos, reflow)!;
|
|
||||||
}
|
|
||||||
Rect cur = ctx.get_cursor_position(s, text_bounds, text_alignment, elem.text.te.cursor, reflow)!;
|
|
||||||
cur.w = 2;
|
cur.w = 2;
|
||||||
ctx.push_rect(cur, parent.z_index, &&(Style){.bg = style.fg})!;
|
ctx.push_rect(cur, parent.z_index, &&(Style){.bg = style.fg})!;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user