Features Help Download

Lomse Hacking Guide

3.3. Layouting: graphical model creation

Wikipedia defines layout, in reference to computer graphics, as the process of calculating the position of objects in space, subject to various constraints.

In lomse, layouting will be used with this meaning, and will refer to the process of creating the graphical model, that is, deciding wich graphical symbols (lines, notes, rests, texts, etc.) will be used to build the visual representation of a lomse document, as well as deciding all details about this: where to place the symbols and their attributes (color, size, etc.). For this, the layouting process traverses the internal model, taking decisions about the suitable graphical symbols to use, and their sizes and positions, and builds the graphical model.

For deciding the general layout of a document, a flow based approach is followed. Flow based means that it is assumed that most of the time it is possible to compute the geometry in a single pass, as the internal model is traversed. ImoObj objects later in the flow typically do not affect the geometry of objects that are earlier in the flow, so layout can proceed left-to-right, top-to-bottom through the document tree. There are exceptions: for example, scores and tables require more complex approaches.

Layouting is a recursive process. It begins at the root ImoDocument object, which corresponds to the lenmusdoc tag of the LDP representation. Layouting continues recursively through all of the internal model hierarchy, computing all the graphical information for each ImoObj object. As the tree is traversed, GmoObj objectes are created and are organized in a tree. The final result is the graphical model, a tree of objects representing all the graphical information: lines, symbols, positions, sizes, and all other attributes.

3.3.1. Layouters, Engravers and Engrouters

All graphical model creation is controlled by three main objects hierarchies: layouters, engravers, and engrouters.

Layouter objects
They are responsible for layouting an object. They know how to organice the space, creating areas (boxes) for placing content, assigning space and position to them, and adding the content. In practise, the main objective of Layouter objects is to create the GmoBox objects. And, in general, creating the content for these boxes is a delegated task, for engravers, engrouters or other layouters.
Engraver objects
They know how to create shapes, that is, their main responsibility is to create the symbols (lines, texts, music symbols, etc.) within the specified areas (GmoBox objects). In practise, this implies that the main objective of Engraver derived objects is to create the GmoShape objects.

Layouters and engravers are abstract classes. They speciallyze into specific derived classes, oriented to deal with the peculiarities and characteristics of every ImoObj. For instance, class ScoreLayouter is responsible for layouting scores and class ParagraphLayouter is responsible for layouting paragraphs. Likewise, class NoteEngraver is responsible for creating shapes for notes, and class ClefEngrver is responsible for creating shapes for clefs.

Engrouter objects

Layouting some objects, such as scores, can be very complex. So tasks has been splitted: layouters assigns space and create boxes, and engravers deals with the details and create shapes.

But for most document atomic objects, layouting is trivial because each object ocupies a rectangular space defined by the object bounding rectangle. Therefore, in these cases, it is more practical (less code to write, test and maintain, and better performance) not to split responsibilities. For instance, determining the space that an image will occupy requires creating the image and measuring it; therefore, splitting responsibilities would imply duplicating code. For this, in these cases, both responsibilities, layouting and engraving, are merged in a single object, the Engrouter (the name is a contraction of engraver and layouter).

Class Engruoter is also an abstract class, and specific engrouters must be derived. For instance, class WordEngrouter is responsible for layouting and creating GmoShapeWord objects, and class ImageEngrouter is responsible for layouting and creating GmoShapeImage objects.

3.3.2. Layouting a document: the algorithm

The graphical model is only created when the document is going to be rendered by a View. Its creation is controlled by a DocLayouter object, a layouter specialized on layouting a ImoDocument object. It all starts by invoking DocLayouter::layout_document(). It creates the GmoBoxDocument box and a the first document page box (GmoBoxDocPage object), computes margins and adds a content wrapper (GmoBoxDocPageContent object) to the page.

Next, it stars traversing the content items. Layouting is a top down recursive operation. Therefore, for layouting each content item, DocLayouter delegates on specific content layouter objects, specialized on layouting each content item type (i.e. score, paragraph, list, table, etc.).

