summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordefanor <defanor@uberspace.net>2019-08-17 23:22:42 +0300
committerdefanor <defanor@uberspace.net>2019-08-17 23:22:42 +0300
commit5772d91183ad4f3a8dc1d5c469bc7d295764b80c (patch)
tree1ff7d28c9f9f7e662562cf004aa2aabdc22c77f8 /src
Add the prototype
Diffstat (limited to 'src')
-rw-r--r--src/Makefile.am10
-rw-r--r--src/blockbox.c50
-rw-r--r--src/blockbox.h50
-rw-r--r--src/browserbox.c1433
-rw-r--r--src/browserbox.h133
-rw-r--r--src/documentbox.c489
-rw-r--r--src/documentbox.h70
-rw-r--r--src/inlinebox.c585
-rw-r--r--src/inlinebox.h119
-rw-r--r--src/main.c134
-rw-r--r--src/tablebox.c407
-rw-r--r--src/tablebox.h84
12 files changed, 3564 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..74c18e7
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,10 @@
+AM_CFLAGS = -Werror -Wall -Wextra -Wno-unused-parameter
+# todo: add -pedantic later, allowing the draft to be relatively messy
+# for now
+
+bin_PROGRAMS = wwwlite
+
+wwwlite_SOURCES = main.c inlinebox.c documentbox.c blockbox.c tablebox.c browserbox.c
+noinst_HEADERS = inlinebox.h documentbox.h blockbox.h tablebox.h browserbox.h
+wwwlite_CFLAGS = $(LIBSOUP_CFLAGS) $(LIBXML_CFLAGS) $(GTK3_CFLAGS) $(AM_CFLAGS)
+wwwlite_LDADD = $(LIBSOUP_LIBS) $(LIBXML_LIBS) $(GTK3_LIBS)
diff --git a/src/blockbox.c b/src/blockbox.c
new file mode 100644
index 0000000..1b3b462
--- /dev/null
+++ b/src/blockbox.c
@@ -0,0 +1,50 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ 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 <https://www.gnu.org/licenses/>.
+*/
+
+#include <gtk/gtk.h>
+#include "blockbox.h"
+#include "inlinebox.h"
+
+G_DEFINE_TYPE (BlockBox, block_box, GTK_TYPE_BOX);
+
+static GtkSizeRequestMode block_box_get_request_mode (GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+block_box_class_init (BlockBoxClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+ widget_class->get_request_mode = block_box_get_request_mode;
+ return;
+}
+
+static void
+block_box_init (BlockBox *bb)
+{
+ return;
+}
+
+GtkWidget *block_box_new (guint spacing)
+{
+ BlockBox *bb = BLOCK_BOX(g_object_new(block_box_get_type(),
+ "orientation", GTK_ORIENTATION_VERTICAL,
+ "spacing", spacing,
+ NULL));
+ return GTK_WIDGET(bb);
+}
diff --git a/src/blockbox.h b/src/blockbox.h
new file mode 100644
index 0000000..6d581ed
--- /dev/null
+++ b/src/blockbox.h
@@ -0,0 +1,50 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ 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 <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef BLOCK_BOX_H
+#define BLOCK_BOX_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define BLOCK_BOX_TYPE (block_box_get_type())
+#define BLOCK_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), BLOCK_BOX_TYPE, BlockBox))
+#define BLOCK_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), BLOCK_BOX_TYPE, BlockBoxClass))
+#define IS_BLOCK_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), BLOCK_BOX_TYPE))
+#define IS_BLOCK_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), BLOCK_BOX_TYPE))
+
+typedef struct _BlockBox BlockBox;
+typedef struct _BlockBoxClass BlockBoxClass;
+
+struct _BlockBox
+{
+ GtkBox parent_instance;
+};
+
+struct _BlockBoxClass
+{
+ GtkBoxClass parent_class;
+};
+
+GType block_box_get_type(void) G_GNUC_CONST;
+GtkWidget *block_box_new(guint spacing);
+
+
+G_END_DECLS
+
+#endif
diff --git a/src/browserbox.c b/src/browserbox.c
new file mode 100644
index 0000000..fe69cde
--- /dev/null
+++ b/src/browserbox.c
@@ -0,0 +1,1433 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ 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 <https://www.gnu.org/licenses/>.
+*/
+
+/* The code in this file is particularly messy, and some of it should
+ be reorganised. */
+
+#include <glib.h>
+#include <gtk/gtk.h>
+#include "browserbox.h"
+#include "inlinebox.h"
+#include "blockbox.h"
+#include "tablebox.h"
+#include "documentbox.h"
+#include <libxml/HTMLparser.h>
+#include <libsoup/soup.h>
+
+
+typedef struct _ImageSetData ImageSetData;
+struct _ImageSetData
+{
+ GtkImage *image;
+ BuilderState *bs;
+};
+
+
+
+G_DEFINE_TYPE (BuilderState, builder_state, G_TYPE_OBJECT);
+G_DEFINE_TYPE (BrowserBox, browser_box, BLOCK_BOX_TYPE);
+
+/* todo: move some of the properties into BrowserBox (or DocumentBox),
+ particularly the ones that are used after rendering. */
+static void builder_state_init (BuilderState *bs)
+{
+ bs->active = TRUE;
+ bs->vbox = NULL;
+ bs->docbox = NULL;
+ bs->root = NULL;
+ bs->stack = g_slist_alloc();
+ bs->stack->data = NULL;
+ bs->text_position = 0;
+ bs->current_attrs = pango_attr_list_new();
+ bs->current_link = NULL;
+ bs->current_word = NULL;
+ bs->ignore_text = FALSE;
+ bs->prev_space = TRUE;
+ bs->pre = FALSE;
+ bs->parser = NULL;
+ bs->uri = NULL;
+ bs->queued_identifiers = NULL;
+ bs->identifiers =
+ g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+ bs->anchor_handler_id = 0;
+ bs->option_value = NULL;
+ bs->ol_numbers = NULL;
+ bs->current_form = NULL;
+}
+
+BuilderState *builder_state_new (GtkWidget *root)
+{
+ BuilderState *bs = g_object_new (BUILDER_STATE_TYPE, NULL);
+ bs->root = root;
+ GtkStyleContext *styleCtx = gtk_widget_get_style_context(root);
+ gtk_style_context_get_color(styleCtx, GTK_STATE_FLAG_LINK, &bs->link_color);
+ return bs;
+}
+
+void builder_state_dispose (GObject *self)
+{
+ BuilderState *bs = BUILDER_STATE(self);
+ if (bs->parser) {
+ htmlFreeParserCtxt(bs->parser);
+ bs->parser = NULL;
+ }
+ if (bs->stack) {
+ g_slist_free(bs->stack);
+ bs->stack = NULL;
+ }
+ if (bs->current_attrs) {
+ pango_attr_list_unref(bs->current_attrs);
+ bs->current_attrs = NULL;
+ }
+ if (bs->identifiers) {
+ g_hash_table_unref(bs->identifiers);
+ bs->identifiers = NULL;
+ }
+ if (bs->queued_identifiers) {
+ g_slist_free(bs->queued_identifiers);
+ bs->queued_identifiers = NULL;
+ }
+ if (bs->option_value) {
+ free(bs->option_value);
+ bs->option_value = NULL;
+ }
+ if (bs->ol_numbers) {
+ g_slist_free_full(bs->ol_numbers, g_free);
+ bs->ol_numbers = NULL;
+ }
+ G_OBJECT_CLASS (builder_state_parent_class)->dispose (self);
+}
+
+static void builder_state_class_init (BuilderStateClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = builder_state_dispose;
+}
+
+
+void scroll_to_identifier(BuilderState *bs, const char *identifier)
+{
+ GtkWidget *target = g_hash_table_lookup(bs->identifiers, identifier);
+ if (target) {
+ GtkAllocation widget_alloc, *alloc;
+ if (GTK_IS_WIDGET(target)) {
+ gtk_widget_get_allocation(target, &widget_alloc);
+ alloc = &widget_alloc;
+ } else if (IS_IB_TEXT(target)) {
+ alloc = &IB_TEXT(target)->alloc;
+ } else {
+ puts("Shouldn't happen");
+ return;
+ }
+ GtkAdjustment *adj =
+ gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(bs->docbox));
+ gtk_adjustment_set_value(adj, alloc->y);
+ gtk_scrolled_window_set_vadjustment(GTK_SCROLLED_WINDOW(bs->docbox),
+ adj);
+ }
+}
+
+void image_set (SoupSession *session, SoupMessage *msg, ImageSetData *isd)
+{
+ /* Just setting a whole image at once for now, progressive loading
+ is left for later. */
+ if (isd->bs->active && msg->response_body->data != NULL) {
+ GdkPixbufLoader *il = gdk_pixbuf_loader_new();
+ GError *err = NULL;
+ gdk_pixbuf_loader_write(il, (unsigned char *)msg->response_body->data,
+ msg->response_body->length, &err);
+ gdk_pixbuf_loader_close(il, &err);
+ GdkPixbuf *pb = gdk_pixbuf_loader_get_pixbuf(il);
+ if (pb != NULL) {
+ /* Temporarily scaling large images on loading: it's imprecise
+ and generally awkward, but better than embedding huge images.
+ Better to resize on window resize and along size allocation
+ in the future, but GTK is unhappy if it's done during size
+ allocation, and without storing the original image, it also
+ leads to poor quality (i.e., perhaps will need a custom
+ GtkImage subtype). */
+ int doc_width = gtk_widget_get_allocated_width(GTK_WIDGET(isd->bs->root));
+ int pb_width = gdk_pixbuf_get_width(pb);
+ int pb_height = gdk_pixbuf_get_height(pb);
+ if (pb_width > doc_width) {
+ GdkPixbuf *old_pb = pb;
+ int new_height = (double)pb_height * (double)doc_width / (double)pb_width;
+ if (new_height < 1) {
+ new_height = 1;
+ }
+ pb = gdk_pixbuf_scale_simple(old_pb, doc_width, new_height,
+ GDK_INTERP_BILINEAR);
+ }
+ if (pb != NULL) {
+ gtk_image_set_from_pixbuf(isd->image, pb);
+ if (pb_width > doc_width) {
+ g_object_unref(pb);
+ }
+ }
+ }
+ g_object_unref(il);
+ }
+ free(isd);
+ g_object_unref(isd->bs);
+}
+
+
+/* Word cache utilities */
+
+guint pango_attr_hash (PangoAttribute *attr)
+{
+ /* todo: that's not a great hash, maybe improve later */
+ return attr->klass->type ^ attr->start_index ^ attr->end_index;
+}
+
+guint wck_hash (WordCacheKey *wck)
+{
+ guint attr_hash = 0;
+ PangoAttrIterator *pai = pango_attr_list_get_iterator(wck->attrs);
+ GSList *attrs = pango_attr_iterator_get_attrs(pai);
+ GSList *ai;
+ for (ai = attrs; ai; ai = ai->next) {
+ attr_hash ^= pango_attr_hash(ai->data);
+ }
+ g_slist_free_full(attrs, (GDestroyNotify)pango_attribute_destroy);
+ pango_attr_iterator_destroy(pai);
+ guint text_hash = g_str_hash(wck->text);
+ return attr_hash ^ text_hash;
+}
+
+gboolean wck_equal (WordCacheKey *wck1, WordCacheKey *wck2)
+{
+ PangoAttrIterator *pai1 = pango_attr_list_get_iterator(wck1->attrs);
+ PangoAttrIterator *pai2 = pango_attr_list_get_iterator(wck2->attrs);
+ GSList *attrs1 = pango_attr_iterator_get_attrs(pai1);
+ GSList *attrs2 = pango_attr_iterator_get_attrs(pai2);
+ GSList *ai1, *ai2;
+ for (ai1 = attrs1, ai2 = attrs2; ai1 || ai2; ai1 = ai1->next, ai2 = ai2->next) {
+ if (( ! (ai1 && ai2)) || ( ! pango_attribute_equal(ai1->data, ai2->data))) {
+ g_slist_free_full(attrs1, (GDestroyNotify)pango_attribute_destroy);
+ g_slist_free_full(attrs2, (GDestroyNotify)pango_attribute_destroy);
+ pango_attr_iterator_destroy(pai1);
+ pango_attr_iterator_destroy(pai2);
+ return FALSE;
+ }
+ }
+ g_slist_free_full(attrs1, (GDestroyNotify)pango_attribute_destroy);
+ g_slist_free_full(attrs2, (GDestroyNotify)pango_attribute_destroy);
+ pango_attr_iterator_destroy(pai1);
+ pango_attr_iterator_destroy(pai2);
+ return g_str_equal(wck1->text, wck2->text);
+}
+
+
+
+
+PangoLayout *get_layout(GtkWidget *widget, const gchar *text,
+ PangoAttrList *attrs)
+{
+ WordCacheKey *wck = malloc(sizeof(WordCacheKey));
+ wck->text = strdup(text);
+ wck->attrs = attrs;
+ pango_attr_list_ref(wck->attrs);
+ PangoLayout *pl = g_hash_table_lookup(word_cache, wck);
+ if (pl == NULL) {
+ pl = gtk_widget_create_pango_layout(widget, text);
+ pango_layout_set_attributes(pl, attrs);
+ g_hash_table_insert(word_cache, wck, pl);
+ } else {
+ free(wck->text);
+ pango_attr_list_unref(wck->attrs);
+ free(wck);
+ }
+ return pl;
+}
+
+PangoAttrList *shift_attributes(PangoAttrList *src_attrs, guint len)
+{
+ PangoAttrIterator *pai;
+ PangoAttrList *new_attrs;
+ PangoAttribute *attr;
+ GSList *iter_al, *al;
+ new_attrs = pango_attr_list_new();
+ pai = pango_attr_list_get_iterator(src_attrs);
+ if (pai != NULL) {
+ do {
+ iter_al = pango_attr_iterator_get_attrs(pai);
+ for (al = iter_al; al; al = al->next) {
+ attr = al->data;
+ if (attr->end_index > len || attr->end_index == G_MAXUINT) {
+ attr->start_index = 0;
+ if (attr->end_index != G_MAXUINT) {
+ attr->end_index -= len;
+ }
+ pango_attr_list_insert(new_attrs, attr);
+ } else {
+ pango_attribute_destroy(attr);
+ }
+ }
+ g_slist_free(iter_al);
+ } while (pango_attr_iterator_next(pai));
+ pango_attr_iterator_destroy(pai);
+ }
+ pango_attr_list_unref(src_attrs);
+ return new_attrs;
+}
+
+void attribute_start(PangoAttrList *attrs, PangoAttribute *attr, guint position)
+{
+ attr->start_index = position;
+ pango_attr_list_insert(attrs, attr);
+}
+
+/* todo: better tracking of attributes is needed; this would end all
+ the matching attributes instead of just a particular one */
+PangoAttrList *attribute_end(PangoAttrList *attrs,
+ PangoAttrType type, guint position)
+{
+ PangoAttrIterator *pai;
+ PangoAttrList *new_attrs;
+ PangoAttribute *attr;
+ GSList *iter_al, *al;
+ new_attrs = pango_attr_list_new();
+ pai = pango_attr_list_get_iterator(attrs);
+ if (pai != NULL) {
+ do {
+ iter_al = pango_attr_iterator_get_attrs(pai);
+ for (al = iter_al; al; al = al->next) {
+ attr = al->data;
+ if (attr->klass->type == type && attr->end_index > position) {
+ attr->end_index = position;
+ }
+ pango_attr_list_change(new_attrs, attr);
+ }
+ g_slist_free(iter_al);
+ } while (pango_attr_iterator_next(pai));
+ pango_attr_iterator_destroy(pai);
+ }
+ pango_attr_list_unref(attrs);
+ return new_attrs;
+}
+
+void ensure_inline_box (BuilderState *bs)
+{
+ if (! IS_INLINE_BOX(bs->stack->data)) {
+ if (GTK_IS_CONTAINER(bs->stack->data)) {
+ InlineBox *ib = inline_box_new();
+ bs->text_position = 0;
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), GTK_WIDGET (ib));
+ gtk_widget_show_all (GTK_WIDGET(ib));
+ bs->stack = g_slist_prepend(bs->stack, ib);
+ } else {
+ puts("neither a text nor a container");
+ return;
+ }
+ }
+}
+
+void anchor_allocated (GtkWidget *widget,
+ GdkRectangle *alloc,
+ BuilderState *bs)
+{
+ GtkAdjustment *adj =
+ gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(bs->docbox));
+ gtk_adjustment_set_value(adj, alloc->y);
+ gtk_scrolled_window_set_vadjustment(GTK_SCROLLED_WINDOW(bs->docbox),
+ adj);
+ g_signal_handler_disconnect(widget, bs->anchor_handler_id);
+ bs->anchor_handler_id = 0;
+}
+
+IBText *add_word(BuilderState *bs, gchar *word, PangoAttrList **attrs)
+{
+ ensure_inline_box(bs);
+ InlineBox *ib = bs->stack->data;
+ IBText *ibt = NULL;
+ if (word[0] != 0) {
+ PangoLayout *pl = get_layout(GTK_WIDGET(ib), word, *attrs);
+ ibt = ib_text_new(pl);
+ inline_box_add_text(ib, ibt);
+ *attrs = shift_attributes(*attrs, strlen(word));
+ if (bs->queued_identifiers) {
+ GSList *ii;
+ for (ii = bs->queued_identifiers; ii; ii = ii->next) {
+ const char *fragment = soup_uri_get_fragment(bs->uri);
+ if (fragment && bs->anchor_handler_id == 0 &&
+ strcmp(ii->data, fragment) == 0) {
+ bs->anchor_handler_id =
+ g_signal_connect (ib, "size-allocate",
+ G_CALLBACK(anchor_allocated), bs);
+ }
+ g_hash_table_insert(bs->identifiers, ii->data, ibt);
+ }
+ g_slist_free(bs->queued_identifiers);
+ bs->queued_identifiers = NULL;
+ }
+ }
+ return ibt;
+}
+
+
+
+
+void history_add (BrowserBox *bb, SoupURI *uri)
+{
+ if (bb->history_position && soup_uri_equal(uri, bb->history_position->data)) {
+ return;
+ }
+ if (bb->history_position != NULL && bb->history_position->next != NULL) {
+ GList *tail = bb->history_position->next;
+ bb->history_position->next = NULL;
+ tail->prev = NULL;
+ g_list_free(tail);
+ }
+ bb->history = g_list_append(bb->history, soup_uri_copy(uri));
+ bb->history_position = g_list_last(bb->history);
+}
+
+gboolean history_back (BrowserBox *bb)
+{
+ if (bb->history_position != NULL && bb->history_position->prev) {
+ bb->history_position = bb->history_position->prev;
+ document_request(bb, soup_uri_copy(bb->history_position->data));
+ return TRUE;
+ }
+ return FALSE;
+}
+
+gboolean history_forward (BrowserBox *bb)
+{
+ if (bb->history_position != NULL && bb->history_position->next) {
+ bb->history_position = bb->history_position->next;
+ document_request(bb, soup_uri_copy(bb->history_position->data));
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void form_submit (GtkButton *button, gpointer ptr)
+{
+ Form *form = ptr;
+ BrowserBox *bb = BROWSER_BOX(form->submission_data);
+ gchar *method = "GET";
+ puts("submitting");
+ if (form->method != NULL) {
+ if (g_ascii_strncasecmp(form->method, "post", 4) == 0) {
+ method = "POST";
+ }
+ }
+ gchar *uri_str = soup_uri_to_string(form->action, FALSE);
+
+ if (form->enctype == ENCTYPE_URLENCODED) {
+ GHashTable *fields = g_hash_table_new(g_str_hash, g_str_equal);
+ GList *fi;
+ for (fi = form->fields; fi; fi = fi->next) {
+ FormField *ff = fi->data;
+ if (GTK_IS_ENTRY(ff->widget)) {
+ g_hash_table_insert(fields, ff->name,
+ (gpointer)gtk_entry_get_text(GTK_ENTRY(ff->widget)));
+ } else if (GTK_IS_COMBO_BOX(ff->widget)) {
+ if (gtk_combo_box_get_active_id(GTK_COMBO_BOX(ff->widget)) != NULL) {
+ g_hash_table_insert(fields, ff->name,
+ (gpointer)gtk_combo_box_get_active_id(GTK_COMBO_BOX(ff->widget)));
+ }
+ }
+ }
+ SoupMessage *sm = soup_form_request_new_from_hash(method, uri_str, fields);
+ g_hash_table_unref(fields);
+ history_add(bb, soup_message_get_uri(sm));
+ document_request_sm(bb, sm);
+ } else if (form->enctype == ENCTYPE_MULTIPART) {
+ puts("multipart, not supported yet");
+ } else if (form->enctype == ENCTYPE_PLAIN) {
+ puts("plain, not supported yet");
+ }
+ g_free(uri_str);
+}
+
+
+
+void sax_characters (BrowserBox *bb, const xmlChar * ch, int len)
+{
+ BuilderState *bs = bb->builder_state;
+ if (bs->ignore_text || IS_TABLE_BOX(bs->stack->data)) {
+ return;
+ }
+
+ char *value = malloc(len + 1);
+ g_strlcpy(value, (const char*)ch, len + 1);
+
+ if (GTK_IS_COMBO_BOX_TEXT(bs->stack->data)) {
+ if (bs->option_value) {
+ gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(bs->stack->data),
+ bs->option_value, value);
+ free(bs->option_value);
+ bs->option_value = NULL;
+ }
+ free(value);
+ return;
+ }
+
+ ensure_inline_box(bs);
+
+ gint i = 0, j = 0;
+ while (i < len) {
+ if (value[i] == ' ' || value[i] == '\n' ||
+ value[i] == '\r' || value[i] == '\t') {
+ gchar c = value[i];
+ value[i] = 0;
+ if (bs->current_word != NULL) {
+ bs->current_word =
+ realloc(bs->current_word,
+ strlen(bs->current_word) + strlen(value + j) + 1);
+ g_strlcpy(bs->current_word + strlen(bs->current_word),
+ value + j, strlen(value + j) + 1);
+ add_word(bs, bs->current_word, &bs->current_attrs);
+ free(bs->current_word);
+ bs->current_word = NULL;
+ } else {
+ add_word(bs, value + j, &bs->current_attrs);
+ }
+ bs->text_position += strlen(value + j);
+ if (bs->pre && c == '\n') {
+ inline_box_break(INLINE_BOX(bs->stack->data));
+ } else {
+ if (bs->pre || ! bs->prev_space) {
+ add_word(bs, " ", &bs->current_attrs);
+ bs->text_position += 1;
+ bs->prev_space = TRUE;
+ }
+ }
+ j = i + 1;
+ } else {
+ bs->prev_space = FALSE;
+ }
+ i++;
+ }
+ if (i > j) {
+ if (bs->current_word == NULL) {
+ bs->current_word = strdup(value + j);
+ } else {
+ bs->current_word =
+ realloc(bs->current_word,
+ strlen(bs->current_word) + strlen(value + j) + 1);
+ g_strlcpy(bs->current_word + strlen(bs->current_word),
+ value + j, strlen(value + j) + 1);
+ }
+ bs->text_position += strlen(value + j);
+ }
+ free(value);
+}
+
+gboolean element_is_blocking (const char *name)
+{
+ /* Not including <div> elements: the results of their inclusion
+ aren't always good, and according to the specification they have
+ no special meaning at all. */
+ return (strcmp(name, "p") == 0 ||
+ strcmp(name, "h1") == 0 || strcmp(name, "h2") == 0 ||
+ strcmp(name, "h3") == 0 || strcmp(name, "h4") == 0 ||
+ strcmp(name, "h5") == 0 || strcmp(name, "h6") == 0 ||
+ strcmp(name, "pre") == 0 || strcmp(name, "ul") == 0 ||
+ strcmp(name, "ol") == 0 || strcmp(name, "li") == 0 ||
+ strcmp(name, "dl") == 0 || strcmp(name, "dt") == 0 ||
+ strcmp(name, "dd") == 0 || strcmp(name, "table") == 0 ||
+ strcmp(name, "td") == 0 || strcmp(name, "th") == 0 ||
+ strcmp(name, "tr") == 0
+ );
+}
+
+gboolean element_flushes_text (const char *name)
+{
+ return (element_is_blocking (name) ||
+ (strcmp(name, "br") == 0 || strcmp(name, "img") == 0 ||
+ strcmp(name, "input") == 0 || strcmp(name, "select") == 0
+ ));
+}
+
+void sax_start_element (BrowserBox *bb,
+ const xmlChar * u_name,
+ const xmlChar ** attrs)
+{
+ BuilderState *bs = bb->builder_state;
+ const char *name = (const char*)u_name;
+
+ if (IS_INLINE_BOX(bs->stack->data)) {
+ if (element_flushes_text(name)) {
+ if (bs->current_word != NULL) {
+ add_word(bs, bs->current_word, &bs->current_attrs);
+ free(bs->current_word);
+ bs->current_word = NULL;
+ }
+ bs->prev_space = TRUE;
+ }
+ if (element_is_blocking(name)) {
+ GSList *next = bs->stack->next;
+ if (next != NULL) {
+ g_slist_free_1(bs->stack);
+ bs->stack = next;
+ }
+ }
+ /* Line breaks */
+ if (strcmp(name, "br") == 0) {
+ inline_box_break(INLINE_BOX(bs->stack->data));
+ }
+ }
+
+ if (IS_BLOCK_BOX(bs->stack->data)) {
+ /* Elements that (may) need inline boxes */
+ if (strcmp(name, "a") == 0 ||
+ strcmp(name, "br") == 0 ||
+ strcmp(name, "img") == 0 ||
+ strcmp(name, "select") == 0 ||
+ strcmp(name, "input") == 0) {
+ ensure_inline_box(bs);
+ }
+ }
+
+ if (IS_BLOCK_BOX(bs->stack->data)) {
+ /* Lists */
+ if (strcmp(name, "dl") == 0 || strcmp(name, "ul") == 0 ||
+ strcmp(name, "ol") == 0) {
+ /* todo: maybe use a dedicated widget for ul and ol */
+ GtkWidget *dl = block_box_new(0);
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), GTK_WIDGET (dl));
+ gtk_widget_show_all(dl);
+ bs->stack = g_slist_prepend(bs->stack, dl);
+ if ((strcmp(name, "ol") == 0) || (strcmp(name, "ul") == 0)) {
+ guint *num = malloc(sizeof(guint));
+ if (strcmp(name, "ol") == 0) {
+ *num = 1;
+ } else if (strcmp(name, "ul") == 0) {
+ *num = 0;
+ }
+ bs->ol_numbers = g_slist_prepend(bs->ol_numbers, num);
+ }
+ }
+
+ if (strcmp(name, "dd") == 0) {
+ GtkWidget *dd = block_box_new(10);
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), GTK_WIDGET (dd));
+ gtk_widget_show_all(dd);
+ bs->stack = g_slist_prepend(bs->stack, dd);
+ gtk_widget_set_margin_start(bs->stack->data, 32);
+ }
+
+ if (bs->ol_numbers) {
+ if (strcmp(name, "li") == 0) {
+ GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
+ gchar *str;
+ guint num = *((guint*)bs->ol_numbers->data);
+ if (num == 0) {
+ str = "*";
+ } else {
+ str = g_strdup_printf("%u.", num);
+ *((guint*)bs->ol_numbers->data) = num + 1;
+ }
+ GtkWidget *lbl = gtk_label_new(str);
+ if (num > 0) {
+ g_free(str);
+ }
+ GtkWidget *vbox = block_box_new(10);
+ gtk_widget_set_valign(lbl, GTK_ALIGN_START);
+ gtk_container_add (GTK_CONTAINER (hbox), GTK_WIDGET (lbl));
+ gtk_container_add (GTK_CONTAINER (hbox), GTK_WIDGET (vbox));
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), hbox);
+ gtk_widget_show_all(hbox);
+ bs->stack = g_slist_prepend(bs->stack, hbox);
+ bs->stack = g_slist_prepend(bs->stack, vbox);
+ }
+ }
+
+ /* Tables */
+ if (strcmp(name, "table") == 0) {
+ GtkWidget *tb = table_box_new();
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), tb);
+ gtk_widget_show_all(tb);
+ bs->stack = g_slist_prepend(bs->stack, tb);
+ }
+
+ /* Preformatted texts */
+ if (strcmp(name, "pre") == 0 && IS_BLOCK_BOX(bs->stack->data)) {
+ InlineBox *ib = inline_box_new();
+ bs->text_position = 0;
+ ib->wrap = FALSE;
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), GTK_WIDGET (ib));
+ gtk_widget_show_all(GTK_WIDGET(ib));
+ bs->stack = g_slist_prepend(bs->stack, ib);
+ bs->pre = TRUE;
+ attribute_start(bs->current_attrs, pango_attr_family_new("mono"), 0);
+ }
+ }
+
+ if (IS_TABLE_BOX(bs->stack->data)) {
+ if (strcmp(name, "tr") == 0) {
+ table_box_add_row(bs->stack->data);
+ }
+
+ if (TABLE_BOX(bs->stack->data)->rows != NULL) {
+ if (strcmp(name, "td") == 0 || strcmp(name, "th") == 0) {
+ GtkWidget *tc = table_cell_new();
+ if (attrs != NULL) {
+ const gchar *rowspan = NULL, *colspan = NULL;
+ guint i;
+ for (i = 0; attrs[i]; i += 2){
+ if (g_strcmp0((const char*)attrs[i], "colspan") == 0) {
+ colspan = (const char*)attrs[i+1];
+ }
+ if (g_strcmp0((const char*)attrs[i], "rowspan") == 0) {
+ rowspan = (const char*)attrs[i+1];
+ }
+ }
+ if (rowspan != NULL) {
+ sscanf(rowspan, "%u", &(TABLE_CELL(tc)->rowspan));
+ if (TABLE_CELL(tc)->rowspan > 65534) {
+ TABLE_CELL(tc)->rowspan = 65534;
+ } else if (TABLE_CELL(tc)->rowspan == 0) {
+ TABLE_CELL(tc)->rowspan = 1;
+ }
+ }
+ if (colspan != NULL) {
+ sscanf(colspan, "%u", &(TABLE_CELL(tc)->colspan));
+ if (TABLE_CELL(tc)->colspan > 65534) {
+ TABLE_CELL(tc)->colspan = 65534;
+ } else if (TABLE_CELL(tc)->colspan == 0) {
+ TABLE_CELL(tc)->colspan = 1;
+ }
+ }
+ }
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), tc);
+ gtk_widget_show_all(tc);
+ bs->stack = g_slist_prepend(bs->stack, tc);
+ }
+ }
+ }
+
+ /* Ignored */
+ if (strcmp(name, "head") == 0 || strcmp(name, "script") == 0 ||
+ strcmp(name, "style") == 0) {
+ bs->ignore_text = TRUE;
+ }
+
+ /* Images */
+ if (IS_INLINE_BOX(bs->stack->data)) {
+ if (strcmp(name, "img") == 0) {
+ guint i;
+ const char *src = NULL;
+ if (attrs != NULL) {
+ for (i = 0; attrs[i]; i += 2){
+ if (strcmp((const char*)attrs[i], "src") == 0) {
+ src = (const char*)attrs[i+1];
+ }
+ }
+ }
+ if (src != NULL) {
+ GtkWidget *image = gtk_image_new_from_file(NULL);
+ if (image != NULL) {
+ /* todo: progressive image loading */
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), image);
+ gtk_widget_show_all(image);
+
+ SoupURI *uri = soup_uri_new_with_base(bs->uri, src);
+ SoupMessage *sm = soup_message_new_from_uri("GET", uri);
+ soup_uri_free(uri);
+ ImageSetData *isd = malloc(sizeof(ImageSetData));
+ isd->image = GTK_IMAGE(image);
+ isd->bs = bs;
+ g_object_ref(bs);
+ soup_session_queue_message(bb->soup_session, sm,
+ (SoupSessionCallback)image_set, isd);
+ if (bs->current_link != NULL) {
+ bs->current_link->objects =
+ g_list_prepend(bs->current_link->objects, image);
+ }
+ }
+ }
+ }
+
+ /* Inputs */
+ if (strcmp(name, "input") == 0) {
+ guint i;
+ const char *type = NULL, *value = NULL, *a_name = NULL;
+ if (attrs != NULL) {
+ for (i = 0; attrs[i]; i += 2){
+ if (g_strcmp0((const char*)attrs[i], "type") == 0) {
+ type = (const char*)attrs[i+1];
+ }
+ if (g_strcmp0((const char*)attrs[i], "value") == 0) {
+ value = (const char*)attrs[i+1];
+ }
+ if (g_strcmp0((const char*)attrs[i], "name") == 0) {
+ a_name = (const char*)attrs[i+1];
+ }
+ }
+ }
+ GtkWidget *input = NULL;
+ if (g_strcmp0(type, "submit") == 0) {
+ input =
+ gtk_button_new_with_label(value == NULL ? "submit" : value);
+ if (bs->current_form != NULL) {
+ g_signal_connect (input, "clicked",
+ G_CALLBACK(form_submit), bs->current_form);
+ }
+ } else if (g_strcmp0(type, "checkbox") == 0) {
+ input = gtk_check_button_new();
+ } else {
+ /* Defaulting to type=text */
+ input = gtk_entry_new();
+ if (value != NULL) {
+ gtk_entry_set_text(GTK_ENTRY(input), value);
+ }
+ if (bs->current_form != NULL) {
+ g_signal_connect (input, "activate",
+ G_CALLBACK(form_submit), bs->current_form);
+ }
+ }
+ if (input != NULL) {
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), input);
+ if (g_strcmp0(type, "hidden") != 0) {
+ gtk_widget_show_all(input);
+ }
+ }
+ if (input != NULL && bs->current_form != NULL && a_name != NULL) {
+ FormField *ff = malloc(sizeof(FormField));
+ ff->name = strdup(a_name);
+ ff->widget = input;
+ bs->current_form->fields = g_list_append(bs->current_form->fields, ff);
+ }
+ }
+ if (strcmp(name, "select") == 0) {
+ const gchar *a_name = NULL;
+ if (attrs != NULL) {
+ guint i;
+ for (i = 0; attrs[i]; i += 2){
+ if (g_strcmp0((const char*)attrs[i], "name") == 0) {
+ a_name = (const char*)attrs[i+1];
+ }
+ }
+ }
+ GtkWidget *cbox = gtk_combo_box_text_new();
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), cbox);
+ bs->stack = g_slist_prepend(bs->stack, cbox);
+ gtk_widget_show_all(cbox);
+ if (bs->current_form != NULL && a_name != NULL) {
+ FormField *ff = malloc(sizeof(FormField));
+ ff->name = strdup(a_name);
+ ff->widget = cbox;
+ bs->current_form->fields = g_list_append(bs->current_form->fields, ff);
+ }
+ }
+
+ /* Links */
+ if (strcmp(name, "a") == 0) {
+ guint i;
+ const gchar *href = NULL;
+ if (attrs != NULL) {
+ for (i = 0; attrs[i]; i += 2){
+ if (strcmp((const char*)attrs[i], "href") == 0) {
+ href = (const char*)attrs[i+1];
+ }
+ }
+ }
+ if (href != NULL) {
+ bs->current_link = ib_link_new(href);
+
+ bs->current_link->start = bs->text_position;
+ INLINE_BOX(bs->stack->data)->links =
+ g_list_append(INLINE_BOX(bs->stack->data)->links, bs->current_link);
+ bs->docbox->links = g_list_append(bs->docbox->links, bs->current_link);
+ }
+ }
+ }
+ if (GTK_IS_COMBO_BOX_TEXT(bs->stack->data)) {
+ if (strcmp(name, "option") == 0) {
+ guint i;
+ if (attrs != NULL) {
+ for (i = 0; attrs[i]; i += 2) {
+ if (strcmp((const char*)attrs[i], "value") == 0) {
+ if (bs->option_value != NULL) {
+ free(bs->option_value);
+ }
+ bs->option_value = strdup((const char*)attrs[i+1]);
+ }
+ }
+ }
+ }
+ }
+
+
+ /* Formatting */
+ if (strcmp(name, "b") == 0 || strcmp(name, "strong") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_BOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "i") == 0 || strcmp(name, "em") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_style_new(PANGO_STYLE_ITALIC),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "code") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_family_new("mono"),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "sub") == 0) {
+ /* todo: avoid using a constant */
+ attribute_start(bs->current_attrs,
+ pango_attr_rise_new(-5 * PANGO_SCALE),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(0.8),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "sup") == 0) {
+ /* todo: avoid using a constant */
+ attribute_start(bs->current_attrs,
+ pango_attr_rise_new(5 * PANGO_SCALE),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(0.8),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h1") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(1.8),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h2") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(1.6),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h3") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(1.4),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h4") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(1.3),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h5") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(1.2),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h6") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(1.1),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "a") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_foreground_new(bs->link_color.red * 65535,
+ bs->link_color.green * 65535,
+ bs->link_color.blue * 65535),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_underline_new(PANGO_UNDERLINE_SINGLE),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ }
+
+ /* Identifiers */
+ if (attrs != NULL) {
+ guint i;
+ for (i = 0; attrs[i]; i += 2){
+ if (strcmp((const char*)attrs[i], "id") == 0 ||
+ (strcmp(name, "a") == 0 &&
+ strcmp((const char*)attrs[i], "name") == 0)) {
+ bs->queued_identifiers =
+ g_slist_prepend(bs->queued_identifiers,
+ strdup((const char*)attrs[i + 1]));
+ }
+ }
+ }
+ if (bs->queued_identifiers && GTK_IS_IMAGE(bs->stack->data)) {
+ GSList *ii;
+ for (ii = bs->queued_identifiers; ii; ii = ii->next) {
+ /* todo: perhaps abstract this into a function, since it's the
+ same for texts. */
+ const char *fragment = soup_uri_get_fragment(bs->uri);
+ if (fragment && bs->anchor_handler_id == 0 &&
+ strcmp(ii->data, fragment) == 0) {
+ bs->anchor_handler_id =
+ g_signal_connect (bs->stack->data, "size-allocate",
+ G_CALLBACK(anchor_allocated), bs);
+ }
+ g_hash_table_insert(bs->identifiers, ii->data, bs->stack->data);
+ }
+ g_slist_free_full(bs->queued_identifiers, g_free);
+ bs->queued_identifiers = NULL;
+ }
+
+ /* Forms */
+ if (strcmp(name, "form") == 0) {
+ Form *form = malloc(sizeof(Form));
+ form->submission_data = (gpointer)bb;
+ form->method = NULL;
+ form->enctype = ENCTYPE_URLENCODED;
+ form->action = NULL;
+ form->fields = NULL;
+ guint i;
+ gchar *action = NULL;
+ if (attrs != NULL) {
+ for (i = 0; attrs[i]; i += 2) {
+ if (strcmp((const char*)attrs[i], "method") == 0) {
+ form->method = strdup((const char*)attrs[i+1]);
+ }
+ if (strcmp((const char*)attrs[i], "enctype") == 0) {
+ if (strcmp((const char*)attrs[i + 1], "multipart/form-data") == 0) {
+ form->enctype = ENCTYPE_MULTIPART;
+ } else if (strcmp((const char*)attrs[i + 1], "text/plain") == 0) {
+ form->enctype = ENCTYPE_PLAIN;
+ }
+ }
+ if (strcmp((const char*)attrs[i], "action") == 0) {
+ action = strdup((const char*)attrs[i+1]);
+ }
+ }
+ }
+ if (action == NULL) {
+ form->action = soup_uri_copy(bs->uri);
+ } else {
+ form->action = soup_uri_new_with_base(bs->uri, action);
+ }
+ bb->forms = g_list_prepend(bb->forms, form);
+ bs->current_form = form;
+ }
+}
+
+void sax_end_element (BrowserBox *bb, const xmlChar *u_name)
+{
+ BuilderState *bs = bb->builder_state;
+ const char *name = (const char*)u_name;
+
+ if (IS_INLINE_BOX(bs->stack->data)) {
+ if (element_flushes_text(name)) {
+ if (bs->current_word != NULL) {
+ add_word(bs, bs->current_word, &bs->current_attrs);
+ free(bs->current_word);
+ bs->current_word = NULL;
+ }
+ bs->prev_space = TRUE;
+ }
+ if (element_is_blocking(name)) {
+ GSList *next = bs->stack->next;
+ if (next != NULL) {
+ g_slist_free_1(bs->stack);
+ bs->stack = next;
+ }
+ bs->prev_space = TRUE;
+ }
+ }
+
+ if ((strcmp(name, "dl") == 0 || strcmp(name, "ul") == 0 ||
+ strcmp(name, "ol") == 0 || strcmp(name, "dd") == 0 ||
+ strcmp(name, "li") == 0 || strcmp(name, "select") == 0)) {
+ GSList *next = bs->stack->next;
+ if (next != NULL) {
+ g_slist_free_1(bs->stack);
+ bs->stack = next;
+ }
+ if (strcmp(name, "ol") == 0 || strcmp(name, "ul") == 0) {
+ GSList *next = bs->ol_numbers->next;
+ g_free(bs->ol_numbers->data);
+ g_slist_free_1(bs->ol_numbers);
+ bs->ol_numbers = next;
+ }
+ }
+ if (bs->stack && strcmp(name, "li") == 0) {
+ /* repeat */
+ GSList *next = bs->stack->next;
+ if (next != NULL) {
+ g_slist_free_1(bs->stack);
+ bs->stack = next;
+ }
+ }
+
+ if (strcmp(name, "option") == 0 && bs->option_value != NULL) {
+ free(bs->option_value);
+ bs->option_value = NULL;
+ }
+
+ /* Tables */
+ if (IS_TABLE_BOX(bs->stack->data)) {
+ if (strcmp(name, "table") == 0) {
+ GSList *next = bs->stack->next;
+ if (next != NULL) {
+ g_slist_free_1(bs->stack);
+ bs->stack = next;
+ }
+ }
+ }
+ if (IS_TABLE_CELL(bs->stack->data)) {
+ if (strcmp(name, "td") == 0 || strcmp(name, "th") == 0) {
+ GSList *next = bs->stack->next;
+ if (next != NULL) {
+ g_slist_free_1(bs->stack);
+ bs->stack = next;
+ }
+ }
+ }
+
+ /* Preformatted texts */
+ if (strcmp(name, "pre") == 0) {
+ bs->pre = FALSE;
+ bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_FAMILY, 0);
+ }
+
+ /* Ignored */
+ if (strcmp(name, "head") == 0 || strcmp(name, "script") == 0 ||
+ strcmp(name, "style") == 0) {
+ bs->ignore_text = FALSE;
+ }
+
+ /* Formatting */
+ if (strcmp(name, "b") == 0 || strcmp(name, "strong") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_WEIGHT,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "i") == 0 || strcmp(name, "em") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_STYLE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "code") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_FAMILY,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "sub") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_RISE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_SCALE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "sup") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_RISE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_SCALE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h1") == 0 || strcmp(name, "h2") == 0 ||
+ strcmp(name, "h3") == 0 || strcmp(name, "h4") == 0 ||
+ strcmp(name, "h5") == 0 || strcmp(name, "h6") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_SCALE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_WEIGHT,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "a") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_FOREGROUND,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_UNDERLINE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ if (bs->current_link != NULL) {
+ bs->current_link->end = bs->text_position;
+ bs->current_link = NULL;
+ }
+ }
+
+ /* Forms */
+ if (strcmp(name, "form") == 0) {
+ bs->current_form = NULL;
+ }
+}
+
+
+
+
+
+void select_text_cb (void *ptr,
+ gchar *str,
+ BrowserBox *bb)
+{
+ printf("Selection: '%s'\n", str);
+}
+
+
+void follow_link_cb (void *ptr,
+ gchar *url,
+ gboolean new_tab,
+ BrowserBox *bb)
+{
+ BuilderState *bs = bb->builder_state;
+ SoupURI *new_uri = soup_uri_new_with_base(bs->uri, url);
+ if (url[0] == '#') {
+ char *uri_str = soup_uri_to_string(new_uri, FALSE);
+ gtk_entry_set_text(GTK_ENTRY(bb->address_bar), uri_str);
+ free(uri_str);
+ soup_uri_free(new_uri);
+ scroll_to_identifier(bs, url + 1);
+ return;
+ }
+ BrowserBox *target_bb = bb;
+ if (new_tab) {
+ target_bb = browser_box_new(NULL);
+ gtk_widget_show_all(GTK_WIDGET(target_bb));
+ target_bb->tabs = bb->tabs;
+ gtk_stack_add_titled(GTK_STACK(target_bb->tabs), GTK_WIDGET(target_bb),
+ url, url);
+ }
+ history_add(target_bb, new_uri);
+ document_request(target_bb, new_uri);
+}
+
+void hover_link_cb (void *ptr,
+ gchar *url,
+ BrowserBox *bb)
+{
+ gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"));
+ gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"),
+ url);
+}
+
+
+xmlSAXHandler sax = {
+ .characters = (charactersSAXFunc)sax_characters,
+ .startElement = (startElementSAXFunc)sax_start_element,
+ .endElement = (endElementSAXFunc)sax_end_element
+};
+
+void document_loaded(SoupSession *session,
+ SoupMessage *msg,
+ gpointer ptr)
+{
+ BrowserBox *bb = ptr;
+ BuilderState *bs = bb->builder_state;
+ if (! bs->active) {
+ return;
+ }
+ htmlParseChunk(bs->parser, "", 0, 1);
+ gtk_widget_grab_focus(GTK_WIDGET(bs->docbox));
+ printf("word cache: %u\n", g_hash_table_size(word_cache));
+ gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"));
+ gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"),
+ "Ready");
+}
+
+void got_chunk(SoupMessage *msg,
+ SoupBuffer *chunk,
+ gpointer ptr)
+{
+ BrowserBox *bb = ptr;
+ BuilderState *bs = bb->builder_state;
+ gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"));
+ gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"),
+ "Loading");
+ if (bs->parser == NULL) {
+ /* todo: maybe move it into got_headers */
+ char *uri_str = soup_uri_to_string(bs->uri, FALSE);
+ bs->parser =
+ htmlCreatePushParserCtxt(&sax, bb, "", 0, uri_str,
+ XML_CHAR_ENCODING_UTF8);
+ free(uri_str);
+ bs->docbox = document_box_new();
+ gtk_container_add (GTK_CONTAINER (bs->root), GTK_WIDGET (bs->docbox));
+ bs->vbox = block_box_new(10);
+ bs->stack->data = bs->vbox;
+ gtk_container_add(GTK_CONTAINER (DOCUMENT_BOX(bs->docbox)->evbox),
+ GTK_WIDGET (bs->vbox));
+ g_signal_connect (bs->docbox, "follow", G_CALLBACK(follow_link_cb), bb);
+ g_signal_connect (bs->docbox, "hover", G_CALLBACK(hover_link_cb), bb);
+ g_signal_connect (bs->docbox, "select", G_CALLBACK(select_text_cb), bb);
+ gtk_widget_show_all(GTK_WIDGET(bs->docbox));
+ gtk_box_set_child_packing(GTK_BOX(bs->root), GTK_WIDGET(bs->docbox),
+ TRUE, TRUE, 0, GTK_PACK_END);
+ }
+ if (bs->active) {
+ htmlParseChunk(bs->parser, chunk->data, chunk->length, 0);
+ }
+ return;
+}
+
+void got_headers(SoupMessage *msg, gpointer ptr)
+{
+ BrowserBox *bb = ptr;
+ gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"));
+ gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"),
+ "Got headers");
+ /* todo: check content type, don't assume HTML */
+ if (bb->builder_state != NULL) {
+ if (bb->builder_state->docbox != NULL) {
+ gtk_widget_destroy(GTK_WIDGET(bb->builder_state->docbox));
+ }
+ g_object_unref(bb->builder_state);
+ }
+ bb->builder_state = builder_state_new(bb->docbox_root);
+ bb->builder_state->uri = soup_uri_copy(soup_message_get_uri(msg));
+ char *uri_str = soup_uri_to_string(bb->builder_state->uri, FALSE);
+ gtk_entry_set_text(GTK_ENTRY(bb->address_bar), uri_str);
+ free(uri_str);
+}
+
+void document_request_sm (BrowserBox *bb, SoupMessage *sm)
+{
+ gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"));
+ gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"),
+ "Requesting");
+ if (bb->builder_state != NULL) {
+ bb->builder_state->active = FALSE;
+ }
+ soup_session_abort(bb->soup_session);
+ g_signal_connect (sm, "got-chunk", (GCallback)got_chunk, bb);
+ g_signal_connect (sm, "got-headers", (GCallback)got_headers, bb);
+ soup_session_queue_message(bb->soup_session, sm,
+ (SoupSessionCallback)document_loaded, bb);
+}
+
+void document_request (BrowserBox *bb, SoupURI *uri)
+{
+ SoupMessage *sm = soup_message_new_from_uri("GET", uri);
+ document_request_sm(bb, sm);
+}
+
+
+void address_bar_activate (GtkEntry *ab, BrowserBox *bb)
+{
+ SoupURI *uri = soup_uri_new(gtk_entry_get_text(ab));
+ if (uri) {
+ history_add(bb, uri);
+ document_request(bb, uri);
+ }
+}
+
+
+
+static void browser_box_dispose (GObject *object) {
+ BrowserBox *bb = BROWSER_BOX(object);
+ GList *form_iter;
+ if (bb->forms != NULL) {
+ for (form_iter = bb->forms; form_iter; form_iter = form_iter->next) {
+ Form *form = form_iter->data;
+ if (form->method != NULL) {
+ free(form->method);
+ form->method = NULL;
+ }
+ if (form->action != NULL) {
+ soup_uri_free(form->action);
+ form->action = NULL;
+ }
+ if (form->fields != NULL) {
+ GList *field_iter;
+ for (field_iter = form->fields; field_iter; field_iter = field_iter->next) {
+ FormField *field = field_iter->data;
+ if (field->name != NULL) {
+ free(field->name);
+ field->name = NULL;
+ }
+ free(field);
+ }
+ g_list_free(form->fields);
+ form->fields = NULL;
+ }
+ free(form);
+ }
+ g_list_free(bb->forms);
+ bb->forms = NULL;
+ }
+ if (bb->history != NULL) {
+ g_list_free_full(bb->history, (GDestroyNotify)soup_uri_free);
+ bb->history = NULL;
+ bb->history_position = NULL;
+ }
+ G_OBJECT_CLASS (browser_box_parent_class)->dispose(object);
+}
+
+static void
+browser_box_class_init (BrowserBoxClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = browser_box_dispose;
+ return;
+}
+
+static void
+browser_box_init (BrowserBox *bb)
+{
+ bb->builder_state = NULL;
+ bb->forms = NULL;
+ bb->history = NULL;
+ bb->history_position = NULL;
+ return;
+}
+
+void document_request (BrowserBox *bb, SoupURI *uri);
+
+
+BrowserBox *browser_box_new (gchar *uri_str)
+{
+ BrowserBox *bb = BROWSER_BOX(g_object_new(browser_box_get_type(),
+ "orientation", GTK_ORIENTATION_VERTICAL,
+ "spacing", 0,
+ NULL));
+ bb->address_bar = gtk_entry_new();
+ gtk_container_add (GTK_CONTAINER(bb), bb->address_bar);
+ g_signal_connect(bb->address_bar, "activate",
+ (GCallback)address_bar_activate, bb);
+
+ bb->docbox_root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_container_add (GTK_CONTAINER(bb), bb->docbox_root);
+ gtk_box_set_child_packing(GTK_BOX(bb), bb->docbox_root, TRUE, TRUE, 0, GTK_PACK_START);
+
+ bb->status_bar = gtk_statusbar_new();
+ gtk_container_add (GTK_CONTAINER(bb), bb->status_bar);
+
+ bb->soup_session =
+ soup_session_new_with_options("user-agent", "WWWLite/0.0.0", NULL);
+
+ /* bb->word_cache = g_hash_table_new((GHashFunc)wck_hash, (GEqualFunc)wck_equal); */
+
+ if (uri_str) {
+ SoupURI *uri = soup_uri_new(uri_str);
+ history_add(bb, uri);
+ document_request(bb, soup_uri_new(uri_str));
+ }
+
+ return bb;
+}
diff --git a/src/browserbox.h b/src/browserbox.h
new file mode 100644
index 0000000..5016c67
--- /dev/null
+++ b/src/browserbox.h
@@ -0,0 +1,133 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ 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 <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef BROWSER_BOX_H
+#define BROWSER_BOX_H
+
+#include <gtk/gtk.h>
+#include <libsoup/soup.h>
+#include "documentbox.h"
+#include "inlinebox.h"
+#include "blockbox.h"
+#include <libxml/HTMLparser.h>
+
+G_BEGIN_DECLS
+
+typedef struct _FormField FormField;
+struct _FormField
+{
+ gchar *name;
+ GtkWidget *widget;
+};
+
+enum {
+ ENCTYPE_URLENCODED,
+ ENCTYPE_MULTIPART,
+ ENCTYPE_PLAIN
+};
+
+typedef struct _Form Form;
+struct _Form
+{
+ gchar *method;
+ int enctype;
+ SoupURI *action;
+ GList *fields;
+ gpointer *submission_data;
+};
+
+#define BUILDER_STATE_TYPE (builder_state_get_type())
+G_DECLARE_FINAL_TYPE (BuilderState, builder_state, BUILDER, STATE, GObject);
+
+struct _BuilderState
+{
+ GObject parent_instance;
+ gboolean active;
+ GtkWidget *root;
+ DocumentBox *docbox;
+ GtkWidget *vbox;
+ GSList *stack;
+ GdkRGBA link_color;
+ guint text_position;
+ PangoAttrList *current_attrs;
+ IBLink *current_link;
+ gchar *current_word;
+ gboolean ignore_text;
+ gboolean prev_space;
+ gboolean pre;
+ htmlParserCtxtPtr parser;
+ SoupURI *uri;
+ GSList *queued_identifiers;
+ GHashTable *identifiers;
+ gulong anchor_handler_id;
+ gchar *option_value;
+ GSList *ol_numbers;
+ Form *current_form;
+};
+
+#define BROWSER_BOX_TYPE (browser_box_get_type())
+#define BROWSER_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), BROWSER_BOX_TYPE, BrowserBox))
+#define BROWSER_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), BROWSER_BOX_TYPE, BrowserBoxClass))
+#define IS_BROWSER_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), BROWSER_BOX_TYPE))
+#define IS_BROWSER_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), BROWSER_BOX_TYPE))
+
+
+typedef struct _BrowserBox BrowserBox;
+typedef struct _BrowserBoxClass BrowserBoxClass;
+
+struct _BrowserBox
+{
+ BlockBox parent_instance;
+ SoupSession *soup_session;
+ BuilderState *builder_state;
+ GtkWidget *address_bar;
+ GtkWidget *docbox_root;
+ GtkWidget *status_bar;
+ GList *forms;
+ GList *history;
+ GList *history_position;
+ GtkStack *tabs;
+ /* GHashTable *word_cache; */
+};
+
+struct _BrowserBoxClass
+{
+ BlockBoxClass parent_class;
+};
+
+GType browser_box_get_type(void) G_GNUC_CONST;
+BrowserBox *browser_box_new(gchar *uri_str);
+
+typedef struct _WordCacheKey WordCacheKey;
+struct _WordCacheKey
+{
+ gchar *text;
+ PangoAttrList *attrs;
+};
+guint wck_hash (WordCacheKey *wck);
+gboolean wck_equal (WordCacheKey *wck1, WordCacheKey *wck2);
+
+void document_request_sm (BrowserBox *bb, SoupMessage *sm);
+void document_request (BrowserBox *bb, SoupURI *uri);
+gboolean history_back (BrowserBox *bb);
+gboolean history_forward (BrowserBox *bb);
+
+GHashTable *word_cache;
+
+G_END_DECLS
+
+#endif
diff --git a/src/documentbox.c b/src/documentbox.c
new file mode 100644
index 0000000..b506385
--- /dev/null
+++ b/src/documentbox.c
@@ -0,0 +1,489 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ 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 <https://www.gnu.org/licenses/>.
+*/
+
+#include <gtk/gtk.h>
+#include "documentbox.h"
+#include "inlinebox.h"
+
+G_DEFINE_TYPE (DocumentBox, document_box, GTK_TYPE_SCROLLED_WINDOW);
+
+
+static void document_box_dispose (GObject *object) {
+ DocumentBox *db = DOCUMENT_BOX(object);
+ if (db->links != NULL) {
+ /* The same links are also referenced from InlineBox, and freed on
+ its disposal, so only the list needs to be freed here. */
+ g_list_free(db->links);
+ db->links = NULL;
+ }
+ G_OBJECT_CLASS (document_box_parent_class)->dispose (object);
+}
+
+enum {
+ FOLLOW,
+ SELECT,
+ HOVER
+};
+
+static guint signals[3];
+
+static GtkSizeRequestMode document_box_get_request_mode (GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+document_box_class_init (DocumentBoxClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+ /* GtkContainerClass *container_class = GTK_CONTAINER_CLASS(klass); */
+ signals[FOLLOW] =
+ g_signal_new("follow",
+ G_TYPE_FROM_CLASS (gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0, /* class_offset */
+ NULL, /* accumulator */
+ NULL, /* accu_data */
+ NULL, /* c_marshaller */
+ G_TYPE_NONE, /* return_type */
+ 2, /* n_params */
+ G_TYPE_STRING,
+ G_TYPE_BOOLEAN);
+ signals[SELECT] =
+ g_signal_new("select",
+ G_TYPE_FROM_CLASS (gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0, /* class_offset */
+ NULL, /* accumulator */
+ NULL, /* accu_data */
+ NULL, /* c_marshaller */
+ G_TYPE_NONE, /* return_type */
+ 1, /* n_params */
+ G_TYPE_STRING);
+ signals[HOVER] =
+ g_signal_new("hover",
+ G_TYPE_FROM_CLASS (gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0, /* class_offset */
+ NULL, /* accumulator */
+ NULL, /* accu_data */
+ NULL, /* c_marshaller */
+ G_TYPE_NONE, /* return_type */
+ 1, /* n_params */
+ G_TYPE_STRING);
+ widget_class->get_request_mode = document_box_get_request_mode;
+ gobject_class->dispose = document_box_dispose;
+ return;
+}
+
+static void
+document_box_init (DocumentBox *db)
+{
+ db->links = NULL;
+}
+
+
+typedef struct _SearchState SearchState;
+struct _SearchState
+{
+ gint x;
+ gint y;
+ guint index;
+ IBText *ibt;
+ InlineBox *ib;
+ guint ib_index;
+};
+
+static void
+text_at_position(GtkWidget *widget, SearchState *st)
+{
+ GtkAllocation alloc;
+ gtk_widget_get_allocation(widget, &alloc);
+ if (st->x >= alloc.x &&
+ st->x <= alloc.x + alloc.width &&
+ st->y >= alloc.y &&
+ st->y <= alloc.y + alloc.height) {
+ if (IS_INLINE_BOX(widget)) {
+ st->ib = INLINE_BOX(widget);
+ GList *ti;
+ guint text_position = 0;
+ for (ti = INLINE_BOX(widget)->children; ti; ti = ti->next) {
+ if (IS_IB_TEXT(ti->data)) {
+ IBText *ibt = IB_TEXT(ti->data);
+ if (st->x >= ibt->alloc.x &&
+ st->x <= ibt->alloc.x + ibt->alloc.width &&
+ st->y >= ibt->alloc.y &&
+ st->y <= ibt->alloc.y + ibt->alloc.height) {
+ gint position;
+ pango_layout_xy_to_index(ibt->layout,
+ (st->x - ibt->alloc.x) * PANGO_SCALE,
+ (st->y - ibt->alloc.y) * PANGO_SCALE,
+ &position,
+ NULL);
+ st->index = position;
+ st->ibt = ibt;
+ st->ib_index = text_position + position;
+ return;
+ }
+ text_position += strlen(pango_layout_get_text(ibt->layout));
+ }
+ }
+ return;
+ } else if (GTK_IS_CONTAINER(widget)) {
+ gtk_container_foreach(GTK_CONTAINER(widget),
+ (GtkCallback)text_at_position, st);
+ }
+ }
+}
+
+static gboolean widget_is_affected(GtkAllocation *wa, GtkAllocation *ta1,
+ GtkAllocation *ta2)
+{
+ if (wa == NULL || ta1 == NULL || ta2 == NULL) {
+ return FALSE;
+ }
+ return
+ gdk_rectangle_intersect(wa, ta1, NULL) ||
+ gdk_rectangle_intersect(wa, ta2, NULL) ||
+ ((ta2->y >= ta1->y &&
+ ((wa->y >= ta1->y) && (wa->y <= ta2->y))) ||
+ ((ta2->y <= ta1->y) &&
+ ((wa->y >= ta2->y) && (wa->y <= ta1->y))));
+}
+
+/* Assuming (Y, X) is strictly increasing */
+static gint compare_positions(GtkAllocation *a1, guint i1,
+ GtkAllocation *a2, guint i2)
+{
+ if ((a1->y < a2->y) ||
+ (a1->y == a2->y && a1->x < a2->x) ||
+ (a1->y == a2->y && a1->x == a2->x && i1 < i2)) {
+ return -1;
+ } else if (a1->y == a2->y && a1->x == a2->x && i1 == i2) {
+ return 0;
+ } else {
+ return 1;
+ }
+}
+
+
+static void
+selection_update (GtkWidget *widget, SelectionState *st)
+{
+ GtkAllocation alloc;
+ gtk_widget_get_allocation(widget, &alloc);
+ if (widget_is_affected(&alloc, &st->selection_start->alloc,
+ &st->selection_end->alloc) ||
+ widget_is_affected(&alloc, &st->selection_start->alloc,
+ &st->selection_prev->alloc) ||
+ widget_is_affected(&alloc, &st->selection_end->alloc,
+ &st->selection_prev->alloc)) {
+ if (IS_INLINE_BOX(widget)) {
+ InlineBox *ib = INLINE_BOX(widget);
+ ib->selection_end = 0;
+ ib->selection_start = 0;
+ GList *ti;
+ guint text_position = 0;
+
+ for (ti = ib->children; ti; ti = ti->next) {
+ if (IS_IB_TEXT(ti->data)) {
+ IBText *ibt = IB_TEXT(ti->data);
+ gint direction = compare_positions(&st->selection_start->alloc,
+ st->selection_start_index,
+ &st->selection_end->alloc,
+ st->selection_end_index);
+ if (direction == -1) {
+ if (st->selection_start == ibt) {
+ ib->selection_start = st->selection_start_index + text_position;
+ st->selecting = TRUE;
+ }
+ if (st->selecting && st->selection_end == ibt) {
+ ib->selection_end = st->selection_end_index + text_position;
+ st->selecting = FALSE;
+ }
+ } else if (direction == 1) {
+ if (st->selection_end == ibt) {
+ ib->selection_start = st->selection_end_index + text_position;
+ st->selecting = TRUE;
+ }
+ if (st->selecting && st->selection_start == ibt) {
+ ib->selection_end = st->selection_start_index + text_position;
+ st->selecting = FALSE;
+ }
+ }
+ text_position += strlen(pango_layout_get_text(ibt->layout));
+ gtk_widget_queue_draw (widget);
+ }
+ }
+ if (st->selecting) {
+ ib->selection_end = text_position;
+ }
+ } else if (GTK_IS_CONTAINER(widget)) {
+ gtk_container_foreach(GTK_CONTAINER(widget),
+ (GtkCallback)selection_update, st);
+ }
+ }
+}
+
+static void
+selection_read (GtkWidget *widget, gchar **str)
+{
+ if (IS_INLINE_BOX(widget)) {
+ InlineBox *ib = INLINE_BOX(widget);
+ if (ib->selection_end == 0) {
+ return;
+ }
+ GList *ti;
+ guint text_position = 0;
+ gboolean affected = FALSE, breaks = FALSE;
+ for (ti = ib->children; ti; ti = ti->next) {
+ if (IS_IB_TEXT(ti->data)) {
+ IBText *ibt = IB_TEXT(ti->data);
+ const gchar *word = pango_layout_get_text(ibt->layout);
+ guint word_len = strlen(word);
+ if (ib->selection_start <= text_position + word_len &&
+ ib->selection_end > text_position) {
+ guint start_offset = 0, end_offset = 0;
+ if (ib->selection_start > text_position) {
+ start_offset = ib->selection_start - text_position;
+ }
+ if (ib->selection_end < text_position + word_len) {
+ end_offset = text_position + word_len - ib->selection_end;
+ }
+ guint len = word_len - start_offset - end_offset;
+ *str = realloc(*str, strlen(*str) + len + 1);
+ g_strlcpy(*str + strlen(*str), word + start_offset, len + 1);
+ affected = TRUE;
+ breaks = TRUE;
+ } else {
+ breaks = FALSE;
+ }
+ text_position += word_len;
+ } else if (breaks && IS_IB_BREAK(ti->data)) {
+ *str = realloc(*str, strlen(*str) + 2);
+ (*str)[strlen(*str) + 1] = 0;
+ (*str)[strlen(*str)] = '\n';
+ breaks = FALSE;
+ }
+ }
+ if (affected) {
+ /* Add one more newline in the end, so that there are newlines
+ between paragraphs. */
+ *str = realloc(*str, strlen(*str) + 2);
+ (*str)[strlen(*str) + 1] = 0;
+ (*str)[strlen(*str)] = '\n';
+ }
+ } else if (GTK_IS_CONTAINER(widget)) {
+ gtk_container_foreach(GTK_CONTAINER(widget),
+ (GtkCallback)selection_read, str);
+ }
+}
+
+static IBLink *find_link (SearchState *ss, gint x, gint y)
+{
+ if (ss->ib && ss->ib->links != NULL) {
+ GList *li;
+ for (li = ss->ib->links; li; li = li->next) {
+ IBLink *link = IB_LINK(li->data);
+ if (ss->ibt) {
+ if (link->start <= ss->ib_index && link->end > ss->ib_index) {
+ return link;
+ }
+ }
+ GList *oi;
+ GtkAllocation alloc;
+ for (oi = link->objects; oi; oi = oi->next) {
+ gtk_widget_get_allocation(oi->data, &alloc);
+ if (alloc.x <= x && alloc.x + alloc.width >= x &&
+ alloc.y <= y && alloc.y + alloc.height >= y) {
+ return link;
+ }
+ }
+ }
+ }
+ return NULL;
+}
+
+static gboolean
+button_press_event_cb (GtkWidget *widget,
+ GdkEventButton *event,
+ DocumentBox *db)
+{
+ if (event->button != 1) {
+ return FALSE;
+ }
+ SearchState ss;
+ gint orig_x, orig_y, ev_orig_x, ev_orig_y;
+ gdk_window_get_origin(gtk_widget_get_parent_window(GTK_WIDGET(db->evbox)),
+ &orig_x, &orig_y);
+ gdk_window_get_origin(event->window, &ev_orig_x, &ev_orig_y);
+ ss.ibt = NULL;
+ ss.x = ev_orig_x - orig_x + event->x;
+ ss.y = ev_orig_y - orig_y + event->y;
+ text_at_position(widget, &ss);
+
+ if (db->sel.selection_end) {
+ /* Remove existing selection */
+ db->sel.selection_prev = db->sel.selection_end;
+ db->sel.selection_prev_index = db->sel.selection_end_index + 1;
+ db->sel.selection_end = db->sel.selection_start;
+ db->sel.selection_end_index = db->sel.selection_start_index;
+ selection_update(widget, &db->sel);
+ }
+
+ if (ss.ibt) {
+ db->sel.selection_active = TRUE;
+ db->sel.selection_start = ss.ibt;
+ db->sel.selection_start_index = ss.index;
+ db->sel.selection_end = ss.ibt;
+ db->sel.selection_end_index = ss.index;
+ /* todo: grab focus when any non-widget space is clicked, not
+ just texts */
+ gtk_widget_grab_focus(GTK_WIDGET(db));
+ }
+ return FALSE;
+}
+
+static gboolean
+motion_notify_event_cb (GtkWidget *widget,
+ GdkEventButton *event,
+ DocumentBox *db)
+{
+ SearchState ss;
+ gint orig_x, orig_y, ev_orig_x, ev_orig_y;
+ gdk_window_get_origin(gtk_widget_get_parent_window(GTK_WIDGET(db->evbox)),
+ &orig_x, &orig_y);
+ gdk_window_get_origin(event->window, &ev_orig_x, &ev_orig_y);
+ ss.ib = NULL;
+ ss.ibt = NULL;
+ ss.x = ev_orig_x - orig_x + event->x;
+ ss.y = ev_orig_y - orig_y + event->y;
+ text_at_position(widget, &ss);
+ if (ss.ibt && db->sel.selection_active) {
+ db->sel.selection_prev = db->sel.selection_end;
+ db->sel.selection_prev_index = db->sel.selection_end_index;
+ db->sel.selection_end = ss.ibt;
+ db->sel.selection_end_index = ss.index;
+ db->sel.selecting = FALSE;
+ selection_update(widget, &db->sel);
+ }
+ IBLink *link = find_link(&ss, event->x, event->y);
+ if (link != NULL) {
+ g_signal_emit(db, signals[HOVER], 0, link->url);
+ }
+ return FALSE;
+}
+
+
+static gboolean
+button_release_event_cb (GtkWidget *widget,
+ GdkEventButton *event,
+ DocumentBox *db)
+{
+ if (event->button != 1 && event->button != 2) {
+ return FALSE;
+ }
+ gchar *str = malloc(1);
+ gboolean got_selection = FALSE;
+ str[0] = 0;
+ selection_read(widget, &str);
+ if (strlen(str) > 0) {
+ /* Strip the last newline */
+ str[strlen(str) - 1] = 0;
+ got_selection = TRUE;
+ }
+ g_signal_emit(db, signals[SELECT], 0, str);
+ g_free(str);
+ db->sel.selection_active = FALSE;
+ if (got_selection) {
+ return FALSE;
+ }
+
+ SearchState ss;
+ gint orig_x, orig_y, ev_orig_x, ev_orig_y;
+ gdk_window_get_origin(gtk_widget_get_parent_window(GTK_WIDGET(db->evbox)),
+ &orig_x, &orig_y);
+ gdk_window_get_origin(event->window, &ev_orig_x, &ev_orig_y);
+ ss.ib = NULL;
+ ss.ibt = NULL;
+ ss.x = ev_orig_x - orig_x + event->x;
+ ss.y = ev_orig_y - orig_y + event->y;
+ text_at_position(widget, &ss);
+
+ IBLink *link = find_link(&ss, event->x, event->y);
+ if (link != NULL) {
+ g_signal_emit(db, signals[FOLLOW], 0, link->url, event->button == 2);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+find_focused (GtkWidget *widget, GObject **focused) {
+ if (IS_INLINE_BOX(widget)) {
+ if (INLINE_BOX(widget)->focused_object != NULL) {
+ *focused = INLINE_BOX(widget)->focused_object;
+ return;
+ }
+ } else if (GTK_IS_CONTAINER(widget)) {
+ gtk_container_foreach(GTK_CONTAINER(widget),
+ (GtkCallback)find_focused, focused);
+ }
+}
+
+
+static gboolean
+key_press_event_cb (GtkWidget *widget, GdkEventKey *event, DocumentBox *db)
+{
+ if (event->keyval == GDK_KEY_Return) {
+ GObject *focused;
+ /* This is inefficient and can be optimised, but there's no
+ perceivable delay, so perhaps it doesn't worth adding more
+ code. */
+ find_focused(widget, &focused);
+ if (focused != NULL && IS_IB_LINK(focused)) {
+ g_signal_emit(db, signals[FOLLOW], 0, IB_LINK(focused)->url,
+ event->state & GDK_CONTROL_MASK);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+DocumentBox *document_box_new ()
+{
+ DocumentBox *db = DOCUMENT_BOX(g_object_new(document_box_get_type(),
+ "hadjustment", NULL,
+ "vadjustment", NULL,
+ NULL));
+ db->evbox = GTK_EVENT_BOX(gtk_event_box_new());
+ gtk_widget_add_events(GTK_WIDGET(db->evbox), GDK_POINTER_MOTION_MASK);
+ gtk_container_add (GTK_CONTAINER (db), GTK_WIDGET (db->evbox));
+
+ g_signal_connect (db->evbox, "button-press-event",
+ G_CALLBACK (button_press_event_cb), db);
+ g_signal_connect (db->evbox, "button-release-event",
+ G_CALLBACK (button_release_event_cb), db);
+ g_signal_connect (db->evbox, "motion-notify-event",
+ G_CALLBACK (motion_notify_event_cb), db);
+ g_signal_connect (db->evbox, "key-press-event",
+ G_CALLBACK (key_press_event_cb), db);
+ db->links = NULL;
+ db->sel.selection_active = FALSE;
+ return db;
+}
diff --git a/src/documentbox.h b/src/documentbox.h
new file mode 100644
index 0000000..351471b
--- /dev/null
+++ b/src/documentbox.h
@@ -0,0 +1,70 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ 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 <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef DOCUMENT_BOX_H
+#define DOCUMENT_BOX_H
+
+#include <gtk/gtk.h>
+#include "inlinebox.h"
+#include <libsoup/soup.h>
+
+G_BEGIN_DECLS
+
+#define DOCUMENT_BOX_TYPE (document_box_get_type())
+#define DOCUMENT_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), DOCUMENT_BOX_TYPE, DocumentBox))
+#define DOCUMENT_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), DOCUMENT_BOX_TYPE, DocumentBoxClass))
+#define IS_DOCUMENT_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), DOCUMENT_BOX_TYPE))
+#define IS_DOCUMENT_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), DOCUMENT_BOX_TYPE))
+
+typedef struct _DocumentBox DocumentBox;
+typedef struct _DocumentBoxClass DocumentBoxClass;
+
+typedef struct _SelectionState SelectionState;
+struct _SelectionState
+{
+ IBText *selection_start;
+ guint selection_start_index;
+ IBText *selection_end;
+ guint selection_end_index;
+ IBText *selection_prev;
+ guint selection_prev_index;
+ gboolean selection_active;
+ gboolean selecting;
+};
+
+struct _DocumentBox
+{
+ GtkScrolledWindow parent_instance;
+ GtkEventBox *evbox;
+ GList *links;
+ SelectionState sel;
+ GdkWindow *event_window;
+ /* GList *forms; */
+};
+
+struct _DocumentBoxClass
+{
+ GtkScrolledWindowClass parent_class;
+};
+
+GType document_box_get_type(void) G_GNUC_CONST;
+DocumentBox *document_box_new(void);
+
+
+G_END_DECLS
+
+#endif
diff --git a/src/inlinebox.c b/src/inlinebox.c
new file mode 100644
index 0000000..43b75fa
--- /dev/null
+++ b/src/inlinebox.c
@@ -0,0 +1,585 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ 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 <https://www.gnu.org/licenses/>.
+*/
+
+#include <gtk/gtk.h>
+#include "inlinebox.h"
+
+
+static GtkSizeRequestMode inline_box_get_request_mode (GtkWidget *widget);
+static void inline_box_get_preferred_width(GtkWidget *widget,
+ gint *minimal, gint *natural);
+static void inline_box_get_preferred_height_for_width(GtkWidget *widget,
+ gint width,
+ gint *minimal,
+ gint *natural);
+static void inline_box_size_allocate(GtkWidget *widget,
+ GtkAllocation *allocation);
+static GType inline_box_child_type(GtkContainer *container);
+static void inline_box_add(GtkContainer *container, GtkWidget *widget);
+static void inline_box_remove(GtkContainer *container, GtkWidget *widget);
+static void inline_box_forall(GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback, gpointer callback_data);
+static void inline_box_dispose (GObject *object);
+static void inline_box_finalize (GObject *object);
+static void ib_text_dispose (GObject *self);
+static void ib_link_dispose (GObject *self);
+
+
+static void ib_text_class_init (IBTextClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = ib_text_dispose;
+}
+
+static void ib_text_init (IBText *self)
+{
+ self->layout = NULL;
+}
+
+static void ib_text_dispose (GObject *self)
+{
+ IBText *ibt = IB_TEXT(self);
+ g_clear_object(&ibt->layout);
+}
+
+IBText *ib_text_new (PangoLayout *layout)
+{
+ IBText *ib_text = g_object_new (IB_TEXT_TYPE, NULL);
+ PangoRectangle extents;
+ pango_layout_get_pixel_extents(layout, NULL, &extents);
+ ib_text->alloc.x = 0;
+ ib_text->alloc.y = 0;
+ ib_text->alloc.width = extents.width;
+ ib_text->alloc.height = extents.height;
+ ib_text->layout = layout;
+ g_object_ref(layout);
+ return IB_TEXT (ib_text);
+}
+
+static void ib_link_class_init (IBLinkClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = ib_link_dispose;
+}
+
+static void ib_link_init (IBLink *self)
+{
+ self->url = NULL;
+ self->objects = NULL;
+}
+
+static void ib_link_dispose (GObject *self)
+{
+ IBLink *ibl = IB_LINK(self);
+ g_free(ibl->url);
+ ibl->url = NULL;
+}
+
+IBLink *ib_link_new (const gchar *url)
+{
+ IBLink *ib_link = g_object_new (IB_LINK_TYPE, NULL);
+ ib_link->url = strdup(url);
+ return ib_link;
+}
+
+
+static void ib_break_class_init (IBBreakClass *klass)
+{
+ return;
+}
+
+static void ib_break_init (IBBreak *self)
+{
+ return;
+}
+
+IBBreak *ib_break_new ()
+{
+ return g_object_new (IB_BREAK_TYPE, NULL);
+}
+
+
+G_DEFINE_TYPE (IBLink, ib_link, G_TYPE_OBJECT);
+G_DEFINE_TYPE (IBBreak, ib_break, G_TYPE_OBJECT);
+G_DEFINE_TYPE (IBText, ib_text, G_TYPE_OBJECT);
+G_DEFINE_TYPE (InlineBox, inline_box, GTK_TYPE_CONTAINER);
+
+
+static gint
+inline_box_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ GList *child;
+ InlineBox *ib = INLINE_BOX(widget);
+ guint text_position = 0;
+ for (child = ib->children; child; child = child->next) {
+ if (GTK_IS_WIDGET(child->data)) {
+ gtk_container_propagate_draw((GTK_CONTAINER(widget)),
+ GTK_WIDGET(child->data), cr);
+ /* todo: render focus around widgets (images in particular)
+ too */
+ } else if (IS_IB_TEXT(child->data)) {
+ IBText *ibt = IB_TEXT(child->data);
+ GtkAllocation alloc;
+ GtkStyleContext *styleCtx = gtk_widget_get_style_context(widget);
+ guint text_len = strlen(pango_layout_get_text(ibt->layout));
+ gtk_widget_get_allocation (widget, &alloc);
+ cairo_translate (cr, -alloc.x, -alloc.y);
+
+ if (ib->selection_start <= text_position + text_len &&
+ ib->selection_end >= text_position) {
+ guint sel_start = ibt->alloc.x, sel_width = ibt->alloc.width;
+ gint x_pos;
+ if (ib->selection_start > text_position) {
+ pango_layout_index_to_line_x(ibt->layout,
+ ib->selection_start - text_position,
+ FALSE, NULL, &x_pos);
+ sel_start += x_pos / PANGO_SCALE;
+ sel_width -= x_pos / PANGO_SCALE;
+ }
+ if (ib->selection_end < text_position + text_len) {
+ pango_layout_index_to_line_x(ibt->layout,
+ ib->selection_end - text_position,
+ FALSE, NULL, &x_pos);
+ sel_width -= ibt->alloc.width - x_pos / PANGO_SCALE;
+ }
+ /* todo: the following seems to render "inactive" selection,
+ but would be nice to render an active one */
+ gtk_style_context_add_class(styleCtx, "rubberband");
+ gtk_render_background(styleCtx, cr, sel_start, ibt->alloc.y,
+ sel_width, ibt->alloc.height);
+ gtk_style_context_remove_class(styleCtx, "rubberband");
+ }
+
+ gtk_render_layout(styleCtx, cr, ibt->alloc.x, ibt->alloc.y, ibt->layout);
+
+ if (ib->focused_object) {
+ if (IS_IB_LINK(ib->focused_object)) {
+ IBLink *ibl = IB_LINK(ib->focused_object);
+ if (ibl->start <= text_position + text_len &&
+ ibl->end > text_position) {
+ int start_index = 0, end_index = text_len;
+ if (ibl->start > text_position) {
+ start_index = ibl->start - text_position;
+ }
+ if (ibl->end < text_position + text_len) {
+ end_index = ibl->end - text_position;
+ }
+ int start_x = 0, end_x = 0;
+ pango_layout_index_to_line_x(ibt->layout, start_index,
+ 0, NULL, &start_x);
+ pango_layout_index_to_line_x(ibt->layout, end_index,
+ 0, NULL, &end_x);
+ gtk_render_focus(styleCtx, cr,
+ ibt->alloc.x + start_x / PANGO_SCALE,
+ ibt->alloc.y,
+ (end_x - start_x) / PANGO_SCALE,
+ ibt->alloc.height);
+ }
+ }
+ }
+ cairo_translate (cr, alloc.x, alloc.y);
+ text_position += text_len;
+ }
+ }
+ return FALSE;
+}
+
+static gboolean
+inline_box_focus (GtkWidget *widget,
+ GtkDirectionType direction)
+{
+ InlineBox *ib = INLINE_BOX(widget);
+ if (ib->children == NULL) {
+ return FALSE;
+ }
+ GList *ci;
+ guint text_position;
+ gboolean focus_next = FALSE;
+
+ if (ib->focused_object == NULL) {
+ focus_next = TRUE;
+ }
+
+ /* todo: allow moving focus inside a single word */
+ for (text_position = 0, ci = ib->children; ci; ci = ci->next) {
+ if (focus_next) {
+ if (GTK_IS_WIDGET(ci->data)) {
+ ib->focused_object = ci->data;
+ if (gtk_widget_child_focus(ci->data, direction)) {
+ gtk_widget_queue_draw(widget);
+ return TRUE;
+ }
+ } else if (ib->links != NULL && IS_IB_TEXT(ci->data)) {
+ GList *li;
+ for (li = ib->links; li; li = li->next) {
+ if (IB_LINK(li->data)->start <=
+ text_position +
+ strlen(pango_layout_get_text(IB_TEXT(ci->data)->layout)) &&
+ IB_LINK(li->data)->end > text_position) {
+ ib->focused_object = li->data;
+ gtk_widget_grab_focus(widget);
+ gtk_widget_queue_draw(widget);
+ return TRUE;
+ }
+ }
+ }
+ }
+ if (ci->data == ib->focused_object) {
+ focus_next = TRUE;
+ }
+
+ if (IS_IB_TEXT(ci->data)) {
+ text_position +=
+ strlen(pango_layout_get_text(IB_TEXT(ci->data)->layout));
+ if (IS_IB_LINK(ib->focused_object) &&
+ text_position >= IB_LINK(ib->focused_object)->end) {
+ focus_next = TRUE;
+ }
+ }
+ }
+ ib->focused_object = NULL;
+ gtk_widget_queue_draw(widget);
+ return FALSE;
+}
+
+
+static void
+inline_box_class_init (InlineBoxClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ gobject_class->dispose = inline_box_dispose;
+ gobject_class->finalize = inline_box_finalize;
+
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+ widget_class->get_request_mode = inline_box_get_request_mode;
+ widget_class->get_preferred_width = inline_box_get_preferred_width;
+ widget_class->get_preferred_height_for_width =
+ inline_box_get_preferred_height_for_width;
+ widget_class->size_allocate = inline_box_size_allocate;
+ widget_class->draw = inline_box_draw;
+ widget_class->focus = inline_box_focus;
+
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS(klass);
+ container_class->child_type = inline_box_child_type;
+ container_class->add = inline_box_add;
+ container_class->remove = inline_box_remove;
+ container_class->forall = inline_box_forall;
+}
+
+static void
+inline_box_init (InlineBox *ib)
+{
+ gtk_widget_set_has_window(GTK_WIDGET(ib), FALSE);
+ gtk_widget_set_can_focus(GTK_WIDGET(ib), TRUE);
+ INLINE_BOX(ib)->children = NULL;
+ INLINE_BOX(ib)->links = NULL;
+ INLINE_BOX(ib)->focused_object = NULL;
+}
+
+
+InlineBox *inline_box_new ()
+{
+ InlineBox *ib = INLINE_BOX(g_object_new(inline_box_get_type(), NULL));
+ ib->selection_start = 0;
+ ib->selection_end = 0;
+ ib->children = NULL;
+ ib->last_child = NULL;
+ ib->wrap = TRUE;
+ return ib;
+}
+
+static void inline_box_dispose (GObject *object)
+{
+ InlineBox *ib = INLINE_BOX(object);
+ if (ib->children != NULL) {
+ GList *il, *next;
+ for (il = ib->children; il; il = next) {
+ next = il->next;
+ if (IS_IB_TEXT(il->data) || IS_IB_BREAK(il->data)) {
+ g_object_unref(il->data);
+ ib->children = g_list_remove(ib->children, il->data);
+ }
+ }
+ }
+ if (ib->links != NULL) {
+ g_list_free_full(ib->links, g_object_unref);
+ ib->links = NULL;
+ }
+ G_OBJECT_CLASS (inline_box_parent_class)->dispose (object);
+}
+
+static void inline_box_finalize (GObject *object)
+{
+ InlineBox *ib = INLINE_BOX(object);
+ g_list_free(ib->children);
+ G_OBJECT_CLASS (inline_box_parent_class)->finalize (object);
+}
+
+static GtkSizeRequestMode inline_box_get_request_mode (GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+inline_box_get_preferred_width(GtkWidget *widget, gint *minimal, gint *natural)
+{
+ GList *child;
+ gint child_min, child_nat, cur_natural;
+ *minimal = 0;
+ *natural = 0;
+ cur_natural = 0;
+ for(child = INLINE_BOX(widget)->children; child; child = child->next) {
+ if (GTK_IS_WIDGET(child->data)) {
+ gtk_widget_get_preferred_width(GTK_WIDGET(child->data),
+ &child_min, &child_nat);
+ if (*minimal < child_min) {
+ *minimal = child_min;
+ }
+ cur_natural += child_nat;
+ } else if (IS_IB_TEXT(child->data)) {
+ if (INLINE_BOX(widget)->wrap) {
+ if (IB_TEXT(child->data)->alloc.width > *minimal) {
+ *minimal = IB_TEXT(child->data)->alloc.width;
+ }
+ } else {
+ /* todo */
+ }
+ cur_natural += IB_TEXT(child->data)->alloc.width;
+ } else if (IS_IB_BREAK(child->data)) {
+ if (cur_natural > *natural) {
+ *natural = cur_natural;
+ }
+ cur_natural = 0;
+ }
+ }
+ if (cur_natural > *natural) {
+ *natural = cur_natural;
+ }
+}
+
+static void inline_box_get_preferred_height_for_width(GtkWidget *widget,
+ gint width,
+ gint *minimal,
+ gint *natural)
+{
+ GtkAllocation alloc, child_alloc;
+ GList *child;
+ gint child_min;
+ alloc.x = 0;
+ alloc.y = 0;
+ alloc.width = width;
+ alloc.height = 0;
+ *minimal = 0;
+ if (g_list_length(INLINE_BOX(widget)->children) > 0) {
+ /* todo: would be better to avoid reusing the same function, since
+ it actually allocates child window sizes. */
+ inline_box_size_allocate(widget, &alloc);
+ for(child = INLINE_BOX(widget)->children; child; child = child->next) {
+ child_min = 0;
+ if (GTK_IS_WIDGET(child->data)) {
+ gtk_widget_get_allocation(GTK_WIDGET(child->data), &child_alloc);
+ child_min = child_alloc.y + child_alloc.height;
+ } else if (IS_IB_TEXT(child->data)) {
+ child_min =
+ IB_TEXT(child->data)->alloc.y + IB_TEXT(child->data)->alloc.height;
+ }
+ if (*minimal < child_min) {
+ *minimal = child_min;
+ }
+ }
+ }
+ *natural = *minimal;
+}
+
+/* todo: this function is rather slow on larger books, in part because
+ of pango_layout_get_baseline, which is better to cache. Maybe
+ replace PangoLayout in IBText with its subtype, which would
+ include cached baseline. */
+int line_baseline(GList *iter, int full_width, gboolean wrap) {
+ int max_baseline = 0, line_width = 0, cur_baseline = 0;
+ for (; iter && (! IS_IB_BREAK(iter->data)); iter = iter->next) {
+ if (IS_IB_TEXT(iter->data)) {
+ cur_baseline = pango_layout_get_baseline(IB_TEXT(iter->data)->layout);
+ line_width += IB_TEXT(iter->data)->alloc.width;
+ } else if (GTK_IS_WIDGET(iter->data)) {
+ int w;
+ gtk_widget_get_preferred_width(iter->data, &w, NULL);
+ line_width += w;
+ }
+ if (wrap && (line_width > full_width)) {
+ break;
+ }
+ if (cur_baseline > max_baseline) {
+ max_baseline = cur_baseline;
+ }
+ }
+ max_baseline /= PANGO_SCALE;
+ return max_baseline;
+}
+
+static void
+inline_box_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
+{
+ gtk_widget_set_allocation(widget, allocation);
+
+ unsigned border_width =
+ gtk_container_get_border_width(GTK_CONTAINER(widget));
+ int full_width = allocation->width - 2 * border_width;
+ int extra_width = full_width;
+
+ int x = allocation->x + border_width;
+ int y = allocation->y + border_width;
+ int line_height = 0, max_baseline;
+
+ GList *iter = INLINE_BOX(widget)->children;
+
+ max_baseline = line_baseline(iter, full_width, INLINE_BOX(widget)->wrap);
+
+ for(; iter; iter = iter->next) {
+ if (GTK_IS_WIDGET(iter->data)) {
+
+ if(!gtk_widget_get_visible(iter->data))
+ continue;
+
+ GtkAllocation child_allocation;
+ gtk_widget_get_preferred_width(iter->data, &child_allocation.width, NULL);
+ gtk_widget_get_preferred_height(iter->data, &child_allocation.height, NULL);
+
+ if (extra_width < child_allocation.width && extra_width < full_width) {
+ x = allocation->x + border_width;
+ y += line_height;
+ extra_width = full_width;
+ line_height = 0;
+ max_baseline = line_baseline(iter, full_width, INLINE_BOX(widget)->wrap);
+ }
+
+ child_allocation.x = x;
+ child_allocation.y = y;
+ gtk_widget_size_allocate(iter->data, &child_allocation);
+ extra_width -= child_allocation.width;
+ x += child_allocation.width;
+ line_height = line_height > child_allocation.height
+ ? line_height
+ : child_allocation.height;
+ } else if (IS_IB_TEXT(iter->data)) {
+ IBText *ibt = IB_TEXT(iter->data);
+ if (INLINE_BOX(widget)->wrap && extra_width < ibt->alloc.width &&
+ extra_width < full_width) {
+ x = allocation->x + border_width;
+ y += line_height;
+ extra_width = full_width;
+ line_height = 0;
+ max_baseline = line_baseline(iter, full_width, INLINE_BOX(widget)->wrap);
+ }
+ int y_offset = max_baseline - pango_layout_get_baseline(ibt->layout) / PANGO_SCALE;
+ ibt->alloc.x = x;
+ ibt->alloc.y = y + y_offset;
+
+ if ((guint)x == allocation->x + border_width &&
+ INLINE_BOX(widget)->wrap &&
+ strcmp(pango_layout_get_text(IB_TEXT(iter->data)->layout), " ") == 0) {
+ /* A space in the beginning of a line, not in <pre> */
+ } else {
+ extra_width -= ibt->alloc.width;
+ x += ibt->alloc.width;
+ line_height = line_height > (ibt->alloc.height + y_offset)
+ ? line_height
+ : (ibt->alloc.height + y_offset);
+ }
+ } else if (IS_IB_BREAK(iter->data)) {
+ x = allocation->x + border_width;
+ y += line_height;
+ extra_width = full_width;
+ max_baseline = line_baseline(iter->next, full_width, INLINE_BOX(widget)->wrap);
+ }
+ }
+}
+
+static GType
+inline_box_child_type(GtkContainer *container)
+{
+ return GTK_TYPE_WIDGET;
+}
+
+void inline_box_add_text(InlineBox *container, IBText *text)
+{
+ container->last_child = g_list_append(container->last_child, text);
+ if (container->children == NULL) {
+ container->children = container->last_child;
+ }
+ if (container->last_child->next != NULL) {
+ container->last_child = container->last_child->next;
+ }
+ gtk_widget_queue_resize(GTK_WIDGET(container));
+}
+
+void inline_box_break(InlineBox *container)
+{
+ container->last_child = g_list_append(container->last_child, ib_break_new());
+ if (container->children == NULL) {
+ container->children = container->last_child;
+ }
+ if (container->last_child->next != NULL) {
+ container->last_child = container->last_child->next;
+ }
+ gtk_widget_queue_resize(GTK_WIDGET(container));
+}
+
+
+static void
+inline_box_add(GtkContainer *container, GtkWidget *widget)
+{
+ InlineBox *ib = INLINE_BOX(container);
+ ib->last_child = g_list_append(ib->last_child, widget);
+ if (ib->children == NULL) {
+ ib->children = ib->last_child;
+ }
+ if (ib->last_child->next != NULL) {
+ ib->last_child = ib->last_child->next;
+ }
+ gtk_widget_set_parent(widget, GTK_WIDGET(container));
+ if(gtk_widget_get_visible(widget))
+ gtk_widget_queue_resize(GTK_WIDGET(container));
+}
+
+static void
+inline_box_remove(GtkContainer *container, GtkWidget *widget)
+{
+ InlineBox *ib = INLINE_BOX (container);
+ gtk_widget_unparent (widget);
+ ib->children = g_list_remove (ib->children, widget);
+}
+
+static void
+inline_box_forall (GtkContainer *container, gboolean include_internals,
+ GtkCallback callback, gpointer callback_data)
+{
+ InlineBox *ib = INLINE_BOX (container);
+ GList *child, *next;
+ child = ib->children;
+ while (child) {
+ /* Current child can be removed and freed, so better remember the
+ next one before running the callback. */
+ next = child->next;
+ if (child && child->data && GTK_IS_WIDGET(child->data)) {
+ (* callback) (GTK_WIDGET(child->data), callback_data);
+ }
+ child = next;
+ }
+}
diff --git a/src/inlinebox.h b/src/inlinebox.h
new file mode 100644
index 0000000..03e5ec8
--- /dev/null
+++ b/src/inlinebox.h
@@ -0,0 +1,119 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ 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 <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef INLINE_BOX_H
+#define INLINE_BOX_H
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+
+/* inline box text */
+
+/* Using just a GObject for it (and not a GtkWidget), since it's a few
+ times slower with GtkWidget. */
+
+#define IB_TEXT_TYPE (ib_text_get_type())
+G_DECLARE_FINAL_TYPE (IBText, ib_text, IB, TEXT, GObject);
+
+struct _IBText {
+ GObject parent_instance;
+ PangoLayout *layout;
+ GtkAllocation alloc;
+};
+
+#define IS_IB_TEXT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), IB_TEXT_TYPE))
+
+IBText* ib_text_new (PangoLayout *layout);
+gboolean ib_text_at_point(IBText *ibt, gint x, gint y, gint *position);
+
+
+/* line break */
+
+#define IB_BREAK_TYPE (ib_break_get_type())
+G_DECLARE_FINAL_TYPE (IBBreak, ib_break, IB, BREAK, GObject);
+
+struct _IBBreak
+{
+ GObject parent_instance;
+};
+
+#define IS_IB_BREAK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), IB_BREAK_TYPE))
+IBBreak *ib_break_new ();
+
+
+
+/* link */
+
+#define IB_LINK_TYPE (ib_link_get_type())
+G_DECLARE_FINAL_TYPE (IBLink, ib_link, IB, LINK, GObject);
+
+struct _IBLink
+{
+ GObject parent_instance;
+ guint start;
+ guint end;
+ GList *objects; /* todo */
+ gchar *url;
+};
+
+#define IS_IB_LINK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), IB_LINK_TYPE))
+IBLink *ib_link_new (const gchar *url);
+
+
+/* inline box */
+
+#define INLINE_BOX_TYPE (inline_box_get_type())
+#define INLINE_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), INLINE_BOX_TYPE, InlineBox))
+#define INLINE_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), INLINE_BOX_TYPE, InlineBoxClass))
+#define IS_INLINE_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), INLINE_BOX_TYPE))
+#define IS_INLINE_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), INLINE_BOX_TYPE))
+
+typedef struct _InlineBox InlineBox;
+typedef struct _InlineBoxClass InlineBoxClass;
+
+struct _InlineBox
+{
+ GtkContainer parent_instance;
+ GList *children;
+ GList *last_child;
+ /* It would be cleaner to store links as children, but that would
+ require additional functions to manage children. Keeping a
+ separate list for now; probably it's not worth the complication,
+ and it's only needed to manage/draw focus. */
+ GList *links;
+ GObject *focused_object;
+ guint selection_start;
+ guint selection_end;
+ gboolean wrap;
+};
+
+struct _InlineBoxClass
+{
+ GtkContainerClass parent_class;
+};
+
+GType inline_box_get_type(void) G_GNUC_CONST;
+InlineBox *inline_box_new(void);
+void inline_box_add_text(InlineBox *container, IBText *text);
+void inline_box_break(InlineBox *container);
+
+G_END_DECLS
+
+#endif
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..65adadf
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,134 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ 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 <https://www.gnu.org/licenses/>.
+*/
+
+#include <glib.h>
+#include <gtk/gtk.h>
+#include "browserbox.h"
+
+gchar **start_uri = NULL;
+
+static GOptionEntry entries[] =
+{
+ { G_OPTION_REMAINING, 0, G_OPTION_FLAG_NONE,
+ G_OPTION_ARG_STRING_ARRAY, &start_uri, "URI", NULL },
+ { NULL }
+};
+
+static gboolean
+key_press_event_cb (GtkWidget *widget, GdkEventKey *ev, GtkStack *tabs)
+{
+ if (ev->state & GDK_CONTROL_MASK) {
+ if (ev->keyval == GDK_KEY_t) {
+ GtkWidget *browser_box = GTK_WIDGET(browser_box_new(NULL));
+ BROWSER_BOX(browser_box)->tabs = tabs;
+ gtk_stack_add_titled(tabs, browser_box, "New tab", "New tab");
+ gtk_widget_show_all(browser_box);
+ gtk_stack_set_visible_child(tabs, browser_box);
+ return TRUE;
+ } else if (ev->keyval == GDK_KEY_w) {
+ GtkWidget *current_tab = gtk_stack_get_visible_child(tabs);
+ if (current_tab) {
+ gtk_widget_destroy(current_tab);
+ }
+ return TRUE;
+ }
+ }
+ GtkWidget *current_tab = gtk_stack_get_visible_child(tabs);
+ if (current_tab != NULL) {
+ BrowserBox *bb = BROWSER_BOX(current_tab);
+ if (ev->keyval == GDK_KEY_Back || ev->keyval == GDK_KEY_BackSpace) {
+ return history_back(bb);
+ } else if (ev->keyval == GDK_KEY_Forward) {
+ return history_forward(bb);
+ }
+ }
+ return FALSE;
+}
+
+static gboolean
+button_press_event_cb (GtkWidget *widget, GdkEventButton *ev, GtkStack *tabs)
+{
+ GtkWidget *current_tab = gtk_stack_get_visible_child(tabs);
+ if (current_tab != NULL) {
+ BrowserBox *bb = BROWSER_BOX(current_tab);
+ if (ev->button == 8) {
+ return history_back(bb);
+ } else if (ev->button == 9) {
+ return history_forward(bb);
+ }
+ }
+ return FALSE;
+}
+
+static void activate (GtkApplication *app, gpointer user_data)
+{
+ GtkWidget *window;
+
+ window = gtk_application_window_new (app);
+ gtk_window_resize(GTK_WINDOW(window), 800, 800);
+ gtk_window_set_title (GTK_WINDOW (window), "WWWLite");
+
+ GtkWidget *evbox = gtk_event_box_new();
+ gtk_container_add (GTK_CONTAINER (window), evbox);
+
+ GtkWidget *box = block_box_new(0);
+ gtk_container_add (GTK_CONTAINER (evbox), box);
+
+ GtkWidget *switcher = gtk_stack_switcher_new();
+ gtk_container_add (GTK_CONTAINER (box), switcher);
+
+ GtkWidget *stack = gtk_stack_new();
+ gtk_stack_switcher_set_stack(GTK_STACK_SWITCHER(switcher), GTK_STACK(stack));
+ gtk_container_add (GTK_CONTAINER (box), stack);
+ gtk_box_set_child_packing(GTK_BOX(box), stack, TRUE, TRUE, 0, GTK_PACK_START);
+
+ GtkWidget *browser_box = GTK_WIDGET(browser_box_new(start_uri != NULL ? start_uri[0] : NULL));
+ gtk_stack_add_titled(GTK_STACK(stack), browser_box, "Tab 1", "Tab 1");
+ BROWSER_BOX(browser_box)->tabs = GTK_STACK(stack);
+
+ g_signal_connect (evbox, "key-press-event",
+ G_CALLBACK (key_press_event_cb), stack);
+ g_signal_connect (evbox, "button-release-event",
+ G_CALLBACK (button_press_event_cb), stack);
+
+ gtk_widget_show_all (window);
+
+ word_cache = g_hash_table_new((GHashFunc)wck_hash, (GEqualFunc)wck_equal);
+ return;
+}
+
+int
+main (int argc, char **argv)
+{
+ GOptionContext *context = g_option_context_new ("[URI]");
+ g_option_context_add_main_entries (context, entries, NULL);
+ g_option_context_add_group (context, gtk_get_option_group(TRUE));
+ GError *error = NULL;
+ GtkApplication *app;
+ int status;
+ if (! g_option_context_parse (context, &argc, &argv, &error)) {
+ g_print("Failed to parse arguments: %s\n", error->message);
+ exit(1);
+ }
+
+ app = gtk_application_new (NULL, G_APPLICATION_FLAGS_NONE);
+ g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
+ status = g_application_run (G_APPLICATION (app), argc, argv);
+ g_object_unref (app);
+
+ return status;
+}
diff --git a/src/tablebox.c b/src/tablebox.c
new file mode 100644
index 0000000..27e1f3d
--- /dev/null
+++ b/src/tablebox.c
@@ -0,0 +1,407 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ 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 <https://www.gnu.org/licenses/>.
+*/
+
+#include <gtk/gtk.h>
+#include <math.h>
+#include "tablebox.h"
+
+G_DEFINE_TYPE (TableCell, table_cell, BLOCK_BOX_TYPE);
+G_DEFINE_TYPE (TableBox, table_box, GTK_TYPE_CONTAINER);
+
+
+/* cell */
+
+static void
+table_cell_class_init (TableCellClass *klass)
+{
+ return;
+}
+
+static void
+table_cell_init (TableCell *tc)
+{
+ tc->colspan = 1;
+ tc->rowspan = 1;
+ return;
+}
+
+GtkWidget *
+table_cell_new ()
+{
+ TableCell *tc = TABLE_CELL(g_object_new(table_cell_get_type(),
+ "orientation", GTK_ORIENTATION_VERTICAL,
+ "spacing", 10,
+ NULL));
+ return GTK_WIDGET(tc);
+}
+
+
+/* table */
+
+static GType table_box_child_type (GtkContainer *container);
+static void table_box_add (GtkContainer *container, GtkWidget *widget);
+static void table_box_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation);
+static void table_box_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback, gpointer callback_data);
+static void table_box_remove (GtkContainer *container, GtkWidget *widget);
+static GtkSizeRequestMode table_box_get_request_mode (GtkWidget *widget);
+static void table_box_get_preferred_width(GtkWidget *widget,
+ gint *minimal, gint *natural);
+static void table_box_get_preferred_height_for_width(GtkWidget *widget,
+ gint width,
+ gint *minimal,
+ gint *natural);
+static void table_box_column_widths (TableBox *tb, GList **min_widths, GList **nat_widths);
+static void table_box_finalize (GObject *object);
+
+static guint padding = 10;
+
+static void
+table_box_class_init (TableBoxClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ gobject_class->finalize = table_box_finalize;
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+ widget_class->size_allocate = table_box_size_allocate;
+ widget_class->get_request_mode = table_box_get_request_mode;
+ widget_class->get_preferred_width = table_box_get_preferred_width;
+ widget_class->get_preferred_height_for_width =
+ table_box_get_preferred_height_for_width;
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS(klass);
+ container_class->child_type = table_box_child_type;
+ container_class->add = table_box_add;
+ container_class->forall = table_box_forall;
+ container_class->remove = table_box_remove;
+ return;
+}
+
+static void
+table_box_init (TableBox *tb)
+{
+ gtk_widget_set_has_window(GTK_WIDGET(tb), FALSE);
+ tb->rows = NULL;
+}
+
+static void table_box_finalize (GObject *object)
+{
+ TableBox *tb = TABLE_BOX(object);
+ g_list_free_full(tb->rows, (GDestroyNotify)g_list_free);
+ G_OBJECT_CLASS (table_box_parent_class)->finalize (object);
+}
+
+GtkWidget *
+table_box_new ()
+{
+ TableBox *tb = TABLE_BOX(g_object_new(table_box_get_type(),
+ NULL));
+ return GTK_WIDGET(tb);
+}
+
+void
+table_box_add_row (TableBox *tb)
+{
+ tb->rows = g_list_append(tb->rows, NULL);
+ return;
+}
+
+static GtkSizeRequestMode
+table_box_get_request_mode (GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+table_box_get_preferred_width(GtkWidget *widget,
+ gint *minimal, gint *natural)
+{
+ TableBox *tb = TABLE_BOX(widget);
+ GList *minimal_widths, *natural_widths, *iter;
+ table_box_column_widths(tb, &minimal_widths, &natural_widths);
+
+ if (minimal != NULL) {
+ *minimal = -padding;
+ for (iter = minimal_widths; iter; iter = iter->next) {
+ *minimal += GPOINTER_TO_INT(iter->data) + padding;
+ }
+ }
+ if (natural != NULL) {
+ *natural = -padding;
+ for (iter = natural_widths; iter; iter = iter->next) {
+ *natural += GPOINTER_TO_INT(iter->data) + padding;
+ }
+ }
+ g_list_free(minimal_widths);
+ g_list_free(natural_widths);
+}
+
+static void
+table_box_get_preferred_height_for_width(GtkWidget *widget,
+ gint width,
+ gint *minimal,
+ gint *natural)
+{
+ GtkAllocation alloc, child_alloc;
+ alloc.x = 0;
+ alloc.y = 0;
+ alloc.width = width;
+ alloc.height = 0;
+ table_box_size_allocate(widget, &alloc);
+
+ TableBox *tb = TABLE_BOX(widget);
+ GList *row, *cell;
+ *minimal = 0;
+ for (row = tb->rows; row; row = row->next) {
+ for (cell = row->data; cell; cell = cell->next) {
+ gtk_widget_get_allocation(cell->data, &child_alloc);
+ if (*minimal < child_alloc.y + child_alloc.height) {
+ *minimal = child_alloc.y + child_alloc.height;
+ }
+ }
+ }
+ *natural = *minimal;
+}
+
+static GType
+table_box_child_type (GtkContainer *container)
+{
+ return TABLE_CELL_TYPE;
+}
+
+static void
+table_box_add (GtkContainer *container, GtkWidget *widget)
+{
+ TableBox *tb = TABLE_BOX(container);
+ GList *row = g_list_last(tb->rows);
+ row->data = g_list_append(row->data, widget);
+ gtk_widget_set_parent(widget, GTK_WIDGET(container));
+ if (gtk_widget_get_visible(widget))
+ gtk_widget_queue_resize(GTK_WIDGET(container));
+}
+
+static void
+table_box_forall (GtkContainer *container, gboolean include_internals,
+ GtkCallback callback, gpointer callback_data)
+{
+ GList *row, *cell, *next_row, *next_cell;
+ row = TABLE_BOX(container)->rows;
+ while (row) {
+ next_row = row->next;
+ cell = row->data;
+ while (cell) {
+ next_cell = cell->next;
+ (* callback) (GTK_WIDGET(cell->data), callback_data);
+ cell = next_cell;
+ }
+ row = next_row;
+ }
+}
+
+static void
+table_box_remove (GtkContainer *container, GtkWidget *widget)
+{
+ TableBox *tb = TABLE_BOX (container);
+ gtk_widget_unparent (widget);
+ GList *row, *cell;
+ for (row = tb->rows; row; row = row->next) {
+ for (cell = row->data; cell; cell = cell->next) {
+ if (cell->data == widget) {
+ row->data = g_list_delete_link(row->data, cell);
+ return;
+ }
+ }
+ }
+}
+
+static void
+table_box_column_widths (TableBox *tb, GList **min_widths, GList **nat_widths)
+{
+ /* todo: would be nice to ensure that all the columns end up being
+ of approximately the same height */
+ GList *row, *cell;
+ GList *minimal_widths = NULL;
+ GList *natural_widths = NULL;
+ GList *descending_cells = NULL;
+ gint cell_x, cell_y, cell_next_x;
+ cell_y = 0;
+ for (row = tb->rows; row; row = row->next) {
+ cell_x = 0;
+ for (cell = row->data; cell; cell = cell->next) {
+ /* Skip the cells spanning from above */
+ while (g_list_nth_data(descending_cells, cell_x)) {
+ cell_x++;
+ }
+
+ TableCell *tc = cell->data;
+ gint min, nat;
+ gtk_widget_get_preferred_width(GTK_WIDGET(tc), &min, &nat);
+ min /= tc->colspan;
+ nat /= tc->colspan;
+ for (cell_next_x = cell_x; cell_next_x < cell_x + tc->colspan; cell_next_x++) {
+ while (g_list_nth(minimal_widths, cell_next_x) == NULL) {
+ minimal_widths = g_list_append(minimal_widths, GINT_TO_POINTER(0));
+ }
+ while (g_list_nth(natural_widths, cell_next_x) == NULL) {
+ natural_widths = g_list_append(natural_widths, GINT_TO_POINTER(0));
+ }
+ while (g_list_nth(descending_cells, cell_next_x) == NULL) {
+ descending_cells = g_list_append(descending_cells, GINT_TO_POINTER(0));
+ }
+ GList *col_min_width = g_list_nth(minimal_widths, cell_next_x);
+ if (GPOINTER_TO_INT(col_min_width->data) < min) {
+ col_min_width->data = GINT_TO_POINTER(min);
+ }
+ GList *col_nat_width = g_list_nth(natural_widths, cell_next_x);
+ if (GPOINTER_TO_INT(col_nat_width->data) < nat) {
+ col_nat_width->data = GINT_TO_POINTER(nat);
+ }
+ /* Update descending cells */
+ GList *descending_cell = g_list_nth(descending_cells, cell_next_x);
+ descending_cell->data = GINT_TO_POINTER(tc->rowspan);
+ }
+ cell_x += tc->colspan;
+ }
+ GList *descending_cell;
+ for (descending_cell = descending_cells; descending_cell;
+ descending_cell = descending_cell->next) {
+ if (GPOINTER_TO_INT(descending_cell->data) > 0) {
+ descending_cell->data--;
+ }
+ }
+ cell_y++;
+ }
+ *min_widths = minimal_widths;
+ *nat_widths = natural_widths;
+ g_list_free(descending_cells);
+}
+
+static void
+table_box_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
+{
+ gtk_widget_set_allocation(widget, allocation);
+ guint border_width = gtk_container_get_border_width(GTK_CONTAINER(widget));
+ gint full_width = allocation->width - 2 * border_width;
+ TableBox *tb = TABLE_BOX(widget);
+ GList *row, *cell;
+ gint x;
+ gint y = allocation->y + border_width;
+ gint row_height;
+
+ GList *minimal_widths = NULL;
+ GList *natural_widths = NULL;
+ gint *descending_cells;
+ gint *pending_heights;
+ gint *actual_widths;
+ gint cell_x, cell_y, cell_next_x;
+
+ int natural_width;
+ table_box_get_preferred_width(widget, NULL, &natural_width);
+ gdouble shrinking = (gdouble)natural_width / (gdouble)full_width;
+
+ table_box_column_widths(tb, &minimal_widths, &natural_widths);
+ gint col_cnt = g_list_length(minimal_widths);
+
+ descending_cells = g_malloc0(sizeof(gint) * col_cnt);
+ pending_heights = g_malloc0(sizeof(gint) * col_cnt);
+ actual_widths = g_malloc0(sizeof(gint) * col_cnt);
+
+ gint extra_width = full_width + padding;
+
+ /* Assign minimal widths to columns */
+ for (cell_x = 0, cell = minimal_widths; cell; cell_x++, cell = cell->next) {
+ gint minimal_width = GPOINTER_TO_INT(cell->data);
+ actual_widths[cell_x] = minimal_width;
+ extra_width -= actual_widths[cell_x] + padding;
+ }
+ /* Distribute remaining width */
+ for (cell_x = 0, cell = natural_widths; cell && extra_width > 0; cell_x++, cell = cell->next) {
+ gint natural_width = GPOINTER_TO_INT(cell->data);
+ if (shrinking <= 1.0) {
+ extra_width -= natural_width - actual_widths[cell_x];
+ actual_widths[cell_x] = natural_width;
+ } else if (natural_width / shrinking > actual_widths[cell_x]) {
+ if (extra_width > natural_width / shrinking - actual_widths[cell_x]) {
+ extra_width -= natural_width / shrinking - actual_widths[cell_x];
+ actual_widths[cell_x] = natural_width / shrinking;
+ } else {
+ actual_widths[cell_x] += extra_width;
+ extra_width = 0;
+ }
+ }
+ }
+
+ cell_y = 0;
+ for (row = tb->rows; row; row = row->next) {
+ cell_x = 0;
+ x = allocation->x + border_width;
+ row_height = 0;
+ for (cell = row->data; cell; cell = cell->next) {
+ /* Skip the cells spanning from above */
+ while (cell_x < col_cnt && descending_cells[cell_x] > 0) {
+ x += actual_widths[cell_x] + padding;
+ cell_x++;
+ }
+ TableCell *tc = cell->data;
+ GtkAllocation child_alloc;
+ child_alloc.width = -padding;
+ for (cell_next_x = cell_x; cell_next_x < cell_x + tc->colspan; cell_next_x++) {
+ child_alloc.width += actual_widths[cell_next_x] + padding;
+ }
+ gtk_widget_get_preferred_height_for_width(cell->data, child_alloc.width,
+ &child_alloc.height, NULL);
+ child_alloc.x = x;
+ child_alloc.y = y;
+ gtk_widget_size_allocate(cell->data, &child_alloc);
+ x += child_alloc.width + padding;
+
+ for (cell_next_x = cell_x; cell_next_x < cell_x + tc->colspan; cell_next_x++) {
+ /* Update descending cells and pending heights */
+ descending_cells[cell_next_x] = tc->rowspan;
+ pending_heights[cell_next_x] = child_alloc.height;
+ }
+ cell_x += tc->colspan;
+ }
+ /* Update descending cells and pending heights, and row_height
+ based on those. */
+ for (cell_x = 0; cell_x < col_cnt; cell_x++) {
+ if (descending_cells[cell_x] > 0) {
+ descending_cells[cell_x]--;
+ if (descending_cells[cell_x] == 0) {
+ if (pending_heights[cell_x] > row_height) {
+ row_height = pending_heights[cell_x];
+ }
+ }
+ }
+ }
+ for (cell_x = 0; cell_x < (gint)g_list_length(minimal_widths); cell_x++) {
+ if (pending_heights[cell_x] > row_height) {
+ pending_heights[cell_x] -= row_height;
+ } else {
+ pending_heights[cell_x] = 0;
+ }
+ }
+ y += row_height;
+ cell_y++;
+ }
+
+ g_list_free(minimal_widths);
+ g_list_free(natural_widths);
+ free(actual_widths);
+ free(descending_cells);
+ free(pending_heights);
+}
diff --git a/src/tablebox.h b/src/tablebox.h
new file mode 100644
index 0000000..3848e30
--- /dev/null
+++ b/src/tablebox.h
@@ -0,0 +1,84 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ 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 <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef TABLE_BOX_H
+#define TABLE_BOX_H
+
+#include <gtk/gtk.h>
+#include "blockbox.h"
+
+G_BEGIN_DECLS
+
+/* cell */
+
+#define TABLE_CELL_TYPE (table_cell_get_type())
+#define TABLE_CELL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), TABLE_CELL_TYPE, TableCell))
+#define TABLE_CELL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), TABLE_CELL_TYPE, TableCellClass))
+#define IS_TABLE_CELL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), TABLE_CELL_TYPE))
+#define IS_TABLE_CELL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), TABLE_CELL_TYPE))
+
+typedef struct _TableCell TableCell;
+typedef struct _TableCellClass TableCellClass;
+
+struct _TableCell
+{
+ BlockBox parent_instance;
+ gint rowspan;
+ gint colspan;
+};
+
+struct _TableCellClass
+{
+ BlockBoxClass parent_class;
+};
+
+GType table_cell_get_type(void) G_GNUC_CONST;
+GtkWidget *table_cell_new();
+
+
+/* table */
+
+#define TABLE_BOX_TYPE (table_box_get_type())
+#define TABLE_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), TABLE_BOX_TYPE, TableBox))
+#define TABLE_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), TABLE_BOX_TYPE, TableBoxClass))
+#define IS_TABLE_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), TABLE_BOX_TYPE))
+#define IS_TABLE_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), TABLE_BOX_TYPE))
+
+typedef struct _TableBox TableBox;
+typedef struct _TableBoxClass TableBoxClass;
+
+struct _TableBox
+{
+ GtkContainer parent_instance;
+ GList *rows;
+};
+
+struct _TableBoxClass
+{
+ GtkContainerClass parent_class;
+};
+
+GType table_box_get_type(void) G_GNUC_CONST;
+GtkWidget *table_box_new();
+void table_box_add_row(TableBox *tb);
+/* void table_box_get_dimensions (TableBox *tb, guint *cols, guint *rows); */
+/* void table_box_get_column_widths (TableBox *tb, gint *minimal, gint *natural); */
+
+
+G_END_DECLS
+
+#endif