nuklear的设计与源码分析
   7 分钟阅读    邵晨峰

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);
}