[ui] Add functions for flow-based automatic layout

This should be suitable for laying out text objects with word-wrap,
where each view is a "word" or break between "words". This should be
useful for any other objects that could benefit from similar layout
rules. All eight flows are supported left-right-top-down (English and
most European languages), right-left-top-down (Arabic and similar),
top-down-right-left (Chinese, Japanese, Korean), top-down-left-right,
as well as bottom-up variants of those four.

More work is needed for support of things like views being centered on
the flow line rather than on one edge (depends on flow direction),
offset views, and others. Suppression of "spaces" at the beginning of a
line is supported but not tested.
This commit is contained in:
Bill Currie 2022-09-29 23:54:12 +09:00
parent 31945c6e01
commit 378584f41d
4 changed files with 511 additions and 0 deletions

View file

@ -64,6 +64,7 @@ typedef enum {
grav_southwest, ///< +ve X right, +ve Y up, -X left, -ve Y down
grav_west, ///< +ve X right, +ve Y down, -X left, -ve Y up
grav_northwest, ///< +ve X right, +ve Y down, -X left, -ve Y up
grav_flow, ///< controlled by view_flow
} grav_t;
extern struct exprtype_s grav_t_type;
@ -96,6 +97,7 @@ struct view_s {
unsigned visible:1; ///< If false, view_draw() skips this view.
unsigned resize_x:1; ///< If true, view's width follows parent's.
unsigned resize_y:1; ///< If true, view's height follows parent's.
unsigned bol_suppress:1; ///< If true, view_flow skips at start of line.
view_t *parent; ///< The parent view.
view_t **children; ///< The child views.
int num_children; ///< Number of child views in view.
@ -254,6 +256,15 @@ void view_setgeometry (view_t *view, int xp, int yp, int xl, int yl);
*/
void view_setgravity (view_t *view, grav_t grav);
void view_flow_right_down (view_t *view);
void view_flow_right_up (view_t *view);
void view_flow_left_down (view_t *view);
void view_flow_left_up (view_t *view);
void view_flow_down_right (view_t *view);
void view_flow_up_right (view_t *view);
void view_flow_down_left (view_t *view);
void view_flow_up_left (view_t *view);
///@}
#endif//__QF_ui_view_h

View file

@ -1,4 +1,5 @@
libs_ui_tests = \
libs/ui/test/test-flow \
libs/ui/test/test-txtbuffer \
libs/ui/test/test-vrect
@ -6,6 +7,10 @@ TESTS += $(libs_ui_tests)
check_PROGRAMS += $(libs_ui_tests)
libs_ui_test_test_flow_SOURCES=libs/ui/test/test-flow.c
libs_ui_test_test_flow_LDADD=libs/ui/libQFui.la
libs_ui_test_test_flow_DEPENDENCIES=libs/ui/libQFui.la
libs_ui_test_test_txtbuffer_SOURCES=libs/ui/test/test-txtbuffer.c
libs_ui_test_test_txtbuffer_LDADD=libs/ui/libQFui.la
libs_ui_test_test_txtbuffer_DEPENDENCIES=libs/ui/libQFui.la

284
libs/ui/test/test-flow.c Normal file
View file

@ -0,0 +1,284 @@
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <stdio.h>
#include "QF/ui/view.h"
typedef struct {
struct {
int xlen, ylen;
};
int bol_suppress;
struct {
struct {
int xpos, ypos;
};
struct {
int xrel, yrel;
};
struct {
int xabs, yabs;
};
} expect;
} testdata_t;
#define array_size(array) (sizeof (array) / sizeof(array[0]))
static testdata_t right_down_views[] = {
{{ 48, 8}, 0, { { 0, 0}, { 0, 0}, { 8, 16} }}, // 0
{{128, 8}, 0, { { 48, 0}, { 48, 0}, { 56, 16} }},
{{ 64, 8}, 0, { {176, 0}, {176, 0}, {184, 16} }},
{{ 32, 8}, 0, { { 0, 0}, { 0, 8}, { 8, 24} }}, // 3
{{224, 8}, 0, { { 32, 0}, { 32, 8}, { 40, 24} }},
{{ 64, 8}, 0, { { 0, 0}, { 0, 16}, { 8, 32} }}, // 5
{{ 64, 8}, 0, { { 64, 0}, { 64, 16}, { 72, 32} }},
{{ 64, 8}, 0, { {128, 0}, {128, 16}, {136, 32} }},
{{ 32, 8}, 0, { {192, 0}, {192, 16}, {200, 32} }},
{{288, 8}, 0, { { 0, 0}, { 0, 24}, { 8, 40} }}, // 9
{{ 48, 8}, 0, { { 0, 0}, { 0, 32}, { 8, 48} }}, // 10
{{128, 8}, 0, { { 48, 0}, { 48, 32}, { 56, 48} }},
{{ 64, 8}, 0, { {176, 0}, {176, 32}, {184, 48} }},
};
#define right_down_count array_size(right_down_views)
static testdata_t right_up_views[] = {
{{ 48, 8}, 0, { { 0, 0}, { 0,248}, { 8,264} }}, // 0
{{128, 8}, 0, { { 48, 0}, { 48,248}, { 56,264} }},
{{ 64, 8}, 0, { {176, 0}, {176,248}, {184,264} }},
{{ 32, 8}, 0, { { 0, 0}, { 0,240}, { 8,256} }}, // 3
{{224, 8}, 0, { { 32, 0}, { 32,240}, { 40,256} }},
{{ 64, 8}, 0, { { 0, 0}, { 0,232}, { 8,248} }}, // 5
{{ 64, 8}, 0, { { 64, 0}, { 64,232}, { 72,248} }},
{{ 64, 8}, 0, { {128, 0}, {128,232}, {136,248} }},
{{ 32, 8}, 0, { {192, 0}, {192,232}, {200,248} }},
{{288, 8}, 0, { { 0, 0}, { 0,224}, { 8,240} }}, // 9
{{ 48, 8}, 0, { { 0, 0}, { 0,216}, { 8,232} }}, // 10
{{128, 8}, 0, { { 48, 0}, { 48,216}, { 56,232} }},
{{ 64, 8}, 0, { {176, 0}, {176,216}, {184,232} }},
};
#define right_up_count array_size(right_up_views)
static testdata_t left_down_views[] = {
{{ 48, 8}, 0, { {208, 0}, {208, 0}, {216, 16} }}, // 0
{{128, 8}, 0, { { 80, 0}, { 80, 0}, { 88, 16} }},
{{ 64, 8}, 0, { { 16, 0}, { 16, 0}, { 24, 16} }},
{{ 32, 8}, 0, { {224, 0}, {224, 8}, {232, 24} }}, // 3
{{224, 8}, 0, { { 0, 0}, { 0, 8}, { 8, 24} }},
{{ 64, 8}, 0, { {192, 0}, {192, 16}, {200, 32} }}, // 5
{{ 64, 8}, 0, { {128, 0}, {128, 16}, {136, 32} }},
{{ 64, 8}, 0, { { 64, 0}, { 64, 16}, { 72, 32} }},
{{ 32, 8}, 0, { { 32, 0}, { 32, 16}, { 40, 32} }},
{{288, 8}, 0, { {-32, 0}, {-32, 24}, {-24, 40} }}, // 9
{{ 48, 8}, 0, { {208, 0}, {208, 32}, {216, 48} }}, // 10
{{128, 8}, 0, { { 80, 0}, { 80, 32}, { 88, 48} }},
{{ 64, 8}, 0, { { 16, 0}, { 16, 32}, { 24, 48} }},
};
#define left_down_count array_size(left_down_views)
static testdata_t left_up_views[] = {
{{ 48, 8}, 0, { {208, 0}, {208,248}, {216,264} }}, // 0
{{128, 8}, 0, { { 80, 0}, { 80,248}, { 88,264} }},
{{ 64, 8}, 0, { { 16, 0}, { 16,248}, { 24,264} }},
{{ 32, 8}, 0, { {224, 0}, {224,240}, {232,256} }}, // 3
{{224, 8}, 0, { { 0, 0}, { 0,240}, { 8,256} }},
{{ 64, 8}, 0, { {192, 0}, {192,232}, {200,248} }}, // 5
{{ 64, 8}, 0, { {128, 0}, {128,232}, {136,248} }},
{{ 64, 8}, 0, { { 64, 0}, { 64,232}, { 72,248} }},
{{ 32, 8}, 0, { { 32, 0}, { 32,232}, { 40,248} }},
{{288, 8}, 0, { {-32, 0}, {-32,224}, {-24,240} }}, // 9
{{ 48, 8}, 0, { {208, 0}, {208,216}, {216,232} }}, // 10
{{128, 8}, 0, { { 80, 0}, { 80,216}, { 88,232} }},
{{ 64, 8}, 0, { { 16, 0}, { 16,216}, { 24,232} }},
};
#define left_up_count array_size(left_up_views)
static testdata_t down_right_views[] = {
{{ 8, 48}, 0, { { 0, 0}, { 0, 0}, { 8, 16} }}, // 0
{{ 8,128}, 0, { { 0, 48}, { 0, 48}, { 8, 64} }},
{{ 8, 64}, 0, { { 0,176}, { 0,176}, { 8,192} }},
{{ 8, 32}, 0, { { 0, 0}, { 8, 0}, { 16, 16} }}, // 3
{{ 8,224}, 0, { { 0, 32}, { 8, 32}, { 16, 48} }},
{{ 8, 64}, 0, { { 0, 0}, { 16, 0}, { 24, 16} }}, // 5
{{ 8, 64}, 0, { { 0, 64}, { 16, 64}, { 24, 80} }},
{{ 8, 64}, 0, { { 0,128}, { 16,128}, { 24,144} }},
{{ 8, 32}, 0, { { 0,192}, { 16,192}, { 24,208} }},
{{ 8,288}, 0, { { 0, 0}, { 24, 0}, { 32, 16} }}, // 9
{{ 8, 48}, 0, { { 0, 0}, { 32, 0}, { 40, 16} }}, // 10
{{ 8,128}, 0, { { 0, 48}, { 32, 48}, { 40, 64} }},
{{ 8, 64}, 0, { { 0,176}, { 32,176}, { 40,192} }},
};
#define down_right_count array_size(down_right_views)
static testdata_t up_right_views[] = {
{{ 8, 48}, 0, { { 0,208}, { 0,208}, { 8,224} }}, // 0
{{ 8,128}, 0, { { 0, 80}, { 0, 80}, { 8, 96} }},
{{ 8, 64}, 0, { { 0, 16}, { 0, 16}, { 8, 32} }},
{{ 8, 32}, 0, { { 0,224}, { 8,224}, { 16,240} }}, // 3
{{ 8,224}, 0, { { 0, 0}, { 8, 0}, { 16, 16} }},
{{ 8, 64}, 0, { { 0,192}, { 16,192}, { 24,208} }}, // 5
{{ 8, 64}, 0, { { 0,128}, { 16,128}, { 24,144} }},
{{ 8, 64}, 0, { { 0, 64}, { 16, 64}, { 24, 80} }},
{{ 8, 32}, 0, { { 0, 32}, { 16, 32}, { 24, 48} }},
{{ 8,288}, 0, { { 0,-32}, { 24,-32}, { 32,-16} }}, // 9
{{ 8, 48}, 0, { { 0,208}, { 32,208}, { 40,224} }}, // 10
{{ 8,128}, 0, { { 0, 80}, { 32, 80}, { 40, 96} }},
{{ 8, 64}, 0, { { 0, 16}, { 32, 16}, { 40, 32} }},
};
#define up_right_count array_size(up_right_views)
static testdata_t down_left_views[] = {
{{ 8, 48}, 0, { { 0, 0}, {248, 0}, {256, 16} }}, // 0
{{ 8,128}, 0, { { 0, 48}, {248, 48}, {256, 64} }},
{{ 8, 64}, 0, { { 0,176}, {248,176}, {256,192} }},
{{ 8, 32}, 0, { { 0, 0}, {240, 0}, {248, 16} }}, // 3
{{ 8,224}, 0, { { 0, 32}, {240, 32}, {248, 48} }},
{{ 8, 64}, 0, { { 0, 0}, {232, 0}, {240, 16} }}, // 5
{{ 8, 64}, 0, { { 0, 64}, {232, 64}, {240, 80} }},
{{ 8, 64}, 0, { { 0,128}, {232,128}, {240,144} }},
{{ 8, 32}, 0, { { 0,192}, {232,192}, {240,208} }},
{{ 8,288}, 0, { { 0, 0}, {224, 0}, {232, 16} }}, // 9
{{ 8, 48}, 0, { { 0, 0}, {216, 0}, {224, 16} }}, // 10
{{ 8,128}, 0, { { 0, 48}, {216, 48}, {224, 64} }},
{{ 8, 64}, 0, { { 0,176}, {216,176}, {224,192} }},
};
#define down_left_count array_size(down_left_views)
static testdata_t up_left_views[] = {
{{ 8, 48}, 0, { { 0,208}, {248,208}, {256,224} }}, // 0
{{ 8,128}, 0, { { 0, 80}, {248, 80}, {256, 96} }},
{{ 8, 64}, 0, { { 0, 16}, {248, 16}, {256, 32} }},
{{ 8, 32}, 0, { { 0,224}, {240,224}, {248,240} }}, // 3
{{ 8,224}, 0, { { 0, 0}, {240, 0}, {248, 16} }},
{{ 8, 64}, 0, { { 0,192}, {232,192}, {240,208} }}, // 5
{{ 8, 64}, 0, { { 0,128}, {232,128}, {240,144} }},
{{ 8, 64}, 0, { { 0, 64}, {232, 64}, {240, 80} }},
{{ 8, 32}, 0, { { 0, 32}, {232, 32}, {240, 48} }},
{{ 8,288}, 0, { { 0,-32}, {224,-32}, {232,-16} }}, // 9
{{ 8, 48}, 0, { { 0,208}, {216,208}, {224,224} }}, // 10
{{ 8,128}, 0, { { 0, 80}, {216, 80}, {224, 96} }},
{{ 8, 64}, 0, { { 0, 16}, {216, 16}, {224, 32} }},
};
#define up_left_count array_size(up_left_views)
static void
print_view (view_t *view)
{
printf ("%s[%3d %3d %3d %3d %3d %3d %3d %3d]\n",
view->parent ? " " : "****",
view->xpos, view->ypos, view->xlen, view->ylen,
view->xrel, view->yrel, view->xabs, view->yabs);
view_draw (view);
}
static int
test_flow (testdata_t *child_views, int count, void (*flow) (view_t *))
{
view_t *flow_view = view_new (0, 0, 256, 256, grav_northwest);
flow_view->setgeometry = flow;
flow_view->draw = print_view;
for (int i = 0; i < count; i++) {
testdata_t *td = &child_views[i];
view_t *child = view_new (0, 0, td->xlen, td->ylen, grav_flow);
child->bol_suppress = td->bol_suppress;
child->draw = print_view;
view_add (flow_view, child);
}
view_move (flow_view, 8, 16);
int ret = 0;
for (int i = 0; i < flow_view->num_children; i++) {
testdata_t *td = &child_views[i];
view_t *child = flow_view->children[i];
if (child->xpos != td->expect.xpos
|| child->ypos != td->expect.ypos
|| child->xrel != td->expect.xrel
|| child->yrel != td->expect.yrel
|| child->xabs != td->expect.xabs
|| child->yabs != td->expect.yabs) {
ret = 1;
printf ("child %d misflowed:\n"
" [%3d %3d %3d %3d %3d %3d]\n", i,
td->expect.xpos, td->expect.ypos,
td->expect.xrel, td->expect.yrel,
td->expect.xabs, td->expect.yabs);
print_view (child);
}
}
return ret;
}
int
main (void)
{
int ret = 0;
if (test_flow (right_down_views, right_down_count, view_flow_right_down)) {
printf ("right-down failed\n");
ret = 1;
}
if (test_flow (right_up_views, right_up_count, view_flow_right_up)) {
printf ("right-up failed\n");
ret = 1;
}
if (test_flow (left_down_views, left_down_count, view_flow_left_down)) {
printf ("left-down failed\n");
ret = 1;
}
if (test_flow (left_up_views, left_up_count, view_flow_left_up)) {
printf ("left-up failed\n");
ret = 1;
}
if (test_flow (down_right_views, down_right_count, view_flow_down_right)) {
printf ("down-right failed\n");
ret = 1;
}
if (test_flow (up_right_views, up_right_count, view_flow_up_right)) {
printf ("up-right failed\n");
ret = 1;
}
if (test_flow (down_left_views, down_left_count, view_flow_down_left)) {
printf ("down-left failed\n");
ret = 1;
}
if (test_flow (up_left_views, up_left_count, view_flow_up_left)) {
printf ("up-left failed\n");
ret = 1;
}
return ret;
}

View file

@ -40,6 +40,7 @@
#include <stdlib.h>
#include "QF/cexpr.h"
#include "QF/mathlib.h"
#include "QF/ui/view.h"
static exprenum_t grav_t_enum;
@ -133,6 +134,8 @@ setgeometry (view_t *view)
view->xrel = view->xpos;
view->yrel = view->ypos;
break;
case grav_flow:
break;
}
view->xabs = par->xabs + view->xrel;
view->yabs = par->yabs + view->yrel;
@ -284,3 +287,211 @@ view_setgravity (view_t *view, grav_t grav)
view->gravity = grav;
setgeometry (view);
}
typedef struct flowline_s {
struct flowline_s *next;
int first_child;
int child_count;
int cursor;
int height;
} flowline_t;
#define NEXT_LINE(line, child_index) \
do { \
line->next = alloca (sizeof (flowline_t)); \
memset (line->next, 0, sizeof (flowline_t)); \
line = line->next; \
line->first_child = child_index; \
} while (0)
static void
flow_right (view_t *view, void (*set_rows) (view_t *, flowline_t *))
{
flowline_t flowline = {};
flowline_t *line = &flowline;
for (int i = 0; i < view->num_children; i++) {
view_t *child = view->children[i];
if (line->cursor && line->cursor + child->xlen > view->xlen) {
NEXT_LINE(line, i);
}
child->xpos = line->cursor;
if (child->xpos || !child->bol_suppress) {
line->cursor += child->xlen;
}
line->height = max (child->ylen, line->height);
line->child_count++;
}
set_rows (view, &flowline);
}
static void
flow_left (view_t *view, void (*set_rows) (view_t *, flowline_t *))
{
flowline_t flowline = {};
flowline_t *line = &flowline;
line->cursor = view->xlen;
for (int i = 0; i < view->num_children; i++) {
view_t *child = view->children[i];
if (line->cursor < view->xlen && line->cursor - child->xlen < 0) {
NEXT_LINE(line, i);
line->cursor = view->xlen;
}
if (child->xpos < view->xlen || !child->bol_suppress) {
line->cursor -= child->xlen;
}
child->xpos = line->cursor;
line->height = max (child->ylen, line->height);
line->child_count++;
}
set_rows (view, &flowline);
}
static void
flow_down (view_t *view, void (*set_rows) (view_t *, flowline_t *))
{
flowline_t flowline = {};
flowline_t *line = &flowline;
for (int i = 0; i < view->num_children; i++) {
view_t *child = view->children[i];
if (line->cursor && line->cursor + child->ylen > view->ylen) {
NEXT_LINE(line, i);
}
child->ypos = line->cursor;
if (child->ypos || !child->bol_suppress) {
line->cursor += child->ylen;
}
line->height = max (child->xlen, line->height);
line->child_count++;
}
set_rows (view, &flowline);
}
static void
flow_up (view_t *view, void (*set_rows) (view_t *, flowline_t *))
{
flowline_t flowline = {};
flowline_t *line = &flowline;
line->cursor = view->ylen;
for (int i = 0; i < view->num_children; i++) {
view_t *child = view->children[i];
if (line->cursor < view->ylen && line->cursor - child->ylen < 0) {
NEXT_LINE(line, i);
line->cursor = view->ylen;
}
if (child->ypos < view->ylen || !child->bol_suppress) {
line->cursor -= child->ylen;
}
child->ypos = line->cursor;
line->height = max (child->xlen, line->height);
line->child_count++;
}
set_rows (view, &flowline);
}
static void
set_rows_down (view_t *view, flowline_t *flowlines)
{
int cursor = 0;
for (flowline_t *line = flowlines; line; line = line->next) {
cursor += line->height;
for (int i = 0; i < line->child_count; i++) {
view_t *child = view->children[line->first_child + i];
child->xrel = child->xpos;
child->yrel = cursor + child->ypos - child->ylen;
}
}
}
static void
set_rows_up (view_t *view, flowline_t *flowlines)
{
int cursor = view->ylen;
for (flowline_t *line = flowlines; line; line = line->next) {
for (int i = 0; i < line->child_count; i++) {
view_t *child = view->children[line->first_child + i];
child->xrel = child->xpos;
child->yrel = cursor + child->ypos - child->ylen;
}
cursor -= line->height;
}
}
static void
set_columns_right (view_t *view, flowline_t *flowlines)
{
int cursor = 0;
for (flowline_t *line = flowlines; line; line = line->next) {
for (int i = 0; i < line->child_count; i++) {
view_t *child = view->children[line->first_child + i];
child->xrel = cursor + child->xpos;
child->yrel = child->ypos;
}
cursor += line->height;
}
}
static void
set_columns_left (view_t *view, flowline_t *flowlines)
{
int cursor = view->xlen;
for (flowline_t *line = flowlines; line; line = line->next) {
cursor -= line->height;
for (int i = 0; i < line->child_count; i++) {
view_t *child = view->children[line->first_child + i];
child->xrel = cursor + child->xpos;
child->yrel = child->ypos;
}
}
}
VISIBLE void
view_flow_right_down (view_t *view)
{
flow_right (view, set_rows_down);
}
VISIBLE void
view_flow_right_up (view_t *view)
{
flow_right (view, set_rows_up);
}
VISIBLE void
view_flow_left_down (view_t *view)
{
flow_left (view, set_rows_down);
}
VISIBLE void
view_flow_left_up (view_t *view)
{
flow_left (view, set_rows_up);
}
VISIBLE void
view_flow_down_right (view_t *view)
{
flow_down (view, set_columns_right);
}
VISIBLE void
view_flow_up_right (view_t *view)
{
flow_up (view, set_columns_right);
}
VISIBLE void
view_flow_down_left (view_t *view)
{
flow_down (view, set_columns_left);
}
VISIBLE void
view_flow_up_left (view_t *view)
{
flow_up (view, set_columns_left);
}