mirror of
https://gitlab.gnome.org/GNOME/gtk.git
synced 2024-11-14 12:41:07 +00:00
de9fb93157
Tue Feb 29 13:29:17 GMT 2000 Tony Gale <gale@gtk.org> missed a couple of corrections
488 lines
21 KiB
Plaintext
488 lines
21 KiB
Plaintext
Date: Sun, 14 Sep 1997 20:17:06 -0700 (PDT)
|
|
From: Josh MacDonald <jmacd@CS.Berkeley.EDU>
|
|
To: gnome@athena.nuclecu.unam.mx, gtk-list@redhat.com
|
|
Subject: [gtk-list] gtktext widget internal documentation
|
|
|
|
|
|
Pete convinced me to just write up the text widget and let someone else
|
|
finish it. I'm pretty busy and have other commitments now. Sorry. I think
|
|
I'm not the most qualified for some of the remaining work anyway, because I
|
|
don't really know Gtk and it's event model very well. Most of the work so
|
|
far was possible without knowing Gtk all that well, it was simply a data
|
|
structure exercise (though after reading this you might say it was a fairly
|
|
complicated data structure exercise). I'm happy to answer questions.
|
|
|
|
-josh
|
|
|
|
|
|
High level description:
|
|
|
|
There are several layers of data structure to the widget. They are
|
|
seperated from each other as much as possible. The first is a gapped
|
|
text segment similar to the data structure Emacs uses for representing
|
|
text. Then there is a property list, which stores text properties for
|
|
various ranges of text. There is no direct relation between the text
|
|
property list and the gapped text segment. Finally there is a drawn
|
|
line parameter cache to speed calculations when drawing and redrawing
|
|
lines on screen. In addition to these data structures, there are
|
|
structures to help iterate over text in the buffer.
|
|
|
|
The gapped text segment is quite simple. It's parameters are (all
|
|
parameters I mention here are in the structure GtkText):
|
|
|
|
guchar* text;
|
|
guint text_len;
|
|
guint gap_position;
|
|
guint gap_size;
|
|
guint text_end;
|
|
|
|
TEXT is the buffer, TEXT_LEN is its allocated length. TEXT_END is the
|
|
length of the text, including the gap. GAP_POSITION is the start of
|
|
the gap, and GAP_SIZE is the gap's length. Therefore, TEXT_END -
|
|
GAP_SIZE is the length of the text in the buffer. The macro
|
|
TEXT_LENGTH returns this value. To get the value of a character in
|
|
the buffer, use the macro TEXT_INDEX(TEXT,INDEX). This macro tests
|
|
whether the index is less than the GAP_POSITION and returns
|
|
TEXT[INDEX] or returns TEXT[GAP_SIZE+INDEX]. The function
|
|
MOVE_GAP_TO_POINT positions the gap to a particular index. The
|
|
function MAKE_FORWARD_SPACE lengthens the gap to provide room for a
|
|
certain number of characters.
|
|
|
|
The property list is a doubly linked list (GList) of text property
|
|
data for each contiguous set of characters with similar properties.
|
|
The data field of the GList points to a TextProperty structure, which
|
|
contains:
|
|
|
|
TextFont* font;
|
|
GdkColor* back_color;
|
|
GdkColor* fore_color;
|
|
guint length;
|
|
|
|
Currently, only font and color data are contained in the property
|
|
list, but it can be extended by modifying the INSERT_TEXT_PROPERTY,
|
|
TEXT_PROPERTIES_EQUAL, and a few other procedures. The text property
|
|
structure does not contain an absolute offset, only a length. As a
|
|
result, inserting a character into the buffer simply requires moving
|
|
the gap to the correct position, making room in the buffer, and either
|
|
inserting a new property or extending the old one. This logic is done
|
|
by INSERT_TEXT_PROPERTY. A similar procedure exists to delete from
|
|
the text property list, DELETE_TEXT_PROPERTY. Since the property
|
|
structure doesn't contain an offset, insertion into the list is an
|
|
O(1) operation. All such operations act on the insertion point, which
|
|
is the POINT field of the GtkText structure.
|
|
|
|
The GtkPropertyMark structure is used for keeping track of the mapping
|
|
between absolute buffer offsets and positions in the property list.
|
|
These will be referred to as property marks. Generally, there are
|
|
four property marks the system keeps track of. Two are trivial, the
|
|
beginning and the end of the buffer are easy to find. The other two
|
|
are the insertion point (POINT) and the cursor point (CURSOR_MARK).
|
|
All operations on the text buffer are done using a property mark as a
|
|
sort of cursor to keep track of the alignment of the property list and
|
|
the absolute buffer offset. The GtkPropertyMark structure contains:
|
|
|
|
GList* property;
|
|
guint offset;
|
|
guint index;
|
|
|
|
PROPERTY is a pointer at the current property list element. INDEX is
|
|
the absolute buffer index, and OFFSET is the offset of INDEX from the
|
|
beginning of PROPERTY. It is essential to keep property marks valid,
|
|
or else you will have the wrong text properties at each property mark
|
|
transition. An important point is that all property marks are invalid
|
|
after a buffer modification unless care is taken to keep them
|
|
accurate. That is the difficulty of the insert and delete operations,
|
|
because as the next section describes, line data is cached and by
|
|
neccesity contains text property marks. The functions for operating
|
|
and computing property marks are:
|
|
|
|
void advance_mark (GtkPropertyMark* mark);
|
|
void decrement_mark (GtkPropertyMark* mark);
|
|
void advance_mark_n (GtkPropertyMark* mark, gint n);
|
|
void decrement_mark_n (GtkPropertyMark* mark, gint n);
|
|
void move_mark_n (GtkPropertyMark* mark, gint n);
|
|
|
|
GtkPropertyMark find_mark (GtkText* text, guint mark_position);
|
|
GtkPropertyMark find_mark_near (GtkText* text, guint mark_position,
|
|
const GtkPropertyMark* near);
|
|
|
|
ADVANCE_MARK and DECREMENT_MARK modify the mark by plus or minus one
|
|
buffer index. ADVANCE_MARK_N and DECREMENT_MARK_N modify the mark by
|
|
plus or minus N indices. MOVE_MARK_N accepts a positive or negative
|
|
argument. FIND_MARK returns a mark at MARK_POSITION using a linear
|
|
search from the nearest known property mark (the beginning, the end,
|
|
the point, etc). FIND_MARK_NEAR also does a linear search, but
|
|
searches from the NEAR argument. A number of macros exist at the top
|
|
of the file for doing things like getting the current text property,
|
|
or some component of the current property. See the MARK_* macros.
|
|
|
|
Next there is a LineParams structure which contains all the
|
|
information neccesary to draw one line of text on screen. When I say
|
|
"line" here, I do not mean one line of text seperated by newlines,
|
|
rather I mean one row of text on screen. It is a matter of policy how
|
|
visible lines are chosen and there are currently two policies,
|
|
line-wrap and no-line-wrap. I suspect it would not be difficult to
|
|
implement new policies for doing such things as justification. The
|
|
LineParams structure includes the following fields:
|
|
|
|
guint font_ascent;
|
|
guint font_descent;
|
|
guint pixel_width;
|
|
guint displayable_chars;
|
|
guint wraps : 1;
|
|
|
|
PrevTabCont tab_cont;
|
|
PrevTabCont tab_cont_next;
|
|
|
|
GtkPropertyMark start;
|
|
GtkPropertyMark end;
|
|
|
|
FONT_ASCENT and FONT_DESCENT are the maximum ascent and descent of any
|
|
character in the line. PIXEL_WIDTH is the number of pixels wide the
|
|
drawn region is, though I don't think it's actually being used
|
|
currently. You may wish to remove this field, eventually, though I
|
|
suspect it will come in handy implementing horizontal scrolling.
|
|
DISPLAYABLE_CHARS is the number of characters in the line actually
|
|
drawn. This may be less than the number of characters in the line
|
|
when line wrapping is off (see below). The bitflag WRAPS tells
|
|
whether the next line is a continuation of this line. START and END
|
|
are the marks at the beginning and end of the line. Note that END is
|
|
the actual last character, not one past it, so the smallest line
|
|
(containing, for example, one newline) has START == END. TAB_CONT and
|
|
TAB_CONT_NEXT are for computation of tab positions. I will discuss
|
|
them later.
|
|
|
|
A point about the end of the buffer. You may be tempted to consider
|
|
working with the buffer as an array of length TEXT_LENGTH(TEXT), but
|
|
you have to be careful that the editor allows you to position your
|
|
cursor at the last index of the buffer, one past the last character.
|
|
The macro LAST_INDEX(TEXT, MARK) returns true if MARK is positioned at
|
|
this index. If you see or add a special case in the code for this
|
|
end-of-buffer case, make sure to use LAST_INDEX if you can. Very
|
|
often, the last index is treated as a newline.
|
|
|
|
[ One way the last index is special is that, although it is always
|
|
part of some property, it will never be part of a property of
|
|
length 1 unless there are no other characters in the text. That
|
|
is, its properties are always that of the preceding character,
|
|
if any.
|
|
|
|
There is a fair bit of special case code to mantain this condition -
|
|
which is needed so that user has control over the properties of
|
|
characters inserted at the last position. OWT 2/9/98 ]
|
|
|
|
Tab stops are variable width. A list of tab stops is contained in the
|
|
GtkText structure:
|
|
|
|
GList *tab_stops;
|
|
gint default_tab_width;
|
|
|
|
The elements of tab_stops are integers casted to gpointer. This is a
|
|
little bogus, but works. For example:
|
|
|
|
text->default_tab_width = 4;
|
|
text->tab_stops = NULL;
|
|
text->tab_stops = g_list_prepend (text->tab_stops, (void*)8);
|
|
text->tab_stops = g_list_prepend (text->tab_stops, (void*)8);
|
|
|
|
is how these fields are initialized, currently. This means that the
|
|
first two tabs occur at 8 and 16, and every 4 characters thereafter.
|
|
Tab stops are used in the computation of line geometry (to fill in a
|
|
LineParams structure), and the width of the space character in the
|
|
current font is used. The PrevTabCont structure, of which two are
|
|
stored per line, is used to compute the geometry of lines which may
|
|
have wrapped and carried part of a tab with them:
|
|
|
|
guint pixel_offset;
|
|
TabStopMark tab_start;
|
|
|
|
PIXEL_OFFSET is the number of pixels at which the line should start,
|
|
and tab_start is a tab stop mark, which is similar to a property mark,
|
|
only it keeps track of the mapping between line position (column) and
|
|
the next tab stop. A TabStopMark contains:
|
|
|
|
GList* tab_stops;
|
|
gint to_next_tab;
|
|
|
|
TAB_STOPS is a pointer into the TAB_STOPS field of the GtkText
|
|
structure. TO_NEXT_TAB is the number of characters before the next
|
|
tab. The functions ADVANCE_TAB_MARK and ADVANCE_TAB_MARK_N advance
|
|
these marks. The LineParams structure contains two PrevTabCont
|
|
structures, which each contain a tab stop. The first (TAB_CONT) is
|
|
for computing the beginning pixel offset, as mentioned above. The
|
|
second (TAB_CONT_NEXT) is used to initialize the TAB_CONT field of the
|
|
next line if it wraps.
|
|
|
|
Since computing the parameters of a line are fairly complicated, I
|
|
have one interface that should be all you ever need to figure out
|
|
something about a line. The function FIND_LINE_PARAMS computes the
|
|
parameters of a single line. The function LINE_PARAMS_ITERATE is used
|
|
for computing the properties of some number (> 0) of sequential lines.
|
|
|
|
void
|
|
line_params_iterate (GtkText* text,
|
|
const GtkPropertyMark* mark0,
|
|
const PrevTabCont* tab_mark0,
|
|
gboolean alloc,
|
|
gpointer data,
|
|
LineIteratorFunction iter);
|
|
|
|
where LineIteratorFunction is:
|
|
|
|
typedef gint (*LineIteratorFunction) (GtkText* text,
|
|
LineParams* lp,
|
|
gpointer data);
|
|
|
|
The arguments are a text widget (TEXT), the property mark at the
|
|
beginning of the first line (MARK0), the tab stop mark at the
|
|
beginning of that line (TAB_MARK0), whether to heap-allocate the
|
|
LineParams structure (ALLOC), some client data (DATA), and a function
|
|
to call with the parameters of each line. TAB_MARK0 may be NULL, but
|
|
if so MARK0 MUST BE A REAL LINE START (not a continued line start; it
|
|
is preceded by a newline). If TAB_MARK0 is not NULL, MARK0 may be any
|
|
line start (continued or not). See the code for examples. The
|
|
function ITER is called with each LineParams computed. If ALLOC was
|
|
true, LINE_PARAMS_ITERATE heap-allocates the LineParams and does not
|
|
free them. Otherwise, no storage is permanently allocated. ITER
|
|
should return TRUE when it wishes to continue no longer.
|
|
|
|
There are currently two uses of LINE_PARAMS_ITERATE:
|
|
|
|
* Compute the total buffer height for setting the parameters of the
|
|
scroll bars. This is done in SET_VERTICAL_SCROLL each time the
|
|
window is resized. When horizontal scrolling is added, depending on
|
|
the policy chosen, the max line width can be computed here as well.
|
|
|
|
* Computing geometry of some pixel height worth of lines. This is
|
|
done in FETCH_LINES, FETCH_LINES_BACKWARD, FETCH_LINES_FORWARD, etc.
|
|
|
|
The GtkText structure contains a cache of the LineParams data for all
|
|
visible lines:
|
|
|
|
GList *current_line;
|
|
GList *line_start_cache;
|
|
|
|
guint first_line_start_index;
|
|
guint first_cut_pixels;
|
|
guint first_onscreen_hor_pixel;
|
|
guint first_onscreen_ver_pixel;
|
|
|
|
LINE_START_CACHE is a doubly linked list of LineParams. CURRENT_LINE
|
|
is a transient piece of data which is set in varoius places such as
|
|
the mouse click code. Generally, it is the line on which the cursor
|
|
property mark CURSOR_MARK is on. LINE_START_CACHE points to the first
|
|
visible line and may contain PREV pointers if the cached data of
|
|
offscreen lines is kept around. I haven't come up with a policy. The
|
|
cache can keep more lines than are visible if desired, but the result
|
|
is that inserts and deletes will then become slower as the entire
|
|
cache has to be "corrected". Right now it doesn't delete from the
|
|
cache (it should). As a result, scrolling through the whole buffer
|
|
once will fill the cache with an entry for each line, and subsequent
|
|
modifications will be slower than they should
|
|
be. FIRST_LINE_START_INDEX is the index of the *REAL* line start of
|
|
the first line. That is, if the first visible line is a continued
|
|
line, this is the index of the real line start (preceded by a
|
|
newline). FIRST_CUT_PIXELS is the number of pixels which are not
|
|
drawn on the first visible line. If FIRST_CUT_PIXELS is zero, the
|
|
whole line is visible. FIRST_ONSCREEN_HOR_PIXEL is not used.
|
|
FIRST_ONSCREEN_VER_PIXEL is the absolute pixel which starts the
|
|
visible region. This is used for setting the vertical scroll bar.
|
|
|
|
Other miscellaneous things in the GtkText structure:
|
|
|
|
Gtk specific things:
|
|
|
|
GtkWidget widget;
|
|
|
|
GdkWindow *text_area;
|
|
|
|
GtkAdjustment *hadj;
|
|
GtkAdjustment *vadj;
|
|
|
|
GdkGC *gc;
|
|
|
|
GdkPixmap* line_wrap_bitmap;
|
|
GdkPixmap* line_arrow_bitmap;
|
|
|
|
These are pretty self explanatory, especially if you know Gtk.
|
|
LINE_WRAP_BITMAP and LINE_ARROW_BITMAP are two bitmaps used to
|
|
indicate that a line wraps and is continued offscreen, respectively.
|
|
|
|
Some flags:
|
|
|
|
guint has_cursor : 1;
|
|
guint is_editable : 1;
|
|
guint line_wrap : 1;
|
|
guint freeze : 1;
|
|
guint has_selection : 1;
|
|
guint own_selection : 1;
|
|
|
|
HAS_CURSOR is true iff the cursor is visible. IS_EDITABLE is true iff
|
|
the user is allowed to modify the buffer. If IS_EDITABLE is false,
|
|
HAS_CURSOR is guaranteed to be false. If IS_EDITABLE is true,
|
|
HAS_CURSOR starts out false and is set to true the first time the user
|
|
clicks in the window. LINE_WRAP is where the line-wrap policy is
|
|
set. True means wrap lines, false means continue lines offscreen,
|
|
horizontally.
|
|
|
|
The text properties list:
|
|
|
|
GList *text_properties;
|
|
GList *text_properties_end;
|
|
|
|
A scratch area used for constructing a contiguous piece of the buffer
|
|
which may otherwise span the gap. It is not strictly neccesary
|
|
but simplifies the drawing code because it does not need to deal with
|
|
the gap.
|
|
|
|
guchar* scratch_buffer;
|
|
guint scratch_buffer_len;
|
|
|
|
The last vertical scrollbar position. Currently this looks the same
|
|
as FIRST_ONSCREEN_VER_PIXEL. I can't remember why I have two values.
|
|
Perhaps someone should clean this up.
|
|
|
|
gint last_ver_value;
|
|
|
|
The cursor:
|
|
|
|
gint cursor_pos_x;
|
|
gint cursor_pos_y;
|
|
GtkPropertyMark cursor_mark;
|
|
gchar cursor_char;
|
|
gchar cursor_char_offset;
|
|
gint cursor_virtual_x;
|
|
gint cursor_drawn_level;
|
|
|
|
CURSOR_POS_X and CURSOR_POS_Y are the screen coordinates of the
|
|
cursor. CURSOR_MARK is the buffer position. CURSOR_CHAR is
|
|
TEXT_INDEX (TEXT, CURSOR_MARK.INDEX) if a drawable character, or 0 if
|
|
it is whitespace, which is treated specially. CURSOR_CHAR_OFFSET is
|
|
the pixel offset above the base of the line at which it should be
|
|
drawn. Note that the base of the line is not the "baseline" in the
|
|
traditional font metric sense. A line (LineParams) is
|
|
FONT_ASCENT+FONT_DESCENT high (use the macro LINE_HEIGHT). The
|
|
"baseline" is FONT_DESCENT below the base of the line. I think this
|
|
requires a drawing.
|
|
|
|
0 AAAAAAA
|
|
1 AAAAAAA
|
|
2 AAAAAAAAA
|
|
3 AAAAAAAAA
|
|
4 AAAAA AAAAA
|
|
5 AAAAA AAAAA
|
|
6 AAAAA AAAAA
|
|
7 AAAAA AAAAA
|
|
8 AAAAA AAAAA
|
|
9 AAAAAAAAAAAAAAAAA
|
|
10 AAAAAAAAAAAAAAAAA
|
|
11 AAAAA AAAAA
|
|
12 AAAAA AAAAA
|
|
13 AAAAAA AAAAAA
|
|
14______________AAAAA___________AAAAA__________________________________
|
|
15
|
|
16
|
|
17
|
|
18
|
|
19
|
|
20
|
|
|
|
This line is 20 pixels high, has FONT_ASCENT=14, FONT_DESCENT=6. It's
|
|
"base" is at y=20. Characters are drawn at y=14. The LINE_START
|
|
macro returns the pixel height. The LINE_CONTAINS macro is true if
|
|
the line contains a certain buffer index. The LINE_STARTS_AT macro is
|
|
true if the line starts at a certain buffer index. The
|
|
LINE_START_PIXEL is the pixel offset the line should be drawn at,
|
|
according the the tab continuation of the previous line.
|
|
|
|
Exposure and drawing:
|
|
|
|
Exposure is handled from the EXPOSE_TEXT function. It assumes that
|
|
the LINE_START_CACHE and all its parameters are accurate and simply
|
|
exposes any line which is in the exposure region. It calls the
|
|
CLEAR_AREA function to clear the background and/or lay down a pixmap
|
|
background. The text widget has a scrollable pixmap background, which
|
|
is implemented in CLEAR_AREA. CLEAR_AREA does the math to figure out
|
|
how to tile the pixmap itself so that it can scroll the text with a
|
|
copy area call. If the CURSOR argument to EXPOSE_TEXT is true, it
|
|
also draws the cursor.
|
|
|
|
The function DRAW_LINE draws a single line, doing all the tab and
|
|
color computations neccesary. The function DRAW_LINE_WRAP draws the
|
|
line wrap bitmap at the end of the line if it wraps. TEXT_EXPOSE will
|
|
expand the cached line data list if it has to by calling
|
|
FETCH_LINES_FORWARD. The functions DRAW_CURSOR and UNDRAW_CURSOR draw
|
|
and undraw the cursor. They count the number of draws and undraws so
|
|
that the cursor may be undrawn even if the cursor is already undrawn
|
|
and the re-draw will not occur too early. This is useful in handling
|
|
scrolling.
|
|
|
|
Handling of the cursor is a little messed up, I should add. It has to
|
|
be undrawn and drawn at various places. Something better needs to be
|
|
done about this, because it currently doesn't do the right thing in
|
|
certain places. I can't remember where very well. Look for the calls
|
|
to DRAW_CURSOR and UNDRAW_CURSOR.
|
|
|
|
RECOMPUTE_GEOMETRY is called when the geometry of the window changes
|
|
or when it is first drawn. This is probably not done right. My
|
|
biggest weakness in writing this code is that I've never written a
|
|
widget before so I got most of the event handling stuff wrong as far
|
|
as Gtk is concerned. Fortunatly, most of the code is unrelated and
|
|
simply an exercise in data structure manipulation.
|
|
|
|
Scrolling:
|
|
|
|
Scrolling is fairly straighforward. It looks at the top line, and
|
|
advances it pixel by pixel until the FIRST_CUT_PIXELS equals the line
|
|
height and then advances the LINE_START_CACHE. When it runs out of
|
|
lines it fetches more. The function SCROLL_INT is used to scroll from
|
|
inside the code, it calls the appropriate functions and handles
|
|
updating the scroll bars. It dispatches a change event which causes
|
|
Gtk to call the correct scroll action, which then enters SCROLL_UP or
|
|
SCROLL_DOWN. Careful with the cursor during these changes.
|
|
|
|
Insertion, deletion:
|
|
|
|
There's some confusion right now over what to do with the cursor when
|
|
it's offscreen due to scrolling. This is a policy decision. I don't
|
|
know what's best. Spencer criticized me for forcing it to stay
|
|
onscreen. It shouldn't be hard to make stuff work with the cursor
|
|
offscreen.
|
|
|
|
Currently I've got functions to do insertion and deletion of a single
|
|
character. It's fairly complicated. In order to do efficient pasting
|
|
into the buffer, or write code that modifies the buffer while the
|
|
buffer is drawn, it needs to do multiple characters at at time. This
|
|
is the hardest part of what remains. Currently, gtk_text_insert does
|
|
not reexpose the modified lines. It needs to. Pete did this wrong at
|
|
one point and I disabled modification completely, I don't know what
|
|
the current state of things are. The functions
|
|
INSERT_CHAR_LINE_EXPOSE and DELETE_CHAR_LINE_EXPOSE do the work.
|
|
Here's pseudo code for insert. Delete is quite similar.
|
|
|
|
insert character into the buffer
|
|
update the text property list
|
|
move the point
|
|
undraw the cursor
|
|
correct all LineParams cache entries after the insertion point
|
|
compute the new height of the modified line
|
|
compare with the old height of the modified line
|
|
remove the old LineParams from the cache
|
|
insert the new LineParams into the cache
|
|
if the lines are of different height, do a copy area to move the
|
|
area below the insertion down
|
|
expose the current line
|
|
update the cursor mark
|
|
redraw the cursor
|
|
|
|
What needs to be done:
|
|
|
|
Horizintal scrolling, robustness, testing, selection handling. If you
|
|
want to work in the text widget pay attention to the debugging
|
|
facilities I've written at the end of gtktext.c. I'm sorry I waited
|
|
so long to try and pass this off. I'm super busy with school and
|
|
work, and when I have free time my highest priority is another version
|
|
of PRCS.
|
|
|
|
Feel free to ask me questions.
|