Skip to content

Instantly share code, notes, and snippets.

@memononen
Created December 13, 2021 20:03
Show Gist options
  • Save memononen/fc11737ee78bbc5aeee0cb95c1c564b8 to your computer and use it in GitHub Desktop.
Save memononen/fc11737ee78bbc5aeee0cb95c1c564b8 to your computer and use it in GitHub Desktop.
#include "milayout.h"
#include <stdlib.h>
#include <stdarg.h>
static float mi__minf(float a, float b) { return a < b ? a : b; }
static float mi__maxf(float a, float b) { return a > b ? a : b; }
static int mi__getGrow(MIbox* box, int main)
{
return box->content.xy[main] > 0 ? box->grow : (box->grow | 1);
}
void miMeasureContent(MIbox* box)
{
MIsize contained = { .width = 0.0f, .height = 0.0f };
int main = box->layout.dir;
int cross = box->layout.dir ^ 1;
for (MIbox* child = box->child; child; child = child->next) {
miMeasureContent(child);
}
for (MIbox* child = box->child; child; child = child->next) {
contained.xy[main] += child->rect.size.xy[main] + (child->next ? box->layout.spacing : 0);
contained.xy[cross] = mi__maxf(contained.xy[cross], child->rect.size.xy[cross]);
}
box->rect.width = (box->content.width > 0.0f ? box->content.width : contained.width) + box->layout.pad.x * 2;
box->rect.height = (box->content.height > 0.0f ? box->content.height : contained.height) + box->layout.pad.y * 2;
}
static void mi__layoutBoxes(MIbox* box)
{
int main = box->layout.dir;
int cross = box->layout.dir ^ 1;
float sizeSum = 0.0f;
int growSum = 0;
int numChild = 0;
for (MIbox* child = box->child; child; child = child->next) {
growSum += mi__getGrow(child, main);
sizeSum += child->rect.size.xy[main];
if (child->next) sizeSum += box->layout.spacing;
numChild++;
}
if (!numChild)
return;
MIpoint orig = {
.x = box->rect.x + box->layout.pad.x,
.y = box->rect.y + box->layout.pad.y
};
MIsize inner = {
.width = mi__maxf(0, box->rect.width - box->layout.pad.x * 2),
.height = mi__maxf(0, box->rect.height - box->layout.pad.y * 2)
};
float avail = inner.xy[main] - sizeSum;
float scale = 1.0f;
float grow = 0.0f;
if (avail >= 0) {
// Extra space, figure out how to distribute
if (box->layout.pack == MI_FILL) {
// Grow or scale proportionally to fit.
if (growSum > 0)
grow = avail / (float)growSum;
else
scale = sizeSum > 0.f ? inner.xy[main] / sizeSum : 0.0f;
} else if (box->layout.pack == MI_START) {
orig.xy[main] += 0;
} else if (box->layout.pack == MI_CENTER) {
orig.xy[main] += avail/2;
} else if (box->layout.pack == MI_END) {
orig.xy[main] += avail;
}
} else {
// Shrink to fit
if (box->layout.overflow == MI_FIT) {
scale = sizeSum > 0.f ? inner.xy[main] / sizeSum : 0.0f;
}
}
for (MIbox* child = box->child; child; child = child->next) {
child->rect.pos = orig;
// Align cross
if (child->rect.size.xy[cross] >= inner.xy[cross]) {
child->rect.size.xy[cross] = inner.xy[cross];
} else {
if (box->layout.align == MI_FILL)
child->rect.size.xy[cross] = inner.xy[cross];
else if (box->layout.align == MI_END)
child->rect.pos.xy[cross] += inner.xy[cross] - child->rect.size.xy[cross];
else if (box->layout.align == MI_CENTER)
child->rect.pos.xy[cross] += inner.xy[cross]/2 - child->rect.size.xy[cross]/2;
}
// Pack main
int childGrow = mi__getGrow(child, main);
if (avail >= 0 && childGrow > 0 && grow > 0.0f)
child->rect.size.xy[main] += (float)childGrow * grow;
else
child->rect.size.xy[main] *= scale;
orig.xy[main] += child->rect.size.xy[main] + box->layout.spacing;
mi__layoutBoxes(child);
}
}
void miBoxAddChildrenArgs(MIbox* box, ...)
{
// Find tail of the child list
MIbox* prev = NULL;
for (MIbox* child = box->child; child; child = child->next)
prev = child;
// Add all children
va_list list;
va_start(list, box);
MIbox* child = va_arg(list, MIbox*);
while (child != NULL) {
if (prev)
prev->next = child;
else
box->child = child;
prev = child;
child = va_arg(list, MIbox*);
}
va_end(list);
}
void miBoxAddChild(MIbox* box, MIbox* newChild)
{
MIbox* prev = NULL;
for (MIbox* child = box->child; child; child = child->next)
prev = child;
if (prev)
prev->next = newChild;
else
box->child = newChild;
}
static void mi__boxOffset(MIbox* box, MIpoint delta)
{
box->rect.x += delta.x;
box->rect.y += delta.y;
for (MIbox* child = box->child; child; child = child->next)
mi__boxOffset(child, delta);
}
void miBoxMoveTo(MIbox* box, MIpoint pos)
{
MIpoint delta = { .x = pos.x - box->rect.x, .y = pos.y - box->rect.y };
box->rect.x = pos.x;
box->rect.y = pos.y;
for (MIbox* child = box->child; child; child = child->next)
mi__boxOffset(child, delta);
}
void miBoxLayout(MIbox* box)
{
miMeasureContent(box);
mi__layoutBoxes(box);
}
void miBoxLayoutToRect(MIbox* box, MIrect rect, MIlayout layout)
{
MIbox* savedNext = box->next;
box->next = NULL;
MIbox container = {
.rect = rect,
.content.width = mi__maxf(0.0f, rect.width - layout.pad.x*2),
.content.height = mi__maxf(0.0f, rect.height - layout.pad.y*2),
.layout = layout,
.child = box
};
miBoxLayout(&container);
box->next = savedNext;
}
void miRectCut(MIrect* rect, MIbox* item, MIlayout layout)
{
int main = layout.dir;
int cross = layout.dir ^ 1;
MIpoint orig = {
.x = rect->x + layout.pad.x,
.y = rect->y + layout.pad.y
};
MIsize inner = {
.width = mi__maxf(0, rect->width - layout.pad.x*2),
.height = mi__maxf(0, rect->height - layout.pad.y*2)
};
// Align
if (layout.align == MI_FILL)
item->rect.size.xy[cross] = inner.xy[cross];
else if (layout.align == MI_END)
orig.xy[cross] += inner.xy[cross] - item->rect.size.xy[cross];
else if (layout.align == MI_CENTER)
orig.xy[cross] += inner.xy[cross]/2 - item->rect.size.xy[cross]/2;
// Pack
float advance = item->rect.size.xy[main] + layout.spacing;
if (layout.pack == MI_END) {
orig.xy[main] += inner.xy[main] - item->rect.size.xy[main];
rect->pos.xy[main] += mi__minf(0, rect->size.xy[main] - advance);
rect->size.xy[main] = mi__maxf(0, rect->size.xy[main] - advance);
} else { // Threat everything else as pack start
rect->pos.xy[main] += advance;
rect->size.xy[main] = mi__maxf(0, rect->size.xy[main] - advance);
}
miBoxMoveTo(item, orig);
}
void miRectCutAndLayout(MIrect* rect, MIbox* box, MIlayout layout)
{
miBoxLayoutToRect(box, *rect, layout);
miRectCut(rect, box, layout);
}
MIrect miRectInset(MIrect rect, MIsize inset)
{
inset.x = mi__minf(inset.x, rect.width*0.5f);
inset.y = mi__minf(inset.y, rect.height*0.5f);
return (MIrect){
.x = rect.x + inset.x,
.y = rect.y + inset.y,
.width = rect.width - inset.x*2,
.height = rect.height - inset.y*2
};
}
MIrect miRectOutset(MIrect rect, MIsize outset)
{
return (MIrect){
.x = rect.x - outset.x,
.y = rect.y - outset.y,
.width = rect.width + outset.x*2,
.height = rect.height + outset.y*2
};
}
MIrect miRectIntersect(MIrect rectA, MIrect rectB)
{
float minx = mi__maxf(rectA.x, rectB.x);
float miny = mi__maxf(rectA.y, rectB.y);
float maxx = mi__minf(rectA.x + rectA.width, rectB.x + rectB.width);
float maxy = mi__minf(rectA.y + rectA.height, rectB.y + rectB.height);
return (MIrect){
.x = minx,
.y = miny,
.width = mi__maxf(0.0f, maxx - minx),
.height = mi__maxf(0.0f, maxy - miny)
};
}
static float mi__align(float size, int align)
{
if (align == MI_CENTER)
return size * 0.5f;
else if (align == MI_END)
return size;
return 0.0f;
}
MIpoint miRectCorner(MIrect rect, int alignx, int aligny)
{
return (MIpoint) {
.x = rect.x + mi__align(rect.width, alignx),
.y = rect.y + mi__align(rect.height, aligny)
};
}
MIpoint miPointAdd(MIpoint a, MIpoint b)
{
return (MIpoint) {
.x = a.x + b.x,
.y = a.y + b.y
};
}
#ifndef MILAYOUT_H
#define MILAYOUT_H
enum MIdirection {
MI_ROW = 0,
MI_COL,
};
enum MIpackAndAlign {
MI_FILL = 0,
MI_START,
MI_CENTER,
MI_END,
};
enum MIoverflow {
MI_FIT = 0,
MI_SCROLL,
};
struct MIpoint {
union {
struct { float x, y; };
struct { float xy[2]; };
};
};
typedef struct MIpoint MIpoint;
struct MIsize {
union {
struct { float x, y; };
struct { float width, height; };
struct { float xy[2]; };
};
};
typedef struct MIsize MIsize;
struct MIrect {
union {
struct { float x, y, width, height; };
struct { MIpoint pos; MIsize size; };
};
};
typedef struct MIrect MIrect;
struct MIlayout {
MIsize pad;
float spacing;
unsigned char dir, align, pack, overflow;
};
typedef struct MIlayout MIlayout;
struct MIbox {
MIsize content;
unsigned char grow;
MIlayout layout;
MIrect rect;
struct MIbox* child;
struct MIbox* next;
};
typedef struct MIbox MIbox;
#define miBoxAddChildren(box, ...) miBoxAddChildrenArgs(box, __VA_ARGS__, NULL)
void miBoxAddChildrenArgs(MIbox* box, ...);
void miBoxAddChild(MIbox* box, MIbox* newChild);
void miBoxMoveTo(MIbox* box, MIpoint pos);
void miMeasureContent(MIbox* box);
void miBoxLayout(MIbox* box);
void miBoxLayoutToRect(MIbox* box, MIrect rect, MIlayout layout);
void miRectCut(MIrect* rect, MIbox* box, MIlayout layout);
void miRectCutAndLayout(MIrect* rect, MIbox* box, MIlayout layout);
MIrect miRectInset(MIrect rect, MIsize inset);
MIrect miRectOutset(MIrect rect, MIsize outset);
MIrect miRectIntersect(MIrect rectA, MIrect rectB);
MIpoint miRectCorner(MIrect rect, int alignx, int aligny);
MIpoint miPointAdd(MIpoint a, MIpoint b);
#endif // MILAYOUT_H
// Menu Layout
MIitem item1 = { .text="Item 1", .detail="Alt+Shift+Space" };
MIitem item2 = { .flags=MI_ITEM_CHECKED, .icon=ICON_EMOJI_PEOPLE, .text="People", .detail="Alt+P" };
MIitem item3 = { .flags=MI_ITEM_SUBMENU, .icon=ICON_EMAIL, .text="Email" };
MIbox itemSearchCont = { .layout.dir = MI_ROW, .layout.spacing = 4, };
MIbox itemSearchIcon = { .content = miMeasureIcon() };
MIbox itemSearchInput = {};
miBoxAddChildren(&itemSearchCont, &itemSearchIcon, &itemSearchInput);
MIbox itemBox1 = { .content = miMeasureItem(item1) };
MIbox itemBox2 = { .content = miMeasureItem(item2) };
MIbox itemBox3 = { .content = miMeasureItem(item3) };
MIbox menuCont = {
.rect.pos = miRectCorner(button3.rect, MI_START, MI_END),
.layout.dir = MI_COL, .layout.pack = MI_START,
.layout.spacing = 4, .layout.pad.x = 6, .layout.pad.y = 6,
};
miBoxAddChildren(&menuCont, &itemSearchCont, &itemBox1, &itemBox2, &itemBox3);
miBoxLayout(&menuCont);
// Menu logic
MIhandle panelHandle = miPanelBegin(MI_PANEL_POPUP, menuCont.rect);
miIcon(itemSearchIcon.rect, ICON_SEARCH);
MIhandle menuInput = miInput(itemSearchInput.rect, (MIinput){.text=text, .maxText=sizeof(text)});
if (miBlurred(menuInput))
miPopupHide(menu);
miItem(itemBox1.rect, item1);
miItem(itemBox2.rect, item2);
MIhandle item3Handle = miItem(itemBox3.rect, item3);
miPanelEnd(panelHandle);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment