add link button (Cocoa)

Sat, 11 Oct 2025 13:31:49 +0200

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sat, 11 Oct 2025 13:31:49 +0200
changeset 835
a0e1ff100512
parent 834
8801df33144f
child 836
5a8485ff7f54

add link button (Cocoa)

make/xcode/toolkit/toolkit.xcodeproj/project.pbxproj file | annotate | diff | comparison | revisions
make/xcode/toolkit/toolkit/main.m file | annotate | diff | comparison | revisions
ui/cocoa/button.h file | annotate | diff | comparison | revisions
ui/cocoa/button.m file | annotate | diff | comparison | revisions
--- a/make/xcode/toolkit/toolkit.xcodeproj/project.pbxproj	Sat Oct 11 12:00:07 2025 +0200
+++ b/make/xcode/toolkit/toolkit.xcodeproj/project.pbxproj	Sat Oct 11 13:31:49 2025 +0200
@@ -53,6 +53,9 @@
 		ED8687E52D999CF3002F3EC2 /* menu.m in Sources */ = {isa = PBXBuildFile; fileRef = ED8687E42D999CF3002F3EC2 /* menu.m */; };
 		ED99F04A2E5CBD2E00A4CC97 /* widget.m in Sources */ = {isa = PBXBuildFile; fileRef = ED99F0492E5CBD2E00A4CC97 /* widget.m */; };
 		EDB452C32E302C65006FB12D /* image.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB452C22E302C65006FB12D /* image.m */; };
+		EDC315A92E9A739300403776 /* json.c in Sources */ = {isa = PBXBuildFile; fileRef = EDC315A62E9A739300403776 /* json.c */; };
+		EDC315AA2E9A739300403776 /* properties.c in Sources */ = {isa = PBXBuildFile; fileRef = EDC315A72E9A739300403776 /* properties.c */; };
+		EDC315AB2E9A739300403776 /* streams.c in Sources */ = {isa = PBXBuildFile; fileRef = EDC315A82E9A739300403776 /* streams.c */; };
 		EDCD22272E59EEF5000612AF /* list.m in Sources */ = {isa = PBXBuildFile; fileRef = EDCD22262E59EEF5000612AF /* list.m */; };
 		EDCD22352E59F3B1000612AF /* ListDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = EDCD22342E59F3B1000612AF /* ListDataSource.m */; };
 		EDCD22382E5A160A000612AF /* ListDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = EDCD22372E5A160A000612AF /* ListDelegate.m */; };
