/* SPDX-FileCopyrightText: 2013-2026 - Sébastien Wilmet <swilmet@gnome.org>
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */

#include "amtk-tree-view-scrolled-window-sizing.h"

/**
 * SECTION:amtk-tree-view-scrolled-window-sizing
 * @title: AmtkTreeViewScrolledWindowSizing
 * @short_description: Custom #GtkScrolledWindow sizing when it contains a #GtkTreeView
 *
 * #AmtkTreeViewScrolledWindowSizing is a subclass of #GtkScrolledWindow to
 * provide custom sizing when it contains a #GtkTreeView.
 *
 * If the #GtkTreeView is small enough, its natural size is taken. If it exceeds
 * a certain size, a smaller size is applied with the height at a row boundary.
 *
 * To configure the size limits, you can use the following properties:
 * - #GtkScrolledWindow:max-content-width
 * - #GtkScrolledWindow:max-content-height
 * - #AmtkTreeViewScrolledWindowSizing:monitor-limit-enabled
 *
 * One use-case is to show a #GtkTreeView in a popup #GtkWindow.
 *
 * # Limitations
 *
 * The current #AmtkTreeViewScrolledWindowSizing implementation has several
 * limitations, so there is room for improvement in future versions.
 *
 * The restrictions:
 * - The #GtkTreeView:headers-visible property should be %FALSE.
 * - The #GtkTreeView should contain a list, not a tree.
 * - All rows of the #GtkTreeView should have the same height, see the
 *   #GtkTreeView:fixed-height-mode property.
 */

struct _AmtkTreeViewScrolledWindowSizingPrivate
{
	guint monitor_limit_enabled : 1;
};

enum
{
	PROP_0,
	PROP_MONITOR_LIMIT_ENABLED,
	N_PROPERTIES
};

static GParamSpec *properties[N_PROPERTIES];

G_DEFINE_TYPE_WITH_PRIVATE (AmtkTreeViewScrolledWindowSizing,
			    amtk_tree_view_scrolled_window_sizing,
			    GTK_TYPE_SCROLLED_WINDOW)

static void
get_monitor_size (AmtkTreeViewScrolledWindowSizing *self,
		  gint                             *width,
		  gint                             *height)
{
	GtkWidget *toplevel;
	GdkDisplay *display;
	GdkWindow *window;
	GdkMonitor *monitor;
	GdkRectangle monitor_geometry = { 0 };

	if (width != NULL)
	{
		*width = -1;
	}
	if (height != NULL)
	{
		*height = -1;
	}

	if (!gtk_widget_get_realized (GTK_WIDGET (self)))
	{
		return;
	}

	toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
	display = gtk_widget_get_display (toplevel);
	window = gtk_widget_get_window (toplevel);
	monitor = gdk_display_get_monitor_at_window (display, window);
	gdk_monitor_get_geometry (monitor, &monitor_geometry);

	if (width != NULL)
	{
		*width = monitor_geometry.width;
	}
	if (height != NULL)
	{
		*height = monitor_geometry.height;
	}
}

/* Returns: the maximum width, or -1 if no limits. */
static gint
get_width_limit (AmtkTreeViewScrolledWindowSizing *self)
{
	gint max_content_width;
	gint monitor_width = -1;

	max_content_width = gtk_scrolled_window_get_max_content_width (GTK_SCROLLED_WINDOW (self));

	if (!self->priv->monitor_limit_enabled)
	{
		return max_content_width;
	}

	get_monitor_size (self, &monitor_width, NULL);

	if (monitor_width == -1)
	{
		return max_content_width;
	}
	else if (max_content_width == -1)
	{
		return monitor_width;
	}

	/* Both are set. */
	return MIN (max_content_width, monitor_width);
}

/* Returns: the maximum height, or -1 if no limits. */
static gint
get_height_limit (AmtkTreeViewScrolledWindowSizing *self)
{
	gint max_content_height;
	gint monitor_height = -1;

	max_content_height = gtk_scrolled_window_get_max_content_height (GTK_SCROLLED_WINDOW (self));

	if (!self->priv->monitor_limit_enabled)
	{
		return max_content_height;
	}

	get_monitor_size (self, NULL, &monitor_height);

	if (monitor_height == -1)
	{
		return max_content_height;
	}
	else if (max_content_height == -1)
	{
		return monitor_height;
	}

	/* Both are set. */
	return MIN (max_content_height, monitor_height);
}

/* This is a simplistic implementation, for something more complete see
 * gtkentrycompletion.c.
 */
