Use statically-allocated thread private data
[projects/chimara/chimara.git] / libchimara / window.c
index 5f2bd5b349673bb2b46a755e46c3da8906c93240..a4366b4b8ca4c6f11c15da94c38c504f42b53386 100644 (file)
@@ -1,8 +1,84 @@
+#include <glib.h>
 #include "window.h"
 #include "magic.h"
 #include "chimara-glk-private.h"
+#include "gi_dispa.h"
+#include "pager.h"
 
-extern ChimaraGlkPrivate *glk_data;
+extern GPrivate glk_data_key;
+
+static winid_t
+window_new_common(glui32 rock)
+{
+       ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key);
+       winid_t win = g_new0(struct glk_window_struct, 1);
+       
+       win->magic = MAGIC_WINDOW;
+       win->rock = rock;
+       if(glk_data->register_obj)
+               win->disprock = (*glk_data->register_obj)(win, gidisp_Class_Window);
+       
+       win->window_node = g_node_new(win);
+       
+       /* Every window has a window stream, but printing to it might have no effect */
+       win->window_stream = stream_new_common(0);
+       win->window_stream->file_mode = filemode_Write;
+       win->window_stream->type = STREAM_TYPE_WINDOW;
+       win->window_stream->window = win;
+       win->window_stream->style = "normal";
+       win->window_stream->glk_style = "normal";
+
+       win->echo_stream = NULL;
+       win->input_request_type = INPUT_REQUEST_NONE;
+       win->line_input_buffer = NULL;
+       win->line_input_buffer_unicode = NULL;
+       win->history = NULL;
+       win->echo_line_input = TRUE;
+       win->echo_current_line_input = TRUE;
+       win->extra_line_terminators = NULL;
+       win->current_extra_line_terminators = NULL;
+
+       /* Initialise the buffer */
+       win->buffer = g_string_sized_new(1024);
+
+       /* Initialise hyperlink table */
+       win->hyperlinks = g_hash_table_new_full(g_int_hash, g_int_equal, g_free, g_free);
+       
+       return win;
+}
+
+/* Internal function: do all the stuff necessary to close a window. Call only
+ from Glk thread. */
+static void
+window_close_common(winid_t win, gboolean destroy_node)
+{
+       ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key);
+
+       if(glk_data->unregister_obj) 
+       {
+        (*glk_data->unregister_obj)(win, gidisp_Class_Window, win->disprock);
+        win->disprock.ptr = NULL;
+    }
+       
+       if(destroy_node)
+               g_node_destroy(win->window_node);
+       
+       win->magic = MAGIC_FREE;
+       
+       g_list_foreach(win->history, (GFunc)g_free, NULL);
+       g_list_free(win->history);
+       g_slist_free(win->extra_line_terminators);
+       g_slist_free(win->current_extra_line_terminators);
+       
+       g_string_free(win->buffer, TRUE);
+       g_hash_table_destroy(win->hyperlinks);
+       g_free(win->current_hyperlink);
+
+       if(win->backing_store)
+               cairo_surface_destroy(win->backing_store);
+
+       g_free(win);
+}
 
 /**
  * glk_window_iterate:
@@ -24,7 +100,8 @@ winid_t
 glk_window_iterate(winid_t win, glui32 *rockptr)
 {
        VALID_WINDOW_OR_NULL(win, return NULL);
-       
+
+       ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key);
        GNode *retnode;
        
        if(win == NULL)
@@ -73,8 +150,8 @@ glk_window_get_rock(winid_t win)
  * glk_window_get_type:
  * @win: A window.
  *
- * Returns @win's type, one of #wintype_Blank, #wintype_Pair,
- * #wintype_TextBuffer, #wintype_TextGrid, or #wintype_Graphics.
+ * Returns @win's type, one of %wintype_Blank, %wintype_Pair,
+ * %wintype_TextBuffer, %wintype_TextGrid, or %wintype_Graphics.
  *
  * Returns: The window's type.
  */
@@ -100,7 +177,11 @@ winid_t
 glk_window_get_parent(winid_t win)
 {
        VALID_WINDOW(win, return NULL);
+
        /* Value will also be NULL if win is the root window */
+       if(win->window_node->parent == NULL)
+               return NULL;
+
        return (winid_t)win->window_node->parent->data;
 }
 
@@ -121,8 +202,8 @@ glk_window_get_sibling(winid_t win)
        if(G_NODE_IS_ROOT(win->window_node))
                return NULL;
        if(win->window_node->next)