@@ -175,6 +178,12 @@
 		ED99F04C2E5CBE5000A4CC97 /* widget.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = widget.h; path = /Users/olaf/Projekte/toolkit/ui/ui/widget.h; sourceTree = "<absolute>"; };
 		EDB452C12E302C65006FB12D /* image.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = image.h; path = /Users/olaf/Projekte/toolkit/ui/cocoa/image.h; sourceTree = "<absolute>"; };
 		EDB452C22E302C65006FB12D /* image.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = image.m; path = /Users/olaf/Projekte/toolkit/ui/cocoa/image.m; sourceTree = "<absolute>"; };
+		EDC315A32E9A736900403776 /* json.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = json.h; path = /Users/olaf/Projekte/toolkit/ucx/cx/json.h; sourceTree = "<absolute>"; };
+		EDC315A42E9A736900403776 /* properties.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = properties.h; path = /Users/olaf/Projekte/toolkit/ucx/cx/properties.h; sourceTree = "<absolute>"; };
+		EDC315A52E9A736900403776 /* streams.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = streams.h; path = /Users/olaf/Projekte/toolkit/ucx/cx/streams.h; sourceTree = "<absolute>"; };
+		EDC315A62E9A739300403776 /* json.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = json.c; path = /Users/olaf/Projekte/toolkit/ucx/json.c; sourceTree = "<absolute>"; };
+		EDC315A72E9A739300403776 /* properties.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = properties.c; path = /Users/olaf/Projekte/toolkit/ucx/properties.c; sourceTree = "<absolute>"; };
+		EDC315A82E9A739300403776 /* streams.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = streams.c; path = /Users/olaf/Projekte/toolkit/ucx/streams.c; sourceTree = "<absolute>"; };
 		EDCD22252E59EEF5000612AF /* list.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = list.h; path = /Users/olaf/Projekte/toolkit/ui/cocoa/list.h; sourceTree = "<absolute>"; };
 		EDCD22262E59EEF5000612AF /* list.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = list.m; path = /Users/olaf/Projekte/toolkit/ui/cocoa/list.m; sourceTree = "<absolute>"; };
 		EDCD22332E59F3B1000612AF /* ListDataSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ListDataSource.h; path = /Users/olaf/Projekte/toolkit/ui/cocoa/ListDataSource.h; sourceTree = "<absolute>"; };
@@ -258,6 +267,9 @@
 		ED6580F82CFF1A1200F5402F /* ucx */ = {
 			isa = PBXGroup;
 			children = (
+				EDC315A62E9A739300403776 /* json.c */,
+				EDC315A72E9A739300403776 /* properties.c */,
+				EDC315A82E9A739300403776 /* streams.c */,
 				ED6580F92CFF1A3000F5402F /* allocator.c */,
 				ED6580FA2CFF1A3000F5402F /* array_list.c */,
 				ED6580FB2CFF1A3000F5402F /* buffer.c */,
@@ -299,6 +311,9 @@
 				ED65810D2CFF1A3000F5402F /* test.h */,
 				ED65810E2CFF1A3000F5402F /* tree.h */,
 				ED65810F2CFF1A3000F5402F /* utils.h */,
+				EDC315A32E9A736900403776 /* json.h */,
+				EDC315A42E9A736900403776 /* properties.h */,
+				EDC315A52E9A736900403776 /* streams.h */,
 			);
 			name = cx;
 			path = /Users/olaf/Projekte/toolkit/ucx/cx;
@@ -471,6 +486,9 @@
 				ED2F55AE2E34FAD800A84793 /* Toolbar.m in Sources */,
 				ED65811E2CFF1A3000F5402F /* tree.c in Sources */,
 				ED6581202CFF1A3000F5402F /* mempool.c in Sources */,
+				EDC315A92E9A739300403776 /* json.c in Sources */,
+				EDC315AA2E9A739300403776 /* properties.c in Sources */,
+				EDC315AB2E9A739300403776 /* streams.c in Sources */,
 				ED6581212CFF1A3000F5402F /* map.c in Sources */,
 				ED6581222CFF1A3000F5402F /* hash_map.c in Sources */,
 				ED6581232CFF1A3000F5402F /* array_list.c in Sources */,
--- a/make/xcode/toolkit/toolkit/main.m	Sat Oct 11 12:00:07 2025 +0200
+++ b/make/xcode/toolkit/toolkit/main.m	Sat Oct 11 13:31:49 2025 +0200
@@ -41,6 +41,7 @@
     UiList *list2;
     UiList *sidebar_list;
     UiList *sidebar_list2;
+    UiString *link;
 } MyDocument;
 
 MyDocument* create_doc(void) {
@@ -76,6 +77,8 @@
     ui_list_append(doc->sidebar_list2, "Item 3");
     ui_list_append(doc->sidebar_list2, "Item 4");
     
+    doc->link = ui_string_new(ctx, "link");
+    
     return doc;
 }
 
@@ -138,12 +141,21 @@
     
     
     ui_left_panel0(obj) {
-        ui_button(obj, .label = "left");
+        ui_grid(obj, .margin_left = 10, .margin_right = 10, .columnspacing = 10, .rowspacing = 10, .fill = TRUE) {
+            ui_button(obj, .label = "left", .hexpand = TRUE, .hfill = TRUE);
+            ui_newline(obj);
+            
+            ui_linkbutton(obj, .varname = "link", .hexpand = TRUE, .hfill = TRUE);
+        }
     }
     
     ui_right_panel0(obj) {
-        ui_button(obj, .label = "right");
+        ui_grid(obj, .margin_left = 10, .margin_right = 10, .fill = TRUE) {
+            ui_button(obj, .label = "right", .hexpand = TRUE, .hfill = TRUE);
+        }
     }
+    
+    ui_linkbutton_value_set(doc->link, "unixwork", "https://unixwork.de/");
 
     
     UiModel *model = ui_model(obj->ctx, UI_STRING, "Column 0", UI_STRING, "Column 1", UI_STRING, "Column 2", -1);
