summaryrefslogtreecommitdiff
path: root/src/inlinebox.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/inlinebox.c')
-rw-r--r--src/inlinebox.c585
1 files changed, 585 insertions, 0 deletions
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;
+ }
+}