Loading Render Layouts

A render call names a layout, but it does not need to know where that layout is stored. A Loader connects the logical name to source text. You can keep layouts in an editable directory, compile them into the application, or supply them from another source that your application owns. This page shows each approach and explains how several loaders can work together.

Logical Layout Names

The environment passes a validated logical name such as lessons/tempo.txt to its loaders. Names use lowercase ASCII letters, digits, -, _, ., and /. They cannot start or end with a slash, contain empty path components, or exceed 200 code points. The name is the same whether the source lives on disk or in a compiled resource set. Includes and inherited layouts use these names too, so a layout tree can move between storage choices without editing its references.

A loader returns a LayoutSource when it finds the name, or std::nullopt when it does not. The environment tries the next loader after a miss. An actual loading error is reported as RenderError during rendering; it is not treated as a miss.

For a custom loader, prefer RenderError with category Load and set the requested name on its RenderErrorContext. Existing specific runtime exceptions such as PathError may also propagate; the environment wraps them as loading errors and preserves the cause.

Loading Layouts with FileSystemLoader

FileSystemLoader looks below one or more existing, absolute directories. It maps lessons/tempo.txt to the corresponding relative file below each directory. When given several directories, it searches them in the order supplied. The following demo uses two directories with the same layout name; the first one wins.

text/RenderLayouts/LoaderExamples.cpp

void fileLoader() {
    const auto first = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    const auto second = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (first->path() / "lesson.txt"_el).content().writeTextOrThrow("İlk alıştırma: {{ pattern }}\n"_el);
    (second->path() / "lesson.txt"_el).content().writeTextOrThrow("İkinci alıştırma: {{ pattern }}\n"_el);

    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(
        el::render::FileSystemLoader::create(el::util::List<el::Path>{first->path(), second->path()}));
    el::io::print(environment->render("lesson.txt"_el, el::render::Context{}.set("pattern"_el, "yavaş-hızlı"_el)));
}

Output

İlk alıştırma: yavaş-hızlı

Use an absolute directory that your application knows, rather than depending on its current working directory. The loader only accepts regular files below its roots, does not traverse symbolic links below a root, and reads valid UTF-8 layout text. FileSystemLoaderOptions is accepted by the factory; it currently has no configurable settings.

Loading Compiled Layouts with ResourceLoader

Compiled layouts are useful when the application ships a stable set of defaults. ResourceLoader resolves a name against an exact resource identifier and an optional path prefix. In this demo, the logical name note.txt becomes the resource key (render-layouts, tempo/note.txt).

The resource is added to the demo target at build time:

erbsland_core_add_resources(
        TARGET render_layouts
        DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/data"
        IDENTIFIER "render-layouts"
        RECURSIVE
        SUFFIXES ".txt"
)

The runtime code still asks the environment for the logical name:

text/RenderLayouts/LoaderExamples.cpp

void resourceLoader() {
    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(el::render::ResourceLoader::create("render-layouts"_el, "tempo"_el));
    el::io::print(environment->render(
        "note.txt"_el, el::render::Context{}.set("pattern"_el, "yavaş-hızlı"_el).set("bpm"_el, 84)));
}

Output

Çalışma: yavaş-hızlı (84 BPM)

The one-argument factory uses the application’s resource manager. An overload accepts an explicit ResourcesConstPtr when resources come from a provider you supply; the loader retains that provider. Create an application-backed loader after the application is initialized. The optional prefix is a normalized relative path with / separators; it does not change the name passed to render(). For the build-time resource workflow, see Compiling Resources into an Application or Library.

Loader Priorities and Local Overrides

Register more than one loader when a user-editable layout should take precedence over a compiled default. addLayoutLoader(loader, priority) searches larger priorities first; equal priorities keep registration order. For example, register a resource loader at the default priority, then a file-system loader with priority 10. If the file exists, it is used; otherwise, the resource loader may provide the layout.

text/RenderLayouts/LoaderExamples.cpp

void layeredLoaders() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "note.txt"_el).content().writeTextOrThrow("Yerel: {{ pattern }}\n"_el);

    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(el::render::ResourceLoader::create("render-layouts"_el, "tempo"_el));
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()), 10);
    const auto context = el::render::Context{}.set("pattern"_el, "yavaş-hızlı"_el);
    el::io::print(environment->render("note.txt"_el, context));
    el::io::print(environment->render("default.txt"_el, context));
}

Output

Yerel: yavaş-hızlı
Varsayılan: yavaş-hızlı

This choice is made for every named layout, including included layouts and inherited parents. One local partial can therefore override its compiled counterpart without copying the entire layout tree. When automatic reload is enabled, the environment checks sources again at render time and picks up changed revisions.

Implementing a Custom Layout Loader

You can implement Loader for layouts held by an application service or generated from another store. The example below keeps one small layout in its loader. The class appears with the demo because it is the part you would adapt for your own source.

text/RenderLayouts/CustomLoader.cpp

/// Supply a layout source from application-owned storage.
///
/// A loader gets a validated logical name and returns a source only when it owns that name. This example
/// has one stable layout; a service-backed loader would look it up and derive a revision from its content.
/// @notest{Demo-only loader verified by the documentation executable.}
class LessonLoader final : public el::render::Loader {
public:
    [[nodiscard]] auto load(const el::String &layout) -> std::optional<el::render::LayoutSource> override {
        if (layout != "lesson.txt"_el) {
            return std::nullopt;
        }
        return el::render::LayoutSource{"Alıştırma: {{ pattern }}\n"_el, "lesson:built-in"_el, "1"_el};
    }
};