--- a/ui/cocoa/button.h	Sat Oct 11 12:00:07 2025 +0200
+++ b/ui/cocoa/button.h	Sat Oct 11 13:31:49 2025 +0200
@@ -40,6 +40,21 @@
 
 @end
 
+@interface UiLinkButtonData : NSObject
+@property UiObject *obj;
+@property (weak) NSTextField *textfield;
+@property (strong) NSString *label;
+@property (strong) NSString *uri;
+@property BOOL visited;
+@property ui_callback onclick;
+@property void *onclickdata;
+
+- (id)init:(UiObject*)obj textfield:(NSTextField*)textfield;
+- (void)setLinkDataFromJson:(const char*)jsonStr;
+- (void)buildLink;
+
+@end
+
 
 int64_t ui_togglebutton_get(UiInteger *i);
 void ui_togglebutton_set(UiInteger *i, int64_t value);
@@ -49,3 +64,6 @@
 
 int64_t ui_radiobuttons_get(UiInteger *i);
 void ui_radiobuttons_set(UiInteger *i, int64_t value);
+
+char* ui_linkbutton_get(UiString *s);
+void ui_linkbutton_set(UiString *s, const char *str);
--- a/ui/cocoa/button.m	Sat Oct 11 12:00:07 2025 +0200
+++ b/ui/cocoa/button.m	Sat Oct 11 13:31:49 2025 +0200
@@ -31,6 +31,9 @@
 #import "Container.h"
 #import <objc/runtime.h>
 
+#import <cx/buffer.h>
+#import <cx/json.h>
+
 UIWIDGET ui_button_create(UiObject* obj, UiButtonArgs *args) {
     NSButton *button = [[NSButton alloc] init];
     button.translatesAutoresizingMaskIntoConstraints = NO;
@@ -273,3 +276,187 @@
         index++;
     }
 }