-               return (winid_t)win->window_node->next;
-       return (winid_t)win->window_node->prev;
+               return (winid_t)win->window_node->next->data;
+       return (winid_t)win->window_node->prev->data;
 }
 
 /**
@@ -135,6 +216,7 @@ glk_window_get_sibling(winid_t win)
 winid_t
 glk_window_get_root()
 {
+       ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key);
        if(glk_data->root_window == NULL)
                return NULL;
        return (winid_t)glk_data->root_window->data;
@@ -145,15 +227,16 @@ glk_window_get_root()
  * @split: The window to split to create the new window. Must be 0 if there
  * are no windows yet.
  * @method: Position of the new window and method of size computation. One of
- * #winmethod_Above, #winmethod_Below, #winmethod_Left, or #winmethod_Right
- * OR'ed with #winmethod_Fixed or #winmethod_Proportional. If @wintype is
- * #wintype_Blank, then #winmethod_Fixed is not allowed.
+ * %winmethod_Above, %winmethod_Below, %winmethod_Left, or %winmethod_Right
+ * OR'ed with %winmethod_Fixed or %winmethod_Proportional. If @wintype is
+ * %wintype_Blank, then %winmethod_Fixed is not allowed. May also be OR'ed with
+ * %winmethod_Border or %winmethod_NoBorder.
  * @size: Size of the new window, in percentage points if @method is
- * #winmethod_Proportional, otherwise in characters if @wintype is 
- * #wintype_TextBuffer or #wintype_TextGrid, or pixels if @wintype is
- * #wintype_Graphics.
- * @wintype: Type of the new window. One of #wintype_Blank, #wintype_TextGrid,
- * #wintype_TextBuffer, or #wintype_Graphics.
+ * %winmethod_Proportional, otherwise in characters if @wintype is 
+ * %wintype_TextBuffer or %wintype_TextGrid, or pixels if @wintype is
+ * %wintype_Graphics.
+ * @wintype: Type of the new window. One of %wintype_Blank, %wintype_TextGrid,
+ * %wintype_TextBuffer, or %wintype_Graphics.
  * @rock: The new window's rock value.
  *
  * Creates a new window. If there are no windows, the first three arguments are
@@ -163,9 +246,13 @@ glk_window_get_root()
  *
  * If any windows exist, new windows must be created by splitting existing
  * ones. @split is the window you want to split; this <emphasis>must 
- * not</emphasis> be zero. @method is a mask of constants to specify the
- * direction and the split method (see below). @size is the size of the split.
- * @wintype is the type of window you're creating, and @rock is the rock.
+ * not</emphasis> be zero. @method specifies the direction and the split method
+ * (see below). @size is the size of the split. @wintype is the type of window
+ * you're creating, and @rock is the rock.
+ *
+ * The method argument must be the logical-or of a direction constant
+ * (%winmethod_Above, %winmethod_Below, %winmethod_Left, %winmethod_Right) and a
+ * split-method constant (%winmethod_Fixed, %winmethod_Proportional).
  *
  * Remember that it is possible that the library will be unable to create a new
  * window, in which case glk_window_open() will return %NULL.
@@ -211,19 +298,19 @@ glk_window_get_root()
  * 
  * So to create a text buffer window which takes the top 40% of the original
  * window's space, you would execute
- * |[ newwin = #glk_window_open(win, #winmethod_Above | #winmethod_Proportional, 40, #wintype_TextBuffer, 0); ]|
+ * |[ newwin = glk_window_open(win, winmethod_Above | winmethod_Proportional, 40, wintype_TextBuffer, 0); ]|
  *
  * To create a text grid which is always five lines high, at the bottom of the
  * original window, you would do
- * |[ newwin = #glk_window_open(win, #winmethod_Below | #winmethod_Fixed, 5, #wintype_TextGrid, 0); ]|
+ * |[ newwin = glk_window_open(win, winmethod_Below | winmethod_Fixed, 5, wintype_TextGrid, 0); ]|
  * 
  * Note that the meaning of the @size argument depends on the @method argument.
- * If the method is #winmethod_Fixed, it also depends on the @wintype argument.
+ * If the method is %winmethod_Fixed, it also depends on the @wintype argument.
  * The new window is then called the <quote>key window</quote> of this split,
  * because its window type determines how the split size is computed.
  * 
  * <note><para>
- *   For #winmethod_Proportional splits, you can still call the new window the
+ *   For %winmethod_Proportional splits, you can still call the new window the
  *   <quote>key window</quote>. But the key window is not important for
  *   proportional splits, because the size will always be computed as a simple
  *   ratio of the available space, not a fixed size of one child window.
@@ -260,7 +347,8 @@ glk_window_get_root()
  * <quote>O</quote>. C gets two rows; A gets the rest. All done.
  * 
  * Then the user maliciously starts squeezing the window down, in stages:
- * <informaltable frame="none"><tgroup cols="5"><tbody><row valign="top">
+ * <informaltable xml:id="chimara-Figure-Squeezing-Window" frame="none">
+ * <tgroup cols="5"><tbody><row valign="top">
  * <entry><mediaobject><imageobject><imagedata fileref="fig5-7a.png"/>
  * </imageobject></mediaobject></entry>
  * <entry><mediaobject><imageobject><imagedata fileref="fig7b.png"/>
@@ -328,16 +416,16 @@ glk_window_get_root()
  *  A   C   
  * </literallayout></textobject></mediaobject></entry> 
  * </row></tbody></tgroup></informaltable>
- * After the first split, the new pair window (O1, which covers the whole
- * screen) knows that its first child (A) is above the second, and gets 50% of
- * its own area. (A is the key window for this split, but a proportional split
- * doesn't care about key windows.)
+ * The initial window is A. After the first split, the new pair window (O1,
+ * which covers the whole screen) knows that its new child (B) is below A, and
+ * gets 50% of its own area. (B is the key window for this split, but a
+ * proportional split doesn't care about key windows.)
  * 
- * After the second split, all this remains true; O1 knows that its first child
- * gets 50% of its space, and A is O1's key window. But now O1's first child is
- * O2 instead of A. The newer pair window (O2) knows that its first child (C)
- * is above the second, and gets a fixed size of two rows. (As measured in C's
- * font, because C is O2's key window.)
+ * After the <emphasis>second</emphasis> split, all this remains true; O1 knows
+ * that its first child gets 50% of its space, and B is O1's key window. But
+ * now O1's first child is O2 instead of A. The newer pair window (O2) knows
+ * that its first child (C) is above the second, and gets a fixed size of two
+ * rows. (As measured in C's font, because C is O2's key window.)
  * 
  * If we split C, now, the resulting pair will still be two C-font rows high
  * &mdash; that is, tall enough for two lines of whatever font C displays. For
@@ -374,6 +462,11 @@ glk_window_open(winid_t split, glui32 method, glui32 size, glui32 wintype,
                 glui32 rock)
 {
        VALID_WINDOW_OR_NULL(split, return NULL);
+       g_return_val_if_fail(!(((method & winmethod_DivisionMask) == winmethod_Proportional) && size > 100), NULL);
+       if(method != (method & (winmethod_DirMask | winmethod_DivisionMask | winmethod_BorderMask)))
+               WARNING("Unrecognized bits in method constant");
+
+       ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key);
 
        if(split == NULL && glk_data->root_window != NULL)
        {
@@ -384,11 +477,8 @@ glk_window_open(winid_t split, glui32 method, glui32 size, glui32 wintype,
        gdk_threads_enter();
        
        /* Create the new window */
