Files
logos-tutorial/tutorial-qml-ui-app.md

579 lines
20 KiB
Markdown
Raw Permalink Normal View History

2026-03-18 14:23:44 +01:00
# Tutorial Part 2: Building a QML UI for Your Logos Module
2026-03-18 14:23:44 +01:00
This is Part 2 of the Logos module tutorial series. In [Part 1](tutorial-wrapping-c-library.md) you wrapped a C library as a Logos core module. Now you'll build a **QML user interface** that calls that module — first isolated with `nix run`, then packaged and loaded into `logos-basecamp`.
2026-03-18 14:23:44 +01:00
**What you'll build:** A `calc_ui` QML plugin with input fields and buttons that call `calc_module` methods (add, multiply, factorial, fibonacci) through the Logos bridge.
**What you'll learn:**
2026-03-06 16:45:49 +00:00
- How QML UI plugins work in the Logos platform
- The `logos.callModule()` bridge that connects QML to core modules
- The project structure and metadata for a QML plugin
2026-03-18 14:23:44 +01:00
- How to package and install your UI into `logos-basecamp`
**Prerequisites:**
2026-03-06 16:45:49 +00:00
- Completed [Part 1](tutorial-wrapping-c-library.md) — you have a working `calc_module`, and its shared library exists in `logos-calc-module/lib/` (`.so` on Linux, `.dylib` on macOS)
- Nix with flakes enabled (same as Part 1)
- Basic familiarity with QML (Qt's declarative UI language)
---
## How QML UI Plugins Work
Before writing code, let's understand the architecture:
```
2026-03-18 14:23:44 +01:00
+-------------------+ logos.callModule() +-------------------+
| calc_ui | --------------------------> | calc_module |
| Main.qml (QML) | IPC (Qt Remote Objects) | C++ plugin |
+-------------------+ +-------------------+
^ ^
└──────────────── loaded by ───────────────────────┘
logos-basecamp / logos-standalone-app
```
Key points:
2026-03-18 14:23:44 +01:00
- **No compilation.** A QML plugin is just `.qml` files and a `metadata.json`.
- **Sandboxed.** No network access, no filesystem access outside the module directory.
- **The `logos` bridge** is injected by the host. Call core modules with `logos.callModule("module", "method", [args])`.
- **Entry point** is always `Main.qml`.
---
2026-03-18 14:23:44 +01:00
## Step 1: Scaffold
2026-03-18 14:23:44 +01:00
Use the QML module template from `logos-module-builder`:
```bash
mkdir logos-calc-ui && cd logos-calc-ui
nix flake init -t github:logos-co/logos-module-builder/tutorial-v1#ui-qml-module
2026-03-18 14:23:44 +01:00
git init && git add -A
```
2026-04-01 17:57:24 +02:00
> **Note:** The generated `flake.nix` uses an unpinned `logos-module-builder` URL. Replace it with the pinned version shown in [Step 4](#step-4-update-flakenix) to ensure reproducible builds.
2026-03-18 14:23:44 +01:00
This gives you:
2026-03-18 14:23:44 +01:00
```
logos-calc-ui/
├── flake.nix # Nix build + nix run support
├── metadata.json # Plugin metadata
└── Main.qml # Your UI (starter template)
```
---
2026-03-18 14:23:44 +01:00
## Step 2: Update `metadata.json`
2026-04-01 12:21:06 +02:00
Replace the template contents with your plugin's details. The template may generate an extra `nix` section — keep it as-is, it's used by the builder:
```json
{
"name": "calc_ui",
"version": "1.0.0",
"description": "Calculator UI - QML frontend for the calc_module",
"type": "ui_qml",
"main": "Main.qml",
"dependencies": ["calc_module"],
"category": "tools",
2026-04-01 12:21:06 +02:00
"icon": "icons/calc.png",
"nix": {
"packages": {
"build": [],
"runtime": []
},
"external_libraries": [],
"cmake": {
"find_packages": [],
"extra_sources": [],
"extra_include_dirs": [],
"extra_link_libraries": []
}
}
}
```
2026-04-01 17:57:24 +02:00
Create the icon directory and add a placeholder icon. The icon is displayed in the `logos-basecamp` sidebar when the module is loaded:
```bash
mkdir -p icons
# Copy any PNG here — or generate a 64×64 placeholder:
echo "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAmElEQVR4nO3QMREAIBDAsFeEN3ziCWRkoEP2XmedfX82OkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAO0BN/SiO/PatoIAAAAASUVORK5CYII=" | base64 -d > icons/calc.png
```
2026-03-18 14:23:44 +01:00
The `dependencies` field tells the host to load `calc_module` before showing your UI.
2026-04-01 11:44:30 +02:00
> **Naming convention:** Each entry in `dependencies` must match the `name` field in that module's own `metadata.json`. When adding a dependency as a flake input, the **input attribute name** must also match the dependency name — e.g., `calc_module.url = "github:logos-co/logos-tutorial/tutorial-v1?dir=logos-calc-module"`. The URL can point to any repo, but the attribute name is how the builder resolves dependencies.
---
## Step 3: Write `Main.qml`
2026-03-18 14:23:44 +01:00
Replace the starter file with the calculator UI:
```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"
font.pixelSize: 20
font.weight: Font.DemiBold
2026-03-18 14:23:44 +01:00
color: "#ffffff"
Layout.alignment: Qt.AlignHCenter
}
2026-03-18 14:23:44 +01:00
// ── Two-operand operations ─────────────────────────────
RowLayout {
spacing: 12
Layout.fillWidth: true
2026-03-18 14:23:44 +01:00
TextField {
id: inputA
placeholderText: "a"
Layout.preferredWidth: 80
validator: IntValidator {}
}
2026-03-18 14:23:44 +01:00
TextField {
id: inputB
placeholderText: "b"
Layout.preferredWidth: 80
validator: IntValidator {}
}
2026-03-18 14:23:44 +01:00
Button {
text: "Add"
onClicked: callTwoOp("add", inputA.text, inputB.text)
}
2026-03-18 14:23:44 +01:00
Button {
text: "Multiply"
onClicked: callTwoOp("multiply", inputA.text, inputB.text)
}
}
2026-03-18 14:23:44 +01:00
// ── Single-operand operations ──────────────────────────
RowLayout {
spacing: 12
Layout.fillWidth: true
2026-03-18 14:23:44 +01:00
TextField {
id: inputN
placeholderText: "n"
Layout.preferredWidth: 80
validator: IntValidator { bottom: 0 }
}
2026-03-18 14:23:44 +01:00
Button {
text: "Factorial"
onClicked: callOneOp("factorial", inputN.text)
}
2026-03-18 14:23:44 +01:00
Button {
text: "Fibonacci"
onClicked: callOneOp("fibonacci", inputN.text)
}
Button {
text: "libcalc version"
onClicked: callModule("libVersion", [])
}
}
// ── Result display ─────────────────────────────────────
Rectangle {
Layout.fillWidth: true
2026-03-18 14:23:44 +01:00
height: 56
color: root.errorText.length > 0 ? "#3d1a1a" : "#1a2d1a"
radius: 8
2026-03-18 14:23:44 +01:00
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 }
}
2026-03-18 14:23:44 +01:00
// ── Logos bridge helpers ───────────────────────────────────
function callModule(method, args) {
root.errorText = ""
root.result = ""
if (typeof logos === "undefined" || !logos.callModule) {
2026-03-18 14:23:44 +01:00
root.errorText = "Logos bridge not available"
return
}
2026-03-18 14:23:44 +01:00
root.result = String(logos.callModule("calc_module", method, args))
}
function callTwoOp(method, a, b) {
2026-03-18 14:23:44 +01:00
if (a === "" || b === "") { root.errorText = "Enter values for a and b"; return }
callModule(method, [parseInt(a), parseInt(b)])
}
function callOneOp(method, n) {
2026-03-18 14:23:44 +01:00
if (n === "") { root.errorText = "Enter a value for n"; return }
callModule(method, [parseInt(n)])
}
}
```
2026-03-18 14:23:44 +01:00
The `logos` object is injected by the host at runtime. The `callModule` helper checks for it and routes calls through the IPC bridge to `calc_module`.
---
2026-03-18 14:23:44 +01:00
## Step 4: Update `flake.nix`
The template already has everything wired up. Update the description and add `calc_module` as a dependency input:
```nix
{
description = "Calculator QML UI Plugin for Logos - frontend for calc_module";
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder/tutorial-v1";
2026-04-01 11:44:30 +02:00
calc_module.url = "github:logos-co/logos-tutorial/tutorial-v1?dir=logos-calc-module"; # must match dependency name in metadata.json
# calc_module.url = "path:../logos-calc-module"; # local checkout (development)
};
2026-03-27 14:22:05 +01:00
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
```
2026-03-27 14:22:05 +01:00
`mkLogosQmlModule` handles everything — it stages QML files, metadata, and icons into a plugin directory, bundles all module dependencies (direct and transitive) from their LGX packages, and automatically wires up `apps.default` so `nix run .` launches the UI in a standalone window with all required backend modules self-contained. `flakeInputs = inputs` passes all inputs so that dependencies declared in `metadata.json` are resolved automatically — note that the input attribute name (`calc_module`) must match the dependency name.
The `calc_module.url` can be either:
- **`github:`** — use the published tutorial-v1 repo.
- **`path:`** — use your local checkout (for development).
> **Important:** Whichever URL scheme you use, `calc_module` must be built with its shared library (`.so` on Linux, `.dylib` on macOS) present in `lib/`.
> **Tip:** If `flake.nix` keeps the `github:` URL, use `--override-input calc_module path:../logos-calc-module` at build/run time to use your local checkout.
---
2026-03-18 14:23:44 +01:00
## Step 5: Test with `nix run`
2026-03-18 14:23:44 +01:00
### 5.1 UI only (layout preview)
```bash
git add -A
2026-04-01 17:57:24 +02:00
nix flake update # regenerate flake.lock to match the pinned inputs in flake.nix
git add flake.lock
2026-03-18 14:23:44 +01:00
nix run .
```
2026-03-18 14:23:44 +01:00
The app opens immediately. No modules are loaded, so clicking buttons shows "Logos bridge not available" — but you can verify the layout and styling look correct.
### 5.2 Full functionality (with modules)
2026-04-01 17:57:24 +02:00
The standalone app automatically bundles and loads all module dependencies declared in `metadata.json`. To test with your local `calc_module` from Part 1:
If the shared library is missing, rebuild it first:
```bash
# Check:
ls ../logos-calc-module/lib/libcalc.so # Linux
ls ../logos-calc-module/lib/libcalc.dylib # macOS
# Rebuild if missing:
cd ../logos-calc-module/lib
gcc -shared -fPIC -o libcalc.so libcalc.c # Linux
# gcc -shared -fPIC -o libcalc.dylib libcalc.c # macOS
cd ../../logos-calc-ui
```
Run with a local override:
```bash
2026-04-01 17:57:24 +02:00
nix run . --override-input calc_module path:../logos-calc-module
```
2026-03-18 14:23:44 +01:00
Clicking **Add**, **Multiply**, **Factorial**, or **Fibonacci** now calls the real module.
If `flake.nix` already uses `calc_module.url = "path:../logos-calc-module"`, run without override:
```bash
nix run .
```
---
2026-03-18 14:23:44 +01:00
## Step 6: Using the Logos Design System
2026-03-18 14:23:44 +01:00
`logos-basecamp` has `logos-design-system` on its QML import path. You can use its themed components directly without any extra setup in your module.
2026-03-18 14:23:44 +01:00
```qml
import Logos.Theme 1.0
import Logos.Controls 1.0
```
Replace the plain `Button` and `TextField` with the styled equivalents:
```qml
// Instead of Button:
LogosButton {
text: "Add"
onClicked: callTwoOp("add", inputA.text, inputB.text)
}
// Instead of TextField:
LogosTextField {
id: inputA
placeholderText: "a"
}
// Use theme colors instead of hardcoded hex values:
Rectangle {
color: Theme.palette.backgroundSecondary
// ...
Text { color: Theme.palette.text }
}
```
Available components: `LogosButton`, `LogosTextField`, `LogosText`, `LogosTabButton`.
Available theme tokens via `Theme.palette`:
- Colors: `background`, `backgroundSecondary`, `backgroundMuted`, `text`, `textMuted`, `border`, `overlayOrange`
- Spacing: `Theme.spacing.radiusSmall`, `Theme.spacing.radiusXlarge`
- Typography: `Theme.typography.secondaryText`, `Theme.typography.weightMedium`
---
## Step 7: Load in `logos-basecamp`
2026-03-25 10:37:15 +01:00
### 7.1 Bundle as LGX packages
2026-03-18 14:23:44 +01:00
2026-04-01 17:57:24 +02:00
Create `.lgx` packages for both dev and portable variants. Use `--out-link` to avoid overwriting the `result` symlink:
```bash
2026-03-18 14:23:44 +01:00
# Package calc_module (from Part 1)
cd ../logos-calc-module
2026-04-01 17:57:24 +02:00
nix build '.#lgx' --out-link result-lgx
nix build '.#lgx-portable' --out-link result-lgx-portable
2026-03-18 14:23:44 +01:00
# Package the QML UI plugin
2026-03-25 10:37:15 +01:00
cd ../logos-calc-ui
2026-04-01 17:57:24 +02:00
nix build '.#lgx' --out-link result-lgx
nix build '.#lgx-portable' --out-link result-lgx-portable
2026-03-18 14:23:44 +01:00
```
2026-03-26 11:48:27 +01:00
> For more bundling options (standalone bundler syntax, cross-platform packaging), see the [Developer Guide — Bundling with nix-bundle-lgx](logos-developer-guide.md#32-bundling-with-nix-bundle-lgx).
2026-03-25 10:37:15 +01:00
### 7.2 Build and run logos-basecamp
2026-03-18 14:23:44 +01:00
2026-03-25 10:37:15 +01:00
Build logos-basecamp, launch it once to preinstall its bundled modules, then install your modules.
2026-03-18 14:23:44 +01:00
2026-03-25 10:37:15 +01:00
> **Note:** `logos-basecamp` does not accept `--modules-dir` or `--ui-plugins-dir` CLI flags. It manages its own data directory and preinstalls bundled modules (main_ui, package_manager, etc.) on first launch.
2026-03-18 14:23:44 +01:00
```bash
# Build logos-basecamp
nix build 'github:logos-co/logos-basecamp/tutorial-v1' -o basecamp-result
2026-03-18 14:23:44 +01:00
2026-03-25 10:37:15 +01:00
# Launch once to preinstall bundled modules, then close it
./basecamp-result/bin/logos-basecamp
```
2026-03-18 14:23:44 +01:00
2026-03-25 10:37:15 +01:00
Basecamp creates its data directory on first launch. To find where it is, check the log output for `plugins directory` or look for the directory that contains `modules/` and `plugins/` subdirectories:
```bash
# macOS (typical path, may vary):
ls ~/Library/Application\ Support/Logos/
# Linux (typical path, may vary):
ls ~/.local/share/Logos/
```
The dev build directory is named `LogosBasecampDev` (portable builds use `LogosBasecamp`).
2026-04-01 17:57:24 +02:00
Install your modules using `lgpm`. First, set `BASECAMP_DIR` to your platform's path:
```bash
# macOS:
BASECAMP_DIR="$HOME/Library/Application Support/Logos/LogosBasecampDev"
# Linux:
BASECAMP_DIR="$HOME/.local/share/Logos/LogosBasecampDev"
```
2026-03-25 10:37:15 +01:00
```bash
2026-03-18 14:23:44 +01:00
# Build lgpm CLI
nix build 'github:logos-co/logos-package-manager/tutorial-v1#cli' --out-link ./pm
2026-03-18 14:23:44 +01:00
2026-03-25 10:37:15 +01:00
# Install core module
2026-04-01 17:57:24 +02:00
./pm/bin/lgpm --modules-dir "$BASECAMP_DIR/modules" \
install --file ../logos-calc-module/result-lgx/*.lgx
2026-03-18 14:23:44 +01:00
2026-03-25 10:37:15 +01:00
# Install UI plugin
2026-04-01 17:57:24 +02:00
./pm/bin/lgpm --ui-plugins-dir "$BASECAMP_DIR/plugins" \
install --file result-lgx/*.lgx
2026-03-18 14:23:44 +01:00
2026-03-25 10:37:15 +01:00
# Launch basecamp -- your modules appear alongside the built-in ones
./basecamp-result/bin/logos-basecamp
```
2026-04-01 17:57:24 +02:00
### 7.3 Portable basecamp build (optional)
The dev build above depends on nix store paths at runtime. For a self-contained portable build that works without nix:
```bash
# Build portable basecamp (bundles all Qt frameworks/libraries)
nix build 'github:logos-co/logos-basecamp/tutorial-v1#bin-bundle-dir' -o basecamp-portable
2026-04-01 17:57:24 +02:00
# Launch once to preinstall bundled modules
./basecamp-portable/bin/logos-basecamp
```
The portable build uses a different data directory (`LogosBasecamp` instead of `LogosBasecampDev`). Set `BASECAMP_DIR` to your platform's path:
```bash
# macOS:
BASECAMP_DIR="$HOME/Library/Application Support/Logos/LogosBasecamp"
# Linux:
BASECAMP_DIR="$HOME/.local/share/Logos/LogosBasecamp"
```
Install your modules using the **portable** `.lgx` variants:
```bash
# Install core module (use portable variant)
./pm/bin/lgpm --modules-dir "$BASECAMP_DIR/modules" \
install --file ../logos-calc-module/result-lgx-portable/*.lgx
# Install UI plugin (use portable variant)
./pm/bin/lgpm --ui-plugins-dir "$BASECAMP_DIR/plugins" \
install --file result-lgx-portable/*.lgx
# Launch
./basecamp-portable/bin/logos-basecamp
```
> **Important:** Portable basecamp requires portable `.lgx` variants (`result-lgx-portable`), and the dev build requires dev variants (`result-lgx`). Mixing them will cause loading failures.
### 7.4 Install via logos-basecamp UI
2026-03-18 14:23:44 +01:00
2026-03-25 10:37:15 +01:00
Instead of using `lgpm` on the command line, you can install modules through the basecamp UI:
2026-03-18 14:23:44 +01:00
2026-03-25 10:37:15 +01:00
1. Launch `logos-basecamp`
2. Go to **Package Manager**
3. Click **Install from file**
2026-04-01 17:57:24 +02:00
4. Select `../logos-calc-module/result-lgx/*.lgx` — installs `calc_module`
5. Repeat for `result-lgx/*.lgx` — installs `calc_ui`
2026-03-25 10:37:15 +01:00
The "Calculator UI" tab appears in the sidebar. Clicking it loads your `Main.qml`.
2026-04-01 17:57:24 +02:00
### 7.5 Live reloading with `logos-standalone-app`
2026-03-25 10:37:15 +01:00
For rapid iteration on QML without rebuilding, set `QML_PATH` to your QML source directory:
2026-03-18 14:23:44 +01:00
```bash
2026-04-01 12:21:06 +02:00
QML_PATH=$PWD nix run .
2026-03-18 14:23:44 +01:00
```
2026-03-25 10:37:15 +01:00
Edit `Main.qml`, close and re-run — changes appear immediately without `nix build`. When `QML_PATH` is set, the plugin loads QML files from the filesystem instead of from Qt resources, so your edits are picked up on each launch.
2026-03-25 10:37:15 +01:00
> This does not work with `logos-basecamp`. Basecamp loads QML plugins from its own data directory, so changes to your source files are not reflected until you rebuild and reinstall the `.lgx` package.
2026-04-01 17:57:24 +02:00
### 7.6 Testing without any runtime
2026-03-25 10:37:15 +01:00
You can open `Main.qml` in any QML viewer (e.g., `qml` from Qt) to test the layout. The `logos` bridge won't be available, so clicking buttons will show "Logos bridge not available" -- but you can verify the layout and styling work correctly.
```bash
# If you have Qt installed
qml Main.qml
```
---
2026-03-25 10:37:15 +01:00
## Known Limitations
2026-03-31 19:00:16 +02:00
### QML-to-C++ type coercion
2026-03-31 19:00:16 +02:00
When calling C++ module methods from QML via `logos.callModule()`, arguments are passed through IPC as `QVariant` values. The runtime automatically coerces mismatched types to match the target method signature — for example, a `double` sent from QML will be converted to `int` if the method expects `int`, and numeric strings will be converted to their numeric types.
2026-03-07 15:04:37 +00:00
2026-03-31 19:00:16 +02:00
This means you can define methods with their natural parameter types (`int`, `bool`, `double`, etc.) and calls from QML will work without manual conversion:
2026-03-25 10:37:15 +01:00
```cpp
2026-03-31 19:00:16 +02:00
// This works — the runtime coerces arguments automatically
Q_INVOKABLE int add(int a, int b) { return a + b; }
```
2026-03-31 19:00:16 +02:00
> **Note:** Type coercion uses `QVariant::convert()`, which rounds (not truncates) when converting `double` to `int` — e.g., `3.7` becomes `4`.
2026-03-25 10:37:15 +01:00
### QML changes not appearing after rebuild
Qt caches compiled QML on disk. If you update your `Main.qml`, rebuild and reinstall the `.lgx`, but the old UI still appears, the cache is stale. Fix by disabling the cache before launching:
```bash
QML_DISABLE_DISK_CACHE=1 ./basecamp-result/bin/logos-basecamp
```
### UI module not loading or basecamp behaving unexpectedly
When switching between portable and dev builds of basecamp, or running multiple basecamp instances, the data directory can get into a bad state (stale modules, mixed variants, corrupted preinstall). Clear it and let basecamp re-preinstall on next launch:
```bash
2026-04-01 17:57:24 +02:00
# Remove basecamp's data directory
# macOS:
2026-03-25 10:37:15 +01:00
rm -rf ~/Library/Application\ Support/Logos/LogosBasecampDev
2026-04-01 17:57:24 +02:00
# Linux:
rm -rf ~/.local/share/Logos/LogosBasecampDev
2026-03-25 10:37:15 +01:00
# Relaunch — basecamp will re-preinstall its bundled modules
./basecamp-result/bin/logos-basecamp
```
Then reinstall your custom modules.
2026-03-07 15:04:37 +00:00
---
2026-03-18 14:23:44 +01:00
## Recap
2026-03-06 16:45:49 +00:00
2026-03-18 14:23:44 +01:00
| | Core Module (Part 1) | QML UI Plugin (Part 2) |
|---|---|---|
| Language | C++ | QML / JavaScript |
| Files | `.cpp`, `.h`, `CMakeLists.txt`, `metadata.json` | `Main.qml`, `metadata.json` |
2026-03-18 14:23:44 +01:00
| Compilation | Yes (CMake → `.so`) | No (file copy) |
| `metadata.type` | `"core"` | `"ui_qml"` |
| Test command | `logoscore -m ./result/lib -l calc_module` | `nix run .` |
| Calls other modules | Via `LogosAPI*` (C++) | Via `logos.callModule()` (JS) |
---
## What's Next
2026-03-18 14:23:44 +01:00
- **Add more methods** to `calc_module` and call them from QML
- **Use Logos Design System** styled components for consistent look and feel
- **Build a C++ UI module** for cases where QML sandboxing is too restrictive — see [Developer Guide](logos-developer-guide.md), Section 7.2