re-implemented text box

also includes
	- small layout fix for grow elements
	- ElemEvents now includes has_focus flag
This commit is contained in:
Alessandro Mauri 2025-09-13 19:47:58 +02:00
parent 81cc3dae65
commit 34b92c93b4
8 changed files with 237 additions and 45 deletions

View File

@ -38,13 +38,14 @@ bitstruct ElemFlags : uint {
bitstruct ElemEvents : uint {
bool key_press : 0;
bool key_release : 1;
bool key_hold : 2;
bool key_repeat : 2;
bool mouse_hover : 3;
bool mouse_press : 4;
bool mouse_release : 5;
bool mouse_hold : 6;
bool update : 7;
bool text_input : 8;
bool has_focus : 9;
}
// element structure
@ -334,7 +335,7 @@ macro bool Ctx.elem_focus(&ctx, Elem *elem)
fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
{
bool hover = ctx.is_hovered(elem);
bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_LEFT));
bool focus = ctx.elem_focus(elem) || (hover && ctx.is_mouse_pressed(BTN_LEFT));
if (ctx.is_mouse_pressed(BTN_ANY) && !hover){
focus = false;
@ -345,11 +346,15 @@ fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
if (focus) { ctx.focus_id = elem.id; }
ElemEvents ev = {
.mouse_hover = hover,
.mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY),
.has_focus = focus,
.mouse_hover = hover,
.mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY),
.mouse_release = hover && focus && ctx.is_mouse_released(BTN_ANY),
.mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY),
.text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
.mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY),
.key_press = focus && ctx.input.events.key_press,
.key_release = focus && ctx.input.events.key_release,
.key_repeat = focus && ctx.input.events.key_repeat,
.text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
};
return ev;
}

View File

@ -300,15 +300,20 @@ fn TextSize? Ctx.measure_string(&ctx, String text)
return ts;
}
fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue)
// layout a string inside a bounding box, following the given alignment (anchor).
// returns the position of the cursor, the returned height is the line height and the width is the last
// character's advance value
fn Rect? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, isz cursor = -1)
{
if (text.len == 0 || bounds.w <= 0 || bounds.h <= 0) return;
Font* font = &ctx.font;
short line_height = (short)font.line_height();
Rect cursor_rect = {.h = line_height};
if (text == "" || bounds.w <= 0 || bounds.h <= 0) return cursor_rect;
ctx.push_scissor(bounds, z_index)!;
Font* font = &ctx.font;
Id texture_id = font.id;
short baseline = (short)font.ascender;
short line_height = (short)font.line_height();
short line_gap = (short)font.linegap;
short space_width = font.get_glyph(' ').adv!;
short tab_width = space_width * TAB_SIZE;
@ -375,6 +380,7 @@ fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_
if (line_end == line_start) break;
// with the line width calculate the right origin and layout the line
origin.x = bounds.x;
switch (anchor) {
case TOP_LEFT: nextcase;
case LEFT: nextcase;
@ -391,6 +397,9 @@ fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_
origin.x += (short)(bounds.w - line_width);
}
cursor_rect.x = origin.x;
cursor_rect.y = origin.y;
// see the fixme when measuring the height
//ctx.push_rect({.x = origin.x,.y=origin.y,.w=(short)line_width,.h=(short)string_height}, z_index, &&(Style){.bg=0xff000042u.@to_rgba()})!;
String line = text[line_start:line_end-line_start];
@ -419,9 +428,15 @@ fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_
};
ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
//ctx.push_rect(b, z_index, &&(Style){.bg=0x0000ff66u.@to_rgba()})!;
if (line_start + off < cursor) {
cursor_rect.x = origin.x + gp.adv;
cursor_rect.y = origin.y;
cursor_rect.w = gp.adv;
}
origin.x += gp.adv;
}
}
// done with the line
line_start = line_end;
@ -429,4 +444,6 @@ fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_
} while(line_end < text.len);
ctx.reset_scissor(z_index)!;
return cursor_rect;
}

View File

@ -13,6 +13,9 @@ bitstruct InputEvents : uint {
bool mouse_scroll : 4; // mouse scroll wheel. x or y
bool text_input : 5;
bool mod_key : 6;
bool key_press : 7;
bool key_release : 8;
bool key_repeat : 9;
}
bitstruct MouseButtons : uint {
@ -156,6 +159,21 @@ fn void Ctx.input_mouse_wheel(&ctx, short x, short y, float scale = 1.0)
ctx.input.events.mouse_scroll = x !=0 || y != 0;
}
fn void Ctx.input_key_press(&ctx)
{
ctx.input.events.key_press = true;
}
fn void Ctx.input_key_release(&ctx)
{
ctx.input.events.key_release = true;
}
fn void Ctx.input_key_repeat(&ctx)
{
ctx.input.events.key_repeat = true;
}
// append utf-8 encoded text to the context text input
fn void Ctx.input_text_utf8(&ctx, char[] text)
{
@ -196,6 +214,7 @@ fn void Ctx.input_char(&ctx, char c)
}
fn String Ctx.get_keys(&ctx) => (String)ctx.input.keyboard.text[:ctx.input.keyboard.text_len];
fn ModKeys Ctx.get_mod(&ctx) => ctx.input.keyboard.modkeys;
// Modifier keys, like control or backspace
// TODO: make this call repetible to input modkeys one by one

