Implement AspectContainer and FlowGrid
This commit is contained in:
parent
3280c51fa5
commit
3a54e96d8b
11 changed files with 1160 additions and 47 deletions
|
|
@ -52,9 +52,17 @@ let package = Package(
|
|||
.product(name: "Logging", package: "swift-log"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "CAspectContainer",
|
||||
dependencies: [
|
||||
.product(name: "CAdw", package: "adwaita-swift")
|
||||
]
|
||||
),
|
||||
|
||||
.target(
|
||||
name: "LuminateUI",
|
||||
dependencies: [
|
||||
"CAspectContainer",
|
||||
"LuminateCore",
|
||||
"LuminateDI",
|
||||
"LuminateObservationMacros",
|
||||
|
|
|
|||
251
Sources/CAspectContainer/aspect_container.c
Normal file
251
Sources/CAspectContainer/aspect_container.c
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
#include "aspect_container.h"
|
||||
|
||||
struct _AspectContainer {
|
||||
GtkWidget parent_instance;
|
||||
float aspect_ratio;
|
||||
int max_width;
|
||||
};
|
||||
|
||||
enum {
|
||||
PROP_0,
|
||||
PROP_ASPECT_RATIO,
|
||||
PROP_MAX_WIDTH,
|
||||
LAST_PROP
|
||||
};
|
||||
|
||||
static GParamSpec *props[LAST_PROP];
|
||||
|
||||
G_DEFINE_TYPE(AspectContainer, aspect_container, GTK_TYPE_WIDGET)
|
||||
|
||||
static void
|
||||
aspect_container_set_property(GObject *object, guint prop_id,
|
||||
const GValue *value, GParamSpec *pspec)
|
||||
{
|
||||
AspectContainer *self = ASPECT_CONTAINER(object);
|
||||
switch (prop_id) {
|
||||
case PROP_ASPECT_RATIO:
|
||||
aspect_container_set_aspect_ratio(self, g_value_get_float(value));
|
||||
break;
|
||||
case PROP_MAX_WIDTH:
|
||||
aspect_container_set_max_width(self, g_value_get_int(value));
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
aspect_container_get_property(GObject *object, guint prop_id,
|
||||
GValue *value, GParamSpec *pspec)
|
||||
{
|
||||
AspectContainer *self = ASPECT_CONTAINER(object);
|
||||
switch (prop_id) {
|
||||
case PROP_ASPECT_RATIO:
|
||||
g_value_set_float(value, self->aspect_ratio);
|
||||
break;
|
||||
case PROP_MAX_WIDTH:
|
||||
g_value_set_int(value, self->max_width);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
aspect_container_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(aspect_container_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static GtkSizeRequestMode
|
||||
aspect_container_get_request_mode(GtkWidget *widget)
|
||||
{
|
||||
(void)widget;
|
||||
return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
|
||||
}
|
||||
|
||||
static void
|
||||
aspect_container_size_allocate(GtkWidget *widget,
|
||||
int width, int height, int baseline)
|
||||
{
|
||||
GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
GtkAllocation alloc = {
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = width,
|
||||
.height = height,
|
||||
};
|
||||
gtk_widget_size_allocate(child, &alloc, baseline);
|
||||
}
|
||||
|
||||
static void
|
||||
aspect_container_measure(GtkWidget *widget,
|
||||
GtkOrientation orientation,
|
||||
int for_size,
|
||||
int *minimum,
|
||||
int *natural,
|
||||
int *minimum_baseline,
|
||||
int *natural_baseline)
|
||||
{
|
||||
AspectContainer *self = ASPECT_CONTAINER(widget);
|
||||
|
||||
if (minimum_baseline)
|
||||
*minimum_baseline = -1;
|
||||
if (natural_baseline)
|
||||
*natural_baseline = -1;
|
||||
|
||||
GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||
gboolean has_child = child && gtk_widget_get_visible(child);
|
||||
|
||||
if (orientation == GTK_ORIENTATION_HORIZONTAL) {
|
||||
if (has_child)
|
||||
gtk_widget_measure(child, GTK_ORIENTATION_HORIZONTAL, -1,
|
||||
NULL, NULL, NULL, NULL);
|
||||
|
||||
int w = self->max_width > 0 ? self->max_width : 0;
|
||||
if (minimum) *minimum = w;
|
||||
if (natural) *natural = w;
|
||||
} else {
|
||||
if (for_size >= 0) {
|
||||
int w = self->max_width > 0 && for_size > self->max_width
|
||||
? self->max_width : for_size;
|
||||
int h_min = (int)(w * self->aspect_ratio);
|
||||
int h_nat = (int)(w * self->aspect_ratio);
|
||||
if (minimum) *minimum = h_min;
|
||||
if (natural) *natural = h_nat;
|
||||
} else if (has_child) {
|
||||
int child_min_w = 0, child_nat_w = 0;
|
||||
gtk_widget_measure(child, GTK_ORIENTATION_HORIZONTAL, -1,
|
||||
&child_min_w, &child_nat_w, NULL, NULL);
|
||||
|
||||
int min_w = child_min_w;
|
||||
int nat_w = child_nat_w >= child_min_w ? child_nat_w : child_min_w;
|
||||
|
||||
int req_w = 0, req_h = 0;
|
||||
gtk_widget_get_size_request(widget, &req_w, &req_h);
|
||||
if (req_w > min_w) min_w = req_w;
|
||||
if (req_w > nat_w) nat_w = req_w;
|
||||
|
||||
if (self->max_width > 0) {
|
||||
if (min_w > self->max_width) min_w = self->max_width;
|
||||
if (nat_w > self->max_width) nat_w = self->max_width;
|
||||
}
|
||||
|
||||
int h_min = (int)(min_w * self->aspect_ratio);
|
||||
int h_nat = (int)(nat_w * self->aspect_ratio);
|
||||
if (minimum) *minimum = h_min;
|
||||
if (natural) *natural = h_nat;
|
||||
} else {
|
||||
if (minimum) *minimum = 0;
|
||||
if (natural) *natural = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
aspect_container_compute_expand(GtkWidget *widget,
|
||||
gboolean *hexpand_p,
|
||||
gboolean *vexpand_p)
|
||||
{
|
||||
GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||
if (child) {
|
||||
*hexpand_p = gtk_widget_compute_expand(child, GTK_ORIENTATION_HORIZONTAL);
|
||||
*vexpand_p = gtk_widget_compute_expand(child, GTK_ORIENTATION_VERTICAL);
|
||||
} else {
|
||||
*hexpand_p = FALSE;
|
||||
*vexpand_p = FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
aspect_container_class_init(AspectContainerClass *klass)
|
||||
{
|
||||
GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
|
||||
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
|
||||
|
||||
gobject_class->set_property = aspect_container_set_property;
|
||||
gobject_class->get_property = aspect_container_get_property;
|
||||
gobject_class->dispose = aspect_container_dispose;
|
||||
|
||||
widget_class->get_request_mode = aspect_container_get_request_mode;
|
||||
widget_class->size_allocate = aspect_container_size_allocate;
|
||||
widget_class->measure = aspect_container_measure;
|
||||
widget_class->compute_expand = aspect_container_compute_expand;
|
||||
|
||||
props[PROP_ASPECT_RATIO] = g_param_spec_float(
|
||||
"aspect-ratio", NULL, NULL,
|
||||
0.0, G_MAXFLOAT, 1.0,
|
||||
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
|
||||
|
||||
props[PROP_MAX_WIDTH] = g_param_spec_int(
|
||||
"max-width", NULL, NULL,
|
||||
0, G_MAXINT, 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, "aspectcontainer");
|
||||
gtk_widget_class_set_accessible_role(widget_class, GTK_ACCESSIBLE_ROLE_GENERIC);
|
||||
}
|
||||
|
||||
static void
|
||||
aspect_container_init(AspectContainer *self)
|
||||
{
|
||||
self->aspect_ratio = 1.0;
|
||||
self->max_width = 0;
|
||||
}
|
||||
|
||||
GtkWidget *
|
||||
aspect_container_new(float aspect_ratio)
|
||||
{
|
||||
return g_object_new(ASPECT_TYPE_CONTAINER,
|
||||
"aspect-ratio", aspect_ratio,
|
||||
NULL);
|
||||
}
|
||||
|
||||
void
|
||||
aspect_container_set_aspect_ratio(AspectContainer *self, float ratio)
|
||||
{
|
||||
g_return_if_fail(ASPECT_IS_CONTAINER(self));
|
||||
if (self->aspect_ratio == ratio)
|
||||
return;
|
||||
self->aspect_ratio = ratio;
|
||||
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ASPECT_RATIO]);
|
||||
gtk_widget_queue_resize(GTK_WIDGET(self));
|
||||
}
|
||||
|
||||
float
|
||||
aspect_container_get_aspect_ratio(AspectContainer *self)
|
||||
{
|
||||
g_return_val_if_fail(ASPECT_IS_CONTAINER(self), 1.0);
|
||||
return self->aspect_ratio;
|
||||
}
|
||||
|
||||
void
|
||||
aspect_container_set_max_width(AspectContainer *self, int max_width)
|
||||
{
|
||||
g_return_if_fail(ASPECT_IS_CONTAINER(self));
|
||||
if (self->max_width == max_width)
|
||||
return;
|
||||
self->max_width = max_width;
|
||||
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_MAX_WIDTH]);
|
||||
gtk_widget_queue_resize(GTK_WIDGET(self));
|
||||
}
|
||||
|
||||
int
|
||||
aspect_container_get_max_width(AspectContainer *self)
|
||||
{
|
||||
g_return_val_if_fail(ASPECT_IS_CONTAINER(self), 0);
|
||||
return self->max_width;
|
||||
}
|
||||
431
Sources/CAspectContainer/flow_grid.c
Normal file
431
Sources/CAspectContainer/flow_grid.c
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
#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;
|
||||
}
|
||||
31
Sources/CAspectContainer/include/aspect_container.h
Normal file
31
Sources/CAspectContainer/include/aspect_container.h
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#ifndef ASPECT_CONTAINER_H
|
||||
#define ASPECT_CONTAINER_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
#define ASPECT_TYPE_CONTAINER (aspect_container_get_type())
|
||||
#define ASPECT_CONTAINER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), ASPECT_TYPE_CONTAINER, AspectContainer))
|
||||
#define ASPECT_IS_CONTAINER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), ASPECT_TYPE_CONTAINER))
|
||||
|
||||
typedef struct _AspectContainer AspectContainer;
|
||||
typedef struct _AspectContainerClass AspectContainerClass;
|
||||
|
||||
struct _AspectContainerClass {
|
||||
GtkWidgetClass parent_class;
|
||||
};
|
||||
|
||||
GType aspect_container_get_type (void);
|
||||
GtkWidget *aspect_container_new (float aspect_ratio);
|
||||
|
||||
void aspect_container_set_aspect_ratio (AspectContainer *self,
|
||||
float ratio);
|
||||
float aspect_container_get_aspect_ratio (AspectContainer *self);
|
||||
void aspect_container_set_max_width (AspectContainer *self,
|
||||
int max_width);
|
||||
int aspect_container_get_max_width (AspectContainer *self);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif /* ASPECT_CONTAINER_H */
|
||||
41
Sources/CAspectContainer/include/flow_grid.h
Normal file
41
Sources/CAspectContainer/include/flow_grid.h
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#ifndef FLOW_GRID_H
|
||||
#define FLOW_GRID_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
#define FLOW_TYPE_GRID (flow_grid_get_type())
|
||||
#define FLOW_GRID(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FLOW_TYPE_GRID, FlowGrid))
|
||||
#define FLOW_IS_GRID(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FLOW_TYPE_GRID))
|
||||
|
||||
typedef struct _FlowGrid FlowGrid;
|
||||
typedef struct _FlowGridClass FlowGridClass;
|
||||
|
||||
struct _FlowGridClass {
|
||||
GtkWidgetClass parent_class;
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
FLOW_GRID_JUSTIFY_START,
|
||||
FLOW_GRID_JUSTIFY_CENTER,
|
||||
FLOW_GRID_JUSTIFY_END,
|
||||
} FlowGridJustify;
|
||||
|
||||
GType flow_grid_get_type (void);
|
||||
GtkWidget *flow_grid_new (int minimum_size,
|
||||
int column_spacing,
|
||||
int row_spacing);
|
||||
|
||||
void flow_grid_set_minimum_size (FlowGrid *self, int size);
|
||||
int flow_grid_get_minimum_size (FlowGrid *self);
|
||||
void flow_grid_set_column_spacing (FlowGrid *self, int spacing);
|
||||
int flow_grid_get_column_spacing (FlowGrid *self);
|
||||
void flow_grid_set_row_spacing (FlowGrid *self, int spacing);
|
||||
int flow_grid_get_row_spacing (FlowGrid *self);
|
||||
void flow_grid_set_justify (FlowGrid *self, FlowGridJustify justify);
|
||||
FlowGridJustify flow_grid_get_justify (FlowGrid *self);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif /* FLOW_GRID_H */
|
||||
139
Sources/LuminateUI/Components/AspectContainer.swift
Normal file
139
Sources/LuminateUI/Components/AspectContainer.swift
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
//
|
||||
// AspectContainer.swift
|
||||
//
|
||||
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
|
||||
import Adwaita
|
||||
import CAspectContainer
|
||||
|
||||
/// A single‑child container that preserves a fixed aspect ratio.
|
||||
///
|
||||
/// Unlike GTK's built‑in `GtkAspectFrame`, this container overrides
|
||||
/// `measure()` to report the correct height (`width × ratio`) during
|
||||
/// the layout pass. This makes it work correctly inside `ScrollView`,
|
||||
/// `AdwCarousel`, and other flexible layouts where the parent needs
|
||||
/// to know the child's preferred size.
|
||||
///
|
||||
/// ```swift
|
||||
/// AspectContainer(aspectRatio: 16.0 / 9.0) {
|
||||
/// .child {
|
||||
/// Picture()
|
||||
/// .data(imageData)
|
||||
/// .halign(.fill)
|
||||
/// .hexpand()
|
||||
/// }
|
||||
/// .maxWidth(800)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Ported 1:1 from the Gelata project's `AspectRatioContainer` Rust widget.
|
||||
public struct AspectContainer: Widget {
|
||||
|
||||
#if exposeGeneratedAppearUpdateFunctions
|
||||
public var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
|
||||
public var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
|
||||
#else
|
||||
var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
|
||||
var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
|
||||
#endif
|
||||
|
||||
/// The child widget.
|
||||
var child: Body?
|
||||
/// The desired width‑to‑height ratio.
|
||||
var aspectRatio: Float
|
||||
/// An optional maximum width in pixels (0 = no limit).
|
||||
var maxWidth: Int?
|
||||
|
||||
/// Create an aspect‑ratio container.
|
||||
/// - Parameter aspectRatio: Desired `width / height` ratio.
|
||||
public init(aspectRatio: Float) {
|
||||
self.aspectRatio = aspectRatio
|
||||
}
|
||||
|
||||
// MARK: - Widget
|
||||
|
||||
public func container<Data>(data: WidgetData, type: Data.Type) -> ViewStorage
|
||||
where Data: ViewRenderData {
|
||||
let storage = ViewStorage(aspect_container_new(aspectRatio)?.opaque())
|
||||
for function in appearFunctions {
|
||||
function(storage, data)
|
||||
}
|
||||
if let childStorage = child?.storage(data: data, type: type) {
|
||||
storage.content["child"] = [childStorage]
|
||||
gtk_widget_set_parent(
|
||||
childStorage.opaquePointer?.cast(),
|
||||
storage.opaquePointer?.cast())
|
||||
}
|
||||
return storage
|
||||
}
|
||||
|
||||
public func update<Data>(
|
||||
_ storage: ViewStorage,
|
||||
data: WidgetData,
|
||||
updateProperties: Bool,
|
||||
type: Data.Type
|
||||
) where Data: ViewRenderData {
|
||||
storage.modify { widget in
|
||||
if let childStorage = storage.content["child"]?.first {
|
||||
child?.updateStorage(
|
||||
childStorage,
|
||||
data: data,
|
||||
updateProperties: updateProperties,
|
||||
type: type
|
||||
)
|
||||
}
|
||||
if let maxWidth,
|
||||
updateProperties,
|
||||
(storage.previousState as? Self)?.maxWidth != maxWidth
|
||||
{
|
||||
aspect_container_set_max_width(widget, maxWidth.cInt)
|
||||
}
|
||||
if updateProperties,
|
||||
(storage.previousState as? Self)?.aspectRatio != aspectRatio
|
||||
{
|
||||
aspect_container_set_aspect_ratio(widget, aspectRatio)
|
||||
}
|
||||
}
|
||||
for function in updateFunctions {
|
||||
function(storage, data, updateProperties)
|
||||
}
|
||||
if updateProperties {
|
||||
storage.previousState = self
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Modifiers
|
||||
|
||||
extension AspectContainer {
|
||||
|
||||
/// Set the child widget.
|
||||
public func child(@ViewBuilder _ child: () -> Body) -> Self {
|
||||
modify { $0.child = child() }
|
||||
}
|
||||
|
||||
/// Set an optional maximum width in pixels.
|
||||
/// When set to a positive value, the container's width is clamped
|
||||
/// before computing the height from the aspect ratio.
|
||||
public func maxWidth(_ maxWidth: Int?) -> Self {
|
||||
modify { $0.maxWidth = maxWidth }
|
||||
}
|
||||
|
||||
}
|
||||
232
Sources/LuminateUI/Components/FlowGrid.swift
Normal file
232
Sources/LuminateUI/Components/FlowGrid.swift
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
//
|
||||
// FlowGrid.swift
|
||||
//
|
||||
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
|
||||
import Adwaita
|
||||
import CAspectContainer
|
||||
|
||||
/// Controls how the last row is aligned when it doesn't fill the width.
|
||||
public enum FlowGridJustifySetting: Int {
|
||||
|
||||
/// Last row items are aligned to the start.
|
||||
case start
|
||||
/// Last row items are centered.
|
||||
case center
|
||||
/// Last row items are aligned to the end.
|
||||
case end
|
||||
|
||||
var cValue: FlowGridJustify {
|
||||
switch self {
|
||||
case .start: FLOW_GRID_JUSTIFY_START
|
||||
case .center: FLOW_GRID_JUSTIFY_CENTER
|
||||
case .end: FLOW_GRID_JUSTIFY_END
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// A reflowing grid that arranges children in columns.
|
||||
///
|
||||
/// Unlike `GtkFlowBox`, this widget re‑measures children during every layout
|
||||
/// pass using the actual column width. This avoids stale size caches and
|
||||
/// ensures the `AspectContainer` receives the correct `for_size` for its
|
||||
/// height computation.
|
||||
///
|
||||
/// ```swift
|
||||
/// FlowGrid(items, id: \.id) { item in
|
||||
/// HomePosterCell(item: item, navigation: $navigation)
|
||||
/// }
|
||||
/// .columnSpacing(16)
|
||||
/// .rowSpacing(16)
|
||||
/// .halign(.fill)
|
||||
/// ```
|
||||
public struct FlowGrid<Element, Identifier>: Widget where Identifier: Hashable {
|
||||
|
||||
#if exposeGeneratedAppearUpdateFunctions
|
||||
public var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
|
||||
public var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
|
||||
#else
|
||||
var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
|
||||
var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
|
||||
#endif
|
||||
|
||||
/// The minimum column width.
|
||||
var minimumSize: Int = 200
|
||||
/// The spacing between columns.
|
||||
var columnSpacing: Int = 0
|
||||
/// The spacing between rows.
|
||||
var rowSpacing: Int = 0
|
||||
/// How the last row is aligned.
|
||||
var justify: FlowGridJustifySetting = .start
|
||||
|
||||
/// The elements to display.
|
||||
var elements: [Element]
|
||||
/// The content builder.
|
||||
var content: (Element) -> Body
|
||||
/// The identifier key path.
|
||||
var id: KeyPath<Element, Identifier>
|
||||
|
||||
/// Initialize `FlowGrid`.
|
||||
public init(
|
||||
_ elements: [Element],
|
||||
id: KeyPath<Element, Identifier>,
|
||||
@ViewBuilder content: @escaping (Element) -> Body
|
||||
) {
|
||||
self.elements = elements
|
||||
self.content = content
|
||||
self.id = id
|
||||
}
|
||||
|
||||
// MARK: - Widget
|
||||
|
||||
public func container<Data>(data: WidgetData, type: Data.Type) -> ViewStorage
|
||||
where Data: ViewRenderData {
|
||||
let storage = ViewStorage(
|
||||
flow_grid_new(minimumSize.cInt, columnSpacing.cInt, rowSpacing.cInt)?.opaque()
|
||||
)
|
||||
for function in appearFunctions {
|
||||
function(storage, data)
|
||||
}
|
||||
return storage
|
||||
}
|
||||
|
||||
public func update<Data>(
|
||||
_ storage: ViewStorage,
|
||||
data: WidgetData,
|
||||
updateProperties: Bool,
|
||||
type: Data.Type
|
||||
) where Data: ViewRenderData {
|
||||
storage.modify { widget in
|
||||
|
||||
// --- Properties ---
|
||||
if updateProperties,
|
||||
(storage.previousState as? Self)?.minimumSize != minimumSize
|
||||
{
|
||||
flow_grid_set_minimum_size(widget, minimumSize.cInt)
|
||||
}
|
||||
if updateProperties,
|
||||
(storage.previousState as? Self)?.columnSpacing != columnSpacing
|
||||
{
|
||||
flow_grid_set_column_spacing(widget, columnSpacing.cInt)
|
||||
}
|
||||
if updateProperties,
|
||||
(storage.previousState as? Self)?.rowSpacing != rowSpacing
|
||||
{
|
||||
flow_grid_set_row_spacing(widget, rowSpacing.cInt)
|
||||
}
|
||||
if updateProperties,
|
||||
(storage.previousState as? Self)?.justify != justify
|
||||
{
|
||||
flow_grid_set_justify(widget, justify.cValue)
|
||||
}
|
||||
|
||||
// --- Children ---
|
||||
var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? []
|
||||
let oldElements = storage.fields["element"] as? [Element] ?? []
|
||||
|
||||
var oldByID: [Identifier: ViewStorage] = [:]
|
||||
for (i, oldElement) in oldElements.enumerated() where i < contentStorage.count {
|
||||
oldByID[oldElement[keyPath: id]] = contentStorage[i]
|
||||
}
|
||||
|
||||
var newContentStorage: [ViewStorage] = []
|
||||
var lastChild: OpaquePointer?
|
||||
|
||||
for element in elements {
|
||||
let elementID = element[keyPath: id]
|
||||
if let existingStorage = oldByID.removeValue(forKey: elementID) {
|
||||
newContentStorage.append(existingStorage)
|
||||
lastChild = existingStorage.opaquePointer
|
||||
} else {
|
||||
let child = content(element).storage(data: data, type: type)
|
||||
gtk_widget_set_parent(child.opaquePointer?.cast(), widget?.cast())
|
||||
newContentStorage.append(child)
|
||||
lastChild = child.opaquePointer
|
||||
}
|
||||
}
|
||||
|
||||
for (_, staleStorage) in oldByID {
|
||||
gtk_widget_unparent(staleStorage.opaquePointer?.cast())
|
||||
}
|
||||
|
||||
storage.fields["element"] = elements
|
||||
storage.content[.mainContent] = newContentStorage
|
||||
|
||||
for (index, element) in elements.enumerated() {
|
||||
content(element).updateStorage(
|
||||
newContentStorage[index],
|
||||
data: data,
|
||||
updateProperties: updateProperties,
|
||||
type: type
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for function in updateFunctions {
|
||||
function(storage, data, updateProperties)
|
||||
}
|
||||
if updateProperties {
|
||||
storage.previousState = self
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Modifiers
|
||||
|
||||
extension FlowGrid {
|
||||
|
||||
/// The minimum column width in pixels.
|
||||
public func minimumSize(_ minimumSize: Int) -> Self {
|
||||
modify { $0.minimumSize = minimumSize }
|
||||
}
|
||||
|
||||
/// The spacing between columns in pixels.
|
||||
public func columnSpacing(_ columnSpacing: Int) -> Self {
|
||||
modify { $0.columnSpacing = columnSpacing }
|
||||
}
|
||||
|
||||
/// The spacing between rows in pixels.
|
||||
public func rowSpacing(_ rowSpacing: Int) -> Self {
|
||||
modify { $0.rowSpacing = rowSpacing }
|
||||
}
|
||||
|
||||
/// How the last row is aligned.
|
||||
public func justify(_ justify: FlowGridJustifySetting) -> Self {
|
||||
modify { $0.justify = justify }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Convenience Initializer for Identifiable Elements
|
||||
|
||||
extension FlowGrid where Element: Identifiable, Identifier == Element.ID {
|
||||
|
||||
/// Initialize `FlowGrid` with identifiable elements.
|
||||
public init(
|
||||
_ elements: [Element],
|
||||
@ViewBuilder content: @escaping (Element) -> Body
|
||||
) {
|
||||
self.elements = elements
|
||||
self.content = content
|
||||
self.id = \.id
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ import LuminateDI
|
|||
struct HomePosterCell: View {
|
||||
|
||||
let item: Components.Schemas.BaseItemDto
|
||||
let width: Int = 200
|
||||
let minWidth: Int = 200
|
||||
@Binding var navigation: NavigationStack<Page>
|
||||
@Injected(\.client) var client
|
||||
@Injected(\.imageService) var imageService
|
||||
|
|
@ -55,7 +55,7 @@ struct HomePosterCell: View {
|
|||
loadImage()
|
||||
}
|
||||
}
|
||||
.frame(minWidth: width + (Constants.padding * 2))
|
||||
.frame(minWidth: minWidth + (Constants.padding * 2))
|
||||
.halign(.fill)
|
||||
.valign(.start)
|
||||
.hexpand(false)
|
||||
|
|
@ -65,48 +65,27 @@ struct HomePosterCell: View {
|
|||
|
||||
@ViewBuilder
|
||||
private var imageSection: Body {
|
||||
AspectFrame(ratio: 1.5)
|
||||
AspectContainer(aspectRatio: 1.5)
|
||||
.child {
|
||||
image
|
||||
.halign(.fill)
|
||||
.hexpand()
|
||||
.overflow(.hidden)
|
||||
.card()
|
||||
}
|
||||
.obeyChild(false)
|
||||
.xalign(0.5)
|
||||
.yalign(0.5)
|
||||
.halign(.fill)
|
||||
.hexpand()
|
||||
.overflow(.hidden)
|
||||
.card()
|
||||
// image
|
||||
// .halign(.fill)
|
||||
// .hexpand()
|
||||
// .frame(minHeight: Int(Double(width) * 1.5))
|
||||
// .overflow(.hidden)
|
||||
// .card()
|
||||
.frame(minWidth: minWidth)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var image: Body {
|
||||
if let imageData {
|
||||
// Picture()
|
||||
// .contentFit(.cover)
|
||||
// .data(imageData)
|
||||
// .valign(.fill)
|
||||
// .halign(.fill)
|
||||
// .vexpand()
|
||||
// .hexpand()
|
||||
// .transition(.crossfade)
|
||||
Overlay()
|
||||
.overlay {
|
||||
Picture()
|
||||
.contentFit(.cover)
|
||||
.data(imageData)
|
||||
.valign(.fill)
|
||||
.halign(.fill)
|
||||
.vexpand()
|
||||
.hexpand()
|
||||
}
|
||||
Picture()
|
||||
.contentFit(.cover)
|
||||
.data(imageData)
|
||||
.valign(.fill)
|
||||
.halign(.fill)
|
||||
.vexpand()
|
||||
.hexpand()
|
||||
.transition(.crossfade)
|
||||
} else {
|
||||
Spinner()
|
||||
|
|
@ -150,7 +129,7 @@ struct HomePosterCell: View {
|
|||
itemId: itemId,
|
||||
imageType: .primary,
|
||||
tag: tag,
|
||||
maxWidth: 200
|
||||
maxWidth: 400
|
||||
)
|
||||
else { return }
|
||||
let data = try? await imageService.loadImage(url: url)
|
||||
|
|
|
|||
|
|
@ -57,15 +57,16 @@ public struct ItemGrid: View {
|
|||
}
|
||||
if isLoading {
|
||||
Spinner()
|
||||
.transition(.crossfade)
|
||||
} else {
|
||||
WrapBox(items, id: \.id) { item in
|
||||
FlowGrid(items, id: \.id) { item in
|
||||
HomePosterCell(item: item, navigation: $navigation)
|
||||
}
|
||||
.lineSpacing(16)
|
||||
.childSpacing(16)
|
||||
.justify(JustifyMode.none)
|
||||
.justifyLastLine(false)
|
||||
.halign(.start)
|
||||
.columnSpacing(16)
|
||||
.rowSpacing(16)
|
||||
.minimumSize(216)
|
||||
.halign(.fill)
|
||||
.transition(.crossfade)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
|
|
|
|||
|
|
@ -45,14 +45,13 @@ public struct LibraryGrid: View {
|
|||
.title3()
|
||||
.halign(.start)
|
||||
.padding(10, .horizontal)
|
||||
WrapBox(libraries, id: \.id) { item in
|
||||
FlowGrid(libraries, id: \.id) { item in
|
||||
HomePosterCell(item: item, navigation: $navigation)
|
||||
}
|
||||
.lineSpacing(16)
|
||||
.childSpacing(16)
|
||||
.justify(.fill)
|
||||
.justifyLastLine(false)
|
||||
.halign(.start)
|
||||
.columnSpacing(16)
|
||||
.rowSpacing(16)
|
||||
.minimumSize(216)
|
||||
.halign(.fill)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ public struct MediaRow: View {
|
|||
.padding(16, .trailing)
|
||||
}
|
||||
}
|
||||
// .propagateNaturalHeight()
|
||||
.vscrollbarPolicy(.never)
|
||||
.hscrollbarPolicy(.external)
|
||||
.style("undershoot-start")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue