Skip to content

7GUIs Cells#

Example's page

This page describes an example listed in 7guis category.

Spreadsheet processor (Excel).

Challenges: change propagation, widget customization, implementing a more authentic/involved GUI application.

The task is to create a simple but usable spreadsheet application. The spreadsheet should be scrollable. The rows should be numbered from 0 to 99 and the columns from A to Z. Double-clicking a cell C lets the user change C’s formula. After having finished editing the formula is parsed and evaluated and its updated value is shown in C. In addition, all cells which depend on C must be reevaluated. This process repeats until there are no more changes in the values of any cell ( change propagation). Note that one should not just recompute the value of every cell but only of those cells that depend on another cell’s changed value. If there is an already provided spreadsheet widget it should not be used. Instead, another similar widget (like JTable in Swing) should be customized to become a reusable spreadsheet widget.

Cells is a more authentic and involved task that tests if a particular approach also scales to a somewhat bigger application. The two primary GUI-related challenges are intelligent propagation of changes and widget customization. Admittedly, there is a substantial part that is not necessarily very GUI-related but that is just the nature of a more authentic challenge. A good solution’s change propagation will not involve much effort and the customization of a widget should not prove too difficult. The domain-specific code is clearly separated from the GUI-specific code. The resulting spreadsheet widget is reusable.

Source Code#

Repository

CMakeLists.txt#

1
2
3
aui_executable(aui.example.cells)
aui_link(aui.example.cells PRIVATE aui::views)
aui_enable_tests(aui.example.cells)

tests/FormulaTests.cpp#

#include <gtest/gtest.h>
#include "Formula.h"
#include "Spreadsheet.h"

class Cells_Formula: public testing::Test {
public:

protected:
    Spreadsheet mSpreadsheet{glm::uvec2(100)};
};

TEST_F(Cells_Formula, Constant) {
    EXPECT_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "0")), 0.0);
}

TEST_F(Cells_Formula, String) {
    EXPECT_EQ(std::get<AString>(formula::evaluate(mSpreadsheet, "test")), "test");
}

TEST_F(Cells_Formula, EConstant) {
    EXPECT_DOUBLE_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "=1")), 1.0);
}

TEST_F(Cells_Formula, Math1) {
    EXPECT_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "=1+2")), 3);
}

TEST_F(Cells_Formula, Math2) {
    EXPECT_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "=5/2")), 2.5f);
}

TEST_F(Cells_Formula, Math3) {
    EXPECT_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "=(2+3)*4")), 20);
}

TEST_F(Cells_Formula, Math4) {
    EXPECT_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "=4*(2+3)")), 20);
}

TEST_F(Cells_Formula, Math5) {
    mSpreadsheet[{0, 0}].expression = "1";
    mSpreadsheet[{1, 0}].expression = "2";
    mSpreadsheet[{2, 0}].expression = "3";
    EXPECT_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "=A0+B0*C0")), 7);
}

TEST_F(Cells_Formula, Math6) {
    mSpreadsheet[{0, 0}].expression = "1";
    mSpreadsheet[{1, 0}].expression = "2";
    mSpreadsheet[{2, 0}].expression = "3";
    EXPECT_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "=(A0+B0)*C0")), 9);
}

TEST_F(Cells_Formula, FSum1) {
    EXPECT_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "=SUM(1;2)")), 3);
}

TEST_F(Cells_Formula, FSum2) {
    EXPECT_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "=SUM(1;2;3)")), 6);
}

TEST_F(Cells_Formula, CellRef) {
    mSpreadsheet[{0, 0}].expression = "228";
    EXPECT_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "=A0")), 228);

    mSpreadsheet[{0, 0}].expression = "229";
    EXPECT_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "=A0")), 229);
}

TEST_F(Cells_Formula, ChangePropagation) {
    mSpreadsheet[{1, 0}].expression = "=A0+1";

    mSpreadsheet[{0, 0}].expression = "228";
    EXPECT_EQ(std::get<double>(mSpreadsheet[{1, 0}].value.value()), 229);

    mSpreadsheet[{0, 0}].expression = "0";
    EXPECT_EQ(std::get<double>(mSpreadsheet[{1, 0}].value.value()), 1);
}

TEST_F(Cells_Formula, Range1) {
    mSpreadsheet[{0, 0}].expression = "1";
    mSpreadsheet[{0, 1}].expression = "2";
    mSpreadsheet[{0, 2}].expression = "3";
    mSpreadsheet[{1, 0}].expression = "3";
    EXPECT_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "=SUM(A:A)")), 6);
}

TEST_F(Cells_Formula, Range2) {
    mSpreadsheet[{0, 0}].expression = "1";
    mSpreadsheet[{0, 1}].expression = "2";
    mSpreadsheet[{0, 2}].expression = "3";
    mSpreadsheet[{1, 0}].expression = "4";
    mSpreadsheet[{1, 1}].expression = "5";
    mSpreadsheet[{1, 2}].expression = "6";
    mSpreadsheet[{2, 0}].expression = "7";
    mSpreadsheet[{2, 1}].expression = "8";
    mSpreadsheet[{2, 2}].expression = "9";
    // 1 4 7
    // 2 5 8
    // 3 6 9
    EXPECT_EQ(std::get<double>(formula::evaluate(mSpreadsheet, "=SUM(A0:B1)")), 12);
}


TEST_F(Cells_Formula, EvaluationLoop) {
    mSpreadsheet[{ 0, 0 }].expression = "=A1";
    mSpreadsheet[{ 0, 1 }].expression = "=A0";
    EXPECT_EQ(std::get<AString>(*mSpreadsheet[{ 0, 1 }].value), "#LOOP!");
}

src/Formula.h#

#pragma once


#include <AUI/Common/AString.h>

class Spreadsheet;

namespace formula {
    struct Range {
        glm::uvec2 from{}, to{};

        bool operator==(const Range&) const = default;
        bool operator!=(const Range&) const = default;
    };

    using Value = std::variant<std::nullopt_t, double, AString, Range>;
    using Precompiled = std::function<formula::Value(const Spreadsheet& spreadsheet)>;
    formula::Value evaluate(const Spreadsheet& spreadsheet, const AString& expression);
    Precompiled precompile(const AString& expression);
}

src/Cell.h#

#pragma once

#include <AUI/Common/AString.h>
#include <AUI/Common/AProperty.h>
#include "Formula.h"

class Spreadsheet;

struct Cell {
private:
    formula::Precompiled precompile();
    APropertyPrecomputed<formula::Precompiled> expressionPrecompiled = [&] { return precompile(); };
    formula::Value evaluate();

public:
    Spreadsheet* spreadsheet = nullptr;
    AProperty<AString> expression;
    APropertyPrecomputed<formula::Value> value = [&] { return (*expressionPrecompiled)(*spreadsheet); };

    static AString columnName(unsigned index);
    static AString rowName(unsigned index);

    static constexpr auto UNDEFINED = std::numeric_limits<unsigned>::max();

    static glm::uvec2 fromName(const AString& name);
};

src/Formula.cpp#

// Created by alex2772 on 3/7/25.
//

#include "Formula.h"
#include "AUI/Common/AVector.h"
#include "AUI/Util/ATokenizer.h"
#include "Tokens.h"
#include "AUI/Logging/ALogger.h"
#include "AST.h"
#include "AUI/Util/AEvaluationLoopException.h"

formula::Value formula::evaluate(const Spreadsheet& spreadsheet, const AString& expression) {
    return precompile(expression)(spreadsheet);
}

formula::Precompiled formula::precompile(const AString& expression) {
    if (expression.empty()) {
        return [](const Spreadsheet&) { return std::nullopt; };
    }
    if (auto d = expression.toDouble()) {
        return [d = *d](const Spreadsheet&) { return d; };
    }
    if (!expression.startsWith('=')) {
        return [stringConstant = expression](const Spreadsheet&) { return stringConstant; };
    }
    try {
        auto tokens = token::parse(ATokenizer(expression));
        auto p = ast::parseExpression(tokens);

        return [p = std::shared_ptr(std::move(p))](const Spreadsheet& ctx) -> formula::Value {
            try {
                return p->evaluate(ctx);
            } catch (const AEvaluationLoopException& e) {
                return "#LOOP!";
            } catch (const AException& e) {
                return "#{}!"_format(e.getMessage());
            }
        };
    } catch (const AException& e) {
        ALogger::err("Formula") << "Can't parse expression " << expression << "\n" << e;
        return [msg = e.getMessage()](const Spreadsheet&) { return "#{}!"_format(msg); };
    }
}

src/Cell.cpp#

#include "Cell.h"

AString Cell::columnName(unsigned int index) {
    return AString(U'A') + index;
}
AString Cell::rowName(unsigned int index) {
    return AString::number(index);
}

formula::Value Cell::evaluate() {
    return formula::evaluate(*spreadsheet, expression);
}

glm::uvec2 Cell::fromName(const AString& name) {
    glm::uvec2 out{UNDEFINED};
    auto it = name.begin();
    for (;it != name.end() && 'A' <= *it && *it <= 'Z'; ++it) {
        if (out.x == UNDEFINED) { out.x = 0; }
        out.x *= 26;
        out.x += *it - 'A';
    }
    for (;it != name.end() && '0' <= *it && *it <= '9'; ++it) {
        if (out.y == UNDEFINED) { out.y = 0; }
        out.y *= 10;
        out.y += *it - '0';
    }

    return out;
}

formula::Precompiled Cell::precompile() {
    return formula::precompile(expression);
}

src/Functions.h#

#pragma once

#include "Spreadsheet.h"
#include <AUI/Common/AMap.h>

namespace functions {
struct Ctx {
    const Spreadsheet& spreadsheet;
    AVector<formula::Value> args;
};
using Invocable = std::function<formula::Value(Ctx ctx)>;

const AMap<AString, Invocable>& predefined();
}

src/Spreadsheet.h#

#pragma once

#include "Cell.h"
#include "AUI/Common/AVector.h"
#include "AUI/Common/AException.h"

class Spreadsheet {
public:
    explicit Spreadsheet(glm::uvec2 size) : mSize(size) {
        mCells.resize(size.x * size.y);
        for (auto& v : mCells) {
            v = std::make_unique<Cell>();
            v->spreadsheet = this;
        }
    }

    Cell& operator[](glm::uvec2 pos) { return *mCells[pos.y * mSize.x + pos.x]; }

    const Cell& operator[](glm::uvec2 pos) const {
        if (glm::any(glm::greaterThanEqual(pos, mSize))) {
            throw AException("OUTOFBOUNDS");
        }
        return *mCells[pos.y * mSize.x + pos.x];
    }

    auto operator[](formula::Range range) const {
        if (range.from.x == Cell::UNDEFINED) {
            range.from.x = 0;
            if (range.to.x != Cell::UNDEFINED) {
                throw AException("BADRANGE");
            }
            range.to.x = mSize.x - 1;
        }
        if (range.from.y == Cell::UNDEFINED) {
            range.from.y = 0;
            if (range.to.y != Cell::UNDEFINED) {
                throw AException("BADRANGE");
            }
            range.to.y = mSize.y - 1;
        }
        if (range.to.x == Cell::UNDEFINED || range.to.y == Cell::UNDEFINED) {
            throw AException("BADRANGE");
        }
        range = { .from = glm::min(range.from, range.to), .to = glm::max(range.from, range.to) };

        struct RangeIterator {
            const Spreadsheet* spreadsheet;
            formula::Range range;
            glm::uvec2 current;

            RangeIterator operator++() {
                current.x += 1;
                if (current.x <= range.to.x) {
                    return *this;
                }
                current.x = range.from.x;
                current.y += 1;
                if (current.y <= range.to.y) {
                    return *this;
                }
                current = { Cell::UNDEFINED, Cell::UNDEFINED };
                return *this;
            }

            const Cell& operator*() const {
                return (*spreadsheet)[current];
            }

            bool operator==(const RangeIterator&) const = default;
            bool operator!=(const RangeIterator&) const = default;
        };
        return aui::range(
            RangeIterator { .spreadsheet = this, .range = range, .current = range.from },
            RangeIterator { .spreadsheet = this, .range = range, .current = { Cell::UNDEFINED, Cell::UNDEFINED } });
    }

    glm::uvec2 size() const { return mSize; }

private:
    glm::uvec2 mSize;
    AVector<_unique<Cell>> mCells;
};

src/Tokens.h#

#pragma once

#include <AUI/Common/AObject.h>
#include "AUI/Util/ATokenizer.h"

namespace token {
struct Identifier {
    AString name;
};
struct Double {
    double value;
};
struct Semicolon {};   // ;
struct LPar {};        // (
struct RPar {};        // )
struct Colon {};       // :
struct Plus {};        // +
struct Minus {};       // -
struct Asterisk {};    // *
struct Slash {};       // /
struct LAngle {};      // <
struct RAngle {};      // >
struct StringLiteral {
    AString value;
};

using Any = std::variant<Identifier, Double, Semicolon, LPar, RPar, Colon, Plus, Minus, Asterisk, Slash, LAngle, RAngle, StringLiteral>;

AVector<token::Any> parse(aui::no_escape<ATokenizer> t);

}   // namespace token

src/AST.cpp#

#include <range/v3/all.hpp>
#include "AST.h"
#include "AUI/Traits/variant.h"
#include "Functions.h"
#include <AUI/Common/AMap.h>

using namespace ast;

namespace {

template <typename type>
constexpr size_t got = aui::variant::index_of<token::Any, type>::value;

template <typename T, typename Variant>
const T& expect(const Variant& variant) {
    if (std::holds_alternative<T>(variant)) {
        return std::get<T>(variant);
    }
    throw AException("VALUE {}"_format(AClass<T>::name()).uppercase());
}

struct BinaryOperatorNode : public INode {
    _unique<INode> left;
    _unique<INode> right;
};

template <typename F>
struct BinaryOperatorNodeImpl : BinaryOperatorNode {
    virtual ~BinaryOperatorNodeImpl() = default;

    formula::Value evaluate(const Spreadsheet& ctx) override {
        return double(F {}(expect<double>(left->evaluate(ctx)), expect<double>(right->evaluate(ctx))));
    }
};

struct DoubleNode : INode {
    double value;
    explicit DoubleNode(double value) : value(value) {}
    ~DoubleNode() override = default;

    formula::Value evaluate(const Spreadsheet& ctx) override { return value; }
};

struct StringLiteralNode : INode {
    AString value;
    explicit StringLiteralNode(AString value) : value(std::move(value)) {}
    ~StringLiteralNode() override = default;

    formula::Value evaluate(const Spreadsheet& ctx) override { return value; }
};

struct RangeNode : INode {
    formula::Range range;
    explicit RangeNode(const formula::Range& range) : range(range) {}

    ~RangeNode() override = default;

    formula::Value evaluate(const Spreadsheet& ctx) override { return range; }
};

struct IdentifierNode : INode {
    AString name;
    explicit IdentifierNode(AString name) : name(std::move(name)) {}
    ~IdentifierNode() = default;
    formula::Value evaluate(const Spreadsheet& ctx) override {
        auto result = *ctx[Cell::fromName(name)].value;
        if (std::holds_alternative<std::nullopt_t>(result)) {
            return 0.0;
        }
        return result;
    }
};

class AstState {
public:
    explicit AstState(std::span<token::Any> tokens) : mTokens(tokens) {}

    _unique<INode> parseExpression() {
        _unique<INode> temporaryValue;   // storage for temporary non-binary nodes such ast constants, function calls,
                                         // etc

        struct BinaryOperatorAndItsPriority {
            BinaryOperatorNode* op;
            int priority = -1;
            _unique<BinaryOperatorNode> owning;
        };

        AVector<BinaryOperatorAndItsPriority> binaryOperators;

        auto putValue = [&](_unique<INode> node) {
            if (temporaryValue) {
                throw AException("SYNTAX");
            }
            if (!binaryOperators.empty()) {
                if (binaryOperators.last().op->right) {
                    throw AException {};
                }
                binaryOperators.last().op->right = std::move(node);
                return;
            }
            temporaryValue = std::move(node);
        };

        auto takeValue = [&] {
            if (!temporaryValue) {
                throw AException {};
            }
            auto v = std::move(temporaryValue);
            temporaryValue = nullptr;   // to be sure
            return v;
        };

        enum class Priority {
            // to do last
            ASSIGNMENT,
            COMPARISON,
            BINARY_SHIFT,
            PLUS_MINUS,
            ASTERISK_SLASH,
            ARRAY_ACCESS,
            MEMBER_ACCESS,
            // to do first
        };

        auto handleBinaryOperator = [&]<aui::derived_from<BinaryOperatorNode> T>(Priority p) {
            mIterator++;
            const int currentPriority = int(p);

            if (temporaryValue) {
                auto out = std::make_unique<T>();
                out->left = std::move(temporaryValue);
                binaryOperators << BinaryOperatorAndItsPriority {
                    .op = out.get(),
                    .priority = currentPriority,
                    .owning = std::move(out),
                };
                AUI_ASSERT(temporaryValue == nullptr);
                return;
            }

            for (const auto& o : binaryOperators | ranges::views::reverse) {
                if (o.priority < currentPriority && o.op->right) {
                    // steal rhs
                    auto currentOperator = std::make_unique<T>();
                    currentOperator->left = std::move(o.op->right);
                    auto ptr = currentOperator.get();
                    o.op->right = std::move(currentOperator);
                    binaryOperators << BinaryOperatorAndItsPriority {
                        .op = ptr,
                        .priority = currentPriority,
                    };
                    return;
                }
            }
            if (!binaryOperators.empty()) {
                auto root = std::min_element(
                    binaryOperators.begin(), binaryOperators.end(),
                    [](const BinaryOperatorAndItsPriority& lhs, const BinaryOperatorAndItsPriority& rhs) {
                        return lhs.priority < rhs.priority;
                    });
                auto out = std::make_unique<T>();
                AUI_ASSERT(root->owning != nullptr);
                out->left = std::move(root->owning);
                binaryOperators << BinaryOperatorAndItsPriority {
                    .op = out.get(),
                    .priority = currentPriority,
                    .owning = std::move(out),
                };
                AUI_ASSERT(temporaryValue == nullptr);
                return;
            }

            throw AException {};
        };

        auto handleUnaryOperator = [&]<aui::derived_from<INode> T>() {

        };

        for (; mIterator != mTokens.end();) {
            const auto& currentTokenValue = currentToken();
            switch (currentTokenValue.index()) {
                case got<token::Identifier>: {
                    if (auto it = std::next(mIterator);
                        it != mTokens.end()) {
                        switch (it->index()) {
                            case got<token::LPar>:
                                putValue(parseFunctionCall());
                                continue;
                            case got<token::Colon>:
                                putValue(parseRange());
                                continue;
                            default:
                                break;
                        }
                    }
                    putValue(parseIdentifier());
                    mIterator++;
                    break;
                }

                case got<token::Plus>: {
                    handleBinaryOperator.operator()<BinaryOperatorNodeImpl<std::plus<>>>(Priority::PLUS_MINUS);
                    break;
                }

                case got<token::Minus>: {
                    handleBinaryOperator.operator()<BinaryOperatorNodeImpl<std::minus<>>>(Priority::PLUS_MINUS);
                    break;
                }

                case got<token::Asterisk>: {   // pointer dereference or multiply
                    handleBinaryOperator.
                    operator()<BinaryOperatorNodeImpl<std::multiplies<>>>(Priority::ASTERISK_SLASH);
                    break;
                }

                case got<token::Slash>: {   // divide
                    handleBinaryOperator.
                    operator()<BinaryOperatorNodeImpl<std::divides<>>>(Priority::ASTERISK_SLASH);
                    break;
                }

                case got<token::LAngle>: {
                    handleBinaryOperator.
                        operator()<BinaryOperatorNodeImpl<std::less<>>>(Priority::COMPARISON);
                    break;
                }

                case got<token::RAngle>: {
                    handleBinaryOperator.
                        operator()<BinaryOperatorNodeImpl<std::greater<>>>(Priority::COMPARISON);
                    break;
                }

                case got<token::Double>: {
                    putValue(parseDouble());
                    ++mIterator;
                    break;
                }

                case got<token::StringLiteral>: {
                    putValue(parseStringLiteral());
                    ++mIterator;
                    break;
                }

                case got<token::LPar>: {
                    ++mIterator;
                    putValue(parseExpression());
                    expect<token::RPar>();
                    ++mIterator;
                    break;
                }

                default:
                    goto naxyi;
            }
        }
    naxyi:
        if (temporaryValue && !binaryOperators.empty()) {
            // should assign it to some operator
            for (const auto& o : binaryOperators | ranges::views::reverse) {
                if (o.op->right == nullptr) {
                    o.op->right = std::move(temporaryValue);
                    AUI_ASSERT(binaryOperators.first().owning != nullptr);
                    return std::move(binaryOperators.first().owning);
                }
            }
            throw AException {};
        }
        if (!binaryOperators.empty()) {
            auto e = ranges::min_element(binaryOperators, [](const auto& l, const auto& r) {
                return l.priority <= r.priority;
            });
            AUI_ASSERT(e->owning != nullptr);
            return std::move(e->owning);
        }
        if (temporaryValue) {
            return temporaryValue;
        }
        throw AException {};
    }

private:
    std::span<token::Any> mTokens;
    std::span<token::Any>::iterator mIterator = mTokens.begin();

    const token::Any& currentToken() { return *safeIteratorRead(mIterator); }

    std::span<token::Any>::iterator safeIteratorRead(std::span<token::Any>::iterator it) {
        if (it == mTokens.end()) {
            throw AException("END");
        }
        return it;
    }

    template <typename T>
    const T& expect() {
        return ::expect<T>(currentToken());
    }

    _unique<INode> parseFunctionCall() {
        struct FunctionCall : INode {
            functions::Invocable function;
            AVector<_unique<INode>> args;

            ~FunctionCall() override = default;
            formula::Value evaluate(const Spreadsheet& ctx) override {
                return function(functions::Ctx {
                  .spreadsheet = ctx,
                  .args = AVector(
                      args | ranges::view::transform([&](const _unique<INode>& node) { return node->evaluate(ctx); }) |
                      ranges::to_vector),
                });
            }
        };
        auto out = std::make_unique<FunctionCall>();
        out->function = functions::predefined().at(expect<token::Identifier>().name.uppercase());
        mIterator++;
        expect<token::LPar>();
        mIterator++;

        for (;;) {
            switch (auto it = safeIteratorRead(mIterator); it->index()) {
                case got<token::RPar>:
                    ++mIterator;
                    return out;
                case got<token::Semicolon>:
                    ++mIterator;
                    break;
                default:
                    out->args << parseExpression();
            }
        }

        return out;
    }

    _unique<INode> parseIdentifier() { return std::make_unique<IdentifierNode>(expect<token::Identifier>().name); }

    _unique<INode> parseDouble() { return std::make_unique<DoubleNode>(expect<token::Double>().value); }
    _unique<INode> parseStringLiteral() { return std::make_unique<StringLiteralNode>(expect<token::StringLiteral>().value); }

    _unique<INode> parseRange() {
        formula::Range rng;
        rng.from = Cell::fromName(expect<token::Identifier>().name);
        mIterator++;
        expect<token::Colon>();
        mIterator++;
        rng.to = Cell::fromName(expect<token::Identifier>().name);
        mIterator++;
        return std::make_unique<RangeNode>(rng);
    }
};

}   // namespace

_unique<INode> ast::parseExpression(std::span<token::Any> tokens) { return AstState { tokens }.parseExpression(); }

src/Tokens.cpp#

#include "Tokens.h"

