Cursor is misaligned when being moved

Hi,

how can I fix the misalignment when moving the cursor in the below textbox drawn on a Gtk drawing area?

cursor

    switch (event->keyval)
	{
		case GDK_KEY_Left:
			iter = pango_layout_get_iter(textbox->layout);
			pango_layout_iter_get_char_extents(iter, &rect);
			textbox->cursor_x -= rect.width / PANGO_SCALE;
			pango_layout_iter_free (iter);
		break;

        case GDK_KEY_Right:
			iter = pango_layout_get_iter(textbox->layout);
			pango_layout_iter_get_char_extents(iter, &rect);
            textbox->cursor_x += rect.width / PANGO_SCALE;
			pango_layout_iter_free (iter);
        break;

The cursor is being moved with:

// Draw the cursor
	textbox->cursor_visible = !textbox->cursor_visible;
	if (textbox->cursor_visible)
	{
		gtk_render_insertion_cursor(context, cr,
									textbox->cursor_x, textbox->cursor_y,
									textbox->layout, textbox->text->len,
									PANGO_DIRECTION_LTR);
	}

Probably not what you’re looking to hear, but that’s outdated Gtk 3 API that’s no longer present in Gtk 4. The new API is built around cursor positioning using iters, so there are no x/y offsets to confuse things. And because iters are invalidated by buffer manipulations, the cursor can also be represented within the GtkTextBuffer itself as a GtkTextMark, a particular kind of placeholder markup. The Gtk 4 Text Widget Overview explains:

Most text manipulation is accomplished with iterators, represented by a GtkTextIter. An iterator represents a position between two characters in the text buffer. GtkTextIter is a struct designed to be allocated on the stack; it’s guaranteed to be copiable by value and never contain any heap-allocated data. Iterators are not valid indefinitely; whenever the buffer is modified in a way that affects the number of characters in the buffer, all outstanding iterators become invalid. (Note that deleting 5 characters and then reinserting 5 still invalidates iterators, though you end up with the same number of characters you pass through a state with a different number).

Because of this, iterators can’t be used to preserve positions across buffer modifications. To preserve a position, the GtkTextMark object is ideal. You can think of a mark as an invisible cursor or insertion point; it floats in the buffer, saving a position. If the text surrounding the mark is deleted, the mark remains in the position the text once occupied; if text is inserted at the mark, the mark ends up either to the left or to the right of the new text, depending on its gravity. The standard text cursor in left-to-right languages is a mark with right gravity, because it stays to the right of inserted text.

Like tags, marks can be either named or anonymous. There are two marks built-in to GtkTextBuffer; these are named “insert” and “selection_bound” and refer to the insertion point and the boundary of the selection which is not the insertion point, respectively. If no text is selected, these two marks will be in the same position. You can manipulate what is selected and where the cursor appears by moving these marks around.

If you want to place the cursor in response to a user action, be sure to use gtk_text_buffer_place_cursor(), which moves both at once without causing a temporary selection (moving one then the other temporarily selects the range in between the old and new positions).

It also mentions, at the end of the page:

The gtk4-demo application that comes with GTK contains more example code for GtkTextView.

…That being said, the gtk3-demo application has example GtkTextView code as well, and it seems to follow a similar pattern. It never once calls gtk_render_insert_cursor() — I don’t think you’re meant to be doing that, as the application. What it does do is query the cursor position from the buffer, using similar principles to the Gtk 4 overview:

(The mark_set_callback is bound to the GtkTextBuffer’s mark-set signal, in the UI definition. It will report cursor motions performed by the user; the widget itself handles both the moving and the drawing.)

static void
mark_set_callback (GtkTextBuffer         *buffer,
                   const GtkTextIter     *new_location,
                   GtkTextMark           *mark,
                   DemoApplicationWindow *window)
{

The buffer’s changed signal is also bound directly to update_statusbar.

static void
update_statusbar (GtkTextBuffer         *buffer,
                  DemoApplicationWindow *window)
{
  gchar *msg;
  gint row, col;
  gint count;
  GtkTextIter iter;

  /* clear any previous message, underflow is allowed */
  gtk_statusbar_pop (GTK_STATUSBAR (window->status), 0);

  count = gtk_text_buffer_get_char_count (buffer);

  gtk_text_buffer_get_iter_at_mark (buffer,
                                    &iter,
                                    gtk_text_buffer_get_insert (buffer));

  row = gtk_text_iter_get_line (&iter);
  col = gtk_text_iter_get_line_offset (&iter);

  msg = g_strdup_printf ("Cursor at row %d column %d - %d chars in document",
                         row, col, count);

  gtk_statusbar_push (GTK_STATUSBAR (window->status), 0, msg);

  g_free (msg);
}

The application.ui code that creates the GtkTextView and its buffer, omitting the tool, menu, and statusbars:

<?xml version="1.0"?>
<interface>
<template class="DemoApplicationWindow" parent="GtkApplicationWindow">
  <property name="title" translatable="yes">Application Class</property>
  <property name="default-width">200</property>
  <property name="default-height">200</property>
  <property name="icon-name">document-open</property>
  <child>
    <object class="GtkGrid">
      <property name="visible">1</property>
      <child>
        <object class="GtkScrolledWindow">
          <property name="visible">1</property>
          <property name="shadow-type">in</property>
          <child>
            <object class="GtkTextView">
              <property name="visible">1</property>
              <property name="hexpand">1</property>
              <property name="vexpand">1</property>
              <property name="buffer">buffer</property>
            </object>
          </child>
        </object>
        <packing>
          <property name="left-attach">0</property>
          <property name="top-attach">2</property>
        </packing>
      </child>
    </object>
  </child>
</template>
<object class="GtkTextBuffer" id="buffer">
  <signal name="changed" handler="update_statusbar"/>
  <signal name="mark-set" handler="mark_set_callback"/>
</object>

Hi thank you for the long explanation but I’m using GtkDrawingArea to draw the textbox and I don’t have access to GtkTextBuffer.

Is the misalignment equal to a constant value? If Yes, then you need to set the initial value of cursor_x in different manner.

Otherwise it is useful to monitor how cursor_x is changing. Can it get negative? The visible part of your code does not prevent it.

You seem to use one and the same value (length of the string?) in the index parameter of gtk_render_insertion_cursor, which can be the cause for the issue. You also might want to move the iter, created by pango_layout_get_iter, to the position of the character whose dimensions you want to get.

Hi, thank you for your reply. I solved by using a combination of pango_layout_xy_to_index() and pango_layout_get_cursor_pos() and by drawing the cursor with cairo calls.

1 Like

This topic was automatically closed 45 days after the last reply. New replies are no longer allowed.