diff --git a/birb/birbcustomfilter.h b/birb/birbcustomfilter.h
new file mode 100644
--- /dev/null
+++ b/birb/birbcustomfilter.h
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 Birb Developers
+ *
+ * Birb is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(BIRB_GLOBAL_HEADER_INSIDE) && !defined(BIRB_COMPILATION)
+# error "only <birb.h> may be included directly"
+#endif
+
+#ifndef BIRB_CUSTOM_FILTER_H
+#define BIRB_CUSTOM_FILTER_H
+
+#include <gio/gio.h>
+
+#include "birbfilter.h"
+#include "birbversion.h"
+
+G_BEGIN_DECLS
+
+#define BIRB_TYPE_CUSTOM_FILTER (birb_custom_filter_get_type())
+
+/**
+ * BirbCustomFilterFunc:
+ * @item: The item.
+ * @data: User supplied data.
+ *
+ * User function that is called to determine if item should be put into a
+ * list or not.
+ *
+ * Returns: %TRUE to keep the item in the list, or %FALSE to ignore it.
+ *
+ * Since: 0.3
+ */
+typedef gboolean (*BirbCustomFilterFunc)(GObject *item, gpointer data);
+
+/**
+ * BirbCustomFilter:
+ *
+ * A [class@Filter] that uses a [callback@CustomFilterFunc] to determine which
+ * items should be kept.
+ *
+ * Since: 0.3
+ */
+BIRB_AVAILABLE_IN_0_3
+G_DECLARE_FINAL_TYPE(BirbCustomFilter, birb_custom_filter, BIRB, CUSTOM_FILTER,
+                     BirbFilter)
+
+/**
+ * birb_custom_filter_new:
+ * @match_func: (nullable): The function used for matching.
+ * @data: (nullable): User data to pass to @match_func.
+ * @destroy_notify: (nullable): A destroy notify for @data.
+ *
+ * Creates a new filter using @match_func to determine what to filter.
+ *
+ * If @match_func is %NULL, the filter will match all items.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 0.3
+ */
+BIRB_AVAILABLE_IN_0_3
+BirbCustomFilter *birb_custom_filter_new(BirbCustomFilterFunc match_func, gpointer data, GDestroyNotify destroy_notify);
+
+/**
+ * birb_custom_filter_set_filter_func:
+ * @filter: The instance.
+ * @match_func: (nullable): The new function used for matching.
+ * @data: (nullable): User data to pass to @match_func.
+ * @destroy_notify: (nullable): A destroy notify for @data.
+ *
+ * Sets a new matching function for @filter.
+ *
+ * If @match_func is %NULL, the filter will match all items.
+ *
+ * If a previous function was set, its %destroy_notify will be called now.
+ *
+ * Since: 0.3
+ */
+BIRB_AVAILABLE_IN_0_3
+void birb_custom_filter_set_filter_func(BirbCustomFilter *filter, BirbCustomFilterFunc match_func, gpointer data, GDestroyNotify destroy_notify);
+
+G_END_DECLS
+
+#endif /* BIRB_CUSTOM_FILTER_H */
# HG changeset patch
# Node ID 4fcfbad613e8f26b18db42946a679502b2a0fad8
# Parent  148a81fb5777ce14b95345e2765d73b156d46921
diff --git a/birb/birbcustomfilter.c b/birb/birbcustomfilter.c
new file mode 100644
--- /dev/null
+++ b/birb/birbcustomfilter.c
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 Birb Developers
+ *
+ * Birb is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "birbcustomfilter.h"
+
+struct _BirbCustomFilter {
+	BirbFilter parent;
+
+	BirbCustomFilterFunc match_func;
+	gpointer data;
+	GDestroyNotify destroy_notify;
+};
+
+/******************************************************************************
+ * BirbFilter Implementation
+ *****************************************************************************/
+static gboolean
+birb_custom_filter_matches(BirbFilter *filter, GObject *item) {
+	BirbCustomFilter *custom = BIRB_CUSTOM_FILTER(filter);
+
+	if(custom->match_func == NULL) {
+		return TRUE;
+	}
+
+	return custom->match_func(item, custom->data);
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_TYPE(BirbCustomFilter, birb_custom_filter, BIRB_TYPE_FILTER)
+
+static void
+birb_custom_filter_dispose(GObject *obj) {
+	BirbCustomFilter *filter = BIRB_CUSTOM_FILTER(obj);
+
+	if(filter->destroy_notify != NULL) {
+		filter->destroy_notify(filter->data);
+	}
+
+	G_OBJECT_CLASS(birb_custom_filter_parent_class)->dispose(obj);
+}
+
+static void
+birb_custom_filter_init(G_GNUC_UNUSED BirbCustomFilter *filter) {
+}
+
+static void
+birb_custom_filter_class_init(BirbCustomFilterClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+	BirbFilterClass *filter_class = BIRB_FILTER_CLASS(klass);
+
+	obj_class->dispose = birb_custom_filter_dispose;
+
+	filter_class->matches = birb_custom_filter_matches;
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+BirbCustomFilter *
+birb_custom_filter_new(BirbCustomFilterFunc match_func, gpointer data,
+                       GDestroyNotify destroy_notify)
+{
+	BirbCustomFilter *filter = g_object_new(BIRB_TYPE_CUSTOM_FILTER, NULL);
+
+	birb_custom_filter_set_filter_func(filter, match_func, data,
+	                                   destroy_notify);
+
+	return filter;
+}
+
+void
+birb_custom_filter_set_filter_func(BirbCustomFilter *filter,
+                                   BirbCustomFilterFunc match_func,
+                                   gpointer data,
+                                   GDestroyNotify destroy_notify)
+{
+	g_return_if_fail(BIRB_IS_CUSTOM_FILTER(filter));
+
+	if(filter->destroy_notify != NULL) {
+		filter->destroy_notify(filter->data);
+	}
+
+	filter->match_func = match_func;
+	filter->data = data;
+	filter->destroy_notify = destroy_notify;
+
+	birb_filter_changed(BIRB_FILTER(filter));
+}
diff --git a/birb/meson.build b/birb/meson.build
--- a/birb/meson.build
+++ b/birb/meson.build
@@ -2,6 +2,7 @@
 
 BIRB_SOURCES = [
   'birbactionmenu.c',
+  'birbcustomfilter.c',
   'birbdatetime.c',
   'birbfilter.c',
   'birbqueuedoutputstream.c',
@@ -10,6 +11,7 @@
 
 BIRB_HEADERS = [
   'birbactionmenu.h',
+  'birbcustomfilter.h',
   'birbdatetime.h',
   'birbfilter.h',
   'birbqueuedoutputstream.h',
diff --git a/birb/tests/meson.build b/birb/tests/meson.build
--- a/birb/tests/meson.build
+++ b/birb/tests/meson.build
@@ -1,5 +1,6 @@
 PROGRAMS = [
   'action_menu',
+  'custom_filter',
   'date_time',
   'filter',
   'queued_output_stream',
diff --git a/birb/tests/test_custom_filter.c b/birb/tests/test_custom_filter.c
new file mode 100644
--- /dev/null
+++ b/birb/tests/test_custom_filter.c
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2024 Birb Developers
+ *
+ * Birb is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * This program 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 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
+ */
+
+#include <glib.h>
+
+#include <birb.h>
+
+/******************************************************************************
+ * Filter Functions
+ *****************************************************************************/
+static gboolean
+test_birb_custom_filter_match_all_func(GObject *item,
+                                       G_GNUC_UNUSED gpointer data)
+{
+	g_assert_true(G_IS_OBJECT(item));
+
+	return TRUE;
+}
+
+static gboolean
+test_birb_custom_filter_match_all_data_func(GObject *item, gpointer data) {
+	g_assert_nonnull(data);
+	g_assert_true(G_IS_OBJECT(item));
+
+	return TRUE;
+}
+
+/******************************************************************************
+ * New Tests
+ *****************************************************************************/
+static void
+test_birb_custom_filter_new_null(void) {
+	BirbCustomFilter *filter = NULL;
+
+	filter = birb_custom_filter_new(NULL, NULL, NULL);
+
+	g_assert_true(BIRB_IS_CUSTOM_FILTER(filter));
+
+	g_assert_finalize_object(filter);
+}
+
+static void
+test_birb_custom_filter_new_null_with_data_and_destroy(void) {
+	BirbCustomFilter *filter = NULL;
+	GObject *item = NULL;
+
+	item = g_object_new(G_TYPE_OBJECT, NULL);
+
+	filter = birb_custom_filter_new(NULL, g_object_ref(item), g_object_unref);
+
+	g_assert_true(BIRB_IS_CUSTOM_FILTER(filter));
+
+	g_assert_finalize_object(filter);
+	g_assert_finalize_object(item);
+}
+
+static void
+test_birb_custom_filter_new_with_match_func(void) {
+	BirbCustomFilter *filter = NULL;
+
+	filter = birb_custom_filter_new(test_birb_custom_filter_match_all_func,
+	                                NULL, NULL);
+
+	g_assert_true(BIRB_IS_CUSTOM_FILTER(filter));
+
+	g_assert_finalize_object(filter);
+}
+
+static void
+test_birb_custom_filter_new_with_match_func_and_data(void) {
+	BirbCustomFilter *filter = NULL;
+	guint data = 0;
+
+	filter = birb_custom_filter_new(test_birb_custom_filter_match_all_data_func,
+	                                &data, NULL);
+
+	g_assert_true(BIRB_IS_CUSTOM_FILTER(filter));
+
+	g_assert_finalize_object(filter);
+}
+
+static void
+test_birb_custom_filter_new_full(void) {
+	BirbCustomFilter *filter = NULL;
+	GObject *item = NULL;
+
+	item = g_object_new(G_TYPE_OBJECT, NULL);
+
+	filter = birb_custom_filter_new(test_birb_custom_filter_match_all_data_func,
+	                                g_object_ref(item), g_object_unref);
+
+	g_assert_true(BIRB_IS_CUSTOM_FILTER(filter));
+
+	g_assert_finalize_object(filter);
+	g_assert_finalize_object(item);
+}
+
+/******************************************************************************
+ * Matches tests
+ *****************************************************************************/
+static gboolean
+test_birb_custom_filter_matches_func(GObject *item, gpointer data) {
+	gboolean result = GPOINTER_TO_INT(data);
+
+	g_assert_true(G_IS_OBJECT(item));
+
+	return result;
+}
+
+static void
+test_birb_custom_filter_matches(gconstpointer data) {
+	BirbCustomFilter *filter = NULL;
+	GObject *item = NULL;
+	gboolean result = GPOINTER_TO_INT(data);
+
+	item = g_object_new(G_TYPE_OBJECT, NULL);
+
+	filter = birb_custom_filter_new(test_birb_custom_filter_matches_func,
+	                                (gpointer)data, NULL);
+
+	g_assert_true(birb_filter_matches(BIRB_FILTER(filter), item) == result);
+
+	g_assert_finalize_object(filter);
+	g_assert_finalize_object(item);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char **argv) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_set_nonfatal_assertions();
+
+	g_test_add_func("/custom-filter/new/null",
+	                test_birb_custom_filter_new_null);
+	g_test_add_func("/custom-filter/new/null-with-data-and-destroy",
+	                test_birb_custom_filter_new_null_with_data_and_destroy);
+	g_test_add_func("/custom-filter/new/with-match-func",
+	                test_birb_custom_filter_new_with_match_func);
+	g_test_add_func("/custom-filter/new/with-match-func-and-data",
+	                test_birb_custom_filter_new_with_match_func_and_data);
+	g_test_add_func("/custom-filter/new/full",
+	                test_birb_custom_filter_new_full);
+
+	g_test_add_data_func("/custom-filter/matches/true",
+	                     GINT_TO_POINTER(TRUE),
+	                     test_birb_custom_filter_matches);
+	g_test_add_data_func("/custom-filter/matches/false",
+	                     GINT_TO_POINTER(FALSE),
+	                     test_birb_custom_filter_matches);
+
+	return g_test_run();
+}