AVector<token::Any> token::parse(aui::no_escape<ATokenizer> t) {
    AVector<token::Any> out;
    t->readChar();   // =
    try {
        while (!t->isEof()) {
            switch (char c = t->readChar()) {
                case ' ':
                    break;
                case '(':
                    out << token::LPar {};
                    break;
                case ')':
                    out << token::RPar {};
                    break;
                case ';':
                    out << token::Semicolon {};
                    break;
                case ':':
                    out << token::Colon {};
                    break;
                case '+':
                    out << token::Plus {};
                    break;
                case '-':
                    out << token::Minus {};
                    break;
                case '*':
                    out << token::Asterisk {};
                    break;
                case '/':
                    out << token::Slash {};
                    break;
                case '<':
                    out << token::LAngle {};
                    break;
                case '>':
                    out << token::RAngle {};
                    break;
                case '\'':
                    out << token::StringLiteral { t->readStringUntilUnescaped('\'') };
                    break;
                case '"':
                    out << token::StringLiteral { t->readStringUntilUnescaped('"') };
                    break;
                default:
                    if ('0' <= c && c <= '9') {
                        t->reverseByte();
                        out << token::Double { t->readFloat() };
                        continue;
                    }
                    if ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
                        t->reverseByte();
                        out << token::Identifier { t->readStringWhile([](char c) -> bool { return std::isalnum(c); }) };
                        continue;
                    }
                    throw AException("UNEXPECTED {}"_format(c));
            }
        }
    } catch (const AEOFException&) {}
    return out;
}

src/main.cpp#

#include <AUI/Platform/Entry.h>
#include <AUI/Platform/AWindow.h>
#include <AUI/Util/UIBuildingHelpers.h>
#include <AUI/View/AButton.h>
#include "AUI/View/ATextField.h"
#include "AUI/View/AScrollArea.h"
#include "AUI/View/AGridSplitter.h"
#include "Spreadsheet.h"

using namespace declarative;
using namespace ass;

struct State {
    Spreadsheet spreadsheet{glm::uvec2 { 'Z' - 'A' + 1, 100 }};
    AProperty<AString> currentExpression;
};

static _<AView> labelTitle(AString s) {
    return _new<ALabel>(std::move(s)) AUI_WITH_STYLE {
        Opacity { 0.5f },
        ATextAlign::CENTER,
    };
}

class CellView : public AViewContainer {
public:
    CellView(_<State> state, Cell& cell) : mState(std::move(state)), mCell(cell) { inflateLabel(); }
    int getContentMinimumWidth() override { return 0; }
    int getContentMinimumHeight() override { return 0; }

private:
    _<State> mState;
    Cell& mCell;
    AAbstractSignal::AutoDestroyedConnection mConnection;

    void inflateLabel() {
        mConnection = connect(mCell.value, [this](const formula::Value& v) {
            ALayoutInflater::inflate(
                this,
                std::visit(
                    aui::lambda_overloaded {
                      [](std::nullopt_t) -> _<AView> { return _new<AView>(); },
                      [](double v) -> _<AView> { return Label { "{}"_format(v) } AUI_WITH_STYLE { ATextAlign::RIGHT }; },
                      [](const AString& v) -> _<AView> { return Label { "{}"_format(v) }; },
                      [](const formula::Range& v) -> _<AView> { return Label { "#RANGE?" }; },
                    },
                    v));
            connect(getViews().first()->clicked, me::inflateEditor);
        });
    }

    void inflateEditor() {
        mState->currentExpression = mCell.expression;
        ALayoutInflater::inflate(
            this,
            _new<ATextField>() AUI_WITH_STYLE {
                  MinSize { 0 },
                  Margin { 0 },
                  BorderRadius { 0 },
                } AUI_LET {
                    it && mState->currentExpression;
                    it->focus();
                    connect(it->focusLost, me::commitExpression);
                });
    }

    void commitExpression() {
        mCell.expression = mState->currentExpression;
        inflateLabel();
    }
};