-       winid_t win = g_new0(struct glk_window_struct, 1);
-       win->magic = MAGIC_WINDOW;
-       win->rock = rock;
+       winid_t win = window_new_common(rock);
        win->type = wintype;
-       win->window_node = g_node_new(win);
 
        switch(wintype)
        {
@@ -403,98 +493,145 @@ glk_window_open(winid_t split, glui32 method, glui32 size, glui32 wintype,
                        /* A blank window has no size */
                        win->unit_width = 0;
                        win->unit_height = 0;
-                       /* You can print to a blank window's stream, but it does nothing */
-                       win->window_stream = window_stream_new(win);
-                       win->echo_stream = NULL;
                }
                        break;
                
                case wintype_TextGrid:
                {
                    GtkWidget *textview = gtk_text_view_new();
+                       GtkTextBuffer *textbuffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(textview) );
 
                    gtk_text_view_set_wrap_mode( GTK_TEXT_VIEW(textview), GTK_WRAP_NONE );
                    gtk_text_view_set_editable( GTK_TEXT_VIEW(textview), FALSE );
                        gtk_widget_show(textview);
                                
-                       /* Set the window's font */
-                       gtk_widget_modify_font(textview, glk_data->monospace_font_desc);
+                       /* Create the styles available to the window stream */
+                       style_init_textgrid(textbuffer);
+                       gtk_widget_modify_font( textview, get_current_font(wintype) );
                    
                    win->widget = textview;
                    win->frame = textview;
                        
                        /* Determine the size of a "0" character in pixels */
                        PangoLayout *zero = gtk_widget_create_pango_layout(textview, "0");
-                       pango_layout_set_font_description(zero, glk_data->monospace_font_desc);
+                       pango_layout_set_font_description( zero, get_current_font(wintype) );
                        pango_layout_get_pixel_size(zero, &(win->unit_width), &(win->unit_height));
                        g_object_unref(zero);
-                       
-                       /* Set the other parameters (width and height are set later) */
-                       win->window_stream = window_stream_new(win);
-                       win->echo_stream = NULL;
-                       win->input_request_type = INPUT_REQUEST_NONE;
-                       win->line_input_buffer = NULL;
-                       win->line_input_buffer_unicode = NULL;
+                       /* width and height are set later */
                        
                        /* Connect signal handlers */
-                       win->keypress_handler = g_signal_connect( G_OBJECT(textview), "key-press-event", G_CALLBACK(on_window_key_press_event), win );
-                       g_signal_handler_block( G_OBJECT(textview), win->keypress_handler );
+                       win->char_input_keypress_handler = g_signal_connect(textview, "key-press-event", G_CALLBACK(on_char_input_key_press_event), win);
+                       g_signal_handler_block(textview, win->char_input_keypress_handler);
+                       win->line_input_keypress_handler = g_signal_connect(textview, "key-press-event", G_CALLBACK(on_line_input_key_press_event), win);
+                       g_signal_handler_block(textview, win->line_input_keypress_handler);
+                       win->shutdown_keypress_handler = g_signal_connect(textview, "key-press-event", G_CALLBACK(on_shutdown_key_press_event), win);
+                       g_signal_handler_block(textview, win->shutdown_keypress_handler);
+                       win->button_press_event_handler = g_signal_connect( textview, "button-press-event", G_CALLBACK(on_window_button_press), win );
+                       g_signal_handler_block(textview, win->button_press_event_handler);
                }
                    break;
                
                case wintype_TextBuffer:
                {
+                       GtkWidget *overlay = gtk_overlay_new();
                        GtkWidget *scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
                        GtkWidget *textview = gtk_text_view_new();
+                       GtkWidget *pager = gtk_button_new_with_label("More");
+                       GtkWidget *image = gtk_image_new_from_stock(GTK_STOCK_GO_DOWN, GTK_ICON_SIZE_BUTTON);
                        GtkTextBuffer *textbuffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(textview) );
 
                        gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW(scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC );
-                       
+
+                       gtk_button_set_image( GTK_BUTTON(pager), image );
+                       gtk_widget_set_halign(pager, GTK_ALIGN_END);
+                       gtk_widget_set_valign(pager, GTK_ALIGN_END);
+                       gtk_widget_set_no_show_all(pager, TRUE);
+
                        gtk_text_view_set_wrap_mode( GTK_TEXT_VIEW(textview), GTK_WRAP_WORD_CHAR );
                        gtk_text_view_set_editable( GTK_TEXT_VIEW(textview), FALSE );
+                       gtk_text_view_set_pixels_inside_wrap( GTK_TEXT_VIEW(textview), 3 );
+                       gtk_text_view_set_left_margin( GTK_TEXT_VIEW(textview), 20 );
+                       gtk_text_view_set_right_margin( GTK_TEXT_VIEW(textview), 20 );
 
                        gtk_container_add( GTK_CONTAINER(scrolledwindow), textview );
