#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;
}
}
gchar*
inline_box_get_text (InlineBox *ib)
{
GList *child;
gchar **words = calloc(g_list_length(ib->children) + 1, sizeof(gchar*));
guint n = 0;
for (child = ib->children; child; child = child->next) {
if (IS_IB_TEXT(child->data)) {
words[n] = (gchar*)pango_layout_get_text(IB_TEXT(child->data)->layout);
n++;
}
}
gchar *result = g_strjoinv(NULL, words);
free(words);
return result;
}
guint
inline_box_get_text_length (InlineBox *ib)
{
GList *child;
guint len = 0;
for (child = ib->children; child; child = child->next) {
if (IS_IB_TEXT(child->data)) {
len += strlen(pango_layout_get_text(IB_TEXT(child->data)->layout));
}
}
return len;
}
gint
inline_box_search (InlineBox *ib,
guint start,
gint end,
const gchar *str)
{
gchar *orig_text = inline_box_get_text(ib);
gchar *text = g_utf8_strdown(orig_text, -1);
g_free(orig_text);
if (end != -1) {
end -= start;
}
gchar *result = g_strstr_len(text + start, end, str);
g_free(text);
if (result != NULL) {
return result - text;
}
return -1;
}