add text line widget, add text wrapping

This commit is contained in:
Matthew 2025-09-13 15:51:05 +10:00
parent de90479aa7
commit 9f8e4aff59
5 changed files with 342 additions and 223 deletions

@ -1 +1 @@
Subproject commit f30edc3bbe9fdfda5eb429a214fdf050a0012af0
Subproject commit 29a98de0e05cb04001b42a55b1ae094ab42f2ab1

View File

@ -4,6 +4,7 @@ import dlib.alloc;
import dlib.util;
import core.stdc.stdio : EOF;
import parsing;
import std.format : sformat;
struct FlatBuffer
{
@ -25,7 +26,6 @@ struct LineBuffers
{
Arena arena;
u8[][] lines;
u32[] lengths;
}
FlatBuffer
@ -108,15 +108,15 @@ Insert(FlatBuffer* buffer, u8[] insert, u64 length, Range pos)
// TODO: handle case for when lines are longer than line buffer
void
GetLines(FlatBuffer* buffer, LineBuffers* linebufs, u64 start_line, u64 length)
GetLines(FlatBuffer* buffer, LineBuffers* linebufs, u64 start_line, u64 length, bool add_hash = false)
{
assert(start_line < buffer.line_count, "GetLines failure: start is not less than line_count");
assert(linebufs != null, "GetLines failure: linebufs is null");
length = length > buffer.line_count ? buffer.line_count : length;
Reset(&linebufs.arena);
linebufs.lines = AllocArray!(u8[])(&linebufs.arena, length);
linebufs.lengths = AllocArray!(u32)(&linebufs.arena, length);
u64 extra_buffer = add_hash ? 10 : 0;
i64 start = -1;
u64 line = 0;
u64 current_line = 0;
@ -136,9 +136,8 @@ GetLines(FlatBuffer* buffer, LineBuffers* linebufs, u64 start_line, u64 length)
if (start < 0 && new_line)
{
linebufs.lines[current_line] = AllocArray!(u8)(&linebufs.arena, 1);
linebufs.lines[current_line] = AllocArray!(u8)(&linebufs.arena, 1 + extra_buffer);
linebufs.lines[current_line][0] = '\n';
linebufs.lengths[current_line] = 1;
current_line += 1;
continue;
}
@ -151,9 +150,8 @@ GetLines(FlatBuffer* buffer, LineBuffers* linebufs, u64 start_line, u64 length)
if (new_line)
{
linebufs.lines[current_line] = AllocArray!(u8)(&linebufs.arena, i-start);
linebufs.lines[current_line][0 .. $] = buffer.data[start .. i];
linebufs.lengths[current_line] = cast(u32)(i-start);
linebufs.lines[current_line] = AllocArray!(u8)(&linebufs.arena, (i-start) + extra_buffer);
linebufs.lines[current_line][0 .. i-start] = buffer.data[start .. i];
current_line += 1;
start = -1;
continue;
@ -161,9 +159,21 @@ GetLines(FlatBuffer* buffer, LineBuffers* linebufs, u64 start_line, u64 length)
if (i == buffer.length-1)
{
linebufs.lines[current_line] = AllocArray!(u8)(&linebufs.arena, buffer.length-start);
linebufs.lines[current_line][0 .. $] = buffer.data[start .. buffer.length];
linebufs.lengths[current_line] = cast(u32)(buffer.length-start);
linebufs.lines[current_line] = AllocArray!(u8)(&linebufs.arena, (buffer.length-start) + extra_buffer);
linebufs.lines[current_line][0 .. buffer.length-start] = buffer.data[start .. buffer.length];
}
}
if (add_hash)
{
for (u64 i = 0; i < linebufs.lines.length; i += 1)
{
if (linebufs.lines[i].length > 0)
{
u8[10] buf = 0;
(cast(char[])buf).sformat("##%08s", i);
linebufs.lines[i][linebufs.lines[i].length - extra_buffer .. $] = buf[0 .. $];
}
}
}
}

View File

@ -29,16 +29,16 @@ struct Editor
Arena temp_arena;
PlatformWindow* window;
Renderer rd;
ImageView font_atlas;
Pipeline pipeline;
DescSetLayout desc_set_layout;
DescSet desc_set;
PipelineLayout pipeline_layout;
PushConst pc;
Renderer rd;
ImageView font_atlas;
Pipeline pipeline;
DescSetLayout desc_set_layout;
DescSet desc_set;
PipelineLayout pipeline_layout;
PushConst pc;
FlatBuffer[] buffers;
Tokenizer[] tokenizers;
FlatBuffer[] buffers;
Tokenizer[] tokenizers;
u8[][] buffer_names;
u32 buffer_count;
FlatBuffer* active_buffer;
@ -48,7 +48,7 @@ struct Editor
FontFace font;
FontAtlasBuf atlas_buf;
UVec2 res;
Vec2 res;
}
struct PushConst
@ -70,6 +70,82 @@ struct Vertex
u32 texture;
}
void
Cycle(Editor* ed, Inputs* inputs)
{
Reset(&ed.temp_arena);
ResetScratch(MB(4));
BeginBuild(inputs);
static UIPanel panel = {
id: CastStr!(u8)("##main_panel"),
pct: 1.0,
axis: A2D.X,
color: Vec4(0.2, 0.4, 0.8, 1.0),
};
static UIPanel panel_l = {
id: CastStr!(u8)("##panel_l"),
pct: 0.5,
axis: A2D.Y,
color: Vec4(0.2, 0.4, 0.8, 1.0),
};
static UIPanel panel_r = {
id: CastStr!(u8)("##panel_r"),
pct: 0.5,
axis: A2D.Y,
color: Vec4(0.4, 0.3, 0.7, 1.0),
};
BeginFrame(&ed.rd);
Vec2 ext = GetExtent(&ed.rd);
if (ext != ed.res)
{
ed.res = ext;
Ortho(&ed.pc.projection, 0.0, 0.0, ext.x, ext.y, 1000.0, 0.1);
}
u32 rows = cast(u32)(ext.y / ed.atlas_buf.atlas.size) + 5;
Panel(&panel);
{
Panel(&panel_l);
{
if (ed.active_buffer != null)
{
GetLines(&ed.buffers[0], &ed.linebufs, 0, rows);
for(u64 i = 0; i < ed.linebufs.lines.length; i += 1)
{
TextLine(ed.linebufs.lines[i], ed.atlas_buf.atlas.size, i);
}
}
}
EndPanel();
Panel(&panel_r);
{
}
EndPanel();
}
EndPanel();
BeginRendering(&ed.rd);
PushConstants(&ed.rd, ed.pipeline, &ed.pc);
Bind(&ed.rd, ed.pipeline, ed.desc_set);
EndBuild();
FinishRendering(&ed.rd);
SubmitAndPresent(&ed.rd);
}
Editor
CreateEditor(PlatformWindow* window, u8[] buffer_data, u8[] buffer_name)
{
@ -175,140 +251,6 @@ CreateEditor(PlatformWindow* window, u8[] buffer_data, u8[] buffer_name)
return editor;
}
void
Cycle(Editor* ed, Inputs* inputs)
{
Reset(&ed.temp_arena);
BeginBuild(inputs);
static UIPanel[11] panels = [
{
id: CastStr!(u8)("##panel_1"),
pct: 0.3,
axis: A2D.X,
color: Vec4(0.2, 0.5, 0.8, 1.0),
},
{
id: CastStr!(u8)("##sub_panel_1"),
pct: 0.125,
axis: A2D.X,
color: Vec4(0.2, 0.4, 0.5, 1.0),
},
{
id: CastStr!(u8)("##sub_panel_2"),
pct: 0.675,
axis: A2D.X,
color: Vec4(0.9, 0.6, 0.5, 1.0),
},
{
id: CastStr!(u8)("##sub_panel_3"),
pct: 0.2,
axis: A2D.X,
color: Vec4(1.0, 0.4, 0.5, 1.0),
},
{
id: CastStr!(u8)("##panel_2"),
pct: 0.3,
axis: A2D.X,
color: Vec4(0.5, 0.2, 0.45, 1.0),
},
{
id: CastStr!(u8)("##panel_3"),
pct: 0.4,
axis: A2D.X,
color: Vec4(0.3, 0.7, 0.6, 1.0),
},
{
id: CastStr!(u8)("##sub_panel_4"),
pct: 0.25,
axis: A2D.Y,
color: Vec4(0.33, 0.4, 0.8, 1.0),
},
{
id: CastStr!(u8)("##sub_sub_panel_1"),
pct: 0.4,
axis: A2D.X,
color: Vec4(1.0, 0.0, 0.0, 1.0),
},
{
id: CastStr!(u8)("##sub_sub_panel_2"),
pct: 0.6,
axis: A2D.X,
color: Vec4(1.0, 1.0, 0.0, 1.0),
},
{
id: CastStr!(u8)("##sub_panel_5"),
pct: 0.55,
axis: A2D.X,
color: Vec4(0.9, 0.2, 0.3, 1.0),
},
{
id: CastStr!(u8)("##sub_panel_6"),
pct: 0.2,
axis: A2D.X,
color: Vec4(0.2, 0.76, 0.5, 1.0),
},
];
Panel(&panels[0]);
{
Panel(&panels[1]);
EndPanel();
Panel(&panels[2]);
EndPanel();
Panel(&panels[3]);
EndPanel();
}
EndPanel();
Panel(&panels[4]);
EndPanel();
Panel(&panels[5]);
{
Panel(&panels[6]);
{
Panel(&panels[7]);
EndPanel();
Panel(&panels[8]);
EndPanel();
}
EndPanel();
Panel(&panels[9]);
EndPanel();
Panel(&panels[10]);
EndPanel();
}
EndPanel();
BeginFrame(&ed.rd);
UVec2 ext = UVec2(GetExtent(&ed.rd));
if (ext != ed.res)
{
ed.res = ext;
Ortho(&ed.pc.projection, 0.0, 0.0, cast(f32)(ext.x), cast(f32)(ext.y), 1000.0, 0.1);
}
BeginRendering(&ed.rd);
PushConstants(&ed.rd, ed.pipeline, &ed.pc);
Bind(&ed.rd, ed.pipeline, ed.desc_set);
EndBuild();
FinishRendering(&ed.rd);
SubmitAndPresent(&ed.rd);
}
void
DrawText(Editor* ed, f32 x, f32 y, f32 px, string str)
{

View File

@ -7,6 +7,9 @@ import dlib.fonts;
import vulkan;
import widgets;
import std.stdio;
import std.math.rounding : ceil;
import std.format : sformat;
import core.stdc.string : memset;
import editor;
@ -31,8 +34,9 @@ enum UIFlags
{
None = 0x00,
DrawBackground = 0x01,
Clickable = 0x02,
Draggable = 0x04,
DrawText = 0x02,
Clickable = 0x04,
Draggable = 0x08,
}
alias UIF = UIFlags;
@ -60,8 +64,10 @@ struct UICtx
Arena arena;
Renderer* rd;
Inputs* inputs;
u64 frame;
u64 f_idx;
UIBuffer buffer;
UIBuffer[FRAME_OVERLAP] buffers;
UIItem* root;
UIItemNode* top_parent;
@ -77,6 +83,8 @@ struct UICtx
Vec4 text_color;
Axis2D layout_axis;
Vec2 adjustment;
u32 tab_width;
f32 text_size;
}
struct UIItemStackList
@ -125,8 +133,8 @@ struct UISize
struct UIBuffer
{
MappedBuffer!(Vertex) mapped_vtx;
MappedBuffer!(u32) mapped_idx;
MappedBuffer!(Vertex) m_vtx;
MappedBuffer!(u32) m_idx;
Vertex[] vtx;
u32[] idx;
u32 count;
@ -167,8 +175,16 @@ InitUICtx(Renderer* rd, FontAtlas atlas)
Arena arena = CreateArena(MB(4));
MappedBuffer!(Vertex) m_vtx = CreateMappedBuffer!(Vertex)(rd, BT.Vertex, 5000);
MappedBuffer!(u32) m_idx = CreateMappedBuffer!(u32)(rd, BT.Index, 15000);
UIBuffer[FRAME_OVERLAP] buffers;
u64 vertex_size = 10000;
for(u64 i = 0; i < FRAME_OVERLAP; i += 1)
{
buffers[i].m_vtx = CreateMappedBuffer!(Vertex)(rd, BT.Vertex, vertex_size);
buffers[i].m_idx = CreateMappedBuffer!(u32)(rd, BT.Index, cast(u64)(ceil(vertex_size * 1.5)));
buffers[i].vtx = buffers[i].m_vtx.data;
buffers[i].idx = buffers[i].m_idx.data;
}
UICtx ctx = {
rd: rd,
@ -179,12 +195,9 @@ InitUICtx(Renderer* rd, FontAtlas atlas)
top_parent: g_UI_NIL_NODE,
prev_sibling: g_UI_NIL_NODE,
drag_item: g_UI_NIL,
buffer: {
mapped_vtx: m_vtx,
mapped_idx: m_idx,
vtx: m_vtx.data,
idx: m_idx.data,
},
text_size: 14.0,
tab_width: 2,
buffers: buffers,
};
g_ui_ctx = ctx;
@ -361,13 +374,25 @@ SetLayoutAxis(Axis2D axis)
ctx.layout_axis = axis;
}
void
SetTextSize(f32 size)
{
g_ui_ctx.text_size = size;
}
void
BeginBuild(Inputs* inputs)
{
UICtx* ctx = GetCtx();
ctx.f_idx = ctx.frame%FRAME_OVERLAP;
ctx.inputs = inputs;
ctx.buffer.count = 0;
memset(ctx.buffers[ctx.f_idx].vtx.ptr, 0, Vertex.sizeof * ctx.buffers[ctx.f_idx].count);
memset(ctx.buffers[ctx.f_idx].idx.ptr, 0, u32.sizeof * ctx.buffers[ctx.f_idx].count);
ctx.buffers[ctx.f_idx].count = 0;
ctx.panel_level = 0;
ctx.root = Root();
@ -400,8 +425,10 @@ EndBuild()
first = true;
}
BindBuffers(g_ui_ctx.rd, &g_ui_ctx.buffer.mapped_idx, &g_ui_ctx.buffer.mapped_vtx);
DrawIndexed(g_ui_ctx.rd, 6, g_ui_ctx.buffer.count, 0);
BindBuffers(ctx.rd, &ctx.buffers[ctx.f_idx].m_idx, &ctx.buffers[ctx.f_idx].m_vtx);
DrawIndexed(ctx.rd, 6, ctx.buffers[ctx.f_idx].count, 0);
ctx.frame += 1;
}
void
@ -473,7 +500,15 @@ DrawUI(UICtx* ctx, UIItem* item)
{
if (!Nil(item))
{
DrawRect(ctx, item);
if (item.flags & UIF.DrawBackground)
{
DrawRect(ctx, item);
}
if (item.flags & UIF.DrawText)
{
DrawLine(item);
}
DrawUI(ctx, item.first);
DrawUI(ctx, item.next);
@ -567,20 +602,33 @@ RootSize()
return size;
}
UIKey
MakeKey(u8[] text, u8[] hash)
{
UIKey key;
key.text = text;
key.hash = Hash(text, hash);
return key;
}
UIKey
MakeKey(u8[] id)
{
UIKey key;
u32 pos = 0;
i64 pos = 0;
u32 hash_count = 0;
for(u32 i = 0; i < id.length; i += 1)
for(i64 i = id.length-1; i >= 0; i -= 1)
{
if (hash_count == 2)
{
if (id[i] == '#')
{
hash_count += 1;
pos = i;
}
break;
@ -588,11 +636,7 @@ MakeKey(u8[] id)
if (id[i] == '#')
{
if (hash_count == 0)
{
pos = i;
}
pos = i;
hash_count += 1;
}
}
@ -616,6 +660,21 @@ MakeKey(u8[] id)
return key;
}
pragma(inline) UIItem*
Get(UIKey key)
{
Result!(UIItem*) result = g_ui_ctx.items[key.hash];
if (!result.ok)
{
result.value = Alloc!(UIItem)(&g_ui_ctx.arena);
Push(&g_ui_ctx.items, key.hash, result.value);
}
result.value.key = key;
return result.value;
}
UIItem*
Get(string id)
{
@ -627,48 +686,132 @@ UIItem*
Get(u8[] id)
{
UIKey key = MakeKey(id);
Result!(UIItem*) result = g_ui_ctx.items[key.hash];
if (!result.ok)
{
result.value = Alloc!(UIItem)(&g_ui_ctx.arena);
Push(&g_ui_ctx.items, key.hash, result.value);
result.value.key = key;
}
return result.value;
return Get(key);
}
f32
CalcTextWidth(u8[] str)
{
u32 tab_width = 2; //g_ui_ctx.tab_width;
u32 tab_width = g_ui_ctx.tab_width;
Glyph* space = g_ui_ctx.atlas.glyphs.ptr + ' ';
f32 width;
f32 width = 0.0;
for(u64 i = 0; i < str.length; i += 1)
{
Glyph* g = g_ui_ctx.atlas.glyphs.ptr + str.ptr[i];
width += GlyphWidth(g_ui_ctx.atlas.glyphs.ptr + str.ptr[i]);
}
if (g.ch == '\t')
return width;
}
pragma(inline) f32
GlyphWidth(Glyph* g)
{
f32 width = 0.0;
if (g.ch == '\t')
{
width += g_ui_ctx.atlas.glyphs[' '].advance * cast(f32)(g_ui_ctx.tab_width);
}
else
{
width += g.advance;
}
return width;
}
Node!(u8[])*
MakeMultiline(u8[] text, f32 width, u64 line_no)
{
f32 scaled_width = width * (g_ui_ctx.atlas.size/g_ui_ctx.text_size);
f32 text_width = CalcTextWidth(text);
u64 line_count = cast(u64)(ceil(text_width/scaled_width));
Node!(u8[])* node = null;
if (line_count > 0)
{
f32 w = 0.0;
u64 line = 0;
u64 start = 0;
const u64 extra_buf = 10;
for(u64 i = 0; i < text.length; i += 1)
{
width += space.advance * cast(f32)(tab_width);
}
else
{
width += g.advance;
f32 ch_w = GlyphWidth(g_ui_ctx.atlas.glyphs.ptr + text[i]);
if (ch_w + w > scaled_width || i == text.length-1)
{
u64 len = i-start+1;
u8[10] buf = 0;
(cast(char[])buf).sformat("##%04s%04s", line_no, line);
u8[] str = ScratchAlloc!(u8)(len+extra_buf);
str[0 .. len] = text[start .. start+len];
str[len .. len+extra_buf] = buf[0 .. $];
Node!(u8[])* n = node;
for(;;)
{
if (node == null)
{
node = ScratchAlloc!(Node!(u8[]))();
node.value = str;
node.next = null;
break;
}
if (n.next == null)
{
n.next = ScratchAlloc!(Node!(u8[]))();
n.next.value = str;
n.next.next = null;
break;
}
n = n.next;
}
line += 1;
start = i;
w = 0.0;
}
w += ch_w;
}
}
return width; // * g_ui_ctx.text_scale;
return node;
}
void
DrawLine(UIItem* item)
{
UICtx* ctx = GetCtx();
f32 y = item.rect.y0 + ctx.text_size;
f32 x = item.rect.x0;
FontAtlas* atlas = &ctx.atlas;
for(u64 i = 0; i < item.key.text.length && item.key.text[i] != '\0'; i += 1)
{
u8 ch = item.key.text.ptr[i];
if (ch < 128)
{
DrawGlyph(&atlas.glyphs[ch], atlas.size/ctx.text_size, &x, y);
}
}
}
pragma(inline) void
DrawGlyph(Glyph* glyph, f32 scale, f32* x_pos, f32 y, Vec4 col = Vec4(1.0))
{
if (glyph.atlas_left != glyph.atlas_right)
if (glyph.ch == '\t')
{
Vertex* v = g_ui_ctx.buffer.vtx.ptr + g_ui_ctx.buffer.count;
*x_pos += glyph.advance * (GetCtx().tab_width - 1);
}
else if (glyph.atlas_left != glyph.atlas_right && glyph.ch != '\n')
{
UICtx* ctx = GetCtx();
Vertex* v = ctx.buffers[ctx.f_idx].vtx.ptr + ctx.buffers[ctx.f_idx].count;
f32 r = glyph.plane_right * scale;
f32 l = glyph.plane_left * scale;
@ -693,7 +836,7 @@ DrawGlyph(Glyph* glyph, f32 scale, f32* x_pos, f32 y, Vec4 col = Vec4(1.0))
v.texture = 1;
AddUIIndices();
AddUIIndices(ctx);
}
*x_pos += glyph.advance * scale;
@ -703,7 +846,7 @@ pragma(inline) void
DrawRect(UICtx* ctx, UIItem* item)
{
// Y reversed
Vertex* v = ctx.buffer.vtx.ptr + ctx.buffer.count;
Vertex* v = ctx.buffers[ctx.f_idx].vtx.ptr + ctx.buffers[ctx.f_idx].count;
v.dst_start = item.rect.vec0;
v.dst_end = item.rect.vec1;
v.cols = item.color;
@ -712,20 +855,20 @@ DrawRect(UICtx* ctx, UIItem* item)
v.edge_softness = 0.0;
v.raised = 0.0;
AddUIIndices();
AddUIIndices(ctx);
}
void
AddUIIndices()
pragma(inline) void
AddUIIndices(UICtx* ctx)
{
g_ui_ctx.buffer.idx[0] = 0;
g_ui_ctx.buffer.idx[1] = 1;
g_ui_ctx.buffer.idx[2] = 2;
g_ui_ctx.buffer.idx[3] = 2;
g_ui_ctx.buffer.idx[4] = 1;
g_ui_ctx.buffer.idx[5] = 3;
ctx.buffers[ctx.f_idx].idx[0] = 0;
ctx.buffers[ctx.f_idx].idx[1] = 1;
ctx.buffers[ctx.f_idx].idx[2] = 2;
ctx.buffers[ctx.f_idx].idx[3] = 2;
ctx.buffers[ctx.f_idx].idx[4] = 1;
ctx.buffers[ctx.f_idx].idx[5] = 3;
g_ui_ctx.buffer.count += 1;
ctx.buffers[ctx.f_idx].count += 1;
}
bool

View File

@ -114,8 +114,8 @@ Panel(UIPanel* panel)
parent_end = parent.rect.vec1.y;
}
u8[128] buf = 0;
(cast(char[])buf).sformat("sep_%s", cast(char[])panel.id);
u8[] buf = ScratchAlloc!(u8)(panel.id.length + 5);
(cast(char[])buf).sformat("%s_sep", cast(char[])panel.id);
separator = Get(buf);
@ -127,12 +127,11 @@ Panel(UIPanel* panel)
if (separator.signal & UIS.Dragged && pos != 0.0)
{
f32 pct = Remap(pos, 0.0, parent_start-parent_end, 0.0, 1.0);
panel.pct -= pct;
panel.prev.pct += pct;
Logf("%f", pct);
Logf("%s %f %s %f", cast(char[])panel.id, panel.pct, cast(char[])panel.prev.id, panel.prev.pct);
if (CheckPanelBounds(panel.pct - pct) && CheckPanelBounds(panel.prev.pct + pct))
{
panel.pct -= pct;
panel.prev.pct += pct;
}
}
}
@ -151,6 +150,12 @@ Panel(UIPanel* panel)
return item;
}
bool
CheckPanelBounds(f32 pct)
{
return pct >= 0.0 && pct <= 1.0;
}
void
Separator(UIItem* item, f32 x_size, f32 y_size, Axis2D axis)
{
@ -162,6 +167,25 @@ Separator(UIItem* item, f32 x_size, f32 y_size, Axis2D axis)
BuildItem(item, UISize(x_t, x_size), UISize(y_t, y_size), UIF.DrawBackground|UIF.Draggable);
}
void
TextLine(u8[] text, f32 text_size, u64 line_no)
{
UIItem* parent = PeekParent();
if (!Nil(parent) && parent.size.x > 0.0)
{
SetColor(Vec4(1.0));
SetTextSize(text_size);
Node!(u8[])* lines = MakeMultiline(text, parent.size.x, line_no);
for(Node!(u8[])* line = lines; line != null; line = line.next)
{
UIItem* item = Get(line.value);
BuildItem(item, UISize(ST.Percentage, 1.0), UISize(ST.Pixels, text_size), UIF.DrawText);
}
}
}
void
EndPanel()
{