Skip to content

AUI Contacts#

Example's page

This page describes an example listed in ui category.

Usage of AUI_DECLARATIVE_FOR to make a contacts-like application.

UI is defined using a declarative syntax, where the structure and layout of the UI are specified as a series of function calls.

The application uses an AProperty named mContacts to store a vector of contact objects. Each contact object has properties like displayName, note, etc.

Source Code#

Repository

CMakeLists.txt#

1
2
3
aui_executable(aui.example.contacts)

aui_link(aui.example.contacts PRIVATE aui::core aui::views)

src/main.cpp#

#include <range/v3/all.hpp>
#include <AUI/View/AForEachUI.h>
#include <AUI/Platform/Entry.h>
#include "AUI/Platform/AWindow.h"
#include "AUI/Util/UIBuildingHelpers.h"
#include "AUI/View/AScrollArea.h"
#include "AUI/Model/AListModel.h"
#include <AUI/View/ATextField.h>
#include <AUI/View/AText.h>
#include "model/PredefinedContacts.h"

#include <view/ContactDetailsView.h>
#include <view/common.h>
#include <AUI/View/ASpacerFixed.h>
#include "AUI/Platform/AMessageBox.h"

using namespace declarative;
using namespace ass;
using namespace std::chrono_literals;

static constexpr auto CONTACTS_SORT = ranges::actions::sort(std::less {}, [](const _<Contact>& c) -> decltype(auto) { return *c->displayName; });

static auto groupLetter(const AString& s) { return s.empty() ? AChar(U'_') : s.first(); }

class ContactsWindow : public AWindow {
public:
    ContactsWindow() : AWindow("AUI Contacts", 600_dp, 300_dp) {
        setContents(
            Horizontal {
              AScrollArea::Builder()
                      .withContents(
                          Vertical {
                            _new<ATextField>() && mSearchQuery,
                            AText::fromString(predefined::DISCLAIMER) AUI_WITH_STYLE { ATextAlign::CENTER },
                            SpacerFixed(8_dp),
                            CustomLayout {} & mSearchQuery.readProjected([&](const AString& q) {
                                if (q.empty()) {
                                    return indexedList();
                                }
                                return searchQueryList();
                            }),
                            Label {}
                                & mSearchQuery.readProjected([](const AString& s) { return s.empty(); }) > &AView::setVisible
                                & mContactCount.readProjected([](std::size_t c) {
                                return "{} contact(s)"_format(c);
                            }) AUI_WITH_STYLE { FontSize { 10_pt }, ATextAlign::CENTER, Margin { 8_dp } },
                          } AUI_WITH_STYLE { Padding(0, 8_dp) })
                      .build() AUI_WITH_STYLE { Expanding(0, 1), MinSize(200_dp) },

              CustomLayout::Expanding {} & mSelectedContact.readProjected([this](const _<Contact>& selectedContact) -> _<AView> {
                  auto editor = contactDetails(selectedContact);
                  if (editor != nullptr) {
                      connect(selectedContact->displayName.changed, editor, [this] {
                          *mContacts.writeScope() |= CONTACTS_SORT;
                      });
                      connect(editor->deleteAction, me::deleteCurrentContact);
                  }
                  return editor;
              }) AUI_WITH_STYLE { Expanding(), MinSize(300_dp), BackgroundSolid { AColor::WHITE } },
            } AUI_WITH_STYLE {
              Padding(0),
            });
    }

private:
    AProperty<AVector<_<Contact>>> mContacts =
            predefined::PERSONS | ranges::views::transform([](Contact& p) { return _new<Contact>(std::move(p)); }) |
            ranges::to_vector | CONTACTS_SORT;
    APropertyPrecomputed<std::size_t> mContactCount = [this] { return mContacts->size(); };
    AProperty<_<Contact>> mSelectedContact = nullptr;
    AProperty<AString> mSearchQuery;
    APropertyPrecomputed<AString> mSearchQueryLowercased = [this] { return mSearchQuery->lowercase(); };

