Creating an application

About the UFZ::Application class

The UntitledFlipperZero library uses the UFZ::Application class to represent the current application instance. This object manages your whole application.

It looks like this:

class Application
{
public:
    explicit Application(const std::vector<UWidget*>& widgetsRef, void* userPointer, const std::function<void(Application&)>& begin, uint32_t tickPeriod = 0) noexcept;
    void run(const std::vector<UWidget*>& widgetsRef, void* userPointer, const std::function<void(Application&)>& begin, uint32_t tickPeriod = 0) noexcept;

    template<typename T>
    T* getWidget(size_t i) noexcept;

    const ViewDispatcher& getViewDispatcher() noexcept;
    const SceneManager& getSceneManager() noexcept;
    const Filesystem& getFilesystem() noexcept;

    void* getUserPointer() noexcept;

    void destroy() noexcept;
};

The most important member functions are the following:

  1. run or the non-default constructor - Initialises the application with a list of widgets, a user pointer, a begin callback function, and a tick interval that defaults to 0. If the tick interval is above 0 a tick handler will be added.
  2. getWidget - given a widget ID, returns the widget with the specified type
  3. getUserPointer - Returns the user pointer that was set using the init function
  4. getViewDispatcher and getSceneManager - Return the view dispatcher and scene manager respectively. More info here
  5. destroy - Call this before returning from the entry point

Caution

Always call destroy before returning from the entry point. The UFZ::Application class DOES NOT HAVE RAII!

Creating an instance of the application

To create an instance of the application, do the following:

  1. Call its default constructor
  2. Call its non-default constructor or the init member function
  3. Call destroy or let the destructor handle deallocation

Example:

UFZ::Application application{};
... // Create list and popup widgets here
application.run({ &list, &popup }, nullptr);

Creating widgets

All built-in widgets depend on the UFZ::UWidget abstract class, which looks like this:

class UWidget
{
public:
    UWidget(AppSceneOnEnterCallback onEnter, AppSceneOnEventCallback onEvent, AppSceneOnExitCallback onExit, const std::vector<View*>& additionalViews = {}) noexcept
        : enter(onEnter), event(onEvent), exit(onExit), views(additionalViews)
    {
    }

    void destroy();
    virtual void reset() noexcept = 0;
    Application* application = nullptr;
};

As you can see, each widget is created using a constructor with an initialiser list, that depends on 3 callback functions for the following events:

  1. Enter - when the widget is opened
  2. Event - when the widget's main action is triggered
  3. Exit - when the widget is closed

It also takes a list of UFZ::View, which defaults to an empty list. More info can be found here.

A list of built-in widget types:

  1. Menu - Similar to the main application menu. Displays large menu items with animated icons
  2. ButtonMenu - A button menu
  3. ButtonPanel - A button panel
  4. ByteInput - Input using bytes
  5. DialogEx - A customisable popup dialog with 3 options
  6. EmptyScreen - An empty screen
  7. Loading - A loading screen
  8. Popup - A simple popup
  9. Submenu - A minimal list of text-only items
  10. TextBox - A text box
  11. TextInput - An on-screen keyboard & input box widget
  12. VariableItemList
  13. Widget - A custom widget builder class

More info can be found in the UntitledFlipperZero source code down from the specified line.

Creating a simple "Hello, World!" popup

To create a simple popup, create an enum of scenes, so that you can easily track the ID later, when the application becomes larger:

enum Scenes
{
    LANDING_POPUP
};

Next, create the 3 event callbacks for the popup:

void popup_enter(void* context) noexcept
{
    auto* popup = GET_WIDGET_P(context, UFZ::Popup, LANDING_POPUP); // Gets a widget instance from an ID
    popup->reset(); // Reset the image so that it's already empty
    popup->setContext(popup->application) // Build the widget
            .setHeader("Hello, World!", 64, 4, AlignCenter, AlignTop)
            .setIcon(-1, -1, nullptr) // Disable the optional icon
            .setText("This text describes the popup and can be longer", 4, 16, AlignLeft, AlignTop);
    RENDER_VIEW(popup->application, LANDING_POPUP); // Render the newly-created widget
}

bool popup_event(void* context, SceneManagerEvent event) noexcept
{
    // No event handling for the widget's event callback
    UNUSED(context); UNUSED(event);
    return false;
}

void popup_exit(void* context) noexcept
{
    UNUSED(context);
}

Next, in your application's entry point, create an instance of the UFZ::Popup class and add it to your application like this:

UFZ::Application application{};
UFZ::Popup popup{ popup_enter, popup_event, popup_exit };
application.init({ &popup }, nullptr);

Now run the application, and you should be able to see your popup.

Views and the view stack system

