JustUI/src/jlayout_box.c

350 lines
9.6 KiB
C

#include <justui/jlayout.h>
#include <justui/jwidget.h>
#include <justui/jwidget-api.h>
#include "jlayout_p.h"
#include "util.h"
#include <openlibm.h>
//---
// Flexbox-like box layout
//---
jlayout_box *jlayout_get_hbox(void *w0)
{
J_CAST(w)
return (w->layout == J_LAYOUT_HBOX) ? &w->layout_box : NULL;
}
jlayout_box *jlayout_get_vbox(void *w0)
{
J_CAST(w)
return (w->layout == J_LAYOUT_VBOX) ? &w->layout_box : NULL;
}
jlayout_box *jlayout_set_hbox(void *w0)
{
J_CAST(w)
w->layout = J_LAYOUT_HBOX;
jlayout_box *l = &w->layout_box;
l->spacing = 0;
return l;
}
jlayout_box *jlayout_set_vbox(void *w0)
{
J_CAST(w)
w->layout = J_LAYOUT_VBOX;
jlayout_box *l = &w->layout_box;
l->spacing = 0;
return l;
}
void jlayout_box_csize(void *w0)
{
/* Compute as if the layout is vertical first */
J_CAST(w)
jlayout_box *l = &w->layout_box;
int horiz = (w->layout == J_LAYOUT_HBOX);
int main = 0;
int cross = 0;
for(int k = 0; k < w->child_count; k++) {
jwidget *child = w->children[k];
if(!child->visible) continue;
jwidget_msize(child);
main += (k > 0 ? l->spacing : 0) + (horiz ? child->w : child->h);
cross = max(cross, (horiz ? child->h : child->w));
}
if(horiz) {
w->w = main;
w->h = cross;
}
else {
w->w = cross;
w->h = main;
}
}
//---
// Distribution system
//
// The basic idea of space redistribution is to give each widget extra space
// proportional to their stretch rates in the relevant direction. However, the
// addition of maximum size constraints means that widgets can decline some of
// the extra space being allocated.
//
// This system defines the result of expansion as a function of the "expansion
// factor". As the expansion factor increases, every widget stretches at a
// speed proportional to its stretch rate, until it reaches its maximum size.
//
// Extra widget size
// |
// + .-------- Maximum size
// | .`
// | .` <- Slope: widget stretch rate
// |.`
// 0 +-------+------> Expansion factor
// 0 ^
// Breaking point
//
// The extra space allocated to widgets is the sum of this function for every
// widget considered for expansion. Since every widget has a possibly different
// breaking point, a maximal interval of expansion factor that has no breaking
// point is called a "run". During each run, the slope for the total space
// remains constant, and a unit of expansion factor corresponds to one pixel
// being allocated in the container. Thus, whenever the expansion factor
// increases of (slope), every widget (w) gets (w->stretch) new pixels.
//
// The functions below simulate the expansion by determining the breaking
// points of the widgets and allocating extra space during each run. Once the
// total extra space allocated reaches the available space, simulation stops
// and the allocation is recorded by assigning actual size to widgets.
//---
/* This "expansion" structure tracks information relating to a single child
widget during the space distribution process. */
typedef struct {
/* Child index */
uint8_t id;
/* Stretch rate, sum of stretch rates is the "slope" */
uint8_t stretch;
/* Maximum size augmentation */
int16_t max;
/* Extra space allocate in the previous runs, in pixels */
float allocated;
/* Breaking point for the current run, as a number of pixels to distribute
to the whole system */
float breaking_point;
} exp_t;
/* Determine whether a widget can expand any further. */
static bool can_expand(exp_t *e)
{
return (e->stretch > 0 && e->allocated < e->max);
}
/* Compute the slope for the current run. */
static uint compute_slope(exp_t elements[], size_t n)
{
uint slope = 0;
for(size_t i = 0; i < n; i++) {
if(can_expand(&elements[i])) slope += elements[i].stretch;
}
return slope;
}
/* Compute the breaking point for every expanding widget. Returns the amount of
pixels to allocate in order to reach the next breaking point. */
static float compute_breaking_points(exp_t elements[], size_t n, uint slope)
{
float closest = HUGE_VALF;
for(size_t i = 0; i < n; i++) {
exp_t *e = &elements[i];
if(!can_expand(e)) continue;
/* Up to (e->max - e->allocated) pixels can be added to this widget.
With the factor of (slope / e->stretch), we get the number of pixels
to add to the container in order to reach the threshold. */
e->breaking_point = (e->max - e->allocated) * (slope / e->stretch);
closest = min(e->breaking_point, closest);
}
return closest;
}
/* Allocate floating-point space to widgets. This is the core of the
distribution system, it produces (e->allocated) for every element. */
static void allocate_space(exp_t elements[], size_t n, float available)
{
/* One iteration per run */
while(available > 0) {
/* Slope for this run; if zero, no more widget can grow */
uint slope = compute_slope(elements, n);
if(!slope) break;
/* Closest breaking point, amount of space to distribute this run */
float breaking = compute_breaking_points(elements, n, slope);
float run_budget = min(breaking, available);
/* Give everyone their share of run_budget */
for(size_t i = 0; i < n; i++) {
exp_t *e = &elements[i];
if(!can_expand(e)) continue;
e->allocated += (run_budget * e->stretch) / slope;
/* Avoid floating-point errors when reaching the maximum size: test
(e->breaking) instead of (e->allocated), and assign (e->max) to
eliminate risks of rounding down */
if(e->breaking_point == run_budget) e->allocated = e->max;
}
available -= run_budget;
}
}
/* Stable insertion sort: order children by decreasing fractional allocation */
static void sort_by_fractional_allocation(exp_t elements[], size_t n)
{
for(size_t spot = 0; spot < n - 1; spot++) {
/* Find the element with the max fractional value in [spot..size] */
float max_frac = 0;
int max_frac_who = -1;
for(size_t i = spot; i < n; i++) {
exp_t *e = &elements[i];
float frac = e->allocated - floorf(e->allocated);
if(max_frac_who < 0 || frac > max_frac) {
max_frac = frac;
max_frac_who = i;
}
}
/* Give that element the spot */
exp_t temp = elements[spot];
elements[spot] = elements[max_frac_who];
elements[max_frac_who] = temp;
}
}
/* Round allocations so that they add up to the available space */
static void round_allocations(exp_t elements[], size_t n, int available_space)
{
/* Prepare to give everyone the floor of their allocation */
for(size_t i = 0; i < n; i++) {
exp_t *e = &elements[i];
available_space -= floorf(e->allocated);
}
/* Sort by decreasing fractional allocation then add one extra pixel to
the (available_space) children with highest fractional allocation */
sort_by_fractional_allocation(elements, n);
for(size_t i = 0; i < n; i++) {
exp_t *e = &elements[i];
e->allocated = floorf(e->allocated);
if(can_expand(e) && (int)i < available_space) e->allocated += 1;
}
}
void jlayout_box_apply(void *w0)
{
J_CAST(w)
jlayout_box const *l = &w->layout_box;
int horiz = (w->layout == J_LAYOUT_HBOX);
if(!w->child_count) return;
/* Content width and height */
int cw = jwidget_content_width(w);
int ch = jwidget_content_height(w);
/* Allocatable width and height (which excludes spacing) */
int total_spacing = (w->child_count - 1) * l->spacing;
int aw = cw - (horiz ? total_spacing : 0);
int ah = ch - (horiz ? 0 : total_spacing);
/* Length along the main axis, including spacing */
int length = 0;
/* Expanding widgets' information for extra space distribution */
size_t n = w->child_count;
exp_t elements[n];
bool has_started = false;
for(size_t i = 0; i < w->child_count; i++) {
jwidget *child = w->children[i];
/* Maximum size to enforce: this is the acceptable size closest to our
allocatable size */
int max_w = clamp(aw, child->min_w, child->max_w);
int max_h = clamp(ah, child->min_h, child->max_h);
/* Start by setting every child to an acceptable size */
child->w = clamp(child->w, child->min_w, max_w);
child->h = clamp(child->h, child->min_h, max_h);
/* Initialize expanding widgets' information */
elements[i].id = i;
elements[i].allocated = 0.0f;
elements[i].breaking_point = -1.0f;
/* Determine natural length along the container, and stretch child
along the perpendicular direction if possible */
if(!child->visible) {
elements[i].stretch = 0;
elements[i].max = 0;
continue;
}
if(has_started) length += l->spacing;
if(horiz) {
length += child->w;
if(child->stretch_y > 0) child->h = max_h;
elements[i].stretch = child->stretch_x;
elements[i].max = max(max_w - child->w, 0);
if(child->stretch_force && child->stretch_x > 0)
elements[i].max = max(aw - child->w, 0);
}
else {
length += child->h;
if(child->stretch_x > 0) child->w = max_w;
elements[i].stretch = child->stretch_y;
elements[i].max = max(max_h - child->h, 0);
if(child->stretch_force && child->stretch_y > 0)
elements[i].max = max(ah - child->h, 0);
}
has_started = true;
}
/* Distribute extra space along the line */
int extra_space = (horiz ? cw : ch) - length;
allocate_space(elements, n, extra_space);
round_allocations(elements, n, extra_space);
/* Update widgets for extra space */
for(size_t i = 0; i < n; i++) {
exp_t *e = &elements[i];
jwidget *child = w->children[e->id];
if(!child->visible) continue;
if(horiz)
child->w += e->allocated;
else
child->h += e->allocated;
}
/* Position everyone */
int position = 0;
for(size_t i = 0; i < n; i++) {
jwidget *child = w->children[i];
if(!child->visible) continue;
if(horiz) {
child->x = position;
child->y = (ch - child->h) / 2;
position += child->w + l->spacing;
}
else {
child->x = (cw - child->w) / 2;
child->y = position;
position += child->h + l->spacing;
}
}
}