diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4c4a3ccecd0c..40edc6397a3c 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -137,6 +137,7 @@ FILE(GLOB SOURCE_FILES
"dtgtk/gradientslider.c"
"dtgtk/icon.c"
"dtgtk/paint.c"
+ "dtgtk/paint_cell.c"
"dtgtk/range.c"
"dtgtk/resetlabel.c"
"dtgtk/sidepanel.c"
diff --git a/src/dtgtk/paint.c b/src/dtgtk/paint.c
index ae2fe893fafd..07ffa1d3a84d 100644
--- a/src/dtgtk/paint.c
+++ b/src/dtgtk/paint.c
@@ -2140,6 +2140,32 @@ void dtgtk_cairo_paint_help(cairo_t *cr, const gint x, const gint y, const gint
FINISH
}
+void dtgtk_cairo_paint_info(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, gint flags, void *data)
+{
+ PREAMBLE(0.95, 1, 0, 0)
+
+ // dot (filled so it reads as a tittle, not a tiny ring)
+ cairo_arc(cr, 0.5, 0.22, 0.05, 0.0, 2.0 * M_PI);
+ cairo_fill(cr);
+
+ // stem
+ cairo_move_to(cr, 0.5, 0.40);
+ cairo_line_to(cr, 0.5, 0.72);
+ // top serif (asymmetric, ends at stem centreline)
+ cairo_move_to(cr, 0.35, 0.40);
+ cairo_line_to(cr, 0.50, 0.40);
+ // bottom serif
+ cairo_move_to(cr, 0.35, 0.72);
+ cairo_line_to(cr, 0.65, 0.72);
+ // outer circle
+ cairo_new_sub_path(cr);
+ cairo_arc(cr, 0.5, 0.5, 0.45, 0.0, 2.0 * M_PI);
+
+ cairo_stroke(cr);
+
+ FINISH
+}
+
void dtgtk_cairo_paint_grouping(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, const gint flags, void *data)
{
PREAMBLE(1, 1, 0, 0)
diff --git a/src/dtgtk/paint.h b/src/dtgtk/paint.h
index ee7c3cc0267d..29e607d563e2 100644
--- a/src/dtgtk/paint.h
+++ b/src/dtgtk/paint.h
@@ -159,6 +159,8 @@ void dtgtk_cairo_paint_messages(cairo_t *cr, gint x, gint y, gint w, gint h, gin
void dtgtk_cairo_paint_styles(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data);
/** paint the ? help label */
void dtgtk_cairo_paint_help(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data);
+/** paint the i info label */
+void dtgtk_cairo_paint_info(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data);
/** paint the grouping icon. */
void dtgtk_cairo_paint_grouping(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data);
/** paint the preferences wheel. */
diff --git a/src/dtgtk/paint_cell.c b/src/dtgtk/paint_cell.c
new file mode 100644
index 000000000000..dd3650573908
--- /dev/null
+++ b/src/dtgtk/paint_cell.c
@@ -0,0 +1,122 @@
+/*
+ This file is part of darktable,
+ Copyright (C) 2026 darktable developers.
+
+ darktable is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ darktable is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with darktable. If not, see .
+*/
+
+#include "dtgtk/paint_cell.h"
+#include "common/darktable.h"
+#include "gui/gtk.h"
+
+G_DEFINE_TYPE(GtkDarktablePaintCell, dtgtk_paint_cell, GTK_TYPE_CELL_RENDERER)
+
+// icon edge length, derived from the widget's line height
+static int _paint_cell_compute_size(GtkWidget *widget)
+{
+ int s = DT_PIXEL_APPLY_DPI(12);
+ if(widget)
+ {
+ PangoContext *pctx = gtk_widget_get_pango_context(widget);
+ const PangoFontDescription *fd = pango_context_get_font_description(pctx);
+ PangoFontMetrics *m = pango_context_get_metrics(pctx, fd, NULL);
+ const int line_h = (pango_font_metrics_get_ascent(m)
+ + pango_font_metrics_get_descent(m)) / PANGO_SCALE;
+ pango_font_metrics_unref(m);
+ if(line_h > 0) s = line_h;
+ }
+ return s;
+}
+
+static void _paint_cell_get_preferred_width(GtkCellRenderer *r,
+ GtkWidget *widget,
+ gint *minimum_size,
+ gint *natural_size)
+{
+ (void)r;
+ const int s = _paint_cell_compute_size(widget);
+ if(minimum_size) *minimum_size = s;
+ if(natural_size) *natural_size = s;
+}
+
+static void _paint_cell_get_preferred_height(GtkCellRenderer *r,
+ GtkWidget *widget,
+ gint *minimum_size,
+ gint *natural_size)
+{
+ (void)r;
+ const int s = _paint_cell_compute_size(widget);
+ if(minimum_size) *minimum_size = s;
+ if(natural_size) *natural_size = s;
+}
+
+static void _paint_cell_render(GtkCellRenderer *r,
+ cairo_t *cr,
+ GtkWidget *widget,
+ const GdkRectangle *bg_area,
+ const GdkRectangle *cell_area,
+ GtkCellRendererState flags)
+{
+ (void)bg_area; (void)flags;
+ GtkDarktablePaintCell *self = DTGTK_PAINT_CELL(r);
+ if(!self->paint) return;
+
+ GdkRGBA fg;
+ GtkStyleContext *ctx = gtk_widget_get_style_context(widget);
+ gtk_style_context_get_color(ctx, gtk_widget_get_state_flags(widget), &fg);
+
+ const int mx = cell_area->width / 5;
+ const int my = cell_area->height / 5;
+
+ cairo_save(cr);
+ gdk_cairo_set_source_rgba(cr, &fg);
+ self->paint(cr,
+ cell_area->x + mx, cell_area->y + my,
+ cell_area->width - 2 * mx,
+ cell_area->height - 2 * my,
+ self->paint_flags, self->paint_data);
+ cairo_restore(cr);
+}
+
+static void dtgtk_paint_cell_class_init(GtkDarktablePaintCellClass *klass)
+{
+ GtkCellRendererClass *cr_class = GTK_CELL_RENDERER_CLASS(klass);
+ cr_class->get_preferred_width = _paint_cell_get_preferred_width;
+ cr_class->get_preferred_height = _paint_cell_get_preferred_height;
+ cr_class->render = _paint_cell_render;
+}
+
+static void dtgtk_paint_cell_init(GtkDarktablePaintCell *self)
+{
+ self->paint = NULL;
+ self->paint_flags = 0;
+ self->paint_data = NULL;
+}
+
+GtkCellRenderer *dtgtk_paint_cell_new(DTGTKCairoPaintIconFunc paint,
+ gint paint_flags,
+ void *paint_data)
+{
+ GtkDarktablePaintCell *cell = g_object_new(dtgtk_paint_cell_get_type(), NULL);
+ cell->paint = paint;
+ cell->paint_flags = paint_flags;
+ cell->paint_data = paint_data;
+ return GTK_CELL_RENDERER(cell);
+}
+
+// clang-format off
+// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
+// vim: shiftwidth=2 expandtab tabstop=2 cindent
+// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
+// clang-format on
diff --git a/src/dtgtk/paint_cell.h b/src/dtgtk/paint_cell.h
new file mode 100644
index 000000000000..1fa1636d4ef0
--- /dev/null
+++ b/src/dtgtk/paint_cell.h
@@ -0,0 +1,56 @@
+/*
+ This file is part of darktable,
+ Copyright (C) 2026 darktable developers.
+
+ darktable is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ darktable is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with darktable. If not, see .
+*/
+
+#pragma once
+
+#include "paint.h"
+#include
+
+G_BEGIN_DECLS
+
+#define DTGTK_TYPE_PAINT_CELL dtgtk_paint_cell_get_type()
+G_DECLARE_FINAL_TYPE(GtkDarktablePaintCell, dtgtk_paint_cell,
+ DTGTK, PAINT_CELL, GtkCellRenderer)
+
+struct _GtkDarktablePaintCell
+{
+ GtkCellRenderer parent;
+ DTGTKCairoPaintIconFunc paint;
+ gint paint_flags;
+ void *paint_data;
+};
+
+/** Cell renderer that draws a dtgtk cairo paint function directly
+ * into the tree view's cairo context — the same approach
+ * dtgtk_button uses for its icon. avoids the GtkCellRendererPixbuf
+ * intermediate, which softens edges at icon sizes.
+ *
+ * Visibility is controlled via the standard GtkCellRenderer "visible"
+ * property, typically bound to a model column via
+ * gtk_tree_view_column_new_with_attributes(..., "visible", COL, NULL). */
+GtkCellRenderer *dtgtk_paint_cell_new(DTGTKCairoPaintIconFunc paint,
+ gint paint_flags,
+ void *paint_data);
+
+G_END_DECLS
+
+// clang-format off
+// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
+// vim: shiftwidth=2 expandtab tabstop=2 cindent
+// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
+// clang-format on
diff --git a/src/gui/preferences_ai.c b/src/gui/preferences_ai.c
index 8935250ed9d0..49344610c52f 100644
--- a/src/gui/preferences_ai.c
+++ b/src/gui/preferences_ai.c
@@ -20,6 +20,7 @@
#include "bauhaus/bauhaus.h"
#include "dtgtk/button.h"
#include "dtgtk/paint.h"
+#include "dtgtk/paint_cell.h"
#include "ai/backend.h"
#include "common/ai_models.h"
#include "common/darktable.h"
@@ -69,7 +70,7 @@ enum
{
COL_SELECTED,
COL_NAME,
- COL_INFO, // info icon column (static "ⓘ" text)
+ COL_INFO, // info icon visibility flag (TRUE on downloaded rows)
COL_VERSION,
COL_TASK,
COL_ENABLED,
@@ -257,8 +258,7 @@ static void _refresh_model_list(dt_prefs_ai_data_t *data)
? (model->version ? model->version : "0.0") : "–",
COL_ID,
model->id,
- COL_INFO,
- is_downloaded ? "\xe2\x93\x98" : "", // U+24D8 CIRCLED LATIN SMALL LETTER I
+ COL_INFO, is_downloaded,
-1);
dt_ai_model_free(model);
}
@@ -1220,12 +1220,9 @@ static gboolean _info_active_at_bin(dt_prefs_ai_data_t *data,
&& column == data->info_col)
{
GtkTreeIter iter;
- gchar *info = NULL;
if(gtk_tree_model_get_iter(GTK_TREE_MODEL(data->model_store), &iter, path))
gtk_tree_model_get(GTK_TREE_MODEL(data->model_store),
- &iter, COL_INFO, &info, -1);
- active = (info && info[0]);
- g_free(info);
+ &iter, COL_INFO, &active, -1);
}
if(path) gtk_tree_path_free(path);
return active;
@@ -1309,13 +1306,12 @@ static gboolean _on_info_button_press(GtkWidget *widget,
if(gtk_tree_model_get_iter(GTK_TREE_MODEL(data->model_store), &iter, path))
{
gchar *model_id = NULL;
- gchar *info = NULL;
+ gboolean has_info = FALSE;
gtk_tree_model_get(GTK_TREE_MODEL(data->model_store),
- &iter, COL_ID, &model_id, COL_INFO, &info, -1);
- if(model_id && info && info[0])
+ &iter, COL_ID, &model_id, COL_INFO, &has_info, -1);
+ if(model_id && has_info)
_show_model_card(data, model_id);
g_free(model_id);
- g_free(info);
}
gtk_tree_path_free(path);
return TRUE;
@@ -1713,7 +1709,7 @@ void init_tab_ai(GtkWidget *dialog, GtkWidget *stack)
NUM_COLS,
G_TYPE_BOOLEAN, // selected
G_TYPE_STRING, // name
- G_TYPE_STRING, // info icon
+ G_TYPE_BOOLEAN, // info icon visible
G_TYPE_STRING, // version
G_TYPE_STRING, // task
G_TYPE_BOOLEAN, // enabled
@@ -1782,11 +1778,12 @@ void init_tab_ai(GtkWidget *dialog, GtkWidget *stack)
gtk_tree_view_append_column(GTK_TREE_VIEW(data->model_list), name_col);
// info icon column — click opens model card
- GtkCellRenderer *info_renderer = gtk_cell_renderer_text_new();
+ GtkCellRenderer *info_renderer
+ = dtgtk_paint_cell_new(dtgtk_cairo_paint_info, 0, NULL);
data->info_col = gtk_tree_view_column_new_with_attributes(
"",
info_renderer,
- "text",
+ "visible",
COL_INFO,
NULL);
gtk_tree_view_column_set_clickable(data->info_col, FALSE);