    void deleteCurrentContact() {
        if (mSelectedContact == nullptr) {
            return;
        }
        if (AMessageBox::show(this,
                              "Do you really want to delete?",
                              "This action is irreversible!",
                              AMessageBox::Icon::NONE, AMessageBox::Button::YES_NO) != AMessageBox::ResultButton::YES) {
            return;
        }
        mContacts.writeScope()->removeFirst(mSelectedContact);
        mSelectedContact = nullptr;
    }

    _<AView> indexedList() {
        return AUI_DECLARATIVE_FOR(group, *mContacts | ranges::views::chunk_by([](const _<Contact>& lhs, const _<Contact>& rhs) {
                                return groupLetter(lhs->displayName) == groupLetter(rhs->displayName);
                            }), AVerticalLayout) {
            auto firstContact = *ranges::begin(group);
            auto firstLetter = groupLetter(firstContact->displayName);
            ALogger::info("Test") << "Computing view for group " << AString(1, firstLetter);
            return Vertical {
                Label { firstLetter } AUI_WITH_STYLE {
                                        Opacity(0.5f),
                                        Padding { 12_dp, 0, 4_dp },
                                        Margin { 0 },
                                        FontSize { 8_pt },
                                      },
                common_views::divider(),
                AUI_DECLARATIVE_FOR(i, group, AVerticalLayout) {
                    ALogger::info("Test") << "Computing view for item " << i->displayName;
                    return contactPreview(i);
                },
            };
        };
    }

    _<AView> searchQueryList() {
        auto searchFilter = ranges::views::filter([&](const _<Contact>& c) {
            for (const auto& field : { c->displayName, c->note }) {
                if (field->lowercase().contains(mSearchQueryLowercased.value())) {
                    return true;
                }
            }
            return false;
        });
        return AUI_DECLARATIVE_FOR(i, *mContacts | searchFilter, AVerticalLayout) {
            return contactPreview(i);
        };
    }

    _<AView> contactPreview(const _<Contact>& contact) {
        return Vertical {
            Label {} & contact->displayName AUI_WITH_STYLE { Padding { 8_dp, 0 }, Margin { 0 }, ATextOverflow::ELLIPSIS },
            common_views::divider(),
        } AUI_LET {
            connect(it->clicked, [this, contact] { mSelectedContact = contact; });
        };
    }

    _<ContactDetailsView> contactDetails(const _<Contact>& contact) {
        if (!contact) {
            return nullptr;
        }
        return _new<ContactDetailsView>(contact);
    }
};

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

src/view/common.h#

1
2
3
4
5
6
7
#pragma once

#include <AUI/View/AView.h>

namespace common_views {
_<AView> divider();
}

src/view/ContactDetailsView.cpp#

#include "ContactDetailsView.h"
#include "AUI/View/ATextField.h"
#include "common.h"
#include "AUI/Platform/AMessageBox.h"
#include <AUI/Util/UIBuildingHelpers.h>
#include <AUI/View/AButton.h>
#include <AUI/View/AScrollArea.h>
#include <AUI/View/ATextArea.h>

using namespace ass;
using namespace declarative;

static constexpr auto EDITOR_CONTENT_MAX_WIDTH = 400_dp;

namespace {
_<AView> profilePhoto(const _<Contact>& contact) {
    return Centered {
        Label {} & contact->displayName.readProjected([](const AString& s) {
            return s.empty() ? "?" : AString(1, s.first()).uppercase();
        }) AUI_WITH_STYLE { Opacity(0.5f), FontSize { 32_dp } },
    } AUI_WITH_STYLE {
        FixedSize { 64_dp },
        BorderRadius { 32_dp },
        BackgroundGradient { AColor::GRAY.lighter(0.5f), AColor::GRAY, 163_deg },
    };
}

template <typename T>
_<AView> viewer(AProperty<T>& property) {
    return Label {} & property.readProjected([](const T& v) { return "{}"_format(v); });
}

template <typename T>
_<AView> editor(AProperty<T>& property);

template <>
_<AView> editor(AProperty<AString>& property) {
    return _new<ATextField>() && property;
}
}   // namespace

