431 lines
12 KiB
C
431 lines
12 KiB
C
#include "flow_grid.h"
|
|
|
|
#include <math.h>
|
|
|
|
struct _FlowGrid {
|
|
GtkWidget parent_instance;
|
|
int minimum_size;
|
|
int column_spacing;
|
|
int row_spacing;
|
|
FlowGridJustify justify;
|
|
};
|
|
|
|
enum {
|
|
PROP_0,
|
|
PROP_MINIMUM_SIZE,
|
|
PROP_COLUMN_SPACING,
|
|
PROP_ROW_SPACING,
|
|
PROP_JUSTIFY,
|
|
LAST_PROP
|
|
};
|
|
|
|
static GParamSpec *props[LAST_PROP];
|
|
|
|
G_DEFINE_TYPE(FlowGrid, flow_grid, GTK_TYPE_WIDGET)
|
|
|
|
static void
|
|
flow_grid_set_property(GObject *object, guint prop_id,
|
|
const GValue *value, GParamSpec *pspec)
|
|
{
|
|
FlowGrid *self = FLOW_GRID(object);
|
|
switch (prop_id) {
|
|
case PROP_MINIMUM_SIZE:
|
|
flow_grid_set_minimum_size(self, g_value_get_int(value));
|
|
break;
|
|
case PROP_COLUMN_SPACING:
|
|
flow_grid_set_column_spacing(self, g_value_get_int(value));
|
|
break;
|
|
case PROP_ROW_SPACING:
|
|
flow_grid_set_row_spacing(self, g_value_get_int(value));
|
|
break;
|
|
case PROP_JUSTIFY:
|
|
flow_grid_set_justify(self, (FlowGridJustify)g_value_get_int(value));
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
flow_grid_get_property(GObject *object, guint prop_id,
|
|
GValue *value, GParamSpec *pspec)
|
|
{
|
|
FlowGrid *self = FLOW_GRID(object);
|
|
switch (prop_id) {
|
|
case PROP_MINIMUM_SIZE:
|
|
g_value_set_int(value, self->minimum_size);
|
|
break;
|
|
case PROP_COLUMN_SPACING:
|
|
g_value_set_int(value, self->column_spacing);
|
|
break;
|
|
case PROP_ROW_SPACING:
|
|
g_value_set_int(value, self->row_spacing);
|
|
break;
|
|
case PROP_JUSTIFY:
|
|
g_value_set_int(value, self->justify);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
flow_grid_dispose(GObject *object)
|
|
{
|
|
GtkWidget *widget = GTK_WIDGET(object);
|
|
GtkWidget *child;
|
|
while ((child = gtk_widget_get_first_child(widget)))
|
|
gtk_widget_unparent(child);
|
|
G_OBJECT_CLASS(flow_grid_parent_class)->dispose(object);
|
|
}
|
|
|
|
static int
|
|
flow_grid_count_visible_children(GtkWidget *widget)
|
|
{
|
|
int count = 0;
|
|
GtkWidget *child;
|
|
for (child = gtk_widget_get_first_child(widget);
|
|
child != NULL;
|
|
child = gtk_widget_get_next_sibling(child)) {
|
|
if (gtk_widget_should_layout(child))
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static void
|
|
flow_grid_collect_visible_children(GtkWidget *widget, GtkWidget **children, int max)
|
|
{
|
|
int i = 0;
|
|
GtkWidget *child;
|
|
for (child = gtk_widget_get_first_child(widget);
|
|
child != NULL && i < max;
|
|
child = gtk_widget_get_next_sibling(child)) {
|
|
if (gtk_widget_should_layout(child))
|
|
children[i++] = child;
|
|
}
|
|
}
|
|
|
|
static void
|
|
flow_grid_compute_layout(GtkWidget *widget, int for_size,
|
|
int *out_num_columns, int *out_num_rows,
|
|
double *out_column_width)
|
|
{
|
|
FlowGrid *self = FLOW_GRID(widget);
|
|
int n_children = flow_grid_count_visible_children(widget);
|
|
|
|
if (n_children == 0) {
|
|
*out_num_columns = 1;
|
|
*out_num_rows = 0;
|
|
*out_column_width = (double)self->minimum_size;
|
|
return;
|
|
}
|
|
|
|
int num_columns = (for_size + self->column_spacing)
|
|
/ (self->minimum_size + self->column_spacing);
|
|
num_columns = num_columns < 1 ? 1 : num_columns;
|
|
num_columns = num_columns > n_children ? n_children : num_columns;
|
|
|
|
double column_width;
|
|
if (n_children == num_columns)
|
|
column_width = (double)self->minimum_size;
|
|
else {
|
|
column_width = (for_size - self->column_spacing * (num_columns - 1))
|
|
/ (double)num_columns;
|
|
if (column_width < self->minimum_size)
|
|
column_width = self->minimum_size;
|
|
}
|
|
|
|
int num_rows = (int)ceil((double)n_children / num_columns);
|
|
|
|
*out_num_columns = num_columns;
|
|
*out_num_rows = num_rows;
|
|
*out_column_width = column_width;
|
|
}
|
|
|
|
static GtkSizeRequestMode
|
|
flow_grid_get_request_mode(GtkWidget *widget)
|
|
{
|
|
return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
|
|
}
|
|
|
|
static void
|
|
flow_grid_measure(GtkWidget *widget,
|
|
GtkOrientation orientation,
|
|
int for_size,
|
|
int *minimum,
|
|
int *natural,
|
|
int *minimum_baseline,
|
|
int *natural_baseline)
|
|
{
|
|
FlowGrid *self = FLOW_GRID(widget);
|
|
|
|
if (minimum_baseline)
|
|
*minimum_baseline = -1;
|
|
if (natural_baseline)
|
|
*natural_baseline = -1;
|
|
|
|
if (for_size <= 0) {
|
|
if (minimum) *minimum = 0;
|
|
if (natural) *natural = 0;
|
|
return;
|
|
}
|
|
|
|
int n_children = flow_grid_count_visible_children(widget);
|
|
if (n_children == 0) {
|
|
if (minimum) *minimum = 0;
|
|
if (natural) *natural = 0;
|
|
return;
|
|
}
|
|
|
|
GtkWidget **children = g_newa(GtkWidget*, n_children);
|
|
flow_grid_collect_visible_children(widget, children, n_children);
|
|
|
|
int num_columns, num_rows;
|
|
double column_width;
|
|
flow_grid_compute_layout(widget, for_size, &num_columns, &num_rows, &column_width);
|
|
|
|
int *min_row_heights = g_newa(int, num_rows);
|
|
int *nat_row_heights = g_newa(int, num_rows);
|
|
memset(min_row_heights, 0, num_rows * sizeof(int));
|
|
memset(nat_row_heights, 0, num_rows * sizeof(int));
|
|
|
|
for (int i = 0; i < n_children; i++) {
|
|
int cw = (int)(column_width + 0.5);
|
|
if (cw < 0) cw = -1;
|
|
|
|
int child_min = 0, child_nat = 0;
|
|
gtk_widget_measure(children[i], orientation, cw,
|
|
&child_min, &child_nat, NULL, NULL);
|
|
int row = i / num_columns;
|
|
if (child_min > min_row_heights[row])
|
|
min_row_heights[row] = child_min;
|
|
if (child_nat > nat_row_heights[row])
|
|
nat_row_heights[row] = child_nat;
|
|
}
|
|
|
|
int spacing = self->row_spacing * (num_rows - 1);
|
|
int total_min = 0, total_nat = 0;
|
|
for (int r = 0; r < num_rows; r++) {
|
|
total_min += min_row_heights[r];
|
|
total_nat += nat_row_heights[r];
|
|
}
|
|
total_min += spacing;
|
|
total_nat += spacing;
|
|
|
|
if (minimum) *minimum = total_min;
|
|
if (natural) *natural = total_nat > total_min ? total_nat : total_min;
|
|
}
|
|
|
|
static void
|
|
flow_grid_size_allocate(GtkWidget *widget,
|
|
int width, int height, int baseline)
|
|
{
|
|
FlowGrid *self = FLOW_GRID(widget);
|
|
|
|
int n_children = flow_grid_count_visible_children(widget);
|
|
if (n_children == 0)
|
|
return;
|
|
|
|
GtkWidget **children = g_newa(GtkWidget*, n_children);
|
|
flow_grid_collect_visible_children(widget, children, n_children);
|
|
|
|
int num_columns, num_rows;
|
|
double column_width;
|
|
flow_grid_compute_layout(widget, width, &num_columns, &num_rows, &column_width);
|
|
|
|
int *row_heights = g_newa(int, num_rows);
|
|
memset(row_heights, 0, num_rows * sizeof(int));
|
|
|
|
for (int i = 0; i < n_children; i++) {
|
|
int cw = (int)(column_width + 0.5);
|
|
if (cw < 0) cw = -1;
|
|
|
|
int child_min = 0;
|
|
gtk_widget_measure(children[i], GTK_ORIENTATION_VERTICAL, cw,
|
|
&child_min, NULL, NULL, NULL);
|
|
int row = i / num_columns;
|
|
if (child_min > row_heights[row])
|
|
row_heights[row] = child_min;
|
|
}
|
|
|
|
for (int i = 0; i < n_children; i++) {
|
|
int idx = i;
|
|
int column = idx % num_columns;
|
|
int row = idx / num_columns;
|
|
|
|
int x_offset = 0;
|
|
if (row + 1 == num_rows) {
|
|
int n_in_last_row = n_children % num_columns;
|
|
if (n_in_last_row == 0)
|
|
n_in_last_row = num_columns;
|
|
double row_width = column_width * n_in_last_row
|
|
+ self->column_spacing * (n_in_last_row - 1);
|
|
double empty_space = width - row_width;
|
|
switch (self->justify) {
|
|
case FLOW_GRID_JUSTIFY_CENTER:
|
|
x_offset = (int)(empty_space / 2.0 + 0.5);
|
|
break;
|
|
case FLOW_GRID_JUSTIFY_END:
|
|
x_offset = (int)(empty_space + 0.5);
|
|
break;
|
|
default:
|
|
x_offset = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
int y_offset = 0;
|
|
for (int r = 0; r < row; r++)
|
|
y_offset += row_heights[r];
|
|
|
|
int x = x_offset
|
|
+ (int)(column * column_width + 0.5)
|
|
+ column * self->column_spacing;
|
|
int y = y_offset + row * self->row_spacing;
|
|
|
|
int child_w = (int)(column_width + 0.5);
|
|
int child_h = row_heights[row];
|
|
|
|
GtkAllocation alloc = {
|
|
.x = x,
|
|
.y = y,
|
|
.width = child_w,
|
|
.height = child_h,
|
|
};
|
|
gtk_widget_size_allocate(children[i], &alloc, -1);
|
|
}
|
|
}
|
|
|
|
static void
|
|
flow_grid_class_init(FlowGridClass *klass)
|
|
{
|
|
GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
|
|
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
|
|
|
|
gobject_class->set_property = flow_grid_set_property;
|
|
gobject_class->get_property = flow_grid_get_property;
|
|
gobject_class->dispose = flow_grid_dispose;
|
|
|
|
widget_class->get_request_mode = flow_grid_get_request_mode;
|
|
widget_class->measure = flow_grid_measure;
|
|
widget_class->size_allocate = flow_grid_size_allocate;
|
|
|
|
props[PROP_MINIMUM_SIZE] = g_param_spec_int(
|
|
"minimum-size", NULL, NULL,
|
|
1, G_MAXINT, 200,
|
|
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
|
|
|
|
props[PROP_COLUMN_SPACING] = g_param_spec_int(
|
|
"column-spacing", NULL, NULL,
|
|
0, G_MAXINT, 0,
|
|
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
|
|
|
|
props[PROP_ROW_SPACING] = g_param_spec_int(
|
|
"row-spacing", NULL, NULL,
|
|
0, G_MAXINT, 0,
|
|
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
|
|
|
|
props[PROP_JUSTIFY] = g_param_spec_int(
|
|
"justify", NULL, NULL,
|
|
0, 2, 0,
|
|
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
|
|
|
|
g_object_class_install_properties(gobject_class, LAST_PROP, props);
|
|
|
|
gtk_widget_class_set_css_name(widget_class, "flowgrid");
|
|
gtk_widget_class_set_accessible_role(widget_class, GTK_ACCESSIBLE_ROLE_GROUP);
|
|
}
|
|
|
|
static void
|
|
flow_grid_init(FlowGrid *self)
|
|
{
|
|
self->minimum_size = 200;
|
|
self->column_spacing = 0;
|
|
self->row_spacing = 0;
|
|
self->justify = FLOW_GRID_JUSTIFY_START;
|
|
}
|
|
|
|
GtkWidget *
|
|
flow_grid_new(int minimum_size, int column_spacing, int row_spacing)
|
|
{
|
|
return g_object_new(FLOW_TYPE_GRID,
|
|
"minimum-size", minimum_size,
|
|
"column-spacing", column_spacing,
|
|
"row-spacing", row_spacing,
|
|
NULL);
|
|
}
|
|
|
|
void
|
|
flow_grid_set_minimum_size(FlowGrid *self, int size)
|
|
{
|
|
g_return_if_fail(FLOW_IS_GRID(self));
|
|
if (self->minimum_size == size)
|
|
return;
|
|
self->minimum_size = size;
|
|
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_MINIMUM_SIZE]);
|
|
gtk_widget_queue_resize(GTK_WIDGET(self));
|
|
}
|
|
|
|
int
|
|
flow_grid_get_minimum_size(FlowGrid *self)
|
|
{
|
|
g_return_val_if_fail(FLOW_IS_GRID(self), 200);
|
|
return self->minimum_size;
|
|
}
|
|
|
|
void
|
|
flow_grid_set_column_spacing(FlowGrid *self, int spacing)
|
|
{
|
|
g_return_if_fail(FLOW_IS_GRID(self));
|
|
if (self->column_spacing == spacing)
|
|
return;
|
|
self->column_spacing = spacing;
|
|
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_COLUMN_SPACING]);
|
|
gtk_widget_queue_resize(GTK_WIDGET(self));
|
|
}
|
|
|
|
int
|
|
flow_grid_get_column_spacing(FlowGrid *self)
|
|
{
|
|
g_return_val_if_fail(FLOW_IS_GRID(self), 0);
|
|
return self->column_spacing;
|
|
}
|
|
|
|
void
|
|
flow_grid_set_row_spacing(FlowGrid *self, int spacing)
|
|
{
|
|
g_return_if_fail(FLOW_IS_GRID(self));
|
|
if (self->row_spacing == spacing)
|
|
return;
|
|
self->row_spacing = spacing;
|
|
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ROW_SPACING]);
|
|
gtk_widget_queue_resize(GTK_WIDGET(self));
|
|
}
|
|
|
|
int
|
|
flow_grid_get_row_spacing(FlowGrid *self)
|
|
{
|
|
g_return_val_if_fail(FLOW_IS_GRID(self), 0);
|
|
return self->row_spacing;
|
|
}
|
|
|
|
void
|
|
flow_grid_set_justify(FlowGrid *self, FlowGridJustify justify)
|
|
{
|
|
g_return_if_fail(FLOW_IS_GRID(self));
|
|
if (self->justify == justify)
|
|
return;
|
|
self->justify = justify;
|
|
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_JUSTIFY]);
|
|
gtk_widget_queue_resize(GTK_WIDGET(self));
|
|
}
|
|
|
|
FlowGridJustify
|
|
flow_grid_get_justify(FlowGrid *self)
|
|
{
|
|
g_return_val_if_fail(FLOW_IS_GRID(self), FLOW_GRID_JUSTIFY_START);
|
|
return self->justify;
|
|
}
|