-                       gtk_widget_show_all(scrolledwindow);
+                       gtk_container_add( GTK_CONTAINER(overlay), scrolledwindow );
+                       gtk_overlay_add_overlay( GTK_OVERLAY(overlay), pager );
+                       gtk_widget_show_all(overlay);
 
-                       /* Set the window's font */
-                       gtk_widget_modify_font(textview, glk_data->default_font_desc);
-                       
                        win->widget = textview;
-                       win->frame = scrolledwindow;
-                       
+                       win->scrolledwindow = scrolledwindow;
+                       win->pager = pager;
+                       win->frame = overlay;
+
+                       /* Create the styles available to the window stream */
+                       style_init_textbuffer(textbuffer);
+                       gtk_widget_modify_font( textview, get_current_font(wintype) );
+
                        /* Determine the size of a "0" character in pixels */
                        PangoLayout *zero = gtk_widget_create_pango_layout(textview, "0");
-                       pango_layout_set_font_description(zero, glk_data->default_font_desc);
+                       pango_layout_set_font_description( zero, get_current_font(wintype) );
                        pango_layout_get_pixel_size(zero, &(win->unit_width), &(win->unit_height));
                        g_object_unref(zero);
-                       
-                       /* Set the other parameters */
-                       win->window_stream = window_stream_new(win);
-                       win->echo_stream = NULL;
-                       win->input_request_type = INPUT_REQUEST_NONE;
-                       win->line_input_buffer = NULL;
-                       win->line_input_buffer_unicode = NULL;
 
                        /* Connect signal handlers */
-                       win->keypress_handler = g_signal_connect( G_OBJECT(textview), "key-press-event", G_CALLBACK(on_window_key_press_event), win );
-                       g_signal_handler_block( G_OBJECT(textview), win->keypress_handler );
+                       
+                       /* Pager */
+                       g_signal_connect_after( textview, "size-allocate", G_CALLBACK(pager_after_size_allocate), win );
+                       win->pager_keypress_handler = g_signal_connect( textview, "key-press-event", G_CALLBACK(pager_on_key_press_event), win );
+                       g_signal_handler_block(textview, win->pager_keypress_handler);
+                       GtkAdjustment *adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scrolledwindow));
+                       win->pager_adjustment_handler = g_signal_connect_after(adj, "value-changed", G_CALLBACK(pager_after_adjustment_changed), win);
+                       g_signal_connect(pager, "clicked", G_CALLBACK(pager_on_clicked), win);
+
+                       /* Char and line input */
+                       win->char_input_keypress_handler = g_signal_connect( textview, "key-press-event", G_CALLBACK(on_char_input_key_press_event), win );
+                       g_signal_handler_block(textview, win->char_input_keypress_handler);
+                       win->line_input_keypress_handler = g_signal_connect( textview, "key-press-event", G_CALLBACK(on_line_input_key_press_event), win );
+                       g_signal_handler_block(textview, win->line_input_keypress_handler);
+                       win->insert_text_handler = g_signal_connect_after( textbuffer, "insert-text", G_CALLBACK(after_window_insert_text), win );
+                       g_signal_handler_block(textbuffer, win->insert_text_handler);
 
-                       win->insert_text_handler = g_signal_connect_after( G_OBJECT(textbuffer), "insert-text", G_CALLBACK(after_window_insert_text), win );
-                       g_signal_handler_block( G_OBJECT(textbuffer), win->insert_text_handler );
+                       /* Shutdown key press */
+                       win->shutdown_keypress_handler = g_signal_connect( textview, "key-press-event", G_CALLBACK(on_shutdown_key_press_event), win );
+                       g_signal_handler_block(textview, win->shutdown_keypress_handler);                       
 
                        /* Create an editable tag to indicate uneditable parts of the window
                        (for line input) */
                        gtk_text_buffer_create_tag(textbuffer, "uneditable", "editable", FALSE, "editable-set", TRUE, NULL);
 
-                       /* Create the default styles available to the window stream */
-                       style_init_textbuffer(textbuffer);
-
-                       /* Mark the position where the user will input text */
+                       /* Mark the position where the user will input text and the end mark */
                        GtkTextIter end;
                        gtk_text_buffer_get_end_iter(textbuffer, &end);
                        gtk_text_buffer_create_mark(textbuffer, "input_position", &end, TRUE);
+                       gtk_text_buffer_create_mark(textbuffer, "end_position", &end, FALSE);
+
+                       /* Create the pager position mark; it stands for the last character in the buffer
+                        that has been on-screen */
+                       gtk_text_buffer_create_mark(textbuffer, "pager_position", &end, TRUE);
                }
                        break;
+
+               case wintype_Graphics:
+               {
+                   GtkWidget *image = gtk_drawing_area_new();
+                       gtk_widget_show(image);
+
+                       win->unit_width = 1;
+                       win->unit_height = 1;
+                   win->widget = image;
+                   win->frame = image;
+                       win->background_color = 0x00FFFFFF;
+                       win->backing_store = NULL;
+
+                       /* Connect signal handlers */
+                       win->button_press_event_handler = g_signal_connect(image, "button-press-event", G_CALLBACK(on_window_button_press), win);
+                       g_signal_handler_block(image, win->button_press_event_handler);
+                       win->shutdown_keypress_handler = g_signal_connect(image, "key-press-event", G_CALLBACK(on_shutdown_key_press_event), win);
+                       g_signal_handler_block(image, win->shutdown_keypress_handler);                  
+                       g_signal_connect(image, "configure-event", G_CALLBACK(on_graphics_configure), win);
+                       g_signal_connect(image, "draw", G_CALLBACK(on_graphics_draw), win);
+               }
+                   break;
                        
                default:
                        gdk_threads_leave();
