Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
scroll_wrap.c
Go to the documentation of this file.
1/*
2 * This file is part of Ansel,
3 * Copyright (C) 2026 Aurélien PIERRE.
4 *
5 * Ansel is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * Ansel is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19#include "widgets/scroll_wrap.h"
20
21#include "system/macros.h" // IS_NULL_PTR
22#include "system/mem_alloc.h"
26#include <glib/gi18n.h>
27#include "widgets/container.h"
28
29static const char *const DT_GUI_WIDGET_AUTO_HEIGHT_KEY = "dt-gui-widget-auto-height";
30
31// ---- Resizable drawing area (the histogram-scope paradigm, made reusable) -------------------
32// A fixed-pixel-height GtkDrawingArea (or any widget sized by height-request) made vertically
33// resizable by the shared grip primitive, with the height persisted to config. Unlike the
34// scroll-wrap helper above, the content draws itself and is not scrolled; we only manage its
35// height-request, so the drawing code keeps reading the live allocation as usual.
36
37static const char *const DT_UI_RESIZABLE_AREA_KEY = "dt-ui-resizable-area";
38
40{
41 char *config_str; // conf key persisting the user-chosen height (px); owned
42 int min_height; // minimum height floor, device pixels
43 int last_height; // last applied height, shared with the drag handle
45
47{
48 gint height = DT_PIXEL_APPLY_DPI(10);
49
50 if(GTK_IS_TREE_VIEW(w))
51 {
52 gint row_height = 0;
53
55 for(int c = 0; c < num_columns; c++)
56 {
57 gint cell_height = 0;
61 }
63 gtk_widget_style_get_property(w, "vertical-separator", &separation);
64
66 }
67 else if(GTK_IS_TEXT_VIEW(w))
68 {
71 g_object_unref(layout);
72 }
73 else
74 {
76 if(child)
77 {
79 }
80 }
81
82 return height;
83}
84
85// find the scrolled window parent of a treeview, if any
87{
88 if(!GTK_IS_WIDGET(w)) return NULL;
89
90 GtkWidget *parent = w;
91 while(parent)
92 {
93 if(GTK_IS_SCROLLED_WINDOW(parent)) break;
94 parent = gtk_widget_get_parent(parent);
95 }
96
97 return GTK_IS_SCROLLED_WINDOW(parent) ? parent : NULL;
98}
99
100// Counts only visible items (those whose parents are expanded)
102{
103 if(!GTK_IS_TREE_MODEL(model)) return 0;
104
106 gboolean valid = parent ? gtk_tree_model_iter_children(model, &iter, parent)
108 int count = 0;
109
110 while(valid)
111 {
112 count++;
113
114 // If this item is expanded, recursively count its visible children
116 {
118 if(path)
119 {
120 if(gtk_tree_view_row_expanded(treeview, path))
121 {
122 count += _treeview_count_visible_rows(treeview, model, &iter);
123 }
124 gtk_tree_path_free(path);
125 }
126 }
127
129 }
130
131 return count;
132}
133
135{
136 if(!GTK_IS_TEXT_VIEW(textview)) return 0;
137
139 if(!GTK_IS_TEXT_BUFFER(buffer)) return 0;
140
141 // For text views, use the number of logical lines in the buffer.
142 return MAX(0, gtk_text_buffer_get_line_count(buffer));
143}
144
146{
147 if(IS_NULL_PTR(state)) return;
148
149 GtkTreeModel *model = state->model;
150 if(model)
151 {
152 if(state->model_row_inserted) g_signal_handler_disconnect(model, state->model_row_inserted);
153 if(state->model_row_deleted) g_signal_handler_disconnect(model, state->model_row_deleted);
154 if(state->model_row_changed) g_signal_handler_disconnect(model, state->model_row_changed);
155 if(state->model_rows_reordered) g_signal_handler_disconnect(model, state->model_rows_reordered);
156 g_object_remove_weak_pointer(G_OBJECT(model), (gpointer *)&state->model);
157 }
158
159 if(GTK_IS_TREE_VIEW(treeview))
160 {
161 if(state->model_row_expanded) g_signal_handler_disconnect(treeview, state->model_row_expanded);
162 if(state->model_row_collapsed) g_signal_handler_disconnect(treeview, state->model_row_collapsed);
163 }
164
165 state->model = NULL;
166 state->model_row_inserted = 0;
167 state->model_row_deleted = 0;
168 state->model_row_changed = 0;
169 state->model_rows_reordered = 0;
170 state->model_row_expanded = 0;
171 state->model_row_collapsed = 0;
172}
173
175{
176 if(IS_NULL_PTR(state)) return;
177
178 GtkTextBuffer *buffer = state->buffer;
179 if(buffer)
180 {
181 if(state->buffer_changed) g_signal_handler_disconnect(buffer, state->buffer_changed);
182 g_object_remove_weak_pointer(G_OBJECT(buffer), (gpointer *)&state->buffer);
183 }
184
185 state->buffer = NULL;
186 state->buffer_changed = 0;
187}
188
201
212{
214 if(IS_NULL_PTR(state)) return;
215
217 if(!GTK_IS_SCROLLED_WINDOW(sw)) return;
218
219 const gint max_height = _resizable_scroll_max_height();
220 const gint min_size = MAX(1, state->min_size);
221 int stored = 0;
222 const gboolean has_conf = dt_widget_stored_int(state->config_str, &stored);
223
224 gint height;
225 gint increment = 0;
226
227 if(state->mode == DT_UI_RESIZE_STATIC)
228 {
229 // Fixed height: the user's persisted size, or min_size as the default. Independent of content,
230 // so a content refresh (e.g. on hovering another thumbnail) never shifts the layout.
231 height = CLAMP(has_conf ? stored : min_size, min_size, max_height);
232 }
233 else
234 {
235 // Dynamic: fit to content, capped by the user's persisted height (or the window ceiling).
236 const gboolean row_based = GTK_IS_TREE_VIEW(w) || GTK_IS_TEXT_VIEW(w);
237 increment = row_based ? _get_container_row_heigth(w) : 0;
238
239 gint content = 0;
240 if(GTK_IS_TREE_VIEW(w))
241 {
243 content = MAX(1, rows) * increment;
244 }
245 else if(GTK_IS_TEXT_VIEW(w))
246 {
247 const int rows = _textview_count_visible_rows(w);
248 content = MAX(1, rows) * increment;
249 }
250 else
251 {
253 }
254
255 const gint cap = has_conf ? CLAMP(stored, min_size, max_height) : max_height;
256 height = CLAMP(MIN(content, cap), min_size, max_height);
257
258 // snap to whole rows for lists/textviews to avoid clipped half-rows
259 if(increment > 0)
260 {
261 height += increment - 1;
262 height -= height % increment;
263 }
264 }
265 state->last_height = height;
266
267 GtkBorder padding;
269 gtk_widget_get_state_flags(sw), &padding);
270
271 gint old_height = 0;
273 const gint new_height = height + padding.top + padding.bottom + (GTK_IS_TEXT_VIEW(w) ? 2 : 0);
276}
277
278static void _widget_auto_update(GtkWidget *widget)
279{
281}
282
283static gboolean _resizable_scroll_draw(GtkWidget *w, cairo_t *cr, gpointer user_data)
284{
286 return FALSE;
287}
288
289static void _resizable_scroll_realize(GtkWidget *w, gpointer user_data)
290{
292}
293
294// Drag handle accessors: bare (pre-padding) target height, kept consistent with the sizing rule.
295static int _resizable_scroll_handle_get_size(gpointer user_data)
296{
297 GtkWidget *w = GTK_WIDGET(user_data);
299 return state ? state->last_height : 0;
300}
301
302static int _resizable_scroll_handle_resize(int requested_size, gboolean finished, gpointer user_data)
303{
304 GtkWidget *w = GTK_WIDGET(user_data);
306 if(IS_NULL_PTR(state) || IS_NULL_PTR(state->config_str)) return requested_size;
307
308 const gint value = CLAMP(requested_size, MAX(1, state->min_size), _resizable_scroll_max_height());
309 dt_widget_store_int(state->config_str, value);
311 return state->last_height;
312}
313
315 gpointer user_data)
316{
317 (void)model;
318 (void)path;
319 (void)iter;
321}
322
323static void _widget_auto_model_row_deleted(GtkTreeModel *model, GtkTreePath *path, gpointer user_data)
324{
325 (void)model;
326 (void)path;
328}
329
331 gpointer user_data)
332{
333 (void)model;
334 (void)path;
335 (void)iter;
337}
338
340 gint *new_order, gpointer user_data)
341{
342 (void)model;
343 (void)path;
344 (void)iter;
347}
348
350 gpointer user_data)
351{
352 (void)tree_view;
354 (void)path;
356}
357
359 gpointer user_data)
360{
361 (void)tree_view;
363 (void)path;
364
365 // Recalculate tree view height after loading the data
367}
368
369static void _widget_auto_text_buffer_changed(GtkTextBuffer *buffer, gpointer user_data)
370{
371 (void)buffer;
373}
374
376{
377 if(!GTK_IS_TREE_VIEW(treeview)) return;
378
380 if(IS_NULL_PTR(state)) return;
381
383 if(model == state->model) return;
384
386 if(!GTK_IS_TREE_MODEL(model)) return;
387
388 state->model = model;
389 g_object_add_weak_pointer(G_OBJECT(model), (gpointer *)&state->model);
390 state->model_row_inserted = g_signal_connect(model, "row-inserted",
392 state->model_row_deleted = g_signal_connect(model, "row-deleted",
394 state->model_row_changed = g_signal_connect(model, "row-changed",
396 state->model_rows_reordered = g_signal_connect(model, "rows-reordered",
398 state->model_row_expanded = g_signal_connect(treeview, "row-expanded",
400 state->model_row_collapsed = g_signal_connect(treeview, "row-collapsed",
402}
403
405{
406 if(!GTK_IS_TEXT_VIEW(textview)) return;
407
409 if(IS_NULL_PTR(state)) return;
410
412 if(buffer == state->buffer) return;
413
415 if(!GTK_IS_TEXT_BUFFER(buffer)) return;
416
417 state->buffer = buffer;
418 g_object_add_weak_pointer(G_OBJECT(buffer), (gpointer *)&state->buffer);
419 state->buffer_changed = g_signal_connect(buffer, "changed",
421}
422
423static void _widget_auto_on_model_changed(GObject *treeview, GParamSpec *pspec, gpointer user_data)
424{
425 (void)pspec;
426 (void)user_data;
429}
430
431static void _widget_auto_on_buffer_changed(GObject *textview, GParamSpec *pspec, gpointer user_data)
432{
433 (void)pspec;
434 (void)user_data;
437}
438
448
465GtkWidget *dt_ui_scroll_wrap(GtkWidget *w, gint min_size, char *config_str, dt_ui_resize_mode_t mode)
466{
469 if(GTK_IS_TREE_VIEW(w)) dt_gui_add_class(sw, "dt_recessed_scroll");
471
472 // Per-widget sizing state, freed with w.
474 state->config_str = g_strdup(config_str);
475 state->min_size = MAX(1, DT_PIXEL_APPLY_DPI(min_size));
476 state->mode = mode;
478
479 if(mode == DT_UI_RESIZE_DYNAMIC)
480 {
481 // Sizing triggers. Lists/textviews recompute from their model/buffer change signals (cheap and
482 // exact) plus a one-shot on realize, when row heights become accurate -- we deliberately avoid a
483 // per-draw recount, which would re-walk a large model on every redraw. Generic content has no
484 // such signals, so it recomputes on draw using its (GTK-cached) preferred height.
485 if(GTK_IS_TREE_VIEW(w))
486 {
489 }
490 else if(GTK_IS_TEXT_VIEW(w))
491 {
494 }
495 else
496 {
498 }
501 }
502 else
503 {
504 // Static: height is content-independent, so we only size once after realization (and on drag).
506 }
507
508 // Drag grip floating on the scrolled window's bottom edge (overlay): it takes no layout space,
509 // so the wrapper leaves no margin-like gap and stays aligned with neighbouring widgets. The grip
510 // is centered on the bottom border via CSS and is invisible until hovered.
512 _("Drag to resize"),
515
516 GtkWidget *over = gtk_overlay_new();
519
521 return over;
522}
523
528{
529 if(!GTK_IS_CONTAINER(wrapper)) return NULL;
531 GtkWidget *sw = NULL;
532 for(GList *l = children; l; l = g_list_next(l))
533 {
534 if(GTK_IS_SCROLLED_WINDOW(l->data))
535 {
536 sw = GTK_WIDGET(l->data);
537 break;
538 }
539 }
541 return sw;
542}
543
544static void _resizable_area_free(gpointer data)
545{
547 if(IS_NULL_PTR(state)) return;
548 g_free(state->config_str);
549 dt_free(state);
550}
551
552static int _resizable_area_get_size(gpointer user_data)
553{
554 GtkWidget *area = GTK_WIDGET(user_data);
556 if(state) return state->last_height;
558}
559
560static int _resizable_area_resize(int requested_size, gboolean finished, gpointer user_data)
561{
562 GtkWidget *area = GTK_WIDGET(user_data);
564 if(IS_NULL_PTR(state)) return requested_size;
565
566 const int height = CLAMP(requested_size, MAX(1, state->min_height), _resizable_scroll_max_height());
567 state->last_height = height;
569 if(finished) dt_widget_store_int(state->config_str, height);
570 return height;
571}
572
573GtkWidget *dt_ui_resizable_drawing_area(GtkWidget *area, char *config_str, int default_height, int min_height)
574{
576 state->config_str = g_strdup(config_str);
577 state->min_height = MAX(1, DT_PIXEL_APPLY_DPI(min_height));
578
580 dt_widget_stored_int(config_str, &height);
582 state->last_height = height;
585
586 // Drag grip floating on the area's bottom edge (an overlay, not a packed sibling), so it takes
587 // no layout space -- the area stays flush with neighbouring widgets, no margin-like gap. It sits
588 // over the graph's bottom inset/axis margin (graphs reserve one), invisible until hovered.
590 _("Drag to resize"),
592
593 GtkWidget *over = gtk_overlay_new();
594 gtk_container_add(GTK_CONTAINER(over), area);
596 return over;
597}
598
599// clang-format off
600// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
601// vim: shiftwidth=2 expandtab tabstop=2 cindent
602// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
603// clang-format on
#define FALSE
Definition ashift_lsd.c:158
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
GtkWidget * dt_gui_container_first_child(GtkContainer *container)
Definition container.c:43
const char * model
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:65
#define dt_free(ptr)
Definition mem_alloc.h:97
uint32_t height
Definition mipmap_cache.c:1
GtkWidget * dtgtk_resize_handle_new(GtkOrientation orientation, gboolean invert, const char *tooltip, dtgtk_resize_handle_get_size_f get_size, dtgtk_resize_handle_resize_f resize, gpointer user_data)
Create a themed handle widget driving one-dimensional resize gestures.
static void _widget_auto_model_rows_reordered(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gint *new_order, gpointer user_data)
static int _resizable_scroll_handle_resize(int requested_size, gboolean finished, gpointer user_data)
static void _widget_auto_on_buffer_changed(GObject *textview, GParamSpec *pspec, gpointer user_data)
static int _textview_count_visible_rows(GtkWidget *textview)
static int _resizable_area_resize(int requested_size, gboolean finished, gpointer user_data)
static void _widget_auto_connect_model(GtkWidget *treeview)
static void _widget_auto_model_row_expanded(GtkTreeView *tree_view, GtkTreeIter *expanded_iter, GtkTreePath *path, gpointer user_data)
static void _widget_auto_model_row_changed(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
GtkWidget * dt_ui_resizable_drawing_area(GtkWidget *area, char *config_str, int default_height, int min_height)
Make a self-drawing widget (typically a GtkDrawingArea graph or scope) vertically resizable.
static int _resizable_area_get_size(gpointer user_data)
static gint _get_container_row_heigth(GtkWidget *w)
Definition scroll_wrap.c:46
static void _widget_auto_connect_buffer(GtkWidget *textview)
static void _resizable_scroll_realize(GtkWidget *w, gpointer user_data)
static void _widget_auto_height_free(gpointer data)
static void _widget_auto_model_row_collapsed(GtkTreeView *tree_view, GtkTreeIter *collapsed_iter, GtkTreePath *path, gpointer user_data)
static int _resizable_scroll_handle_get_size(gpointer user_data)
static void _widget_auto_model_row_deleted(GtkTreeModel *model, GtkTreePath *path, gpointer user_data)
static void _widget_auto_disconnect_buffer(dt_gui_widget_auto_height_t *state)
static GtkWidget * _search_parent_scrolled_window(GtkWidget *w)
Definition scroll_wrap.c:86
GtkWidget * dt_ui_scroll_wrap_get_scrolled_window(GtkWidget *wrapper)
Return the inner scrolled window of a dt_ui_scroll_wrap() wrapper, or NULL.
static void _widget_auto_text_buffer_changed(GtkTextBuffer *buffer, gpointer user_data)
static gint _resizable_scroll_max_height(void)
Window-height ceiling shared by the auto-size rule and the drag handle.
static void _resizable_area_free(gpointer data)
static const char *const DT_GUI_WIDGET_AUTO_HEIGHT_KEY
Definition scroll_wrap.c:29
static void _widget_auto_model_row_inserted(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
static void _widget_auto_on_model_changed(GObject *treeview, GParamSpec *pspec, gpointer user_data)
static void _resizable_scroll_apply(GtkWidget *w)
The single sizing rule for every dt_ui_scroll_wrap area.
static void _widget_auto_update(GtkWidget *widget)
static const char *const DT_UI_RESIZABLE_AREA_KEY
Definition scroll_wrap.c:37
static int _treeview_count_visible_rows(GtkTreeView *treeview, GtkTreeModel *model, GtkTreeIter *parent)
GtkWidget * dt_ui_scroll_wrap(GtkWidget *w, gint min_size, char *config_str, dt_ui_resize_mode_t mode)
Wrap a scrollable content widget in a recessed, vertically resizable scrolled window.
static void _widget_auto_disconnect_model(dt_gui_widget_auto_height_t *state, GtkWidget *treeview)
static gboolean _resizable_scroll_draw(GtkWidget *w, cairo_t *cr, gpointer user_data)
dt_ui_resize_mode_t
Definition scroll_wrap.h:39
@ DT_UI_RESIZE_STATIC
Definition scroll_wrap.h:46
@ DT_UI_RESIZE_DYNAMIC
Definition scroll_wrap.h:42
static const dt_aligned_pixel_simd_t value
Definition simd.h:144
const float uint32_t state[4]
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29
GtkWidget * dt_widget_root_window(void)
gboolean dt_widget_stored_int(const char *key, int *value)
void dt_widget_store_int(const char *key, int value)
#define DT_PIXEL_APPLY_DPI(value)
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)