[gnome-builder] libide-webkit: add libide-webkit library



commit f2aacc1b8689bf7fe96cf809fc74d66b8e126288
Author: Christian Hergert <chergert redhat com>
Date:   Mon Jul 11 17:54:34 2022 -0700

    libide-webkit: add libide-webkit library
    
    This provides knowledge that webkit is available and a page which can be
    used to display web content in a new frame.

 src/libide/webkit/ide-html-generator.c             | 297 +++++++++
 src/libide/webkit/ide-html-generator.h             |  67 ++
 src/libide/webkit/ide-text-buffer-html-generator.c | 205 ++++++
 src/libide/webkit/ide-text-buffer-html-generator.h |  31 +
 src/libide/webkit/ide-url-bar.c                    | 453 +++++++++++++
 src/libide/webkit/ide-url-bar.h                    |  37 ++
 src/libide/webkit/ide-url-bar.ui                   |  94 +++
 src/libide/webkit/ide-webkit-page.c                | 708 +++++++++++++++++++++
 src/libide/webkit/ide-webkit-page.h                |  62 ++
 src/libide/webkit/ide-webkit-page.ui               | 103 +++
 src/libide/webkit/ide-webkit-plugin.c              |  11 +-
 src/libide/webkit/ide-webkit-util.c                | 280 ++++++++
 src/libide/webkit/ide-webkit-util.h                |  39 ++
 src/libide/webkit/libide-webkit.gresource.xml      |   2 +
 src/libide/webkit/libide-webkit.h                  |  27 +
 src/libide/webkit/meson.build                      |  41 +-
 16 files changed, 2454 insertions(+), 3 deletions(-)
