i_slint_backend_qt/qt_widgets/
slider.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 i_slint_core::{
5    input::{key_codes, FocusEventResult, FocusReason, KeyEventType},
6    items::PointerEventButton,
7};
8
9use super::*;
10
11#[derive(Default, Copy, Clone, Debug, PartialEq)]
12#[repr(C)]
13// Also used by the NativeScrollView
14pub(super) struct NativeSliderData {
15    pub active_controls: u32,
16    /// For sliders, this is a bool, For scroll area: 1 == horizontal, 2 == vertical
17    pub pressed: u8,
18    pub pressed_x: f32,
19    pub pressed_val: f32,
20}
21
22type FloatArg = (f32,);
23
24#[repr(C)]
25#[derive(FieldOffsets, Default, SlintElement)]
26#[pin]
27pub struct NativeSlider {
28    pub orientation: Property<Orientation>,
29    pub enabled: Property<bool>,
30    pub has_focus: Property<bool>,
31    pub value: Property<f32>,
32    pub minimum: Property<f32>,
33    pub maximum: Property<f32>,
34    pub step: Property<f32>,
35    pub cached_rendering_data: CachedRenderingData,
36    data: Property<NativeSliderData>,
37    pub changed: Callback<FloatArg>,
38    pub released: Callback<FloatArg>,
39    widget_ptr: std::cell::Cell<SlintTypeErasedWidgetPtr>,
40    animation_tracker: Property<i32>,
41}
42
43cpp! {{
44void initQSliderOptions(QStyleOptionSlider &option, bool pressed, bool enabled, int active_controls, int minimum, int maximum, int value, bool vertical) {
45    option.subControls = QStyle::SC_SliderGroove | QStyle::SC_SliderHandle;
46    option.activeSubControls = { active_controls };
47    if (vertical) {
48        option.orientation = Qt::Vertical;
49    } else {
50        option.orientation = Qt::Horizontal;
51        option.state |= QStyle::State_Horizontal;
52    }
53    option.maximum = maximum;
54    option.minimum = minimum;
55    option.sliderPosition = value;
56    option.sliderValue = value;
57    if (enabled) {
58        option.state |= QStyle::State_Enabled;
59    } else {
60        option.palette.setCurrentColorGroup(QPalette::Disabled);
61    }
62    if (pressed) {
63        option.state |= QStyle::State_Sunken | QStyle::State_MouseOver;
64    }
65}
66}}
67
68impl Item for NativeSlider {
69    fn init(self: Pin<&Self>, _self_rc: &ItemRc) {
70        let animation_tracker_property_ptr = Self::FIELD_OFFSETS.animation_tracker.apply_pin(self);
71        self.widget_ptr.set(cpp! { unsafe [animation_tracker_property_ptr as "void*"] -> SlintTypeErasedWidgetPtr as "std::unique_ptr<SlintTypeErasedWidget>" {
72            return make_unique_animated_widget<QSlider>(animation_tracker_property_ptr);
73        }})
74    }
75
76    fn layout_info(
77        self: Pin<&Self>,
78        orientation: Orientation,
79        _window_adapter: &Rc<dyn WindowAdapter>,
80        _self_rc: &ItemRc,
81    ) -> LayoutInfo {
82        let enabled = self.enabled();
83        // Slint slider supports floating point ranges, while Qt uses integer. To support (0..1) ranges
84        // of values, scale up a little, before truncating to integer values.
85        let value = (self.value() * 1024.0) as i32;
86        let min = (self.minimum() * 1024.0) as i32;
87        let max = (self.maximum() * 1024.0) as i32;
88        let data = self.data();
89        let active_controls = data.active_controls;
90        let pressed = data.pressed;
91        let vertical = self.orientation() == Orientation::Vertical;
92        let widget: NonNull<()> = SlintTypeErasedWidgetPtr::qwidget_ptr(&self.widget_ptr);
93
94        let size = cpp!(unsafe [
95            enabled as "bool",
96            value as "int",
97            min as "int",
98            max as "int",
99            active_controls as "int",
100            pressed as "bool",
101            vertical as "bool",
102            widget as "QWidget*"
103        ] -> qttypes::QSize as "QSize" {
104            ensure_initialized();
105            QStyleOptionSlider option;
106            initQSliderOptions(option, pressed, enabled, active_controls, min, max, value, vertical);
107            auto style = qApp->style();
108            auto thick = style->pixelMetric(QStyle::PM_SliderThickness, &option, widget);
109            return style->sizeFromContents(QStyle::CT_Slider, &option, QSize(0, thick), widget);
110        });
111        let (width, height) = (size.width as f32, size.height as f32);
112        match orientation {
113            Orientation::Horizontal => {
114                if !vertical {
115                    LayoutInfo {
116                        min: width,
117                        preferred: width,
118                        stretch: 1.,
119                        ..LayoutInfo::default()
120                    }
121                } else {
122                    LayoutInfo {
123                        min: height,
124                        preferred: height,
125                        max: height,
126                        ..LayoutInfo::default()
127                    }
128                }
129            }
130            Orientation::Vertical => {
131                if !vertical {
132                    LayoutInfo {
133                        min: height,
134                        preferred: height,
135                        max: height,
136                        ..LayoutInfo::default()
137                    }
138                } else {
139                    LayoutInfo {
140                        min: width,
141                        preferred: width,
142                        stretch: 1.,
143                        ..LayoutInfo::default()
144                    }
145                }
146            }
147        }
148    }
149
150    fn input_event_filter_before_children(
151        self: Pin<&Self>,
152        _: &MouseEvent,
153        _window_adapter: &Rc<dyn WindowAdapter>,
154        _self_rc: &ItemRc,
155    ) -> InputEventFilterResult {
156        InputEventFilterResult::ForwardEvent
157    }
158
159    #[allow(clippy::unnecessary_cast)] // MouseEvent uses Coord
160    fn input_event(
161        self: Pin<&Self>,
162        event: &MouseEvent,
163        window_adapter: &Rc<dyn WindowAdapter>,
164        self_rc: &i_slint_core::items::ItemRc,
165    ) -> InputEventResult {
166        let size: qttypes::QSize = get_size!(self_rc);
167        let enabled = self.enabled();
168        // Slint slider supports floating point ranges, while Qt uses integer. To support (0..1) ranges
169        // of values, scale up a little, before truncating to integer values.
170        let value = (self.value() * 1024.0) as i32;
171        let min = (self.minimum() * 1024.0) as i32;
172        let max = (self.maximum() * 1024.0) as i32;
173        let mut data = self.data();
174        let active_controls = data.active_controls;
175        let pressed: bool = data.pressed != 0;
176        let vertical = self.orientation() == Orientation::Vertical;
177        let pos = event
178            .position()
179            .map(|p| qttypes::QPoint { x: p.x as _, y: p.y as _ })
180            .unwrap_or_default();
181        let widget: NonNull<()> = SlintTypeErasedWidgetPtr::qwidget_ptr(&self.widget_ptr);
182
183        let new_control = cpp!(unsafe [
184            pos as "QPoint",
185            size as "QSize",
186            enabled as "bool",
187            value as "int",
188            min as "int",
189            max as "int",
190            active_controls as "int",
191            pressed as "bool",
192            vertical as "bool",
193            widget as "QWidget*"
194        ] -> u32 as "int" {
195            ensure_initialized();
196            QStyleOptionSlider option;
197            initQSliderOptions(option, pressed, enabled, active_controls, min, max, value, vertical);
198            auto style = qApp->style();
199            option.rect = { QPoint{}, size };
200            return style->hitTestComplexControl(QStyle::CC_Slider, &option, pos, widget);
201        });
202        let result = match event {
203            _ if !enabled => {
204                data.pressed = 0;
205                InputEventResult::EventIgnored
206            }
207            MouseEvent::Pressed {
208                position: pos,
209                button: PointerEventButton::Left,
210                click_count: _,
211            } => {
212                if !self.has_focus() {
213                    WindowInner::from_pub(window_adapter.window()).set_focus_item(
214                        self_rc,
215                        true,
216                        FocusReason::PointerClick,
217                    );
218                }
219                data.pressed_x = if vertical { pos.y as f32 } else { pos.x as f32 };
220                data.pressed = 1;
221                data.pressed_val = self.value();
222                InputEventResult::GrabMouse
223            }
224            MouseEvent::Exit | MouseEvent::Released { button: PointerEventButton::Left, .. } => {
225                if data.pressed != 0 {
226                    Self::FIELD_OFFSETS.released.apply_pin(self).call(&(self.value(),));
227                }
228                data.pressed = 0;
229                InputEventResult::EventAccepted
230            }
231            MouseEvent::Moved { position: pos } => {
232                let (coord, size) =
233                    if vertical { (pos.y, size.height) } else { (pos.x, size.width) };
234                if data.pressed != 0 {
235                    // FIXME: use QStyle::subControlRect to find out the actual size of the groove
236                    let new_val = data.pressed_val
237                        + ((coord as f32) - data.pressed_x) * (self.maximum() - self.minimum())
238                            / size as f32;
239                    self.set_value(new_val);
240                    InputEventResult::GrabMouse
241                } else {
242                    InputEventResult::EventIgnored
243                }
244            }
245            MouseEvent::Wheel { delta_x, delta_y, .. } => {
246                let new_val = self.value() + delta_x + delta_y;
247                self.set_value(new_val);
248                InputEventResult::EventAccepted
249            }
250            MouseEvent::Pressed { button, .. } | MouseEvent::Released { button, .. } => {
251                debug_assert_ne!(*button, PointerEventButton::Left);
252                InputEventResult::EventIgnored
253            }
254            MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
255        };
256        data.active_controls = new_control;
257
258        self.data.set(data);
259        result
260    }
261
262    fn capture_key_event(
263        self: Pin<&Self>,
264        _event: &KeyEvent,
265        _window_adapter: &Rc<dyn WindowAdapter>,
266        _self_rc: &ItemRc,
267    ) -> KeyEventResult {
268        KeyEventResult::EventIgnored
269    }
270
271    fn key_event(
272        self: Pin<&Self>,
273        event: &KeyEvent,
274        _window_adapter: &Rc<dyn WindowAdapter>,
275        _self_rc: &ItemRc,
276    ) -> KeyEventResult {
277        if self.enabled() {
278            let Some(keycode) = event.text.chars().next() else {
279                return KeyEventResult::EventIgnored;
280            };
281            let vertical = self.orientation() == Orientation::Vertical;
282
283            if (!vertical && keycode == key_codes::RightArrow)
284                || (vertical && keycode == key_codes::DownArrow)
285            {
286                if event.event_type == KeyEventType::KeyPressed {
287                    self.set_value(self.value() + self.step());
288                } else if event.event_type == KeyEventType::KeyReleased {
289                    Self::FIELD_OFFSETS.released.apply_pin(self).call(&(self.value(),));
290                }
291                return KeyEventResult::EventAccepted;
292            }
293            if (!vertical && keycode == key_codes::LeftArrow)
294                || (vertical && keycode == key_codes::UpArrow)
295            {
296                if event.event_type == KeyEventType::KeyPressed {
297                    self.set_value(self.value() - self.step());
298                } else if event.event_type == KeyEventType::KeyReleased {
299                    Self::FIELD_OFFSETS.released.apply_pin(self).call(&(self.value(),));
300                }
301                return KeyEventResult::EventAccepted;
302            }
303            if keycode == key_codes::Home {
304                if event.event_type == KeyEventType::KeyPressed {
305                    self.set_value(self.minimum());
306                } else if event.event_type == KeyEventType::KeyReleased {
307                    Self::FIELD_OFFSETS.released.apply_pin(self).call(&(self.value(),));
308                }
309                return KeyEventResult::EventAccepted;
310            }
311            if keycode == key_codes::End {
312                if event.event_type == KeyEventType::KeyPressed {
313                    self.set_value(self.maximum());
314                } else if event.event_type == KeyEventType::KeyReleased {
315                    Self::FIELD_OFFSETS.released.apply_pin(self).call(&(self.value(),));
316                }
317                return KeyEventResult::EventAccepted;
318            }
319        }
320        KeyEventResult::EventIgnored
321    }
322
323    fn focus_event(
324        self: Pin<&Self>,
325        event: &FocusEvent,
326        _window_adapter: &Rc<dyn WindowAdapter>,
327        _self_rc: &ItemRc,
328    ) -> FocusEventResult {
329        if self.enabled() {
330            Self::FIELD_OFFSETS
331                .has_focus
332                .apply_pin(self)
333                .set(matches!(event, FocusEvent::FocusIn(_)));
334            FocusEventResult::FocusAccepted
335        } else {
336            FocusEventResult::FocusIgnored
337        }
338    }
339
340    fn_render! { this dpr size painter widget initial_state =>
341        let enabled = this.enabled();
342        let has_focus = this.has_focus();
343        // Slint slider supports floating point ranges, while Qt uses integer. To support (0..1) ranges
344        // of values, scale up a little, before truncating to integer values.
345        let value = (this.value() * 1024.0) as i32;
346        let min = (this.minimum() * 1024.0) as i32;
347        let max = (this.maximum() * 1024.0) as i32;
348        let data = this.data();
349        let active_controls = data.active_controls;
350        let pressed = data.pressed;
351        let vertical = this.orientation() == Orientation::Vertical;
352
353        cpp!(unsafe [
354            painter as "QPainterPtr*",
355            widget as "QWidget*",
356            enabled as "bool",
357            has_focus as "bool",
358            value as "int",
359            min as "int",
360            max as "int",
361            size as "QSize",
362            active_controls as "int",
363            pressed as "bool",
364            vertical as "bool",
365            dpr as "float",
366            initial_state as "int"
367        ] {
368            QStyleOptionSlider option;
369            option.styleObject = widget;
370            option.state |= QStyle::State(initial_state);
371            if (has_focus) {
372                option.state |= QStyle::State_HasFocus | QStyle::State_KeyboardFocusChange | QStyle::State_Item;
373            }
374            option.rect = QRect(QPoint(), size / dpr);
375            initQSliderOptions(option, pressed, enabled, active_controls, min, max, value, vertical);
376            auto style = qApp->style();
377            style->drawComplexControl(QStyle::CC_Slider, &option, painter->get(), widget);
378        });
379    }
380
381    fn bounding_rect(
382        self: core::pin::Pin<&Self>,
383        _window_adapter: &Rc<dyn WindowAdapter>,
384        _self_rc: &ItemRc,
385        geometry: LogicalRect,
386    ) -> LogicalRect {
387        geometry
388    }
389
390    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
391        false
392    }
393}
394
395impl NativeSlider {
396    fn set_value(self: Pin<&Self>, new_val: f32) {
397        let new_val = new_val.max(self.minimum()).min(self.maximum());
398        self.value.set(new_val);
399        Self::FIELD_OFFSETS.changed.apply_pin(self).call(&(new_val,));
400    }
401}
402
403impl ItemConsts for NativeSlider {
404    const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
405        Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection();
406}
407
408declare_item_vtable! {
409fn slint_get_NativeSliderVTable() -> NativeSliderVTable for NativeSlider
410}