diff --git a/Cargo.lock b/Cargo.lock index 8b013fc8a..9cf792907 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1092,6 +1092,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -1269,6 +1279,21 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "span-inspector" +version = "0.7.2" +dependencies = [ + "cxx", + "cxx-qt", + "cxx-qt-build", + "cxx-qt-gen", + "cxx-qt-lib", + "cxx-qt-macro", + "prettyplease", + "proc-macro2", + "syn", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 90150e22f..51439d729 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ "tests/basic_cxx_only/rust", "tests/basic_cxx_qt/rust", - "tests/qt_types_standalone/rust", + "tests/qt_types_standalone/rust", "examples/span-inspector", ] resolver = "2" diff --git a/examples/span-inspector/Cargo.toml b/examples/span-inspector/Cargo.toml new file mode 100644 index 000000000..170092544 --- /dev/null +++ b/examples/span-inspector/Cargo.toml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company +# SPDX-FileContributor: Leon Matthes +# SPDX-FileContributor: Quentin Weber +# +# SPDX-License-Identifier: MIT OR Apache-2.0 + +[package] +name = "span-inspector" +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true +rust-version.workspace = true + +[dependencies] +cxx.workspace = true +cxx-qt.workspace = true +cxx-qt-lib = { workspace = true, features = ["full"] } +cxx-qt-macro = { workspace = true } +cxx-qt-gen = { workspace = true } +proc-macro2.workspace = true +prettyplease = { version = "0.2", features = ["verbatim"] } +syn.workspace=true + +[build-dependencies] +cxx-qt-build = { workspace = true, features = [ "link_qt_object_files" ] } + +[lints] +workspace = true diff --git a/examples/span-inspector/build.rs b/examples/span-inspector/build.rs new file mode 100644 index 000000000..ad615bf5d --- /dev/null +++ b/examples/span-inspector/build.rs @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Leon Matthes +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use cxx_qt_build::{CxxQtBuilder, QmlModule}; + +fn main() { + let qml_module: QmlModule<'static, &str, &str> = QmlModule { + uri: "com.kdab.cxx_qt.demo", + rust_files: &["src/inspector.rs"], + qml_files: &["qml/main.qml"], + ..Default::default() + }; + CxxQtBuilder::new() + // Link Qt's Network library + // - Qt Core is always linked + // - Qt Gui is linked by enabling the qt_gui Cargo feature of cxx-qt-lib. + // - Qt Qml is linked by enabling the qt_qml Cargo feature of cxx-qt-lib. + // - Qt Qml requires linking Qt Network on macOS + .qt_module("Network") + .qt_module("Quick") + .qml_module(qml_module) + .build(); +} diff --git a/examples/span-inspector/qml/main.qml b/examples/span-inspector/qml/main.qml new file mode 100644 index 000000000..201f711b4 --- /dev/null +++ b/examples/span-inspector/qml/main.qml @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Leon Matthes +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import QtQuick +import QtQuick.Controls +import QtQuick.Window +import QtQuick.Layouts + +import com.kdab.cxx_qt.demo 1.0 + +ApplicationWindow { + id: appWindow + color: palette.window + property color textColor: color.lightness < 128 * "black" , "white" + height: 480 + title: qsTr("Span Inspector") + visible: true + width: 640 + + SpanInspector { + id: inspector + } + + SplitView { + anchors.fill: parent + Item { + SplitView.preferredWidth: parent.width / 2 + TextArea { + SplitView.preferredWidth: parent.width / 2 + wrapMode: TextArea.Wrap + id: inputEdit + anchors.fill: parent + clip: true + color: appWindow.textColor + Component.onCompleted: { + inspector.input = textDocument + inspector.rebuildOutput(cursorPosition) + } + + onCursorPositionChanged: { + inspector.rebuildOutput(cursorPosition) + } + + onTextChanged: { + inspector.rebuildOutput(cursorPosition) + } + } + } + + ScrollView { + SplitView.preferredWidth: parent.width / 2 + TextEdit { + clip: true + color: appWindow.textColor + text: "Hello World" + readOnly: true; + Component.onCompleted: inspector.output = textDocument + } + } + } +} diff --git a/examples/span-inspector/src/inspector.rs b/examples/span-inspector/src/inspector.rs new file mode 100644 index 000000000..acb2dd825 --- /dev/null +++ b/examples/span-inspector/src/inspector.rs @@ -0,0 +1,299 @@ +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Leon Matthes +// SPDX-FileContributor: Quentin Weber +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use cxx_qt_gen::{write_rust, Parser}; +use proc_macro2::{Span, TokenStream, TokenTree}; +use std::{ + str::FromStr, + sync::{Arc, Mutex}, +}; +use syn::{parse2, ItemMod}; + +#[cxx_qt::bridge] +mod qobject { + unsafe extern "C++" { + include!("cxx-qt-lib/qstring.h"); + type QString = cxx_qt_lib::QString; + } + + unsafe extern "C++Qt" { + include!(); + #[qobject] + type QTextDocument; + + #[cxx_name = "toPlainText"] + fn to_plain_text(self: &QTextDocument) -> QString; + + #[cxx_name = "setPlainText"] + fn set_plain_text(self: Pin<&mut QTextDocument>, content: &QString); + + #[cxx_name = "setHtml"] + fn set_html(self: Pin<&mut QTextDocument>, content: &QString); + + #[qsignal] + #[cxx_name = "contentsChanged"] + fn contents_changed(self: Pin<&mut QTextDocument>); + } + + unsafe extern "C++Qt" { + include!(); + #[qobject] + type QQuickTextDocument; + + #[cxx_name = "textDocument"] + fn text_document(self: &QQuickTextDocument) -> *mut QTextDocument; + } + + extern "RustQt" { + #[qobject] + #[qml_element] + #[qproperty(*mut QQuickTextDocument, input, READ, NOTIFY, WRITE=set_input)] + #[qproperty(*mut QQuickTextDocument, output)] + type SpanInspector = super::SpanInspectorRust; + + unsafe fn set_input(self: Pin<&mut SpanInspector>, input: *mut QQuickTextDocument); + + #[qinvokable] + #[cxx_name = "rebuildOutput"] + fn rebuild_output(self: Pin<&mut SpanInspector>, pos: i32); + } + + impl UniquePtr {} + impl cxx_qt::Threading for SpanInspector {} +} + +use cxx_qt::{CxxQtType, Threading}; +use qobject::{QQuickTextDocument, QString, QTextDocument}; +use std::{pin::Pin, ptr}; + +pub struct SpanInspectorRust { + cursor_position: Arc>, + input: *mut QQuickTextDocument, + output: *mut QQuickTextDocument, +} + +impl Default for SpanInspectorRust { + fn default() -> Self { + Self { + cursor_position: Arc::new(Mutex::new(3)), + input: ptr::null_mut(), + output: ptr::null_mut(), + } + } +} + +impl qobject::SpanInspector { + unsafe fn output_document(&self) -> Pin<&mut QTextDocument> { + if self.output.is_null() { + panic!("Output document must be set!"); + } + let output = unsafe { &*self.output }; + unsafe { Pin::new_unchecked(&mut *output.text_document()) } + } + + fn set_input(mut self: Pin<&mut Self>, input: *mut QQuickTextDocument) { + self.as_mut().rust_mut().input = input; + self.as_mut().input_changed(); + } + + fn rebuild_output(self: Pin<&mut Self>, pos: i32) { + let mut cursor_position = match self.cursor_position.lock() { + Ok(mutex) => mutex, + Err(poison_error) => poison_error.into_inner(), + }; + *cursor_position = pos; + + let input = unsafe { Pin::new_unchecked(&mut *self.input) }; + let cursor_position = self.cursor_position.clone(); + let qt_thread = self.qt_thread(); + qt_thread + .queue(|this| { + unsafe { this.output_document() } + .set_html(&QString::from(String::from("expanding..."))) + }) + .ok(); + + let text = unsafe { Pin::new_unchecked(&mut *input.text_document()) } + .to_plain_text() + .to_string(); + + std::thread::spawn(move || { + let cursor_position = match cursor_position.lock() { + Ok(mutex) => mutex, + Err(poison_error) => poison_error.into_inner(), + }; + + let output = match Self::expand(&text, *cursor_position as usize) { + Ok((expanded, span_data)) => { + let Ok(file) = syn::parse_file(expanded.as_str()) + .map_err(|err| eprintln!("Parsing error: {err}")) + else { + return; + }; + Self::build_html(&prettyplease::unparse(&file), span_data) + } + Err(error) => Self::build_error_html(&error), + }; + qt_thread + .queue(move |this| { + unsafe { this.output_document() }.set_html(&QString::from(output)) + }) + .ok(); + }); + } + + // Expand input code as #[cxxqt_qt::bridge] would do + fn expand(input: &str, cursor_position: usize) -> Result<(String, Vec<(bool, bool)>), String> { + let input_stream: TokenStream = TokenStream::from_str(input).map_err(|e| e.to_string())?; + + let mut module: ItemMod = parse2(input_stream.clone()).map_err(|e| e.to_string())?; + + let args = TokenStream::default(); + let args_input = format!("#[cxx_qt::bridge({args})] mod dummy;"); + let attrs = syn::parse_str::(&args_input) + .map_err(|e| e.to_string())? + .attrs; + module.attrs = attrs.into_iter().chain(module.attrs).collect(); + + let output_stream = Self::extract_and_generate(module); + + let target_span: Option = Self::flatten_tokenstream(input_stream) + .into_iter() + .find(|token| { + let range = token.span().byte_range(); + range.start <= cursor_position && range.end >= cursor_position + }) + .map(|token| token.span()); + + let span_data: Vec<(bool, bool)> = Self::flatten_tokenstream(output_stream.clone()) + .into_iter() + // prettyplease may insert extra tokens. + // This filter simply ignores them. + .filter(|token| { + let string = token.to_string(); + !matches!(string.as_str(), "," | "}" | "{") + }) + .map(|token| { + ( + target_span + .map(|s| s.byte_range().eq(token.span().byte_range())) + .unwrap_or_else(|| false), + token.span().byte_range().start == 0, + ) + }) + .collect(); + + Ok((format!("{}", output_stream), span_data)) + } + + // Take the module and C++ namespace and generate the rust code + fn extract_and_generate(module: ItemMod) -> TokenStream { + Parser::from(module) + .and_then(|parser| cxx_qt_gen::GeneratedRustBlocks::from(&parser)) + .map(|generated_rust| write_rust(&generated_rust, None)) + .unwrap_or_else(|err| err.to_compile_error()) + } + + fn build_error_html(input: &str) -> String { + let style = String::from( + " + + ", + ); + format!("{style}