@@ -514,14 +651,8 @@ glk_window_open(winid_t split, glui32 method, glui32 size, glui32 wintype,
        {
                /* When splitting, construct a new parent window
                 * copying most characteristics from the window that is being split */
-               winid_t pair = g_new0(struct glk_window_struct, 1);
-               pair->magic = MAGIC_WINDOW;
-               pair->rock = 0;
+               winid_t pair = window_new_common(0);
                pair->type = wintype_Pair;
-               pair->window_node = g_node_new(pair);
-               /* You can print to a pair window's window stream, but it has no effect */
-               pair->window_stream = window_stream_new(pair);
-               pair->echo_stream = NULL;
 
                /* The pair window must know about its children's split method */
                pair->key_window = win;
@@ -559,33 +690,26 @@ glk_window_open(winid_t split, glui32 method, glui32 size, glui32 wintype,
                glk_data->root_window = win->window_node;
        }
 
-       /* Set the window as a child of the Glk widget */
+       /* Set the window as a child of the Glk widget, don't trigger an arrange event */
+       g_mutex_lock(&glk_data->arrange_lock);
+       glk_data->needs_rearrange = TRUE;
+       glk_data->ignore_next_arrange_event = TRUE;
+       g_mutex_unlock(&glk_data->arrange_lock);
        gtk_widget_set_parent(win->frame, GTK_WIDGET(glk_data->self));
        gtk_widget_queue_resize(GTK_WIDGET(glk_data->self));
        
-       gdk_threads_leave();
-       
-       /* For blank or pair windows, this is almost a no-op. For text grid and
-        text buffer windows, this will wait for GTK to draw the window. Otherwise,
-        opening a window and getting its size immediately will give you the wrong
-        size. */
-       glk_window_get_size(win, NULL, NULL);
-       
     /* For text grid windows, fill the buffer with blanks. */
     if(wintype == wintype_TextGrid)
     {
         /* Create the cursor position mark */
-               gdk_threads_enter();
         GtkTextIter begin;
         GtkTextBuffer *buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(win->widget) );
         gtk_text_buffer_get_start_iter(buffer, &begin);
         gtk_text_buffer_create_mark(buffer, "cursor_position", &begin, TRUE);
-        gdk_threads_leave();
-               
-        /* Fill the buffer with blanks and move the cursor to the upper left */
-        glk_window_clear(win);
-    }
+       }
 
+       gdk_threads_leave();
+    glk_window_clear(win);
        return win;
 }
 
@@ -601,24 +725,17 @@ remove_key_windows(GNode *node, winid_t closing_win)
 }
 
 /* Internal function: destroy this window's GTK widgets, window streams, 
- and those of all its children */
+ and those of all its children. GDK threads must be locked. */
 static void
 destroy_windows_below(winid_t win, stream_result_t *result)
 {
        switch(win->type)
        {
                case wintype_Blank:
-                       gdk_threads_enter();
-                       gtk_widget_unparent(win->widget);
-                       gdk_threads_leave();
-                       break;
-       
            case wintype_TextGrid:
                case wintype_TextBuffer:
-                       gdk_threads_enter();
+               case wintype_Graphics:
                        gtk_widget_unparent(win->frame);
-                       gdk_threads_leave();
-                       /* TODO: Cancel all input requests */
                        break;
 
                case wintype_Pair:
@@ -641,8 +758,7 @@ free_winids_below(winid_t win)
                free_winids_below(win->window_node->children->data);
                free_winids_below(win->window_node->children->next->data);
        }
-       win->magic = MAGIC_FREE;
-       g_free(win);
+       window_close_common(win, FALSE);
 }
 
 /**
@@ -711,6 +827,10 @@ void
 glk_window_close(winid_t win, stream_result_t *result)
 {
        VALID_WINDOW(win, return);
+
+       ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key);
+
+       gdk_threads_enter(); /* Prevent redraw while we're trashing the window */
        
        /* If any pair windows have this window or its children as a key window,
         set their key window to NULL */
@@ -730,12 +850,16 @@ glk_window_close(winid_t win, stream_result_t *result)
        /* Parent window changes from a split window into the sibling window */
        /* The parent of any window is either a pair window or NULL */
        GNode *pair_node = win->window_node->parent;
-       g_node_destroy(win->window_node);
        /* If win was not the root window: */
        if(pair_node != NULL)
        {
                gboolean new_child_on_left = ( pair_node == g_node_first_sibling(pair_node) );
-               GNode *sibling_node = pair_node->children; /* only one child left */
+
+               /* Lookup our sibling */
+               GNode *sibling_node = pair_node->children;
+               if(sibling_node == win->window_node)
+                       sibling_node = sibling_node->next;
+
                GNode *new_parent_node = pair_node->parent;
                g_node_unlink(pair_node);
                g_node_unlink(sibling_node);
@@ -753,24 +877,22 @@ glk_window_close(winid_t win, stream_result_t *result)
                                g_node_append(new_parent_node, sibling_node);
                }
 
-               winid_t pair = (winid_t) pair_node->data;
-               g_node_destroy(pair_node);
-               
-               pair->magic = MAGIC_FREE;
-               g_free(pair);
+               stream_close_common( ((winid_t) pair_node->data)->window_stream, NULL );
+               window_close_common( (winid_t) pair_node->data, TRUE);
        } 
        else /* it was the root window */
        {
                glk_data->root_window = NULL;
        }
 