The first thing the specialized layouter has to do is to create the main box for the item content (that’s what DocLayouter did!). For instance, a ParagraphLayouter will create a GmoBoxParagraph. Then, it adds the box to the parent box and sets its size and margins, and proceeds to adds content for this box, while checking for available space. A parent node will always have its main box created before any of its descendants will have theirs created. And positioning is refined along this process.

When a layouter decides that a shape must be created, it delegates on an Engraver object. For example, note shapes are created by NoteEngraver object and clefs are created by ClefEngraver object. The create_shape() method for each Engraver object computes information about the shape to create and their positions. Most of the times, the computed position is just a first approximation but, in other cases, it is the final position set by the user and saved in the internal model.

If current specialized layouter (i.e. ParagraphLayouter) fills up the current page, control is returned to the DocLayouter. It checks if current specialized layouter has finished. If not, adds another page and the current specialized layouter is again invoked to continue layouting its content.

Finally, when current item is layouted, DocLayouter proceeds with next item, creates a new specialized layouter for that item type, and delegates on it to layout the item in current page.

The process repeats until all the document content is layouted.

Entering into more details, layouting is started by invoking Interactor‘s method create_graphic_model. Layouting begins at the root ImoDocument object, which corresponds to the lenmusdoc tag of the LDP representation. Therefore, the interactor just creates a document layouter and asks it to layout the document:

void Interactor::create_graphic_model()
{
    ...
    DocLayouter layouter( m_pDoc->get_im_model(), m_libScope);
    layouter.layout_document();
    m_pGraphicModel = layouter.get_gm_model();
    ...
}

All layouters derive from base class Layouter. This base class contains most of the necessary code and derived classes do not have to override it. The only pure virtual methods that derived classes must code are two:

  1. create_item_main_box(). This method is responsible for creating the main box for the item content, adding it to the parent box and setting its size and margins. For instance, a ParagraphLayouter will create a GmoBoxParagraph:

    void BoxContentLayouter::create_main_box(GmoBox* pParentBox, UPoint pos,
                                            LUnits width, LUnits height)
    {
        m_pItemMainBox = LOMSE_NEW GmoBoxParagraph(m_pPara);
        pParentBox->add_child_box(m_pItemMainBox);
    
        m_pItemMainBox->set_origin(pos);
        m_pItemMainBox->set_width(width);
        m_pItemMainBox->set_height(height);
    }
  2. layout_in_box(). This method is responsible for creating the item content. It must stop if no more space in parent box, that is, if end of page is reached. In this case, control must be retutned to parent layouter who will allocate a new page and invoke again layout_in_box() to continue layouting the item. Therefore it is important to note that this method can be invoked more times for layouting an item and, as consequence:

    • This method must not initialize anything. All global initializations for layouting an item must be done in prepare_to_start_layout(), that is invoked before the first invokation to layout_in_box().
    • layout_in_box() method must not re-start layouting the item. It always must continue layouting from current state.

    As an example, here is the code for ParagraphLayouter::layout_in_box():

    void BoxContentLayouter::layout_in_box()
    {
        set_cursor_and_available_space();
        page_initializations(m_pItemMainBox);
    
        if (!is_line_ready())
            prepare_line();
    
        if (!is_line_ready())    //empty paragraph
            advance_current_line_space(m_pageCursor.x);
    
        while(is_line_ready() && enough_space_in_box())
        {
            add_line();
            prepare_line();
        }
    
        bool fMoreText = is_line_ready();
        if (!fMoreText)
            m_pItemMainBox->add_shapes_to_tables();
    
        set_layout_is_finished( !fMoreText );
    }

These two methods, create_item_main_box() and layout_in_box(), enclose the layouting algorithm for each main content object type (score, paragraph, table, etc.), whereas Layouter base class implements the recursing loop and all common methods for several tasks, such as controlling page breaks or creating the specialized layouters.

