feat: add zoom and canvas panning

This commit is contained in:
grayTerminal-sh 2026-07-21 17:33:22 +02:00
parent 13a391b449
commit 866a304f2f
2 changed files with 910 additions and 4 deletions

View file

@ -67,6 +67,17 @@ void investigation_graph_view_clear(
InvestigationGraphView *graph_view
);
/**
* @brief Réinitialise le zoom et la position du canvas.
*
* Le graphe emprunté reste associé à la vue.
*
* @param graph_view Vue graphique à recentrer.
*/
void investigation_graph_view_reset_view(
InvestigationGraphView *graph_view
);
/**
* @brief Libère la structure d'encapsulation de la vue.
*

View file

@ -23,6 +23,12 @@
#define INVESTIGATION_GRAPH_VIEW_ARROW_LENGTH 9.0
#define INVESTIGATION_GRAPH_VIEW_ARROW_HALF_WIDTH 5.0
#define INVESTIGATION_GRAPH_VIEW_MIN_ZOOM 0.25
#define INVESTIGATION_GRAPH_VIEW_MAX_ZOOM 3.0
#define INVESTIGATION_GRAPH_VIEW_SCROLL_ZOOM_SPEED 0.08
#define INVESTIGATION_GRAPH_VIEW_MIN_SCROLL_FACTOR 0.50
#define INVESTIGATION_GRAPH_VIEW_MAX_SCROLL_FACTOR 1.50
/**
* @brief Position privée d'une entité dans la vue.
*/
@ -42,9 +48,577 @@ struct InvestigationGraphView
{
GtkWidget *drawing_area;
GtkGesture *zoom_gesture;
GtkGesture *drag_gesture;
GtkEventController *scroll_controller;
GtkEventController *motion_controller;
const InvestigationGraphModel *graph_model;
double zoom;
double offset_x;
double offset_y;
double zoom_gesture_start_zoom;
double zoom_gesture_start_offset_x;
double zoom_gesture_start_offset_y;
double zoom_gesture_anchor_x;
double zoom_gesture_anchor_y;
double pointer_x;
double pointer_y;
double drag_start_x;
double drag_start_y;
double drag_origin_offset_x;
double drag_origin_offset_y;
gboolean pointer_position_valid;
gboolean dragging;
};
/**
* @brief Borne un facteur de zoom aux limites autorisées.
*/
static double investigation_graph_view_clamp_zoom(
double zoom
)
{
if (zoom < INVESTIGATION_GRAPH_VIEW_MIN_ZOOM)
{
return INVESTIGATION_GRAPH_VIEW_MIN_ZOOM;
}
if (zoom > INVESTIGATION_GRAPH_VIEW_MAX_ZOOM)
{
return INVESTIGATION_GRAPH_VIEW_MAX_ZOOM;
}
return zoom;
}
/**
* @brief Applique un zoom en conservant un point d'écran immobile.
*/
static void investigation_graph_view_zoom_at_point(
InvestigationGraphView *graph_view,
double requested_zoom,
double anchor_x,
double anchor_y
)
{
double previous_zoom =
0.0;
double logical_x =
0.0;
double logical_y =
0.0;
double new_zoom =
0.0;
if (graph_view == NULL ||
graph_view->drawing_area == NULL)
{
return;
}
previous_zoom =
graph_view->zoom;
if (previous_zoom <= 0.0)
{
previous_zoom =
1.0;
}
new_zoom =
investigation_graph_view_clamp_zoom(
requested_zoom
);
logical_x =
(
anchor_x -
graph_view->offset_x
) /
previous_zoom;
logical_y =
(
anchor_y -
graph_view->offset_y
) /
previous_zoom;
graph_view->zoom =
new_zoom;
graph_view->offset_x =
anchor_x -
(logical_x * new_zoom);
graph_view->offset_y =
anchor_y -
(logical_y * new_zoom);
gtk_widget_queue_draw(
graph_view->drawing_area
);
}
/**
* @brief Mémorise la dernière position connue du pointeur.
*/
static void investigation_graph_view_on_pointer_motion(
GtkEventControllerMotion *controller,
double x,
double y,
gpointer user_data
)
{
InvestigationGraphView *graph_view =
user_data;
(void) controller;
if (graph_view == NULL)
{
return;
}
graph_view->pointer_x =
x;
graph_view->pointer_y =
y;
graph_view->pointer_position_valid =
TRUE;
}
/**
* @brief Oublie la position du pointeur lorsqu'il quitte le canvas.
*/
static void investigation_graph_view_on_pointer_leave(
GtkEventControllerMotion *controller,
gpointer user_data
)
{
InvestigationGraphView *graph_view =
user_data;
(void) controller;
if (graph_view == NULL)
{
return;
}
graph_view->pointer_position_valid =
FALSE;
}
/**
* @brief Mémorise l'état initial d'un pincement.
*/
static void investigation_graph_view_on_zoom_begin(
GtkGesture *gesture,
GdkEventSequence *sequence,
gpointer user_data
)
{
InvestigationGraphView *graph_view =
user_data;
double anchor_x =
0.0;
double anchor_y =
0.0;
gboolean center_available =
FALSE;
(void) sequence;
if (graph_view == NULL ||
graph_view->drawing_area == NULL)
{
return;
}
center_available =
gtk_gesture_get_bounding_box_center(
gesture,
&anchor_x,
&anchor_y
);
if (!center_available)
{
anchor_x =
(double) gtk_widget_get_width(
graph_view->drawing_area
) /
2.0;
anchor_y =
(double) gtk_widget_get_height(
graph_view->drawing_area
) /
2.0;
}
graph_view->zoom_gesture_start_zoom =
graph_view->zoom;
graph_view->zoom_gesture_start_offset_x =
graph_view->offset_x;
graph_view->zoom_gesture_start_offset_y =
graph_view->offset_y;
graph_view->zoom_gesture_anchor_x =
anchor_x;
graph_view->zoom_gesture_anchor_y =
anchor_y;
}
/**
* @brief Applique le facteur cumulé transmis par GtkGestureZoom.
*/
static void investigation_graph_view_on_scale_changed(
GtkGestureZoom *gesture,
double scale,
gpointer user_data
)
{
InvestigationGraphView *graph_view =
user_data;
double start_zoom =
0.0;
double new_zoom =
0.0;
double logical_x =
0.0;
double logical_y =
0.0;
(void) gesture;
if (graph_view == NULL ||
graph_view->drawing_area == NULL ||
scale <= 0.0)
{
return;
}
start_zoom =
graph_view->zoom_gesture_start_zoom;
if (start_zoom <= 0.0)
{
start_zoom =
1.0;
}
new_zoom =
investigation_graph_view_clamp_zoom(
start_zoom * scale
);
logical_x =
(
graph_view->zoom_gesture_anchor_x -
graph_view->zoom_gesture_start_offset_x
) /
start_zoom;
logical_y =
(
graph_view->zoom_gesture_anchor_y -
graph_view->zoom_gesture_start_offset_y
) /
start_zoom;
graph_view->zoom =
new_zoom;
graph_view->offset_x =
graph_view->zoom_gesture_anchor_x -
(logical_x * new_zoom);
graph_view->offset_y =
graph_view->zoom_gesture_anchor_y -
(logical_y * new_zoom);
gtk_widget_queue_draw(
graph_view->drawing_area
);
}
/**
* @brief Gère Ctrl + défilement comme solution de zoom de secours.
*/
static gboolean investigation_graph_view_on_scroll(
GtkEventControllerScroll *controller,
double delta_x,
double delta_y,
gpointer user_data
)
{
InvestigationGraphView *graph_view =
user_data;
GdkModifierType modifier_state =
0;
double anchor_x =
0.0;
double anchor_y =
0.0;
double zoom_factor =
1.0;
(void) delta_x;
if (graph_view == NULL ||
graph_view->drawing_area == NULL ||
delta_y == 0.0)
{
return FALSE;
}
modifier_state =
gtk_event_controller_get_current_event_state(
GTK_EVENT_CONTROLLER(
controller
)
);
if ((modifier_state & GDK_CONTROL_MASK) == 0)
{
return FALSE;
}
if (graph_view->pointer_position_valid)
{
anchor_x =
graph_view->pointer_x;
anchor_y =
graph_view->pointer_y;
}
else
{
anchor_x =
(double) gtk_widget_get_width(
graph_view->drawing_area
) /
2.0;
anchor_y =
(double) gtk_widget_get_height(
graph_view->drawing_area
) /
2.0;
}
zoom_factor =
1.0 -
(
delta_y *
INVESTIGATION_GRAPH_VIEW_SCROLL_ZOOM_SPEED
);
if (zoom_factor <
INVESTIGATION_GRAPH_VIEW_MIN_SCROLL_FACTOR)
{
zoom_factor =
INVESTIGATION_GRAPH_VIEW_MIN_SCROLL_FACTOR;
}
if (zoom_factor >
INVESTIGATION_GRAPH_VIEW_MAX_SCROLL_FACTOR)
{
zoom_factor =
INVESTIGATION_GRAPH_VIEW_MAX_SCROLL_FACTOR;
}
investigation_graph_view_zoom_at_point(
graph_view,
graph_view->zoom * zoom_factor,
anchor_x,
anchor_y
);
return TRUE;
}
/**
* @brief Commence le déplacement de l'ensemble du canvas.
*/
static void investigation_graph_view_on_drag_begin(
GtkGestureDrag *gesture,
double start_x,
double start_y,
gpointer user_data
)
{
InvestigationGraphView *graph_view =
user_data;
GdkModifierType modifier_state =
0;
if (graph_view == NULL ||
graph_view->drawing_area == NULL)
{
return;
}
modifier_state =
gtk_event_controller_get_current_event_state(
GTK_EVENT_CONTROLLER(
gesture
)
);
if ((modifier_state & GDK_SHIFT_MASK) == 0)
{
graph_view->dragging =
FALSE;
gtk_gesture_set_state(
GTK_GESTURE(
gesture
),
GTK_EVENT_SEQUENCE_DENIED
);
return;
}
gtk_gesture_set_state(
GTK_GESTURE(
gesture
),
GTK_EVENT_SEQUENCE_CLAIMED
);
graph_view->drag_start_x =
start_x;
graph_view->drag_start_y =
start_y;
graph_view->drag_origin_offset_x =
graph_view->offset_x;
graph_view->drag_origin_offset_y =
graph_view->offset_y;
graph_view->dragging =
TRUE;
gtk_widget_set_cursor_from_name(
graph_view->drawing_area,
"grabbing"
);
}
/**
* @brief Déplace le canvas selon le décalage cumulé du geste.
*/
static void investigation_graph_view_on_drag_update(
GtkGestureDrag *gesture,
double offset_x,
double offset_y,
gpointer user_data
)
{
InvestigationGraphView *graph_view =
user_data;
(void) gesture;
if (graph_view == NULL ||
graph_view->drawing_area == NULL ||
!graph_view->dragging)
{
return;
}
graph_view->offset_x =
graph_view->drag_origin_offset_x +
offset_x;
graph_view->offset_y =
graph_view->drag_origin_offset_y +
offset_y;
gtk_widget_queue_draw(
graph_view->drawing_area
);
}
/**
* @brief Termine le déplacement du canvas.
*/
static void investigation_graph_view_on_drag_end(
GtkGestureDrag *gesture,
double offset_x,
double offset_y,
gpointer user_data
)
{
InvestigationGraphView *graph_view =
user_data;
(void) gesture;
if (graph_view == NULL ||
graph_view->drawing_area == NULL)
{
return;
}
if (graph_view->dragging)
{
graph_view->offset_x =
graph_view->drag_origin_offset_x +
offset_x;
graph_view->offset_y =
graph_view->drag_origin_offset_y +
offset_y;
}
graph_view->dragging =
FALSE;
gtk_widget_set_cursor_from_name(
graph_view->drawing_area,
"default"
);
gtk_widget_queue_draw(
graph_view->drawing_area
);
}
/**
* @brief Retourne le texte principal d'une entité.
*
@ -1441,6 +2015,26 @@ static void investigation_graph_view_draw(
goto draw_error;
}
/*
* Les coordonnées de disposition restent exprimées dans l'espace logique
* du graphe. Cairo applique ensuite la navigation de la vue.
*/
cairo_save(
cairo_context
);
cairo_translate(
cairo_context,
graph_view->offset_x,
graph_view->offset_y
);
cairo_scale(
cairo_context,
graph_view->zoom,
graph_view->zoom
);
/*
* Les relations sont dessinées en premier afin que les nœuds restent
* lisibles au premier plan.
@ -1470,6 +2064,10 @@ static void investigation_graph_view_draw(
);
}
cairo_restore(
cairo_context
);
goto cleanup;
draw_error:
@ -1586,6 +2184,173 @@ InvestigationGraphView *investigation_graph_view_new(void)
NULL
);
graph_view->zoom_gesture =
gtk_gesture_zoom_new();
graph_view->drag_gesture =
gtk_gesture_drag_new();
graph_view->scroll_controller =
gtk_event_controller_scroll_new(
GTK_EVENT_CONTROLLER_SCROLL_VERTICAL
);
graph_view->motion_controller =
gtk_event_controller_motion_new();
if (graph_view->zoom_gesture == NULL ||
graph_view->drag_gesture == NULL ||
graph_view->scroll_controller == NULL ||
graph_view->motion_controller == NULL)
{
investigation_graph_view_free(
graph_view
);
return NULL;
}
g_signal_connect(
graph_view->zoom_gesture,
"begin",
G_CALLBACK(
investigation_graph_view_on_zoom_begin
),
graph_view
);
g_signal_connect(
graph_view->zoom_gesture,
"scale-changed",
G_CALLBACK(
investigation_graph_view_on_scale_changed
),
graph_view
);
gtk_gesture_single_set_button(
GTK_GESTURE_SINGLE(
graph_view->drag_gesture
),
GDK_BUTTON_PRIMARY
);
g_signal_connect(
graph_view->drag_gesture,
"drag-begin",
G_CALLBACK(
investigation_graph_view_on_drag_begin
),
graph_view
);
g_signal_connect(
graph_view->drag_gesture,
"drag-update",
G_CALLBACK(
investigation_graph_view_on_drag_update
),
graph_view
);
g_signal_connect(
graph_view->drag_gesture,
"drag-end",
G_CALLBACK(
investigation_graph_view_on_drag_end
),
graph_view
);
g_signal_connect(
graph_view->scroll_controller,
"scroll",
G_CALLBACK(
investigation_graph_view_on_scroll
),
graph_view
);
g_signal_connect(
graph_view->motion_controller,
"enter",
G_CALLBACK(
investigation_graph_view_on_pointer_motion
),
graph_view
);
g_signal_connect(
graph_view->motion_controller,
"motion",
G_CALLBACK(
investigation_graph_view_on_pointer_motion
),
graph_view
);
g_signal_connect(
graph_view->motion_controller,
"leave",
G_CALLBACK(
investigation_graph_view_on_pointer_leave
),
graph_view
);
gtk_widget_add_controller(
graph_view->drawing_area,
GTK_EVENT_CONTROLLER(
graph_view->zoom_gesture
)
);
gtk_widget_add_controller(
graph_view->drawing_area,
GTK_EVENT_CONTROLLER(
graph_view->drag_gesture
)
);
gtk_widget_add_controller(
graph_view->drawing_area,
graph_view->scroll_controller
);
gtk_widget_add_controller(
graph_view->drawing_area,
graph_view->motion_controller
);
graph_view->zoom =
1.0;
graph_view->offset_x =
0.0;
graph_view->offset_y =
0.0;
graph_view->drag_start_x =
0.0;
graph_view->drag_start_y =
0.0;
graph_view->drag_origin_offset_x =
0.0;
graph_view->drag_origin_offset_y =
0.0;
graph_view->dragging =
FALSE;
gtk_widget_set_cursor_from_name(
graph_view->drawing_area,
"default"
);
return graph_view;
}
@ -1614,8 +2379,8 @@ void investigation_graph_view_set_graph(
graph_view->graph_model =
graph_model;
gtk_widget_queue_draw(
graph_view->drawing_area
investigation_graph_view_reset_view(
graph_view
);
}
@ -1631,11 +2396,72 @@ void investigation_graph_view_clear(
graph_view->graph_model =
NULL;
gtk_widget_queue_draw(
graph_view->drawing_area
investigation_graph_view_reset_view(
graph_view
);
}
void investigation_graph_view_reset_view(
InvestigationGraphView *graph_view
)
{
if (graph_view == NULL)
{
return;
}
graph_view->zoom =
1.0;
graph_view->offset_x =
0.0;
graph_view->offset_y =
0.0;
graph_view->drag_start_x =
0.0;
graph_view->drag_start_y =
0.0;
graph_view->drag_origin_offset_x =
0.0;
graph_view->drag_origin_offset_y =
0.0;
graph_view->zoom_gesture_start_zoom =
1.0;
graph_view->zoom_gesture_start_offset_x =
0.0;
graph_view->zoom_gesture_start_offset_y =
0.0;
graph_view->zoom_gesture_anchor_x =
0.0;
graph_view->zoom_gesture_anchor_y =
0.0;
graph_view->dragging =
FALSE;
if (graph_view->drawing_area != NULL)
{
gtk_widget_set_cursor_from_name(
graph_view->drawing_area,
"default"
);
gtk_widget_queue_draw(
graph_view->drawing_area
);
}
}
void investigation_graph_view_free(
InvestigationGraphView *graph_view
)
@ -1658,6 +2484,74 @@ void investigation_graph_view_free(
NULL,
NULL
);
if (graph_view->zoom_gesture != NULL)
{
g_signal_handlers_disconnect_by_data(
graph_view->zoom_gesture,
graph_view
);
gtk_widget_remove_controller(
graph_view->drawing_area,
GTK_EVENT_CONTROLLER(
graph_view->zoom_gesture
)
);
graph_view->zoom_gesture =
NULL;
}
if (graph_view->drag_gesture != NULL)
{
g_signal_handlers_disconnect_by_data(
graph_view->drag_gesture,
graph_view
);
gtk_widget_remove_controller(
graph_view->drawing_area,
GTK_EVENT_CONTROLLER(
graph_view->drag_gesture
)
);
graph_view->drag_gesture =
NULL;
}
if (graph_view->scroll_controller != NULL)
{
g_signal_handlers_disconnect_by_data(
graph_view->scroll_controller,
graph_view
);
gtk_widget_remove_controller(
graph_view->drawing_area,
graph_view->scroll_controller
);
graph_view->scroll_controller =
NULL;
}
if (graph_view->motion_controller != NULL)
{
g_signal_handlers_disconnect_by_data(
graph_view->motion_controller,
graph_view
);
gtk_widget_remove_controller(
graph_view->drawing_area,
graph_view->motion_controller
);
graph_view->motion_controller =
NULL;
}
}
graph_view->drawing_area =
@ -1667,3 +2561,4 @@ void investigation_graph_view_free(
graph_view
);
}