-       win->magic = MAGIC_FREE;
-       g_free(win);
+       window_close_common(win, FALSE);
 
        /* Schedule a redraw */
-       gdk_threads_enter();
+       g_mutex_lock(&glk_data->arrange_lock);
+       glk_data->needs_rearrange = TRUE;
+       glk_data->ignore_next_arrange_event = TRUE;
+       g_mutex_unlock(&glk_data->arrange_lock);
        gtk_widget_queue_resize( GTK_WIDGET(glk_data->self) );
-       gdk_window_process_all_updates();
        gdk_threads_leave();
 }
 
@@ -792,15 +914,17 @@ glk_window_close(winid_t win, stream_result_t *result)
  * <varlistentry>
  *  <term>Text grid</term>
  *  <listitem><para>
- *   This will clear the window, filling all positions with blanks. The window
- *   cursor is moved to the top left corner (position 0,0).
+ *   This will clear the window, filling all positions with blanks (in the
+ *   normal style). The window cursor is moved to the top left corner (position
+ *   0,0).
  *  </para></listitem>
  * </varlistentry>
  * <varlistentry>
  *  <term>Graphics</term>
  *  <listitem><para>
  *   Clears the entire window to its current background color. See <link
- *   linkend="chimara-Graphics-Windows">Graphics Windows</link>.
+ *   linkend="chimara-The-Types-of-Windows&num;wintype-Graphics">Graphics 
+ *   Windows</link>.
  *  </para></listitem>
  * </varlistentry>
  * <varlistentry>
@@ -816,7 +940,9 @@ glk_window_clear(winid_t win)
 {
        VALID_WINDOW(win, return);
        g_return_if_fail(win->input_request_type != INPUT_REQUEST_LINE && win->input_request_type != INPUT_REQUEST_LINE_UNICODE);
-       
+
+       ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key);
+
        switch(win->type)
        {
                case wintype_Blank:
@@ -827,6 +953,12 @@ glk_window_clear(winid_t win)
                case wintype_TextGrid:
                    /* fill the buffer with blanks */
                {
+                       /* Wait for the window's size to be updated */
+                       g_mutex_lock(&glk_data->arrange_lock);
+                       if(glk_data->needs_rearrange)
+                               g_cond_wait(&glk_data->rearranged, &glk_data->arrange_lock);
+                       g_mutex_unlock(&glk_data->arrange_lock);
+
                    gdk_threads_enter();
                    
             /* Manually put newlines at the end of each row of characters in the buffer; manual newlines make resizing the window's grid easier. */
@@ -844,9 +976,12 @@ glk_window_clear(winid_t win)
             gtk_text_buffer_set_text(textbuffer, text, -1);
             g_free(text);
             
-            GtkTextIter begin;
-            gtk_text_buffer_get_start_iter(textbuffer, &begin);
-            gtk_text_buffer_move_mark_by_name(textbuffer, "cursor_position", &begin);
+            GtkTextIter start, end;
+            gtk_text_buffer_get_start_iter(textbuffer, &start);
+            gtk_text_buffer_get_end_iter(textbuffer, &end);
+                       style_apply(win, &start, &end);
+
+            gtk_text_buffer_move_mark_by_name(textbuffer, "cursor_position", &start);
                    
                    gdk_threads_leave();
                }
@@ -865,6 +1000,24 @@ glk_window_clear(winid_t win)
                        gdk_threads_leave();
                }
                        break;
+
+               case wintype_Graphics:
+               {
+                       GtkAllocation allocation;
+
+                       /* Wait for the window's size to be updated */
+                       g_mutex_lock(&glk_data->arrange_lock);
+                       if(glk_data->needs_rearrange)
+                               g_cond_wait(&glk_data->rearranged, &glk_data->arrange_lock);
+                       g_mutex_unlock(&glk_data->arrange_lock);
+
+                       gdk_threads_enter();
+                       gtk_widget_get_allocation(win->widget, &allocation);
+                       gdk_threads_leave();
+
+                       glk_window_erase_rect(win, 0, 0, allocation.width, allocation.height);
+               }
+                       break;
                
                default:
                        ILLEGAL_PARAM("Unknown window type: %d", win->type);
@@ -873,16 +1026,23 @@ glk_window_clear(winid_t win)
 
 /**
  * glk_set_window:
- * @win: A window.
+ * @win: A window, or %NULL.
  *
- * Sets the current stream to @win's window stream. It is exactly equivalent to
- * <code>#glk_stream_set_current(#glk_window_get_stream(@win))</code>.
+ * Sets the current stream to @win's window stream. If @win is %NULL, it is
+ * equivalent to
+ * |[ glk_stream_set_current(NULL); ]|
+ * If @win is not %NULL, it is equivalent to
+ * |[ glk_stream_set_current(glk_window_get_stream(win)); ]|
+ * See <link linkend="chimara-Streams">Streams</link>.
  */
 void
 glk_set_window(winid_t win)
 {
        VALID_WINDOW_OR_NULL(win, return);
-       glk_stream_set_current( glk_window_get_stream(win) );
+       if(win)
+               glk_stream_set_current( glk_window_get_stream(win) );
+       else
+               glk_stream_set_current(NULL);
 }
 
 /**
@@ -975,6 +1135,9 @@ glk_window_get_size(winid_t win, glui32 *widthptr, glui32 *heightptr)
 {
        VALID_WINDOW(win, return);
 
+       GtkAllocation allocation;
+       ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key);
+
     switch(win->type)
     {
         case wintype_Blank:
@@ -986,20 +1149,17 @@ glk_window_get_size(winid_t win, glui32 *widthptr, glui32 *heightptr)
             break;
             
         case wintype_TextGrid:
+                       /* Wait until the window's size is current */
+                       g_mutex_lock(&glk_data->arrange_lock);
+                       if(glk_data->needs_rearrange)
+                               g_cond_wait(&glk_data->rearranged, &glk_data->arrange_lock);
+                       g_mutex_unlock(&glk_data->arrange_lock);
+
                        gdk_threads_enter();