Every widget contains an instance of UFZ::View, which is a class that allows for interacting with the currently displayed content. It looks like this:

class View
{
public:
    void setDeferredSetupCallback(const std::function<void(View&)>& f) noexcept;

    [[nodiscard]] const View& setDrawCallback(ViewDrawCallback callback) const noexcept;
    [[nodiscard]] const View& setInputCallback(ViewInputCallback callback) const noexcept;
    [[nodiscard]] const View& setCustomCallback(ViewCustomCallback callback) const noexcept;

    [[nodiscard]] const View& setPreviousCallback(ViewNavigationCallback callback) const noexcept;
    [[nodiscard]] const View& setEnterCallback(ViewCallback callback) const noexcept;
    [[nodiscard]] const View& setExitCallback(ViewCallback callback) const noexcept;

    [[nodiscard]] const View& setUpdateCallback(ViewUpdateCallback callback) const noexcept;

    [[nodiscard]] const View& setUpdateCallbackContext(void* context) const noexcept;
    [[nodiscard]] const View& setContext(void* context) const noexcept;

    [[nodiscard]] const View& setOrientation(ViewOrientation orientation) const noexcept;

    [[nodiscard]] const View& allocateModel(ViewModelType type, size_t size) const noexcept;
    [[nodiscard]] const View& freeModel() const noexcept;
    [[nodiscard]] void* getModel() const noexcept;
    [[nodiscard]] const View& commitModel(bool bUpdate) const noexcept;
};

Views have a number of callbacks that you can set, where you can handle different types of events.

Adding additional views to a widget

As said previously, every widget has a UFZ::View instance, however you can add additional views to the widget. This is a side effect of an unfortunate feature of the Flipper Zero GUI service design.

When using a default widget, you cannot use custom event handlers, without overriding the default handler. To fix this issue, internally, we use a view stack, which merges all views in the stack into 1 view that can be added to the view dispatcher.

To add another UFZ::View to a widget, do the following:

  1. Create an instance of the UFZ::View class after default-initialising the application
  2. Call the UFZ::View::setDeferredCallback function with a callback function that will add an event handler to the view
  3. Add a pointer to the view in the additionalViews variable, part of the given widget's initialiser list

Example:

UFZ::Application application{}

UFZ::View view{};
view.setDeferredSetupCallack([](UFZ::View& v) -> void {
    view.setInputCallback([](InputEvent* event, void* context) -> bool {
        if (event->type == InputTypePress)
            FURI_LOG_I("MYAPP", "Key is pressed");
    });
});

UFZ::Popup popup{ popup_enter, popup_event, popup_exit, { &view } };
application.init({ &popup }, nullptr);

The part of the application that's responsible for navigating between scenes is the UFZ::SceneManager class. It looks like this:

class SceneManager
{
public:
    void setSceneState(uint32_t id, uint32_t state) const noexcept;
    [[nodiscard]] uint32_t getSceneState(uint32_t id) const noexcept;

    [[nodiscard]] bool handleCustomEvent(uint32_t event) const noexcept;
    [[nodiscard]] bool handleBackEvent() const noexcept;
    void handleTickEvent() const noexcept;

    void nextScene(uint32_t id) const noexcept;
    [[nodiscard]] bool previousScene() const noexcept;
    [[nodiscard]] bool hasPreviousScene(uint32_t id) const noexcept;

    [[nodiscard]] bool searchAndSwitchToPreviousScene(uint32_t id) const noexcept;
    bool searchAndSwitchToPreviousSceneOneOf(const uint32_t* ids, size_t idsSize) const noexcept;
    [[nodiscard]] bool searchAndSwitchToAnotherScene(uint32_t id) const noexcept;
};

There are 2 ways to navigate to a scene:

  1. By preserving the previous scenes list - Allows you to go to the last scenes using the back button
  2. By wiping the previous scenes list

To go to a scene, while preserving the scenes list, use the SceneManager::nextScene member function like this:

app->getSceneManager().nextScene(numericID);

To go to a scene, while wiping the scenes list, use the SceneManager::searchAndSwitchToAnotherScene, like this:

app->getSceneManager().searchAndSwitchToAnotherScene(numericID);

Caution

Calls to the scene manager SHOULD ONLY BE DONE IN THE EVENT CALLBACK FOR A WIDGET. To route the event from a widget's callback call(the callback function that's called when the user interacts with the widget) use the SEND_CUSTOM_EVENT(application, SceneID) macro.

Tip

You can use the NEXT_SCENE and FORCE_NEXT_SCENE macros, instead of calling the UFZ::SceneManager::nextScene and UFZ::SceneManager::searchAndSwitchToAnotherScene functions respectively.