/// Supply layouts from a source owned by the application.
///
/// A custom Loader returns a LayoutSource for a known name and std::nullopt for other names. Its origin is
/// used in diagnostics, while its revision identifies changes when automatic reload is enabled.
/// @notest{Demo function verified by the documentation executable.}
void customLoader() {
    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(std::make_shared<LessonLoader>());
    el::io::print(environment->render("lesson.txt"_el, el::render::Context{}.set("pattern"_el, "yavaş-hızlı"_el)));
}

Output

Alıştırma: yavaş-hızlı

load() receives a name that the environment has already validated. Return std::nullopt only when your source has no layout by that name. A present source needs its exact UTF-8 text, a stable origin for diagnostics, and a revision token that changes when its content changes. The loader may be called from concurrent renders, so its load() implementation must be thread-safe.

For the environment settings that accompany these loaders, continue with Configuring the Render Environment. The rendering reference describes the values supplied to a loaded layout.

Complete demo sources

text/RenderLayouts/LoaderExamples.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include <DemoCommon.hpp>
#include <erbsland/List.hpp>
#include <erbsland/Path.hpp>
#include <erbsland/PathContent.hpp>
#include <erbsland/PathOperations.hpp>
#include <erbsland/render/Context.hpp>
#include <erbsland/render/Environment.hpp>
#include <erbsland/render/FileSystemLoader.hpp>
#include <erbsland/render/Loader.hpp>
#include <erbsland/render/ResourceLoader.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/TempDirectory.hpp>

#include <memory>
#include <optional>

namespace demo {

/// Load named layouts from a directory that an application can edit.
///
/// A file-system loader maps a logical name to a relative file below an existing absolute directory. Several
/// directories can be searched in order, allowing a local layout to take precedence over a bundled one.
/// @notest{Demo function verified by the documentation executable.}
void fileLoader() {
    const auto first = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    const auto second = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (first->path() / "lesson.txt"_el).content().writeTextOrThrow("İlk alıştırma: {{ pattern }}\n"_el);
    (second->path() / "lesson.txt"_el).content().writeTextOrThrow("İkinci alıştırma: {{ pattern }}\n"_el);

    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(
        el::render::FileSystemLoader::create(el::util::List<el::Path>{first->path(), second->path()}));
    el::io::print(environment->render("lesson.txt"_el, el::render::Context{}.set("pattern"_el, "yavaş-hızlı"_el)));
}

/// Find a layout embedded in the application through its resource identifier.
///
/// The optional prefix places the logical layout tree inside the compiled resource set. The layout name
/// remains the same name used by render, include, and inheritance.
/// @notest{Demo function verified by the documentation executable.}
void resourceLoader() {
    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(el::render::ResourceLoader::create("render-layouts"_el, "tempo"_el));
    el::io::print(environment->render(
        "note.txt"_el, el::render::Context{}.set("pattern"_el, "yavaş-hızlı"_el).set("bpm"_el, 84)));
}

/// Prefer an editable file while retaining compiled layouts as defaults.
///
/// The environment tries a higher-priority file loader first. A missing local layout falls through to the
/// resource loader, while a local layout replaces its compiled counterpart.
/// @notest{Demo function verified by the documentation executable.}
void layeredLoaders() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "note.txt"_el).content().writeTextOrThrow("Yerel: {{ pattern }}\n"_el);

    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(el::render::ResourceLoader::create("render-layouts"_el, "tempo"_el));
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()), 10);
    const auto context = el::render::Context{}.set("pattern"_el, "yavaş-hızlı"_el);
    el::io::print(environment->render("note.txt"_el, context));
    el::io::print(environment->render("default.txt"_el, context));
}

}

text/RenderLayouts/CustomLoader.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include <DemoCommon.hpp>
#include <erbsland/render/Context.hpp>
#include <erbsland/render/Environment.hpp>
#include <erbsland/render/LayoutSource.hpp>
#include <erbsland/render/Loader.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/String.hpp>

#include <memory>
#include <optional>

namespace demo {

/// Supply a layout source from application-owned storage.
///
/// A loader gets a validated logical name and returns a source only when it owns that name. This example
/// has one stable layout; a service-backed loader would look it up and derive a revision from its content.
/// @notest{Demo-only loader verified by the documentation executable.}
class LessonLoader final : public el::render::Loader {
public:
    [[nodiscard]] auto load(const el::String &layout) -> std::optional<el::render::LayoutSource> override {
        if (layout != "lesson.txt"_el) {
            return std::nullopt;
        }
        return el::render::LayoutSource{"Alıştırma: {{ pattern }}\n"_el, "lesson:built-in"_el, "1"_el};
    }
};

/// Supply layouts from a source owned by the application.
///
/// A custom Loader returns a LayoutSource for a known name and std::nullopt for other names. Its origin is
/// used in diagnostics, while its revision identifies changes when automatic reload is enabled.
/// @notest{Demo function verified by the documentation executable.}
void customLoader() {
    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(std::make_shared<LessonLoader>());
    el::io::print(environment->render("lesson.txt"_el, el::render::Context{}.set("pattern"_el, "yavaş-hızlı"_el)));
}

}