static gint
get_row_height (AmtkTreeViewScrolledWindowSizing *self,
		gint                              tree_view_natural_height)
{
	GtkTreeView *tree_view;
	GtkTreeModel *model;
	gint n_rows;

	tree_view = amtk_tree_view_scrolled_window_sizing_get_tree_view (self);
	if (tree_view == NULL)
	{
		return 0;
	}

	model = gtk_tree_view_get_model (tree_view);
	if (model == NULL)
	{
		return 0;
	}

	n_rows = gtk_tree_model_iter_n_children (model, NULL);
	if (n_rows == 0)
	{
		return 0;
	}

	return tree_view_natural_height / n_rows;
}

static gint
compute_height (AmtkTreeViewScrolledWindowSizing *self)
{
	GtkTreeView *tree_view;
	GtkRequisition natural_size = { 0 };
	gint height_limit;
	gint row_height;
	gint n_rows_allowed;

	tree_view = amtk_tree_view_scrolled_window_sizing_get_tree_view (self);
	if (tree_view == NULL)
	{
		return 0;
	}

	gtk_widget_get_preferred_size (GTK_WIDGET (tree_view), NULL, &natural_size);

	height_limit = get_height_limit (self);

	if (height_limit == -1 ||
	    natural_size.height <= height_limit)
	{
		return natural_size.height;
	}

	row_height = get_row_height (self, natural_size.height);
	n_rows_allowed = row_height != 0 ? height_limit / row_height : 0;
	return n_rows_allowed * row_height;
}

static gint
compute_width (AmtkTreeViewScrolledWindowSizing *self)
{
	GtkTreeView *tree_view;
	GtkRequisition natural_size = { 0 };
	gint width_limit;

	tree_view = amtk_tree_view_scrolled_window_sizing_get_tree_view (self);
	if (tree_view == NULL)
	{
		return 0;
	}

	gtk_widget_get_preferred_size (GTK_WIDGET (tree_view), NULL, &natural_size);

	width_limit = get_width_limit (self);

	if (width_limit == -1 ||
	    natural_size.width <= width_limit)
	{
		return natural_size.width;
	}

	return width_limit;
}

static void
amtk_tree_view_scrolled_window_sizing_get_preferred_width (GtkWidget *widget,
							   gint      *min_width,
							   gint      *natural_width)
{
	AmtkTreeViewScrolledWindowSizing *self = AMTK_TREE_VIEW_SCROLLED_WINDOW_SIZING (widget);
	gint width;

	width = compute_width (self);

	if (GTK_WIDGET_CLASS (amtk_tree_view_scrolled_window_sizing_parent_class)->get_preferred_width != NULL)
	{
		gint min_width_parent = 0;

		GTK_WIDGET_CLASS (amtk_tree_view_scrolled_window_sizing_parent_class)->get_preferred_width (widget,
													    &min_width_parent,
													    NULL);
		width = MAX (width, min_width_parent);
	}

	width = MAX (width, 0);

	if (min_width != NULL)
	{
		*min_width = width;
	}
	if (natural_width != NULL)
	{
		*natural_width = width;
	}
}

static void
amtk_tree_view_scrolled_window_sizing_get_preferred_height (GtkWidget *widget,
							    gint      *min_height,
							    gint      *natural_height)
{
	AmtkTreeViewScrolledWindowSizing *self = AMTK_TREE_VIEW_SCROLLED_WINDOW_SIZING (widget);
	gint height;

	height = compute_height (self);

	if (GTK_WIDGET_CLASS (amtk_tree_view_scrolled_window_sizing_parent_class)->get_preferred_height != NULL)
	{
		gint min_height_parent = 0;

		GTK_WIDGET_CLASS (amtk_tree_view_scrolled_window_sizing_parent_class)->get_preferred_height (widget,
													     &min_height_parent,
													     NULL);
		height = MAX (height, min_height_parent);
	}

	height = MAX (height, 0);

	if (min_height != NULL)
	{
		*min_height = height;
	}
	if (natural_height != NULL)
	{
		*natural_height = height;
	}
}

static void
amtk_tree_view_scrolled_window_sizing_get_property (GObject    *object,
                                                    guint       prop_id,
                                                    GValue     *value,
                                                    GParamSpec *pspec)
{
	AmtkTreeViewScrolledWindowSizing *self = AMTK_TREE_VIEW_SCROLLED_WINDOW_SIZING (object);

	switch (prop_id)
	{
		case PROP_MONITOR_LIMIT_ENABLED:
			g_value_set_boolean (value, amtk_tree_view_scrolled_window_sizing_get_monitor_limit (self));
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
			break;
	}
}

