text input wip

This commit is contained in:
jacob 2026-03-28 21:46:23 -05:00
parent 97711b0970
commit c017e93695
6 changed files with 594 additions and 23 deletions

View File

@ -114,23 +114,11 @@
// TODO: Eventually hard-code to something like 128 if we ever decide to support Apple M-series
#define IsolationSize 64
//- Windows NTDDI version
// TODO: Remove this
#if 0
#if IsCompilerMsvc
#define NTDDI_WIN11_DT 0x0C0A0000
#define NTDDI_VERSION 0x0A000000
#if IsRtcEnabled
#define _ALLOW_RTCc_IN_STL 1
#endif
#endif
#endif
////////////////////////////////////////////////////////////
//~ C headers
#if IsCpu
// C standard library
//- C standard library
#include <stdint.h>
#include <stdarg.h>
#include <memory.h>
@ -150,7 +138,7 @@
// -wmmintrin.h AES
// -immintrin.h AVX, AVX2, FMA
#include <intrin.h>
#include <nmmintrin.h> // SSE4.2
#include <nmmintrin.h>
#endif
#endif

View File

@ -139,7 +139,8 @@ Struct(ControllerEvent)
b32 is_repeat;
// ControllerEventKind_Text
u32 text_codepoint;
u32 text_chars_count;
u8 text_chars[4];
// ControllerEventKind_CursorMove
Vec2 cursor_pos;

View File