---
diff --git a/src/libide/webkit/ide-html-generator.c b/src/libide/webkit/ide-html-generator.c
new file mode 100644
index 000000000..7e9485346
--- /dev/null
+++ b/src/libide/webkit/ide-html-generator.c
@@ -0,0 +1,297 @@
+/* ide-html-generator.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-html-generator"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-html-generator.h"
+#include "ide-text-buffer-html-generator.h"
+
+typedef struct
+{
+  char *base_uri;
+} IdeHtmlGeneratorPrivate;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (IdeHtmlGenerator, ide_html_generator, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_BASE_URI,
+  N_PROPS
+};
+
+enum {
+  INVALIDATE,
+  N_SIGNALS
+};
+
+static guint signals[N_SIGNALS];
+static GParamSpec *properties[N_PROPS];
+
+static void
+ide_html_generator_real_generate_async (IdeHtmlGenerator    *self,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_html_generator_real_generate_async);
+  ide_task_return_unsupported_error (task);
+}
+
+static GBytes *
+ide_html_generator_real_generate_finish (IdeHtmlGenerator  *self,
+                                         GAsyncResult      *result,
+                                         GError           **error)
+{
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static void
+ide_html_generator_dispose (GObject *object)
+{
+  IdeHtmlGenerator *self = (IdeHtmlGenerator *)object;
+  IdeHtmlGeneratorPrivate *priv = ide_html_generator_get_instance_private (self);
+
+  g_clear_pointer (&priv->base_uri, g_free);
+
+  G_OBJECT_CLASS (ide_html_generator_parent_class)->dispose (object);
+}
+
+static void
+ide_html_generator_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdeHtmlGenerator *self = IDE_HTML_GENERATOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_BASE_URI:
+      g_value_set_string (value, ide_html_generator_get_base_uri (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_html_generator_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdeHtmlGenerator *self = IDE_HTML_GENERATOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_BASE_URI:
+      ide_html_generator_set_base_uri (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_html_generator_class_init (IdeHtmlGeneratorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_html_generator_dispose;
+  object_class->get_property = ide_html_generator_get_property;
+  object_class->set_property = ide_html_generator_set_property;
+
+  klass->generate_async = ide_html_generator_real_generate_async;
+  klass->generate_finish = ide_html_generator_real_generate_finish;
+
+  properties [PROP_BASE_URI] =
+    g_param_spec_string ("base-uri", NULL, NULL, NULL,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_EXPLICIT_NOTIFY |
+                          G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeHtmlGenerator::invalidate:
+   *
+   * The "invalidate" signal is emitted when contents have changed.
+   *
+   * This signal will be emitted by subclasses when the contents have changed
+   * and HTML will need to be regenerated.
+   */
+  signals [INVALIDATE] =
+    g_signal_new ("invalidate",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeHtmlGeneratorClass, invalidate),
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE, 0);
+}
+
+static void
+ide_html_generator_init (IdeHtmlGenerator *self)
+{
+}
+
+/**
+ * ide_html_generator_generate_async:
+ * @self: a #IdeHtmlGenerator
+ * @cancellable: a #GCancellable
+ * @callback: a function to call after completion
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously generate HTML.
+ *
+ * This virtual function should be implemented by subclasses to generate
+ * HTML based on some form of input (which is left to the subclass).
+ *
+ * Upon completion, @callback is called and expected to call
+ * ide_html_generator_generate_finish() to retrieve the result.
+ */
+void
+ide_html_generator_generate_async (IdeHtmlGenerator    *self,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_HTML_GENERATOR (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_HTML_GENERATOR_GET_CLASS (self)->generate_async (self, cancellable, callback, user_data);
+}
+
+/**
+ * ide_html_generator_generate_finish:
+ * @self: a #IdeHtmlGenerator
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError
+ *
+ * Completes a request to generate HTML.
+ *
+ * This function is used to complete a request to generate HTML from some
+ * form of input, asynchronously. The content of the HTML is dependent on
+ * the subclass implementation of #IdeHtmlGenerator.
+ *
+ * It is required that the resulting bytes have a NULL terminator at
+ * the end which is not part of the bytes length.
+ *
+ * Returns: (transfer full): a #GBytes if successful; otherwise %NULL
+ *   and @error is set.
+ */
+GBytes *
+ide_html_generator_generate_finish (IdeHtmlGenerator  *self,
+                                    GAsyncResult      *result,
+                                    GError           **error)
+{
+  GBytes *ret;
+
+  g_return_val_if_fail (IDE_IS_HTML_GENERATOR (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  ret = IDE_HTML_GENERATOR_GET_CLASS (self)->generate_finish (self, result, error);
+
+#ifdef G_ENABLE_DEBUG
+  if (ret != NULL)
+    {
+      const guint8 *data;
+      gsize len;
+
+      data = g_bytes_get_data (ret, &len);
+      g_assert (data[len] == 0);
+    }
+#endif
+
+  return ret;
+}
+
+/**
+ * ide_html_generator_invalidate:
+ * @self: a #IdeHtmlGenerator
+ *
+ * Notifies that the last generated HTML is now invalid.
+ *
+ * This is used by subclasses to denote that the HTML contents
+ * have changed and will need to be regenerated.
+ */
+void
+ide_html_generator_invalidate (IdeHtmlGenerator *self)
+{
+  g_return_if_fail (IDE_IS_HTML_GENERATOR (self));
+
+  g_signal_emit (self, signals [INVALIDATE], 0);
+}
+
+/**
+ * ide_html_generator_new_for_buffer:
+ * @buffer: a #GtkTextBuffer
+ *
+ * Create a 1:1 HTML generator for a buffer.
+ *
+ * Creates a #IdeHtmlGenerator that passes the content directly from
+ * what is found in a #GtkTextBuffer.
+ *
+ * Returns: (transfer full): an #IdeHtmlGenerator
+ */
+IdeHtmlGenerator *
+ide_html_generator_new_for_buffer (GtkTextBuffer *buffer)
+{
+  g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), NULL);
+
+  return g_object_new (IDE_TYPE_TEXT_BUFFER_HTML_GENERATOR,
+                       "buffer", buffer,
+                       NULL);
+}
+
+const char *
+ide_html_generator_get_base_uri (IdeHtmlGenerator *self)
+{
+  IdeHtmlGeneratorPrivate *priv = ide_html_generator_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_HTML_GENERATOR (self), NULL);
+
+  return priv->base_uri;
+}
+
+void
+ide_html_generator_set_base_uri (IdeHtmlGenerator *self,
+                                 const char       *base_uri)
+{
+  IdeHtmlGeneratorPrivate *priv = ide_html_generator_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_HTML_GENERATOR (self));
+
+  if (g_strcmp0 (priv->base_uri, base_uri) != 0)
+    {
+      g_free (priv->base_uri);
+      priv->base_uri = g_strdup (base_uri);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BASE_URI]);
+    }
+}
diff --git a/src/libide/webkit/ide-html-generator.h b/src/libide/webkit/ide-html-generator.h
new file mode 100644
index 000000000..aef2ec92d
--- /dev/null
+++ b/src/libide/webkit/ide-html-generator.h
@@ -0,0 +1,67 @@
+/* ide-html-generator.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HTML_GENERATOR (ide_html_generator_get_type())
+
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (IdeHtmlGenerator, ide_html_generator, IDE, HTML_GENERATOR, GObject)
+
+struct _IdeHtmlGeneratorClass
+{
+  GObjectClass parent_class;
+
+  void    (*invalidate)      (IdeHtmlGenerator     *self);
+  void    (*generate_async)  (IdeHtmlGenerator     *self,
+                              GCancellable         *cancellable,
+                              GAsyncReadyCallback   callback,
+                              gpointer              user_data);
+  GBytes *(*generate_finish) (IdeHtmlGenerator     *self,
+                              GAsyncResult         *result,
+                              GError              **error);
+};
+
+IDE_AVAILABLE_IN_ALL
+const char       *ide_html_generator_get_base_uri    (IdeHtmlGenerator     *self);
+IDE_AVAILABLE_IN_ALL
+void              ide_html_generator_set_base_uri    (IdeHtmlGenerator     *self,
+                                                      const char           *base_uri);
+IDE_AVAILABLE_IN_ALL
+void              ide_html_generator_invalidate      (IdeHtmlGenerator     *self);
+IDE_AVAILABLE_IN_ALL
+void              ide_html_generator_generate_async  (IdeHtmlGenerator     *self,
+                                                      GCancellable         *cancellable,
+                                                      GAsyncReadyCallback   callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_ALL
+GBytes           *ide_html_generator_generate_finish (IdeHtmlGenerator     *self,
+                                                      GAsyncResult         *result,
+                                                      GError              **error);
+IDE_AVAILABLE_IN_ALL
+IdeHtmlGenerator *ide_html_generator_new_for_buffer  (GtkTextBuffer        *buffer);
+
+G_END_DECLS
diff --git a/src/libide/webkit/ide-text-buffer-html-generator.c 
b/src/libide/webkit/ide-text-buffer-html-generator.c
new file mode 100644
index 000000000..5764c2148
--- /dev/null
+++ b/src/libide/webkit/ide-text-buffer-html-generator.c
@@ -0,0 +1,205 @@
+/* ide-text-buffer-html-generator.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-text-buffer-html-generator"
+
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-threading.h>
+
+#include "ide-text-buffer-html-generator.h"
+
+struct _IdeTextBufferHtmlGenerator
+{
+  IdeHtmlGenerator parent_instance;
+  GSignalGroup *buffer_signals;
+};
+
+enum {
+  PROP_0,
+  PROP_BUFFER,
+  N_PROPS
+};
+
+G_DEFINE_FINAL_TYPE (IdeTextBufferHtmlGenerator, ide_text_buffer_html_generator, IDE_TYPE_HTML_GENERATOR)
+
+static GParamSpec *properties [N_PROPS];
+
+static inline GBytes *
+get_buffer_bytes (GtkTextBuffer *buffer)
+{
+  GtkTextIter begin, end;
+  char *slice;
+
+  gtk_text_buffer_get_bounds (buffer, &begin, &end);
+  slice = gtk_text_iter_get_slice (&begin, &end);
+
+  return g_bytes_new_take (slice, strlen (slice));
+}
+
+static void
+ide_text_buffer_html_generator_generate_async (IdeHtmlGenerator    *generator,
+                                               GCancellable        *cancellable,
+                                               GAsyncReadyCallback  callback,
+                                               gpointer             user_data)
+{
+  IdeTextBufferHtmlGenerator *self = (IdeTextBufferHtmlGenerator *)generator;
+  g_autoptr(GtkTextBuffer) buffer = NULL;
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_TEXT_BUFFER_HTML_GENERATOR (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_text_buffer_html_generator_generate_async);
+
+  buffer = g_signal_group_dup_target (self->buffer_signals);
+
+  if (IDE_IS_BUFFER (buffer))
+    ide_task_return_pointer (task,
+                             ide_buffer_dup_content (IDE_BUFFER (buffer)),
+                             g_bytes_unref);
+  else if (GTK_IS_TEXT_BUFFER (buffer))
+    ide_task_return_pointer (task, get_buffer_bytes (buffer), g_bytes_unref);
+  else
+    ide_task_return_pointer (task,
+                             g_bytes_new_take (g_strdup (""), 0),
+                             g_bytes_unref);
+}
+
+static GBytes *
+ide_text_buffer_html_generator_generate_finish (IdeHtmlGenerator  *generator,
+                                                GAsyncResult      *result,
+                                                GError           **error)
+{
+  g_assert (IDE_IS_TEXT_BUFFER_HTML_GENERATOR (generator));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static gboolean
+file_to_base_uri (GBinding     *binding,
+                  const GValue *from,
+                  GValue       *to,
+                  gpointer      user_data)
+{
+  g_value_set_string (to, g_file_get_uri (g_value_get_object (from)));
+  return TRUE;
+}
+
+static void
+ide_text_buffer_html_generator_set_buffer (IdeTextBufferHtmlGenerator *self,
+                                           GtkTextBuffer              *buffer)
+{
+  g_assert (IDE_IS_TEXT_BUFFER_HTML_GENERATOR (self));
+  g_assert (!buffer || GTK_IS_TEXT_BUFFER (buffer));
+
+  g_signal_group_set_target (self->buffer_signals, buffer);
+
+  if (IDE_IS_BUFFER (buffer))
+    g_object_bind_property_full (buffer, "file",
+                                 self, "base-uri",
+                                 G_BINDING_SYNC_CREATE,
+                                 file_to_base_uri,
+                                 NULL, NULL, NULL);
+}
+
+static void
+ide_text_buffer_html_generator_dispose (GObject *object)
+{
+  IdeTextBufferHtmlGenerator *self = (IdeTextBufferHtmlGenerator *)object;
+
+  g_clear_object (&self->buffer_signals);
+
+  G_OBJECT_CLASS (ide_text_buffer_html_generator_parent_class)->dispose (object);
+}
+
+static void
+ide_text_buffer_html_generator_get_property (GObject    *object,
+                                             guint       prop_id,
+                                             GValue     *value,
+                                             GParamSpec *pspec)
+{
+  IdeTextBufferHtmlGenerator *self = IDE_TEXT_BUFFER_HTML_GENERATOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      g_value_take_object (value, g_signal_group_dup_target (self->buffer_signals));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_text_buffer_html_generator_set_property (GObject      *object,
+                                             guint         prop_id,
+                                             const GValue *value,
+                                             GParamSpec   *pspec)
+{
+  IdeTextBufferHtmlGenerator *self = IDE_TEXT_BUFFER_HTML_GENERATOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      ide_text_buffer_html_generator_set_buffer (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_text_buffer_html_generator_class_init (IdeTextBufferHtmlGeneratorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeHtmlGeneratorClass *generator_class = IDE_HTML_GENERATOR_CLASS (klass);
+
+  object_class->dispose = ide_text_buffer_html_generator_dispose;
+  object_class->get_property = ide_text_buffer_html_generator_get_property;
+  object_class->set_property = ide_text_buffer_html_generator_set_property;
+
+  generator_class->generate_async = ide_text_buffer_html_generator_generate_async;
+  generator_class->generate_finish = ide_text_buffer_html_generator_generate_finish;
+
+  properties [PROP_BUFFER] =
+    g_param_spec_object ("buffer", NULL, NULL,
+                         GTK_TYPE_TEXT_BUFFER,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_text_buffer_html_generator_init (IdeTextBufferHtmlGenerator *self)
+{
+  self->buffer_signals = g_signal_group_new (GTK_TYPE_TEXT_BUFFER);
+
+  g_signal_group_connect_object (self->buffer_signals,
+                                 "changed",
+                                 G_CALLBACK (ide_html_generator_invalidate),
+                                 self,
+                                 G_CONNECT_SWAPPED);
+}
diff --git a/src/libide/webkit/ide-text-buffer-html-generator.h 
b/src/libide/webkit/ide-text-buffer-html-generator.h
new file mode 100644
index 000000000..843910b33
--- /dev/null
+++ b/src/libide/webkit/ide-text-buffer-html-generator.h
@@ -0,0 +1,31 @@
+/* ide-text-buffer-html-generator.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "ide-html-generator.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEXT_BUFFER_HTML_GENERATOR (ide_text_buffer_html_generator_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeTextBufferHtmlGenerator, ide_text_buffer_html_generator, IDE, 
TEXT_BUFFER_HTML_GENERATOR, IdeHtmlGenerator)
+
+G_END_DECLS
diff --git a/src/libide/webkit/ide-url-bar.c b/src/libide/webkit/ide-url-bar.c
new file mode 100644
index 000000000..0bc3d9194
--- /dev/null
+++ b/src/libide/webkit/ide-url-bar.c
@@ -0,0 +1,453 @@
+/* ide-url-bar.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-url-bar"
+
+#include "config.h"
+
+#include <libide-io.h>
+#include <libide-gtk.h>
+
+#include "ide-url-bar.h"
+#include "ide-webkit-util.h"
+
+struct _IdeUrlBar
+{
+  GtkWidget      parent_instance;
+
+  /* Owned references */
+  WebKitWebView *web_view;
+  GBindingGroup *web_view_bindings;
+  GSignalGroup  *web_view_signals;
+
+  /* Weak References */
+  IdeAnimation   *animation;
+
+  /* Template references */
+  GtkOverlay     *overlay;
+  GtkStack       *stack;
+  GtkLabel       *url_display;
+  GtkText        *url_editable;
+  GtkProgressBar *load_progress;
+  GtkImage       *security_image;
+};
+
+enum {
+  PROP_0,
+  PROP_WEB_VIEW,
+  N_PROPS
+};
+
+G_DEFINE_FINAL_TYPE (IdeUrlBar, ide_url_bar, GTK_TYPE_WIDGET)
+
+static GParamSpec *properties [N_PROPS];
+
+static const char *
+get_security_icon_name (IdeWebkitSecurityLevel security_level)
+{
+  switch (security_level)
+    {
+    case IDE_WEBKIT_SECURITY_LEVEL_LOCAL_PAGE:
+    case IDE_WEBKIT_SECURITY_LEVEL_TO_BE_DETERMINED:
+      return NULL;
+
+    case IDE_WEBKIT_SECURITY_LEVEL_NONE:
+    case IDE_WEBKIT_SECURITY_LEVEL_UNACCEPTABLE_CERTIFICATE:
+      return "lock-small-open-symbolic";
+
+    case IDE_WEBKIT_SECURITY_LEVEL_STRONG_SECURITY:
+      return "lock-small-symbolic";
+
+    default:
+      return NULL;
+    }
+}
+
+static void
+on_web_view_load_changed_cb (IdeUrlBar       *self,
+                             WebKitLoadEvent  load_event,
+                             WebKitWebView   *web_view)
+{
+  g_assert (IDE_IS_URL_BAR (self));
+  g_assert (WEBKIT_IS_WEB_VIEW (web_view));
+
+  switch (load_event)
+    {
+    case WEBKIT_LOAD_COMMITTED:
+    case WEBKIT_LOAD_FINISHED: {
+      IdeWebkitSecurityLevel security_level;
+
+      security_level = ide_webkit_util_get_security_level (web_view);
+      g_object_set (self->security_image,
+                    "icon-name", get_security_icon_name (security_level),
+                    NULL);
+      break;
+    }
+
+    case WEBKIT_LOAD_REDIRECTED:
+    case WEBKIT_LOAD_STARTED:
+      g_object_set (self->security_image,
+                    "icon-name", "content-loading-symbolic",
+                    NULL);
+      break;
+
+    default:
+      break;
+    }
+}
+
+static void
+on_editable_focus_enter_cb (IdeUrlBar               *self,
+                            GtkEventControllerFocus *focus)
+{
+  g_assert (IDE_IS_URL_BAR (self));
+  g_assert (GTK_IS_EVENT_CONTROLLER_FOCUS (focus));
+
+}
+
+static void
+on_editable_focus_leave_cb (IdeUrlBar               *self,
+                            GtkEventControllerFocus *focus)
+{
+  g_assert (IDE_IS_URL_BAR (self));
+  g_assert (GTK_IS_EVENT_CONTROLLER_FOCUS (focus));
+
+}
+
+static void
+on_editable_activate_cb (IdeUrlBar   *self,
+                         GtkEditable *editable)
+{
+  g_autofree char *expanded = NULL;
+  g_autofree char *normalized = NULL;
+  const char *uri;
+
+  g_assert (IDE_IS_URL_BAR (self));
+  g_assert (GTK_IS_EDITABLE (editable));
+
+  if (self->web_view == NULL)
+    return;
+
+  uri = gtk_editable_get_text (editable);
+  if (uri == NULL || uri[0] == 0)
+    return;
+
+  /* Expand ~/ access to home directory first */
+  if (g_str_has_prefix (uri, "~/"))
+    uri = expanded = ide_path_expand (uri);
+
+  normalized = ide_webkit_util_normalize_address (uri);
+
+  webkit_web_view_load_uri (self->web_view, normalized);
+  gtk_stack_set_visible_child_name (self->stack, "display");
+  gtk_widget_grab_focus (GTK_WIDGET (self->web_view));
+}
+
+static void
+on_click_gesture_pressed_cb (IdeUrlBar       *self,
+                             int              n_presses,
+                             double           x,
+                             double           y,
+                             GtkGestureClick *click)
+{
+  const char *name;
+
+  g_assert (IDE_IS_URL_BAR (self));
+  g_assert (GTK_IS_GESTURE_CLICK (click));
+
+  if (self->web_view == NULL)
+    return;
+
+  name = gtk_stack_get_visible_child_name (self->stack);
+
+  /* On first click, just change to the text field immediately so that
+   * we can propagate the event to that widget instead of the label.
+   */
+  if (n_presses == 1)
+    {
+      if (g_strcmp0 (name, "edit") != 0)
+        {
+          const char *uri = webkit_web_view_get_uri (self->web_view);
+
+          gtk_editable_set_text (GTK_EDITABLE (self->url_editable), uri ? uri : "");
+          gtk_stack_set_visible_child_name (self->stack, "edit");
+          gtk_widget_grab_focus (GTK_WIDGET (self->url_editable));
+          gtk_editable_select_region (GTK_EDITABLE (self->url_editable), 0, -1);
+          gtk_gesture_set_state (GTK_GESTURE (click), GTK_EVENT_SEQUENCE_CLAIMED);
+          return;
+        }
+    }
+
+  gtk_gesture_set_state (GTK_GESTURE (click), GTK_EVENT_SEQUENCE_DENIED);
+}
+
+static void
+on_web_view_notify_is_loading_cb (IdeUrlBar     *self,
+                                  GParamSpec    *pspec,
+                                  WebKitWebView *web_view)
+{
+  g_assert (IDE_IS_URL_BAR (self));
+  g_assert (WEBKIT_IS_WEB_VIEW (web_view));
+
+  if (webkit_web_view_is_loading (web_view))
+    {
+      gtk_progress_bar_set_fraction (self->load_progress, 0);
+      gtk_widget_show (GTK_WIDGET (self->load_progress));
+    }
+  else
+    {
+      ide_gtk_widget_hide_with_fade (GTK_WIDGET (self->load_progress));
+    }
+}
+
+static void
+on_web_view_notify_estimated_load_progress_cb (IdeUrlBar     *self,
+                                               GParamSpec    *pspec,
+                                               WebKitWebView *web_view)
+{
+  IdeAnimation *anim;
+  double progress;
+
+  g_assert (IDE_IS_URL_BAR (self));
+  g_assert (WEBKIT_IS_WEB_VIEW (web_view));
+
+  progress = webkit_web_view_get_estimated_load_progress (web_view);
+
+  /* First cancel any previous animation */
+  if ((anim = self->animation))
+    {
+      g_clear_weak_pointer (&self->animation);
+      ide_animation_stop (anim);
+    }
+
+  /* Short-circuit if we're not actively loading or we are jumping
+   * backwards in progress instead of forwards.
+   */
+  if (!webkit_web_view_is_loading (web_view) ||
+      progress < gtk_progress_bar_get_fraction (self->load_progress))
+    {
+      gtk_progress_bar_set_fraction (self->load_progress, progress);
+      return;
+    }
+
+  anim = ide_object_animate (self->load_progress,
+                             IDE_ANIMATION_LINEAR,
+                             200,
+                             NULL,
+                             "fraction", progress,
+                             NULL);
+  g_set_weak_pointer (&self->animation, anim);
+}
+
+static gboolean
+ide_url_bar_grab_focus (GtkWidget *widget)
+{
+  IdeUrlBar *self = (IdeUrlBar *)widget;
+
+  g_assert (IDE_IS_URL_BAR (self));
+
+  if (self->web_view == NULL)
+    return FALSE;
+
+  gtk_stack_set_visible_child_name (self->stack, "edit");
+  gtk_widget_grab_focus (GTK_WIDGET (self->url_editable));
+  gtk_editable_select_region (GTK_EDITABLE (self->url_editable), 0, -1);
+
+  return TRUE;
+}
+
+static gboolean
+focus_view_callback (GtkWidget *widget,
+                     GVariant  *params,
+                     gpointer   user_data)
+{
+  IdeUrlBar *self = (IdeUrlBar *)widget;
+
+  g_assert (IDE_IS_URL_BAR (self));
+
+  if (self->web_view != NULL)
+    return gtk_widget_grab_focus (GTK_WIDGET (self->web_view));
+
+  return FALSE;
+}
+
+static void
+ide_url_bar_dispose (GObject *object)
+{
+  IdeUrlBar *self = (IdeUrlBar *)object;
+
+  g_clear_object (&self->web_view_bindings);
+  g_clear_object (&self->web_view_signals);
+  g_clear_object (&self->web_view);
+
+  g_clear_pointer ((GtkWidget **)&self->overlay, gtk_widget_unparent);
+
+  G_OBJECT_CLASS (ide_url_bar_parent_class)->dispose (object);
+}
+
+static void
+ide_url_bar_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  IdeUrlBar *self = IDE_URL_BAR (object);
+
+  switch (prop_id)
+    {
+    case PROP_WEB_VIEW:
+      g_value_set_object (value, ide_url_bar_get_web_view (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_url_bar_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  IdeUrlBar *self = IDE_URL_BAR (object);
+
+  switch (prop_id)
+    {
+    case PROP_WEB_VIEW:
+      ide_url_bar_set_web_view (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_url_bar_class_init (IdeUrlBarClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = ide_url_bar_dispose;
+  object_class->get_property = ide_url_bar_get_property;
+  object_class->set_property = ide_url_bar_set_property;
+
+  widget_class->grab_focus = ide_url_bar_grab_focus;
+
+  properties [PROP_WEB_VIEW] =
+    g_param_spec_object ("web-view", NULL, NULL,
+                         WEBKIT_TYPE_WEB_VIEW,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "entry");
+  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/webkit/ide-url-bar.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeUrlBar, url_display);
+  gtk_widget_class_bind_template_child (widget_class, IdeUrlBar, url_editable);
+  gtk_widget_class_bind_template_child (widget_class, IdeUrlBar, load_progress);
+  gtk_widget_class_bind_template_child (widget_class, IdeUrlBar, overlay);
+  gtk_widget_class_bind_template_child (widget_class, IdeUrlBar, security_image);
+  gtk_widget_class_bind_template_child (widget_class, IdeUrlBar, stack);
+  gtk_widget_class_bind_template_callback (widget_class, on_click_gesture_pressed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, on_editable_focus_enter_cb);
+  gtk_widget_class_bind_template_callback (widget_class, on_editable_focus_leave_cb);
+  gtk_widget_class_bind_template_callback (widget_class, on_editable_activate_cb);
+
+  gtk_widget_class_add_binding (widget_class, GDK_KEY_Escape, 0, focus_view_callback, NULL);
+}
+
+static void
+ide_url_bar_init (IdeUrlBar *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->web_view_signals = g_signal_group_new (WEBKIT_TYPE_WEB_VIEW);
+  g_signal_group_connect_object (self->web_view_signals,
+                                 "notify::estimated-load-progress",
+                                 G_CALLBACK (on_web_view_notify_estimated_load_progress_cb),
+                                 self,
+                                 G_CONNECT_SWAPPED);
+  g_signal_group_connect_object (self->web_view_signals,
+                                 "notify::is-loading",
+                                 G_CALLBACK (on_web_view_notify_is_loading_cb),
+                                 self,
+                                 G_CONNECT_SWAPPED);
+  g_signal_group_connect_object (self->web_view_signals,
+                                 "load-changed",
+                                 G_CALLBACK (on_web_view_load_changed_cb),
+                                 self,
+                                 G_CONNECT_SWAPPED);
+
+  self->web_view_bindings = g_binding_group_new ();
+  g_binding_group_bind (self->web_view_bindings, "uri",
+                        self->url_display, "label",
+                        G_BINDING_SYNC_CREATE);
+
+  gtk_widget_set_cursor_from_name (GTK_WIDGET (self->url_display), "text");
+}
+
+WebKitWebView *
+ide_url_bar_get_web_view (IdeUrlBar *self)
+{
+  g_return_val_if_fail (IDE_IS_URL_BAR (self), NULL);
+
+  return self->web_view;
+}
+
+void
+ide_url_bar_set_web_view (IdeUrlBar     *self,
+                          WebKitWebView *web_view)
+{
+  g_return_if_fail (IDE_IS_URL_BAR (self));
+  g_return_if_fail (!web_view || WEBKIT_IS_WEB_VIEW (web_view));
+
+  if (g_set_object (&self->web_view, web_view))
+    {
+      g_binding_group_set_source (self->web_view_bindings, web_view);
+      g_signal_group_set_target (self->web_view_signals, web_view);
+
+      gtk_widget_hide (GTK_WIDGET (self->load_progress));
+      gtk_widget_set_can_focus (GTK_WIDGET (self), web_view != NULL);
+      g_object_set (self->security_image,
+                    "icon-name", NULL,
+                    NULL);
+
+      if (self->web_view != NULL)
+        {
+          const char *uri = webkit_web_view_get_uri (self->web_view);
+
+          gtk_editable_set_text (GTK_EDITABLE (self->url_editable), uri ? uri : "");
+
+          if (gtk_widget_has_focus (GTK_WIDGET (self->url_editable)))
+            gtk_editable_select_region (GTK_EDITABLE (self->url_editable), 0, -1);
+
+          on_web_view_notify_estimated_load_progress_cb (self, NULL, self->web_view);
+
+          /* TODO: Update security image if we ever share a url bar for multiple
+           *       web views.
+           */
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_WEB_VIEW]);
+    }
+}
diff --git a/src/libide/webkit/ide-url-bar.h b/src/libide/webkit/ide-url-bar.h
new file mode 100644
index 000000000..dd0d68a21
--- /dev/null
+++ b/src/libide/webkit/ide-url-bar.h
@@ -0,0 +1,37 @@
+/* ide-url-bar.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <webkit2/webkit2.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_URL_BAR (ide_url_bar_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeUrlBar, ide_url_bar, IDE, URL_BAR, GtkWidget)
+
+IdeUrlBar     *ide_url_bar_new          (void);
+WebKitWebView *ide_url_bar_get_web_view (IdeUrlBar *self);
+void           ide_url_bar_set_web_view (IdeUrlBar     *self,
+                                         WebKitWebView *web_view);
+
+G_END_DECLS
diff --git a/src/libide/webkit/ide-url-bar.ui b/src/libide/webkit/ide-url-bar.ui
new file mode 100644
index 000000000..ee53cfba0
--- /dev/null
+++ b/src/libide/webkit/ide-url-bar.ui
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeUrlBar" parent="GtkWidget">
+    <child>
+      <object class="GtkGestureClick">
+        <property name="propagation-phase">capture</property>
+        <signal name="pressed" handler="on_click_gesture_pressed_cb" swapped="true" object="IdeUrlBar"/>
+      </object>
+    </child>
+    <child>
+      <object class="GtkOverlay" id="overlay">
+        <child type="overlay">
+          <object class="GtkProgressBar" id="load_progress">
+            <property name="hexpand">true</property>
+            <property name="valign">end</property>
+            <property name="margin-start">6</property>
+            <property name="margin-end">6</property>
+            <property name="visible">false</property>
+            <style>
+              <class name="osd"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStack" id="stack">
+            <property name="hexpand">true</property>
+            <property name="transition-type">none</property>
+            <property name="hhomogeneous">true</property>
+            <child>
+              <object class="GtkStackPage">
+                <property name="name">display</property>
+                <property name="child">
+                  <object class="GtkBox">
+                    <child>
+                      <object class="GtkBox" id="display_controls">
+                        <property name="margin-start">6</property>
+                        <property name="margin-end">6</property>
+                        <property name="spacing">3</property>
+                        <child>
+                          <object class="GtkImage" id="security_image">
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="url_display">
+                        <property name="hexpand">true</property>
+                        <property name="xalign">0</property>
+                      </object>
+                    </child>
+                  </object>
+                </property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkStackPage">
+                <property name="name">edit</property>
+                <property name="child">
+                  <object class="GtkBox">
+                    <child>
+                      <object class="GtkBox" id="edit_controls">
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkText" id="url_editable">
+                        <property name="hexpand">true</property>
+                        <property name="enable-emoji-completion">false</property>
+                        <property name="input-purpose">url</property>
+                        <signal name="activate" handler="on_editable_activate_cb" swapped="true" 
object="IdeUrlBar"/>
+                        <child>
+                          <object class="GtkEventControllerFocus">
+                            <signal name="enter" handler="on_editable_focus_enter_cb" swapped="true" 
object="IdeUrlBar"/>
+                            <signal name="leave" handler="on_editable_focus_leave_cb" swapped="true" 
object="IdeUrlBar"/>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkSizeGroup">
+    <property name="mode">horizontal</property>
+    <widgets>
+      <widget name="edit_controls"/>
+      <widget name="display_controls"/>
+    </widgets>
+  </object>
+</interface>
diff --git a/src/libide/webkit/ide-webkit-page.c b/src/libide/webkit/ide-webkit-page.c
new file mode 100644
index 000000000..f8af06ffe
--- /dev/null
+++ b/src/libide/webkit/ide-webkit-page.c
@@ -0,0 +1,708 @@
+/* ide-webkit-page.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-webkit-page"
+
+#include "config.h"
+
+#include <webkit2/webkit2.h>
+
+#include "ide-webkit-page.h"
+#include "ide-url-bar.h"
+
+typedef struct
+{
+  GtkStack           *reload_stack;
+  GtkCenterBox       *toolbar;
+  IdeUrlBar          *url_bar;
+  WebKitSettings     *web_settings;
+  WebKitWebView      *web_view;
+
+  GSimpleActionGroup *actions;
+
+  IdeHtmlGenerator   *generator;
+
+  guint               dirty : 1;
+  guint               generating : 1;
+  guint               disposed : 1;
+} IdeWebkitPagePrivate;
+
+enum {
+  PROP_0,
+  PROP_SHOW_TOOLBAR,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeWebkitPage, ide_webkit_page, IDE_TYPE_PAGE)
+
+static GParamSpec *properties [N_PROPS];
+
+static gboolean
+transform_title_with_fallback (GBinding     *binding,
+                               const GValue *from_value,
+                               GValue       *to_value,
+                               gpointer      user_data)
+{
+  IdeWebkitPage *self = user_data;
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+  const char *title;
+
+  g_assert (G_IS_BINDING (binding));
+  g_assert (G_VALUE_HOLDS_STRING (from_value));
+  g_assert (G_VALUE_HOLDS_STRING (to_value));
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+
+  title = g_value_get_string (from_value);
+  if (ide_str_empty0 (title))
+    title = webkit_web_view_get_uri (priv->web_view);
+  g_value_set_string (to_value, title);
+  return TRUE;
+}
+
+static gboolean
+transform_cairo_surface_to_gicon (GBinding     *binding,
+                                  const GValue *from_value,
+                                  GValue       *to_value,
+                                  gpointer      user_data)
+{
+  IdeWebkitPage *self = user_data;
+  cairo_surface_t *surface;
+  GdkPixbuf *pixbuf;
+  int favicon_width;
+  int favicon_height;
+  int width;
+  int height;
+
+  g_assert (G_IS_BINDING (binding));
+  g_assert (G_VALUE_HOLDS_POINTER (from_value));
+  g_assert (G_VALUE_HOLDS_OBJECT (to_value));
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+
+  /* No ownership transfer */
+  surface = g_value_get_pointer (from_value);
+
+  if (surface == NULL)
+    {
+      g_value_take_object (to_value, g_themed_icon_new ("web-browser-symbolic"));
+      return TRUE;
+    }
+
+  width = 16 * gtk_widget_get_scale_factor (GTK_WIDGET (self));
+  height = 16 * gtk_widget_get_scale_factor (GTK_WIDGET (self));
+  favicon_width = cairo_image_surface_get_width (surface);
+  favicon_height = cairo_image_surface_get_height (surface);
+  pixbuf = gdk_pixbuf_get_from_surface (surface, 0, 0, favicon_width, favicon_height);
+
+  if ((favicon_width != width || favicon_height != height))
+    {
+      GdkPixbuf *scaled_pixbuf = gdk_pixbuf_scale_simple (pixbuf, width, height, GDK_INTERP_BILINEAR);
+      g_object_unref (pixbuf);
+      pixbuf = scaled_pixbuf;
+    }
+
+  g_assert (!pixbuf || G_IS_ICON (pixbuf));
+
+  g_value_take_object (to_value, pixbuf);
+
+  return TRUE;
+}
+
+static void
+on_toolbar_notify_visible_cb (IdeWebkitPage *self,
+                              GParamSpec    *pspec,
+                              GtkWidget     *toolbar)
+{
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+  g_assert (GTK_IS_WIDGET (toolbar));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_TOOLBAR]);
+}
+
+static gboolean
+ide_webkit_page_grab_focus (GtkWidget *widget)
+{
+  IdeWebkitPage *self = (IdeWebkitPage *)widget;
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+  const char *uri;
+
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+
+  uri = webkit_web_view_get_uri (priv->web_view);
+
+  if (ide_str_empty0 (uri))
+    return gtk_widget_grab_focus (GTK_WIDGET (priv->url_bar));
+  else
+    return gtk_widget_grab_focus (GTK_WIDGET (priv->web_view));
+}
+
+static gboolean
+on_web_view_decide_policy_cb (IdeWebkitPage            *self,
+                              WebKitPolicyDecision     *decision,
+                              WebKitPolicyDecisionType  decision_type,
+                              WebKitWebView            *web_view)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+  g_assert (WEBKIT_IS_POLICY_DECISION (decision));
+  g_assert (WEBKIT_IS_WEB_VIEW (web_view));
+
+  if (priv->generator == NULL)
+    return FALSE;
+
+  if (decision_type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION)
+    {
+      WebKitNavigationAction *action = webkit_navigation_policy_decision_get_navigation_action 
(WEBKIT_NAVIGATION_POLICY_DECISION (decision));
+      WebKitURIRequest *request = webkit_navigation_action_get_request (action);
+      const char *uri = webkit_uri_request_get_uri (request);
+      const char *base_uri = ide_html_generator_get_base_uri (priv->generator);
+
+      if (!ide_str_equal0 (uri, base_uri))
+        {
+          GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (self));
+          ide_gtk_show_uri_on_window (GTK_WINDOW (root), uri, g_get_monotonic_time (), NULL);
+          webkit_policy_decision_ignore (decision);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+static void
+go_forward_action (GSimpleAction *action,
+                   GVariant      *param,
+                   gpointer       user_data)
+{
+  IdeWebkitPage *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+
+  ide_webkit_page_go_forward (self);
+
+  IDE_EXIT;
+}
+
+static void
+go_back_action (GSimpleAction *action,
+                GVariant      *param,
+                gpointer       user_data)
+{
+  IdeWebkitPage *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+
+  ide_webkit_page_go_back (self);
+
+  IDE_EXIT;
+}
+
+static void
+reload_action (GSimpleAction *action,
+               GVariant      *param,
+               gpointer       user_data)
+{
+  IdeWebkitPage *self = user_data;
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+
+  webkit_web_view_reload (priv->web_view);
+
+  IDE_EXIT;
+}
+
+static void
+stop_action (GSimpleAction *action,
+             GVariant      *param,
+             gpointer       user_data)
+{
+  IdeWebkitPage *self = user_data;
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+
+  webkit_web_view_stop_loading (priv->web_view);
+
+  IDE_EXIT;
+}
+
+static const GActionEntry actions[] = {
+  { "go-forward", go_forward_action },
+  { "go-back", go_back_action },
+  { "reload", reload_action },
+  { "stop", stop_action },
+};
+
+static void
+set_action_enabled (IdeWebkitPage *self,
+                    const char    *action_name,
+                    gboolean       enabled)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+  GAction *action;
+
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+  g_assert (action_name != NULL);
+
+  if (!(action = g_action_map_lookup_action (G_ACTION_MAP (priv->actions), action_name)))
+    {
+      g_critical ("Failed to locate action %s", action_name);
+      return;
+    }
+
+  if (!G_IS_SIMPLE_ACTION (action))
+    {
+      g_critical ("Implausible, %s is not a GSimpleAction",
+                  G_OBJECT_TYPE_NAME (action));
+      return;
+    }
+
+  g_simple_action_set_enabled (G_SIMPLE_ACTION (action), enabled);
+}
+
+static void
+on_back_forward_list_changed_cb (IdeWebkitPage             *self,
+                                 WebKitBackForwardListItem *item_added,
+                                 const GList               *items_removed,
+                                 WebKitBackForwardList     *list)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+  g_assert (WEBKIT_IS_BACK_FORWARD_LIST (list));
+
+  set_action_enabled (self, "go-forward",
+                      webkit_web_view_can_go_forward (priv->web_view));
+  set_action_enabled (self, "go-back",
+                      webkit_web_view_can_go_back (priv->web_view));
+
+  IDE_EXIT;
+}
+
+static void
+ide_webkit_page_update_reload (IdeWebkitPage *self)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+  const char *uri;
+  gboolean loading;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+
+  loading = webkit_web_view_is_loading (priv->web_view);
+  uri = webkit_web_view_get_uri (priv->web_view);
+
+  set_action_enabled (self, "reload", !loading && !ide_str_empty0 (uri));
+  set_action_enabled (self, "stop", loading);
+
+  if (loading)
+    gtk_stack_set_visible_child_name (priv->reload_stack, "stop");
+  else
+    gtk_stack_set_visible_child_name (priv->reload_stack, "reload");
+
+  IDE_EXIT;
+}
+
+static void
+add_property_action (gpointer    object,
+                     const char *property_name,
+                     GActionMap *action_map)
+{
+  g_autoptr(GPropertyAction) action = NULL;
+
+  g_assert (G_IS_OBJECT (object));
+  g_assert (property_name != NULL);
+  g_assert (G_IS_ACTION_MAP (action_map));
+
+  action = g_property_action_new (property_name, object, property_name);
+
+  if (action != NULL)
+    g_action_map_add_action (action_map, G_ACTION (action));
+}
+
+static void
+ide_webkit_page_constructed (GObject *object)
+{
+  IdeWebkitPage *self = (IdeWebkitPage *)object;
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+  GtkStyleContext *context;
+  GdkRGBA color;
+
+  G_OBJECT_CLASS (ide_webkit_page_parent_class)->constructed (object);
+
+  context = gtk_widget_get_style_context (GTK_WIDGET (priv->web_view));
+  if (gtk_style_context_lookup_color (context, "theme_base_color", &color))
+    webkit_web_view_set_background_color (WEBKIT_WEB_VIEW (priv->web_view), &color);
+}
+
+static void
+ide_webkit_page_dispose (GObject *object)
+{
+  IdeWebkitPage *self = (IdeWebkitPage *)object;
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+
+  priv->disposed = TRUE;
+
+  g_clear_object (&priv->generator);
+  g_clear_object (&priv->actions);
+
+  G_OBJECT_CLASS (ide_webkit_page_parent_class)->dispose (object);
+}
+
+static void
+ide_webkit_page_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeWebkitPage *self = IDE_WEBKIT_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_SHOW_TOOLBAR:
+      g_value_set_boolean (value, ide_webkit_page_get_show_toolbar (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_webkit_page_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeWebkitPage *self = IDE_WEBKIT_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_SHOW_TOOLBAR:
+      ide_webkit_page_set_show_toolbar (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_webkit_page_class_init (IdeWebkitPageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = ide_webkit_page_constructed;
+  object_class->dispose = ide_webkit_page_dispose;
+  object_class->get_property = ide_webkit_page_get_property;
+  object_class->set_property = ide_webkit_page_set_property;
+
+  widget_class->grab_focus = ide_webkit_page_grab_focus;
+
+  properties [PROP_SHOW_TOOLBAR] =
+    g_param_spec_boolean ("show-toolbar",
+                          "Show Toolbar",
+                          "Show Toolbar",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/webkit/ide-webkit-page.ui");
+
+  gtk_widget_class_bind_template_child_private (widget_class, IdeWebkitPage, reload_stack);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeWebkitPage, toolbar);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeWebkitPage, url_bar);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeWebkitPage, web_settings);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeWebkitPage, web_view);
+  gtk_widget_class_bind_template_callback (widget_class, on_toolbar_notify_visible_cb);
+  gtk_widget_class_bind_template_callback (widget_class, ide_webkit_page_update_reload);
+  gtk_widget_class_bind_template_callback (widget_class, on_web_view_decide_policy_cb);
+
+  g_type_ensure (WEBKIT_TYPE_SETTINGS);
+  g_type_ensure (WEBKIT_TYPE_WEB_VIEW);
+  g_type_ensure (IDE_TYPE_URL_BAR);
+}
+
+static void
+ide_webkit_page_init (IdeWebkitPage *self)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+  WebKitBackForwardList *list;
+
+  panel_widget_set_can_maximize (PANEL_WIDGET (self), TRUE);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_object_bind_property_full (priv->web_view, "title", self, "title", 0,
+                               transform_title_with_fallback,
+                               NULL, self, NULL);
+  g_object_bind_property_full (priv->web_view, "favicon", self, "icon", 0,
+                               transform_cairo_surface_to_gicon,
+                               NULL, self, NULL);
+
+  priv->actions = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (priv->actions),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self),
+                                  "web",
+                                  G_ACTION_GROUP (priv->actions));
+
+  add_property_action (priv->web_settings,
+                       "enable-javascript",
+                       G_ACTION_MAP (priv->actions));
+
+  list = webkit_web_view_get_back_forward_list (priv->web_view);
+  g_signal_connect_object (list,
+                           "changed",
+                           G_CALLBACK (on_back_forward_list_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  set_action_enabled (self, "go-forward", FALSE);
+  set_action_enabled (self, "go-back", FALSE);
+  set_action_enabled (self, "reload", FALSE);
+  set_action_enabled (self, "stop", FALSE);
+}
+
+IdeWebkitPage *
+ide_webkit_page_new (void)
+{
+  return g_object_new (IDE_TYPE_WEBKIT_PAGE, NULL);
+}
+
+void
+ide_webkit_page_load_uri (IdeWebkitPage *self,
+                          const char    *uri)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WEBKIT_PAGE (self));
+  g_return_if_fail (uri != NULL);
+
+  webkit_web_view_load_uri (priv->web_view, uri);
+}
+
+gboolean
+ide_webkit_page_get_show_toolbar (IdeWebkitPage *self)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_WEBKIT_PAGE (self), FALSE);
+
+  return gtk_widget_get_visible (GTK_WIDGET (priv->toolbar));
+}
+
+void
+ide_webkit_page_set_show_toolbar (IdeWebkitPage *self,
+                                  gboolean       show_toolbar)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WEBKIT_PAGE (self));
+
+  gtk_widget_set_visible (GTK_WIDGET (priv->toolbar), show_toolbar);
+}
+
+gboolean
+ide_webkit_page_focus_address (IdeWebkitPage *self)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_WEBKIT_PAGE (self), FALSE);
+
+  return gtk_widget_grab_focus (GTK_WIDGET (priv->url_bar));
+}
+
+void
+ide_webkit_page_go_back (IdeWebkitPage *self)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+  WebKitBackForwardList *list;
+  WebKitBackForwardListItem *item;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_WEBKIT_PAGE (self));
+
+  list = webkit_web_view_get_back_forward_list (priv->web_view);
+  item = webkit_back_forward_list_get_back_item (list);
+
+  g_return_if_fail (item != NULL);
+
+  webkit_web_view_go_to_back_forward_list_item (priv->web_view, item);
+
+  IDE_EXIT;
+}
+
+void
+ide_webkit_page_go_forward (IdeWebkitPage *self)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+  WebKitBackForwardList *list;
+  WebKitBackForwardListItem *item;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_WEBKIT_PAGE (self));
+
+  list = webkit_web_view_get_back_forward_list (priv->web_view);
+  item = webkit_back_forward_list_get_forward_item (list);
+
+  g_return_if_fail (item != NULL);
+
+  webkit_web_view_go_to_back_forward_list_item (priv->web_view, item);
+
+  IDE_EXIT;
+}
+
+void
+ide_webkit_page_reload (IdeWebkitPage *self)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WEBKIT_PAGE (self));
+
+  if (webkit_web_view_is_loading (priv->web_view))
+    webkit_web_view_stop_loading (priv->web_view);
+
+  webkit_web_view_reload (priv->web_view);
+}
+
+void
+ide_webkit_page_reload_ignoring_cache (IdeWebkitPage *self)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WEBKIT_PAGE (self));
+
+  if (webkit_web_view_is_loading (priv->web_view))
+    webkit_web_view_stop_loading (priv->web_view);
+
+  webkit_web_view_reload_bypass_cache (priv->web_view);
+}
+
+static void
+ide_webkit_page_generate_cb (GObject      *object,
+                             GAsyncResult *result,
+                             gpointer      user_data)
+{
+  IdeHtmlGenerator *generator = (IdeHtmlGenerator *)object;
+  g_autoptr(IdeWebkitPage) self = user_data;
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GBytes) bytes = NULL;
+
+  g_assert (IDE_IS_HTML_GENERATOR (generator));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+
+  priv->generating = FALSE;
+
+  if (!(bytes = ide_html_generator_generate_finish (generator, result, &error)))
+    {
+      /* Don't try to spin again in this case by checking dirty */
+      g_warning ("Failed to generate HTML: %s", error->message);
+      return;
+    }
+
+  if (priv->disposed)
+    return;
+
+  webkit_web_view_load_html (priv->web_view,
+                             (const char *)g_bytes_get_data (bytes, NULL),
+                             ide_html_generator_get_base_uri (generator));
+
+  /* See if we need to run again, and check for re-entrantcy */
+  if (priv->dirty && !priv->generating)
+    {
+      priv->dirty = FALSE;
+      priv->generating = TRUE;
+      ide_html_generator_generate_async (generator,
+                                         NULL,
+                                         ide_webkit_page_generate_cb,
+                                         g_steal_pointer (&self));
+    }
+}
+
+static void
+ide_webkit_page_generator_invalidate_cb (IdeWebkitPage    *self,
+                                         IdeHtmlGenerator *generator)
+{
+  IdeWebkitPagePrivate *priv = ide_webkit_page_get_instance_private (self);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WEBKIT_PAGE (self));
+  g_assert (IDE_IS_HTML_GENERATOR (generator));
+
+  priv->dirty = TRUE;
+
+  if (priv->generating)
+    return;
+
+  priv->generating = TRUE;
+  priv->dirty = FALSE;
+
+  ide_html_generator_generate_async (generator,
+                                     NULL,
+                                     ide_webkit_page_generate_cb,
+                                     g_object_ref (self));
+}
+
+IdeWebkitPage *
+ide_webkit_page_new_for_generator (IdeHtmlGenerator *generator)
+{
+  IdeWebkitPage *self;
+  IdeWebkitPagePrivate *priv;
+
+  g_return_val_if_fail (IDE_IS_HTML_GENERATOR (generator), NULL);
+
+  self = ide_webkit_page_new ();
+  priv = ide_webkit_page_get_instance_private (self);
+
+  priv->generator = g_object_ref (generator);
+  g_signal_connect_object (priv->generator,
+                           "invalidate",
+                           G_CALLBACK (ide_webkit_page_generator_invalidate_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  ide_webkit_page_generator_invalidate_cb (self, generator);
+
+  return self;
+}
diff --git a/src/libide/webkit/ide-webkit-page.h b/src/libide/webkit/ide-webkit-page.h
new file mode 100644
index 000000000..091511fbe
--- /dev/null
+++ b/src/libide/webkit/ide-webkit-page.h
@@ -0,0 +1,62 @@
+/* ide-webkit-page.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-gui.h>
+
+#include "ide-html-generator.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_WEBKIT_PAGE (ide_webkit_page_get_type())
+
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (IdeWebkitPage, ide_webkit_page, IDE, WEBKIT_PAGE, IdePage)
+
+struct _IdeWebkitPageClass
+{
+  IdePageClass parent_class;
+};
+
+IDE_AVAILABLE_IN_ALL
+IdeWebkitPage *ide_webkit_page_new                   (void);
+IDE_AVAILABLE_IN_ALL
+IdeWebkitPage *ide_webkit_page_new_for_generator     (IdeHtmlGenerator     *generator);
+IDE_AVAILABLE_IN_ALL
+void           ide_webkit_page_load_uri              (IdeWebkitPage        *self,
+                                                      const char           *uri);
+IDE_AVAILABLE_IN_ALL
+gboolean       ide_webkit_page_focus_address         (IdeWebkitPage        *self);
+IDE_AVAILABLE_IN_ALL
+gboolean       ide_webkit_page_get_show_toolbar      (IdeWebkitPage        *self);
+IDE_AVAILABLE_IN_ALL
+void           ide_webkit_page_set_show_toolbar      (IdeWebkitPage        *self,
+                                                      gboolean              show_toolbar);
+IDE_AVAILABLE_IN_ALL
+void           ide_webkit_page_go_back               (IdeWebkitPage        *self);
+IDE_AVAILABLE_IN_ALL
+void           ide_webkit_page_go_forward            (IdeWebkitPage        *self);
+IDE_AVAILABLE_IN_ALL
+void           ide_webkit_page_reload                (IdeWebkitPage        *self);
+IDE_AVAILABLE_IN_ALL
+void           ide_webkit_page_reload_ignoring_cache (IdeWebkitPage        *self);
+
+G_END_DECLS
diff --git a/src/libide/webkit/ide-webkit-page.ui b/src/libide/webkit/ide-webkit-page.ui
new file mode 100644
index 000000000..c809725d2
--- /dev/null
+++ b/src/libide/webkit/ide-webkit-page.ui
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeWebkitPage" parent="IdePage">
+    <property name="icon-name">web-browser-symbolic</property>
+    <property name="title" translatable="yes">Blank page</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="WebKitWebView" id="web_view">
+            <property name="settings">web_settings</property>
+            <property name="hexpand">true</property>
+            <property name="vexpand">true</property>
+            <signal name="notify::is-loading" handler="ide_webkit_page_update_reload" swapped="true" 
object="IdeWebkitPage"/>
+            <signal name="notify::uri" handler="ide_webkit_page_update_reload" swapped="true" 
object="IdeWebkitPage"/>
+            <signal name="decide-policy" handler="on_web_view_decide_policy_cb" swapped="true" 
object="IdeWebkitPage"/>
+          </object>
+        </child>
+        <child>
+          <object class="GtkSeparator"/>
+        </child>
+        <child>
+          <object class="GtkCenterBox" id="toolbar">
+            <signal name="notify::visible" handler="on_toolbar_notify_visible_cb" swapped="true" 
object="IdeWebkitPage"/>
+            <property name="orientation">horizontal</property>
+            <style>
+              <class name="toolbar"/>
+            </style>
+            <child type="start">
+              <object class="GtkBox">
+                <property name="orientation">horizontal</property>
+                <property name="spacing">3</property>
+                <property name="valign">center</property>
+                <child>
+                  <object class="GtkButton">
+                    <property name="icon-name">go-previous-symbolic</property>
+                    <property name="action-name">web.go-back</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkButton">
+                    <property name="icon-name">go-next-symbolic</property>
+                    <property name="action-name">web.go-forward</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkStack" id="reload_stack">
+                    <child>
+                      <object class="GtkStackPage">
+                        <property name="name">reload</property>
+                        <property name="child">
+                          <object class="GtkButton">
+                            <property name="icon-name">view-refresh-symbolic</property>
+                            <property name="action-name">web.reload</property>
+                          </object>
+                        </property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkStackPage">
+                        <property name="name">stop</property>
+                        <property name="child">
+                          <object class="GtkButton">
+                            <property name="icon-name">stop-sign-symbolic</property>
+                            <property name="action-name">web.stop</property>
+                          </object>
+                        </property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child type="center">
+              <object class="IdeUrlBar" id="url_bar">
+                <property name="hexpand">true</property>
+                <property name="valign">center</property>
+                <property name="web_view">web_view</property>
+              </object>
+            </child>
+            <child type="end">
+              <object class="GtkMenuButton">
+                <property name="icon-name">open-menu-symbolic</property>
+                <property name="menu-model">primary_menu</property>
+                <property name="direction">up</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="WebKitSettings" id="web_settings"/>
+  <menu id="primary_menu">
+    <section>
+      <attribute name="label" translatable="yes">Settings</attribute>
+      <item>
+        <attribute name="label" translatable="yes">Allow JavaScript</attribute>
+        <attribute name="action">web.enable-javascript</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/libide/webkit/ide-webkit-plugin.c b/src/libide/webkit/ide-webkit-plugin.c
index bbc84b7e0..6f7009c1d 100644
--- a/src/libide/webkit/ide-webkit-plugin.c
+++ b/src/libide/webkit/ide-webkit-plugin.c
@@ -26,16 +26,25 @@
 #include <webkit2/webkit2.h>
 #include <girepository.h>
 
+#include <libide-core.h>
+
+#include "ide-webkit-page.h"
+
 _IDE_EXTERN void _ide_webkit_register_types (PeasObjectModule *module);
 
 void
 _ide_webkit_register_types (PeasObjectModule *module)
 {
   WebKitWebContext *context;
+  g_autoptr(GError) error = NULL;
 
   g_type_ensure (WEBKIT_TYPE_WEB_VIEW);
-  g_irepository_require (NULL, "WebKit2", "4.0", 0, NULL);
+  g_type_ensure (IDE_TYPE_WEBKIT_PAGE);
+
+  if (!g_irepository_require (NULL, "WebKit2", "5.0", 0, &error))
+    g_warning ("%s", error->message);
 
   context = webkit_web_context_get_default ();
   webkit_web_context_set_sandbox_enabled (context, TRUE);
+  webkit_web_context_set_favicon_database_directory (context, NULL);
 }
diff --git a/src/libide/webkit/ide-webkit-util.c b/src/libide/webkit/ide-webkit-util.c
new file mode 100644
index 000000000..8e8b29dbe
--- /dev/null
+++ b/src/libide/webkit/ide-webkit-util.c
@@ -0,0 +1,280 @@
+/* ide-webkit-util.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/* Much of the code within this file is derived from numerous files
+ * within the Epiphany web browser. The original copyright is provided
+ * below.
+ */
+/*
+ *  Copyright © 2000-2003 Marco Pesenti Gritti
+ *  Copyright © 2002 Marco Pesenti Gritti
+ *  Copyright © 2003, 2004, 2005 Christian Persch
+ *  Copyright © 2004 Crispin Flowerday
+ *  Copyright © 2004 Adam Hooper
+ *  Copyright © 2008, 2009 Gustavo Noronha Silva
+ *  Copyright © 2009, 2010, 2014 Igalia S.L.
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany 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.
+ *
+ *  Epiphany 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 Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-webkit-util"
+
+#include "config.h"
+
+#include <libsoup/soup.h>
+
+#include "ide-webkit-util.h"
+
+#define DOMAIN_REGEX "^localhost(\\.[^[:space:]]+)?(:\\d+)?(:[0-9]+)?(/.*)?$|" \
+                     "^[^\\.[:space:]]+\\.[^\\.[:space:]]+.*$|"
+
+static char *
+string_find_and_replace (const char *haystack,
+                         const char *to_find,
+                         const char *to_repl)
+{
+  GString *str;
+
+  g_assert (haystack);
+  g_assert (to_find);
+  g_assert (to_repl);
+
+  str = g_string_new (haystack);
+  g_string_replace (str, to_find, to_repl, 0);
+  return g_string_free (str, FALSE);
+}
+
+static char *
+string_get_host_name (const char *url)
+{
+  g_autoptr(GUri) uri = NULL;
+
+  if (url == NULL ||
+      g_str_has_prefix (url, "file://") ||
+      g_str_has_prefix (url, "about:"))
+    return NULL;
+
+  uri = g_uri_parse (url, G_URI_FLAGS_NONE, NULL);
+  /* If uri is NULL it's very possible that we just got
+   * something without a scheme, let's try to prepend
+   * 'http://' */
+  if (uri == NULL) {
+    char *effective_url = g_strconcat ("http://";, url, NULL);
+    uri = g_uri_parse (effective_url, G_URI_FLAGS_NONE, NULL);
+    g_free (effective_url);
+  }
+
+  if (uri == NULL)
+    return NULL;
+
+  return g_strdup (g_uri_get_host (uri));
+}
+
+static gboolean
+address_has_web_scheme (const char *address)
+{
+  gboolean has_web_scheme;
+  int colonpos;
+
+  if (address == NULL)
+    return FALSE;
+
+  colonpos = (int)((strstr (address, ":")) - address);
+
+  if (colonpos < 0)
+    return FALSE;
+
+  has_web_scheme = !(g_ascii_strncasecmp (address, "http", colonpos) &&
+                     g_ascii_strncasecmp (address, "https", colonpos) &&
+                     g_ascii_strncasecmp (address, "file", colonpos) &&
+                     g_ascii_strncasecmp (address, "javascript", colonpos) &&
+                     g_ascii_strncasecmp (address, "data", colonpos) &&
+                     g_ascii_strncasecmp (address, "blob", colonpos) &&
+                     g_ascii_strncasecmp (address, "about", colonpos) &&
+                     g_ascii_strncasecmp (address, "gopher", colonpos) &&
+                     g_ascii_strncasecmp (address, "inspector", colonpos) &&
+                     g_ascii_strncasecmp (address, "webkit", colonpos));
+
+  return has_web_scheme;
+}
+
+static gboolean
+address_is_existing_absolute_filename (const char *address)
+{
+  g_autofree char *real_address = NULL;
+
+  if (strchr (address, '#') == NULL) {
+    real_address = g_strdup (address);
+  } else {
+    gint pos;
+
+    pos = g_strstr_len (address, -1, "#") - address;
+    real_address = g_strndup (address, pos);
+  }
+
+  return g_path_is_absolute (real_address) &&
+         g_file_test (real_address, G_FILE_TEST_EXISTS);
+}
+
+static gboolean
+is_host_with_port (const char *address)
+{
+  g_auto (GStrv) split = NULL;
+  gint64 port = 0;
+
+  if (strchr (address, ' '))
+    return FALSE;
+
+  split = g_strsplit (address, ":", -1);
+  if (g_strv_length (split) == 2)
+    port = g_ascii_strtoll (split[1], NULL, 10);
+
+  return port != 0;
+}
+
+static char *
+ensure_host_name_is_lowercase (const char *address)
+{
+  g_autofree gchar *host = string_get_host_name (address);
+  g_autofree gchar *lowercase_host = NULL;
+
+  if (host == NULL)
+    return g_strdup (address);
+
+  lowercase_host = g_utf8_strdown (host, -1);
+
+  if (strcmp (host, lowercase_host) != 0)
+    return string_find_and_replace (address, host, lowercase_host);
+  else
+    return g_strdup (address);
+}
+
+
+/* Does various normalization rules to make sure @input_address ends up
+ * with a URI scheme (e.g. absolute filenames or "localhost"), changes
+ * the URI scheme to something more appropriate when needed and lowercases
+ * the hostname.
+ */
+char *
+ide_webkit_util_normalize_address (const char *input_address)
+{
+  char *effective_address = NULL;
+  g_autofree gchar *address = NULL;
+
+  g_return_val_if_fail (input_address != NULL, NULL);
+
+  address = ensure_host_name_is_lowercase (input_address);
+
+  if (address_is_existing_absolute_filename (address))
+    return g_strconcat ("file://", address, NULL);
+
+  if (strcmp (address, "about:gpu") == 0)
+    return g_strdup ("webkit://gpu");
+
+  if (!address_has_web_scheme (address)) {
+    const char *scheme;
+
+    scheme = g_uri_peek_scheme (address);
+
+    /* Auto-prepend http:// to anything that is not
+     * one according to GLib, because it probably will be
+     * something like "google.com". Special case localhost(:port)
+     * and IP(:port), because GUri, correctly, thinks it is a
+     * URI with scheme being localhost/IP and, optionally, path
+     * being the port. Ideally we should check if we have a
+     * handler for the scheme, and since we'll fail for localhost
+     * and IP, we'd fallback to loading it as a domain. */
+    if (!scheme ||
+        !g_strcmp0 (scheme, "localhost") ||
+        g_hostname_is_ip_address (scheme) ||
+        is_host_with_port (address))
+      effective_address = g_strconcat ("http://";, address, NULL);
+  }
+
+  return effective_address ? effective_address : g_strdup (address);
+}
+
+static char *
+hostname_to_tld (const char *hostname)
+{
+  g_auto(GStrv) parts = NULL;
+  guint length;
+
+  parts = g_strsplit (hostname, ".", 0);
+  length = g_strv_length (parts);
+
+  if (length >= 1)
+    return g_strdup (parts[length - 1]);
+
+  return g_strdup ("");
+}
+
+IdeWebkitSecurityLevel
+ide_webkit_util_get_security_level (WebKitWebView *web_view)
+{
+  IdeWebkitSecurityLevel security_level;
+  GTlsCertificateFlags tls_errors = 0;
+  WebKitSecurityManager *security_manager;
+  WebKitWebContext *web_context;
+  GTlsCertificate *certificate = NULL;
+  g_autoptr(GUri) guri = NULL;
+  g_autofree char *tld = NULL;
+  const char *uri;
+
+  g_return_val_if_fail (WEBKIT_IS_WEB_VIEW (web_view), 0);
+
+  uri = webkit_web_view_get_uri (web_view);
+  web_context = webkit_web_view_get_context (web_view);
+  security_manager = webkit_web_context_get_security_manager (web_context);
+  guri = g_uri_parse (uri, G_URI_FLAGS_NONE, NULL);
+
+  if (guri && g_uri_get_host (guri))
+    tld = hostname_to_tld (g_uri_get_host (guri));
+
+  if (!guri ||
+      g_strcmp0 (tld, "127.0.0.1") == 0 ||
+      g_strcmp0 (tld, "::1") == 0 ||
+      g_strcmp0 (tld, "localhost") == 0 || /* We trust localhost to be local since glib!616. */
+      webkit_security_manager_uri_scheme_is_local (security_manager, g_uri_get_scheme (guri)) ||
+      webkit_security_manager_uri_scheme_is_empty_document (security_manager, g_uri_get_scheme (guri)))
+    security_level = IDE_WEBKIT_SECURITY_LEVEL_LOCAL_PAGE;
+  else if (webkit_web_view_get_tls_info (web_view, &certificate, &tls_errors))
+    security_level = tls_errors == 0 ?
+                     IDE_WEBKIT_SECURITY_LEVEL_STRONG_SECURITY : 
IDE_WEBKIT_SECURITY_LEVEL_UNACCEPTABLE_CERTIFICATE;
+  else if (webkit_web_view_is_loading (web_view))
+    security_level = IDE_WEBKIT_SECURITY_LEVEL_TO_BE_DETERMINED;
+  else
+    security_level = IDE_WEBKIT_SECURITY_LEVEL_NONE;
+
+  return security_level;
+}
diff --git a/src/libide/webkit/ide-webkit-util.h b/src/libide/webkit/ide-webkit-util.h
new file mode 100644
index 000000000..859113f00
--- /dev/null
+++ b/src/libide/webkit/ide-webkit-util.h
@@ -0,0 +1,39 @@
+/* ide-webkit-util.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <webkit2/webkit2.h>
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  IDE_WEBKIT_SECURITY_LEVEL_NONE,
+  IDE_WEBKIT_SECURITY_LEVEL_LOCAL_PAGE,
+  IDE_WEBKIT_SECURITY_LEVEL_STRONG_SECURITY,
+  IDE_WEBKIT_SECURITY_LEVEL_UNACCEPTABLE_CERTIFICATE,
+  IDE_WEBKIT_SECURITY_LEVEL_TO_BE_DETERMINED,
+} IdeWebkitSecurityLevel;
+
+char                   *ide_webkit_util_normalize_address  (const char    *input_address);
+IdeWebkitSecurityLevel  ide_webkit_util_get_security_level (WebKitWebView *web_view);
+
+G_END_DECLS
diff --git a/src/libide/webkit/libide-webkit.gresource.xml b/src/libide/webkit/libide-webkit.gresource.xml
index f1621dbaa..b665760ee 100644
--- a/src/libide/webkit/libide-webkit.gresource.xml
+++ b/src/libide/webkit/libide-webkit.gresource.xml
@@ -2,5 +2,7 @@
 <gresources>
   <gresource prefix="/plugins/webkit">
     <file>webkit.plugin</file>
+    <file preprocess="xml-stripblanks">ide-webkit-page.ui</file>
+    <file preprocess="xml-stripblanks">ide-url-bar.ui</file>
   </gresource>
 </gresources>
diff --git a/src/libide/webkit/libide-webkit.h b/src/libide/webkit/libide-webkit.h
new file mode 100644
index 000000000..9bde8d723
--- /dev/null
+++ b/src/libide/webkit/libide-webkit.h
@@ -0,0 +1,27 @@
+/* libide-webkit.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-gui.h>
+
+#define IDE_WEBKIT_INSIDE
+# include "ide-webkit-page.h"
+#undef IDE_WEBKIT_INSIDE
diff --git a/src/libide/webkit/meson.build b/src/libide/webkit/meson.build
index fce477b36..43011c64a 100644
--- a/src/libide/webkit/meson.build
+++ b/src/libide/webkit/meson.build
@@ -1,10 +1,35 @@
+libwebkit_dep = dependency('webkit2gtk-5.0', required: false)
+
+if libwebkit_dep.found()
+
+libide_webkit_header_dir = join_paths(libide_header_dir, 'webkit')
+libide_webkit_header_subdir = join_paths(libide_header_subdir, 'webkit')
+libide_include_directories += include_directories('.')
 
 #
 # Sources
 #
 
-libide_webkit_sources = [
+libide_webkit_private_sources = [
   'ide-webkit-plugin.c',
+  'ide-webkit-util.c',
+  'ide-text-buffer-html-generator.c',
+  'ide-url-bar.c',
+]
+
+#
+# Public API Headers
+#
+
+libide_webkit_public_headers = [
+  'libide-webkit.h',
+  'ide-html-generator.h',
+  'ide-webkit-page.h',
+]
+
+libide_webkit_public_sources = [
+  'ide-html-generator.c',
+  'ide-webkit-page.c',
 ]
 
 #
@@ -17,7 +42,8 @@ libide_webkit_resources = gnome.compile_resources(
   c_name: 'ide_webkit',
 )
 libide_webkit_generated_headers = [libide_webkit_resources[1]]
-libide_webkit_sources += libide_webkit_resources
+
+libide_webkit_sources = libide_webkit_resources + libide_webkit_public_sources + 
libide_webkit_private_sources
 
 #
 # Dependencies
@@ -26,6 +52,7 @@ libide_webkit_sources += libide_webkit_resources
 libide_webkit_deps = [
   libwebkit_dep,
   libpeas_dep,
+  libide_gui_dep,
 ]
 
 #
@@ -43,3 +70,13 @@ libide_webkit_dep = declare_dependency(
   include_directories: include_directories('.'),
               sources: libide_webkit_generated_headers,
 )
+
+gnome_builder_private_sources += files(libide_webkit_private_sources)
+gnome_builder_public_sources += files(libide_webkit_public_sources)
+gnome_builder_public_headers += files(libide_webkit_public_headers)
+gnome_builder_include_subdirs += libide_webkit_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-webkit.h', '-DIDE_WEBKIT_COMPILATION']
+
+install_headers(libide_webkit_public_headers, subdir: libide_webkit_header_subdir)
+
+endif


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]