From 26951f43488c348de98ea6e59646f37cd3ccbde7 Mon Sep 17 00:00:00 2001 From: Andrii Ryzhkov Date: Fri, 22 May 2026 13:13:10 +0200 Subject: [PATCH] Replace platform-dependent info glyph in AI prefs model card with cairo-drawn icon --- src/CMakeLists.txt | 1 + src/dtgtk/paint.c | 26 +++++++++ src/dtgtk/paint.h | 2 + src/dtgtk/paint_cell.c | 122 +++++++++++++++++++++++++++++++++++++++ src/dtgtk/paint_cell.h | 56 ++++++++++++++++++ src/gui/preferences_ai.c | 25 ++++---- 6 files changed, 218 insertions(+), 14 deletions(-) create mode 100644 src/dtgtk/paint_cell.c create mode 100644 src/dtgtk/paint_cell.h 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);