View File

@ -88,6 +88,12 @@ macro Point Layout.get_dimensions(&el)
// GROSS HACK FOR EXACT DIMENSIONS
if (el.w.@is_exact()) dim.x = el.w.min + el.content_offset.x + el.content_offset.w;
if (el.h.@is_exact()) dim.y = el.h.min + el.content_offset.y + el.content_offset.h;
// GROSS HACK FOR GROW DIMENSIONS
// FIXME: does this always work?
if (el.w.@is_grow()) dim.x = 0;
if (el.h.@is_grow()) dim.y = 0;
return dim;
}

View File

@ -0,0 +1,108 @@
module ugui;
import grapheme;
import std::ascii;
struct TextEdit {
char[] buffer;
usz chars;
usz cursor;
}
fn String TextEdit.to_string(&te) => (String)te.buffer[:te.chars];
fn String TextEdit.until_cursor(&te) => (String)te.buffer[:te.cursor];
fn String TextEdit.from_cursor(&te) => (String)te.buffer[te.cursor..];
// implement text editing operations on the buffer
// returns true if the buffer is full
fn bool Ctx.text_edit(&ctx, TextEdit* te)
{
String in = ctx.get_keys();
ModKeys mod = ctx.get_mod();
usz free = te.buffer.len - te.chars;
usz after = te.chars - te.cursor;
// append text input to the buffer
if (in.len <= free) {
// make space
te.buffer[te.cursor+in.len : after] = te.buffer[te.cursor : after];
// insert characters
te.buffer[te.cursor : in.len] = in[..];
// increment characters and cursor
te.chars += in.len;
te.cursor += in.len;
free -= in.len;
} else {
return true;
}
// handle modkeys
if (te.chars) {
// handle backspace and delete
if (mod.bkspc) {
if (te.cursor > 0) {
// 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.cursor -= how_many;
te.chars -= how_many;
free += how_many;
}
}
if (mod.del) {
if (after > 0 && te.cursor < te.chars) {
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.chars -= how_many;
after -= how_many;
free += how_many;
}
}
// handle arrow keys
if (mod.left) {
if (te.cursor > 0) {
usz how_many = mod & KMOD_CTRL ? te.until_cursor().prev_word_off() : te.until_cursor().prev_char_off();
te.cursor -= how_many;
after += how_many;
}
}
if (mod.right) {
if (after > 0) {
usz how_many = mod & KMOD_CTRL ? te.from_cursor().next_word_off() : te.from_cursor().next_char_off();
te.cursor += how_many;
after -= how_many;
}
}
if (mod.up) {
// TODO
}
if (mod.down) {
// TODO
}
}
return free == 0;
}
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);
fn isz char[].prev_char_off(b)
{
foreach_r (off, c: b) {
if (c & 0xC0 == 0x80) continue;
return b.len - off;
}
return b.len;
}
fn isz char[].prev_word_off(b)
{
for (isz off = b.len-1; off > 0;) {
isz c_off = b[..off].prev_char_off();
off -= c_off;
if (ascii::is_punct(b[off]) || ascii::is_space(b[off])) return b.len - off - 1;
}
return b.len;
}

View File

