From 5afe51908377c59a044012d03f58b1ba59db781d Mon Sep 17 00:00:00 2001 From: Khushboo Mehta Date: Thu, 19 Mar 2026 19:42:15 +0100 Subject: [PATCH] feat: add tutprial for c++ ui --- logos-calc-cpp-ui/CMakeLists.txt | 32 + logos-calc-cpp-ui/flake.nix | 43 ++ logos-calc-cpp-ui/icons/calc.png | Bin 0 -> 91 bytes logos-calc-cpp-ui/metadata.json | 10 + logos-calc-cpp-ui/module.yaml | 19 + logos-calc-cpp-ui/src/calc_backend.cpp | 10 + logos-calc-cpp-ui/src/calc_backend.h | 27 + logos-calc-cpp-ui/src/calc_ui_cpp_plugin.cpp | 42 + logos-calc-cpp-ui/src/calc_ui_cpp_plugin.h | 24 + logos-calc-cpp-ui/src/qml/Main.qml | 100 +++ logos-developer-guide.md | 34 + tutorial-cpp-ui-app.md | 769 +++++++++++++++++++ 12 files changed, 1110 insertions(+) create mode 100644 logos-calc-cpp-ui/CMakeLists.txt create mode 100644 logos-calc-cpp-ui/flake.nix create mode 100644 logos-calc-cpp-ui/icons/calc.png create mode 100644 logos-calc-cpp-ui/metadata.json create mode 100644 logos-calc-cpp-ui/module.yaml create mode 100644 logos-calc-cpp-ui/src/calc_backend.cpp create mode 100644 logos-calc-cpp-ui/src/calc_backend.h create mode 100644 logos-calc-cpp-ui/src/calc_ui_cpp_plugin.cpp create mode 100644 logos-calc-cpp-ui/src/calc_ui_cpp_plugin.h create mode 100644 logos-calc-cpp-ui/src/qml/Main.qml create mode 100644 tutorial-cpp-ui-app.md diff --git a/logos-calc-cpp-ui/CMakeLists.txt b/logos-calc-cpp-ui/CMakeLists.txt new file mode 100644 index 0000000..8e51270 --- /dev/null +++ b/logos-calc-cpp-ui/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.14) +project(CalcUiCppPlugin LANGUAGES CXX) + +if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT}) + include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake) +else() + message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.") +endif() + +logos_module( + NAME calc_ui_cpp + SOURCES + src/calc_ui_cpp_plugin.h + src/calc_ui_cpp_plugin.cpp + src/calc_backend.h + src/calc_backend.cpp + INCLUDE_DIRS + ${CMAKE_CURRENT_SOURCE_DIR}/interfaces +) + +find_package(Qt6 REQUIRED COMPONENTS Widgets Quick QuickWidgets) +target_link_libraries(calc_ui_cpp_module_plugin PRIVATE + Qt6::Widgets + Qt6::Quick + Qt6::QuickWidgets +) + +qt_add_resources(calc_ui_cpp_module_plugin "qml_resources" + PREFIX "/" + FILES + src/qml/Main.qml +) diff --git a/logos-calc-cpp-ui/flake.nix b/logos-calc-cpp-ui/flake.nix new file mode 100644 index 0000000..49204d3 --- /dev/null +++ b/logos-calc-cpp-ui/flake.nix @@ -0,0 +1,43 @@ +{ + description = "Calculator C++ UI plugin for Logos - widget frontend for calc_module"; + + inputs = { + logos-module-builder.url = "github:logos-co/logos-module-builder"; + nixpkgs.follows = "logos-module-builder/nixpkgs"; + + logos-standalone-app.url = "github:logos-co/logos-standalone-app"; + logos-standalone-app.inputs.logos-liblogos.inputs.nixpkgs.follows = + "logos-module-builder/nixpkgs"; + + calc_module.url = "github:logos-co/logos-tutorial?dir=logos-calc-module"; + }; + + outputs = { self, logos-module-builder, logos-standalone-app, nixpkgs, calc_module }: + let + systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); + moduleOutputs = logos-module-builder.lib.mkLogosModule { + src = ./.; + configFile = ./module.yaml; + moduleInputs = { inherit calc_module; }; + }; + in + moduleOutputs // { + apps = forAllSystems (system: + let + pkgs = import nixpkgs { inherit system; }; + standalone = logos-standalone-app.packages.${system}.default; + plugin = moduleOutputs.packages.${system}.default; + pluginDir = pkgs.runCommand "calc-ui-cpp-plugin-dir" {} '' + mkdir -p $out/icons + cp ${plugin}/lib/*_plugin.* $out/ + cp ${./metadata.json} $out/metadata.json + cp ${./icons/calc.png} $out/icons/calc.png + ''; + run = pkgs.writeShellScript "run-calc-ui-cpp-standalone" '' + exec ${standalone}/bin/logos-standalone-app "${pluginDir}" "$@" + ''; + in { default = { type = "app"; program = "${run}"; }; } + ); + }; +} diff --git a/logos-calc-cpp-ui/icons/calc.png b/logos-calc-cpp-ui/icons/calc.png new file mode 100644 index 0000000000000000000000000000000000000000..045c1ccf97043ef787a9903f6f18447763a6aaad GIT binary patch literal 91 zcmeAS@N?(olHy`uVBq!ia0vp^G9b*s1SJ3FdmIK*N}eu`Ar*6yfA}*WFvxnbV&USp gJ}E0W!-$8K;ZuToL_%di7f>~Wr>mdKI;Vst02L1!kpKVy literal 0 HcmV?d00001 diff --git a/logos-calc-cpp-ui/metadata.json b/logos-calc-cpp-ui/metadata.json new file mode 100644 index 0000000..ddedbde --- /dev/null +++ b/logos-calc-cpp-ui/metadata.json @@ -0,0 +1,10 @@ +{ + "name": "calc_ui_cpp", + "version": "1.0.0", + "description": "Calculator C++ UI — widget frontend for calc_module", + "type": "ui", + "main": "calc_ui_cpp_plugin", + "dependencies": ["calc_module"], + "category": "tools", + "icon": "icons/calc.png" +} diff --git a/logos-calc-cpp-ui/module.yaml b/logos-calc-cpp-ui/module.yaml new file mode 100644 index 0000000..b3e3b98 --- /dev/null +++ b/logos-calc-cpp-ui/module.yaml @@ -0,0 +1,19 @@ +name: calc_ui_cpp +version: 1.0.0 +type: ui +category: tools +description: "Calculator C++ UI — widget frontend for calc_module" + +dependencies: + - calc_module + +nix_packages: + build: [] + runtime: [] + +external_libraries: [] + +cmake: + find_packages: [] + extra_sources: [] + proto_files: [] diff --git a/logos-calc-cpp-ui/src/calc_backend.cpp b/logos-calc-cpp-ui/src/calc_backend.cpp new file mode 100644 index 0000000..e5522c2 --- /dev/null +++ b/logos-calc-cpp-ui/src/calc_backend.cpp @@ -0,0 +1,10 @@ +#include "calc_backend.h" + +CalcBackend::CalcBackend(LogosAPI* api, QObject* parent) + : QObject(parent), m_logos(new LogosModules(api)) {} + +int CalcBackend::add(int a, int b) { return m_logos->calc_module.add(a, b); } +int CalcBackend::multiply(int a, int b) { return m_logos->calc_module.multiply(a, b); } +int CalcBackend::factorial(int n) { return m_logos->calc_module.factorial(n); } +int CalcBackend::fibonacci(int n) { return m_logos->calc_module.fibonacci(n); } +QString CalcBackend::libVersion() { return m_logos->calc_module.libVersion(); } diff --git a/logos-calc-cpp-ui/src/calc_backend.h b/logos-calc-cpp-ui/src/calc_backend.h new file mode 100644 index 0000000..9b9b022 --- /dev/null +++ b/logos-calc-cpp-ui/src/calc_backend.h @@ -0,0 +1,27 @@ +#ifndef CALC_BACKEND_H +#define CALC_BACKEND_H + +#include +#include +#include "logos_sdk.h" // generated at build time from module.yaml dependencies + +class LogosAPI; + +class CalcBackend : public QObject +{ + Q_OBJECT + +public: + explicit CalcBackend(LogosAPI* api, QObject* parent = nullptr); + + Q_INVOKABLE int add(int a, int b); + Q_INVOKABLE int multiply(int a, int b); + Q_INVOKABLE int factorial(int n); + Q_INVOKABLE int fibonacci(int n); + Q_INVOKABLE QString libVersion(); + +private: + LogosModules* m_logos; // generated umbrella wrapper +}; + +#endif // CALC_BACKEND_H diff --git a/logos-calc-cpp-ui/src/calc_ui_cpp_plugin.cpp b/logos-calc-cpp-ui/src/calc_ui_cpp_plugin.cpp new file mode 100644 index 0000000..9fc293c --- /dev/null +++ b/logos-calc-cpp-ui/src/calc_ui_cpp_plugin.cpp @@ -0,0 +1,42 @@ +#include "calc_ui_cpp_plugin.h" +#include "calc_backend.h" +#include "logos_api.h" +#include +#include +#include +#include +#include + +CalcUiCppPlugin::CalcUiCppPlugin(QObject* parent) : QObject(parent) {} +CalcUiCppPlugin::~CalcUiCppPlugin() {} + +QWidget* CalcUiCppPlugin::createWidget(LogosAPI* logosAPI) +{ + auto* backend = new CalcBackend(logosAPI); + + auto* quickWidget = new QQuickWidget(); + quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); + quickWidget->rootContext()->setContextProperty("backend", backend); + + // Dev mode: set QML_PATH to the directory containing Main.qml to load + // from the filesystem without rebuilding. Example: export QML_PATH=$PWD/src/qml + QString devSource = qgetenv("QML_PATH"); + QUrl qmlUrl = devSource.isEmpty() + ? QUrl("qrc:/src/qml/Main.qml") + : QUrl::fromLocalFile(QDir(devSource).filePath("Main.qml")); + + quickWidget->setSource(qmlUrl); + + if (quickWidget->status() == QQuickWidget::Error) { + qWarning() << "CalcUiCppPlugin: failed to load QML"; + for (const auto& e : quickWidget->errors()) + qWarning() << e.toString(); + } + + return quickWidget; +} + +void CalcUiCppPlugin::destroyWidget(QWidget* widget) +{ + delete widget; +} diff --git a/logos-calc-cpp-ui/src/calc_ui_cpp_plugin.h b/logos-calc-cpp-ui/src/calc_ui_cpp_plugin.h new file mode 100644 index 0000000..b75c5ef --- /dev/null +++ b/logos-calc-cpp-ui/src/calc_ui_cpp_plugin.h @@ -0,0 +1,24 @@ +#ifndef CALC_UI_CPP_PLUGIN_H +#define CALC_UI_CPP_PLUGIN_H + +#include +#include +#include + +class LogosAPI; + +class CalcUiCppPlugin : public QObject, public IComponent +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID IComponent_iid FILE "metadata.json") + Q_INTERFACES(IComponent) + +public: + explicit CalcUiCppPlugin(QObject* parent = nullptr); + ~CalcUiCppPlugin() override; + + Q_INVOKABLE QWidget* createWidget(LogosAPI* logosAPI = nullptr) override; + void destroyWidget(QWidget* widget) override; +}; + +#endif // CALC_UI_CPP_PLUGIN_H diff --git a/logos-calc-cpp-ui/src/qml/Main.qml b/logos-calc-cpp-ui/src/qml/Main.qml new file mode 100644 index 0000000..e1b7849 --- /dev/null +++ b/logos-calc-cpp-ui/src/qml/Main.qml @@ -0,0 +1,100 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: root + + property string result: "" + property string errorText: "" + + ColumnLayout { + anchors.fill: parent + anchors.margins: 24 + spacing: 16 + + // ── Title ────────────────────────────────────────────── + Text { + text: "Logos Calculator (C++ backend)" + font.pixelSize: 20 + color: "#ffffff" + Layout.alignment: Qt.AlignHCenter + } + + // ── Two-operand operations ───────────────────────────── + RowLayout { + spacing: 12 + Layout.fillWidth: true + + TextField { + id: inputA + placeholderText: "a" + Layout.preferredWidth: 80 + validator: IntValidator {} + } + + TextField { + id: inputB + placeholderText: "b" + Layout.preferredWidth: 80 + validator: IntValidator {} + } + + Button { + text: "Add" + onClicked: root.result = String(backend.add(inputA.text, inputB.text)) + } + + Button { + text: "Multiply" + onClicked: root.result = String(backend.multiply(inputA.text, inputB.text)) + } + } + + // ── Single-operand operations ────────────────────────── + RowLayout { + spacing: 12 + Layout.fillWidth: true + + TextField { + id: inputN + placeholderText: "n" + Layout.preferredWidth: 80 + validator: IntValidator { bottom: 0 } + } + + Button { + text: "Factorial" + onClicked: root.result = String(backend.factorial(inputN.text)) + } + + Button { + text: "Fibonacci" + onClicked: root.result = String(backend.fibonacci(inputN.text)) + } + + Button { + text: "libcalc version" + onClicked: root.result = backend.libVersion() + } + } + + // ── Result display ───────────────────────────────────── + Rectangle { + Layout.fillWidth: true + height: 56 + color: root.errorText.length > 0 ? "#3d1a1a" : "#1a2d1a" + radius: 8 + + Text { + anchors.centerIn: parent + text: root.errorText.length > 0 ? root.errorText + : (root.result.length > 0 ? root.result : "Enter values and press a button") + color: root.errorText.length > 0 ? "#f85149" : "#56d364" + font.pixelSize: 15 + } + } + + Item { Layout.fillHeight: true } + } +} diff --git a/logos-developer-guide.md b/logos-developer-guide.md index 1c3cd5a..ff3b774 100644 --- a/logos-developer-guide.md +++ b/logos-developer-guide.md @@ -907,6 +907,40 @@ To create a module with a native Qt widget UI: 2. Set `"type": "ui"` in your metadata 3. Return a `QWidget*` from `createWidget()` +`IComponent.h` is not part of the SDK — each UI module vendors its own copy in `interfaces/IComponent.h`: + +```cpp +// interfaces/IComponent.h (copy verbatim into your module) +#pragma once +#include +#include +#include + +class LogosAPI; + +class IComponent { +public: + virtual ~IComponent() = default; + virtual QWidget* createWidget(LogosAPI* logosAPI = nullptr) = 0; + virtual void destroyWidget(QWidget* widget) = 0; +}; + +#define IComponent_iid "com.logos.component.IComponent" +Q_DECLARE_INTERFACE(IComponent, IComponent_iid) +``` + +Expose it via `INCLUDE_DIRS` in your `CMakeLists.txt`: + +```cmake +logos_module( + NAME my_ui_module + SOURCES src/my_ui_plugin.h src/my_ui_plugin.cpp + INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/interfaces +) +``` + +Then implement the plugin: + ```cpp #include diff --git a/tutorial-cpp-ui-app.md b/tutorial-cpp-ui-app.md new file mode 100644 index 0000000..e93230e --- /dev/null +++ b/tutorial-cpp-ui-app.md @@ -0,0 +1,769 @@ +# Tutorial Part 3: Building a C++ UI Module + +This is Part 3 of the Logos module tutorial series. In [Part 2](tutorial-qml-ui-app.md) you built a QML UI plugin. Now you'll build a **native C++ Qt widget plugin** that calls `calc_module` through a typed backend class. + +**What you'll build:** A `calc_ui_cpp` C++ plugin with two options for the UI: + +- **Option A — QML loaded from C++:** A `QQuickWidget` inside the plugin loading the same `Main.qml` as the QML plugin, with `CalcBackend` exposed as a context property — plus dev mode for editing QML without rebuilding +- **Option B — Pure Qt widget:** `QPushButton`, `QLineEdit`, `QLabel` wired directly to a backend class + +**Why C++ over QML-only?** + +| | QML plugin (Part 2) | C++ UI plugin (Part 3) | +|---|---|---| +| Compilation | No | Yes (CMake) | +| Backend calls | Via `logos.callModule()` IPC bridge | Via `LogosAPI*` directly in C++ | +| Type safety | Weak — all args travel as `QVariant` | Strong — C++ types preserved | +| Sandboxing | Yes | No | +| QML support | Native | Optional via `QQuickWidget` | + +The C++ backend class also fixes the type coercion issue from Part 2 — `int` arguments stay `int` all the way to the module. + +**Prerequisites:** + +- Completed [Part 1](tutorial-wrapping-c-library.md) — you have a working `calc_module` +- Nix with flakes enabled + +--- + +## How It Works + +``` ++----------------------+ CalcBackend::add(3, 5) +-------------------+ +| calc_ui_cpp | --------------------------------> | calc_module | +| C++ Qt plugin | LogosAPI* / invokeRemoteMethod | C++ plugin | +| createWidget() | | add(int, int) | ++----------------------+ +-------------------+ + ^ + | loaded by + v + logos-standalone-app / logos-basecamp +``` + +The plugin implements `createWidget()` which returns a `QWidget*`. The widget is shown in the host app's window. A `CalcBackend` class holds `LogosAPI*` and makes typed calls to `calc_module`. + +--- + +## Step 1: Scaffold + +```bash +mkdir logos-calc-ui-cpp && cd logos-calc-ui-cpp +nix flake init -t github:logos-co/logos-module-builder#ui-module +git init && git add -A +``` + +This gives you: + +``` +logos-calc-ui-cpp/ +├── flake.nix +├── module.yaml +├── metadata.json +├── CMakeLists.txt +└── src/ + ├── ui_example_interface.h + ├── ui_example_plugin.h + └── ui_example_plugin.cpp +``` + +Rename the source files to match your module: + +```bash +mv src/ui_example_interface.h src/calc_ui_cpp_interface.h +mv src/ui_example_plugin.h src/calc_ui_cpp_plugin.h +mv src/ui_example_plugin.cpp src/calc_ui_cpp_plugin.cpp +``` + +--- + +## Step 2: `module.yaml` + +```yaml +name: calc_ui_cpp +version: 1.0.0 +type: ui +category: tools +description: "Calculator C++ UI — widget frontend for calc_module" + +dependencies: + - calc_module + +nix_packages: + build: [] + runtime: [] + +external_libraries: [] + +cmake: + find_packages: [] + extra_sources: [] + proto_files: [] +``` + +--- + +## Step 3: `metadata.json` + +```json +{ + "name": "calc_ui_cpp", + "version": "1.0.0", + "description": "Calculator C++ UI — widget frontend for calc_module", + "type": "ui", + "main": "calc_ui_cpp_plugin", + "dependencies": ["calc_module"], + "category": "tools" +} +``` + +--- + +## Step 4: `CMakeLists.txt` + +```cmake +cmake_minimum_required(VERSION 3.14) +project(CalcUiCppPlugin LANGUAGES CXX) + +if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT}) + include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake) +else() + message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.") +endif() + +logos_module( + NAME calc_ui_cpp + SOURCES + src/calc_ui_cpp_interface.h + src/calc_ui_cpp_plugin.h + src/calc_ui_cpp_plugin.cpp + src/calc_backend.h + src/calc_backend.cpp +) + +find_package(Qt6 REQUIRED COMPONENTS Widgets) +target_link_libraries(calc_ui_cpp_module_plugin PRIVATE Qt6::Widgets) +``` + +> For Option A (QML inside the plugin) you will add `Quick QuickWidgets` and `qt_add_resources` — covered in [Step 7](#step-7-option-a--qml-loaded-from-c). + +--- + +## Step 5: Interface Header (`src/calc_ui_cpp_interface.h`) + +```cpp +#ifndef CALC_UI_CPP_INTERFACE_H +#define CALC_UI_CPP_INTERFACE_H + +#include "interface.h" + +class CalcUiCppInterface : public PluginInterface +{ +public: + virtual ~CalcUiCppInterface() = default; +}; + +#define CalcUiCppInterface_iid "org.logos.CalcUiCppInterface" +Q_DECLARE_INTERFACE(CalcUiCppInterface, CalcUiCppInterface_iid) + +#endif // CALC_UI_CPP_INTERFACE_H +``` + +--- + +## Step 6: Backend Class + +The backend class is the key addition over the QML plugin. It holds a `LogosModules*` wrapper — a typed C++ SDK generated at build time from `module.yaml` — and exposes `Q_INVOKABLE` methods that call `calc_module` through it. Because the calls go through a generated typed class, argument types are preserved — no `QString`/`int` coercion issues. + +### How the generated SDK works + +When `module.yaml` declares `dependencies: [calc_module]` and `calc_module` is passed as a flake input via `moduleInputs`, the build system runs `logos-cpp-generator` before compilation. This produces: + +- `logos_sdk.h` / `logos_sdk.cpp` — the `LogosModules` umbrella class with one typed member per dependency +- `calc_module_api.h` / `calc_module_api.cpp` — the per-module wrapper included by `logos_sdk.h` + +`LogosModules` is constructed with a `LogosAPI*` and provides a member named after each declared dependency (snake_case). All IPC routing happens inside the generated code — your backend just calls methods directly: + +```cpp +m_logos->calc_module.add(3, 5) // typed: int add(int, int) over IPC +``` + +This is the same pattern used in production modules such as `logos-storage-ui`. + +### `src/calc_backend.h` + +```cpp +#ifndef CALC_BACKEND_H +#define CALC_BACKEND_H + +#include +#include +#include "logos_sdk.h" // generated at build time from module.yaml dependencies + +class LogosAPI; + +class CalcBackend : public QObject +{ + Q_OBJECT + +public: + explicit CalcBackend(LogosAPI* api, QObject* parent = nullptr); + + Q_INVOKABLE int add(int a, int b); + Q_INVOKABLE int multiply(int a, int b); + Q_INVOKABLE int factorial(int n); + Q_INVOKABLE int fibonacci(int n); + Q_INVOKABLE QString libVersion(); + +private: + LogosModules* m_logos; // generated umbrella wrapper +}; + +#endif // CALC_BACKEND_H +``` + +### `src/calc_backend.cpp` + +```cpp +#include "calc_backend.h" + +CalcBackend::CalcBackend(LogosAPI* api, QObject* parent) + : QObject(parent), m_logos(new LogosModules(api)) {} + +int CalcBackend::add(int a, int b) { return m_logos->calc_module.add(a, b); } +int CalcBackend::multiply(int a, int b) { return m_logos->calc_module.multiply(a, b); } +int CalcBackend::factorial(int n) { return m_logos->calc_module.factorial(n); } +int CalcBackend::fibonacci(int n) { return m_logos->calc_module.fibonacci(n); } +QString CalcBackend::libVersion() { return m_logos->calc_module.libVersion(); } +``` + +`LogosModules` is constructed once with `LogosAPI*`. Each member (`calc_module`) is a generated proxy that routes calls to the corresponding module process over Qt Remote Objects IPC. No raw `invokeRemoteMethod`, no string method names, no manual `QVariant` unwrapping. + +--- + +## Step 7: Option A — QML Loaded from C++ + +The plugin loads `src/qml/Main.qml` into a `QQuickWidget` and exposes `CalcBackend` as a QML context property. The QML is identical in structure to `logos-calc-ui/Main.qml` (Part 2), but calls `backend.*` methods directly instead of routing through the `logos.callModule()` IPC bridge — so argument types are preserved and there is no sandboxing overhead. + +### 7.1 Add the QML file + +Create `src/qml/Main.qml`. The structure mirrors `logos-calc-ui/Main.qml` exactly; the only difference is that buttons call `backend.*` methods directly instead of routing through `logos.callModule(...)`: + +```qml +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: root + + property string result: "" + property string errorText: "" + + ColumnLayout { + anchors.fill: parent + anchors.margins: 24 + spacing: 16 + + // ── Title ────────────────────────────────────────────── + Text { + text: "Logos Calculator (C++ backend)" + font.pixelSize: 20 + color: "#ffffff" + Layout.alignment: Qt.AlignHCenter + } + + // ── Two-operand operations ───────────────────────────── + RowLayout { + spacing: 12 + Layout.fillWidth: true + + TextField { id: inputA; placeholderText: "a"; Layout.preferredWidth: 80; validator: IntValidator {} } + TextField { id: inputB; placeholderText: "b"; Layout.preferredWidth: 80; validator: IntValidator {} } + + Button { + text: "Add" + onClicked: root.result = String(backend.add(inputA.text, inputB.text)) + } + Button { + text: "Multiply" + onClicked: root.result = String(backend.multiply(inputA.text, inputB.text)) + } + } + + // ── Single-operand operations ────────────────────────── + RowLayout { + spacing: 12 + Layout.fillWidth: true + + TextField { id: inputN; placeholderText: "n"; Layout.preferredWidth: 80; validator: IntValidator { bottom: 0 } } + + Button { text: "Factorial"; onClicked: root.result = String(backend.factorial(inputN.text)) } + Button { text: "Fibonacci"; onClicked: root.result = String(backend.fibonacci(inputN.text)) } + Button { text: "libcalc version"; onClicked: root.result = backend.libVersion() } + } + + // ── Result display ───────────────────────────────────── + Rectangle { + Layout.fillWidth: true + height: 56 + color: root.errorText.length > 0 ? "#3d1a1a" : "#1a2d1a" + radius: 8 + Text { + anchors.centerIn: parent + text: root.errorText.length > 0 ? root.errorText + : (root.result.length > 0 ? root.result : "Enter values and press a button") + color: root.errorText.length > 0 ? "#f85149" : "#56d364" + font.pixelSize: 15 + } + } + + Item { Layout.fillHeight: true } + } +} +``` + +### 7.2 Update `CMakeLists.txt` + +Add `Quick` and `QuickWidgets`, and embed the QML as a Qt resource: + +```cmake +cmake_minimum_required(VERSION 3.14) +project(CalcUiCppPlugin LANGUAGES CXX) + +if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT}) + include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake) +else() + message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.") +endif() + +logos_module( + NAME calc_ui_cpp + SOURCES + src/calc_ui_cpp_interface.h + src/calc_ui_cpp_plugin.h + src/calc_ui_cpp_plugin.cpp + src/calc_backend.h + src/calc_backend.cpp +) + +find_package(Qt6 REQUIRED COMPONENTS Widgets Quick QuickWidgets) +target_link_libraries(calc_ui_cpp_module_plugin PRIVATE + Qt6::Widgets + Qt6::Quick + Qt6::QuickWidgets +) + +qt_add_resources(calc_ui_cpp_module_plugin "qml_resources" + PREFIX "/" + FILES + src/qml/Main.qml +) +``` + +### 7.3 `createWidget()` — load QML + +Replace `calc_ui_cpp_plugin.cpp` with: + +```cpp +#include "calc_ui_cpp_plugin.h" +#include "calc_backend.h" +#include "logos_api.h" +#include +#include +#include +#include +#include + +CalcUiCppPlugin::CalcUiCppPlugin(QObject* parent) : QObject(parent) {} +CalcUiCppPlugin::~CalcUiCppPlugin() {} + +void CalcUiCppPlugin::initLogos(LogosAPI* api) +{ + m_logosAPI = api; +} + +QWidget* CalcUiCppPlugin::createWidget(LogosAPI* logosAPI) +{ + auto* backend = new CalcBackend(logosAPI); + + auto* quickWidget = new QQuickWidget(); + quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); + quickWidget->rootContext()->setContextProperty("backend", backend); + + // Dev mode: set QML_PATH to the directory containing Main.qml to load + // from the filesystem without rebuilding. Example: export QML_PATH=$PWD/src/qml + QString devSource = qgetenv("QML_PATH"); + QUrl qmlUrl = devSource.isEmpty() + ? QUrl("qrc:/src/qml/Main.qml") + : QUrl::fromLocalFile(QDir(devSource).filePath("Main.qml")); + + quickWidget->setSource(qmlUrl); + + if (quickWidget->status() == QQuickWidget::Error) { + qWarning() << "CalcUiCppPlugin: failed to load QML"; + for (const auto& e : quickWidget->errors()) + qWarning() << e.toString(); + } + + return quickWidget; +} + +void CalcUiCppPlugin::destroyWidget(QWidget* widget) +{ + delete widget; +} +``` + +### 7.4 Dev Mode + +When `QML_PATH` is set, the plugin loads `Main.qml` from disk instead of the embedded resource. You can edit QML layout, styling, and property bindings without a Nix rebuild — just restart the app to pick up changes. + +```bash +# Run with dev mode enabled +QML_PATH=$PWD/src/qml \ + nix run . --override-input calc_module path:../logos-calc-module -- \ + --modules-dir ./modules +``` + +> **What still requires a rebuild:** +> - Changes to `.cpp` / `.h` files (backend logic, plugin interface) +> - Changes to `CMakeLists.txt` or `module.yaml` +> +> **What does not require a rebuild:** +> - Any `.qml` change — layout, styling, property bindings, JS logic + +--- + +## Step 8: Option B — Pure Qt Widget + +The plugin creates a standard Qt widget using layouts and connects button clicks to the backend. No QML, no additional Qt modules — just `Qt6::Widgets`. + +### `src/calc_ui_cpp_plugin.h` + +```cpp +#ifndef CALC_UI_CPP_PLUGIN_H +#define CALC_UI_CPP_PLUGIN_H + +#include +#include +#include +#include "calc_ui_cpp_interface.h" + +class LogosAPI; + +class CalcUiCppPlugin : public QObject, public CalcUiCppInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID CalcUiCppInterface_iid FILE "metadata.json") + Q_INTERFACES(CalcUiCppInterface PluginInterface) + +public: + explicit CalcUiCppPlugin(QObject* parent = nullptr); + ~CalcUiCppPlugin() override; + + QString name() const override { return "calc_ui_cpp"; } + QString version() const override { return "1.0.0"; } + + Q_INVOKABLE void initLogos(LogosAPI* api); + + Q_INVOKABLE QWidget* createWidget(LogosAPI* logosAPI = nullptr); + Q_INVOKABLE void destroyWidget(QWidget* widget); + +signals: + void eventResponse(const QString& eventName, const QVariantList& args); + +private: + LogosAPI* m_logosAPI = nullptr; +}; + +#endif // CALC_UI_CPP_PLUGIN_H +``` + +### `src/calc_ui_cpp_plugin.cpp` + +```cpp +#include "calc_ui_cpp_plugin.h" +#include "calc_backend.h" +#include "logos_api.h" +#include +#include +#include +#include +#include +#include + +CalcUiCppPlugin::CalcUiCppPlugin(QObject* parent) : QObject(parent) {} +CalcUiCppPlugin::~CalcUiCppPlugin() {} + +void CalcUiCppPlugin::initLogos(LogosAPI* api) +{ + m_logosAPI = api; +} + +QWidget* CalcUiCppPlugin::createWidget(LogosAPI* logosAPI) +{ + auto* backend = new CalcBackend(logosAPI); + + auto* widget = new QWidget(); + auto* layout = new QVBoxLayout(widget); + layout->setContentsMargins(24, 24, 24, 24); + layout->setSpacing(16); + + // ── Title ────────────────────────────────────────────────── + auto* title = new QLabel("Logos Calculator (C++)"); + title->setAlignment(Qt::AlignHCenter); + layout->addWidget(title); + + // ── Two-operand row ──────────────────────────────────────── + auto* twoOpRow = new QHBoxLayout(); + auto* inputA = new QLineEdit(); inputA->setPlaceholderText("a"); inputA->setMaximumWidth(80); + auto* inputB = new QLineEdit(); inputB->setPlaceholderText("b"); inputB->setMaximumWidth(80); + auto* addBtn = new QPushButton("Add"); + auto* mulBtn = new QPushButton("Multiply"); + twoOpRow->addWidget(inputA); + twoOpRow->addWidget(inputB); + twoOpRow->addWidget(addBtn); + twoOpRow->addWidget(mulBtn); + twoOpRow->addStretch(); + layout->addLayout(twoOpRow); + + // ── Single-operand row ───────────────────────────────────── + auto* oneOpRow = new QHBoxLayout(); + auto* inputN = new QLineEdit(); inputN->setPlaceholderText("n"); inputN->setMaximumWidth(80); + auto* facBtn = new QPushButton("Factorial"); + auto* fibBtn = new QPushButton("Fibonacci"); + auto* verBtn = new QPushButton("libcalc version"); + oneOpRow->addWidget(inputN); + oneOpRow->addWidget(facBtn); + oneOpRow->addWidget(fibBtn); + oneOpRow->addWidget(verBtn); + oneOpRow->addStretch(); + layout->addLayout(oneOpRow); + + // ── Result display ───────────────────────────────────────── + auto* resultLabel = new QLabel("Enter values and press a button"); + resultLabel->setAlignment(Qt::AlignHCenter); + layout->addWidget(resultLabel); + + layout->addStretch(); + + // ── Wire up buttons ──────────────────────────────────────── + auto show = [resultLabel](const QString& v) { resultLabel->setText(v); }; + + QObject::connect(addBtn, &QPushButton::clicked, [=] { + show(QString::number(backend->add(inputA->text().toInt(), inputB->text().toInt()))); + }); + QObject::connect(mulBtn, &QPushButton::clicked, [=] { + show(QString::number(backend->multiply(inputA->text().toInt(), inputB->text().toInt()))); + }); + QObject::connect(facBtn, &QPushButton::clicked, [=] { + show(QString::number(backend->factorial(inputN->text().toInt()))); + }); + QObject::connect(fibBtn, &QPushButton::clicked, [=] { + show(QString::number(backend->fibonacci(inputN->text().toInt()))); + }); + QObject::connect(verBtn, &QPushButton::clicked, [=] { + show(backend->libVersion()); + }); + + return widget; +} + +void CalcUiCppPlugin::destroyWidget(QWidget* widget) +{ + delete widget; +} +``` + +--- + +## Step 9: `flake.nix` + +Same pattern as Part 2 — `mkLogosModule` for the build, `apps` merged on top for `nix run`. + +**Important — `moduleInputs`:** Because `module.yaml` declares `dependencies: [calc_module]`, the build system runs `logos-cpp-generator` before compiling your C++ sources. The generator introspects `calc_module`'s built plugin to produce `logos_sdk.h` / `logos_sdk.cpp` (and per-module `calc_module_api.h` / `calc_module_api.cpp`). These are the files your backend includes as `#include "logos_sdk.h"`. For this to work, `calc_module` must be available as a built Nix package at code-generation time — that is what `moduleInputs` provides. Without it, the build fails with `'logos_sdk.h' file not found`. + +```nix +{ + description = "Calculator C++ UI plugin for Logos - widget frontend for calc_module"; + + inputs = { + logos-module-builder.url = "github:logos-co/logos-module-builder"; + nixpkgs.follows = "logos-module-builder/nixpkgs"; + + logos-standalone-app.url = "github:logos-co/logos-standalone-app"; + logos-standalone-app.inputs.logos-liblogos.inputs.nixpkgs.follows = + "logos-module-builder/nixpkgs"; + + calc_module.url = "github:logos-co/logos-tutorial?dir=logos-calc-module"; + }; + + outputs = { self, logos-module-builder, logos-standalone-app, nixpkgs, calc_module }: + let + systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); + moduleOutputs = logos-module-builder.lib.mkLogosModule { + src = ./.; + configFile = ./module.yaml; + moduleInputs = { inherit calc_module; }; + }; + in + moduleOutputs // { + apps = forAllSystems (system: + let + pkgs = import nixpkgs { inherit system; }; + standalone = logos-standalone-app.packages.${system}.default; + plugin = moduleOutputs.packages.${system}.default; + pluginDir = pkgs.runCommand "calc-ui-cpp-plugin-dir" {} '' + mkdir -p $out + cp ${plugin}/lib/*_plugin.* $out/ + cp ${./metadata.json} $out/metadata.json + ''; + run = pkgs.writeShellScript "run-calc-ui-cpp-standalone" '' + exec ${standalone}/bin/logos-standalone-app "${pluginDir}" "$@" + ''; + in { default = { type = "app"; program = "${run}"; }; } + ); + }; +} +``` + +--- + +## Step 10: Build and Test + +### 10.1 Build + +```bash +git add -A +nix build --override-input calc_module path:../logos-calc-module +``` + +Inspect the output: + +```bash +lm ./result/lib/calc_ui_cpp_plugin.dylib +``` + +You should see `createWidget` and `destroyWidget` in the methods list. + +### 10.2 UI only (layout preview) + +```bash +nix run . --override-input calc_module path:../logos-calc-module +``` + +The widget opens. No backend connected yet, so button clicks will silently return 0 (CalcBackend logs a warning when `calc_module` is not connected). + +> **Why `--override-input`?** `calc_module.url` in `flake.nix` points to the published GitHub URL. For local development, `--override-input` redirects it to the local sibling directory. This is the same mechanism `ws build --local` / `ws build --auto-local` uses throughout the workspace. + +### 10.3 Full functionality (with modules) + +```bash +nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm +mkdir -p modules + +# Install capability_module +nix bundle --bundler 'github:logos-co/nix-bundle-lgx' \ + 'github:logos-co/logos-capability-module' -o lgx-capability +./pm/bin/lgpm --modules-dir ./modules install --file lgx-capability/*.lgx + +# Bundle and install calc_module (from Part 1) +cd ../logos-calc-module +nix bundle --bundler 'github:logos-co/nix-bundle-lgx' '.#lib' -o lgx-result +cd ../logos-calc-ui-cpp +./pm/bin/lgpm --modules-dir ./modules install --file ../logos-calc-module/lgx-result/*.lgx + +nix run . --override-input calc_module path:../logos-calc-module -- --modules-dir ./modules +``` + +--- + +## Step 11: Load in `logos-basecamp` + +### 11.1 Create LGX packages + +```bash +# Package calc_module (from Part 1) +cd ../logos-calc-module +nix bundle --bundler 'github:logos-co/nix-bundle-lgx#portable' '.#lib' -o lgx-calc-module +cd ../logos-calc-ui-cpp + +# Package the C++ UI plugin +nix bundle --bundler 'github:logos-co/nix-bundle-lgx#portable' '.' -o lgx-calc-ui-cpp +``` + +### 11.2 Install via logos-basecamp UI + +1. Open `logos-basecamp` +2. Go to **Package Manager** +3. Click **Install from file** +4. Select `lgx-calc-module/*.lgx` — installs `calc_module` +5. Repeat for `lgx-calc-ui-cpp/*.lgx` — installs `calc_ui_cpp` + +The "Calculator" tab appears in the sidebar. + +### 11.3 Install via CLI (alternative) + +```bash +nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm +./pm/bin/lgpm install --file lgx-calc-module/*.lgx +./pm/bin/lgpm install --file lgx-calc-ui-cpp/*.lgx +``` + +### 11.4 Build logos-basecamp from source + +Build a local `logos-basecamp` binary, then use `lgpm` to populate a modules directory and run it: + +```bash +# Build logos-basecamp +nix build 'github:logos-co/logos-basecamp' -o basecamp-result + +# Create module directories +mkdir -p modules ui-plugins + +# Build lgpm CLI +nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm + +# Install capability_module (required by all UI plugins) +nix bundle --bundler 'github:logos-co/nix-bundle-lgx' \ + 'github:logos-co/logos-capability-module' -o lgx-capability +./pm/bin/lgpm --modules-dir ./modules install --file lgx-capability/*.lgx + +# Bundle and install calc_module (local, not portable) +cd ../logos-calc-module +nix bundle --bundler 'github:logos-co/nix-bundle-lgx' '.#lib' -o lgx-calc-module-local +cd ../logos-calc-ui-cpp +./pm/bin/lgpm --modules-dir ./modules install --file ../logos-calc-module/lgx-calc-module-local/*.lgx + +# Bundle and install the C++ UI plugin +nix bundle --bundler 'github:logos-co/nix-bundle-lgx' '.' -o lgx-calc-ui-cpp-local +./pm/bin/lgpm --modules-dir ./ui-plugins install --file lgx-calc-ui-cpp-local/*.lgx + +# Run basecamp with the populated directories +./basecamp-result/bin/logos-basecamp \ + --modules-dir ./modules \ + --ui-plugins-dir ./ui-plugins +``` + +> **Local vs portable:** A locally-built `logos-basecamp` (via `nix build`) expects **local** `.lgx` packages (built without `#portable`). Portable builds (AppImage, macOS app bundle) expect **portable** `.lgx` packages. + +--- + +## Recap: Three Module Types + +| | Core (Part 1) | QML UI (Part 2) | C++ UI (Part 3) | +|---|---|---|---| +| Language | C++ | QML / JS | C++ (+ optional QML) | +| Compilation | Yes | No | Yes | +| Backend calls | Exposed via `Q_INVOKABLE` | `logos.callModule()` IPC | `LogosAPI*` → `invokeRemoteMethod()` | +| Type safety | Strong | Weak (QVariant/QString) | Strong | +| Sandboxed | No | Yes | No | +| QML support | — | Native | Via `QQuickWidget` | +| Template | `#default` | `#ui-qml-module` | `#ui-module` | + +## What's Next + +- **Generated type-safe wrappers** — instead of raw `invokeRemoteMethod`, use `logos-cpp-generator` to generate a typed `CalcModuleClient` class. See [Developer Guide](logos-developer-guide.md) Section 6.2 +- **Events** — core modules emit `eventResponse` signals; connect to them from your backend class via `LogosAPIClient` +- **Use the Logos Design System** in Option B QML — `import Logos.Theme` and `import Logos.Controls` are available when running inside `logos-basecamp`