Oters - Graphics
Direct Mode
The Oters standard library comes with two ways of including graphics. The first, called Direct Mode, is by directly drawing shapes, text and images directly onto the window in a “canvas” style. Functions relating to drawing such graphics are correspondingly found in the gui::shape
, gui::text
and gui::image
submodules. To draw these elements such that they persist on screen, they must be drawn each frame using streams. For example:
let my_text = (gui::text::draw_text
("Hello World!", (0, 0), 18, gui::color::red)
) << @my_text
This line draws the text “Hello World!” onto the screen until the program is closed. It does this by constructing a stream with constant value such that the function draw_text
is called each frame. The actual type of the stream is Stream<int>
as this is the return value of draw_text
, but this doesn’t reflect what the stream is actually doing.
Immediate Mode
The second way, called Immediate Mode, of including graphics is through the widget library, more precisely the gui::widget
submodule. This submodule provides a collection of functions that create UI elements such that they are positioned automatically by the system. All elements must be attached to a frame, but only the root element and its children will actually be drawn on the screen. Moreover, each element is a tuple: an element ID which is used to group elements together or attach the root element, and an element stream which draws the elements as well as provides user input. Even if the element stream is not used elsewhere in the program, it is important that it is bound to a variable, otherwise it will not be executed, and therefore the element will not be drawn onto the window.
A simple widget program is as follows:
let ui = gui::frame (50,50)
let (btn_id, btn_stream)
= gui::widget::button ui (100, 30) (const "Click me!")
let _ = gui::attach_root (ui, btn_id)
Line 1 creates the frame and binds it to variable ui
. A frame serves like an anchor point around which all the attached elements can be positioned.
Lines 2 and 3 create a button using the gui::widget::button function. All widget functions take as first argument the frame onto which they will be attached. The button element additionally takes a size and an input Stream<string> that serves as the button’s label. In this case the button’s label is a constant stream with the value “Click me!” so the label will not change. The output of the \verb | button | function is a tuple as explained just above. btn_stream is specifically a Stream<bool> , which corresponds to whether the button has been clicked in the current frame. |
Finally, Line 4 attaches the button using its ID to the frame. Without this line, the button will not be drawn on the screen since only the frame’s root element and its children are drawn.
To group widgets together, we use the vgroup
and hgroup
elements. Aside from a frame ID and size parameters, these two elements take in a stream of element lists and an alignment parameter. The Stream of element lists holds the elements to be grouped at each frame. Elements are arranged in list order. This allows the arrangements to be dynamic, allowing certain elements to be added and removed after a certain event. The alignment parameter then specifies how the elements should be aligned in the grouping.
vgroup
s group elements together vertically in rows. Take the following code:
let (btn1_id, btn1_stream) = gui::widget::button ui (100, 30) (const "Click")
let (btn2_id, btn2_stream) = gui::widget::button ui (100, 30) (const "me!")
let (lab_id, lab_stream) = gui::widget::label ui (200, 30) (const "Label!")
let (vgrp_id, vgrp_stream)
= gui::widget::vgroup ui (250, 100) (const [btn1_id, btn_2, lab_id]) (Alignment::Left)
The dimensions of the vgroup
is (250, 100). This means that it will attempt to fill a row of elements until their collective width is greater than 250 pixels. So in the example, the two buttons will be put onto the same row, while the label will be dropped to the next row underneath the buttons. Moreover, between each element there is a padding of 10 pixels. This padding is taken into account when calculating elements’ collective widths. If the vgroup
had width 200, then the two buttons would not fit on the same row since together they would take up 210 pixels with the padding. There is also vertical padding between rows. This is also 10 pixels, and starts from the tallest element in the row.
If an element is too wide to fit in a row it will not be displayed. Similarly, if there is not enough vertical space to accommodate all elements in the group, then the ones that don’t fit will be dropped. Finally, the alignment Alignment::Left
simply indicates that the elements are positioned starting from the left, like how this text is displayed.
Here is a similar example with an hgroup
that lays elements out horizontally.
let (btn1_id, btn1_stream) = gui::widget::button ui (100, 30) (const "Click")
let (lab_id, lab_stream) = gui::widget::label ui (200, 30) (const "Label!")
let (btn2_id, btn2_stream) = gui::widget::button ui (100, 30) (const "me!")
let (hgrp_id, hgrp_stream)
= gui::widget::hgroup ui (400, 50) (const [btn1_id, lab_id, btn_2]) (Alignment::Top)
Now instead of positioning the elements in rows, they are positioned in columns but in the same manner. Here, all the elements are positioned in different columns as the consecutive elements have collective heights greater than the hgroup
’s. Moreover, notice that I have changed the order of the elements. So first btn1
will be positioned, in the first column, then the label then btn2
. However, in total the elements take up \(100 + 10 + 200 +10 + 100 = 420 > 400\) pixels meaning that they don’t all fit in the group. This means that the last element will have to be dropped. Thus, btn2
will not be drawn.