diff -r 81c4f73236a4 -r c3f2f16fa4b8 ui/cocoa/list.m --- a/ui/cocoa/list.m Sat Oct 04 14:54:25 2025 +0200 +++ b/ui/cocoa/list.m Sun Oct 19 21:20:08 2025 +0200 @@ -30,6 +30,11 @@ #import "ListDelegate.h" #import +#import +#import + +#import + static void* getvalue_wrapper(UiList *list, void *elm, int row, int col, void *userdata, UiBool *freeResult) { ui_getvaluefunc getvalue = (ui_getvaluefunc)userdata; return getvalue(elm, col); @@ -354,3 +359,449 @@ [combobox selectItemAtIndex: -1]; } } + + +/* --------------------------- SourceList --------------------------- */ + +static void sublist_free(const CxAllocator *a, UiSubList *sl) { + cxFree(a, (char*)sl->varname); + cxFree(a, (char*)sl->header); +} + +static UiSubList copy_sublist(const CxAllocator *a, UiSubList *sl) { + UiSubList new_sl; + new_sl.value = sl->value; + new_sl.varname = sl->varname ? cx_strdup_a(a, cx_str(sl->varname)).ptr : NULL; + new_sl.header = sl->header ? cx_strdup_a(a, cx_str(sl->header)).ptr : NULL; + new_sl.separator = sl->separator; + new_sl.userdata = sl->userdata; + return new_sl; +} + +static CxList* copy_sublists(const CxAllocator *a, UiSourceListArgs *args) { + if(args->sublists) { + size_t max = args->numsublists; + if(max == 0) { + max = INT_MAX; + } + + CxList *sublists = cxArrayListCreate(a, NULL, sizeof(UiSubList), args->numsublists); + sublists->collection.advanced_destructor = (cx_destructor_func2)sublist_free; + + for(int i=0;isublists[i]; + if(sl->value == NULL && sl->varname == NULL) { + break; + } + + UiSubList new_sl = copy_sublist(a, sl); + cxListAdd(sublists, &new_sl); + } + + return sublists; + } + return NULL; +} + +UIWIDGET ui_sourcelist_create(UiObject *obj, UiSourceListArgs *args) { + // create views + NSScrollView *scrollview = [[NSScrollView alloc] init]; + scrollview.autoresizingMask = NSViewWidthSizable; + scrollview.hasVerticalScroller = YES; + scrollview.hasHorizontalScroller = NO; + scrollview.autohidesScrollers = YES; + + NSOutlineView *outline = [[NSOutlineView alloc]init]; + NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:@"x"]; + [outline addTableColumn:column]; + outline.outlineTableColumn = column; + outline.headerView = NULL; + outline.rowSizeStyle = NSTableViewRowSizeStyleDefault; + outline.usesAutomaticRowHeights = YES; + outline.indentationPerLevel = 0; + + outline.style = NSTableViewStyleSourceList; + + // Make background transparent so vibrancy shows through + scrollview.drawsBackground = NO; + + scrollview.documentView = outline; + + UiLayout layout = UI_ARGS2LAYOUT(args); + ui_container_add(obj, scrollview, &layout); + + // datasource and delegate + UiSourceList *data = [[UiSourceList alloc] init:obj outline:outline]; + data.sublists = copy_sublists(obj->ctx->allocator, args); + UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->dynamic_sublist, args->varname, UI_VAR_LIST); + if(var) { + UiList *list = var->value; + list->obj = (__bridge void*)data; + list->update = ui_sourcelist_update; + } + data.dynamic_sublists = var; + data.getvalue = args->getvalue; + data.getvaluedata = args->getvaluedata; + data.onactivate = args->onactivate; + data.onactivatedata = args->onactivatedata; + data.onbuttonclick = args->onbuttonclick; + data.onactivatedata = args->onbuttonclickdata; + [data update:-1]; + + outline.dataSource = data; + outline.delegate = data; + + [data update:-1]; + + objc_setAssociatedObject(outline, "ui_datasource", data, OBJC_ASSOCIATION_RETAIN); + + return (__bridge void*)scrollview; +} + +void ui_sourcelist_update(UiList *list, int row) { + UiSourceList *sourcelist = (__bridge UiSourceList*)list->obj; + [sourcelist update:row]; +} + + +/* + * Data Source and Delegate for the sourcelist NSOutlineView + */ +@implementation UiSourceList + +- (id)init:(UiObject*)obj outline:(NSOutlineView*)view { + _obj = obj; + _outlineView = view; + _sections = [[NSMutableArray alloc] initWithCapacity:16]; + return self; +} + +- (void)dealloc { + cxListFree(_sublists); +} + +- (void)update:(int)row { + // TODO: check row + + [_sections removeAllObjects]; + + CxIterator i = cxListIterator(_sublists); + int index = 0; + int rownum = 0; + cx_foreach(UiSubList *, sl, i) { + UiSourceListItem *section = [[UiSourceListItem alloc] init:self sublist:sl]; + section.sublistIndex = index; + section.rownum = rownum; + section.sublistStartRow = rownum; + [section update:-1]; + [_sections addObject:section]; + index++; + rownum += 1 + section.items.count; + } + + [_outlineView reloadData]; + [_outlineView expandItem:nil expandChildren:YES]; +} + +// NSOutlineViewDataSource implementation + +- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { + if(item == nil) { + return _sections.count; + } else { + UiSourceListItem *i = item; + return i.items.count; + } +} + +- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { + UiSourceListItem *i = item; + return [i isSection] ? YES : NO; +} + +- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { + UiSourceListItem *i = item; + if(i) { + return [i.items objectAtIndex:index]; + } + return [_sections objectAtIndex:index]; +} + +- (void)outlineView:(NSOutlineView *)outlineView + setObjectValue:(id)object + forTableColumn:(NSTableColumn *)tableColumn + byItem:(id)item +{ + +} + +// NSOutlineViewDelegate implementation + +- (NSView *)outlineView:(NSOutlineView *)outlineView + viewForTableColumn:(NSTableColumn *)tableColumn + item:(id)item +{ + UiSourceListItem *i = item; + + NSTableCellView *cell = [[NSTableCellView alloc] init]; + cell.identifier = @"cell"; + // Icon + NSImageView *iconView = [[NSImageView alloc] initWithFrame:NSZeroRect]; + iconView.translatesAutoresizingMaskIntoConstraints = NO; + [cell addSubview:iconView]; + cell.imageView = iconView; + + // Label + //NSTextField *textField = [NSTextField labelWithString:@""]; + NSTextField *textField = [[NSTextField alloc] initWithFrame:NSZeroRect]; + textField.translatesAutoresizingMaskIntoConstraints = NO; + textField.bezeled = NO; + textField.editable = NO; + textField.drawsBackground = NO; + textField.selectable = NO; + textField.lineBreakMode = NSLineBreakByTruncatingTail; + + + [cell addSubview:textField]; + cell.textField = textField; + + if([i isSection]) { + NSFont *font = [NSFont boldSystemFontOfSize:[NSFont systemFontSize]*0.85]; + //NSFont *font = [NSFont preferredFontForTextStyle:NSFontTextStyleCaption1 options:@{}]; + NSDictionary *attrs = @{ + NSFontAttributeName: font, + NSForegroundColorAttributeName: [NSColor tertiaryLabelColor] + }; + textField.attributedStringValue = [[NSAttributedString alloc] initWithString:i.label attributes:attrs]; + + // Layout constraints + [NSLayoutConstraint activateConstraints:@[ + [iconView.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:0], + [iconView.bottomAnchor constraintEqualToAnchor:cell.bottomAnchor constant:-1], + + [textField.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:0], + [textField.bottomAnchor constraintEqualToAnchor:cell.bottomAnchor constant:-1], + [textField.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:0], + ]]; + } else { + textField.stringValue = i.label; + + // Layout constraints + [NSLayoutConstraint activateConstraints:@[ + [iconView.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:0], + [iconView.centerYAnchor constraintEqualToAnchor:cell.centerYAnchor], + + [textField.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:0], + [textField.centerYAnchor constraintEqualToAnchor:cell.centerYAnchor], + [textField.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:0], + ]]; + } + + return cell; +} + +- (NSTableRowView *) outlineView:(NSOutlineView *) outlineView + rowViewForItem:(id)item { + UiSourceListItem *it = item; + UiSourceListRow *row = [[UiSourceListRow alloc]init]; + if([it isSection] && it.sublist->header) { + row.showDisclosureButton = YES; + } + return row; +} + +- (BOOL) outlineView:(NSOutlineView *) outlineView + shouldSelectItem:(id)item +{ + UiSourceListItem *i = item; + return [i isSection] ? NO : YES; +} + +- (CGFloat) outlineView:(NSOutlineView *) outlineView + heightOfRowByItem:(id) item +{ + UiSourceListItem *i = item; + CGFloat rowHeight = outlineView.rowHeight; + if([i isSection]) { + if(i.sublist->header) { + rowHeight += i.sublistIndex == 0 ? -12 : 4; + } else { + rowHeight = i.sublistIndex == 0 ? 0.1 : 12; + } + } + return rowHeight; +} + +- (void) outlineViewSelectionDidChange:(NSNotification *) notification { + UiEvent event; + event.obj = _obj; + event.window = event.obj->window; + event.document = event.obj->ctx->document; + event.eventdata = NULL; + event.eventdatatype = 0; + event.intval = 0; + event.set = ui_get_setop(); + + UiSubListEventData sublistEvent; + + NSInteger selectedRow = _outlineView.selectedRow; + if(selectedRow >= 0) { + UiSourceListItem *item = [_outlineView itemAtRow:selectedRow]; + UiSourceListItem *parent = item.parent; + UiSubList *sublist = parent != nil ? parent.sublist : item.sublist; + UiVar *var = parent != nil ? parent.var : item.var; + if(item && var) { + sublistEvent.list = var->value; + sublistEvent.sublist_index = parent ? parent.sublistIndex : item.sublistIndex; + sublistEvent.row_index = (int)selectedRow - item.sublistStartRow - 1; + sublistEvent.sublist_userdata = sublist ? sublist->userdata : NULL; + sublistEvent.event_data = item.eventdata; + sublistEvent.row_data = sublistEvent.list->get(sublistEvent.list, sublistEvent.row_index); + + event.eventdata = &sublistEvent; + event.eventdatatype = UI_EVENT_DATA_SUBLIST; + } + } + + if(_onactivate) { + _onactivate(&event, _onactivatedata); + } +} + +@end + +/* + * Outline datasource item + * Is used for sections (sublists) and individual items + */ +@implementation UiSourceListItem + +- (id)init:(UiSourceList*)sourcelist sublist:(UiSubList*)sublist { + _sourcelist = sourcelist; + _sublist = sublist; + _items = [[NSMutableArray alloc]initWithCapacity:16]; + if(sublist->header) { + _label = [[NSString alloc]initWithUTF8String:sublist->header]; + } else { + _label = @""; + } + UiVar *var = uic_widget_var(sourcelist.obj->ctx, + sourcelist.obj->ctx, + sublist->value, + sublist->varname, + UI_VAR_LIST); + _var = var; + return self; +} + +- (id)init:(UiSubListItem*)item parent:(UiSourceListItem*)parent { + _parent = parent; + if(item->label) { + _label = [[NSString alloc]initWithUTF8String:item->label]; + } else { + _label = @""; + } + _eventdata = item->eventdata; + return self; +} + +- (BOOL)isSection { + return _sublist != NULL; +} + +- (void)update:(int)row { + // TODO: check row + + [_items removeAllObjects]; + if(_var == NULL) { + return; + } + UiList *list = _var->value; + void *elm = list->first(list); + int index = 0; + while(elm) { + UiSubListItem item = { NULL, NULL, NULL, NULL, NULL, NULL }; + if(_sourcelist.getvalue) { + _sourcelist.getvalue(list, _sublist->userdata, elm, index, &item, _sourcelist.getvaluedata); + } else { + item.label = strdup(elm); + } + + UiSourceListItem *it = [[UiSourceListItem alloc] init:&item parent:self]; + it.sublistIndex = index; + it.rownum = self.rownum + index; + it.sublistStartRow = _parent ? _parent.sublistStartRow : _sublistStartRow; + [_items addObject:it]; + + elm = list->next(list); + index++; + } +} + +@end + +/* + * Custom NSTableRowView implementation + * Moves the disclosure button to the right side + * Handles mouse hover events (for hiding the disclosure button) + */ +@implementation UiSourceListRow + +- (void)layout { + [super layout]; + + for (NSView *subview in self.subviews) { + if ([subview.identifier isEqualToString:NSOutlineViewDisclosureButtonKey] || + [subview.identifier isEqualToString:NSOutlineViewShowHideButtonKey]) + { + NSRect frame = subview.frame; + frame.origin.x = self.bounds.size.width - frame.size.width - 16.0; + subview.frame = frame; + + if(!_hover) { + subview.hidden = YES; + } + + if(subview != _disclosureButton) { + // init disclosure button + _disclosureButton = (NSButton*)subview; + if ([subview isKindOfClass:[NSButton class]]) { + NSButton *button = (NSButton*)subview; + button.contentTintColor = [NSColor tertiaryLabelColor]; + } + } + + + } else if ([subview.identifier isEqualToString:@"cell"]) { + NSRect frame = subview.frame; + frame.origin.x = 16; + subview.frame = frame; + } + } +} + +- (void)updateTrackingAreas { + [super updateTrackingAreas]; + if(_trackingArea != nil) { + [self removeTrackingArea:_trackingArea]; + } + _trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds + options:NSTrackingMouseEnteredAndExited | + NSTrackingActiveInActiveApp | + NSTrackingInVisibleRect + owner:self + userInfo:nil]; + [self addTrackingArea:_trackingArea]; +} + +- (void)mouseEntered:(NSEvent *)event { + _hover = YES; + _disclosureButton.hidden = _showDisclosureButton ? NO : YES; +} + +- (void)mouseExited:(NSEvent *)event { + _hover = NO; + _disclosureButton.hidden = YES; +} + +@end