static void
amtk_tree_view_scrolled_window_sizing_set_property (GObject      *object,
                                                    guint         prop_id,
                                                    const GValue *value,
                                                    GParamSpec   *pspec)
{
	AmtkTreeViewScrolledWindowSizing *self = AMTK_TREE_VIEW_SCROLLED_WINDOW_SIZING (object);

	switch (prop_id)
	{
		case PROP_MONITOR_LIMIT_ENABLED:
			amtk_tree_view_scrolled_window_sizing_set_monitor_limit (self, g_value_get_boolean (value));
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
			break;
	}
}

static void
amtk_tree_view_scrolled_window_sizing_class_init (AmtkTreeViewScrolledWindowSizingClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

	object_class->get_property = amtk_tree_view_scrolled_window_sizing_get_property;
	object_class->set_property = amtk_tree_view_scrolled_window_sizing_set_property;

	widget_class->get_preferred_width = amtk_tree_view_scrolled_window_sizing_get_preferred_width;
	widget_class->get_preferred_height = amtk_tree_view_scrolled_window_sizing_get_preferred_height;

	/**
	 * AmtkTreeViewScrolledWindowSizing:monitor-limit-enabled:
	 *
	 * Whether to take the #GdkMonitor size into account as a maximum width
	 * and height to apply.
	 *
	 * Since: 5.10
	 */
	properties[PROP_MONITOR_LIMIT_ENABLED] =
		g_param_spec_boolean ("monitor-limit-enabled",
				      "monitor-limit-enabled",
				      "",
				      TRUE,
				      G_PARAM_READWRITE |
				      G_PARAM_CONSTRUCT |
				      G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties (object_class, N_PROPERTIES, properties);
}

static void
amtk_tree_view_scrolled_window_sizing_init (AmtkTreeViewScrolledWindowSizing *self)
{
	self->priv = amtk_tree_view_scrolled_window_sizing_get_instance_private (self);
}

/**
 * amtk_tree_view_scrolled_window_sizing_new:
 *
 * Returns: (transfer floating): a new #AmtkTreeViewScrolledWindowSizing.
 * Since: 5.10
 */
AmtkTreeViewScrolledWindowSizing *
amtk_tree_view_scrolled_window_sizing_new (void)
{
	return g_object_new (AMTK_TYPE_TREE_VIEW_SCROLLED_WINDOW_SIZING, NULL);
}

/**
 * amtk_tree_view_scrolled_window_sizing_get_tree_view:
 * @self: an #AmtkTreeViewScrolledWindowSizing.
 *
 * Returns: (transfer none) (nullable): the #GtkTreeView, if the child widget is
 *   a #GtkTreeView. Otherwise %NULL is returned.
 * Since: 5.10
 */
GtkTreeView *
amtk_tree_view_scrolled_window_sizing_get_tree_view (AmtkTreeViewScrolledWindowSizing *self)
{
	GtkWidget *child;

	g_return_val_if_fail (AMTK_IS_TREE_VIEW_SCROLLED_WINDOW_SIZING (self), NULL);

	child = gtk_bin_get_child (GTK_BIN (self));

	return GTK_IS_TREE_VIEW (child) ? GTK_TREE_VIEW (child) : NULL;
}

/**
 * amtk_tree_view_scrolled_window_sizing_get_monitor_limit:
 * @self: an #AmtkTreeViewScrolledWindowSizing.
 *
 * Returns: the current value of the
 *   #AmtkTreeViewScrolledWindowSizing:monitor-limit-enabled property.
 * Since: 5.10
 */
gboolean
amtk_tree_view_scrolled_window_sizing_get_monitor_limit (AmtkTreeViewScrolledWindowSizing *self)
{
	g_return_val_if_fail (AMTK_IS_TREE_VIEW_SCROLLED_WINDOW_SIZING (self), FALSE);
	return self->priv->monitor_limit_enabled;
}

/**
 * amtk_tree_view_scrolled_window_sizing_set_monitor_limit:
 * @self: an #AmtkTreeViewScrolledWindowSizing.
 * @monitor_limit: the new value.
 *
 * Sets the #AmtkTreeViewScrolledWindowSizing:monitor-limit-enabled property.
 *
 * Since: 5.10
 */
void
amtk_tree_view_scrolled_window_sizing_set_monitor_limit (AmtkTreeViewScrolledWindowSizing *self,
							 gboolean                          monitor_limit)
{
	g_return_if_fail (AMTK_IS_TREE_VIEW_SCROLLED_WINDOW_SIZING (self));

	monitor_limit = monitor_limit != FALSE;

	if (self->priv->monitor_limit_enabled != monitor_limit)
	{
		self->priv->monitor_limit_enabled = monitor_limit;

		gtk_widget_queue_resize (GTK_WIDGET (self));

		g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MONITOR_LIMIT_ENABLED]);
	}
}