@ -258,6 +258,213 @@ void V_DrawPoint(Vec2 p, Vec4 srgb)
V_DrawShape(ui_shape, AffineIdentity, srgb, 24, V_DrawFlag_None);
}
////////////////////////////////////////////////////////////
//~ Text box
void V_ApplyTextBoxDelta(V_TextBoxState *tb, V_TextBoxDelta delta)
{
V_TextBoxDeltaNode tbd_node = { .delta = delta };
V_ApplyTextBoxDeltas(tb, (V_TextBoxDeltaList) { .first = &tbd_node, .last = &tbd_node });
}
void V_ApplyTextBoxDeltas(V_TextBoxState *tb, V_TextBoxDeltaList deltas)
{
for (V_TextBoxDeltaNode *tbd_node = deltas.first; tbd_node; tbd_node = tbd_node->next)
{
V_TextBoxDelta delta = tbd_node->delta;
// if (!(delta.flags & V_TextBoxDeltaFlag_DontUpdateCursor))
// {
// tb->start = ClampI64(delta.start, 0, tb->len);
// tb->end = ClampI64(delta.end, 0, tb->len);
// }
// Navigate
{
{
if (delta.flags & V_TextBoxDeltaFlag_NavDirect)
{
tb->start = delta.direct_start;
tb->end = delta.direct_end;
}
if (delta.flags & V_TextBoxDeltaFlag_NavLeft)
{
if (delta.flags & V_TextBoxDeltaFlag_NavLine)
{
// FIXME: Real line start
tb->end = 0;
}
else if (delta.flags & V_TextBoxDeltaFlag_NavWord)
{
for (i64 pos = ClampI64(tb->end - 1, 0, tb->len); pos >= 0; --pos)
{
tb->end = pos - 1;
if (tb->text[pos] == ' ')
{
break;
}
}
}
else
{
tb->end -= 1;
}
}
if (delta.flags & V_TextBoxDeltaFlag_NavRight)
{
if (delta.flags & V_TextBoxDeltaFlag_NavLine)
{
// FIXME: Real line end
tb->end = I64Max;
}
else if (delta.flags & V_TextBoxDeltaFlag_NavWord)
{
for (i64 pos = ClampI64(tb->end + 1, 0, tb->len); pos < tb->len; ++pos)
{
tb->end = pos + 1;
if (tb->text[pos] == ' ')
{
break;
}
}
}
else
{
tb->end += 1;
}
}
}
if (!(delta.flags & V_TextBoxDeltaFlag_NavSelect))
{
tb->start = tb->end;
}
tb->start = ClampI64(tb->start, 0, tb->len);
tb->end = ClampI64(tb->end, 0, tb->len);
}
// Update text
if (delta.flags & V_TextBoxDeltaFlag_UpdateText)
{
RngI64 replace = Zi;
// Remove selected text
{
replace = RNGI64(MinI64(tb->start, tb->end), MaxI64(tb->start, tb->end));
if (replace.max > replace.min)
{
// FIXME: Truncate
i64 move_len = ClampI64(tb->len - replace.max, 0, countof(tb->text) - replace.max);
for (i64 src_pos = replace.max; src_pos < (replace.max + move_len); ++src_pos)
{
i64 dst_pos = src_pos + move_len;
tb->text[dst_pos] = tb->text[src_pos];
}
}
tb->len -= replace.max - replace.min;
}
// Insert new text
if (delta.text.len > 0)
{
i64 insert_len = ClampI64(delta.text.len, 0, countof(tb->text) - replace.min);
// Make room
// FIXME: Truncate
i64 move_len = ClampI64(tb->len - replace.min, 0, countof(tb->text) - replace.min);
for (i64 src_pos = replace.min; src_pos < (replace.min + move_len); ++src_pos)
{
i64 dst_pos = src_pos + move_len;
tb->text[dst_pos] = tb->text[src_pos];
}
// Copy from delta
for (i64 src_pos = 0; src_pos < insert_len; ++src_pos)
{
i64 dst_pos = replace.min + src_pos;
tb->text[dst_pos] = delta.text.text[src_pos];
}
tb->len += insert_len;
}
// FIXME: Expand selection on insert
// tb->start = ClampI64(tb->start, 0, tb->len);
// tb->end = ClampI64(tb->end, 0, tb->len);
tb->end = ClampI64(tb->end, 0, tb->len);
tb->start = tb->end;
}
// if (!(delta.flags & V_TextBoxDeltaFlag_DontUpdateText))
// {
// RngI64 replace = Zi;
// // Remove selected text
// {
// replace = RNGI64(MinI64(delta.start, delta.end), MaxI64(delta.start, delta.end));
// replace.min = MaxI64(replace.min, 0);
// replace.max = MinI64(replace.max, tb->len);
// if (replace.max > replace.min)
// {
// // FIXME: Truncate
// i64 move_len = ClampI64(tb->len - replace.max, 0, countof(tb->text) - replace.max);
// for (i64 src_pos = replace.max; src_pos < (replace.max + move_len); ++src_pos)
// {
// i64 dst_pos = src_pos + move_len;
// tb->text[dst_pos] = tb->text[src_pos];
// }
// }
// tb->len -= replace.max - replace.min;
// }
// // Insert new text
// if (delta.text.len > 0)
// {
// i64 insert_len = ClampI64(delta.text.len, 0, countof(tb->text) - replace.min);
// // Make room
// // FIXME: Truncate
// i64 move_len = ClampI64(tb->len - replace.min, 0, countof(tb->text) - replace.min);
// for (i64 src_pos = replace.min; src_pos < (replace.min + move_len); ++src_pos)
// {
// i64 dst_pos = src_pos + move_len;
// tb->text[dst_pos] = tb->text[src_pos];
// }
// // Copy from delta
// for (i64 src_pos = 0; src_pos < insert_len; ++src_pos)
// {
// i64 dst_pos = replace.min + src_pos;
// tb->text[dst_pos] = delta.text.text[src_pos];
// }
// tb->len += insert_len;
// }
// }
}
}
String V_StringFromTextBox(V_TextBoxState *tb)
{
return STRING(tb->len, tb->text);
}
////////////////////////////////////////////////////////////
//~ Timeline helpers
@ -802,7 +1009,9 @@ void V_TickForever(WaveLaneCtx *lane)
NET_PipeStatistics vis_pipe_stats = NET_StatsFromPipe(net_pipe);
//////////////////////////////
//- Process controller events into vis cmds
//- Process controller events
V_TextBoxDeltaList text_input_deltas = Zi;
frame->has_mouse_focus = UI_IsKeyNil(ui_frame->hot_box) || UI_MatchKey(ui_frame->hot_box, vis_box);
frame->has_keyboard_focus = 1;
@ -839,6 +1048,7 @@ void V_TickForever(WaveLaneCtx *lane)
ControllerEvent cev = window_frame.controller_events.events[cev_idx];
b32 down = cev.kind == ControllerEventKind_ButtonDown;
b32 up = cev.kind == ControllerEventKind_ButtonUp;
String text = cev.kind == ControllerEventKind_Text ? STRING(cev.text_chars_count, cev.text_chars) : STRING(0, 0);
b32 ignore = 0;
if (down)
@ -877,10 +1087,70 @@ void V_TickForever(WaveLaneCtx *lane)
break;
}
}
if (shortcut != 0 && down)
if (down && shortcut)
{
V_PushVisCmd(shortcut->cmd_name);
}
else
{
V_TextBoxDelta delta = Zi;
{
if (down && cev.button == Button_Left)
{
delta.flags |= V_TextBoxDeltaFlag_NavLeft;
}
if (down && cev.button == Button_Right)
{
delta.flags |= V_TextBoxDeltaFlag_NavRight;
}
if (down && cev.button == Button_Home)
{
delta.flags |= V_TextBoxDeltaFlag_NavLeft | V_TextBoxDeltaFlag_NavLine;
}
if (down && cev.button == Button_End)
{
delta.flags |= V_TextBoxDeltaFlag_NavRight | V_TextBoxDeltaFlag_NavLine;
}
if (down && cev.button == Button_A && frame->held_buttons[Button_Ctrl])
{
delta.flags |= V_TextBoxDeltaFlag_NavSelect | V_TextBoxDeltaFlag_NavDirect;
delta.direct_start = 0;
delta.direct_end = I64Max;
}
if (down && cev.button == Button_Backspace)
{
delta.text = Lit("");
delta.flags |= V_TextBoxDeltaFlag_NavSelect | V_TextBoxDeltaFlag_NavLeft | V_TextBoxDeltaFlag_UpdateText;
}
if (down && cev.button == Button_Delete)
{
delta.text = Lit("");
delta.flags |= V_TextBoxDeltaFlag_NavSelect | V_TextBoxDeltaFlag_NavRight | V_TextBoxDeltaFlag_UpdateText;
}
if (text.len > 0)
{
delta.flags |= V_TextBoxDeltaFlag_UpdateText;
delta.text = text;
}
if (delta.flags)
{
if (frame->held_buttons[Button_Shift])
{
delta.flags |= V_TextBoxDeltaFlag_NavSelect;
}
if (frame->held_buttons[Button_Ctrl])
{
delta.flags |= V_TextBoxDeltaFlag_NavWord;
}
}
}
if (delta.flags)
{
V_TextBoxDeltaNode *tbd_node = PushStruct(frame->arena, V_TextBoxDeltaNode);
tbd_node->delta = delta;
SllQueuePush(text_input_deltas.first, text_input_deltas.last, tbd_node);
}
}
}
frame->held_buttons[hotkey.button] = down;
}
@ -3990,6 +4260,9 @@ void V_TickForever(WaveLaneCtx *lane)
if (palette->is_showing || palette->show > 0.001)
{
f32 item_size_px = UI_FNT(1.5, 1).v;
f32 tweak_size_px = UI_FNT(1.25, 1).v;
palette->key = UI_KeyF("command palette");
UI_Checkpoint palette_cp = UI_PushCP(UI_NilKey);
{
@ -4061,6 +4334,257 @@ void V_TickForever(WaveLaneCtx *lane)
UI_PopCP(UI_TopCP());
}
//////////////////////////////
//- Build searchbox
b32 is_searching = 0;
String search_text = Zi;
{
UI_Size search_height = UI_PIX(item_size_px * 1.25, 1);
UI_SetNext(ChildAlignment, UI_Region_Left);
UI_PushCP(UI_BuildRow());
{
//- Search icon
{
f32 size_px = UI_Top(FontSize) * 1.25;
UI_SetNext(Rounding, UI_RGROW(0.75 * theme.rounding));
UI_SetNext(ChildAlignment, UI_Region_Center);
UI_SetNext(BackgroundColor, 0);
// UI_SetNext(BorderColor, reset_bd);
UI_SetNext(BorderSize, 0);
UI_SetNext(Rounding, 0);
UI_SetNext(TextColor, theme.col.hint);
UI_SetNext(Width, UI_PIX(size_px * 1.5, 1));
UI_SetNext(Height, UI_PIX(size_px * 1.5, 1));
// UI_SetNext(Flags, UI_BoxFlag_CaptureMouse);
UI_SetNext(FontSize, size_px * theme.h6);
UI_BuildIcon(theme.icon_font, UI_Icon_Search);
// UI_BuildRow();
}
//- Search box
{
UI_Key search_box = UI_KeyF("search box");
UI_BoxReport search_report = UI_ReportsFromKey(search_box).draw;
V_TextBoxState *search_state = &palette->search_state;
// if (search_report.m1.downs)
if (search_report.m1.downs || !UI_MatchKey(search_box, V.keyboard_focus_box))
{
V.keyboard_focus_box = search_box;
V.text_input_ns = frame->time_ns;
}
b32 has_focus = UI_MatchKey(search_box, V.keyboard_focus_box);
if (search_report.is_hot)
{
WND_SetCursor(window_frame, WND_CursorKind_Text);
}
if (has_focus && text_input_deltas.first)
{
V_ApplyTextBoxDeltas(search_state, text_input_deltas);
}
// FIXME: Remove this
if (frame->held_buttons[Button_B] && !prev_frame->held_buttons[Button_B])
{
V_TextBoxDelta delta = Zi;
// delta.kind = V_DeltaKind_
delta.text = Lit("Hi");
// delta.range = RNGI64(I64Max, I64Max);
// delta.start = search_state->start;
// delta.end = search_state->end;
// delta.start = 0;
// delta.end = 1;
delta.flags |= V_TextBoxDeltaFlag_UpdateText;
V_ApplyTextBoxDelta(search_state, delta);
}
// {
// V_TextBoxDelta delta = Zi;
// // FIXME: Remove this
// if (frame->held_buttons[Button_Left] && !prev_frame->held_buttons[Button_Left])
// {
// delta.flags |= V_TextBoxDeltaFlag_NavLeft;
// }
// if (frame->held_buttons[Button_Right] && !prev_frame->held_buttons[Button_Right])
// {
// delta.flags |= V_TextBoxDeltaFlag_NavRight;
// }
// if (delta.flags)
// {
// if (frame->held_buttons[Button_Shift])
// {
// delta.flags |= V_TextBoxDeltaFlag_NavSelect;
// }
// if (frame->held_buttons[Button_Ctrl])
// {
// delta.flags |= V_TextBoxDeltaFlag_NavWord;
// }
// V_ApplyTextBoxDelta(search_state, delta);
// V.text_input_ns = frame->time_ns;
// }
// }
search_text = V_StringFromTextBox(search_state);
is_searching = search_text.len != 0;
String display_text = search_text;
Vec4 display_text_color = Color_White;
if (!is_searching)
{
display_text_color = theme.col.hint;
display_text = Lit(" Search...");
}
// Vec4 raah_color = Color_Black;
Vec4 raah_color = Zi;
UI_SetNext(Height, search_height);
UI_SetNext(Width, UI_GROW(1, 0));
UI_SetNext(BackgroundColor, raah_color);
// UI_SetNext(BorderColor, item_border_color);
UI_SetNext(BorderSize, 0);
// UI_SetNext(Rounding, UI_RPIX(5));
UI_SetNext(ChildAlignment, UI_Region_Left);
UI_SetNext(TextColor, display_text_color);
UI_SetNext(Text, display_text);
UI_SetNext(Flags, UI_BoxFlag_DrawText | UI_BoxFlag_CaptureMouse);
UI_PushCP(UI_BuildRowEx(search_box));
{
f32 font_size = UI_Top(FontSize);
GC_FontKey font = UI_Top(Font);
//- Text caret/selection
if (has_focus)
{
UI_Key caret_box = UI_KeyF("search caret");
UI_Key selection_box = UI_KeyF("search selection");
UI_Size caret_width = UI_PIX(1, 1);
UI_Size caret_height = UI_PIX(search_height.v, 1);
Vec4 selection_color = VEC4(0, 0.25, 0.75, 0.5);
Vec4 caret_color = VEC4(1, 1, 1, 0.75);
// caret_color.a = AbsF32(SinF32(SecondsFromNs(frame->time_ns - V.text_input_ns) * 4));
// caret_color.a = SinF32(SecondsFromNs(frame->time_ns - V.text_input_ns)) > 0.5 ? 1 : 0;
// caret_color.a = (CosF32(SecondsFromNs(frame->time_ns - V.text_input_ns) * 5) + 1) * 0.5;
caret_color.a *= AbsF32(CosF32(SecondsFromNs(frame->time_ns - V.text_input_ns) * 3));
f32 start_offset_px = 0;
f32 end_offset_px = 0;
// caret_offset_px = TweakFloat("RAAAAAAAAAAAAAAAAAAAH", 0, 0, 100);
// caret_offset_px =
// FIXME: Remove this, use UI computed run
if (search_text.len > 0)
{
String32 codepoints = String32FromString(frame->arena, search_text);
GC_Run run = GC_RunFromString32(frame->arena, codepoints, font, font_size);
// FIXME: Don't use rect idx
// FIXME: Selection
// FIXME: Offset & scissor
for (i64 rect_idx = 0; rect_idx < (i64)run.rects_count; ++rect_idx)
{
if (rect_idx < search_state->start)
{
GC_RunRect rect = run.rects[rect_idx];
start_offset_px = rect.baseline_pos + rect.advance;
}
if (rect_idx < search_state->end)
{
GC_RunRect rect = run.rects[rect_idx];
end_offset_px = rect.baseline_pos + rect.advance;
}
if (rect_idx >= search_state->start && rect_idx >= search_state->end)
{
break;
}
}
}
// {
// i64 offset_
// }
// Selection
{
f32 min = MinF32(start_offset_px, end_offset_px);
f32 max = MaxF32(start_offset_px, end_offset_px);
UI_SetNext(Width, UI_PIX(max - min, 1));
UI_SetNext(Height, caret_height);
UI_SetNext(FloatingPos, VEC2(min, 0));
UI_SetNext(Anchor, UI_Region_Left);
UI_SetNext(Flags, UI_BoxFlag_Floating, UI_BoxFlag_CaptureMouse);
UI_SetNext(BorderSize, 0);
UI_SetNext(BackgroundColor, selection_color);
UI_SetNext(FontSize, font_size);
UI_BuildBoxEx(selection_box);
}
// Caret
{
UI_SetNext(Width, caret_width);
UI_SetNext(Height, caret_height);
UI_SetNext(FloatingPos, VEC2(end_offset_px, 0));
UI_SetNext(Anchor, UI_Region_Left);
UI_SetNext(Flags, UI_BoxFlag_Floating, UI_BoxFlag_CaptureMouse);
UI_SetNext(BorderSize, 0);
UI_SetNext(BackgroundColor, caret_color);
UI_SetNext(FontSize, font_size);
UI_BuildBoxEx(caret_box);
}
}
}
UI_PopCP(UI_TopCP());
}
}
UI_PopCP(UI_TopCP());
}
//////////////////////////////
//- Build palette items list
@ -4178,9 +4702,6 @@ void V_TickForever(WaveLaneCtx *lane)
item_border_color = LerpSrgb(item_border_color, theme.col.button_active, item_rep.hot);
}
f32 item_size_px = UI_FNT(1.5, 1).v;
f32 tweak_size_px = UI_FNT(1.25, 1).v;
UI_SetNext(Tint, 0);
UI_PushCP(UI_BuildRow());
{

View File

@ -154,16 +154,63 @@ Struct(V_Notif)
String msg;
};
////////////////////////////////////////////////////////////
//~ Text box types
#define V_MaxTextBoxLen 1024
Enum(V_TextBoxDeltaFlag)
{
V_TextBoxDeltaFlag_None = 0,
V_TextBoxDeltaFlag_UpdateText = (1 << 0),
V_TextBoxDeltaFlag_NavDirect = (1 << 1),
V_TextBoxDeltaFlag_NavLeft = (1 << 2),
V_TextBoxDeltaFlag_NavRight = (1 << 3),
V_TextBoxDeltaFlag_NavWord = (1 << 4),
V_TextBoxDeltaFlag_NavLine = (1 << 5),
V_TextBoxDeltaFlag_NavSelect = (1 << 6),
};
Struct(V_TextBoxDelta)
{
V_TextBoxDeltaFlag flags;
String text;
i64 direct_start;
i64 direct_end;
};
Struct(V_TextBoxDeltaNode)
{
V_TextBoxDeltaNode *next;
V_TextBoxDelta delta;
};
Struct(V_TextBoxDeltaList)
{
V_TextBoxDeltaNode *first;
V_TextBoxDeltaNode *last;
};
Struct(V_TextBoxState)
{
i64 len;
u8 text[V_MaxTextBoxLen];
i64 start;
i64 end;
};
////////////////////////////////////////////////////////////
//~ Palette types
Struct(V_Palette)
{
// Persistent state
Vec2 pos;
UI_Key key;
b32 is_showing;
f32 show;
V_TextBoxState search_state;
};
////////////////////////////////////////////////////////////
@ -303,6 +350,9 @@ Struct(V_Ctx)
V_Panel *root_panel;
V_Window *dragging_window;
UI_Key keyboard_focus_box;
i64 text_input_ns;
i64 connect_try_ns;
V_ObservationBin observation_bins[V_ObservationBinsCount];
@ -349,6 +399,14 @@ void V_DrawLine(Vec2 p0, Vec2 p1, Vec4 srgb);
void V_DrawRect(Rng2 rect, Vec4 srgb, V_DrawFlag flags);
void V_DrawPoint(Vec2 p, Vec4 srgb);
////////////////////////////////////////////////////////////
//~ Text box
void V_ApplyTextBoxDelta(V_TextBoxState *tb, V_TextBoxDelta delta);
void V_ApplyTextBoxDeltas(V_TextBoxState *tb, V_TextBoxDeltaList deltas);
String V_StringFromTextBox(V_TextBoxState *tb);
////////////////////////////////////////////////////////////
//~ Timeline helpers

View File

@ -1536,6 +1536,7 @@ void UI_EndFrame(UI_Frame *frame, i32 vsync)
GC_Run raw_run_unscaled = box->glyph_run;
GC_Run raw_run = UI_ScaleRun(frame->arena, raw_run_unscaled, box->solved_scale);
// TODO: Check that sprite image is not empty
b32 is_visible = 1;
is_visible = is_visible && (box->desc.tint.w >= 0.0025);
is_visible = is_visible && (box->screen_rect.p1.x - box->screen_rect.p0.x > 0.0025);

View File

@ -328,9 +328,11 @@ LRESULT CALLBACK WND_W32_WindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM l
}
if ((codepoint >= 32 && codepoint != 127) || codepoint == '\t' || codepoint == '\n')
{
Utf8EncodeResult encoded = EncodeUtf8(codepoint);
ControllerEvent event = Zi;
event.kind = ControllerEventKind_Text;
event.text_codepoint = codepoint;
event.text_chars_count = MinU32(encoded.count8, countof(event.text_chars));
CopyBytes(event.text_chars, encoded.chars8, event.text_chars_count);
WND_W32_PushEvent(window, event);
}
} break;