@ -3,9 +3,9 @@ module ugui;
import std::io;
struct ElemText {
usz cursor; // cursor offset
Id hash;
TextSize size;
TextEdit* te;
}
macro Ctx.text(&ctx, String text, ...)
@ -35,10 +35,10 @@ fn void? Ctx.text_id(&ctx, Id id, String text)
ctx.layout_string(text, elem.bounds.pad(elem.layout.content_offset), TOP_LEFT, parent.div.z_index, style.fg)!;
}
/*
macro Ctx.text_box(&ctx, Rect size, char[] text, usz* text_len, ...)
=> ctx.text_box_id(@compute_id($vasplat), size, text, text_len);
fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_len)
macro Ctx.text_box(&ctx, Size w, Size h, TextEdit* te, ...)
=> ctx.text_box_id(@compute_id($vasplat), w, h, te);
fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Size w, Size h, TextEdit* te)
{
id = ctx.gen_id(id)!;
@ -46,38 +46,45 @@ fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_le
Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
Style* style = ctx.styles.get_style(@str_hash("text-box"));
elem.text.str = (String)text;
elem.text.te = te;
// layout the text box
elem.bounds = ctx.layout_element(parent, size, style);
Id text_hash = te.to_string().hash();
if (elem.flags.is_new || elem.text.hash != text_hash) {
elem.text.size = ctx.measure_string(te.to_string())!;
}
elem.text.hash = text_hash;
elem.layout.w = w;
elem.layout.h = h;
elem.layout.text = elem.text.size;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_grow(elem, parent);
update_parent_size(elem, parent);
// check input and update the text
elem.events = ctx.get_elem_events(elem);
if (elem.events.text_input) {
usz l = ctx.input.keyboard.text_len;
char[] t = ctx.input.keyboard.text[..l];
if (l != 0 && l < text.len - *text_len) {
text[*text_len..*text_len+l] = t[..];
*text_len += l;
}
if (ctx.input.keyboard.modkeys.bkspc) {
*text_len = *text_len > 0 ? *text_len-1 : 0;
}
if (elem.events.text_input || elem.events.key_press) {
ctx.text_edit(elem.text.te);
}
elem.text.cursor = *text_len;
// draw the box
short line_height = (short)ctx.font.line_height();
Rect text_box = elem.bounds;
ctx.push_rect(text_box, parent.div.z_index, style)!;
ctx.push_string(text_box, text[:*text_len], parent.div.z_index, style.fg, true)!;
// TODO: draw cursor
Rect bg_bounds = elem.bounds.pad(style.margin);
Rect text_bounds = elem.bounds.pad(elem.layout.content_offset);
ctx.push_rect(bg_bounds, parent.div.z_index, style)!;
Rect cur;
cur = ctx.layout_string(elem.text.te.to_string(), text_bounds, TOP_LEFT, parent.div.z_index, style.fg, elem.text.te.cursor)!;
// draw the cursor if the element has focus
if (elem.events.has_focus) {
cur.w = 2;
// FIXME: gross hack for when text is empty
if (cur.x == 0 && cur.y == 0) {
cur.x = text_bounds.x;
cur.y = text_bounds.y;
}
ctx.push_rect(cur, parent.div.z_index, &&(Style){.bg = style.fg})!;
}
return elem.events;
}
*/

View File

@ -61,3 +61,14 @@ slider {
secondary: #458588ff;
accent: #fabd2fff;
}
text-box {
bg: #4a4543ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
border: 1;
padding: 4;
margin: 2;
}

View File

@ -120,6 +120,13 @@ fn int main(String[] args)
// ========================================================================================== //
io::printfn("imported %d styles", ui.import_style_from_file(STYLESHEET_PATH));
// ========================================================================================== //
// OTHER VARIABLES //
// ========================================================================================== //
TextEdit te;
te.buffer = mem::new_array(char, 256);
defer mem::free(te.buffer);
isz frame;
double fps;
time::Clock clock;
@ -128,7 +135,6 @@ fn int main(String[] args)
Times ui_times;
Times draw_times;
// ========================================================================================== //
// MAIN LOOP //
// ========================================================================================== //
@ -147,11 +153,21 @@ fn int main(String[] args)
switch (e.type) {
case EVENT_QUIT:
quit = true;
case EVENT_KEY_UP: nextcase;
case EVENT_KEY_UP:
ui.input_key_release();
nextcase;
case EVENT_KEY_DOWN:
ui.input_key_press();
if (e.key.repeat) ui.input_key_repeat();
mod.rctrl = e.key.key == K_RCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.rctrl;
mod.lctrl = e.key.key == K_LCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.lctrl;
mod.bkspc = e.key.key == K_BACKSPACE ? !!(e.type == EVENT_KEY_DOWN) : mod.bkspc;
mod.del = e.key.key == K_DELETE ? !!(e.type == EVENT_KEY_DOWN) : mod.del;
mod.up = e.key.key == K_UP ? !!(e.type == EVENT_KEY_DOWN) : mod.up;
mod.down = e.key.key == K_DOWN ? !!(e.type == EVENT_KEY_DOWN) : mod.down;
mod.left = e.key.key == K_LEFT ? !!(e.type == EVENT_KEY_DOWN) : mod.left;
mod.right = e.key.key == K_RIGHT ? !!(e.type == EVENT_KEY_DOWN) : mod.right;
// pressing ctrl+key or alt+key does not generate a character as such no
// TEXT_INPUT event is generated. When those keys are pressed we have to
@ -206,7 +222,7 @@ $switch APPLICATION:
$case "debug":
debug_app(&ui);
$case "calculator":
calculator(&ui);
calculator(&ui, &te);
$endswitch
// Timings counter
@ -326,7 +342,7 @@ fn void debug_app(ugui::Ctx* ui)
import std::os::process;
fn void calculator(ugui::Ctx* ui)
fn void calculator(ugui::Ctx* ui, TextEdit* te)
{
static char[128] buffer;
static usz len;
@ -421,6 +437,9 @@ fn void calculator(ugui::Ctx* ui)
ui.slider_hor(ugui::@exact(100), ugui::@exact(20), &f)!!;
ui.slider_ver(ugui::@exact(20), ugui::@exact(100), &f)!!;
}!!;
ui.@div(ugui::@grow(), ugui::@fit(), anchor: CENTER, scroll_y: true) {
ui.text_box(ugui::@grow(), ugui::@exact(100), te)!!;
}!!;
}!!; }!!;
}