i_slint_backend_qt/qt_widgets/
spinbox.rs1use crate::key_generated;
5use i_slint_core::{
6 input::{FocusEventResult, FocusReason, KeyEventType},
7 items::TextHorizontalAlignment,
8 platform::PointerEventButton,
9};
10
11use super::*;
12
13#[derive(Default, Copy, Clone, Debug, PartialEq)]
14#[repr(C)]
15struct NativeSpinBoxData {
16 active_controls: u32,
17 pressed: bool,
18}
19
20type IntArg = (i32,);
21
22#[repr(C)]
23#[derive(FieldOffsets, Default, SlintElement)]
24#[pin]
25pub struct NativeSpinBox {
26 pub enabled: Property<bool>,
27 pub has_focus: Property<bool>,
28 pub value: Property<i32>,
29 pub minimum: Property<i32>,
30 pub maximum: Property<i32>,
31 pub step_size: Property<i32>,
32 pub horizontal_alignment: Property<TextHorizontalAlignment>,
33 pub cached_rendering_data: CachedRenderingData,
34 pub edited: Callback<IntArg>,
35 data: Property<NativeSpinBoxData>,
36 widget_ptr: std::cell::Cell<SlintTypeErasedWidgetPtr>,
37 animation_tracker: Property<i32>,
38}
39
40cpp! {{
41void initQSpinBoxOptions(QStyleOptionSpinBox &option, bool pressed, bool enabled, int active_controls) {
42auto style = qApp->style();
43option.activeSubControls = QStyle::SC_None;
44option.subControls = QStyle::SC_SpinBoxEditField | QStyle::SC_SpinBoxUp | QStyle::SC_SpinBoxDown;
45if (style->styleHint(QStyle::SH_SpinBox_ButtonsInsideFrame, nullptr, nullptr))
46 option.subControls |= QStyle::SC_SpinBoxFrame;
47option.activeSubControls = {active_controls};
48if (enabled) {
49 option.state |= QStyle::State_Enabled;
50} else {
51 option.palette.setCurrentColorGroup(QPalette::Disabled);
52}
53if (pressed) {
54 option.state |= QStyle::State_Sunken | QStyle::State_MouseOver;
55}
56option.stepEnabled = QAbstractSpinBox::StepDownEnabled | QAbstractSpinBox::StepUpEnabled;
60option.frame = true;
61}
62}}
63
64impl Item for NativeSpinBox {
65 fn init(self: Pin<&Self>, _self_rc: &ItemRc) {
66 let animation_tracker_property_ptr = Self::FIELD_OFFSETS.animation_tracker.apply_pin(self);
67 self.widget_ptr.set(cpp! { unsafe [animation_tracker_property_ptr as "void*"] -> SlintTypeErasedWidgetPtr as "std::unique_ptr<SlintTypeErasedWidget>" {
68 return make_unique_animated_widget<QSpinBox>(animation_tracker_property_ptr);
69 }})
70 }
71
72 fn layout_info(
73 self: Pin<&Self>,
74 orientation: Orientation,
75 _window_adapter: &Rc<dyn WindowAdapter>,
76 _self_rc: &ItemRc,
77 ) -> LayoutInfo {
78 let data = self.data();
80 let active_controls = data.active_controls;
81 let pressed = data.pressed;
82 let enabled = self.enabled();
83 let widget: NonNull<()> = SlintTypeErasedWidgetPtr::qwidget_ptr(&self.widget_ptr);
84
85 let size = cpp!(unsafe [
86 active_controls as "int",
88 pressed as "bool",
89 enabled as "bool",
90 widget as "QWidget*"
91 ] -> qttypes::QSize as "QSize" {
92 ensure_initialized();
93 auto style = qApp->style();
94
95 QStyleOptionSpinBox option;
96 initQSpinBoxOptions(option, pressed, enabled, active_controls);
97
98 QStyleOptionFrame frame;
99 frame.state = option.state;
100 frame.lineWidth = style->styleHint(QStyle::SH_SpinBox_ButtonsInsideFrame, &option, nullptr) ? 0
101 : style->pixelMetric(QStyle::PM_DefaultFrameWidth, &option, nullptr);
102 frame.midLineWidth = 0;
103 auto content = option.fontMetrics.boundingRect("0000");
104 const QSize margins(2 * 2, 2 * 1); auto line_edit_size = style->sizeFromContents(QStyle::CT_LineEdit, &frame, content.size() + margins, widget);
106 return style->sizeFromContents(QStyle::CT_SpinBox, &option, line_edit_size, widget);
107 });
108 match orientation {
109 Orientation::Horizontal => LayoutInfo {
110 min: size.width as f32,
111 preferred: size.width as f32,
112 stretch: 1.,
113 ..LayoutInfo::default()
114 },
115 Orientation::Vertical => LayoutInfo {
116 min: size.height as f32,
117 preferred: size.height as f32,
118 max: size.height as f32,
119 ..LayoutInfo::default()
120 },
121 }
122 }
123
124 fn input_event_filter_before_children(
125 self: Pin<&Self>,
126 _: &MouseEvent,
127 _window_adapter: &Rc<dyn WindowAdapter>,
128 _self_rc: &ItemRc,
129 ) -> InputEventFilterResult {
130 InputEventFilterResult::ForwardEvent
131 }
132
133 fn input_event(
134 self: Pin<&Self>,
135 event: &MouseEvent,
136 window_adapter: &Rc<dyn WindowAdapter>,
137 self_rc: &i_slint_core::items::ItemRc,
138 ) -> InputEventResult {
139 let size: qttypes::QSize = get_size!(self_rc);
140 let enabled = self.enabled();
141 let mut data = self.data();
142 let active_controls = data.active_controls;
143 let pressed = data.pressed;
144 let step_size = self.step_size();
145 let widget: NonNull<()> = SlintTypeErasedWidgetPtr::qwidget_ptr(&self.widget_ptr);
146
147 let pos = event
148 .position()
149 .map(|p| qttypes::QPoint { x: p.x as _, y: p.y as _ })
150 .unwrap_or_default();
151
152 let new_control = cpp!(unsafe [
153 pos as "QPoint",
154 size as "QSize",
155 enabled as "bool",
156 active_controls as "int",
157 pressed as "bool",
158 widget as "QWidget*"
159 ] -> u32 as "int" {
160 ensure_initialized();
161 auto style = qApp->style();
162
163 QStyleOptionSpinBox option;
164 option.rect = { QPoint{}, size };
165 initQSpinBoxOptions(option, pressed, enabled, active_controls);
166
167 return style->hitTestComplexControl(QStyle::CC_SpinBox, &option, pos, widget);
168 });
169 let changed = new_control != active_controls
170 || match event {
171 MouseEvent::Pressed { .. } => {
172 data.pressed = true;
173 true
174 }
175 MouseEvent::Exit => {
176 data.pressed = false;
177 true
178 }
179 MouseEvent::Released { button, .. } => {
180 data.pressed = false;
181 let left_button = *button == PointerEventButton::Left;
182 if new_control == cpp!(unsafe []->u32 as "int" { return QStyle::SC_SpinBoxUp;})
183 && enabled
184 && left_button
185 {
186 let v = self.value();
187 if v < self.maximum() {
188 let new_val = v + step_size;
189 self.value.set(new_val);
190 Self::FIELD_OFFSETS.edited.apply_pin(self).call(&(new_val,));
191 }
192 }
193 if new_control
194 == cpp!(unsafe []->u32 as "int" { return QStyle::SC_SpinBoxDown;})
195 && enabled
196 && left_button
197 {
198 let v = self.value();
199 if v > self.minimum() {
200 let new_val = v - step_size;
201 self.value.set(new_val);
202 Self::FIELD_OFFSETS.edited.apply_pin(self).call(&(new_val,));
203 }
204 }
205 true
206 }
207 MouseEvent::Moved { .. } => false,
208 MouseEvent::Wheel { delta_y, .. } => {
209 if *delta_y > 0. {
210 let v = self.value();
211 if v < self.maximum() {
212 let new_val = v + step_size;
213 self.value.set(new_val);
214 Self::FIELD_OFFSETS.edited.apply_pin(self).call(&(new_val,));
215 }
216 } else if *delta_y < 0. {
217 let v = self.value();
218 if v > self.minimum() {
219 let new_val = v - step_size;
220 self.value.set(new_val);
221 Self::FIELD_OFFSETS.edited.apply_pin(self).call(&(new_val,));
222 }
223 }
224
225 true
226 }
227 MouseEvent::DragMove(..) | MouseEvent::Drop(..) => false,
228 };
229 data.active_controls = new_control;
230 if changed {
231 self.data.set(data);
232 }
233
234 if let MouseEvent::Pressed { .. } = event {
235 if !self.has_focus() {
236 WindowInner::from_pub(window_adapter.window()).set_focus_item(
237 self_rc,
238 true,
239 FocusReason::PointerClick,
240 );
241 }
242 }
243 InputEventResult::EventAccepted
244 }
245
246 fn capture_key_event(
247 self: Pin<&Self>,
248 _event: &KeyEvent,
249 _window_adapter: &Rc<dyn WindowAdapter>,
250 _self_rc: &ItemRc,
251 ) -> KeyEventResult {
252 KeyEventResult::EventIgnored
253 }
254
255 fn key_event(
256 self: Pin<&Self>,
257 event: &KeyEvent,
258 _window_adapter: &Rc<dyn WindowAdapter>,
259 _self_rc: &ItemRc,
260 ) -> KeyEventResult {
261 if !self.enabled() || event.event_type != KeyEventType::KeyPressed {
262 return KeyEventResult::EventIgnored;
263 }
264 if event.text.starts_with(i_slint_core::input::key_codes::UpArrow)
265 && self.value() < self.maximum()
266 {
267 let new_val = self.value() + self.step_size();
268 self.value.set(new_val);
269 Self::FIELD_OFFSETS.edited.apply_pin(self).call(&(new_val,));
270 KeyEventResult::EventAccepted
271 } else if event.text.starts_with(i_slint_core::input::key_codes::DownArrow)
272 && self.value() > self.minimum()
273 {
274 let new_val = self.value() - self.step_size();
275 self.value.set(new_val);
276 Self::FIELD_OFFSETS.edited.apply_pin(self).call(&(new_val,));
277 KeyEventResult::EventAccepted
278 } else {
279 KeyEventResult::EventIgnored
280 }
281 }
282
283 fn focus_event(
284 self: Pin<&Self>,
285 event: &FocusEvent,
286 _window_adapter: &Rc<dyn WindowAdapter>,
287 _self_rc: &ItemRc,
288 ) -> FocusEventResult {
289 match event {
290 FocusEvent::FocusIn(_) => {
291 if self.enabled() {
292 self.has_focus.set(true);
293 }
294 }
295 FocusEvent::FocusOut(_) => {
296 self.has_focus.set(false);
297 }
298 }
299 FocusEventResult::FocusAccepted
300 }
301
302 fn_render! { this dpr size painter widget initial_state =>
303 let value: i32 = this.value();
304 let enabled = this.enabled();
305 let has_focus = this.has_focus();
306 let data = this.data();
307 let active_controls = data.active_controls;
308 let pressed = data.pressed;
309
310 let horizontal_alignment = match this.horizontal_alignment() {
311 TextHorizontalAlignment::Left => key_generated::Qt_AlignmentFlag_AlignLeft,
312 TextHorizontalAlignment::Center => key_generated::Qt_AlignmentFlag_AlignHCenter,
313 TextHorizontalAlignment::Right => key_generated::Qt_AlignmentFlag_AlignRight,
314 };
315
316 cpp!(unsafe [
317 painter as "QPainterPtr*",
318 widget as "QWidget*",
319 value as "int",
320 enabled as "bool",
321 has_focus as "bool",
322 size as "QSize",
323 active_controls as "int",
324 pressed as "bool",
325 dpr as "float",
326 initial_state as "int",
327 horizontal_alignment as "int"
328 ] {
329 auto style = qApp->style();
330 QStyleOptionSpinBox option;
331 option.styleObject = widget;
332 option.state |= QStyle::State(initial_state);
333 if (enabled && has_focus) {
334 option.state |= QStyle::State_HasFocus;
335 }
336 option.rect = QRect(QPoint(), size / dpr);
337 initQSpinBoxOptions(option, pressed, enabled, active_controls);
338 style->drawComplexControl(QStyle::CC_SpinBox, &option, painter->get(), widget);
339
340 static_cast<QAbstractSpinBox*>(widget)->setAlignment(Qt::AlignRight);
341 QStyleOptionFrame frame;
342 frame.state = option.state;
343 frame.palette = option.palette;
344 frame.lineWidth = style->styleHint(QStyle::SH_SpinBox_ButtonsInsideFrame, &option, widget) ? 0
345 : style->pixelMetric(QStyle::PM_DefaultFrameWidth, &option, widget);
346 frame.midLineWidth = 0;
347 frame.rect = style->subControlRect(QStyle::CC_SpinBox, &option, QStyle::SC_SpinBoxEditField, widget);
348 style->drawPrimitive(QStyle::PE_PanelLineEdit, &frame, painter->get(), widget);
349 QRect text_rect = qApp->style()->subElementRect(QStyle::SE_LineEditContents, &frame, widget);
350 text_rect.adjust(1, 2, 1, 2);
351 (*painter)->setPen(option.palette.color(QPalette::Text));
352 (*painter)->drawText(text_rect, QString::number(value), QTextOption(static_cast<Qt::AlignmentFlag>(horizontal_alignment)));
353 });
354 }
355
356 fn bounding_rect(
357 self: core::pin::Pin<&Self>,
358 _window_adapter: &Rc<dyn WindowAdapter>,
359 _self_rc: &ItemRc,
360 geometry: LogicalRect,
361 ) -> LogicalRect {
362 geometry
363 }
364
365 fn clips_children(self: core::pin::Pin<&Self>) -> bool {
366 false
367 }
368}
369
370impl ItemConsts for NativeSpinBox {
371 const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
372 Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection();
373}
374
375declare_item_vtable! {
376fn slint_get_NativeSpinBoxVTable() -> NativeSpinBoxVTable for NativeSpinBox
377}