i_slint_backend_qt/qt_widgets/
spinbox.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use 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}
56/*if (active_controls) {
57        option.state |= QStyle::State_MouseOver;
58    }*/
59option.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 value: i32 = self.value();
79        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            //value as "int",
87            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); // QLineEditPrivate::verticalMargin and QLineEditPrivate::horizontalMargin
105            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}