1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
//! Push-button widget with click callbacks.
//!
//! Wraps [`QPushButton`](https://doc.qt.io/qt-6/qpushbutton.html).
use cxx::let_cxx_string;
use crate::ffi;
use crate::signal;
use crate::widget::AsWidget;
/// A pressable button with an optional click callback.
///
/// `PushButton` uses a **builder pattern**: call [`PushButton::new`] to
/// obtain a [`Builder`], chain `.on_clicked(f)`, `.parent(w)`, then call
/// `.build()` or `.show()`.
///
/// # Signals
///
/// | Method | Qt signal | When |
/// |---|---|---|
/// | [`Builder::on_clicked`] | `QPushButton::clicked` | Button is pressed and released |
///
/// # Memory safety
///
/// Signal closures are stored on the heap and passed to C++ as `u64`
/// tokens. On [`Drop`]:
///
/// - **No Qt parent:** closures are reclaimed, then the C++ object is
/// deleted. Safe — no more signals can fire after deletion.
/// - **Has Qt parent:** all signals are disconnected first, then closures
/// are reclaimed. The C++ object is left alone (Qt deletes it).
///
/// # Example
///
/// ```no_run
/// use qtrs::PushButton;
///
/// let btn = PushButton::new("Click me")
/// .on_clicked(|| println!("clicked!"))
/// .build();
/// ```
pub struct PushButton {
ptr: *mut ffi::QPushButton,
has_parent: bool,
#[allow(dead_code)]
text: String,
signal_handles: Vec<crate::signal::SignalHandle>,
}
impl PushButton {
/// Start building a new `QPushButton`.
///
/// Returns a [`Builder`]. Chain configuration, then call `.build()`.
pub fn new(text: impl Into<String>) -> Builder {
Builder::new(text.into())
}
/// Get the current button text.
pub fn text(&self) -> &str {
&self.text
}
/// Update the button text at runtime.
///
/// This calls
/// [`QPushButton::setText`](https://doc.qt.io/qt-6/qabstractbutton.html#text-prop).
pub fn set_text(&mut self, text: impl Into<String>) {
debug_assert!(!self.ptr.is_null(), "PushButton::set_text on null pointer");
self.text = text.into();
let_cxx_string!(c_text = &self.text);
unsafe {
ffi::QPushButton_setText(self.ptr, &c_text);
}
}
/// Show this button.
pub fn show(&self) {
debug_assert!(!self.ptr.is_null());
unsafe { ffi::QPushButton_show(self.ptr) };
}
/// Set the button icon from a file path.
pub fn set_icon(&self, path: &str) {
debug_assert!(!self.ptr.is_null());
let_cxx_string!(c = path);
unsafe { ffi::QPushButton_setIcon(self.ptr, &c); }
}
/// Set whether the button has a flat appearance.
pub fn set_flat(&self, flat: bool) {
debug_assert!(!self.ptr.is_null());
unsafe { ffi::QPushButton_setFlat(self.ptr, flat); }
}
/// Returns `true` if the button is flat.
pub fn is_flat(&self) -> bool {
debug_assert!(!self.ptr.is_null());
unsafe { ffi::QPushButton_isFlat(self.ptr) }
}
/// Set this as the default button (activated by Enter key).
pub fn set_default(&self, def: bool) {
debug_assert!(!self.ptr.is_null());
unsafe { ffi::QPushButton_setDefault(self.ptr, def); }
}
/// Set auto-default behavior (activated by Enter when focused).
pub fn set_auto_default(&self, def: bool) {
debug_assert!(!self.ptr.is_null());
unsafe { ffi::QPushButton_setAutoDefault(self.ptr, def); }
}
/// Connect a click callback to an already-existing button.
///
/// This is the runtime equivalent of
/// [`Builder::on_clicked`] — useful when the button was loaded
/// from a `.ui` file rather than built in Rust.
pub fn connect_clicked<F: Fn()>(&mut self, f: F) {
debug_assert!(!self.ptr.is_null());
let handle = signal::leak_void(f);
unsafe { ffi::QPushButton_onClicked(self.ptr, handle.token); }
self.signal_handles.push(handle);
}
/// Wrap an existing `QPushButton*` obtained via `findChild`.
/// The button already has a Qt parent, so Drop will not delete it.
#[doc(hidden)]
pub(crate) fn from_raw(ptr: *mut ffi::QPushButton, text: &str) -> Self {
debug_assert!(!ptr.is_null());
Self { ptr, has_parent: true, text: text.to_string(), signal_handles: Vec::new() }
}
}
impl AsWidget for PushButton {
fn widget_ptr(&self) -> *mut ffi::QWidget {
debug_assert!(!self.ptr.is_null(), "PushButton::widget_ptr on null pointer");
unsafe { ffi::toQWidget_QPushButton(self.ptr) }
}
fn set_has_parent(&mut self) {
self.has_parent = true;
}
}
impl Drop for PushButton {
fn drop(&mut self) {
if self.ptr.is_null() { return; }
if self.has_parent {
// Disconnect all Qt signals so no more callbacks fire,
// then reclaim closures safely (no use-after-free).
unsafe { ffi::QWidget_disconnectAll(self.ptr as *mut _); }
for h in self.signal_handles.drain(..) {
unsafe { h.reclaim(); }
}
} else {
for h in self.signal_handles.drain(..) {
unsafe { h.reclaim(); }
}
unsafe { ffi::QPushButton_delete(self.ptr) };
}
self.ptr = std::ptr::null_mut();
}
}
// ============================================================
// Builder
// ============================================================
/// Builder for [`PushButton`].
///
/// Collects text, parent, and signal callbacks, then creates the C++
/// `QPushButton` (and connects signals) in [`build`](Self::build).
pub struct Builder {
text: String,
icon: Option<String>,
flat: Option<bool>,
default: Option<bool>,
on_clicked: Option<Box<dyn Fn()>>,
parent: Option<*mut ffi::QWidget>,
}
impl Builder {
fn new(text: String) -> Self {
Self { text, icon: None, flat: None, default: None, on_clicked: None, parent: None }
}
/// Set the button icon from a file path.
pub fn icon(mut self, path: impl Into<String>) -> Self { self.icon = Some(path.into()); self }
/// Set whether the button has a flat appearance.
pub fn flat(mut self, flat: bool) -> Self { self.flat = Some(flat); self }
/// Make this the default button (activated by Enter).
pub fn default(mut self, def: bool) -> Self { self.default = Some(def); self }
/// Set the click callback.
///
/// The closure will be called **each time** the button is clicked.
/// It is stored on the heap and reclaimed when the button is dropped
/// (only if the button has no Qt parent — see the [memory safety]
/// note on [`PushButton`]).
pub fn on_clicked<F: Fn() + 'static>(mut self, f: F) -> Self {
self.on_clicked = Some(Box::new(f));
self
}
/// Set the parent widget.
///
/// The parent manages this button's C++ lifetime. Do not drop the
/// parent before the button.
pub fn parent(mut self, parent: &dyn AsWidget) -> Self {
self.parent = Some(parent.widget_ptr());
self
}
/// Create the C++ `QPushButton`, connect signals, and return the
/// Rust wrapper.
///
/// This is the terminal method of the builder pattern.
pub fn build(self) -> PushButton {
let_cxx_string!(c_text = &self.text);
let ptr = unsafe {
ffi::QPushButton_new(
&c_text,
self.parent.unwrap_or(std::ptr::null_mut()),
)
};
assert!(!ptr.is_null(), "QPushButton_new returned null");
let has_parent = self.parent.is_some();
let mut signal_handles = Vec::new();
// Apply builder settings
if let Some(ref path) = self.icon {
let_cxx_string!(c = path);
unsafe { ffi::QPushButton_setIcon(ptr, &c); }
}
if let Some(flat) = self.flat { unsafe { ffi::QPushButton_setFlat(ptr, flat); } }
if let Some(def) = self.default { unsafe { ffi::QPushButton_setDefault(ptr, def); } }
// Connect click signal if a callback was provided.
if let Some(cb) = self.on_clicked {
let handle = signal::leak_void(cb);
unsafe { ffi::QPushButton_onClicked(ptr, handle.token); }
signal_handles.push(handle);
}
PushButton {
ptr,
has_parent,
text: self.text,
signal_handles,
}
}
/// Build and immediately show the button.
pub fn show(self) -> PushButton {
let btn = self.build();
btn.show();
btn
}
}