template <typename T>
_<AView> ContactDetailsView::presentation(AProperty<T>& property) {
    if (mEditorMode) {
        return editor(property) << ".row-value";
    }
    return viewer(property) << ".row-value";
}

ContactDetailsView::ContactDetailsView(_<Contact> contact) : mContact(std::move(contact)) {
    mOriginalContact = mContact;
    setExtraStylesheet(AStylesheet {
      {
        c(".row-value"),
        Expanding(1, 0),
      },
    });
    connect(mEditorMode, [this] {
        setContents(Vertical::Expanding {
          AScrollArea::Builder().withContents(Centered {
            Vertical::Expanding {
              Horizontal {
                profilePhoto(mContact),
                Centered::Expanding {
                  presentation(mContact->displayName) AUI_WITH_STYLE { FontSize { 12_pt } },
                },
              } AUI_WITH_STYLE { Margin { 8_dp, {} } },
              row("Phone", mContact->phone),
              row("Address", mContact->address),
              row("Email", mContact->email),
              row("Homepage", mContact->homepage),
              Horizontal::Expanding {
                Vertical {
                  Label { "Note" } AUI_WITH_STYLE { FixedSize { 100_dp, {} }, Opacity { 0.5f }, ATextAlign::RIGHT },
                },
                _new<ATextArea>() && mContact->note,
              } AUI_WITH_STYLE {
                    MinSize { {}, 100_dp },
                  },
            } AUI_WITH_STYLE { MaxSize(EDITOR_CONTENT_MAX_WIDTH, {}), Padding(8_dp) },
          }),
          Centered {
            Horizontal::Expanding {
              SpacerExpanding(),
              Button { mEditorMode ? "Discard" : "Delete" } AUI_LET { connect(it->clicked, me::drop); },
              Button { mEditorMode ? "Done" : "Edit" } AUI_LET { connect(it->clicked, me::toggleEdit); },
            } AUI_WITH_STYLE { MaxSize(EDITOR_CONTENT_MAX_WIDTH, {}), Padding(4_dp) },
          },
        });
    });
}

void ContactDetailsView::drop() {
    if (!mEditorMode) {
        // delete
        emit deleteAction;
        return;
    }

    // discard
    if (AMessageBox::show(dynamic_cast<AWindow*>(AWindow::current()), "Do you really want to discard?", "This action is irreversible!", AMessageBox::Icon::NONE, AMessageBox::Button::YES_NO) != AMessageBox::ResultButton::YES) {
        return;
    }
    mContact = mOriginalContact;
    mEditorMode = false;
}

void ContactDetailsView::toggleEdit() {
    if (mEditorMode) {
        // done
        *mOriginalContact = std::move(*mContact);
        mContact = mOriginalContact;
    } else {
        // edit
        mContact = _new<Contact>(*mOriginalContact);
    }
    mEditorMode = !mEditorMode;
}

template <typename T>
_<AView> ContactDetailsView::row(AString title, AProperty<T>& property) {
    if (!mEditorMode) {
        if (property == T {}) {
            return nullptr;
        }
    }
    return Vertical {
        Horizontal {
          Label { std::move(title) } AUI_WITH_STYLE { FixedSize { 100_dp, {} }, Opacity { 0.5f }, ATextAlign::RIGHT },
          presentation(property),
        },
        common_views::divider(),
    };
}

src/view/common.cpp#

1
2
3
4
5
6
7
8
9
#include "common.h"
#include <AUI/Util/UIBuildingHelpers.h>

using namespace ass;
using namespace declarative;

_<AView> common_views::divider() {
    return _new<AView>() AUI_WITH_STYLE { FixedSize { {}, 1_px }, BackgroundSolid { AColor::GRAY } };
}

src/view/ContactDetailsView.h#

#pragma once

#include <model/Contact.h>
#include <AUI/View/AViewContainer.h>

