fix build with newest toolkit version default tip

Sat, 13 Dec 2025 15:58:58 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sat, 13 Dec 2025 15:58:58 +0100
changeset 115
e57ca2747782
parent 114
3da24640513a

fix build with newest toolkit version

application/application.c file | annotate | diff | comparison | revisions
application/davcontroller.c file | annotate | diff | comparison | revisions
application/settings.c file | annotate | diff | comparison | revisions
application/window.c file | annotate | diff | comparison | revisions
ucx/allocator.c file | annotate | diff | comparison | revisions
ucx/array_list.c file | annotate | diff | comparison | revisions
ucx/buffer.c file | annotate | diff | comparison | revisions
ucx/cx/allocator.h file | annotate | diff | comparison | revisions
ucx/cx/array_list.h file | annotate | diff | comparison | revisions
ucx/cx/buffer.h file | annotate | diff | comparison | revisions
ucx/cx/common.h file | annotate | diff | comparison | revisions
ucx/cx/json.h file | annotate | diff | comparison | revisions
ucx/cx/linked_list.h file | annotate | diff | comparison | revisions
ucx/cx/properties.h file | annotate | diff | comparison | revisions
ucx/cx/string.h file | annotate | diff | comparison | revisions
ucx/cx/tree.h file | annotate | diff | comparison | revisions
ucx/hash_map.c file | annotate | diff | comparison | revisions
ucx/json.c file | annotate | diff | comparison | revisions
ucx/kv_list.c file | annotate | diff | comparison | revisions
ucx/linked_list.c file | annotate | diff | comparison | revisions
ucx/list.c file | annotate | diff | comparison | revisions
ucx/printf.c file | annotate | diff | comparison | revisions
ucx/properties.c file | annotate | diff | comparison | revisions
ucx/string.c file | annotate | diff | comparison | revisions
ucx/tree.c file | annotate | diff | comparison | revisions
ui/cocoa/ListDataSource.h file | annotate | diff | comparison | revisions
ui/cocoa/list.h file | annotate | diff | comparison | revisions
ui/cocoa/list.m file | annotate | diff | comparison | revisions
ui/cocoa/toolkit.m file | annotate | diff | comparison | revisions
ui/cocoa/window.m file | annotate | diff | comparison | revisions
ui/common/app.c file | annotate | diff | comparison | revisions
ui/common/app.h file | annotate | diff | comparison | revisions
ui/common/args.c file | annotate | diff | comparison | revisions
ui/common/args.h file | annotate | diff | comparison | revisions
ui/common/context.c file | annotate | diff | comparison | revisions
ui/common/context.h file | annotate | diff | comparison | revisions
ui/common/menu.c file | annotate | diff | comparison | revisions
ui/common/menu.h file | annotate | diff | comparison | revisions
ui/common/message.c file | annotate | diff | comparison | revisions
ui/common/message.h file | annotate | diff | comparison | revisions
ui/common/object.c file | annotate | diff | comparison | revisions
ui/common/object.h file | annotate | diff | comparison | revisions
ui/common/objs.mk file | annotate | diff | comparison | revisions
ui/common/properties.c file | annotate | diff | comparison | revisions
ui/common/threadpool.c file | annotate | diff | comparison | revisions
ui/common/threadpool.h file | annotate | diff | comparison | revisions
ui/common/toolbar.c file | annotate | diff | comparison | revisions
ui/common/toolbar.h file | annotate | diff | comparison | revisions
ui/common/types.c file | annotate | diff | comparison | revisions
ui/common/types.h file | annotate | diff | comparison | revisions
ui/common/utils.c file | annotate | diff | comparison | revisions
ui/common/utils.h file | annotate | diff | comparison | revisions
ui/common/wrapper.c file | annotate | diff | comparison | revisions
ui/common/wrapper.h file | annotate | diff | comparison | revisions
ui/gtk/button.c file | annotate | diff | comparison | revisions
ui/gtk/container.c file | annotate | diff | comparison | revisions
ui/gtk/container.h file | annotate | diff | comparison | revisions
ui/gtk/entry.c file | annotate | diff | comparison | revisions
ui/gtk/graphics.c file | annotate | diff | comparison | revisions
ui/gtk/headerbar.c file | annotate | diff | comparison | revisions
ui/gtk/headerbar.h file | annotate | diff | comparison | revisions
ui/gtk/image.c file | annotate | diff | comparison | revisions
ui/gtk/list.c file | annotate | diff | comparison | revisions
ui/gtk/list.h file | annotate | diff | comparison | revisions
ui/gtk/menu.c file | annotate | diff | comparison | revisions
ui/gtk/text.c file | annotate | diff | comparison | revisions
ui/gtk/toolbar.c file | annotate | diff | comparison | revisions
ui/gtk/toolkit.c file | annotate | diff | comparison | revisions
ui/gtk/toolkit.h file | annotate | diff | comparison | revisions
ui/gtk/webview.c file | annotate | diff | comparison | revisions
ui/gtk/widget.c file | annotate | diff | comparison | revisions
ui/gtk/widget.h file | annotate | diff | comparison | revisions
ui/gtk/window.c file | annotate | diff | comparison | revisions
ui/motif/Fsb.c file | annotate | diff | comparison | revisions
ui/motif/Fsb.h file | annotate | diff | comparison | revisions
ui/motif/FsbP.h file | annotate | diff | comparison | revisions
ui/motif/Grid.c file | annotate | diff | comparison | revisions
ui/motif/Grid.h file | annotate | diff | comparison | revisions
ui/motif/button.c file | annotate | diff | comparison | revisions
ui/motif/container.c file | annotate | diff | comparison | revisions
ui/motif/container.h file | annotate | diff | comparison | revisions
ui/motif/entry.c file | annotate | diff | comparison | revisions
ui/motif/entry.h file | annotate | diff | comparison | revisions
ui/motif/label.c file | annotate | diff | comparison | revisions
ui/motif/list.c file | annotate | diff | comparison | revisions
ui/motif/list.h file | annotate | diff | comparison | revisions
ui/motif/menu.c file | annotate | diff | comparison | revisions
ui/motif/menu.h file | annotate | diff | comparison | revisions
ui/motif/objs.mk file | annotate | diff | comparison | revisions
ui/motif/pathbar.c file | annotate | diff | comparison | revisions
ui/motif/pathbar.h file | annotate | diff | comparison | revisions
ui/motif/text.c file | annotate | diff | comparison | revisions
ui/motif/text.h file | annotate | diff | comparison | revisions
ui/motif/toolbar.c file | annotate | diff | comparison | revisions
ui/motif/toolkit.c file | annotate | diff | comparison | revisions
ui/motif/toolkit.h file | annotate | diff | comparison | revisions
ui/motif/window.c file | annotate | diff | comparison | revisions
ui/motif/window.h file | annotate | diff | comparison | revisions
ui/qt/container.cpp file | annotate | diff | comparison | revisions
ui/qt/container.h file | annotate | diff | comparison | revisions
ui/qt/list.cpp file | annotate | diff | comparison | revisions
ui/qt/list.h file | annotate | diff | comparison | revisions
ui/qt/menu.cpp file | annotate | diff | comparison | revisions
ui/qt/model.cpp file | annotate | diff | comparison | revisions
ui/qt/model.h file | annotate | diff | comparison | revisions
ui/qt/qt5.pro file | annotate | diff | comparison | revisions
ui/qt/toolbar.cpp file | annotate | diff | comparison | revisions
ui/qt/toolkit.cpp file | annotate | diff | comparison | revisions
ui/qt/window.cpp file | annotate | diff | comparison | revisions
ui/ui/button.h file | annotate | diff | comparison | revisions
ui/ui/container.h file | annotate | diff | comparison | revisions
ui/ui/entry.h file | annotate | diff | comparison | revisions
ui/ui/image.h file | annotate | diff | comparison | revisions
ui/ui/list.h file | annotate | diff | comparison | revisions
ui/ui/menu.h file | annotate | diff | comparison | revisions
ui/ui/properties.h file | annotate | diff | comparison | revisions
ui/ui/text.h file | annotate | diff | comparison | revisions
ui/ui/toolbar.h file | annotate | diff | comparison | revisions
ui/ui/toolkit.h file | annotate | diff | comparison | revisions
ui/ui/ui.h file | annotate | diff | comparison | revisions
ui/ui/webview.h file | annotate | diff | comparison | revisions
ui/ui/widget.h file | annotate | diff | comparison | revisions
ui/ui/win32.h file | annotate | diff | comparison | revisions
ui/ui/window.h file | annotate | diff | comparison | revisions
ui/win32/button.c file | annotate | diff | comparison | revisions
ui/win32/button.h file | annotate | diff | comparison | revisions
ui/win32/container.c file | annotate | diff | comparison | revisions
ui/win32/container.h file | annotate | diff | comparison | revisions
ui/win32/grid.c file | annotate | diff | comparison | revisions
ui/win32/list.c file | annotate | diff | comparison | revisions
ui/win32/list.h file | annotate | diff | comparison | revisions
ui/win32/menu.c file | annotate | diff | comparison | revisions
ui/win32/menu.h file | annotate | diff | comparison | revisions
ui/win32/objs.mk file | annotate | diff | comparison | revisions
ui/win32/text.c file | annotate | diff | comparison | revisions
ui/win32/text.h file | annotate | diff | comparison | revisions
ui/win32/toolkit.c file | annotate | diff | comparison | revisions
ui/win32/window.c file | annotate | diff | comparison | revisions
ui/win32/window.h file | annotate | diff | comparison | revisions
ui/winui/list.h file | annotate | diff | comparison | revisions
ui/winui/table.h file | annotate | diff | comparison | revisions
ui/winui/toolkit.cpp file | annotate | diff | comparison | revisions
ui/winui/window.cpp file | annotate | diff | comparison | revisions
ui/winui/winui.vcxproj file | annotate | diff | comparison | revisions
ui/winui/winui.vcxproj.filters file | annotate | diff | comparison | revisions
--- a/application/application.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/application/application.c	Sat Dec 13 15:58:58 2025 +0100
@@ -87,15 +87,15 @@
     ui_menu("File") {
         ui_menuitem(.label = "New Window", .onclick = action_window_new);
         ui_menuseparator();
-        ui_menuitem(.label = "New Folder", .onclick = action_mkcol, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-        ui_menuitem(.label = "New File", .onclick = action_newfile, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-        ui_menuitem(.label = "Delete", .onclick = action_delete, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem(.label = "New Folder", .onclick = action_mkcol, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem(.label = "New File", .onclick = action_newfile, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem(.label = "Delete", .onclick = action_delete, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
         ui_menuseparator();
-        ui_menuitem(.label = "Upload File", .onclick = action_upload_file, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-        ui_menuitem(.label = "Upload Folder", .onclick = action_upload_dir, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-        ui_menuitem(.label = "Download", .onclick = action_download, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
-        ui_menuitem("Open Properties", .onclick = action_open_properties, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
-        ui_menuitem("Open as Text File", .onclick = action_open_properties, .onclickdata = "text/plain", .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+        ui_menuitem(.label = "Upload File", .onclick = action_upload_file, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem(.label = "Upload Folder", .onclick = action_upload_dir, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem(.label = "Download", .onclick = action_download, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+        ui_menuitem("Open Properties", .onclick = action_open_properties, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+        ui_menuitem("Open as Text File", .onclick = action_open_properties, .onclickdata = "text/plain", .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
         ui_menuseparator();
         ui_menuitem(.label = "Close", .onclick = action_window_close);
     }
@@ -105,9 +105,9 @@
     }
     
     ui_menu("Go") {
-        ui_menuitem(.label = "Parent", .onclick = action_go_parent, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-        ui_menuitem(.label = "Back", .onclick = action_go_back, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-        ui_menuitem(.label = "Forward", .onclick = action_go_forward, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem(.label = "Parent", .onclick = action_go_parent, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem(.label = "Back", .onclick = action_go_back, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem(.label = "Forward", .onclick = action_go_forward, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
         ui_menuseparator();
         ui_menu("Repositories") {
             ui_menu_itemlist(.varname = "repolist", .getvalue = davrepo_getname, .onselect = action_repo_selected);
@@ -121,28 +121,28 @@
     // toolbar
     ui_toolbar_item("Home", .icon = UI_ICON_HOME);
     ui_toolbar_item("NewWindow", .icon = UI_ICON_NEW_WINDOW, .onclick = action_window_new);
-    ui_toolbar_item("Refresh", .icon = UI_ICON_REFRESH, .onclick = action_refresh, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+    ui_toolbar_item("Refresh", .icon = UI_ICON_REFRESH, .onclick = action_refresh, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
     ui_toolbar_menu("Repo", .label = "Repository") {
         ui_menu_itemlist(.varname = "repolist", .getvalue = davrepo_getname, .onselect = action_repo_selected);
     }
-    ui_toolbar_item("NewFolder", .icon = UI_ICON_NEW_FOLDER, .onclick = action_mkcol, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-    ui_toolbar_item("NewFile", .icon = UI_ICON_ADD, .onclick = action_newfile, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-    ui_toolbar_item("Upload", .label = "Upload", .icon = UI_ICON_UPLOAD, .onclick = action_upload_file, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-    ui_toolbar_item("Download", .icon = UI_ICON_SAVE_LOCAL, .onclick = action_download, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
-    ui_toolbar_item("Remove", .icon = UI_ICON_DELETE, .onclick = action_delete, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+    ui_toolbar_item("NewFolder", .icon = UI_ICON_NEW_FOLDER, .onclick = action_mkcol, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+    ui_toolbar_item("NewFile", .icon = UI_ICON_ADD, .onclick = action_newfile, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+    ui_toolbar_item("Upload", .label = "Upload", .icon = UI_ICON_UPLOAD, .onclick = action_upload_file, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+    ui_toolbar_item("Download", .icon = UI_ICON_SAVE_LOCAL, .onclick = action_download, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+    ui_toolbar_item("Remove", .icon = UI_ICON_DELETE, .onclick = action_delete, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
     ui_toolbar_toggleitem("LocalBrowser", .icon = UI_ICON_DOCK_LEFT, .label = "Local Browser");
     ui_toolbar_toggleitem("PreviewPane", .icon = UI_ICON_DOCK_RIGHT);
 
     ui_toolbar_appmenu() {
         ui_menuitem("New Window", .onclick = action_window_new);
         ui_menuseparator();
-        ui_menuitem(.label = "New Folder", .onclick = action_mkcol, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-        ui_menuitem(.label = "New File", .onclick = action_newfile, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-        ui_menuitem("Download", .onclick = action_download, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
-        ui_menuitem("Upload Files", .onclick = action_upload_file, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-        ui_menuitem("Upload Directory", .onclick = action_upload_dir, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-        ui_menuitem("Open Properties", .onclick = action_open_properties, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
-        ui_menuitem("Open as Text File", .onclick = action_open_properties, .onclickdata = "text/plain", .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+        ui_menuitem(.label = "New Folder", .onclick = action_mkcol, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem(.label = "New File", .onclick = action_newfile, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem("Download", .onclick = action_download, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+        ui_menuitem("Upload Files", .onclick = action_upload_file, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem("Upload Directory", .onclick = action_upload_dir, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem("Open Properties", .onclick = action_open_properties, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+        ui_menuitem("Open as Text File", .onclick = action_open_properties, .onclickdata = "text/plain", .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
         ui_menuseparator();
         ui_menu("Downloads/Uploads") {
             ui_menu_itemlist(.varname = "transferlist", .getvalue = transfers_getlabel, .onselect = action_transfer_selected);
--- a/application/davcontroller.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/application/davcontroller.c	Sat Dec 13 15:58:58 2025 +0100
@@ -78,7 +78,7 @@
 
     ui_list_update(browser->resources);
     
-    ui_set_group(ui->ctx, APP_STATE_BROWSER_SESSION);
+    ui_set_state(ui->ctx, APP_STATE_BROWSER_SESSION);
 }
 
 // ------------------------------ davbrowser_connect2repo ------------------------------
@@ -465,7 +465,7 @@
     }
     
     cxmutstr wtitle = cx_asprintf("Upload to: %s", ui_get(browser->path));
-    UiObject *dialog = ui_simple_window(wtitle.ptr, NULL);
+    UiObject *dialog = ui_simple_window(wtitle.ptr);
     free(wtitle.ptr);
 
     DavFileUpload *upload = dav_upload_create(browser, dialog, files);
@@ -476,7 +476,7 @@
 
 void davbrowser_download(UiObject *ui, DavBrowser *browser, DavResource *reslist, const char *local_path) {
     cxmutstr wtitle = cx_asprintf("Download to: %s", local_path);
-    UiObject *dialog = ui_simple_window(wtitle.ptr, NULL);
+    UiObject *dialog = ui_simple_window(wtitle.ptr);
     free(wtitle.ptr);
     
     DavFileDownload *download = dav_download_create(browser, dialog, reslist, local_path);
@@ -1114,7 +1114,7 @@
         cxmutstr errormsg = cx_asprintf("Upload failed: %d", upload->sn->error); // TODO: add full error message
         ui_dialog(event->obj, .title = "Error", .content = errormsg.ptr, .closebutton_label = "OK");
         free(errormsg.ptr);
-        ui_set_group(event->obj->ctx, RESOURCEVIEWER_STATE_MODIFIED);
+        ui_set_state(event->obj->ctx, RESOURCEVIEWER_STATE_MODIFIED);
     }
     
     free(upload->text.ptr);
@@ -1134,7 +1134,7 @@
         cxmutstr errormsg = cx_asprintf("Proppatch failed: %d", res->sn->error); // TODO: add full error message
         ui_dialog(event->obj, .title = "Error", .content = errormsg.ptr, .closebutton_label = "OK");
         free(errormsg.ptr);
-        ui_set_group(event->obj->ctx, RESOURCEVIEWER_STATE_MODIFIED);
+        ui_set_state(event->obj->ctx, RESOURCEVIEWER_STATE_MODIFIED);
         res->properties_modified = TRUE;
     } else {
         CxList *properties = res->properties->data;
@@ -1177,7 +1177,7 @@
         ui_threadpool_job(res->dav_queue, ui, jobthr_store_properties, res, uithr_store_properties_finished, res);
     }
     
-    ui_unset_group(ui->ctx, RESOURCEVIEWER_STATE_MODIFIED);
+    ui_unset_state(ui->ctx, RESOURCEVIEWER_STATE_MODIFIED);
 }
 
 void dav_resourceviewer_destroy(DavResourceViewer *res) {
@@ -1187,7 +1187,7 @@
 void dav_resourceviewer_property_remove(DavResourceViewer *res, DavPropertyList *prop) {
     if(!prop->isnew) {
         dav_remove_property_ns(res->current, prop->ns, prop->name);
-        ui_set_group(res->obj->ctx, RESOURCEVIEWER_STATE_MODIFIED);
+        ui_set_state(res->obj->ctx, RESOURCEVIEWER_STATE_MODIFIED);
         res->properties_modified = TRUE;
     }
     
@@ -1210,7 +1210,7 @@
     prop->value_simplified = NULL;
     prop->update = TRUE;
     
-    ui_set_group(res->obj->ctx, RESOURCEVIEWER_STATE_MODIFIED);
+    ui_set_state(res->obj->ctx, RESOURCEVIEWER_STATE_MODIFIED);
     res->properties_modified = TRUE;
     ui_list_update(res->properties);
 }
@@ -1230,7 +1230,7 @@
     prop->update = TRUE;
     
     ui_list_append(res->properties, prop);
-    ui_set_group(res->obj->ctx, RESOURCEVIEWER_STATE_MODIFIED);
+    ui_set_state(res->obj->ctx, RESOURCEVIEWER_STATE_MODIFIED);
     res->properties_modified = TRUE;
     ui_list_update(res->properties);
 }
--- a/application/settings.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/application/settings.c	Sat Dec 13 15:58:58 2025 +0100
@@ -54,10 +54,10 @@
     UiListSelection *selection = event->eventdata;
     SettingsWindow *settings = event->window;
     if(selection->count > 0) {
-        ui_set_group(event->obj->ctx, SETTINGS_STATE_REPOLIST_SELECTED);
+        ui_set_state(event->obj->ctx, SETTINGS_STATE_REPOLIST_SELECTED);
         settings->selected_repo = selection->rows[0];
     } else {
-        ui_unset_group(event->obj->ctx, SETTINGS_STATE_REPOLIST_SELECTED);
+        ui_unset_state(event->obj->ctx, SETTINGS_STATE_REPOLIST_SELECTED);
     }
 }
 
@@ -87,7 +87,7 @@
     dav_repository_remove_and_free(settings->config, repo);
     settings_update_repolist(settings);
     settings->selected_repo = -1;
-    ui_unset_group(event->obj->ctx, SETTINGS_STATE_REPOLIST_SELECTED);
+    ui_unset_state(event->obj->ctx, SETTINGS_STATE_REPOLIST_SELECTED);
 }
 
 static void editrepo_go_back(UiEvent *event, void *userdata) {
@@ -341,10 +341,10 @@
     UiListSelection *sel = event->eventdata;
     if(sel->count > 0) {
         settings->credentials_location_selected_index = sel->rows[0];
-        ui_set_group(event->obj->ctx, SETTINGS_STATE_CREDENTIALS_LOCATION_SELECTED);
+        ui_set_state(event->obj->ctx, SETTINGS_STATE_CREDENTIALS_LOCATION_SELECTED);
     } else {
         settings->credentials_location_selected_index = -1;
-        ui_unset_group(event->obj->ctx, SETTINGS_STATE_CREDENTIALS_LOCATION_SELECTED);
+        ui_unset_state(event->obj->ctx, SETTINGS_STATE_CREDENTIALS_LOCATION_SELECTED);
     }
 }
 
@@ -516,13 +516,13 @@
         ui_newline(dialog);
         
         ui_llabel(dialog, .label = "Type");
-        ui_combobox(dialog, .list = wdata->type, .groups = UI_GROUPS(ADDKEY_DIALOG_STATE_NO_IMPORT));
+        ui_dropdown(dialog, .list = wdata->type, .states = UI_GROUPS(ADDKEY_DIALOG_STATE_NO_IMPORT));
         ui_newline(dialog);
         
         ui_llabel(dialog, .value = wdata->message, .colspan = 2);
     }
     
-    ui_set_group(dialog->ctx, ADDKEY_DIALOG_STATE_NO_IMPORT);
+    ui_set_state(dialog->ctx, ADDKEY_DIALOG_STATE_NO_IMPORT);
     ui_list_setselection(wdata->type, 0);
     
     ui_show(dialog);
@@ -547,12 +547,12 @@
         DavCfgKey *key = ui_list_get(settings->keys_list, sel->rows[0]);
         if(key) {
             settings_edit_key(settings, key);
-            ui_set_group(event->obj->ctx, SETTINGS_STATE_KEYS_SELECTED);
+            ui_set_state(event->obj->ctx, SETTINGS_STATE_KEYS_SELECTED);
         }
     } else {
         settings->keys_selected_index = -1;
         settings_clear_key(settings);
-        ui_unset_group(event->obj->ctx, SETTINGS_STATE_KEYS_SELECTED);
+        ui_unset_state(event->obj->ctx, SETTINGS_STATE_KEYS_SELECTED);
     }
 }
 
@@ -626,7 +626,7 @@
     PwdStore *pwdstore = get_pwdstore();
     pwdstore = pwdstore ? pwdstore_clone(pwdstore) : pwdstore_new();
     
-    UiObject *obj = ui_simple_window("Settings", NULL);
+    UiObject *obj = ui_simple_window("Settings");
     ui_context_closefunc(obj->ctx, settings_close, NULL);
     SettingsWindow *wdata = ui_malloc(obj->ctx, sizeof(SettingsWindow));
     memset(wdata, 0, sizeof(SettingsWindow));
@@ -650,8 +650,8 @@
                     ui_grid(obj, .margin = 16, .columnspacing = 10, .rowspacing = 10, .fill = TRUE) {
                         ui_hbox(obj, .spacing = 4) {
                             ui_button(obj, .label = "Add", .onclick = repolist_add);
-                            ui_button(obj, .label = "Edit", .onclick = repolist_edit, .groups = UI_GROUPS(SETTINGS_STATE_REPOLIST_SELECTED));
-                            ui_button(obj, .label = "Remove", .onclick = repolist_remove, .groups = UI_GROUPS(SETTINGS_STATE_REPOLIST_SELECTED));
+                            ui_button(obj, .label = "Edit", .onclick = repolist_edit, .states = UI_GROUPS(SETTINGS_STATE_REPOLIST_SELECTED));
+                            ui_button(obj, .label = "Remove", .onclick = repolist_remove, .states = UI_GROUPS(SETTINGS_STATE_REPOLIST_SELECTED));
                         }
                         ui_newline(obj);
                         
@@ -687,7 +687,7 @@
                                 ui_llabel(obj, .label = "Credentials", .style = UI_LABEL_STYLE_TITLE, .colspan = 2);
                                 ui_newline(obj);
                                 ui_hbox(obj, .spacing = 4, .colspan = 2) {
-                                    ui_combobox(obj, .list = wdata->repo_credentials);
+                                    ui_dropdown(obj, .list = wdata->repo_credentials);
                                     ui_button(obj, .label = "New Credentials", .onclick = credentials_new);
                                 }
                                 ui_newline(obj);
@@ -707,12 +707,12 @@
                                 ui_llabel(obj, .label = "Encryption", .style = UI_LABEL_STYLE_TITLE, .colspan = 2);
                                 ui_newline(obj);
                                 
-                                ui_checkbox(obj, .label = "Enable client-side encryption", .value = wdata->repo_encryption, .colspan = 2, .enable_group = SETTINGS_STATE_REPO_ENCRYPTION);
+                                ui_checkbox(obj, .label = "Enable client-side encryption", .value = wdata->repo_encryption, .colspan = 2, .enable_state = SETTINGS_STATE_REPO_ENCRYPTION);
                                 ui_newline(obj);
                                 ui_llabel(obj, .label = "Default key");
                                 ui_hbox(obj, .spacing = 4) {
-                                    ui_combobox(obj, .list = wdata->repo_keys, .groups = UI_GROUPS(SETTINGS_STATE_REPO_ENCRYPTION));
-                                    ui_button(obj, .label = "Generate Key", .onclick = keys_add, .onclickdata = "repo", .groups = UI_GROUPS(SETTINGS_STATE_REPO_ENCRYPTION));
+                                    ui_dropdown(obj, .list = wdata->repo_keys, .states = UI_GROUPS(SETTINGS_STATE_REPO_ENCRYPTION));
+                                    ui_button(obj, .label = "Generate Key", .onclick = keys_add, .onclickdata = "repo", .states = UI_GROUPS(SETTINGS_STATE_REPO_ENCRYPTION));
                                 }
                                 ui_newline(obj);
                                 
@@ -727,7 +727,7 @@
                                 
                                 ui_llabel(obj, .label = "TLS Version");
                                 ui_hbox0(obj) {
-                                    ui_combobox(obj, .list = wdata->repo_tls_versions);
+                                    ui_dropdown(obj, .list = wdata->repo_tls_versions);
                                 }
                                 ui_newline(obj);
                                 ui_checkbox(obj, .label = "Disable TLS verification", .value = wdata->repo_disable_verification, .colspan = 2);
@@ -751,22 +751,22 @@
                 ui_vbox(obj, .spacing = 4) {
                     ui_hbox(obj, .spacing = 4) {
                         ui_button(obj, .label = "Add", .onclick = credentials_add);
-                        ui_button(obj, .label = "Remove", .onclick = credentials_remove, .groups = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED));
+                        ui_button(obj, .label = "Remove", .onclick = credentials_remove, .states = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED));
                     }
                     ui_listview(obj, .list = wdata->credentials_users, .fill = TRUE, .onselection = credentials_onselect);
                 }
                 
                 ui_grid(obj, .columnspacing = 30, .rowspacing = 10) {
                     ui_llabel(obj, .label = "Identifier");
-                    ui_textfield(obj, .value = wdata->credentials_id, .hexpand = TRUE, .groups = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED));
+                    ui_textfield(obj, .value = wdata->credentials_id, .hexpand = TRUE, .states = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED));
                     ui_newline(obj);
                     
                     ui_llabel(obj, .label = "User");
-                    ui_textfield(obj, .value = wdata->credentials_user, .hexpand = TRUE, .groups = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED));
+                    ui_textfield(obj, .value = wdata->credentials_user, .hexpand = TRUE, .states = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED));
                     ui_newline(obj);
                     
                     ui_llabel(obj, .label = "Password");
-                    ui_passwordfield(obj, .value = wdata->credentials_password, .hexpand = TRUE, .groups = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED));
+                    ui_passwordfield(obj, .value = wdata->credentials_password, .hexpand = TRUE, .states = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED));
                     ui_newline(obj);
                     
                     
@@ -784,13 +784,13 @@
 #else
                         ui_callback credentials_activate_callback = NULL;
 #endif
-                        ui_listview(obj, .list = wdata->credentials_locations, .onactivate = credentials_activate_callback, .onselection = credentials_location_onselect, .colspan = 2, .fill = TRUE, .groups = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED));
+                        ui_listview(obj, .list = wdata->credentials_locations, .onactivate = credentials_activate_callback, .onselection = credentials_location_onselect, .colspan = 2, .fill = TRUE, .states = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED));
                         ui_vbox(obj, .spacing = 4) {
-                            ui_button(obj, .label = "Add", .onclick = credentials_location_add, .groups = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED));
-                            ui_button(obj, .label = "Edit", .onclick = credentials_location_edit, .groups = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED, SETTINGS_STATE_CREDENTIALS_LOCATION_SELECTED));
-                            ui_button(obj, .label = "Remove", .onclick = credentials_location_remove, .groups = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED, SETTINGS_STATE_CREDENTIALS_LOCATION_SELECTED));
-                            ui_button(obj, .label = "Move Up", .onclick = credentials_location_up, .groups = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED, SETTINGS_STATE_CREDENTIALS_LOCATION_SELECTED));
-                            ui_button(obj, .label = "Move Down", .onclick = credentials_location_down, .groups = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED, SETTINGS_STATE_CREDENTIALS_LOCATION_SELECTED));
+                            ui_button(obj, .label = "Add", .onclick = credentials_location_add, .states = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED));
+                            ui_button(obj, .label = "Edit", .onclick = credentials_location_edit, .states = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED, SETTINGS_STATE_CREDENTIALS_LOCATION_SELECTED));
+                            ui_button(obj, .label = "Remove", .onclick = credentials_location_remove, .states = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED, SETTINGS_STATE_CREDENTIALS_LOCATION_SELECTED));
+                            ui_button(obj, .label = "Move Up", .onclick = credentials_location_up, .states = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED, SETTINGS_STATE_CREDENTIALS_LOCATION_SELECTED));
+                            ui_button(obj, .label = "Move Down", .onclick = credentials_location_down, .states = UI_GROUPS(SETTINGS_STATE_CREDENTIALS_SELECTED, SETTINGS_STATE_CREDENTIALS_LOCATION_SELECTED));
                         }
                     }
                 }
@@ -802,20 +802,20 @@
                 ui_vbox(obj, .spacing = 4) {
                     ui_hbox(obj, .spacing = 4) {
                         ui_button(obj, .label = "Add", .onclick = keys_add);
-                        ui_button(obj, .label = "Remove", .onclick = keys_remove, .groups = UI_GROUPS(SETTINGS_STATE_KEYS_SELECTED));
+                        ui_button(obj, .label = "Remove", .onclick = keys_remove, .states = UI_GROUPS(SETTINGS_STATE_KEYS_SELECTED));
                     }
                     ui_listview(obj, .list = wdata->keys_list, .fill = TRUE, .onselection = keys_onselect, .getvalue = keylist_getvalue);
                 }
                 
                 ui_grid(obj, .columnspacing = 30, .rowspacing = 10) {
                     ui_llabel(obj, .label = "Identifier");
-                    ui_textfield(obj, .value = wdata->key_name, .groups = UI_GROUPS(SETTINGS_STATE_KEYS_SELECTED));
+                    ui_textfield(obj, .value = wdata->key_name, .states = UI_GROUPS(SETTINGS_STATE_KEYS_SELECTED));
                     ui_newline(obj);
                     ui_llabel(obj, .label = "Type");
-                    ui_textfield(obj, .value = wdata->key_type, .groups = UI_GROUPS(SETTINGS_STATE_DISABLED));
+                    ui_textfield(obj, .value = wdata->key_type, .states = UI_GROUPS(SETTINGS_STATE_DISABLED));
                     ui_newline(obj);
                     ui_llabel(obj, .label = "File");
-                    ui_textfield(obj, .value = wdata->key_file, .groups = UI_GROUPS(SETTINGS_STATE_KEYS_SELECTED));
+                    ui_textfield(obj, .value = wdata->key_file, .states = UI_GROUPS(SETTINGS_STATE_KEYS_SELECTED));
                 }
             }
         }
@@ -1273,7 +1273,7 @@
     
     ui_list_update(settings->credentials_locations);
     
-    ui_set_group(settings->obj->ctx, SETTINGS_STATE_CREDENTIALS_SELECTED);
+    ui_set_state(settings->obj->ctx, SETTINGS_STATE_CREDENTIALS_SELECTED);
 }
 
 void settings_credentials_clear(SettingsWindow *settings) {
@@ -1287,7 +1287,7 @@
     ui_list_clear(settings->credentials_locations);
     ui_list_update(settings->credentials_locations);
     
-    ui_unset_group(settings->obj->ctx, SETTINGS_STATE_CREDENTIALS_SELECTED);
+    ui_unset_state(settings->obj->ctx, SETTINGS_STATE_CREDENTIALS_SELECTED);
 }
 
 int settings_credentials_save(SettingsWindow *settings) {
--- a/application/window.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/application/window.c	Sat Dec 13 15:58:58 2025 +0100
@@ -52,25 +52,25 @@
     
     // initialize the browser context menu
     ui_contextmenu(&contextmenu) {
-        ui_menuitem(.label = "New Folder", .onclick = action_mkcol, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-        ui_menuitem(.label = "New File", .onclick = action_newfile, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem(.label = "New Folder", .onclick = action_mkcol, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem(.label = "New File", .onclick = action_newfile, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
         ui_menuseparator();
-        //ui_menuitem(.label = "Cut", .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
-        //ui_menuitem(.label = "Copy", .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
-        //ui_menuitem(.label = "Paste", .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
-        ui_menuitem(.label = "Download", .onclick = action_download, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
-        ui_menuitem(.label = "Delete", .onclick = action_delete, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
-        ui_menuitem(.label = "Select All", .onclick = action_selectall, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        //ui_menuitem(.label = "Cut", .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+        //ui_menuitem(.label = "Copy", .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+        //ui_menuitem(.label = "Paste", .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+        ui_menuitem(.label = "Download", .onclick = action_download, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+        ui_menuitem(.label = "Delete", .onclick = action_delete, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
+        ui_menuitem(.label = "Select All", .onclick = action_selectall, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION));
         ui_menuseparator();
-        ui_menuitem(.label = "Rename", .onclick = action_rename, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+        ui_menuitem(.label = "Rename", .onclick = action_rename, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
         ui_menuseparator();
-        ui_menuitem("Open Properties", .onclick = action_open_properties, .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
-        ui_menuitem("Open as Text File", .onclick = action_open_properties, .onclickdata = "text/plain", .groups = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+        ui_menuitem("Open Properties", .onclick = action_open_properties, .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
+        ui_menuitem("Open as Text File", .onclick = action_open_properties, .onclickdata = "text/plain", .states = UI_GROUPS(APP_STATE_BROWSER_SESSION, APP_STATE_BROWSER_SELECTION));
     }
 }
 
 UiObject* window_create(void) {
-    UiObject* obj = ui_window("iDAV", NULL);
+    UiObject* obj = ui_window("iDAV");
     ui_window_size(obj, 900, 700);
 
     MainWindow* wdata = ui_malloc(obj->ctx, sizeof (MainWindow));
@@ -186,12 +186,12 @@
 
 void resourceviewer_new(DavBrowser *browser, const char *path, DavResourceViewType type) {
     const char *name = util_resource_name(path);
-    UiObject *win = ui_simple_window(name, NULL);
+    UiObject *win = ui_simple_window(name);
     ui_window_size(win, 600, 600);
     
     ui_headerbar(win, .showtitle = TRUE) {
         ui_headerbar_start(win) {
-            ui_button(win, .label = "Save", .onclick = action_resourceviewer_save, .groups = UI_GROUPS(RESOURCEVIEWER_STATE_MODIFIED));
+            ui_button(win, .label = "Save", .onclick = action_resourceviewer_save, .states = UI_GROUPS(RESOURCEVIEWER_STATE_MODIFIED));
         }
     }
     
@@ -264,8 +264,8 @@
                             .onactivate = action_resourceviewer_property_activate);
                     ui_hbox(win, .margin = 4, .spacing = 4) {
                         ui_button(win, .label = "Add", .onclick = action_resourceviewer_property_add);
-                        ui_button(win, .label = "Edit", .onclick = action_resourceviewer_property_edit, .groups = UI_GROUPS(RESOURCEVIEWER_STATE_PROP_SELECTED));
-                        ui_button(win, .label = "Remove", .onclick = action_resourceviewer_property_remove, .groups = UI_GROUPS(RESOURCEVIEWER_STATE_PROP_SELECTED));
+                        ui_button(win, .label = "Edit", .onclick = action_resourceviewer_property_edit, .states = UI_GROUPS(RESOURCEVIEWER_STATE_PROP_SELECTED));
+                        ui_button(win, .label = "Remove", .onclick = action_resourceviewer_property_remove, .states = UI_GROUPS(RESOURCEVIEWER_STATE_PROP_SELECTED));
                     }
                 }
             }
@@ -485,9 +485,9 @@
 void action_list_selection(UiEvent *event, void *data) {
     UiListSelection *selection = event->eventdata;
     if (selection->count > 0) {
-        ui_set_group(event->obj->ctx, APP_STATE_BROWSER_SELECTION);
+        ui_set_state(event->obj->ctx, APP_STATE_BROWSER_SELECTION);
     } else {
-        ui_unset_group(event->obj->ctx, APP_STATE_BROWSER_SELECTION);
+        ui_unset_state(event->obj->ctx, APP_STATE_BROWSER_SELECTION);
     }
 }
 
@@ -548,7 +548,7 @@
 void action_resourceviewer_text_modified(UiEvent *event, void *data) {
     DavResourceViewer *doc = event->document;
     if(doc->loaded) {
-        ui_set_group(event->obj->ctx, RESOURCEVIEWER_STATE_MODIFIED);
+        ui_set_state(event->obj->ctx, RESOURCEVIEWER_STATE_MODIFIED);
     }
 }
 
@@ -561,10 +561,10 @@
     DavResourceViewer *doc = event->document;
     UiListSelection *selection = event->eventdata;
     if(selection->count == 1) {
-        ui_set_group(event->obj->ctx, RESOURCEVIEWER_STATE_PROP_SELECTED);
+        ui_set_state(event->obj->ctx, RESOURCEVIEWER_STATE_PROP_SELECTED);
         doc->selected_property = ui_list_get(doc->properties, selection->rows[0]);
     } else {
-        ui_unset_group(event->obj->ctx, RESOURCEVIEWER_STATE_PROP_SELECTED);
+        ui_unset_state(event->obj->ctx, RESOURCEVIEWER_STATE_PROP_SELECTED);
         doc->selected_property = NULL;
     }
 }
@@ -641,11 +641,11 @@
     DavResourceViewer *res = data;
     switch(ui_get(res->property_type)) {
         case 0: {
-            ui_unset_group(event->obj->ctx, RESOURCEVIEWER_STATE_PROP_XML);
+            ui_unset_state(event->obj->ctx, RESOURCEVIEWER_STATE_PROP_XML);
             break;
         }
         case 1: {
-            ui_set_group(event->obj->ctx, RESOURCEVIEWER_STATE_PROP_XML);
+            ui_set_state(event->obj->ctx, RESOURCEVIEWER_STATE_PROP_XML);
             char *ns = ui_get(res->property_ns);
             char *nsdef = ui_get(res->property_nsdef);
             if(strlen(nsdef) == 0) {
@@ -690,7 +690,7 @@
         ui_newline(obj);
         
         ui_llabel(obj, .label = "Namespace Declarations");
-        ui_textfield(obj, .hexpand = TRUE, .value = res->property_nsdef, .groups = UI_GROUPS(RESOURCEVIEWER_STATE_PROP_XML));
+        ui_textfield(obj, .hexpand = TRUE, .value = res->property_nsdef, .states = UI_GROUPS(RESOURCEVIEWER_STATE_PROP_XML));
         ui_newline(obj);
         
         ui_textarea(obj, .value = res->property_value, .hexpand = TRUE, .vexpand = TRUE, .colspan = 2);
@@ -706,7 +706,7 @@
             ui_set(res->property_type, 0);
             ui_set(res->property_nsdef, "");
             ui_set(res->property_value, prop->value_full);
-            ui_unset_group(obj->ctx, RESOURCEVIEWER_STATE_PROP_XML);
+            ui_unset_state(obj->ctx, RESOURCEVIEWER_STATE_PROP_XML);
         } else if(prop->xml) {
             ui_set(res->property_type, 1);
             cxmutstr xml;
@@ -716,7 +716,7 @@
             ui_set(res->property_value, xml.ptr);
             free(xml.ptr);
             free(nsdef.ptr);
-            ui_set_group(obj->ctx, RESOURCEVIEWER_STATE_PROP_XML);
+            ui_set_state(obj->ctx, RESOURCEVIEWER_STATE_PROP_XML);
         }
     } else {
         ui_set(res->property_ns, "");
--- a/ucx/allocator.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/allocator.c	Sat Dec 13 15:58:58 2025 +0100
@@ -31,6 +31,35 @@
 #include <errno.h>
 #include <string.h>
 
+#ifdef _WIN32
+#include <Windows.h>
+#include <sysinfoapi.h>
+unsigned long cx_system_page_size(void) {
+    static unsigned long ps = 0;
+    if (ps == 0) {
+        SYSTEM_INFO sysinfo;
+        GetSystemInfo(&sysinfo);
+        ps = (unsigned long) sysinfo.dwPageSize;
+    }
+    return ps;
+}
+#else
+#include <unistd.h>
+unsigned long cx_system_page_size(void) {
+    static unsigned long ps = 0;
+    if (ps == 0) {
+        long sc = sysconf(_SC_PAGESIZE);
+        if (sc < 0) {
+            // fallback for systems which do not report a value here
+            ps = 4096; // LCOV_EXCL_LINE
+        } else {
+            ps = (unsigned long) sc;
+        }
+    }
+    return ps;
+}
+#endif
+
 static void *cx_malloc_stdlib(
         cx_attr_unused void *d,
         size_t n
@@ -79,6 +108,11 @@
         void **mem,
         size_t n
 ) {
+    if (n == 0) {
+        free(*mem);
+        *mem = NULL;
+        return 0;
+    }
     void *nmem = realloc(*mem, n);
     if (nmem == NULL) {
         return 1; // LCOV_EXCL_LINE
@@ -93,6 +127,11 @@
         size_t nmemb,
         size_t size
 ) {
+    if (nmemb == 0 || size == 0) {
+        free(*mem);
+        *mem = NULL;
+        return 0;
+    }
     size_t n;
     if (cx_szmul(nmemb, size, &n)) {
         errno = EOVERFLOW;
@@ -156,6 +195,11 @@
         void **mem,
         size_t n
 ) {
+    if (n == 0) {
+        cxFree(allocator, *mem);
+        *mem = NULL;
+        return 0;
+    }
     void *nmem = allocator->cl->realloc(allocator->data, *mem, n);
     if (nmem == NULL) {
         return 1; // LCOV_EXCL_LINE
@@ -171,6 +215,11 @@
         size_t nmemb,
         size_t size
 ) {
+    if (nmemb == 0 || size == 0) {
+        cxFree(allocator, *mem);
+        *mem = NULL;
+        return 0;
+    }
     void *nmem = cxReallocArray(allocator, *mem, nmemb, size);
     if (nmem == NULL) {
         return 1; // LCOV_EXCL_LINE
@@ -194,3 +243,7 @@
 ) {
     allocator->cl->free(allocator->data, mem);
 }
+
+void cxFreeDefault(void *mem) {
+    cxDefaultAllocator->cl->free(cxDefaultAllocator->data, mem);
+}
--- a/ucx/array_list.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/array_list.c	Sat Dec 13 15:58:58 2025 +0100
@@ -42,10 +42,11 @@
         cx_attr_unused CxArrayReallocator *alloc
 ) {
     size_t n;
+    // LCOV_EXCL_START
     if (cx_szmul(new_capacity, elem_size, &n)) {
         errno = EOVERFLOW;
         return NULL;
-    }
+    } // LCOV_EXCL_STOP
     return cxReallocDefault(array, n);
 }
 
@@ -66,10 +67,11 @@
 ) {
     // check for overflow
     size_t n;
+    // LCOV_EXCL_START
     if (cx_szmul(new_capacity, elem_size, &n)) {
         errno = EOVERFLOW;
         return NULL;
-    }
+    } // LCOV_EXCL_STOP
 
     // retrieve the pointer to the actual allocator
     const CxAllocator *al = alloc->allocator;
@@ -108,13 +110,11 @@
  *
  * @param current_capacity the current capacity of the array
  * @param needed_capacity the required capacity of the array
- * @param maximum_capacity the maximum capacity (given by the data type)
  * @return the new capacity
  */
 static size_t cx_array_grow_capacity(
     size_t current_capacity,
-    size_t needed_capacity,
-    size_t maximum_capacity
+    size_t needed_capacity
 ) {
     if (current_capacity >= needed_capacity) {
         return current_capacity;
@@ -125,12 +125,7 @@
     else if (cap < 1024) alignment = 64;
     else if (cap < 8192) alignment = 512;
     else alignment = 1024;
-
-    if (cap - 1 > maximum_capacity - alignment) {
-        return maximum_capacity;
-    } else {
-        return cap - (cap % alignment) + alignment;
-    }
+    return cap - (cap % alignment) + alignment;
 }
 
 int cx_array_reserve(
@@ -288,7 +283,7 @@
     const size_t newsize = oldsize < minsize ? minsize : oldsize;
 
     // reallocate if necessary
-    const size_t newcap = cx_array_grow_capacity(oldcap, newsize, max_size);
+    const size_t newcap = cx_array_grow_capacity(oldcap, newsize);
     if (newcap > oldcap) {
         // check if we need to repair the src pointer
         uintptr_t targetaddr = (uintptr_t) *target;
@@ -372,17 +367,18 @@
     if (elem_count == 0) return 0;
 
     // overflow check
+    // LCOV_EXCL_START
     if (elem_count > SIZE_MAX - *size) {
         errno = EOVERFLOW;
         return 1;
     }
+    // LCOV_EXCL_STOP
 
     // store some counts
     const size_t old_size = *size;
     const size_t old_capacity = *capacity;
     // the necessary capacity is the worst case assumption, including duplicates
-    const size_t needed_capacity = cx_array_grow_capacity(old_capacity,
-        old_size + elem_count, SIZE_MAX);
+    const size_t needed_capacity = cx_array_grow_capacity(old_capacity, old_size + elem_count);
 
     // if we need more than we have, try a reallocation
     if (needed_capacity > old_capacity) {
@@ -421,21 +417,23 @@
     // while there are both source and buffered elements left,
     // copy them interleaving
     while (si < elem_count && bi < new_size) {
-        // determine how many source elements can be inserted
+        // determine how many source elements can be inserted.
+        // the first element that shall not be inserted is the smallest element
+        // that is strictly larger than the first buffered element
+        // (located at the index of the infimum plus one).
+        // the infimum is guaranteed to exist:
+        // - if all src elements are larger,
+        //   there is no buffer, and this loop is skipped
+        // - if any src element is smaller or equal, the infimum exists
+        // - when all src elements that are smaller are copied, the second part
+        //   of this loop body will copy the remaining buffer (emptying it)
+        // Therefore, the buffer can never contain an element that is smaller
+        // than any element in the source and the infimum exists.
         size_t copy_len, bytes_copied;
-        copy_len = cx_array_binary_search_sup(
-                src,
-                elem_count - si,
-                elem_size,
-                bptr,
-                cmp_func
+        copy_len = cx_array_binary_search_inf(
+            src, elem_count - si, elem_size, bptr, cmp_func
         );
-        // binary search gives us the smallest index;
-        // we also want to include equal elements here
-        while (si + copy_len < elem_count
-                && cmp_func(bptr, src+copy_len*elem_size) == 0) {
-            copy_len++;
-        }
+        copy_len++;
 
         // copy the source elements
         if (copy_len > 0) {
@@ -512,26 +510,17 @@
             // duplicates allowed or nothing inserted yet: simply copy everything
             memcpy(dest, src, elem_size * (elem_count - si));
         } else {
-            if (dest != *target) {
-                // skip all source elements that equal the last element
-                char *last = dest - elem_size;
-                while (si < elem_count) {
-                    if (last != NULL && cmp_func(last, src) == 0) {
-                        src += elem_size;
-                        si++;
-                        (*size)--;
-                    } else {
-                        break;
-                    }
-                }
-            }
-            // we must check the elements in the chunk one by one
+            // we must check the remaining source elements one by one
+            // to skip the duplicates.
+            // Note that no source element can equal the last element in the
+            // destination, because that would have created an insertion point
+            // and a buffer, s.t. the above loop already handled the duplicates
             while (si < elem_count) {
                 // find a chain of elements that can be copied
                 size_t copy_len = 1, skip_len = 0;
                 {
                     const char *left_src = src;
-                    while (si + copy_len < elem_count) {
+                    while (si + copy_len + skip_len < elem_count) {
                         const char *right_src = left_src + elem_size;
                         int d = cmp_func(left_src,  right_src);
                         if (d < 0) {
@@ -599,7 +588,8 @@
         cmp_func, sorted_data, elem_size, elem_count, reallocator, false);
 }
 
-size_t cx_array_binary_search_inf(
+// implementation that finds ANY index
+static size_t cx_array_binary_search_inf_impl(
         const void *arr,
         size_t size,
         size_t elem_size,
@@ -644,13 +634,6 @@
         result = cmp_func(elem, arr_elem);
         if (result == 0) {
             // found it!
-            // check previous elements;
-            // when they are equal, report the smallest index
-            arr_elem -= elem_size;
-            while (pivot_index > 0 && cmp_func(elem, arr_elem) == 0) {
-                pivot_index--;
-                arr_elem -= elem_size;
-            }
             return pivot_index;
         } else if (result < 0) {
             // element is smaller than pivot, continue search left
@@ -665,6 +648,24 @@
     return result < 0 ? (pivot_index - 1) : pivot_index;
 }
 
+size_t cx_array_binary_search_inf(
+        const void *arr,
+        size_t size,
+        size_t elem_size,
+        const void *elem,
+        cx_compare_func cmp_func
+) {
+    size_t index = cx_array_binary_search_inf_impl(
+        arr, size, elem_size, elem, cmp_func);
+    // in case of equality, report the largest index
+    const char *e = ((const char *) arr) + (index + 1) * elem_size;
+    while (index + 1 < size && cmp_func(e, elem) == 0) {
+        e += elem_size;
+        index++;
+    }
+    return index;
+}
+
 size_t cx_array_binary_search(
         const void *arr,
         size_t size,
@@ -690,16 +691,25 @@
         const void *elem,
         cx_compare_func cmp_func
 ) {
-    size_t inf = cx_array_binary_search_inf(
+    size_t index = cx_array_binary_search_inf_impl(
             arr, size, elem_size, elem, cmp_func
     );
-    if (inf == size) {
-        // no infimum means, first element is supremum
+    const char *e = ((const char *) arr) + index * elem_size;
+    if (index == size) {
+        // no infimum means the first element is supremum
         return 0;
-    } else if (cmp_func(((const char *) arr) + inf * elem_size, elem) == 0) {
-        return inf;
+    } else if (cmp_func(e, elem) == 0) {
+        // found an equal element, search the smallest index
+        e -= elem_size; // e now contains the element at index-1
+        while (index > 0 && cmp_func(e, elem) == 0) {
+            e -= elem_size;
+            index--;
+        }
+        return index;
     } else {
-        return inf + 1;
+        // we already have the largest index of the infimum (by design)
+        // the next element is the supremum (or there is no supremum)
+        return index + 1;
     }
 }
 
@@ -792,14 +802,13 @@
 
     // guarantee enough capacity
     if (arl->capacity < list->collection.size + n) {
-        const size_t new_capacity = cx_array_grow_capacity(arl->capacity,
-            list->collection.size + n, SIZE_MAX);
+        const size_t new_capacity = cx_array_grow_capacity(arl->capacity,list->collection.size + n);
         if (cxReallocateArray(
                 list->collection.allocator,
                 &arl->data, new_capacity,
                 list->collection.elem_size)
         ) {
-            return 0;
+            return 0; // LCOV_EXCL_LINE
         }
         arl->capacity = new_capacity;
     }
@@ -843,7 +852,7 @@
             &arl->reallocator
     )) {
         // array list implementation is "all or nothing"
-        return 0;
+        return 0;  // LCOV_EXCL_LINE
     } else {
         return n;
     }
@@ -868,7 +877,7 @@
             &arl->reallocator
     )) {
         // array list implementation is "all or nothing"
-        return 0;
+        return 0;  // LCOV_EXCL_LINE
     } else {
         return n;
     }
@@ -895,7 +904,7 @@
     if (iter->index < list->collection.size) {
         if (cx_arl_insert_element(list,
                 iter->index + 1 - prepend, elem) == NULL) {
-            return 1;
+            return 1; // LCOV_EXCL_LINE
         }
         iter->elem_count++;
         if (prepend != 0) {
@@ -905,7 +914,7 @@
         return 0;
     } else {
         if (cx_arl_insert_element(list, list->collection.size, elem) == NULL) {
-            return 1;
+            return 1;  // LCOV_EXCL_LINE
         }
         iter->elem_count++;
         iter->index = list->collection.size;
--- a/ucx/buffer.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/buffer.c	Sat Dec 13 15:58:58 2025 +0100
@@ -32,28 +32,10 @@
 #include <string.h>
 #include <errno.h>
 
-#ifdef _WIN32
-#include <Windows.h>
-#include <sysinfoapi.h>
-static unsigned long system_page_size() {
-    static unsigned long ps = 0;
-    if (ps == 0) {
-        SYSTEM_INFO sysinfo;
-        GetSystemInfo(&sysinfo);
-        ps = sysinfo.dwPageSize;
-    }
-    return ps;
-}
-#define SYSTEM_PAGE_SIZE system_page_size()
-#else
-#include <unistd.h>
-#define SYSTEM_PAGE_SIZE sysconf(_SC_PAGESIZE)
-#endif
-
 static int buffer_copy_on_write(CxBuffer* buffer) {
     if (0 == (buffer->flags & CX_BUFFER_COPY_ON_WRITE)) return 0;
     void *newspace = cxMalloc(buffer->allocator, buffer->capacity);
-    if (NULL == newspace) return -1;
+    if (NULL == newspace) return -1;  // LCOV_EXCL_LINE
     memcpy(newspace, buffer->space, buffer->size);
     buffer->space = newspace;
     buffer->flags &= ~CX_BUFFER_COPY_ON_WRITE;
@@ -78,37 +60,24 @@
     buffer->flags = flags;
     if (!space) {
         buffer->bytes = cxMalloc(allocator, capacity);
-        if (buffer->bytes == NULL) {
-            return -1; // LCOV_EXCL_LINE
-        }
+        if (buffer->bytes == NULL) return -1; // LCOV_EXCL_LINE
         buffer->flags |= CX_BUFFER_FREE_CONTENTS;
     } else {
         buffer->bytes = space;
     }
     buffer->capacity = capacity;
+    buffer->max_capacity = SIZE_MAX;
     buffer->size = 0;
     buffer->pos = 0;
 
-    buffer->flush = NULL;
-
     return 0;
 }
 
-int cxBufferEnableFlushing(
-    CxBuffer *buffer,
-    CxBufferFlushConfig config
-) {
-    buffer->flush = cxMallocDefault(sizeof(CxBufferFlushConfig));
-    if (buffer->flush == NULL) return -1; // LCOV_EXCL_LINE
-    memcpy(buffer->flush, &config, sizeof(CxBufferFlushConfig));
-    return 0;
-}
-
 void cxBufferDestroy(CxBuffer *buffer) {
-    if (buffer->flags & CX_BUFFER_FREE_CONTENTS) {
+    if ((buffer->flags & (CX_BUFFER_FREE_CONTENTS | CX_BUFFER_DO_NOT_FREE))
+            == CX_BUFFER_FREE_CONTENTS) {
         cxFree(buffer->allocator, buffer->bytes);
     }
-    cxFreeDefault(buffer->flush);
     memset(buffer, 0, sizeof(CxBuffer));
 }
 
@@ -122,7 +91,7 @@
         allocator = cxDefaultAllocator;
     }
     CxBuffer *buf = cxMalloc(allocator, sizeof(CxBuffer));
-    if (buf == NULL) return NULL;
+    if (buf == NULL) return NULL; // LCOV_EXCL_LINE
     if (0 == cxBufferInit(buf, space, capacity, allocator, flags)) {
         return buf;
     } else {
@@ -183,6 +152,35 @@
 
 }
 
+size_t cxBufferPop(CxBuffer *buffer, size_t size, size_t nitems) {
+    size_t len;
+    if (cx_szmul(size, nitems, &len)) {
+        // LCOV_EXCL_START
+        errno = EOVERFLOW;
+        return 0;
+        // LCOV_EXCL_STOP
+    }
+    if (len == 0) return 0;
+    if (len > buffer->size) {
+        if (size == 1) {
+            // simple case: everything can be discarded
+            len = buffer->size;
+        } else {
+            // complicated case: misaligned bytes must stay
+            size_t misalignment = buffer->size % size;
+            len = buffer->size - misalignment;
+        }
+    }
+    buffer->size -= len;
+
+    // adjust position, if required
+    if (buffer->pos > buffer->size) {
+        buffer->pos = buffer->size;
+    }
+
+    return len / size;
+}
+
 void cxBufferClear(CxBuffer *buffer) {
     if (0 == (buffer->flags & CX_BUFFER_COPY_ON_WRITE)) {
         memset(buffer->bytes, 0, buffer->size);
@@ -200,36 +198,13 @@
     return buffer->pos >= buffer->size;
 }
 
-int cxBufferMinimumCapacity(
-        CxBuffer *buffer,
-        size_t newcap
-) {
-    if (newcap <= buffer->capacity) {
+int cxBufferReserve(CxBuffer *buffer, size_t newcap) {
+    if (newcap == buffer->capacity) {
         return 0;
     }
-
-    unsigned long pagesize = SYSTEM_PAGE_SIZE;
-    // if page size is larger than 64 KB - for some reason - truncate to 64 KB
-    if (pagesize > 65536) pagesize = 65536;
-    if (newcap < pagesize) {
-        // when smaller as one page, map to the next power of two
-        newcap--;
-        newcap |= newcap >> 1;
-        newcap |= newcap >> 2;
-        newcap |= newcap >> 4;
-        // last operation only needed for pages larger 4096 bytes
-        // but if/else would be more expensive than just doing this
-        newcap |= newcap >> 8;
-        newcap++;
-    } else {
-        // otherwise, map to a multiple of the page size
-        newcap -= newcap % pagesize;
-        newcap += pagesize;
-        // note: if newcap is already page aligned,
-        // this gives a full additional page (which is good)
+    if (newcap > buffer->max_capacity) {
+        return -1;
     }
-
-
     const int force_copy_flags = CX_BUFFER_COPY_ON_WRITE | CX_BUFFER_COPY_ON_EXTEND;
     if (buffer->flags & force_copy_flags) {
         void *newspace = cxMalloc(buffer->allocator, newcap);
@@ -242,13 +217,60 @@
         return 0;
     } else if (cxReallocate(buffer->allocator,
                      (void **) &buffer->bytes, newcap) == 0) {
+        buffer->flags |= CX_BUFFER_FREE_CONTENTS;
         buffer->capacity = newcap;
+        if (buffer->size > newcap) {
+            buffer->size = newcap;
+        }
         return 0;
     } else {
         return -1; // LCOV_EXCL_LINE
     }
 }
 
+int cxBufferMaximumCapacity(CxBuffer *buffer, size_t capacity) {
+    if (capacity < buffer->capacity) {
+        return -1;
+    }
+    buffer->max_capacity = capacity;
+    return 0;
+}
+
+int cxBufferMinimumCapacity(CxBuffer *buffer, size_t newcap) {
+    if (newcap <= buffer->capacity) {
+        return 0;
+    }
+    if (newcap > buffer->max_capacity) {
+        return -1;
+    }
+    if (newcap < buffer->max_capacity) {
+        unsigned long pagesize = cx_system_page_size();
+        // if page size is larger than 64 KB - for some reason - truncate to 64 KB
+        if (pagesize > 65536) pagesize = 65536;
+        if (newcap < pagesize) {
+            // when smaller as one page, map to the next power of two
+            newcap--;
+            newcap |= newcap >> 1;
+            newcap |= newcap >> 2;
+            newcap |= newcap >> 4;
+            // last operation only needed for pages larger 4096 bytes
+            // but if/else would be more expensive than just doing this
+            newcap |= newcap >> 8;
+            newcap++;
+        } else {
+            // otherwise, map to a multiple of the page size
+            newcap -= newcap % pagesize;
+            newcap += pagesize;
+            // note: if newcap is already page aligned,
+            // this gives a full additional page (which is good)
+        }
+        if (newcap > buffer->max_capacity) {
+            newcap = buffer->max_capacity;
+        }
+    }
+    return cxBufferReserve(buffer, newcap);
+}
+
 void cxBufferShrink(
         CxBuffer *buffer,
         size_t reserve
@@ -271,60 +293,15 @@
     }
 }
 
-static size_t cx_buffer_flush_helper(
-        const CxBuffer *buffer,
-        const unsigned char *src,
-        size_t size,
-        size_t nitems
-) {
-    // flush data from an arbitrary source
-    // does not need to be the buffer's contents
-    size_t max_items = buffer->flush->blksize / size;
-    size_t fblocks = 0;
-    size_t flushed_total = 0;
-    while (nitems > 0 && fblocks < buffer->flush->blkmax) {
-        fblocks++;
-        size_t items = nitems > max_items ? max_items : nitems;
-        size_t flushed = buffer->flush->wfunc(
-            src, size, items, buffer->flush->target);
-        if (flushed > 0) {
-            flushed_total += flushed;
-            src += flushed * size;
-            nitems -= flushed;
-        } else {
-            // if no bytes can be flushed out anymore, we give up
-            break;
-        }
-    }
-    return flushed_total;
-}
-
-static size_t cx_buffer_flush_impl(CxBuffer *buffer, size_t size) {
-    // flush the current contents of the buffer
-    unsigned char *space = buffer->bytes;
-    size_t remaining = buffer->pos / size;
-    size_t flushed_total = cx_buffer_flush_helper(
-        buffer, space, size, remaining);
-
-    // shift the buffer left after flushing
-    // IMPORTANT: up to this point, copy on write must have been
-    // performed already, because we can't do error handling here
-    cxBufferShiftLeft(buffer, flushed_total*size);
-
-    return flushed_total;
-}
-
-size_t cxBufferFlush(CxBuffer *buffer) {
-    if (buffer_copy_on_write(buffer)) return 0;
-    return cx_buffer_flush_impl(buffer, 1);
-}
-
 size_t cxBufferWrite(
         const void *ptr,
         size_t size,
         size_t nitems,
         CxBuffer *buffer
 ) {
+    // trivial case
+    if (size == 0 || nitems == 0) return 0;
+
     // optimize for easy case
     if (size == 1 && (buffer->capacity - buffer->pos) >= nitems) {
         if (buffer_copy_on_write(buffer)) return 0;
@@ -336,98 +313,52 @@
         return nitems;
     }
 
-    size_t len, total_flushed = 0;
-cx_buffer_write_retry:
+    size_t len;
     if (cx_szmul(size, nitems, &len)) {
         errno = EOVERFLOW;
-        return total_flushed;
+        return 0;
     }
     if (buffer->pos > SIZE_MAX - len) {
         errno = EOVERFLOW;
-        return total_flushed;
+        return 0;
     }
+    const size_t required = buffer->pos + len;
 
-    size_t required = buffer->pos + len;
-    bool perform_flush = false;
+    // check if we need to auto-extend
     if (required > buffer->capacity) {
         if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
-            if (buffer->flush != NULL && required > buffer->flush->threshold) {
-                perform_flush = true;
-            } else {
-                if (cxBufferMinimumCapacity(buffer, required)) {
-                    return total_flushed; // LCOV_EXCL_LINE
-                }
-            }
-        } else {
-            if (buffer->flush != NULL) {
-                perform_flush = true;
-            } else {
-                // truncate data, if we can neither extend nor flush
-                len = buffer->capacity - buffer->pos;
-                if (size > 1) {
-                    len -= len % size;
-                }
-                nitems = len / size;
+            size_t newcap = required < buffer->max_capacity
+                    ? required : buffer->max_capacity;
+            if (cxBufferMinimumCapacity(buffer, newcap)) {
+                return 0; // LCOV_EXCL_LINE
             }
         }
     }
 
+    // check again and truncate data if capacity is still not enough
+    if (required > buffer->capacity) {
+        len = buffer->capacity - buffer->pos;
+        if (size > 1) {
+            len -= len % size;
+        }
+        nitems = len / size;
+    }
+
     // check here and not above because of possible truncation
     if (len == 0) {
-        return total_flushed;
+        return 0;
     }
 
     // check if we need to copy
     if (buffer_copy_on_write(buffer)) return 0;
 
     // perform the operation
-    if (perform_flush) {
-        size_t items_flushed;
-        if (buffer->pos == 0) {
-            // if we don't have data in the buffer, but are instructed
-            // to flush, it means that we are supposed to relay the data
-            items_flushed = cx_buffer_flush_helper(buffer, ptr, size, nitems);
-            if (items_flushed == 0) {
-                // we needed to relay data, but could not flush anything
-                // i.e. we have to give up to avoid endless trying
-                return 0;
-            }
-            nitems -= items_flushed;
-            total_flushed += items_flushed;
-            if (nitems > 0) {
-                ptr = ((unsigned char*)ptr) + items_flushed * size;
-                goto cx_buffer_write_retry;
-            }
-            return total_flushed;
-        } else {
-            items_flushed = cx_buffer_flush_impl(buffer, size);
-            if (items_flushed == 0) {
-                // flush target is full, let's try to truncate
-                size_t remaining_space;
-                if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
-                    remaining_space = buffer->flush->threshold > buffer->pos
-                                          ? buffer->flush->threshold - buffer->pos
-                                          : 0;
-                } else {
-                    remaining_space = buffer->capacity > buffer->pos
-                                          ? buffer->capacity - buffer->pos
-                                          : 0;
-                }
-                nitems = remaining_space / size;
-                if (nitems == 0) {
-                    return total_flushed;
-                }
-            }
-            goto cx_buffer_write_retry;
-        }
-    } else {
-        memcpy(buffer->bytes + buffer->pos, ptr, len);
-        buffer->pos += len;
-        if (buffer->pos > buffer->size) {
-            buffer->size = buffer->pos;
-        }
-        return total_flushed + nitems;
+    memcpy(buffer->bytes + buffer->pos, ptr, len);
+    buffer->pos += len;
+    if (buffer->pos > buffer->size) {
+        buffer->size = buffer->pos;
     }
+    return nitems;
 }
 
 size_t cxBufferAppend(
@@ -436,20 +367,13 @@
         size_t nitems,
         CxBuffer *buffer
 ) {
-    size_t pos = buffer->pos;
-    size_t append_pos = buffer->size;
-    buffer->pos = append_pos;    
-    size_t written = cxBufferWrite(ptr, size, nitems, buffer);
-    // the buffer might have been flushed
-    // we must compute a possible delta for the position
-    // expected: pos = append_pos + written
-    // -> if this is not the case, there is a delta
-    size_t delta = append_pos + written*size - buffer->pos;
-    if (delta > pos) {
-        buffer->pos = 0;
-    } else {
-        buffer->pos = pos - delta;
-    }
+    // trivial case
+    if (size == 0 || nitems == 0) return 0;
+
+    const size_t pos = buffer->pos;
+    buffer->pos = buffer->size;
+    const size_t written = cxBufferWrite(ptr, size, nitems, buffer);
+    buffer->pos = pos;
     return written;
 }
 
@@ -467,19 +391,35 @@
 }
 
 int cxBufferTerminate(CxBuffer *buffer) {
-    if (0 == cxBufferPut(buffer, 0)) {
-        buffer->size = buffer->pos - 1;
-        return 0;
+    // try to extend / shrink the buffer
+    if (buffer->pos >= buffer->capacity) {
+        if ((buffer->flags & CX_BUFFER_AUTO_EXTEND) == 0) {
+            return -1;
+        }
+        if (cxBufferReserve(buffer, buffer->pos + 1)) {
+            return -1; // LCOV_EXCL_LINE
+        }
     } else {
-        return -1;
+        buffer->size = buffer->pos;
+        cxBufferShrink(buffer, 1);
+        // set the capacity explicitly, in case shrink was skipped due to CoW
+        buffer->capacity = buffer->size + 1;
     }
+
+    // check if we are still on read-only memory
+    if (buffer_copy_on_write(buffer)) return -1;
+
+    // write the terminator and exit
+    buffer->space[buffer->pos] = '\0';
+    return 0;
 }
 
-size_t cxBufferPutString(
-        CxBuffer *buffer,
-        const char *str
-) {
-    return cxBufferWrite(str, 1, strlen(str), buffer);
+size_t cx_buffer_put_string(CxBuffer *buffer, cxstring str) {
+    return cxBufferWrite(str.ptr, 1, str.length, buffer);
+}
+
+size_t cx_buffer_append_string(CxBuffer *buffer, cxstring str) {
+    return cxBufferAppend(str.ptr, 1, str.length, buffer);
 }
 
 size_t cxBufferRead(
--- a/ucx/cx/allocator.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/cx/allocator.h	Sat Dec 13 15:58:58 2025 +0100
@@ -146,6 +146,17 @@
                              const CxAllocator *allocator, void *data);
 
 /**
+ * Returns the system's memory page size.
+ *
+ * If the page size cannot be retrieved from the system,
+ * a default of 4096 bytes is assumed.
+ *
+ * @return the system's memory page size in bytes
+ */
+cx_attr_nodiscard
+CX_EXPORT unsigned long cx_system_page_size(void);
+
+/**
  * Reallocate a previously allocated block and changes the pointer in-place,
  * if necessary.
  *
@@ -428,9 +439,9 @@
  */
 #define cxReallocArrayDefault(...) cxReallocArray(cxDefaultAllocator, __VA_ARGS__)
 /**
- * Convenience macro that invokes cxFree() with the cxDefaultAllocator.
+ * Convenience function that invokes cxFree() with the cxDefaultAllocator.
  */
-#define cxFreeDefault(...) cxFree(cxDefaultAllocator, __VA_ARGS__)
+CX_EXPORT void cxFreeDefault(void *mem);
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/cx/array_list.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/cx/array_list.h	Sat Dec 13 15:58:58 2025 +0100
@@ -676,6 +676,9 @@
  * in @p arr that is less or equal to @p elem with respect to @p cmp_func.
  * When no such element exists, @p size is returned.
  *
+ * When such an element exists more than once, the largest index of all those
+ * elements is returned.
+ *
  * If @p elem is contained in the array, this is identical to
  * #cx_array_binary_search().
  *
@@ -698,6 +701,9 @@
 /**
  * Searches an item in a sorted array.
  *
+ * When such an element exists more than once, the largest index of all those
+ * elements is returned.
+ *
  * If the array is not sorted with respect to the @p cmp_func, the behavior
  * is undefined.
  *
@@ -722,6 +728,9 @@
  * in @p arr that is greater or equal to @p elem with respect to @p cmp_func.
  * When no such element exists, @p size is returned.
  *
+ * When such an element exists more than once, the smallest index of all those
+ * elements is returned.
+ *
  * If @p elem is contained in the array, this is identical to
  * #cx_array_binary_search().
  *
--- a/ucx/cx/buffer.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/cx/buffer.h	Sat Dec 13 15:58:58 2025 +0100
@@ -48,6 +48,7 @@
 
 #include "common.h"
 #include "allocator.h"
+#include "string.h"
 
 #ifdef __cplusplus
 extern "C" {
@@ -89,6 +90,13 @@
 #define CX_BUFFER_COPY_ON_EXTEND 0x08
 
 /**
+ * If this flag is enabled, the buffer will never free its contents regardless of #CX_BUFFER_FREE_CONTENTS.
+ *
+ * This is useful, for example, when you want to keep a pointer to the data after destroying the buffer.
+ */
+#define CX_BUFFER_DO_NOT_FREE 0x10
+
+/**
  * Function pointer for cxBufferWrite that is compatible with cx_write_func.
  * @see cx_write_func
  */
@@ -99,59 +107,6 @@
  */
 #define cxBufferReadFunc  ((cx_read_func) cxBufferRead)
 
-/**
- * Configuration for automatic flushing.
- */
-struct cx_buffer_flush_config_s {
-    /**
-     * The buffer may not extend beyond this threshold before starting to flush.
-     *
-     * Only used when the buffer uses #CX_BUFFER_AUTO_EXTEND.
-     * The threshold will be the maximum capacity the buffer is extended to
-     * before flushing.
-     */
-    size_t threshold;
-    /**
-     * The block size for the elements to flush.
-     */
-    size_t blksize;
-    /**
-     * The maximum number of blocks to flush in one cycle.
-     *
-     * @attention while it is guaranteed that cxBufferFlush() will not flush
-     * more blocks, this is not necessarily the case for cxBufferWrite().
-     * After performing a flush cycle, cxBufferWrite() will retry the write
-     * operation and potentially trigger another flush cycle, until the
-     * flush target accepts no more data.
-     */
-    size_t blkmax;
-
-    /**
-     * The target for the write function.
-     */
-    void *target;
-
-    /**
-     * The write-function used for flushing.
-     * If NULL, the flushed content gets discarded.
-     */
-    cx_write_func wfunc;
-};
-
-/**
- * Type alias for the flush configuration struct.
- *
- * @code
- * struct cx_buffer_flush_config_s {
- *     size_t threshold;
- *     size_t blksize;
- *     size_t blkmax;
- *     void *target;
- *     cx_write_func wfunc;
- * };
- * @endcode
- */
-typedef struct cx_buffer_flush_config_s CxBufferFlushConfig;
 
 /** Structure for the UCX buffer data. */
 struct cx_buffer_s {
@@ -168,16 +123,12 @@
     };
     /** The allocator to use for automatic memory management. */
     const CxAllocator *allocator;
-    /**
-     * Optional flush configuration
-     *
-     * @see cxBufferEnableFlushing()
-     */
-    CxBufferFlushConfig *flush;
     /** Current position of the buffer. */
     size_t pos;
     /** Current capacity (i.e. maximum size) of the buffer. */
     size_t capacity;
+    /** Maximum capacity that this buffer may grow to. */
+    size_t max_capacity;
     /** Current size of the buffer content. */
     size_t size;
     /**
@@ -231,23 +182,6 @@
         const CxAllocator *allocator, int flags);
 
 /**
- * Configures the buffer for flushing.
- *
- * Flushing can happen automatically when data is written
- * to the buffer (see cxBufferWrite()) or manually when
- * cxBufferFlush() is called.
- *
- * @param buffer the buffer
- * @param config the flush configuration
- * @retval zero success
- * @retval non-zero failure
- * @see cxBufferFlush()
- * @see cxBufferWrite()
- */
-cx_attr_nonnull
-CX_EXPORT int cxBufferEnableFlushing(CxBuffer *buffer, CxBufferFlushConfig config);
-
-/**
  * Destroys the buffer contents.
  *
  * Has no effect if the #CX_BUFFER_FREE_CONTENTS feature is not enabled.
@@ -388,6 +322,20 @@
 CX_EXPORT int cxBufferSeek(CxBuffer *buffer, off_t offset, int whence);
 
 /**
+ * Discards items from the end of the buffer.
+ *
+ * When the current position points to a byte that gets discarded,
+ * the position is set to the buffer size.
+ *
+ * @param buffer the buffer
+ * @param size the size of one item
+ * @param nitems the number of items to discard
+ * @return the actual number of discarded items
+ */
+cx_attr_nonnull
+CX_EXPORT size_t cxBufferPop(CxBuffer *buffer, size_t size, size_t nitems);
+
+/**
  * Clears the buffer by resetting the position and deleting the data.
  *
  * The data is deleted by zeroing it with a call to memset().
@@ -425,11 +373,51 @@
 cx_attr_nonnull cx_attr_nodiscard
 CX_EXPORT bool cxBufferEof(const CxBuffer *buffer);
 
+/**
+ * Ensures that the buffer has the required capacity.
+ *
+ * If the current capacity is not sufficient, the buffer will be extended.
+ * If the current capacity is larger, the buffer is shrunk and superfluous
+ * content is discarded.
+ *
+ * This function will reserve no more bytes than requested, in contrast to
+ * cxBufferMinimumCapacity(), which may reserve more bytes to improve the
+ * number of future necessary reallocations.
+ *
+ * @param buffer the buffer
+ * @param capacity the required capacity for this buffer
+ * @retval zero on success
+ * @retval non-zero on allocation failure
+ * @see cxBufferShrink()
+ * @see cxBufferMinimumCapacity()
+ */
+cx_attr_nonnull
+CX_EXPORT int cxBufferReserve(CxBuffer *buffer, size_t capacity);
+
+/**
+ * Limits the buffer's capacity.
+ *
+ * If the current capacity is already larger, this function fails and returns
+ * non-zero.
+ *
+ * The capacity limit will affect auto-extension features, as well as future
+ * calls to cxBufferMinimumCapacity() and cxBufferReserve().
+ *
+ * @param buffer the buffer
+ * @param capacity the maximum allowed capacity for this buffer
+ * @retval zero the limit is applied
+ * @retval non-zero the new limit is smaller than the current capacity
+ * @see cxBufferReserve()
+ * @see cxBufferMinimumCapacity()
+ */
+cx_attr_nonnull
+CX_EXPORT int cxBufferMaximumCapacity(CxBuffer *buffer, size_t capacity);
 
 /**
  * Ensures that the buffer has a minimum capacity.
  *
- * If the current capacity is not sufficient, the buffer will be extended.
+ * If the current capacity is not sufficient, the buffer will be generously
+ * extended.
  *
  * The new capacity will be a power of two until the system's page size is reached.
  * Then, the new capacity will be a multiple of the page size.
@@ -438,6 +426,8 @@
  * @param capacity the minimum required capacity for this buffer
  * @retval zero the capacity was already sufficient or successfully increased
  * @retval non-zero on allocation failure
+ * @see cxBufferMaximumCapacity()
+ * @see cxBufferReserve()
  * @see cxBufferShrink()
  */
 cx_attr_nonnull
@@ -457,6 +447,7 @@
  *
  * @param buffer the buffer
  * @param reserve the number of bytes that shall remain reserved
+ * @see cxBufferReserve()
  * @see cxBufferMinimumCapacity()
  */
 cx_attr_nonnull
@@ -465,33 +456,13 @@
 /**
  * Writes data to a CxBuffer.
  *
- * If automatic flushing is not enabled, the data is simply written into the
- * buffer at the current position, and the position of the buffer is increased
- * by the number of bytes written.
- *
- * If flushing is enabled and the buffer needs to flush, the data is flushed to
- * the target until the target signals that it cannot take more data by
- * returning zero via the respective write function. In that case, the remaining
- * data in this buffer is shifted to the beginning of this buffer so that the
- * newly available space can be used to append as much data as possible.
- *
- * This function only stops writing more elements when the flush target and this
- * buffer are both incapable of taking more data or all data has been written.
+ * If auto-extension is enabled, the buffer's capacity is automatically
+ * increased when it is not large enough to hold all data.
+ * By default, the capacity grows indefinitely, unless limited with
+ * cxBufferMaximumCapacity().
+ * When auto-extension fails, this function writes no data and returns zero.
  *
- * If, after flushing, the number of items that shall be written still exceeds
- * the capacity or flush threshold, this function tries to write all items directly
- * to the flush target, if possible.
- *
- * The number returned by this function is the number of elements from
- * @c ptr that could be written to either the flush target or the buffer.
- * That means it does @em not include the number of items that were already in
- * the buffer and were also flushed during the process.
- *
- * @attention
- * When @p size is larger than one and the contents of the buffer are not aligned
- * with @p size, flushing stops after all complete items have been flushed, leaving
- * the misaligned part in the buffer.
- * Afterward, this function only writes as many items as possible to the buffer.
+ * The position of the buffer is moved alongside the written data.
  *
  * @note The signature is compatible with the fwrite() family of functions.
  *
@@ -531,62 +502,6 @@
         size_t nitems, CxBuffer *buffer);
 
 /**
- * Performs a single flush-run on the specified buffer.
- *
- * Does nothing when the position in the buffer is zero.
- * Otherwise, the data until the current position minus
- * one is considered for flushing.
- * Note carefully that flushing will never exceed the
- * current @em position, even when the size of the
- * buffer is larger than the current position.
- *
- * One flush run will try to flush @c blkmax many
- * blocks of size @c blksize until either the @p buffer
- * has no more data to flush or the write function
- * used for flushing returns zero.
- *
- * The buffer is shifted left for that many bytes
- * the flush operation has successfully flushed.
- *
- * @par Example 1
- * Assume you have a buffer with size 340 and you are
- * at position 200. The flush configuration is
- * @c blkmax=4 and @c blksize=64 .
- * Assume that the entire flush operation is successful.
- * All 200 bytes on the left-hand-side from the current
- * position are written.
- * That means the size of the buffer is now 140 and the
- * position is zero.
- *
- * @par Example 2
- * Same as Example 1, but now the @c blkmax is 1.
- * The size of the buffer is now 276, and the position is 136.
- *
- * @par Example 3
- * Same as Example 1, but now assume the flush target
- * only accepts 100 bytes before returning zero.
- * That means the flush operation manages to flush
- * one complete block and one partial block, ending
- * up with a buffer with size 240 and position 100.
- *
- * @remark Just returns zero when flushing was not enabled with
- * cxBufferEnableFlushing().
- *
- * @remark When the buffer uses copy-on-write, the memory
- * is copied first, before attempting any flush.
- * This is, however, considered an erroneous use of the
- * buffer because it makes little sense to put
- * readonly data into an UCX buffer for flushing instead
- * of writing it directly to the target.
- *
- * @param buffer the buffer
- * @return the number of successfully flushed bytes
- * @see cxBufferEnableFlushing()
- */
-cx_attr_nonnull
-CX_EXPORT size_t cxBufferFlush(CxBuffer *buffer);
-
-/**
  * Reads data from a CxBuffer.
  *
  * The position of the buffer is increased by the number of bytes read.
@@ -610,8 +525,9 @@
  *
  * The least significant byte of the argument is written to the buffer. If the
  * end of the buffer is reached and #CX_BUFFER_AUTO_EXTEND feature is enabled,
- * the buffer capacity is extended by cxBufferMinimumCapacity(). If the feature
- * is disabled or the buffer extension fails, @c EOF is returned.
+ * the buffer capacity is extended, unless a limit set by
+ * cxBufferMaximumCapacity() is reached.
+ * If the feature is disabled or the buffer extension fails, @c EOF is returned.
  *
  * On successful writing, the position of the buffer is increased.
  *
@@ -620,8 +536,8 @@
  *
  * @param buffer the buffer to write to
  * @param c the character to write
- * @return the byte that has been written or @c EOF when the end of the stream is
- * reached, and automatic extension is not enabled or not possible
+ * @return the byte that has been written or @c EOF when the end of the
+ * stream is reached, and automatic extension is not enabled or not possible
  * @see cxBufferTerminate()
  */
 cx_attr_nonnull
@@ -630,28 +546,61 @@
 /**
  * Writes a terminating zero to a buffer at the current position.
  *
- * If successful, sets the size to the current position and advances the position by one.
+ * If successful, also sets the size to the current position and shrinks the buffer.
  *
  * The purpose of this function is to have the written data ready to be used as
  * a C string with the buffer's size being the length of that string.
  *
  * @param buffer the buffer to write to
  * @return zero, if the terminator could be written, non-zero otherwise
+ * @see cxBufferShrink()
  */
 cx_attr_nonnull
 CX_EXPORT int cxBufferTerminate(CxBuffer *buffer);
 
 /**
- * Writes a string to a buffer.
- *
- * This is a convenience function for <code>cxBufferWrite(str, 1, strlen(str), buffer)</code>.
+ * Internal function - do not use.
  *
  * @param buffer the buffer
- * @param str the zero-terminated string
+ * @param str the string
  * @return the number of bytes written
+ * @see cxBufferPutString()
+ */
+cx_attr_nonnull
+CX_EXPORT size_t cx_buffer_put_string(CxBuffer *buffer, cxstring str);
+
+/**
+ * Writes a string to a buffer with cxBufferWrite().
+ *
+ * @param buffer (@c CxBuffer*) the buffer
+ * @param str (any string) the zero-terminated string
+ * @return (@c size_t) the number of bytes written
+ * @see cxBufferWrite()
+ * @see cx_strcast()
  */
-cx_attr_nonnull cx_attr_cstr_arg(2)
-CX_EXPORT size_t cxBufferPutString(CxBuffer *buffer, const char *str);
+#define cxBufferPutString(buffer, str) cx_buffer_put_string(buffer, cx_strcast(str))
+
+/**
+ * Internal function - do not use.
+ *
+ * @param buffer the buffer
+ * @param str the string
+ * @return the number of bytes written
+ * @see cxBufferPutString()
+ */
+cx_attr_nonnull
+CX_EXPORT size_t cx_buffer_append_string(CxBuffer *buffer, cxstring str);
+
+/**
+ * Appends a string to a buffer with cxBufferAppend().
+ *
+ * @param buffer (@c CxBuffer*) the buffer
+ * @param str (any string) the zero-terminated string
+ * @return (@c size_t) the number of bytes written
+ * @see cxBufferAppend()
+ * @see cx_strcast()
+ */
+#define cxBufferAppendString(buffer, str) cx_buffer_append_string(buffer, cx_strcast(str))
 
 /**
  * Gets a character from a buffer.
--- a/ucx/cx/common.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/cx/common.h	Sat Dec 13 15:58:58 2025 +0100
@@ -80,10 +80,10 @@
 #define UCX_COMMON_H
 
 /** Major UCX version as integer constant. */
-#define UCX_VERSION_MAJOR   3
+#define UCX_VERSION_MAJOR   4
 
 /** Minor UCX version as integer constant. */
-#define UCX_VERSION_MINOR   1
+#define UCX_VERSION_MINOR   0
 
 /** Version constant which ensures to increase monotonically. */
 #define UCX_VERSION (((UCX_VERSION_MAJOR)<<16)|UCX_VERSION_MINOR)
@@ -284,6 +284,9 @@
  */
 #define CX_INLINE __attribute__((always_inline)) static inline
 #else
+/**
+ * Declares a function to be inlined.
+ */
 #define CX_INLINE static inline
 #endif
 /**
--- a/ucx/cx/json.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/cx/json.h	Sat Dec 13 15:58:58 2025 +0100
@@ -41,6 +41,7 @@
 #include "string.h"
 #include "buffer.h"
 #include "array_list.h"
+#include "map.h"
 
 #include <string.h>
 
@@ -188,9 +189,10 @@
  */
 typedef struct cx_json_array_s CxJsonArray;
 /**
- * Type alias for the JSON object struct.
+ * Type alias for the map representing a JSON object.
+ * The map contains pointers of type @c CxJsonValue.
  */
-typedef struct cx_json_object_s CxJsonObject;
+typedef CxMap* CxJsonObject;
 /**
  * Type alias for a JSON string.
  */
@@ -209,46 +211,13 @@
 typedef enum cx_json_literal CxJsonLiteral;
 
 /**
- * Type alias for a key/value pair in a JSON object.
- */
-typedef struct cx_json_obj_value_s CxJsonObjValue;
-
-/**
  * JSON array structure.
  */
 struct cx_json_array_s {
     /**
      * The array data.
      */
-    CX_ARRAY_DECLARE(CxJsonValue*, array);
-};
-
-/**
- * JSON object structure.
- */
-struct cx_json_object_s {
-    /**
-     * The key/value entries.
-     */
-    CX_ARRAY_DECLARE(CxJsonObjValue, values);
-    /**
-     * The original indices to reconstruct the order in which the members were added.
-     */
-    size_t *indices;
-};
-
-/**
- * Structure for a key/value entry in a JSON object.
- */
-struct cx_json_obj_value_s {
-    /**
-     * The key (or name in JSON terminology) of the value.
-     */
-    cxmutstr name;
-    /**
-     * The value.
-     */
-    CxJsonValue *value;
+    CX_ARRAY_DECLARE(CxJsonValue*, data);
 };
 
 /**
@@ -295,7 +264,7 @@
          * The literal type if the type is #CX_JSON_LITERAL.
          */
         CxJsonLiteral literal;
-    } value;
+    };
 };
 
 /**
@@ -349,11 +318,11 @@
     CxJsonValue *parsed;
 
     /**
-     * A pointer to an intermediate state of a currently parsed object member.
+     * The name of a not yet completely parsed object member.
      *
      * Never access this value manually.
      */
-    CxJsonObjValue uncompleted_member;
+    cxmutstr uncompleted_member_name;
 
     /**
      * State stack.
@@ -439,10 +408,6 @@
      */
     bool pretty;
     /**
-     * Set false to output the members in the order in which they were added.
-     */
-    bool sort_members;
-    /**
      * The maximum number of fractional digits in a number value.
      * The default value is 6 and values larger than 15 are reduced to 15.
      * Note that the actual number of digits may be lower, depending on the concrete number.
@@ -508,6 +473,33 @@
 CX_EXPORT int cxJsonWrite(void* target, const CxJsonValue* value,
         cx_write_func wfunc, const CxJsonWriter* settings);
 
+
+/**
+ * Produces a compact string representation of the specified JSON value.
+ *
+ * @param value the JSON value
+ * @param allocator the allocator for the string
+ * @return the produced string
+ * @see cxJsonWrite()
+ * @see cxJsonWriterCompact()
+ * @see cxJsonToPrettyString()
+ */
+cx_attr_nonnull_arg(1)
+CX_EXPORT cxmutstr cxJsonToString(CxJsonValue *value, const CxAllocator *allocator);
+
+/**
+ * Produces a pretty string representation of the specified JSON value.
+ *
+ * @param value the JSON value
+ * @param allocator the allocator for the string
+ * @return the produced string
+ * @see cxJsonWrite()
+ * @see cxJsonWriterPretty()
+ * @see cxJsonToString()
+ */
+cx_attr_nonnull_arg(1)
+CX_EXPORT cxmutstr cxJsonToPrettyString(CxJsonValue *value, const CxAllocator *allocator);
+
 /**
  * Initializes the JSON interface.
  *
@@ -530,8 +522,8 @@
 /**
  * Destroys and re-initializes the JSON interface.
  *
- * You might want to use this to reset the parser after
- * encountering a syntax error.
+ * You must use this to reset the parser after encountering a syntax error
+ * if you want to continue using it.
  *
  * @param json the JSON interface
  */
@@ -556,7 +548,7 @@
  * @retval non-zero internal allocation error
  * @see cxJsonFill()
  */
-cx_attr_nonnull cx_attr_access_r(2, 3)
+cx_attr_nonnull_arg(1) cx_attr_access_r(2, 3)
 CX_EXPORT int cxJsonFilln(CxJson *json, const char *buf, size_t len);
 
 
@@ -592,6 +584,36 @@
  */
 #define cxJsonFill(json, str) cx_json_fill(json, cx_strcast(str))
 
+
+/**
+ * Internal function - use cxJsonFromString() instead.
+ *
+ * @param allocator the allocator for the JSON value
+ * @param str the string to parse
+ * @param value a pointer where the JSON value shall be stored to
+ * @return status code
+ */
+cx_attr_nonnull_arg(3)
+CX_EXPORT CxJsonStatus cx_json_from_string(const CxAllocator *allocator,
+            cxstring str, CxJsonValue **value);
+
+/**
+ * Parses a string into a JSON value.
+ *
+ * @param allocator (@c CxAllocator*) the allocator for the JSON value
+ * @param str (any string) the string to parse
+ * @param value (@c CxJsonValue**) a pointer where the JSON value shall be stored to
+ * @retval CX_JSON_NO_ERROR success
+ * @retval CX_JSON_NO_DATA the string was empty or blank
+ * @retval CX_JSON_INCOMPLETE_DATA the string unexpectedly ended
+ * @retval CX_JSON_BUFFER_ALLOC_FAILED allocating internal buffer space failed
+ * @retval CX_JSON_VALUE_ALLOC_FAILED allocating memory for the CxJsonValue failed
+ * @retval CX_JSON_FORMAT_ERROR_NUMBER the JSON text contains an illegally formatted number
+ * @retval CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN JSON syntax error
+ */
+#define cxJsonFromString(allocator, str, value) \
+        cx_json_from_string(allocator, cx_strcast(str), value)
+
 /**
  * Creates a new (empty) JSON object.
  *
@@ -641,28 +663,27 @@
 /**
  * Creates a new JSON string.
  *
+ * Internal function - use cxJsonCreateString() instead.
+ *
  * @param allocator the allocator to use
  * @param str the string data
  * @return the new JSON value or @c NULL if allocation fails
- * @see cxJsonCreateString()
  * @see cxJsonObjPutString()
- * @see cxJsonArrAddStrings()
+ * @see cxJsonArrAddCxStrings()
  */
-cx_attr_nodiscard cx_attr_nonnull_arg(2) cx_attr_cstr_arg(2)
-CX_EXPORT CxJsonValue* cxJsonCreateString(const CxAllocator* allocator, const char *str);
+cx_attr_nodiscard
+CX_EXPORT CxJsonValue* cx_json_create_string(const CxAllocator* allocator, cxstring str);
 
 /**
  * Creates a new JSON string.
  *
- * @param allocator the allocator to use
- * @param str the string data
- * @return the new JSON value or @c NULL if allocation fails
- * @see cxJsonCreateCxString()
- * @see cxJsonObjPutCxString()
+ * @param allocator (@c CxAllocator*) the allocator to use
+ * @param str the string
+ * @return (@c CxJsonValue*) the new JSON value or @c NULL if allocation fails
+ * @see cxJsonObjPutString()
  * @see cxJsonArrAddCxStrings()
  */
-cx_attr_nodiscard
-CX_EXPORT CxJsonValue* cxJsonCreateCxString(const CxAllocator* allocator, cxstring str);
+#define cxJsonCreateString(allocator, str) cx_json_create_string(allocator, cx_strcast(str))
 
 /**
  * Creates a new JSON literal.
@@ -760,10 +781,7 @@
 /**
  * Adds or replaces a value within a JSON object.
  *
- * The value will be directly added and not copied.
- *
- * @note If a value with the specified @p name already exists,
- * it will be (recursively) freed with its own allocator.
+ * Internal function - use cxJsonObjPut().
  *
  * @param obj the JSON object
  * @param name the name of the value
@@ -772,11 +790,29 @@
  * @retval non-zero allocation failure
  */
 cx_attr_nonnull
-CX_EXPORT int cxJsonObjPut(CxJsonValue* obj, cxstring name, CxJsonValue* child);
+CX_EXPORT int cx_json_obj_put(CxJsonValue* obj, cxstring name, CxJsonValue* child);
+
+/**
+ * Adds or replaces a value within a JSON object.
+ *
+ * The value will be directly added and not copied.
+ *
+ * @note If a value with the specified @p name already exists,
+ * it will be (recursively) freed with its own allocator.
+ *
+ * @param obj (@c CxJsonValue*) the JSON object
+ * @param name (any string) the name of the value
+ * @param child (@c CxJsonValue*) the value
+ * @retval zero success
+ * @retval non-zero allocation failure
+ */
+#define cxJsonObjPut(obj, name, child) cx_json_obj_put(obj, cx_strcast(name), child)
 
 /**
  * Creates a new JSON object and adds it to an existing object.
  *
+ * Internal function - use cxJsonObjPutObj().
+ *
  * @param obj the target JSON object
  * @param name the name of the new value
  * @return the new value or @c NULL if allocation fails
@@ -784,11 +820,24 @@
  * @see cxJsonCreateObj()
  */
 cx_attr_nonnull
-CX_EXPORT CxJsonValue* cxJsonObjPutObj(CxJsonValue* obj, cxstring name);
+CX_EXPORT CxJsonValue* cx_json_obj_put_obj(CxJsonValue* obj, cxstring name);
+
+/**
+ * Creates a new JSON object and adds it to an existing object.
+ *
+ * @param obj (@c CxJsonValue*) the target JSON object
+ * @param name (any string) the name of the new value
+ * @return (@c CxJsonValue*) the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateObj()
+ */
+#define cxJsonObjPutObj(obj, name) cx_json_obj_put_obj(obj, cx_strcast(name))
 
 /**
  * Creates a new JSON array and adds it to an object.
  *
+ * Internal function - use cxJsonObjPutArr().
+ *
  * @param obj the target JSON object
  * @param name the name of the new value
  * @return the new value or @c NULL if allocation fails
@@ -796,11 +845,24 @@
  * @see cxJsonCreateArr()
  */
 cx_attr_nonnull
-CX_EXPORT CxJsonValue* cxJsonObjPutArr(CxJsonValue* obj, cxstring name);
+CX_EXPORT CxJsonValue* cx_json_obj_put_arr(CxJsonValue* obj, cxstring name);
+
+/**
+ * Creates a new JSON array and adds it to an object.
+ *
+ * @param obj (@c CxJsonValue*) the target JSON object
+ * @param name (any string) the name of the new value
+ * @return (@c CxJsonValue*) the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateArr()
+ */
+#define cxJsonObjPutArr(obj, name) cx_json_obj_put_arr(obj, cx_strcast(name))
 
 /**
  * Creates a new JSON number and adds it to an object.
  *
+ * Internal function - use cxJsonObjPutNumber().
+ *
  * @param obj the target JSON object
  * @param name the name of the new value
  * @param num the numeric value
@@ -809,11 +871,25 @@
  * @see cxJsonCreateNumber()
  */
 cx_attr_nonnull
-CX_EXPORT CxJsonValue* cxJsonObjPutNumber(CxJsonValue* obj, cxstring name, double num);
+CX_EXPORT CxJsonValue* cx_json_obj_put_number(CxJsonValue* obj, cxstring name, double num);
+
+/**
+ * Creates a new JSON number and adds it to an object.
+ *
+ * @param obj (@c CxJsonValue*) the target JSON object
+ * @param name (any string) the name of the new value
+ * @param num (@c double) the numeric value
+ * @return (@c CxJsonValue*) the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateNumber()
+ */
+#define cxJsonObjPutNumber(obj, name, num) cx_json_obj_put_number(obj, cx_strcast(name), num)
 
 /**
  * Creates a new JSON number, based on an integer, and adds it to an object.
  *
+ * Internal function - use cxJsonObjPutInteger().
+ *
  * @param obj the target JSON object
  * @param name the name of the new value
  * @param num the numeric value
@@ -822,12 +898,24 @@
  * @see cxJsonCreateInteger()
  */
 cx_attr_nonnull
-CX_EXPORT CxJsonValue* cxJsonObjPutInteger(CxJsonValue* obj, cxstring name, int64_t num);
+CX_EXPORT CxJsonValue* cx_json_obj_put_integer(CxJsonValue* obj, cxstring name, int64_t num);
+
+/**
+ * Creates a new JSON number, based on an integer, and adds it to an object.
+ *
+ * @param obj (@c CxJsonValue*) the target JSON object
+ * @param name (any string) the name of the new value
+ * @param num (@c int64_t) the numeric value
+ * @return (@c CxJsonValue*) the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateInteger()
+ */
+#define cxJsonObjPutInteger(obj, name, num) cx_json_obj_put_integer(obj, cx_strcast(name), num)
 
 /**
  * Creates a new JSON string and adds it to an object.
  *
- * The string data is copied.
+ * Internal function - use cxJsonObjPutString()
  *
  * @param obj the target JSON object
  * @param name the name of the new value
@@ -836,27 +924,28 @@
  * @see cxJsonObjPut()
  * @see cxJsonCreateString()
  */
-cx_attr_nonnull cx_attr_cstr_arg(3)
-CX_EXPORT CxJsonValue* cxJsonObjPutString(CxJsonValue* obj, cxstring name, const char* str);
+cx_attr_nonnull
+CX_EXPORT CxJsonValue* cx_json_obj_put_string(CxJsonValue* obj, cxstring name, cxstring str);
 
 /**
  * Creates a new JSON string and adds it to an object.
  *
  * The string data is copied.
  *
- * @param obj the target JSON object
- * @param name the name of the new value
- * @param str the string data
- * @return the new value or @c NULL if allocation fails
+ * @param obj (@c CxJsonValue*) the target JSON object
+ * @param name (any string) the name of the new value
+ * @param str (any string) the string data
+ * @return (@c CxJsonValue*) the new value or @c NULL if allocation fails
  * @see cxJsonObjPut()
- * @see cxJsonCreateCxString()
+ * @see cxJsonCreateString()
  */
-cx_attr_nonnull
-CX_EXPORT CxJsonValue* cxJsonObjPutCxString(CxJsonValue* obj, cxstring name, cxstring str);
+#define cxJsonObjPutString(obj, name, str) cx_json_obj_put_string(obj, cx_strcast(name), cx_strcast(str))
 
 /**
  * Creates a new JSON literal and adds it to an object.
  *
+ * Internal function - use cxJsonObjPutLiteral().
+ *
  * @param obj the target JSON object
  * @param name the name of the new value
  * @param lit the type of literal
@@ -865,7 +954,19 @@
  * @see cxJsonCreateLiteral()
  */
 cx_attr_nonnull
-CX_EXPORT CxJsonValue* cxJsonObjPutLiteral(CxJsonValue* obj, cxstring name, CxJsonLiteral lit);
+CX_EXPORT CxJsonValue* cx_json_obj_put_literal(CxJsonValue* obj, cxstring name, CxJsonLiteral lit);
+
+/**
+ * Creates a new JSON literal and adds it to an object.
+ *
+ * @param obj (@c CxJsonValue*) the target JSON object
+ * @param name (any string) the name of the new value
+ * @param lit (@c CxJsonLiteral) the type of literal
+ * @return (@c CxJsonValue*) the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateLiteral()
+ */
+#define cxJsonObjPutLiteral(obj, name, lit) cx_json_obj_put_literal(obj, cx_strcast(name), lit)
 
 /**
  * Recursively deallocates the memory of a JSON value.
@@ -998,7 +1099,7 @@
  */
 cx_attr_nonnull
 CX_INLINE bool cxJsonIsBool(const CxJsonValue *value) {
-    return cxJsonIsLiteral(value) && value->value.literal != CX_JSON_NULL;
+    return cxJsonIsLiteral(value) && value->literal != CX_JSON_NULL;
 }
 
 /**
@@ -1015,7 +1116,7 @@
  */
 cx_attr_nonnull
 CX_INLINE bool cxJsonIsTrue(const CxJsonValue *value) {
-    return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_TRUE;
+    return cxJsonIsLiteral(value) && value->literal == CX_JSON_TRUE;
 }
 
 /**
@@ -1032,7 +1133,7 @@
  */
 cx_attr_nonnull
 CX_INLINE bool cxJsonIsFalse(const CxJsonValue *value) {
-    return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_FALSE;
+    return cxJsonIsLiteral(value) && value->literal == CX_JSON_FALSE;
 }
 
 /**
@@ -1045,7 +1146,7 @@
  */
 cx_attr_nonnull
 CX_INLINE bool cxJsonIsNull(const CxJsonValue *value) {
-    return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_NULL;
+    return cxJsonIsLiteral(value) && value->literal == CX_JSON_NULL;
 }
 
 /**
@@ -1123,7 +1224,7 @@
  */
 cx_attr_nonnull
 CX_INLINE bool cxJsonAsBool(const CxJsonValue *value) {
-    return value->value.literal == CX_JSON_TRUE;
+    return value->literal == CX_JSON_TRUE;
 }
 
 /**
@@ -1137,7 +1238,7 @@
  */
 cx_attr_nonnull
 CX_INLINE size_t cxJsonArrSize(const CxJsonValue *value) {
-    return value->value.array.array_size;
+    return value->array.data_size;
 }
 
 /**
@@ -1188,10 +1289,24 @@
 CX_EXPORT CxIterator cxJsonArrIter(const CxJsonValue *value);
 
 /**
- * Returns an iterator over the JSON object members.
+ * Returns the size of a JSON object.
+ *
+ * If the @p value is not a JSON object, the behavior is undefined.
  *
- * The iterator yields values of type @c CxJsonObjValue* which
- * contain the name and value of the member.
+ * @param value the JSON value
+ * @return the size of the object, i.e., the number of key/value pairs
+ * @see cxJsonIsObject()
+ */
+cx_attr_nonnull
+CX_INLINE size_t cxJsonObjSize(const CxJsonValue *value) {
+    return cxCollectionSize(value->object);
+}
+
+/**
+ * Returns a map iterator over the JSON object members.
+ *
+ * The iterator yields values of type @c CxMapEntry* which
+ * contain the name and the @c CxJsonObjValue* of the member.
  *
  * If the @p value is not a JSON object, the behavior is undefined.
  *
@@ -1200,7 +1315,7 @@
  * @see cxJsonIsObject()
  */
 cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT CxIterator cxJsonObjIter(const CxJsonValue *value);
+CX_EXPORT CxMapIterator cxJsonObjIter(const CxJsonValue *value);
 
 /**
  * Internal function, do not use.
--- a/ucx/cx/linked_list.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/cx/linked_list.h	Sat Dec 13 15:58:58 2025 +0100
@@ -62,7 +62,14 @@
      */
     off_t loc_data;
     /**
+     * Location of extra data (optional).
+     * Negative when no extra data is requested.
+     * @see cx_linked_list_extra_data()
+     */
+    off_t loc_extra;
+    /**
      * Additional bytes to allocate @em behind the payload (e.g. for metadata).
+     * @see cx_linked_list_extra_data()
      */
     size_t extra_data_len;
     /**
@@ -112,6 +119,23 @@
         cxLinkedListCreate(NULL, NULL, elem_size)
 
 /**
+ * Instructs the linked list to reserve extra data in each node.
+ *
+ * The extra data will be aligned and placed behind the element data.
+ * The exact location in the node is stored in the @c loc_extra field
+ * of the linked list.
+ *
+ * You should usually not use this function except when you are creating an
+ * own linked-list implementation that is based on the UCX linked list and
+ * needs to store extra data in each node.
+ *
+ * @param list the list (must be a linked list)
+ * @param len the length of the extra data
+ */
+cx_attr_nonnull
+CX_EXPORT void cx_linked_list_extra_data(cx_linked_list *list, size_t len);
+
+/**
  * Finds the node at a certain index.
  *
  * This function can be used to start at an arbitrary position within the list.
--- a/ucx/cx/properties.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/cx/properties.h	Sat Dec 13 15:58:58 2025 +0100
@@ -41,9 +41,6 @@
 #include "map.h"
 #include "buffer.h"
 
-#include <stdio.h>
-#include <string.h>
-
 #ifdef __cplusplus
 extern "C" {
 #endif
@@ -80,9 +77,6 @@
      * The character, when appearing at the end of a line, continues that line.
      * This is '\' by default.
      */
-    /**
-     * Reserved for future use.
-     */
     char continuation;
 };
 
@@ -141,23 +135,16 @@
      */
     CX_PROPERTIES_BUFFER_ALLOC_FAILED,
     /**
-     * Initializing the properties source failed.
-     *
-     * @see cx_properties_read_init_func
+     * A file operation failed.
+     * Only for cxPropertiesLoad().
+     * It is system-specific if errno is set.
      */
-    CX_PROPERTIES_READ_INIT_FAILED,
+    CX_PROPERTIES_FILE_ERROR,
     /**
-     * Reading from a properties source failed.
-     *
-     * @see cx_properties_read_func
+     * A map operation failed.
+     * Only for cxPropertiesLoad().
      */
-    CX_PROPERTIES_READ_FAILED,
-    /**
-     * Sinking a k/v-pair failed.
-     *
-     * @see cx_properties_sink_func
-     */
-    CX_PROPERTIES_SINK_FAILED,
+    CX_PROPERTIES_MAP_ERROR,
 };
 
 /**
@@ -190,134 +177,6 @@
  */
 typedef struct cx_properties_s CxProperties;
 
-
-/**
- * Typedef for a properties sink.
- */
-typedef struct cx_properties_sink_s CxPropertiesSink;
-
-/**
- * A function that consumes a k/v-pair in a sink.
- *
- * The sink could be a map, and the sink function would be calling
- * a map function to store the k/v-pair.
- *
- * @param prop the properties interface that wants to sink a k/v-pair
- * @param sink the sink
- * @param key the key
- * @param value the value
- * @retval zero success
- * @retval non-zero sinking the k/v-pair failed
- */
-typedef int(*cx_properties_sink_func)(
-        CxProperties *prop,
-        CxPropertiesSink *sink,
-        cxstring key,
-        cxstring value
-);
-
-/**
- * Defines a sink for k/v-pairs.
- */
-struct cx_properties_sink_s {
-    /**
-     * The sink object.
-     */
-    void *sink;
-    /**
-     * Optional custom data.
-     */
-    void *data;
-    /**
-     * A function for consuming k/v-pairs into the sink.
-     */
-    cx_properties_sink_func sink_func;
-};
-
-
-/**
- * Typedef for a properties source.
- */
-typedef struct cx_properties_source_s CxPropertiesSource;
-
-/**
- * A function that reads data from a source.
- *
- * When the source is depleted, implementations SHALL provide an empty
- * string in the @p target and return zero.
- * A non-zero return value is only permitted in case of an error.
- *
- * The meaning of the optional parameters is implementation-dependent.
- *
- * @param prop the properties interface that wants to read from the source
- * @param src the source
- * @param target a string buffer where the read data shall be stored
- * @retval zero success
- * @retval non-zero reading the data failed
- */
-typedef int(*cx_properties_read_func)(
-        CxProperties *prop,
-        CxPropertiesSource *src,
-        cxstring *target
-);
-
-/**
- * A function that may initialize additional memory for the source.
- *
- * @param prop the properties interface that wants to read from the source
- * @param src the source
- * @retval zero initialization was successful
- * @retval non-zero otherwise
- */
-typedef int(*cx_properties_read_init_func)(
-        CxProperties *prop,
-        CxPropertiesSource *src
-);
-
-/**
- * A function that cleans memory initialized by the read_init_func.
- *
- * @param prop the properties interface that wants to read from the source
- * @param src the source
- */
-typedef void(*cx_properties_read_clean_func)(
-        CxProperties *prop,
-        CxPropertiesSource *src
-);
-
-/**
- * Defines a properties source.
- */
-struct cx_properties_source_s {
-    /**
-     * The source object.
-     *
-     * For example, a file stream or a string.
-     */
-    void *src;
-    /**
-     * Optional additional data pointer.
-     */
-    void *data_ptr;
-    /**
-     * Optional size information.
-     */
-    size_t data_size;
-    /**
-     * A function that reads data from the source.
-     */
-    cx_properties_read_func read_func;
-    /**
-     * Optional function that may prepare the source for reading data.
-     */
-    cx_properties_read_init_func read_init_func;
-    /**
-     * Optional function that cleans additional memory allocated by the
-     * read_init_func.
-     */
-    cx_properties_read_clean_func read_clean_func;
-};
-
 /**
  * Initialize a properties interface.
  *
@@ -465,100 +324,83 @@
 CX_EXPORT CxPropertiesStatus cxPropertiesNext(CxProperties *prop, cxstring *key, cxstring *value);
 
 /**
- * Creates a properties sink for an UCX map.
- *
- * The values stored in the map will be pointers to freshly allocated,
- * zero-terminated C strings (@c char*), which means the @p map should have been
- * created with #CX_STORE_POINTERS.
- *
- * The cxDefaultAllocator will be used unless you specify a custom
- * allocator in the optional @c data field of the returned sink.
- *
- * @param map the map that shall consume the k/v-pairs.
- * @return the sink
- * @see cxPropertiesLoad()
+ * The size of the stack memory that `cxPropertiesLoad()` will reserve with `cxPropertiesUseStack()`.
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT CxPropertiesSink cxPropertiesMapSink(CxMap *map);
+CX_EXPORT extern const unsigned cx_properties_load_buf_size;
 
 /**
- * Creates a properties source based on an UCX string.
- *
- * @param str the string
- * @return the properties source
- * @see cxPropertiesLoad()
+ * The size of the stack memory that `cxPropertiesLoad()` will use to read contents from the file.
  */
-cx_attr_nodiscard
-CX_EXPORT CxPropertiesSource cxPropertiesStringSource(cxstring str);
-
-/**
- * Creates a properties source based on C string with the specified length.
- *
- * @param str the string
- * @param len the length
- * @return the properties source
- * @see cxPropertiesLoad()
- */
-cx_attr_nonnull cx_attr_nodiscard cx_attr_access_r(1, 2)
-CX_EXPORT CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len);
+CX_EXPORT extern const unsigned cx_properties_load_fill_size;
 
 /**
- * Creates a properties source based on a C string.
- *
- * The length will be determined with strlen(), so the string MUST be
- * zero-terminated.
+ * Internal function - use cxPropertiesLoad() instead.
  *
- * @param str the string
- * @return the properties source
- * @see cxPropertiesLoad()
+ * @param config the parser config
+ * @param allocator the allocator for the values
+ * @param filename the file name
+ * @param target the target map
+ * @return status code
  */
-cx_attr_nonnull cx_attr_nodiscard cx_attr_cstr_arg(1)
-CX_EXPORT CxPropertiesSource cxPropertiesCstrSource(const char *str);
+cx_attr_nonnull_arg(4)
+CX_EXPORT CxPropertiesStatus cx_properties_load(CxPropertiesConfig config,
+        const CxAllocator *allocator, cxstring filename, CxMap *target);
 
 /**
- * Creates a properties source based on an FILE.
- *
- * @param file the file
- * @param chunk_size how many bytes may be read in one operation
+ * Loads properties from a file and inserts them into a map.
  *
- * @return the properties source
- * @see cxPropertiesLoad()
- */
-cx_attr_nonnull cx_attr_nodiscard cx_attr_access_r(1)
-CX_EXPORT CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size);
-
-
-/**
- * Loads properties data from a source and transfers it to a sink.
+ * Entries are added to the map, possibly overwriting existing entries.
+ *
+ * The map must either store pointers of type @c char*, or elements of type cxmutstr.
+ * Any other configuration is not supported.
  *
- * This function tries to read as much data from the source as possible.
- * When the source was completely consumed and at least on k/v-pair was found,
- * the return value will be #CX_PROPERTIES_NO_ERROR.
- * When the source was consumed but no k/v-pairs were found, the return value
- * will be #CX_PROPERTIES_NO_DATA.
- * In case the source data ends unexpectedly, the #CX_PROPERTIES_INCOMPLETE_DATA
- * is returned. In that case you should call this function again with the same
- * sink and either an updated source or the same source if the source is able to
- * yield the missing data.
+ * @note When the parser finds an error, all successfully parsed keys before the error
+ * are added to the map nonetheless.
  *
- * The other result codes apply, according to their description.
- *
- * @param prop the properties interface
- * @param sink the sink
- * @param source the source
- * @retval CX_PROPERTIES_NO_ERROR (zero) a key/value pair was found
- * @retval CX_PROPERTIES_READ_INIT_FAILED initializing the source failed
- * @retval CX_PROPERTIES_READ_FAILED reading from the source failed
- * @retval CX_PROPERTIES_SINK_FAILED sinking the properties into the sink failed
- * @retval CX_PROPERTIES_NO_DATA the source did not provide any key/value pairs
- * @retval CX_PROPERTIES_INCOMPLETE_DATA the source did not provide enough data
+ * @param config the parser config
+ * @param allocator the allocator for the values that will be stored in the map
+ * @param filename (any string) the absolute or relative path to the file
+ * @param target (@c CxMap*) the map where the properties shall be added
+ * @retval CX_PROPERTIES_NO_ERROR (zero) at least one key/value pair was found
+ * @retval CX_PROPERTIES_NO_DATA the file is syntactically OK, but does not contain properties
+ * @retval CX_PROPERTIES_INCOMPLETE_DATA unexpected end of file
  * @retval CX_PROPERTIES_INVALID_EMPTY_KEY the properties data contains an illegal empty key
  * @retval CX_PROPERTIES_INVALID_MISSING_DELIMITER the properties data contains a line without delimiter
  * @retval CX_PROPERTIES_BUFFER_ALLOC_FAILED an internal allocation was necessary but failed
+ * @retval CX_PROPERTIES_FILE_ERROR a file operation failed; depending on the system @c errno might be set
+ * @retval CX_PROPERTIES_MAP_ERROR storing a key/value pair in the map failed
+ * @see cxPropertiesLoadDefault()
  */
-cx_attr_nonnull
-CX_EXPORT CxPropertiesStatus cxPropertiesLoad(CxProperties *prop,
-        CxPropertiesSink sink, CxPropertiesSource source);
+#define cxPropertiesLoad(config, allocator, filename, target) \
+    cx_properties_load(config, allocator, cx_strcast(filename), target)
+
+/**
+ * Loads properties from a file and inserts them into a map with a default config.
+ *
+ * Entries are added to the map, possibly overwriting existing entries.
+ *
+ * The map must either store pointers of type @c char*, or elements of type cxmutstr.
+ * Any other configuration is not supported.
+ *
+ * @note When the parser finds an error, all successfully parsed keys before the error
+ * are added to the map nonetheless.
+ *
+ * @param allocator the allocator for the values that will be stored in the map
+ * @param filename (any string) the absolute or relative path to the file
+ * @param target (@c CxMap*) the map where the properties shall be added
+ * @retval CX_PROPERTIES_NO_ERROR (zero) at least one key/value pair was found
+ * @retval CX_PROPERTIES_NO_DATA the file is syntactically OK, but does not contain properties
+ * @retval CX_PROPERTIES_INCOMPLETE_DATA unexpected end of file
+ * @retval CX_PROPERTIES_INVALID_EMPTY_KEY the properties data contains an illegal empty key
+ * @retval CX_PROPERTIES_INVALID_MISSING_DELIMITER the properties data contains a line without delimiter
+ * @retval CX_PROPERTIES_BUFFER_ALLOC_FAILED an internal allocation was necessary but failed
+ * @retval CX_PROPERTIES_FILE_ERROR a file operation failed; depending on the system @c errno might be set
+ * @retval CX_PROPERTIES_MAP_ERROR storing a key/value pair in the map failed
+ * @see cxPropertiesLoad()
+ */
+#define cxPropertiesLoadDefault(allocator, filename, target) \
+    cx_properties_load(cx_properties_config_default, allocator, cx_strcast(filename), target)
+
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/cx/string.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/cx/string.h	Sat Dec 13 15:58:58 2025 +0100
@@ -256,7 +256,7 @@
 }
 cx_attr_nodiscard
 CX_CPPDECL cxstring cx_strcast(const unsigned char *str) {
-    return cx_str(static_cast<const char*>(str));
+    return cx_str(reinterpret_cast<const char*>(str));
 }
 extern "C" {
 #else
--- a/ucx/cx/tree.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/cx/tree.h	Sat Dec 13 15:58:58 2025 +0100
@@ -88,6 +88,7 @@
     ptrdiff_t loc_next;
     /**
      * The total number of distinct nodes that have been passed so far.
+     * This includes the current node.
      */
     size_t counter;
     /**
@@ -185,6 +186,7 @@
     ptrdiff_t loc_next;
     /**
      * The total number of distinct nodes that have been passed so far.
+     * This includes the currently visited node.
      */
     size_t counter;
     /**
@@ -641,17 +643,13 @@
  * Structure for holding the base data of a tree.
  */
 struct cx_tree_s {
+    CX_COLLECTION_BASE;
     /**
      * The tree class definition.
      */
     const cx_tree_class *cl;
 
     /**
-     * Allocator to allocate new nodes.
-     */
-    const CxAllocator *allocator;
-
-    /**
      * A pointer to the root node.
      *
      * Will be @c NULL when @c size is 0.
@@ -669,21 +667,6 @@
     cx_tree_node_create_func node_create;
 
     /**
-     * An optional simple destructor for the tree nodes.
-     */
-    cx_destructor_func simple_destructor;
-
-    /**
-     * An optional advanced destructor for the tree nodes.
-     */
-    cx_destructor_func2 advanced_destructor;
-
-    /**
-     * The pointer to additional data that is passed to the advanced destructor.
-     */
-    void *destructor_data;
-
-    /**
      * A function to compare two nodes.
      */
     cx_tree_search_func search;
@@ -694,11 +677,6 @@
     cx_tree_search_data_func search_data;
 
     /**
-     * The number of currently stored elements.
-     */
-    size_t size;
-
-    /**
      * Offset in the node struct for the parent pointer.
      */
     ptrdiff_t loc_parent;
@@ -1094,7 +1072,7 @@
  * @see cxTreeIterate()
  */
 cx_attr_nonnull cx_attr_nodiscard
-CxTreeVisitor cxTreeVisit(CxTree *tree);
+CX_EXPORT CxTreeVisitor cxTreeVisit(CxTree *tree);
 
 /**
  * Sets the (new) parent of the specified child.
--- a/ucx/hash_map.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/hash_map.c	Sat Dec 13 15:58:58 2025 +0100
@@ -117,7 +117,7 @@
                 allocator,
                 sizeof(struct cx_hash_map_element_s) + map->collection.elem_size
         );
-        if (e == NULL) return NULL;
+        if (e == NULL) return NULL; // LCOV_EXCL_LINE
 
         // write the value
         if (value == NULL) {
@@ -130,10 +130,10 @@
 
         // copy the key
         void *kd = cxMalloc(allocator, key.len);
-        if (kd == NULL) {
+        if (kd == NULL) { // LCOV_EXCL_START
             cxFree(allocator, e);
             return NULL;
-        }
+        } // LCOV_EXCL_STOP
         memcpy(kd, key.data, key.len);
         e->key.data = kd;
         e->key.len = key.len;
@@ -447,15 +447,16 @@
 
         size_t new_bucket_count = (map->collection.size * 5) >> 1;
         if (new_bucket_count < hash_map->bucket_count) {
+            // LCOV_EXCL_START
             errno = EOVERFLOW;
             return 1;
-        }
+        } // LCOV_EXCL_STOP
         struct cx_hash_map_element_s **new_buckets = cxCalloc(
                 map->collection.allocator,
                 new_bucket_count, sizeof(struct cx_hash_map_element_s *)
         );
 
-        if (new_buckets == NULL) return 1;
+        if (new_buckets == NULL) return 1; // LCOV_EXCL_LINE
 
         // iterate through the elements and assign them to their new slots
         for (size_t slot = 0; slot < hash_map->bucket_count; slot++) {
--- a/ucx/json.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/json.c	Sat Dec 13 15:58:58 2025 +0100
@@ -27,6 +27,7 @@
  */
 
 #include "cx/json.h"
+#include "cx/kv_list.h"
 
 #include <string.h>
 #include <assert.h>
@@ -41,90 +42,10 @@
 
 static CxJsonValue cx_json_value_nothing = {.type = CX_JSON_NOTHING};
 
-static int json_cmp_objvalue(const void *l, const void *r) {
-    const CxJsonObjValue *left = l;
-    const CxJsonObjValue *right = r;
-    return cx_strcmp(cx_strcast(left->name), cx_strcast(right->name));
-}
-
-static size_t json_find_objvalue(const CxJsonValue *obj, cxstring name) {
-    assert(obj->type == CX_JSON_OBJECT);
-    CxJsonObjValue kv_dummy;
-    kv_dummy.name = cx_mutstrn((char*) name.ptr, name.length);
-    return cx_array_binary_search(
-            obj->value.object.values,
-            obj->value.object.values_size,
-            sizeof(CxJsonObjValue),
-            &kv_dummy,
-            json_cmp_objvalue
-    );
-}
-
-static int json_add_objvalue(CxJsonValue *objv, CxJsonObjValue member) {
-    assert(objv->type == CX_JSON_OBJECT);
-    const CxAllocator * const al = objv->allocator;
-    CxJsonObject *obj = &(objv->value.object);
-
-    // determine the index where we need to insert the new member
-    size_t index = cx_array_binary_search_sup(
-        obj->values,
-        obj->values_size,
-        sizeof(CxJsonObjValue),
-        &member, json_cmp_objvalue
-    );
-
-    // is the name already present?
-    if (index < obj->values_size && 0 == json_cmp_objvalue(&member, &obj->values[index])) {
-        // free the original value
-        cx_strfree_a(al, &obj->values[index].name);
-        cxJsonValueFree(obj->values[index].value);
-        // replace the item
-        obj->values[index] = member;
-
-        // nothing more to do
-        return 0;
-    }
-
-    // determine the old capacity and reserve for one more element
-    CxArrayReallocator arealloc = cx_array_reallocator(al, NULL);
-    size_t oldcap = obj->values_capacity;
-    if (cx_array_simple_reserve_a(&arealloc, obj->values, 1)) return 1;
-
-    // check the new capacity, if we need to realloc the index array
-    size_t newcap = obj->values_capacity;
-    if (newcap > oldcap) {
-        if (cxReallocateArray(al, &obj->indices, newcap, sizeof(size_t))) {
-            return 1;
-        }
-    }
-
-    // check if append or insert
-    if (index < obj->values_size) {
-        // move the other elements
-        memmove(
-            &obj->values[index+1],
-            &obj->values[index],
-            (obj->values_size - index) * sizeof(CxJsonObjValue)
-        );
-        // increase indices for the moved elements
-        for (size_t i = 0; i < obj->values_size ; i++) {
-            if (obj->indices[i] >= index) {
-                obj->indices[i]++;
-            }
-        }
-    }
-
-    // insert the element and set the index
-    obj->values[index] = member;
-    obj->indices[obj->values_size] = index;
-    obj->values_size++;
-
-    return 0;
-}
-
 static void token_destroy(CxJsonToken *token) {
     if (token->allocated) {
         cx_strfree(&token->content);
+        token->allocated = false;
     }
 }
 
@@ -307,7 +228,9 @@
         }
     }
 
-    if (ttype != CX_JSON_NO_TOKEN) {
+    if (ttype == CX_JSON_NO_TOKEN) {
+        return CX_JSON_NO_DATA;
+    } else {
         // uncompleted token
         size_t uncompleted_len = json->buffer.size - token_part_start;
         if (json->uncompleted.tokentype == CX_JSON_NO_TOKEN) {
@@ -334,9 +257,8 @@
         }
         // advance the buffer position - we saved the stuff in the uncompleted token
         json->buffer.pos += uncompleted_len;
+        return CX_JSON_INCOMPLETE_DATA;
     }
-
-    return CX_JSON_INCOMPLETE_DATA;
 }
 
 // converts a Unicode codepoint to utf8
@@ -437,7 +359,7 @@
             } else if (c == 'u') {
                 char utf8buf[4];
                 unsigned utf8len = unescape_unicode_string(
-                    cx_strn(str.ptr + i - 1, str.length + 1 - i),
+                    cx_strn(str.ptr + i - 1, str.length - i),
                     utf8buf
                 );
                 if(utf8len > 0) {
@@ -456,7 +378,7 @@
             } else {
                 // TODO: discuss the behavior for unrecognized escape sequences
                 //       most parsers throw an error here - we just ignore it
-                result.ptr[result.length++] = '\\';
+                result.ptr[result.length++] = '\\'; // LCOV_EXCL_LINE
             }
 
             result.ptr[result.length++] = c;
@@ -473,7 +395,7 @@
     return result;
 }
 
-static cxmutstr escape_string(cxmutstr str, bool escape_slash) {
+static cxmutstr escape_string(cxstring str, bool escape_slash) {
     // note: this function produces the string without enclosing quotes
     // the reason is that we don't want to allocate memory just for that
     CxBuffer buf = {0};
@@ -519,11 +441,27 @@
             cxBufferPut(&buf, c);
         }
     }
-    if (!all_printable) {
-        str = cx_mutstrn(buf.space, buf.size);
+    cxmutstr ret;
+    if (all_printable) {
+        // don't copy the string when we don't need to escape anything
+        ret = cx_mutstrn((char*)str.ptr, str.length);
+    } else {
+        ret = cx_mutstrn(buf.space, buf.size);
     }
     cxBufferDestroy(&buf);
-    return str;
+    return ret;
+}
+
+static CxJsonObject json_create_object_map(const CxAllocator *allocator) {
+    // TODO: we might want to add a comparator that is sorting the elements by their key
+    CxMap *map = cxKvListCreateAsMap(allocator, NULL, CX_STORE_POINTERS);
+    if (map == NULL) return NULL; // LCOV_EXCL_LINE
+    cxDefineDestructor(map, cxJsonValueFree);
+    return map;
+}
+
+static void json_free_object_map(CxJsonObject obj) {
+    cxMapFree(obj);
 }
 
 static CxJsonValue* json_create_value(CxJson *json, CxJsonValueType type) {
@@ -534,14 +472,11 @@
     v->type = type;
     v->allocator = json->allocator;
     if (type == CX_JSON_ARRAY) {
-        cx_array_initialize_a(json->allocator, v->value.array.array, 16);
-        if (v->value.array.array == NULL) goto create_json_value_exit_error; // LCOV_EXCL_LINE
+        cx_array_initialize_a(json->allocator, v->array.data, 16);
+        if (v->array.data == NULL) goto create_json_value_exit_error; // LCOV_EXCL_LINE
     } else if (type == CX_JSON_OBJECT) {
-        cx_array_initialize_a(json->allocator, v->value.object.values, 16);
-        v->value.object.indices = cxCalloc(json->allocator, 16, sizeof(size_t));
-        if (v->value.object.values == NULL ||
-            v->value.object.indices == NULL)
-            goto create_json_value_exit_error; // LCOV_EXCL_LINE
+        v->object = json_create_object_map(json->allocator);
+        if (v->object == NULL) goto create_json_value_exit_error; // LCOV_EXCL_LINE
     }
 
     // add the new value to a possible parent
@@ -550,17 +485,17 @@
         assert(parent != NULL);
         if (parent->type == CX_JSON_ARRAY) {
             CxArrayReallocator value_realloc = cx_array_reallocator(json->allocator, NULL);
-            if (cx_array_simple_add_a(&value_realloc, parent->value.array.array, v)) {
+            if (cx_array_simple_add_a(&value_realloc, parent->array.data, v)) {
                 goto create_json_value_exit_error; // LCOV_EXCL_LINE
             }
         } else if (parent->type == CX_JSON_OBJECT) {
             // the member was already created after parsing the name
-            assert(json->uncompleted_member.name.ptr != NULL);
-            json->uncompleted_member.value = v;
-            if (json_add_objvalue(parent, json->uncompleted_member))  {
+            // store the pointer of the uncompleted value in the map
+            assert(json->uncompleted_member_name.ptr != NULL);
+            if (cxMapPut(parent->object, json->uncompleted_member_name, v)) {
                 goto create_json_value_exit_error; // LCOV_EXCL_LINE
             }
-            json->uncompleted_member.name = (cxmutstr) {NULL, 0};
+            cx_strfree_a(json->allocator, &json->uncompleted_member_name);
         } else {
             assert(false); // LCOV_EXCL_LINE
         }
@@ -624,10 +559,8 @@
     }
     cxJsonValueFree(json->parsed);
     json->parsed = NULL;
-    if (json->uncompleted_member.name.ptr != NULL) {
-        cx_strfree_a(json->allocator, &json->uncompleted_member.name);
-        json->uncompleted_member = (CxJsonObjValue){{NULL, 0}, NULL};
-    }
+    token_destroy(&json->uncompleted);
+    cx_strfree_a(json->allocator, &json->uncompleted_member_name);
 }
 
 void cxJsonReset(CxJson *json) {
@@ -640,6 +573,7 @@
     if (cxBufferEof(&json->buffer)) {
         // reinitialize the buffer
         cxBufferDestroy(&json->buffer);
+        if (buf == NULL) buf = ""; // buffer must not be initialized with NULL
         cxBufferInit(&json->buffer, (char*) buf, size,
             NULL, CX_BUFFER_AUTO_EXTEND | CX_BUFFER_COPY_ON_WRITE);
         json->buffer.size = size;
@@ -719,7 +653,7 @@
                 if (str.ptr == NULL) {
                     return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
                 }
-                vbuf->value.string = str;
+                vbuf->string = str;
                 return_rec(CX_JSON_NO_ERROR);
             }
             case CX_JSON_TOKEN_INTEGER:
@@ -729,12 +663,13 @@
                     return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
                 }
                 if (type == CX_JSON_INTEGER) {
-                    if (cx_strtoi64(token.content, &vbuf->value.integer, 10)) {
+                    if (cx_strtoi64(token.content, &vbuf->integer, 10)) {
                         return_rec(CX_JSON_FORMAT_ERROR_NUMBER);
                     }
                 } else {
-                    if (cx_strtod(token.content, &vbuf->value.number)) {
-                        return_rec(CX_JSON_FORMAT_ERROR_NUMBER);
+                    if (cx_strtod(token.content, &vbuf->number)) {
+                        // TODO: at the moment this is unreachable, because the tokenizer is already stricter than cx_strtod()
+                        return_rec(CX_JSON_FORMAT_ERROR_NUMBER);  // LCOV_EXCL_LINE
                     }
                 }
                 return_rec(CX_JSON_NO_ERROR);
@@ -744,11 +679,11 @@
                     return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
                 }
                 if (0 == cx_strcmp(cx_strcast(token.content), cx_str("true"))) {
-                    vbuf->value.literal = CX_JSON_TRUE;
+                    vbuf->literal = CX_JSON_TRUE;
                 } else if (0 == cx_strcmp(cx_strcast(token.content), cx_str("false"))) {
-                    vbuf->value.literal = CX_JSON_FALSE;
+                    vbuf->literal = CX_JSON_FALSE;
                 } else {
-                    vbuf->value.literal = CX_JSON_NULL;
+                    vbuf->literal = CX_JSON_NULL;
                 }
                 return_rec(CX_JSON_NO_ERROR);
             }
@@ -784,8 +719,8 @@
             if (name.ptr == NULL) {
                 return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
             }
-            assert(json->uncompleted_member.name.ptr == NULL);
-            json->uncompleted_member.name = name;
+            assert(json->uncompleted_member_name.ptr == NULL);
+            json->uncompleted_member_name = name;
             assert(json->vbuf_size > 0);
 
             // next state
@@ -815,19 +750,19 @@
     } else {
         // should be unreachable
         assert(false);
-        return_rec(-1);
+        return_rec(-1); // LCOV_EXCL_LINE
     }
 }
 
 CxJsonStatus cxJsonNext(CxJson *json, CxJsonValue **value) {
-    // check if buffer has been filled
+    // initialize output value
+    *value = &cx_json_value_nothing;
+
+    // check if the buffer has been filled
     if (json->buffer.space == NULL) {
         return CX_JSON_NULL_DATA;
     }
 
-    // initialize output value
-    *value = &cx_json_value_nothing;
-
     // parse data
     CxJsonStatus result;
     do {
@@ -858,29 +793,54 @@
     return result;
 }
 
+CxJsonStatus cx_json_from_string(const CxAllocator *allocator,
+            cxstring str, CxJsonValue **value) {
+    *value = &cx_json_value_nothing;
+    CxJson parser;
+    cxJsonInit(&parser, allocator);
+    if (cxJsonFill(&parser, str)) {
+        // LCOV_EXCL_START
+        cxJsonDestroy(&parser);
+        return CX_JSON_BUFFER_ALLOC_FAILED;
+        // LCOV_EXCL_STOP
+    }
+    CxJsonStatus status = cxJsonNext(&parser, value);
+    // check if we consume the total string
+    CxJsonValue *chk_value = NULL;
+    CxJsonStatus chk_status = CX_JSON_NO_DATA;
+    if (status == CX_JSON_NO_ERROR) {
+        chk_status = cxJsonNext(&parser, &chk_value);
+    }
+    cxJsonDestroy(&parser);
+    if (chk_status == CX_JSON_NO_DATA) {
+        return status;
+    } else {
+        cxJsonValueFree(*value);
+        // if chk_value is nothing, the free is harmless
+        cxJsonValueFree(chk_value);
+        *value = &cx_json_value_nothing;
+        return CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN;
+    }
+
+}
+
 void cxJsonValueFree(CxJsonValue *value) {
     if (value == NULL || value->type == CX_JSON_NOTHING) return;
     switch (value->type) {
         case CX_JSON_OBJECT: {
-            CxJsonObject obj = value->value.object;
-            for (size_t i = 0; i < obj.values_size; i++) {
-                cxJsonValueFree(obj.values[i].value);
-                cx_strfree_a(value->allocator, &obj.values[i].name);
-            }
-            cxFree(value->allocator, obj.values);
-            cxFree(value->allocator, obj.indices);
+            json_free_object_map(value->object);
             break;
         }
         case CX_JSON_ARRAY: {
-            CxJsonArray array = value->value.array;
-            for (size_t i = 0; i < array.array_size; i++) {
-                cxJsonValueFree(array.array[i]);
+            CxJsonArray array = value->array;
+            for (size_t i = 0; i < array.data_size; i++) {
+                cxJsonValueFree(array.data[i]);
             }
-            cxFree(value->allocator, array.array);
+            cxFree(value->allocator, array.data);
             break;
         }
         case CX_JSON_STRING: {
-            cxFree(value->allocator, value->value.string.ptr);
+            cxFree(value->allocator, value->string.ptr);
             break;
         }
         default: {
@@ -896,15 +856,8 @@
     if (v == NULL) return NULL;
     v->allocator = allocator;
     v->type = CX_JSON_OBJECT;
-    cx_array_initialize_a(allocator, v->value.object.values, 16);
-    if (v->value.object.values == NULL) { // LCOV_EXCL_START
-        cxFree(allocator, v);
-        return NULL;
-        // LCOV_EXCL_STOP
-    }
-    v->value.object.indices = cxCalloc(allocator, 16, sizeof(size_t));
-    if (v->value.object.indices == NULL) { // LCOV_EXCL_START
-        cxFree(allocator, v->value.object.values);
+    v->object = json_create_object_map(allocator);
+    if (v->object == NULL) { // LCOV_EXCL_START
         cxFree(allocator, v);
         return NULL;
         // LCOV_EXCL_STOP
@@ -918,8 +871,8 @@
     if (v == NULL) return NULL;
     v->allocator = allocator;
     v->type = CX_JSON_ARRAY;
-    cx_array_initialize_a(allocator, v->value.array.array, 16);
-    if (v->value.array.array == NULL) { cxFree(allocator, v); return NULL; }
+    cx_array_initialize_a(allocator, v->array.data, 16);
+    if (v->array.data == NULL) { cxFree(allocator, v); return NULL; }
     return v;
 }
 
@@ -929,7 +882,7 @@
     if (v == NULL) return NULL;
     v->allocator = allocator;
     v->type = CX_JSON_NUMBER;
-    v->value.number = num;
+    v->number = num;
     return v;
 }
 
@@ -939,15 +892,11 @@
     if (v == NULL) return NULL;
     v->allocator = allocator;
     v->type = CX_JSON_INTEGER;
-    v->value.integer = num;
+    v->integer = num;
     return v;
 }
 
-CxJsonValue* cxJsonCreateString(const CxAllocator* allocator, const char* str) {
-    return cxJsonCreateCxString(allocator, cx_str(str));
-}
-
-CxJsonValue* cxJsonCreateCxString(const CxAllocator* allocator, cxstring str) {
+CxJsonValue* cx_json_create_string(const CxAllocator* allocator, cxstring str) {
     if (allocator == NULL) allocator = cxDefaultAllocator;
     CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
     if (v == NULL) return NULL;
@@ -955,7 +904,7 @@
     v->type = CX_JSON_STRING;
     cxmutstr s = cx_strdup_a(allocator, str);
     if (s.ptr == NULL) { cxFree(allocator, v); return NULL; }
-    v->value.string = s;
+    v->string = s;
     return v;
 }
 
@@ -965,7 +914,7 @@
     if (v == NULL) return NULL;
     v->allocator = allocator;
     v->type = CX_JSON_LITERAL;
-    v->value.literal = lit;
+    v->literal = lit;
     return v;
 }
 
@@ -1020,7 +969,7 @@
     CxJsonValue** values = cxCallocDefault(count, sizeof(CxJsonValue*));
     if (values == NULL) return -1;
     for (size_t i = 0; i < count; i++) {
-        values[i] = cxJsonCreateCxString(arr->allocator, str[i]);
+        values[i] = cxJsonCreateString(arr->allocator, str[i]);
         if (values[i] == NULL) { json_arr_free_temp(values, count); return -1; }
     }
     int ret = cxJsonArrAddValues(arr, values, count);
@@ -1044,67 +993,52 @@
     CxArrayReallocator value_realloc = cx_array_reallocator(arr->allocator, NULL);
     assert(arr->type == CX_JSON_ARRAY);
     return cx_array_simple_copy_a(&value_realloc,
-            arr->value.array.array,
-            arr->value.array.array_size,
+            arr->array.data,
+            arr->array.data_size,
             val, count
     );
 }
 
-int cxJsonObjPut(CxJsonValue* obj, cxstring name, CxJsonValue* child) {
-    cxmutstr k = cx_strdup_a(obj->allocator, name);
-    if (k.ptr == NULL) return -1;
-    CxJsonObjValue kv = {k, child};
-    if (json_add_objvalue(obj, kv)) {
-        cx_strfree_a(obj->allocator, &k);
-        return 1;
-    } else {
-        return 0;
-    }
+int cx_json_obj_put(CxJsonValue* obj, cxstring name, CxJsonValue* child) {
+    return cxMapPut(obj->object, name, child);
 }
 
-CxJsonValue* cxJsonObjPutObj(CxJsonValue* obj, cxstring name) {
+CxJsonValue* cx_json_obj_put_obj(CxJsonValue* obj, cxstring name) {
     CxJsonValue* v = cxJsonCreateObj(obj->allocator);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutArr(CxJsonValue* obj, cxstring name) {
+CxJsonValue* cx_json_obj_put_arr(CxJsonValue* obj, cxstring name) {
     CxJsonValue* v = cxJsonCreateArr(obj->allocator);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutNumber(CxJsonValue* obj, cxstring name, double num) {
+CxJsonValue* cx_json_obj_put_number(CxJsonValue* obj, cxstring name, double num) {
     CxJsonValue* v = cxJsonCreateNumber(obj->allocator, num);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutInteger(CxJsonValue* obj, cxstring name, int64_t num) {
+CxJsonValue* cx_json_obj_put_integer(CxJsonValue* obj, cxstring name, int64_t num) {
     CxJsonValue* v = cxJsonCreateInteger(obj->allocator, num);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutString(CxJsonValue* obj, cxstring name, const char* str) {
+CxJsonValue* cx_json_obj_put_string(CxJsonValue* obj, cxstring name, cxstring str) {
     CxJsonValue* v = cxJsonCreateString(obj->allocator, str);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutCxString(CxJsonValue* obj, cxstring name, cxstring str) {
-    CxJsonValue* v = cxJsonCreateCxString(obj->allocator, str);
-    if (v == NULL) return NULL;
-    if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
-    return v;
-}
-
-CxJsonValue* cxJsonObjPutLiteral(CxJsonValue* obj, cxstring name, CxJsonLiteral lit) {
+CxJsonValue* cx_json_obj_put_literal(CxJsonValue* obj, cxstring name, CxJsonLiteral lit) {
     CxJsonValue* v = cxJsonCreateLiteral(obj->allocator, lit);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL;}
@@ -1112,98 +1046,84 @@
 }
 
 CxJsonValue *cxJsonArrGet(const CxJsonValue *value, size_t index) {
-    if (index >= value->value.array.array_size) {
+    if (index >= value->array.data_size) {
         return &cx_json_value_nothing;
     }
-    return value->value.array.array[index];
+    return value->array.data[index];
 }
 
 CxJsonValue *cxJsonArrRemove(CxJsonValue *value, size_t index) {
-    if (index >= value->value.array.array_size) {
+    if (index >= value->array.data_size) {
         return NULL;
     }
-    CxJsonValue *ret = value->value.array.array[index];
+    CxJsonValue *ret = value->array.data[index];
     // TODO: replace with a low level cx_array_remove()
-    size_t count = value->value.array.array_size - index - 1;
+    size_t count = value->array.data_size - index - 1;
     if (count > 0) {
-        memmove(value->value.array.array + index, value->value.array.array + index + 1, count * sizeof(CxJsonValue*));
+        memmove(value->array.data + index, value->array.data + index + 1, count * sizeof(CxJsonValue*));
     }
-    value->value.array.array_size--;
+    value->array.data_size--;
     return ret;
 }
 
 char *cxJsonAsString(const CxJsonValue *value) {
-    return value->value.string.ptr;
+    return value->string.ptr;
 }
 
 cxstring cxJsonAsCxString(const CxJsonValue *value) {
-    return cx_strcast(value->value.string);
+    return cx_strcast(value->string);
 }
 
 cxmutstr cxJsonAsCxMutStr(const CxJsonValue *value) {
-    return value->value.string;
+    return value->string;
 }
 
 double cxJsonAsDouble(const CxJsonValue *value) {
     if (value->type == CX_JSON_INTEGER) {
-        return (double) value->value.integer;
+        return (double) value->integer;
     } else {
-        return value->value.number;
+        return value->number;
     }
 }
 
 int64_t cxJsonAsInteger(const CxJsonValue *value) {
     if (value->type == CX_JSON_INTEGER) {
-        return value->value.integer;
+        return value->integer;
     } else {
-        return (int64_t) value->value.number;
+        return (int64_t) value->number;
     }
 }
 
 CxIterator cxJsonArrIter(const CxJsonValue *value) {
     return cxIteratorPtr(
-        value->value.array.array,
-        value->value.array.array_size,
+        value->array.data,
+        value->array.data_size,
         true // arrays need to keep order
     );
 }
 
-CxIterator cxJsonObjIter(const CxJsonValue *value) {
-    return cxIterator(
-        value->value.object.values,
-        sizeof(CxJsonObjValue),
-        value->value.object.values_size,
-        true // TODO: objects do not always need to keep order
-    );
+CxMapIterator cxJsonObjIter(const CxJsonValue *value) {
+    return cxMapIterator(value->object);
 }
 
 CxJsonValue *cx_json_obj_get(const CxJsonValue *value, cxstring name) {
-    size_t index = json_find_objvalue(value, name);
-    if (index >= value->value.object.values_size) {
+    CxJsonValue *v = cxMapGet(value->object, name);
+    if (v == NULL) {
         return &cx_json_value_nothing;
     } else {
-        return value->value.object.values[index].value;
+        return v;
     }
 }
 
 CxJsonValue *cx_json_obj_remove(CxJsonValue *value, cxstring name) {
-    size_t index = json_find_objvalue(value, name);
-    if (index >= value->value.object.values_size) {
-        return NULL;
-    } else {
-        CxJsonObjValue kv = value->value.object.values[index];
-        cx_strfree_a(value->allocator, &kv.name);
-        // TODO: replace with cx_array_remove() / cx_array_remove_fast()
-        value->value.object.values_size--;
-        memmove(value->value.object.values + index, value->value.object.values + index + 1, (value->value.object.values_size - index) * sizeof(CxJsonObjValue));
-        return kv.value;
-    }
+    CxJsonValue *v = NULL;
+    cxMapRemoveAndGet(value->object, name, &v);
+    return v;
 }
 
 CxJsonWriter cxJsonWriterCompact(void) {
     return (CxJsonWriter) {
         false,
-        true,
         6,
         false,
         4,
@@ -1214,7 +1134,6 @@
 CxJsonWriter cxJsonWriterPretty(bool use_spaces) {
     return (CxJsonWriter) {
         true,
-        true,
         6,
         use_spaces,
         4,
@@ -1279,17 +1198,8 @@
                 expected++;
             }
             depth++;
-            size_t elem_count = value->value.object.values_size;
-            for (size_t look_idx = 0; look_idx < elem_count; look_idx++) {
-                // get the member either via index array or directly
-                size_t elem_idx = settings->sort_members
-                                      ? look_idx
-                                      : value->value.object.indices[look_idx];
-                CxJsonObjValue *member = &value->value.object.values[elem_idx];
-                if (settings->sort_members) {
-                    depth++;depth--;
-                }
-
+            CxMapIterator member_iter = cxJsonObjIter(value);
+            cx_foreach(const CxMapEntry *, member, member_iter) {
                 // possible indentation
                 if (settings->pretty) {
                     if (cx_json_writer_indent(target, wfunc, settings, depth)) {
@@ -1299,26 +1209,27 @@
 
                 // the name
                 actual += wfunc("\"", 1, 1, target);
-                cxmutstr name = escape_string(member->name, settings->escape_slash);
+                cxstring key = cx_strn(member->key->data, member->key->len);
+                cxmutstr name = escape_string(key, settings->escape_slash);
                 actual += wfunc(name.ptr, 1, name.length, target);
-                if (name.ptr != member->name.ptr) {
-                    cx_strfree(&name);
-                }
                 actual += wfunc("\"", 1, 1, target);
                 const char *obj_name_sep = ": ";
                 if (settings->pretty) {
                     actual += wfunc(obj_name_sep, 1, 2, target);
-                    expected += 4 + member->name.length;
+                    expected += 4 + name.length;
                 } else {
                     actual += wfunc(obj_name_sep, 1, 1, target);
-                    expected += 3 + member->name.length;
+                    expected += 3 + name.length;
+                }
+                if (name.ptr != key.ptr) {
+                    cx_strfree(&name);
                 }
 
                 // the value
                 if (cx_json_write_rec(target, member->value, wfunc, settings, depth)) return 1;
 
                 // end of object-value
-                if (look_idx < elem_count - 1) {
+                if (member_iter.index < member_iter.elem_count - 1) {
                     const char *obj_value_sep = ",\n";
                     if (settings->pretty) {
                         actual += wfunc(obj_value_sep, 1, 2, target);
@@ -1350,7 +1261,9 @@
                 if (cx_json_write_rec(
                         target, element,
                         wfunc, settings, depth)
-                ) return 1;
+                ) {
+                    return 1; // LCOV_EXCL_LINE
+                }
 
                 if (iter.index < iter.elem_count - 1) {
                     const char *arr_value_sep = ", ";
@@ -1369,13 +1282,14 @@
         }
         case CX_JSON_STRING: {
             actual += wfunc("\"", 1, 1, target);
-            cxmutstr str = escape_string(value->value.string, settings->escape_slash);
+            cxmutstr str = escape_string(cx_strcast(value->string),
+                settings->escape_slash);
             actual += wfunc(str.ptr, 1, str.length, target);
-            if (str.ptr != value->value.string.ptr) {
+            actual += wfunc("\"", 1, 1, target);
+            expected += 2 + str.length;
+            if (str.ptr != value->string.ptr) {
                 cx_strfree(&str);
             }
-            actual += wfunc("\"", 1, 1, target);
-            expected += 2 + value->value.string.length;
             break;
         }
         case CX_JSON_NUMBER: {
@@ -1383,7 +1297,7 @@
             // because of the way how %g is defined, we need to
             // double the precision and truncate ourselves
             precision = 1 + (precision > 15 ? 30 : 2 * precision);
-            snprintf(numbuf, 40, "%.*g", precision, value->value.number);
+            snprintf(numbuf, 40, "%.*g", precision, value->number);
             char *dot, *exp;
             unsigned char max_digits;
             // find the decimal separator and hope that it's one of . or ,
@@ -1447,17 +1361,17 @@
             break;
         }
         case CX_JSON_INTEGER: {
-            snprintf(numbuf, 32, "%" PRIi64, value->value.integer);
+            snprintf(numbuf, 32, "%" PRIi64, value->integer);
             size_t len = strlen(numbuf);
             actual += wfunc(numbuf, 1, len, target);
             expected += len;
             break;
         }
         case CX_JSON_LITERAL: {
-            if (value->value.literal == CX_JSON_TRUE) {
+            if (value->literal == CX_JSON_TRUE) {
                 actual += wfunc("true", 1, 4, target);
                 expected += 4;
-            } else if (value->value.literal == CX_JSON_FALSE) {
+            } else if (value->literal == CX_JSON_FALSE) {
                 actual += wfunc("false", 1, 5, target);
                 expected += 5;
             } else {
@@ -1495,3 +1409,35 @@
     }
     return cx_json_write_rec(target, value, wfunc, settings, 0);
 }
+
+static cxmutstr cx_json_to_string(CxJsonValue *value, const CxAllocator *allocator, CxJsonWriter *writer) {
+    if (allocator == NULL) allocator = cxDefaultAllocator;
+    CxBuffer buffer;
+    if (cxBufferInit(&buffer, NULL, 128, allocator,
+        CX_BUFFER_AUTO_EXTEND | CX_BUFFER_DO_NOT_FREE)) {
+        return (cxmutstr){NULL, 0};
+    }
+    if (cx_json_write_rec(&buffer, value, cxBufferWriteFunc, writer, 0)
+            || cxBufferTerminate(&buffer)) {
+        // LCOV_EXCL_START
+        buffer.flags &= ~CX_BUFFER_DO_NOT_FREE;
+        cxBufferDestroy(&buffer);
+        return (cxmutstr){NULL, 0};
+        // LCOV_EXCL_STOP
+    } else {
+        cxmutstr str = cx_mutstrn(buffer.space, buffer.size);
+        cxBufferDestroy(&buffer);
+        return str;
+    }
+
+}
+
+cxmutstr cxJsonToString(CxJsonValue *value, const CxAllocator *allocator) {
+    CxJsonWriter writer = cxJsonWriterCompact();
+    return cx_json_to_string(value, allocator, &writer);
+}
+
+cxmutstr cxJsonToPrettyString(CxJsonValue *value, const CxAllocator *allocator) {
+    CxJsonWriter writer = cxJsonWriterPretty(true);
+    return cx_json_to_string(value, allocator, &writer);
+}
--- a/ucx/kv_list.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/kv_list.c	Sat Dec 13 15:58:58 2025 +0100
@@ -98,7 +98,7 @@
 }
 
 static CxHashKey *cx_kv_list_loc_key(cx_kv_list *list, void *node_data) {
-    return (CxHashKey*)((char*)node_data + list->list.base.collection.elem_size);
+    return (CxHashKey*)((char*)node_data - list->list.loc_data + list->list.loc_extra);
 }
 
 static void cx_kvl_deallocate(struct cx_list_s *list) {
@@ -285,6 +285,7 @@
 
 static void cx_kvl_map_deallocate(struct cx_map_s *map) {
     cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
+    cx_kv_list_update_destructors(kv_list);
     kv_list->map_methods->deallocate(map);
     kv_list->list_methods->deallocate(&kv_list->list.base);
 }
@@ -296,41 +297,7 @@
     kv_list->map_methods->clear(map);
 }
 
-static void *cx_kvl_map_put(CxMap *map, CxHashKey key, void *value) {
-    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
-    // if the hash has not yet been computed, do it now
-    if (key.hash == 0) {
-        cx_hash_murmur(&key);
-    }
-
-    // reserve memory in the map first
-    void **map_data = kv_list->map_methods->put(map, key, NULL);
-    if (map_data == NULL) return NULL; // LCOV_EXCL_LINE
-
-    // insert the data into the list (which most likely destroys the sorted property)
-    kv_list->list.base.collection.sorted = false;
-    void *node_data = kv_list->list_methods->insert_element(
-        &kv_list->list.base, kv_list->list.base.collection.size,
-        kv_list->list.base.collection.store_pointer ? &value : value);
-    if (node_data == NULL) { // LCOV_EXCL_START
-        // non-destructively remove the key again
-        kv_list->map_methods->remove(&kv_list->map->map_base.base, key, &map_data);
-        return NULL;
-    } // LCOV_EXCL_STOP
-
-    // write the node pointer to the map entry
-    *map_data = node_data;
-
-    // copy the key to the node data
-    CxHashKey *key_ptr = cx_kv_list_loc_key(kv_list, node_data);
-    *key_ptr = key;
-
-    // we must return node_data here and not map_data,
-    // because the node_data is the actual element of this collection
-    return node_data;
-}
-
-void *cx_kvl_map_get(const CxMap *map, CxHashKey key) {
+static void *cx_kvl_map_get(const CxMap *map, CxHashKey key) {
     cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
     void *node_data = kv_list->map_methods->get(map, key);
     if (node_data == NULL) return NULL; // LCOV_EXCL_LINE
@@ -338,7 +305,7 @@
     return kv_list->list.base.collection.store_pointer ? *(void**)node_data : node_data;
 }
 
-int cx_kvl_map_remove(CxMap *map, CxHashKey key, void *targetbuf) {
+static int cx_kvl_map_remove(CxMap *map, CxHashKey key, void *targetbuf) {
     cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
 
     void *node_data;
@@ -381,6 +348,43 @@
     return 0;
 }
 
+static void *cx_kvl_map_put(CxMap *map, CxHashKey key, void *value) {
+    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
+    // if the hash has not yet been computed, do it now
+    if (key.hash == 0) {
+        cx_hash_murmur(&key);
+    }
+
+    // remove any existing element first
+    cx_kvl_map_remove(map, key, NULL);
+
+    // now reserve new memory in the map
+    void **map_data = kv_list->map_methods->put(map, key, NULL);
+    if (map_data == NULL) return NULL; // LCOV_EXCL_LINE
+
+    // insert the data into the list (which most likely destroys the sorted property)
+    kv_list->list.base.collection.sorted = false;
+    void *node_data = kv_list->list_methods->insert_element(
+        &kv_list->list.base, kv_list->list.base.collection.size,
+        kv_list->list.base.collection.store_pointer ? &value : value);
+    if (node_data == NULL) { // LCOV_EXCL_START
+        // non-destructively remove the key again
+        kv_list->map_methods->remove(&kv_list->map->map_base.base, key, &map_data);
+        return NULL;
+    } // LCOV_EXCL_STOP
+
+    // write the node pointer to the map entry
+    *map_data = node_data;
+
+    // copy the key to the node data
+    CxHashKey *key_ptr = cx_kv_list_loc_key(kv_list, node_data);
+    *key_ptr = key;
+
+    // we must return node_data here and not map_data,
+    // because the node_data is the actual element of this collection
+    return node_data;
+}
+
 static void *cx_kvl_iter_current_entry(const void *it) {
     const CxMapIterator *iter = it;
     return (void*)&iter->entry;
@@ -455,7 +459,7 @@
     return iter->elem != NULL;
 }
 
-CxMapIterator cx_kvl_map_iterator(const CxMap *map, enum cx_map_iterator_type type) {
+static CxMapIterator cx_kvl_map_iterator(const CxMap *map, enum cx_map_iterator_type type) {
     CxMapIterator iter = {0};
 
     iter.type = type;
@@ -559,7 +563,7 @@
     CxList *list = cxLinkedListCreate(allocator, comparator, elem_size);
     if (list == NULL) return NULL; // LCOV_EXCL_LINE
     cx_linked_list *ll = (cx_linked_list*)list;
-    ll->extra_data_len = sizeof(CxHashKey);
+    cx_linked_list_extra_data(ll, sizeof(CxHashKey));
     CxMap *map = cxHashMapCreate(allocator, CX_STORE_POINTERS, 0);
     if (map == NULL) { // LCOV_EXCL_START
         cxListFree(list);
--- a/ucx/linked_list.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/linked_list.c	Sat Dec 13 15:58:58 2025 +0100
@@ -31,6 +31,15 @@
 #include <string.h>
 #include <assert.h>
 
+#if __STDC_VERSION__ < 202311L
+// we cannot simply include stdalign.h
+// because Solaris is not entirely C11 complaint
+#ifndef __alignof_is_defined
+#define alignof _Alignof
+#define __alignof_is_defined 1
+#endif
+#endif
+
 // LOW LEVEL LINKED LIST FUNCTIONS
 
 #define CX_LL_PTR(cur, off) (*(void**)(((char*)(cur))+(off)))
@@ -516,7 +525,7 @@
     void *sbo[CX_LINKED_LIST_SORT_SBO_SIZE];
     void **sorted = length >= CX_LINKED_LIST_SORT_SBO_SIZE ?
                     cxMallocDefault(sizeof(void *) * length) : sbo;
-    if (sorted == NULL) abort();
+    if (sorted == NULL) abort(); // LCOV_EXCL_LINE
     void *rc, *lc;
 
     lc = ls;
@@ -692,8 +701,13 @@
 }
 
 static void *cx_ll_malloc_node(const cx_linked_list *list) {
-    return cxZalloc(list->base.collection.allocator,
-                    list->loc_data + list->base.collection.elem_size + list->extra_data_len);
+    size_t n;
+    if (list->extra_data_len == 0) {
+        n = list->loc_data + list->base.collection.elem_size;
+    } else {
+        n = list->loc_extra + list->extra_data_len;
+    }
+    return cxZalloc(list->base.collection.allocator, n);
 }
 
 static int cx_ll_insert_at(
@@ -1215,7 +1229,7 @@
         return result;
     } else {
         if (cx_ll_insert_element(list, list->collection.size, elem) == NULL) {
-            return 1;
+            return 1; // LCOV_EXCL_LINE
         }
         iter->elem_count++;
         iter->index = list->collection.size;
@@ -1267,12 +1281,22 @@
 
     cx_linked_list *list = cxCalloc(allocator, 1, sizeof(cx_linked_list));
     if (list == NULL) return NULL;
-    list->extra_data_len = 0;
     list->loc_prev = 0;
     list->loc_next = sizeof(void*);
     list->loc_data = sizeof(void*)*2;
+    list->loc_extra = -1;
+    list->extra_data_len = 0;
     cx_list_init((CxList*)list, &cx_linked_list_class,
             allocator, comparator, elem_size);
 
     return (CxList *) list;
 }
+
+void cx_linked_list_extra_data(cx_linked_list *list, size_t len) {
+    list->extra_data_len = len;
+
+    off_t loc_extra = list->loc_data + list->base.collection.elem_size;
+    size_t alignment = alignof(void*);
+    size_t padding = alignment - (loc_extra % alignment);
+    list->loc_extra = loc_extra + padding;
+}
--- a/ucx/list.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/list.c	Sat Dec 13 15:58:58 2025 +0100
@@ -186,7 +186,11 @@
 }
 
 static int cx_pl_change_capacity(struct cx_list_s *list, size_t cap) {
-    return list->climpl->change_capacity(list, cap);
+    if (list->climpl->change_capacity == NULL) {
+        return 0;
+    } else {
+        return list->climpl->change_capacity(list, cap);
+    }
 }
 
 static struct cx_iterator_s cx_pl_iterator(
@@ -1026,7 +1030,7 @@
         CxIterator src_iter = cxListIterator(src);
         CxIterator other_iter = cxListIterator(other);
         while (cxIteratorValid(src_iter) || cxIteratorValid(other_iter)) {
-            void *src_elem, *other_elem;
+            void *src_elem = NULL, *other_elem = NULL;
             int d;
             if (!cxIteratorValid(src_iter)) {
                 other_elem = cxIteratorCurrent(other_iter);
@@ -1124,4 +1128,4 @@
         return 0;
     }
     return list->cl->change_capacity(list, cxCollectionSize(list));
-}
\ No newline at end of file
+}
--- a/ucx/printf.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/printf.c	Sat Dec 13 15:58:58 2025 +0100
@@ -61,8 +61,10 @@
     va_copy(ap2, ap);
     int ret = vsnprintf(buf, CX_PRINTF_SBO_SIZE, fmt, ap);
     if (ret < 0) {
+        // LCOV_EXCL_START
         va_end(ap2);
         return ret;
+        // LCOV_EXCL_STOP
     } else if (ret < CX_PRINTF_SBO_SIZE) {
         va_end(ap2);
         return (int) wfc(buf, 1, ret, stream);
@@ -121,8 +123,10 @@
         if (s.ptr) {
             ret = vsnprintf(s.ptr, len, fmt, ap2);
             if (ret < 0) {
+                // LCOV_EXCL_START
                 cxFree(a, s.ptr);
                 s.ptr = NULL;
+                // LCOV_EXCL_STOP
             } else {
                 s.length = (size_t) ret;
             }
@@ -162,7 +166,7 @@
         if (ptr) {
             int newret = vsnprintf(ptr, newlen, fmt, ap2);
             if (newret < 0) {
-                cxFree(alloc, ptr);
+                cxFree(alloc, ptr); // LCOV_EXCL_LINE
             } else {
                 *len = newlen;
                 *str = ptr;
@@ -207,7 +211,7 @@
         if (ptr) {
             int newret = vsnprintf(ptr, newlen, fmt, ap2);
             if (newret < 0) {
-                cxFree(alloc, ptr);
+                cxFree(alloc, ptr); // LCOV_EXCL_LINE
             } else {
                 *len = newlen;
                 *str = ptr;
--- a/ucx/properties.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/properties.c	Sat Dec 13 15:58:58 2025 +0100
@@ -29,12 +29,15 @@
 #include "cx/properties.h"
 
 #include <assert.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
 
 const CxPropertiesConfig cx_properties_config_default = {
-        '=',
-        '#',
-        '\0',
-        '\0',
+    '=',
+    '#',
+    '\0',
+    '\0',
     '\\',
 };
 
@@ -94,13 +97,30 @@
 
     // a pointer to the buffer we want to read from
     CxBuffer *current_buffer = &prop->input;
-
+    
+    char comment1 = prop->config.comment1;
+    char comment2 = prop->config.comment2;
+    char comment3 = prop->config.comment3;
+    char delimiter = prop->config.delimiter;
+    char continuation = prop->config.continuation;
+    
     // check if we have rescued data
     if (!cxBufferEof(&prop->buffer)) {
         // check if we can now get a complete line
         cxstring input = cx_strn(prop->input.space + prop->input.pos,
             prop->input.size - prop->input.pos);
         cxstring nl = cx_strchr(input, '\n');
+        while (nl.length > 0) {
+            // check for line continuation
+            char previous = nl.ptr > input.ptr ? nl.ptr[-1] : prop->buffer.space[prop->buffer.size-1];
+            if (previous == continuation) {
+                // this nl is a line continuation, check the next newline
+                nl = cx_strchr(cx_strsubs(nl, 1), '\n');
+            } else {
+                break;
+            }
+        }
+        
         if (nl.length > 0) {
             // we add as much data to the rescue buffer as we need
             // to complete the line
@@ -120,19 +140,14 @@
             // still not enough data, copy input buffer to internal buffer
             if (cxBufferAppend(input.ptr, 1,
                 input.length, &prop->buffer) < input.length) {
-                return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
+                return CX_PROPERTIES_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
             }
             // reset the input buffer (make way for a re-fill)
             cxBufferReset(&prop->input);
             return CX_PROPERTIES_INCOMPLETE_DATA;
         }
     }
-
-    char comment1 = prop->config.comment1;
-    char comment2 = prop->config.comment2;
-    char comment3 = prop->config.comment3;
-    char delimiter = prop->config.delimiter;
-
+   
     // get one line and parse it
     while (!cxBufferEof(current_buffer)) {
         const char *buf = current_buffer->space + current_buffer->pos;
@@ -145,6 +160,7 @@
         size_t delimiter_index = 0;
         size_t comment_index = 0;
         bool has_comment = false;
+        bool has_continuation = false;
 
         size_t i = 0;
         char c = 0;
@@ -159,6 +175,9 @@
                 if (delimiter_index == 0 && !has_comment) {
                     delimiter_index = i;
                 }
+            } else if (delimiter_index > 0 && c == continuation && i+1 < len && buf[i+1] == '\n') {
+                has_continuation = true;
+                i++;
             } else if (c == '\n') {
                 break;
             }
@@ -223,10 +242,53 @@
             k = cx_strtrim(k);
             val = cx_strtrim(val);
             if (k.length > 0) {
+                current_buffer->pos += i + 1; 
+                assert(current_buffer->pos <= current_buffer->size);
+                assert(current_buffer != &prop->buffer || current_buffer->pos == current_buffer->size);
+                
+                if (has_continuation) {
+                    char *ptr = (char*)val.ptr;
+                    if (current_buffer != &prop->buffer) {
+                        // move value to the rescue buffer
+                        if (prop->buffer.space == NULL) {
+                            cxBufferInit(&prop->buffer, NULL, 256, NULL, CX_BUFFER_AUTO_EXTEND);
+                        }
+                        prop->buffer.size = 0;
+                        prop->buffer.pos = 0;
+                        if (cxBufferWrite(val.ptr, 1, val.length, &prop->buffer) != val.length) {
+                            return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
+                        }
+                        val.ptr = prop->buffer.space;
+                        ptr = prop->buffer.space;
+                    }
+                    // value.ptr is now inside the rescue buffer and we can
+                    // remove the continuation character from the value
+                    bool trim = false;
+                    size_t x = 0;
+                    for(size_t j=0;j<val.length;j++) {
+                        c = ptr[j];
+                        if (j+1 < val.length && c == '\\' && ptr[j+1] == '\n') {
+                            // skip continuation and newline character
+                            j++;
+                            trim = true; // enable trim in the next line
+                            continue;
+                        }
+                        if (j > x) {
+                            if (trim) {
+                                if (isspace((unsigned char)c)) {
+                                    continue;
+                                }
+                                trim = false;
+                            }
+                            ptr[x] = c;
+                        }
+                        x++;
+                    }
+                    val.length = x;
+                }
                 *key = k;
                 *value = val;
-                current_buffer->pos += i + 1;
-                assert(current_buffer->pos <= current_buffer->size);
+                
                 return CX_PROPERTIES_NO_ERROR;
             } else {
                 return CX_PROPERTIES_INVALID_EMPTY_KEY;
@@ -241,180 +303,96 @@
     return CX_PROPERTIES_NO_DATA;
 }
 
-static int cx_properties_sink_map(
-        cx_attr_unused CxProperties *prop,
-        CxPropertiesSink *sink,
-        cxstring key,
-        cxstring value
-) {
-    CxMap *map = sink->sink;
-    CxAllocator *alloc = sink->data;
-    cxmutstr v = cx_strdup_a(alloc, value);
-    int r = cxMapPut(map, key, v.ptr);
-    if (r != 0) cx_strfree_a(alloc, &v);
-    return r;
-}
-
-CxPropertiesSink cxPropertiesMapSink(CxMap *map) {
-    CxPropertiesSink sink;
-    sink.sink = map;
-    sink.data = (void*) cxDefaultAllocator;
-    sink.sink_func = cx_properties_sink_map;
-    return sink;
-}
+#ifndef CX_PROPERTIES_LOAD_FILL_SIZE
+#define CX_PROPERTIES_LOAD_FILL_SIZE 1024
+#endif
+const unsigned cx_properties_load_fill_size = CX_PROPERTIES_LOAD_FILL_SIZE;
+#ifndef CX_PROPERTIES_LOAD_BUF_SIZE
+#define CX_PROPERTIES_LOAD_BUF_SIZE 256
+#endif
+const unsigned cx_properties_load_buf_size = CX_PROPERTIES_LOAD_BUF_SIZE;
 
-static int cx_properties_read_string(
-        CxProperties *prop,
-        CxPropertiesSource *src,
-        cxstring *target
-) {
-    if (prop->input.space == src->src) {
-        // when the input buffer already contains the string
-        // we have nothing more to provide
-        target->length = 0;
-    } else {
-        target->ptr = src->src;
-        target->length = src->data_size;
+CxPropertiesStatus cx_properties_load(CxPropertiesConfig config,
+        const CxAllocator *allocator, cxstring filename, CxMap *target) {
+    if (allocator == NULL) {
+        allocator = cxDefaultAllocator;
     }
-    return 0;
-}
-
-static int cx_properties_read_file(
-        cx_attr_unused CxProperties *prop,
-        CxPropertiesSource *src,
-        cxstring *target
-) {
-    target->ptr = src->data_ptr;
-    target->length = fread(src->data_ptr, 1, src->data_size, src->src);
-    return ferror((FILE*)src->src);
-}
-
-static int cx_properties_read_init_file(
-        cx_attr_unused CxProperties *prop,
-        CxPropertiesSource *src
-) {
-    src->data_ptr = cxMallocDefault(src->data_size);
-    if (src->data_ptr == NULL) return 1;
-    return 0;
-}
 
-static void cx_properties_read_clean_file(
-        cx_attr_unused CxProperties *prop,
-        CxPropertiesSource *src
-) {
-    cxFreeDefault(src->data_ptr);
-}
-
-CxPropertiesSource cxPropertiesStringSource(cxstring str) {
-    CxPropertiesSource src;
-    src.src = (void*) str.ptr;
-    src.data_size = str.length;
-    src.data_ptr = NULL;
-    src.read_func = cx_properties_read_string;
-    src.read_init_func = NULL;
-    src.read_clean_func = NULL;
-    return src;
-}
-
-CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len) {
-    CxPropertiesSource src;
-    src.src = (void*) str;
-    src.data_size = len;
-    src.data_ptr = NULL;
-    src.read_func = cx_properties_read_string;
-    src.read_init_func = NULL;
-    src.read_clean_func = NULL;
-    return src;
-}
+    // sanity check for the map
+    const bool use_cstring = cxCollectionStoresPointers(target);
+    if (!use_cstring && cxCollectionElementSize(target) != sizeof(cxmutstr)) {
+        return CX_PROPERTIES_MAP_ERROR;
+    }
 
-CxPropertiesSource cxPropertiesCstrSource(const char *str) {
-    CxPropertiesSource src;
-    src.src = (void*) str;
-    src.data_size = strlen(str);
-    src.data_ptr = NULL;
-    src.read_func = cx_properties_read_string;
-    src.read_init_func = NULL;
-    src.read_clean_func = NULL;
-    return src;
-}
+    // create a duplicate to guarantee zero-termination
+    cxmutstr fname = cx_strdup(filename);
+    if (fname.ptr == NULL) {
+        return CX_PROPERTIES_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
+    }
 
-CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size) {
-    CxPropertiesSource src;
-    src.src = file;
-    src.data_size = chunk_size;
-    src.data_ptr = NULL;
-    src.read_func = cx_properties_read_file;
-    src.read_init_func = cx_properties_read_init_file;
-    src.read_clean_func = cx_properties_read_clean_file;
-    return src;
-}
-
-CxPropertiesStatus cxPropertiesLoad(
-        CxProperties *prop,
-        CxPropertiesSink sink,
-        CxPropertiesSource source
-) {
-    assert(source.read_func != NULL);
-    assert(sink.sink_func != NULL);
-
-    // initialize reader
-    if (source.read_init_func != NULL) {
-        if (source.read_init_func(prop, &source)) {
-            return CX_PROPERTIES_READ_INIT_FAILED;
-        }
+    // open the file
+    FILE *f = fopen(fname.ptr, "r");
+    if (f == NULL) {
+        cx_strfree(&fname);
+        return CX_PROPERTIES_FILE_ERROR;
     }
 
-    // transfer the data from the source to the sink
+    // initialize the parser
+    char linebuf[cx_properties_load_buf_size];
+    char fillbuf[cx_properties_load_fill_size];
     CxPropertiesStatus status;
-    CxPropertiesStatus kv_status = CX_PROPERTIES_NO_DATA;
-    bool found = false;
+    CxProperties parser;
+    cxPropertiesInit(&parser, config);
+    cxPropertiesUseStack(&parser, linebuf, cx_properties_load_buf_size);
+
+    // read/fill/parse loop
+    status = CX_PROPERTIES_NO_DATA;
+    size_t keys_found = 0;
     while (true) {
-        // read input
-        cxstring input;
-        if (source.read_func(prop, &source, &input)) {
-            status = CX_PROPERTIES_READ_FAILED;
+        size_t r = fread(fillbuf, 1, cx_properties_load_fill_size, f);
+        if (ferror(f)) {
+            status = CX_PROPERTIES_FILE_ERROR;
+            break;
+        }
+        if (r == 0) {
             break;
         }
-
-        // no more data - break
-        if (input.length == 0) {
-            if (found) {
-                // something was found, check the last kv_status
-                if (kv_status == CX_PROPERTIES_INCOMPLETE_DATA) {
-                    status = CX_PROPERTIES_INCOMPLETE_DATA;
-                } else {
-                    status = CX_PROPERTIES_NO_ERROR;
-                }
-            } else {
-                // nothing found
-                status = CX_PROPERTIES_NO_DATA;
-            }
+        if (cxPropertiesFilln(&parser, fillbuf, r)) {
+            status = CX_PROPERTIES_BUFFER_ALLOC_FAILED;
             break;
         }
-
-        // set the input buffer and read the k/v-pairs
-        cxPropertiesFill(prop, input);
-
-        do {
-            cxstring key, value;
-            kv_status = cxPropertiesNext(prop, &key, &value);
-            if (kv_status == CX_PROPERTIES_NO_ERROR) {
-                found = true;
-                if (sink.sink_func(prop, &sink, key, value)) {
-                    kv_status = CX_PROPERTIES_SINK_FAILED;
+        cxstring key, value;
+        while (true) {
+            status = cxPropertiesNext(&parser, &key, &value);
+            if (status != CX_PROPERTIES_NO_ERROR) {
+                break;
+            } else {
+                cxmutstr v = cx_strdup_a(allocator, value);
+                if (v.ptr == NULL) {
+                    status = CX_PROPERTIES_MAP_ERROR;
+                    break;
                 }
+                void *mv = use_cstring ? (void*)v.ptr : &v;
+                if (cxMapPut(target, key, mv)) {
+                    cx_strfree(&v);
+                    status = CX_PROPERTIES_MAP_ERROR;
+                    break;
+                }
+                keys_found++;
             }
-        } while (kv_status == CX_PROPERTIES_NO_ERROR);
-
-        if (kv_status > CX_PROPERTIES_OK) {
-            status = kv_status;
+        }
+        if (status > CX_PROPERTIES_OK) {
             break;
         }
     }
 
-    if (source.read_clean_func != NULL) {
-        source.read_clean_func(prop, &source);
+    // cleanup and exit
+    fclose(f);
+    cxPropertiesDestroy(&parser);
+    cx_strfree(&fname);
+    if (status == CX_PROPERTIES_NO_DATA && keys_found > 0) {
+        return CX_PROPERTIES_NO_ERROR;
+    } else {
+        return status;
     }
-
-    return status;
 }
--- a/ucx/string.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/string.c	Sat Dec 13 15:58:58 2025 +0100
@@ -91,7 +91,7 @@
         cxstring src
 ) {
     if (cxReallocate(alloc, &dest->ptr, src.length + 1)) {
-        return 1;
+        return 1; // LCOV_EXCL_LINE
     }
 
     memcpy(dest->ptr, src.ptr, src.length);
@@ -137,7 +137,7 @@
     size_t slen = str.length;
     for (size_t i = 0; i < count; i++) {
         cxstring s = va_arg(ap, cxstring);
-        if (slen > SIZE_MAX - str.length) overflow = true;
+        if (slen > SIZE_MAX - s.length) overflow = true;
         slen += s.length;
     }
     va_end(ap);
@@ -156,10 +156,10 @@
     } else {
         newstr = cxRealloc(alloc, str.ptr, slen + 1);
     }
-    if (newstr == NULL) {
+    if (newstr == NULL) { // LCOV_EXCL_START
         va_end(ap2);
         return (cxmutstr) {NULL, 0};
-    }
+    } // LCOV_EXCL_STOP
     str.ptr = newstr;
 
     // concatenate strings
@@ -521,10 +521,12 @@
             cxMalloc(allocator, string.length + 1),
             string.length
     };
+    // LCOV_EXCL_START
     if (result.ptr == NULL) {
         result.length = 0;
         return result;
     }
+    // LCOV_EXCL_STOP
     memcpy(result.ptr, string.ptr, string.length);
     result.ptr[string.length] = '\0';
     return result;
--- a/ucx/tree.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ucx/tree.c	Sat Dec 13 15:58:58 2025 +0100
@@ -566,7 +566,7 @@
         ptrdiff_t loc_next
 ) {
     *cnode = cfunc(src, cdata);
-    if (*cnode == NULL) return 1;
+    if (*cnode == NULL) return 1;  // LCOV_EXCL_LINE
     cx_tree_zero_pointers(*cnode, cx_tree_ptr_locations);
 
     void *match = NULL;
@@ -627,7 +627,7 @@
 
         // create the new node
         void *new_node = cfunc(elem, cdata);
-        if (new_node == NULL) return processed;
+        if (new_node == NULL) return processed;  // LCOV_EXCL_LINE
         cx_tree_zero_pointers(new_node, cx_tree_ptr_locations);
 
         // start searching from current node
@@ -731,18 +731,18 @@
     void *node;
     if (tree->root == NULL) {
         node = tree->node_create(data, tree);
-        if (node == NULL) return 1;
+        if (node == NULL) return 1;  // LCOV_EXCL_LINE
         cx_tree_zero_pointers(node, cx_tree_node_layout(tree));
         tree->root = node;
-        tree->size = 1;
+        tree->collection.size = 1;
         return 0;
     }
     int result = cx_tree_add(data, tree->search, tree->node_create,
                 tree, &node, tree->root, cx_tree_node_layout(tree));
     if (0 == result) {
-        tree->size++;
+        tree->collection.size++;
     } else {
-        cxFree(tree->allocator, node);
+        cxFree(tree->collection.allocator, node);
     }
     return result;
 }
@@ -758,7 +758,7 @@
         // use the first element from the iter to create the root node
         void **eptr = iter->current(iter);
         void *node = tree->node_create(*eptr, tree);
-        if (node == NULL) return 0;
+        if (node == NULL) return 0;  // LCOV_EXCL_LINE
         cx_tree_zero_pointers(node, cx_tree_node_layout(tree));
         tree->root = node;
         ins = 1;
@@ -767,9 +767,9 @@
     void *failed;
     ins += cx_tree_add_iter(iter, n, tree->search, tree->node_create,
                                   tree, &failed, tree->root, cx_tree_node_layout(tree));
-    tree->size += ins;
+    tree->collection.size += ins;
     if (ins < n) {
-        cxFree(tree->allocator, failed);
+        cxFree(tree->collection.allocator, failed);
     }
     return ins;
 }
@@ -780,7 +780,7 @@
         const void *data,
         size_t depth
 ) {
-    if (tree->root == NULL) return NULL;
+    if (tree->root == NULL) return NULL;  // LCOV_EXCL_LINE
 
     void *found;
     if (0 == cx_tree_search_data(
@@ -818,24 +818,21 @@
     assert(search_func != NULL);
     assert(search_data_func != NULL);
 
-    CxTree *tree = cxMalloc(allocator, sizeof(CxTree));
-    if (tree == NULL) return NULL;
+    CxTree *tree = cxZalloc(allocator, sizeof(CxTree));
+    if (tree == NULL) return NULL;  // LCOV_EXCL_LINE
 
     tree->cl = &cx_tree_default_class;
-    tree->allocator = allocator;
+    tree->collection.allocator = allocator;
     tree->node_create = create_func;
     tree->search = search_func;
     tree->search_data = search_data_func;
-    tree->simple_destructor = NULL;
-    tree->advanced_destructor = (cx_destructor_func2) cxFree;
-    tree->destructor_data = (void *) allocator;
+    tree->collection.advanced_destructor = (cx_destructor_func2) cxFree;
+    tree->collection.destructor_data = (void *) allocator;
     tree->loc_parent = loc_parent;
     tree->loc_children = loc_children;
     tree->loc_last_child = loc_last_child;
     tree->loc_prev = loc_prev;
     tree->loc_next = loc_next;
-    tree->root = NULL;
-    tree->size = 0;
 
     return tree;
 }
@@ -845,7 +842,7 @@
     if (tree->root != NULL) {
         cxTreeClear(tree);
     }
-    cxFree(tree->allocator, tree);
+    cxFree(tree->collection.allocator, tree);
 }
 
 CxTree *cxTreeCreateWrapped(const CxAllocator *allocator, void *root,
@@ -856,47 +853,41 @@
     }
     assert(root != NULL);
 
-    CxTree *tree = cxMalloc(allocator, sizeof(CxTree));
-    if (tree == NULL) return NULL;
+    CxTree *tree = cxZalloc(allocator, sizeof(CxTree));
+    if (tree == NULL) return NULL;  // LCOV_EXCL_LINE
 
     tree->cl = &cx_tree_default_class;
     // set the allocator anyway, just in case...
-    tree->allocator = allocator;
-    tree->node_create = NULL;
-    tree->search = NULL;
-    tree->search_data = NULL;
-    tree->simple_destructor = NULL;
-    tree->advanced_destructor = NULL;
-    tree->destructor_data = NULL;
+    tree->collection.allocator = allocator;
     tree->loc_parent = loc_parent;
     tree->loc_children = loc_children;
     tree->loc_last_child = loc_last_child;
     tree->loc_prev = loc_prev;
     tree->loc_next = loc_next;
     tree->root = root;
-    tree->size = cxTreeSubtreeSize(tree, root);
+    tree->collection.size = cxTreeSubtreeSize(tree, root);
     return tree;
 }
 
 void cxTreeSetParent(CxTree *tree, void *parent, void *child) {
     size_t loc_parent = tree->loc_parent;
     if (tree_parent(child) == NULL) {
-        tree->size++;
+        tree->collection.size++;
     }
     cx_tree_link(parent, child, cx_tree_node_layout(tree));
 }
 
 void cxTreeAddChildNode(CxTree *tree, void *parent, void *child) {
     cx_tree_link(parent, child, cx_tree_node_layout(tree));
-    tree->size++;
+    tree->collection.size++;
 }
 
 int cxTreeAddChild(CxTree *tree, void *parent, const void *data) {
     void *node = tree->node_create(data, tree);
-    if (node == NULL) return 1;
+    if (node == NULL) return 1; // LCOV_EXCL_LINE
     cx_tree_zero_pointers(node, cx_tree_node_layout(tree));
     cx_tree_link(parent, node, cx_tree_node_layout(tree));
-    tree->size++;
+    tree->collection.size++;
     return 0;
 }
 
@@ -948,7 +939,7 @@
 }
 
 size_t cxTreeSize(CxTree *tree) {
-    return tree->size;
+    return tree->collection.size;
 }
 
 size_t cxTreeDepth(CxTree *tree) {
@@ -1002,7 +993,7 @@
     if (loc_last_child >= 0) tree_last_child(node) = NULL;
 
     // the tree now has one member less
-    tree->size--;
+    tree->collection.size--;
 
     return 0;
 }
@@ -1010,12 +1001,12 @@
 void cxTreeRemoveSubtree(CxTree *tree, void *node) {
     if (node == tree->root) {
         tree->root = NULL;
-        tree->size = 0;
+        tree->collection.size = 0;
         return;
     }
     size_t subtree_size = cxTreeSubtreeSize(tree, node);
     cx_tree_unlink(node, cx_tree_node_layout(tree));
-    tree->size -= subtree_size;
+    tree->collection.size -= subtree_size;
 }
 
 int cxTreeDestroyNode(
@@ -1025,12 +1016,7 @@
 ) {
     int result = cxTreeRemoveNode(tree, node, relink_func);
     if (result == 0) {
-        if (tree->simple_destructor) {
-            tree->simple_destructor(node);
-        }
-        if (tree->advanced_destructor) {
-            tree->advanced_destructor(tree->destructor_data, node);
-        }
+        cx_invoke_destructor(tree, node);
         return 0;
     } else {
         return result;
@@ -1045,15 +1031,10 @@
     );
     cx_foreach(void *, child, iter) {
         if (iter.exiting) {
-            if (tree->simple_destructor) {
-                tree->simple_destructor(child);
-            }
-            if (tree->advanced_destructor) {
-                tree->advanced_destructor(tree->destructor_data, child);
-            }
+            cx_invoke_destructor(tree, child);
         }
     }
-    tree->size -= iter.counter;
+    tree->collection.size -= iter.counter;
     if (node == tree->root) {
         tree->root = NULL;
     }
@@ -1071,6 +1052,7 @@
         cxFreeDefault(q);
         q = next;
     }
+    visitor->queue_next = visitor->queue_last = NULL;
 }
 
 CxTreeIterator cxTreeIterateSubtree(CxTree *tree, void *node, bool visit_on_exit) {
--- a/ui/cocoa/ListDataSource.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/cocoa/ListDataSource.h	Sat Dec 13 15:58:58 2025 +0100
@@ -27,7 +27,7 @@
  */
 
 #import "toolkit.h"
-#import "../ui/tree.h"
+#import "../ui/list.h"
 
 @interface ListDataSource : NSObject <NSTableViewDataSource>
 
--- a/ui/cocoa/list.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/cocoa/list.h	Sat Dec 13 15:58:58 2025 +0100
@@ -28,7 +28,7 @@
 
 #import "toolkit.h"
 #import "Container.h"
-#import "../ui/tree.h"
+#import "../ui/list.h"
 
 #import "ListDataSource.h"
 
--- a/ui/cocoa/list.m	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/cocoa/list.m	Sat Dec 13 15:58:58 2025 +0100
@@ -179,7 +179,7 @@
         
         ListDataSource *dataSource = [[ListDataSource alloc] init:cols var:var getvalue:getvalue getvaluedata:getvaluedata];
         if(model) {
-            dataSource.model = ui_model_copy(obj->ctx, model);
+            dataSource.model = model;
         }
         
         tableview.dataSource = dataSource;
@@ -263,7 +263,7 @@
 
 @end
 
-UIWIDGET ui_combobox_create(UiObject* obj, UiListArgs *args) {
+UIWIDGET ui_dropdown_create(UiObject* obj, UiListArgs *args) {
     NSComboBox *dropdown = [[NSComboBox alloc] init];
     dropdown.editable = NO;
     
--- a/ui/cocoa/toolkit.m	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/cocoa/toolkit.m	Sat Dec 13 15:58:58 2025 +0100
@@ -33,6 +33,7 @@
 #include "../common/menu.h"
 #include "../common/toolbar.h"
 #include "../common/threadpool.h"
+#include "../common/app.h"
 
 #import "image.h"
 #import "menu.h"
@@ -46,13 +47,6 @@
 static int app_argc;
 static const char **app_argv;
 
-static ui_callback   startup_func;
-static void          *startup_data;
-static ui_callback   open_func;
-static void          *open_data;
-static ui_callback   exit_func;
-static void          *exit_data;
-
 static UiBool        exit_on_shutdown;
 
 /* ------------------- App Init / Event Loop functions ------------------- */
@@ -85,21 +79,6 @@
     return application_name;
 }
 
-void ui_onstartup(ui_callback f, void *userdata) {
-    startup_func = f;
-    startup_data = userdata;
-}
-
-void ui_onopen(ui_callback f, void *userdata) {
-    open_func = f;
-    open_data = userdata;
-}
-
-void ui_onexit(ui_callback f, void *userdata) {
-    exit_func = f;
-    exit_data = userdata;
-}
-
 void ui_app_exit_on_shutdown(UiBool exitapp) {
     exit_on_shutdown = exitapp;
 }
@@ -111,9 +90,7 @@
     e.document = NULL;
     e.eventdata = NULL;
     e.intval = 0;
-    if(startup_func) {
-        startup_func(&e, startup_data);
-    }
+    uic_application_startup(&e);
 }
 
 void ui_cocoa_onopen(const char *file) {
@@ -123,9 +100,7 @@
     e.document = NULL;
     e.eventdata = NULL;
     e.intval = 0;
-    if(open_func) {
-        open_func(&e, open_data);
-    }
+    uic_application_open(&e);
 }
 
 void ui_cocoa_onexit(void) {
@@ -135,9 +110,7 @@
     e.document = NULL;
     e.eventdata = NULL;
     e.intval = 0;
-    if(exit_func) {
-        exit_func(&e, exit_data);
-    }
+    uic_application_exit(&e);
 }
 
 void ui_main(void) {
--- a/ui/cocoa/window.m	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/cocoa/window.m	Sat Dec 13 15:58:58 2025 +0100
@@ -67,21 +67,18 @@
     return obj;
 }
 
-UiObject* ui_window(const char *title, void *window_data) {
+UiObject* ui_window(const char *title) {
     UiObject *obj = create_window(title, FALSE, FALSE, FALSE);
-    obj->window = window_data;
     return obj;
 }
 
-UiObject* ui_simple_window(const char *title, void *window_data) {
+UiObject* ui_simple_window(const char *title) {
     UiObject *obj = create_window(title, TRUE, FALSE, FALSE);
-    obj->window = window_data;
     return obj;
 }
 
-UiObject* ui_sidebar_window(const char *title, void *window_data) {
+UiObject* ui_sidebar_window(const char *title) {
     UiObject *obj = create_window(title, FALSE, TRUE, FALSE);
-    obj->window = window_data;
     return obj;
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/common/app.c	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,71 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "app.h"
+
+static ui_callback   startup_func;
+static void          *startup_data;
+static ui_callback   open_func;
+void                 *open_data;
+static ui_callback   exit_func;
+void                 *exit_data;
+
+
+void ui_onstartup(ui_callback f, void *userdata) {
+    startup_func = f;
+    startup_data = userdata;
+}
+
+void ui_onopen(ui_callback f, void *userdata) {
+    open_func = f;
+    open_data = userdata;
+}
+
+void ui_onexit(ui_callback f, void *userdata) {
+    exit_func = f;
+    exit_data = userdata;
+}
+
+
+void uic_application_startup(UiEvent *event) {
+    if(startup_func) {
+        startup_func(event, startup_data);
+    }
+}
+
+void uic_application_open(UiEvent *event) {
+    if(open_func) {
+        open_func(event, open_data);
+    }
+}
+
+void uic_application_exit(UiEvent *event) {
+    if(exit_func) {
+        exit_func(event, exit_data);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/common/app.h	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,48 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef UIC_APP_H
+#define UIC_APP_H
+
+#include "../ui/toolkit.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void uic_application_startup(UiEvent *event);
+void uic_application_open(UiEvent *event);
+void uic_application_exit(UiEvent *event);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UIC_APP_H */
+
--- a/ui/common/args.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/args.c	Sat Dec 13 15:58:58 2025 +0100
@@ -175,10 +175,10 @@
     free((void*)args->lbutton2);
     free((void*)args->rbutton3);
     free((void*)args->rbutton4);
-    free((void*)args->lbutton1_groups);
-    free((void*)args->lbutton2_groups);
-    free((void*)args->rbutton3_groups);
-    free((void*)args->rbutton4_groups);
+    free((void*)args->lbutton1_states);
+    free((void*)args->lbutton2_states);
+    free((void*)args->rbutton3_states);
+    free((void*)args->rbutton4_states);
     free(args);
 }
 
@@ -310,16 +310,16 @@
     args->onclickdata = onclickdata;
 }
 
-void ui_toolbar_item_args_set_groups(UiToolbarItemArgs *args, int *states, int numstates) {
-    args->groups = calloc(numstates+1, sizeof(int));
-    memcpy((void*)args->groups, states, numstates * sizeof(int));
-    ((int*)args->groups)[numstates] = -1;
+void ui_toolbar_item_args_set_states(UiToolbarItemArgs *args, int *states, int numstates) {
+    args->states = calloc(numstates+1, sizeof(int));
+    memcpy((void*)args->states, states, numstates * sizeof(int));
+    ((int*)args->states)[numstates] = -1;
 }
 void ui_toolbar_item_args_free(UiToolbarItemArgs *args) {
     free((void*)args->label);
     free((void*)args->icon);
     free((void*)args->tooltip);
-    free((void*)args->groups);
+    free((void*)args->states);
     free(args);
 }
 
@@ -355,10 +355,10 @@
     args->onchangedata = onchangedata;
 }
 
-void ui_toolbar_toggleitem_args_set_groups(UiToolbarToggleItemArgs *args,int *states, int numstates) {
-    args->groups = calloc(numstates+1, sizeof(int));
-    memcpy((void*)args->groups, states, numstates * sizeof(int));
-    ((int*)args->groups)[numstates] = -1;
+void ui_toolbar_toggleitem_args_set_states(UiToolbarToggleItemArgs *args,int *states, int numstates) {
+    args->states = calloc(numstates+1, sizeof(int));
+    memcpy((void*)args->states, states, numstates * sizeof(int));
+    ((int*)args->states)[numstates] = -1;
 }
 
 void ui_toolbar_toggleitem_args_free(UiToolbarToggleItemArgs *args) {
@@ -366,7 +366,7 @@
     free((void*)args->icon);
     free((void*)args->tooltip);
     free((void*)args->varname);
-    free((void*)args->groups);
+    free((void*)args->states);
     free(args);
 }
 
@@ -1383,10 +1383,10 @@
     args->onclickdata = onclickdata;
 }
 
-void ui_button_args_set_groups(UiButtonArgs *args, int *states, int numstates) {
-    args->groups = calloc(numstates+1, sizeof(int));
-    memcpy((void*)args->groups, states, numstates * sizeof(int));
-    ((int*)args->groups)[numstates] = -1;
+void ui_button_args_set_states(UiButtonArgs *args, int *states, int numstates) {
+    args->states = calloc(numstates+1, sizeof(int));
+    memcpy((void*)args->states, states, numstates * sizeof(int));
+    ((int*)args->states)[numstates] = -1;
 }
 
 void ui_button_args_free(UiButtonArgs *args) {
@@ -1395,7 +1395,7 @@
     free((void*)args->label);
     free((void*)args->icon);
     free((void*)args->tooltip);
-    free((void*)args->groups);
+    free((void*)args->states);
     free(args);
 }
 
@@ -1502,14 +1502,14 @@
     args->value = value;
 }
 
-void ui_toggle_args_set_enablegroup(UiToggleArgs *args, int group) {
-    args->enable_group = group;
-}
-
-void ui_toggle_args_set_groups(UiToggleArgs *args, int *states, int numstates) {
-    args->groups = calloc(numstates+1, sizeof(int));
-    memcpy((void*)args->groups, states, numstates * sizeof(int));
-    ((int*)args->groups)[numstates] = -1;
+void ui_toggle_args_set_enablestate(UiToggleArgs *args, int state) {
+    args->enable_state = state;
+}
+
+void ui_toggle_args_set_states(UiToggleArgs *args, int *states, int numstates) {
+    args->states = calloc(numstates+1, sizeof(int));
+    memcpy((void*)args->states, states, numstates * sizeof(int));
+    ((int*)args->states)[numstates] = -1;
 }
 
 void ui_toggle_args_free(UiToggleArgs *args) {
@@ -1519,7 +1519,7 @@
     free((void*)args->icon);
     free((void*)args->tooltip);
     free((void*)args->varname);
-    free((void*)args->groups);
+    free((void*)args->states);
     free(args);
 }
 
@@ -1633,10 +1633,10 @@
     args->value = value;
 }
 
-void ui_linkbutton_args_set_groups(UiLinkButtonArgs *args, int *states, int numstates) {
-    args->groups = calloc(numstates+1, sizeof(int));
-    memcpy((void*)args->groups, states, numstates * sizeof(int));
-    ((int*)args->groups)[numstates] = -1;
+void ui_linkbutton_args_set_states(UiLinkButtonArgs *args, int *states, int numstates) {
+    args->states = calloc(numstates+1, sizeof(int));
+    memcpy((void*)args->states, states, numstates * sizeof(int));
+    ((int*)args->states)[numstates] = -1;
 }
 
 void ui_linkbutton_args_free(UiLinkButtonArgs *args) {
@@ -1645,7 +1645,7 @@
     free((void*)args->label);
     free((void*)args->uri);
     free((void*)args->varname);
-    free((void*)args->groups);
+    free((void*)args->states);
     free(args);
 }
 
@@ -1815,10 +1815,10 @@
     args->contextmenu = menubuilder;
 }
 
-void ui_list_args_set_groups(UiListArgs *args, int *states, int numstates) {
-    args->groups = calloc(numstates+1, sizeof(int));
-    memcpy((void*)args->groups, states, numstates * sizeof(int));
-    ((int*)args->groups)[numstates] = -1;
+void ui_list_args_set_states(UiListArgs *args, int *states, int numstates) {
+    args->states = calloc(numstates+1, sizeof(int));
+    memcpy((void*)args->states, states, numstates * sizeof(int));
+    ((int*)args->states)[numstates] = -1;
 }
 
 void ui_list_args_free(UiListArgs *args) {
@@ -1831,7 +1831,7 @@
         }
         free(args->static_elements);
     }
-    free((void*)args->groups);
+    free((void*)args->states);
     free(args);
 }
 
@@ -1971,7 +1971,7 @@
     free((void*)args->style_class);
     free((void*)args->varname);
     free((void*)args->sublists);
-    free((void*)args->groups);
+    free((void*)args->states);
     free(args);
 }
 
@@ -2071,17 +2071,17 @@
     args->value = value;
 }
 
-void ui_textarea_args_set_groups(UiTextAreaArgs *args, int *states, int numstates) {
-    args->groups = calloc(numstates+1, sizeof(int));
-    memcpy((void*)args->groups, states, numstates * sizeof(int));
-    ((int*)args->groups)[numstates] = -1;
+void ui_textarea_args_set_states(UiTextAreaArgs *args, int *states, int numstates) {
+    args->states = calloc(numstates+1, sizeof(int));
+    memcpy((void*)args->states, states, numstates * sizeof(int));
+    ((int*)args->states)[numstates] = -1;
 }
 
 void ui_textarea_args_free(UiTextAreaArgs *args) {
     free((void*)args->name);
     free((void*)args->style_class);
     free((void*)args->varname);
-    free((void*)args->groups);
+    free((void*)args->states);
     free(args);
 }
 
@@ -2191,17 +2191,17 @@
     args->value = value;
 }
 
-void ui_textfield_args_set_groups(UiTextFieldArgs *args, int *states, int numstates) {
-    args->groups = calloc(numstates+1, sizeof(int));
-    memcpy((void*)args->groups, states, numstates * sizeof(int));
-    ((int*)args->groups)[numstates] = -1;
+void ui_textfield_args_set_states(UiTextFieldArgs *args, int *states, int numstates) {
+    args->states = calloc(numstates+1, sizeof(int));
+    memcpy((void*)args->states, states, numstates * sizeof(int));
+    ((int*)args->states)[numstates] = -1;
 }
 
 void ui_textfield_args_free(UiTextFieldArgs *args) {
     free((void*)args->name);
     free((void*)args->style_class);
     free((void*)args->varname);
-    free((void*)args->groups);
+    free((void*)args->states);
     free(args);
 }
 
@@ -2314,17 +2314,17 @@
     args->rangevalue = value;
 }
 
-void ui_spinbox_args_set_groups(UiSpinBoxArgs *args, int *states, int numstates) {
-    args->groups = calloc(numstates+1, sizeof(int));
-    memcpy((void*)args->groups, states, numstates * sizeof(int));
-    ((int*)args->groups)[numstates] = -1;
+void ui_spinbox_args_set_states(UiSpinBoxArgs *args, int *states, int numstates) {
+    args->states = calloc(numstates+1, sizeof(int));
+    memcpy((void*)args->states, states, numstates * sizeof(int));
+    ((int*)args->states)[numstates] = -1;
 }
 
 void ui_spinbox_args_free(UiSpinBoxArgs *args) {
     free((void*)args->name);
     free((void*)args->style_class);
     free((void*)args->varname);
-    free((void*)args->groups);
+    free((void*)args->states);
     free(args);
 }
 
@@ -2414,17 +2414,17 @@
     args->value = value;
 }
 
-void ui_webview_args_set_groups(UiWebviewArgs *args, int *states, int numstates) {
-    args->groups = calloc(numstates+1, sizeof(int));
-    memcpy((void*)args->groups, states, numstates * sizeof(int));
-    ((int*)args->groups)[numstates] = -1;
+void ui_webview_args_set_states(UiWebviewArgs *args, int *states, int numstates) {
+    args->states = calloc(numstates+1, sizeof(int));
+    memcpy((void*)args->states, states, numstates * sizeof(int));
+    ((int*)args->states)[numstates] = -1;
 }
 
 void ui_webview_args_free(UiWebviewArgs *args) {
     free((void*)args->name);
     free((void*)args->style_class);
     free((void*)args->varname);
-    free((void*)args->groups);
+    free((void*)args->states);
     free(args);
 }
 
--- a/ui/common/args.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/args.h	Sat Dec 13 15:58:58 2025 +0100
@@ -36,7 +36,7 @@
 #include "../ui/entry.h"
 #include "../ui/menu.h"
 #include "../ui/toolbar.h"
-#include "../ui/tree.h"
+#include "../ui/list.h"
 #include "../ui/text.h"
 #include "../ui/webview.h"
 #include "../ui/widget.h"
@@ -107,7 +107,7 @@
 UIEXPORT void ui_toolbar_item_args_set_tooltip(UiToolbarItemArgs *args, const char *tooltip);
 UIEXPORT void ui_toolbar_item_args_set_onclick(UiToolbarItemArgs *args, ui_callback callback);
 UIEXPORT void ui_toolbar_item_args_set_onclickdata(UiToolbarItemArgs *args, void *onclickdata);
-UIEXPORT void ui_toolbar_item_args_set_groups(UiToolbarItemArgs *args, int *states, int numstates);
+UIEXPORT void ui_toolbar_item_args_set_states(UiToolbarItemArgs *args, int *states, int numstates);
 UIEXPORT void ui_toolbar_item_args_free(UiToolbarItemArgs *args);
 
 UIEXPORT UiToolbarToggleItemArgs* ui_toolbar_toggleitem_args_new(void);
@@ -117,7 +117,7 @@
 UIEXPORT void ui_toolbar_toggleitem_args_set_varname(UiToolbarToggleItemArgs *args, const char *varname);
 UIEXPORT void ui_toolbar_toggleitem_args_set_onchange(UiToolbarToggleItemArgs *args, ui_callback callback);
 UIEXPORT void ui_toolbar_toggleitem_args_set_onchangedata(UiToolbarToggleItemArgs *args, void *onchangedata);
-UIEXPORT void ui_toolbar_toggleitem_args_set_groups(UiToolbarToggleItemArgs *args, int *states, int numstates);
+UIEXPORT void ui_toolbar_toggleitem_args_set_states(UiToolbarToggleItemArgs *args, int *states, int numstates);
 UIEXPORT void ui_toolbar_toggleitem_args_free(UiToolbarToggleItemArgs *args);
 
 UIEXPORT UiToolbarMenuArgs* ui_toolbar_menu_args_new(void);
@@ -137,7 +137,7 @@
 UIEXPORT void ui_container_args_set_margin_left(UiContainerArgs *args, int value);
 UIEXPORT void ui_container_args_set_margin_right(UiContainerArgs *args, int value);
 UIEXPORT void ui_container_args_set_margin_top(UiContainerArgs *args, int value);
-UIEXPORT void ui_container_args_set_margin_right(UiContainerArgs *args, int value);
+UIEXPORT void ui_container_args_set_margin_bottom(UiContainerArgs *args, int value);
 UIEXPORT void ui_container_args_set_colspan(UiContainerArgs *args, int colspan);
 UIEXPORT void ui_container_args_set_rowspan(UiContainerArgs *args, int rowspan);
 UIEXPORT void ui_container_args_set_def_hexpand(UiContainerArgs *args, UiBool value);
@@ -346,7 +346,7 @@
 UIEXPORT void ui_button_args_set_labeltype(UiButtonArgs *args, int labeltype);
 UIEXPORT void ui_button_args_set_onclick(UiButtonArgs *args, ui_callback callback);
 UIEXPORT void ui_button_args_set_onclickdata(UiButtonArgs *args, void *onclickdata);
-UIEXPORT void ui_button_args_set_groups(UiButtonArgs *args, int *states, int numstates);
+UIEXPORT void ui_button_args_set_states(UiButtonArgs *args, int *states, int numstates);
 UIEXPORT void ui_button_args_free(UiButtonArgs *args);
 
 UIEXPORT UiToggleArgs* ui_toggle_args_new(void);
@@ -373,8 +373,8 @@
 UIEXPORT void ui_toggle_args_set_onchangedata(UiToggleArgs *args, void *onchangedata);
 UIEXPORT void ui_toggle_args_set_varname(UiToggleArgs *args, const char *varname);
 UIEXPORT void ui_toggle_args_set_value(UiToggleArgs *args, UiInteger *value);
-UIEXPORT void ui_toggle_args_set_enablegroup(UiToggleArgs *args, int group);
-UIEXPORT void ui_toggle_args_set_groups(UiToggleArgs *args, int *states, int numstates);
+UIEXPORT void ui_toggle_args_set_enablestate(UiToggleArgs *args, int state);
+UIEXPORT void ui_toggle_args_set_states(UiToggleArgs *args, int *states, int numstates);
 UIEXPORT void ui_toggle_args_free(UiToggleArgs *args);
 
 UIEXPORT UiLinkButtonArgs* ui_linkbutton_args_new(void);
@@ -401,7 +401,7 @@
 UIEXPORT void ui_linkbutton_args_set_onclickdata(UiLinkButtonArgs *args, void *userdata);
 UIEXPORT void ui_linkbutton_args_set_nofollow(UiLinkButtonArgs *args, UiBool value);
 UIEXPORT void ui_linkbutton_args_set_type(UiLinkButtonArgs *args, UiLinkType type);
-UIEXPORT void ui_linkbutton_args_set_groups(UiLinkButtonArgs *args, int *states, int numstates);
+UIEXPORT void ui_linkbutton_args_set_states(UiLinkButtonArgs *args, int *states, int numstates);
 UIEXPORT void ui_linkbutton_args_free(UiLinkButtonArgs *args);
 
 UIEXPORT UiListArgs* ui_list_args_new(void);
@@ -443,7 +443,7 @@
 UIEXPORT void ui_list_args_set_onsavedata(UiListArgs *args, void *userdata);
 UIEXPORT void ui_list_args_set_multiselection(UiListArgs *args, UiBool multiselection);
 UIEXPORT void ui_list_args_set_contextmenu(UiListArgs *args, UiMenuBuilder *menubuilder);
-UIEXPORT void ui_list_args_set_groups(UiListArgs *args, int *states, int numstates);
+UIEXPORT void ui_list_args_set_states(UiListArgs *args, int *states, int numstates);
 UIEXPORT void ui_list_args_free(UiListArgs *args);
 
 UIEXPORT UiSourceListArgs* ui_sourcelist_args_new(void);
@@ -495,7 +495,7 @@
 UIEXPORT void ui_textarea_args_set_onchangedata(UiTextAreaArgs *args, void *onchangedata);
 UIEXPORT void ui_textarea_args_set_varname(UiTextAreaArgs *args, const char *varname);
 UIEXPORT void ui_textarea_args_set_value(UiTextAreaArgs *args, UiText *value);
-UIEXPORT void ui_textarea_args_set_groups(UiTextAreaArgs *args, int *states, int numstates);
+UIEXPORT void ui_textarea_args_set_states(UiTextAreaArgs *args, int *states, int numstates);
 UIEXPORT void ui_textarea_args_free(UiTextAreaArgs *args);
 
 UIEXPORT UiTextFieldArgs* ui_textfield_args_new(void);
@@ -520,7 +520,7 @@
 UIEXPORT void ui_textfield_args_set_onactivatedata(UiTextFieldArgs *args, void *onactivatedata);
 UIEXPORT void ui_textfield_args_set_varname(UiTextFieldArgs *args, const char *varname);
 UIEXPORT void ui_textfield_args_set_value(UiTextFieldArgs *args, UiString *value);
-UIEXPORT void ui_textfield_args_set_groups(UiTextFieldArgs *args, int *states, int numstates);
+UIEXPORT void ui_textfield_args_set_states(UiTextFieldArgs *args, int *states, int numstates);
 UIEXPORT void ui_textfield_args_free(UiTextFieldArgs *args);
 
 UIEXPORT UiSpinBoxArgs* ui_spinbox_args_new(void);
@@ -549,7 +549,7 @@
 UIEXPORT void ui_spinbox_args_set_intvalue(UiSpinBoxArgs *args, UiInteger *value);
 UIEXPORT void ui_spinbox_args_set_doublevalue(UiSpinBoxArgs *args, UiDouble *value);
 UIEXPORT void ui_spinbox_args_set_rangevalue(UiSpinBoxArgs *args, UiRange *value);
-UIEXPORT void ui_spinbox_args_set_groups(UiSpinBoxArgs *args, int *states, int numstates);
+UIEXPORT void ui_spinbox_args_set_states(UiSpinBoxArgs *args, int *states, int numstates);
 UIEXPORT void ui_spinbox_args_free(UiSpinBoxArgs *args);
 
 UIEXPORT UiWebviewArgs* ui_webview_args_new(void);
@@ -570,7 +570,7 @@
 UIEXPORT void ui_webview_args_set_style_class(UiWebviewArgs *args, const char *classname);
 UIEXPORT void ui_webview_args_set_varname(UiWebviewArgs *args, const char *varname);
 UIEXPORT void ui_webview_args_set_value(UiWebviewArgs *args, UiGeneric *value);
-UIEXPORT void ui_webview_args_set_groups(UiWebviewArgs *args, int *states, int numstates);
+UIEXPORT void ui_webview_args_set_states(UiWebviewArgs *args, int *states, int numstates);
 UIEXPORT void ui_webview_args_free(UiWebviewArgs *args);
 
 #ifdef __cplusplus
--- a/ui/common/context.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/context.c	Sat Dec 13 15:58:58 2025 +0100
@@ -64,8 +64,8 @@
     ctx->vars = cxHashMapCreate(mp->allocator, CX_STORE_POINTERS, 16);
     
     ctx->documents = cxLinkedListCreate(mp->allocator, cx_cmp_ptr, CX_STORE_POINTERS);
-    ctx->group_widgets = cxLinkedListCreate(mp->allocator, cx_cmp_ptr, sizeof(UiGroupWidget));
-    ctx->groups = cxArrayListCreate(mp->allocator, cx_cmp_int, sizeof(int), 32);
+    ctx->state_widgets = cxLinkedListCreate(mp->allocator, cx_cmp_ptr, sizeof(UiStateWidget));
+    ctx->states = cxArrayListCreate(mp->allocator, cx_cmp_int, sizeof(int), 32);
     
     ctx->attach_document = uic_context_attach_document;
     ctx->detach_document2 = uic_context_detach_document;
@@ -92,11 +92,17 @@
 }
 
 void uic_context_prepare_close(UiContext *ctx) {
-    cxListClear(ctx->groups);
-    cxListClear(ctx->group_widgets);
+    cxListClear(ctx->states);
+    cxListClear(ctx->state_widgets);
 }
 
 void uic_context_attach_document(UiContext *ctx, void *document) {
+    if(ctx->single_document_mode) {
+        if(ctx->document) {
+            uic_context_detach_document(ctx, ctx->document);
+        }
+    }
+    
     cxListAdd(ctx->documents, document);
     ctx->document = document;
     
@@ -114,7 +120,7 @@
             UiVar *docvar = cxMapGet(doc_ctx->vars, *entry->key);
             if(docvar) {
                 // bind var to document var
-                uic_copy_binding(var, docvar, TRUE);
+                uic_copy_var_binding(var, docvar, TRUE);
                 cxIteratorFlagRemoval(i);
             }
         }
@@ -124,16 +130,19 @@
 }
 
 static void uic_context_unbind_vars(UiContext *ctx) {
+    ui_onchange_events_enable(FALSE);
     CxMapIterator mi = cxMapIterator(ctx->vars);
     cx_foreach(CxMapEntry*, entry, mi) {
+        printf("detach %.*s\n", (int)entry->key->len, (char*)entry->key->data);
         UiVar *var = entry->value;
         // var->from && var->from_ctx && var->from_ctx != ctx
         uic_save_var(var);
         if(var->from) {
-            uic_copy_binding(var, var->from, FALSE);
+            uic_copy_var_binding(var, var->from, FALSE);
             cxMapPut(var->from->from_ctx->vars, *entry->key, var->from);
             var->from = NULL;
         }
+        uic_unbind_var(var);
     }
     
     if(ctx->documents) {
@@ -143,6 +152,8 @@
             uic_context_unbind_vars(subctx);
         }
     }
+    
+    ui_onchange_events_enable(TRUE);
 }
 
 void uic_context_detach_document(UiContext *ctx, void *document) {
@@ -197,6 +208,14 @@
     return ctx_getvar(ctx, key);
 }
 
+UiVar* uic_get_var_t(UiContext *ctx,const char *name, UiVarType type) {
+    UiVar *var = uic_get_var(ctx, name);
+    if(var && var->type == type) {
+        return var;
+    }
+    return NULL;
+}
+
 UiVar* uic_create_var(UiContext *ctx, const char *name, UiVarType type) {
     UiVar *var = uic_get_var(ctx, name);
     if(var) {
@@ -213,8 +232,9 @@
     var->original_value = NULL;
     var->from = NULL;
     var->from_ctx = ctx;
-
-    cxMempoolSetDestructor(var, (cx_destructor_func)uic_unbind_var);
+    var->bound = FALSE;
+    
+    cxMempoolSetDestructor(var, (cx_destructor_func)uic_unbind_var); // TODO: use another destructor that cleans the value (UiString free, UiText destroy, ...)
 
     cxMapPut(ctx->vars, name, var);
     
@@ -266,6 +286,40 @@
     return val;
 }
 
+// destroys a value, that was created by uic_create_value
+void uic_destroy_value(UiContext *ctx, UiVarType type, void *value) {
+    switch(type) {
+        default: {
+            ui_free(ctx, value);
+            break;
+        }
+        case UI_VAR_SPECIAL: break;
+        case UI_VAR_STRING: {
+            UiString *s = value;
+            if(s->value.free) {
+                s->value.free(s->value.ptr);
+            }
+            ui_free(ctx, value);
+        }
+        case UI_VAR_TEXT: {
+            UiText *t = value;
+            if(t->value.free) {
+                t->value.free(t->value.ptr);
+                t->value.free = NULL;
+                t->value.ptr = NULL;
+            }
+            if(t->destroy) {
+                t->destroy(t);
+            }
+            ui_free(ctx, value);
+        }
+        case UI_VAR_LIST: {
+            ui_list_free(ctx, value);
+            break;
+        }
+    }
+}
+
 
 UiVar* uic_widget_var(UiContext *toplevel, UiContext *current, void *value, const char *varname, UiVarType type) {
     if (value) {
@@ -278,7 +332,7 @@
 }
 
 
-void uic_copy_binding(UiVar *from, UiVar *to, UiBool copytodoc) {
+void uic_copy_var_binding(UiVar *from, UiVar *to, UiBool copytodoc) {
     // check type
     if(from->type != to->type) {
         fprintf(stderr, "UI Error: var has incompatible type.\n");
@@ -301,32 +355,36 @@
         }
     }
     
-    ui_setop_enable(TRUE);
+    uic_copy_value_binding(from->type, fromvalue, tovalue);
+}
+
+void uic_copy_value_binding(UiVarType type, void *from, void *to) {
+     ui_setop_enable(TRUE);
     
     // copy binding
-    // we don't copy the observer, because the from var has never one
-    switch(from->type) {
+    // we don't copy the observer, because the from value never has oberservers
+    switch(type) {
         default: fprintf(stderr, "uic_copy_binding: wtf!\n"); break;
         case UI_VAR_SPECIAL: break;
         case UI_VAR_INTEGER: {
-            UiInteger *f = fromvalue;
-            UiInteger *t = tovalue;
+            UiInteger *f = from;
+            UiInteger *t = to;
             if(!f->obj) break;
             uic_int_copy(f, t);
             t->set(t, t->value);
             break;
         }
         case UI_VAR_DOUBLE: {
-            UiDouble *f = fromvalue;
-            UiDouble *t = tovalue;
+            UiDouble *f = from;
+            UiDouble *t = to;
             if(!f->obj) break;
             uic_double_copy(f, t);
             t->set(t, t->value);
             break;
         }
         case UI_VAR_STRING: {
-            UiString *f = fromvalue;
-            UiString *t = tovalue;
+            UiString *f = from;
+            UiString *t = to;
             if(!f->obj) break;
             uic_string_copy(f, t);
             char *tvalue = t->value.ptr ? t->value.ptr : "";
@@ -335,23 +393,23 @@
             break;
         }
         case UI_VAR_TEXT: {
-            UiText *f = fromvalue;
-            UiText *t = tovalue;
+            UiText *f = from;
+            UiText *t = to;
             if(!f->obj) break;
             uic_text_copy(f, t);
             t->restore(t);
             break;
         }
         case UI_VAR_LIST: {         
-            UiList *f = fromvalue;
-            UiList *t = tovalue;
+            UiList *f = from;
+            UiList *t = to;
             uic_list_copy(f, t);
             ui_list_update(t);
             break;
         }
         case UI_VAR_RANGE: {
-            UiRange *f = fromvalue;
-            UiRange *t = tovalue;
+            UiRange *f = from;
+            UiRange *t = to;
             if(!f->obj) break;
             uic_range_copy(f, t);
             t->setextent(t, t->extent);
@@ -360,8 +418,8 @@
             break;
         }
         case UI_VAR_GENERIC: {
-            UiGeneric *f = fromvalue;
-            UiGeneric *t = tovalue;
+            UiGeneric *f = from;
+            UiGeneric *t = to;
             if(!f->obj) break;
             uic_generic_copy(f, t);
             t->set(t, t->value, t->type);
@@ -398,59 +456,56 @@
     }
 }
 
+const char *uic_type2str(UiVarType type) {
+    switch(type) {
+        default: return "";
+        case UI_VAR_INTEGER: return "int";
+        case UI_VAR_DOUBLE: return "double";
+        case UI_VAR_STRING: return "string";
+        case UI_VAR_TEXT: return "text";
+        case UI_VAR_LIST: return "list";
+        case UI_VAR_RANGE: return "range";
+        case UI_VAR_GENERIC: return "generic";
+    }
+}
+
 void uic_reg_var(UiContext *ctx, const char *name, UiVarType type, void *value) {
-    // TODO: do we need/want this? Why adding vars to a context after
-    // widgets reference these? Workarounds:
-    // 1. add vars to ctx before creating ui
-    // 2. create ui, create new document with vars, attach doc
-    // also it would be possible to create a function, that scans unbound vars
-    // and connects them to available vars
-    /*
-    UiContext *rootctx = uic_root_context(ctx); 
-    UiVar *b = NULL;
-    if(rootctx->bound) {
-        // some widgets are already bound to some vars
-        b = ucx_map_cstr_get(rootctx->bound, name);
-        if(b) {
-            // a widget is bound to a var with this name
-            // if ctx is the root context we can remove the var from bound
-            // because set_doc or detach can't fuck things up
-            if(ctx == rootctx) {
-                ucx_map_cstr_remove(ctx->bound, name);
-                // TODO: free stuff
-            }
+    UiVar *var = cxMapGet(ctx->vars, name);
+    if(!var) {
+        // create new var and add it to the context var map
+        var = ui_malloc(ctx, sizeof(UiVar));
+        cxMapPut(ctx->vars, name, var);
+    } else {
+        // override var with new value
+        if(var->type != type) {
+            fprintf(stderr, "Error: var %s type mismatch: %s - %s\n", name, uic_type2str(var->type), uic_type2str(type));
+            return;
         }
+        if(var->bound) {
+            fprintf(stderr, "Error: var %s already bound\n", name);
+            return;
+        }
+        UiInteger *prev_value = var->value;
+        uic_copy_value_binding(type, prev_value, value);
+        uic_destroy_value(var->from_ctx, var->type, var->value);
     }
-    */
     
-    // create new var and add it to doc's vars
-    UiVar *var = ui_malloc(ctx, sizeof(UiVar));
     var->type = type;
     var->value = value;
     var->from = NULL;
     var->from_ctx = ctx;
-    size_t oldcount = cxMapSize(ctx->vars);
-    cxMapPut(ctx->vars, name, var);
-    if(cxMapSize(ctx->vars) != oldcount + 1) {
-        fprintf(stderr, "UiError: var '%s' already exists\n", name);
-    }
-    
-    // TODO: remove?
-    // a widget is already bound to a var with this name
-    // copy the binding (like uic_context_set_document)
-    /*
-    if(b) {
-        uic_copy_binding(b, var, TRUE);
-    }
-    */
+    var->bound = TRUE;
 }
 
-void uic_remove_bound_var(UiContext *ctx, UiVar *var) {
-    // TODO
+// public API
+
+void* ui_context_get_document(UiContext *ctx) {
+    return ctx->document;
 }
 
-
-// public API
+void ui_context_single_attachment_mode(UiContext *ctx, UiBool enable) {
+    ctx->single_document_mode = enable;
+}
 
 void ui_attach_document(UiContext *ctx, void *document) {
     uic_context_attach_document(ctx, document);
@@ -478,48 +533,48 @@
 }
 
 
-void ui_set_group(UiContext *ctx, int group) {
-    if(!cxListIndexValid(ctx->groups, cxListFind(ctx->groups, &group))) {
-        cxListAdd(ctx->groups, &group);
+void ui_set_state(UiContext *ctx, int state) {
+    if(!cxListIndexValid(ctx->states, cxListFind(ctx->states, &state))) {
+        cxListAdd(ctx->states, &state);
     }
     
     // enable/disable group widgets
-    uic_check_group_widgets(ctx);
+    uic_check_state_widgets(ctx);
 }
 
-void ui_unset_group(UiContext *ctx, int group) {
-    int i = cxListFind(ctx->groups, &group);
+void ui_unset_state(UiContext *ctx, int state) {
+    int i = cxListFind(ctx->states, &state);
     if(i != -1) {
-        cxListRemove(ctx->groups, i);
+        cxListRemove(ctx->states, i);
     }
     
     // enable/disable group widgets
-    uic_check_group_widgets(ctx);
+    uic_check_state_widgets(ctx);
 }
 
-int* ui_active_groups(UiContext *ctx, int *ngroups) {
-    *ngroups = cxListSize(ctx->groups);
-    return cxListAt(ctx->groups, 0);
+int* ui_active_states(UiContext *ctx, int *nstates) {
+    *nstates = cxListSize(ctx->states);
+    return cxListAt(ctx->states, 0);
 }
 
-void uic_check_group_widgets(UiContext *ctx) {
+void uic_check_state_widgets(UiContext *ctx) {
     int ngroups = 0;
-    int *groups = ui_active_groups(ctx, &ngroups);
+    int *groups = ui_active_states(ctx, &ngroups);
     
-    CxIterator i = cxListIterator(ctx->group_widgets);
-    cx_foreach(UiGroupWidget *, gw, i) {
-        char *check = calloc(1, gw->numgroups);
+    CxIterator i = cxListIterator(ctx->state_widgets);
+    cx_foreach(UiStateWidget *, gw, i) {
+        char *check = calloc(1, gw->numstates);
         
         for(int i=0;i<ngroups;i++) {
-            for(int k=0;k<gw->numgroups;k++) {
-                if(groups[i] == gw->groups[k]) {
+            for(int k=0;k<gw->numstates;k++) {
+                if(groups[i] == gw->states[k]) {
                     check[k] = 1;
                 }
             }
         }
         
         int enable = 1;
-        for(int i=0;i<gw->numgroups;i++) {
+        for(int i=0;i<gw->numstates;i++) {
             if(check[i] == 0) {
                 enable = 0;
                 break;
@@ -530,70 +585,70 @@
     }
 }
 
-void ui_widget_set_groups(UiContext *ctx, UIWIDGET widget, ui_enablefunc enable, ...) {
+void ui_widget_set_states(UiContext *ctx, UIWIDGET widget, ui_enablefunc enable, ...) {
     if(enable == NULL) {
         enable = (ui_enablefunc)ui_set_enabled;
     }
-    // get groups
-    CxList *groups = cxArrayListCreate(cxDefaultAllocator, NULL, sizeof(int), 16);
+    // get states
+    CxList *states = cxArrayListCreate(cxDefaultAllocator, NULL, sizeof(int), 16);
     va_list ap;
     va_start(ap, enable);
-    int group;
-    while((group = va_arg(ap, int)) != -1) {
-        cxListAdd(groups, &group);
+    int state;
+    while((state = va_arg(ap, int)) != -1) {
+        cxListAdd(states, &state);
     }
     va_end(ap);
     
-    uic_add_group_widget(ctx, widget, enable, groups);
+    uic_add_state_widget(ctx, widget, enable, states);
     
-    cxListFree(groups);
+    cxListFree(states);
 }
 
-void ui_widget_set_groups2(UiContext *ctx, UIWIDGET widget, ui_enablefunc enable, const int *groups, int ngroups) {
+void ui_widget_set_states2(UiContext *ctx, UIWIDGET widget, ui_enablefunc enable, const int *states, int nstates) {
     if(enable == NULL) {
         enable = (ui_enablefunc)ui_set_enabled;
     }
-    CxList *ls = cxArrayListCreate(cxDefaultAllocator, NULL, sizeof(int), ngroups);
-    for(int i=0;i<ngroups;i++) {
-        cxListAdd(ls, groups+i);
+    CxList *ls = cxArrayListCreate(cxDefaultAllocator, NULL, sizeof(int), nstates);
+    for(int i=0;i<nstates;i++) {
+        cxListAdd(ls, states+i);
     }
-    uic_add_group_widget(ctx, widget, enable, ls);
+    uic_add_state_widget(ctx, widget, enable, ls);
     cxListFree(ls);
 }
 
 void ui_widget_set_visibility_states(UiContext *ctx, UIWIDGET widget, const int *states, int nstates) {
-    ui_widget_set_groups2(ctx, widget, (ui_enablefunc)ui_set_visible, states, nstates);
+    ui_widget_set_states2(ctx, widget, (ui_enablefunc)ui_set_visible, states, nstates);
 }
 
-size_t uic_group_array_size(const int *groups) {
+size_t uic_state_array_size(const int *states) {
     int i;
-    for(i=0;groups[i] >= 0;i++) { }
+    for(i=0;states[i] >= 0;i++) { }
     return i;
 }
 
-void uic_add_group_widget(UiContext *ctx, void *widget, ui_enablefunc enable, CxList *groups) {
-    uic_add_group_widget_i(ctx, widget, enable, cxListAt(groups, 0), cxListSize(groups));
+void uic_add_state_widget(UiContext *ctx, void *widget, ui_enablefunc enable, CxList *states) {
+    uic_add_state_widget_i(ctx, widget, enable, cxListAt(states, 0), cxListSize(states));
 }
 
-void uic_add_group_widget_i(UiContext *ctx, void *widget, ui_enablefunc enable, const int *groups, size_t numgroups) {
+void uic_add_state_widget_i(UiContext *ctx, void *widget, ui_enablefunc enable, const int *states, size_t numstates) {
     const CxAllocator *a = ctx->allocator;
-    UiGroupWidget gw;
+    UiStateWidget gw;
     
     gw.widget = widget;
     gw.enable = enable;
-    gw.numgroups = numgroups;
-    gw.groups = cxCalloc(a, numgroups, sizeof(int));
+    gw.numstates = numstates;
+    gw.states = cxCalloc(a, numstates, sizeof(int));
     
-    // copy groups
-    if(groups) {
-        memcpy(gw.groups, groups, gw.numgroups * sizeof(int));
+    // copy states
+    if(states) {
+        memcpy(gw.states, states, gw.numstates * sizeof(int));
     }
     
-    cxListAdd(ctx->group_widgets, &gw);
+    cxListAdd(ctx->state_widgets, &gw);
 }
 
-void uic_remove_group_widget(UiContext *ctx, void *widget) {
-    (void)cxListFindRemove(ctx->group_widgets, widget);
+void uic_remove_state_widget(UiContext *ctx, void *widget) {
+    (void)cxListFindRemove(ctx->state_widgets, widget);
 }
 
 UIEXPORT void *ui_allocator(UiContext *ctx) {
@@ -638,3 +693,33 @@
 void  ui_set_destructor(void *mem, ui_destructor_func destr) {
     cxMempoolSetDestructor(mem, (cx_destructor_func)destr);
 }
+
+UiInteger* ui_get_int_var(UiContext *ctx, const char *name) {
+    UiVar *var = uic_get_var_t(ctx, name, UI_VAR_INTEGER);
+    return var ? var->value : NULL;
+}
+
+UiDouble* ui_get_double_var(UiContext *ctx, const char *name) {
+    UiVar *var = uic_get_var_t(ctx, name, UI_VAR_DOUBLE);
+    return var ? var->value : NULL;
+}
+
+UiString* ui_get_string_var(UiContext *ctx, const char *name) {
+    UiVar *var = uic_get_var_t(ctx, name, UI_VAR_STRING);
+    return var ? var->value : NULL;
+}
+
+UiText* ui_get_text_var(UiContext *ctx, const char *name) {
+    UiVar *var = uic_get_var_t(ctx, name, UI_VAR_TEXT);
+    return var ? var->value : NULL;
+}
+
+UiRange* ui_get_range_var(UiContext *ctx, const char *name) {
+    UiVar *var = uic_get_var_t(ctx, name, UI_VAR_RANGE);
+    return var ? var->value : NULL;
+}
+
+UiGeneric* ui_get_generic_var(UiContext *ctx, const char *name) {
+    UiVar *var = uic_get_var_t(ctx, name, UI_VAR_GENERIC);
+    return var ? var->value : NULL;
+}
--- a/ui/common/context.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/context.h	Sat Dec 13 15:58:58 2025 +0100
@@ -43,7 +43,7 @@
 typedef struct UiVar            UiVar;
 typedef struct UiListPtr        UiListPtr;
 typedef struct UiListVar        UiListVar;
-typedef struct UiGroupWidget    UiGroupWidget;
+typedef struct UiStateWidget    UiStateWidget;
 typedef struct UiDestroyHandler UiDestroyHandler;
 
 typedef enum UiVarType {
@@ -69,8 +69,8 @@
     
     CxMap         *vars;
     
-    CxList        *groups; // int list
-    CxList        *group_widgets; // UiGroupWidget list
+    CxList        *states; // int list
+    CxList        *state_widgets; // UiGroupWidget list
     
     void (*attach_document)(UiContext *ctx, void *document);
     void (*detach_document2)(UiContext *ctx, void *document); 
@@ -84,8 +84,10 @@
     GtkAccelGroup *accel_group;
 #endif 
 #endif
-
-
+    
+    // allow only one document to be attached
+    // attaching a document will automatically detach the current document
+    UiBool single_document_mode;
     
     ui_callback   close_callback;
     void          *close_data;
@@ -97,13 +99,14 @@
     UiVarType type;
     UiVar     *from;
     UiContext *from_ctx;
+    UiBool    bound;
 };
 
-struct UiGroupWidget {
+struct UiStateWidget {
     void          *widget;
     ui_enablefunc enable;
-    int           *groups;
-    int           numgroups;
+    int           *states;
+    int           numstates;
 };
 
 struct UiDestroyHandler {
@@ -127,25 +130,27 @@
 void uic_context_detach_all(UiContext *ctx);
 
 UiVar* uic_get_var(UiContext *ctx, const char *name);
+UiVar* uic_get_var_t(UiContext *ctx, const char *name, UiVarType type);
 UiVar* uic_create_var(UiContext *ctx, const char *name, UiVarType type);
 UiVar* uic_create_value_var(UiContext *ctx, void *value);
 void* uic_create_value(UiContext *ctx, UiVarType type);
+void uic_destroy_value(UiContext *ctx, UiVarType type, void *value);
 
 UiVar* uic_widget_var(UiContext *toplevel, UiContext *current, void *value, const char *varname, UiVarType type);
 
-void uic_copy_binding(UiVar *from, UiVar *to, UiBool copytodoc);
+void uic_copy_var_binding(UiVar *from, UiVar *to, UiBool copytodoc);
+void uic_copy_value_binding(UiVarType type, void *from, void *to);
 void uic_save_var(UiVar *var);
 void uic_unbind_var(UiVar *var);
+const char *uic_type2str(UiVarType type);
 
 void uic_reg_var(UiContext *ctx, const char *name, UiVarType type, void *value);
 
-void uic_remove_bound_var(UiContext *ctx, UiVar *var);
-
-size_t uic_group_array_size(const int *groups);
-void uic_check_group_widgets(UiContext *ctx);
-void uic_add_group_widget(UiContext *ctx, void *widget, ui_enablefunc enable, CxList *groups);
-void uic_add_group_widget_i(UiContext *ctx, void *widget, ui_enablefunc enable, const int *groups, size_t numgroups);
-void uic_remove_group_widget(UiContext *ctx, void *widget);
+size_t uic_state_array_size(const int *states);
+void uic_check_state_widgets(UiContext *ctx);
+void uic_add_state_widget(UiContext *ctx, void *widget, ui_enablefunc enable, CxList *states);
+void uic_add_state_widget_i(UiContext *ctx, void *widget, ui_enablefunc enable, const int *states, size_t numstates);
+void uic_remove_state_widget(UiContext *ctx, void *widget);
 
 #ifdef	__cplusplus
 }
--- a/ui/common/menu.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/menu.c	Sat Dec 13 15:58:58 2025 +0100
@@ -89,20 +89,20 @@
     return s ? strdup(s) : NULL;
 }
 
-int* uic_copy_groups(const int* groups, size_t *ngroups) {
-    *ngroups = 0;
-    if (!groups) {
+int* uic_copy_states(const int* states, size_t *nstates) {
+    *nstates = 0;
+    if (!states) {
         return NULL;
     }
 
     size_t n;
-    for (n = 0; groups[n] > -1; n++) { }
+    for (n = 0; states[n] > -1; n++) { }
 
-    if (ngroups > 0) {
+    if (nstates > 0) {
         int* newarray = calloc(n+1, sizeof(int));
-        memcpy(newarray, groups, n * sizeof(int));
+        memcpy(newarray, states, n * sizeof(int));
         newarray[n] = -1;
-        *ngroups = n;
+        *nstates = n;
         return newarray;
     }
     return NULL;
@@ -152,7 +152,7 @@
     item->icon = nl_strdup(args->icon);
     item->userdata = args->onclickdata;
     item->callback = args->onclick;
-    item->groups = uic_copy_groups(args->groups, &item->ngroups);
+    item->states = uic_copy_states(args->states, &item->nstates);
 
     add_item((UiMenuItemI*)item);
 }
@@ -179,7 +179,7 @@
     item->varname = nl_strdup(args->varname);
     item->userdata = args->onchangedata;
     item->callback = args->onchange;
-    item->groups = uic_copy_groups(args->groups, &item->ngroups);
+    item->states = uic_copy_states(args->nstates, &item->nstates);
     
     add_item((UiMenuItemI*)item);
 }
@@ -196,7 +196,7 @@
     item->varname = nl_strdup(args->varname);
     item->userdata = args->onchangedata;
     item->callback = args->onchange;
-    item->groups = uic_copy_groups(args->groups, &item->ngroups);
+    item->states = uic_copy_states(args->nstates, &item->nstates);
 
     add_item((UiMenuItemI*)item);
 }
@@ -296,14 +296,14 @@
         }
         case UI_MENU_ITEM: {
             UiMenuItem *i = (UiMenuItem*)item;
-            free(i->groups);
+            free(i->states);
             free(i->label);
             free(i->icon);
             break;
         }
         case UI_MENU_CHECK_ITEM: {
             UiMenuCheckItem *i = (UiMenuCheckItem*)item;
-            free(i->groups);
+            free(i->states);
             free(i->label);
             free(i->icon);
             free(i->varname);
@@ -311,7 +311,7 @@
         }
         case UI_MENU_RADIO_ITEM: {
             UiMenuRadioItem *i = (UiMenuRadioItem*)item;
-            free(i->groups);
+            free(i->states);
             free(i->label);
             free(i->icon);
             free(i->varname);
--- a/ui/common/menu.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/menu.h	Sat Dec 13 15:58:58 2025 +0100
@@ -79,8 +79,8 @@
     char           *label;
     char           *icon;
     void           *userdata;
-    int            *groups;
-    size_t         ngroups;
+    int            *states;
+    size_t         nstates;
 };
 
 struct UiMenuCheckItem {
@@ -90,8 +90,8 @@
     char           *varname;
     ui_callback    callback;
     void           *userdata;
-    int            *groups;
-    size_t         ngroups;
+    int            *states;
+    size_t         nstates;
 };
 
 struct UiMenuRadioItem {
@@ -101,8 +101,8 @@
     char           *varname;
     ui_callback    callback;
     void           *userdata;
-    int            *groups;
-    size_t         ngroups;
+    int            *states;
+    size_t         nstates;
 };
 
 struct UiMenuItemList {
@@ -129,7 +129,7 @@
 
 void uic_add_menu_to_stack(UiMenu* menu);
 
-int* uic_copy_groups(const int* groups, size_t *ngroups);
+int* uic_copy_states(const int* states, size_t *nstates);
 
 void uic_set_tmp_eventdata(void *eventdata, int type);
 void* uic_get_tmp_eventdata(void);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/common/message.c	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,214 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef _WIN32
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "message.h"
+
+int uic_message_send_(UiMessageHandler *handler, cxstring msg) {
+    return handler->send(handler, msg);
+}
+
+UiMessageHandler* uic_simple_msg_handler(int in, int out, msg_received_callback callback) {
+    UiSimpleMessageHandler *handler = malloc(sizeof(UiSimpleMessageHandler));
+    handler->handler.start = uic_simple_msg_handler_start;
+    handler->handler.stop = uic_simple_msg_handler_stop;
+    handler->handler.send = uic_simple_msg_handler_send;
+    handler->handler.callback = callback;
+    handler->in = in;
+    handler->out = out;
+    handler->outbuf = cxBufferCreate(NULL, 4096, NULL, CX_BUFFER_FREE_CONTENTS | CX_BUFFER_AUTO_EXTEND);
+    handler->stop = 0;
+    pthread_mutex_init(&handler->queue_lock, NULL);
+    pthread_mutex_init(&handler->avlbl_lock, NULL);
+    pthread_cond_init(&handler->available, NULL);  
+    return (UiMessageHandler*)handler;
+}
+
+int uic_simple_msg_handler_start(UiMessageHandler *handler) {
+    UiSimpleMessageHandler *sh = (UiSimpleMessageHandler*)handler;
+    if(pthread_create(&sh->in_thread, NULL, uic_simple_msg_handler_in_thread, sh)) {
+        return 1;
+    }
+    if(pthread_create(&sh->out_thread, NULL, uic_simple_msg_handler_out_thread, sh)) {
+        return 1;
+    }
+    return 0;
+}
+
+int uic_simple_msg_handler_stop(UiMessageHandler *handler) {
+    UiSimpleMessageHandler *sh = (UiSimpleMessageHandler*)handler;
+    pthread_mutex_lock(&sh->queue_lock);
+    sh->stop = 0;
+    pthread_cond_signal(&sh->available);
+    pthread_mutex_unlock(&sh->queue_lock);
+    close(sh->in);
+    sh->in = -1;
+    
+    pthread_join(sh->in_thread, NULL);
+    pthread_join(sh->out_thread, NULL);
+    
+    return 0;
+}
+
+int uic_simple_msg_handler_send(UiMessageHandler *handler, cxstring msg) {
+    UiSimpleMessageHandler *sh = (UiSimpleMessageHandler*)handler;
+    pthread_mutex_lock(&sh->queue_lock);
+    char header[32];
+    snprintf(header, 32, "%zu\n", msg.length);
+    cxBufferPutString(sh->outbuf, header);
+    cxBufferWrite(msg.ptr, 1, msg.length, sh->outbuf);
+    pthread_cond_signal(&sh->available);
+    pthread_mutex_unlock(&sh->queue_lock);
+    return 0;
+}
+
+#define HEADERBUF_SIZE 64
+
+void* uic_simple_msg_handler_in_thread(void *data) {
+    UiSimpleMessageHandler *handler = data;
+    
+    char *msg = NULL;
+    size_t msg_size = 0;
+    size_t msg_pos = 0; // currently received message length
+    
+    char headerbuf[HEADERBUF_SIZE];
+    size_t headerpos = 0;
+    
+    char buf[2048];
+    ssize_t r;
+    while((r = read(handler->in, buf, 2024)) > 0) {
+        char *buffer = buf;
+        size_t available = r;
+        
+        while(available > 0) {
+            if(msg) {
+                // read message
+                size_t need = msg_size - msg_pos;
+                size_t cplen = r > need ? need : available;
+                memcpy(msg+msg_pos, buffer, cplen);
+                buffer += cplen;
+                available -= cplen;
+                msg_pos += cplen;
+                if(msg_pos == msg_size) {
+                    // message complete
+                    //fprintf(stderr, "send: %.*s\n", (int)msg_size, msg);
+                    if(handler->handler.callback) {
+                        handler->handler.callback(cx_strn(msg, msg_size));
+                    }
+                    free(msg);
+                    msg = NULL;
+                    msg_size = 0;
+                    msg_pos = 0;
+                }
+            } else {
+                size_t header_max = HEADERBUF_SIZE - headerpos - 1;
+                if(header_max > available) {
+                    header_max = available;
+                }
+                // search for line break
+                int i;
+                int header_complete = 0;
+                for(i=0;i<header_max;i++) {
+                    if(buffer[i] == '\n') {
+                        header_complete = 1;
+                        break;
+                    }
+                }
+                i++;
+                memcpy(headerbuf+headerpos, buffer, i);
+                headerpos += i;
+                buffer += i;
+                available -= i;
+                
+                if(header_complete) {
+                    headerbuf[headerpos-1] = 0; // terminate buffer
+                    char *end;
+                    long length = strtol(headerbuf, &end, 10);
+                    if(*end == '\0') {
+                        //fprintf(stderr, "header: %d\n", (int)length);
+                        msg = malloc(length);
+                        msg_size = length;
+                        headerpos = 0;
+                    } else {
+                        fprintf(stderr, "Error: invalid message {%s}\n", headerbuf);
+                    }
+                } else if(headerpos+1 >= HEADERBUF_SIZE) {
+                    fprintf(stderr, "Error: message header too big\n");
+                    exit(-1);
+                }
+            }
+        }
+        
+        
+    }
+    perror("error");
+    fprintf(stderr, "stop simple_msg_handler_in_thread\n");
+    
+    return NULL;
+}
+
+void* uic_simple_msg_handler_out_thread(void *data) {
+    UiSimpleMessageHandler *handler = data;
+    CxBuffer *buffer = handler->outbuf;
+    
+    pthread_mutex_lock(&handler->queue_lock);
+    
+    for(;;) {
+        if(buffer->pos == 0) {
+            pthread_cond_wait(&handler->available, &handler->queue_lock);
+            continue;
+        } else {
+            size_t n = buffer->pos;
+            size_t pos = 0;
+            while(n > 0) {
+                ssize_t w = write(handler->out, buffer->space + pos, n);
+                if(w <= 0) {
+                    fprintf(stderr, "Error: output error\n");
+                    break;
+                }
+                n -= w;
+                pos += w;
+            }
+            if(n > 0) {
+                break; // error
+            }
+            buffer->pos = 0;
+        }
+    }
+    
+    pthread_mutex_unlock(&handler->queue_lock);
+    
+    return NULL;
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/common/message.h	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,91 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef UIC_MESSAGE_H
+#define UIC_MESSAGE_H
+
+
+#include <cx/string.h>
+#include <cx/json.h>
+#include <cx/buffer.h>
+
+#ifndef _WIN32
+#include <pthread.h>
+#endif
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+typedef struct UiMessageHandler UiMessageHandler;
+
+typedef void(*msg_received_callback)(cxstring msg);
+
+struct UiMessageHandler {
+    int (*start)(UiMessageHandler *handler);
+    int (*stop)(UiMessageHandler *handler);
+    int (*send)(UiMessageHandler *handler, cxstring msg);
+    
+    msg_received_callback callback;
+};
+
+typedef struct UiSimpleMessageHandler {
+    UiMessageHandler handler;
+    int in;
+    int out;
+#ifndef _WIN32
+    pthread_t in_thread;
+    pthread_t out_thread;
+    pthread_mutex_t queue_lock;
+    pthread_mutex_t avlbl_lock;
+    pthread_cond_t  available;
+#endif
+    CxBuffer *outbuf;
+    int stop;
+} UiSimpleMessageHandler;
+
+int uic_message_send_(UiMessageHandler *handler, cxstring msg);
+#define uic_message_send(handler, msg) uic_message_send_(handler, cx_strcast(msg))
+
+UiMessageHandler* uic_simple_msg_handler(int in, int out, msg_received_callback callback);
+int uic_simple_msg_handler_start(UiMessageHandler *handler);
+int uic_simple_msg_handler_stop(UiMessageHandler *handler);
+int uic_simple_msg_handler_send(UiMessageHandler *handler, cxstring msg);
+
+void* uic_simple_msg_handler_in_thread(void *data);
+void* uic_simple_msg_handler_out_thread(void *data);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UIC_MESSAGE_H */
+
--- a/ui/common/object.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/object.c	Sat Dec 13 15:58:58 2025 +0100
@@ -32,6 +32,7 @@
 #include "context.h"
 
 #include <cx/linked_list.h>
+#include <cx/hash_map.h>
 
 #include "../ui/container.h"
 
@@ -108,7 +109,7 @@
 UiObject* uic_object_new_toplevel(void) {
     fflush(stdout);
     CxMempool *mp = cxMempoolCreateSimple(256);
-    UiObject *obj = cxCalloc(mp->allocator, 1, sizeof(UiObject));
+    UiObject *obj = cxCalloc(mp->allocator, 1, sizeof(UiObjectPrivate));
     fflush(stdout);
     obj->ctx = uic_context(obj, mp);
     obj->ctx->parent = ui_global_context();
@@ -119,7 +120,7 @@
 }
 
 UiObject* uic_ctx_object_new(UiContext *ctx, UIWIDGET widget) {
-    UiObject *newobj = cxCalloc(ctx->allocator, 1, sizeof(UiObject));
+    UiObject *newobj = cxCalloc(ctx->allocator, 1, sizeof(UiObjectPrivate));
     newobj->ctx = ctx;
     newobj->widget = widget;
     uic_object_created(newobj);
@@ -170,3 +171,22 @@
         fprintf(stderr, "Error: uic_object_remove_second_last_container expected at least 2 containers\n");
     }
 }
+
+// public API
+
+void ui_object_set(UiObject *obj, const char *key, void *data) {
+    UiObjectPrivate *p = (UiObjectPrivate*)obj;
+    if(!p->ext) {
+        p->ext = cxHashMapCreate(obj->ctx->mp->allocator, CX_STORE_POINTERS, 4);
+    }
+    if(data) {
+        cxMapPut(p->ext, key, data);
+    } else {
+        cxMapRemove(p->ext, key);
+    }
+}
+
+void* ui_object_get(UiObject *obj, const char *key) {
+    UiObjectPrivate *p = (UiObjectPrivate*)obj;
+    return p->ext ? cxMapGet(p->ext, key) : NULL;
+}
--- a/ui/common/object.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/object.h	Sat Dec 13 15:58:58 2025 +0100
@@ -30,10 +30,16 @@
 #define	UIC_OBJECT_H
 
 #include "../ui/toolkit.h"
+#include <cx/map.h>
 
 #ifdef	__cplusplus
 extern "C" {
 #endif
+    
+typedef struct UiObjectPrivate {
+    UiObject obj;
+    CxMap *ext;
+} UiObjectPrivate;
 
 typedef void (*ui_object_callback)(UiObject *obj, void *userdata);
 
--- a/ui/common/objs.mk	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/objs.mk	Sat Dec 13 15:58:58 2025 +0100
@@ -34,14 +34,16 @@
 COMMON_OBJ += object$(OBJ_EXT)
 COMMON_OBJ += container$(OBJ_EXT)
 COMMON_OBJ += types$(OBJ_EXT)
+COMMON_OBJ += app$(OBJ_EXT)
 COMMON_OBJ += properties$(OBJ_EXT)
 COMMON_OBJ += menu$(OBJ_EXT)
 COMMON_OBJ += toolbar$(OBJ_EXT)
-COMMON_OBJ += ucx_properties$(OBJ_EXT)
 COMMON_OBJ += threadpool$(OBJ_EXT)
 COMMON_OBJ += condvar$(OBJ_EXT)
 COMMON_OBJ += args$(OBJ_EXT)
 COMMON_OBJ += wrapper$(OBJ_EXT)
+COMMON_OBJ += utils$(OBJ_EXT)
+COMMON_OBJ += message$(OBJ_EXT)
 
 TOOLKITOBJS += $(COMMON_OBJ:%=$(COMMON_OBJPRE)uic_%)
 TOOLKITSOURCE += $(COMMON_OBJ:%$(OBJ_EXT)=common/%.c)
--- a/ui/common/properties.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/properties.c	Sat Dec 13 15:58:58 2025 +0100
@@ -44,7 +44,7 @@
 #include <cx/buffer.h>
 
 #include <cx/hash_map.h>
-#include "ucx_properties.h"
+#include <cx/properties.h>
 
 static CxMap *application_properties;
 static CxMap *language;
@@ -54,8 +54,27 @@
 static char *locales_dir;
 static char *pixmaps_dir;
 
+static UiBool use_xdg_config_home = TRUE;
+
 #endif
 
+static UiBool load_on_startup = TRUE;
+static void *properties_data = NULL;
+static size_t properties_data_length = 0;
+
+void ui_load_properties_file_on_startup(UiBool enable) {
+    load_on_startup = enable;
+}
+
+void ui_set_properties_data(const char *str, size_t len) {
+    if(str && len > 0) {
+        properties_data = malloc(len);
+        memcpy(properties_data, str, len);
+    } else {
+        properties_data = NULL;
+        properties_data_length = 0;
+    }
+}
 
 char* ui_getappdir(void) {
     if(ui_appname() == NULL) {
@@ -73,6 +92,8 @@
 #define UI_ENV_HOME "USERPROFILE"
 #endif
 
+#define UI_XDG_CONFIG_HOME_VAR "XDG_CONFIG_HOME"
+
 char* ui_configfile(const char *name) {
     const char *appname = ui_appname();
     if(!appname) {
@@ -102,8 +123,23 @@
     // on Windows the app dir is $USERPROFILE/AppData/Local/$APPNAME/
     cxBufferPutString(&buf, "AppData\\Local\\");
 #else
-    // app dir is $HOME/.$APPNAME/
-    cxBufferPut(&buf, '.');
+    if(use_xdg_config_home) {
+        // app dir is $HOME/.config/$APPNAME/
+        char *xdghome = getenv(UI_XDG_CONFIG_HOME_VAR);
+        size_t xdghomelen = xdghome ? strlen(xdghome) : 0;
+        if(xdghome && xdghomelen > 0) {
+            cxBufferSeek(&buf, 0, SEEK_SET);
+            cxBufferPutString(&buf, xdghome);
+            if(xdghome[xdghomelen-1] != '/') {
+                cxBufferPut(&buf, '/');
+            }
+        } else {
+            cxBufferPutString(&buf, ".config/");
+        }
+    } else {
+        cxBufferPut(&buf, '.');
+    }
+    
 #endif
     cxBufferPutString(&buf, appname);
     cxBufferPut(&buf, UI_PATH_SEPARATOR);
@@ -126,10 +162,53 @@
 #endif
 } 
 
+static int load_properties(FILE *file, CxMap *map) {
+    CxProperties prop;
+    cxPropertiesInitDefault(&prop);
+    char buf[8192];
+    size_t r;
+    CxPropertiesStatus status = CX_PROPERTIES_NO_ERROR;
+    while((r = fread(buf, 1, 8192, file)) > 0) {
+        cxPropertiesFilln(&prop, buf, r);
+        cxstring key;
+        cxstring value;
+        while((status = cxPropertiesNext(&prop, &key, &value)) == CX_PROPERTIES_NO_ERROR) {
+            cxMapPut(map, key, cx_strdup(value).ptr);
+        }
+        if(status > CX_PROPERTIES_OK) {
+            break;
+        }
+    }
+    return status <= CX_PROPERTIES_NO_DATA ? 0 : 1;
+}
+
 void uic_load_app_properties() {
     application_properties = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, 128);
     application_properties->collection.simple_destructor = free;
     
+    if(!load_on_startup) {
+        if(properties_data) {
+            CxProperties prop;
+            cxPropertiesInitDefault(&prop);
+            
+            CxPropertiesStatus status = CX_PROPERTIES_NO_ERROR;
+            cxPropertiesFilln(&prop, properties_data, properties_data_length);
+            
+            cxstring key;
+            cxstring value;
+            while((status = cxPropertiesNext(&prop, &key, &value)) == CX_PROPERTIES_NO_ERROR) {
+                cxMapPut(application_properties, key, cx_strdup(value).ptr);
+            }
+            
+            if(status > CX_PROPERTIES_NO_DATA) {
+                fprintf(stderr, "Error: cannot load properties: %d\n", (int)status);
+            } else {
+                cxMapRehash(application_properties);
+            }
+        }
+        return;
+    }
+    
     if(!ui_appname()) {
         // applications without name cannot load app properties
         return;
@@ -139,9 +218,37 @@
     if(!dir) {
         return;
     }
+    size_t len = strlen(dir);
+    if(len < 2) {
+        return;
+    }
     if(ui_mkdir(dir)) {
+        if(errno == ENOENT) {
+            char *parent = strdup(dir);
+            for(int i=len-2;i>=0;i--) {
+                if(parent[i] == '/') {
+                    parent[i] = 0;
+                    break;
+                }
+            }
+            // try creating the parent
+            int err = ui_mkdir(parent);
+            if(err) {
+                fprintf(stderr, "Error: Cannot create directory %s: %s\n", parent, strerror(errno));
+                free(parent);
+                free(dir);
+                return;
+            }
+            free(parent);
+            
+            // try dir again
+            if(!ui_mkdir(dir)) {
+                errno = EEXIST; // success
+            }
+        }
+        
         if(errno != EEXIST) {
-            fprintf(stderr, "Ui Error: Cannot create directory %s\n", dir);
+            fprintf(stderr, "Error: Cannot create directory %s: %s\n", dir, strerror(errno));
             free(dir);
             return;
         }
@@ -159,8 +266,8 @@
         return;
     }
     
-    if(ucx_properties_load(application_properties, file)) {
-        fprintf(stderr, "Ui Error: Cannot load application properties.\n");
+    if(load_properties(file, application_properties)) {
+        fprintf(stderr, "Error: Cannot load application properties.\n");
     }
     
     fclose(file);
@@ -181,11 +288,13 @@
     }
     
     int ret = 0;
-    if(ucx_properties_store(application_properties, file)) {
-        fprintf(stderr, "Ui Error: Cannot store application properties.\n");
-        ret = 1;
+    CxMapIterator i = cxMapIterator(application_properties);
+    cx_foreach(CxMapEntry *, entry, i) {
+        fprintf(file, "%.*s = %s\n", (int)entry->key->len, (char*)entry->key->data, (char*)entry->value);
     }
     
+    cxMapRehash(application_properties);
+    
     fclose(file);
     free(path);
     
@@ -313,8 +422,8 @@
         return 1;
     }
     
-    if(ucx_properties_load(lang, file)) {
-        fprintf(stderr, "Ui Error: Cannot parse language file: %s.\n", path);
+    if(load_properties(file, lang)) {
+        fprintf(stderr, "Error: Cannot parse language file: %s.\n", path);
     }
     
     fclose(file);
--- a/ui/common/threadpool.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/threadpool.c	Sat Dec 13 15:58:58 2025 +0100
@@ -40,24 +40,20 @@
 
 UiThreadpool* threadpool_new(int min, int max) {
     UiThreadpool *pool = malloc(sizeof(UiThreadpool));
-    pool->queue = NULL;
-    pool->queue_len = 0;
+    pool->queue = ui_queue_create();
     pool->num_idle = 0;
     pool->min_threads = min;
     pool->max_threads = max;
 
-    pthread_mutex_init(&pool->queue_lock, NULL);
-    pthread_mutex_init(&pool->avlbl_lock, NULL);
-    pthread_cond_init(&pool->available, NULL);  
-
     return pool;
 }
 
 int threadpool_start(UiThreadpool *pool) {
+    pool->nthreads = pool->min_threads;
+    pool->threads = calloc(pool->max_threads, sizeof(pthread_t));
     /* create pool threads */
-    for(int i=0;i<pool->min_threads;i++) {
-        pthread_t t;
-        if (pthread_create(&t, NULL, threadpool_func, pool) != 0) {
+    for(int i=0;i<pool->nthreads;i++) {
+        if (pthread_create(&pool->threads[i], NULL, threadpool_func, pool) != 0) {
             fprintf(stderr, "uic: threadpool_start: pthread_create failed: %s", strerror(errno));
             return 1;
         }
@@ -65,6 +61,16 @@
     return 0;
 }
 
+int threadpool_join(UiThreadpool *pool) {
+    int err = 0;
+    for(int i=0;i<pool->nthreads;i++) {
+        if(pthread_join(pool->threads[i], NULL)) {
+            err = 1;
+        }
+    }
+    return err;
+}
+
 void* threadpool_func(void *data) {
     UiThreadpool *pool = (UiThreadpool*)data;
     
@@ -82,69 +88,17 @@
 }
 
 threadpool_job* threadpool_get_job(UiThreadpool *pool) {
-    pthread_mutex_lock(&pool->queue_lock);
-
-    threadpool_job *job = NULL;
-    pool->num_idle++;
-    while(job == NULL) {
-        if(pool->queue_len == 0) {
-            pthread_cond_wait(&pool->available, &pool->queue_lock);
-            continue;
-        } else {
-            pool_queue_t *q = pool->queue;
-            job = q->job;
-            pool->queue = q->next;
-            pool->queue_len--;
-            free(q);
-        }
-    }
-    pool->num_idle--;
-
-    pthread_mutex_unlock(&pool->queue_lock);
+    threadpool_job *job = ui_queue_get_wait(pool->queue); 
     return job;
 }
 
-void threadpool_run(UiThreadpool *pool, job_callback_f func, void *data) {
-    // TODO: handle errors
-    
+void threadpool_run(UiThreadpool *pool, job_callback_f func, void *data) { 
     threadpool_job *job = malloc(sizeof(threadpool_job));
     job->callback = func;
     job->data = data;
-
-    pthread_mutex_lock(&pool->queue_lock);
-    threadpool_enqueue_job(pool, job);
-
-    int create_thread = 0;
-    int destroy_thread = 0;
-    int diff = pool->queue_len - pool->num_idle;
-    
-    //if(pool->queue_len == 1) {
-    pthread_cond_signal(&pool->available);
-    //}
-    
-    pthread_mutex_unlock(&pool->queue_lock);
+    ui_queue_put(pool->queue, job);
 }
 
-void threadpool_enqueue_job(UiThreadpool *pool, threadpool_job *job) {
-    pool_queue_t *q = malloc(sizeof(pool_queue_t));
-    q->job = job;
-    q->next = NULL;
-    
-    if(pool->queue == NULL) {
-        pool->queue = q;
-    } else {
-        pool_queue_t *last_elem = pool->queue;
-        while(last_elem->next != NULL) {
-            last_elem = last_elem->next;
-        }
-        last_elem->next = q;
-    }
-    pool->queue_len++;
-}
-
-
-
-
 
 
 UiThreadpool* ui_threadpool_create(int nthreads) {
@@ -191,6 +145,71 @@
     threadpool_run(pool, ui_threadpool_job_func, job);
 }
 
+/* --------------------------------- Queue --------------------------------- */
+
+UiQueue* ui_queue_create(void) {
+    UiQueue *queue = calloc(1, sizeof(UiQueue));
+    pthread_mutex_init(&queue->lock, NULL);
+    pthread_mutex_init(&queue->avlbl_lock, NULL);
+    pthread_cond_init(&queue->available, NULL);
+    return queue;
+}
+
+void ui_queue_free(UiQueue *queue) {
+    // The queue must be empty, we could free UiQueueElm,
+    // but not the payload data
+    pthread_mutex_destroy(&queue->lock);
+    pthread_mutex_destroy(&queue->avlbl_lock);
+    pthread_cond_destroy(&queue->available);
+    free(queue);
+}
+
+void ui_queue_put(UiQueue *queue, void *data) {
+    // create queue element
+    UiQueueElm *elm = malloc(sizeof(UiQueueElm));
+    elm->data = data;
+    elm->next = NULL;
+    
+    pthread_mutex_lock(&queue->lock);
+    
+    // put queue element at the end of the linked list
+    if(queue->elements) {
+        UiQueueElm *end = queue->elements;
+        while(end->next) {
+            end = end->next;
+        }
+        end->next = elm;
+    } else {
+        queue->elements = elm;
+    }
+    queue->length++;
+    
+    // signal new available data
+    pthread_cond_signal(&queue->available);
+    
+    pthread_mutex_unlock(&queue->lock);
+}
+
+void* ui_queue_get_wait(UiQueue *queue) {
+    pthread_mutex_lock(&queue->lock);
+
+    void *data = NULL;
+    while(data == NULL) {
+        if(queue->length == 0) {
+            pthread_cond_wait(&queue->available, &queue->lock);
+            continue;
+        } else {
+            UiQueueElm *q = queue->elements;
+            data = q->data;
+            queue->elements = q->next;
+            queue->length--;
+            free(q);
+        }
+    }
+
+    pthread_mutex_unlock(&queue->lock);
+    return data;
+}
 
 #endif
 
--- a/ui/common/threadpool.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/threadpool.h	Sat Dec 13 15:58:58 2025 +0100
@@ -39,6 +39,8 @@
 extern "C" {
 #endif
     
+typedef struct UiQueueElm UiQueueElm;
+typedef struct UiQueue    UiQueue;
     
 typedef struct UiJob {
     UiObject      *obj;
@@ -48,20 +50,30 @@
     void          *finish_data;
 } UiJob;
     
+struct UiQueueElm {
+    void       *data;
+    UiQueueElm *next;
+};
+
+struct UiQueue {
+    UiQueueElm      *elements;
+    size_t          length;
+    pthread_mutex_t lock;
+    pthread_mutex_t avlbl_lock;
+    pthread_cond_t  available;
+};
     
 typedef struct  _threadpool_job   threadpool_job;
 typedef void*(*job_callback_f)(void *data);
     
 typedef struct _pool_queue pool_queue_t;
 struct UiThreadpool {
-    pthread_mutex_t queue_lock;
-    pthread_mutex_t avlbl_lock;
-    pthread_cond_t  available;
-    pool_queue_t    *queue;
-    uint32_t        queue_len;
-    uint32_t        num_idle;
-    int             min_threads;
-    int             max_threads;
+    UiQueue   *queue;
+    uint32_t  num_idle;
+    int       min_threads;
+    int       max_threads;
+    pthread_t *threads;
+    int       nthreads;
 };
 
 struct _threadpool_job {
@@ -76,10 +88,15 @@
 
 UiThreadpool* threadpool_new(int min, int max);
 int threadpool_start(UiThreadpool *pool);
+int threadpool_join(UiThreadpool *pool);
 void* threadpool_func(void *data);
 threadpool_job* threadpool_get_job(UiThreadpool *pool);
 void threadpool_run(UiThreadpool *pool, job_callback_f func, void *data);
-void threadpool_enqueue_job(UiThreadpool *pool, threadpool_job *job);
+
+UiQueue* ui_queue_create(void);
+void ui_queue_free(UiQueue *queue);
+void ui_queue_put(UiQueue *queue, void *data);
+void* ui_queue_get_wait(UiQueue *queue);
 
 #ifdef __cplusplus
 }
--- a/ui/common/toolbar.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/toolbar.c	Sat Dec 13 15:58:58 2025 +0100
@@ -57,15 +57,15 @@
     newargs.tooltip = nl_strdup(args->tooltip);
     newargs.onclick = args->onclick;
     newargs.onclickdata = args->onclickdata;
-    newargs.groups = uic_copy_groups(args->groups, ngroups);
-    newargs.visibility_states = uic_copy_groups(args->visibility_states, nvstates);
+    newargs.states = uic_copy_states(args->states, ngroups);
+    newargs.visibility_states = uic_copy_states(args->visibility_states, nvstates);
     return newargs;
 }
 
 void ui_toolbar_item_create(const char* name, UiToolbarItemArgs *args) {
     UiToolbarItem* item = malloc(sizeof(UiToolbarItem));
     item->item.type = UI_TOOLBAR_ITEM;
-    item->args = itemargs_copy(args, &item->ngroups, &item->nvstates);
+    item->args = itemargs_copy(args, &item->nstates, &item->nvstates);
     cxMapPut(toolbar_items, name, item);
 }
 
@@ -78,15 +78,15 @@
     newargs.varname = nl_strdup(args->varname);
     newargs.onchange = args->onchange;
     newargs.onchangedata = args->onchangedata;
-    newargs.groups = uic_copy_groups(args->groups, ngroups);
-    newargs.visibility_states = uic_copy_groups(args->visibility_states, nvstates);
+    newargs.states = uic_copy_states(args->states, ngroups);
+    newargs.visibility_states = uic_copy_states(args->visibility_states, nvstates);
     return newargs;
 }
 
 void ui_toolbar_toggleitem_create(const char* name, UiToolbarToggleItemArgs *args) {
     UiToolbarToggleItem* item = malloc(sizeof(UiToolbarToggleItem));
     item->item.type = UI_TOOLBAR_TOGGLEITEM;
-    item->args = toggleitemargs_copy(args, &item->ngroups, &item->nvstates);
+    item->args = toggleitemargs_copy(args, &item->nstates, &item->nvstates);
     cxMapPut(toolbar_items, name, item);
 }
 
@@ -95,7 +95,7 @@
     newargs.label = nl_strdup(args->label);
     newargs.icon = nl_strdup(args->icon);
     newargs.tooltip = nl_strdup(args->tooltip);
-    newargs.visibility_states = uic_copy_groups(args->visibility_states, nvstates);
+    newargs.visibility_states = uic_copy_states(args->visibility_states, nvstates);
     return newargs;
 }
 
--- a/ui/common/toolbar.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/toolbar.h	Sat Dec 13 15:58:58 2025 +0100
@@ -62,14 +62,14 @@
 struct UiToolbarItem {
     UiToolbarItemI item;
     UiToolbarItemArgs args;
-    size_t ngroups;
+    size_t nstates;
     size_t nvstates;
 };
 
 struct UiToolbarToggleItem {
     UiToolbarItemI item;
     UiToolbarToggleItemArgs args;
-    size_t ngroups;
+    size_t nstates;
     size_t nvstates;
 };
 
--- a/ui/common/types.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/types.c	Sat Dec 13 15:58:58 2025 +0100
@@ -33,13 +33,15 @@
 
 #include <cx/list.h>
 #include <cx/array_list.h>
-#include "../ui/tree.h"
+#include "../ui/list.h"
 #include "types.h"
 #include "context.h"
 #include "../ui/image.h"
 
 static ui_list_init_func default_list_init;
 static void *default_list_init_userdata;
+static ui_list_destroy_func default_list_destroy;
+static void *default_list_destroy_userdata;
 
 UiObserver* ui_observer_new(ui_callback f, void *data) {
     UiObserver *observer = malloc(sizeof(UiObserver));
@@ -105,6 +107,11 @@
     list->count = ui_list_count;
 }
 
+void uic_ucx_list_destroy(UiContext *ctx, UiList *list, void *unused) {
+    cxListFree(list->data);
+    ui_free(ctx, list);
+}
+
 UiList* ui_list_new(UiContext *ctx, const char *name) {
     return ui_list_new2(ctx, name, default_list_init ? default_list_init : uic_ucx_list_init, default_list_init_userdata);
 }
@@ -121,9 +128,13 @@
     return list;
 }
 
-void ui_list_free(UiList *list) {
-    cxListFree(list->data);
-    free(list);
+void ui_list_free(UiContext *ctx, UiList *list) {
+    if(!default_list_destroy) {
+        uic_ucx_list_destroy(ctx, list, NULL);
+    } else {
+        default_list_destroy(ctx, list, default_list_destroy_userdata);
+    }
+    
 }
 
 void* ui_list_first(UiList *list) {
@@ -201,6 +212,7 @@
 
 UiModel* ui_model(UiContext *ctx, ...) {
     UiModel *info = ui_calloc(ctx, 1, sizeof(UiModel));
+    info->ctx = ctx;
     
     va_list ap;
     va_start(ap, ctx);
@@ -240,8 +252,18 @@
 
 #define UI_MODEL_DEFAULT_ALLOC_SIZE 16
 
+
+static void model_notify_observer(UiModel *model, int insert, int delete) {
+    UiModelChangeObserver *obs = model->observer;
+    while(obs) {
+        obs->update(model, obs->userdata, insert, delete);
+        obs = obs->next;
+    }
+}
+
 UiModel* ui_model_new(UiContext *ctx) {
     UiModel *info = ui_calloc(ctx, 1, sizeof(UiModel));
+    info->ctx = ctx;
     info->alloc = UI_MODEL_DEFAULT_ALLOC_SIZE;
     info->types = ui_calloc(ctx, UI_MODEL_DEFAULT_ALLOC_SIZE, sizeof(UiModelType));
     info->titles = ui_calloc(ctx, UI_MODEL_DEFAULT_ALLOC_SIZE, sizeof(char*));
@@ -249,16 +271,21 @@
     return info;
 }
 
-void ui_model_add_column(UiContext *ctx, UiModel *model, UiModelType type, const char *title, int width) {
+void ui_model_add_column(UiModel *model, UiModelType type, const char *title, int width) {
+    UiContext *ctx = model->ctx;
     if(model->columns >= model->alloc) {
         model->alloc += UI_MODEL_DEFAULT_ALLOC_SIZE;
         model->types = ui_realloc(ctx, model->types, model->alloc * sizeof(UiModelType));
         model->titles = ui_realloc(ctx, model->titles, model->alloc * sizeof(char*));
         model->columnsize = ui_realloc(ctx, model->columnsize, model->alloc * sizeof(int));
     }
-    model->types[model->columns] = type;
-    model->titles[model->columns] = ui_strdup(ctx, title);
-    model->columnsize[model->columns] = width;
+    int index = model->columns;
+    model->types[index] = type;
+    model->titles[index] = ui_strdup(ctx, title);
+    model->columnsize[index] = width;
+    
+    model_notify_observer(model, index, -1);
+    
     model->columns++;
 }
 
@@ -266,6 +293,7 @@
     const CxAllocator* a = ctx ? ctx->allocator : cxDefaultAllocator;
 
     UiModel* newmodel = cxMalloc(a, sizeof(UiModel));
+    newmodel->ctx = ctx;
     *newmodel = *model;
 
     newmodel->types = cxCalloc(a, model->columns, sizeof(UiModelType));
@@ -281,11 +309,67 @@
     return newmodel;
 }
 
-void ui_model_free(UiContext *ctx, UiModel *mi) {
-    const CxAllocator* a = ctx ? ctx->allocator : cxDefaultAllocator;
+void ui_model_ref(UiModel *model) {
+    model->ref++;
+}
+
+void ui_model_unref(UiModel *model) { 
+    if(--model->ref == 0) {
+        ui_model_free(model);
+    }
+}
+
+void ui_model_add_observer(UiModel *model, ui_model_update_func update, void *data) {
+    UiModelChangeObserver *observer = ui_malloc(model->ctx, sizeof(UiModelChangeObserver));
+    observer->update = update;
+    observer->userdata = data;
+    observer->next = NULL;
+    
+    if(model->observer) {
+        UiModelChangeObserver *last = model->observer;
+        while(last->next) {
+            last = last->next;
+        }
+        last->next = observer;
+    } else {
+        model->observer = observer;
+    }
+}
+
+void ui_model_remove_observer(UiModel *model, void *data) {
+    if(model->observer) {
+        UiModelChangeObserver *obs = model->observer;
+        UiModelChangeObserver *prev = NULL;
+        while(obs) {
+            if(obs->userdata == data) {
+                // remove
+                if(prev) {
+                    prev->next = obs->next;
+                } else {
+                    model->observer = obs->next;
+                }
+                // free
+                ui_free(model->ctx, obs);
+                break;
+            }
+            prev = obs;
+            obs = obs->next;
+        }
+    }
+}
+
+void ui_model_free(UiModel *mi) {
+    UiContext *ctx = mi->ctx;
+    const CxAllocator* a = ctx->allocator;
     for(int i=0;i<mi->columns;i++) {
         ui_free(ctx, mi->titles[i]);
     }
+    UiModelChangeObserver *obs = mi->observer;
+    while(obs) {
+        UiModelChangeObserver *n = obs->next;
+        cxFree(a, obs);
+        obs = n;
+    }
     cxFree(a, mi->types);
     cxFree(a, mi->titles);
     cxFree(a, mi->columnsize);
@@ -724,9 +808,7 @@
 }
 
 UIEXPORT void ui_listselection_free(UiListSelection selection) {
-    if (selection.rows) {
-        free(selection.rows);
-    }
+    free(selection.rows);
 }
 
 UIEXPORT UiStr ui_str(char *cstr) {
@@ -787,6 +869,7 @@
 }
 
 static int ui_set_op = 0;
+static int ui_onchange_events_enabled = TRUE;
 
 void ui_setop_enable(int set) {
     ui_set_op = set;
@@ -796,6 +879,14 @@
     return ui_set_op;
 }
 
+void ui_onchange_events_enable(UiBool enable) {
+    ui_onchange_events_enabled = enable;
+}
+
+UiBool ui_onchange_events_is_enabled(void) {
+    return ui_onchange_events_enabled;
+}
+
 /* ---------------- List initializers and wrapper functions ---------------- */
 
 void ui_global_list_initializer(ui_list_init_func func, void *userdata) {
@@ -803,6 +894,11 @@
     default_list_init_userdata = userdata;
 }
 
+void ui_global_list_destructor(ui_list_destroy_func func, void *userdata) {
+    default_list_destroy = func;
+    default_list_destroy_userdata = userdata;
+}
+
 void ui_list_class_set_first(UiList *list, void*(*first)(UiList *list)) {
     list->first = first;
 }
--- a/ui/common/types.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/types.h	Sat Dec 13 15:58:58 2025 +0100
@@ -36,6 +36,7 @@
 #endif
     
 void uic_ucx_list_init(UiContext *ctx, UiList *list, void *unused);
+void uic_ucx_list_destroy(UiContext *ctx, UiList *list, void *unused);
     
 void uic_int_copy(UiInteger *from, UiInteger *to);
 void uic_double_copy(UiDouble *from, UiDouble *to);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/common/utils.c	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,151 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2017 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "utils.h"
+#include "properties.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#include <cx/string.h>
+#include <cx/buffer.h>
+
+UiPathElm* ui_default_pathelm_func(const char* full_path, size_t len, size_t* ret_nelm, void* data) {
+    cxstring *pathelms;
+    size_t nelm = cx_strsplit_a(cxDefaultAllocator, cx_strn(full_path, len), CX_STR("/"), 4096, &pathelms);
+
+    if (nelm == 0) {
+        *ret_nelm = 0;
+        return NULL;
+    }
+
+    UiPathElm* elms = (UiPathElm*)calloc(nelm, sizeof(UiPathElm));
+    size_t n = nelm;
+    int j = 0;
+    for (int i = 0; i < nelm; i++) {
+        cxstring c = pathelms[i];
+        if (c.length == 0) {
+            if (i == 0) {
+                c.length = 1;
+            }
+            else {
+                n--;
+                continue;
+            }
+        }
+
+        cxmutstr m = cx_strdup(c);
+        elms[j].name = m.ptr;
+        elms[j].name_len = m.length;
+        
+        size_t elm_path_len = c.ptr + c.length - full_path;
+        cxmutstr elm_path = cx_strdup(cx_strn(full_path, elm_path_len));
+        elms[j].path = elm_path.ptr;
+        elms[j].path_len = elm_path.length;
+
+        j++;
+    }
+    *ret_nelm = n;
+
+    return elms;
+}
+
+void ui_get_window_default_width(int *width, int *height) {
+    const char *width_str = ui_get_property("ui.window.width");
+    const char *height_str = ui_get_property("ui.window.height");
+    if(width_str && height_str) {
+        int w = atoi(width_str);
+        int h = atoi(height_str);
+        if(w > 0 && h > 0) {
+            *width = w;
+            *height = h;
+        }
+    }
+}
+
+// from UCX json.c
+static cxmutstr escape_string(cxstring str, bool escape_slash) {
+    // note: this function produces the string without enclosing quotes
+    // the reason is that we don't want to allocate memory just for that
+    CxBuffer buf = {0};
+
+    bool all_printable = true;
+    for (size_t i = 0; i < str.length; i++) {
+        unsigned char c = str.ptr[i];
+        bool escape = c < 0x20 || c == '\\' || c == '"'
+            || (escape_slash && c == '/');
+
+        if (all_printable && escape) {
+            size_t capa = str.length + 32;
+            char *space = cxMallocDefault(capa);
+            if (space == NULL) return cx_mutstrn(NULL, 0);
+            cxBufferInit(&buf, space, capa, NULL, CX_BUFFER_AUTO_EXTEND);
+            cxBufferWrite(str.ptr, 1, i, &buf);
+            all_printable = false;
+        }
+        if (escape) {
+            cxBufferPut(&buf, '\\');
+            if (c == '\"') {
+                cxBufferPut(&buf, '\"');
+            } else if (c == '\n') {
+                cxBufferPut(&buf, 'n');
+            } else if (c == '\t') {
+                cxBufferPut(&buf, 't');
+            } else if (c == '\r') {
+                cxBufferPut(&buf, 'r');
+            } else if (c == '\\') {
+                cxBufferPut(&buf, '\\');
+            } else if (c == '/') {
+                cxBufferPut(&buf, '/');
+            } else if (c == '\f') {
+                cxBufferPut(&buf, 'f');
+            } else if (c == '\b') {
+                cxBufferPut(&buf, 'b');
+            } else {
+                char code[6];
+                snprintf(code, sizeof(code), "u%04x", (unsigned int) c);
+                cxBufferPutString(&buf, code);
+            }
+        } else if (!all_printable) {
+            cxBufferPut(&buf, c);
+        }
+    }
+    cxmutstr ret;
+    if (all_printable) {
+        // don't copy the string when we don't need to escape anything
+        ret = cx_mutstrn((char*)str.ptr, str.length);
+    } else {
+        ret = cx_mutstrn(buf.space, buf.size);
+    }
+    cxBufferDestroy(&buf);
+    return ret;
+}
+
+cxmutstr ui_escape_string(cxstring str) {
+    return escape_string(str, FALSE);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/common/utils.h	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,56 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2017 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#ifndef UIC_UTILS_H
+#define UIC_UTILS_H
+
+#include "../ui/toolkit.h"
+#include "../ui/text.h"
+
+#include <cx/string.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+UiPathElm* ui_default_pathelm_func(const char* full_path, size_t len, size_t* ret_nelm, void* data);
+
+/*
+ * overrides width/height with values from
+ * ui.window.width and ui.window.height properties if available
+ */
+void ui_get_window_default_width(int *width, int *height);
+
+cxmutstr ui_escape_string(cxstring str);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UIC_UTILS_H */
+
--- a/ui/common/wrapper.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/wrapper.c	Sat Dec 13 15:58:58 2025 +0100
@@ -125,6 +125,11 @@
     cxListRemove(cxlist, index);
 }
 
+void ui_srclist_swap(UiList *list, int i1, int i2) {
+    CxList *cxlist = list->data;
+    cxListSwap(cxlist, i1, i2);
+}
+
 void ui_srclist_clear(UiList *list) {
     CxList *cxlist = list->data;
     cxListClear(cxlist);
--- a/ui/common/wrapper.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/common/wrapper.h	Sat Dec 13 15:58:58 2025 +0100
@@ -30,7 +30,7 @@
 #define UIC_WRAPPER_H
 
 #include "../ui/toolkit.h"
-#include "../ui/tree.h"
+#include "../ui/list.h"
 
 #ifdef __cplusplus
 extern "C" {
@@ -56,6 +56,7 @@
 UIEXPORT void ui_srclist_add(UiList *list, UiSubList *item);
 UIEXPORT void ui_srclist_insert(UiList *list, int index, UiSubList *item);
 UIEXPORT void ui_srclist_remove(UiList *list, int index);
+UIEXPORT void ui_srclist_swap(UiList *list, int i1, int i2);
 UIEXPORT void ui_srclist_clear(UiList *list);
 UIEXPORT int ui_srclist_size(UiList *list);
 UIEXPORT void ui_srclist_generate_sublist_num_data(UiList *list);
--- a/ui/gtk/button.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/button.c	Sat Dec 13 15:58:58 2025 +0100
@@ -106,7 +106,7 @@
 UIWIDGET ui_button_create(UiObject *obj, UiButtonArgs *args) {
     GtkWidget *button = ui_create_button(obj, args->label, args->icon, args->tooltip, args->onclick, args->onclickdata, 0, FALSE);
     ui_set_name_and_style(button, args->name, args->style_class);
-    ui_set_widget_groups(obj->ctx, button, args->groups);
+    ui_set_widget_states(obj->ctx, button, args->states);
     UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end;
     UiLayout layout = UI_ARGS2LAYOUT(args);
     ct->add(ct, button, &layout);
@@ -174,9 +174,9 @@
 
 static void ui_togglebutton_enable_state_callback(GtkToggleButton *widget, UiEventData *event) {
     if(gtk_toggle_button_get_active(widget)) {
-        ui_set_group(event->obj->ctx, event->value);
+        ui_set_state(event->obj->ctx, event->value);
     } else {
-        ui_unset_group(event->obj->ctx, event->value);
+        ui_unset_state(event->obj->ctx, event->value);
     }
 }
 
@@ -308,9 +308,9 @@
             args->value,
             args->onchange,
             args->onchangedata,
-            args->enable_group);
+            args->enable_state);
     ui_set_name_and_style(widget, args->name, args->style_class);
-    ui_set_widget_groups(obj->ctx, widget, args->groups);
+    ui_set_widget_states(obj->ctx, widget, args->states);
     
     UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end;
     UiLayout layout = UI_ARGS2LAYOUT(args);
@@ -351,9 +351,9 @@
 
 static void ui_checkbox_enable_state(GtkCheckButton *widget, UiEventData *event) {
     if(gtk_check_button_get_active(widget)) {
-        ui_set_group(event->obj->ctx, event->value);
+        ui_set_state(event->obj->ctx, event->value);
     } else {
-        ui_unset_group(event->obj->ctx, event->value);
+        ui_unset_state(event->obj->ctx, event->value);
     }
 }
 
@@ -370,10 +370,10 @@
             args->onchange,
             args->onchangedata,
             (ui_toggled_func)ui_checkbox_enable_state,
-            args->enable_group);
+            args->enable_state);
     
     ui_set_name_and_style(widget, args->name, args->style_class);
-    ui_set_widget_groups(obj->ctx, widget, args->groups);
+    ui_set_widget_states(obj->ctx, widget, args->states);
     
     UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end;
     UiLayout layout = UI_ARGS2LAYOUT(args);
@@ -419,7 +419,7 @@
 UIWIDGET ui_switch_create(UiObject* obj, UiToggleArgs *args) {
     GtkWidget *widget = gtk_switch_new();
     ui_set_name_and_style(widget, args->name, args->style_class);
-    ui_set_widget_groups(obj->ctx, widget, args->groups);
+    ui_set_widget_states(obj->ctx, widget, args->states);
     
     UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->value, args->varname, UI_VAR_INTEGER);
     if(var) {
@@ -541,7 +541,7 @@
     
     GtkWidget *rbutton = RADIOBUTTON_NEW(rg, args->label); 
     ui_set_name_and_style(rbutton, args->name, args->style_class);
-    ui_set_widget_groups(obj->ctx, rbutton, args->groups);
+    ui_set_widget_states(obj->ctx, rbutton, args->states);
     if(rgroup) {
 #if GTK_MAJOR_VERSION >= 4
         if(rg) {
@@ -895,7 +895,7 @@
     }
     
     ui_set_name_and_style(button, args->name, args->style_class);
-    ui_set_widget_groups(obj->ctx, button, args->groups);
+    ui_set_widget_states(obj->ctx, button, args->states);
     UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end;
     UiLayout layout = UI_ARGS2LAYOUT(args);
     ct->add(ct, button, &layout);
--- a/ui/gtk/container.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/container.c	Sat Dec 13 15:58:58 2025 +0100
@@ -112,6 +112,21 @@
     return add;
 }
 
+/* -------------------- Custom Container -------------------- */
+
+void ui_custom_container_add(UiContainerPrivate *ct, GtkWidget *widget, UiLayout *layout) {
+    UiCustomContainer *custom = (UiCustomContainer*)ct;
+    custom->add(custom->obj, ct->widget, widget, custom->userdata);
+}
+
+void ui_custom_container_create(UiObject *obj, UIWIDGET widget, ui_addwidget_func add_child, void *userdata) {
+    UiCustomContainer *container = cxZalloc(obj->ctx->allocator, sizeof(UiCustomContainer));
+    container->container.add = ui_custom_container_add;
+    container->container.widget = widget;
+    container->add = add_child;
+    container->userdata = userdata;
+    uic_object_push_container(obj, (UiContainerX*)container);
+}
 
 /* -------------------- Box Container -------------------- */
 UiContainerX* ui_box_container(UiObject *obj, GtkWidget *box, UiSubContainerType type) {
@@ -985,6 +1000,8 @@
             GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
             hb->centerbox = box;
             UI_HEADERBAR_SET_TITLE_WIDGET(ct->widget, box);
+            UI_HEADERBAR_SHOW_TITLE_WIDGET(ct->widget, TRUE);
+            UI_HEADERBAR_SETTINGS(ct->widget);
         }
         BOX_ADD(hb->centerbox, widget);
     }
@@ -1140,6 +1157,7 @@
 
 UiSplitPane* ui_create_splitpane_data(GtkWidget *pane, UiOrientation orientation, int max, int init) {
     UiSplitPane *ct = malloc(sizeof(UiSplitPane));
+    memset(ct, 0, sizeof(UiSplitPane));
     ct->current_pane = pane;
     ct->orientation = orientation;
     ct->max = max;
--- a/ui/gtk/container.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/container.h	Sat Dec 13 15:58:58 2025 +0100
@@ -61,6 +61,13 @@
     int close;
 };
 
+typedef struct UiCustomContainer {
+    UiContainerPrivate container;
+    UiObject *obj;
+    ui_addwidget_func add;
+    void *userdata;
+} UiCustomContainer;
+
 typedef struct UiBoxContainer {
     UiContainerPrivate container;
     UiSubContainerType type;
--- a/ui/gtk/entry.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/entry.c	Sat Dec 13 15:58:58 2025 +0100
@@ -79,7 +79,7 @@
 #endif
     GtkWidget *spin = gtk_spin_button_new_with_range(min, max, args->step);
     ui_set_name_and_style(spin, args->name, args->style_class);
-    ui_set_widget_groups(obj->ctx, spin, args->groups);
+    ui_set_widget_states(obj->ctx, spin, args->states);
     
     if(args->width > 0) {
         gtk_widget_set_size_request(spin, args->width, -1);
--- a/ui/gtk/graphics.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/graphics.c	Sat Dec 13 15:58:58 2025 +0100
@@ -107,14 +107,14 @@
             widget,
             "draw",
             G_CALLBACK(draw_callback),
-            NULL);
+            drawingarea);
 #endif
     
     g_signal_connect(
-                widget,
-                "destroy",
-                G_CALLBACK(destroy_drawingarea),
-                drawingarea);
+            widget,
+            "destroy",
+            G_CALLBACK(destroy_drawingarea),
+            drawingarea);
     
     return widget;
 }
--- a/ui/gtk/headerbar.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/headerbar.c	Sat Dec 13 15:58:58 2025 +0100
@@ -164,7 +164,7 @@
         enum UiToolbarPos pos)
 {
     GtkWidget *button = ui_create_button(obj, item->args.label, item->args.icon, item->args.tooltip, item->args.onclick, item->args.onclickdata, 0, FALSE);
-    ui_set_widget_groups(obj->ctx, button, item->args.groups);
+    ui_set_widget_states(obj->ctx, button, item->args.states);
     ui_set_widget_visibility_states(obj->ctx, button, item->args.visibility_states);
     WIDGET_ADD_CSS_CLASS(button, "flat");
     headerbar_add(headerbar, box, button, pos);
@@ -178,7 +178,7 @@
         enum UiToolbarPos pos)
 {
     GtkWidget *button = gtk_toggle_button_new();
-    ui_set_widget_groups(obj->ctx, button, item->args.groups);
+    ui_set_widget_states(obj->ctx, button, item->args.states);
     ui_set_widget_visibility_states(obj->ctx, button, item->args.visibility_states);
     WIDGET_ADD_CSS_CLASS(button, "flat");
     ui_setup_togglebutton(obj, button, item->args.label, item->args.icon, item->args.tooltip, item->args.varname, NULL, item->args.onchange, item->args.onchangedata, 0);
--- a/ui/gtk/headerbar.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/headerbar.h	Sat Dec 13 15:58:58 2025 +0100
@@ -46,15 +46,20 @@
 #define UI_HEADERBAR_PACK_START(h, w) adw_header_bar_pack_start(ADW_HEADER_BAR(h), w)
 #define UI_HEADERBAR_PACK_END(h, w) adw_header_bar_pack_end(ADW_HEADER_BAR(h), w)
 #define UI_HEADERBAR_SET_TITLE_WIDGET(h, w) adw_header_bar_set_title_widget(ADW_HEADER_BAR(h), w)
+#define UI_HEADERBAR_SHOW_TITLE_WIDGET(h, b) adw_header_bar_set_show_title(ADW_HEADER_BAR(h), b)
+#define UI_HEADERBAR_SETTINGS(h) adw_header_bar_set_centering_policy(ADW_HEADER_BAR(h), ADW_CENTERING_POLICY_LOOSE)
 #else
 #define UI_HEADERBAR GtkHeaderBar*
 #define UI_HEADERBAR_CAST(h) GTK_HEADER_BAR(h)
 #define UI_HEADERBAR_PACK_START(h, w) gtk_header_bar_pack_start(GTK_HEADER_BAR(h), w)
 #define UI_HEADERBAR_PACK_END(h, w) gtk_header_bar_pack_end(GTK_HEADER_BAR(h), w)
+#define UI_HEADERBAR_SETTINGS(h)
 #if GTK_MAJOR_VERSION >= 4
 #define UI_HEADERBAR_SET_TITLE_WIDGET(h, w) gtk_header_bar_set_title_widget(GTK_HEADER_BAR(h), w)
+#define UI_HEADERBAR_SHOW_TITLE_WIDGET(h, b) gtk_header_bar_set_show_title(GTK_HEADER_BAR(h), b)
 #else
 #define UI_HEADERBAR_SET_TITLE_WIDGET(h, w) gtk_header_bar_set_custom_title(GTK_HEADER_BAR(h), w)
+#define UI_HEADERBAR_SHOW_TITLE_WIDGET(h, b) gtk_header_bar_set_show_title(GTK_HEADER_BAR(h), b)
 #endif
 #endif
     
--- a/ui/gtk/image.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/image.c	Sat Dec 13 15:58:58 2025 +0100
@@ -30,6 +30,7 @@
 
 #include "container.h"
 #include "menu.h"
+#include "widget.h"
 #include "../common/context.h"
 #include "../common/object.h"
 
@@ -67,8 +68,14 @@
     GtkWidget *drawingarea = gtk_drawing_area_new();
     GtkWidget *toplevel;
     GtkWidget *widget = drawingarea;
-      
-    gtk_widget_set_size_request(drawingarea, 100, 100);
+    
+    int width = args->width;
+    int height = args->height;
+    if(width == 0 && height == 0) {
+        width = 100;
+        height = 100;
+    }
+    ui_widget_size_request(drawingarea, width, height);
     
 #if GTK_MAJOR_VERSION < 4
     GtkWidget *eventbox = gtk_event_box_new();
--- a/ui/gtk/list.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/list.c	Sat Dec 13 15:58:58 2025 +0100
@@ -80,6 +80,7 @@
     memset(tableview, 0, sizeof(UiListView));
     tableview->obj = obj;
     tableview->model = args->model;
+    tableview->multiselection = args->multiselection;
     tableview->onactivate = args->onactivate;
     tableview->onactivatedata = args->onactivatedata;
     tableview->onselection = args->onselection;
@@ -98,6 +99,11 @@
     tableview->onsave = args->onsave;
     tableview->onsavedata = args->onsavedata;
     
+#if GTK_CHECK_VERSION(4, 0, 0)
+    tableview->coldata.listview = tableview;
+    tableview->coldata.column = 0;
+#endif
+    
     if(args->getvalue2) {
         tableview->getvalue = args->getvalue2;
         tableview->getvaluedata = args->getvalue2data;
@@ -200,7 +206,7 @@
 static void column_factory_setup(GtkListItemFactory *factory, GtkListItem *item, gpointer userdata) {
     UiColData *col = userdata;
     UiModel *model = col->listview->model;
-    UiModelType type = model->types[col->model_column];
+    UiModelType type = model->types[col->column];
     if(type == UI_ICON_TEXT || type == UI_ICON_TEXT_FREE) {
         GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
         GtkWidget *image = gtk_image_new();
@@ -281,16 +287,17 @@
     UiColData *col = userdata;
     UiList *list = col->listview->var ? col->listview->var->value : NULL;
     UiListView *listview = col->listview;
+    int datacolumn = listview->columns[col->column];
      
     ObjWrapper *obj = gtk_list_item_get_item(item);
     UiModel *model = col->listview->model;
-    UiModelType type = model->types[col->model_column];
+    UiModelType type = model->types[col->column];
     
     // cache the GtkListItem
     CxHashKey row_key = cx_hash_key(&obj->i, sizeof(int));
     UiRowItems *row = cxMapGet(listview->bound_rows, row_key);
     if(row) {
-        if(row->items[col->model_column] == NULL) {
+        if(row->items[col->column] == NULL) {
             row->bound++;
         }
     } else {
@@ -298,10 +305,10 @@
         cxMapPut(listview->bound_rows, row_key, row);
         row->bound = 1;
     }
-    row->items[col->model_column] = item;
+    row->items[col->column] = item;
     
     UiBool freevalue = FALSE;
-    void *data = listview->getvalue(list, obj->data, obj->i, col->data_column, listview->getvaluedata, &freevalue);
+    void *data = listview->getvalue(list, obj->data, obj->i, datacolumn, listview->getvaluedata, &freevalue);
     GtkWidget *child = gtk_list_item_get_child(item);
     
     PangoAttrList *attributes = NULL;
@@ -319,7 +326,7 @@
             }
         }
         
-        int style_col = col->data_column;
+        int style_col = datacolumn;
         if(type == UI_ICON_TEXT || type == UI_ICON_TEXT_FREE) {
             style_col++; // col->data_column is the icon, we need the next col for the label
         }
@@ -363,7 +370,7 @@
             
         }
         case UI_ICON_TEXT_FREE: {
-            void *data2 = listview->getvalue(list, obj->data, obj->i, col->data_column+1, listview->getvaluedata, &freevalue);
+            void *data2 = listview->getvalue(list, obj->data, obj->i, datacolumn+1, listview->getvaluedata, &freevalue);
             if(type == UI_ICON_TEXT_FREE) {
                 freevalue = TRUE;
             }
@@ -387,7 +394,7 @@
             if(entry) {
                 entry->listview = col->listview;
                 entry->row = obj->i;
-                entry->col = col->data_column;
+                entry->col = datacolumn;
                 entry->previous_value = strdup(data);
             }
             ENTRY_SET_TEXT(child, data);
@@ -405,13 +412,13 @@
     }
 }
 
-static void column_factory_unbind(GtkSignalListItemFactory *self, GtkListItem *item, UiColData *col) {
+static void column_factory_unbind(GtkSignalListItemFactory *self, GtkListItem *item, UiColData *col) {  
     ObjWrapper *obj = gtk_list_item_get_item(item);
     UiListView *listview = col->listview;
     CxHashKey row_key = cx_hash_key(&obj->i, sizeof(int));
     UiRowItems *row = cxMapGet(listview->bound_rows, row_key);
     if(row) {
-        row->items[col->model_column] = NULL;
+        row->items[col->column] = NULL;
         row->bound--;
         if(row->bound == 0) {
             cxMapRemove(listview->bound_rows, row_key);
@@ -457,17 +464,16 @@
     }
     
     listview->numcolumns = 1;
-    listview->columns = malloc(sizeof(UiColData));
-    listview->columns->listview = listview;
-    listview->columns->data_column = 0;
-    listview->columns->model_column = 0;
+    listview->columns = malloc(sizeof(int));
+    listview->columns[0] = 0;
     
     listview->bound_rows = cxHashMapCreate(NULL, CX_STORE_POINTERS, 128);
     listview->bound_rows->collection.simple_destructor = (cx_destructor_func)free;
      
     GtkListItemFactory *factory = gtk_signal_list_item_factory_new();
-    g_signal_connect(factory, "setup", G_CALLBACK(column_factory_setup), listview->columns);
-    g_signal_connect(factory, "bind", G_CALLBACK(column_factory_bind), listview->columns);
+    g_signal_connect(factory, "setup", G_CALLBACK(column_factory_setup), &listview->coldata);
+    g_signal_connect(factory, "bind", G_CALLBACK(column_factory_bind), &listview->coldata);
+    g_signal_connect(factory, "unbind", G_CALLBACK(column_factory_unbind), &listview->coldata);
     
     GtkSelectionModel *selection_model = create_selection_model(listview, ls, args->multiselection);
     GtkWidget *view = gtk_list_view_new(GTK_SELECTION_MODEL(selection_model), factory);
@@ -540,7 +546,7 @@
     return scroll_area;
 }
 
-UIWIDGET ui_combobox_create(UiObject *obj, UiListArgs *args) {
+UIWIDGET ui_dropdown_create(UiObject *obj, UiListArgs *args) {
     // to simplify things and share code with ui_tableview_create, we also
     // use a UiModel for the listview
     UiModel *model = ui_model(obj->ctx, UI_STRING, "", -1);
@@ -554,17 +560,15 @@
     }
     
     listview->numcolumns = 1;
-    listview->columns = malloc(sizeof(UiColData));
-    listview->columns->listview = listview;
-    listview->columns->data_column = 0;
-    listview->columns->model_column = 0;
+    listview->columns = malloc(sizeof(int));
+    listview->columns[0] = 0;
     
     listview->bound_rows = cxHashMapCreate(NULL, CX_STORE_POINTERS, 128);
     listview->bound_rows->collection.simple_destructor = (cx_destructor_func)free;
     
     GtkListItemFactory *factory = gtk_signal_list_item_factory_new();
-    g_signal_connect(factory, "setup", G_CALLBACK(column_factory_setup), listview->columns);
-    g_signal_connect(factory, "bind", G_CALLBACK(column_factory_bind), listview->columns);
+    g_signal_connect(factory, "setup", G_CALLBACK(column_factory_setup), &listview->coldata);
+    g_signal_connect(factory, "bind", G_CALLBACK(column_factory_bind), &listview->coldata);
     
     GtkWidget *view = gtk_drop_down_new(G_LIST_MODEL(ls), NULL);
     gtk_drop_down_set_factory(GTK_DROP_DOWN(view), factory);
@@ -591,8 +595,8 @@
         
         list->obj = listview;
         list->update = ui_listview_update2;
-        list->getselection = ui_combobox_getselection;
-        list->setselection = ui_combobox_setselection;
+        list->getselection = ui_dropdown_getselection;
+        list->setselection = ui_dropdown_setselection;
         
         ui_update_liststore(ls, list);
     } else if (args->static_elements && args->static_nelm > 0) {
@@ -619,10 +623,34 @@
     gtk_selection_model_select_item(model, index, TRUE);
 }
     
-void ui_combobox_select(UIWIDGET dropdown, int index) {
+void ui_dropdown_select(UIWIDGET dropdown, int index) {
     gtk_drop_down_set_selected(GTK_DROP_DOWN(dropdown), index);
 }
 
+static void add_column(UiListView *tableview, int index) {
+    UiModel *model = tableview->model;
+    
+    UiColData *col = malloc(sizeof(UiColData));
+    col->listview = tableview;
+    col->column = index;
+    
+    GtkListItemFactory *factory = gtk_signal_list_item_factory_new();
+    g_signal_connect(factory, "setup", G_CALLBACK(column_factory_setup), col);
+    g_signal_connect(factory, "bind", G_CALLBACK(column_factory_bind), col);
+    g_object_set_data_full(G_OBJECT(factory), "coldata", col, (GDestroyNotify)free);
+
+    GtkColumnViewColumn *column = gtk_column_view_column_new(model->titles[index], factory);
+    gtk_column_view_column_set_resizable(column, true);
+    gtk_column_view_insert_column(GTK_COLUMN_VIEW(tableview->widget), index, column);
+
+    int size = model->columnsize[index];
+    if(size > 0) {
+        gtk_column_view_column_set_fixed_width(column, size);
+    } else if(size < 0) {
+        gtk_column_view_column_set_expand(column, TRUE);
+    }
+}
+
 UIWIDGET ui_table_create(UiObject *obj, UiListArgs *args) {
     GListStore *ls = g_list_store_new(G_TYPE_OBJECT);
     //g_list_store_append(ls, v1);
@@ -650,9 +678,13 @@
     
     // create columns from UiModel
     UiModel *model = args->model;
-    int columns = model ? model->columns : 0;
+    int columns = 0;
+    if(model) {
+        columns = model->columns;
+        ui_model_add_observer(model, ui_listview_update_model, tableview);
+    }
     
-    tableview->columns = calloc(columns, sizeof(UiColData));
+    tableview->columns = calloc(columns, sizeof(int));
     tableview->numcolumns = columns;
     
     tableview->bound_rows = cxHashMapCreate(NULL, CX_STORE_POINTERS, 128);
@@ -660,30 +692,14 @@
     
     int addi = 0;
     for(int i=0;i<columns;i++) {
-        tableview->columns[i].listview = tableview;
-        tableview->columns[i].model_column = i;
-        tableview->columns[i].data_column = i+addi;
+        tableview->columns[i] = i+addi;
         
         if(model->types[i] == UI_ICON_TEXT || model->types[i] == UI_ICON_TEXT_FREE) {
             // icon+text has 2 data columns
             addi++;
         }
         
-        GtkListItemFactory *factory = gtk_signal_list_item_factory_new();
-        UiColData *col = &tableview->columns[i]; 
-        g_signal_connect(factory, "setup", G_CALLBACK(column_factory_setup), col);
-	g_signal_connect(factory, "bind", G_CALLBACK(column_factory_bind), col);
-        
-        GtkColumnViewColumn *column = gtk_column_view_column_new(model->titles[i], factory);
-        gtk_column_view_column_set_resizable(column, true);
-        gtk_column_view_append_column(GTK_COLUMN_VIEW(view), column);
-        
-        int size = model->columnsize[i];
-        if(size > 0) {
-            gtk_column_view_column_set_fixed_width(column, size);
-        } else if(size < 0) {
-            gtk_column_view_column_set_expand(column, TRUE);
-        }
+        add_column(tableview, i);
     }
     
     // bind listview to list
@@ -734,6 +750,49 @@
     return scroll_area;
 }
 
+void ui_listview_update_model(UiModel *model, void *userdata, int insert_index, int delete_index) {
+    UiListView *listview = userdata;
+    if(insert_index >= listview->numcolumns) {
+        listview->numcolumns = insert_index+1;
+        listview->columns = realloc(listview->columns, listview->numcolumns * sizeof(UiColData));
+    }
+    
+    gtk_column_view_set_model(GTK_COLUMN_VIEW(listview->widget), NULL);
+    cxMapClear(listview->bound_rows);
+    
+    if(insert_index) {
+        int prev = 0;
+        if(insert_index > 0) {
+            prev = listview->columns[insert_index-1];
+        }
+        listview->columns[insert_index] = prev+1;
+        add_column(listview, insert_index);
+        
+        if(insert_index+1 < listview->numcolumns) {
+            // the data index of trailing columns must be adjusted
+            UiModelType type = model->types[insert_index];
+            int add = 1;
+            if(type == UI_ICON_TEXT || type == UI_ICON_TEXT_FREE) {
+                add++;
+            }
+            for(int i=insert_index+1;i<listview->numcolumns;i++) {
+                listview->columns[i] += add;
+            }
+        }
+    } // TODO: delete_index
+    
+    GListStore *ls = g_list_store_new(G_TYPE_OBJECT);
+    GtkSelectionModel *selection_model = create_selection_model(listview, ls, listview->multiselection);
+    gtk_column_view_set_model(GTK_COLUMN_VIEW(listview->widget), selection_model);
+    listview->selectionmodel = selection_model;
+    listview->liststore = ls;
+    
+    if(listview->var) {
+        UiList *list = listview->var->value;
+        ui_list_update(list);
+    }
+}
+
 static UiListSelection selectionmodel_get_selection(GtkSelectionModel *model) {
     UiListSelection sel = { 0, NULL };
     GtkBitset *bitset = gtk_selection_model_get_selection(model);
@@ -860,7 +919,9 @@
 
 void ui_listview_update2(UiList *list, int i) {
     UiListView *view = list->obj;
+    view->current_row = -1;
     if(i < 0) {
+        cxMapClear(view->bound_rows);
         ui_update_liststore(view->liststore, list);
     } else {
         void *value = list->get(list, i);
@@ -873,9 +934,12 @@
             CxHashKey row_key = cx_hash_key(&i, sizeof(int));
             UiRowItems *row = cxMapGet(view->bound_rows, row_key);
             if(row) {
+                UiColData coldata;
+                coldata.listview = view;
                 for(int c=0;c<view->numcolumns;c++) {
                     if(row->items[c] != NULL) {
-                        column_factory_bind(NULL, row->items[c], &view->columns[c]);
+                        coldata.column = c;
+                        column_factory_bind(NULL, row->items[c], &coldata);
                     }
                 }
             }
@@ -915,7 +979,7 @@
     ui_setop_enable(FALSE);
 }
 
-UiListSelection ui_combobox_getselection(UiList *list) {
+UiListSelection ui_dropdown_getselection(UiList *list) {
     UiListView *view = list->obj;
     guint selection = gtk_drop_down_get_selected(GTK_DROP_DOWN(view->widget));
     UiListSelection sel = { 0, NULL };
@@ -927,7 +991,7 @@
     return sel;
 }
 
-void ui_combobox_setselection(UiList *list, UiListSelection selection) {
+void ui_dropdown_setselection(UiList *list, UiListSelection selection) {
     ui_setop_enable(TRUE);
     UiListView *view = list->obj;
     if(selection.count > 0) {
@@ -1156,7 +1220,7 @@
     // create treeview
     GtkWidget *view = gtk_tree_view_new();
     ui_set_name_and_style(view, args->name, args->style_class);
-    ui_set_widget_groups(obj->ctx, view, args->groups);
+    ui_set_widget_states(obj->ctx, view, args->states);
     GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
     GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(NULL, renderer, "text", 0, NULL);
     gtk_tree_view_append_column(GTK_TREE_VIEW(view), column);
@@ -1272,7 +1336,7 @@
     //g_object_unref(path);
 }
     
-void ui_combobox_select(UIWIDGET dropdown, int index) {
+void ui_dropdown_select(UIWIDGET dropdown, int index) {
     gtk_combo_box_set_active(GTK_COMBO_BOX(dropdown), index);
 }
 
@@ -1514,16 +1578,16 @@
 
 
 
-/* --------------------------- ComboBox ---------------------------  */
+/* --------------------------- Dropdown ---------------------------  */
 
-UIWIDGET ui_combobox_create(UiObject *obj, UiListArgs *args) {
+UIWIDGET ui_dropdown_create(UiObject *obj, UiListArgs *args) {
     GtkWidget *combobox = gtk_combo_box_new();
     if(args->width > 0) {
         gtk_widget_set_size_request(combobox, args->width, -1);
     }
     
     ui_set_name_and_style(combobox, args->name, args->style_class);
-    ui_set_widget_groups(obj->ctx, combobox, args->groups);
+    ui_set_widget_states(obj->ctx, combobox, args->states);
     UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end;
     UiLayout layout = UI_ARGS2LAYOUT(args);
     ct->add(ct, combobox, &layout);
@@ -1544,8 +1608,8 @@
     if(var) {
         listview->var = var;
         list->update = ui_combobox_modelupdate;
-        list->getselection = ui_combobox_getselection;
-        list->setselection = ui_combobox_setselection;
+        list->getselection = ui_dropdown_getselection;
+        list->setselection = ui_dropdown_setselection;
         list->obj = listview;
         list->update(list, -1);
     } else if(args->static_nelm > 0) {
@@ -1622,7 +1686,7 @@
     g_object_unref(store);
 }
 
-UiListSelection ui_combobox_getselection(UiList *list) {
+UiListSelection ui_dropdown_getselection(UiList *list) {
     UiListView *combobox = list->obj;
     UiListSelection ret;
     ret.rows = malloc(sizeof(int*));
@@ -1631,7 +1695,7 @@
     return ret;
 }
 
-void ui_combobox_setselection(UiList *list, UiListSelection selection) {
+void ui_dropdown_setselection(UiList *list, UiListSelection selection) {
     ui_setop_enable(TRUE);
     UiListView *combobox = list->obj;
     if(selection.count > 0) {
@@ -2045,6 +2109,10 @@
     if(v->var) {
         ui_destroy_boundvar(v->obj->ctx, v->var);
     }
+    if(v->model) {
+        ui_model_remove_observer(v->model, v);
+        ui_model_unref(v->model);
+    }
     if(v->elements) {
         for(int i=0;i<v->nelm;i++) {
             free(v->elements[i]);
@@ -2160,6 +2228,8 @@
         UiList *list = uisublist.var->value;
         list->obj = sublist_ptr;
         list->update = ui_listbox_list_update;
+        list->getselection = ui_listbox_list_getselection;
+        list->setselection = ui_listbox_list_setselection;
     }
 }
 
@@ -2181,7 +2251,7 @@
     SCROLLEDWINDOW_SET_CHILD(scroll_area, listbox);
     
     ui_set_name_and_style(listbox, args->name, args->style_class);
-    ui_set_widget_groups(obj->ctx, listbox, args->groups);
+    ui_set_widget_states(obj->ctx, listbox, args->states);
     UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end;
     UiLayout layout = UI_ARGS2LAYOUT(args);
     ct->add(ct, scroll_area, &layout);
@@ -2223,6 +2293,8 @@
             UiList *list = var->value;
             list->obj = uilistbox;
             list->update = ui_listbox_dynamic_update;
+            list->getselection = ui_listbox_dynamic_getselection;
+            list->setselection = ui_listbox_dynamic_setselection;
             
             ui_listbox_dynamic_update(list, -1);
         }
@@ -2294,6 +2366,32 @@
     ui_listbox_update(uilistbox, 0, cxListSize(uilistbox->sublists));
 }
 
+void ui_listbox_dynamic_setselection(UiList *list, UiListSelection sel) {
+    UiListBox *uilistbox = list->obj;
+    gtk_list_box_unselect_all(uilistbox->listbox);
+    if(sel.count > 0) {
+        int index = sel.rows[0];
+        if(index >= 0) {
+            GtkListBoxRow *row = gtk_list_box_get_row_at_index(uilistbox->listbox, index);
+            if(row) {
+                gtk_list_box_select_row(uilistbox->listbox, row);
+            }
+        }
+    }
+}
+
+UiListSelection ui_listbox_dynamic_getselection(UiList *list) {
+    UiListSelection sel = { 0, NULL };
+    UiListBox *uilistbox = list->obj;
+    GtkListBoxRow *row = gtk_list_box_get_selected_row(uilistbox->listbox);
+    if(row) {
+        sel.count = 1;
+        sel.rows = malloc(sizeof(int));
+        sel.rows[0] = gtk_list_box_row_get_index(row);
+    }
+    return sel;
+}
+
 void ui_listbox_update(UiListBox *listbox, int from, int to) {
     CxIterator i = cxListIterator(listbox->sublists);
     size_t pos = 0;
@@ -2659,6 +2757,39 @@
     ui_sourcelist_update_finished();
 }
 
+void ui_listbox_list_setselection(UiList *list, UiListSelection sel) {
+    UiListBoxSubList *sublist = list->obj;
+    UiListBox *uilistbox = sublist->listbox;
+    gtk_list_box_unselect_all(uilistbox->listbox);
+    if(sel.count > 0) {
+        int index = sel.rows[0];
+        if(index >= 0 && index < sublist->numitems) {
+            int global_index = sublist->startpos + index;
+            GtkListBoxRow *row = gtk_list_box_get_row_at_index(uilistbox->listbox, global_index);
+            if(row) {
+                gtk_list_box_select_row(uilistbox->listbox, row);
+            }
+        }
+    }
+}
+
+UiListSelection ui_listbox_list_getselection(UiList *list) {
+    UiListSelection sel = { 0, NULL };
+    UiListBoxSubList *sublist = list->obj;
+    UiListBox *uilistbox = sublist->listbox;
+    GtkListBoxRow *row = gtk_list_box_get_selected_row(uilistbox->listbox);
+    if(row) {
+        int index = gtk_list_box_row_get_index(row);
+        size_t startpos = sublist->startpos;
+        if(index >= startpos && index < startpos+sublist->numitems) {
+            sel.count = 1;
+            sel.rows = malloc(sizeof(int));
+            sel.rows[0] = index - startpos;
+        }
+    }
+    return sel;
+}
+
 void ui_listbox_row_activate(GtkListBox *self, GtkListBoxRow *row, gpointer user_data) {
     UiEventDataExt *data = g_object_get_data(G_OBJECT(row), "ui-listbox-row-eventdata");
     if(!data) {
--- a/ui/gtk/list.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/list.h	Sat Dec 13 15:58:58 2025 +0100
@@ -26,10 +26,10 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef TREE_H
-#define	TREE_H
+#ifndef LIST_H
+#define	LIST_H
 
-#include "../ui/tree.h"
+#include "../ui/list.h"
 #include "toolkit.h"
 
 #include <cx/array_list.h>
@@ -38,6 +38,7 @@
 extern "C" {
 #endif
     
+typedef struct UiListView UiListView;
 typedef struct UiColData UiColData;
 
 #if GTK_CHECK_VERSION(4, 10, 0)
@@ -47,11 +48,17 @@
 } UiRowItems;
 #endif
 
-typedef struct UiListView {
+struct UiColData {
+    UiListView *listview;
+    int column;
+};
+
+struct UiListView {
     UiObject          *obj;
     GtkWidget         *widget;
     UiVar             *var;
     UiModel           *model;
+    UiBool            multiselection;
     ui_getvaluefunc2  getvalue;
     void              *getvaluedata;
     ui_getstylefunc   getstyle;
@@ -65,8 +72,9 @@
     CxMap             *bound_rows;
     GListStore        *liststore;
     GtkSelectionModel *selectionmodel;
-    UiColData         *columns;
+    int               *columns;
     int               numcolumns;
+    UiColData         coldata;
     PangoAttrList     *current_row_attributes;
 #else
     int               style_offset;
@@ -85,12 +93,6 @@
     void              *onsavedata;
     UiListSelection   selection;
     
-} UiListView;
-
-struct UiColData {
-    UiListView *listview;
-    int model_column;
-    int data_column;
 };
 
 typedef struct UiTreeEventData {
@@ -136,6 +138,7 @@
 void ui_update_liststore(GListStore *liststore, UiList *list);
 void ui_update_liststore_static(GListStore *liststore, char **elm, size_t nelm);
 
+void ui_listview_update_model(UiModel *model, void *userdata, int insert_index, int delete_index);
 void ui_listview_update2(UiList *list, int i);
 UiListSelection ui_listview_getselection2(UiList *list);
 void ui_listview_setselection2(UiList *list, UiListSelection selection);
@@ -155,6 +158,7 @@
 
 GtkWidget* ui_get_tree_widget(UIWIDGET widget);
 
+void ui_listview_update_model(UiModel *model, void *userdata, int insert_index, int delete_index);
 void ui_listview_update(UiList *list, int i);
 UiListSelection ui_listview_getselection(UiList *list);
 void ui_listview_setselection(UiList *list, UiListSelection selection);
@@ -186,13 +190,18 @@
 GtkWidget* ui_create_combobox(UiObject *obj, UiModel *model, UiVar *var, char **elm, size_t nelm, ui_callback f, void *udata);
 void ui_combobox_change_event(GtkComboBox *widget, UiEventData *e);
 void ui_combobox_modelupdate(UiList *list, int i);
-UiListSelection ui_combobox_getselection(UiList *list);
-void ui_combobox_setselection(UiList *list, UiListSelection selection);
+UiListSelection ui_dropdown_getselection(UiList *list);
+void ui_dropdown_setselection(UiList *list, UiListSelection selection);
 
 void ui_listbox_dynamic_update(UiList *list, int i);
+void ui_listbox_dynamic_setselection(UiList *list, UiListSelection sel);
+UiListSelection ui_listbox_dynamic_getselection(UiList *list);
+
 void ui_listbox_update(UiListBox *listbox, int from, int to);
 void ui_listbox_update_sublist(UiListBox *listbox, UiListBoxSubList *sublist, size_t listbox_insert_index);
 void ui_listbox_list_update(UiList *list, int i);
+void ui_listbox_list_setselection(UiList *list, UiListSelection sel);
+UiListSelection ui_listbox_list_getselection(UiList *list);
 
 void ui_listbox_row_activate(GtkListBox *self, GtkListBoxRow *row, gpointer user_data);
         
@@ -200,5 +209,5 @@
 }
 #endif
 
-#endif	/* TREE_H */
+#endif	/* LIST_H */
 
--- a/ui/gtk/menu.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/menu.c	Sat Dec 13 15:58:58 2025 +0100
@@ -129,11 +129,11 @@
     
     gtk_menu_shell_append(GTK_MENU_SHELL(parent), widget);
     
-    if(i->groups) {
-        CxList *groups = cxArrayListCreateSimple(sizeof(int), i->ngroups);
-        cxListAddArray(groups, i->groups, i->ngroups);
-        uic_add_group_widget(obj->ctx, widget, (ui_enablefunc)ui_set_enabled, groups);
-        cxListFree(groups);
+    if(i->states) {
+        CxList *states = cxArrayListCreateSimple(sizeof(int), i->nstates);
+        cxListAddArray(states, i->states, i->nstates);
+        uic_add_state_widget(obj->ctx, widget, (ui_enablefunc)ui_set_enabled, states);
+        cxListFree(states);
     }
 }
 
@@ -462,10 +462,10 @@
     g_action_map_add_action(obj->ctx->action_map, G_ACTION(action));
     g_object_unref(action);
     
-    if(i->groups) {
-        CxList *groups = cxArrayListCreateSimple(sizeof(int), i->ngroups);
-        cxListAddArray(groups, i->groups, i->ngroups);
-        uic_add_group_widget(obj->ctx, action, (ui_enablefunc)action_enable, groups);
+    if(i->states) {
+        CxList *groups = cxArrayListCreateSimple(sizeof(int), i->nstates);
+        cxListAddArray(groups, i->states, i->nstates);
+        uic_add_state_widget(obj->ctx, action, (ui_enablefunc)action_enable, groups);
         cxListFree(groups);
     }
     
--- a/ui/gtk/text.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/text.c	Sat Dec 13 15:58:58 2025 +0100
@@ -32,6 +32,7 @@
 
 #include "text.h"
 #include "container.h"
+#include "widget.h"
 
 #include <cx/printf.h>
 
@@ -53,9 +54,9 @@
         int sel = gtk_text_buffer_get_selection_bounds (buf, &begin, &end);
         if(sel != textview->last_selection_state) {
             if(sel) {
-                ui_set_group(textview->ctx, UI_GROUP_SELECTION);
+                ui_set_state(textview->ctx, UI_GROUP_SELECTION);
             } else {
-                ui_unset_group(textview->ctx, UI_GROUP_SELECTION);
+                ui_unset_state(textview->ctx, UI_GROUP_SELECTION);
             }
         }
         textview->last_selection_state = sel;
@@ -112,7 +113,7 @@
     
     GtkWidget *text_area = gtk_text_view_new();
     ui_set_name_and_style(text_area, args->name, args->style_class);
-    ui_set_widget_groups(obj->ctx, text_area, args->groups);
+    ui_set_widget_states(obj->ctx, text_area, args->states);
     
     gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text_area), GTK_WRAP_WORD_CHAR);
     g_signal_connect(
@@ -144,17 +145,7 @@
             GTK_POLICY_AUTOMATIC); // GTK_POLICY_ALWAYS  
     SCROLLEDWINDOW_SET_CHILD(scroll_area, text_area);
     
-    if(args->width > 0 || args->height > 0) {
-        int width = args->width;
-        int height = args->height;
-        if(width == 0) {
-            width = -1;
-        }
-        if(height == 0) {
-            height = -1;
-        }
-        gtk_widget_set_size_request(scroll_area, width, height);
-    }
+    ui_widget_size_request(scroll_area, args->width, args->height);
     
     // font and padding
     //PangoFontDescription *font;
@@ -335,6 +326,10 @@
 
 
 void ui_textbuf_changed(GtkTextBuffer *textbuffer, UiTextArea *textarea) {
+    if(!ui_onchange_events_is_enabled()) {
+        return;
+    }
+    
     UiText *value = textarea->var->value;
     
     UiEvent e;
@@ -608,7 +603,7 @@
 static UIWIDGET create_textfield(UiObject *obj, UiBool frameless, UiBool password, UiTextFieldArgs *args) {
     GtkWidget *textfield = gtk_entry_new();
     ui_set_name_and_style(textfield, args->name, args->style_class);
-    ui_set_widget_groups(obj->ctx, textfield, args->groups);
+    ui_set_widget_states(obj->ctx, textfield, args->states);
     
     UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->value, args->varname, UI_VAR_STRING);
     
@@ -698,14 +693,18 @@
 }
 
 void ui_textfield_changed(GtkEditable *editable, UiTextField *textfield) {
-    UiString *value = textfield->var->value;
+    if(!ui_onchange_events_is_enabled()) {
+        return;
+    }
+    
+    UiString *value = textfield->var ? textfield->var->value : NULL;
     
     UiEvent e;
     e.obj = textfield->obj;
     e.window = e.obj->window;
     e.document = textfield->obj->ctx->document;
     e.eventdata = value;
-    e.eventdatatype = UI_EVENT_DATA_TEXT_VALUE;
+    e.eventdatatype = value ? UI_EVENT_DATA_TEXT_VALUE : 0;
     e.intval = 0;
     e.set = ui_get_setop();
     
@@ -720,12 +719,14 @@
 
 void ui_textfield_activate(GtkEntry* self, UiTextField *textfield) {
     if(textfield->onactivate) {
+        UiString *value = textfield->var ? textfield->var->value : NULL;
+        
         UiEvent e;
         e.obj = textfield->obj;
         e.window = e.obj->window;
         e.document = textfield->obj->ctx->document;
-        e.eventdata = NULL;
-        e.eventdatatype = 0;
+        e.eventdata = value;
+        e.eventdatatype = value ? UI_EVENT_DATA_TEXT_VALUE : 0;
         e.intval = 0;
         e.set = ui_get_setop();
         textfield->onactivate(&e, textfield->onactivatedata);
@@ -956,6 +957,10 @@
     pathtf->stack = gtk_stack_new();
     gtk_widget_set_name(pathtf->stack, "path-textfield-box");
     
+    if(args->width > 0) {
+        gtk_widget_set_size_request(pathtf->stack, args->width, -1);
+    }
+    
     UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end;
     UiLayout layout = UI_ARGS2LAYOUT(args);
     ct->add(ct, pathtf->stack, &layout);
@@ -1127,6 +1132,10 @@
             G_CALLBACK(ui_path_textfield_destroy),
             pathtf);
     
+    if(args->width > 0) {
+        gtk_widget_set_size_request(eventbox, args->width, -1);
+    }
+    
     UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end;
     UiLayout layout = UI_ARGS2LAYOUT(args);
     ct->add(ct, eventbox, &layout);
--- a/ui/gtk/toolbar.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/toolbar.c	Sat Dec 13 15:58:58 2025 +0100
@@ -139,7 +139,7 @@
     }
     gtk_tool_item_set_is_important(button, TRUE);
     
-    ui_set_widget_ngroups(obj->ctx, GTK_WIDGET(button), item->args.groups, item->ngroups);
+    ui_set_widget_nstates(obj->ctx, GTK_WIDGET(button), item->args.states, item->nstates);
     
     if(item->args.onclick) {
         UiEventData *event = cxMalloc(
@@ -181,7 +181,7 @@
     if(item->args.tooltip) {
         gtk_tool_item_set_tooltip_text(GTK_TOOL_ITEM(button), item->args.tooltip);
     }
-    ui_set_widget_ngroups(obj->ctx, GTK_WIDGET(button), item->args.groups, item->ngroups);
+    ui_set_widget_nstates(obj->ctx, GTK_WIDGET(button), item->args.states, item->nstates);
     
     UiVar* var = uic_widget_var(obj->ctx, obj->ctx, NULL, item->args.varname, UI_VAR_INTEGER);
     if(var) {
@@ -394,7 +394,7 @@
     if(item->args.icon) {
         ui_button_set_icon_name(button, item->args.icon);
     }
-    ui_set_widget_groups(obj->ctx, button, item->args.groups);
+    ui_set_widget_states(obj->ctx, button, item->args.states);
     
     gtk_header_bar_pack_start(hb, button);
     
--- a/ui/gtk/toolkit.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/toolkit.c	Sat Dec 13 15:58:58 2025 +0100
@@ -39,6 +39,7 @@
 #include "../common/menu.h"
 #include "../common/toolbar.h"
 #include "../common/threadpool.h"
+#include "../common/app.h"
 
 #include <cx/string.h>
 #include <cx/printf.h>
@@ -51,13 +52,6 @@
 
 static const char *application_name;
 
-static ui_callback   startup_func;
-static void          *startup_data;
-static ui_callback   open_func;
-void                 *open_data;
-static ui_callback   exit_func;
-void                 *exit_data;
-
 static ui_callback   appclose_fnc;
 static void          *appclose_udata;
 
@@ -95,30 +89,13 @@
     return application_name;
 }
 
-void ui_onstartup(ui_callback f, void *userdata) {
-    startup_func = f;
-    startup_data = userdata;
-}
-
-void ui_onopen(ui_callback f, void *userdata) {
-    open_func = f;
-    open_data = userdata;
-}
-
-void ui_onexit(ui_callback f, void *userdata) {
-    exit_func = f;
-    exit_data = userdata;
-}
-
 void ui_app_exit_on_shutdown(UiBool exitapp) {
     exit_on_shutdown = exitapp;
 }
 
 #ifdef UI_APPLICATION
 static void app_startup(GtkApplication* app, gpointer userdata) {
-    if(startup_func) {
-        startup_func(NULL, startup_data);
-    }
+    uic_application_startup(NULL);
 }
 
 static void app_activate(GtkApplication* app, gpointer userdata) {
@@ -126,9 +103,7 @@
 }
 
 static void app_shutdown(GtkApplication *app, gpointer userdata) {
-    if(exit_func) {
-        exit_func(NULL, exit_data);
-    }
+    uic_application_exit(NULL);
     ui_app_save_settings();
 }
 
@@ -148,13 +123,9 @@
     
     free(appid.ptr);
 #else
-    if(startup_func) {
-        startup_func(NULL, startup_data);
-    }
+    uic_application_startup(NULL);
     gtk_main();
-    if(exit_func) {
-        exit_func(NULL, exit_data);
-    }
+    uic_application_exit(NULL);
     ui_app_save_settings();
 #endif
     if(exit_on_shutdown) {
@@ -164,7 +135,7 @@
 
 #ifndef UI_GTK2
 void ui_app_quit() {
-    g_application_quit(G_APPLICATION(app));
+    g_application_quit(G_APPLICATION(app)); // TODO: fix, does not work
 }
 
 GtkApplication* ui_get_application() {
@@ -175,7 +146,7 @@
 void ui_show(UiObject *obj) {
     gboolean visible = gtk_widget_is_visible(obj->widget);
     
-    uic_check_group_widgets(obj->ctx);
+    uic_check_state_widgets(obj->ctx);
 #if GTK_MAJOR_VERSION >= 4
     gtk_window_present(GTK_WINDOW(obj->widget));
 #elif GTK_MAJOR_VERSION <= 3
@@ -263,14 +234,14 @@
 #endif
 }
 
-void ui_set_visible(UIWIDGET widget, int visible) {
+void ui_set_visible(UIWIDGET widget, UiBool visible) {
 #if GTK_MAJOR_VERSION >= 4
     gtk_widget_set_visible(widget, visible);
 #else
     if(visible) {
-        gtk_widget_set_no_show_all(widget, FALSE);
         gtk_widget_show_all(widget);
     } else {
+        gtk_widget_set_no_show_all(widget, FALSE);
         gtk_widget_hide(widget);
     }
 #endif
@@ -346,6 +317,11 @@
 
 #if GTK_MAJOR_VERSION == 4
 static const char *ui_gtk_css = 
+".ui-entry-box {\n"
+"  background-color: alpha(currentColor, 0.1);"
+"  border-radius: 6px;"
+"  padding: 7px;"
+"}\n"
 "#path-textfield-box {\n"
 "  background-color: alpha(currentColor, 0.1);"
 "  border-radius: 6px;"
@@ -529,17 +505,17 @@
     }
 }
 
-void ui_set_widget_groups(UiContext *ctx, GtkWidget *widget, const int *groups) {
-    if(!groups) {
+void ui_set_widget_states(UiContext *ctx, GtkWidget *widget, const int *states) {
+    if(!states) {
         return;
     }
-    size_t ngroups = uic_group_array_size(groups);
-    ui_set_widget_ngroups(ctx, widget, groups, ngroups);
+    size_t nstates = uic_state_array_size(states);
+    ui_set_widget_nstates(ctx, widget, states, nstates);
 }
 
-void ui_set_widget_ngroups(UiContext *ctx, GtkWidget *widget, const int *groups, size_t ngroups) {
-    if(ngroups > 0) {
-        uic_add_group_widget_i(ctx, widget, (ui_enablefunc)ui_set_enabled, groups, ngroups);
+void ui_set_widget_nstates(UiContext *ctx, GtkWidget *widget, const int *states, size_t nstates) {
+    if(nstates > 0) {
+        uic_add_state_widget_i(ctx, widget, (ui_enablefunc)ui_set_enabled, states, nstates);
         ui_set_enabled(widget, FALSE);
     }
 }
@@ -548,14 +524,14 @@
     if(!states) {
         return;
     }
-    size_t nstates = uic_group_array_size(states);
+    size_t nstates = uic_state_array_size(states);
     ui_set_widget_nvisibility_states(ctx, widget, states, nstates);
 }
 
 
 void ui_set_widget_nvisibility_states(UiContext *ctx, GtkWidget *widget, const int *states, size_t ngroups) {
     if(ngroups > 0) {
-        uic_add_group_widget_i(ctx, widget, (ui_enablefunc)ui_set_visible, states, ngroups);
+        uic_add_state_widget_i(ctx, widget, (ui_enablefunc)ui_set_visible, states, ngroups);
         ui_set_visible(widget, FALSE);
     }
 }
--- a/ui/gtk/toolkit.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/toolkit.h	Sat Dec 13 15:58:58 2025 +0100
@@ -178,8 +178,8 @@
 int ui_get_scalefactor();
 
 void ui_set_name_and_style(GtkWidget *widget, const char *name, const char *style);
-void ui_set_widget_groups(UiContext *ctx, GtkWidget *widget, const int *groups);
-void ui_set_widget_ngroups(UiContext *ctx, GtkWidget *widget, const int *groups, size_t ngroups);
+void ui_set_widget_states(UiContext *ctx, GtkWidget *widget, const int *states);
+void ui_set_widget_nstates(UiContext *ctx, GtkWidget *widget, const int *states, size_t nstates);
 void ui_set_widget_visibility_states(UiContext *ctx, GtkWidget *widget, const int *states);
 void ui_set_widget_nvisibility_states(UiContext *ctx, GtkWidget *widget, const int *states, size_t ngroups);
 
--- a/ui/gtk/webview.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/webview.c	Sat Dec 13 15:58:58 2025 +0100
@@ -30,6 +30,7 @@
 #include "container.h"
 
 #include "webview.h"
+#include "widget.h"
 
 #ifdef UI_WEBVIEW
 
@@ -38,6 +39,8 @@
     
     ui_set_name_and_style(webview, args->name, args->style_class);
     
+    ui_widget_size_request(webview, args->width, args->height);
+    
     UiVar *var = uic_widget_var(obj->ctx, obj->ctx, args->value, args->varname, UI_VAR_GENERIC);
     if(var) {
         WebViewData *data = malloc(sizeof(WebViewData));
@@ -57,7 +60,7 @@
         }
     }
     
-    ui_set_widget_groups(obj->ctx, webview, args->groups);
+    ui_set_widget_states(obj->ctx, webview, args->states);
     UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end;
     UiLayout layout = UI_ARGS2LAYOUT(args);
     ct->add(ct, webview, &layout);
--- a/ui/gtk/widget.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/widget.c	Sat Dec 13 15:58:58 2025 +0100
@@ -31,7 +31,20 @@
 
 #include "../common/object.h"
 
-UIEXPORT UIWIDGET ui_customwidget_create(UiObject *obj, ui_createwidget_func create_widget, void *userdata, UiWidgetArgs *args) {
+void ui_widget_size_request(UIWIDGET w, int width, int height) {
+    if(width > 0 || height > 0) {
+        if(width == 0) {
+            width = -1;
+        }
+        if(height == 0) {
+            height = -1;
+        }
+        gtk_widget_set_size_request(w, width, height);
+    }
+}
+
+
+UIWIDGET ui_customwidget_create(UiObject *obj, ui_createwidget_func create_widget, void *userdata, UiWidgetArgs *args) {
     UIWIDGET widget = create_widget(obj, args, userdata);
     
     UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end;
--- a/ui/gtk/widget.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/widget.h	Sat Dec 13 15:58:58 2025 +0100
@@ -35,7 +35,12 @@
 extern "C" {
 #endif
 
-
+/*
+ * Sets a widget width/height.
+ * 
+ * If wdith or height is 0, the dimension is not changed
+ */
+void ui_widget_size_request(UIWIDGET w, int width, int height);
 
 
 #ifdef __cplusplus
--- a/ui/gtk/window.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/gtk/window.c	Sat Dec 13 15:58:58 2025 +0100
@@ -35,6 +35,7 @@
 #include "../common/context.h"
 #include "../common/menu.h"
 #include "../common/toolbar.h"
+#include "../common/utils.h"
 
 #include <cx/mempool.h>
 
@@ -141,7 +142,7 @@
     ui_set_property("ui.window.splitview.pos", buf);
 }
 
-static UiObject* create_window(const char *title, void *window_data, UiBool sidebar, UiBool splitview, UiBool simple) {
+static UiObject* create_window(const char *title, UiBool sidebar, UiBool splitview, UiBool simple) {
     UiObject *obj = uic_object_new_toplevel();
    
 #ifdef UI_LIBADWAITA
@@ -152,8 +153,6 @@
     obj->widget = gtk_window_new(GTK_WINDOW_TOPLEVEL);
 #endif
     
-    obj->window = window_data;
-    
 #if GTK_CHECK_VERSION(4, 0, 0)
     obj->ctx->action_map = G_ACTION_MAP(obj->widget);
 #endif
@@ -169,16 +168,7 @@
     int window_width = window_default_width;
     int window_height = window_default_height;
     if(!simple) {
-        const char *width = ui_get_property("ui.window.width");
-        const char *height = ui_get_property("ui.window.height");
-        if(width && height) {
-            int w = atoi(width);
-            int h = atoi(height);
-            if(w > 0 && h > 0) {
-                window_width = w;
-                window_height = h;
-            }
-        }
+        ui_get_window_default_width(&window_width, &window_height);
     }
     gtk_window_set_default_size(
                 GTK_WINDOW(obj->widget),
@@ -205,6 +195,21 @@
             obj);
 #endif
     
+    int splitview_pos = 0;
+    if(splitview) {
+        const char *splitview_pos_str = ui_get_property("ui.window.splitview.pos");
+        splitview_pos= splitview_window_default_pos;
+        if(splitview_pos < 0) {
+            splitview_pos = window_width / 2;
+        }
+        if(splitview_pos_str && splitview_window_use_prop) {
+            int sv_pos = atoi(splitview_pos_str);
+            if(sv_pos > 0) {
+                splitview_pos = sv_pos;
+            }
+        }
+    }
+    
     GtkWidget *vbox = ui_gtk_vbox_new(0);
 #ifdef UI_LIBADWAITA
     GtkWidget *toolbar_view = adw_toolbar_view_new();
@@ -223,18 +228,7 @@
                 G_CALLBACK(save_window_splitview_pos),
                 NULL);
         
-        const char *splitview_pos_str = ui_get_property("ui.window.splitview.pos");
-        int pos = splitview_window_default_pos;
-        if(pos < 0) {
-            pos = window_width / 2;
-        }
-        if(splitview_pos_str && splitview_window_use_prop) {
-            int splitview_pos = atoi(splitview_pos_str);
-            if(splitview_pos > 0) {
-                pos = splitview_pos;
-            }
-        }
-        gtk_paned_set_position(GTK_PANED(content), pos);
+        gtk_paned_set_position(GTK_PANED(content), splitview_pos);
         
         GtkWidget *right_panel = adw_toolbar_view_new();
         GtkWidget *right_vbox = ui_gtk_vbox_new(0);
@@ -345,14 +339,32 @@
     
     GtkWidget *content_box = ui_gtk_vbox_new(0);
     WINDOW_SET_CONTENT(obj->widget, vbox);
-    if(sidebar) {
+    if(sidebar || splitview) {
         GtkWidget *paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
-        GtkWidget *sidebar_vbox = ui_gtk_vbox_new(0);
-        gtk_paned_add1(GTK_PANED(paned), sidebar_vbox);
-        gtk_paned_add2(GTK_PANED(paned), content_box);
+        if(sidebar) {
+            GtkWidget *sidebar_vbox = ui_gtk_vbox_new(0);
+            gtk_paned_add1(GTK_PANED(paned), sidebar_vbox);
+            g_object_set_data(G_OBJECT(obj->widget), "ui_sidebar", sidebar_vbox);
+            gtk_paned_set_position(GTK_PANED(paned), 200);
+        }
+        
+        if(splitview) {
+            GtkWidget *content_paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
+            gtk_paned_set_position(GTK_PANED(content_paned), splitview_pos);
+            gtk_paned_add2(GTK_PANED(paned), content_paned);
+            
+            GtkWidget *right_content_box = ui_gtk_vbox_new(0);
+            gtk_paned_add1(GTK_PANED(content_paned), content_box);
+            gtk_paned_add2(GTK_PANED(content_paned), right_content_box);
+            
+            g_object_set_data(G_OBJECT(obj->widget), "ui_window_splitview", content_paned);
+            g_object_set_data(G_OBJECT(obj->widget), "ui_left_panel", content_box);
+            g_object_set_data(G_OBJECT(obj->widget), "ui_right_panel", right_content_box);
+        } else {
+            gtk_paned_add2(GTK_PANED(paned), content_box);
+        }
+        
         BOX_ADD_EXPAND(GTK_BOX(vbox), paned);
-        g_object_set_data(G_OBJECT(obj->widget), "ui_sidebar", sidebar_vbox);
-        gtk_paned_set_position (GTK_PANED(paned), 200);
     } else {
         BOX_ADD_EXPAND(GTK_BOX(vbox), content_box);
     }
@@ -380,20 +392,20 @@
 }
 
 
-UiObject* ui_window(const char *title, void *window_data) {
-    return create_window(title, window_data, FALSE, FALSE, FALSE);
+UiObject* ui_window(const char *title) {
+    return create_window(title, FALSE, FALSE, FALSE);
 }
 
-UiObject *ui_sidebar_window(const char *title, void *window_data) {
-    return create_window(title, window_data, TRUE, FALSE, FALSE);
+UiObject *ui_sidebar_window(const char *title) {
+    return create_window(title, TRUE, FALSE, FALSE);
 }
 
-UIEXPORT UiObject *ui_splitview_window(const char *title, UiBool sidebar) {
-    return create_window(title, NULL, sidebar, TRUE, FALSE);
+UiObject *ui_splitview_window(const char *title, UiBool sidebar) {
+    return create_window(title, sidebar, TRUE, FALSE);
 }
 
-UiObject* ui_simple_window(const char *title, void *window_data) {
-    return create_window(title, window_data, FALSE, FALSE, TRUE);
+UiObject* ui_simple_window(const char *title) {
+    return create_window(title, FALSE, FALSE, TRUE);
 }
 
 void ui_window_size(UiObject *obj, int width, int height) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/motif/Fsb.c	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,2642 @@
+/*
+ * Copyright 2021 Olaf Wintermann
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a 
+ * copy of this software and associated documentation files (the "Software"), 
+ * to deal in the Software without restriction, including without limitation 
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
+ * and/or sell copies of the Software, and to permit persons to whom the 
+ * Software is furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in 
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+//define FSB_ENABLE_DETAIL
+
+#include "Fsb.h"
+#include "FsbP.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <time.h>
+#include <inttypes.h>
+#include <errno.h>
+
+#include <sys/stat.h>
+#include <limits.h>
+#include <dirent.h>
+#include <fnmatch.h>
+
+#include <Xm/XmAll.h>
+#include <Xm/DropDown.h>
+
+#include "pathbar.h"
+
+#include "../common/utils.h"
+
+#ifdef FSB_ENABLE_DETAIL
+#include <XmL/Grid.h>
+#endif
+
+#define WIDGET_SPACING 5
+#define WINDOW_SPACING 8
+
+#define BUTTON_EXTRA_SPACE 4
+
+#define DATE_FORMAT_SAME_YEAR  "%b %d %H:%M"
+#define DATE_FORMAT_OTHER_YEAR "%b %d  %Y"
+
+#define KB_SUFFIX "KiB"
+#define MB_SUFFIX "MiB"
+#define GB_SUFFIX "GiB"
+#define TB_SUFFIX "TiB"
+
+#define FSB_ERROR_TITLE          "Error"
+#define FSB_ERROR_CHAR           "Character '/' is not allowed in file names"
+#define FSB_ERROR_RENAME         "Cannot rename file: %s"
+#define FSB_ERROR_DELETE         "Cannot delete file: %s"
+#define FSB_ERROR_CREATE_FOLDER  "Cannot create folder: %s"
+#define FSB_ERROR_OPEN_DIR       "Cannot open directory: %s"
+
+#define FSB_DETAIL_HEADINGS "Name|Size|Last Modified"
+
+static void fsb_class_init(void);
+static void fsb_class_part_init (WidgetClass wc);
+static void fsb_init(Widget request, Widget neww, ArgList args, Cardinal *num_args);
+static void fsb_resize(Widget widget);
+static void fsb_realize(Widget widget, XtValueMask *mask, XSetWindowAttributes *attributes);
+static void fsb_destroy(Widget widget);
+static Boolean fsb_set_values(Widget old, Widget request, Widget neww, ArgList args, Cardinal *num_args);
+static Boolean fsb_acceptfocus(Widget widget, Time *time);
+
+static void fsb_insert_child(Widget child);
+
+static void fsb_mapcb(Widget widget, XtPointer u, XtPointer cb);
+
+static int FSBGlobFilter(const char *a, const char *b);
+
+static void FSBUpdateTitle(Widget w);
+
+static void FocusInAP(Widget w, XEvent *event, String *args, Cardinal *nArgs);
+
+static void ErrDialog(XnFileSelectionBox w, const char *title, const char *errmsg);
+
+static void FSBRename(XnFileSelectionBox fsb, const char *path);
+static void FSBDelete(XnFileSelectionBox fsb, const char *path);
+
+static void FSBSelectItem(XnFileSelectionBox fsb, const char *item);
+
+static char* set_selected_path(XnFileSelectionBox data, XmString item);
+
+static void FileContextMenuCB(Widget item, XtPointer index, XtPointer cd);
+
+static Widget CreateContextMenu(XnFileSelectionBox fsb, Widget parent, XtCallbackProc callback);
+
+static void FileListUpdate(Widget fsb, Widget view, FileElm *dirlist, int dircount, FileElm *files, int filecount, const char *filter, int maxnamelen, void *userData);
+static void FileListSelect(Widget fsb, Widget view, const char *item);
+static void FileListCleanup(Widget fsb, Widget view, void *userData);
+static void FileListDestroy(Widget fsb, Widget view, void *userData);
+
+static void FileListActivateCB(Widget w, XnFileSelectionBox data, XmListCallbackStruct *cb);
+static void FileListSelectCB(Widget w, XnFileSelectionBox data, XmListCallbackStruct *cb);
+
+static void FileListWidgetAdd(XnFileSelectionBox fsb, Widget w, int showHidden, const char *filter, FileElm *ls, int count);
+
+#ifdef FSB_ENABLE_DETAIL
+static void FileListDetailUpdate(Widget fsb, Widget view, FileElm *dirlist, int dircount, FileElm *files, int filecount, const char *filter, int maxnamelen, void *userData);
+static void FileListDetailSelect(Widget fsb, Widget view, const char *item);
+static void FileListDetailCleanup(Widget fsb, Widget view, void *userData);
+static void FileListDetailDestroy(Widget fsb, Widget view, void *userData);
+static void FileListDetailAdjustColWidth(Widget grid);
+static void FileListDetailAdd(XnFileSelectionBox fsb, Widget grid, int showHidden, const char *filter, FileElm *ls, int count, int maxWidth);
+#endif
+
+static void FSBNewFolder(Widget w, XnFileSelectionBox data, XtPointer u);
+
+static void FSBHome(Widget w, XnFileSelectionBox data, XtPointer u);
+
+static void FileSelectionCallback(XnFileSelectionBox fsb, XtCallbackList cb, int reason, const char *value);
+
+static void CreateUI(XnFileSelectionBox w);
+static FSBViewWidgets CreateView(XnFileSelectionBox w, FSBViewCreateProc createProc, void *userData, Boolean useDirList);
+static void AddViewMenuItem(XnFileSelectionBox w, const char *name, int viewIndex);
+static void SelectView(XnFileSelectionBox f, int view);
+
+static char* FSBDialogTitle(Widget w);
+
+static FSBViewWidgets CreateListView(Widget fsb, ArgList args, int n, void *userData);
+static FSBViewWidgets CreateDetailView(Widget fsb, ArgList args, int n, void *userData);
+
+static const char* GetHomeDir(void);
+
+static char* ConcatPath(const char *parent, const char *name);
+static char* FileName(char *path);
+static char* ParentPath(const char *path);
+//static int   CheckFileName(const char *name);
+
+static int filedialog_update_dir(XnFileSelectionBox data, const char *path);
+static void filedialog_cleanup_filedata(XnFileSelectionBox data);
+
+
+static XtResource resources[] = {
+    {XmNokCallback, XmCCallback, XmRCallback, sizeof(XtCallbackList), XtOffset(XnFileSelectionBox, fsb.okCallback), XmRCallback, NULL},
+    {XmNcancelCallback, XmCCallback, XmRCallback, sizeof(XtCallbackList), XtOffset(XnFileSelectionBox, fsb.cancelCallback), XmRCallback, NULL},
+    {XnNwidgetSpacing, XmCSpacing, XmRDimension, sizeof(Dimension), XtOffset(XnFileSelectionBox, fsb.widgetSpacing), XmRImmediate, (XtPointer)WIDGET_SPACING},
+    {XnNwindowSpacing, XmCSpacing, XmRDimension, sizeof(Dimension), XtOffset(XnFileSelectionBox, fsb.windowSpacing), XmRImmediate, (XtPointer)WINDOW_SPACING},
+    {XnNfsbType, XnCfsbType, XmRInt, sizeof(int), XtOffset(XnFileSelectionBox, fsb.type), XmRImmediate, (XtPointer)FILEDIALOG_OPEN},
+    {XnNshowHidden, XnCshowHidden, XmRBoolean, sizeof(Boolean), XtOffset(XnFileSelectionBox, fsb.showHidden), XmRImmediate, (XtPointer)False},
+    {XnNshowHiddenButton, XnCshowHiddenButton, XmRBoolean, sizeof(Boolean), XtOffset(XnFileSelectionBox, fsb.showHiddenButton), XmRImmediate, (XtPointer)True},
+    {XnNshowViewMenu, XnCshowViewMenu, XmRBoolean, sizeof(Boolean), XtOffset(XnFileSelectionBox, fsb.showViewMenu), XmRImmediate, (XtPointer)False},
+    {XnNselectedView, XnCselectedView, XmRInt, sizeof(int), XtOffset(XnFileSelectionBox, fsb.selectedview), XmRImmediate, (XtPointer)0},
+    
+    {XnNdirectory, XnCdirectory, XmRString, sizeof(XmString), XtOffset(XnFileSelectionBox, fsb.currentPath), XmRString, NULL},
+    {XnNselectedPath, XnCselectedPath, XmRString, sizeof(XmString), XtOffset(XnFileSelectionBox, fsb.selectedPath), XmRString, NULL},
+    {XnNhomePath, XnChomePath, XmRString, sizeof(XmString), XtOffset(XnFileSelectionBox, fsb.homePath), XmRString, NULL},
+    
+    {XnNfilter,XnCfilter,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.filterStr), XmRString, "*"},
+    {XnNfilterFunc,XnCfilterFunc,XmRFunction,sizeof(FSBFilterFunc),XtOffset(XnFileSelectionBox, fsb.filterFunc), XmRFunction, NULL},
+    
+    {XnNlabelListView,XnClabelListView,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelListView), XmRString, "List"},
+    {XnNlabelDetailView,XnClabelDetailView,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelDetailView), XmRString, "Detail"},
+    {XnNlabelOpenFileTitle,XnClabelOpenFileTitle,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelOpenFileTitle), XmRString, "Open File"},
+    {XnNlabelSaveFileTitle,XnClabelSaveFileTitle,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelSaveFileTitle), XmRString, "Save File"},
+    {XnNlabelDirUp,XnClabelDirUp,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelDirUp), XmRString, "Dir Up"},
+    {XnNlabelHome,XnClabelHome,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelHome), XmRString, "Home"},
+    {XnNlabelNewFolder,XnClabelNewFolder,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelNewFolder), XmRString, "New Folder"},
+    {XnNlabelFilterButton,XnClabelFilterButton,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelFilterButton), XmRString, "Filter"},
+    {XnNlabelShowHiddenFiles,XnClabelShowHiddenFiles,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelShowHiddenFiles), XmRString, "Show hiden files"},
+    {XnNlabelDirectories,XnClabelDirectories,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelDirectories), XmRString, "Directories"},
+    {XnNlabelFiles,XnClabelFiles,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelFiles), XmRString, "Files"},
+    {XnNlabelRename,XnClabelRename,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelRename), XmRString, "Rename"},
+    {XnNlabelDelete,XnClabelDelete,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelDelete), XmRString, "Delete"},
+    {XnNlabelOpen,XnClabelOpen,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelOpen), XmRString, "Open"},
+    {XnNlabelSave,XnClabelSave,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelSave), XmRString, "Save"},
+    {XnNlabelOk,XnClabelOk,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelOk), XmRString, "OK"},
+    {XnNlabelCancel,XnClabelCancel,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelCancel), XmRString, "Cancel"},
+    {XnNlabelHelp,XnClabelHelp,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelHelp), XmRString, "Help"},
+    {XnNlabelFileName,XnClabelFileName,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelFileName), XmRString, "New File Name"},
+    {XnNlabelDirectoryName,XnClabelDirectoryName,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelDirectoryName), XmRString, "Directory name:"},
+    {XnNlabelNewFileName,XnClabelNewFileName,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelNewFileName), XmRString, "New file name:"},
+    {XnNlabelDeleteFile,XnClabelDeleteFile,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelDeleteFile), XmRString, "Delete file '%s'?"},
+    {XnNdetailHeadings,XnCdetailHeadings,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.detailHeadings), XmRString,FSB_DETAIL_HEADINGS},
+    {XnNdateFormatSameYear,XnCdateFormatSameYear,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.dateFormatSameYear), XmRString,DATE_FORMAT_SAME_YEAR},
+    {XnNdateFormatOtherYear,XnNdateFormatOtherYear,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.dateFormatOtherYear), XmRString,DATE_FORMAT_OTHER_YEAR},
+    {XnNsuffixBytes,XnCsuffixBytes,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.suffixBytes), XmRString,"bytes"},
+    {XnNsuffixKB,XnCsuffixKB,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.suffixKB), XmRString,KB_SUFFIX},
+    {XnNsuffixMB,XnCsuffixKB,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.suffixMB), XmRString,MB_SUFFIX},
+    {XnNsuffixGB,XnCsuffixKB,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.suffixGB), XmRString,GB_SUFFIX},
+    {XnNsuffixTB,XnCsuffixKB,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.suffixTB), XmRString,TB_SUFFIX},
+    
+    {XnNerrorTitle,XnCerrorTitle,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.errorTitle), XmRString,FSB_ERROR_TITLE},
+    {XnNerrorIllegalChar,XnCerrorIllegalChar,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.errorIllegalChar), XmRString,FSB_ERROR_CHAR},
+    {XnNerrorRename,XnCerrorRename,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.errorRename), XmRString,FSB_ERROR_RENAME},
+    {XnNerrorCreateFolder,XnCerrorCreateFolder,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.errorFolder), XmRString,FSB_ERROR_CREATE_FOLDER},
+    {XnNerrorDelete,XnCerrorDelete,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.errorDelete), XmRString,FSB_ERROR_DELETE},
+    {XnNerrorOpenDir,XnCerrorOpenDir,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.errorOpenDir), XmRString,FSB_ERROR_OPEN_DIR}
+};
+
+static XtActionsRec actionslist[] = {
+  {"focusIn", FocusInAP},
+  {"NULL", NULL}
+};
+
+
+static char defaultTranslations[] = "<FocusIn>:		        focusIn()";
+
+static XtResource constraints[] = {};
+
+FSBClassRec fsbWidgetClassRec = {
+    // Core Class
+    {
+        (WidgetClass)&xmFormClassRec,
+        "XnFSB",                         // class_name
+        sizeof(FSBRec),                  // widget_size
+        fsb_class_init,                  // class_initialize
+        fsb_class_part_init,             // class_part_initialize
+        FALSE,                           // class_inited
+        fsb_init,                        // initialize
+        NULL,                            // initialize_hook
+        fsb_realize,                     // realize
+        actionslist,                     // actions
+        XtNumber(actionslist),           // num_actions
+        resources,                       // resources
+        XtNumber(resources),             // num_resources
+        NULLQUARK,                       // xrm_class
+        True,                            // compress_motion
+        True,                            // compress_exposure
+        True,                            // compress_enterleave
+        False,                           // visible_interest
+        fsb_destroy,                     // destroy
+        fsb_resize,                      // resize
+        XtInheritExpose,                 // expose
+        fsb_set_values,                  // set_values
+        NULL,                            // set_values_hook
+        XtInheritSetValuesAlmost,        // set_values_almost
+        NULL,                            // get_values_hook
+        fsb_acceptfocus,                 // accept_focus
+        XtVersion,                       // version
+        NULL,                            // callback_offsets
+        defaultTranslations,             // tm_table
+        XtInheritQueryGeometry,          // query_geometry
+        XtInheritDisplayAccelerator,     // display_accelerator
+        NULL,                            // extension
+    },
+    // Composite Class
+    {
+        XtInheritGeometryManager, // geometry_manager 
+        XtInheritChangeManaged,   // change_managed
+        fsb_insert_child,         // insert_child 
+        XtInheritDeleteChild,     // delete_child  
+        NULL,                     // extension   
+    },
+    // Constraint Class
+    {
+        constraints,                 // resources
+        XtNumber(constraints),       // num_resources   
+        sizeof(XmFormConstraintRec), // constraint_size  
+        NULL,                        // initialize 
+        NULL,                        // destroy
+        NULL,                        // set_value
+        NULL,                        // extension 
+    },
+    // XmManager Class
+    {
+        XtInheritTranslations, // translations
+        NULL, // syn_resources
+        0,    // num_syn_resources
+        NULL, // syn_constraint_resources
+        0,    // num_syn_constraint_resources
+        XmInheritParentProcess, // parent_process
+        NULL  // extension
+    },
+    // XmBulletinBoard
+    {
+        FALSE,
+        NULL,
+        XmInheritFocusMovedProc,
+        NULL
+    },
+    // XmForm Class
+    {
+        NULL
+    },
+    // FSB Class
+    {
+        0
+    }
+};
+
+WidgetClass xnFsbWidgetClass = (WidgetClass)&fsbWidgetClassRec;
+
+
+static void fsb_class_init(void) {
+
+}
+
+static void fsb_class_part_init (WidgetClass wc) {
+    FSBClassRec *fsbClass = (FSBClassRec*)wc;
+    XmFormClassRec *formClass = (XmFormClassRec*)xmFormWidgetClass;
+    
+    fsbClass->constraint_class.initialize = formClass->constraint_class.initialize;
+    fsbClass->constraint_class.set_values = formClass->constraint_class.set_values;
+}
+
+
+#define STRDUP_RES(a) if(a) a = strdup(a)
+#define XMS_STRDUP_RES(a) if(a) a = XmStringCopy(a)
+
+static void fsb_init(Widget request, Widget neww, ArgList args, Cardinal *num_args) {
+    XnFileSelectionBox fsb = (XnFileSelectionBox)neww;
+    (xmFormClassRec.core_class.initialize)(request, neww, args, num_args);
+    
+    fsb->fsb.disable_set_values = 0;
+    
+    STRDUP_RES(fsb->fsb.homePath);
+    STRDUP_RES(fsb->fsb.selectedPath);
+    STRDUP_RES(fsb->fsb.currentPath);
+    STRDUP_RES(fsb->fsb.filterStr);
+    STRDUP_RES(fsb->fsb.labelListView);
+    STRDUP_RES(fsb->fsb.labelDetailView);
+    STRDUP_RES(fsb->fsb.labelOpenFileTitle);
+    STRDUP_RES(fsb->fsb.labelSaveFileTitle);
+    XMS_STRDUP_RES(fsb->fsb.labelDirUp);
+    XMS_STRDUP_RES(fsb->fsb.labelHome);
+    XMS_STRDUP_RES(fsb->fsb.labelNewFolder);
+    XMS_STRDUP_RES(fsb->fsb.labelFilterButton);
+    XMS_STRDUP_RES(fsb->fsb.labelShowHiddenFiles);
+    XMS_STRDUP_RES(fsb->fsb.labelDirectories);
+    XMS_STRDUP_RES(fsb->fsb.labelFiles);
+    XMS_STRDUP_RES(fsb->fsb.labelRename);
+    XMS_STRDUP_RES(fsb->fsb.labelDelete);
+    XMS_STRDUP_RES(fsb->fsb.labelOpen);
+    XMS_STRDUP_RES(fsb->fsb.labelSave);
+    XMS_STRDUP_RES(fsb->fsb.labelCancel);
+    XMS_STRDUP_RES(fsb->fsb.labelHelp);
+    XMS_STRDUP_RES(fsb->fsb.labelFileName);
+    XMS_STRDUP_RES(fsb->fsb.labelDirectoryName);
+    XMS_STRDUP_RES(fsb->fsb.labelNewFileName);
+    STRDUP_RES(fsb->fsb.labelDeleteFile);
+    STRDUP_RES(fsb->fsb.detailHeadings);
+    STRDUP_RES(fsb->fsb.dateFormatSameYear);
+    STRDUP_RES(fsb->fsb.dateFormatOtherYear);
+    STRDUP_RES(fsb->fsb.suffixBytes);
+    STRDUP_RES(fsb->fsb.suffixKB);
+    STRDUP_RES(fsb->fsb.suffixMB);
+    STRDUP_RES(fsb->fsb.suffixGB);
+    STRDUP_RES(fsb->fsb.suffixTB);
+    STRDUP_RES(fsb->fsb.errorTitle);
+    STRDUP_RES(fsb->fsb.errorIllegalChar);
+    STRDUP_RES(fsb->fsb.errorRename);
+    STRDUP_RES(fsb->fsb.errorFolder);
+    STRDUP_RES(fsb->fsb.errorDelete);
+    STRDUP_RES(fsb->fsb.errorOpenDir);
+    
+    CreateUI((XnFileSelectionBox)fsb);
+    
+    XtAddCallback(neww, XmNmapCallback, fsb_mapcb, NULL);
+}
+
+#define STR_FREE(a) if(a) free(a)
+#define XMSTR_FREE(a) if(a) XmStringFree(a)
+
+static void fsb_destroy(Widget widget) {
+    XnFileSelectionBox w = (XnFileSelectionBox)widget;
+    
+    // destroy all views
+    for(int i=0;i<w->fsb.numviews;i++) {
+        FSBView v = w->fsb.view[i];
+        v.destroy(widget, v.widget, v.userData);
+    }
+    
+    STR_FREE(w->fsb.homePath);
+    
+    // free filelists
+    filedialog_cleanup_filedata(w);
+    STR_FREE(w->fsb.currentPath);
+    STR_FREE(w->fsb.selectedPath);
+    STR_FREE(w->fsb.filterStr);
+    
+    PathBarDestroy(w->fsb.pathBar);
+    
+    // free strings
+    STR_FREE(w->fsb.labelListView);
+    STR_FREE(w->fsb.labelDetailView);
+    STR_FREE(w->fsb.labelOpenFileTitle);
+    STR_FREE(w->fsb.labelSaveFileTitle);
+    
+    XMSTR_FREE(w->fsb.labelDirUp);
+    XMSTR_FREE(w->fsb.labelHome);
+    XMSTR_FREE(w->fsb.labelNewFolder);
+    XMSTR_FREE(w->fsb.labelFilterButton);
+    XMSTR_FREE(w->fsb.labelShowHiddenFiles);
+    XMSTR_FREE(w->fsb.labelDirectories);
+    XMSTR_FREE(w->fsb.labelFiles);
+    XMSTR_FREE(w->fsb.labelRename);
+    XMSTR_FREE(w->fsb.labelDelete);
+    XMSTR_FREE(w->fsb.labelOpen);
+    XMSTR_FREE(w->fsb.labelSave);
+    XMSTR_FREE(w->fsb.labelCancel);
+    XMSTR_FREE(w->fsb.labelHelp);
+    XMSTR_FREE(w->fsb.labelFileName);
+    XMSTR_FREE(w->fsb.labelDirectoryName);
+    XMSTR_FREE(w->fsb.labelNewFileName);
+    STR_FREE(w->fsb.labelDeleteFile);
+    STR_FREE(w->fsb.detailHeadings);
+    
+    STR_FREE(w->fsb.dateFormatSameYear);
+    STR_FREE(w->fsb.dateFormatOtherYear);
+    STR_FREE(w->fsb.suffixBytes);
+    STR_FREE(w->fsb.suffixKB);
+    STR_FREE(w->fsb.suffixMB);
+    STR_FREE(w->fsb.suffixGB);
+    STR_FREE(w->fsb.suffixTB);
+    
+    STR_FREE(w->fsb.errorTitle);
+    STR_FREE(w->fsb.errorIllegalChar);
+    STR_FREE(w->fsb.errorRename);
+    STR_FREE(w->fsb.errorFolder);
+    STR_FREE(w->fsb.errorDelete);
+    STR_FREE(w->fsb.errorOpenDir);
+}
+
+static void fsb_resize(Widget widget) {
+    XnFileSelectionBox w = (XnFileSelectionBox)widget;
+    (xmFormClassRec.core_class.resize)(widget);
+    
+#ifdef FSB_ENABLE_DETAIL
+    if(w->fsb.view[w->fsb.selectedview].update == FileListDetailUpdate) {
+        FileListDetailAdjustColWidth(w->fsb.grid);
+    }
+#endif
+}
+
+static void fsb_realize(Widget widget, XtValueMask *mask, XSetWindowAttributes *attributes) {
+    XnFileSelectionBox w = (XnFileSelectionBox)widget;   
+    (xmFormClassRec.core_class.realize)(widget, mask, attributes);
+    
+    FSBView view = w->fsb.view[w->fsb.selectedview];
+    XmProcessTraversal(view.focus, XmTRAVERSE_CURRENT);
+    
+#ifdef FSB_ENABLE_DETAIL
+    if(w->fsb.view[w->fsb.selectedview].update == FileListDetailUpdate) {
+        FileListDetailAdjustColWidth(w->fsb.grid);
+    }
+#endif
+}
+
+static void FSBUpdateTitle(Widget w) {
+    if(XtParent(w)->core.widget_class == xmDialogShellWidgetClass) {
+        char *title = FSBDialogTitle(w);
+        XtVaSetValues(XtParent(w), XmNtitle, title, NULL);
+    }
+}
+
+static Boolean fsb_set_values(Widget old, Widget request, Widget neww, ArgList args, Cardinal *num_args) {
+    Boolean r = False;
+
+    XnFileSelectionBox o = (XnFileSelectionBox)old;
+    XnFileSelectionBox n = (XnFileSelectionBox)neww;
+    
+    int setOkBtnLabel = 0;
+    int ismanaged = XtIsManaged(neww);
+    Dimension width, height;
+    if(!ismanaged) {
+        width = n->core.width;
+        height = n->core.height;
+        if(n->fsb.pathBar) {
+            n->fsb.pathBar->disableResize = True;
+        }
+    }
+    
+    if(o->fsb.selectedview != n->fsb.selectedview) {
+        int selectedview = n->fsb.selectedview;
+        n->fsb.selectedview = o->fsb.selectedview;
+        SelectView(n, selectedview);
+    }
+    
+    char *updateDir = NULL;
+    int selectItem = 0;
+    if(o->fsb.selectedPath != n->fsb.selectedPath) {
+        STR_FREE(o->fsb.selectedPath);
+        STRDUP_RES(n->fsb.selectedPath);
+        XmTextFieldSetString(n->fsb.name, FileName(n->fsb.selectedPath));
+        // also update current directory
+        updateDir = ParentPath(n->fsb.selectedPath);
+        selectItem = 1;
+    }
+    if(o->fsb.currentPath != n->fsb.currentPath) {
+        STR_FREE(o->fsb.currentPath);
+        updateDir = strdup(n->fsb.currentPath);
+        n->fsb.currentPath = NULL;
+    }
+    
+    if(o->fsb.filterStr != n->fsb.filterStr) {
+        STR_FREE(o->fsb.filterStr);
+        STRDUP_RES(n->fsb.filterStr);
+        XmTextFieldSetString(XmDropDownGetText(n->fsb.filter), n->fsb.filterStr);
+        if(!updateDir) {
+            filedialog_update_dir(n, NULL);
+        }
+    }
+    
+    if(updateDir) {
+        filedialog_update_dir(n, updateDir);
+        PathBarSetPath(n->fsb.pathBar, updateDir);
+        free(updateDir);
+    }
+    
+    if(o->fsb.type != n->fsb.type) {
+        if(n->fsb.type == FILEDIALOG_OPEN) {
+            XtVaSetValues(n->fsb.workarea, XmNbottomWidget, n->fsb.separator, NULL);
+            XtUnmanageChild(n->fsb.name);
+            XtUnmanageChild(n->fsb.nameLabel);
+        } else {
+            XtManageChild(n->fsb.name);
+            XtManageChild(n->fsb.nameLabel);
+            XtVaSetValues(n->fsb.workarea, XmNbottomWidget, n->fsb.nameLabel, NULL);
+        }
+        FSBUpdateTitle(neww);
+        setOkBtnLabel = 1;
+    }
+      
+    // label strings
+    int updateTitle = 0;
+    if(o->fsb.labelListView != n->fsb.labelListView) {
+        STR_FREE(o->fsb.labelListView);
+        STRDUP_RES(n->fsb.labelListView);
+        XmString label = XmStringCreateLocalized(n->fsb.labelListView);
+        XtVaSetValues(n->fsb.viewSelectorList, XmNlabelString, label, NULL);
+        XmStringFree(label);
+    }
+    if(o->fsb.labelDetailView != n->fsb.labelDetailView) {
+        STR_FREE(o->fsb.labelDetailView);
+        STRDUP_RES(n->fsb.labelDetailView);
+        XmString label = XmStringCreateLocalized(n->fsb.labelDetailView);
+        XtVaSetValues(n->fsb.viewSelectorDetail, XmNlabelString, label, NULL);
+        if(n->fsb.detailToggleButton) {
+            XtVaSetValues(n->fsb.detailToggleButton, XmNlabelString, label, NULL);
+        }
+        XmStringFree(label);
+    }
+    if(o->fsb.labelOpenFileTitle != n->fsb.labelOpenFileTitle) {
+        STR_FREE(o->fsb.labelOpenFileTitle);
+        STRDUP_RES(n->fsb.labelOpenFileTitle);
+        updateTitle = 1;
+    }
+    if(o->fsb.labelSaveFileTitle != n->fsb.labelSaveFileTitle) {
+        STR_FREE(o->fsb.labelSaveFileTitle);
+        STRDUP_RES(n->fsb.labelSaveFileTitle);
+        updateTitle = 1;
+    }
+    
+    if(o->fsb.labelDirUp != n->fsb.labelDirUp) {
+        XMSTR_FREE(o->fsb.labelDirUp);
+        XMS_STRDUP_RES(n->fsb.labelDirUp);
+        XtVaSetValues(n->fsb.dirUp, XmNlabelString, n->fsb.labelDirUp, NULL);
+    }
+    if(o->fsb.labelHome != n->fsb.labelHome) {
+        XMSTR_FREE(o->fsb.labelHome);
+        XMS_STRDUP_RES(n->fsb.labelHome);
+        XtVaSetValues(n->fsb.dirUp, XmNlabelString, n->fsb.labelHome, NULL);
+    }
+    if(o->fsb.labelNewFolder != n->fsb.labelNewFolder) {
+        XMSTR_FREE(o->fsb.labelNewFolder);
+        XMS_STRDUP_RES(n->fsb.labelNewFolder);
+        XtVaSetValues(n->fsb.newFolder, XmNlabelString, n->fsb.labelNewFolder, NULL);
+    }
+    if(o->fsb.labelFilterButton != n->fsb.labelFilterButton) {
+        XMSTR_FREE(o->fsb.labelFilterButton);
+        XMS_STRDUP_RES(n->fsb.labelFilterButton);
+        XtVaSetValues(n->fsb.filterButton, XmNlabelString, n->fsb.labelFilterButton, NULL);
+    }
+    if(o->fsb.labelShowHiddenFiles != n->fsb.labelShowHiddenFiles) {
+        XMSTR_FREE(o->fsb.labelShowHiddenFiles);
+        XMS_STRDUP_RES(n->fsb.labelShowHiddenFiles);
+        XtVaSetValues(n->fsb.showHiddenButtonW, XmNlabelString, n->fsb.labelShowHiddenFiles, NULL);
+    }
+    if(o->fsb.labelDirectories != n->fsb.labelDirectories) {
+        XMSTR_FREE(o->fsb.labelDirectories);
+        XMS_STRDUP_RES(n->fsb.labelDirectories);
+        XtVaSetValues(n->fsb.lsDirLabel, XmNlabelString, n->fsb.labelDirectories, NULL);
+    }
+    if(o->fsb.labelFiles != n->fsb.labelFiles) {
+        XMSTR_FREE(o->fsb.labelFiles);
+        XMS_STRDUP_RES(n->fsb.labelFiles);
+        XtVaSetValues(n->fsb.lsFileLabel, XmNlabelString, n->fsb.labelFiles, NULL);
+    }
+    int recreateContextMenu = 0;
+    if(o->fsb.labelRename != n->fsb.labelRename) {
+        XMSTR_FREE(o->fsb.labelRename);
+        XMS_STRDUP_RES(n->fsb.labelRename);
+        recreateContextMenu = 1;
+    }
+    if(o->fsb.labelDelete != n->fsb.labelDelete) {
+        XMSTR_FREE(o->fsb.labelDelete);
+        XMS_STRDUP_RES(n->fsb.labelDelete);
+        recreateContextMenu = 1;
+    }
+
+    if(o->fsb.labelOpen != n->fsb.labelOpen) {
+        XMSTR_FREE(o->fsb.labelOpen);
+        XMS_STRDUP_RES(n->fsb.labelOpen);
+        setOkBtnLabel = 1;
+    }
+    if(o->fsb.labelSave != n->fsb.labelSave) {
+        XMSTR_FREE(o->fsb.labelSave);
+        XMS_STRDUP_RES(n->fsb.labelSave);
+        setOkBtnLabel = 1;
+    }
+    if(o->fsb.labelCancel != n->fsb.labelCancel) {
+        XMSTR_FREE(o->fsb.labelCancel);
+        XMS_STRDUP_RES(n->fsb.labelCancel);
+        XtVaSetValues(n->fsb.cancelBtn, XmNlabelString, n->fsb.labelCancel, NULL);
+    }
+    if(o->fsb.labelHelp != n->fsb.labelHelp) {
+        XMSTR_FREE(o->fsb.labelHelp);
+        XMS_STRDUP_RES(n->fsb.labelHelp);
+        XtVaSetValues(n->fsb.helpBtn, XmNlabelString, n->fsb.labelHelp, NULL);
+    }
+    if(o->fsb.labelFileName != n->fsb.labelFileName) {
+        XMSTR_FREE(o->fsb.labelFileName);
+        XMS_STRDUP_RES(n->fsb.labelFileName);
+        XtVaSetValues(n->fsb.nameLabel, XmNlabelString, n->fsb.labelFileName, NULL);
+    }
+    if(o->fsb.labelDirectoryName != n->fsb.labelDirectoryName) {
+        XMSTR_FREE(o->fsb.labelDirectoryName);
+        XMS_STRDUP_RES(n->fsb.labelDirectoryName);
+    }
+    if(o->fsb.labelNewFileName != n->fsb.labelNewFileName) {
+        XMSTR_FREE(o->fsb.labelNewFileName);
+        XMS_STRDUP_RES(n->fsb.labelNewFileName);
+    }
+    
+    if(o->fsb.labelDeleteFile != n->fsb.labelDeleteFile) {
+        STR_FREE(o->fsb.labelDeleteFile);
+        STRDUP_RES(n->fsb.labelDeleteFile);
+    }
+#ifdef FSB_ENABLE_DETAIL
+    if(o->fsb.detailHeadings != n->fsb.detailHeadings) {
+        STR_FREE(o->fsb.detailHeadings);
+        STRDUP_RES(n->fsb.detailHeadings);
+        XtVaSetValues(n->fsb.grid, XmNsimpleHeadings, n->fsb.detailHeadings, NULL);
+    }
+#endif
+    if(o->fsb.dateFormatSameYear != n->fsb.dateFormatSameYear) {
+        STR_FREE(o->fsb.dateFormatSameYear);
+        STRDUP_RES(n->fsb.dateFormatSameYear);
+    }
+    if(o->fsb.dateFormatOtherYear != n->fsb.dateFormatOtherYear) {
+        STR_FREE(o->fsb.dateFormatOtherYear);
+        STRDUP_RES(n->fsb.dateFormatOtherYear);
+    }
+    if(o->fsb.suffixBytes != n->fsb.suffixBytes) {
+        STR_FREE(o->fsb.suffixBytes);
+        STRDUP_RES(n->fsb.suffixBytes);
+    }
+    if(o->fsb.suffixMB != n->fsb.suffixMB) {
+        STR_FREE(o->fsb.suffixMB);
+        STRDUP_RES(n->fsb.suffixMB);
+    }
+    if(o->fsb.suffixGB != n->fsb.suffixGB) {
+        STR_FREE(o->fsb.suffixGB);
+        STRDUP_RES(n->fsb.suffixGB);
+    }
+    if(o->fsb.suffixTB != n->fsb.suffixTB) {
+        STR_FREE(o->fsb.suffixTB);
+        STRDUP_RES(n->fsb.suffixTB);
+    }
+    if(o->fsb.errorTitle != n->fsb.errorTitle) {
+        STR_FREE(o->fsb.errorTitle);
+        STRDUP_RES(n->fsb.errorTitle);
+    }
+    if(o->fsb.errorIllegalChar != n->fsb.errorIllegalChar) {
+        STR_FREE(o->fsb.errorIllegalChar);
+        STRDUP_RES(n->fsb.errorIllegalChar);
+    }
+    if(o->fsb.errorRename != n->fsb.errorRename) {
+        STR_FREE(o->fsb.errorRename);
+        STRDUP_RES(n->fsb.errorRename);
+    }
+    if(o->fsb.errorFolder != n->fsb.errorFolder) {
+        STR_FREE(o->fsb.errorFolder);
+        STRDUP_RES(n->fsb.errorFolder);
+    }
+    if(o->fsb.errorDelete != n->fsb.errorDelete) {
+        STR_FREE(o->fsb.errorDelete);
+        STRDUP_RES(n->fsb.errorDelete);
+    }
+    if(o->fsb.errorOpenDir != n->fsb.errorOpenDir) {
+        STR_FREE(o->fsb.errorOpenDir);
+        STRDUP_RES(n->fsb.errorOpenDir);
+    }
+    
+    if(updateTitle) {
+        FSBUpdateTitle(neww);
+    }
+    if(recreateContextMenu) {
+        XtDestroyWidget(n->fsb.listContextMenu);
+        XtDestroyWidget(n->fsb.gridContextMenu);
+        n->fsb.listContextMenu = CreateContextMenu(n, n->fsb.filelist, FileContextMenuCB);
+        n->fsb.gridContextMenu = CreateContextMenu(n, n->fsb.grid, FileContextMenuCB);
+    }
+    if(setOkBtnLabel) {
+        XtVaSetValues(n->fsb.okBtn, XmNlabelString, n->fsb.type == FILEDIALOG_OPEN ? n->fsb.labelOpen : n->fsb.labelSave, NULL);
+    }
+    
+    if(!ismanaged && !n->fsb.disable_set_values) {
+        n->fsb.disable_set_values = 1;
+        XtVaSetValues(neww, XmNwidth, width, XmNheight, height, NULL);
+        n->fsb.disable_set_values = 0;
+        
+        if(n->fsb.pathBar)
+            n->fsb.pathBar->disableResize = False;
+    }
+    
+    if(selectItem) {
+        if(ismanaged) {
+            FSBSelectItem(n, FileName(n->fsb.selectedPath));
+        }
+    }
+     
+    Boolean fr = (xmFormClassRec.core_class.set_values)(old, request, neww, args, num_args);
+    return fr ? fr : r;
+}
+
+static void fsb_insert_child(Widget child) {
+    XnFileSelectionBox p = (XnFileSelectionBox)XtParent(child);
+    (xmFormClassRec.composite_class.insert_child)(child);
+    
+    if(!p->fsb.gui_created) {
+        return;
+    }
+    
+    // custom child widget insert
+    XtVaSetValues(child,
+            XmNbottomAttachment, XmATTACH_WIDGET,
+            XmNbottomWidget, p->fsb.bottom_widget,
+            XmNbottomOffset, p->fsb.widgetSpacing,
+            XmNleftAttachment, XmATTACH_FORM,
+            XmNleftOffset, p->fsb.windowSpacing,
+            XmNrightAttachment, XmATTACH_FORM,
+            XmNrightAttachment, XmATTACH_FORM,
+            XmNrightOffset, p->fsb.windowSpacing,
+            NULL);
+    
+    
+    XtVaSetValues(p->fsb.listform,
+            XmNbottomWidget, child,
+            XmNbottomOffset, 0,
+            NULL);
+    
+    p->fsb.workarea = child;
+}
+
+Boolean fsb_acceptfocus(Widget widget, Time *time) {
+    return 0;
+}
+
+static void fsb_mapcb(Widget widget, XtPointer u, XtPointer cb) {
+    XnFileSelectionBox w = (XnFileSelectionBox)widget;
+    pathbar_resize(w->fsb.pathBar->widget, w->fsb.pathBar, NULL);
+    
+    if(w->fsb.type == FILEDIALOG_OPEN) {
+        FSBView view = w->fsb.view[w->fsb.selectedview];
+        XmProcessTraversal(view.focus, XmTRAVERSE_CURRENT);
+    } else {
+        XmProcessTraversal(w->fsb.name, XmTRAVERSE_CURRENT);
+    }
+    
+    
+    if(w->fsb.selectedPath) {
+        FSBSelectItem(w, FileName(w->fsb.selectedPath));
+    }
+}
+
+static void FocusInAP(Widget w, XEvent *event, String *args, Cardinal *nArgs) {
+    
+}
+
+static int apply_filter(XnFileSelectionBox w, const char *pattern, const char *string) {
+    if(!pattern) return 0;
+    
+    FSBFilterFunc func = w->fsb.filterFunc ? w->fsb.filterFunc : FSBGlobFilter;
+    return func(pattern, string);
+}
+
+static int FSBGlobFilter(const char *a, const char *b) {
+    return fnmatch(a, b, 0);
+}
+
+
+static void errCB(Widget w, XtPointer d, XtPointer cbs) {
+    XtDestroyWidget(w);
+}
+
+static void ErrDialog(XnFileSelectionBox w, const char *title, const char *errmsg) {
+    Arg args[16];
+    int n = 0;
+    
+    XmString titleStr = XmStringCreateLocalized((char*)title);
+    XmString msg = XmStringCreateLocalized((char*)errmsg);
+    
+    XtSetArg(args[n], XmNdialogTitle, titleStr); n++;
+    XtSetArg(args[n], XmNselectionLabelString, msg); n++;
+    XtSetArg(args[n], XmNokLabelString, w->fsb.labelOk); n++;
+    XtSetArg(args[n], XmNcancelLabelString, w->fsb.labelCancel); n++;
+    
+    Widget dialog = XmCreatePromptDialog ((Widget)w, "NewFolderPrompt", args, n);
+    
+    Widget help = XmSelectionBoxGetChild(dialog, XmDIALOG_HELP_BUTTON);
+    XtUnmanageChild(help);
+    Widget cancel = XmSelectionBoxGetChild(dialog, XmDIALOG_CANCEL_BUTTON);
+    XtUnmanageChild(cancel);
+    Widget text = XmSelectionBoxGetChild(dialog, XmDIALOG_TEXT);
+    XtUnmanageChild(text);
+    
+    XtAddCallback(dialog, XmNokCallback, errCB, NULL);
+    
+    XtManageChild(dialog);
+    
+    XmStringFree(titleStr);
+    XmStringFree(msg);
+}
+
+static void rename_file_cb(Widget w, const char *path, XmSelectionBoxCallbackStruct *cb) {
+    XnFileSelectionBox fsb = NULL;
+    XtVaGetValues(w, XmNuserData, &fsb, NULL);
+    
+    char *fileName = NULL;
+    XmStringGetLtoR(cb->value, XmSTRING_DEFAULT_CHARSET, &fileName);
+    
+    // make sure the new file name doesn't contain a path separator
+    if(strchr(fileName, '/')) {
+        ErrDialog(fsb, fsb->fsb.errorTitle, fsb->fsb.errorIllegalChar);
+        XtFree(fileName);
+        return;
+    }
+    
+    char *parentPath = ParentPath(path);
+    char *newPath = ConcatPath(parentPath, fileName);
+    
+    if(rename(path, newPath)) {
+        char errmsg[256];
+        snprintf(errmsg, 256, fsb->fsb.errorRename, strerror(errno));
+        ErrDialog(fsb, fsb->fsb.errorTitle, errmsg);
+    } else {
+        filedialog_update_dir(fsb, parentPath);
+    }
+    
+    free(parentPath);
+    free(newPath);
+    XtFree(fileName);
+    XtDestroyWidget(XtParent(w));
+}
+
+static void selectionbox_cancel(Widget w, XtPointer data, XtPointer d) {
+    XtDestroyWidget(XtParent(w));
+}
+
+static void FSBRename(XnFileSelectionBox fsb, const char *path) {
+    Arg args[16];
+    int n = 0;
+    Widget w = (Widget)fsb;
+    
+    char *name = FileName((char*)path);
+    
+    XmString filename = XmStringCreateLocalized(name);
+    XtSetArg(args[n], XmNselectionLabelString,fsb->fsb.labelNewFileName); n++;
+    XtSetArg(args[n], XmNtextString, filename); n++;
+    XtSetArg(args[n], XmNuserData, fsb); n++;
+    XtSetArg(args[n], XmNdialogTitle, fsb->fsb.labelRename); n++;
+    XtSetArg(args[n], XmNokLabelString, fsb->fsb.labelOk); n++;
+    XtSetArg(args[n], XmNcancelLabelString, fsb->fsb.labelCancel); n++;
+    Widget dialog = XmCreatePromptDialog (w, "RenameFilePrompt", args, n);
+    
+    Widget help = XmSelectionBoxGetChild(dialog, XmDIALOG_HELP_BUTTON);
+    XtUnmanageChild(help);
+    
+    XtAddCallback(dialog, XmNokCallback, (XtCallbackProc)rename_file_cb, (char*)path);
+    XtAddCallback(dialog, XmNcancelCallback, (XtCallbackProc)selectionbox_cancel, NULL);
+    
+    XmStringFree(filename);
+    XtManageChild(dialog);
+}
+
+static void delete_file_cb(Widget w, const char *path, XmSelectionBoxCallbackStruct *cb) {
+    XnFileSelectionBox fsb = NULL;
+    XtVaGetValues(w, XmNuserData, &fsb, NULL);
+    
+    if(unlink(path)) {
+        char errmsg[256];
+        snprintf(errmsg, 256, fsb->fsb.errorDelete, strerror(errno));
+        ErrDialog(fsb, fsb->fsb.errorTitle, errmsg);
+    } else {
+        char *parentPath = ParentPath(path);
+        filedialog_update_dir(fsb, parentPath);
+        free(parentPath);
+    }
+    
+    XtDestroyWidget(XtParent(w));
+}
+
+static void FSBDelete(XnFileSelectionBox fsb, const char *path) {
+    Arg args[16];
+    int n = 0;
+    Widget w = (Widget)fsb;
+    
+    char *name = FileName((char*)path);
+    size_t len = strlen(name);
+    size_t msglen = len + strlen(fsb->fsb.labelDeleteFile) + 4;
+    char *msg = malloc(msglen);
+    snprintf(msg, msglen, fsb->fsb.labelDeleteFile, name);
+    
+    XmString prompt = XmStringCreateLocalized(msg);
+    XtSetArg(args[n], XmNselectionLabelString, prompt); n++;
+    XtSetArg(args[n], XmNuserData, fsb); n++;
+    XtSetArg(args[n], XmNdialogTitle, fsb->fsb.labelDelete); n++;
+    XtSetArg(args[n], XmNokLabelString, fsb->fsb.labelOk); n++;
+    XtSetArg(args[n], XmNcancelLabelString, fsb->fsb.labelCancel); n++;
+    Widget dialog = XmCreatePromptDialog (w, "DeleteFilePrompt", args, n);
+    
+    Widget help = XmSelectionBoxGetChild(dialog, XmDIALOG_HELP_BUTTON);
+    XtUnmanageChild(help);
+    Widget text = XmSelectionBoxGetChild(dialog, XmDIALOG_TEXT);
+    XtUnmanageChild(text);
+    
+    XtAddCallback(dialog, XmNokCallback, (XtCallbackProc)delete_file_cb, (char*)path);
+    XtAddCallback(dialog, XmNcancelCallback, (XtCallbackProc)selectionbox_cancel, NULL);
+    
+    free(msg);
+    XmStringFree(prompt);
+    XtManageChild(dialog);
+}
+
+static void FSBSelectItem(XnFileSelectionBox fsb, const char *item) {
+    FSBView view = fsb->fsb.view[fsb->fsb.selectedview];
+    if(view.select) {
+        view.select((Widget)fsb, view.widget, item);
+    }
+}
+
+static char* set_selected_path(XnFileSelectionBox data, XmString item)
+{
+    char *name = NULL;
+    XmStringGetLtoR(item, XmFONTLIST_DEFAULT_TAG, &name);
+    if(!name) {
+        return NULL;
+    }
+    char *path = ConcatPath(data->fsb.currentPath, name);
+    XtFree(name);
+    
+    if(data->fsb.selectedPath) {
+        free(data->fsb.selectedPath);
+    }
+    data->fsb.selectedPath = path;
+    
+    return path;
+}
+
+// item0: rename
+// item1: delete
+static void FileContextMenuCB(Widget item, XtPointer index, XtPointer cd) {
+    intptr_t i = (intptr_t)index;
+    Widget parent = XtParent(item);
+    XnFileSelectionBox fsb = NULL;
+    XtVaGetValues(parent, XmNuserData, &fsb, NULL);
+    
+    const char *path = fsb->fsb.selectedPath;
+    if(path) {
+        if(i == 0) {
+            FSBRename(fsb, path);
+        } else if(i == 1) {
+            FSBDelete(fsb, path);
+        }
+    }
+}
+
+static Widget CreateContextMenu(XnFileSelectionBox fsb, Widget parent, XtCallbackProc callback) {
+    return XmVaCreateSimplePopupMenu(
+            parent, "popup", callback, XmNpopupEnabled, XmPOPUP_AUTOMATIC,
+            XmNuserData, fsb,
+            XmVaPUSHBUTTON, fsb->fsb.labelRename, 'R', NULL, NULL,
+            XmVaPUSHBUTTON, fsb->fsb.labelDelete, 'D', NULL, NULL,
+            NULL);
+}
+
+
+static void FileListUpdate(Widget fsb, Widget view, FileElm *dirs, int dircount, FileElm *files, int filecount, const char *filter, int maxnamelen, void *userData) {
+    XnFileSelectionBox data = userData;
+    FileListWidgetAdd(data, data->fsb.filelist, data->fsb.showHidden, filter, files, filecount);
+}
+
+static void FileListSelect(Widget fsb, Widget view, const char *item) {
+    XnFileSelectionBox w = (XnFileSelectionBox)fsb;
+    
+    int numItems = 0;
+    XmStringTable items = NULL;
+    XtVaGetValues(w->fsb.filelist, XmNitemCount, &numItems, XmNitems, &items, NULL);
+    
+    for(int i=0;i<numItems;i++) {
+        char *str = NULL;
+        XmStringGetLtoR(items[i], XmFONTLIST_DEFAULT_TAG, &str);
+        if(!strcmp(str, item)) {
+            XmListSelectPos(w->fsb.filelist, i+1, False);
+            break;
+        }
+        XtFree(str);
+    }
+}
+
+static void FileListCleanup(Widget fsb, Widget view, void *userData) {
+    XnFileSelectionBox data = userData;
+    XmListDeleteAllItems(data->fsb.filelist);
+}
+
+static void FileListDestroy(Widget fsb, Widget view, void *userData) {
+    // unused
+}
+
+static void FileListActivateCB(Widget w, XnFileSelectionBox data, XmListCallbackStruct *cb)
+{
+    char *path = set_selected_path(data, cb->item);
+    if(path) {
+        data->fsb.end = True;
+        data->fsb.status = FILEDIALOG_OK;
+        data->fsb.selIsDir = False;
+        FileSelectionCallback(data, data->fsb.okCallback, XmCR_OK, data->fsb.selectedPath);
+    }
+}
+
+static void FileListSelectCB(Widget w, XnFileSelectionBox data, XmListCallbackStruct *cb)
+{
+    if(data->fsb.type == FILEDIALOG_SAVE) {
+        char *name = NULL;
+        XmStringGetLtoR(cb->item, XmFONTLIST_DEFAULT_TAG, &name);
+        XmTextFieldSetString(data->fsb.name, name);
+        XtFree(name);
+    } else {
+        char *path = set_selected_path(data, cb->item);
+        if(path) {
+            data->fsb.selIsDir = False;
+        }
+    }
+}
+
+
+static void FileListWidgetAdd(XnFileSelectionBox fsb, Widget w, int showHidden, const char *filter, FileElm *ls, int count)
+{   
+    if(count > 0) {
+        XmStringTable items = calloc(count, sizeof(XmString));
+        int i = 0;
+        
+        for(int j=0;j<count;j++) {
+            FileElm *e = &ls[j];
+            
+            char *name = FileName(e->path);
+            if((!showHidden && name[0] == '.') || apply_filter(fsb, filter, name)) {
+                continue;
+            }
+            
+            items[i] = XmStringCreateLocalized(name);
+            i++;
+        }
+        XmListAddItems(w, items, i, 0);
+        for(i=0;i<count;i++) {
+            XmStringFree(items[i]);
+        }
+        free(items);
+    }
+}
+
+#ifdef FSB_ENABLE_DETAIL
+static void FileListDetailUpdate(Widget fsb, Widget view, FileElm *dirs, int dircount, FileElm *files, int filecount, const char *filter, int maxnamelen, void *userData) {
+    XnFileSelectionBox data = userData;
+    FileListDetailAdd(data, data->fsb.grid, data->fsb.showHidden, filter, files, filecount, maxnamelen);
+}
+#endif
+
+/*
+ * create file size string with kb/mb/gb/tb suffix
+ */
+static char* size_str(XnFileSelectionBox fsb, FileElm *f) {
+    char *str = malloc(16);
+    uint64_t size = f->size;
+    
+    if(f->isDirectory) {
+        str[0] = '\0';
+    } else if(size < 0x400) {
+        snprintf(str, 16, "%d %s", (int)size, fsb->fsb.suffixBytes);
+    } else if(size < 0x100000) {
+        float s = (float)size/0x400;
+        int diff = (s*100 - (int)s*100);
+        if(diff > 90) {
+            diff = 0;
+            s += 0.10f;
+        }
+        if(size < 0x2800 && diff != 0) {
+            // size < 10 KiB
+            snprintf(str, 16, "%.1f %s", s, fsb->fsb.suffixKB);
+        } else {
+            snprintf(str, 16, "%.0f %s", s, fsb->fsb.suffixKB);
+        }
+    } else if(size < 0x40000000) {
+        float s = (float)size/0x100000;
+        int diff = (s*100 - (int)s*100);
+        if(diff > 90) {
+            diff = 0;
+            s += 0.10f;
+        }
+        if(size < 0xa00000 && diff != 0) {
+            // size < 10 MiB
+            snprintf(str, 16, "%.1f %s", s, fsb->fsb.suffixMB);
+        } else {
+            snprintf(str, 16, "%.0f %s", s, fsb->fsb.suffixMB);
+        }
+    } else if(size < 0x1000000000ULL) {
+        float s = (float)size/0x40000000;
+        int diff = (s*100 - (int)s*100);
+        if(diff > 90) {
+            diff = 0;
+            s += 0.10f;
+        }
+        if(size < 0x280000000 && diff != 0) {
+            // size < 10 GiB
+            snprintf(str, 16, "%.1f %s", s, fsb->fsb.suffixGB);
+        } else {
+            snprintf(str, 16, "%.0f %s", s, fsb->fsb.suffixGB);
+        }
+    } else {
+        size /= 1024;
+        float s = (float)size/0x40000000;
+        int diff = (s*100 - (int)s*100);
+        if(diff > 90) {
+            diff = 0;
+            s += 0.10f;
+        }
+        if(size < 0x280000000 && diff != 0) {
+            // size < 10 TiB
+            snprintf(str, 16, "%.1f %s", s, fsb->fsb.suffixTB);
+        } else {
+            snprintf(str, 16, "%.0f %s", s, fsb->fsb.suffixTB);
+        }
+    }
+    return str;
+}
+
+static char* date_str(XnFileSelectionBox fsb, time_t tm) {
+    struct tm t;
+    struct tm n;
+    time_t now = time(NULL);
+    
+    localtime_r(&tm, &t);
+    localtime_r(&now, &n);
+    
+    char *str = malloc(24);
+    if(t.tm_year == n.tm_year) {
+        strftime(str, 24, fsb->fsb.dateFormatSameYear, &t);
+    } else {
+        strftime(str, 24, fsb->fsb.dateFormatOtherYear, &t);
+    }
+    return str;
+}
+
+#ifdef FSB_ENABLE_DETAIL
+static void FileListDetailAdjustColWidth(Widget grid) {
+    XmLGridColumn column0 = XmLGridGetColumn(grid, XmCONTENT, 0);
+    XmLGridColumn column1 = XmLGridGetColumn(grid, XmCONTENT, 1);
+    XmLGridColumn column2 = XmLGridGetColumn(grid, XmCONTENT, 2);
+    
+    Dimension col0Width = XmLGridColumnWidthInPixels(column0);
+    Dimension col1Width = XmLGridColumnWidthInPixels(column1);
+    Dimension col2Width = XmLGridColumnWidthInPixels(column2);
+    
+    Dimension totalWidth = col0Width + col1Width + col2Width;
+    
+    Dimension gridWidth = 0;
+    Dimension gridShadow = 0;
+    XtVaGetValues(grid, XmNwidth, &gridWidth, XmNshadowThickness, &gridShadow, NULL);
+    
+    Dimension widthDiff = gridWidth - totalWidth - gridShadow - gridShadow;
+    
+    if(gridWidth > totalWidth) {
+            XtVaSetValues(grid,
+            XmNcolumnRangeStart, 0,
+            XmNcolumnRangeEnd, 0,
+            XmNcolumnWidth, col0Width + widthDiff - XmLGridVSBWidth(grid) - 2,
+            XmNcolumnSizePolicy, XmCONSTANT,
+            NULL);
+    }
+}
+
+static void FileListDetailAdd(XnFileSelectionBox fsb, Widget grid, int showHidden, const char *filter, FileElm *ls, int count, int maxWidth)
+{
+    XmLGridAddRows(grid, XmCONTENT, 1, count);
+    
+    int row = 0;
+    for(int i=0;i<count;i++) {
+        FileElm *e = &ls[i];
+        
+        char *name = FileName(e->path);
+        if((!showHidden && name[0] == '.') || (!e->isDirectory && apply_filter(fsb, filter, name))) {
+            continue;
+        }
+        
+        // name
+        XmString str = XmStringCreateLocalized(name);
+        XtVaSetValues(grid,
+                XmNcolumn, 0, 
+                XmNrow, row,
+                XmNcellString, str, NULL);
+        XmStringFree(str);
+        // size
+        char *szbuf = size_str(fsb, e);
+        str = XmStringCreateLocalized(szbuf);
+        XtVaSetValues(grid,
+                XmNcolumn, 1, 
+                XmNrow, row,
+                XmNcellString, str, NULL);
+        free(szbuf);
+        XmStringFree(str);
+        // date
+        char *datebuf = date_str(fsb, e->lastModified);
+        str = XmStringCreateLocalized(datebuf);
+        XtVaSetValues(grid,
+                XmNcolumn, 2, 
+                XmNrow, row,
+                XmNcellString, str, NULL);
+        free(datebuf);
+        XmStringFree(str);
+        
+        XtVaSetValues(grid, XmNrow, row, XmNrowUserData, e, NULL);
+        row++;
+    }
+    
+    // remove unused rows
+    if(count > row) {
+        XmLGridDeleteRows(grid, XmCONTENT, row, count-row);
+    }
+    
+    if(maxWidth < 16) {
+        maxWidth = 16;
+    }
+    
+    XtVaSetValues(grid,
+        XmNcolumnRangeStart, 0,
+        XmNcolumnRangeEnd, 0,
+        XmNcolumnWidth, maxWidth,
+        XmNcellAlignment, XmALIGNMENT_LEFT,
+        XmNcolumnSizePolicy, XmVARIABLE,
+        NULL);
+    XtVaSetValues(grid,
+        XmNcolumnRangeStart, 1,
+        XmNcolumnRangeEnd, 1,
+        XmNcolumnWidth, 9,
+        XmNcellAlignment, XmALIGNMENT_LEFT,
+        XmNcolumnSizePolicy, XmVARIABLE,
+        NULL);
+    XtVaSetValues(grid,
+        XmNcolumnRangeStart, 2,
+        XmNcolumnRangeEnd, 2,
+        XmNcolumnWidth, 16,
+        XmNcellAlignment, XmALIGNMENT_RIGHT,
+        XmNcolumnSizePolicy, XmVARIABLE,
+        NULL);
+    
+    FileListDetailAdjustColWidth(grid);
+}
+
+static void FileListDetailSelect(Widget fsb, Widget view, const char *item) {
+    XnFileSelectionBox w = (XnFileSelectionBox)fsb;
+    
+    int numRows = 0;
+    XtVaGetValues(w->fsb.grid, XmNrows, &numRows, NULL);
+    
+    XmLGridColumn col = XmLGridGetColumn(w->fsb.grid, XmCONTENT, 0);
+    for(int i=0;i<numRows;i++) {
+        XmLGridRow row = XmLGridGetRow(w->fsb.grid, XmCONTENT, i);
+        FileElm *elm = NULL;
+        XtVaGetValues(w->fsb.grid, XmNrowPtr, row, XmNcolumnPtr, col, XmNrowUserData, &elm, NULL);
+        if(elm) {
+            if(!strcmp(item, FileName(elm->path))) {
+                XmLGridSelectRow(w->fsb.grid, i, False);
+                XmLGridFocusAndShowRow(w->fsb.grid, i+1);
+                break;
+            }
+        }
+    }
+}
+
+static void FileListDetailCleanup(Widget fsb, Widget view, void *userData) {
+    XnFileSelectionBox data = userData;
+    // cleanup grid
+    Cardinal rows = 0;
+    XtVaGetValues(data->fsb.grid, XmNrows, &rows, NULL);
+    XmLGridDeleteRows(data->fsb.grid, XmCONTENT, 0, rows);
+}
+
+static void FileListDetailDestroy(Widget fsb, Widget view, void *userData) {
+    // unused
+}
+#endif
+
+static void create_folder(Widget w, XnFileSelectionBox data, XmSelectionBoxCallbackStruct *cbs) {
+    char *fileName = NULL;
+    XmStringGetLtoR(cbs->value, XmSTRING_DEFAULT_CHARSET, &fileName);
+    
+    char *newFolder = ConcatPath(data->fsb.currentPath ? data->fsb.currentPath : "", fileName);
+    if(mkdir(newFolder, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)) {
+        char errmsg[256];
+        snprintf(errmsg, 256, data->fsb.errorFolder, strerror(errno));
+        ErrDialog(data, data->fsb.errorTitle, errmsg);
+    } else {
+        char *p = strdup(data->fsb.currentPath);
+        filedialog_update_dir(data, p);
+        free(p);
+    }
+    free(newFolder);
+    
+    XtDestroyWidget(XtParent(w));
+}
+
+static void new_folder_cancel(Widget w, XnFileSelectionBox data, XtPointer d) {
+    XtDestroyWidget(XtParent(w));
+}
+
+static void FSBNewFolder(Widget w, XnFileSelectionBox data, XtPointer u)
+{
+    Arg args[16];
+    int n = 0;
+    
+    XtSetArg(args[n], XmNdialogTitle, data->fsb.labelNewFolder); n++;
+    XtSetArg (args[n], XmNselectionLabelString, data->fsb.labelDirectoryName); n++;
+    XtSetArg(args[n], XmNokLabelString, data->fsb.labelOk); n++;
+    XtSetArg(args[n], XmNcancelLabelString, data->fsb.labelCancel); n++;
+    Widget dialog = XmCreatePromptDialog (w, "NewFolderPrompt", args, n);
+    
+    Widget help = XmSelectionBoxGetChild(dialog, XmDIALOG_HELP_BUTTON);
+    XtUnmanageChild(help);
+    
+    XtAddCallback(dialog, XmNokCallback, (XtCallbackProc)create_folder, data);
+    XtAddCallback(dialog, XmNcancelCallback, (XtCallbackProc)new_folder_cancel, data);
+    
+    XtManageChild(dialog);
+    
+}
+
+static void FSBHome(Widget w, XnFileSelectionBox data, XtPointer u) {
+    const char *homePath = data->fsb.homePath ? data->fsb.homePath : GetHomeDir();
+    filedialog_update_dir(data, homePath);
+    PathBarSetPath(data->fsb.pathBar, homePath);
+}
+
+
+/*
+ * file_cmp_field
+ * 0: compare path
+ * 1: compare size
+ * 2: compare mtime
+ */
+static int file_cmp_field = 0;
+
+/*
+ * 1 or -1
+ */
+static int file_cmp_order = 1;
+
+static int filecmp(const void *f1, const void *f2)
+{
+    const FileElm *file1 = f1;
+    const FileElm *file2 = f2;
+    if(file1->isDirectory != file2->isDirectory) {
+        return file1->isDirectory < file2->isDirectory;
+    }
+    
+    int cmp_field = file_cmp_field;
+    int cmp_order = file_cmp_order;
+    if(file1->isDirectory) {
+        cmp_field = 0;
+        cmp_order = 1;
+    }
+    
+    int ret = 0;
+    switch(cmp_field) {
+        case 0: {
+            ret = strcmp(FileName(file1->path), FileName(file2->path));
+            break;
+        }
+        case 1: {
+            if(file1->size < file2->size) {
+                ret = -1;
+            } else if(file1->size == file2->size) {
+                ret = 0;
+            } else {
+                ret = 1;
+            }
+            break;
+        }
+        case 2: {
+            if(file1->lastModified < file2->lastModified) {
+                ret = -1;
+            } else if(file1->lastModified == file2->lastModified) {
+                ret = 0;
+            } else {
+                ret = 1;
+            }
+            break;
+        }
+    }
+    
+    return ret * cmp_order;
+}
+
+
+static void free_files(FileElm *ls, int count)
+{
+    for(int i=0;i<count;i++) {
+        if(ls[i].path) {
+            free(ls[i].path);
+        }
+    }
+    free(ls);
+}
+
+static void filedialog_cleanup_filedata(XnFileSelectionBox data)
+{
+    free_files(data->fsb.dirs, data->fsb.dircount);
+    free_files(data->fsb.files, data->fsb.filecount);
+    data->fsb.dirs = NULL;
+    data->fsb.files = NULL;
+    data->fsb.dircount = 0;
+    data->fsb.filecount = 0;
+    data->fsb.maxnamelen = 0;
+}
+
+#define FILE_ARRAY_SIZE 1024
+
+static void file_array_add(FileElm **files, int *alloc, int *count, FileElm elm) {
+    int c = *count;
+    int a = *alloc;
+    if(c >= a) {
+        a *= 2;
+        FileElm *newarray = realloc(*files, sizeof(FileElm) * a);
+        
+        *files = newarray;
+        *alloc = a;
+    }
+    
+    (*files)[c] = elm;
+    c++;
+    *count = c;
+}
+
+static int filedialog_update_dir(XnFileSelectionBox data, const char *path)
+{
+    DIR *dir = NULL;
+    if(path) {
+        // try to check first, if we can open the path
+        dir = opendir(path);
+        if(!dir) {
+            char errmsg[256];
+            snprintf(errmsg, 256, data->fsb.errorOpenDir, strerror(errno));
+            
+            ErrDialog(data, data->fsb.errorTitle, errmsg);
+            return 1;
+        }
+    }
+    
+    FSBView view = data->fsb.view[data->fsb.selectedview];
+    view.cleanup((Widget)data, view.widget, view.userData);
+      
+    if(view.useDirList) {
+        XmListDeleteAllItems(data->fsb.dirlist);
+    }
+    
+    /* read dir and insert items */
+    if(path) {
+        int dircount = 0; 
+        int filecount = 0;
+        size_t maxNameLen = 0;
+        
+        FileElm *dirs = calloc(sizeof(FileElm), FILE_ARRAY_SIZE);
+        FileElm *files = calloc(sizeof(FileElm), FILE_ARRAY_SIZE);
+        int dirs_alloc = FILE_ARRAY_SIZE;
+        int files_alloc = FILE_ARRAY_SIZE;
+        
+        filedialog_cleanup_filedata(data);
+    
+        /* dir reading complete - set the path textfield */  
+        XmTextFieldSetString(data->fsb.path, (char*)path);
+        char *oldPath = data->fsb.currentPath;
+        data->fsb.currentPath = strdup(path);
+        if(oldPath) {
+            free(oldPath);
+        }
+        path = data->fsb.currentPath;
+
+        struct dirent *ent;
+        while((ent = readdir(dir)) != NULL) {
+            if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) {
+                continue;
+            }
+
+            char *entpath = ConcatPath(path, ent->d_name);
+
+            struct stat s;
+            if(stat(entpath, &s)) {
+                free(entpath);
+                continue;
+            }
+
+            FileElm new_entry;
+            new_entry.path = entpath;
+            new_entry.isDirectory = S_ISDIR(s.st_mode);
+            new_entry.size = (uint64_t)s.st_size;
+            new_entry.lastModified = s.st_mtime;
+
+            size_t nameLen = strlen(ent->d_name);
+            if(nameLen > maxNameLen) {
+                maxNameLen = nameLen;
+            }
+
+            if(new_entry.isDirectory) {
+                file_array_add(&dirs, &dirs_alloc, &dircount, new_entry);
+            } else {
+                file_array_add(&files, &files_alloc, &filecount, new_entry);
+            }
+        }
+        closedir(dir);
+        
+        data->fsb.dirs = dirs;
+        data->fsb.files = files;
+        data->fsb.dircount = dircount;
+        data->fsb.filecount = filecount;
+        data->fsb.maxnamelen = maxNameLen;
+        
+        // sort file arrays
+        qsort(dirs, dircount, sizeof(FileElm), filecmp);
+        qsort(files, filecount, sizeof(FileElm), filecmp);
+    }
+    
+    Widget filterTF = XmDropDownGetText(data->fsb.filter);
+    char *filter = XmTextFieldGetString(filterTF);
+    char *filterStr = filter;
+    if(!filter || strlen(filter) == 0) {
+        filterStr = "*";
+    }
+    
+    if(view.useDirList) {
+        FileListWidgetAdd(data, data->fsb.dirlist, data->fsb.showHidden, NULL, data->fsb.dirs, data->fsb.dircount);
+        view.update(
+                (Widget)data,
+                view.widget,
+                NULL,
+                0,
+                data->fsb.files,
+                data->fsb.filecount,
+                filterStr,
+                data->fsb.maxnamelen,
+                view.userData);
+    } else {
+        view.update(
+                (Widget)data,
+                view.widget,
+                data->fsb.dirs,
+                data->fsb.dircount,
+                data->fsb.files,
+                data->fsb.filecount,
+                filterStr,
+                data->fsb.maxnamelen,
+                view.userData);
+    }
+    
+    if(filter) {
+        XtFree(filter);
+    }
+    
+    return 0;
+}
+
+
+static void dirlist_activate(Widget w, XnFileSelectionBox data, XmListCallbackStruct *cb)
+{
+    char *path = set_selected_path(data, cb->item);
+    if(path) {
+        if(!filedialog_update_dir(data, path)) {
+            PathBarSetPath(data->fsb.pathBar, path);
+            data->fsb.selIsDir = TRUE;
+        } 
+    }    
+}
+
+static void dirlist_select(Widget w, XnFileSelectionBox data, XmListCallbackStruct *cb)
+{
+    char *path = set_selected_path(data, cb->item);
+    if(path) {
+        data->fsb.selIsDir = TRUE;
+    }
+}
+
+static void filedialog_enable_detailview(Widget w, XnFileSelectionBox data, XmToggleButtonCallbackStruct *tb) {
+    SelectView(data, tb->set); // 0: list, 1: detail
+}
+
+
+static void filedialog_setshowhidden(
+        Widget w,
+        XnFileSelectionBox data,
+        XmToggleButtonCallbackStruct *tb)
+{
+    data->fsb.showHidden = tb->set;
+    filedialog_update_dir(data, NULL);
+}
+
+static void filedialog_filter(Widget w, XnFileSelectionBox data, XtPointer c)
+{
+    filedialog_update_dir(data, NULL);
+}
+
+static void filedialog_update_filter(Widget w, XnFileSelectionBox data, XtPointer c)
+{
+    filedialog_update_dir(data, NULL);
+    
+}
+
+static void filedialog_goup(Widget w, XnFileSelectionBox data, XtPointer d)
+{
+    char *newPath = ParentPath(data->fsb.currentPath);
+    filedialog_update_dir(data, newPath);
+    PathBarSetPath(data->fsb.pathBar, newPath);
+    free(newPath);
+}
+
+static void filedialog_ok(Widget w, XnFileSelectionBox data, XtPointer d)
+{
+    if(data->fsb.type == FILEDIALOG_SAVE) {
+        char *newName = XmTextFieldGetString(data->fsb.name);
+        if(newName) {
+            if(strchr(newName, '/')) {
+                ErrDialog(data, data->fsb.errorTitle, data->fsb.errorIllegalChar);
+                XtFree(newName);
+                return;
+            }
+            
+            if(strlen(newName) > 0) {
+                char *selPath = ConcatPath(data->fsb.currentPath, newName);
+                if(data->fsb.selectedPath) free(data->fsb.selectedPath);
+                data->fsb.selectedPath = selPath;
+            }
+            XtFree(newName);
+            
+            data->fsb.selIsDir = False;
+        }
+    }
+    
+    if(data->fsb.selectedPath) {
+        if(!data->fsb.selIsDir) {
+            data->fsb.status = FILEDIALOG_OK;
+            data->fsb.end = True;
+            FileSelectionCallback(data, data->fsb.okCallback, XmCR_OK, data->fsb.selectedPath);
+        }
+    }
+}
+
+static void filedialog_cancel(Widget w, XnFileSelectionBox data, XtPointer d)
+{
+    data->fsb.end = 1;
+    data->fsb.status = FILEDIALOG_CANCEL;
+    FileSelectionCallback(data, data->fsb.cancelCallback, XmCR_CANCEL, data->fsb.currentPath);
+}
+
+static void filedialog_help(Widget w, XnFileSelectionBox data, XtPointer d)
+{
+    FileSelectionCallback(data, data->manager.help_callback, XmCR_HELP, data->fsb.currentPath);
+}
+
+static void FileSelectionCallback(XnFileSelectionBox fsb, XtCallbackList cb, int reason, const char *value) {
+    XmFileSelectionBoxCallbackStruct cbs;
+    memset(&cbs, 0, sizeof(XmFileSelectionBoxCallbackStruct));
+    
+    char *dir = fsb->fsb.currentPath;
+    size_t dirlen = dir ? strlen(dir) : 0;
+    if(dir && dirlen > 0) {
+        char *dir2 = NULL;
+        if(dir[dirlen-1] != '/') {
+            // add a trailing / to the dir string 
+            dir2 = malloc(dirlen+2);
+            memcpy(dir2, dir, dirlen);
+            dir2[dirlen] = '/';
+            dir2[dirlen+1] = '\0';
+            dirlen++;
+            dir = dir2;
+        }
+        cbs.dir = XmStringCreateLocalized(dir);
+        cbs.dir_length = dirlen;
+        if(dir2) {
+            free(dir2);
+        }
+    } else {
+        cbs.dir = XmStringCreateLocalized("");
+        cbs.dir_length = 0;
+    }
+    cbs.reason = reason;
+    
+    cbs.value = XmStringCreateLocalized((char*)value);
+    cbs.length = strlen(value);
+    
+    XtCallCallbackList((Widget)fsb, cb, (XtPointer)&cbs);
+    
+    XmStringFree(cbs.dir);
+    XmStringFree(cbs.value);
+}
+
+static void CreateUI(XnFileSelectionBox w) { 
+    Arg args[32];
+    int n = 0;
+    XmString str;
+       
+    int widget_spacing = w->fsb.widgetSpacing;
+    int window_spacing = w->fsb.windowSpacing;
+    
+    Widget form = (Widget)w;
+    int type = w->fsb.type;
+    
+    XtVaSetValues((Widget)w, XmNautoUnmanage, False, NULL);
+     
+    /* upper part of the gui */
+    
+    n = 0;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelDirUp); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNresizable, True); n++;
+    XtSetArg(args[n], XmNarrowDirection, XmARROW_UP); n++;
+    w->fsb.dirUp = XmCreatePushButton(form, "DirUp", args, n);
+    XtManageChild(w->fsb.dirUp);
+    XtAddCallback(w->fsb.dirUp, XmNactivateCallback,
+                 (XtCallbackProc)filedialog_goup, w);
+    
+    // View Option Menu
+    n = 0;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNshadowThickness, 0); n++;
+    Widget viewframe = XmCreateForm(form, "vframe", args, n);
+    XtManageChild(viewframe);
+
+    w->fsb.viewMenu = XmCreatePulldownMenu(viewframe, "menu", NULL, 0);
+    
+    Widget view;
+    if(w->fsb.showViewMenu) {
+        n = 0;
+        XtSetArg(args[n], XmNsubMenuId, w->fsb.viewMenu); n++;
+        XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNmarginHeight, 0); n++;
+        XtSetArg(args[n], XmNmarginWidth, 0); n++;
+        view = XmCreateOptionMenu(viewframe, "option_menu", args, n);
+        XtManageChild(view);
+        w->fsb.viewOption = view;
+        w->fsb.detailToggleButton = NULL;
+    } else {
+        n = 0;
+        str = XmStringCreateLocalized(w->fsb.labelDetailView);
+        XtSetArg(args[n], XmNlabelString, str); n++;
+        XtSetArg(args[n], XmNfillOnSelect, True); n++;
+        XtSetArg(args[n], XmNindicatorOn, False); n++;
+        XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+        if(w->fsb.selectedview == 1) {
+            XtSetArg(args[n], XmNset, 1); n++;
+        }
+        w->fsb.detailToggleButton = XmCreateToggleButton(viewframe, "ToggleDetailView", args, n);
+        XtManageChild(w->fsb.detailToggleButton);
+        view = w->fsb.detailToggleButton;
+        XmStringFree(str);
+        
+        XtAddCallback(
+            w->fsb.detailToggleButton,
+            XmNvalueChangedCallback,
+            (XtCallbackProc)filedialog_enable_detailview,
+            w);
+        
+        w->fsb.viewOption = NULL;
+    }    
+
+    n = 0;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightWidget, view); n++;
+    XtSetArg(args[n], XmNmarginHeight, 0); n++;
+    XtSetArg(args[n], XmNorientation, XmHORIZONTAL); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelNewFolder); n++;
+    w->fsb.newFolder = XmCreatePushButton(viewframe, "NewFolder", args, n);
+    XtManageChild(w->fsb.newFolder);
+    XtAddCallback(
+            w->fsb.newFolder,
+            XmNactivateCallback,
+            (XtCallbackProc)FSBNewFolder,
+            w);
+    
+
+    n = 0;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelHome); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNrightWidget, w->fsb.newFolder); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    w->fsb.home = XmCreatePushButton(viewframe, "Home", args, n);
+    XtManageChild(w->fsb.home);
+    XtAddCallback(
+            w->fsb.home,
+            XmNactivateCallback,
+            (XtCallbackProc)FSBHome,
+            w);
+    
+    // match visual appearance of detailToggleButton with the other buttons
+    if(w->fsb.detailToggleButton) {
+        Dimension highlight, shadow;
+        XtVaGetValues(w->fsb.newFolder, XmNshadowThickness, &shadow, XmNhighlightThickness, &highlight, NULL);
+        XtVaSetValues(w->fsb.detailToggleButton, XmNshadowThickness, shadow, XmNhighlightThickness, highlight, NULL);
+    }
+    
+    // pathbar
+    n = 0;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNleftWidget, w->fsb.dirUp); n++;
+    XtSetArg(args[n], XmNleftOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNrightWidget, viewframe); n++;
+    XtSetArg(args[n], XmNrightOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNshadowType, XmSHADOW_IN); n++;
+    Widget pathBarFrame = XmCreateFrame(form, "pathbar_frame", args, n);
+    XtManageChild(pathBarFrame);
+    w->fsb.pathBar = CreatePathBar(pathBarFrame, args, 0);
+    w->fsb.pathBar->getpathelm = ui_default_pathelm_func;
+    w->fsb.pathBar->updateDir = (updatedir_callback)filedialog_update_dir;
+    w->fsb.pathBar->updateDirData = w;
+    XtManageChild(w->fsb.pathBar->widget);
+    w->fsb.path = XmCreateTextField(form, "textfield", args, 0);
+    
+    XtVaSetValues(w->fsb.dirUp, XmNbottomAttachment, XmATTACH_OPPOSITE_WIDGET, XmNbottomWidget, pathBarFrame, NULL);
+    if(!w->fsb.showViewMenu) {
+        XtVaSetValues(viewframe, XmNbottomAttachment, XmATTACH_OPPOSITE_WIDGET, XmNbottomWidget, pathBarFrame, NULL);
+    }
+    
+    n = 0;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNtopWidget, pathBarFrame); n++;
+    XtSetArg(args[n], XmNtopOffset, 2*widget_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightOffset, window_spacing); n++;
+    w->fsb.filterForm = XmCreateForm(form, "filterform", args, n);
+    XtManageChild(w->fsb.filterForm);
+    
+    n = 0;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelDirectories); n++;
+    w->fsb.lsDirLabel = XmCreateLabel(w->fsb.filterForm, "labelDirs", args, n);
+    XtManageChild(w->fsb.lsDirLabel);
+    
+    n = 0;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_POSITION); n++;
+    XtSetArg(args[n], XmNleftPosition, 35); n++;
+    XtSetArg(args[n], XmNleftOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelFiles); n++;
+    w->fsb.lsFileLabel = XmCreateLabel(w->fsb.filterForm, "labelFiles", args, n);
+    XtManageChild(w->fsb.lsFileLabel);
+      
+    if(w->fsb.showHiddenButton) {
+        n = 0;
+        XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNlabelString, w->fsb.labelShowHiddenFiles); n++;
+        XtSetArg(args[n], XmNset, w->fsb.showHidden); n++;
+        w->fsb.showHiddenButtonW = XmCreateToggleButton(w->fsb.filterForm, "showHidden", args, n);
+        XtManageChild(w->fsb.showHiddenButtonW);
+        XtAddCallback(w->fsb.showHiddenButtonW, XmNvalueChangedCallback,
+                     (XtCallbackProc)filedialog_setshowhidden, w);
+    }
+    
+    n = 0;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelFilterButton); n++;
+    if(w->fsb.showHiddenButton) {
+        XtSetArg(args[n], XmNrightAttachment, XmATTACH_WIDGET); n++;
+        XtSetArg(args[n], XmNrightWidget, w->fsb.showHiddenButtonW); n++;
+    } else {
+        XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    }
+    w->fsb.filterButton = XmCreatePushButton(w->fsb.filterForm, "filedialog_filter", args, n);
+    XtManageChild(w->fsb.filterButton);
+    XtAddCallback(w->fsb.filterButton, XmNactivateCallback,
+                 (XtCallbackProc)filedialog_filter, w);
+    
+    n = 0;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNleftWidget, w->fsb.lsFileLabel); n++;
+    XtSetArg(args[n], XmNleftOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNrightWidget, w->fsb.filterButton); n++;
+    XtSetArg(args[n], XmNrightOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNshowLabel, False); n++;
+    XtSetArg(args[n], XmNuseTextField, True); n++;
+    XtSetArg(args[n], XmNverify, False); n++;
+    w->fsb.filter = XmCreateDropDown(w->fsb.filterForm, "filedialog_filter_textfield", args, n);
+    XtManageChild(w->fsb.filter);
+    XmTextFieldSetString(XmDropDownGetText(w->fsb.filter), w->fsb.filterStr);
+    XtAddCallback(XmDropDownGetText(w->fsb.filter), XmNactivateCallback,
+                 (XtCallbackProc)filedialog_filter, w);
+    XtAddCallback(w->fsb.filter, XmNupdateTextCallback,
+                 (XtCallbackProc)filedialog_update_filter, w);
+    Widget filterList = XmDropDownGetList(w->fsb.filter);
+    str = XmStringCreateSimple("*");
+    XmListAddItem(filterList, str, 0);
+    XmStringFree(str);
+        
+    /* lower part */
+    n = 0;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNtopOffset, widget_spacing * 2); n++;
+    Widget buttons = XmCreateForm(form, "buttons", args, n);
+    XtManageChild(buttons);
+    
+    n = 0;
+    str = type == FILEDIALOG_OPEN ? w->fsb.labelOpen : w->fsb.labelSave;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNlabelString, str); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_POSITION); n++;
+    XtSetArg(args[n], XmNrightPosition, 14); n++;
+    w->fsb.okBtn = XmCreatePushButton(buttons, "filedialog_open", args, n);
+    XtManageChild(w->fsb.okBtn);
+    XmStringFree(str);
+    XtAddCallback(w->fsb.okBtn, XmNactivateCallback,
+                 (XtCallbackProc)filedialog_ok, w);
+    
+    n = 0;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelHelp); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_POSITION); n++;
+    XtSetArg(args[n], XmNleftPosition, 86); n++;
+    w->fsb.helpBtn = XmCreatePushButton(buttons, "filedialog_help", args, n);
+    XtManageChild(w->fsb.helpBtn);
+    XtAddCallback(w->fsb.helpBtn, XmNactivateCallback,
+                 (XtCallbackProc)filedialog_help, w);
+    
+    n = 0;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_POSITION); n++;
+    XtSetArg(args[n], XmNleftPosition, 43); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_POSITION); n++;
+    XtSetArg(args[n], XmNrightPosition, 57); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelCancel); n++;
+    w->fsb.cancelBtn = XmCreatePushButton(buttons, "filedialog_cancel", args, n);
+    XtManageChild(w->fsb.cancelBtn);
+    XtAddCallback(w->fsb.cancelBtn, XmNactivateCallback,
+                 (XtCallbackProc)filedialog_cancel, w);
+    
+    n = 0;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNbottomWidget, buttons); n++;
+    XtSetArg(args[n], XmNbottomOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftOffset, 1); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightOffset, 1); n++;
+    w->fsb.separator = XmCreateSeparator(form, "ofd_separator", args, n);
+    XtManageChild(w->fsb.separator);
+    
+    Widget bottomWidget = w->fsb.separator;
+    
+    n = 0;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNbottomWidget, w->fsb.separator); n++;
+    XtSetArg(args[n], XmNbottomOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightOffset, window_spacing); n++;
+    w->fsb.name = XmCreateTextField(form, "textfield", args, n);
+    XtAddCallback(w->fsb.name, XmNactivateCallback,
+             (XtCallbackProc)filedialog_ok, w);
+
+    n = 0;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNbottomWidget, w->fsb.name); n++;
+    XtSetArg(args[n], XmNbottomOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelFileName); n++;
+    w->fsb.nameLabel = XmCreateLabel(form, "label", args, n);
+        
+    if(type == FILEDIALOG_SAVE) {
+        bottomWidget = w->fsb.nameLabel;
+        XtManageChild(w->fsb.name);
+        XtManageChild(w->fsb.nameLabel);
+    }
+    w->fsb.bottom_widget = bottomWidget;
+    
+    
+    // middle 
+    // form for dir/file lists
+    n = 0;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNtopWidget, w->fsb.filterForm); n++;
+    XtSetArg(args[n], XmNtopOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNbottomWidget, bottomWidget); n++;
+    XtSetArg(args[n], XmNleftOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNrightOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNbottomOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNwidth, 580); n++;
+    XtSetArg(args[n], XmNheight, 400); n++;
+    w->fsb.listform = XmCreateForm(form, "fds_listform", args, n); 
+    
+    // dir/file lists
+    
+    n = 0;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopWidget, w->fsb.lsDirLabel); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_POSITION); n++;
+    XtSetArg(args[n], XmNrightPosition, 35); n++;
+    w->fsb.dirlist = XmCreateScrolledList(w->fsb.listform, "dirlist", args, n);
+    Dimension width, height;
+    XtMakeResizeRequest(w->fsb.dirlist, 150, 200, &width, &height);
+    XtManageChild(w->fsb.dirlist);
+    
+    XtAddCallback(
+            w->fsb.dirlist,
+            XmNdefaultActionCallback,
+            (XtCallbackProc)dirlist_activate,
+            w); 
+    XtAddCallback(
+            w->fsb.dirlist,
+            XmNbrowseSelectionCallback,
+            (XtCallbackProc)dirlist_select,
+            w);
+    
+    // FileList
+    XnFileSelectionBoxAddView(
+            (Widget)w,
+            w->fsb.labelListView,
+            CreateListView,
+            FileListUpdate,
+            FileListSelect,
+            FileListCleanup,
+            FileListDestroy,
+            True,
+            w);
+    
+    // Detail FileList
+#ifdef FSB_ENABLE_DETAIL
+    XnFileSelectionBoxAddView(
+            (Widget)w,
+            w->fsb.labelDetailView,
+            CreateDetailView,
+            FileListDetailUpdate,
+            FileListDetailSelect,
+            FileListDetailCleanup,
+            FileListDetailDestroy,
+            True,
+            w);
+#endif
+       
+    /*
+    n = 0;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNleftWidget, w->fsb.dirlist); n++;
+    XtSetArg(args[n], XmNleftOffset, widget_spacing); n++;
+    //XtSetArg(args[n], XmNbottomAttachment, XmATTACH_WIDGET); n++;
+    //XtSetArg(args[n], XmNbottomWidget, w->fsb.filelist); n++;
+    XtSetArg(args[n], XmNbottomOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelFiles); n++;
+    w->fsb.lsFileLabel = XmCreateLabel(w->fsb.listform, "label", args, n);
+    XtManageChild(w->fsb.lsFileLabel);
+    */
+    
+    XtManageChild(w->fsb.listform);
+    
+    int selview = w->fsb.selectedview;
+    if(selview < 2) {
+        XtManageChild(w->fsb.view[selview].widget);
+    } else {
+        w->fsb.selectedview = 0;
+    }
+    
+    
+    if(w->fsb.selectedPath) {
+        char *parentPath = ParentPath(w->fsb.selectedPath);
+        filedialog_update_dir(w, parentPath);
+        PathBarSetPath(w->fsb.pathBar, parentPath);
+        free(parentPath);
+        
+        if(w->fsb.type == FILEDIALOG_SAVE) {
+            XmTextFieldSetString(w->fsb.name, FileName(w->fsb.selectedPath));
+        }
+    } else {
+        char cwd[PATH_MAX];
+        const char *currentPath = w->fsb.currentPath;
+        if(!currentPath) {
+            if(getcwd(cwd, PATH_MAX)) {
+                currentPath = cwd;
+            } else {
+                currentPath = GetHomeDir();
+            }
+        }
+        
+        filedialog_update_dir(w, currentPath);
+        PathBarSetPath(w->fsb.pathBar, w->fsb.currentPath);
+    }
+
+
+    w->fsb.selectedview = selview;
+         
+    XtVaSetValues((Widget)w, XmNcancelButton, w->fsb.cancelBtn, NULL);
+    
+    w->fsb.gui_created = 1;
+}
+
+static char* FSBDialogTitle(Widget widget) {
+    XnFileSelectionBox w = (XnFileSelectionBox)widget;
+    if(w->fsb.type == FILEDIALOG_OPEN) {
+        return w->fsb.labelOpenFileTitle;
+    } else {
+        return w->fsb.labelSaveFileTitle;
+    }
+}
+
+static FSBViewWidgets CreateView(XnFileSelectionBox w, FSBViewCreateProc create, void *userData, Boolean useDirList) {
+    Arg args[64];
+    int n = 0;
+    
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    if(useDirList) {
+        XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNleftAttachment, XmATTACH_WIDGET); n++;
+        XtSetArg(args[n], XmNleftWidget, w->fsb.dirlist); n++;
+        XtSetArg(args[n], XmNleftOffset, w->fsb.widgetSpacing); n++;
+    } else {
+        XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNtopOffset, w->fsb.widgetSpacing); n++;
+        XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    }
+    
+    return create(w->fsb.listform, args, n, userData);
+}
+
+
+typedef struct FSBViewSelection {
+    XnFileSelectionBox fsb;
+    int index;
+} FSBViewSelection;
+
+static void SelectView(XnFileSelectionBox f, int view) {
+    FSBView current = f->fsb.view[f->fsb.selectedview];
+    FSBView newview = f->fsb.view[view];
+    
+    XtUnmanageChild(current.widget);
+    if(newview.useDirList != current.useDirList) {
+        if(current.useDirList) {
+            XtUnmanageChild(f->fsb.listform);
+        } else {
+            XtManageChild(f->fsb.listform);
+        }
+    }
+    
+    current.cleanup((Widget)f, current.widget, current.userData);
+    XtManageChild(newview.widget);
+    
+    f->fsb.selectedview = view;
+    
+    filedialog_update_dir(f, NULL);
+    XmProcessTraversal(newview.focus, XmTRAVERSE_CURRENT);
+}
+
+static void SelectViewCallback(Widget w, FSBViewSelection *data, XtPointer u) {
+    SelectView(data->fsb, data->index);
+}
+
+static void SelectViewItemDestroy(Widget w, FSBViewSelection *data, XtPointer u) {
+    free(data);
+}
+
+static void AddViewMenuItem(XnFileSelectionBox w, const char *name, int viewIndex) {
+    Arg args[4];
+    int n = 0;
+    
+    XmString label = XmStringCreateLocalized((char*)name);
+    
+    XtSetArg(args[n], XmNlabelString, label); n++;
+    XtSetArg(args[1], XmNpositionIndex, w->fsb.selectedview == w->fsb.numviews ? 0 : w->fsb.numviews+1); n++;
+    Widget item = XmCreatePushButton(w->fsb.viewMenu, "menuitem", args, n);
+    
+    if(viewIndex == 0) {
+        w->fsb.viewSelectorList = item;
+    } else if(viewIndex == 1) {
+        w->fsb.viewSelectorDetail = item;
+    }
+   
+    XtManageChild(item);
+    XmStringFree(label);
+    
+    FSBViewSelection *data = malloc(sizeof(FSBViewSelection));
+    data->fsb = w;
+    data->index = viewIndex;
+    
+    XtAddCallback(
+            item,
+            XmNactivateCallback,
+            (XtCallbackProc)SelectViewCallback,
+            data);
+    XtAddCallback(
+            item,
+            XmNdestroyCallback,
+            (XtCallbackProc)SelectViewItemDestroy,
+            data);
+}
+
+static FSBViewWidgets CreateListView(Widget parent, ArgList args, int n, void *userData) {
+    XnFileSelectionBox fsb = (XnFileSelectionBox)userData;
+    
+    XtSetArg(args[n], XmNshadowThickness, 0); n++;
+    Widget frame = XmCreateFrame(parent, "filelistframe", args, n);
+    
+    fsb->fsb.filelist = XmCreateScrolledList(frame, "filelist", NULL, 0);
+    XtManageChild(fsb->fsb.filelist);
+    
+    XtAddCallback(
+            fsb->fsb.filelist,
+            XmNdefaultActionCallback,
+            (XtCallbackProc)FileListActivateCB,
+            userData); 
+    XtAddCallback(
+            fsb->fsb.filelist,
+            XmNbrowseSelectionCallback,
+            (XtCallbackProc)FileListSelectCB,
+            userData);
+    
+    fsb->fsb.listContextMenu = CreateContextMenu(fsb, fsb->fsb.filelist, FileContextMenuCB);
+    
+    FSBViewWidgets widgets;
+    widgets.view = frame;
+    widgets.focus = fsb->fsb.filelist;
+    return widgets;
+}
+
+#ifdef FSB_ENABLE_DETAIL
+static void set_path_from_row(XnFileSelectionBox data, int row) {
+    FileElm *elm = NULL;
+    XmLGridRow rowPtr = XmLGridGetRow(data->fsb.grid, XmCONTENT, row);
+    XtVaGetValues(data->fsb.grid, XmNrowPtr, rowPtr, XmNrowUserData, &elm, NULL);
+    if(!elm) {
+        fprintf(stderr, "error: no row data\n");
+        return;
+    }
+    
+    char *path = strdup(elm->path);
+    
+    data->fsb.selIsDir = False;
+    if(data->fsb.type == FILEDIALOG_SAVE) {
+        XmTextFieldSetString(data->fsb.name, FileName(path));
+    }
+    
+    if(data->fsb.selectedPath) {
+        free(data->fsb.selectedPath);
+    }
+    data->fsb.selectedPath = path;
+}
+
+static void grid_select(Widget w, XnFileSelectionBox data, XmLGridCallbackStruct *cb) {
+    set_path_from_row(data, cb->row);
+}
+
+static void grid_activate(Widget w, XnFileSelectionBox data, XmLGridCallbackStruct *cb) {
+    set_path_from_row(data, cb->row);
+    data->fsb.end = True;
+    data->fsb.status = FILEDIALOG_OK;
+    
+    FileSelectionCallback(data, data->fsb.okCallback, XmCR_OK, data->fsb.selectedPath);
+}
+ 
+static void grid_key_pressed(Widget w, XnFileSelectionBox data, XmLGridCallbackStruct *cb) {
+    char chars[16];
+    KeySym keysym;
+    int nchars;
+    
+    nchars = XLookupString(&cb->event->xkey, chars, 15, &keysym, NULL);
+    
+    if(nchars == 0) return;
+
+    // if data->showHidden is 0, data->files contains more items than the grid
+    // this means SelectedRow might not be the correct index for data->files
+    // we have to count files manually and increase 'row', if the file
+    // is actually displayed in the grid
+    int row = 0;
+    int selectedRow = XmLGridGetSelectedRow(w);
+    
+    int match = -1;
+    
+    for(int i=0;i<data->fsb.filecount;i++) {
+        const char *name = FileName(data->fsb.files[i].path);
+        if(!data->fsb.showHidden && name[0] == '.') continue;
+        
+        size_t namelen = strlen(name);
+        
+        size_t cmplen = namelen < nchars ? namelen : nchars;
+        if(!memcmp(name, chars, cmplen)) {
+            if(row <= selectedRow) {
+                if(match == -1) {
+                    match = row;
+                }
+            } else {
+                match = row;
+                break;
+            }
+        }
+        
+        row++;
+    }
+    
+    if(match > -1) {
+        XmLGridSelectRow(w, match, True);
+        XmLGridFocusAndShowRow(w, match+1);
+    } else {
+        XBell(XtDisplay(w), 0);
+    }
+}
+
+static void grid_header_clicked(Widget w, XnFileSelectionBox data, XmLGridCallbackStruct *cb) { 
+    int new_cmp_field = 0;
+    switch(cb->column) {
+        case 0: {
+            new_cmp_field = 0;            
+            break;
+        }
+        case 1: {
+            new_cmp_field = 1;
+            break;
+        }
+        case 2: {
+            new_cmp_field = 2;
+            break;
+        }
+    }
+    
+    if(new_cmp_field == file_cmp_field) {
+        file_cmp_order = -file_cmp_order; // revert sort order
+    } else {
+        file_cmp_field = new_cmp_field; // change file cmp order to new field
+        file_cmp_order = 1;
+    }
+    
+    int sort_type = file_cmp_order == 1 ? XmSORT_ASCENDING : XmSORT_DESCENDING;
+    XmLGridSetSort(data->fsb.grid, file_cmp_field, sort_type);
+    
+    qsort(data->fsb.files, data->fsb.filecount, sizeof(FileElm), filecmp);
+    
+    // refresh widget
+    filedialog_update_dir(data, NULL);
+} 
+
+static FSBViewWidgets CreateDetailView(Widget parent, ArgList args, int n, void *userData) {
+    XnFileSelectionBox w = userData;
+    
+    XtSetArg(args[n], XmNshadowThickness, 0); n++;
+    Widget gridcontainer = XmCreateFrame(parent, "gridcontainer", args, n);
+    XtManageChild(gridcontainer);
+    
+    n = 0;
+    XtSetArg(args[n], XmNcolumns, 3); n++;
+    XtSetArg(args[n], XmNheadingColumns, 0); n++;
+    XtSetArg(args[n], XmNheadingRows, 1); n++;
+    XtSetArg(args[n], XmNallowColumnResize, 1); n++;
+    XtSetArg(args[n], XmNsimpleHeadings, w->fsb.detailHeadings); n++;
+    XtSetArg(args[n], XmNhorizontalSizePolicy, XmCONSTANT); n++;
+    
+    w->fsb.grid = XmLCreateGrid(gridcontainer, "grid", args, n);
+    XmLGridSetIgnoreModifyVerify(w->fsb.grid, True);
+    XtManageChild(w->fsb.grid);
+    
+    XtVaSetValues(
+            w->fsb.grid,
+            XmNcellDefaults, True,
+            XtVaTypedArg, XmNblankBackground, XmRString, "white", 6,
+            XtVaTypedArg, XmNcellBackground, XmRString, "white", 6,
+            NULL);
+    
+    XtAddCallback(w->fsb.grid, XmNselectCallback, (XtCallbackProc)grid_select, w);
+    XtAddCallback(w->fsb.grid, XmNactivateCallback, (XtCallbackProc)grid_activate, w);
+    XtAddCallback(w->fsb.grid, XmNheaderClickCallback, (XtCallbackProc)grid_header_clicked, w);
+    XtAddCallback(w->fsb.grid, XmNgridKeyPressedCallback, (XtCallbackProc)grid_key_pressed, w);
+    
+    // context menu
+    w->fsb.gridContextMenu = CreateContextMenu(w, w->fsb.grid, FileContextMenuCB);
+    
+    FSBViewWidgets widgets;
+    widgets.view = gridcontainer;
+    widgets.focus = w->fsb.grid;
+    return widgets;
+}
+#endif
+
+
+/* ------------------------------ Path Utils  ------------------------------ */
+
+const char* GetHomeDir(void) {
+    char *home = getenv("HOME");
+    if(!home) {
+        home = getenv("USERPROFILE");
+        if(!home) {
+            home = "/";
+        }
+    }
+    return home;
+}
+
+static char* ConcatPath(const char *parent, const char *name)
+{ 
+    size_t parentlen = strlen(parent);
+    size_t namelen = strlen(name);
+    
+    size_t pathlen = parentlen + namelen + 2;
+    char *path = malloc(pathlen);
+    
+    memcpy(path, parent, parentlen);
+    if(parentlen > 0 && parent[parentlen-1] != '/') {
+        path[parentlen] = '/';
+        parentlen++;
+    }
+    if(name[0] == '/') {
+        name++;
+        namelen--;
+    }
+    memcpy(path+parentlen, name, namelen);
+    path[parentlen+namelen] = '\0';
+    return path;
+}
+
+static char* FileName(char *path) {
+    int si = 0;
+    int osi = 0;
+    int i = 0;
+    int p = 0;
+    char c;
+    while((c = path[i]) != 0) {
+        if(c == '/') {
+            osi = si;
+            si = i;
+            p = 1;
+        }
+        i++;
+    }
+    
+    char *name = path + si + p;
+    if(name[0] == 0) {
+        name = path + osi + p;
+        if(name[0] == 0) {
+            return path;
+        }
+    }
+    
+    return name;
+}
+
+static char* ParentPath(const char *path) {
+    char *name = FileName((char*)path);
+    size_t namelen = strlen(name);
+    size_t pathlen = strlen(path);
+    size_t parentlen = pathlen - namelen;
+    if(parentlen == 0) {
+        parentlen++;
+    }
+    char *parent = malloc(parentlen + 1);
+    memcpy(parent, path, parentlen);
+    parent[parentlen] = '\0';
+    return parent;
+}
+
+// unused at the moment, maybe reactivate if more illegal characters
+// are defined
+/*
+static int CheckFileName(const char *fileName) {
+    size_t len = strlen(fileName);
+    for(int i=0;i<len;i++) {
+        if(fileName[i] == '/') {
+            return 0;
+        }
+    }
+    return 1;
+}
+*/
+
+
+/* ------------------------------ public API ------------------------------ */
+
+Widget XnCreateFileSelectionDialog(
+        Widget parent,
+        String name,
+        ArgList arglist,
+        Cardinal argcount)
+{
+    Widget dialog = XmCreateDialogShell(parent, "FileDialog", NULL, 0);
+    Widget fsb = XnCreateFileSelectionBox(dialog, name, arglist, argcount);
+    char *title = FSBDialogTitle(fsb);
+    XtVaSetValues(dialog, XmNtitle, title, NULL);
+    return fsb;
+}
+
+Widget XnCreateFileSelectionBox(
+        Widget parent,
+        String name,
+        ArgList arglist,
+        Cardinal argcount)
+{
+    Widget fsb = XtCreateWidget(name, xnFsbWidgetClass, parent, arglist, argcount);
+    return fsb;
+}
+
+void XnFileSelectionBoxAddView(
+        Widget fsb,
+        const char *name,
+        FSBViewCreateProc create,
+        FSBViewUpdateProc update,
+        FSBViewSelectProc select,
+        FSBViewCleanupProc cleanup,
+        FSBViewDestroyProc destroy,
+        Boolean useDirList,
+        void *userData)
+{
+    XnFileSelectionBox f = (XnFileSelectionBox)fsb;
+    if(f->fsb.numviews >= FSB_MAX_VIEWS) {
+        fprintf(stderr, "XnFileSelectionBox: too many views\n");
+        return;
+    }
+    
+    FSBView view;
+    view.update = update;
+    view.select = select;
+    view.cleanup = cleanup;
+    view.destroy = destroy;
+    view.useDirList = useDirList;
+    view.userData = userData;
+    
+    FSBViewWidgets widgets = CreateView(f, create, userData, useDirList);
+    view.widget = widgets.view;
+    view.focus = widgets.focus;
+    
+    AddViewMenuItem(f, name, f->fsb.numviews);
+    
+    f->fsb.view[f->fsb.numviews++] = view;
+}
+
+/*
+void XnFileSelectionBoxSetDirList(Widget fsb, const char **dirlist, size_t nelm) {
+    XnFileSelectionBox f = (XnFileSelectionBox)fsb;
+    PathBarSetDirList(f->fsb.pathBar, dirlist, nelm);
+}
+*/
+
+Widget XnFileSelectionBoxWorkArea(Widget fsb) {
+    XnFileSelectionBox f = (XnFileSelectionBox)fsb;
+    return f->fsb.workarea;
+}
+
+Widget XnFileSelectionBoxGetChild(Widget fsb, enum XnFSBChild child) {
+    XnFileSelectionBox w = (XnFileSelectionBox)fsb;
+    switch(child) {
+        case XnFSB_DIR_UP_BUTTON: return w->fsb.dirUp;
+        case XnFSB_HOME_BUTTON: return w->fsb.home;
+        case XnFSB_NEW_FOLDER_BUTTON: return w->fsb.newFolder;
+        case XnFSB_DETAIL_TOGGLE_BUTTON: return w->fsb.detailToggleButton;
+        case XnFSB_VIEW_OPTION_BUTTON: return w->fsb.viewOption;
+        case XnFSB_FILTER_DROPDOWN: return w->fsb.filter;
+        case XnFSB_FILTER_BUTTON: return w->fsb.filterButton;
+        case XnFSB_SHOW_HIDDEN_TOGGLE_BUTTON: return w->fsb.showHiddenButtonW;
+        case XnFSB_DIRECTORIES_LABEL: return w->fsb.lsDirLabel;
+        case XnFSB_FILES_LABEL: return w->fsb.lsFileLabel;
+        case XnFSB_DIRLIST: return w->fsb.dirlist;
+        case XnFSB_FILELIST: return w->fsb.filelist;
+        case XnFSB_GRID: return w->fsb.grid;
+        case XnFSB_OK_BUTTON: return w->fsb.okBtn;
+        case XnFSB_CANCEL_BUTTON: return w->fsb.cancelBtn;
+        case XnFSB_HELP_BUTTON: return w->fsb.helpBtn;
+    }
+    return NULL;
+}
+
+void XnFileSelectionBoxDeleteFilters(Widget fsb) {
+    XnFileSelectionBox w = (XnFileSelectionBox)fsb;
+    Widget filterList = XmDropDownGetList(w->fsb.filter);
+    XmListDeleteAllItems(filterList);
+}
+
+void XnFileSelectionBoxAddFilter(Widget fsb, const char *filter) {
+    XnFileSelectionBox w = (XnFileSelectionBox)fsb;
+    Widget filterList = XmDropDownGetList(w->fsb.filter);
+    
+    XmString str = XmStringCreateSimple((char*)filter);
+    XmListAddItem(filterList, str, 0);
+    XmStringFree(str);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/motif/Fsb.h	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2021 Olaf Wintermann
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a 
+ * copy of this software and associated documentation files (the "Software"), 
+ * to deal in the Software without restriction, including without limitation 
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
+ * and/or sell copies of the Software, and to permit persons to whom the 
+ * Software is furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in 
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef FSB_H
+#define FSB_H
+
+#include <X11/Intrinsic.h>
+#include <Xm/PrimitiveP.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern WidgetClass xnFsbWidgetClass;
+
+#define FILEDIALOG_OPEN 1
+#define FILEDIALOG_SAVE 2
+    
+#define FILEDIALOG_OK 1
+#define FILEDIALOG_CANCEL 2
+
+#define XnNwidgetSpacing         "fsbWidgetSpacing"
+#define XnNwindowSpacing         "fsbWindowSpacing"
+
+#define XnNfsbType               "fsbType"
+#define XnCfsbType               "fsbType"
+
+#define XnNshowHidden            "showHidden"
+#define XnCshowHidden            "showHidden"
+#define XnNshowHiddenButton      "showHiddenButton"
+#define XnCshowHiddenButton      "showHiddenButton"
+
+#define XnNshowViewMenu          "showViewMenu"
+#define XnCshowViewMenu          "showViewMenu"
+
+#define XnNselectedView          "fsbSelectedView"
+#define XnCselectedView          "fsbSelectedView"
+
+#define XnNdirectory             "directory"
+#define XnCdirectory             "directory"
+#define XnNselectedPath          "selectedPath"
+#define XnCselectedPath          "selectedPath"
+#define XnNhomePath              "homePath"
+#define XnChomePath              "homePath"
+
+#define XnNfilter                "filter"
+#define XnCfilter                "filter"
+
+#define XnNfilterFunc            "filterFunc"
+#define XnCfilterFunc            "filterFunc"
+
+#define XnNlabelListView         "labelListView"
+#define XnClabelListView         "labelListView"
+#define XnNlabelDetailView       "labelDetailView"
+#define XnClabelDetailView       "labelDetailView"
+#define XnNlabelOpenFileTitle    "labelOpenFileTitle"
+#define XnClabelOpenFileTitle    "labelOpenFileTitle"
+#define XnNlabelSaveFileTitle    "labelSaveFileTitle"
+#define XnClabelSaveFileTitle    "labelSaveFileTitel"
+#define XnNlabelDirUp            "labelDirUp"
+#define XnClabelDirUp            "labelDirUp"
+#define XnNlabelHome             "labelHome"
+#define XnClabelHome             "labelHome"
+#define XnNlabelNewFolder        "labelNewFolder"
+#define XnClabelNewFolder        "labelNewFolder"
+#define XnNlabelFilter           "labelFilter"
+#define XnClabelFilter           "labelFilter"
+#define XnNlabelFilterButton     "labelFilterButton"
+#define XnClabelFilterButton     "labelFilterButton"
+#define XnNlabelShowHiddenFiles  "labelShowHiddenFiles"
+#define XnClabelShowHiddenFiles  "labelShowHiddenFiles"
+#define XnNlabelDirectories      "labelDirectories"
+#define XnClabelDirectories      "labelDirectories"
+#define XnNlabelFiles            "labelFiles"
+#define XnClabelFiles            "labelFiles"
+#define XnNlabelRename           "labelRename"
+#define XnClabelRename           "labelRename"
+#define XnNlabelDelete           "labelDelete"
+#define XnClabelDelete           "labelDelete"
+#define XnNlabelOpen             "labelOpen"
+#define XnClabelOpen             "labelOpen"
+#define XnNlabelSave             "labelSave"
+#define XnClabelSave             "labelSave"
+#define XnNlabelOk               "labelOk"
+#define XnClabelOk               "labelOk"
+#define XnNlabelCancel           "labelCancel"
+#define XnClabelCancel           "labelCancel"
+#define XnNlabelHelp             "labelHelp"
+#define XnClabelHelp             "labelHelp"
+#define XnNlabelFileName         "labelFileName"
+#define XnClabelFileName         "labelFileName"
+#define XnNlabelDirectoryName    "labelDirectoryName"
+#define XnClabelDirectoryName    "labelDirectoryName"
+#define XnNlabelNewFileName      "labelNewFileName"
+#define XnClabelNewFileName      "labelNewFileName"
+#define XnNlabelDeleteFile       "labelDeleteFile"
+#define XnClabelDeleteFile       "labelDeleteFile"
+#define XnNdetailHeadings        "detailHeadings"
+#define XnCdetailHeadings        "detailHeadings"
+#define XnNdateFormatSameYear    "dateFormatSameYear"
+#define XnCdateFormatSameYear    "dateFormatSameYear"
+#define XnNdateFormatOtherYear   "dateFormatOtherYear"
+#define XnCdateFormatOtherYear   "dateFormatOtherYear"
+#define XnNsuffixBytes           "suffixBytes"
+#define XnCsuffixBytes           "suffixBytes"
+#define XnNsuffixKB              "suffixKB"
+#define XnCsuffixKB              "suffixKB"
+#define XnNsuffixMB              "suffixMB"
+#define XnCsuffixMB              "suffixMB"
+#define XnNsuffixGB              "suffixGB"
+#define XnCsuffixGB              "suffixGB"
+#define XnNsuffixTB              "suffixTB"
+#define XnCsuffixTB              "suffixTB"
+#define XnNerrorTitle            "errorTitle"
+#define XnCerrorTitle            "errorTitle"
+#define XnNerrorIllegalChar      "errorIllegalChar"
+#define XnCerrorIllegalChar      "errorIllegalChar"
+#define XnNerrorRename           "errorRename"
+#define XnCerrorRename           "errorRename"
+#define XnNerrorCreateFolder     "errorCreateFolder"
+#define XnCerrorCreateFolder     "errorCreateFolder"
+#define XnNerrorDelete           "errorDelete"
+#define XnCerrorDelete           "errorDelete"
+#define XnNerrorOpenDir          "errorOpenDir"
+#define XnCerrorOpenDir          "errorOpenDir"
+
+/*
+ * int FSBFilterFunc(const char *pattern, const char *string)
+ * 
+ * Checks whether the string matches the pattern
+ * 
+ * Return
+ *   zero if the string matches the pattern
+ *   non-zero if there is no match
+ */
+typedef int(*FSBFilterFunc)(const char*, const char*);
+
+
+typedef struct FileElm FileElm;
+struct FileElm {
+    char *path;
+    int isDirectory;
+    unsigned long long size;
+    time_t lastModified;
+};
+
+typedef struct {
+    Widget view;
+    Widget focus;
+} FSBViewWidgets;
+
+enum XnFSBChild {
+    XnFSB_DIR_UP_BUTTON = 0,
+    XnFSB_HOME_BUTTON,
+    XnFSB_NEW_FOLDER_BUTTON,
+    XnFSB_DETAIL_TOGGLE_BUTTON,
+    XnFSB_VIEW_OPTION_BUTTON,
+    XnFSB_FILTER_DROPDOWN,
+    XnFSB_FILTER_BUTTON,
+    XnFSB_SHOW_HIDDEN_TOGGLE_BUTTON,
+    XnFSB_DIRECTORIES_LABEL,
+    XnFSB_FILES_LABEL,
+    XnFSB_DIRLIST,
+    XnFSB_FILELIST,
+    XnFSB_GRID,
+    XnFSB_OK_BUTTON,
+    XnFSB_CANCEL_BUTTON,
+    XnFSB_HELP_BUTTON
+};
+
+typedef FSBViewWidgets(*FSBViewCreateProc)(Widget parent, ArgList args, int n, void *userData);
+typedef void(*FSBViewUpdateProc)(Widget fsb, Widget view, FileElm *dirs, int dircount, FileElm *files, int filecount, const char *filter, int maxnamelen, void *userData);
+typedef void(*FSBViewSelectProc)(Widget fsb, Widget view, const char *item);
+typedef void(*FSBViewCleanupProc)(Widget fsb, Widget view, void *userData);
+typedef void(*FSBViewDestroyProc)(Widget fsb, Widget view, void *userData);
+
+Widget XnCreateFileSelectionDialog(
+        Widget parent,
+        String name,
+        ArgList arglist,
+        Cardinal argcount);
+
+Widget XnCreateFileSelectionBox(
+        Widget parent,
+        String name,
+        ArgList arglist,
+        Cardinal argcount);
+
+void XnFileSelectionBoxAddView(
+        Widget fsb,
+        const char *name,
+        FSBViewCreateProc create,
+        FSBViewUpdateProc update,
+        FSBViewSelectProc select,
+        FSBViewCleanupProc cleanup,
+        FSBViewDestroyProc destroy,
+        Boolean useDirList,
+        void *userData);
+
+//void XnFileSelectionBoxSetDirList(Widget fsb, const char **dirlist, size_t nelm);
+
+Widget XnFileSelectionBoxWorkArea(Widget fsb);
+
+Widget XnFileSelectionBoxGetChild(Widget fsb, enum XnFSBChild child);
+
+void XnFileSelectionBoxDeleteFilters(Widget fsb);
+
+void XnFileSelectionBoxAddFilter(Widget fsb, const char *filter);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* FSB_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/motif/FsbP.h	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2021 Olaf Wintermann
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a 
+ * copy of this software and associated documentation files (the "Software"), 
+ * to deal in the Software without restriction, including without limitation 
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
+ * and/or sell copies of the Software, and to permit persons to whom the 
+ * Software is furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in 
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef FSBP_H
+#define FSBP_H
+
+#include <X11/CoreP.h>
+#include <Xm/XmP.h>
+#include <Xm/PrimitiveP.h>
+#include <Xm/ManagerP.h>
+#include <Xm/FormP.h>
+
+#include "Fsb.h"
+#include "pathbar.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define FSB_MAX_VIEWS 8
+    
+
+typedef struct FSBView FSBView;
+struct FSBView {
+    Widget widget;
+    Widget focus;
+    FSBViewUpdateProc  update;
+    FSBViewSelectProc  select;
+    FSBViewCleanupProc cleanup;
+    FSBViewDestroyProc destroy;
+    void *userData;
+    Boolean useDirList;
+};
+
+    
+typedef struct FSBClassPart {
+    int unused;
+} FSBClassPart;
+
+typedef struct FSBClassRec {
+    CoreClassPart             core_class;
+    CompositeClassPart        composite_class;
+    ConstraintClassPart       constraint_class;
+    XmManagerClassPart        manager_class;
+    XmBulletinBoardClassPart  bulletin_board_class;
+    XmFormClassPart           form_class;
+    FSBClassPart              fsb_class;
+} FSBClassRec;
+
+typedef struct FSBPart {
+    XtCallbackList okCallback;
+    XtCallbackList cancelCallback;
+    
+    Dimension widgetSpacing;
+    Dimension windowSpacing;
+    
+    Boolean showHiddenButton;
+    
+    Widget path;
+    PathBar *pathBar;
+    Widget filter;
+    Widget filterButton;
+    Widget showHiddenButtonW;
+    
+    FSBFilterFunc filterFunc;
+    
+    char *filterStr;
+    
+    Widget dirUp;
+    Widget home;
+    Widget newFolder;
+    
+    Widget viewSelectorList;
+    Widget viewSelectorDetail;
+    
+    Widget viewMenu;
+    Widget viewOption;
+    Widget detailToggleButton;
+    
+    Widget filterForm;
+    Widget lsDirLabel;
+    Widget lsFileLabel;
+    
+    Widget listContextMenu;
+    Widget gridContextMenu;
+    
+    // icon view
+    
+    // dir/file list view
+    Widget listform;
+    Widget dirlist;
+    
+    FSBView view[FSB_MAX_VIEWS];
+    int numviews;
+    int selectedview;
+    
+    Widget filelist;
+    Widget grid;
+    
+    Widget separator;
+    
+    Widget nameLabel;
+    Widget name;
+    
+    Widget bottom_widget;
+    
+    Widget workarea;
+    
+    Widget okBtn;
+    Widget cancelBtn;
+    Widget helpBtn;
+    
+    FileElm *dirs;
+    FileElm *files;
+    int dircount;
+    int filecount;
+    int maxnamelen;
+    
+    char *homePath;
+    
+    char *currentPath;
+    char *selectedPath;
+    int selIsDir;
+    Boolean showHidden;
+    Boolean showViewMenu;
+      
+    int type;
+    
+    int end;
+    int status;
+    
+    int disable_set_values;
+    int gui_created;
+        
+    char *labelListView;
+    char *labelDetailView;
+    char* labelOpenFileTitle;
+    char* labelSaveFileTitle;
+    XmString labelDirUp;
+    XmString labelHome;
+    XmString labelNewFolder;
+    XmString labelFilterButton;
+    XmString labelShowHiddenFiles;
+    XmString labelDirectories;
+    XmString labelFiles;
+    XmString labelRename;
+    XmString labelDelete;
+    XmString labelOpen;
+    XmString labelSave;
+    XmString labelOk;
+    XmString labelCancel;
+    XmString labelHelp;
+    XmString labelFileName;
+    XmString labelDirectoryName;
+    XmString labelNewFileName;
+    char *labelDeleteFile;
+    
+    char *detailHeadings;
+    
+    char *dateFormatSameYear;
+    char *dateFormatOtherYear;
+    char *suffixBytes;
+    char *suffixKB;
+    char *suffixMB;
+    char *suffixGB;
+    char *suffixTB;
+    
+    char *errorTitle;
+    char *errorIllegalChar;
+    char *errorRename;
+    char *errorFolder;
+    char *errorDelete;
+    char *errorOpenDir;
+} FSBPart;
+
+typedef struct FSBRec {
+   CorePart	        core;
+   CompositePart        composite;
+   ConstraintPart       constraint;
+   XmManagerPart        manager;
+   XmBulletinBoardPart  bulletin_board;
+   XmFormPart           form;
+   FSBPart              fsb;
+} FSBRec;
+
+typedef struct FSBRec *XnFileSelectionBox;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* FSBP_H */
+
--- a/ui/motif/Grid.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/Grid.c	Sat Dec 13 15:58:58 2025 +0100
@@ -60,7 +60,7 @@
         XmRDimension,
         sizeof (Dimension),
         XtOffsetOf( GridRec,
-                   mywidget.columnspacing),
+                   grid.columnspacing),
         XmRImmediate,
         (XtPointer) 0
     },
@@ -70,17 +70,47 @@
         XmRDimension,
         sizeof (Dimension),
         XtOffsetOf( GridRec,
-                   mywidget.rowspacing),
+                   grid.rowspacing),
+        XmRImmediate,
+        (XtPointer) 0
+    },
+    {
+        gridPaddingLeft,
+        gridPaddingLeft,
+        XmRDimension,
+        sizeof (Dimension),
+        XtOffsetOf( GridRec,
+                   grid.padding_left),
         XmRImmediate,
         (XtPointer) 0
     },
     {
-        gridMargin,
-        gridMargin,
+        gridPaddingRight,
+        gridPaddingRight,
         XmRDimension,
         sizeof (Dimension),
         XtOffsetOf( GridRec,
-                   mywidget.margin),
+                   grid.padding_right),
+        XmRImmediate,
+        (XtPointer) 0
+    },
+    {
+        gridPaddingTop,
+        gridPaddingTop,
+        XmRDimension,
+        sizeof (Dimension),
+        XtOffsetOf( GridRec,
+                   grid.padding_top),
+        XmRImmediate,
+        (XtPointer) 0
+    },
+    {
+        gridPaddingBottom,
+        gridPaddingBottom,
+        XmRDimension,
+        sizeof (Dimension),
+        XtOffsetOf( GridRec,
+                   grid.padding_bottom),
         XmRImmediate,
         (XtPointer) 0
     }
@@ -305,8 +335,8 @@
 void grid_initialize(Widget request, Widget new, ArgList args, Cardinal num_args) {
     Grid mn = (Grid)new;
     
-    mn->mywidget.max_col = 0;
-    mn->mywidget.max_row = 0;
+    mn->grid.max_col = 0;
+    mn->grid.max_row = 0;
     
 }
 void grid_realize(Widget w,XtValueMask *valueMask,XSetWindowAttributes *attributes) {
@@ -364,17 +394,17 @@
 }
 
 void GridChangeManaged(Widget widget) {
-    
+    grid_place_children((Grid)widget);
 }
 
 Boolean ConstraintSetValues(Widget old, Widget request, Widget neww, ArgList args, Cardinal *num_args) {
     GridConstraintRec *constraints = neww->core.constraints;
     Grid grid = (Grid)XtParent(neww);
-    if(constraints->grid.x > grid->mywidget.max_col) {
-        grid->mywidget.max_col = constraints->grid.x;
+    if(constraints->grid.x > grid->grid.max_col) {
+        grid->grid.max_col = constraints->grid.x;
     }
-    if(constraints->grid.y > grid->mywidget.max_row) {
-        grid->mywidget.max_row = constraints->grid.y;
+    if(constraints->grid.y > grid->grid.max_row) {
+        grid->grid.max_row = constraints->grid.y;
     }
 }
 
@@ -389,25 +419,31 @@
     GridConstraintRec *constraints = neww->core.constraints;
     
     Grid grid = (Grid)XtParent(neww);
-    if(constraints->grid.x > grid->mywidget.max_col) {
-        grid->mywidget.max_col = constraints->grid.x;
+    if(constraints->grid.x > grid->grid.max_col) {
+        grid->grid.max_col = constraints->grid.x;
     }
-    if(constraints->grid.y > grid->mywidget.max_row) {
-        grid->mywidget.max_row = constraints->grid.y;
+    if(constraints->grid.y > grid->grid.max_row) {
+        grid->grid.max_row = constraints->grid.y;
     }
     constraints->grid.pref_width = neww->core.width;
     constraints->grid.pref_height = neww->core.height;
 }
 
 void grid_place_children(Grid w) {
-    int ncols = w->mywidget.max_col+1;
-    int nrows = w->mywidget.max_row+1;
+    if(!XtIsRealized((Widget)w)) {
+        return;
+    }
+    
+    int ncols = w->grid.max_col+1;
+    int nrows = w->grid.max_row+1;
     GridDef *cols = calloc(ncols, sizeof(GridDef));
     GridDef *rows = calloc(nrows, sizeof(GridDef));
     int num_cols_expanding = 0;
     int num_rows_expanding = 0;
-    int req_width = 0;
-    int req_height = 0;
+    int req_width = w->grid.padding_left + w->grid.padding_right;
+    int req_height = w->grid.padding_top + w->grid.padding_bottom;
+    int width = w->core.width;
+    int height = w->core.height;
     
     //printf("container width: %d\n", (int)w->core.width);
     
@@ -428,6 +464,12 @@
             if(constraints->grid.pref_width < constraints->grid.min_width) {
                 constraints->grid.pref_width = constraints->grid.min_width;
             }
+            int elm_width = constraints->grid.pref_width + constraints->grid.margin_left + constraints->grid.margin_right;
+            int elm_height = constraints->grid.pref_height + constraints->grid.margin_top + constraints->grid.margin_bottom;
+            if(!XtIsManaged(child)) {
+                elm_width = 0;
+                elm_height = 0;
+            }
             
             if(constraints->grid.colspan > span_max || constraints->grid.rowspan > span_max) {
                 continue;
@@ -489,12 +531,12 @@
                     span_width = last_col->size;
                     
                 }
-                int diff = constraints->grid.pref_width - span_width;
+                int diff = elm_width - span_width;
                 if(diff > 0) {
                     last_col->size += diff; 
                 }
-            } else if(constraints->grid.pref_width > col->size) {
-                col->size = constraints->grid.pref_width;
+            } else if(elm_width > col->size) {
+                col->size = elm_width;
             }
             // row size
             if(constraints->grid.rowspan > 1) {
@@ -505,12 +547,12 @@
                     span_height = last_row->size;
                     
                 }
-                int diff = constraints->grid.pref_height - span_height;
+                int diff = elm_height - span_height;
                 if(diff > 0) {
                     last_row->size += diff; 
                 }
-            } else if(constraints->grid.pref_height > row->size) {
-                row->size = constraints->grid.pref_height;
+            } else if(elm_height > row->size) {
+                row->size = elm_height;
             }
         }
         span_max = 50000; // not sure if this is unreasonable low or high
@@ -530,10 +572,23 @@
         req_height += rows[i].size;
     }
     
+    int total_colspacing = 0;
+    int total_rowspacing = 0;
+    for(int i=0;i+1<ncols;i++) {
+        if(cols[i].size > 0) {
+            total_colspacing += w->grid.columnspacing;
+        }
+    }
+    for(int i=0;i+1<nrows;i++) {
+        if(rows[i].size > 0) {
+            total_rowspacing += w->grid.rowspacing;
+        }
+    }
+    
     if(req_width > 0 && req_height > 0) {
         // add col/row spacing
-        req_width += (ncols-1)*w->mywidget.columnspacing;
-        req_height += (nrows-1)*w->mywidget.rowspacing;
+        req_width += total_colspacing; //(ncols-1)*w->grid.columnspacing;
+        req_height += total_rowspacing; //(nrows-1)*w->grid.rowspacing;
         
         Widget parent = w->core.parent;
         Dimension rwidth = req_width;
@@ -545,9 +600,9 @@
             //rheight = w->core.height;
         }
         
-        if(!w->mywidget.sizerequest) {
+        if(!w->grid.sizerequest) {
             Dimension actual_width, actual_height;
-            w->mywidget.sizerequest = TRUE;
+            w->grid.sizerequest = TRUE;
             
             //printf("sizerequest: %d x %d\n", (int)req_width, (int)req_height);
             
@@ -558,7 +613,7 @@
             //XtGeometryResult result = XtMakeGeometryRequest((Widget)w, &request, &reply);
             
             XtMakeResizeRequest((Widget)w, req_width, req_height, &actual_width, &actual_height);
-            w->mywidget.sizerequest = FALSE;
+            w->grid.sizerequest = FALSE;
             //printf("size request: %d %d\n", (int)actual_width, (int)actual_height);
         }
         
@@ -568,37 +623,41 @@
     
     // how much space can we add to each expanding col/row
     int hexpand = 0;
-    int width_diff = (int)w->core.width - req_width;
+    int width_diff = width - req_width;
     int hexpand2 = 0;
     if(width_diff > 0 && num_cols_expanding > 0) {
         hexpand = width_diff / num_cols_expanding;
         hexpand2 = width_diff-hexpand*num_cols_expanding;
     }
-    int x = 0;
+    int x = w->grid.padding_left;
     for(int i=0;i<ncols;i++) {
         cols[i].pos = x;
         if(cols[i].expand) {
             cols[i].size += hexpand + hexpand2;
         }
-        x += cols[i].size + w->mywidget.columnspacing;
+        if(cols[i].size > 0) {
+            x += cols[i].size + w->grid.columnspacing;
+        }
         
         hexpand2 = 0;
     }
     
     int vexpand = 0;
-    int height_diff = (int)w->core.height - req_height;
+    int height_diff = height - req_height;
     int vexpand2 = 0;
     if(height_diff > 0 && num_rows_expanding > 0) {
         vexpand = height_diff / num_rows_expanding;
         vexpand2 = height_diff-vexpand*num_rows_expanding;
     }
-    int y = 0;
+    int y = w->grid.padding_bottom;
     for(int i=0;i<nrows;i++) {
         rows[i].pos = y;
         if(rows[i].expand) {
             rows[i].size += vexpand + vexpand2;
         }
-        y += rows[i].size += w->mywidget.rowspacing;
+        if(rows[i].size > 0) {
+            y += rows[i].size += w->grid.rowspacing;
+        }
         
         vexpand2 = 0;
     }
@@ -608,8 +667,8 @@
         GridConstraintRec *constraints = child->core.constraints;
         GridDef c = cols[constraints->grid.x];
         GridDef r = rows[constraints->grid.y];
-        int x = c.pos;
-        int y = r.pos;
+        int x = c.pos + constraints->grid.margin_left;
+        int y = r.pos + constraints->grid.margin_top;
         int width = constraints->grid.pref_width;
         int height = constraints->grid.pref_height;
         if(constraints->grid.hfill) {
@@ -617,12 +676,12 @@
                 Dimension cwidth = 0;
                 for(int j=0;j<constraints->grid.colspan;j++) {
                     if(constraints->grid.x+j < ncols) {
-                        cwidth += cols[constraints->grid.x+j].size + (j > 0 ? w->mywidget.columnspacing : 0);
+                        cwidth += cols[constraints->grid.x+j].size + (j > 0 ? w->grid.columnspacing : 0);
                     }
                 }
                 width = cwidth;
             } else {
-                width = c.size - w->mywidget.columnspacing;
+                width = c.size - w->grid.columnspacing - constraints->grid.margin_left - constraints->grid.margin_right;
             }
         }
         if(constraints->grid.vfill) {
@@ -630,15 +689,15 @@
                 Dimension cheight = 0;
                 for(int j=0;j<constraints->grid.rowspan;j++) {
                     if(constraints->grid.y+j < nrows) {
-                        cheight += rows[constraints->grid.y+j].size + (j > 0 ? w->mywidget.rowspacing : 0);
+                        cheight += rows[constraints->grid.y+j].size + (j > 0 ? w->grid.rowspacing : 0);
                     }
                 }
                 height = cheight;
             } else {
-                height = r.size - w->mywidget.rowspacing;
+                height = r.size - w->grid.rowspacing - constraints->grid.margin_top - constraints->grid.margin_bottom;
             }
         }
-        
+              
         if(width > 0 && height > 0) {
             XtConfigureWidget(child, x, y, width, height, child->core.border_width);
         }
--- a/ui/motif/Grid.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/Grid.h	Sat Dec 13 15:58:58 2025 +0100
@@ -42,8 +42,11 @@
 
 // resources
 #define gridColumnSpacing "gridColumnSpacing"
-#define gridRowSpacing "gridRowSpacing"
-#define gridMargin "gridMargin"
+#define gridRowSpacing    "gridRowSpacing"
+#define gridPaddingLeft   "gridPaddingLeft"
+#define gridPaddingRight  "gridPaddingRight"
+#define gridPaddingTop    "gridPaddingTop"
+#define gridPaddingBottom "gridPaddingBottom"
 
 // constraints    
 #define gridColumn "gridColumn"
@@ -75,21 +78,20 @@
     CoreClassPart        core_class;
     CompositeClassPart   composite_class;
     ConstraintClassPart  constraint_class;
-    XmManagerClassPart  manager_class;
-    GridClassPart    mywidgetclass;
+    XmManagerClassPart   manager_class;
+    GridClassPart        gridwidgetclass;
 } GridClassRec;
 
 
 typedef struct GridPart {
-    int margin_left;
-    int margin_right;
-    int margin_top;
-    int margin_bottom;
+    int padding_left;
+    int padding_right;
+    int padding_top;
+    int padding_bottom;
     int max_col;
     int max_row;
     Dimension columnspacing;
     Dimension rowspacing;
-    Dimension margin;
     
     Boolean sizerequest;
 } GridPart;
@@ -99,7 +101,7 @@
     CompositePart   composite;
     ConstraintPart  constraint;
     XmManagerPart   manager;
-    GridPart    mywidget;
+    GridPart        grid;
 } GridRec;
 
 typedef struct GridContraintPart {
--- a/ui/motif/button.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/button.c	Sat Dec 13 15:58:58 2025 +0100
@@ -61,7 +61,7 @@
     XtManageChild(button);
     ui_container_add(ctn, button);
     
-    ui_set_widget_groups(obj->ctx, button, args->groups);
+    ui_set_widget_groups(obj->ctx, button, args->states);
     
     if(args->onclick) {
         UiEventData *eventdata = malloc(sizeof(UiEventData));
@@ -77,7 +77,7 @@
        XtAddCallback(
                 button,
                 XmNdestroyCallback,
-                (XtCallbackProc)ui_destroy_eventdata,
+                (XtCallbackProc)ui_destroy_data,
                 eventdata);
     }
     
@@ -118,9 +118,9 @@
     XtManageChild(button);
     ui_container_add(ctn, button);
     
-    ui_set_widget_groups(obj->ctx, button, args->groups);
+    ui_set_widget_groups(obj->ctx, button, args->states);
     
-    ui_bind_togglebutton(obj, button, args->varname, args->value, args->onchange, args->onchangedata, args->enable_group);
+    ui_bind_togglebutton(obj, button, args->varname, args->value, args->onchange, args->onchangedata, args->enable_state);
     
     XmStringFree(label);
     return button;
@@ -146,9 +146,9 @@
     XtManageChild(button);
     ui_container_add(ctn, button);
     
-    ui_set_widget_groups(obj->ctx, button, args->groups);
+    ui_set_widget_groups(obj->ctx, button, args->states);
     
-    ui_bind_togglebutton(obj, button, args->varname, args->value, args->onchange, args->onchangedata, args->enable_group);
+    ui_bind_togglebutton(obj, button, args->varname, args->value, args->onchange, args->onchangedata, args->enable_state);
     
     XmStringFree(label);
     return button;
@@ -162,9 +162,9 @@
     if(event->value > 0) {
         // button in configured to enable/disable states
         if(tb->set) {
-            ui_set_group(event->obj->ctx, event->value);
+            ui_set_state(event->obj->ctx, event->value);
         } else {
-            ui_unset_group(event->obj->ctx, event->value);
+            ui_unset_state(event->obj->ctx, event->value);
         }
     }
     
@@ -224,7 +224,7 @@
     XtAddCallback(
             widget,
             XmNdestroyCallback,
-            (XtCallbackProc)ui_destroy_eventdata,
+            (XtCallbackProc)ui_destroy_data,
             event);
 }
 
@@ -249,9 +249,9 @@
     if(event->value > 0) {
         // button in configured to enable/disable states
         if(tb->set) {
-            ui_set_group(event->obj->ctx, event->value);
+            ui_set_state(event->obj->ctx, event->value);
         } else {
-            ui_unset_group(event->obj->ctx, event->value);
+            ui_unset_state(event->obj->ctx, event->value);
         }
     }
     
@@ -341,7 +341,7 @@
     XtAddCallback(
             rbutton,
             XmNdestroyCallback,
-            (XtCallbackProc)ui_destroy_eventdata,
+            (XtCallbackProc)ui_destroy_data,
             event);
 }
 
@@ -365,7 +365,7 @@
     XtManageChild(button);
     ui_container_add(ctn, button);
     
-    ui_set_widget_groups(obj->ctx, button, args->groups);
+    ui_set_widget_groups(obj->ctx, button, args->states);
     
     UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->value, args->varname, UI_VAR_INTEGER);
     if(var) {
@@ -402,7 +402,7 @@
     event->userdata = args->onchangedata;
     event->observers = NULL;
     event->var = var;
-    event->value = args->enable_group;
+    event->value = args->enable_state;
     XtAddCallback(
             button,
             XmNvalueChangedCallback,
@@ -411,7 +411,7 @@
     XtAddCallback(
             button,
             XmNdestroyCallback,
-            (XtCallbackProc)ui_destroy_eventdata,
+            (XtCallbackProc)ui_destroy_data,
             event);
     
     XmStringFree(label);
--- a/ui/motif/container.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/container.c	Sat Dec 13 15:58:58 2025 +0100
@@ -55,6 +55,18 @@
     container->add(container, widget);
 }
 
+void ui_container_apply_grid_margin(
+        Arg *args,
+        int *n,
+        int margin_left, int margin_right, int margin_top, int margin_bottom)
+{
+    int c = *n;
+    XtSetArg(args[c], gridMarginLeft, margin_left); c++;
+    XtSetArg(args[c], gridMarginRight, margin_right); c++;
+    XtSetArg(args[c], gridMarginTop, margin_top); c++;
+    XtSetArg(args[c], gridMarginBottom, margin_bottom); c++;
+    *n = c;
+}
 
 /* ---------------------------- Box Container ---------------------------- */
 
@@ -66,9 +78,9 @@
     int n = 0;
     
     if(orientation == UI_BOX_VERTICAL) {
-        //XtSetArg(xargs[n], gridRowSpacing, args->spacing); n++;
+        XtSetArg(xargs[n], gridRowSpacing, args->spacing); n++;
     } else {
-        //XtSetArg(xargs[n], gridColumnSpacing, args->spacing); n++;
+        XtSetArg(xargs[n], gridColumnSpacing, args->spacing); n++;
     }
     
     Widget parent = ui_container_prepare(ctn, &layout, xargs, &n);
@@ -102,7 +114,7 @@
 }
 
 static Widget ui_box_container_prepare(UiBoxContainer *box, UiLayout *layout, Arg *args, int *n) {
-    int a = *n;
+    ui_container_apply_grid_margin(args, n, layout->margin_left, layout->margin_right, layout->margin_top, layout->margin_bottom);
     box->n++;
     return box->container.widget;
 }
@@ -151,7 +163,6 @@
     UiLayout layout = UI_ARGS2LAYOUT(args);
     
     Widget parent = ui_container_prepare(ctn, &layout, xargs, &n);
-    XtSetArg(xargs[n], gridMargin, args->margin); n++;
     XtSetArg(xargs[n], gridColumnSpacing, args->columnspacing); n++;
     XtSetArg(xargs[n], gridRowSpacing, args->rowspacing); n++;
     Widget grid = XtCreateManagedWidget(args->name ? args->name : "gridcontainer", gridClass, parent, xargs, n);
@@ -218,6 +229,7 @@
     }
     
     *n = a;
+    ui_container_apply_grid_margin(args, n, layout->margin_left, layout->margin_right, layout->margin_top, layout->margin_bottom);
     return ctn->widget;
 }
 
@@ -237,9 +249,15 @@
     UiLayout layout = UI_ARGS2LAYOUT(args);
     
     Widget parent = ui_container_prepare(ctn, &layout, xargs, &n);
+    if(args->width > 0) {
+        XtSetArg(xargs[n], XmNwidth, args->width); n++;
+    }
+    if(args->height > 0) {
+        XtSetArg(xargs[n], XmNheight, args->height); n++;
+    }
     
     char *name = args->name ? (char*)args->name : "frame";
-    Widget frame = XmCreateFrame(parent, name, xargs, 6);
+    Widget frame = XmCreateFrame(parent, name, xargs, n);
     XtManageChild(frame);
     ui_container_add(ctn, frame);
     
@@ -259,7 +277,8 @@
     UiContainerArgs sub_args = {
         .spacing = args->spacing,
         .columnspacing = args->columnspacing,
-        .rowspacing = args->rowspacing
+        .rowspacing = args->rowspacing,
+        .margin = args->padding
     };
     switch(args->subcontainer) {
         default: break;
@@ -334,9 +353,10 @@
     if(numbuttons == 0) {
         return;
     }
+    width--;
     int button_width = width / numbuttons;
     int x = 0;
-    
+        
     CxIterator i = cxListIterator(tabview->tabs);
     cx_foreach(UiTab *, tab, i) {
         if(i.index + 1 == numbuttons) {
@@ -432,6 +452,7 @@
     tabview->current_index = -1;
     
     UiTabViewContainer *ct = ui_malloc(obj->ctx, sizeof(UiTabViewContainer));
+    memset(ct, 0, sizeof(UiTabViewContainer));
     ct->container.widget = form;
     ct->container.type = UI_CONTAINER_TABVIEW;
     ct->container.prepare = ui_tabview_container_prepare;
@@ -682,6 +703,12 @@
     int n = 0;
     
     XtSetArg(xargs[n], XmNscrollingPolicy, XmAUTOMATIC); n++;
+    if(args->width > 0) {
+        XtSetArg(xargs[n], XmNwidth, args->width); n++;
+    }
+    if(args->height > 0) {
+        XtSetArg(xargs[n], XmNheight, args->height); n++;
+    }
     
     Widget parent = ui_container_prepare(ctn, &layout, xargs, &n);
     Widget scrolledwindow = XmCreateScrolledWindow(parent, "scrolledwindow", xargs, n);
--- a/ui/motif/container.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/container.h	Sat Dec 13 15:58:58 2025 +0100
@@ -129,6 +129,10 @@
 
 Widget ui_container_prepare(UiContainerPrivate *container, UiLayout *layout, Arg *args, int *n);
 void ui_container_add(UiContainerPrivate *container, Widget widget);
+void ui_container_apply_grid_margin(
+        Arg *args,
+        int *n,
+        int margin_left, int margin_right, int margin_top, int margin_bottom);
 
 void ui_motif_tabview_select(UiMotifTabView *tabview, int tab);
 void ui_motif_tabview_add_tab(UiMotifTabView *tabview, int index, const char *name, Widget child);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/motif/entry.c	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,281 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "entry.h"
+
+
+UIWIDGET ui_spinbox_create(UiObject *obj, UiSpinBoxArgs *args) {
+    Arg xargs[16];
+    int n = 0;
+    
+    double min = args->min;
+    double max = args->max != 0 ? args->max : 1000;
+    
+    UiVar *var = NULL;
+    UiVarType vartype = 0;
+    if(args->varname) {
+        var = uic_get_var(obj->ctx, args->varname);
+        if(var) {
+            vartype = var->type;
+        } else {
+            var = uic_widget_var(obj->ctx, obj->ctx, args->rangevalue, args->varname, UI_VAR_RANGE);
+            vartype = UI_VAR_RANGE;
+        }
+    }
+    
+    if(!var) {
+        if(args->intvalue) {
+            var = uic_widget_var(obj->ctx, obj->ctx, args->intvalue, NULL, UI_VAR_INTEGER);
+            vartype = UI_VAR_INTEGER;
+        } else if(args->doublevalue) {
+            var = uic_widget_var(obj->ctx, obj->ctx, args->doublevalue, NULL, UI_VAR_DOUBLE);
+            vartype = UI_VAR_DOUBLE;
+        } else if(args->rangevalue) {
+            var = uic_widget_var(obj->ctx, obj->ctx, args->rangevalue, NULL, UI_VAR_RANGE);
+            vartype = UI_VAR_RANGE;
+        }
+    }
+    
+    if(vartype == UI_VAR_RANGE) {
+        UiRange *r = var->value;
+        min = r->min;
+        max = r->max;
+    }
+    if(args->step == 0) {
+        args->step = 1;
+    }
+    
+    UiContainerPrivate *ctn = ui_obj_container(obj);
+    UiLayout layout = UI_ARGS2LAYOUT(args);
+
+    
+    XtSetArg(xargs[n], XmNminimumValue, 0); n++;
+    XtSetArg(xargs[n], XmNmaximumValue, 100); n++;
+    XtSetArg(xargs[n], XmNincrementValue, 1); n++;
+    XtSetArg(xargs[n], XmNspinBoxChildType, XmNUMERIC); n++;
+    
+    Widget parent = ui_container_prepare(ctn, &layout, xargs, &n);
+    
+    char *name = args->name ? (char*)args->name : "button";
+    Widget spinbox = XmCreateSimpleSpinBox(parent, name, xargs, n);
+    XtManageChild(spinbox);
+    ui_container_add(ctn, spinbox);
+    
+    ui_set_widget_groups(obj->ctx, spinbox, args->states);
+    
+    WidgetList children;
+    Cardinal num_children;
+    unsigned char type;
+    
+    Widget textfield = NULL;
+    XtVaGetValues(
+            spinbox,
+            XmNchildren, &children,
+            XmNnumChildren, &num_children,
+            NULL);
+
+    for(int i = 0;i<num_children;i++) {
+        XtVaGetValues(children[i], XmNspinBoxChildType, &type, NULL);
+        Widget w = children[i];
+        if(type == XmNUMERIC) {
+            textfield = children[i];
+        }
+    }
+     
+    UiSpinBox *data = malloc(sizeof(UiSpinBox));
+    data->obj = obj;
+    data->textfield = textfield;
+    data->var = var;
+    data->vartype = vartype;
+    data->obs = NULL;
+    data->onchange = args->onchange;
+    data->onchangedata = args->onchangedata;
+    data->value = 0;
+    data->min = min;
+    data->max = max;
+    data->increment = args->step;
+    data->digits = args->digits;
+    
+    UiObserver **obs = NULL;
+    if(var) {
+        double value = 0;
+        switch(vartype) {
+            default: break;
+            case UI_VAR_INTEGER: {
+                UiInteger *i = var->value;
+                i->get = ui_spinbutton_getint;
+                i->set = ui_spinbutton_setint;
+                i->obj = data;
+                value = (double)i->value;
+                obs = &i->observers;
+                break;
+            }
+            case UI_VAR_DOUBLE: {
+                UiDouble *d = var->value;
+                d->get = ui_spinbutton_getdouble;
+                d->set = ui_spinbutton_setdouble;
+                d->obj = data;
+                value = d->value;
+                obs = &d->observers;
+                break;
+            }
+            case UI_VAR_RANGE: {
+                UiRange *r = var->value;
+                r->get = ui_spinbutton_getrangeval;
+                r->set = ui_spinbutton_setrangeval;
+                r->setrange = ui_spinbutton_setrange;
+                r->setextent = ui_spinbutton_setextent;
+                r->obj = data;
+                value = r->value;
+                obs = &r->observers;
+                break;
+            }
+        }
+        ui_spinbox_set_value(data, value);
+    }
+    data->obs = obs;
+    
+    XtAddCallback(
+            spinbox,
+            XmNvalueChangedCallback,
+            (XtCallbackProc)ui_spinbox_value_changed,
+            data);
+    
+    XtAddCallback(
+            spinbox,
+            XmNdestroyCallback,
+            (XtCallbackProc)ui_destroy_data,
+            data);
+    
+    XmTextFieldSetString(textfield, "0");
+    
+    
+    return spinbox;
+}
+
+void ui_spinbox_set_value(UiSpinBox *spinbox, double value) {
+    if(value < spinbox->min) {
+        value = spinbox->min;
+    }
+    if(value > spinbox->max) {
+        value = spinbox->max;
+    }
+    
+    char buf[32];
+    snprintf(buf, 32, "%.*f", spinbox->digits, spinbox->value);
+    XmTextFieldSetString(spinbox->textfield, buf);
+    spinbox->value = value;
+}
+
+void ui_spinbox_value_changed(Widget widget, UiSpinBox *spinbox, XmSpinBoxCallbackStruct *cb) {
+    Boolean update_value = TRUE;
+    double value = spinbox->value;
+    switch(cb->reason) {
+        case XmCR_OK: {
+            update_value = FALSE;
+            break;
+        }
+        case XmCR_SPIN_NEXT: {
+            value += spinbox->increment;
+            break;
+        }
+        case XmCR_SPIN_PRIOR: {
+            value -= spinbox->increment;
+            break;
+        }
+    }
+    
+    if(update_value) {
+        ui_spinbox_set_value(spinbox, value);
+        
+        UiEvent event;
+        event.obj = spinbox->obj;
+        event.window = event.obj->window;
+        event.document = event.obj->ctx->document;
+        event.eventdata = NULL;
+        event.eventdatatype = 0;
+        event.intval = (int64_t)value;
+        event.set = ui_get_setop();
+        
+        if(spinbox->onchange) {
+            spinbox->onchange(&event, spinbox->onchangedata);
+        }
+
+        UiObserver *obs = *spinbox->obs;
+        ui_notify_evt(*spinbox->obs, &event);
+    }
+}
+
+int64_t ui_spinbutton_getint(UiInteger *i) {
+    UiSpinBox *spinbox = i->obj;
+    i->value = (int64_t)spinbox->value;
+    return i->value;
+}
+
+void ui_spinbutton_setint(UiInteger *i, int64_t val) {
+    UiSpinBox *spinbox = i->obj;
+    ui_spinbox_set_value(spinbox, (double)val);
+    i->value = spinbox->value;
+}
+
+double ui_spinbutton_getdouble(UiDouble *d) {
+    UiSpinBox *spinbox = d->obj;
+    d->value = spinbox->value;
+    return d->value;
+}
+
+void ui_spinbutton_setdouble(UiDouble *d, double val) {
+    UiSpinBox *spinbox = d->obj;
+    ui_spinbox_set_value(spinbox, val);
+    d->value = spinbox->value;
+}
+
+double ui_spinbutton_getrangeval(UiRange *r) {
+    UiSpinBox *spinbox = r->obj;
+    r->value = spinbox->value;
+    return r->value;
+}
+
+void ui_spinbutton_setrangeval(UiRange *r, double val) {
+    UiSpinBox *spinbox = r->obj;
+    ui_spinbox_set_value(spinbox, val);
+    r->value = spinbox->value;
+}
+void ui_spinbutton_setrange(UiRange *r, double min, double max) {
+    UiSpinBox *spinbox = r->obj;
+    spinbox->min = min;
+    spinbox->max = max;
+    r->min = min;
+    r->max = max;
+}
+
+void ui_spinbutton_setextent(UiRange *r, double extent) {
+    UiSpinBox *spinbox = r->obj;
+    spinbox->increment = extent;
+    r->extent = extent;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/motif/entry.h	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,77 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef ENTRY_H
+#define ENTRY_H
+
+#include "../ui/entry.h"
+#include "container.h"
+#include "toolkit.h"
+
+#include <Xm/SSpinB.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct UiSpinBox {
+    UiObject *obj;
+    Widget textfield;
+    UiVar *var;
+    UiVarType vartype;
+    UiObserver **obs;
+    ui_callback onchange;
+    void* onchangedata;
+    double value;
+    double min;
+    double max;
+    double increment;
+    int digits;
+} UiSpinBox;
+
+void ui_spinbox_set_value(UiSpinBox *spinbox, double value);
+    
+void ui_spinbox_value_changed(Widget widget, UiSpinBox *spinbox, XmSpinBoxCallbackStruct *cb);
+
+int64_t ui_spinbutton_getint(UiInteger *i);
+void ui_spinbutton_setint(UiInteger *i, int64_t val);
+
+double ui_spinbutton_getdouble(UiDouble *d);
+void ui_spinbutton_setdouble(UiDouble *d, double val);
+
+double ui_spinbutton_getrangeval(UiRange *r);
+void ui_spinbutton_setrangeval(UiRange *r, double val);
+void ui_spinbutton_setrange(UiRange *r, double min, double max);
+void ui_spinbutton_setextent(UiRange *r, double extent);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ENTRY_H */
+
--- a/ui/motif/label.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/label.c	Sat Dec 13 15:58:58 2025 +0100
@@ -110,15 +110,15 @@
 }
 
 void ui_label_set(UiString *s, const char *str) {
+    Widget w = s->obj;
+    XmString s1 = XmStringCreateLocalized(str ? (char*)str : "");
+    XtVaSetValues(w, XmNlabelString, s1, NULL);
+    XmStringFree(s1);
     if(s->value.free) {
         s->value.free(s->value.ptr);
         s->value.free = NULL;
         s->value.ptr = NULL;
     }
-    Widget w = s->obj;
-    XmString s1 = XmStringCreateLocalized(str ? (char*)str : "");
-    XtVaSetValues(w, XmNlabelString, s1, NULL);
-    XmStringFree(s1);
 }
 
 /* -------------------------- progressbar/spiner -------------------------- */
--- a/ui/motif/list.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/list.c	Sat Dec 13 15:58:58 2025 +0100
@@ -28,6 +28,7 @@
 
 #include <stdio.h>
 #include <stdlib.h>
+#include <Xm/Xm.h>
 
 #include "container.h"
 
@@ -62,6 +63,9 @@
     } else {
         XtSetArg(xargs[n], XmNselectionPolicy, XmSINGLE_SELECT); n++;
     }
+    if(args->height > 0) {
+        XtSetArg(xargs[n], XmNheight, args->height); n++;
+    }
     
     char *name = args->name ? (char*)args->name : "listview";
     Widget parent = ui_container_prepare(ctn, &layout, xargs, &n);
@@ -91,6 +95,8 @@
     listview->onselection = args->onselection;
     listview->onselectiondata = args->onselectiondata;
     
+    char **static_elements = args->static_elements;
+    size_t static_nelm = args->static_nelm;
     if(var) {
         UiList *list = var->value;
         list->obj = listview;
@@ -98,6 +104,23 @@
         list->getselection = ui_listview_getselection;
         list->setselection = ui_listview_setselection;
         ui_listview_update(list, 0);
+    } else if(static_elements && static_nelm > 0) {
+        XmStringTable items = calloc(static_nelm, sizeof(XmString));
+        for(int i=0;i<static_nelm;i++) {
+            items[i] = XmStringCreateLocalized(static_elements[i]);
+        }
+        XtVaSetValues(
+                listview->widget,
+                XmNitems, items,
+                XmNitemCount,
+                static_nelm,
+                NULL);
+        for (int i=0;i<static_nelm;i++) {
+            XmStringFree(items[i]);
+        }
+        free(items);
+        listview->getvalue = getvalue_wrapper;
+        listview->getvaluedata = ui_strmodel_getvalue;
     }
     
     XtAddCallback(
@@ -114,19 +137,36 @@
     XtAddCallback(
                 widget,
                 XmNextendedSelectionCallback,
-                (XtCallbackProc)ui_listview_selection,
+                (XtCallbackProc)ui_listview_selection_changed,
                 listview);
     XtAddCallback(
                 widget,
                 XmNsingleSelectionCallback,
-                (XtCallbackProc)ui_listview_selection,
+                (XtCallbackProc)ui_listview_selection_changed,
                 listview);
     
     return widget;
 }
 
+void ui_listview_select(UIWIDGET listview, int index) {
+    XmListDeselectAllItems(listview);
+    XmListSelectPos(listview, index+1, False);
+}
+
+int ui_listview_selection(UIWIDGET listview) {
+    int *selpositions = NULL;
+    int numpos = 0;
+    XtVaGetValues(listview, XmNselectedPositions, &selpositions, XmNselectedPositionCount, &numpos, NULL);
+    return numpos > 0 ? selpositions[0] : -1;
+}
+
 void ui_listview_destroy(Widget w, UiListView *listview, XtPointer d) {
-    // TODO
+    ui_listselection_free(listview->current_selection);
+    if(listview->model) {
+        ui_model_remove_observer(listview->model, listview);
+        ui_model_unref(listview->model);
+    }
+    free(listview);
 }
 
 static void list_callback(UiObject *obj, UiListSelection sel, ui_callback callback, void *userdata) {
@@ -144,8 +184,12 @@
     UiListSelection sel = { cb->selected_item_count, NULL };
     if(sel.count > 0) {
         sel.rows = calloc(sel.count, sizeof(int));
-        for(int i=0;i<sel.count;i++) {
-            sel.rows[i] = cb->selected_item_positions[i]-1;
+        if(sel.count == 1) {
+            sel.rows[0] = cb->item_position-1;
+        } else if(cb->selected_item_positions) {
+            for(int i=0;i<sel.count;i++) {
+                sel.rows[i] = cb->selected_item_positions[i]-1;
+            }
         }
     }
     free(listview->current_selection.rows);
@@ -159,7 +203,7 @@
     }
 }
 
-void ui_listview_selection(Widget w, UiListView *listview, XmListCallbackStruct *cb) {
+void ui_listview_selection_changed(Widget w, UiListView *listview, XmListCallbackStruct *cb) {
     listview_save_selection(listview, cb);
     if(listview->onselection) {
         list_callback(listview->obj, listview->current_selection, listview->onselection, listview->onselectiondata);
@@ -209,10 +253,18 @@
 
 UiListSelection ui_listview_getselection(UiList *list) {
     UiListView *listview = list->obj;
-    UiListSelection sel = { listview->current_selection.count, NULL };
-    if(sel.count > 0) {
-        sel.rows = calloc(sel.count, sizeof(int));
-        memcpy(sel.rows, listview->current_selection.rows, sel.count*sizeof(int));
+    UiListSelection sel = { 0, NULL };
+    int *selpositions = NULL;
+    int numpos = 0;
+    XtVaGetValues(listview->widget, XmNselectedPositions, &selpositions, XmNselectedPositionCount, &numpos, NULL);
+    if(numpos > 0) {
+        sel.rows = calloc(numpos, sizeof(int));
+        sel.count = numpos;
+        memcpy(sel.rows, selpositions, numpos*sizeof(int));
+        // motif selected positions start at index 1 -> translate positions
+        for(int i=0;i<numpos;i++) {
+            sel.rows[i]--;
+        }
     }
     return sel;
 }
@@ -233,7 +285,7 @@
 
 /* ------------------------------- Drop Down ------------------------------- */
 
-static void ui_dropdown_selection(
+static void ui_dropdown_selection_changed(
         Widget w,
         UiListView *listview,
         XmComboBoxCallbackStruct *cb)
@@ -260,7 +312,7 @@
     }
 }
 
-UIWIDGET ui_combobox_create(UiObject* obj, UiListArgs *args) {
+UIWIDGET ui_dropdown_create(UiObject* obj, UiListArgs *args) {
     Arg xargs[16];
     int n = 0;
     
@@ -295,13 +347,32 @@
     listview->onselection = args->onselection;
     listview->onselectiondata = args->onselectiondata;
     
+    char **static_elements = args->static_elements;
+    size_t static_nelm = args->static_nelm;
     if(var) {
         UiList *list = var->value;
         list->obj = listview;
         list->update = ui_listview_update;
-        list->getselection = ui_listview_getselection;
-        list->setselection = ui_listview_setselection;
+        list->getselection = ui_dropdown_getselection;
+        list->setselection = ui_dropdown_setselection;
         ui_listview_update(list, 0);
+    } else if(static_elements && static_nelm > 0) {
+        XmStringTable items = calloc(static_nelm, sizeof(XmString));
+        for(int i=0;i<static_nelm;i++) {
+            items[i] = XmStringCreateLocalized(static_elements[i]);
+        }
+        XtVaSetValues(
+                listview->widget,
+                XmNitems, items,
+                XmNitemCount,
+                static_nelm,
+                NULL);
+        for (int i=0;i<static_nelm;i++) {
+            XmStringFree(items[i]);
+        }
+        free(items);
+        listview->getvalue = getvalue_wrapper;
+        listview->getvaluedata = ui_strmodel_getvalue;
     }
     
     XtAddCallback(
@@ -312,8 +383,39 @@
     XtAddCallback(
                 widget,
                 XmNselectionCallback,
-                (XtCallbackProc)ui_dropdown_selection,
+                (XtCallbackProc)ui_dropdown_selection_changed,
                 listview);
     
     return widget;
 }
+
+void ui_dropdown_setselection(UiList *list, UiListSelection selection) {
+    UiListView *listview = list->obj;
+    if(selection.count > 0) {
+        XtVaSetValues(listview->widget, XmNselectedPosition, selection.rows[0], NULL);
+    } else {
+        XtVaSetValues(listview->widget, XmNselectedPosition, 0, NULL);
+    }
+}
+
+UiListSelection ui_dropdown_getselection(UiList *list) {
+    UiListView *listview = list->obj;
+    int pos = -1;
+    XtVaGetValues(listview->widget, XmNselectedPosition, &pos, NULL);
+    UiListSelection sel = { 0, NULL };
+    if(pos >= 0) {
+        sel.rows = malloc(sizeof(int));
+        sel.rows[0] = pos;
+        sel.count = 1;
+    }
+    return sel;
+}
+
+void ui_dropdown_select(UIWIDGET dropdown, int index) {
+    XtVaSetValues(dropdown, XmNselectedPosition, index, NULL);
+}
+
+int ui_dropdown_selection(UIWIDGET dropdown) {
+    int pos = -1;
+    XtVaGetValues(dropdown, XmNselectedPosition, &pos, NULL);
+}
--- a/ui/motif/list.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/list.h	Sat Dec 13 15:58:58 2025 +0100
@@ -30,7 +30,7 @@
 #define	LIST_H
 
 #include "toolkit.h"
-#include "../ui/tree.h"
+#include "../ui/list.h"
 #include "../common/context.h"
 
 #ifdef	__cplusplus
@@ -63,12 +63,15 @@
 void ui_listview_destroy(Widget w, UiListView *listview, XtPointer d);
 
 void ui_listview_activate(Widget w, UiListView *listview, XmListCallbackStruct *cb);
-void ui_listview_selection(Widget w, UiListView *listview, XmListCallbackStruct *cb);
+void ui_listview_selection_changed(Widget w, UiListView *listview, XmListCallbackStruct *cb);
 
 void ui_listview_update(UiList *list, int i);
 UiListSelection ui_listview_getselection(UiList *list);
 void ui_listview_setselection(UiList *list, UiListSelection selection);
 
+void ui_dropdown_setselection(UiList *list, UiListSelection selection);
+UiListSelection ui_dropdown_getselection(UiList *list);
+
 void* ui_strmodel_getvalue(void *elm, int column);
 
 #ifdef	__cplusplus
--- a/ui/motif/menu.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/menu.c	Sat Dec 13 15:58:58 2025 +0100
@@ -33,7 +33,6 @@
 #include "menu.h"
 #include "button.h"
 #include "toolkit.h"
-#include "stock.h"
 #include "container.h"
 #include "../common/context.h"
 #include "../common/menu.h"
@@ -55,10 +54,10 @@
     /* UI_MENU_SEPARATOR       */ add_menuseparator_widget
 };
 
-void ui_create_menubar(UiObject *obj, Widget window) {
+Widget ui_create_menubar(UiObject *obj, Widget window) {
     UiMenu *menus_begin = uic_get_menu_list();
     if(!menus_begin) {
-        return;
+        return NULL;
     }
     
     Widget menubar = XmCreateMenuBar(window, "menubar", NULL, 0);
@@ -70,6 +69,8 @@
         add_menu_widget(menubar, 0, &menu->item, obj);
         ls = (UiMenu*)ls->item.next;
     }
+    
+    return menubar;
 }
 
 void ui_add_menu_items(Widget parent, int i, UiMenu *menu, UiObject *obj) {
@@ -93,16 +94,15 @@
         XtSetArg(args[n], XmNlabelString, s); n++;
     }
     
-    Widget submenu = XmVaCreateSimplePulldownMenu(parent, "menu_pulldown", i, NULL, NULL);
+    Widget submenu = XmCreatePulldownMenu(parent, "menu_pulldown", NULL, 0);
     XtSetArg(args[n], XmNsubMenuId, submenu); n++;
-    Widget menuItem = XtCreateManagedWidget(
+    (void)XtCreateManagedWidget(
             "menuitem",
             xmCascadeButtonWidgetClass,
             parent,
             args,
             n);
     
-    
     if(s) {
         XmStringFree(s);
     }
@@ -145,11 +145,11 @@
        XtAddCallback(
                 mitem,
                 XmNdestroyCallback,
-                (XtCallbackProc)ui_destroy_eventdata,
+                (XtCallbackProc)ui_destroy_data,
                 eventdata);
     }
     
-    ui_set_widget_groups(obj->ctx, mitem, it->groups);
+    ui_set_widget_groups(obj->ctx, mitem, it->states);
 }
 
 void add_menuseparator_widget(Widget p, int i, UiMenuItemI *item, UiObject *obj) {
@@ -181,7 +181,7 @@
     
     ui_bind_togglebutton(obj, checkbox, it->varname, NULL, it->callback, it->userdata, 0);
     
-    ui_set_widget_groups(obj->ctx, checkbox, it->groups);
+    ui_set_widget_groups(obj->ctx, checkbox, it->states);
 }
 
 void add_radioitem_widget(Widget p, int index, UiMenuItemI *item, UiObject *obj) {
@@ -337,7 +337,7 @@
             XtAddCallback(
                     mitem,
                     XmNdestroyCallback,
-                    (XtCallbackProc)ui_destroy_eventdata,
+                    (XtCallbackProc)ui_destroy_data,
                     eventdata);
         }
         
--- a/ui/motif/menu.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/menu.h	Sat Dec 13 15:58:58 2025 +0100
@@ -52,7 +52,7 @@
     
 typedef void(*ui_menu_add_f)(Widget, int, UiMenuItemI*, UiObject*);
     
-void ui_create_menubar(UiObject *obj, Widget window);
+Widget ui_create_menubar(UiObject *obj, Widget window);
 void ui_add_menu_widget(Widget parent, int i, UiMenuItemI *item, UiObject *obj);
 
 void add_menu_widget(Widget parent, int i, UiMenuItemI *item, UiObject *obj);
--- a/ui/motif/objs.mk	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/objs.mk	Sat Dec 13 15:58:58 2025 +0100
@@ -30,7 +30,6 @@
 MOTIF_OBJPRE = $(OBJ_DIR)$(MOTIF_SRC_DIR)
 
 MOTIFOBJ = toolkit.o
-MOTIFOBJ += stock.o
 MOTIFOBJ += window.o
 MOTIFOBJ += widget.o
 MOTIFOBJ += container.o
@@ -39,12 +38,15 @@
 MOTIFOBJ += button.o
 MOTIFOBJ += label.o
 MOTIFOBJ += text.o
+MOTIFOBJ += pathbar.o
 MOTIFOBJ += list.o
 MOTIFOBJ += graphics.o
 MOTIFOBJ += range.o
 MOTIFOBJ += dnd.o
 MOTIFOBJ += image.o
 MOTIFOBJ += Grid.o
+MOTIFOBJ += entry.o
+MOTIFOBJ += Fsb.o
 
 TOOLKITOBJS += $(MOTIFOBJ:%=$(MOTIF_OBJPRE)%)
 TOOLKITSOURCE += $(MOTIFOBJ:%.o=motif/%.c)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/motif/pathbar.c	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,439 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "pathbar.h"
+
+#include <unistd.h>
+#include <cx/string.h>
+
+
+
+void pathbar_resize(Widget w, PathBar *p, XtPointer d)
+{
+    Dimension width, height;
+    XtVaGetValues(w, XmNwidth, &width, XmNheight, &height, NULL);
+    
+    Dimension *segW = (void*)XtCalloc(p->numSegments, sizeof(Dimension));
+    
+    Dimension maxHeight = 0;
+    
+    /* get width/height from all widgets */
+    Dimension pathWidth = 0;
+    for(int i=0;i<p->numSegments;i++) {
+        Dimension segWidth;
+        Dimension segHeight;
+        XtVaGetValues(p->pathSegments[i], XmNwidth, &segWidth, XmNheight, &segHeight, NULL);
+        segW[i] = segWidth;
+        pathWidth += segWidth;
+        if(segHeight > maxHeight) {
+            maxHeight = segHeight;
+        }
+    }
+    Dimension tfHeight;
+    XtVaGetValues(p->textfield, XmNheight, &tfHeight, NULL);
+    if(tfHeight > maxHeight) {
+        maxHeight = tfHeight;
+    }
+    
+    Boolean arrows = False;
+    if(pathWidth + 10 > width) {
+        arrows = True;
+        pathWidth += p->lw + p->rw;
+    }
+    
+    /* calc max visible widgets */
+    int start = 0;
+    if(arrows) {
+        Dimension vis = p->lw+p->rw;
+        for(int i=p->numSegments;i>0;i--) {
+            Dimension segWidth = segW[i-1];
+            if(vis + segWidth + 10 > width) {
+                start = i;
+                arrows = True;
+                break;
+            }
+            vis += segWidth;
+        }
+    } else {
+        p->shift = 0;
+    }
+    
+    int leftShift = 0;
+    if(p->shift < 0) {
+        if(start + p->shift < 0) {
+            leftShift = start;
+            start = 0;
+            p->shift = -leftShift;
+        } else {
+            leftShift = -p->shift; /* negative shift */
+            start += p->shift;
+        }
+    }
+    
+    int x = 0;
+    if(arrows) {
+        XtManageChild(p->left);
+        XtManageChild(p->right);
+        x = p->lw;
+    } else {
+        XtUnmanageChild(p->left);
+        XtUnmanageChild(p->right);
+    }
+    
+    for(int i=0;i<p->numSegments;i++) {
+        if(i >= start && i < p->numSegments - leftShift && !p->input) {
+            XtVaSetValues(p->pathSegments[i], XmNx, x, XmNy, 0, XmNheight, maxHeight, NULL);
+            x += segW[i];
+            XtManageChild(p->pathSegments[i]);
+        } else {
+            XtUnmanageChild(p->pathSegments[i]);
+        }
+    }
+    
+    if(arrows) {
+        XtVaSetValues(p->left, XmNx, 0, XmNy, 0, XmNheight, maxHeight, NULL);
+        XtVaSetValues(p->right, XmNx, x, XmNy, 0, XmNheight, maxHeight, NULL);
+    }
+    
+    free(segW);
+    
+    Dimension rw, rh;
+    XtMakeResizeRequest(w, width, maxHeight, &rw, &rh);
+    
+    XtVaSetValues(p->textfield, XmNwidth, rw, XmNheight, rh, NULL);
+}
+
+static void pathbarActivateTF(PathBar *p)
+{
+    XtUnmanageChild(p->left);
+    XtUnmanageChild(p->right);
+    XNETextSetSelection(p->textfield, 0, XNETextGetLastPosition(p->textfield), 0);
+    XtManageChild(p->textfield);
+    p->input = 1;
+
+    XmProcessTraversal(p->textfield, XmTRAVERSE_CURRENT);
+
+    pathbar_resize(p->widget, p, NULL);
+}
+
+void PathBarActivateTextfield(PathBar *p)
+{
+    p->focus = 1;
+    pathbarActivateTF(p);
+}
+
+void pathbar_input(Widget w, PathBar *p, XtPointer c)
+{
+    XmDrawingAreaCallbackStruct *cbs = (XmDrawingAreaCallbackStruct*)c;
+    XEvent *xevent = cbs->event;
+    
+    if (cbs->reason == XmCR_INPUT) {
+        if (xevent->xany.type == ButtonPress) {
+            p->focus = 0;
+            pathbarActivateTF(p);
+        }
+    }
+}
+
+void pathbar_losingfocus(Widget w, PathBar *p, XtPointer c)
+{
+    if(--p->focus < 0) {
+        p->input = False;
+        XtUnmanageChild(p->textfield);
+    }
+}
+
+static cxmutstr concat_path_s(cxstring base, cxstring path) {
+    if(!path.ptr) {
+        path = CX_STR("");
+    }
+    
+    int add_separator = 0;
+    if(base.length != 0 && base.ptr[base.length-1] == '/') {
+        if(path.ptr[0] == '/') {
+            base.length--;
+        }
+    } else {
+        if(path.length == 0 || path.ptr[0] != '/') {
+            add_separator = 1;
+        }
+    }
+    
+    cxmutstr url;
+    if(add_separator) {
+        url = cx_strcat(3, base, CX_STR("/"), path);
+    } else {
+        url = cx_strcat(2, base, path);
+    }
+    
+    return url;
+}
+
+char* pathbar_concat_path(const char *path1, const char *path2) {
+    return concat_path_s(cx_str(path1), cx_str(path2)).ptr;
+}
+
+void pathbar_pathinput(Widget w, PathBar *p, XtPointer d)
+{
+    char *newpath = XNETextGetString(p->textfield);
+    if(newpath) {
+        if(newpath[0] == '~') {
+            char *p = newpath+1;
+            char *home = getenv("HOME");
+            char *cp = pathbar_concat_path(home, p);
+            XtFree(newpath);
+            newpath = cp;
+        } else if(newpath[0] != '/') {
+            char curdir[2048];
+            curdir[0] = 0;
+            getcwd(curdir, 2048);
+            char *cp = pathbar_concat_path(curdir, newpath);
+            XtFree(newpath);
+            newpath = cp;
+        }
+        
+        /* update path */
+        PathBarSetPath(p, newpath);
+        if(p->updateDir) {
+            p->updateDir(p->updateDirData, newpath, -1);
+        }
+        XtFree(newpath);
+        
+        /* hide textfield and show path as buttons */
+        XtUnmanageChild(p->textfield);
+        pathbar_resize(p->widget, p, NULL);
+        
+        if(p->focus_widget) {
+            XmProcessTraversal(p->focus_widget, XmTRAVERSE_CURRENT);
+        }
+    }
+}
+
+void pathbar_shift_left(Widget w, PathBar *p, XtPointer d)
+{
+    p->shift--;
+    pathbar_resize(p->widget, p, NULL);
+}
+
+void pathbar_shift_right(Widget w, PathBar *p, XtPointer d)
+{
+    if(p->shift < 0) {
+        p->shift++;
+    }
+    pathbar_resize(p->widget, p, NULL);
+}
+
+static void pathTextEH(Widget widget, XtPointer data, XEvent *event, Boolean *dispatch) {
+    PathBar *pb = data;
+    if(event->type == KeyReleaseMask) {
+        if(event->xkey.keycode == 9) {
+            XtUnmanageChild(pb->textfield);
+            pathbar_resize(pb->widget, pb, NULL);
+            *dispatch = False;
+        } else if(event->xkey.keycode == 36) {
+            pathbar_pathinput(pb->textfield, pb, NULL);
+            *dispatch = False;
+        }
+    }
+}
+
+PathBar* CreatePathBar(Widget parent, ArgList args, int n)
+{
+    PathBar *bar = (PathBar*)XtMalloc(sizeof(PathBar));
+    bar->path = NULL;
+    bar->updateDir = NULL;
+    bar->updateDirData = NULL;
+    
+    bar->focus_widget = NULL;
+    
+    bar->getpathelm = NULL;
+    bar->getpathelmdata = NULL;
+    bar->current_pathelms = NULL;
+    
+    bar->shift = 0;
+    
+    XtSetArg(args[n], XmNmarginWidth, 0); n++;
+    XtSetArg(args[n], XmNmarginHeight, 0); n++;
+    bar->widget = XmCreateDrawingArea(parent, "pathbar", args, n);
+    XtAddCallback(
+            bar->widget,
+            XmNresizeCallback,
+            (XtCallbackProc)pathbar_resize,
+            bar);
+    XtAddCallback(
+            bar->widget,
+            XmNinputCallback,
+            (XtCallbackProc)pathbar_input,
+            bar);
+    
+    Arg a[4];
+    XtSetArg(a[0], XmNshadowThickness, 0);
+    XtSetArg(a[1], XmNx, 0);
+    XtSetArg(a[2], XmNy, 0);
+    bar->textfield = XNECreateText(bar->widget, "pbtext", a, 3);
+    bar->input = 0;
+    XtAddCallback(
+            bar->textfield,
+            XmNlosingFocusCallback,
+            (XtCallbackProc)pathbar_losingfocus,
+            bar);
+    XtAddCallback(bar->textfield, XmNactivateCallback,
+                 (XtCallbackProc)pathbar_pathinput, bar);
+    XtAddEventHandler(bar->textfield, KeyPressMask | KeyReleaseMask, FALSE, pathTextEH, bar);
+    
+    XtSetArg(a[0], XmNarrowDirection, XmARROW_LEFT);
+    bar->left = XmCreateArrowButton(bar->widget, "pbbutton", a, 1);
+    XtSetArg(a[0], XmNarrowDirection, XmARROW_RIGHT);
+    bar->right = XmCreateArrowButton(bar->widget, "pbbutton", a, 1);
+    XtAddCallback(
+                bar->left,
+                XmNactivateCallback,
+                (XtCallbackProc)pathbar_shift_left,
+                bar);
+    XtAddCallback(
+                bar->right,
+                XmNactivateCallback,
+                (XtCallbackProc)pathbar_shift_right,
+                bar);
+    
+    Pixel bg;
+    XtVaGetValues(bar->textfield, XmNbackground, &bg, NULL);
+    XtVaSetValues(bar->widget, XmNbackground, bg, NULL);
+    
+    XtManageChild(bar->left);
+    XtManageChild(bar->right);
+    
+    XtVaGetValues(bar->left, XmNwidth, &bar->lw, NULL);
+    XtVaGetValues(bar->right, XmNwidth, &bar->rw, NULL);
+    
+    bar->segmentAlloc = 16;
+    bar->numSegments = 0;
+    bar->pathSegments = (Widget*)XtCalloc(16, sizeof(Widget));
+    
+    bar->selection = 0;
+    
+    return bar;
+}
+
+void PathBarChangeDir(Widget w, PathBar *bar, XtPointer c)
+{
+    XmToggleButtonSetState(bar->pathSegments[bar->selection], False, False);
+    
+    int i;
+    for(i=0;i<bar->numSegments;i++) {  
+        if(bar->pathSegments[i] == w) {
+            bar->selection = i;
+            XmToggleButtonSetState(w, True, False);
+            break;
+        }
+    }
+    
+    UiPathElm elm = bar->current_pathelms[i];
+    cxmutstr path = cx_strdup(cx_strn(elm.path, elm.path_len));
+    if(bar->updateDir) {
+        XNETextSetString(bar->textfield, path.ptr);
+        bar->updateDir(bar->updateDirData, path.ptr, i);
+    }
+    free(path.ptr);
+}
+
+static void ui_pathelm_destroy(UiPathElm *elms, size_t nelm) {
+    for(int i=0;i<nelm;i++) {
+        free(elms[i].name);
+        free(elms[i].path);
+    }
+    free(elms);
+}
+
+void PathBarSetPath(PathBar *bar, const char *path)
+{
+    if(bar->path) {
+        free(bar->path);
+    }
+    bar->path = strdup(path);
+    
+    for(int i=0;i<bar->numSegments;i++) {
+        XtDestroyWidget(bar->pathSegments[i]);
+    }
+    XtUnmanageChild(bar->textfield);
+    XtManageChild(bar->left);
+    XtManageChild(bar->right);
+    bar->input = False;
+    
+    Arg args[4];
+    XmString str;
+    
+    bar->numSegments = 0;
+    
+    ui_pathelm_destroy(bar->current_pathelms, bar->numSegments);
+    size_t nelm = 0;
+    UiPathElm* path_elm = bar->getpathelm(bar->path, strlen(bar->path), &nelm, bar->getpathelmdata);
+    if (!path_elm) {
+        return;
+    }
+    bar->current_pathelms = path_elm;
+    bar->numSegments = nelm;
+    bar->pathSegments = realloc(bar->pathSegments, nelm * sizeof(Widget*));
+    
+    for(int i=0;i<nelm;i++) {
+        UiPathElm elm = path_elm[i];
+        
+        cxmutstr name = cx_strdup(cx_strn(elm.name, elm.name_len));
+        str = XmStringCreateLocalized(elm.name);
+        free(name.ptr);
+        
+        XtSetArg(args[0], XmNlabelString, str);
+        XtSetArg(args[1], XmNfillOnSelect, True);
+        XtSetArg(args[2], XmNindicatorOn, False);
+        Widget button = XmCreateToggleButton(bar->widget, "pbbutton", args, 3);
+        XtAddCallback(
+                button,
+                XmNvalueChangedCallback,
+                (XtCallbackProc)PathBarChangeDir,
+                bar);
+        XmStringFree(str);
+        
+        bar->pathSegments[i] = button;
+    }
+    
+    bar->selection = bar->numSegments-1;
+    XmToggleButtonSetState(bar->pathSegments[bar->selection], True, False);
+    
+    XNETextSetString(bar->textfield, (char*)path);
+    XNETextSetInsertionPosition(bar->textfield, XNETextGetLastPosition(bar->textfield));
+    
+    pathbar_resize(bar->widget, bar, NULL);
+}
+
+void PathBarDestroy(PathBar *pathbar) {
+    if(pathbar->path) {
+        XtFree(pathbar->path);
+    }
+    XtFree((void*)pathbar->pathSegments);
+    XtFree((void*)pathbar);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/motif/pathbar.h	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,95 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef PATHBAR_H
+#define PATHBAR_H
+
+#include <Xm/XmAll.h>
+
+#include "../ui/text.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define XNECreateText(parent,name,args,count)   XmCreateTextField(parent,name,args,count)
+#define XNETextSetString(widget,value)          XmTextFieldSetString(widget,value)
+#define XNETextGetString(widget)                XmTextFieldGetString(widget)
+#define XNETextGetLastPosition(widget)          XmTextFieldGetLastPosition(widget)  
+#define XNETextSetInsertionPosition(widget, i)  XmTextFieldSetInsertionPosition(widget, i)  
+#define XNETextSetSelection(w, f, l, t)         XmTextFieldSetSelection(w, f, l, t)
+
+typedef void(*updatedir_callback)(void*,char*,int);
+
+typedef struct PathBar {  
+    Widget widget;
+    Widget textfield;
+    
+    Widget focus_widget;
+    
+    Widget left;
+    Widget right;
+    Dimension lw;
+    Dimension rw;
+    
+    int shift;
+    
+    UiPathElm *current_pathelms;
+    Widget *pathSegments;
+    size_t numSegments;
+    size_t segmentAlloc;
+    
+    char *path;
+    int selection;
+    Boolean input;
+    
+    int focus;
+    
+    Boolean disableResize;
+    
+    updatedir_callback updateDir;
+    void *updateDirData;
+    
+    ui_pathelm_func getpathelm;
+    void *getpathelmdata;
+} PathBar;
+
+PathBar* CreatePathBar(Widget parent, ArgList args, int n);
+void PathBarSetPath(PathBar *bar, const char *path);
+void PathBarDestroy(PathBar *bar);
+
+void pathbar_resize(Widget w, PathBar *p, XtPointer d);
+
+char* pathbar_concat_path(const char *path1, const char *path2);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PATHBAR_H */
+
--- a/ui/motif/text.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/text.c	Sat Dec 13 15:58:58 2025 +0100
@@ -32,6 +32,9 @@
 
 #include "text.h"
 #include "container.h"
+#include "pathbar.h"
+
+#include "../common/utils.h"
 
 #include <cx/string.h>
 
@@ -210,9 +213,9 @@
     int sel = left < right ? 1 : 0;
     if(sel != textarea->last_selection_state) {
         if(sel) {
-            ui_set_group(textarea->obj->ctx, UI_GROUP_SELECTION);
+            ui_set_state(textarea->obj->ctx, UI_GROUP_SELECTION);
         } else {
-            ui_unset_group(textarea->obj->ctx, UI_GROUP_SELECTION);
+            ui_unset_state(textarea->obj->ctx, UI_GROUP_SELECTION);
         }
     }
     textarea->last_selection_state = sel;
@@ -405,10 +408,20 @@
     XtManageChild(textfield);
     ui_container_add(ctn, textfield);
     
-    ui_set_widget_groups(obj->ctx, textfield, args->groups);
+    ui_set_widget_groups(obj->ctx, textfield, args->states);
+    
+    UiEventDataExt *eventdata = malloc(sizeof(UiEventDataExt));
+    memset(eventdata, 0, sizeof(UiEventDataExt));
+    eventdata->obj = obj;
+    eventdata->callback = args->onactivate;
+    eventdata->userdata = args->onactivatedata;
+    eventdata->callback2 = args->onchange;
+    eventdata->userdata2 = args->onchangedata;
     
     UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->value, args->varname, UI_VAR_STRING);
     if(var) {
+        eventdata->customdata0 = var;
+        
         UiString *value = (UiString*)var->value;
         value->obj = textfield;
         value->get = ui_textfield_get;
@@ -419,9 +432,54 @@
         }
     }
     
+    XtAddCallback(
+            textfield,
+            XmNactivateCallback,
+            (XtCallbackProc)ui_textfield_activate,
+            eventdata);
+    XtAddCallback(
+            textfield,
+            XmNvalueChangedCallback,
+            (XtCallbackProc)ui_textfield_value_changed,
+            eventdata);
+    XtAddCallback(
+            textfield,
+            XmNdestroyCallback,
+            (XtCallbackProc)ui_destroy_data,
+            eventdata);
+    
     return textfield;
 }
 
+static void textfield_event(UiEventDataExt *eventdata, ui_callback callback, void *userdata) {
+    if(callback) {
+        UiVar *var = eventdata->customdata0;
+        UiString *value = var ? var->value : NULL;
+        
+        UiEvent e;
+        e.obj = eventdata->obj;
+        e.window = e.obj->window;
+        e.document = e.obj->ctx->document;
+        e.eventdata = value;
+        e.eventdatatype = value ? UI_EVENT_DATA_TEXT_VALUE : 0;
+        e.intval = 0;
+        e.set = ui_get_setop();
+        callback(&e, userdata);
+    }
+}
+
+void ui_textfield_activate(Widget widget, XtPointer ud, XtPointer cb) {
+    UiEventDataExt *eventdata = ud;
+    textfield_event(ud, eventdata->callback, eventdata->userdata);
+}
+
+void ui_textfield_value_changed(Widget widget, XtPointer ud, XtPointer cb) {
+    UiEventDataExt *eventdata = ud;
+    if(ui_onchange_events_is_enabled()) {
+        textfield_event(ud, eventdata->callback2, eventdata->userdata2);
+    }
+}
+
 UIWIDGET ui_textfield_create(UiObject *obj, UiTextFieldArgs *args) {
     return create_textfield(obj, args, FALSE, FALSE);
 }
@@ -453,459 +511,6 @@
 }
 
 
-
-
-
-/* -------------------- path bar -------------------- */
-
-#define XNECreateText(parent,name,args,count)   XmCreateTextField(parent,name,args,count)
-#define XNETextSetString(widget,value)          XmTextFieldSetString(widget,value)
-#define XNETextGetString(widget)                XmTextFieldGetString(widget)
-#define XNETextGetLastPosition(widget)          XmTextFieldGetLastPosition(widget)  
-#define XNETextSetInsertionPosition(widget, i)  XmTextFieldSetInsertionPosition(widget, i)  
-#define XNETextSetSelection(w, f, l, t)         XmTextFieldSetSelection(w, f, l, t)
-
-typedef void(*updatedir_callback)(void*,char*,int);
-
-typedef struct PathBar {  
-    Widget widget;
-    Widget textfield;
-    
-    Widget focus_widget;
-    
-    Widget left;
-    Widget right;
-    Dimension lw;
-    Dimension rw;
-    
-    int shift;
-    
-    UiPathElm *current_pathelms;
-    Widget *pathSegments;
-    size_t numSegments;
-    size_t segmentAlloc;
-    
-    char *path;
-    int selection;
-    Boolean input;
-    
-    int focus;
-    
-    updatedir_callback updateDir;
-    void *updateDirData;
-    
-    ui_pathelm_func getpathelm;
-    void *getpathelmdata;
-} PathBar;
-
-void PathBarSetPath(PathBar *bar, const char *path);
-
-void pathbar_resize(Widget w, PathBar *p, XtPointer d)
-{
-    Dimension width, height;
-    XtVaGetValues(w, XmNwidth, &width, XmNheight, &height, NULL);
-    
-    Dimension *segW = (void*)XtCalloc(p->numSegments, sizeof(Dimension));
-    
-    Dimension maxHeight = 0;
-    
-    /* get width/height from all widgets */
-    Dimension pathWidth = 0;
-    for(int i=0;i<p->numSegments;i++) {
-        Dimension segWidth;
-        Dimension segHeight;
-        XtVaGetValues(p->pathSegments[i], XmNwidth, &segWidth, XmNheight, &segHeight, NULL);
-        segW[i] = segWidth;
-        pathWidth += segWidth;
-        if(segHeight > maxHeight) {
-            maxHeight = segHeight;
-        }
-    }
-    Dimension tfHeight;
-    XtVaGetValues(p->textfield, XmNheight, &tfHeight, NULL);
-    if(tfHeight > maxHeight) {
-        maxHeight = tfHeight;
-    }
-    
-    Boolean arrows = False;
-    if(pathWidth + 10 > width) {
-        arrows = True;
-        pathWidth += p->lw + p->rw;
-    }
-    
-    /* calc max visible widgets */
-    int start = 0;
-    if(arrows) {
-        Dimension vis = p->lw+p->rw;
-        for(int i=p->numSegments;i>0;i--) {
-            Dimension segWidth = segW[i-1];
-            if(vis + segWidth + 10 > width) {
-                start = i;
-                arrows = True;
-                break;
-            }
-            vis += segWidth;
-        }
-    } else {
-        p->shift = 0;
-    }
-    
-    int leftShift = 0;
-    if(p->shift < 0) {
-        if(start + p->shift < 0) {
-            leftShift = start;
-            start = 0;
-            p->shift = -leftShift;
-        } else {
-            leftShift = -p->shift; /* negative shift */
-            start += p->shift;
-        }
-    }
-    
-    int x = 0;
-    if(arrows) {
-        XtManageChild(p->left);
-        XtManageChild(p->right);
-        x = p->lw;
-    } else {
-        XtUnmanageChild(p->left);
-        XtUnmanageChild(p->right);
-    }
-    
-    for(int i=0;i<p->numSegments;i++) {
-        if(i >= start && i < p->numSegments - leftShift && !p->input) {
-            XtVaSetValues(p->pathSegments[i], XmNx, x, XmNy, 0, XmNheight, maxHeight, NULL);
-            x += segW[i];
-            XtManageChild(p->pathSegments[i]);
-        } else {
-            XtUnmanageChild(p->pathSegments[i]);
-        }
-    }
-    
-    if(arrows) {
-        XtVaSetValues(p->left, XmNx, 0, XmNy, 0, XmNheight, maxHeight, NULL);
-        XtVaSetValues(p->right, XmNx, x, XmNy, 0, XmNheight, maxHeight, NULL);
-    }
-    
-    free(segW);
-    
-    Dimension rw, rh;
-    XtMakeResizeRequest(w, width, maxHeight, &rw, &rh);
-    
-    XtVaSetValues(p->textfield, XmNwidth, rw, XmNheight, rh, NULL);
-}
-
-static void pathbarActivateTF(PathBar *p)
-{
-    XtUnmanageChild(p->left);
-    XtUnmanageChild(p->right);
-    XNETextSetSelection(p->textfield, 0, XNETextGetLastPosition(p->textfield), 0);
-    XtManageChild(p->textfield);
-    p->input = 1;
-
-    XmProcessTraversal(p->textfield, XmTRAVERSE_CURRENT);
-
-    pathbar_resize(p->widget, p, NULL);
-}
-
-void PathBarActivateTextfield(PathBar *p)
-{
-    p->focus = 1;
-    pathbarActivateTF(p);
-}
-
-void pathbar_input(Widget w, PathBar *p, XtPointer c)
-{
-    XmDrawingAreaCallbackStruct *cbs = (XmDrawingAreaCallbackStruct*)c;
-    XEvent *xevent = cbs->event;
-    
-    if (cbs->reason == XmCR_INPUT) {
-        if (xevent->xany.type == ButtonPress) {
-            p->focus = 0;
-            pathbarActivateTF(p);
-        }
-    }
-}
-
-void pathbar_losingfocus(Widget w, PathBar *p, XtPointer c)
-{
-    if(--p->focus < 0) {
-        p->input = False;
-        XtUnmanageChild(p->textfield);
-    }
-}
-
-static cxmutstr concat_path_s(cxstring base, cxstring path) {
-    if(!path.ptr) {
-        path = CX_STR("");
-    }
-    
-    int add_separator = 0;
-    if(base.length != 0 && base.ptr[base.length-1] == '/') {
-        if(path.ptr[0] == '/') {
-            base.length--;
-        }
-    } else {
-        if(path.length == 0 || path.ptr[0] != '/') {
-            add_separator = 1;
-        }
-    }
-    
-    cxmutstr url;
-    if(add_separator) {
-        url = cx_strcat(3, base, CX_STR("/"), path);
-    } else {
-        url = cx_strcat(2, base, path);
-    }
-    
-    return url;
-}
-
-static char* ConcatPath(const char *path1, const char *path2) {
-    return concat_path_s(cx_str(path1), cx_str(path2)).ptr;
-}
-
-void pathbar_pathinput(Widget w, PathBar *p, XtPointer d)
-{
-    char *newpath = XNETextGetString(p->textfield);
-    if(newpath) {
-        if(newpath[0] == '~') {
-            char *p = newpath+1;
-            char *home = getenv("HOME");
-            char *cp = ConcatPath(home, p);
-            XtFree(newpath);
-            newpath = cp;
-        } else if(newpath[0] != '/') {
-            char curdir[2048];
-            curdir[0] = 0;
-            getcwd(curdir, 2048);
-            char *cp = ConcatPath(curdir, newpath);
-            XtFree(newpath);
-            newpath = cp;
-        }
-        
-        /* update path */
-        PathBarSetPath(p, newpath);
-        if(p->updateDir) {
-            p->updateDir(p->updateDirData, newpath, -1);
-        }
-        XtFree(newpath);
-        
-        /* hide textfield and show path as buttons */
-        XtUnmanageChild(p->textfield);
-        pathbar_resize(p->widget, p, NULL);
-        
-        if(p->focus_widget) {
-            XmProcessTraversal(p->focus_widget, XmTRAVERSE_CURRENT);
-        }
-    }
-}
-
-void pathbar_shift_left(Widget w, PathBar *p, XtPointer d)
-{
-    p->shift--;
-    pathbar_resize(p->widget, p, NULL);
-}
-
-void pathbar_shift_right(Widget w, PathBar *p, XtPointer d)
-{
-    if(p->shift < 0) {
-        p->shift++;
-    }
-    pathbar_resize(p->widget, p, NULL);
-}
-
-static void pathTextEH(Widget widget, XtPointer data, XEvent *event, Boolean *dispatch) {
-    PathBar *pb = data;
-    if(event->type == KeyReleaseMask) {
-        if(event->xkey.keycode == 9) {
-            XtUnmanageChild(pb->textfield);
-            pathbar_resize(pb->widget, pb, NULL);
-            *dispatch = False;
-        } else if(event->xkey.keycode == 36) {
-            pathbar_pathinput(pb->textfield, pb, NULL);
-            *dispatch = False;
-        }
-    }
-}
-
-PathBar* CreatePathBar(Widget parent, ArgList args, int n)
-{
-    PathBar *bar = (PathBar*)XtMalloc(sizeof(PathBar));
-    bar->path = NULL;
-    bar->updateDir = NULL;
-    bar->updateDirData = NULL;
-    
-    bar->focus_widget = NULL;
-    
-    bar->getpathelm = NULL;
-    bar->getpathelmdata = NULL;
-    bar->current_pathelms = NULL;
-    
-    bar->shift = 0;
-    
-    XtSetArg(args[n], XmNmarginWidth, 0); n++;
-    XtSetArg(args[n], XmNmarginHeight, 0); n++;
-    bar->widget = XmCreateDrawingArea(parent, "pathbar", args, n);
-    XtAddCallback(
-            bar->widget,
-            XmNresizeCallback,
-            (XtCallbackProc)pathbar_resize,
-            bar);
-    XtAddCallback(
-            bar->widget,
-            XmNinputCallback,
-            (XtCallbackProc)pathbar_input,
-            bar);
-    
-    Arg a[4];
-    XtSetArg(a[0], XmNshadowThickness, 0);
-    XtSetArg(a[1], XmNx, 0);
-    XtSetArg(a[2], XmNy, 0);
-    bar->textfield = XNECreateText(bar->widget, "pbtext", a, 3);
-    bar->input = 0;
-    XtAddCallback(
-            bar->textfield,
-            XmNlosingFocusCallback,
-            (XtCallbackProc)pathbar_losingfocus,
-            bar);
-    XtAddCallback(bar->textfield, XmNactivateCallback,
-                 (XtCallbackProc)pathbar_pathinput, bar);
-    XtAddEventHandler(bar->textfield, KeyPressMask | KeyReleaseMask, FALSE, pathTextEH, bar);
-    
-    XtSetArg(a[0], XmNarrowDirection, XmARROW_LEFT);
-    bar->left = XmCreateArrowButton(bar->widget, "pbbutton", a, 1);
-    XtSetArg(a[0], XmNarrowDirection, XmARROW_RIGHT);
-    bar->right = XmCreateArrowButton(bar->widget, "pbbutton", a, 1);
-    XtAddCallback(
-                bar->left,
-                XmNactivateCallback,
-                (XtCallbackProc)pathbar_shift_left,
-                bar);
-    XtAddCallback(
-                bar->right,
-                XmNactivateCallback,
-                (XtCallbackProc)pathbar_shift_right,
-                bar);
-    
-    Pixel bg;
-    XtVaGetValues(bar->textfield, XmNbackground, &bg, NULL);
-    XtVaSetValues(bar->widget, XmNbackground, bg, NULL);
-    
-    XtManageChild(bar->left);
-    XtManageChild(bar->right);
-    
-    XtVaGetValues(bar->left, XmNwidth, &bar->lw, NULL);
-    XtVaGetValues(bar->right, XmNwidth, &bar->rw, NULL);
-    
-    bar->segmentAlloc = 16;
-    bar->numSegments = 0;
-    bar->pathSegments = (Widget*)XtCalloc(16, sizeof(Widget));
-    
-    bar->selection = 0;
-    
-    return bar;
-}
-
-void PathBarChangeDir(Widget w, PathBar *bar, XtPointer c)
-{
-    XmToggleButtonSetState(bar->pathSegments[bar->selection], False, False);
-    
-    int i;
-    for(i=0;i<bar->numSegments;i++) {  
-        if(bar->pathSegments[i] == w) {
-            bar->selection = i;
-            XmToggleButtonSetState(w, True, False);
-            break;
-        }
-    }
-    
-    UiPathElm elm = bar->current_pathelms[i];
-    cxmutstr path = cx_strdup(cx_strn(elm.path, elm.path_len));
-    if(bar->updateDir) {
-        XNETextSetString(bar->textfield, path.ptr);
-        bar->updateDir(bar->updateDirData, path.ptr, i);
-    }
-    free(path.ptr);
-}
-
-static void ui_pathelm_destroy(UiPathElm *elms, size_t nelm) {
-    for(int i=0;i<nelm;i++) {
-        free(elms[i].name);
-        free(elms[i].path);
-    }
-    free(elms);
-}
-
-void PathBarSetPath(PathBar *bar, const char *path)
-{
-    if(bar->path) {
-        free(bar->path);
-    }
-    bar->path = strdup(path);
-    
-    for(int i=0;i<bar->numSegments;i++) {
-        XtDestroyWidget(bar->pathSegments[i]);
-    }
-    XtUnmanageChild(bar->textfield);
-    XtManageChild(bar->left);
-    XtManageChild(bar->right);
-    bar->input = False;
-    
-    Arg args[4];
-    XmString str;
-    
-    bar->numSegments = 0;
-    
-    ui_pathelm_destroy(bar->current_pathelms, bar->numSegments);
-    size_t nelm = 0;
-    UiPathElm* path_elm = bar->getpathelm(bar->path, strlen(bar->path), &nelm, bar->getpathelmdata);
-    if (!path_elm) {
-        return;
-    }
-    bar->current_pathelms = path_elm;
-    bar->numSegments = nelm;
-    bar->pathSegments = realloc(bar->pathSegments, nelm * sizeof(Widget*));
-    
-    for(int i=0;i<nelm;i++) {
-        UiPathElm elm = path_elm[i];
-        
-        cxmutstr name = cx_strdup(cx_strn(elm.name, elm.name_len));
-        str = XmStringCreateLocalized(elm.name);
-        free(name.ptr);
-        
-        XtSetArg(args[0], XmNlabelString, str);
-        XtSetArg(args[1], XmNfillOnSelect, True);
-        XtSetArg(args[2], XmNindicatorOn, False);
-        Widget button = XmCreateToggleButton(bar->widget, "pbbutton", args, 3);
-        XtAddCallback(
-                button,
-                XmNvalueChangedCallback,
-                (XtCallbackProc)PathBarChangeDir,
-                bar);
-        XmStringFree(str);
-        
-        bar->pathSegments[i] = button;
-    }
-    
-    bar->selection = bar->numSegments-1;
-    XmToggleButtonSetState(bar->pathSegments[bar->selection], True, False);
-    
-    XNETextSetString(bar->textfield, (char*)path);
-    XNETextSetInsertionPosition(bar->textfield, XNETextGetLastPosition(bar->textfield));
-    
-    pathbar_resize(bar->widget, bar, NULL);
-}
-
-void PathBarDestroy(PathBar *pathbar) {
-    if(pathbar->path) {
-        XtFree(pathbar->path);
-    }
-    XtFree((void*)pathbar->pathSegments);
-    XtFree((void*)pathbar);
-}
-
-
 /* ---------------------------- Path Text Field ---------------------------- */
 
 static void destroy_pathbar(Widget w, XtPointer *data, XtPointer d) {
@@ -915,47 +520,6 @@
     XtFree((void*)pathbar);
 }
 
-// TODO: move to common
-static UiPathElm* default_pathelm_func(const char* full_path, size_t len, size_t* ret_nelm, void* data) {
-    cxstring *pathelms;
-    size_t nelm = cx_strsplit_a(cxDefaultAllocator, cx_strn(full_path, len), CX_STR("/"), 4096, &pathelms);
-
-    if (nelm == 0) {
-        *ret_nelm = 0;
-        return NULL;
-    }
-
-    UiPathElm* elms = (UiPathElm*)calloc(nelm, sizeof(UiPathElm));
-    size_t n = nelm;
-    int j = 0;
-    for (int i = 0; i < nelm; i++) {
-        cxstring c = pathelms[i];
-        if (c.length == 0) {
-            if (i == 0) {
-                c.length = 1;
-            }
-            else {
-                n--;
-                continue;
-            }
-        }
-
-        cxmutstr m = cx_strdup(c);
-        elms[j].name = m.ptr;
-        elms[j].name_len = m.length;
-        
-        size_t elm_path_len = c.ptr + c.length - full_path;
-        cxmutstr elm_path = cx_strdup(cx_strn(full_path, elm_path_len));
-        elms[j].path = elm_path.ptr;
-        elms[j].path_len = elm_path.length;
-
-        j++;
-    }
-    *ret_nelm = n;
-
-    return elms;
-}
-
 static void pathbar_activate(void *data, char *path, int index) {
     UiEventData *event = data;
     UiEvent evt;
@@ -981,7 +545,7 @@
 
     PathBar *pathbar = CreatePathBar(parent, xargs, n);
     if(!args->getpathelm) {
-        pathbar->getpathelm= default_pathelm_func;
+        pathbar->getpathelm= ui_default_pathelm_func;
     } else {
         pathbar->getpathelm = args->getpathelm;
         pathbar->getpathelmdata = args->getpathelmdata;
@@ -1018,7 +582,7 @@
         XtAddCallback(
                 pathbar->widget,
                 XmNdestroyCallback,
-                (XtCallbackProc)ui_destroy_eventdata,
+                (XtCallbackProc)ui_destroy_data,
                 eventdata);
     }
     
--- a/ui/motif/text.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/text.h	Sat Dec 13 15:58:58 2025 +0100
@@ -86,6 +86,9 @@
 int ui_check_insertstr(char *oldstr, int oldlen, char *newstr, int newlen);
 void ui_free_textbuf_op(UiTextBufOp *op);
 
+void ui_textfield_activate(Widget widget, XtPointer ud, XtPointer cb);
+void ui_textfield_value_changed(Widget widget, XtPointer ud, XtPointer cb);
+
 char* ui_textfield_get(UiString *str);
 void ui_textfield_set(UiString *str, const char *value);
 
--- a/ui/motif/toolbar.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/toolbar.c	Sat Dec 13 15:58:58 2025 +0100
@@ -33,7 +33,6 @@
 
 #include "toolbar.h"
 #include "button.h"
-#include "stock.h"
 #include "list.h"
 
 #include <cx/hash_map.h>
--- a/ui/motif/toolkit.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/toolkit.c	Sat Dec 13 15:58:58 2025 +0100
@@ -34,11 +34,11 @@
 #include "toolkit.h"
 #include "toolbar.h"
 #include "container.h"
-#include "stock.h"
 #include "../common/menu.h"
 #include "../common/toolbar.h"
 #include "../common/document.h"
 #include "../common/properties.h"
+#include "../common/app.h"
 #include <cx/buffer.h>
 
 #include <X11/Intrinsic.h>
@@ -49,13 +49,6 @@
 static Widget active_window;
 static const char *application_name;
 
-static ui_callback   startup_func;
-static void          *startup_data;
-static ui_callback   open_func;
-static void          *open_data;
-static ui_callback   exit_func;
-static void          *exit_data;
-
 static ui_callback appclose_fnc;
 static void *appclose_udata;
 
@@ -84,11 +77,17 @@
 	NULL
 };
 
+static String *fallback_resources = fallback;
+
 void input_proc(XtPointer data, int *source, XtInputId *iid) {
     void *ptr;
     read(event_pipe[0], &ptr, sizeof(void*));
 }
 
+void ui_motif_set_fallback_resources(String *fallbackres) {
+    fallback_resources = fallbackres;
+}
+
 void ui_init(const char *appname, int argc, char **argv) { 
     application_name = appname;
     uic_init_global_context();
@@ -96,7 +95,7 @@
     XtToolkitInitialize();
     XtSetLanguageProc(NULL, NULL, NULL);
     app = XtCreateApplicationContext();
-    XtAppSetFallbackResources(app, fallback);
+    XtAppSetFallbackResources(app, fallback_resources);
     
     display =  XtOpenDisplay(app, NULL, appname, appname, NULL, 0, &argc, argv);
     
@@ -128,40 +127,21 @@
     return display;
 }
 
-void ui_onstartup(ui_callback f, void *userdata) {
-    startup_func = f;
-    startup_data = userdata;
-}
-
-void ui_onopen(ui_callback f, void *userdata) {
-    open_func = f;
-    open_data = userdata;
-}
-
-void ui_onexit(ui_callback f, void *userdata) {
-    exit_func = f;
-    exit_data = userdata;
-}
-
 void ui_app_exit_on_shutdown(UiBool exitapp) {
     exit_on_shutdown = exitapp;
 }
 
 void ui_main() {
-    if(startup_func) {
-        startup_func(NULL, startup_data);
-    }
+    uic_application_startup(NULL);
     XtAppMainLoop(app);
-    if(exit_func) {
-        exit_func(NULL, exit_data);
-    }
+    uic_application_exit(NULL);
     uic_store_app_properties();
     if(exit_on_shutdown) {
         exit(0);
     }
 }
 
-void ui_exit_mainloop() {
+void ui_app_quit() {
     XtAppSetExitFlag(app);
 }
 
@@ -174,7 +154,7 @@
 }
 
 void ui_show(UiObject *obj) {
-    uic_check_group_widgets(obj->ctx);
+    uic_check_state_widgets(obj->ctx);
     if(!XtIsRealized(obj->widget)) {
         XtRealizeWidget(obj->widget);
         obj->ref++;
@@ -220,6 +200,15 @@
     return TRUE;
 }
 
+static Boolean ui_mainthread_job(void *data) {
+    UiJob *job = data;
+    if(job->job_func) {
+        job->job_func(job->job_data);
+    }
+    free(job);
+    return TRUE;
+}
+
 static void* ui_jobthread(void *data) {
     UiJob *job = data;
     int result = job->job_func(job->job_data);
@@ -231,6 +220,15 @@
     return NULL;
 }
 
+void ui_call_mainthread(ui_threadfunc tf, void* td) {
+    UiJob *job = malloc(sizeof(UiJob));
+    memset(job, 0, sizeof(UiJob));
+    job->job_func = tf;
+    job->job_data = td;
+    write(event_pipe[1], &job, sizeof(void*)); // hack
+    XtAppAddWorkProc(app, ui_mainthread_job, job);
+}
+
 void ui_job(UiObject *obj, ui_threadfunc tf, void *td, ui_callback f, void *fd) {
     UiJob *job = malloc(sizeof(UiJob));
     job->obj = obj;
@@ -325,7 +323,7 @@
             4);
 }
 
-void ui_destroy_eventdata(Widget w, XtPointer data, XtPointer d) {
+void ui_destroy_data(Widget w, XtPointer data, XtPointer d) {
     free(data);
 }
 
@@ -333,13 +331,13 @@
     if(!groups) {
         return;
     }
-    size_t ngroups = uic_group_array_size(groups);
+    size_t ngroups = uic_state_array_size(groups);
     ui_set_widget_ngroups(ctx, widget, groups, ngroups);
 }
 
 void ui_set_widget_ngroups(UiContext *ctx, Widget widget, const int *groups, size_t ngroups) {
     if(ngroups > 0) {
-        uic_add_group_widget_i(ctx, widget, (ui_enablefunc)ui_set_enabled, groups, ngroups);
+        uic_add_state_widget_i(ctx, widget, (ui_enablefunc)ui_set_enabled, groups, ngroups);
         ui_set_enabled(widget, FALSE);
     }
 }
--- a/ui/motif/toolkit.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/toolkit.h	Sat Dec 13 15:58:58 2025 +0100
@@ -82,10 +82,9 @@
 typedef enum UiOrientation UiOrientation;
 enum UiOrientation { UI_HORIZONTAL = 0, UI_VERTICAL };
 
-void ui_exit_mainloop();
-
 XtAppContext ui_motif_get_app(void);
 Display* ui_motif_get_display(void);
+void ui_motif_set_fallback_resources(String *fallback);
 
 void ui_set_active_window(Widget w);
 Widget ui_get_active_window();
@@ -93,7 +92,7 @@
 void ui_secondary_event_loop(int *loop);
 void ui_window_dark_theme(Display *dp, Window window);
 
-void ui_destroy_eventdata(Widget w, XtPointer data, XtPointer d);
+void ui_destroy_data(Widget w, XtPointer data, XtPointer d);
 
 void ui_set_widget_groups(UiContext *ctx, Widget widget, const int *groups) ;
 void ui_set_widget_ngroups(UiContext *ctx, Widget widget, const int *groups, size_t ngroups);
--- a/ui/motif/window.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/window.c	Sat Dec 13 15:58:58 2025 +0100
@@ -28,15 +28,22 @@
 
 #include <stdio.h>
 #include <stdlib.h>
+#include <errno.h>
+#include <limits.h>
+#include <unistd.h>
+
+#include "window.h"
 
 #include "toolkit.h"
 #include "menu.h"
 #include "toolbar.h"
 #include "container.h"
-#include "../ui/window.h"
+#include "pathbar.h"
 #include "../common/context.h"
+#include "../common/utils.h"
 
 #include "Grid.h"
+#include "Fsb.h"
 
 #include <cx/mempool.h>
 
@@ -50,7 +57,7 @@
     uic_object_destroy(obj);
     nwindows--;
     if(nwindows == 0) {
-        ui_exit_mainloop();
+        ui_app_quit();
     }
 }
 
@@ -67,20 +74,28 @@
 }
 
 
-static UiObject* create_window(const char *title, void *window_data, Boolean simple) {
+static UiObject* create_window(const char *title, Boolean simple) {
     CxMempool *mp = cxMempoolCreateSimple(256);
     const CxAllocator *a = mp->allocator;
     UiObject *obj = uic_object_new_toplevel();
-    obj->window = window_data;
     obj->destroy = ui_window_widget_destroy;
     
+    int window_width = window_default_width;
+    int window_height = window_default_height;
+    if(!simple) {
+        ui_get_window_default_width(&window_width, &window_height);
+    }
+    
+    UiMotifAppWindow *appwindow = cxZalloc(a, sizeof(UiMotifAppWindow));
+    ui_object_set(obj, "ui_motif_app_window", appwindow);
+    
     Arg args[16];
     int n = 0;
     XtSetArg(args[n], XmNtitle, title); n++;
     XtSetArg(args[n], XmNminWidth, 100); n++;
     XtSetArg(args[n], XmNminHeight, 50); n++;
-    XtSetArg(args[n], XmNwidth, window_default_width); n++;
-    XtSetArg(args[n], XmNheight, window_default_height); n++;
+    XtSetArg(args[n], XmNwidth, window_width); n++;
+    XtSetArg(args[n], XmNheight, window_height); n++;
     
     Widget toplevel = XtAppCreateShell(
             ui_appname(),
@@ -110,7 +125,7 @@
     
     // menu
     if(!simple) {
-        ui_create_menubar(obj, window);
+        appwindow->menubar = ui_create_menubar(obj, window);
     }
     
     // content frame
@@ -135,10 +150,161 @@
     return obj;
 } 
 
-UiObject* ui_window(const char *title, void *window_data) {
-    return create_window(title, window_data, FALSE);
+UiObject* ui_window(const char *title) {
+    return create_window(title, FALSE);
+}
+
+UiObject* ui_simple_window(const char *title) {
+    return create_window(title, TRUE);
+}
+
+void ui_window_size(UiObject *obj, int width, int height) {
+    XtVaSetValues(obj->widget, XmNwidth, width, XmNheight, height, NULL);
+}
+
+void ui_window_default_size(int width, int height) {
+    window_default_width = width;
+    window_default_height = height;
+}
+
+void ui_window_menubar_set_visible(UiObject *obj, UiBool visible) {
+    UiMotifAppWindow *window = ui_object_get(obj, "ui_motif_app_window");
+    if(window) {
+        if(window->menubar) {
+            ui_set_visible(window->menubar, visible);
+        }
+    } else {
+        fprintf(stderr, "Error: obj is not an application window\n");
+    }
+}
+
+static Atom net_wm_state;
+static Atom net_wm_state_fullscreen;
+static int net_wm_atoms_initialized = 0;
+
+void ui_window_fullscreen(UiObject *obj, UiBool fullscreen) {
+    Display *dpy = XtDisplay(obj->widget);
+    
+    // init net_wm_state atoms
+    if(!net_wm_atoms_initialized) {
+        net_wm_state = XInternAtom(dpy, "_NET_WM_STATE", False);
+        net_wm_state_fullscreen = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False);
+        net_wm_atoms_initialized = 1;
+    }
+    
+    XEvent ev;
+    memset(&ev, 0, sizeof(XEvent));
+    ev.type = ClientMessage;
+    ev.xclient.window = XtWindow(obj->widget);
+    ev.xclient.message_type = net_wm_state;
+    ev.xclient.format = 32;
+    ev.xclient.data.l[0] = fullscreen ? 1 : 0;
+    ev.xclient.data.l[1] = net_wm_state_fullscreen;
+    ev.xclient.data.l[2] = 0;
+    XSendEvent(dpy, DefaultRootWindow(dpy), False, SubstructureNotifyMask | SubstructureRedirectMask, &ev);
+}
+
+static void filedialog_event(UiEventData *event, int result, UiFileList flist) {
+    UiEvent evt;
+    evt.obj = event->obj;
+    evt.document = evt.obj->ctx->document;
+    evt.window = evt.obj->window;
+    evt.intval = result;
+    
+    evt.eventdata = &flist;
+    evt.eventdatatype = UI_EVENT_DATA_FILE_LIST;
+    
+    if(event->callback) {
+        event->callback(&evt, event->userdata);
+    }
 }
 
-UiObject* ui_simple_window(const char *title, void *window_data) {
-    return create_window(title, window_data, TRUE);
+static void filedialog_select(
+        Widget widget,
+        UiEventData *data,
+        XmFileSelectionBoxCallbackStruct *selection)
+{
+    UiFileList flist;
+    
+    char *value = NULL;
+    XmStringGetLtoR(selection->value, XmSTRING_DEFAULT_CHARSET, &value);
+    flist.files = &value;
+    flist.nfiles = 1;
+    
+    filedialog_event(data, 1, flist);
+    
+    XtFree(value);
+    
+    XtUnmanageChild(widget);
+    XtDestroyWidget(widget);
+}
+
+static void filedialog_cancel(
+        Widget widget,
+        UiEventData *data,
+        XmFileSelectionBoxCallbackStruct *selection)
+{
+    UiFileList flist;
+    flist.files = NULL;
+    flist.nfiles = 0;
+    filedialog_event(data, 0, flist);
+    
+    XtUnmanageChild(widget);
+    XtDestroyWidget(widget);
 }
+
+void ui_openfiledialog(UiObject *obj, unsigned int mode, ui_callback file_selected_callback, void *cbdata) {
+    Widget dialog = XnCreateFileSelectionDialog(obj->widget, "dialog", NULL, 0);
+    
+    UiEventData *data = malloc(sizeof(UiEventData));
+    memset(data, 0, sizeof(UiEventData));
+    data->obj = obj;
+    data->callback = file_selected_callback;
+    data->userdata = cbdata;
+    
+    XtAddCallback(dialog, XmNokCallback, (XtCallbackProc)filedialog_select, data);
+    XtAddCallback(dialog, XmNcancelCallback, (XtCallbackProc)filedialog_cancel, data);
+    //XtAddCallback(dialog, XmNhelpCallback, (XtCallbackProc)filedialog_help, wd);
+    
+    XtManageChild(dialog);
+}
+
+void ui_savefiledialog(UiObject *obj, const char *name, ui_callback file_selected_callback, void *cbdata) {
+    Arg args[16];
+    int n = 0;
+    
+    // Save File Dialog needs this parameter
+    XtSetArg(args[n], XnNfsbType, FILEDIALOG_SAVE); n++;
+    char *selectedpath = (char*)name;
+    if(name) {
+        if(name[0] != '/') {
+            char cwd[PATH_MAX];
+            if(getcwd(cwd, PATH_MAX)) {
+                pathbar_concat_path(cwd, name);
+            } else {
+                fprintf(stderr, "Error: getcwd failed: %s\n", strerror(errno));
+                selectedpath = NULL;
+            }
+        }
+        if(selectedpath) {
+            XtSetArg(args[n], XnNselectedPath, selectedpath); n++;
+        }
+    }
+    Widget dialog = XnCreateFileSelectionDialog(obj->widget, "dialog", args, n);
+    
+    UiEventData *data = malloc(sizeof(UiEventData));
+    memset(data, 0, sizeof(UiEventData));
+    data->obj = obj;
+    data->callback = file_selected_callback;
+    data->userdata = cbdata;
+    
+    XtAddCallback(dialog, XmNokCallback, (XtCallbackProc)filedialog_select, data);
+    XtAddCallback(dialog, XmNcancelCallback, (XtCallbackProc)filedialog_cancel, data);
+    //XtAddCallback(dialog, XmNhelpCallback, (XtCallbackProc)filedialog_help, wd);
+    
+    XtManageChild(dialog);
+    
+    if(selectedpath != name) {
+        free(selectedpath);
+    }
+}
--- a/ui/motif/window.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/motif/window.h	Sat Dec 13 15:58:58 2025 +0100
@@ -29,11 +29,18 @@
 #ifndef WINDOW_H
 #define WINDOW_H
 
+#include "../ui/window.h"
+#include "../ui/widget.h"
+
 #ifdef __cplusplus
 extern "C" {
 #endif
+    
+#define UI_MOTIF_APP_WINDOW 0xabcd
 
-
+typedef struct UiMotifAppWindow {
+    Widget menubar;
+} UiMotifAppWindow;
 
 
 #ifdef __cplusplus
--- a/ui/qt/container.cpp	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/qt/container.cpp	Sat Dec 13 15:58:58 2025 +0100
@@ -82,7 +82,14 @@
     bool fill = layout.fill;
     if(hasStretchedWidget && fill) {
         fill = false;
-        fprintf(stderr, "UiError: container has 2 filled widgets");
+        fprintf(stderr, "UiError: container has 2 filled widgets\n");
+    }
+    
+    if(singleChild) {
+        fill = true;
+        if(hasChild) {
+            fprintf(stderr, "UiError: single child container already has a child\n");
+        }
     }
     
     uic_layout_setup_margin(&layout);
@@ -103,6 +110,7 @@
         hasStretchedWidget = true;
     }
     current = widget;
+    hasChild = true;
 }
 
 UIWIDGET ui_box(UiObject *obj, UiContainerArgs *args, QBoxLayout::Direction dir) {
@@ -239,6 +247,64 @@
 }
 
 
+/* ------------------------------ Frame ------------------------------ */
+
+UIWIDGET ui_frame_create(UiObject *obj, UiFrameArgs *args) {
+    UiContainerPrivate *ctn = (UiContainerPrivate*)ui_obj_container(obj);
+    UiLayout layout = UI_ARGS2LAYOUT(args);
+    
+    bool createBox = true;
+    bool singleChild = false;
+    QBoxLayout::Direction dir = QBoxLayout::TopToBottom;
+    QGroupBox *widget = new QGroupBox();
+    if(args->label) {
+        widget->setTitle(args->label);
+    }
+    
+    switch(args->subcontainer) {
+        default: break;
+        case UI_CONTAINER_HBOX: {
+            dir = QBoxLayout::LeftToRight;
+            break;
+        }
+        case UI_CONTAINER_GRID: {
+            createBox = false;
+            break;
+        }
+        case UI_CONTAINER_NO_SUB: {
+            singleChild = true;
+        }
+    }
+    
+    if(createBox) {
+        QBoxLayout *box = new QBoxLayout(dir);
+        widget->setLayout(box);
+        
+        UiBoxContainer *container = new UiBoxContainer(box);
+        ui_obj_add_container(obj, container);
+
+        container->singleChild = singleChild;
+    } else {
+        QGridLayout *grid = new QGridLayout();
+        widget->setLayout(grid);
+
+        ui_obj_add_container(obj, new UiGridContainer(
+                grid,
+                args->padding,
+                args->columnspacing,
+                args->rowspacing,
+                false,
+                false,
+                false,
+                false));
+    }
+    
+    
+    ctn->add(widget, layout);
+    
+    return widget;
+}
+
 /* ---------------------------- UiSidebar ---------------------------- */
 
 UIWIDGET ui_sidebar_create(UiObject *obj, UiSidebarArgs *args) {
--- a/ui/qt/container.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/qt/container.h	Sat Dec 13 15:58:58 2025 +0100
@@ -39,6 +39,7 @@
 #include <QTabWidget>
 #include <QStackedWidget>
 #include <QSplitter>
+#include <QGroupBox>
 
 #define ui_obj_container(obj) (UiContainerPrivate*)((UiContainerX*)obj->container_end)->container
 
@@ -54,6 +55,8 @@
 public:
     QBoxLayout            *box;
     bool                  hasStretchedWidget = false;
+    bool                  singleChild = false;
+    bool                  hasChild = false;
     QSpacerItem           *space;
     QBoxLayout::Direction direction;
     
--- a/ui/qt/list.cpp	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/qt/list.cpp	Sat Dec 13 15:58:58 2025 +0100
@@ -32,6 +32,7 @@
 #include <QTreeView>
 #include <QTreeWidgetItem>
 #include <QListView>
+#include <QComboBox>
 
 extern "C" void* ui_strmodel_getvalue(void *elm, int column) {
     return column == 0 ? elm : NULL;
@@ -46,7 +47,7 @@
     return NULL;
 }
 
-UIWIDGET ui_listview_create(UiObject* obj, UiListArgs *args) {
+UIWIDGET ui_listview_create(UiObject *obj, UiListArgs *args) {
     UiContainerPrivate *ctn = ui_obj_container(obj);
     
     QListView *view = new QListView();
@@ -92,7 +93,7 @@
     return view;
 }
 
-UIWIDGET ui_table_create(UiObject* obj, UiListArgs *args) {
+UIWIDGET ui_table_create(UiObject *obj, UiListArgs *args) {
     UiContainerPrivate *ctn = ui_obj_container(obj);
     
     QTreeView *view = new QTreeView();
@@ -141,3 +142,42 @@
     
     return view;
 }
+
+UIWIDGET ui_dropdown_create(UiObject *obj, UiListArgs *args) {
+    UiContainerPrivate *ctn = ui_obj_container(obj);
+    
+    QComboBox *view = new QComboBox();
+    UiLayout layout = UI_ARGS2LAYOUT(args);
+    ctn->add(view, layout);
+    
+    ui_getvaluefunc2 getvalue = nullptr;
+    void *getvaluedata = nullptr;
+    if(args->getvalue2) {
+        getvalue = args->getvalue2;
+        getvaluedata = args->getvalue2data;
+    } else if(args->getvalue) {
+        getvalue = getvalue_wrapper;
+        getvaluedata = (void*)args->getvalue;
+    } else {
+        getvalue = getvalue_wrapper;
+        getvaluedata = (void*)ui_strmodel_getvalue;
+    }
+    
+    UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST);
+    
+    ListModel *model = new ListModel(obj, view, var, getvalue, getvaluedata);
+    view->setModel(model);
+    
+    if(var) {
+        UiList *list = (UiList*)var->value;
+        list->update = ui_listmodel_update;
+        list->getselection = ui_listmodel_getselection;
+        list->setselection = ui_listmodel_setselection;
+        list->obj = model;
+    }
+    
+    model->setActivationCallback(args->onactivate, args->onactivatedata);
+    model->setSelectionCallback(args->onselection, args->onselectiondata);
+    
+    return view;
+}
--- a/ui/qt/list.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/qt/list.h	Sat Dec 13 15:58:58 2025 +0100
@@ -29,7 +29,7 @@
 #ifndef TREE_H
 #define	TREE_H
 
-#include "../ui/tree.h"
+#include "../ui/list.h"
 #include "model.h"
 
 #include <QTableView>
--- a/ui/qt/menu.cpp	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/qt/menu.cpp	Sat Dec 13 15:58:58 2025 +0100
@@ -38,7 +38,6 @@
 #include "../common/menu.h"
 #include "../ui/properties.h"
 #include "../ui/window.h"
-#include "stock.h"
 #include "container.h"
 
 
@@ -88,8 +87,8 @@
     }
     
     if(states) {
-        size_t nstates = uic_group_array_size(states);
-        uic_add_group_widget_i(obj->ctx, action, (ui_enablefunc)ui_action_enable, states, nstates);
+        size_t nstates = uic_state_array_size(states);
+        uic_add_state_widget_i(obj->ctx, action, (ui_enablefunc)ui_action_enable, states, nstates);
         action->setEnabled(false);
     }
     
@@ -98,7 +97,7 @@
 
 void add_menuitem_widget(QMenu *parent, int i, UiMenuItemI *item, UiObject *obj) {
     UiMenuItem *it = (UiMenuItem*)item;
-    UiAction *action = create_action(obj, it->icon, it->label, it->callback, it->userdata, it->groups);
+    UiAction *action = create_action(obj, it->icon, it->label, it->callback, it->userdata, it->states);
     parent->addAction(action);
     QObject::connect(action, SIGNAL(triggered()), action, SLOT(trigger()));
 }
@@ -110,7 +109,7 @@
 void add_checkitem_widget(QMenu *parent, int i, UiMenuItemI *item, UiObject *obj) {
     UiMenuCheckItem *it = (UiMenuCheckItem*)item;
     
-    UiAction *action = create_action(obj, it->icon, it->label, it->callback, it->userdata, it->groups);
+    UiAction *action = create_action(obj, it->icon, it->label, it->callback, it->userdata, it->states);
     parent->addAction(action);
     action->setCheckable(true);
     action->prepare_event = ui_checkableaction_prepare_event;
@@ -130,7 +129,7 @@
 void add_radioitem_widget(QMenu *parent, int index, UiMenuItemI *item, UiObject *obj) {
     UiMenuRadioItem *it = (UiMenuRadioItem*)item;
     
-    UiAction *action = create_action(obj, it->icon, it->label, it->callback, it->userdata, it->groups);
+    UiAction *action = create_action(obj, it->icon, it->label, it->callback, it->userdata, it->states);
     parent->addAction(action);
     action->setCheckable(true);
     action->prepare_event = ui_actiongroup_prepare_event;
--- a/ui/qt/model.cpp	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/qt/model.cpp	Sat Dec 13 15:58:58 2025 +0100
@@ -29,9 +29,23 @@
 #include "model.h"
 
 
-ListModel::ListModel(UiObject *obj, QListView *view, UiVar *var, ui_getvaluefunc2 getvalue, void *getvaluedata){
+ListModel::ListModel(UiObject *obj, QListView *view, UiVar *var, ui_getvaluefunc2 getvalue, void *getvaluedata) {
     this->obj = obj;
-    this->view = view;
+    this->listview = view;
+    this->combobox = nullptr;
+    this->var = var;
+    this->getvalue = getvalue;
+    this->getvaluedata = getvaluedata;
+    this->onactivate = nullptr;
+    this->onactivatedata = nullptr;
+    this->onselection = nullptr;
+    this->onselectiondata = nullptr;
+}
+
+ListModel::ListModel(UiObject *obj, QComboBox *view, UiVar *var, ui_getvaluefunc2 getvalue, void *getvaluedata) {
+    this->obj = obj;
+    this->combobox = view;
+    this->listview = nullptr;
     this->var = var;
     this->getvalue = getvalue;
     this->getvaluedata = getvaluedata;
@@ -85,7 +99,12 @@
 }
 
 void ListModel::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) {
-    UiListSelection sel = ui_selection_model_to_selection(view->selectionModel());
+    UiListSelection sel;
+    if(listview) {
+        sel = ui_selection_model_to_selection(listview->selectionModel());
+    } else {
+        // TODO
+    }
     
     UiEvent event;
     event.obj = obj;
@@ -257,10 +276,19 @@
         QModelIndex index = model->index(sel.rows[i]);
         selection.select(index, index);
     }
-    model->view->selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect);
+    if(model->listview) {
+        model->listview->selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect);
+    } else if(model->combobox) {
+        // TODO
+    }
 }
 
 UiListSelection ui_listmodel_getselection(UiList *list) {
     ListModel *model = (ListModel*)list->obj;
-    return ui_selection_model_to_selection(model->view->selectionModel());
+    if(model->listview) {
+        return ui_selection_model_to_selection(model->listview->selectionModel());
+    } else {
+        UiListSelection sel = { 0, NULL };
+        return sel;
+    }
 }
--- a/ui/qt/model.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/qt/model.h	Sat Dec 13 15:58:58 2025 +0100
@@ -30,10 +30,11 @@
 #define	MODEL_H
 
 #include "toolkit.h"
-#include "../ui/tree.h"
+#include "../ui/list.h"
 #include "../common/context.h"
 #include <QListView>
 #include <QTreeView>
+#include <QComboBox>
 #include <QAbstractListModel>
 #include <QAbstractTableModel>
 #include <QAbstractItemModel>
@@ -54,9 +55,11 @@
 public:
     UiObject    *obj;
     UiVar       *var;
-    QListView   *view;
+    QListView   *listview;
+    QComboBox   *combobox;
     
     ListModel(UiObject *obj, QListView *view, UiVar *var, ui_getvaluefunc2 getvalue, void *getvaluedata);
+    ListModel(UiObject *obj, QComboBox *view, UiVar *var, ui_getvaluefunc2 getvalue, void *getvaluedata);
     
     void setActivationCallback(ui_callback f, void *userdata);
     void setSelectionCallback(ui_callback f, void *userdata);
--- a/ui/qt/qt5.pro	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/qt/qt5.pro	Sat Dec 13 15:58:58 2025 +0100
@@ -43,7 +43,6 @@
 SOURCES += window.cpp
 SOURCES += menu.cpp
 SOURCES += toolbar.cpp
-SOURCES += stock.cpp
 SOURCES += container.cpp
 SOURCES += text.cpp
 SOURCES += model.cpp
@@ -59,7 +58,6 @@
 HEADERS += window.h
 HEADERS += menu.h
 HEADERS += toolbar.h
-HEADERS += stock.h
 HEADERS += container.h
 HEADERS += text.h
 HEADERS += model.h
--- a/ui/qt/toolbar.cpp	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/qt/toolbar.cpp	Sat Dec 13 15:58:58 2025 +0100
@@ -30,7 +30,6 @@
 
 #include "toolbar.h"
 #include "menu.h"
-#include "stock.h"
 
 static void add_items(UiObject *obj, QToolBar *toolbar, CxList *defaults, CxMap *items);
 static void create_item(UiObject *obj, QToolBar *toolbar, UiToolbarItemI *i);
--- a/ui/qt/toolkit.cpp	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/qt/toolkit.cpp	Sat Dec 13 15:58:58 2025 +0100
@@ -31,22 +31,15 @@
 
 #include "toolkit.h"
 #include "window.h"
-#include "stock.h"
 
 #include "../common/document.h"
 #include "../common/properties.h"
 #include "../common/menu.h"
 #include "../common/toolbar.h"
+#include "../common/app.h"
 
 static const char *application_name;
 
-static ui_callback   startup_func;
-static void          *startup_data;
-static ui_callback   open_func;
-static void          *open_data;
-static ui_callback   exit_func;
-static void          *exit_data;
-
 static int is_toplevel_realized = 0;
 
 static int app_argc;
@@ -73,33 +66,14 @@
     return application_name;
 }
 
-void ui_onstartup(ui_callback f, void *userdata) {
-    startup_func = f;
-    startup_data = userdata;
-}
-
-void ui_onopen(ui_callback f, void *userdata) {
-    open_func = f;
-    open_data = userdata;
-}
-
-void ui_onexit(ui_callback f, void *userdata) {
-    exit_func = f;
-    exit_data = userdata;
-}
-
 void ui_app_exit_on_shutdown(UiBool exitapp) {
     exit_on_shutdown = exitapp;
 }
 
 void ui_main() {
-    if(startup_func) {
-        startup_func(NULL, startup_data);
-    }
+    uic_application_startup(NULL);
     application->exec();
-    if(exit_func) {
-        exit_func(NULL, exit_data);
-    }
+    uic_application_exit(NULL);
     uic_store_app_properties();
     
     delete application;
--- a/ui/qt/window.cpp	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/qt/window.cpp	Sat Dec 13 15:58:58 2025 +0100
@@ -41,9 +41,8 @@
 #include <QDockWidget>
 #include <QMessageBox>
 
-static UiObject* create_window(const char *title, void *window_data, bool simple, bool sidebar = false) {
+static UiObject* create_window(const char *title, bool simple, bool sidebar = false) {
     UiObject *obj = uic_object_new_toplevel();
-    obj->window = window_data;
     obj->next = NULL;
     
     QMainWindow *window = new QMainWindow();
@@ -73,16 +72,16 @@
     return obj;
 }
 
-UiObject* ui_window(const char *title, void *window_data) {
-    return create_window(title, window_data, false);
+UiObject* ui_window(const char *title) {
+    return create_window(title, false);
 }
 
-UiObject* ui_simplewindow(char *title, void *window_data) {
-    return create_window(title, window_data, true);
+UiObject* ui_simple_window(const char *title) {
+    return create_window(title, true);
 }
 
-UiObject *ui_sidebar_window(const char *title, void *window_data) {
-    return create_window(title, window_data, false, true);
+UiObject* ui_sidebar_window(const char *title) {
+    return create_window(title, false, true);
 }
 
 void ui_dialog_create(UiObject *parent, UiDialogArgs *args) {
--- a/ui/ui/button.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/button.h	Sat Dec 13 15:58:58 2025 +0100
@@ -65,7 +65,7 @@
     ui_callback onclick;
     void *onclickdata;
     
-    const int *groups;
+    const int *states;
 } UiButtonArgs;
 
 typedef struct UiToggleArgs {
@@ -93,9 +93,9 @@
     const char *varname;
     ui_callback onchange;
     void *onchangedata;
-    int enable_group;
+    int enable_state;
     
-    const int *groups;
+    const int *states;
 } UiToggleArgs;
 
 typedef struct UiLinkButtonArgs {
@@ -124,7 +124,7 @@
     UiBool nofollow;
     UiLinkType type;
     
-    const int *groups;
+    const int *states;
 } UiLinkButtonArgs;
  
 #define ui_button(obj, ...) ui_button_create(obj, &(UiButtonArgs){ __VA_ARGS__ } )
--- a/ui/ui/container.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/container.h	Sat Dec 13 15:58:58 2025 +0100
@@ -98,6 +98,8 @@
     int margin_bottom;
     int colspan;
     int rowspan;
+    int width;
+    int height;
     const char *name;
     const char *style_class;
 
@@ -310,13 +312,14 @@
 #define ui_left_panel0(obj) for(ui_left_panel_create(obj, &(UiSidebarArgs){ 0 });ui_container_finish(obj);ui_container_begin_close(obj))
 #define ui_right_panel0(obj) for(ui_right_panel_create(obj, &(UiSidebarArgs){ 0 });ui_container_finish(obj);ui_container_begin_close(obj))
 
-
 #define ui_vbox_w(obj, w, ...) for(w = ui_vbox_create(obj, &(UiContainerArgs){ __VA_ARGS__ });ui_container_finish(obj);ui_container_begin_close(obj))
 #define ui_hbox_w(obj, w, ...) for(w = ui_hbox_create(obj, &(UiContainerArgs){ __VA_ARGS__ });ui_container_finish(obj);ui_container_begin_close(obj))
 #define ui_grid_w(obj, w, ...) for(w = ui_grid_create(obj, &(UiContainerArgs){ __VA_ARGS__ });ui_container_finish(obj);ui_container_begin_close(obj))
 #define ui_tabview_w(obj, w, ...) for(w = ui_tabview_create(obj, &(UiTabViewArgs){ __VA_ARGS__ });ui_container_finish(obj);ui_container_begin_close(obj))
 #define ui_scrolledwindow_w(obj, w, ...) for(w = ui_scrolledwindow_create(obj, &(UiFrameArgs){ __VA_ARGS__ });ui_container_finish(obj);ui_container_begin_close(obj))
 
+#define ui_custom_container(ob, widget, addfunc, data) for(ui_custom_container_create(obj, widget, addfunc, data);ui_container_finish(obj);ui_container_begin_close(obj))
+
 #define ui_hsplitpane(obj, ...) for(ui_hsplitpane_create(obj, &(UiSplitPaneArgs){ __VA_ARGS__ });ui_container_finish(obj);ui_container_begin_close(obj))
 #define ui_vsplitpane(obj, ...) for(ui_vsplitpane_create(obj, &(UiSplitPaneArgs){ __VA_ARGS__ });ui_container_finish(obj);ui_container_begin_close(obj))
 #define ui_hsplitpane0(obj) for(ui_hsplitpane_create(obj, &(UiSplitPaneArgs){ 0 });ui_container_finish(obj);ui_container_begin_close(obj))
@@ -367,6 +370,9 @@
 
 UIEXPORT void ui_newline(UiObject *obj);
 
+typedef void(*ui_addwidget_func)(UiObject *obj, UIWIDGET parent, UIWIDGET child, void *userdata);
+UIEXPORT void ui_custom_container_create(UiObject *obj, UIWIDGET widget, ui_addwidget_func add_child, void *userdata);
+
 // TODO
 UIEXPORT UiTabbedPane* ui_tabbed_document_view(UiObject *obj);
 UIEXPORT UiObject* ui_document_tab(UiTabbedPane *view);
--- a/ui/ui/entry.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/entry.h	Sat Dec 13 15:58:58 2025 +0100
@@ -65,7 +65,7 @@
     ui_callback onchange;
     void* onchangedata;
     
-    const int *groups;
+    const int *states;
 } UiSpinBoxArgs;
 
 
--- a/ui/ui/image.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/image.h	Sat Dec 13 15:58:58 2025 +0100
@@ -58,6 +58,8 @@
     int margin_bottom;
     int colspan;
     int rowspan;
+    int width;
+    int height;
     const char *name;
     const char *style_class;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/ui/list.h	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,367 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2017 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef UI_TREE_H
+#define	UI_TREE_H
+
+#include "toolkit.h"
+
+#ifdef	__cplusplus
+extern "C" {
+#endif
+
+typedef struct UiModel          UiModel;
+typedef struct UiListCallbacks  UiListCallbacks;
+typedef struct UiListDnd        UiListDnd;
+
+typedef struct UiListArgs       UiListArgs;
+typedef struct UiSourceListArgs UiSourceListArgs;
+
+typedef struct UiSubList        UiSubList;
+typedef struct UiSubListItem    UiSubListItem;
+
+typedef enum UiModelType {
+    UI_STRING = 0,
+    UI_STRING_FREE,
+    UI_INTEGER,
+    UI_ICON,
+    UI_ICON_TEXT,
+    UI_ICON_TEXT_FREE,
+    UI_STRING_EDITABLE,
+    UI_BOOL_EDITABLE
+} UiModelType;
+
+typedef struct UiCellValue {
+    union {
+        const char *string;
+        int64_t i;
+        UiBool b;
+    };
+    UiModelType type;
+} UiCellValue;
+
+typedef UiBool (*ui_list_savefunc)(UiList *list, int row, int col, UiCellValue *value, void *userdata);
+
+typedef void (*ui_model_update_func)(UiModel *model, void *userdata, int insert_index, int delete_index);
+
+typedef struct UiModelChangeObserver UiModelChangeObserver;
+struct UiModelChangeObserver {
+    ui_model_update_func update;
+    void *userdata;
+    UiModelChangeObserver *next;
+};
+
+struct UiModel {
+    UiContext *ctx;
+    
+    /*
+     * number of columns
+     */
+    int columns;
+    
+    /*
+     * current allocation size (internal)
+     */
+    int alloc;
+    
+    /*
+     * array of column types
+     * array length is the number of columns
+     */
+    UiModelType *types;
+    
+    /*
+     * array of column titles
+     * array length is the number of columns
+     */
+    char **titles;
+    
+    /*
+     * array of column size hints
+     */
+    int *columnsize;
+    
+    /*
+     * Model change observers, that will be called when
+     * columns are added or removed
+     */
+    UiModelChangeObserver *observer;
+    
+    /*
+     * reference counter
+     */
+    int ref;
+};
+
+struct UiListCallbacks {
+    /*
+     * selection callback
+     */
+    ui_callback activate;
+    
+    /*
+     * cursor callback
+     */
+    ui_callback selection;
+    
+    /*
+     * userdata for all callbacks
+     */
+    void *userdata;
+};
+
+struct UiListArgs {
+    UiBool fill;
+    UiBool hexpand;
+    UiBool vexpand;
+    UiBool hfill;
+    UiBool vfill;
+    UiBool override_defaults;
+    int margin;
+    int margin_left;
+    int margin_right;
+    int margin_top;
+    int margin_bottom;
+    int colspan;
+    int rowspan;
+    int width;
+    int height;
+
+    const char *name;
+    const char *style_class;
+    UiList *list;
+    const char* varname;
+    UiModel *model;
+    char **static_elements;
+    size_t static_nelm;
+    ui_getvaluefunc getvalue;
+    ui_getvaluefunc2 getvalue2;
+    void *getvalue2data;
+    ui_getstylefunc getstyle;
+    void *getstyledata;
+    ui_callback onactivate;
+    void *onactivatedata;
+    ui_callback onselection;
+    void *onselectiondata;
+    ui_callback ondragstart;
+    void *ondragstartdata;
+    ui_callback ondragcomplete;
+    void *ondragcompletedata;
+    ui_callback ondrop;
+    void *ondropdata;
+    UiBool multiselection;
+    UiMenuBuilder *contextmenu;
+    ui_list_savefunc onsave;
+    void *onsavedata;
+    
+    const int *states;
+};
+
+typedef void (*ui_sublist_getvalue_func)(UiList *list, void *sublist_userdata, void *rowdata, int index, UiSubListItem *item, void *userdata);
+
+struct UiSubList {
+    UiList *value;
+    const char *varname;
+    const char *header;
+    UiBool separator;
+    void *userdata;
+};
+
+typedef struct UiSubListEventData {
+    UiList *list;
+    int    sublist_index;
+    int    row_index;
+    void   *row_data;
+    void   *sublist_userdata;
+    void   *event_data;
+} UiSubListEventData;
+
+/*
+ * list item members must be filled by the sublist getvalue func
+ * all members must be allocated (by malloc, strdup, ...) the pointer
+ * will be passed to free
+ */
+struct UiSubListItem {
+    char          *icon;
+    char          *label;
+    char          *button_icon;
+    char          *button_label;
+    UiMenuBuilder *button_menu;
+    char          *badge;
+    void          *eventdata;
+};
+
+struct UiSourceListArgs {
+    UiBool fill;
+    UiBool hexpand;
+    UiBool vexpand;
+    UiBool hfill;
+    UiBool vfill;
+    UiBool override_defaults;
+    int margin;
+    int margin_left;
+    int margin_right;
+    int margin_top;
+    int margin_bottom;
+    int colspan;
+    int rowspan;
+    int width;
+    int height;
+    const char *name;
+    const char *style_class;
+    
+    const int *states;
+    
+    /*
+     * static list of sublists
+     * a sublist must have a varname or a value
+     * 
+     * the last entry in the list must contain all NULL values or numsublists
+     * must contain the number of sublists
+     * 
+     * sublists can be NULL, in which case sublists are dynamically loaded
+     * from dynamic_sublist/varname
+     */
+    UiSubList *sublists;
+    /*
+     * optional number of sublists
+     * if the value is 0, it is assumed, that sublists is null-terminated
+     * (last item contains only NULL values)
+     */
+    size_t numsublists;
+    
+    /*
+     * list value, that contains UiSubList* elements
+     */
+    UiList *dynamic_sublist;
+    
+    /*
+     * load sublists dynamically from a variable with the specified name
+     */
+    const char *varname;
+    
+    
+    /*
+     * callback for each list item, that should fill all necessary
+     * UiSubListItem fields
+     */
+    ui_sublist_getvalue_func getvalue;
+    
+    /*
+     * getvalue_func userdata
+     */
+    void *getvaluedata;
+    
+    /*
+     * is a sublist header a selectable item
+     */
+    UiBool header_is_item;
+    
+    /*
+     * activated when a list item is selected
+     */
+    ui_callback onactivate;
+    void        *onactivatedata;
+    
+    /*
+     * activated, when the additional list item button is clicked
+     */
+    ui_callback onbuttonclick;
+    void        *onbuttonclickdata;
+    
+    UiMenuBuilder *contextmenu;
+};
+
+#define UI_SUBLIST(...) (UiSubList){ __VA_ARGS__ }
+#define UI_SUBLISTS(...) (UiSubList[]){ __VA_ARGS__, (UiSubList){NULL,NULL,NULL,0} }
+
+
+/*
+ * Creates an UiModel, that specifies columns for a table widget.
+ *
+ * For each column a column type (UiModelType) and a title string
+ * (char*) must be specified. The column list must be terminated
+ * with -1.
+ *
+ * UiModel *model = ui_model(ctx, UI_STRING, "Column 1", UI_STRING, "Column 2", -1);
+ */
+UIEXPORT UiModel* ui_model(UiContext *ctx, ...);
+UIEXPORT UiModel* ui_model_new(UiContext *ctx);
+UIEXPORT void ui_model_add_column(UiModel *model, UiModelType type, const char *title, int width);
+UIEXPORT UiModel* ui_model_copy(UiContext *ctx, UiModel* model);
+UIEXPORT void ui_model_ref(UiModel *model);
+UIEXPORT void ui_model_unref(UiModel *model);
+UIEXPORT void ui_model_add_observer(UiModel *model, ui_model_update_func update, void *data);
+UIEXPORT void ui_model_remove_observer(UiModel *model, void *data);
+UIEXPORT void ui_model_free(UiModel *mi);
+
+#define ui_listview(obj, ...) ui_listview_create(obj, &(UiListArgs) { __VA_ARGS__ } )
+#define ui_table(obj, ...) ui_table_create(obj, &(UiListArgs) { __VA_ARGS__ } )
+#define ui_dropdown(obj, ...) ui_dropdown_create(obj, &(UiListArgs) { __VA_ARGS__ } )
+#define ui_breadcrumbbar(obj, ...) ui_breadcrumbbar_create(obj, &(UiListArgs) { __VA_ARGS__ } )
+#define ui_sourcelist(obj, ...) ui_sourcelist_create(obj, &(UiSourceListArgs) { __VA_ARGS__ } )
+
+UIEXPORT UIWIDGET ui_listview_create(UiObject *obj, UiListArgs *args);
+UIEXPORT UIWIDGET ui_table_create(UiObject *obj, UiListArgs *args);
+UIEXPORT UIWIDGET ui_dropdown_create(UiObject *obj, UiListArgs *args);
+UIEXPORT UIWIDGET ui_breadcrumbbar_create(UiObject *obj, UiListArgs *args);
+
+UIEXPORT void ui_listview_select(UIWIDGET listview, int index);
+UIEXPORT int ui_listview_selection(UIWIDGET listview);
+UIEXPORT void ui_dropdown_select(UIWIDGET dropdown, int index);
+UIEXPORT int ui_dropdown_selection(UIWIDGET dropdown);
+
+UIEXPORT UIWIDGET ui_sourcelist_create(UiObject *obj, UiSourceListArgs *args);
+
+UIEXPORT void ui_sublist_item_set_icon(UiSubListItem *item, const char *icon);
+UIEXPORT void ui_sublist_item_set_label(UiSubListItem *item, const char *label);
+UIEXPORT void ui_sublist_item_set_button_icon(UiSubListItem *item, const char *button_icon);
+UIEXPORT void ui_sublist_item_set_button_label(UiSubListItem *item, const char *button_label);
+UIEXPORT void ui_sublist_item_set_button_menu(UiSubListItem *item, UiMenuBuilder *menu);
+UIEXPORT void ui_sublist_item_set_badge(UiSubListItem *item, const char *badge);
+UIEXPORT void ui_sublist_item_set_eventdata(UiSubListItem *item, void *eventdata);
+
+
+
+/*
+ * Only relevant for some language bindings
+ */
+typedef void(*ui_sourcelist_update_func)(void);
+
+/*
+ * The sourcelist update callback is called after any source list
+ * sublist update is completed
+ */
+UIEXPORT void ui_sourcelist_set_update_callback(ui_sourcelist_update_func cb);
+
+#ifdef	__cplusplus
+}
+#endif
+
+#endif	/* UI_TREE_H */
+
--- a/ui/ui/menu.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/menu.h	Sat Dec 13 15:58:58 2025 +0100
@@ -43,7 +43,7 @@
 	ui_callback onclick;
 	void* onclickdata;
 
-	const int* groups;
+	const int* states;
 } UiMenuItemArgs;
 
 typedef struct UiMenuToggleItemArgs {
@@ -54,7 +54,7 @@
 	ui_callback onchange;
 	void* onchangedata;
 
-	const int* groups;
+	const int* nstates;
 } UiMenuToggleItemArgs;
 
 typedef struct UiMenuItemListArgs {
--- a/ui/ui/properties.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/properties.h	Sat Dec 13 15:58:58 2025 +0100
@@ -35,6 +35,9 @@
 extern "C" {
 #endif
 
+void ui_load_properties_file_on_startup(UiBool enable);
+void ui_set_properties_data(const char *str, size_t len);
+    
 const char* ui_get_property(const char *name);
 void  ui_set_property(const char *name, const char *value);
 const char* ui_set_default_property(const char *name, const char *value);
--- a/ui/ui/text.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/text.h	Sat Dec 13 15:58:58 2025 +0100
@@ -59,7 +59,7 @@
     ui_callback onchange;
     void *onchangedata;
     
-    const int *groups;
+    const int *states;
 } UiTextAreaArgs;
     
 typedef struct UiTextFieldArgs {
@@ -87,7 +87,7 @@
     ui_callback onactivate;
     void *onactivatedata;
     
-    const int *groups;
+    const int *states;
 } UiTextFieldArgs;
 
 typedef struct UiPathElmRet {
@@ -115,6 +115,7 @@
     int margin_bottom;
     int colspan;
     int rowspan;
+    int width;
     const char *name;
     const char *style_class;
 
--- a/ui/ui/toolbar.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/toolbar.h	Sat Dec 13 15:58:58 2025 +0100
@@ -44,7 +44,7 @@
     ui_callback onclick;
     void* onclickdata;
 
-    const int *groups;
+    const int *states;
     const int *visibility_states;
 } UiToolbarItemArgs;
 
@@ -57,7 +57,7 @@
     ui_callback onchange;
     void *onchangedata;
 
-    const int *groups;
+    const int *states;
     const int *visibility_states;
 } UiToolbarToggleItemArgs;
 
--- a/ui/ui/toolkit.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/toolkit.h	Sat Dec 13 15:58:58 2025 +0100
@@ -146,6 +146,14 @@
 #endif
 
 
+#elif UI_SERVER
+
+typedef struct UiWidget UiWidget;
+
+#define UIWIDGET UiWidget*
+#define UIWINDOW UiWidget*
+#define UIMENU   void*
+
 #endif
 
 #ifndef TRUE
@@ -417,6 +425,7 @@
 };
     
 typedef void (*ui_list_init_func)(UiContext *ctx, UiList *list, void *userdata);
+typedef void (*ui_list_destroy_func)(UiContext *ctx, UiList *list, void *userdata);
 
 /*
  * abstract list
@@ -541,6 +550,7 @@
 UIEXPORT void ui_app_exit_on_shutdown(UiBool exitapp);
 
 UIEXPORT void ui_main(void);
+UIEXPORT void ui_app_quit(void);
 UIEXPORT void ui_show(UiObject *obj);
 UIEXPORT void ui_close(UiObject *obj);
 
@@ -557,16 +567,18 @@
 
 UIEXPORT UiContext* ui_document_context(void *doc);
 
+UIEXPORT void* ui_context_get_document(UiContext *ctx);
+UIEXPORT void ui_context_single_attachment_mode(UiContext *ctx, UiBool enable);
 UIEXPORT void ui_attach_document(UiContext *ctx, void *document);
 UIEXPORT void ui_detach_document(UiContext *ctx, void *document);
 
-UIEXPORT void ui_widget_set_groups(UiContext *ctx, UIWIDGET widget, ui_enablefunc enable, ...);
-UIEXPORT void ui_widget_set_groups2(UiContext *ctx, UIWIDGET widget, ui_enablefunc enable, const int *groups, int ngroups);
+UIEXPORT void ui_widget_set_states(UiContext *ctx, UIWIDGET widget, ui_enablefunc enable, ...);
+UIEXPORT void ui_widget_set_states2(UiContext *ctx, UIWIDGET widget, ui_enablefunc enable, const int *states, int nstates);
 UIEXPORT void ui_widget_set_visibility_states(UiContext *ctx, UIWIDGET widget, const int *states, int nstates);
 
-UIEXPORT void ui_set_group(UiContext *ctx, int group);
-UIEXPORT void ui_unset_group(UiContext *ctx, int group);
-UIEXPORT int* ui_active_groups(UiContext *ctx, int *ngroups);
+UIEXPORT void ui_set_state(UiContext *ctx, int state);
+UIEXPORT void ui_unset_state(UiContext *ctx, int state);
+UIEXPORT int* ui_active_states(UiContext *ctx, int *nstates);
     
 UIEXPORT void* ui_allocator(UiContext *ctx);
 UIEXPORT void* ui_cx_mempool(UiContext *ctx);
@@ -625,6 +637,13 @@
 UIEXPORT void ui_var_set_string(UiContext *ctx, const char *name, char *value);
 UIEXPORT char* ui_var_get_string(UiContext *ctx, const char *name);
 
+UIEXPORT UiInteger* ui_get_int_var(UiContext *ctx, const char *name);
+UIEXPORT UiDouble* ui_get_double_var(UiContext *ctx, const char *name);
+UIEXPORT UiString* ui_get_string_var(UiContext *ctx, const char *name);
+UIEXPORT UiText* ui_get_text_var(UiContext *ctx, const char *name);
+UIEXPORT UiRange* ui_get_range_var(UiContext *ctx, const char *name);
+UIEXPORT UiGeneric* ui_get_generic_var(UiContext *ctx, const char *name);
+
 UIEXPORT UiObserver* ui_observer_new(ui_callback f, void *data);
 UIEXPORT UiObserver* ui_obsvlist_add(UiObserver *list, UiObserver *observer);
 UIEXPORT UiObserver* ui_add_observer(UiObserver *list, ui_callback f, void *data);
@@ -635,7 +654,7 @@
 
 UIEXPORT UiList* ui_list_new(UiContext *ctx, const char *name);
 UIEXPORT UiList* ui_list_new2(UiContext *ctx, const char *name, ui_list_init_func init, void *userdata);
-UIEXPORT void ui_list_free(UiList *list);
+UIEXPORT void ui_list_free(UiContext *ctx, UiList *list);
 UIEXPORT void* ui_list_first(UiList *list);
 UIEXPORT void* ui_list_next(UiList *list);
 UIEXPORT void* ui_list_get(UiList *list, int i);
@@ -680,9 +699,12 @@
 
 UIEXPORT void ui_setop_enable(int set);
 UIEXPORT int ui_get_setop(void);
+UIEXPORT void ui_onchange_events_enable(UiBool enable);
+UIEXPORT UiBool ui_onchange_events_is_enabled(void);
 
     
 UIEXPORT void ui_global_list_initializer(ui_list_init_func func, void *userdata);
+UIEXPORT void ui_global_list_destructor(ui_list_destroy_func func, void *userdata);
 UIEXPORT void ui_list_class_set_first(UiList *list, void*(*first)(UiList *list));
 UIEXPORT void ui_list_class_set_next(UiList *list, void*(*next)(UiList *list));
 UIEXPORT void ui_list_class_set_get(UiList *list, void*(*get)(UiList *list, int i));
@@ -690,6 +712,9 @@
 UIEXPORT void ui_list_class_set_data(UiList *list, void *data);
 UIEXPORT void ui_list_class_set_iter(UiList *list, void *iter);
 
+UIEXPORT void ui_object_set(UiObject *obj, const char *key, void *data);
+UIEXPORT void* ui_object_get(UiObject *obj, const char *key);
+
 #ifdef	__cplusplus
 }
 #endif
--- a/ui/ui/ui.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/ui.h	Sat Dec 13 15:58:58 2025 +0100
@@ -35,11 +35,10 @@
 #include "menu.h"
 #include "toolbar.h"
 #include "window.h"
-#include "stock.h"
 #include "button.h"
 #include "text.h"
 #include "properties.h"
-#include "tree.h"
+#include "list.h"
 #include "graphics.h"
 #include "entry.h"
 #include "range.h"
--- a/ui/ui/webview.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/webview.h	Sat Dec 13 15:58:58 2025 +0100
@@ -60,13 +60,15 @@
     int margin_bottom;
     int colspan;
     int rowspan;
+    int width;
+    int height;
     const char *name;
     const char *style_class;
     
     UiGeneric *value;
     const char *varname;
     
-    const int* groups;
+    const int* states;
 } UiWebviewArgs;
 
 #define ui_webview(obj, ...) ui_webview_create(obj, &(UiWebviewArgs){ __VA_ARGS__ } )
--- a/ui/ui/widget.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/widget.h	Sat Dec 13 15:58:58 2025 +0100
@@ -64,6 +64,8 @@
 typedef UIWIDGET(*ui_createwidget_func)(UiObject *obj, UiWidgetArgs *args, void *userdata);
 #elif defined(UI_WIN32)
 typedef UIWIDGET(*ui_createwidget_func)(UiObject *obj, UiWidgetArgs *args, void *userdata);
+#elif defined(UI_SERVER)
+typedef UIWIDGET(*ui_createwidget_func)(UiObject *obj, UiWidgetArgs *args, void *userdata);
 #endif
 UIEXPORT UIWIDGET ui_customwidget_create(UiObject *obj, ui_createwidget_func create_widget, void *userdata, UiWidgetArgs *args);
 
--- a/ui/ui/win32.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/win32.h	Sat Dec 13 15:58:58 2025 +0100
@@ -49,7 +49,7 @@
 };
 
 struct W32WidgetClass {
-    void (*eventproc)(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+    int (*eventproc)(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
     void (*show)(W32Widget *widget, BOOLEAN show);
     void (*enable)(W32Widget *widget, BOOLEAN enable);
     W32Size (*get_preferred_size)(W32Widget *widget);
--- a/ui/ui/window.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/ui/window.h	Sat Dec 13 15:58:58 2025 +0100
@@ -61,10 +61,10 @@
     const char *lbutton2;
     const char *rbutton3;
     const char *rbutton4;
-    const int *lbutton1_groups;
-    const int *lbutton2_groups;
-    const int *rbutton3_groups;
-    const int *rbutton4_groups;
+    const int *lbutton1_states;
+    const int *lbutton2_states;
+    const int *rbutton3_states;
+    const int *rbutton4_states;
     int default_button;
     int width;
     int height;
@@ -72,10 +72,10 @@
     void *onclickdata;
 } UiDialogWindowArgs;
 
-UIEXPORT UiObject *ui_window(const char *title, void *window_data);
-UIEXPORT UiObject *ui_sidebar_window(const char *title, void *window_data);
+UIEXPORT UiObject *ui_window(const char *title);
+UIEXPORT UiObject *ui_sidebar_window(const char *title);
 UIEXPORT UiObject *ui_splitview_window(const char *title, UiBool sidebar);
-UIEXPORT UiObject *ui_simple_window(const char *title, void *window_data);
+UIEXPORT UiObject *ui_simple_window(const char *title);
 UIEXPORT UiObject *ui_dialog_window_create(UiObject *parent, UiDialogWindowArgs *args);
 
 #define ui_dialog_window(parent, ...) ui_dialog_window_create(parent, &(UiDialogWindowArgs){ __VA_ARGS__ });
@@ -83,6 +83,9 @@
 
 UIEXPORT void ui_window_size(UiObject *obj, int width, int height);
 UIEXPORT void ui_window_default_size(int width, int height);
+UIEXPORT void ui_window_menubar_set_visible(UiObject *obj, UiBool visible);
+
+UIEXPORT void ui_window_fullscreen(UiObject *obj, UiBool fullscreen);
 
 UIEXPORT void ui_splitview_window_set_pos(UiObject *obj, int pos);
 UIEXPORT int ui_splitview_window_get_pos(UiObject *obj);
--- a/ui/win32/button.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/win32/button.c	Sat Dec 13 15:58:58 2025 +0100
@@ -32,6 +32,8 @@
 #include <stdio.h>
 #include <stdlib.h>
 
+#include <cx/array_list.h>
+
 #include <commctrl.h>
 
 static W32WidgetClass button_widget_class = {
@@ -42,6 +44,22 @@
     .destroy  = w32_widget_default_destroy
 };
 
+static W32WidgetClass togglebutton_widget_class = {
+    .eventproc = ui_togglebutton_eventproc,
+    .enable = w32_widget_default_enable,
+    .show = w32_widget_default_show,
+    .get_preferred_size = ui_button_get_preferred_size,
+    .destroy  = w32_widget_default_destroy
+};
+
+static W32WidgetClass radiobutton_widget_class = {
+    .eventproc = ui_radiobutton_eventproc,
+    .enable = w32_widget_default_enable,
+    .show = w32_widget_default_show,
+    .get_preferred_size = ui_button_get_preferred_size,
+    .destroy  = w32_widget_default_destroy
+};
+
 UIWIDGET ui_button_create(UiObject *obj, UiButtonArgs *args) {
     HINSTANCE hInstance = GetModuleHandle(NULL);
     UiContainerPrivate *container = ui_obj_container(obj);
@@ -82,11 +100,12 @@
     return size;
 }
 
-void ui_button_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+int ui_button_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
     UiWidget *w = (UiWidget*)widget;
 
-    printf("button eventproc\n");
-    fflush(stdout);
+    if (uMsg != WM_COMMAND) {
+        return 0;
+    }
 
     UiEvent e;
     e.obj = w->obj;
@@ -100,4 +119,200 @@
     if (w->callback) {
         w->callback(&e, w->callbackdata);
     }
+
+    return 0;
 }
+
+static UIWIDGET create_togglebutton(UiObject *obj, UiToggleArgs *args, unsigned long type) {
+    HINSTANCE hInstance = GetModuleHandle(NULL);
+    UiContainerPrivate *container = ui_obj_container(obj);
+    HWND parent = ui_container_get_parent(container);
+    UiLayout layout = UI_ARGS2LAYOUT(args);
+
+    HWND hwnd = CreateWindow(
+            "BUTTON",
+            args->label,
+            WS_VISIBLE | WS_CHILD | type,
+            0, 0, 100, 30,
+            parent,
+            (HMENU)1,
+            hInstance,
+            NULL);
+    ui_win32_set_ui_font(hwnd);
+
+    W32Widget *widget = w32_widget_create(&togglebutton_widget_class, hwnd, sizeof(UiWidget));
+    ui_container_add(container, widget, &layout);
+
+    UiVar *var = uic_widget_var(obj->ctx, obj->ctx, args->value, args->varname, UI_VAR_INTEGER);
+    if (var) {
+        UiInteger *i = var->value;
+        i->obj = widget;
+        i->get = ui_togglebutton_get;
+        i->set = ui_togglebutton_set;
+        if (i->value != 0) {
+            SendMessage(hwnd, BM_SETCHECK, BST_CHECKED, 0);
+        }
+    }
+
+    UiWidget *btn = (UiWidget*)widget;
+    btn->obj = obj;
+    btn->var = var;
+    btn->callback = args->onchange;
+    btn->callbackdata = args->onchangedata;
+
+    return widget;
+}
+
+UIWIDGET ui_togglebutton_create(UiObject *obj, UiToggleArgs *args) {
+    return create_togglebutton(obj, args, BS_AUTOCHECKBOX | BS_PUSHLIKE);
+}
+
+UIWIDGET ui_checkbox_create(UiObject *obj, UiToggleArgs *args) {
+    return create_togglebutton(obj, args, BS_AUTOCHECKBOX);
+}
+
+UIWIDGET ui_switch_create(UiObject *obj, UiToggleArgs *args) {
+    return create_togglebutton(obj, args, BS_AUTOCHECKBOX);
+}
+
+int64_t ui_togglebutton_get(UiInteger *i) {
+    UiWidget *btn = (UiWidget*)i->obj;
+    LRESULT state = SendMessage(btn->widget.hwnd, BM_GETCHECK, 0, 0);
+    i->value = state;
+    return state;
+}
+
+void ui_togglebutton_set(UiInteger *i, int64_t v) {
+    UiWidget *btn = (UiWidget*)i->obj;
+    WPARAM state = v ? BST_CHECKED : BST_UNCHECKED;
+    SendMessage(btn->widget.hwnd, BM_SETCHECK, state, 0);
+    i->value = v;
+}
+
+int ui_togglebutton_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+    if (uMsg != WM_COMMAND) {
+        return 0;
+    }
+    UiWidget *w = (UiWidget*)widget;
+
+    UiEvent e;
+    e.obj = w->obj;
+    e.document = e.obj->ctx->document;
+    e.window = e.obj->window;
+    e.eventdata = NULL;
+    e.eventdatatype = 0;
+    e.intval = SendMessage(w->widget.hwnd, BM_GETCHECK, 0, 0);
+    e.set = ui_get_setop();
+
+    if (w->callback) {
+        w->callback(&e, w->callbackdata);
+    }
+
+    return 0;
+}
+
+
+UIWIDGET ui_radiobutton_create(UiObject *obj, UiToggleArgs *args) {
+    HINSTANCE hInstance = GetModuleHandle(NULL);
+    UiContainerPrivate *container = ui_obj_container(obj);
+    HWND parent = ui_container_get_parent(container);
+    UiLayout layout = UI_ARGS2LAYOUT(args);
+
+    HWND hwnd = CreateWindow(
+            "BUTTON",
+            args->label,
+            WS_VISIBLE | WS_CHILD | BS_AUTORADIOBUTTON | WS_GROUP ,
+            0, 0, 100, 30,
+            parent,
+            (HMENU)1,
+            hInstance,
+            NULL);
+    ui_win32_set_ui_font(hwnd);
+
+    W32Widget *widget = w32_widget_create(&radiobutton_widget_class, hwnd, sizeof(UiWidget));
+    ui_container_add(container, widget, &layout);
+    UiWidget *btn = (UiWidget*)widget;
+
+    UiVar *var = uic_widget_var(obj->ctx, obj->ctx, args->value, args->varname, UI_VAR_INTEGER);
+    if (var) {
+        UiInteger *i = var->value;
+        // Use a CxList as binding object (i->obj) and add all radiobuttons to it
+        // The first radiobutton, which binds to this var, creates the CxList
+        CxList *group = NULL;
+        if (i->obj) {
+            group = i->obj;
+        } else {
+            group = cxArrayListCreate(obj->ctx->allocator, NULL, CX_STORE_POINTERS, 8);
+            i->obj = group;
+        }
+
+        cxListAdd(group, btn);
+        if (i->value == cxListSize(group)) {
+            // TODO: select
+        }
+    }
+
+    btn->obj = obj;
+    btn->var = var;
+    btn->callback = args->onchange;
+    btn->callbackdata = args->onchangedata;
+
+    return widget;
+}
+
+int ui_radiobutton_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+    if (uMsg != WM_COMMAND) {
+        return 0;
+    }
+    UiWidget *w = (UiWidget*)widget;
+
+    int checked = SendMessage(w->widget.hwnd, BM_GETCHECK, 0, 0);
+    if (!checked) {
+        return 0; // ignore uncheck events
+    }
+
+    int b = 0;
+    if (w->var) {
+        UiInteger *i = w->var->value;
+        CxList *group = i->obj;
+        // Find selected index and uncheck all radiobuttons in this group
+        // that are not this event widget
+        CxIterator iter = cxListIterator(group);
+        cx_foreach(UiWidget *, radiobutton, iter) {
+            if (radiobutton == w) {
+                i->value = iter.index+1;
+                b = i->value;
+            } else {
+                SendMessage(radiobutton->widget.hwnd, BM_SETCHECK, BST_UNCHECKED, 0);
+            }
+        }
+    }
+
+    UiEvent e;
+    e.obj = w->obj;
+    e.document = e.obj->ctx->document;
+    e.window = e.obj->window;
+    e.eventdata = NULL;
+    e.eventdatatype = 0;
+    e.intval = b;
+    e.set = ui_get_setop();
+
+    if (w->callback) {
+        w->callback(&e, w->callbackdata);
+    }
+
+    return 0;
+}
+
+int64_t ui_radiobutton_get(UiInteger *i) {
+    return i->value;
+}
+
+void ui_radiobutton_set(UiInteger *i, int64_t v) {
+    CxList *group = i->obj;
+    CxIterator iter = cxListIterator(group);
+    cx_foreach(UiWidget *, radiobutton, iter) {
+        SendMessage(radiobutton->widget.hwnd, BM_SETCHECK, iter.index+1 == v ? BST_CHECKED : BST_UNCHECKED, 0);
+    }
+    i->value = v;
+}
--- a/ui/win32/button.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/win32/button.h	Sat Dec 13 15:58:58 2025 +0100
@@ -34,6 +34,14 @@
 
 W32Size ui_button_get_preferred_size(W32Widget *widget);
 
-void ui_button_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+int ui_button_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+
+int64_t ui_togglebutton_get(UiInteger *i);
+void ui_togglebutton_set(UiInteger *i, int64_t v);
+int ui_togglebutton_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+
+int64_t ui_radiobutton_get(UiInteger *i);
+void ui_radiobutton_set(UiInteger *i, int64_t v);
+int ui_radiobutton_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
 
 #endif //BUTTON_H
--- a/ui/win32/container.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/win32/container.c	Sat Dec 13 15:58:58 2025 +0100
@@ -35,7 +35,7 @@
 
 
 static W32WidgetClass grid_layout_widget_class = {
-    .eventproc = NULL,
+    .eventproc = ui_grid_widget_event,
     .enable = NULL,
     .show = w32_widget_default_show,
     .get_preferred_size = ui_grid_layout_get_preferred_size,
@@ -62,6 +62,18 @@
     ctn->container.newline = FALSE;
 }
 
+int ui_grid_widget_event(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+    if (uMsg == WM_ERASEBKGND) {
+        HDC hdc = (HDC)wParam;
+        UiGridWidget *grid = (UiGridWidget*)widget;
+        RECT rc;
+        GetClientRect(hwnd, &rc);
+        FillRect(hdc, &rc, grid->brush);
+        return 1;
+    }
+    return 0;
+}
+
 W32Size ui_grid_layout_get_preferred_size(W32Widget *widget) {
     UiGridLayout *grid = widget->layoutmanager;
     W32Size size;
@@ -134,26 +146,27 @@
             TEXT("STATIC"),
             NULL,
             WS_CHILD | WS_VISIBLE,
-            0, 0, 100, 100,
+            0, 0, 300, 300,
             parent,
             NULL,
             hInstance,
             NULL);
     SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)ui_default_eventproc);
 
-    W32Widget *widget = w32_widget_new(&grid_layout_widget_class, hwnd);
-    ui_container_add(container, widget, &layout);
+    UiGridWidget *widget = w32_widget_create(&grid_layout_widget_class, hwnd, sizeof(UiGridWidget));
+    ui_container_add(container, (W32Widget*)widget, &layout);
 
     UiContainerX *gridContainer = ui_grid_container_create(obj, hwnd, args->columnspacing, args->rowspacing, INSETS_ZERO);
     uic_object_push_container(obj, gridContainer);
 
     UiGridLayoutContainer *grid = (UiGridLayoutContainer*)gridContainer;
-    widget->layout = (W32LayoutFunc)ui_grid_layout;
-    widget->layoutmanager = grid->layout;
+    widget->widget.layout = (W32LayoutFunc)ui_grid_layout;
+    widget->widget.layoutmanager = grid->layout;
+    widget->brush = GetSysColorBrush(COLOR_BTNFACE);
     grid->layout->preferred_width = 200;
     grid->layout->preferred_height = 200;
 
-    return widget;
+    return (W32Widget*)widget;
 }
 
 UiContainerX* ui_grid_container_create(UiObject *obj, HWND hwnd, short columnspacing, short rowspacing, GridEdgeInsets padding) {
--- a/ui/win32/container.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/win32/container.h	Sat Dec 13 15:58:58 2025 +0100
@@ -40,6 +40,7 @@
 typedef struct UiContainerPrivate    UiContainerPrivate;
 typedef struct UiGridLayoutContainer UiGridLayoutContainer;
 typedef struct UiBoxContainer        UiBoxContainer;
+typedef struct UiGridWidget          UiGridWidget;
 
 enum UiBoxOrientation {
     UI_BOX_VERTICAL = 0,
@@ -88,10 +89,16 @@
     UiBool def_vfill;
 };
 
+struct UiGridWidget {
+    W32Widget widget;
+    HBRUSH brush;
+};
+
 UiContainerPrivate* ui_obj_container(UiObject *obj);
 HWND ui_container_get_parent(UiContainerPrivate *ctn);
 void ui_container_add(UiContainerPrivate *ctn, W32Widget *widget, UiLayout *layout);
 
+int ui_grid_widget_event(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
 W32Size ui_grid_layout_get_preferred_size(W32Widget *widget);
 
 UiContainerX* ui_box_container_create(UiObject *obj, HWND hwnd, UiBoxOrientation orientation, short spacing, GridEdgeInsets padding);
--- a/ui/win32/grid.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/win32/grid.c	Sat Dec 13 15:58:58 2025 +0100
@@ -293,7 +293,9 @@
 
         child_x = col->pos + elm->layout.margin.left;
         child_y = row->pos + elm->layout.margin.top;
-        SetWindowPos(elm->widget->hwnd, NULL, child_x, child_y, child_width, child_height, SWP_NOZORDER);
+        SetWindowPos(elm->widget->hwnd, NULL, child_x, child_y, child_width, child_height, SWP_NOZORDER | SWP_NOACTIVATE);
+        InvalidateRect(elm->widget->hwnd, NULL, TRUE);
+        //UpdateWindow(elm->widget->hwnd);
         if (elm->widget->layout) {
             elm->widget->layout(elm->widget->layoutmanager, child_width, child_height);
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/win32/list.c	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,501 @@
+/*
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <cx/array_list.h>
+
+#include "list.h"
+#include "container.h"
+
+
+
+static W32WidgetClass listview_widget_class = {
+    .eventproc = ui_listview_eventproc,
+    .enable = w32_widget_default_enable,
+    .show = w32_widget_default_show,
+    .get_preferred_size = ui_listview_get_preferred_size,
+    .destroy  = w32_widget_default_destroy
+};
+
+static void* strmodel_getvalue(UiList *list, void *elm, int row, int col, void *userdata, UiBool *freeResult) {
+    return col == 0 ? elm : NULL;
+}
+
+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);
+}
+
+static void* null_getvalue(UiList *list, void *elm, int row, int col, void *userdata, UiBool *freeResult) {
+    return NULL;
+}
+
+/*
+ * Creates an UiListView widget object and initializes it from the UiListArgs
+ */
+static UiListView* create_listview_widget(UiObject *obj, W32WidgetClass *widget_class, HWND hwnd, UiListArgs *args, UiBool table) {
+    UiListView *listview = w32_widget_create(widget_class, hwnd, sizeof(UiListView));
+    listview->widget.hwnd = hwnd;
+    listview->obj = obj;
+    listview->preferred_width = args->width ? args->width : 300; // 300: default width/height
+    listview->preferred_height = args->height ? args->height : 300;
+    listview->onactivate = args->onactivate;
+    listview->onactivatedata = args->onactivatedata;
+    listview->onselection = args->onselection;
+    listview->onselectiondata = args->onselectiondata;
+    listview->ondragstart = args->ondragstart;
+    listview->ondragstartdata = args->ondragstartdata;
+    listview->ondragcomplete = args->ondragcomplete;
+    listview->ondragcompletedata = args->ondragcompletedata;
+    listview->ondrop = args->ondrop;
+    listview->ondropdata = args->ondropdata;
+    listview->istable = table;
+
+    // convert ui_getvaluefunc into ui_getvaluefunc2 if necessary
+    ui_getvaluefunc2 getvalue = args->getvalue2;
+    void *getvaluedata = args->getvalue2data;
+    if(!getvalue) {
+        if(args->getvalue) {
+            getvalue = getvalue_wrapper;
+            getvaluedata = (void*)args->getvalue;
+        } else {
+            getvalue = table ? null_getvalue : strmodel_getvalue;
+        }
+    }
+    listview->getvalue = getvalue;
+    listview->getvaluedata = getvaluedata;
+
+    listview->var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST);
+
+    return listview;
+}
+
+static UIWIDGET listview_create(UiObject *obj, UiListArgs *args, UiBool table) {
+    HINSTANCE hInstance = GetModuleHandle(NULL);
+    UiContainerPrivate *container = ui_obj_container(obj);
+    HWND parent = ui_container_get_parent(container);
+    UiLayout layout = UI_ARGS2LAYOUT(args);
+
+    HWND hwnd = CreateWindowEx(
+            WS_EX_CLIENTEDGE,
+            WC_LISTVIEW,
+            "",
+            WS_CHILD | WS_VISIBLE | LVS_REPORT,
+            0, 0, 100, 100,
+            parent,
+            (HMENU)1337,
+            hInstance,
+            NULL);
+    ui_win32_set_ui_font(hwnd);
+    ListView_SetExtendedListViewStyle(
+        hwnd,
+        LVS_EX_FULLROWSELECT //| LVS_EX_GRIDLINES
+    );
+
+    UiListView *listview = create_listview_widget(obj, &listview_widget_class, hwnd, args, table);
+    ui_container_add(container, (W32Widget*)listview, &layout);
+
+    // init list model
+    // always initialize listview->model
+    int numcolumns = 0;
+    if (table) {
+        if (args->model) {
+            listview->model = ui_model_copy(obj->ctx, args->model);
+            numcolumns = listview->model->columns;
+        } else {
+            listview->model = ui_model_new(obj->ctx);
+        }
+    } else {
+        UiModel *model = ui_model_new(obj->ctx);
+        ui_model_add_column(model, UI_STRING, "Test", -1);
+        listview->model = model;
+        numcolumns = 1;
+    }
+
+    // create columns
+    UiModel *model = listview->model;
+    for (int i=0;i<numcolumns;i++) {
+        LVCOLUMN col;
+        UiModelType type = model->types[i];
+        char *title = model->titles[i];
+        size_t titlelen = title ? strlen(title) : 0;
+        int size = model->columnsize[i];
+        switch (type) {
+            default: {
+                col.mask = LVCF_TEXT | LVCF_WIDTH;
+                col.pszText = title;
+                col.cx = size > 0 ? size : titlelen*10+5;
+                break;
+            }
+            case UI_ICON: {
+                break; // TODO
+            }
+        }
+        ListView_InsertColumn(hwnd, i, &col);
+    }
+
+    // bind the listview to the provided UiList
+    if (listview->var) {
+        UiList *list = listview->var->value;
+        list->obj = listview;
+        list->update = ui_listview_update;
+        list->getselection = ui_listview_getselection_impl;
+        list->setselection = ui_listview_setselection_impl;
+
+        ui_listview_update(list, -1);
+    } else if (!table && args->static_elements && args->static_nelm > 0) {
+        char **static_elements = args->static_elements;
+        size_t static_nelm = args->static_nelm;
+        LVITEM item;
+        item.mask = LVIF_TEXT;
+        item.iSubItem = 0;
+        for (int i=0;i<static_nelm;i++) {
+            item.iItem = i;
+            item.pszText = static_elements[i];
+            ListView_InsertItem(hwnd, &item);
+        }
+        listview->getvalue = strmodel_getvalue;
+        listview->getvaluedata = NULL;
+    }
+
+    return (W32Widget*)listview;
+}
+
+static UiListSelection listview_get_selection2(HWND hwnd) {
+    UiListSelection sel = { 0, NULL };
+
+    CX_ARRAY_DECLARE(int, indices);
+    cx_array_initialize(indices, 8);
+
+    int index = -1;
+    while ((index = ListView_GetNextItem(hwnd, index, LVNI_SELECTED)) != -1) {
+        cx_array_simple_add(indices, index);
+    }
+
+    if (indices_size > 0) {
+        sel.rows = indices;
+        sel.count = indices_size;
+    }
+
+    return sel;
+}
+
+static UiListSelection listview_get_selection(UiListView *listview) {
+    HWND hwnd = listview->widget.hwnd;
+    return listview_get_selection2(hwnd);
+}
+
+// listview class event proc
+int ui_listview_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+    UiListView *listview = (UiListView*)widget;
+    switch (uMsg) {
+        case WM_NOTIFY: {
+            LPNMHDR hdr = (LPNMHDR)lParam;
+            switch (hdr->code) {
+                case LVN_ITEMCHANGED: {
+                    LPNMLISTVIEW lv = (LPNMLISTVIEW)lParam;
+                    int row = lv->iItem;
+                    if ((lv->uChanged & LVIF_STATE) && (lv->uNewState & LVIS_SELECTED) && listview->onselection) {
+                        UiListSelection sel = listview_get_selection(listview);
+
+                        UiEvent event;
+                        event.obj = listview->obj;
+                        event.window = listview->obj->window;
+                        event.document = listview->obj->ctx->document;
+                        event.eventdata = &sel;
+                        event.eventdatatype = UI_EVENT_DATA_LIST_SELECTION;
+                        event.intval = row;
+                        event.set = ui_get_setop();
+                        listview->onselection(&event, listview->onselectiondata);
+
+                        ui_listselection_free(sel);
+                    }
+                    break;
+                }
+                case LVN_ITEMACTIVATE: {
+                    LPNMLISTVIEW lv = (LPNMLISTVIEW)lParam;
+                    int row = lv->iItem;
+                    if (listview->onactivate) {
+                        UiEvent event;
+                        event.obj = listview->obj;
+                        event.window = listview->obj->window;
+                        event.document = listview->obj->ctx->document;
+                        event.eventdata = NULL;
+                        event.eventdatatype = UI_EVENT_DATA_LIST_ELM;
+                        event.intval = row;
+                        event.set = ui_get_setop();
+
+                        if (listview->var) {
+                            UiList *list = listview->var->value;
+                            event.eventdata = list->get(list, row);
+                            event.eventdatatype = UI_EVENT_DATA_LIST_ELM;
+                        }
+
+                        listview->onactivate(&event, listview->onactivatedata);
+                    }
+                    break;
+                }
+            }
+            break;
+        }
+    }
+
+    return 0;
+}
+
+W32Size ui_listview_get_preferred_size(W32Widget *widget) {
+    UiListView *listview = (UiListView*)widget;
+    W32Size size;
+    size.width = listview->preferred_width;
+    size.height = listview->preferred_height;
+    return size;
+}
+
+/*
+ * Creates and inserts an LVITEM
+ *
+ * list: An UiList bound to an UiListView
+ * row: row index
+ * elm: list element (same as list->get(list, row))
+ */
+static void insert_item(UiList *list, int row, void *elm) {
+    UiListView *listview = (UiListView*)list->obj;
+    HWND hwnd = listview->widget.hwnd;
+    UiModel *model = listview->model;
+
+    LVITEM item;
+    item.mask = LVIF_TEXT;
+    item.iItem = row;
+    item.iSubItem = 0;
+    int idx = -1;
+    for (int col=0;col<model->columns;col++) {
+        UiBool freeResult = FALSE;
+        // convert the list element to a value, that can be displayed in the list view
+        // TODO: handle all model types
+        char *str = listview->getvalue(list, elm, row, col, listview->getvaluedata, &freeResult);
+        if (col == 0) {
+            item.pszText = str;
+            idx = ListView_InsertItem(hwnd, &item);
+        } else {
+            ListView_SetItemText(hwnd, idx, col, str);
+        }
+
+        if (freeResult) {
+            free(str);
+        }
+    }
+}
+
+/*
+ * UiList->update function
+ *
+ * Updates one or all rows
+ * row: list index or -1 for updating all rows
+ */
+void ui_listview_update(UiList *list, int row) {
+    UiListView *listview = (UiListView*)list->obj;
+    HWND hwnd = listview->widget.hwnd;
+    UiModel *model = listview->model;
+    if (row < 0) {
+        ListView_DeleteAllItems(hwnd);
+        void *elm = list->first(list);
+        int row = 0;
+        while (elm) {
+            insert_item(list, row, elm);
+            elm = list->next(list);
+            row++;
+        }
+    } else {
+        ListView_DeleteItem(hwnd, row);
+        void *elm = list->get(list, row);
+        insert_item(list, row, elm);
+    }
+
+    // re-adjust all columns
+    for (int i=0;i<model->columns;i++) {
+        ListView_SetColumnWidth(hwnd, i, LVSCW_AUTOSIZE);
+    }
+}
+
+UiListSelection ui_listview_getselection_impl(UiList *list) {
+    UiListView *listview = (UiListView*)list->obj;
+    return listview_get_selection(listview);
+}
+
+void ui_listview_setselection_impl(UiList *list, UiListSelection selection) {
+
+}
+
+// public API
+UIWIDGET ui_listview_create(UiObject *obj, UiListArgs *args) {
+    return listview_create(obj, args, FALSE);
+}
+
+// public API
+UIWIDGET ui_table_create(UiObject *obj, UiListArgs *args) {
+    return listview_create(obj, args, TRUE);
+}
+
+void ui_listview_select(UIWIDGET listview, int index) {
+
+}
+
+int ui_listview_selection(UIWIDGET listview) {
+    W32Widget *w = (W32Widget*)listview;
+    UiListSelection sel = listview_get_selection2(w->hwnd);
+    int index = -1;
+    if (sel.count > 0) {
+        index = sel.rows[0];
+    }
+    free(sel.rows);
+    return index;
+}
+
+/* ------------------------------------ DropDown ------------------------------------*/
+
+static W32WidgetClass dropdown_widget_class = {
+    .eventproc = ui_dropdown_eventproc,
+    .enable = w32_widget_default_enable,
+    .show = w32_widget_default_show,
+    .get_preferred_size = ui_dropdown_get_preferred_size,
+    .destroy  = w32_widget_default_destroy
+};
+
+UIWIDGET ui_dropdown_create(UiObject *obj, UiListArgs *args) {
+    HINSTANCE hInstance = GetModuleHandle(NULL);
+    UiContainerPrivate *container = ui_obj_container(obj);
+    HWND parent = ui_container_get_parent(container);
+    UiLayout layout = UI_ARGS2LAYOUT(args);
+
+    HWND hwnd = CreateWindowEx(
+            WS_EX_CLIENTEDGE,
+            WC_COMBOBOX,
+            "",
+            WS_CHILD | WS_VISIBLE | WS_VSCROLL | CBS_DROPDOWNLIST,
+            0, 0, 100, 100,
+            parent,
+            (HMENU)1337,
+            hInstance,
+            NULL);
+    ui_win32_set_ui_font(hwnd);
+
+    UiListView *dropdown = create_listview_widget(obj, &dropdown_widget_class, hwnd, args, FALSE);
+    ui_container_add(container, (W32Widget*)dropdown, &layout);
+
+    // bind the dropdown to the provided UiList
+    if (dropdown->var) {
+        UiList *list = dropdown->var->value;
+        list->obj = dropdown;
+        list->update = ui_dropdown_update;
+        list->getselection = ui_dropdown_getselection_impl;
+        list->setselection = ui_dropdown_setselection_impl;
+
+        ui_dropdown_update(list, -1);
+    } else if (args->static_elements && args->static_nelm > 0) {
+        char **static_elements = args->static_elements;
+        size_t static_nelm = args->static_nelm;
+        for (int i=0;i<static_nelm;i++) {
+            SendMessage(hwnd, CB_ADDSTRING, 0, (LPARAM)static_elements[i]);
+        }
+        dropdown->getvalue = strmodel_getvalue;
+        dropdown->getvaluedata = NULL;
+    }
+
+    return (W32Widget*)dropdown;
+}
+
+int ui_dropdown_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+    return 0;
+}
+
+W32Size ui_dropdown_get_preferred_size(W32Widget *widget) {
+    W32Size size;
+    size.width = 200;
+    size.height = 30;
+    return size;
+}
+
+static void dropdown_insert_item(UiList *list, int row, void *elm) {
+    UiListView *listview = (UiListView*)list->obj;
+    HWND hwnd = listview->widget.hwnd;
+
+    UiBool freeResult = FALSE;
+    char *str = listview->getvalue(list, elm, row, 0, listview->getvaluedata, &freeResult);
+    SendMessage(hwnd, CB_ADDSTRING, 0, (LPARAM)str);
+
+    if (freeResult) {
+        free(str);
+    }
+}
+
+void ui_dropdown_update(UiList *list, int row) {
+    UiListView *listview = (UiListView*)list->obj;
+    HWND hwnd = listview->widget.hwnd;
+    if (row < 0) {
+        SendMessage(hwnd, CB_RESETCONTENT, 0, 0);
+
+        void *elm = list->first(list);
+        int row = 0;
+        while (elm) {
+            dropdown_insert_item(list, row, elm);
+            elm = list->next(list);
+            row++;
+        }
+    } else {
+        SendMessage(hwnd, CB_DELETESTRING, row, 0);
+        void *elm = list->get(list, row);
+        dropdown_insert_item(list, row, elm);
+    }
+}
+
+UiListSelection ui_dropdown_getselection_impl(UiList *list) {
+    UiListSelection sel = { 0, NULL };
+    UiListView *listview = (UiListView*)list->obj;
+    int index = (int)SendMessage(listview->widget.hwnd, CB_GETCURSEL, 0, 0);
+    if (index >= 0) {
+        sel.rows = malloc(sizeof(int));
+        sel.rows[0] = index;
+        sel.count = 1;
+    }
+    return sel;
+}
+
+void ui_dropdown_setselection_impl(UiList *list, UiListSelection selection) {
+    UiListView *listview = (UiListView*)list->obj;
+    SendMessage(listview->widget.hwnd, CB_SETCURSEL, 0, 0);
+}
+
+void ui_dropdown_select(UIWIDGET dropdown, int index) {
+    SendMessage(dropdown->hwnd, CB_SETCURSEL, 0, 0);
+}
+
+int ui_dropdown_selection(UIWIDGET dropdown) {
+    return SendMessage(dropdown->hwnd, CB_GETCURSEL, 0, 0);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/win32/list.h	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,83 @@
+/*
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef LIST_H
+#define LIST_H
+
+#include "toolkit.h"
+#include "../ui/list.h"
+#include "win32.h"
+#include <commctrl.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct UiListView {
+    W32Widget widget;
+    UiObject *obj;
+    UiVar *var;
+    ui_getvaluefunc2 getvalue;
+    void *getvaluedata;
+    ui_getstylefunc getstyle;
+    void *getstyledata;
+    UiModel *model;
+    UiBool istable;
+    int preferred_width;
+    int preferred_height;
+    ui_callback onactivate;
+    void* onactivatedata;
+    ui_callback onselection;
+    void* onselectiondata;
+    ui_callback ondragstart;
+    void* ondragstartdata;
+    ui_callback ondragcomplete;
+    void* ondragcompletedata;
+    ui_callback ondrop;
+    void* ondropdata;
+} UiListView;
+
+int ui_listview_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+W32Size ui_listview_get_preferred_size(W32Widget *widget);
+
+void ui_listview_update(UiList *list, int row);
+UiListSelection ui_listview_getselection_impl(UiList *list);
+void ui_listview_setselection_impl(UiList *list, UiListSelection selection);
+
+int ui_dropdown_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+W32Size ui_dropdown_get_preferred_size(W32Widget *widget);
+
+void ui_dropdown_update(UiList *list, int row);
+UiListSelection ui_dropdown_getselection_impl(UiList *list);
+void ui_dropdown_setselection_impl(UiList *list, UiListSelection selection);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //_LIST_H
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/win32/menu.c	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,97 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2024 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "menu.h"
+
+static ui_menu_add_f createMenuItem[] = {
+    /* UI_MENU                 */ ui_add_menu,
+    /* UI_MENU_ITEM            */ ui_add_menu_item,
+    /* UI_MENU_CHECK_ITEM      */ ui_add_menu_checkitem,
+    /* UI_MENU_RADIO_ITEM      */ ui_add_menu_radioitem,
+    /* UI_MENU_ITEM_LIST       */ ui_add_menu_list,
+    /* UI_MENU_CHECKITEM_LIST  */ ui_add_menu_checklist,
+    /* UI_MENU_RADIOITEM_LIST  */ ui_add_menu_radiolist,
+    /* UI_MENU_SEPARATOR       */ ui_add_menu_separator
+};
+
+
+HMENU ui_create_main_menu(UiObject *obj) {
+    UiMenu *menu = uic_get_menu_list();
+    if (!menu) {
+        return NULL;
+    }
+
+    HMENU hMenu = CreateMenu();
+    ui_add_menu(hMenu, 0, &menu->item, obj);
+
+
+
+    return hMenu;
+}
+
+void ui_add_menu(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj) {
+    UiMenu *menu = (UiMenu*)item;
+    HMENU hMenu = CreatePopupMenu();
+    AppendMenu(parent, MF_POPUP, (UINT_PTR)hMenu, menu->label);
+
+    int i = 0;
+    UiMenuItemI *child = menu->items_begin;
+    while (child) {
+        createMenuItem[child->type](hMenu, i++, child, obj);
+        child = child->next;
+    }
+}
+
+void ui_add_menu_item(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj) {
+    UiMenuItem *i = (UiMenuItem*)item;
+    AppendMenu(parent, MF_STRING, 0, i->label);
+}
+
+void ui_add_menu_checkitem(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj) {
+
+}
+
+void ui_add_menu_radioitem(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj) {
+
+}
+
+void ui_add_menu_list(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj) {
+
+}
+
+void ui_add_menu_checklist(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj) {
+
+}
+
+void ui_add_menu_radiolist(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj) {
+
+}
+
+void ui_add_menu_separator(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj) {
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/win32/menu.h	Sat Dec 13 15:58:58 2025 +0100
@@ -0,0 +1,57 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef MENU_H
+#define MENU_H
+
+#include "toolkit.h"
+#include "../ui/menu.h"
+#include "../common/menu.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void(*ui_menu_add_f)(HMENU, int, UiMenuItemI*, UiObject*);
+
+HMENU ui_create_main_menu(UiObject *obj);
+
+void ui_add_menu(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj);
+void ui_add_menu_item(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj);
+void ui_add_menu_checkitem(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj);
+void ui_add_menu_radioitem(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj);
+void ui_add_menu_list(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj);
+void ui_add_menu_checklist(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj);
+void ui_add_menu_radiolist(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj);
+void ui_add_menu_separator(HMENU parent, int pos, UiMenuItemI *item, UiObject *obj);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //MENU_H
\ No newline at end of file
--- a/ui/win32/objs.mk	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/win32/objs.mk	Sat Dec 13 15:58:58 2025 +0100
@@ -38,6 +38,8 @@
 WIN32OBJ += button.obj
 WIN32OBJ += grid.obj
 WIN32OBJ += text.obj
+WIN32OBJ += list.obj
+WIN32OBJ += menu.obj
 
 TOOLKITOBJS += $(WIN32OBJ:%=$(WIN32_OBJPRE)%)
 TOOLKITSOURCE += $(WIN32OBJ:%.obj=win32/%.c)
--- a/ui/win32/text.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/win32/text.c	Sat Dec 13 15:58:58 2025 +0100
@@ -28,6 +28,135 @@
 
 #include "text.h"
 
+static W32WidgetClass textarea_widget_class = {
+    .eventproc = ui_textarea_eventproc,
+    .enable = w32_widget_default_enable,
+    .show = w32_widget_default_show,
+    .get_preferred_size = ui_textarea_get_preferred_size,
+    .destroy  = w32_widget_default_destroy
+};
+
+UIWIDGET ui_textarea_create(UiObject *obj, UiTextAreaArgs *args) {
+    HINSTANCE hInstance = GetModuleHandle(NULL);
+    UiContainerPrivate *container = ui_obj_container(obj);
+    HWND parent = ui_container_get_parent(container);
+    UiLayout layout = UI_ARGS2LAYOUT(args);
+
+    int width = args->width >= 0 ? args->width : 100;
+    int height = args->height >= 0 ? args->height : 100;
+
+    HWND hwnd = CreateWindowEx(
+            0,
+            "EDIT",
+            "",
+            WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_WANTRETURN | WS_VSCROLL,
+            0, 0, width, height,
+            parent,
+            (HMENU)0,
+            hInstance,
+            NULL);
+    ui_win32_set_ui_font(hwnd);
+
+    W32Widget *widget = w32_widget_create(&textarea_widget_class, hwnd, sizeof(UiTextArea));
+    ui_container_add(container, widget, &layout);
+
+    UiTextArea *textarea = (UiTextArea*)widget;
+    textarea->width = width;
+    textarea->widget.var = uic_widget_var(obj->ctx, obj->ctx, args->value, args->varname, UI_VAR_TEXT);
+    textarea->widget.callback = args->onchange;
+    textarea->widget.callbackdata = args->onchangedata;
+
+    if (textarea->widget.var) {
+        UiText *t = textarea->widget.var->value;
+
+        if (t->value.ptr) {
+            // TODO: set textarea string
+        }
+        t->obj = widget;
+        t->save = ui_textarea_save;
+        t->destroy = ui_textarea_destroy;
+        t->restore = ui_textarea_restore;
+        t->get = ui_textarea_get;
+        t->set = ui_textarea_set;
+        t->getsubstr = ui_textarea_getsubstr;
+        t->insert = ui_textarea_insert;
+        t->setposition = ui_textarea_setposition;
+        t->position = ui_textarea_position;
+        t->setselection = ui_textarea_setselection;
+        t->selection = ui_textarea_selection;
+        t->length = ui_textarea_length;
+        t->remove = ui_textarea_remove;
+    }
+
+    return widget;
+}
+
+W32Size ui_textarea_get_preferred_size(W32Widget *widget) {
+    W32Size size;
+    UiTextArea *textarea = (UiTextArea*)widget;
+    size.width = textarea->width;
+    size.height = textarea->height;
+    return size;
+}
+
+int ui_textarea_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+    return 0;
+}
+
+void  ui_textarea_save(UiText *text) {
+
+}
+
+void  ui_textarea_destroy(UiText *text) {
+
+}
+
+void  ui_textarea_restore(UiText *text) {
+
+}
+
+void  ui_textarea_set(UiText *text, const char *str) {
+
+}
+
+char* ui_textarea_get(UiText *text) {
+    return NULL;
+}
+
+char* ui_textarea_getsubstr(UiText *text, int begin, int end) {
+    return NULL;
+}
+
+void  ui_textarea_insert(UiText *text, int pos, char *str) {
+
+}
+
+void  ui_textarea_setposition(UiText *text, int pos) {
+
+}
+
+int   ui_textarea_position(UiText *text) {
+    return 0;
+}
+
+void  ui_textarea_setselection(UiText *text, int begin, int end) {
+
+}
+
+void  ui_textarea_selection(UiText *text, int *begin, int *end) {
+
+}
+
+int   ui_textarea_length(UiText *text) {
+    return 0;
+}
+
+void  ui_textarea_remove(UiText *text, int begin, int end) {
+
+}
+
+/* ----------------------------- TextField ----------------------------- */
+
 static W32WidgetClass textfield_widget_class = {
     .eventproc = ui_textfield_eventproc,
     .enable = w32_widget_default_enable,
@@ -84,8 +213,8 @@
     return (W32Size){ .width = textfield->width, .height = 32};
 }
 
-void ui_textfield_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
-
+int ui_textfield_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+    return 0;
 }
 
 char* ui_textfield_get(UiString *s) {
--- a/ui/win32/text.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/win32/text.h	Sat Dec 13 15:58:58 2025 +0100
@@ -33,13 +33,36 @@
 #include "container.h"
 #include "toolkit.h"
 
+typedef struct UiTextArea {
+    UiWidget widget;
+    int width;
+    int height;
+} UiTextArea;
+
 typedef struct UiTextField {
     UiWidget widget;
     int width;
 } UiTextField;
 
+W32Size ui_textarea_get_preferred_size(W32Widget *widget);
+int ui_textarea_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+
+void  ui_textarea_save(UiText *text);
+void  ui_textarea_destroy(UiText *text);
+void  ui_textarea_restore(UiText *text);
+void  ui_textarea_set(UiText *text, const char *str);
+char* ui_textarea_get(UiText *text);
+char* ui_textarea_getsubstr(UiText *text, int begin, int end);
+void  ui_textarea_insert(UiText *text, int pos, char *str);
+void  ui_textarea_setposition(UiText *text, int pos);
+int   ui_textarea_position(UiText *text);
+void  ui_textarea_setselection(UiText *text, int begin, int end);
+void  ui_textarea_selection(UiText *text, int *begin, int *end);
+int   ui_textarea_length(UiText *text);
+void  ui_textarea_remove(UiText *text, int begin, int end);
+
 W32Size ui_textfield_get_preferred_size(W32Widget *widget);
-void ui_textfield_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+int ui_textfield_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
 
 char* ui_textfield_get(UiString *s);
 void ui_textfield_set(UiString *s, const char *value);
--- a/ui/win32/toolkit.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/win32/toolkit.c	Sat Dec 13 15:58:58 2025 +0100
@@ -35,6 +35,7 @@
 #include "../common/toolbar.h"
 #include "../common/document.h"
 #include "../common/properties.h"
+#include "../common/app.h"
 
 #include "../ui/widget.h"
 
@@ -45,13 +46,6 @@
 
 static const char *application_name;
 
-static ui_callback   startup_func;
-static void          *startup_data;
-static ui_callback   open_func;
-void                 *open_data;
-static ui_callback   exit_func;
-void                 *exit_data;
-
 static HFONT ui_font = NULL;
 
 void ui_init(const char *appname, int argc, char **argv) {
@@ -62,9 +56,10 @@
     uic_toolbar_init();
     uic_load_app_properties();
 
-    ui_window_init();
-
-    INITCOMMONCONTROLSEX icex = { sizeof(icex), ICC_WIN95_CLASSES };
+    INITCOMMONCONTROLSEX icex = {
+        sizeof(icex),
+        ICC_WIN95_CLASSES | ICC_LISTVIEW_CLASSES | ICC_TREEVIEW_CLASSES | ICC_BAR_CLASSES | ICC_TAB_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES
+    };
     InitCommonControlsEx(&icex);
 
     SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
@@ -72,6 +67,8 @@
     NONCLIENTMETRICS ncm = { sizeof(ncm) };
     SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, FALSE);
     ui_font = CreateFontIndirect(&ncm.lfMessageFont);
+
+    ui_window_init();
 }
 
 HFONT ui_win32_get_font(void) {
@@ -88,25 +85,8 @@
     return application_name;
 }
 
-void ui_onstartup(ui_callback f, void *userdata) {
-    startup_func = f;
-    startup_data = userdata;
-}
-
-void ui_onopen(ui_callback f, void *userdata) {
-    open_func = f;
-    open_data = userdata;
-}
-
-void ui_onexit(ui_callback f, void *userdata) {
-    exit_func = f;
-    exit_data = userdata;
-}
-
 void ui_main() {
-    if(startup_func) {
-        startup_func(NULL, startup_data);
-    }
+    uic_application_startup(NULL);
 
     // event loop
     MSG msg;
@@ -115,9 +95,7 @@
         DispatchMessage(&msg);
     }
 
-    if(exit_func) {
-        exit_func(NULL, exit_data);
-    }
+    uic_application_exit(NULL);
     uic_store_app_properties();
 }
 
@@ -128,8 +106,11 @@
 LRESULT CALLBACK ui_default_eventproc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
     W32Widget *widget = (W32Widget*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
     if (widget && widget->wclass->eventproc) {
-        widget->wclass->eventproc(widget, hwnd, uMsg, wParam, lParam);
+        if (widget->wclass->eventproc(widget, hwnd, uMsg, wParam, lParam)) {
+            return 1;
+        }
     }
+
     switch(uMsg) {
         case WM_DESTROY: {
             PostQuitMessage(0);
@@ -141,16 +122,30 @@
             if (cmdWidget && cmdWidget->wclass->eventproc) {
                 cmdWidget->wclass->eventproc(cmdWidget, hwnd, uMsg, wParam, lParam);
             }
+            break;
+        }
+        case WM_NOTIFY: {
+            LPNMHDR hdr = (LPNMHDR)lParam;
+            HWND hwndCtrl = hdr->hwndFrom;
+            W32Widget *cmdWidget = (W32Widget*)GetWindowLongPtr(hwndCtrl, GWLP_USERDATA);
+            if (cmdWidget && cmdWidget->wclass->eventproc) {
+                cmdWidget->wclass->eventproc(cmdWidget, hwnd, uMsg, wParam, lParam);
+            }
+            break;
         }
         case WM_SIZE: {
             int width  = LOWORD(lParam);
             int height = HIWORD(lParam);
-            if (widget->layout) {
+            if (widget && widget->layout) {
                 widget->layout(widget->layoutmanager, width, height);
             }
             break;
         }
-        default: return DefWindowProc(hwnd, uMsg, wParam, lParam);
+        default: break;//return DefWindowProc(hwnd, uMsg, wParam, lParam);
     }
-    return 0;
-}
\ No newline at end of file
+    return DefWindowProc(hwnd, uMsg, wParam, lParam);;
+}
+
+void ui_call_mainthread(ui_threadfunc tf, void* td) {
+    // TODO
+}
--- a/ui/win32/window.c	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/win32/window.c	Sat Dec 13 15:58:58 2025 +0100
@@ -39,6 +39,7 @@
 #include <stdlib.h>
 
 #include "win32.h"
+#include "menu.h"
 
 static W32WidgetClass w32_toplevel_widget_class = {
 	.eventproc = ui_window_widget_event,
@@ -54,9 +55,9 @@
 
 
 void ui_window_init(void) {
-	hInstance = GetModuleHandle(NULL);
-	
-	WNDCLASSEX wc = { sizeof(WNDCLASSEX) };
+    hInstance = GetModuleHandle(NULL);
+
+    WNDCLASSEX wc = { sizeof(WNDCLASSEX) };
     wc.lpfnWndProc = ui_default_eventproc;
     wc.hInstance = hInstance;
     wc.lpszClassName = mainWindowClass;
@@ -69,49 +70,59 @@
     }
 }
 
-static UiObject* create_window(const char *title, void *window_data, bool simple) {
+static UiObject* create_window(const char *title, bool simple) {
     UiObject *obj = uic_object_new_toplevel();
-    obj->window = window_data;
 
-	HWND hwnd = CreateWindowExA(
-			0,
-			"UiMainWindow",
-			title,
-			WS_OVERLAPPEDWINDOW,
+    HWND hwnd = CreateWindowExA(
+            0,
+            "UiMainWindow",
+            title,
+            WS_OVERLAPPEDWINDOW,
+            CW_USEDEFAULT,
             CW_USEDEFAULT,
-			CW_USEDEFAULT,
-			800,
-			600,
+            800,
+            600,
+            NULL,
             NULL,
-			NULL,
-			hInstance,
-			NULL);
+            hInstance,
+            NULL);
+
+    if (!simple) {
+        HMENU menubar = ui_create_main_menu(obj);
+        if (menubar) {
+            SetMenu(hwnd, menubar);
+        }
+    }
 
     UpdateWindow(hwnd);
 
-	UiContainerX *container = ui_box_container_create(obj, hwnd, UI_BOX_VERTICAL, 0, INSETS_ZERO);
-	uic_object_push_container(obj, container);
-	UiBoxContainer *box = (UiBoxContainer*)container;
+    UiContainerX *container = ui_box_container_create(obj, hwnd, UI_BOX_VERTICAL, 0, INSETS_ZERO);
+    uic_object_push_container(obj, container);
+    UiBoxContainer *box = (UiBoxContainer*)container;
 
-	UiWindow *widget = w32_widget_create(&w32_toplevel_widget_class, hwnd, sizeof(UiWindow));
-	widget->obj = obj;
-	widget->widget.layout = (W32LayoutFunc)ui_grid_layout;
-	widget->widget.layoutmanager = box->layout;
-	obj->widget = (W32Widget*)widget;
-	obj->ref = 1;
+    UiWindow *widget = w32_widget_create(&w32_toplevel_widget_class, hwnd, sizeof(UiWindow));
+    widget->obj = obj;
+    widget->widget.layout = (W32LayoutFunc)ui_grid_layout;
+    widget->widget.layoutmanager = box->layout;
+    obj->widget = (W32Widget*)widget;
+    obj->ref = 1;
 
-	return obj;
+    return obj;
 }
 
-UiObject *ui_window(const char *title, void *window_data) {
-	return create_window(title, window_data, FALSE);
+UiObject *ui_window(const char *title) {
+    return create_window(title, FALSE);
 }
 
+UiObject *ui_simple_window(const char *title) {
+    return create_window(title, TRUE);
+}
 
-void ui_window_widget_event(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
-	//UiWindow *window = (UiWindow*)widget;
+int ui_window_widget_event(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+    //UiWindow *window = (UiWindow*)widget;
+    return 0;
 }
 
 void ui_window_widget_show(W32Widget *w, BOOLEAN show) {
-	ShowWindow(w->hwnd, show ? SW_SHOWNORMAL : SW_HIDE);
+    ShowWindow(w->hwnd, show ? SW_SHOWNORMAL : SW_HIDE);
 }
--- a/ui/win32/window.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/win32/window.h	Sat Dec 13 15:58:58 2025 +0100
@@ -48,7 +48,7 @@
 
 void ui_window_init(void);
 
-void ui_window_widget_event(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+int ui_window_widget_event(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
 void ui_window_widget_show(W32Widget *w, BOOLEAN show);
 
 #ifdef	__cplusplus
--- a/ui/winui/list.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/winui/list.h	Sat Dec 13 15:58:58 2025 +0100
@@ -28,7 +28,7 @@
 
 #pragma once
 
-#include "../ui/tree.h"
+#include "../ui/list.h"
 #include "toolkit.h"
 
 #include "../ui/container.h"
--- a/ui/winui/table.h	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/winui/table.h	Sat Dec 13 15:58:58 2025 +0100
@@ -28,7 +28,7 @@
 
 #pragma once
 
-#include "../ui/tree.h"
+#include "../ui/list.h"
 #include "toolkit.h"
 #include "dnd.h"
 
--- a/ui/winui/toolkit.cpp	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/winui/toolkit.cpp	Sat Dec 13 15:58:58 2025 +0100
@@ -37,6 +37,7 @@
 #include "../common/document.h"
 #include "../common/toolbar.h"
 #include "../common/properties.h"
+#include "../common/app.h"
 
 #include "icons.h"
 
@@ -55,15 +56,6 @@
 
 static const char* application_name;
 
-static ui_callback   startup_func;
-static void* startup_data;
-
-static ui_callback   open_func;
-void* open_data;
-
-static ui_callback   exit_func;
-void* exit_data;
-
 static ui_callback   appclose_fnc;
 
 static void* appclose_udata;
@@ -75,19 +67,14 @@
 
 void ui_app_run_startup() {
 	uiDispatcherQueue = winrt::Microsoft::UI::Dispatching::DispatcherQueue::GetForCurrentThread();
-	
-	if (startup_func) {
-		startup_func(NULL, startup_data);
-	}
+	uic_application_startup(NULL);
 }
 
 class App : public ApplicationT<App, IXamlMetadataProvider> {
 public:
 	void OnLaunched(LaunchActivatedEventArgs const&) {
 		Resources().MergedDictionaries().Append(XamlControlsResources());
-		if (startup_func) {
-			startup_func(NULL, startup_data);
-		}
+		uic_application_startup(NULL);
 
 		//auto window = make<winui::implementation::MainWindow>();
 		//window.Activate();
@@ -175,21 +162,6 @@
 	return application_name;
 }
 
-void ui_onstartup(ui_callback f, void* userdata) {
-	startup_func = f;
-	startup_data = userdata;
-}
-
-void ui_onopen(ui_callback f, void* userdata) {
-	open_func = f;
-	open_data = userdata;
-}
-
-void ui_onexit(ui_callback f, void* userdata) {
-	exit_func = f;
-	exit_data = userdata;
-}
-
 void ui_main() {
 	/*
 	init_apartment();
--- a/ui/winui/window.cpp	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/winui/window.cpp	Sat Dec 13 15:58:58 2025 +0100
@@ -68,89 +68,88 @@
 	obj->wobj->window.Close();
 }
 
-UiObject* ui_window(const char* title, void* window_data) {
-	UiObject* obj = ui_simple_window(title, window_data);
+UiObject* ui_window(const char* title) {
+    UiObject* obj = ui_simple_window(title);
 
-	/*
-	if (uic_get_menu_list()) {
-		// create/add menubar
-		MenuBar mb = ui_create_menubar(obj);
-		mb.VerticalAlignment(VerticalAlignment::Top);
-		obj->container->Add(mb, false);
-	}
-	*/
+    /*
+    if (uic_get_menu_list()) {
+            // create/add menubar
+            MenuBar mb = ui_create_menubar(obj);
+            mb.VerticalAlignment(VerticalAlignment::Top);
+            obj->container->Add(mb, false);
+    }
+    */
 
-	if (uic_toolbar_isenabled()) {
-		// create a grid for the toolbar: ColumnDefinitions="Auto, *, Auto"
-		Grid toolbar_grid = Grid();
-		GridLength gl;
-		gl.Value = 0;
-		gl.GridUnitType = GridUnitType::Auto;
+    if (uic_toolbar_isenabled()) {
+        // create a grid for the toolbar: ColumnDefinitions="Auto, *, Auto"
+        Grid toolbar_grid = Grid();
+        GridLength gl;
+        gl.Value = 0;
+        gl.GridUnitType = GridUnitType::Auto;
 
-		ColumnDefinition coldef0 = ColumnDefinition();
-		coldef0.Width(gl);
-		toolbar_grid.ColumnDefinitions().Append(coldef0);
+        ColumnDefinition coldef0 = ColumnDefinition();
+        coldef0.Width(gl);
+        toolbar_grid.ColumnDefinitions().Append(coldef0);
 
-		gl.Value = 1;
-		gl.GridUnitType = GridUnitType::Star;
-		ColumnDefinition coldef1 = ColumnDefinition();
-		coldef1.Width(gl);
-		toolbar_grid.ColumnDefinitions().Append(coldef1);
+        gl.Value = 1;
+        gl.GridUnitType = GridUnitType::Star;
+        ColumnDefinition coldef1 = ColumnDefinition();
+        coldef1.Width(gl);
+        toolbar_grid.ColumnDefinitions().Append(coldef1);
 
-		gl.Value = 0;
-		gl.GridUnitType = GridUnitType::Auto;
-		ColumnDefinition coldef2 = ColumnDefinition();
-		coldef2.Width(gl);
-		toolbar_grid.ColumnDefinitions().Append(coldef2);
+        gl.Value = 0;
+        gl.GridUnitType = GridUnitType::Auto;
+        ColumnDefinition coldef2 = ColumnDefinition();
+        coldef2.Width(gl);
+        toolbar_grid.ColumnDefinitions().Append(coldef2);
 
-		// rowdef
-		gl.Value = 0;
-		gl.GridUnitType = GridUnitType::Auto;
-		RowDefinition rowdef = RowDefinition();
-		rowdef.Height(gl);
-		toolbar_grid.RowDefinitions().Append(rowdef);
+        // rowdef
+        gl.Value = 0;
+        gl.GridUnitType = GridUnitType::Auto;
+        RowDefinition rowdef = RowDefinition();
+        rowdef.Height(gl);
+        toolbar_grid.RowDefinitions().Append(rowdef);
 
 
-		// create commandbar
-		CxList* def_l = uic_get_toolbar_defaults(UI_TOOLBAR_LEFT);
-		CxList* def_c = uic_get_toolbar_defaults(UI_TOOLBAR_CENTER);
-		CxList* def_r = uic_get_toolbar_defaults(UI_TOOLBAR_RIGHT);
+        // create commandbar
+        CxList* def_l = uic_get_toolbar_defaults(UI_TOOLBAR_LEFT);
+        CxList* def_c = uic_get_toolbar_defaults(UI_TOOLBAR_CENTER);
+        CxList* def_r = uic_get_toolbar_defaults(UI_TOOLBAR_RIGHT);
 
-		bool addappmenu = true;
-		if (cxListSize(def_r) > 0) {
-			CommandBar toolbar_r = ui_create_toolbar(obj, def_r, addappmenu);
-			toolbar_grid.SetColumn(toolbar_r, 2);
-			toolbar_grid.SetRow(toolbar_r, 0);
-			toolbar_grid.Children().Append(toolbar_r);
-			addappmenu = false;
-		}
-		if (cxListSize(def_c) > 0) {
-			CommandBar toolbar_c = ui_create_toolbar(obj, def_c, addappmenu);
-			toolbar_c.HorizontalAlignment(HorizontalAlignment::Center);
-			toolbar_grid.SetColumn(toolbar_c, 1);
-			toolbar_grid.SetRow(toolbar_c, 0);
-			toolbar_grid.Children().Append(toolbar_c);
-			addappmenu = false;
-		}
-		if (cxListSize(def_l) > 0) {
-			CommandBar toolbar_l = ui_create_toolbar(obj, def_l, addappmenu);
-			toolbar_grid.SetColumn(toolbar_l, 0);
-			toolbar_grid.SetRow(toolbar_l, 0);
-			toolbar_grid.Children().Append(toolbar_l);
-		}
+        bool addappmenu = true;
+        if (cxListSize(def_r) > 0) {
+            CommandBar toolbar_r = ui_create_toolbar(obj, def_r, addappmenu);
+            toolbar_grid.SetColumn(toolbar_r, 2);
+            toolbar_grid.SetRow(toolbar_r, 0);
+            toolbar_grid.Children().Append(toolbar_r);
+            addappmenu = false;
+        }
+        if (cxListSize(def_c) > 0) {
+            CommandBar toolbar_c = ui_create_toolbar(obj, def_c, addappmenu);
+            toolbar_c.HorizontalAlignment(HorizontalAlignment::Center);
+            toolbar_grid.SetColumn(toolbar_c, 1);
+            toolbar_grid.SetRow(toolbar_c, 0);
+            toolbar_grid.Children().Append(toolbar_c);
+            addappmenu = false;
+        }
+        if (cxListSize(def_l) > 0) {
+            CommandBar toolbar_l = ui_create_toolbar(obj, def_l, addappmenu);
+            toolbar_grid.SetColumn(toolbar_l, 0);
+            toolbar_grid.SetRow(toolbar_l, 0);
+            toolbar_grid.Children().Append(toolbar_l);
+        }
 
-		toolbar_grid.VerticalAlignment(VerticalAlignment::Top);
-		obj->container->Add(toolbar_grid, false);
-	}
+        toolbar_grid.VerticalAlignment(VerticalAlignment::Top);
+        obj->container->Add(toolbar_grid, false);
+    }
 
-	return obj;
+    return obj;
 }
 
-UIEXPORT UiObject* ui_simple_window(const char *title, void *window_data) {
+UIEXPORT UiObject* ui_simple_window(const char *title) {
 	UiObject* obj = uic_object_new_toplevel();
 
 	obj->ctx = uic_context(obj, mp);
-	obj->window = window_data;
 
 	Window window = Window();
 	//Window window = make<winui::implementation::MainWindow>();
--- a/ui/winui/winui.vcxproj	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/winui/winui.vcxproj	Sat Dec 13 15:58:58 2025 +0100
@@ -142,7 +142,7 @@
     <ClInclude Include="..\ui\text.h" />
     <ClInclude Include="..\ui\toolbar.h" />
     <ClInclude Include="..\ui\toolkit.h" />
-    <ClInclude Include="..\ui\tree.h" />
+    <ClInclude Include="..\ui\list.h" />
     <ClInclude Include="..\ui\ui.h" />
     <ClInclude Include="..\ui\window.h" />
     <ClInclude Include="appmenu.h" />
--- a/ui/winui/winui.vcxproj.filters	Sun Dec 07 20:00:33 2025 +0100
+++ b/ui/winui/winui.vcxproj.filters	Sat Dec 13 15:58:58 2025 +0100
@@ -108,7 +108,7 @@
     <ClInclude Include="..\ui\toolkit.h">
       <Filter>public</Filter>
     </ClInclude>
-    <ClInclude Include="..\ui\tree.h">
+    <ClInclude Include="..\ui\list.h">
       <Filter>public</Filter>
     </ClInclude>
     <ClInclude Include="..\ui\ui.h">

mercurial