-                       /* Wait for the window to be drawn, and then cache the width and height */
-                       gdk_window_process_all_updates();
-                       while(win->widget->allocation.width == 1 && win->widget->allocation.height == 1)
-                   {
-                       /* Release the GDK lock momentarily */
-                       gdk_threads_leave();
-                       gdk_threads_enter();
-                       while(gtk_events_pending())
-                           gtk_main_iteration();
-                   }
-                   
-                       win->width = (glui32)(win->widget->allocation.width / win->unit_width);
-                   win->height = (glui32)(win->widget->allocation.height / win->unit_height);
+                       gtk_widget_get_allocation(win->widget, &allocation);
+                       /* Cache the width and height */
+                       win->width = (glui32)(allocation.width / win->unit_width);
+                   win->height = (glui32)(allocation.height / win->unit_height);
             gdk_threads_leave();
                        
             if(widthptr != NULL)
@@ -1009,30 +1169,34 @@ glk_window_get_size(winid_t win, glui32 *widthptr, glui32 *heightptr)
             break;
             
         case wintype_TextBuffer:
-            /* TODO: Glk wants to be able to get its windows' sizes as soon as they are created, but GTK doesn't decide on their sizes until they are drawn. The drawing happens somewhere in an idle function. A good method would be to make an educated guess of the window's size using the ChimaraGlk widget's size. */
+                       /* Wait until the window's size is current */
+                       g_mutex_lock(&glk_data->arrange_lock);
+                       if(glk_data->needs_rearrange)
+                               g_cond_wait(&glk_data->rearranged, &glk_data->arrange_lock);
+                       g_mutex_unlock(&glk_data->arrange_lock);
+
             gdk_threads_enter();
-            /*if(win->widget->allocation.width == 1 && win->widget->allocation.height == 1)
-            {
-                g_warning("glk_window_get_size: The Glk program requested the size of a window before it was allocated screen space by GTK. The window size is just an educated guess.");
-                guess the size from the parent window;
-                break;
-            } */
+            gtk_widget_get_allocation(win->widget, &allocation);
+            if(widthptr != NULL)
+                *widthptr = (glui32)(allocation.width / win->unit_width);
+            if(heightptr != NULL)
+                *heightptr = (glui32)(allocation.height / win->unit_height);
+            gdk_threads_leave();
             
-            /* Instead, we wait for GTK to draw the widget. This is probably very slow and should be fixed. */
-            gdk_window_process_all_updates();
-                       while(win->widget->allocation.width == 1 && win->widget->allocation.height == 1)
-            {
-                /* Release the GDK lock momentarily */
-                gdk_threads_leave();
-                gdk_threads_enter();
-                while(gtk_events_pending())
-                    gtk_main_iteration();
-            }
-                
+            break;
+
+               case wintype_Graphics:
+                       g_mutex_lock(&glk_data->arrange_lock);
+                       if(glk_data->needs_rearrange)
+                               g_cond_wait(&glk_data->rearranged, &glk_data->arrange_lock);
+                       g_mutex_unlock(&glk_data->arrange_lock);
+
+            gdk_threads_enter();
+            gtk_widget_get_allocation(win->widget, &allocation);
             if(widthptr != NULL)
-                *widthptr = (glui32)(win->widget->allocation.width / win->unit_width);
+                *widthptr = (glui32)(allocation.width);
             if(heightptr != NULL)
-                *heightptr = (glui32)(win->widget->allocation.height / win->unit_height);
+                *heightptr = (glui32)(allocation.height);
             gdk_threads_leave();
             
             break;
@@ -1041,7 +1205,125 @@ glk_window_get_size(winid_t win, glui32 *widthptr, glui32 *heightptr)
             ILLEGAL_PARAM("Unknown window type: %u", win->type);
     }
 }