class ContactDetailsView : public AViewContainerBase {
public:
    ContactDetailsView(_<Contact> contact);

signals:
    emits<> deleteAction;

private:
    _<Contact> mContact;
    _<Contact> mOriginalContact;
    AProperty<bool> mEditorMode = false;

    template<typename T>
    _<AView> presentation(AProperty<T>& property);

    template<typename T>
    _<AView> row(AString title, AProperty<T>& property);

    void drop();
    void toggleEdit();
};

src/model/PredefinedContacts.h#

// Copyright (C) 2020-2025 Alex2772 and Contributors
//
// SPDX-License-Identifier: MPL-2.0
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#include "Contact.h"

namespace predefined {
static constexpr auto DISCLAIMER = "Any similarity to actual persons, living or dead, that appear in this C++ program, are purely coincidental";
inline std::array PERSONS = {
  Contact{ .displayName = "Bryce Adelstein Lelbach" },
  Contact{ .displayName = "Matt Austern" },
  Contact{ .displayName = "Aaron Ballman" },
  Contact{ .displayName = "JF Bastien" },
  Contact{ .displayName = "Dean Michael Berris" },
  Contact{ .displayName = "Hans Boehm" },
  Contact{ .displayName = "Chandler Carruth" },
  Contact{ .displayName = "Stephen D. Clamage" },
  Contact{ .displayName = "Ben Craig" },
  Contact{ .displayName = "Guy Davidson" },
  Contact{ .displayName = "Hana Dusíková" },
  Contact{ .displayName = "Stefanus Du Toit" },
  Contact{ .displayName = "Glen Fernandes" },
  Contact{ .displayName = "Marco Foco" },
  Contact{ .displayName = "J. Daniel Garcia" },
  Contact{ .displayName = "Peter Gottschling" },
  Contact{ .displayName = "Bernhard Manfred Gruber" },
  Contact{ .displayName = "Michael Hava" },
  Contact{ .displayName = "Howard Hinnant" },
  Contact{ .displayName = "Tom Honermann" },
  Contact{ .displayName = "Erich Keane" },
  Contact{ .displayName = "Kyle Kloepper" },
  Contact{ .displayName = "Dietmar Kühl" },
  Contact{ .displayName = "Inbal Levi" },
  Contact{ .displayName = "Lisa Lippincott" },
  Contact{ .displayName = "William M. (Mike) Miller" },
  Contact{ .displayName = "Clark Nelson" },
  Contact{ .displayName = "Eric Niebler" },
  Contact{ .displayName = "Roger Orr" },
  Contact{ .displayName = "P.J. Plauger" },
  Contact{ .displayName = "Antony Polukhin" },
  Contact{ .displayName = "Mateusz Pusz" },
  Contact{ .displayName = "Nina Dinka Ranns" },
  Contact{ .displayName = "Bill Seymour" },
  Contact{ .displayName = "Peter Sommerlad" },
  Contact{ .displayName = "Bryan St. Amour" },
  Contact{ .displayName = "Bjarne Stroustrup" },
  Contact{ .displayName = "Herb Sutter" },
  Contact{ .displayName = "Andrew Sutton" },
  Contact{ .displayName = "Daveed Vandevoorde" },
  Contact{ .displayName = "JC van Winkel" },
  Contact{ .displayName = "Vassil Vassilev" },
  Contact{ .displayName = "Ville Voutilainen" },
  Contact{ .displayName = "Michael Wong" },
  Contact{ .displayName = "Jeffrey Yasskin" },
  Contact{ .displayName = "Niall Douglas" },
};
}

src/model/Contact.h#

// Copyright (C) 2020-2025 Alex2772 and Contributors
//
// SPDX-License-Identifier: MPL-2.0
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#pragma once

#include <AUI/Common/AObject.h>
#include "AUI/Common/AProperty.h"

struct Contact {
    AProperty<AString> displayName;
    AProperty<AString> phone;
    AProperty<AString> address;
    AProperty<AString> email;
    AProperty<AString> homepage;
    AProperty<AString> note;
};