Adding things
Three common changes, start to finish. Each builds on The cxx-qt bridge.
A setting
A setting is a value in ui.toml that QML reads and writes live. The bridge for it is generated by kushi from a declaration in crates/omikuji/build.rs. For a new bool show_clock:
-
Add the field to the core struct in
omikuji-core/src/ui_settings.rs(the right section, e.g.DisplaySettings) and to that section’sDefault. -
Declare it in the
ObjectBridgebuilder incrates/omikuji/build.rs, next to the other props:
#![allow(unused)]
fn main() {
.prop_at("show_clock", kushi::Kind::Bool, "display.show_clock")
}
The last argument is the field’s path inside UiSettings. The generated names derive from the first: property showClock, invokable applyShowClock.
- In QML, read
uiSettings.showClockand write throughuiSettings.applyShowClock(value).
The property, the apply invokable, persistence, and hot reload are all generated. When the setter needs logic (clamping, a side effect, a signal), declare it with .prop_custom_apply(...) instead and write apply_show_clock by hand in src/bridge/ui_settings.rs; apply_ui_scale and apply_discord_rpc are the existing examples. .prop_readonly(...) declares a property with no setter (fill_fields, radius_scale). List-shaped settings that cross as JSON use .json_accessor(...); categories is the model.
A per-game setting
Per-game config (the fields in GameSettingsPage: wine version, env, launch options) lives on the Game struct in library.toml, not ui.toml, and it doesn’t use properties: the whole game config crosses to QML as one string-keyed map, so a new field is one row in a table.
-
Add the field to the right struct on
Gameinomikuji-core/src/library/mod.rs(WineConfig,LaunchConfig, …), with a serde default. -
Register it in the
game_fields!table incrates/omikuji/src/bridge/game_model.rs:
#![allow(unused)]
fn main() {
"wine.my_flag" => bool, wine.my_flag,
}
That row wires both directions: reading the field into the config map and writing it back from QML. The kind tag picks the conversion (str, path, bool, int, json for maps and vecs, args for launch args). Add readonly after the kind for a read-only field.
- In the settings tab QML (
TabRunnerOptions,TabSystem, …), add the control. Read the value fromconfig["wine.my_flag"]and write changes through the page’supdateField("wine.my_flag", value), which updates the draft and refreshesconfig.
A runtime tool
Runtime tools (umu, legendary, jadeite, …) are fetched by the component system. To add one:
-
Add a variant to
SettingsKeyinomikuji-core/src/components/spec.rs. -
Add the download URL to
ComponentsSettingsinomikuji-core/src/settings.rsand itsDefault. These are release-latest API URLs (GitHub, Codeberg) or a direct URL. -
Map the key to the URL in
url_forinomikuji-core/src/components/mod.rs. -
Add the
ComponentSpecinomikuji-core/src/components/specs.rs:
#![allow(unused)]
fn main() {
ComponentSpec {
name: "mytool",
source: Source::GithubRelease { asset_matcher: |n| n.ends_with(".tar.gz") },
extract: ExtractStrategy::TarGz { inner_path: "mytool" },
dest: "mytool",
settings_key: SettingsKey::MyTool,
trigger: Trigger::OnDemand,
},
}
- Pick the trigger.
Eagerfetches at boot (only umu uses this).OnDemandfetches when something callscomponents::ensure(...). ForOnDemand, add thatensurecall at the point the tool becomes necessary (a store login, a game install).epic_toolsandgacha_toolsincomponents/mod.rsare the existing examples.
A dialog
Dialogs are built on DialogCard, which owns the backdrop, dim, click-outside, Esc, shadow, and scrolling. A dialog is a config of its slots.
- Create
qml/components/dialogs/MyDialog.qml:
import QtQuick
import "../widgets"
DialogCard {
id: root
title: "Do the thing?"
maxWidth: 480
body: Column {
spacing: theme.space.md
Text { text: "..."; color: theme.text }
}
actions: Row {
spacing: theme.space.sm
M3Button { text: "Cancel"; variant: "text"; onClicked: root.close() }
M3Button { text: "Confirm"; variant: "tonal"; onClicked: { root.close() } }
}
onCloseRequested: close()
}
-
Register it in
qml_filesinbuild.rs. -
Instantiate it once where it’s used (a page, or
Main.qml), give it anid, and call.open()to show it.
The slots take a Component, but a plain element works directly. Don’t add your own backdrop or scrollbar, DialogCard owns those. For a fixed-height list dialog, set fillHeight: true and scrollable: false.