Page MenuHomePhorge

No OneTemporary

Size
14 KB
Referenced Files
None
Subscribers
None
diff --git a/doc.org b/doc.org
index 82735a5..b88207d 100644
--- a/doc.org
+++ b/doc.org
@@ -1,109 +1,125 @@
#+PROPERTY: header-args :exports code
* What is lager?
- library to help with *value-oriented design*
- store application state as a *value* type
- state transformations are described in pure functions (*reducers*), src_c++{auto update(Model, Action) -> Model;}
- actions are also value types
* What is a value type?
- copyable
- equality comparable
#+BEGIN_SRC c++
T a = ...;
T b = a;
REQUIRE(a == b);
#+END_SRC
- identity does not depend on a reference
#+BEGIN_SRC c++
// not a value type
struct Person
{
std::string name;
std::vector<std::weak_ptr<Person>> contacts;
};
// value type
struct Person
{
using IdType = std::string;
IdType id;
std::string name;
std::vector<IdType> contacts;
};
#+END_SRC
* Basic example of a value-oriented program
#+INCLUDE: "src/basic-example/logic.hpp" src c++
#+INCLUDE: "src/basic-example/logic.cpp" src c++
#+INCLUDE: "src/basic-example/main.cpp" src c++
The basic example just covered doesn't even make use of lager (!!!)
* Benefits of using lager?
- Testability
- libkazv: [[https://lily-is.land/kazv/libkazv][76.9%]] test coverage
- libQuotient: [[https://sonarcloud.io/component_measures?metric=coverage&selected=quotient-im_libQuotient%3AQuotient&id=quotient-im_libQuotient][41.6%]] (raw), 46.9% (adjusted)
#+INCLUDE: "src/basic-example/test.cpp" src c++
- Offload work into worker threads
#+BEGIN_SRC c++
QtConcurrent::run([model]() { // capture model by value
save(model);
});
#+END_SRC
- Performance of copying? Structural sharing, immer containers
- Undo/redo for free
- History is a list of models
- Undo/redo = changing the current index
- Normal actions = push a new model onto the list
* How to do IO?
** Reducers revisited
- src_c++{auto update(Model, Action) -> Model;}
- But also src_c++{auto update(Model, Action) -> std::pair<Model, Effect>;}
- What is an effect? A (maybe non-pure) function that takes a *context* as an argument.
- A *store* contains the model and the context. It also has an event loop. Reducers will be run in the event loop.
- The store can be constructed with *dependencies*, which can be accessed through the context.
- The context can also be used to dispatch actions.
#+INCLUDE: "src/effects-example/main.cpp" src c++
* How can lager help you integrate value-oriented design with a Qt application?
** Missing part: view
- In the example, src_c++{show()} is the view.
- We want to show the state as a src_c++{QWidget} or src_c++{QQuickItem}.
- Signal/slot connections to update display values.
- [[https://sinusoid.es/lager/cursors.html][Cursors]]
- src_c++{lager::reader} - readonly cursors
- readers can be watched
- src_c++{lager::store} is a reader
- map-transformations and some lenses
- src_c++{LAGER_QT} and src_c++{LAGER_QT_READER}
* Practical application problems
* List views
[[https://github.com/arximboldi/lager/issues/179]]
** Number as model in QML ListView
+=main.cpp=
+#+INCLUDE "src/list-view/main.cpp" src c++
+=demo-app.hpp=
+#+INCLUDE "src/list-view/demo-app.hpp" src c++
+=demo-app.cpp=
+#+INCLUDE "src/list-view/demo-app.cpp" src c++
+=demo-list.hpp=
+#+INCLUDE "src/list-view/demo-list.hpp" src c++
+=demo-list.cpp=
+#+INCLUDE "src/list-view/demo-list.cpp" src c++
+=demo-item.hpp=
+#+INCLUDE "src/list-view/demo-item.hpp" src c++
+=demo-item.cpp=
+#+INCLUDE "src/list-view/demo-item.cpp" src c++
+=Main.qml=
+#+INCLUDE "src/list-view/Main.qml" src qml
** Using a src_c++{QAbstractListModel}
* Post-dispatch then-continuations
[[https://github.com/arximboldi/lager/issues/96]], [[https://github.com/arximboldi/lager/issues/106]]
- lager does not support this
- Resolved by creating another kind of store
- Effects return a Promise (for C++ programmers, it's more like src_c++{std::future}) instead of void
- src_c++{dispatch} can be src_c++{then}-ed, the callback will be run after the returned future becomes ready
* Offloading work into worker threads
- src_c++{ctx.dispatch} is thread-safe, can use it to pass down the result of a heavy computation
- When heavy computation needs to be atomic, put the event loop into a worker thread
- Create a secondary store that has an event loop in the UI thread
- This is needed because cursor operations are not thread-safe [[https://github.com/arximboldi/lager/issues/118]]
* Using src_c++{lager::state} (leave to Dmitry)
* Appendix
** Calculating libQuotient coverage adjusted
libQuotient includes the generated csapi folder into coverage, while libkazv does not. Here is the code that calculates the coverage of libQuotient excluding the csapi folder.
#+NAME: libquotient-coverage
#+BEGIN_SRC perl
my $csapiCoverage = 0.169;
my $csapiUncovered = 1_668;
my $totalLines = 11_229;
my $totalUncovered = 6_561;
my $csapiLines = $csapiUncovered / (1-$csapiCoverage);
my $remainingLines = $totalLines - $csapiLines;
my $remainingUncovered = $totalUncovered - $csapiUncovered;
my $remainingCoverage = 1 - $remainingUncovered / $remainingLines;
$remainingCoverage;
#+END_SRC
#+RESULTS: libquotient-coverage
: 0.46940827964562
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 82b00c5..0ff893e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,2 +1,3 @@
add_subdirectory(basic-example)
add_subdirectory(effects-example)
+add_subdirectory(list-view)
diff --git a/src/list-view/CMakeLists.txt b/src/list-view/CMakeLists.txt
new file mode 100644
index 0000000..703a6fa
--- /dev/null
+++ b/src/list-view/CMakeLists.txt
@@ -0,0 +1,25 @@
+qt_add_qml_module(list-view-qmlmodule
+ STATIC
+ URI land.lilyis.tusooa.lagerqtdemo.listview
+ VERSION 0.0
+ SOURCES demo-list.cpp demo-item.cpp demo-app.cpp
+ QML_FILES Main.qml
+)
+
+target_link_libraries(list-view-qmlmodule
+ PUBLIC
+ Qt6::Quick
+ Qt6::Concurrent
+)
+
+add_executable(list-view-demo
+ main.cpp
+)
+
+target_link_libraries(list-view-demo
+ PRIVATE
+ Qt6::Gui
+ Qt6::Widgets
+ list-view-qmlmodule
+ list-view-qmlmoduleplugin
+)
diff --git a/src/list-view/Main.qml b/src/list-view/Main.qml
new file mode 100644
index 0000000..4513739
--- /dev/null
+++ b/src/list-view/Main.qml
@@ -0,0 +1,37 @@
+
+import QtQuick
+import QtQuick.Controls
+import org.kde.kirigami as Kirigami
+import land.lilyis.tusooa.lagerqtdemo.listview
+
+Kirigami.ApplicationWindow {
+ id: root
+ property DemoApp app: DemoApp {}
+ property DemoList list: app.list()
+ pageStack.initialPage: Kirigami.ScrollablePage {
+ ListView {
+ model: list.count
+
+ delegate: Kirigami.SwipeListItem {
+ id: delegateItem
+ required property int index
+ property DemoItem item: list.at(index)
+ contentItem: TextInput {
+ text: item.text
+ onTextChanged: item.text = text
+ }
+ actions: [
+ Kirigami.Action {
+ icon.name: 'delete'
+ text: 'Remove'
+ onTriggered: root.list.remove(delegateItem.index)
+ }
+ ]
+ }
+ }
+ }
+ footer: Button {
+ text: 'Add'
+ onClicked: list.add(`Item ${list.count}`)
+ }
+}
diff --git a/src/list-view/demo-app.cpp b/src/list-view/demo-app.cpp
new file mode 100644
index 0000000..23455fc
--- /dev/null
+++ b/src/list-view/demo-app.cpp
@@ -0,0 +1,30 @@
+
+#include <immer/config.hpp>
+#include <lager/config.hpp>
+#include <lager/event_loop/qt.hpp>
+#include "demo-app.hpp"
+#include "demo-list.hpp"
+
+struct DemoApp::Private
+{
+ lager::store<ListAction, ListModel> store;
+};
+
+DemoApp::DemoApp(QObject *parent)
+ : QObject(parent)
+ , m_d(new Private{lager::make_store<ListAction>(
+ ListModel{},
+ lager::with_qt_event_loop{*this}
+ )})
+{
+}
+
+DemoApp::~DemoApp() = default;
+
+DemoList *DemoApp::list() const
+{
+ return new DemoList(
+ m_d->store,
+ m_d->store
+ );
+}
diff --git a/src/list-view/demo-app.hpp b/src/list-view/demo-app.hpp
new file mode 100644
index 0000000..1ac2ea0
--- /dev/null
+++ b/src/list-view/demo-app.hpp
@@ -0,0 +1,28 @@
+
+#pragma once
+#include <immer/config.hpp>
+#include <lager/config.hpp>
+#include <memory>
+#include <lager/extra/qt.hpp>
+#include <lager/store.hpp>
+#include <QObject>
+#include <QQmlEngine>
+#include "model.hpp"
+Q_MOC_INCLUDE("demo-list.hpp")
+
+class DemoList;
+class DemoApp : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ explicit DemoApp(QObject *parent = nullptr);
+ ~DemoApp() override;
+
+ Q_INVOKABLE DemoList *list() const;
+
+private:
+ struct Private;
+ std::unique_ptr<Private> m_d;
+};
diff --git a/src/list-view/demo-item.cpp b/src/list-view/demo-item.cpp
new file mode 100644
index 0000000..d4cda77
--- /dev/null
+++ b/src/list-view/demo-item.cpp
@@ -0,0 +1,20 @@
+
+#include <immer/config.hpp>
+#include <lager/config.hpp>
+#include <lager/extra/qt.hpp>
+#include <lager/setter.hpp>
+#include "demo-item.hpp"
+
+DemoItem::DemoItem(lager::reader<ItemModel> item, std::function<void(QString)> updater, QObject *parent)
+ : QObject(parent)
+ , LAGER_QT(text)(lager::with_setter(
+ item.map([](ItemModel m) {
+ return QString::fromStdString(m.text);
+ }).make(),
+ updater,
+ lager::automatic_tag{}
+ ))
+{
+}
+
+DemoItem::~DemoItem() = default;
diff --git a/src/list-view/demo-item.hpp b/src/list-view/demo-item.hpp
new file mode 100644
index 0000000..d4c7105
--- /dev/null
+++ b/src/list-view/demo-item.hpp
@@ -0,0 +1,24 @@
+
+#pragma once
+#include <immer/config.hpp>
+#include <lager/config.hpp>
+#include <lager/extra/qt.hpp>
+#include <lager/reader.hpp>
+#include <QObject>
+#include <QString>
+#include <QQmlEngine>
+#include "model.hpp"
+
+class DemoItem : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+public:
+ explicit DemoItem(lager::reader<ItemModel> item, std::function<void(QString)> updater, QObject *parent = nullptr);
+
+ ~DemoItem() override;
+
+ LAGER_QT_CURSOR(QString, text);
+};
diff --git a/src/list-view/demo-list.cpp b/src/list-view/demo-list.cpp
new file mode 100644
index 0000000..a7932b4
--- /dev/null
+++ b/src/list-view/demo-list.cpp
@@ -0,0 +1,42 @@
+
+#include <immer/config.hpp>
+#include <lager/config.hpp>
+#include <lager/lenses/optional.hpp>
+#include <lager/lenses/at.hpp>
+#include "demo-list.hpp"
+#include "demo-item.hpp"
+
+DemoList::DemoList(
+ lager::reader<ListModel> list,
+ lager::context<ListAction> ctx,
+ QObject *parent)
+ : QObject(parent)
+ , m_list(list)
+ , m_ctx(ctx)
+ , LAGER_QT(count)(m_list.map([](ListModel list) -> int {
+ return list.items.size();
+ }))
+{
+}
+
+DemoList::~DemoList() = default;
+
+DemoItem *DemoList::at(int index) const
+{
+ return new DemoItem(
+ m_list[&ListModel::items][index][lager::lenses::or_default],
+ [ctx=m_ctx, index](QString newText) {
+ ctx.dispatch(EditItemAction{index, newText.toStdString()});
+ }
+ );
+}
+
+void DemoList::add(QString text)
+{
+ m_ctx.dispatch(AddItemAction{text.toStdString()});
+}
+
+void DemoList::remove(int index)
+{
+ m_ctx.dispatch(RemoveItemAction{index});
+}
diff --git a/src/list-view/demo-list.hpp b/src/list-view/demo-list.hpp
new file mode 100644
index 0000000..b57ec7f
--- /dev/null
+++ b/src/list-view/demo-list.hpp
@@ -0,0 +1,37 @@
+
+#pragma once
+#include <immer/config.hpp>
+#include <lager/config.hpp>
+#include <lager/context.hpp>
+#include <lager/reader.hpp>
+#include <lager/extra/qt.hpp>
+#include <QObject>
+#include <QQmlEngine>
+#include "model.hpp"
+Q_MOC_INCLUDE("demo-item.hpp")
+
+class DemoItem;
+class DemoList : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+ lager::reader<ListModel> m_list;
+ lager::context<ListAction> m_ctx;
+
+public:
+ explicit DemoList(
+ lager::reader<ListModel> list,
+ lager::context<ListAction> ctx,
+ QObject *parent = nullptr);
+ ~DemoList() override;
+
+ LAGER_QT_READER(int, count);
+
+ Q_INVOKABLE DemoItem *at(int index) const;
+
+ Q_INVOKABLE void add(QString text);
+
+ Q_INVOKABLE void remove(int index);
+};
diff --git a/src/list-view/main.cpp b/src/list-view/main.cpp
new file mode 100644
index 0000000..84e0e67
--- /dev/null
+++ b/src/list-view/main.cpp
@@ -0,0 +1,14 @@
+
+#include <QApplication>
+#include <QQmlApplicationEngine>
+
+using namespace Qt::Literals::StringLiterals;
+
+int main(int argc, char *argv[])
+{
+ QApplication app(argc, argv);
+ QQmlApplicationEngine engine;
+ engine.loadFromModule(u"land.lilyis.tusooa.lagerqtdemo.listview"_s, u"Main"_s);
+
+ return app.exec();
+}
diff --git a/src/list-view/model.hpp b/src/list-view/model.hpp
new file mode 100644
index 0000000..4538c53
--- /dev/null
+++ b/src/list-view/model.hpp
@@ -0,0 +1,78 @@
+
+#pragma once
+#include <immer/config.hpp>
+#include <lager/config.hpp>
+#include <immer/flex_vector.hpp>
+#include <string>
+#include <variant>
+
+struct ItemModel
+{
+ std::string text;
+ friend inline bool operator==(const ItemModel &a, const ItemModel &b)
+ {
+ return a.text == b.text;
+ }
+
+ friend inline bool operator!=(const ItemModel &a, const ItemModel &b)
+ {
+ return !(a == b);
+ }
+};
+
+struct ListModel
+{
+ immer::flex_vector<ItemModel> items;
+ friend inline bool operator==(const ListModel &a, const ListModel &b)
+ {
+ return a.items == b.items;
+ }
+
+ friend inline bool operator!=(const ListModel &a, const ListModel &b)
+ {
+ return !(a == b);
+ }
+};
+
+struct AddItemAction
+{
+ std::string text;
+};
+
+struct RemoveItemAction
+{
+ int index;
+};
+
+struct EditItemAction
+{
+ int index;
+ std::string text;
+};
+
+using ListAction = std::variant<AddItemAction, RemoveItemAction, EditItemAction>;
+
+[[nodiscard]] inline ListModel update(ListModel m, AddItemAction a)
+{
+ m.items = m.items.push_back({a.text});
+ return m;
+}
+
+[[nodiscard]] inline ListModel update(ListModel m, RemoveItemAction a)
+{
+ m.items = m.items.erase(a.index);
+ return m;
+}
+
+[[nodiscard]] inline ListModel update(ListModel m, EditItemAction a)
+{
+ m.items = m.items.set(a.index, {a.text});
+ return m;
+}
+
+[[nodiscard]] inline ListModel update(ListModel m, ListAction a)
+{
+ return std::visit([&](auto action) {
+ return update(m, action);
+ }, a);
+}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Nov 24, 3:50 PM (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39458
Default Alt Text
(14 KB)

Event Timeline