#include "flow_grid.h" #include 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; }