class CellsView : public AViewContainer {
public:
    CellsView(_<State> state) : mState(std::move(state)) {
        ALayoutInflater::inflate(
            this,
            AGridSplitter::Builder()
                    .noDefaultSpacers()
                    .withItems([&] {
                        AVector<AVector<_<AView>>> views;
                        views.resize(mState->spreadsheet.size().y + 1);
                        for (auto& c : views) {
                            c.resize(mState->spreadsheet.size().x + 1);
                        }

                        views[0][0] = _new<AView>();   // blank
                        for (unsigned i = 0; i < mState->spreadsheet.size().x; ++i) {
                            views[0][i + 1] = Centered{ labelTitle(Cell::columnName(i)) } AUI_WITH_STYLE { Expanding(1, 0) };
                        }
                        for (unsigned row = 0; row < mState->spreadsheet.size().y; ++row) {
                            views[row + 1][0] = labelTitle("{}"_format(Cell::rowName(row)));
                            for (unsigned column = 0; column < mState->spreadsheet.size().x; ++column) {
                                views[row + 1][column + 1] = _new<CellView>(mState, mState->spreadsheet[{ column, row }]) AUI_WITH_STYLE {
                                    BackgroundSolid { AColor::WHITE },
                                };
                            }
                        }
                        return views;
                    }())
                    .build() AUI_WITH_STYLE { Expanding(), LayoutSpacing { 1_dp }, MinSize { 80_dp * float(mState->spreadsheet.size().x), {} } });
    }

private:
    _<State> mState;
};

class CellsWindow : public AWindow {
public:
    CellsWindow() : AWindow("AUI - 7GUIs - Cells", 500_dp, 400_dp) {
        setContents(Centered {
          AScrollArea::Builder()
                  .withContents(Horizontal { _new<CellsView>(_new<State>()) })
                  .build() AUI_WITH_STYLE {
                Expanding(),
                ScrollbarAppearance(ScrollbarAppearance::ALWAYS, ScrollbarAppearance::ALWAYS),
              },
        } AUI_WITH_STYLE { Padding(0) });
    }
};

AUI_ENTRY {
    _new<CellsWindow>()->show();
    return 0;
}

src/Functions.cpp#

// Created by alex2772 on 3/7/25.
//

#include <range/v3/all.hpp>
#include "Functions.h"

namespace {
template<aui::invocable<formula::Value> Callback>
void forEachArgAndRangeCell(const functions::Ctx& ctx, Callback&& callback) {
    for (const auto& arg : ctx.args) {
        if (auto rng = std::get_if<formula::Range>(&arg)) {
            for (const auto& cell : ctx.spreadsheet[*rng]) {
                callback(cell.value);
            }
            continue;
        }
        callback(arg);
    }
}
}

const AMap<AString, functions::Invocable>& functions::predefined() {
    static AMap<AString, functions::Invocable> out = {
        { "SUM",
          [](Ctx ctx) {
              double accumulator = 0.0;
              forEachArgAndRangeCell(ctx, [&](const formula::Value& v) {
                  if (auto d = std::get_if<double>(&v)) {
                      accumulator += *d;
                  }
              });
              return accumulator;
          } },
        { "COUNT",
            [](Ctx ctx) {
              int accumulator  = 0;
              forEachArgAndRangeCell(ctx, [&](const formula::Value& v) {
                if (std::holds_alternative<std::nullopt_t>(v)) {
                    return;
                }
                accumulator++;
              });
              return double(accumulator);
            } },
        { "IF",
            [](Ctx ctx) {
              if (ctx.args.size() != 3) {
                  throw AException("ARG");
              }
              auto condition = std::get_if<double>(&ctx.args[0]);
              if (condition == nullptr) {
                  throw AException("ARG0");
              }
              if (*condition == 0.0) {
                  return ctx.args[2];
              }
              return ctx.args[1];
            } },
    };
    return out;
}

src/AST.h#

#pragma once

#include <AUI/Common/AObject.h>
#include "Tokens.h"
#include "Formula.h"

namespace ast {

class INodeVisitor {
public:
    virtual ~INodeVisitor() = default;

};

class INode {
public:
    INode() = default;
    virtual ~INode() = default;

    virtual formula::Value evaluate(const Spreadsheet& ctx) = 0;
};

_unique<INode> parseExpression(std::span<token::Any> tokens);

}