--- a/ui/gtk/list.c Thu Sep 10 17:36:06 2026 +0200 +++ b/ui/gtk/list.c Fri Sep 11 17:27:12 2026 +0200 @@ -3046,6 +3046,10 @@ free(v); } +static void ui_destroy_listbox2_row(UiListBox2Row *row) { + cxListFree(row->children); +} + UIWIDGET ui_sourcelist2_create(UiObject *obj, UiSourceList2Args *args) { #ifdef UI_GTK3 GtkWidget *listbox = g_object_new(ui_sidebar_list_box_get_type(), NULL); @@ -3071,11 +3075,10 @@ UiListBox2 *uilistbox = malloc(sizeof(UiListBox2)); uilistbox->obj = obj; uilistbox->listbox = GTK_LIST_BOX(listbox); - uilistbox->rows = cxArrayListCreate(NULL, CX_STORE_POINTERS, 64); + uilistbox->rows = cxArrayListCreate(NULL, sizeof(UiListBox2Row), 32); + cxSetDestructor(uilistbox->rows, ui_destroy_listbox2_row); uilistbox->getvalue = args->getvalue; uilistbox->getvaluedata = args->getvaluedata; - uilistbox->getdepth = args->getdepth; - uilistbox->getdepthdata = args->getdepthdata; uilistbox->onactivate = args->onactivate; uilistbox->onactivatedata = args->onactivatedata; uilistbox->onactivate_action = args->onactivate_action ? strdup(args->onactivate_action) : NULL; @@ -3111,7 +3114,7 @@ G_CALLBACK(ui_destroy_sourcelist2), uilistbox); - if(args->onactivate) { + if(args->onactivate || args->onactivate_action) { g_signal_connect( listbox, "row-activated", @@ -3122,16 +3125,125 @@ return scroll_area; } - -static int listbox2_update_item(UiListBox2 *listbox, GtkWidget *row, UiList *list, void *elm, int index) { - UiSubListItem item = { NULL, NULL, NULL, NULL, NULL, NULL }; - if(listbox->getvalue) { - listbox->getvalue(list,elm, index, &item, listbox->getvaluedata); +static void listbox2_button_clicked(GtkWidget *button, UiListBox2Row *data) { + UiEvent event; + event.obj = data->listbox->obj; + event.window = event.obj->window; + event.document = event.obj->ctx->document; + event.eventdata = NULL; + event.eventdatatype = UI_EVENT_DATA_NULL; + event.intval = data->index; + event.set = ui_get_setop(); + + if(data->listbox->onbuttonclick) { + data->listbox->onbuttonclick(&event, data->listbox->onbuttonclickdata); } - GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); + if(data->listbox->onbuttonclick_action) { + uic_action_callback(&event, data->listbox->onbuttonclick_action); + } +} + + +static void listbox2_node_children_update_visibility(UiListBox2Row *rowdata, int visible) { + CxIterator i = cxListIterator(rowdata->children); + int child_visible = visible ? rowdata->expanded : 0; + cx_foreach(UiListBox2Row *, child, i) { + gtk_widget_set_visible(child->row, child_visible); + listbox2_node_children_update_visibility(child, child_visible); + } +} + +#if GTK_CHECK_VERSION(4, 0, 0) + +void listbox2_node_expand( + GtkGestureClick *self, + gint n_press, + gdouble x, + gdouble y, + UiListBox2Row *rowdata) +{ + rowdata->expanded = !rowdata->expanded; + if(rowdata->expanded) { + gtk_image_set_from_icon_name(GTK_IMAGE(rowdata->expander), "pan-down-symbolic"); + } else { + gtk_image_set_from_icon_name(GTK_IMAGE(rowdata->expander), "pan-end-symbolic"); + } + listbox2_node_children_update_visibility(rowdata, rowdata->expanded); +} + +#endif +#if GTK_MAJOR_VERSION == 3 + +#endif + +#define LISTBOX2_ITEM_HBOX_SPACING 10 +static void listbox2_update_item(UiListBox2Row *rowdata, GtkWidget *row, UiSourceListItem *item, int identation) { + GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, LISTBOX2_ITEM_HBOX_SPACING); - return 0; + rowdata->expanded = 1; + GtkWidget *expander = gtk_image_new_from_icon_name("pan-down-symbolic"); + int exp_width = 0; +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_measure(expander, + GTK_ORIENTATION_HORIZONTAL, + -1, + &exp_width, + NULL, + NULL, + NULL); + + GtkGesture *click = gtk_gesture_click_new(); + gtk_widget_add_controller(expander, GTK_EVENT_CONTROLLER(click)); + g_signal_connect(click, "released", G_CALLBACK(listbox2_node_expand), rowdata); +#else + int exp_height; + gtk_widget_get_preferred_width(expander, &exp_width, &exp_height); +#endif + gtk_widget_set_margin_start(hbox, identation * (exp_width+LISTBOX2_ITEM_HBOX_SPACING)); + + WIDGET_NO_SHOW_ALL(expander); + gtk_widget_set_visible(expander, FALSE); + // normal items have the expander icon in the front + if(!item->is_header) { + BOX_ADD(hbox, expander); + } + rowdata->expander = expander; + + if(item->icon) { + GtkWidget *icon = ICON_IMAGE(item->icon); + BOX_ADD(hbox, icon); + } + GtkWidget *label = gtk_label_new(item->label); + gtk_widget_set_halign(label, GTK_ALIGN_START); + + BOX_ADD_EXPAND(hbox, label); + + // button + if(item->button_label || item->button_icon) { + GtkWidget *button = gtk_button_new(); + gtk_button_set_label(GTK_BUTTON(button), item->button_label); + ui_button_set_icon_name(button, item->button_icon); + WIDGET_ADD_CSS_CLASS(button, "flat"); + BOX_ADD(hbox, button); + g_signal_connect( + button, + "clicked", + G_CALLBACK(listbox2_button_clicked), + rowdata); + gtk_widget_set_visible(button, FALSE); + WIDGET_NO_SHOW_ALL(button); + } + + // headings have the expander at the end + if(item->is_header) { + BOX_ADD(hbox, expander); + WIDGET_ADD_CSS_CLASS(label, "ui-listbox-header-row"); + rowdata->heading = TRUE; + } + + LISTBOX_ROW_SET_CHILD(row, hbox); + g_object_set_data(G_OBJECT(row), "ui-listbox-row-data", rowdata); } static void listbox2_update_all(UiListBox2 *listbox) { @@ -3145,29 +3257,104 @@ return; } + UiListBox2Row **stack; + size_t stack_size = 16; + stack = calloc(stack_size, sizeof(UiListBox2Row*)); int index = 0; + UiBool heading = FALSE; void *elm = list->first(list); while(elm) { + UiSourceListItem item = { 0 }; + if(listbox->getvalue) { + listbox->getvalue(list, elm, index, &item, listbox->getvaluedata); + } + GtkWidget *row = gtk_list_box_row_new(); - cxListAdd(listbox->rows, row); gtk_list_box_append(listbox->listbox, row); - int node_depth = listbox2_update_item(listbox, row, list, elm, index); + UiListBox2Row row_data = {0}; + row_data.listbox = listbox; + row_data.row = row; + row_data.index = index; + row_data.depth = item.depth; + cxListAdd(listbox->rows, &row_data); + UiListBox2Row *row_data_ptr = cxListLast(listbox->rows); + listbox2_update_item(row_data_ptr, row, &item, item.depth - (item.depth > 0 ? heading : 0)); + + if(item.depth >= stack_size) { + stack_size += 8; + stack = realloc(stack, stack_size * sizeof(UiListBox2Row*)); + } + stack[item.depth] = row_data_ptr; + + if(item.depth > 0) { + // add row to parent + UiListBox2Row *parent = item.depth-1 < stack_size ? stack[item.depth-1] : NULL; + if(parent) { + if(!parent->children) { + parent->children = cxArrayListCreate(NULL, CX_STORE_POINTERS, 8); + gtk_widget_set_visible(parent->expander, TRUE); + WIDGET_ENABLE_SHOW_ALL(parent->expander); + } + cxListAdd(parent->children, row_data_ptr); + } else { + fprintf(stderr, "Error: sourcelist2: illegal depth: no parent found\n"); + } + } else { + heading = item.is_header != 0; + } + + free(item.label); + free(item.icon); + free(item.badge); + free(item.button_label); + free(item.button_icon); elm = list->next(list); index++; } + + cxListShrink(listbox->rows); + free(stack); } void ui_listbox2_row_activate(GtkListBox *self, GtkListBoxRow *row, gpointer user_data) { + UiListBox2Row *rowdata = g_object_get_data(G_OBJECT(row), "ui-listbox-row-data"); + if(!rowdata) { + return; + } + UiListBox2 *listbox = rowdata->listbox; + UiVar *var = listbox->var; + if(!var) { + return; // shouldn't happen + } + UiList *list = var->value; + void *elm = list->get(list, rowdata->index); + + UiEvent event; + event.obj = listbox->obj; + event.window = event.obj->window; + event.document = event.obj->ctx->document; + event.eventdatatype = UI_EVENT_DATA_LIST_ELM; + event.eventdata = elm; + event.intval = rowdata->index; + event.set = ui_get_setop(); + + if(listbox->onactivate) { + listbox->onactivate(&event, listbox->onactivatedata); + } + if(listbox->onactivate_action) { + uic_action_callback(&event, listbox->onactivate_action); + } } void ui_listbox2_update(UiList *list, int i) { UiListBox2 *listbox = list->obj; if(i >= 0) { - // TODO + // TODO: update single row + listbox2_update_all(listbox); } else { listbox2_update_all(listbox); }