+
+
+/* --------------------------- Link Button --------------------------- */
+
+@implementation UiLinkButtonData
+
+- (id)init:(UiObject*)obj textfield:(NSTextField*)textfield {
+    _obj = obj;
+    _textfield = textfield;
+    return self;
+}
+
+- (void)setLinkDataFromJson:(const char*)jsonStr {
+    CxJson json;
+    cxJsonInit(&json, NULL);
+    cxJsonFill(&json, jsonStr);
+    
+    CxJsonValue *value;
+    if(cxJsonNext(&json, &value) == CX_JSON_NO_ERROR) {
+        if(cxJsonIsObject(value)) {
+            CxJsonValue *label = cxJsonObjGet(value, "label");
+            CxJsonValue *uri = cxJsonObjGet(value, "uri");
+            CxJsonValue *visited = cxJsonObjGet(value, "visited");
+            if(label) {
+                char *str = cxJsonIsString(label) ? cxJsonAsString(label) : NULL;
+                if(str) {
+                    _label = [[NSString alloc]initWithUTF8String:str];
+                } else {
+                    _label = nil;
+                }
+            }
+            if(uri) {
+                char *str = cxJsonIsString(uri) ? cxJsonAsString(uri) : NULL;
+                if(str) {
+                    _uri = [[NSString alloc]initWithUTF8String:str];
+                } else {
+                    _uri = nil;
+                }
+            }
+            if(visited) {
+                _visited = cxJsonIsBool(visited) ? cxJsonAsBool(visited) : FALSE;
+            }
+        }
+        cxJsonValueFree(value);
+    }
+    cxJsonDestroy(&json);
+    
+    [self buildLink];
+}
+
+- (void)buildLink {
+    NSString *label = _label ? _label : @"";
+    
+    NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:label];
+    [attrString beginEditing];
+    if(_uri) {
+        [attrString addAttribute:NSLinkAttributeName value:_uri range:NSMakeRange(0, attrString.length)];
+    }
+    [attrString addAttribute:NSForegroundColorAttributeName value:[NSColor systemBlueColor] range:NSMakeRange(0, attrString.length)];
+    [attrString addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:NSMakeRange(0, attrString.length)];
+    [attrString endEditing];
+
+    [_textfield setAttributedStringValue:attrString];
+}
+
+@end
+
+static char* create_linkbutton_jsonvalue(const char *label, const char *uri, UiBool include_null, UiBool visited, UiBool set_visited) {
+    CxJsonValue *obj = cxJsonCreateObj(NULL);
+    if(label) {
+        cxJsonObjPutString(obj, CX_STR("label"), label);
+    } else if(include_null) {
+        cxJsonObjPutLiteral(obj, CX_STR("label"), CX_JSON_NULL);
+    }
+    
+    if(uri) {
+        cxJsonObjPutString(obj, CX_STR("uri"), uri);
+    } else if(include_null) {
+        cxJsonObjPutLiteral(obj, CX_STR("uri"), CX_JSON_NULL);
+    }
+    
+    if(set_visited) {
+        cxJsonObjPutLiteral(obj, CX_STR("visited"), visited ? CX_JSON_TRUE : CX_JSON_FALSE);
+    }
+    
+    CxJsonWriter writer = cxJsonWriterCompact();
+    CxBuffer buf;
+    cxBufferInit(&buf, NULL, 128, NULL, CX_BUFFER_AUTO_EXTEND);
+    cxJsonWrite(&buf, obj, (cx_write_func)cxBufferWrite, &writer);
+    cxJsonValueFree(obj);
+    cxBufferTerminate(&buf);
+    
+    return buf.space;
+}
+
+UIWIDGET ui_linkbutton_create(UiObject *obj, UiLinkButtonArgs *args) {
+    NSTextField *label = [[NSTextField alloc] init];
+    label.editable = NO;
+    label.bezeled = NO;
+    label.drawsBackground = NO;
+    label.allowsEditingTextAttributes = YES;
+    label.selectable = YES;
+    
+    UiLayout layout = UI_ARGS2LAYOUT(args);
+    ui_container_add(obj, label, &layout);
+    
+    UiLinkButtonData *data = [[UiLinkButtonData alloc]init:obj textfield:label];
+    objc_setAssociatedObject(label, "linkdata", data, OBJC_ASSOCIATION_RETAIN);
+    
+    UiVar *var = uic_widget_var(obj->ctx, obj->ctx, args->value, args->varname, UI_VAR_STRING);
+    if(var) {
+        UiString *s = var->value;
+        s->obj = (__bridge void*)data;
+        s->get = ui_linkbutton_get;
+        s->set = ui_linkbutton_set;
+        
+        if(s->value.ptr) {
+            [data setLinkDataFromJson:s->value.ptr];
+        }
+    }
+    
+    return (__bridge void*)label;
+}
+
+char* ui_linkbutton_get(UiString *s) {
+    return NULL; // TODO
+}
+
+void ui_linkbutton_set(UiString *s, const char *str) {
+    UiLinkButtonData *data = (__bridge UiLinkButtonData*)s->obj;
+    [data setLinkDataFromJson:str];
+}
+
+
+
+void ui_linkbutton_value_set(UiString *str, const char *label, const char *uri) {
+    char *value = create_linkbutton_jsonvalue(label, uri, TRUE, FALSE, TRUE);
+    ui_set(str, value);
+    free(value);
+}
+
+void ui_linkbutton_value_set_label(UiString *str, const char *label) {
+    char *value = create_linkbutton_jsonvalue(label, NULL, FALSE, FALSE, TRUE);
+    ui_set(str, value);
+    free(value);
+}
+
+void ui_linkbutton_value_set_uri(UiString *str, const char *uri) {
+    char *value = create_linkbutton_jsonvalue(NULL, uri, FALSE, FALSE, TRUE);
+    ui_set(str, value);
+    free(value);
+}
+
+void ui_linkbutton_value_set_visited(UiString *str, UiBool visited) {
+    char *value = create_linkbutton_jsonvalue(NULL, NULL, FALSE, visited, TRUE);
+    ui_set(str, value);
+    free(value);
+}
+
+// TODO
+
+void ui_linkbutton_set_label(UIWIDGET button, const char *label) {
+    
+}
+
+void ui_linkbutton_set_uri(UIWIDGET button, const char *label) {
+    
+}
+
+void ui_linkbutton_set_visited(UIWIDGET button, UiBool visited) {
+    
+}
+
+char* ui_linkbutton_get_label(UIWIDGET button) {
+    return NULL;
+}
+
+char* ui_linkbutton_get_uri(UIWIDGET button) {
+    return NULL;
+}
+
+UiBool ui_linkbutton_get_visited(UIWIDGET button) {
+    return FALSE;
+}

mercurial