Page MenuHomePhorge

No OneTemporary

Size
15 KB
Referenced Files
None
Subscribers
None
diff --git a/doc.org b/doc.org
index b920a24..df2cd58 100644
--- a/doc.org
+++ b/doc.org
@@ -1,188 +1,197 @@
#+PROPERTY: header-args :exports code
#+HTML_HEAD: <link rel="stylesheet" href="./style.css">
* Intro
Using [[https://github.com/arximboldi/lager][lager]] with Qt/QML
- tusooa, maintainer of [[https://kazv.chat][kazv]], [[https://lily-is.land/tusooa]] [[https://invent.kde.org/tusooaw]]
- Dmitry Kazakov, developer of [[https://krita.org][Krita]], https://invent.kde.org/dkazakov
* What is lager?
- Library to help with *value-oriented design*
- Store application state as a *value* type (*model*)
- 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++
+ // Assuming member-wise operator==()
+
// 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
- You can still use pointers internally
* Basic example of a value-oriented program
=logic.hpp=
#+INCLUDE: "src/basic-example/logic.hpp" src c++
=logic.cpp=
#+INCLUDE: "src/basic-example/logic.cpp" src c++
=main.cpp=
#+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)
=test.cpp=
#+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, [[https://github.com/arximboldi/immer][immer]] containers (constant time copy ctor)
- In Qt: QSharedDataPointer (simple, all-or-nothing sharing)
- 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
- [[https://github.com/arximboldi/ewig][ewig]], a text editor
* How to do IO?
** Reducers revisited
- Recall reducers are pure functions
- 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.
=model.hpp=
#+INCLUDE: "src/effects-example/model.hpp" src c++
=main.cpp=
#+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]]
+- [[https://sinusoid.es/lager/cursors.html][Cursors]] (I wrote that doc!)
- src_c++{lager::reader} - readonly cursors
- src_c++{reader}s can be watched
- src_c++{lager::store} is a reader
- - src_c++{map}-transformations and some lenses
+ - src_c++{map}-transformations
+ #+BEGIN_SRC c++
+ lager::reader<A> ra = ...;
+ lager::reader<B> rb = ra.map([](const A &a) -> B {
+ return ...;
+ });
+ #+END_SRC
+ - and lense transformations (Dmitry will cover this later)
- src_c++{LAGER_QT} and src_c++{LAGER_QT_READER}
=demo-app.hpp=
#+INCLUDE: "src/qml-example/demo-app.hpp" src c++
=demo-app.cpp=
#+INCLUDE: "src/qml-example/demo-app.cpp" src c++
=Main.qml=
#+INCLUDE: "src/qml-example/Main.qml" src c++
=main.cpp=
#+INCLUDE: "src/qml-example/main.cpp" src c++
* Practical application problems
Or: what =lager='s author didn't tell you
* List views
[[https://github.com/arximboldi/lager/issues/179]]
** Number as model in QML ListView
=model.hpp=
#+INCLUDE: "src/list-view/model.hpp" src c++
=demo-app.hpp=
#+INCLUDE: "src/list-view/demo-app.hpp" src c++
=demo-list.hpp=
#+INCLUDE: "src/list-view/demo-list.hpp" src c++
=demo-item.hpp=
#+INCLUDE: "src/list-view/demo-item.hpp" src c++
=demo-app.cpp=
#+INCLUDE: "src/list-view/demo-app.cpp" src c++
=demo-list.cpp=
#+INCLUDE: "src/list-view/demo-list.cpp" src c++
=demo-item.cpp=
#+INCLUDE: "src/list-view/demo-item.cpp" src c++
=Main.qml=
#+INCLUDE: "src/list-view/Main.qml" src qml
- Problem: scroll will reset every time we add/remove an item
- https://github.com/arximboldi/lager/issues/179
** Using a src_c++{QAbstractListModel}
=demo-list.hpp=
#+INCLUDE: "src/list-view-qt-model/demo-list.hpp" src c++
=demo-list.cpp=
#+INCLUDE: "src/list-view-qt-model/demo-list.cpp" src c++
=Main.qml=
#+INCLUDE: "src/list-view-qt-model/Main.qml" src qml
* Post-dispatch then-continuations
[[https://github.com/arximboldi/lager/issues/96]], [[https://github.com/arximboldi/lager/issues/106]]
#+BEGIN_SRC c++
// XXX Why can't I just use QMetaObject::invokeMethod
// with QueuedConnection?
// Let's bear with it now. Will go back to this later.
QTimer::singleShot(100, obj, [=]() {
repl(obj, ctx);
});
#+END_SRC
- If I use src_c++{invokeMethod}, what happens if I enter 2?
- src_c++{IncrementTwiceAction} is dispatched (reducer is queued)
- src_c++{repl} is queued
- Reducer is run
- Effect is run
- Two src_c++{IncrementAction} are dispatched
- src_c++{repl} is run
- It blocks the thread! Execution of reducers for the src_c++{IncrementAction}s are not run!
- We want it to run after src_c++{IncrementTwiceAction} has really finished
- If action A's effect dispatches B and B's effect dispatches C and C's effect dispatches D
- Want the chain to finish all the way
- lager does not support this (limited support at the time)
- Resolved by creating [[https://lily-is.land/kazv/libkazv/-/blob/servant/src/store/store.hpp?ref_type=heads][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
#+INCLUDE: "src/libkazvstore-example/main.cpp" src c++
* 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]]
=model.hpp=
#+INCLUDE: "src/multithreading/model.hpp" src c++
=demo-app.hpp=
#+INCLUDE: "src/multithreading/demo-app.hpp" src c++
=demo-app.cpp=
#+INCLUDE: "src/multithreading/demo-app.cpp" src c++
=Main.qml=
#+INCLUDE: "src/multithreading/Main.qml" src qml
* 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/list-view-qt-model/Main.qml b/src/list-view-qt-model/Main.qml
index 9e6d901..bc7c123 100644
--- a/src/list-view-qt-model/Main.qml
+++ b/src/list-view-qt-model/Main.qml
@@ -1,37 +1,37 @@
import QtQuick
import QtQuick.Controls
import org.kde.kirigami as Kirigami
import land.lilyis.tusooa.lagerqtdemo.listviewqtmodel
Kirigami.ApplicationWindow {
id: root
property DemoApp app: DemoApp {}
property DemoList list: app.list()
pageStack.initialPage: Kirigami.ScrollablePage {
ListView {
model: list
delegate: Kirigami.SwipeListItem {
id: delegateItem
required property int index
property DemoItem item: list.at(index)
- contentItem: TextInput {
+ contentItem: TextField {
text: item.text
- onTextChanged: item.text = text
+ onTextChanged: item.setText(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-qt-model/demo-list.cpp b/src/list-view-qt-model/demo-list.cpp
index 0534985..94f0cad 100644
--- a/src/list-view-qt-model/demo-list.cpp
+++ b/src/list-view-qt-model/demo-list.cpp
@@ -1,77 +1,79 @@
#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)
: QAbstractListModel(parent)
, m_internalCount(0)
, m_list(list)
, m_ctx(ctx)
, LAGER_QT(count)(m_list.map([](ListModel list) -> int {
return list.items.size();
}))
{
m_internalCount = count();
connect(this, &DemoList::countChanged, this, &DemoList::updateInternalCount);
}
DemoList::~DemoList() = default;
DemoItem *DemoList::at(int index) const
{
return new DemoItem(
- m_list[&ListModel::items][index][lager::lenses::or_default],
+ m_list.map([index](const ListModel &model) {
+ return index >= model.items.size()
+ ? ItemModel{}
+ : model.items[index];
+ }),
[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});
}
QVariant DemoList::data(
[[maybe_unused]] const QModelIndex & index,
[[maybe_unused]] int role) const
{
return QVariant();
}
int DemoList::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
} else {
return m_internalCount;
}
}
void DemoList::updateInternalCount()
{
auto curCount = count();
auto oldCount = m_internalCount;
if (curCount > oldCount) {
beginInsertRows(QModelIndex(), oldCount, curCount - 1);
m_internalCount = curCount;
endInsertRows();
} else if (curCount < oldCount) {
beginRemoveRows(QModelIndex(), curCount, oldCount - 1);
m_internalCount = curCount;
endRemoveRows();
}
}
diff --git a/src/list-view/Main.qml b/src/list-view/Main.qml
index 4513739..9f57c71 100644
--- a/src/list-view/Main.qml
+++ b/src/list-view/Main.qml
@@ -1,37 +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 {
+ contentItem: TextField {
text: item.text
- onTextChanged: item.text = text
+ onTextChanged: item.setText(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-item.cpp b/src/list-view/demo-item.cpp
index d4cda77..f24140c 100644
--- a/src/list-view/demo-item.cpp
+++ b/src/list-view/demo-item.cpp
@@ -1,20 +1,22 @@
#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{}
- ))
+ , m_updater(updater)
+ , LAGER_QT(text)(item.map([](ItemModel m) {
+ return QString::fromStdString(m.text);
+ }))
{
}
DemoItem::~DemoItem() = default;
+
+void DemoItem::setText(const QString &text)
+{
+ m_updater(text);
+}
diff --git a/src/list-view/demo-item.hpp b/src/list-view/demo-item.hpp
index d4c7105..8076cea 100644
--- a/src/list-view/demo-item.hpp
+++ b/src/list-view/demo-item.hpp
@@ -1,24 +1,28 @@
#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("")
+ std::function<void(QString)> m_updater;
+
public:
explicit DemoItem(lager::reader<ItemModel> item, std::function<void(QString)> updater, QObject *parent = nullptr);
~DemoItem() override;
- LAGER_QT_CURSOR(QString, text);
+ LAGER_QT_READER(QString, text);
+
+ Q_INVOKABLE void setText(const QString &text);
};
diff --git a/src/list-view/demo-list.cpp b/src/list-view/demo-list.cpp
index a7932b4..f273f2b 100644
--- a/src/list-view/demo-list.cpp
+++ b/src/list-view/demo-list.cpp
@@ -1,42 +1,44 @@
#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],
+ m_list.map([index](const ListModel &model) {
+ return index >= model.items.size()
+ ? ItemModel{}
+ : model.items[index];
+ }),
[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});
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 25, 11:29 PM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40067
Default Alt Text
(15 KB)

Event Timeline