use std::fmt::Write as _;
use std::path::{Path, PathBuf};
pub fn stage_files(files: impl IntoIterator<Item = impl AsRef<Path>>, dir: &Path) -> Vec<PathBuf> {
let mut seen = std::collections::HashSet::new();
files
.into_iter()
.map(|f| {
let src = f.as_ref();
let name = src
.file_name()
.unwrap_or_else(|| panic!("kushi: staged path {} has no file name", src.display()));
assert!(
seen.insert(name.to_owned()),
"kushi: staged files must have unique names, duplicate: {}",
name.to_string_lossy()
);
println!("cargo::rerun-if-changed={}", src.display());
let body = std::fs::read_to_string(src).unwrap_or_else(|e| {
panic!("kushi: failed to read staged file {}: {e}", src.display())
});
let dest = dir.join(name);
write_if_changed(&dest, &body);
dest
})
.collect()
}
fn write_if_changed(path: &Path, body: &str) {
let unchanged = std::fs::read_to_string(path)
.map(|old| old == body)
.unwrap_or(false);
if !unchanged {
std::fs::write(path, body).expect("kushi: failed to write generated bridge");
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum Kind {
Bool,
F64,
I32,
QString,
}
impl Kind {
fn bridge_ty(self) -> &'static str {
match self {
Kind::Bool => "bool",
Kind::F64 => "f64",
Kind::I32 => "i32",
Kind::QString => "QString",
}
}
fn rust_ty(self) -> &'static str {
match self {
Kind::Bool => "bool",
Kind::F64 => "f64",
Kind::I32 => "i32",
Kind::QString => "cxx_qt_lib::QString",
}
}
fn data_ty(self) -> &'static str {
match self {
Kind::Bool => "bool",
Kind::F64 => "f64",
Kind::I32 => "i32",
Kind::QString => "String",
}
}
fn read_data_expr(self, accessor: &str) -> String {
match self {
Kind::QString => format!("cxx_qt_lib::QString::from(&s.{accessor})"),
_ => format!("s.{accessor}"),
}
}
fn to_data_expr(self, name: &str) -> String {
match self {
Kind::QString => format!("self.{name}.to_string()"),
_ => format!("self.{name}"),
}
}
}
#[derive(Clone, Copy, PartialEq)]
enum Apply {
Generated,
Custom,
ReadOnly,
}
struct Prop {
name: String,
kind: Kind,
default: Option<String>,
path: Option<String>,
apply: Apply,
}
impl Prop {
fn data_path(&self) -> &str {
self.path
.as_deref()
.expect("kushi: props in external mode need a data path; use prop_at, prop_custom_apply or prop_readonly")
}
}
struct RawField {
name: String,
ty: String,
from_expr: String,
snapshot_stmt: Option<String>,
}
struct External {
type_path: String,
load_expr: String,
}
struct JsonAccessor {
name: String,
ty: String,
path: String,
changed: String,
}
pub struct ObjectBridge {
qobject: String,
path_expr: String,
external: Option<External>,
props: Vec<Prop>,
signals: Vec<String>,
custom_invokables: Vec<(String, String)>,
raw_fields: Vec<RawField>,
json_accessors: Vec<JsonAccessor>,
reload_hook: Option<String>,
threading: bool,
}
impl ObjectBridge {
pub fn new(qobject: &str) -> Self {
Self {
qobject: qobject.to_string(),
path_expr: r#"std::path::PathBuf::from("settings.toml")"#.to_string(),
external: None,
props: Vec::new(),
signals: Vec::new(),
custom_invokables: Vec::new(),
raw_fields: Vec::new(),
json_accessors: Vec::new(),
reload_hook: None,
threading: false,
}
}
pub fn toml_path(mut self, expr: &str) -> Self {
self.path_expr = expr.to_string();
self
}
pub fn external_data(mut self, type_path: &str, load_expr: &str) -> Self {
self.external = Some(External {
type_path: type_path.to_string(),
load_expr: load_expr.to_string(),
});
self
}
pub fn prop(mut self, name: &str, kind: Kind, default: &str) -> Self {
assert_snake(name);
let default = match kind {
Kind::QString => format!("{:?}.to_string()", default),
_ => default.to_string(),
};
self.props.push(Prop {
name: name.to_string(),
kind,
default: Some(default),
path: None,
apply: Apply::Generated,
});
self
}
pub fn prop_at(self, name: &str, kind: Kind, path: &str) -> Self {
self.push_external_prop(name, kind, path, Apply::Generated)
}
pub fn prop_custom_apply(self, name: &str, kind: Kind, path: &str) -> Self {
self.push_external_prop(name, kind, path, Apply::Custom)
}
pub fn prop_readonly(self, name: &str, kind: Kind, path: &str) -> Self {
self.push_external_prop(name, kind, path, Apply::ReadOnly)
}
fn push_external_prop(mut self, name: &str, kind: Kind, path: &str, apply: Apply) -> Self {
assert_snake(name);
self.props.push(Prop {
name: name.to_string(),
kind,
default: None,
path: Some(path.to_string()),
apply,
});
self
}
pub fn qsignal(mut self, name: &str) -> Self {
assert_snake(name);
self.signals.push(name.to_string());
self
}
pub fn custom_invokable(mut self, cxx_name: &str, signature: &str) -> Self {
self.custom_invokables
.push((cxx_name.to_string(), signature.to_string()));
self
}
pub fn raw_field(mut self, name: &str, ty: &str, from_expr: &str) -> Self {
assert_snake(name);
self.raw_fields.push(RawField {
name: name.to_string(),
ty: ty.to_string(),
from_expr: from_expr.to_string(),
snapshot_stmt: None,
});
self
}
pub fn raw_field_persisted(
mut self,
name: &str,
ty: &str,
from_expr: &str,
snapshot_stmt: &str,
) -> Self {
assert_snake(name);
self.raw_fields.push(RawField {
name: name.to_string(),
ty: ty.to_string(),
from_expr: from_expr.to_string(),
snapshot_stmt: Some(snapshot_stmt.to_string()),
});
self
}
pub fn json_accessor(mut self, name: &str, ty: &str, path: &str, changed: &str) -> Self {
assert_snake(name);
assert_snake(changed);
assert!(
!self.signals.contains(&changed.to_string())
&& !self.json_accessors.iter().any(|a| a.changed == changed),
"kushi: json_accessor declares its own '{changed}' qsignal, remove the duplicate declaration"
);
self.json_accessors.push(JsonAccessor {
name: name.to_string(),
ty: ty.to_string(),
path: path.to_string(),
changed: changed.to_string(),
});
self
}
pub fn reload_hook(mut self, fn_name: &str) -> Self {
assert_snake(fn_name);
self.reload_hook = Some(fn_name.to_string());
self
}
pub fn threading(mut self) -> Self {
self.threading = true;
self
}
pub fn write_into(&self, dir: &Path) -> PathBuf {
let path = dir.join(format!("{}.rs", snake(&self.qobject)));
write_if_changed(&path, &self.generate());
path
}
pub fn generate(&self) -> String {
match &self.external {
Some(_) => self.generate_external(),
None => self.generate_owned(),
}
}
fn emit_bridge_module(&self, out: &mut String) {
let q = &self.qobject;
let rust = format!("{q}Rust");
let has_qstring =
self.external.is_some() || self.props.iter().any(|p| p.kind == Kind::QString);
out.push_str("#[cxx_qt::bridge]\npub mod qobject {\n");
if has_qstring {
out.push_str(concat!(
" unsafe extern \"C++\" {\n",
" include!(\"cxx-qt-lib/qstring.h\");\n",
" type QString = cxx_qt_lib::QString;\n",
" }\n\n",
));
}
out.push_str(" extern \"RustQt\" {\n #[qobject]\n #[qml_element]\n");
for p in &self.props {
let cam = camel(&p.name);
if cam == p.name {
let _ = writeln!(
out,
" #[qproperty({}, {})]",
p.kind.bridge_ty(),
p.name
);
} else {
let _ = writeln!(
out,
" #[qproperty({}, {}, cxx_name = \"{}\")]",
p.kind.bridge_ty(),
p.name,
cam
);
}
}
let _ = writeln!(out, " type {q} = super::{rust};");
out.push_str(" }\n\n");
let all_signals: Vec<&String> = self
.signals
.iter()
.chain(self.json_accessors.iter().map(|a| &a.changed))
.collect();
if !all_signals.is_empty() {
out.push_str(" unsafe extern \"RustQt\" {\n");
for (i, s) in all_signals.iter().enumerate() {
if i > 0 {
out.push('\n');
}
let _ = writeln!(out, " #[qsignal]");
let _ = writeln!(out, " #[cxx_name = \"{}\"]", camel(s));
let _ = writeln!(out, " fn {s}(self: Pin<&mut {q}>);");
}
out.push_str(" }\n\n");
}
if self.threading {
let _ = writeln!(out, " impl cxx_qt::Threading for {q} {{}}\n");
}
out.push_str(" unsafe extern \"RustQt\" {\n");
for p in &self.props {
if p.apply == Apply::ReadOnly {
continue;
}
let value_ty = match p.kind {
Kind::QString => "&QString".to_string(),
k => k.bridge_ty().to_string(),
};
let _ = writeln!(out, " #[qinvokable]");
let _ = writeln!(out, " #[cxx_name = \"apply{}\"]", pascal(&p.name));
let _ = writeln!(
out,
" fn apply_{}(self: Pin<&mut {q}>, value: {value_ty});\n",
p.name
);
}
for a in &self.json_accessors {
let _ = writeln!(out, " #[qinvokable]");
let _ = writeln!(out, " #[cxx_name = \"{}Json\"]", camel(&a.name));
let _ = writeln!(out, " fn {}_json(self: &{q}) -> QString;\n", a.name);
let _ = writeln!(out, " #[qinvokable]");
let _ = writeln!(
out,
" #[cxx_name = \"apply{}Json\"]",
pascal(&a.name)
);
let _ = writeln!(
out,
" fn apply_{}_json(self: Pin<&mut {q}>, json: &QString);\n",
a.name
);
}
for (cxx, sig) in &self.custom_invokables {
let _ = writeln!(out, " #[qinvokable]");
let _ = writeln!(out, " #[cxx_name = \"{cxx}\"]");
let _ = writeln!(out, " {sig}\n");
}
out.push_str(concat!(
" #[qinvokable]\n",
" #[cxx_name = \"reloadFromDisk\"]\n",
));
let _ = writeln!(out, " fn reload_from_disk(self: Pin<&mut {q}>);");
out.push_str(" }\n}\n\nuse std::pin::Pin;\n");
}
fn emit_apply_bodies(&self, out: &mut String) {
for p in &self.props {
if p.apply != Apply::Generated {
continue;
}
let (value_ty, set_expr) = match p.kind {
Kind::QString => (
"&cxx_qt_lib::QString".to_string(),
"value.clone()".to_string(),
),
k => (k.bridge_ty().to_string(), "value".to_string()),
};
let _ = writeln!(
out,
" fn apply_{n}(mut self: Pin<&mut Self>, value: {value_ty}) {{\n self.as_mut().set_{n}({set_expr});\n self.persist();\n }}\n",
n = p.name
);
}
}
fn generate_owned(&self) -> String {
assert!(
self.props.iter().all(|p| p.default.is_some()),
"kushi: prop_at/prop_custom_apply/prop_readonly require external_data(); use prop() in owned mode"
);
assert!(
self.raw_fields.is_empty()
&& self.reload_hook.is_none()
&& self.json_accessors.is_empty(),
"kushi: raw fields, json accessors and reload_hook require external_data()"
);
let q = &self.qobject;
let rust = format!("{q}Rust");
let data = format!("{q}Data");
let mut out = String::new();
out.push_str("// generated by kushi, do not edit\n\n");
self.emit_bridge_module(&mut out);
out.push('\n');
out.push_str("#[derive(serde::Serialize, serde::Deserialize)]\n#[serde(default)]\n");
let _ = writeln!(out, "pub struct {data} {{");
for p in &self.props {
let _ = writeln!(out, " pub {}: {},", p.name, p.kind.data_ty());
}
out.push_str("}\n\n");
let _ = writeln!(
out,
"impl Default for {data} {{\n fn default() -> Self {{\n Self {{"
);
for p in &self.props {
let _ = writeln!(
out,
" {}: {},",
p.name,
p.default.as_deref().unwrap()
);
}
out.push_str(" }\n }\n}\n\n");
let _ = writeln!(out, "impl {data} {{");
let _ = writeln!(
out,
" pub fn path() -> std::path::PathBuf {{\n {}\n }}\n",
self.path_expr
);
out.push_str(concat!(
" pub fn load() -> Self {\n",
" std::fs::read_to_string(Self::path())\n",
" .ok()\n",
" .and_then(|s| toml::from_str(&s).ok())\n",
" .unwrap_or_default()\n",
" }\n\n",
" pub fn save(&self) {\n",
" let path = Self::path();\n",
" if let Some(parent) = path.parent() {\n",
" let _ = std::fs::create_dir_all(parent);\n",
" }\n",
" if let Ok(body) = toml::to_string_pretty(self) {\n",
" let _ = std::fs::write(path, body);\n",
" }\n",
" }\n",
"}\n\n",
));
let _ = writeln!(out, "pub struct {rust} {{");
for p in &self.props {
let _ = writeln!(out, " pub {}: {},", p.name, p.kind.rust_ty());
}
out.push_str("}\n\n");
let _ = writeln!(
out,
"impl Default for {rust} {{\n fn default() -> Self {{\n Self::from_settings(&{data}::load())\n }}\n}}\n"
);
let _ = writeln!(
out,
"impl {rust} {{\n fn from_settings(s: &{data}) -> Self {{\n Self {{"
);
for p in &self.props {
let _ = writeln!(
out,
" {}: {},",
p.name,
p.kind.read_data_expr(&p.name)
);
}
out.push_str(" }\n }\n}\n\n");
let _ = writeln!(out, "impl qobject::{q} {{");
let _ = writeln!(
out,
" pub(crate) fn snapshot(&self) -> {data} {{\n {data} {{"
);
for p in &self.props {
let _ = writeln!(
out,
" {}: {},",
p.name,
p.kind.to_data_expr(&p.name)
);
}
out.push_str(" }\n }\n\n fn persist(self: Pin<&mut Self>) {\n self.snapshot().save();\n }\n\n");
self.emit_apply_bodies(&mut out);
let _ = writeln!(
out,
" fn reload_from_disk(mut self: Pin<&mut Self>) {{\n let s = {data}::load();"
);
for p in &self.props {
let _ = writeln!(
out,
" self.as_mut().set_{}({});",
p.name,
p.kind.read_data_expr(&p.name)
);
}
out.push_str(" }\n}\n");
out
}
fn generate_external(&self) -> String {
let ext = self.external.as_ref().unwrap();
let q = &self.qobject;
let rust = format!("{q}Rust");
let ty = &ext.type_path;
let load = &ext.load_expr;
let needs_rust_mut = !self.json_accessors.is_empty()
|| self.raw_fields.iter().any(|f| f.snapshot_stmt.is_some());
let mut out = String::new();
out.push_str("// generated by kushi, do not edit\n\n");
self.emit_bridge_module(&mut out);
if needs_rust_mut {
out.push_str("use cxx_qt::CxxQtType;\n");
}
out.push('\n');
let _ = writeln!(out, "pub struct {rust} {{");
for p in &self.props {
let _ = writeln!(out, " pub {}: {},", p.name, p.kind.rust_ty());
}
for a in &self.json_accessors {
let _ = writeln!(out, " pub {}: {},", a.name, a.ty);
}
for f in &self.raw_fields {
let _ = writeln!(out, " pub {}: {},", f.name, f.ty);
}
out.push_str("}\n\n");
let _ = writeln!(
out,
"impl Default for {rust} {{\n fn default() -> Self {{\n Self::from_settings(&{load})\n }}\n}}\n"
);
let _ = writeln!(
out,
"impl {rust} {{\n fn from_settings(s: &{ty}) -> Self {{\n Self {{"
);
for p in &self.props {
let _ = writeln!(
out,
" {}: {},",
p.name,
p.kind.read_data_expr(p.data_path())
);
}
for a in &self.json_accessors {
let _ = writeln!(out, " {}: s.{}.clone(),", a.name, a.path);
}
for f in &self.raw_fields {
let _ = writeln!(out, " {}: {},", f.name, f.from_expr);
}
out.push_str(" }\n }\n}\n\n");
let _ = writeln!(out, "impl qobject::{q} {{");
let _ = writeln!(
out,
" pub(crate) fn snapshot(&self) -> {ty} {{\n let mut s = {load};"
);
for p in &self.props {
let _ = writeln!(
out,
" s.{} = {};",
p.data_path(),
p.kind.to_data_expr(&p.name)
);
}
for a in &self.json_accessors {
let _ = writeln!(out, " s.{} = self.{}.clone();", a.path, a.name);
}
for f in &self.raw_fields {
if let Some(stmt) = &f.snapshot_stmt {
let _ = writeln!(out, " {stmt}");
}
}
out.push_str(" s\n }\n\n");
self.emit_apply_bodies(&mut out);
for a in &self.json_accessors {
let label = a.name.replace('_', " ");
let _ = writeln!(
out,
" fn {}_json(&self) -> cxx_qt_lib::QString {{\n let json = serde_json::to_string(&self.{}).unwrap_or_else(|_| \"[]\".to_string());\n cxx_qt_lib::QString::from(&json)\n }}\n",
a.name, a.name
);
let _ = writeln!(
out,
" fn apply_{n}_json(mut self: Pin<&mut Self>, json: &cxx_qt_lib::QString) {{\n match serde_json::from_str::<{ty}>(&json.to_string()) {{\n Ok(entries) => {{\n self.as_mut().rust_mut().get_mut().{n} = entries;\n self.as_mut().persist();\n self.as_mut().{changed}();\n }}\n Err(e) => tracing::error!(\"bad {label} json: {{}}\", e),\n }}\n }}\n",
n = a.name,
ty = a.ty,
changed = a.changed,
label = label
);
}
let _ = writeln!(
out,
" fn reload_from_disk(mut self: Pin<&mut Self>) {{\n let s = {load};"
);
for p in &self.props {
let _ = writeln!(
out,
" self.as_mut().set_{}({});",
p.name,
p.kind.read_data_expr(p.data_path())
);
}
for a in &self.json_accessors {
let _ = writeln!(
out,
" self.as_mut().rust_mut().get_mut().{} = s.{}.clone();",
a.name, a.path
);
let _ = writeln!(out, " self.as_mut().{}();", a.changed);
}
for f in &self.raw_fields {
if f.snapshot_stmt.is_some() {
let _ = writeln!(
out,
" self.as_mut().rust_mut().get_mut().{} = {};",
f.name, f.from_expr
);
}
}
if let Some(hook) = &self.reload_hook {
let _ = writeln!(out, " self.as_mut().{hook}(&s);");
}
out.push_str(" }\n}\n");
out
}
}
enum RoleDef {
Plain {
name: String,
kind: Kind,
path: String,
},
Computed {
name: String,
func: String,
},
}
impl RoleDef {
fn name(&self) -> &str {
match self {
RoleDef::Plain { name, .. } => name,
RoleDef::Computed { name, .. } => name,
}
}
fn value_expr(&self) -> String {
match self {
RoleDef::Plain {
kind: Kind::QString,
path,
..
} => {
format!("cxx_qt_lib::QVariant::from(&cxx_qt_lib::QString::from(&*item.{path}))")
}
RoleDef::Plain { path, .. } => format!("cxx_qt_lib::QVariant::from(&item.{path})"),
RoleDef::Computed { func, .. } => format!("{func}(item)"),
}
}
}
pub struct ListModelBridge {
qobject: String,
item_type: String,
items_name: String,
items_init: String,
custom_default: bool,
roles: Vec<RoleDef>,
qproperties: Vec<(String, Kind)>,
raw_signals: Vec<String>,
snapshot_map: Option<String>,
custom_invokables: Vec<(String, String)>,
raw_invokables: Vec<String>,
row_ops: bool,
file_stem: Option<String>,
}
impl ListModelBridge {
pub fn new(qobject: &str) -> Self {
Self {
qobject: qobject.to_string(),
item_type: String::new(),
items_name: "items".to_string(),
items_init: "Vec::new()".to_string(),
custom_default: false,
roles: Vec::new(),
qproperties: Vec::new(),
raw_signals: Vec::new(),
snapshot_map: None,
custom_invokables: Vec::new(),
raw_invokables: Vec::new(),
row_ops: false,
file_stem: None,
}
}
pub fn file_stem(mut self, stem: &str) -> Self {
assert_snake(stem);
self.file_stem = Some(stem.to_string());
self
}
pub fn item_type(mut self, path: &str) -> Self {
self.item_type = path.to_string();
self
}
pub fn items_name(mut self, name: &str) -> Self {
assert_snake(name);
self.items_name = name.to_string();
self
}
pub fn items_init(mut self, expr: &str) -> Self {
self.items_init = expr.to_string();
self
}
pub fn custom_default(mut self) -> Self {
self.custom_default = true;
self
}
pub fn qproperty(mut self, name: &str, kind: Kind) -> Self {
assert_snake(name);
self.qproperties.push((name.to_string(), kind));
self
}
pub fn qsignal_raw(mut self, signature: &str) -> Self {
self.raw_signals.push(signature.to_string());
self
}
pub fn custom_invokable_raw(mut self, signature: &str) -> Self {
self.raw_invokables.push(signature.to_string());
self
}
pub fn row_ops(mut self) -> Self {
self.row_ops = true;
self
}
pub fn role(mut self, name: &str, kind: Kind, path: &str) -> Self {
assert_snake(name);
self.roles.push(RoleDef::Plain {
name: name.to_string(),
kind,
path: path.to_string(),
});
self
}
pub fn role_fn(mut self, name: &str, func: &str) -> Self {
assert_snake(name);
self.roles.push(RoleDef::Computed {
name: name.to_string(),
func: func.to_string(),
});
self
}
pub fn snapshot_map(mut self, cxx_name: &str) -> Self {
self.snapshot_map = Some(cxx_name.to_string());
self
}
pub fn custom_invokable(mut self, cxx_name: &str, signature: &str) -> Self {
self.custom_invokables
.push((cxx_name.to_string(), signature.to_string()));
self
}
pub fn write_into(&self, dir: &Path) -> PathBuf {
let stem = self
.file_stem
.clone()
.unwrap_or_else(|| snake(&self.qobject));
let path = dir.join(format!("{stem}.rs"));
write_if_changed(&path, &self.generate());
path
}
pub fn generate(&self) -> String {
let q = &self.qobject;
let rust = format!("{q}Rust");
let mut out = String::new();
out.push_str("// generated by kushi, do not edit\n\n");
out.push_str("#[cxx_qt::bridge]\npub mod qobject {\n");
out.push_str(concat!(
" unsafe extern \"C++\" {\n",
" include!(<QtCore/QAbstractListModel>);\n",
" type QAbstractListModel;\n\n",
" include!(\"cxx-qt-lib/qmodelindex.h\");\n",
" type QModelIndex = cxx_qt_lib::QModelIndex;\n",
" include!(\"cxx-qt-lib/qvariant.h\");\n",
" type QVariant = cxx_qt_lib::QVariant;\n",
" include!(\"cxx-qt-lib/qstring.h\");\n",
" type QString = cxx_qt_lib::QString;\n",
" include!(\"cxx-qt-lib/qbytearray.h\");\n",
" type QByteArray = cxx_qt_lib::QByteArray;\n",
" include!(\"cxx-qt-lib/qhash.h\");\n",
" type QHash_i32_QByteArray = cxx_qt_lib::QHash<cxx_qt_lib::QHashPair_i32_QByteArray>;\n",
" include!(\"cxx-qt-lib/qlist.h\");\n",
" type QList_i32 = cxx_qt_lib::QList<i32>;\n",
));
if self.snapshot_map.is_some() {
out.push_str(concat!(
" include!(\"cxx-qt-lib/qmap.h\");\n",
" type QMap_QString_QVariant = cxx_qt_lib::QMap<cxx_qt_lib::QMapPair_QString_QVariant>;\n",
));
}
out.push_str(" }\n\n");
out.push_str(" extern \"RustQt\" {\n #[qobject]\n #[qml_element]\n #[base = QAbstractListModel]\n");
for (name, kind) in &self.qproperties {
let cam = camel(name);
if &cam == name {
let _ = writeln!(out, " #[qproperty({}, {})]", kind.bridge_ty(), name);
} else {
let _ = writeln!(
out,
" #[qproperty({}, {}, cxx_name = \"{}\")]",
kind.bridge_ty(),
name,
cam
);
}
}
let _ = writeln!(out, " type {q} = super::{rust};");
out.push_str(" }\n\n");
if !self.raw_signals.is_empty() {
out.push_str(" unsafe extern \"RustQt\" {\n");
for (i, sig) in self.raw_signals.iter().enumerate() {
if i > 0 {
out.push('\n');
}
let _ = writeln!(out, " #[qsignal]");
let _ = writeln!(out, " {sig}");
}
out.push_str(" }\n\n");
}
out.push_str(" unsafe extern \"RustQt\" {\n");
out.push_str(concat!(
" #[cxx_name = \"rowCount\"]\n",
" #[cxx_override]\n",
));
let _ = writeln!(
out,
" fn row_count(self: &{q}, parent: &QModelIndex) -> i32;\n"
);
out.push_str(" #[cxx_override]\n");
let _ = writeln!(
out,
" fn data(self: &{q}, index: &QModelIndex, role: i32) -> QVariant;\n"
);
out.push_str(concat!(
" #[cxx_name = \"roleNames\"]\n",
" #[cxx_override]\n",
));
let _ = writeln!(
out,
" fn role_names(self: &{q}) -> QHash_i32_QByteArray;\n"
);
if let Some(cxx) = &self.snapshot_map {
let _ = writeln!(out, " #[qinvokable]");
let _ = writeln!(out, " #[cxx_name = \"{cxx}\"]");
let _ = writeln!(
out,
" fn {}(self: &{q}, index: i32) -> QMap_QString_QVariant;\n",
snake(cxx)
);
}
for (cxx, sig) in &self.custom_invokables {
let _ = writeln!(out, " #[qinvokable]");
let _ = writeln!(out, " #[cxx_name = \"{cxx}\"]");
let _ = writeln!(out, " {sig}\n");
}
for sig in &self.raw_invokables {
let _ = writeln!(out, " #[qinvokable]");
let _ = writeln!(out, " {sig}\n");
}
if self.row_ops {
let _ = writeln!(
out,
" #[cxx_name = \"beginInsertRows\"]\n #[inherit]\n fn begin_insert_rows(self: Pin<&mut {q}>, parent: &QModelIndex, first: i32, last: i32);\n"
);
let _ = writeln!(
out,
" #[cxx_name = \"endInsertRows\"]\n #[inherit]\n fn end_insert_rows(self: Pin<&mut {q}>);\n"
);
let _ = writeln!(
out,
" #[cxx_name = \"beginRemoveRows\"]\n #[inherit]\n fn begin_remove_rows(self: Pin<&mut {q}>, parent: &QModelIndex, first: i32, last: i32);\n"
);
let _ = writeln!(
out,
" #[cxx_name = \"endRemoveRows\"]\n #[inherit]\n fn end_remove_rows(self: Pin<&mut {q}>);\n"
);
}
out.push_str(concat!(
" #[cxx_name = \"index\"]\n",
" #[inherit]\n",
));
let _ = writeln!(
out,
" fn model_index(self: &{q}, row: i32, column: i32, parent: &QModelIndex) -> QModelIndex;\n"
);
out.push_str(concat!(
" #[cxx_name = \"dataChanged\"]\n",
" #[inherit]\n",
));
let _ = writeln!(
out,
" fn data_changed(self: Pin<&mut {q}>, top_left: &QModelIndex, bottom_right: &QModelIndex, roles: &QList_i32);"
);
out.push_str(" }\n}\n\nuse std::pin::Pin;\n\n");
for (i, r) in self.roles.iter().enumerate() {
let _ = writeln!(
out,
"const ROLE_{}: i32 = {:#06X};",
r.name().to_uppercase(),
0x0100 + i
);
}
out.push('\n');
let _ = writeln!(out, "pub struct {rust} {{");
let _ = writeln!(out, " pub {}: Vec<{}>,", self.items_name, self.item_type);
for (name, kind) in &self.qproperties {
let _ = writeln!(out, " pub {}: {},", name, kind.rust_ty());
}
out.push_str("}\n\n");
if !self.custom_default {
let _ = writeln!(
out,
"impl Default for {rust} {{\n fn default() -> Self {{\n Self {{"
);
let _ = writeln!(out, " {}: {},", self.items_name, self.items_init);
for (name, _) in &self.qproperties {
let _ = writeln!(out, " {name}: Default::default(),");
}
out.push_str(" }\n }\n}\n\n");
}
let _ = writeln!(out, "impl qobject::{q} {{");
let _ = writeln!(
out,
" fn row_count(&self, _parent: &cxx_qt_lib::QModelIndex) -> i32 {{\n self.{}.len() as i32\n }}\n",
self.items_name
);
out.push_str(" fn data(&self, index: &cxx_qt_lib::QModelIndex, role: i32) -> cxx_qt_lib::QVariant {\n let row = index.row() as usize;\n");
let _ = writeln!(
out,
" let Some(item) = self.{}.get(row) else {{",
self.items_name
);
out.push_str(concat!(
" return cxx_qt_lib::QVariant::default();\n",
" };\n",
" match role {\n",
));
for r in &self.roles {
let _ = writeln!(
out,
" ROLE_{} => {},",
r.name().to_uppercase(),
r.value_expr()
);
}
out.push_str(" _ => cxx_qt_lib::QVariant::default(),\n }\n }\n\n");
out.push_str(concat!(
" fn role_names(&self) -> cxx_qt_lib::QHash<cxx_qt_lib::QHashPair_i32_QByteArray> {\n",
" let mut hash = cxx_qt_lib::QHash::<cxx_qt_lib::QHashPair_i32_QByteArray>::default();\n",
));
for r in &self.roles {
let _ = writeln!(
out,
" hash.insert_clone(&ROLE_{}, &cxx_qt_lib::QByteArray::from(\"{}\"));",
r.name().to_uppercase(),
camel(r.name())
);
}
out.push_str(" hash\n }\n\n");
if let Some(cxx) = &self.snapshot_map {
let _ = writeln!(
out,
" fn {}(&self, index: i32) -> cxx_qt_lib::QMap<cxx_qt_lib::QMapPair_QString_QVariant> {{",
snake(cxx)
);
out.push_str(" let mut map = cxx_qt_lib::QMap::<cxx_qt_lib::QMapPair_QString_QVariant>::default();\n");
let _ = writeln!(
out,
" let Some(item) = self.{}.get(index as usize) else {{",
self.items_name
);
out.push_str(" return map;\n };\n");
for r in &self.roles {
let _ = writeln!(
out,
" map.insert(cxx_qt_lib::QString::from(\"{}\"), {});",
camel(r.name()),
r.value_expr()
);
}
out.push_str(" map\n }\n\n");
}
out.push_str(concat!(
" pub fn notify_row_changed(mut self: Pin<&mut Self>, row: i32) {\n",
" let idx = self.as_ref().model_index(row, 0, &cxx_qt_lib::QModelIndex::default());\n",
" let roles = cxx_qt_lib::QList::<i32>::default();\n",
" self.as_mut().data_changed(&idx, &idx, &roles);\n",
" }\n",
"}\n",
));
out
}
}
fn assert_snake(name: &str) {
let ok = !name.is_empty()
&& !name.starts_with(|c: char| c.is_ascii_digit())
&& name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
assert!(
ok,
"kushi: name '{name}' must be snake_case (it becomes rust idents and the camelCase cxx_name)"
);
}
fn camel(s: &str) -> String {
let mut parts = s.split('_');
let mut out = parts.next().unwrap_or_default().to_string();
for part in parts {
out.push_str(&capitalize(part));
}
out
}
fn pascal(s: &str) -> String {
s.split('_').map(capitalize).collect()
}
fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
fn snake(s: &str) -> String {
let mut out = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
out.push('_');
}
out.extend(c.to_lowercase());
} else {
out.push(c);
}
}
out
}