Modules

I wanted to avoid writing a large single Limbo file containing all the code for new types. That means putting code into a module and using it from the main program. If you have already learned Python, using modules in Limbo is quite confusing. I find that I need to think more in terms of C programming, especially when writing my own modules.

Creating a new module

I created a new module for the Label type, calling the module Panels and creating the panels.m file to contain declarations for all the types and functions it exposes:

Panels: module
{
    PATH: con "panels.dis";

    default_font: ref Draw->Font;
    display: ref Draw->Display;

    init: fn(ctx: ref Draw->Context);

    Label: adt
    {
        left, centre, right: con iota;

        text: string;
        font: ref Draw->Font;
        alignment: int;

        draw: fn(label: self ref Label, rect: Draw->Rect);
    };
};

The PATH definition is a bit unusual for a Limbo module in that it references a file using a relative path — panels.dis will be located in the same directory as the files that use it. Normally, the location for a compiled module would be in the /dis directory and, in this case, would be /dis/panels.dis or something similar.

The panels.m file doesn't include or load any other modules. Most module interface files in the /module directory are like this. It is up to a Limbo file that includes the module to ensure that other modules needed by it are included before it is itself included. The declarations in the module interface file are interpreted in the context of the program including them.

The code for the Panels module itself (panels.b) includes the panels.m file after other modules so that the types it refers to are already defined.

implement Panels;

include "sys.m";
include "draw.m";
Display, Font, Image, Rect: import draw;

include "panels.m";

draw: Draw;

The Label.draw method now appears in this module, and has been extended to accept nil as a value for the font, which causes the function to use the default font.

Label.draw(label: self ref Label, rect: Rect)
{
    x: int;
    font := label.font;
    if (font == nil)
        font = default_font;

    case label.alignment {
    Label.left =>
        x = rect.min.x;
    Label.centre =>
        width := label.font.width(label.text);
        x = rect.min.x + (rect.dx() - width) / 2;
    Label.right =>
        width := label.font.width(label.text);
        x = rect.max.x - width;
    * =>
        x = rect.min.x;
    }

    display.image.text((x, rect.min.y), display.white, (0,0), label.font,
                       label.text);
}

An init function is provided for the module as a whole. It creates a display and sets up any common resources — currently just the default font. It is up to the user of the module to call this function before using any other features of the module.

init(ctx: ref Draw->Context)
{
    draw = load Draw Draw->PATH;
    display = Display.allocate(nil);
    default_font = Font.open(display, "*default*");
}

With the panels.b and panels.m files in place, let's look at a simple program that uses the module.

Using the module

The labels.b module recreates the previous example, importing the panels.m file after other module interface files to avoid compiler errors.

implement Test;

include "sys.m";
include "draw.m";
draw: Draw;
# These imports are needed to allow method calls on these types:
Display, Image, Rect: import draw;

include "panels.m";
panels: Panels;
default_font, display, Label: import panels;

Test: module
{
    init: fn(nil: ref Draw->Context, nil: list of string);
};

The init function is similar to before, except that the Panels module needs to be loaded and initialised before use.

init(context: ref Draw->Context, nil: list of string)
{
    panels = load Panels Panels->PATH;
    draw = load Draw Draw->PATH;
    
    # Initialise the panels instance.
    panels->init(context);

    # Draw the background, filling the entire screen.
    display.image.draw(Rect((0,0), (320,240)), display.rgb(0,0,64),
                       nil, (0,0));

    # Create labels to show the text with different alignments.
    left_label := ref Label("Left", default_font, Label.left);
    centre_label := ref Label("Centre", default_font, Label.centre);
    right_label := ref Label("Right", default_font, Label.right);

    # Draw the labels.
    left_label.draw(Rect((2,50), (318,90)));
    centre_label.draw(Rect((2,100), (318,140)));
    right_label.draw(Rect((2,150), (318,190)));
}

Having a module to hold user interface types should hopefully make it easier to organise the code more effectively and write more concise programs.


David Boddie
18 December 2021