The most important method is, perhaps, Layouter::layout_item(). It is responsible for managing the layout of an item. Each layouter receives, in method layout_item(), the parent box in which current item has to be layouted. The parent box provides information about position and available space in current page. Here is an excerpt of the code:

void Layouter::layout_item(ImoContentObj* pItem, GmoBox* pParentBox)
{
    m_pCurLayouter = create_layouter(pItem);

    m_pCurLayouter->prepare_to_start_layout();
    while (!m_pCurLayouter->is_item_layouted())
    {
        m_pCurLayouter->create_main_box(pParentBox, m_pageCursor,
                                        m_availableWidth, m_availableHeight);
        m_pCurLayouter->layout_in_box();
        m_pCurLayouter->set_box_height();

        if (!m_pCurLayouter->is_item_layouted())
            pParentBox = start_new_page();
    }
    m_pCurLayouter->add_end_margins();
    ...
}

The method starts by creating an specialized layouter for that item. Layouting is an operation defined at ImoContentObj level and layouters are created by factory class LayouterFactory. Once the specialized layouter is created, its method prepare_to_start_layout() is invoked to initialize the layouter and prepare it to start the layout of the item. Next, the loop for filling pages (if necessary) starts:

  1. Method create_item_main_box(), as described before, must create the main box for the item content, add the box to the parent box and set, tentativelly, its size and margins. Normally, all remaining available page space is allocated for this child box.
  2. Method layout_in_box(), also described before, adds content for this box, while checking for available space. When layout is finished, either because all item content is layouted or because no more space available in current page, control returns.
  3. Method set_box_height() is invoked for final adjustments (if necessary) of current box size. If all remaining available page space was initially allocated for this box, method set_box_height() is the right place for updating box size with the space really occupied.
  4. Finally, when item content is fully layouted, the loop is exited. Otherwise, if some item content is pending, a new page is allocated and the loop repeats until all content is layouted.

The last step, when the specialized layouter has finished layouting current item is to update the available space in parent box, by discounting the real used space for current layouted item (including margins, borders and paddings) and adding bottom margins.

The described algorithm is the high level algorithm. For details on how each content type (scores, paragraphs, etc.) are layouted it is necessary to study methods create_item_main_box() and layout_in_box() for each specific layouter. I will explain the most importants and illustrative layouters (ScoreLayouter and ParagraphLayouter) in later sections.

3.3.3. Adding GmoObj objects to the tree

There are two methods for adding objects to the graphical model. The first one is for adding boxes:

void GmoBox::add_child_box(GmoBox* child);

add_child_box links child box to parent. Parent box can be orphand, that is, its parent is NULL; this implies that has not yet been added to the tree.

The other method is for adding shapes:

void GmoBox::add_shape(GmoShape* shape, int layer);

add_shape adds a shape to a box (its parent). Again, parent box can be orphand.

Important

Some parts of the graphical model are complex to build. For instance, scores. For this reason, it can not be assumed that boxes and shapes are added to the graphical model as soon as they are created. Normally it is like that, but there are exceptions. For instance, during score layouting, GmoBoxSlice is not added to the tree as soon as it is created. First, slices are created, then, justification and line breaking takes place and, finally, slices are added to the model.

3.3.4. Maintaining model coherence

When an object is added to the graphical model, apart from linking it to tree it is necessary to maintain some auxiliary tables. For instance, the collection relating GmoObj and ImoObj objects, and the global collection of shapes maintained in each GmoBoxDocPage.

Unfortunately, as seen in previous section, when an object is added to a box, it can not be assumed that the box is already included in the model. Therefore, it is not possible, at that time, to know some necessary information, such as the parent GmoBoxDocPage.

As a consequence, as nothing guaranties that the parent is not orphan, an auxiliary method is used for maintaining references in auxiliary tables:

void GmoBox::add_shapes_to_tables();

When this method is invoked, it is assumed that the box and all its descendants are properly rooted in the tree.

Important

It is responsibility of each layouter to invoke method add_shapes_to_tables when the box is fully layouted and has been added to the tree.

(To be continued ...)