+
+/**
+ * glk_window_set_arrangement:
+ * @win: a pair window to rearrange.
+ * @method: new method of size computation. One of %winmethod_Above, 
+ * %winmethod_Below, %winmethod_Left, or %winmethod_Right OR'ed with 
+ * %winmethod_Fixed or %winmethod_Proportional.
+ * @size: new size constraint, in percentage points if @method is
+ * %winmethod_Proportional, otherwise in characters if @win's type is 
+ * %wintype_TextBuffer or %wintype_TextGrid, or pixels if @win's type is
+ * %wintype_Graphics.
+ * @keywin: new key window, or %NULL to leave the key window unchanged.
+ *
+ * Changes the size of an existing split &mdash; that is, it changes the 
+ * constraint of a given pair window.
+ * 
+ * Consider the example above, where D has collapsed to zero height. Say D was a
+ * text buffer window. You could make a more useful layout by doing
+ * |[
+ * winid_t o2;
+ * o2 = glk_window_get_parent(d);
+ * glk_window_set_arrangement(o2, winmethod_Above | winmethod_Fixed, 3, d);
+ * ]|
+ * That would set D (the upper child of O2) to be O2's key window, and give it a
+ * fixed size of 3 rows.
+ * 
+ * If you later wanted to expand D, you could do
+ * |[ glk_window_set_arrangement(o2, winmethod_Above | winmethod_Fixed, 5, NULL); ]|
+ * That expands D to five rows. Note that, since O2's key window is already set 
+ * to D, it is not necessary to provide the @keywin argument; you can pass %NULL
+ * to mean <quote>leave the key window unchanged.</quote>
+ * 
+ * If you do change the key window of a pair window, the new key window 
+ * <emphasis>must</emphasis> be a descendant of that pair window. In the current
+ * example, you could change O2's key window to be A, but not B. The key window
+ * also cannot be a pair window itself.
+ * 
+ * |[ glk_window_set_arrangement(o2, winmethod_Below | winmethod_Fixed, 3, NULL); ]|
+ * This changes the constraint to be on the <emphasis>lower</emphasis> child of 
+ * O2, which is A. The key window is still D; so A would then be three rows high
+ * as measured in D's font, and D would get the rest of O2's space. That may not
+ * be what you want. To set A to be three rows high as measured in A's font, you
+ * would do
+ * |[ glk_window_set_arrangement(o2, winmethod_Below | winmethod_Fixed, 3, a); ]|
+ * 
+ * Or you could change O2 to a proportional split:
+ * |[ glk_window_set_arrangement(o2, winmethod_Below | winmethod_Proportional, 30, NULL); ]|
+ * or
+ * |[ glk_window_set_arrangement(o2, winmethod_Above | winmethod_Proportional, 70, NULL); ]|
+ * These do exactly the same thing, since 30&percnt; above is the same as 
+ * 70&percnt; below. You don't need to specify a key window with a proportional
+ * split, so the @keywin argument is %NULL. (You could actually specify either A
+ * or D as the key window, but it wouldn't affect the result.)
+ * 
+ * Whatever constraint you set, glk_window_get_size() will tell you the actual 
+ * window size you got.
+ * 
+ * Note that you can resize windows, and alter the Border/NoBorder flag. But you
+ * can't flip or rotate them. You can't move A above D, or change O2 to a
+ * vertical split where A is left or right of D.
+ * <note><para>
+ *   To get this effect you could close one of the windows, and re-split the 
+ *   other one with glk_window_open().
+ * </para></note>
+ */
+void
+glk_window_set_arrangement(winid_t win, glui32 method, glui32 size, winid_t keywin)
+{
+       VALID_WINDOW(win, return);
+       VALID_WINDOW_OR_NULL(keywin, return);
+       g_return_if_fail(win->type == wintype_Pair);
+       if(keywin)
+       {
+               g_return_if_fail(keywin->type != wintype_Pair);
+               g_return_if_fail(g_node_is_ancestor(win->window_node, keywin->window_node));
+       }
+       g_return_if_fail(method == (method & (winmethod_DirMask | winmethod_DivisionMask)));
+       g_return_if_fail(!(((method & winmethod_DivisionMask) == winmethod_Proportional) && size > 100));
+
+       ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key);
+
+       win->split_method = method;
+       win->constraint_size = size;
+       if(keywin)
+               win->key_window = keywin;
+
+       /* Tell GTK to rearrange the windows */
+       gdk_threads_enter();
+       g_mutex_lock(&glk_data->arrange_lock);
+       glk_data->needs_rearrange = TRUE;
+       glk_data->ignore_next_arrange_event = TRUE;
+       g_mutex_unlock(&glk_data->arrange_lock);
+       gtk_widget_queue_resize(GTK_WIDGET(glk_data->self));
+       gdk_threads_leave();
+}
+
+/**
+ * glk_window_get_arrangement:
+ * @win: a pair window.
+ * @methodptr: return location for the constraint flags of @win, or %NULL.
+ * @sizeptr: return location for the constraint size of @win, or %NULL.
+ * @keywinptr: return location for the key window of @win, or %NULL.
+ *
+ * Queries the constraint of a given pair window.
+ */
+void
+glk_window_get_arrangement(winid_t win, glui32 *methodptr, glui32 *sizeptr, winid_t *keywinptr)
+{
+       VALID_WINDOW(win, return);
+       g_return_if_fail(win->type == wintype_Pair);
+       
+       if(methodptr)
+               *methodptr = win->split_method;
+       if(sizeptr)
+               *sizeptr = win->constraint_size;
+       if(keywinptr)
+               *keywinptr = win->key_window;
+}
+
 /**
  * glk_window_move_cursor:
  * @win: A text grid window.
@@ -1077,6 +1359,20 @@ glk_window_move_cursor(winid_t win, glui32 xpos, glui32 ypos)
 {
        VALID_WINDOW(win, return);
        g_return_if_fail(win->type == wintype_TextGrid);
+
+       flush_window_buffer(win);
+
+       ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key);
+
+       /* Wait until the window's size is current */
+       g_mutex_lock(&glk_data->arrange_lock);
+       if(glk_data->needs_rearrange)
+               g_cond_wait(&glk_data->rearranged, &glk_data->arrange_lock);
+       g_mutex_unlock(&glk_data->arrange_lock);
+
+       /* Don't do anything if the window is shrunk down to nothing */
+       if(win->width == 0 || win->height == 0)
+               return;
        
        /* Calculate actual position if cursor is moved past the right edge */
        if(xpos >= win->width)
@@ -1084,6 +1380,7 @@ glk_window_move_cursor(winid_t win, glui32 xpos, glui32 ypos)
            ypos += xpos / win->width;
            xpos %= win->width;
        }
+
        /* Go to the end if the cursor is moved off the bottom edge */
        if(ypos >= win->height)
        {
@@ -1101,4 +1398,3 @@ glk_window_move_cursor(winid_t win, glui32 xpos, glui32 ypos)
        
        gdk_threads_leave();
 }
-