WIP: lunasvg implementation, things stopped working
This commit is contained in:
12
src/main.cpp
12
src/main.cpp
@@ -10,11 +10,13 @@
|
||||
#include "midi/midi.h"
|
||||
#include "audio/audio.h"
|
||||
#include "ui/ui_core.h"
|
||||
#include "ui/ui_icons.h"
|
||||
#include "ui/ui_widgets.h"
|
||||
|
||||
// [cpp]
|
||||
#include "base/base_inc.cpp"
|
||||
#include "ui/ui_core.cpp"
|
||||
#include "ui/ui_icons.cpp"
|
||||
#include "ui/ui_widgets.cpp"
|
||||
#ifdef __APPLE__
|
||||
#include "platform/platform_macos.mm"
|
||||
@@ -808,6 +810,16 @@ int main(int argc, char **argv) {
|
||||
setup_menus(window);
|
||||
ui_widgets_init();
|
||||
|
||||
// Rasterize icon atlas and upload to GPU
|
||||
{
|
||||
S32 iw, ih;
|
||||
U8 *icon_atlas = ui_icons_rasterize_atlas(&iw, &ih, 48);
|
||||
if (icon_atlas) {
|
||||
renderer_create_icon_atlas(renderer, icon_atlas, iw, ih);
|
||||
free(icon_atlas);
|
||||
}
|
||||
}
|
||||
|
||||
AppState app = {};
|
||||
app.window = window;
|
||||
app.renderer = renderer;
|
||||
|
||||
@@ -26,3 +26,6 @@ void renderer_set_clear_color(Renderer *renderer, float r, float g, float b
|
||||
// user_data should be the Renderer pointer.
|
||||
struct Vec2F32;
|
||||
Vec2F32 renderer_measure_text(const char *text, int32_t length, float font_size, void *user_data);
|
||||
|
||||
// Upload an R8 icon atlas texture for icon rendering
|
||||
void renderer_create_icon_atlas(Renderer *renderer, const uint8_t *data, int32_t w, int32_t h);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "renderer/renderer.h"
|
||||
#include "ui/ui_core.h"
|
||||
#include "ui/ui_icons.h"
|
||||
|
||||
#include <d3d12.h>
|
||||
#include <dxgi1_5.h>
|
||||
@@ -201,6 +202,10 @@ struct Renderer {
|
||||
F32 font_atlas_size; // font size the atlas was built at
|
||||
F32 font_line_height;
|
||||
|
||||
// Icon atlas
|
||||
ID3D12Resource *icon_texture;
|
||||
ID3D12DescriptorHeap *icon_srv_heap;
|
||||
|
||||
// GDI text measurement
|
||||
HDC measure_dc;
|
||||
HFONT measure_font;
|
||||
@@ -976,7 +981,7 @@ static void emit_text_glyphs(DrawBatch *batch, Renderer *r,
|
||||
////////////////////////////////
|
||||
// Flush helper: issues a draw call for accumulated vertices, then resets batch
|
||||
|
||||
static void flush_batch(Renderer *r, DrawBatch *batch, UINT buf_idx) {
|
||||
static void flush_batch(Renderer *r, DrawBatch *batch, UINT buf_idx, ID3D12DescriptorHeap *tex_heap = nullptr) {
|
||||
if (batch->index_count == 0) return;
|
||||
|
||||
r->command_list->SetPipelineState(r->pipeline_state);
|
||||
@@ -985,10 +990,11 @@ static void flush_batch(Renderer *r, DrawBatch *batch, UINT buf_idx) {
|
||||
float constants[4] = { (float)r->width, (float)r->height, 0, 0 };
|
||||
r->command_list->SetGraphicsRoot32BitConstants(0, 4, constants, 0);
|
||||
|
||||
// Bind font texture
|
||||
r->command_list->SetDescriptorHeaps(1, &r->srv_heap);
|
||||
// Bind texture (font or icon)
|
||||
ID3D12DescriptorHeap *heap = tex_heap ? tex_heap : r->srv_heap;
|
||||
r->command_list->SetDescriptorHeaps(1, &heap);
|
||||
r->command_list->SetGraphicsRootDescriptorTable(1,
|
||||
r->srv_heap->GetGPUDescriptorHandleForHeapStart());
|
||||
heap->GetGPUDescriptorHandleForHeapStart());
|
||||
|
||||
r->command_list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
||||
|
||||
@@ -1053,6 +1059,8 @@ void renderer_destroy(Renderer *r) {
|
||||
if (r->index_buffers[i]) r->index_buffers[i]->Release();
|
||||
}
|
||||
if (r->font_texture) r->font_texture->Release();
|
||||
if (r->icon_texture) r->icon_texture->Release();
|
||||
if (r->icon_srv_heap) r->icon_srv_heap->Release();
|
||||
if (r->pipeline_state) r->pipeline_state->Release();
|
||||
if (r->root_signature) r->root_signature->Release();
|
||||
|
||||
@@ -1133,6 +1141,23 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
batch.vertex_count = 0;
|
||||
batch.index_count = 0;
|
||||
|
||||
// Track which texture is currently bound (0 = font, 1 = icon)
|
||||
int bound_texture = 0;
|
||||
|
||||
auto bind_font = [&]() {
|
||||
if (bound_texture != 0) {
|
||||
flush_batch(r, &batch, buf_idx, r->srv_heap);
|
||||
bound_texture = 0;
|
||||
}
|
||||
};
|
||||
|
||||
auto bind_icon = [&]() {
|
||||
if (bound_texture != 1 && r->icon_srv_heap) {
|
||||
flush_batch(r, &batch, buf_idx);
|
||||
bound_texture = 1;
|
||||
}
|
||||
};
|
||||
|
||||
for (int32_t i = 0; i < render_commands.length; i++) {
|
||||
Clay_RenderCommand *cmd = Clay_RenderCommandArray_Get(&render_commands, i);
|
||||
Clay_BoundingBox bb = cmd->boundingBox;
|
||||
@@ -1177,6 +1202,7 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
} break;
|
||||
|
||||
case CLAY_RENDER_COMMAND_TYPE_TEXT: {
|
||||
bind_font();
|
||||
Clay_TextRenderData *text = &cmd->renderData.text;
|
||||
emit_text_glyphs(&batch, r, bb, text->textColor,
|
||||
text->stringContents.chars, text->stringContents.length,
|
||||
@@ -1185,7 +1211,8 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
|
||||
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: {
|
||||
// Flush before changing scissor
|
||||
flush_batch(r, &batch, buf_idx);
|
||||
ID3D12DescriptorHeap *heap = bound_texture == 1 ? r->icon_srv_heap : r->srv_heap;
|
||||
flush_batch(r, &batch, buf_idx, heap);
|
||||
D3D12_RECT clip = {};
|
||||
clip.left = (LONG)Max(bb.x, 0.f);
|
||||
clip.top = (LONG)Max(bb.y, 0.f);
|
||||
@@ -1197,7 +1224,8 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
} break;
|
||||
|
||||
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: {
|
||||
flush_batch(r, &batch, buf_idx);
|
||||
ID3D12DescriptorHeap *heap = bound_texture == 1 ? r->icon_srv_heap : r->srv_heap;
|
||||
flush_batch(r, &batch, buf_idx, heap);
|
||||
D3D12_RECT full_scissor = { 0, 0, (LONG)r->width, (LONG)r->height };
|
||||
r->command_list->RSSetScissorRects(1, &full_scissor);
|
||||
} break;
|
||||
@@ -1207,6 +1235,7 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
if (custom->customData) {
|
||||
CustomRenderType type = *(CustomRenderType *)custom->customData;
|
||||
if (type == CUSTOM_RENDER_VGRADIENT) {
|
||||
bind_font();
|
||||
CustomGradientData *grad = (CustomGradientData *)custom->customData;
|
||||
Clay_Color tc = grad->top_color;
|
||||
Clay_Color bc = grad->bottom_color;
|
||||
@@ -1217,6 +1246,20 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
custom->cornerRadius.topLeft, custom->cornerRadius.topRight,
|
||||
custom->cornerRadius.bottomRight, custom->cornerRadius.bottomLeft,
|
||||
1.0f);
|
||||
} else if (type == CUSTOM_RENDER_ICON) {
|
||||
bind_icon();
|
||||
CustomIconData *icon = (CustomIconData *)custom->customData;
|
||||
Clay_Color c = icon->color;
|
||||
float cr = c.r / 255.f, cg = c.g / 255.f;
|
||||
float cb = c.b / 255.f, ca = c.a / 255.f;
|
||||
UI_IconInfo *info = &g_icons[icon->icon_id];
|
||||
emit_quad(&batch,
|
||||
bb.x, bb.y, bb.x + bb.width, bb.y + bb.height,
|
||||
info->u0, info->v0, info->u1, info->v1,
|
||||
cr, cg, cb, ca,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 1.0f);
|
||||
}
|
||||
}
|
||||
} break;
|
||||
@@ -1228,7 +1271,8 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
}
|
||||
|
||||
// Flush remaining
|
||||
flush_batch(r, &batch, buf_idx);
|
||||
ID3D12DescriptorHeap *heap = bound_texture == 1 ? r->icon_srv_heap : r->srv_heap;
|
||||
flush_batch(r, &batch, buf_idx, heap);
|
||||
}
|
||||
|
||||
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
|
||||
@@ -1245,6 +1289,101 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
r->frame_index++;
|
||||
}
|
||||
|
||||
void renderer_create_icon_atlas(Renderer *r, const uint8_t *data, int32_t w, int32_t h) {
|
||||
// Create texture resource
|
||||
D3D12_HEAP_PROPERTIES heap_props = {};
|
||||
heap_props.Type = D3D12_HEAP_TYPE_DEFAULT;
|
||||
|
||||
D3D12_RESOURCE_DESC tex_desc = {};
|
||||
tex_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
|
||||
tex_desc.Width = w;
|
||||
tex_desc.Height = h;
|
||||
tex_desc.DepthOrArraySize = 1;
|
||||
tex_desc.MipLevels = 1;
|
||||
tex_desc.Format = DXGI_FORMAT_R8_UNORM;
|
||||
tex_desc.SampleDesc.Count = 1;
|
||||
tex_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
|
||||
|
||||
r->device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_NONE,
|
||||
&tex_desc, D3D12_RESOURCE_STATE_COPY_DEST, nullptr,
|
||||
IID_PPV_ARGS(&r->icon_texture));
|
||||
|
||||
// Upload via staging buffer
|
||||
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint = {};
|
||||
UINT64 total_bytes = 0;
|
||||
r->device->GetCopyableFootprints(&tex_desc, 0, 1, 0, &footprint, nullptr, nullptr, &total_bytes);
|
||||
|
||||
D3D12_HEAP_PROPERTIES upload_heap = {};
|
||||
upload_heap.Type = D3D12_HEAP_TYPE_UPLOAD;
|
||||
|
||||
D3D12_RESOURCE_DESC upload_desc = {};
|
||||
upload_desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
|
||||
upload_desc.Width = total_bytes;
|
||||
upload_desc.Height = 1;
|
||||
upload_desc.DepthOrArraySize = 1;
|
||||
upload_desc.MipLevels = 1;
|
||||
upload_desc.SampleDesc.Count = 1;
|
||||
upload_desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
|
||||
|
||||
ID3D12Resource *upload_buf = nullptr;
|
||||
r->device->CreateCommittedResource(&upload_heap, D3D12_HEAP_FLAG_NONE,
|
||||
&upload_desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr,
|
||||
IID_PPV_ARGS(&upload_buf));
|
||||
|
||||
void *mapped = nullptr;
|
||||
D3D12_RANGE read_range = {0, 0};
|
||||
upload_buf->Map(0, &read_range, &mapped);
|
||||
U8 *dst = (U8 *)mapped;
|
||||
for (int y = 0; y < h; y++) {
|
||||
memcpy(dst + y * footprint.Footprint.RowPitch, data + y * w, w);
|
||||
}
|
||||
upload_buf->Unmap(0, nullptr);
|
||||
|
||||
r->frames[0].command_allocator->Reset();
|
||||
r->command_list->Reset(r->frames[0].command_allocator, nullptr);
|
||||
|
||||
D3D12_TEXTURE_COPY_LOCATION src_loc = {};
|
||||
src_loc.pResource = upload_buf;
|
||||
src_loc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
|
||||
src_loc.PlacedFootprint = footprint;
|
||||
|
||||
D3D12_TEXTURE_COPY_LOCATION dst_loc = {};
|
||||
dst_loc.pResource = r->icon_texture;
|
||||
dst_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
|
||||
dst_loc.SubresourceIndex = 0;
|
||||
|
||||
r->command_list->CopyTextureRegion(&dst_loc, 0, 0, 0, &src_loc, nullptr);
|
||||
|
||||
D3D12_RESOURCE_BARRIER barrier = {};
|
||||
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
|
||||
barrier.Transition.pResource = r->icon_texture;
|
||||
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
|
||||
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
|
||||
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
|
||||
r->command_list->ResourceBarrier(1, &barrier);
|
||||
|
||||
r->command_list->Close();
|
||||
r->command_queue->ExecuteCommandLists(1, (ID3D12CommandList *const *)&r->command_list);
|
||||
wait_for_pending(r);
|
||||
upload_buf->Release();
|
||||
|
||||
// Create separate SRV heap for icon texture
|
||||
D3D12_DESCRIPTOR_HEAP_DESC srv_desc = {};
|
||||
srv_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
|
||||
srv_desc.NumDescriptors = 1;
|
||||
srv_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
|
||||
r->device->CreateDescriptorHeap(&srv_desc, IID_PPV_ARGS(&r->icon_srv_heap));
|
||||
|
||||
D3D12_SHADER_RESOURCE_VIEW_DESC srv_view = {};
|
||||
srv_view.Format = DXGI_FORMAT_R8_UNORM;
|
||||
srv_view.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
|
||||
srv_view.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
|
||||
srv_view.Texture2D.MipLevels = 1;
|
||||
|
||||
r->device->CreateShaderResourceView(r->icon_texture,
|
||||
&srv_view, r->icon_srv_heap->GetCPUDescriptorHandleForHeapStart());
|
||||
}
|
||||
|
||||
void renderer_resize(Renderer *r, int32_t width, int32_t height) {
|
||||
if (width <= 0 || height <= 0) return;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "renderer/renderer.h"
|
||||
#include "ui/ui_core.h"
|
||||
#include "ui/ui_icons.h"
|
||||
|
||||
#import <Metal/Metal.h>
|
||||
#import <QuartzCore/CAMetalLayer.h>
|
||||
@@ -162,6 +163,9 @@ struct Renderer {
|
||||
F32 font_atlas_size;
|
||||
F32 font_line_height;
|
||||
|
||||
// Icon atlas
|
||||
id<MTLTexture> icon_texture;
|
||||
|
||||
// Text measurement (Core Text)
|
||||
CTFontRef measure_font;
|
||||
F32 measure_font_size;
|
||||
@@ -694,6 +698,9 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
batch.vertex_count = 0;
|
||||
batch.index_count = 0;
|
||||
|
||||
// Track which texture is currently bound (0 = font, 1 = icon)
|
||||
int bound_texture = 0;
|
||||
|
||||
auto flush_batch = [&]() {
|
||||
if (batch.index_count == 0) return;
|
||||
|
||||
@@ -708,6 +715,22 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
batch.index_count = 0;
|
||||
};
|
||||
|
||||
auto bind_font_texture = [&]() {
|
||||
if (bound_texture != 0) {
|
||||
flush_batch();
|
||||
[encoder setFragmentTexture:r->font_texture atIndex:0];
|
||||
bound_texture = 0;
|
||||
}
|
||||
};
|
||||
|
||||
auto bind_icon_texture = [&]() {
|
||||
if (bound_texture != 1 && r->icon_texture) {
|
||||
flush_batch();
|
||||
[encoder setFragmentTexture:r->icon_texture atIndex:0];
|
||||
bound_texture = 1;
|
||||
}
|
||||
};
|
||||
|
||||
for (int32_t i = 0; i < render_commands.length; i++) {
|
||||
Clay_RenderCommand *cmd = Clay_RenderCommandArray_Get(&render_commands, i);
|
||||
Clay_BoundingBox bb = cmd->boundingBox;
|
||||
@@ -751,6 +774,7 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
} break;
|
||||
|
||||
case CLAY_RENDER_COMMAND_TYPE_TEXT: {
|
||||
bind_font_texture();
|
||||
Clay_TextRenderData *text = &cmd->renderData.text;
|
||||
emit_text_glyphs(&batch, r, bb, text->textColor,
|
||||
text->stringContents.chars, text->stringContents.length,
|
||||
@@ -779,6 +803,7 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
if (custom->customData) {
|
||||
CustomRenderType type = *(CustomRenderType *)custom->customData;
|
||||
if (type == CUSTOM_RENDER_VGRADIENT) {
|
||||
bind_font_texture();
|
||||
CustomGradientData *grad = (CustomGradientData *)custom->customData;
|
||||
Clay_Color tc = grad->top_color;
|
||||
Clay_Color bc = grad->bottom_color;
|
||||
@@ -789,6 +814,20 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
custom->cornerRadius.topLeft, custom->cornerRadius.topRight,
|
||||
custom->cornerRadius.bottomRight, custom->cornerRadius.bottomLeft,
|
||||
1.0f);
|
||||
} else if (type == CUSTOM_RENDER_ICON) {
|
||||
bind_icon_texture();
|
||||
CustomIconData *icon = (CustomIconData *)custom->customData;
|
||||
Clay_Color c = icon->color;
|
||||
float cr = c.r / 255.f, cg = c.g / 255.f;
|
||||
float cb = c.b / 255.f, ca = c.a / 255.f;
|
||||
UI_IconInfo *info = &g_icons[icon->icon_id];
|
||||
emit_quad(&batch,
|
||||
bb.x, bb.y, bb.x + bb.width, bb.y + bb.height,
|
||||
info->u0, info->v0, info->u1, info->v1,
|
||||
cr, cg, cb, ca,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 1.0f);
|
||||
}
|
||||
}
|
||||
} break;
|
||||
@@ -816,6 +855,20 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
r->frame_index++;
|
||||
}
|
||||
|
||||
void renderer_create_icon_atlas(Renderer *r, const uint8_t *data, int32_t w, int32_t h) {
|
||||
MTLTextureDescriptor *tex_desc = [[MTLTextureDescriptor alloc] init];
|
||||
tex_desc.pixelFormat = MTLPixelFormatR8Unorm;
|
||||
tex_desc.width = w;
|
||||
tex_desc.height = h;
|
||||
tex_desc.usage = MTLTextureUsageShaderRead;
|
||||
|
||||
r->icon_texture = [r->device newTextureWithDescriptor:tex_desc];
|
||||
[r->icon_texture replaceRegion:MTLRegionMake2D(0, 0, w, h)
|
||||
mipmapLevel:0
|
||||
withBytes:data
|
||||
bytesPerRow:w];
|
||||
}
|
||||
|
||||
void renderer_set_clear_color(Renderer *r, float cr, float cg, float cb) {
|
||||
r->clear_r = cr;
|
||||
r->clear_g = cg;
|
||||
|
||||
@@ -113,6 +113,7 @@ static inline uint16_t uifs(float x) { return (uint16_t)(x * g_ui_scale + 0.5f);
|
||||
|
||||
enum CustomRenderType {
|
||||
CUSTOM_RENDER_VGRADIENT = 1,
|
||||
CUSTOM_RENDER_ICON = 2,
|
||||
};
|
||||
|
||||
struct CustomGradientData {
|
||||
@@ -121,6 +122,12 @@ struct CustomGradientData {
|
||||
Clay_Color bottom_color;
|
||||
};
|
||||
|
||||
struct CustomIconData {
|
||||
CustomRenderType type; // CUSTOM_RENDER_ICON
|
||||
S32 icon_id;
|
||||
Clay_Color color;
|
||||
};
|
||||
|
||||
////////////////////////////////
|
||||
// Font sizes
|
||||
|
||||
|
||||
76
src/ui/ui_icons.cpp
Normal file
76
src/ui/ui_icons.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
// ui_icons.cpp - SVG icon rasterization via lunasvg
|
||||
|
||||
#include "ui/ui_icons.h"
|
||||
#include <lunasvg.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
UI_IconInfo g_icons[UI_ICON_COUNT] = {};
|
||||
|
||||
// Simple SVG icon sources (24x24 viewBox)
|
||||
static const char *g_icon_svgs[UI_ICON_COUNT] = {
|
||||
// UI_ICON_CLOSE - X mark
|
||||
R"(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M6 6 L18 18 M18 6 L6 18" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>)",
|
||||
|
||||
// UI_ICON_CHECK - checkmark
|
||||
R"(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M5 12 L10 17 L19 7" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>)",
|
||||
|
||||
// UI_ICON_CHEVRON_DOWN - downward arrow
|
||||
R"(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M7 10 L12 15 L17 10" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>)",
|
||||
};
|
||||
|
||||
U8 *ui_icons_rasterize_atlas(S32 *out_w, S32 *out_h, S32 icon_size) {
|
||||
// Pack icons in a row
|
||||
S32 atlas_w = icon_size * UI_ICON_COUNT;
|
||||
S32 atlas_h = icon_size;
|
||||
|
||||
// Pad to power of 2 isn't necessary for correctness, but ensure minimum size
|
||||
if (atlas_w < 64) atlas_w = 64;
|
||||
if (atlas_h < 64) atlas_h = 64;
|
||||
|
||||
U8 *atlas = (U8 *)calloc(atlas_w * atlas_h, 1);
|
||||
if (!atlas) return nullptr;
|
||||
|
||||
S32 pen_x = 0;
|
||||
for (S32 i = 0; i < UI_ICON_COUNT; i++) {
|
||||
auto doc = lunasvg::Document::loadFromData(g_icon_svgs[i]);
|
||||
if (!doc) continue;
|
||||
|
||||
lunasvg::Bitmap bmp = doc->renderToBitmap(icon_size, icon_size);
|
||||
if (bmp.isNull()) continue;
|
||||
|
||||
// Extract alpha channel from ARGB32 premultiplied into R8
|
||||
U8 *src = bmp.data();
|
||||
S32 bmp_w = bmp.width();
|
||||
S32 bmp_h = bmp.height();
|
||||
S32 stride = bmp.stride();
|
||||
|
||||
for (S32 y = 0; y < bmp_h && y < atlas_h; y++) {
|
||||
for (S32 x = 0; x < bmp_w && (pen_x + x) < atlas_w; x++) {
|
||||
// ARGB32 premultiplied: bytes are B, G, R, A (little-endian)
|
||||
U8 a = src[y * stride + x * 4 + 3];
|
||||
atlas[y * atlas_w + pen_x + x] = a;
|
||||
}
|
||||
}
|
||||
|
||||
// Store UV and pixel info
|
||||
g_icons[i].u0 = (F32)pen_x / (F32)atlas_w;
|
||||
g_icons[i].v0 = 0.0f;
|
||||
g_icons[i].u1 = (F32)(pen_x + bmp_w) / (F32)atlas_w;
|
||||
g_icons[i].v1 = (F32)bmp_h / (F32)atlas_h;
|
||||
g_icons[i].w = (F32)bmp_w;
|
||||
g_icons[i].h = (F32)bmp_h;
|
||||
|
||||
pen_x += icon_size;
|
||||
}
|
||||
|
||||
*out_w = atlas_w;
|
||||
*out_h = atlas_h;
|
||||
return atlas;
|
||||
}
|
||||
23
src/ui/ui_icons.h
Normal file
23
src/ui/ui_icons.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
// ui_icons.h - SVG icon definitions and atlas rasterization via lunasvg
|
||||
|
||||
#include "base/base_inc.h"
|
||||
|
||||
enum UI_IconID {
|
||||
UI_ICON_CLOSE,
|
||||
UI_ICON_CHECK,
|
||||
UI_ICON_CHEVRON_DOWN,
|
||||
UI_ICON_COUNT
|
||||
};
|
||||
|
||||
struct UI_IconInfo {
|
||||
F32 u0, v0, u1, v1; // UV coordinates in icon atlas
|
||||
F32 w, h; // pixel dimensions at rasterized size
|
||||
};
|
||||
|
||||
extern UI_IconInfo g_icons[UI_ICON_COUNT];
|
||||
|
||||
// Rasterizes all icons into an R8 atlas bitmap.
|
||||
// Returns malloc'd data (caller frees). Sets *out_w, *out_h to atlas dimensions.
|
||||
// icon_size is the pixel height to rasterize each icon at.
|
||||
U8 *ui_icons_rasterize_atlas(S32 *out_w, S32 *out_h, S32 icon_size);
|
||||
@@ -10,6 +10,11 @@
|
||||
|
||||
UI_WidgetState g_wstate = {};
|
||||
|
||||
// Icon per-frame pool (forward declaration for begin_frame)
|
||||
#define UI_MAX_ICONS_PER_FRAME 32
|
||||
static CustomIconData g_icon_pool[UI_MAX_ICONS_PER_FRAME];
|
||||
static S32 g_icon_pool_count = 0;
|
||||
|
||||
void ui_widgets_init() {
|
||||
g_wstate = {};
|
||||
}
|
||||
@@ -17,6 +22,7 @@ void ui_widgets_init() {
|
||||
void ui_widgets_begin_frame(PlatformInput input) {
|
||||
g_wstate.input = input;
|
||||
g_wstate.mouse_clicked = (input.mouse_down && !input.was_mouse_down);
|
||||
g_icon_pool_count = 0;
|
||||
g_wstate.cursor_blink += 1.0f / 60.0f;
|
||||
g_wstate.text_input_count = 0;
|
||||
g_wstate.tab_pressed = 0;
|
||||
@@ -96,6 +102,26 @@ static Clay_String clay_str(const char *s) {
|
||||
#define WID(s) CLAY_SID(clay_str(s))
|
||||
#define WIDI(s, i) CLAY_SIDI(clay_str(s), i)
|
||||
|
||||
////////////////////////////////
|
||||
// Icon
|
||||
|
||||
void ui_icon(UI_IconID icon, F32 size, Clay_Color color) {
|
||||
if (g_icon_pool_count >= UI_MAX_ICONS_PER_FRAME) return;
|
||||
|
||||
S32 idx = g_icon_pool_count;
|
||||
CustomIconData *data = &g_icon_pool[g_icon_pool_count++];
|
||||
data->type = CUSTOM_RENDER_ICON;
|
||||
data->icon_id = (S32)icon;
|
||||
data->color = color;
|
||||
|
||||
CLAY(CLAY_IDI("UIIcon", idx),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_FIXED(size), .height = CLAY_SIZING_FIXED(size) },
|
||||
},
|
||||
.custom = { .customData = data }
|
||||
) {}
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Label
|
||||
|
||||
@@ -168,7 +194,7 @@ B32 ui_checkbox(const char *id, const char *label, B32 *value) {
|
||||
.border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } }
|
||||
) {
|
||||
if (*value) {
|
||||
CLAY_TEXT(CLAY_STRING("x"), &g_widget_text_config);
|
||||
ui_icon(UI_ICON_CHECK, WIDGET_CHECKBOX_SIZE * 0.75f, g_theme.text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,11 +733,11 @@ B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected)
|
||||
|
||||
CLAY(WIDI(id, 501),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_FIXED(uis(20)), .height = CLAY_SIZING_FIT() },
|
||||
.childAlignment = { .x = CLAY_ALIGN_X_CENTER },
|
||||
.sizing = { .width = CLAY_SIZING_FIXED(uis(20)), .height = CLAY_SIZING_FIXED(uis(20)) },
|
||||
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
|
||||
}
|
||||
) {
|
||||
CLAY_TEXT(CLAY_STRING("v"), &g_widget_text_config_dim);
|
||||
ui_icon(UI_ICON_CHEVRON_DOWN, uis(12), g_theme.text_dim);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1078,7 +1104,7 @@ B32 ui_window(const char *id, const char *title, B32 *open,
|
||||
.backgroundColor = close_bg,
|
||||
.cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS_SM)
|
||||
) {
|
||||
CLAY_TEXT(CLAY_STRING("x"), &g_widget_text_config_btn);
|
||||
ui_icon(UI_ICON_CLOSE, uis(12), g_theme.button_text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// like which text field is focused or which dropdown is open.
|
||||
|
||||
#include "ui/ui_core.h"
|
||||
#include "ui/ui_icons.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
////////////////////////////////
|
||||
@@ -89,6 +90,9 @@ void ui_text_input_reset_display_bufs();
|
||||
// Widgets
|
||||
// All IDs must be unique string literals (passed to CLAY_ID internally).
|
||||
|
||||
// Icon element (rendered via icon atlas)
|
||||
void ui_icon(UI_IconID icon, F32 size, Clay_Color color);
|
||||
|
||||
// Simple label
|
||||
void ui_label(const char *id, const char *text);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user