User Tools

Site Tools


guide:theprimaryconcepts

NC* is written in the ODIN programming language. Before we begin, it is expected you have some knowledge of low-level programming languages, or ideally a basic understanding of ODIN. This guide will not be teaching programming.

The reasons to choose ODIN are multifaceted, but the primary reason is its simplicity and approachable design, certainly far easier to pick up than C or C++. There will be references to things like cstring and string, where you will be expected to know how to use them in what circumstances.

As ODIN has no concept of references, we will do our best to explain best practices as the by-value concept may seem strange initially for those used to using references. You very rarely need to dereference a pointer in ODIN, unlike in C/C++ where it's practically everywhere. In fact, as time moves on, it is unlikely NC* will be dereferencing anything.

Other things to note:

  • There are few heap allocations in NC*. What is heap allocated is usually done by ODIN automatically, such as the string type, or where we make use of the make or new procedure using the standard allocator. The primary and global state structure, UI_State is heap allocated from the UI_Init procedure, but besides this, everything is stack allocated and moved into the state as required. Where we perform allocations into temp_allocator, we would tell you.
  • In addition to the above, you will find that almost every procedure returning a value is constructed on the stack and expected to be stored in the global state, outside of utility procs. Unless the state is passed in by pointer into the procedure, we make no assumptions on where the final data goes. You are expected to know what you're doing with this data unless directed otherwise in these guides.
  • As ODIN performs everything by value, every local declaration to another data source is copied by default unless you explicitly take a pointer to it. This is a low-level language after all. In the NC* codebase, we favour explicitness over local variables so that it's clear what data is being put where.
  • And finally, touching on the Immediate vs Retained mode argument, NC* draws everything “Immediate”-mode style with an eventual toggle to “Retained” later. At the moment, even though there is a global state for Content, each Component stores a copy. This may change later, but would not work in Retained-mode. Please keep this in mind when reading these guides, as the Immediate vs Retained will naturally prop up. The goal of NC* is not to repeat the mistakes that almost all Retained-mode API's do, which is being opinionated.

Now this is all said and done, let's move on.

Concepts

There are two different concepts in NC* to understand before we can move onto coding.

  • Compositions are structures that contain Shapes and their respective dimensions within the overall Composition, given a bounding dimension. Each Composition is therefore the thing that NC* uses to draw the thing itself, but does not contain any data or behaviours, this comes later.
  • Components, on the other hand, are the things that contain behaviour, data and properties which can either change or modify one or more Compositions. A ComponentDefinition is used to describe the default construction of a component, which can have one or more Compositions. A Component is something that is created and stored into the global UI stack, which takes a reference to a definition to determine what to draw. Component's will “retain” a copy of a Composition that the Component should draw based on its current state.

When creating our own custom components, understanding these two concepts is vital for the construction of these components and how they are drawn.

Below is the expected dependency chain.

  1. ShapeCompositionComponentDefinitionComponent

As you can see, there is no data involved. That's because we are storing data globally and accessing it per Component.

When creating definitions, we give an identifier which we use to construct with later. Then, Components are added to the global state from these definitions which are used to create our interface. If they hold content, the Content type is used for holding our content and is assigned to the global state and accessed via the integer identifier given to us from the global stack. We can use this to move data around easily, instead of trying to use procedures to pass around our data all the time.

In the case of Content, we take advantage of the union type in ODIN which we use to hold a certain specific number of types we would accept. This is compile-time types which are easy to type-check and less error-prone than using the any type we could have used. Note that when we use Content, basic types are copied by value as usual, but user-defined types will be pointers to an underlying type. Moreover, when dealing with these pointers, we are expecting the underlying types to be heap allocated, not stack allocated.

The main reason to do this is because Content as a type in the global stack is a value type, not a pointer, so if data is considered “complex” (i.e., not a basic type), then heap allocate the data and make a reference to it for the Content to use later. This prevents loss of data or losing data in scopes where we expected a value copy. You can use the temp_allocator when allocating if a memory arena is preferred over a straight heap allocation, or your own allocator. The reason the Content type is a union is we can expand this to store values of any type which you, the user, can type-check manually later if desired.

Structure

Besides the basic structure defined above for building interfaces, everything else is left to the user to define how to structure the Components themselves.

All your member variables, procedures and business logic is done yourself if building components from scratch, and NC* will not care much how that's done, besides one simple caveat:

  • If you let NC* do event management for you, it will give you a state you can use to determine things like if the mouse is interacting with your component; or,
  • If you want to go wild, build your own custom event handling within your component and be warned of the risks. We will put in mechanisms in NC* to enable or disable UI interactions based on your own custom component's activities, but be aware that this freedom comes at a cost. Otherwise, building strictly within the confines of NC*'s event handling is easier despite the inherent overhead and tediousness that comes with it.

We try our best to make your life as simple as possible in this regard.

We can move on to the next chapter once you're ready.

guide/theprimaryconcepts.txt · Last modified: 2024/05/18 13:42 by tienery