nuklear的设计与源码分析
更新于 2020年2月 源作者已不再维护,请谨慎选择
最近在逛Github时发现了一个单文件跨平台零依赖的图形库nuklear,这使我产生了兴趣,一个大约两万行的程序,实现了很不错的效果,更神奇的是它是零依赖的,这意味着可以运用到类似单片机等特殊环境,于是我果断Fork了它开始了学习之路zoollcar/nuklear,下面我们分析下nuklear图形库的源代码
综述
首先看一下官方提供的demo(win gui版),后面会一部分一部分的分析
/* win32 实现细节*/
while (running)
{
/* 输入部分 */
/* win32 实现细节 */
nk_input_begin(ctx);
/* input 细节 */
nk_input_end(ctx);
/* GUI 部分 */
if (nk_begin(ctx, "Demo", nk_rect(50, 50, 200, 200),
NK_WINDOW_BORDER|NK_WINDOW_MOVABLE|NK_WINDOW_SCALABLE|
NK_WINDOW_CLOSABLE|NK_WINDOW_MINIMIZABLE|NK_WINDOW_TITLE))
{
enum {EASY, HARD};
static int op = EASY;
static int property = 20;
/* 固定小部件像素宽度(纵向布局) */
/* 创建小部件之前使用 nk_layout_xxx 来说明后面的小部件布局 */
nk_layout_row_static(ctx, 30/* 高度 */, 80/* 元素宽度 */, 1/* 个数 */);
if (nk_button_label(ctx, "button")){
/* 点击事件 */
fprintf(stdout, "button pressed\n");
}
/* 小部件横向动态布局(默认是纵向) */
nk_layout_row_dynamic(ctx, 30, 2);
if (nk_option_label(ctx, "easy", op == EASY)) op = EASY;
if (nk_option_label(ctx, "hard", op == HARD)) op = HARD;
nk_layout_row_dynamic(ctx, 22, 1);
nk_property_int(ctx, "Compression:", 0, &property, 100, 10, 1);
}
nk_end(ctx);
/* 绘制部分 */
nk_gdi_render(nk_rgb(30,30,30)); /* 平台专用渲染 */
}
这里去除了win32实现细节,因为它本身虽然是跨平台的,但并不是所有部分都跨平台
整个程序可以分为三个输入部分,GUI部分,绘制部分,他们的关系是这样的:
graph TD;
输入部分--输入信息-->GUI部分;
GUI部分--绘制命令-->绘制部分;
在 nuklear 中一个称为上下文的变量会贯穿整个程序,其类型为nk_context,其中记录了这个程序几乎所有的信息,包括输入结构体、整体样式、个个窗口、渲染命令缓冲区,它起着连接 nuklear 所有部分的功能
/* 上下文 */
struct nk_context {
struct nk_input input; /* 输入 */
struct nk_style style; /* 样式 */
struct nk_buffer memory; /* 缓冲区 */
struct nk_clipboard clip; /* 剪切板 */
nk_flags last_widget_state; /* 最近一个小部件状态 */
enum nk_button_behavior button_behavior; /* 按钮行为是默认还是被替换的 */
struct nk_configuration_stacks stacks;
float delta_time_seconds; /* 时间(秒) */
/* 其他细节 */
/* 窗口 */
int build;
int use_pool;
struct nk_pool pool;
struct nk_window *begin; /* 窗口链表 */
struct nk_window *end;
struct nk_window *active; /* 激活窗口 */
struct nk_window *current; /* 当前窗口 */
struct nk_page_element *freelist;
unsigned int count;
unsigned int seq;
};
输入部分
input API 负责组织一个由当前鼠标、按键和文本输入所组成的输入状态(nk_input结构体)。 nuklear 并没有直接与系统通信,有输入状态都必须由特定于平台的代码提供的。
nk_input_begin 会清空当前的状态,然后就可以使用nk_input_motion、nk_input_key、nk_input_button、nk_input_scroll、nk_input_char 等 这些输入函数将消息提供给nuklear,nuklear会将这些信息记录在nk_input结构体中。nk_input结构体 在 nk_context 主上下文中。
下面是nk_input的数据结构
/* nk_input结构体,分为键盘和鼠标 */
struct nk_input {
struct nk_keyboard keyboard;
struct nk_mouse mouse;
};
/* 鼠标结构体 */
struct nk_mouse {
struct nk_mouse_button buttons[NK_BUTTON_MAX];
struct nk_vec2 pos;
struct nk_vec2 prev;
struct nk_vec2 delta;
struct nk_vec2 scroll_delta;
unsigned char grab;
unsigned char grabbed;
unsigned char ungrab;
};
/* 键盘结构体 */
struct nk_keyboard {
struct nk_key keys[NK_KEY_MAX];
char text[NK_INPUT_MAX];
int text_len;
};
最后使用 nk_input_end 结束输入过程,这会固定下输入信息,知道下一帧中执行 nk_input_begin 。
GUI部分
这部分是 nuklear 中最精彩的部分,这部分是跨平台的,它会将这部分中注册的小部件(按钮,单选,文字等)转换为渲染部分需要的简单命令
总的来说,它的作用就是用尽可能简单的命令制作出美观的 UI 。
建立一个新窗口的方法是 使用 nk_begin 它会在上下文中新建一个窗口,然后的操作就都在这个窗口进行了
在向窗口加入小部件前需要先设定布局, nuklear 使用的是基于行的布局,使用 nk_layout_xxx 来为当前行设置布局,后面再加入的小部件就使用这个布局提供的位置和大小了。
放置小部件可以调用 nk_button_xxx 、 nk_option_xxx 等函数创建,他们会使用 nk_widget 从窗口的 layout 中得到下一个小部件的位置大小信息,然后将渲染信息加入到渲染命令缓冲区
最后使用 nk_end 结束建立这个窗口,可以建立下一个窗口了
具体来说, nk_begin 建立了一个新窗口, nk_layout_xxx 设置这个窗口的 layout 属性来控制下面小部件的位置和大小, nk_button 等建立小部件的函数使用从窗口中得到位置大小信息,然后处理加入到缓冲区
下面结合代码看一遍:
新建窗口
(为了效果进行了削减,突出重点):
/* 建立一个新窗口,分离的标题和标识符的窗口,允许出现具有相同名称但不同标题的多个窗口 */
NK_API int
nk_begin_titled(struct nk_context *ctx, const char *name, const char *title,
struct nk_rect bounds, nk_flags flags)
{
struct nk_window *win;
struct nk_style *style;
/* 此处省略对参数进行可行性检验的代码 */
/* 下面判断这个名字是否已经存在相同的窗口 */
/* 如果有更新窗口,否则新建窗口 */
style = &ctx->style;
title_len = (int)nk_strlen(name);
/* 将窗口名字计算为一个哈希值,比较哈希值比字符串比较快 */
title_hash = nk_murmur_hash(name, (int)title_len, NK_WINDOW_TITLE);
/* 寻找窗口,返回值为0表示没找到,返回值为一个nk_window指针说明找到了 */
win = nk_find_window(ctx, title_hash, name);
/* 下面创建或更新窗口 */
if (!win) {
/* 创建一个新窗口 create new window */
nk_size name_length = (nk_size)nk_strlen(name);
win = (struct nk_window*)nk_create_window(ctx);
NK_ASSERT(win);
if (!win) return 0;
/* 将窗口添加到 ctx */
if (flags & NK_WINDOW_BACKGROUND)
nk_insert_window(ctx, win, NK_INSERT_FRONT);/* 顶部添加 */
else nk_insert_window(ctx, win, NK_INSERT_BACK);/* 尾部添加 */
/* 初始化命令缓冲区 */
nk_command_buffer_init(&win->buffer, &ctx->memory, NK_CLIPPING_ON);
win->flags = flags;
win->bounds = bounds;
win->name = title_hash;
/* 如果窗口名字比最大长度长,会被截断 */
name_length = NK_MIN(name_length, NK_WINDOW_MAX_NAME-1);
NK_MEMCPY(win->name_string, name, name_length);
win->name_string[name_length] = 0;
/* 不是弹出式窗口 */
win->popup.win = 0;
if (!ctx->active)
ctx->active = win;
} else {
/* 省略更新窗口的代码 */
}
/* 省略后面会对鼠标点击后窗口的排列进行控制,大致就是点哪个哪个放到窗口链表顶端 */
win->layout = (struct nk_panel*)nk_create_panel(ctx);
ctx->current = win;
ret = nk_panel_begin(ctx, title, NK_PANEL_WINDOW);
win->layout->offset_x = &win->scrollbar.x;
win->layout->offset_y = &win->scrollbar.y;
return ret;
}
设置布局函数:
NK_LIB void
nk_row_layout(struct nk_context *ctx, enum nk_layout_format fmt,/* fmt 布局类型 */
float height, int cols, int width)
{
/* 更新当前行并设置当前行的布局 */
struct nk_window *win;
NK_ASSERT(ctx);
NK_ASSERT(ctx->current);
NK_ASSERT(ctx->current->layout);
if (!ctx || !ctx->current || !ctx->current->layout)
return;
win = ctx->current;
/* 创建横向 cols 个面板 高度是 height */
nk_panel_layout(ctx, win, height, cols); /* 在下面 */
/* 根据布局类型设置 */
if (fmt == NK_DYNAMIC)
win->layout->row.type = NK_LAYOUT_DYNAMIC_FIXED;/* 固定宽度 */
else win->layout->row.type = NK_LAYOUT_STATIC_FIXED;/* 动态宽度 */
win->layout->row.ratio = 0;
win->layout->row.filled = 0;
win->layout->row.item_offset = 0;
win->layout->row.item_width = (float)width;
}
NK_LIB void
nk_panel_layout(const struct nk_context *ctx, struct nk_window *win,
float height, int cols)
{
struct nk_panel *layout;
const struct nk_style *style;
struct nk_command_buffer *out;
struct nk_vec2 item_spacing;
struct nk_color color;
/* 省略对参数可行性检验 */
/* 先获得一些预设的配置数据 prefetch some configuration data */
layout = win->layout;
style = &ctx->style;
out = &win->buffer;
color = style->window.background;
item_spacing = style->window.spacing;
/* 更新当前行并设置当前行布局 update the current row and set the current row layout */
/* 通过前面获得的 layout 指针更改后面的小部件都会使用的 win->layout */
layout->row.index = 0;
layout->at_y += layout->row.height;
layout->row.columns = cols;
if (height == 0.0f)
layout->row.height = NK_MAX(height, layout->row.min_height) + item_spacing.y;
else layout->row.height = height + item_spacing.y;
layout->row.item_offset = 0;
if (layout->flags & NK_WINDOW_DYNAMIC) {
/* 为动态区域设置背景 draw background for dynamic panels */
struct nk_rect background;
background.x = win->bounds.x;
background.w = win->bounds.w;
background.y = layout->at_y - 1.0f;
background.h = layout->row.height + 1.0f;
nk_fill_rect(out, background, 0, color);
}
}
创建小部件:
/* 创建按钮 */
NK_API int
nk_button_text_styled(struct nk_context *ctx,
const struct nk_style_button *style, const char *title, int len)
{
struct nk_window *win;
struct nk_panel *layout;
const struct nk_input *in;
struct nk_rect bounds;
enum nk_widget_layout_states state;
/* 省略对参数可行性检验 */
/* 从 nk_context 中得到位置和大小的 nk_rect */
win = ctx->current;
layout = win->layout;
state = nk_widget(&bounds, ctx);
if (!state) return 0;
/* 生成按钮 */
in = (state == NK_WIDGET_ROM || layout->flags & NK_WINDOW_ROM) ? 0 : &ctx->input;
return nk_do_button_text(&ctx->last_widget_state, &win->buffer, bounds,
title, len, style->text_alignment, ctx->button_behavior,
style, in, ctx->style.font);
}
/* 生成按钮函数 */
NK_LIB int
nk_do_button_text(nk_flags *state,
struct nk_command_buffer *out, struct nk_rect bounds,
const char *string, int len, nk_flags align, enum nk_button_behavior behavior,
const struct nk_style_button *style, const struct nk_input *in,
const struct nk_user_font *font)
{
struct nk_rect content;
int ret = nk_false;
/* 省略对参数可行性检验 */
/* 计算和返回按钮点击事件 */
/* 这里的使用 nk_input 结构中的鼠标点击位置计算是否点击到按钮 */
/* 如果点击到按钮,会返回相应的点击事件,这个时间会一路返回到用户代码中的if判断 */
ret = nk_do_button(state, out, bounds, style, in, behavior, &content);
if (style->draw_begin) style->draw_begin(out, style->userdata);/* 绘制开始的回调 */
/* 绘制按钮,生成按钮文字 */
nk_draw_button_text(out, &bounds, &content, *state, style, string, len, align, font);
if (style->draw_end) style->draw_end(out, style->userdata);/* 绘制完成的回调 */
return ret;
}
/* 真正绘制按钮 外观 */
NK_LIB const struct nk_style_item*
nk_draw_button(struct nk_command_buffer *out,
const struct nk_rect *bounds, nk_flags state,
const struct nk_style_button *style)
{
const struct nk_style_item *background;
if (state & NK_WIDGET_STATE_HOVER)
background = &style->hover;
else if (state & NK_WIDGET_STATE_ACTIVED)
background = &style->active;
else background = &style->normal;
if (background->type == NK_STYLE_ITEM_IMAGE) {
/* 如果是图片背景,就绘制图片 */
nk_draw_image(out, *bounds, &background->data.image, nk_white);
} else {
/* 否则绘制矩形 */
nk_fill_rect(out, *bounds, style->rounding, background->data.color);
nk_stroke_rect(out, *bounds, style->rounding, style->border, style->border_color);
}
return background;
}
/* 绘制矩形 */
NK_API void
nk_fill_rect(struct nk_command_buffer *b, struct nk_rect rect,
float rounding, struct nk_color c)
{
struct nk_command_rect_filled *cmd;
/* 省略对参数可行性检验 */
/* 将绘制命令放入命令缓冲区中,先放入再设置属性 */
cmd = (struct nk_command_rect_filled*)
nk_command_buffer_push(b, NK_COMMAND_RECT_FILLED, sizeof(*cmd));
if (!cmd) return;
cmd->rounding = (unsigned short)rounding;
cmd->x = (short)rect.x;
cmd->y = (short)rect.y;
cmd->w = (unsigned short)NK_MAX(0, rect.w);
cmd->h = (unsigned short)NK_MAX(0, rect.h);
cmd->color = c;
}
/* 将命令加入到命令缓冲 所有的绘制命令都用这个 */
NK_LIB void*
nk_command_buffer_push(struct nk_command_buffer* b,
enum nk_command_type t, nk_size size)
{
NK_STORAGE const nk_size align = NK_ALIGNOF(struct nk_command);
struct nk_command *cmd;
nk_size alignment;
void *unaligned;
void *memory;
NK_ASSERT(b);
NK_ASSERT(b->base);
if (!b) return 0;
/* 新建一块缓冲区区域,用来将命令加入缓冲区 */
cmd = (struct nk_command*)nk_buffer_alloc(b->base,NK_BUFFER_FRONT,size,align);
if (!cmd) return 0;
/* 确保下一命令的偏移量是对齐的 make sure the offset to the next command is aligned */
b->last = (nk_size)((nk_byte*)cmd - (nk_byte*)b->base->memory.ptr);
unaligned = (nk_byte*)cmd + size;
memory = NK_ALIGN_PTR(unaligned, align);
alignment = (nk_size)((nk_byte*)memory - (nk_byte*)unaligned);
#ifdef NK_ZERO_COMMAND_MEMORY
NK_MEMSET(cmd, 0, size + alignment);
#endif
cmd->type = t;
cmd->next = b->base->allocated + alignment;
#ifdef NK_INCLUDE_COMMAND_USERDATA
cmd->userdata = b->userdata;
#endif
b->end = cmd->next;
/* 返回建好的新命令指针 */
return cmd;
}
渲染部分
这个库被设计为推给后端绘制 所以它不会直接在屏幕上绘制任何图形. 而是绘制形状、小部件等。 这会缓冲进内存并组成命令队列。 每一帧会有一个带有绘制命令的命令缓冲区,提供给后端 这些绘制命令需要用户在他们自己的后端绘制中实现 之后,命令缓冲区会被清除,并且开始启动一个新帧。
后端是用户编写的在自己设备上可以执行 nuklear 需要的最小命令集的一组函数
其中重点在于遍历GUI部分提供的命令队列,然后根据命令类型执行相应操作
下面看一下 nuklear 自带的在win32 GUI上的实现:
NK_API void
nk_gdi_render(struct nk_color clear)
{
const struct nk_command *cmd;
HDC memory_dc = gdi.memory_dc;
SelectObject(memory_dc, GetStockObject(DC_PEN));
SelectObject(memory_dc, GetStockObject(DC_BRUSH));
nk_gdi_clear(memory_dc, clear);
nk_foreach(cmd, &gdi.ctx)
{
switch (cmd->type) {
case NK_COMMAND_NOP: break;
case NK_COMMAND_SCISSOR: {
const struct nk_command_scissor *s =(const struct nk_command_scissor*)cmd;
nk_gdi_scissor(memory_dc, s->x, s->y, s->w, s->h);
} break;
case NK_COMMAND_LINE: {
const struct nk_command_line *l = (const struct nk_command_line *)cmd;
nk_gdi_stroke_line(memory_dc, l->begin.x, l->begin.y, l->end.x,
l->end.y, l->line_thickness, l->color);
} break;
/* 省略类似的处理 */
}
nk_gdi_blit(gdi.window_dc);
nk_clear(&gdi.ctx);
}