From 5772d91183ad4f3a8dc1d5c469bc7d295764b80c Mon Sep 17 00:00:00 2001 From: defanor Date: Sat, 17 Aug 2019 23:22:42 +0300 Subject: Add the prototype --- src/inlinebox.c | 585 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 585 insertions(+) create mode 100644 src/inlinebox.c (limited to 'src/inlinebox.c') 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 . +*/ + +#include +#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
 */
+      } 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;
+  }
+}
-- 
cgit v1.2.3