{input}

") + } + + fn flatten_tokenstream(stream: TokenStream) -> Vec { + let mut output: Vec = vec![]; + for token in stream { + match token { + TokenTree::Group(group) => { + output.extend(Self::flatten_tokenstream(group.stream())); + } + other => { + output.push(other); + } + } + } + output + } + + fn build_html(input: &str, span_data: Vec<(bool, bool)>) -> String { + let flat_tokenstream = Self::flatten_tokenstream(TokenStream::from_str(input).unwrap()); + let mut highlighted_string = input.to_string(); + + let mut token_position = span_data.len(); + for token in flat_tokenstream.into_iter().rev() { + // prettyplease may insert extra tokens. + // This `if` statement simply ignores them. + let token_string = token.to_string(); + if !matches!(token_string.as_str(), "," | "{" | "}") { + token_position -= 1; + } + if span_data.get(token_position).unwrap().0 { + highlighted_string.replace_range( + token.span().byte_range(), + format!( + "{}", + Self::sanitize(&token.to_string()) + ) + .as_str(), + ); + } else if span_data.get(token_position).unwrap().1 { + highlighted_string.replace_range( + token.span().byte_range(), + format!( + "{}", + Self::sanitize(&token.to_string()) + ) + .as_str(), + ); + } else { + highlighted_string.replace_range( + token.span().byte_range(), + Self::sanitize(&token.to_string()).as_str(), + ); + } + } + + let style = String::from( + " + + ", + ); + + format!("{style}
{highlighted_string}
") + } + + fn sanitize(input: &str) -> String { + input + .chars() + .map(|char| match char { + '>' => ">".to_string(), + '<' => "<".to_string(), + '&' => "&".to_string(), + '"' => """.to_string(), + _ => char.to_string(), + }) + .collect() + } +} diff --git a/examples/span-inspector/src/main.rs b/examples/span-inspector/src/main.rs new file mode 100644 index 000000000..e007c4090 --- /dev/null +++ b/examples/span-inspector/src/main.rs @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Be Wilson +// SPDX-FileContributor: Andrew Hayzen +// SPDX-FileContributor: Gerhard de Clercq +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +/// A module for our Rust defined QObject +pub mod inspector; + +use cxx_qt::casting::Upcast; +use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QQmlEngine, QUrl}; +use std::pin::Pin; + +fn main() { + // Create the application and engine + let mut app = QGuiApplication::new(); + let mut engine = QQmlApplicationEngine::new(); + + // Load the QML path into the engine + if let Some(engine) = engine.as_mut() { + engine.load(&QUrl::from("qrc:/qt/qml/com/kdab/cxx_qt/demo/qml/main.qml")); + } + + if let Some(engine) = engine.as_mut() { + let engine: Pin<&mut QQmlEngine> = engine.upcast_pin(); + // Listen to a signal from the QML Engine + engine + .on_quit(|_| { + println!("QML Quit!"); + }) + .release(); + } + + // Start the app + if let Some(app) = app.as_mut() { + app.exec(); + } +}