qtbridge_runtime/qapp.rs
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only
3
4use std::cell::RefCell;
5use std::rc::Rc;
6
7use cxx::UniquePtr;
8use cxx_qt::casting::Upcast;
9use cxx_qt_lib::QObjectMutPtr;
10use cxx_qt_lib::QQmlEngine;
11use qtbridge_type_lib::{QGuiApplication, QQmlApplicationEngine, QString, QVariant, QVariantMap};
12use crate::qmlelement::QmlElement;
13use crate::qobjectholder::QObjectHolder;
14
15/// Entry point for a QML application.
16///
17/// Wraps the Qt application and QML engine. Configure it with the builder
18/// methods and call [`run`](QApp::run) to start the event loop.
19///
20/// # Example
21///
22/// A minimal “Hello World” application without a Rust backend:
23///
24/// ```rust
25///# use qtbridge_runtime::QApp;
26/// QApp::new()
27/// .load_qml(br#"
28/// import QtQuick
29/// import QtQuick.Controls
30/// Text {
31/// text: "Hello Rust!"
32///# Component.onCompleted: closeTimer.start()
33///# Timer {
34///# id: closeTimer
35///# interval: 1
36///# onTriggered: Qt.quit()
37///# }
38/// }"#)
39/// .run();
40/// ```
41pub struct QApp {
42 engine: UniquePtr<QQmlApplicationEngine>,
43 #[allow(dead_code)]
44 app: UniquePtr<QGuiApplication>,
45 // create QVariant at load time to avoid storing raw QObject pointers.
46 initial_properties: Vec<(QString, Box<dyn FnOnce() -> QVariant>)>,
47}
48
49impl Drop for QApp {
50 fn drop(&mut self) {
51 // Drop the engine first, releasing every JS wrapper: the final
52 // collect then frees all objects Rust no longer holds, so their
53 // Drop runs here instead of leaking at thread exit.
54 self.engine = UniquePtr::null();
55 crate::registry::collect_garbage();
56 }
57}
58
59// Not Default: New needs to be in main thread. Once per process.
60#[allow(clippy::new_without_default)]
61impl QApp {
62 /// Creates the Qt application and QML engine.
63 ///
64 /// Must be called before any QML or GUI functionality is used.
65 pub fn new() -> Self {
66 let app = QGuiApplication::new();
67 crate::qmlprivate::call_qml_register_callbacks();
68 let mut engine = QQmlApplicationEngine::new();
69 // Clean up the registry in sync with the QML
70 // garbage collection (see `registry::install_gc_sentinel`).
71 crate::registry::install_gc_sentinel(engine.pin_mut());
72 Self {
73 engine,
74 app,
75 initial_properties: Vec::new(),
76 }
77 }
78
79 /// Enters the Qt main event loop.
80 ///
81 /// Blocks until the application exits and returns the exit code.
82 /// Usually the last call in `main`.
83 pub fn run(&mut self) -> i32 {
84 self.app.pin_mut().exec()
85 }
86
87 /// Queues an initial property to be set on the root QML object.
88 ///
89 /// Properties are applied when [`load_qml`](QApp::load_qml) or
90 /// [`load_qml_from_file`](QApp::load_qml_from_file) is called.
91 /// Call multiple times to set several properties.
92 ///
93 /// # Example
94 ///
95 /// ```rust
96 ///# use qtbridge_runtime::QApp;
97 /// let prop = 42;
98 ///
99 /// QApp::new()
100 /// .set_initial_property("answer", &prop)
101 /// .load_qml(br#"
102 /// import QtQuick
103 /// import QtQuick.Controls
104 /// ApplicationWindow {
105 /// required property var answer
106 ///# Component.onCompleted: closeTimer.start()
107 ///# Timer {
108 ///# id: closeTimer
109 ///# interval: 1
110 ///# onTriggered: Qt.quit()
111 ///# }
112 /// }"#)
113 /// .run();
114 /// ```
115 pub fn set_initial_property(&mut self, id: &str, value: impl Into<QVariant>) -> &mut Self {
116 let variant = value.into();
117 self.initial_properties.push((QString::from(id), Box::new( move || {
118 variant
119 })));
120 self
121 }
122
123 /// Sets a `#[qobject]` instance as initial property on the root QML
124 /// object, attaching a `QObject` to it first if none exists.
125 ///
126 /// Must be called before [`load_qml`](QApp::load_qml) or
127 /// [`load_qml_from_file`](QApp::load_qml_from_file).
128 /// Call multiple times to set several objects.
129 ///
130 /// # Example
131 ///
132 /// ```rust
133 ///# use std::cell::RefCell;
134 ///# use std::rc::Rc;
135 ///# use qtbridge::{QApp, qobject};
136 /// #[derive(Default)]
137 /// pub struct Backend {
138 /// }
139 /// #[qobject]
140 /// impl Backend {
141 /// }
142 ///
143 /// let backend = Rc::new(RefCell::new(Backend::default()));
144 ///
145 /// QApp::new()
146 /// .set_initial_object("backend", backend)
147 /// .load_qml(br#"
148 /// import QtQuick
149 /// Item {
150 /// required property var backend
151 ///# Component.onCompleted: closeTimer.start()
152 ///# Timer {
153 ///# id: closeTimer
154 ///# interval: 1
155 ///# onTriggered: Qt.quit()
156 ///# }
157 /// }"#)
158 /// .run();
159 /// ```
160 pub fn set_initial_object<T: QObjectHolder>(
161 &mut self, id: &str, object: Rc<RefCell<T>>,
162 ) -> &mut Self {
163 self.initial_properties.push((QString::from(id), Box::new( move || {
164 let ptr = T::rc_ref_cell_to_qobject(&object).cast_mut();
165 let ptr_wrap = unsafe { QObjectMutPtr::from_raw(ptr.cast()) };
166 (&ptr_wrap).into()
167 })));
168 self
169 }
170
171 /// Loads QML source from an in-memory byte slice.
172 ///
173 /// Applies any properties queued with [`set_initial_property`](QApp::set_initial_property)
174 /// before loading.
175 pub fn load_qml(&mut self, code: &[u8]) -> &mut Self {
176 if !self.initial_properties.is_empty() {
177 let mut initial_properties_resolved = QVariantMap::default();
178 for (id, resolve) in std::mem::take(&mut self.initial_properties) {
179 initial_properties_resolved.insert(id, resolve());
180 }
181 self.engine.pin_mut().set_initial_properties(&initial_properties_resolved);
182 }
183 self.engine.pin_mut().load_data(&code.into(), &Default::default());
184 self
185 }
186
187 /// Loads the entry-point QML file by URL.
188 ///
189 /// Use this instead of [`load_qml`](QApp::load_qml) when the QML is
190 /// embedded in a Qt resource (`qrc:`) or accessible as a file path.
191 /// Accepts URLs such as `"qrc:/qt/qml/MyApp/Main.qml"` or
192 /// `"file:///path/to/main.qml"`.
193 ///
194 /// Import paths for any modules the file uses must be registered with
195 /// [`add_import_path`](QApp::add_import_path) before this call.
196 pub fn load_qml_from_file(&mut self, url: &str) -> &mut Self {
197 if !self.initial_properties.is_empty() {
198 let mut initial_properties_resolved = QVariantMap::default();
199 for (id, resolve) in std::mem::take(&mut self.initial_properties) {
200 initial_properties_resolved.insert(id, resolve());
201 }
202 self.engine.pin_mut().set_initial_properties(&initial_properties_resolved);
203 }
204 self.engine.pin_mut().load(&url.into());
205 self
206 }
207
208 /// Adds a directory to the QML engine's module import search path.
209 ///
210 /// Call before [`load_qml_from_file`](QApp::load_qml_from_file) when the
211 /// loaded QML imports modules from a directory the engine would not
212 /// otherwise find. Accepts both URLs and file-system paths.
213 pub fn add_import_path(&mut self, path: &str) -> &mut Self {
214 self.engine.pin_mut().add_import_path(&path.into());
215 self
216 }
217
218 /// Registers `T` with the QML type system, making it instantiable from QML.
219 ///
220 /// ```rust
221 ///# use qtbridge::{QApp, qobject};
222 /// #[derive(Default)]
223 /// pub struct Backend {
224 /// }
225 /// #[qobject]
226 /// impl Backend {
227 /// }
228 ///
229 /// QApp::new()
230 /// .register::<Backend>()
231 /// .load_qml(br#"
232 /// import QtQuick
233 /// import QtQuick.Controls
234 ///# import qtbridge_runtime
235 /// ApplicationWindow {
236 /// Backend {}
237 ///# Component.onCompleted: closeTimer.start()
238 ///# Timer {
239 ///# id: closeTimer
240 ///# interval: 1
241 ///# onTriggered: Qt.quit()
242 ///# }
243 /// }"#)
244 /// .run();
245 /// ```
246 pub fn register<T: QmlElement>(&mut self) -> &mut Self {
247 T::register();
248 self
249 }
250
251 /// Sets the application name reported to the OS.
252 pub fn application_name(&mut self, name: &str) -> &mut Self {
253 self.app.pin_mut().set_application_name(&name.into());
254 self
255 }
256
257 #[doc(hidden)]
258 pub fn qml_engine(&mut self) -> std::pin::Pin<&mut QQmlEngine> {
259 self.engine.pin_mut().upcast_pin()
260 }
261}