Skip to main content

i_slint_backend_qt/qt_widgets/
scrollview.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::input::FocusEventResult;
5use i_slint_core::items::ScrollBarPolicy;
6
7use super::*;
8
9#[repr(C)]
10#[derive(FieldOffsets, Default, SlintElement)]
11#[pin]
12pub struct NativeScrollView {
13    pub horizontal_max: Property<LogicalLength>,
14    pub horizontal_page_size: Property<LogicalLength>,
15    pub horizontal_value: Property<LogicalLength>,
16    pub vertical_max: Property<LogicalLength>,
17    pub vertical_page_size: Property<LogicalLength>,
18    pub vertical_value: Property<LogicalLength>,
19    pub cached_rendering_data: CachedRenderingData,
20    pub native_padding_left: Property<LogicalLength>,
21    pub native_padding_right: Property<LogicalLength>,
22    pub native_padding_top: Property<LogicalLength>,
23    pub native_padding_bottom: Property<LogicalLength>,
24    pub enabled: Property<bool>,
25    pub has_focus: Property<bool>,
26    pub vertical_scrollbar_policy: Property<ScrollBarPolicy>,
27    pub horizontal_scrollbar_policy: Property<ScrollBarPolicy>,
28    pub scrolled: Callback<VoidArg>,
29    data: Property<NativeSliderData>,
30    widget_ptr: std::cell::Cell<SlintTypeErasedWidgetPtr>,
31    animation_tracker: Property<i32>,
32    // TODO: allocate two widgets for each scrollbar and a tracker for each as well,
33    // for animated scrollbars...
34}
35
36impl Item for NativeScrollView {
37    fn init(self: Pin<&Self>, _self_rc: &ItemRc) {
38        let animation_tracker_property_ptr = Self::FIELD_OFFSETS.animation_tracker.apply_pin(self);
39        self.widget_ptr.set(cpp! { unsafe [animation_tracker_property_ptr as "void*"] -> SlintTypeErasedWidgetPtr as "std::unique_ptr<SlintTypeErasedWidget>"  {
40            return make_unique_animated_widget<QWidget>(animation_tracker_property_ptr);
41        }});
42
43        let paddings = Rc::pin(Property::default());
44
45        paddings.as_ref().set_binding(move || {
46        cpp!(unsafe [] -> qttypes::QMargins as "QMargins" {
47            ensure_initialized();
48            QStyleOptionSlider option;
49            initQSliderOptions(option, false, true, 0, 0, 1000, 1000, false);
50
51            int extent = qApp->style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, nullptr);
52            int sliderMin = qApp->style()->pixelMetric(QStyle::PM_ScrollBarSliderMin, &option, nullptr);
53            auto horizontal_size = qApp->style()->sizeFromContents(QStyle::CT_ScrollBar, &option, QSize(extent * 2 + sliderMin, extent), nullptr);
54            option.state ^= QStyle::State_Horizontal;
55            option.orientation = Qt::Vertical;
56            extent = qApp->style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, nullptr);
57            sliderMin = qApp->style()->pixelMetric(QStyle::PM_ScrollBarSliderMin, &option, nullptr);
58            auto vertical_size = qApp->style()->sizeFromContents(QStyle::CT_ScrollBar, &option, QSize(extent, extent * 2 + sliderMin), nullptr);
59
60            QStyleOptionFrame frameOption;
61            frameOption.rect = QRect(QPoint(), QSize(1000, 1000));
62            frameOption.frameShape = QFrame::StyledPanel;
63            frameOption.lineWidth = 1;
64            frameOption.midLineWidth = 0;
65            QRect cr = qApp->style()->subElementRect(QStyle::SE_ShapedFrameContents, &frameOption, nullptr);
66            return {
67                cr.left(),
68                cr.top(),
69                (vertical_size.width() + frameOption.rect.right() - cr.right()),
70                (horizontal_size.height() + frameOption.rect.bottom() - cr.bottom())
71            };
72        })
73    });
74
75        self.native_padding_left.set_binding({
76            let paddings = paddings.clone();
77            move || LogicalLength::new(paddings.as_ref().get().left as _)
78        });
79        self.native_padding_right.set_binding({
80            let paddings = paddings.clone();
81            move || LogicalLength::new(paddings.as_ref().get().right as _)
82        });
83        self.native_padding_top.set_binding({
84            let paddings = paddings.clone();
85            move || LogicalLength::new(paddings.as_ref().get().top as _)
86        });
87        self.native_padding_bottom.set_binding({
88            let paddings = paddings;
89            move || LogicalLength::new(paddings.as_ref().get().bottom as _)
90        });
91    }
92
93    fn layout_info(
94        self: Pin<&Self>,
95        orientation: Orientation,
96        _window_adapter: &Rc<dyn WindowAdapter>,
97        _self_rc: &ItemRc,
98    ) -> LayoutInfo {
99        let min = match orientation {
100            Orientation::Horizontal => self.native_padding_left() + self.native_padding_right(),
101            Orientation::Vertical => self.native_padding_top() + self.native_padding_bottom(),
102        }
103        .get();
104        LayoutInfo { min, preferred: min, stretch: 1., ..LayoutInfo::default() }
105    }
106
107    fn input_event_filter_before_children(
108        self: Pin<&Self>,
109        _: &MouseEvent,
110        _window_adapter: &Rc<dyn WindowAdapter>,
111        _self_rc: &ItemRc,
112    ) -> InputEventFilterResult {
113        InputEventFilterResult::ForwardEvent
114    }
115
116    fn input_event(
117        self: Pin<&Self>,
118        event: &MouseEvent,
119        _window_adapter: &Rc<dyn WindowAdapter>,
120        self_rc: &i_slint_core::items::ItemRc,
121    ) -> InputEventResult {
122        let size: qttypes::QSize = get_size!(self_rc);
123        let mut data = self.data();
124        let active_controls = data.active_controls;
125        let pressed = data.pressed;
126        let left = self.native_padding_left().get();
127        let right = self.native_padding_right().get();
128        let top = self.native_padding_top().get();
129        let bottom = self.native_padding_bottom().get();
130
131        let mut handle_scrollbar = |horizontal: bool,
132                                    pos: qttypes::QPoint,
133                                    size: qttypes::QSize,
134                                    value_prop: Pin<&Property<LogicalLength>>,
135                                    page_size: i32,
136                                    max: i32| {
137            let pressed: bool = data.pressed != 0;
138            let value: i32 = value_prop.get().get() as i32;
139            let new_control = cpp!(unsafe [
140                pos as "QPoint",
141                value as "int",
142                page_size as "int",
143                max as "int",
144                size as "QSize",
145                active_controls as "int",
146                pressed as "bool",
147                horizontal as "bool"
148            ] -> u32 as "int" {
149                ensure_initialized();
150                QStyleOptionSlider option;
151                initQSliderOptions(option, pressed, true, active_controls, 0, max, -value, false);
152                option.pageStep = page_size;
153                if (!horizontal) {
154                    option.state ^= QStyle::State_Horizontal;
155                    option.orientation = Qt::Vertical;
156                }
157                auto style = qApp->style();
158                option.rect = { QPoint{}, size };
159                return style->hitTestComplexControl(QStyle::CC_ScrollBar, &option, pos, nullptr);
160            });
161
162            #[allow(non_snake_case)]
163            let SC_ScrollBarSlider =
164                cpp!(unsafe []->u32 as "int" { return QStyle::SC_ScrollBarSlider;});
165
166            let (pos, size) = if horizontal { (pos.x, size.width) } else { (pos.y, size.height) };
167
168            let result = match event {
169                MouseEvent::Pressed { .. } => {
170                    data.pressed = if horizontal { 1 } else { 2 };
171                    if new_control == SC_ScrollBarSlider {
172                        data.pressed_x = pos as f32;
173                        data.pressed_val = -value as f32;
174                        data.pressed_max = max as f32;
175                    }
176                    data.active_controls = new_control;
177                    InputEventResult::GrabMouse
178                }
179                MouseEvent::Exit => {
180                    data.pressed = 0;
181                    InputEventResult::EventIgnored
182                }
183                MouseEvent::Released { .. } => {
184                    data.pressed = 0;
185                    let new_val = cpp!(unsafe [active_controls as "int", value as "int", max as "int", page_size as "int"] -> i32 as "int" {
186                        switch (active_controls) {
187                            case QStyle::SC_ScrollBarAddPage:
188                                return -value + page_size;
189                            case QStyle::SC_ScrollBarSubPage:
190                                return -value - page_size;
191                            case QStyle::SC_ScrollBarAddLine:
192                                return -value + 3.;
193                            case QStyle::SC_ScrollBarSubLine:
194                                return -value - 3.;
195                            case QStyle::SC_ScrollBarFirst:
196                                return 0;
197                            case QStyle::SC_ScrollBarLast:
198                                return max;
199                            default:
200                                return -value;
201                        }
202                    });
203                    let old_val = value_prop.get();
204                    let new_val = LogicalLength::new(-(new_val.min(max).max(0) as f32));
205                    value_prop.set(new_val);
206                    if new_val != old_val {
207                        Self::FIELD_OFFSETS.scrolled.apply_pin(self).call(&());
208                    }
209                    InputEventResult::EventIgnored
210                }
211                MouseEvent::Moved { .. } => {
212                    if data.pressed != 0 && data.active_controls == SC_ScrollBarSlider {
213                        let max = max as f32;
214
215                        // Update reference points when the size of the viewport changes to
216                        // avoid 'jumping' during scrolling.
217                        // This happens when the height estimate of a ListView changes after
218                        // new items are loaded.
219                        if data.pressed_max != max {
220                            data.pressed_x = pos as f32;
221                            data.pressed_val = -value as f32;
222                            data.pressed_max = max;
223                        }
224
225                        let new_val = data.pressed_val
226                            + ((pos as f32) - data.pressed_x) * (max + (page_size as f32))
227                                / size as f32;
228                        let old_val = value_prop.get();
229                        let new_val = LogicalLength::new(-new_val.min(max).max(0.));
230                        value_prop.set(new_val);
231                        if new_val != old_val {
232                            Self::FIELD_OFFSETS.scrolled.apply_pin(self).call(&());
233                        }
234                        InputEventResult::GrabMouse
235                    } else {
236                        InputEventResult::EventAccepted
237                    }
238                }
239                MouseEvent::Wheel { delta_x, delta_y, .. } => {
240                    let max = max as f32;
241                    let new_val;
242                    if horizontal {
243                        new_val = value as f32 + delta_x;
244                    } else {
245                        new_val = value as f32 + delta_y;
246                    }
247                    let old_val = value_prop.get();
248                    let new_val = LogicalLength::new(new_val.min(0.).max(-max));
249                    value_prop.set(new_val);
250                    if new_val != old_val {
251                        Self::FIELD_OFFSETS.scrolled.apply_pin(self).call(&());
252                    }
253                    InputEventResult::EventAccepted
254                }
255                MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
256            };
257            self.data.set(data);
258            result
259        };
260
261        let pos = event.position().unwrap_or_default();
262
263        if pressed == 2 || (pressed == 0 && pos.x > (size.width as f32 - right)) {
264            handle_scrollbar(
265                false,
266                qttypes::QPoint {
267                    x: (pos.x - (size.width as f32 - right)) as _,
268                    y: (pos.y - top) as _,
269                },
270                qttypes::QSize {
271                    width: (right - left) as _,
272                    height: (size.height as f32 - (bottom + top)) as _,
273                },
274                Self::FIELD_OFFSETS.vertical_value.apply_pin(self),
275                self.vertical_page_size().get() as i32,
276                self.vertical_max().get() as i32,
277            )
278        } else if pressed == 1 || pos.y > (size.height as f32 - bottom) {
279            handle_scrollbar(
280                true,
281                qttypes::QPoint {
282                    x: (pos.x - left) as _,
283                    y: (pos.y - (size.height as f32 - bottom)) as _,
284                },
285                qttypes::QSize {
286                    width: (size.width as f32 - (right + left)) as _,
287                    height: (bottom - top) as _,
288                },
289                Self::FIELD_OFFSETS.horizontal_value.apply_pin(self),
290                self.horizontal_page_size().get() as i32,
291                self.horizontal_max().get() as i32,
292            )
293        } else {
294            Default::default()
295        }
296    }
297
298    fn capture_key_event(
299        self: Pin<&Self>,
300        _event: &KeyEvent,
301        _window_adapter: &Rc<dyn WindowAdapter>,
302        _self_rc: &ItemRc,
303    ) -> KeyEventResult {
304        KeyEventResult::EventIgnored
305    }
306
307    fn key_event(
308        self: Pin<&Self>,
309        _: &KeyEvent,
310        _window_adapter: &Rc<dyn WindowAdapter>,
311        _self_rc: &ItemRc,
312    ) -> KeyEventResult {
313        KeyEventResult::EventIgnored
314    }
315
316    fn focus_event(
317        self: Pin<&Self>,
318        _: &FocusEvent,
319        _window_adapter: &Rc<dyn WindowAdapter>,
320        _self_rc: &ItemRc,
321    ) -> FocusEventResult {
322        FocusEventResult::FocusIgnored
323    }
324
325    fn_render! { this dpr size painter widget initial_state =>
326
327        let data = this.data();
328        let margins = qttypes::QMargins {
329            left: this.native_padding_left().get() as _,
330            top: this.native_padding_top().get() as _,
331            right: this.native_padding_right().get() as _,
332            bottom: this.native_padding_bottom().get() as _,
333        };
334        let enabled: bool = this.enabled();
335        let has_focus: bool = this.has_focus();
336        let vertical_bar_visible = (this.vertical_scrollbar_policy() == ScrollBarPolicy::AlwaysOn) || ((this.vertical_scrollbar_policy() == ScrollBarPolicy::AsNeeded) && (this.vertical_max().get() > 0.0));
337        let horizontal_bar_visible = (this.horizontal_scrollbar_policy() == ScrollBarPolicy::AlwaysOn) || ((this.horizontal_scrollbar_policy() == ScrollBarPolicy::AsNeeded) && (this.horizontal_max().get() > 0.0));
338        let scrollbar_bar_visible = vertical_bar_visible || horizontal_bar_visible;
339        let frame_around_contents = cpp!(unsafe [
340            painter as "QPainterPtr*",
341            widget as "QWidget*",
342            size as "QSize",
343            dpr as "float",
344            margins as "QMargins",
345            enabled as "bool",
346            has_focus as "bool",
347            initial_state as "int",
348            scrollbar_bar_visible as "bool"
349        ] -> bool as "bool" {
350            ensure_initialized();
351            QStyleOptionFrame frameOption;
352            frameOption.styleObject = widget;
353            frameOption.state |= QStyle::State(initial_state);
354            frameOption.frameShape = QFrame::StyledPanel;
355
356            frameOption.lineWidth = 1;
357            frameOption.midLineWidth = 0;
358            frameOption.rect = QRect(QPoint(), size / dpr);
359            frameOption.state |= QStyle::State_Sunken;
360            if (enabled) {
361                frameOption.state |= QStyle::State_Enabled;
362            } else {
363                frameOption.palette.setCurrentColorGroup(QPalette::Disabled);
364            }
365            if (has_focus)
366                frameOption.state |= QStyle::State_HasFocus;
367            //int scrollOverlap = qApp->style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarOverlap, &frameOption, nullptr);
368            bool foac = qApp->style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents, &frameOption, widget);
369            // this assume that the frame size is the same on both side, so that the scrollbar width is (right-left)
370            QSize corner_size = QSize(margins.right() - margins.left(), margins.bottom() - margins.top());
371            if (foac) {
372                frameOption.rect = QRect(QPoint(), (size / dpr) - corner_size);
373                qApp->style()->drawControl(QStyle::CE_ShapedFrame, &frameOption, painter->get(), widget);
374                frameOption.rect = QRect(frameOption.rect.bottomRight() + QPoint(1, 1), corner_size);
375                if (scrollbar_bar_visible) {
376                    qApp->style()->drawPrimitive(QStyle::PE_PanelScrollAreaCorner, &frameOption, painter->get(), widget);
377                }
378            } else {
379                qApp->style()->drawControl(QStyle::CE_ShapedFrame, &frameOption, painter->get(), widget);
380                frameOption.rect = QRect(frameOption.rect.bottomRight() + QPoint(1, 1) - QPoint(margins.right(), margins.bottom()), corner_size);
381                if (scrollbar_bar_visible) {
382                    qApp->style()->drawPrimitive(QStyle::PE_PanelScrollAreaCorner, &frameOption, painter->get(), widget);
383                }
384            }
385            return foac;
386        });
387
388        let draw_scrollbar = |horizontal: bool,
389                              rect: qttypes::QRectF,
390                              value: i32,
391                              page_size: i32,
392                              max: i32,
393                              active_controls: u32,
394                              pressed: bool,
395                              initial_state: i32| {
396            cpp!(unsafe [
397                painter as "QPainterPtr*",
398                widget as "QWidget*",
399                value as "int",
400                page_size as "int",
401                max as "int",
402                rect as "QRectF",
403                active_controls as "int",
404                pressed as "bool",
405                dpr as "float",
406                horizontal as "bool",
407                has_focus as "bool",
408                initial_state as "int"
409            ] {
410                QPainter *painter_ = painter->get();
411                auto r = rect.toAlignedRect();
412                // The mac style may crash on invalid rectangles (#595)
413                if (!r.isValid())
414                    return;
415                // The mac style ignores painter translations (due to CGContextRef redirection) as well as
416                // option.rect's top-left - hence this hack with an intermediate buffer.
417            #if defined(Q_OS_MAC)
418                QImage scrollbar_image(r.size(), QImage::Format_ARGB32_Premultiplied);
419                scrollbar_image.fill(Qt::transparent);
420                {QPainter p(&scrollbar_image); QPainter *painter_ = &p;
421            #else
422                painter_->save();
423                auto cleanup = qScopeGuard([&] { painter_->restore(); });
424                painter_->translate(r.topLeft()); // There is bugs in the styles if the scrollbar is not in (0,0)
425            #endif
426                QStyleOptionSlider option;
427                option.state |= QStyle::State(initial_state);
428                option.rect = QRect(QPoint(), r.size());
429                initQSliderOptions(option, pressed, true, active_controls, 0, max / dpr, -value / dpr, false);
430                option.subControls = QStyle::SC_All;
431                option.pageStep = page_size / dpr;
432                if (has_focus)
433                    option.state |= QStyle::State_HasFocus;
434
435                if (!horizontal) {
436                    option.state ^= QStyle::State_Horizontal;
437                    option.orientation = Qt::Vertical;
438                }
439
440                auto style = qApp->style();
441                style->drawComplexControl(QStyle::CC_ScrollBar, &option, painter_, widget);
442            #if defined(Q_OS_MAC)
443                }
444                (painter_)->drawImage(r.topLeft(), scrollbar_image);
445            #endif
446            });
447        };
448
449        let scrollbars_width = (margins.right - margins.left) as f32;
450        let scrollbars_height = (margins.bottom - margins.top) as f32;
451        if vertical_bar_visible {
452            draw_scrollbar(
453                false,
454                qttypes::QRectF {
455                    x: ((size.width as f32 / dpr) - if frame_around_contents { scrollbars_width } else { margins.right as _ }) as _,
456                    y: (if frame_around_contents { 0 } else { margins.top }) as _,
457                    width: scrollbars_width as _,
458                    height: ((size.height as f32 / dpr) - if frame_around_contents { scrollbars_height } else { (margins.bottom + margins.top) as f32 }) as _,
459                },
460                this.vertical_value().get() as i32,
461                this.vertical_page_size().get() as i32,
462                this.vertical_max().get() as i32,
463                data.active_controls,
464                data.pressed == 2,
465                initial_state
466            );
467        }
468        if horizontal_bar_visible {
469            draw_scrollbar(
470                true,
471                qttypes::QRectF {
472                    x: (if frame_around_contents { 0 } else { margins.left }) as _,
473                    y: ((size.height as f32 / dpr) - if frame_around_contents { scrollbars_height } else { margins.bottom as _ }) as _,
474                    width: ((size.width as f32 / dpr) - if frame_around_contents { scrollbars_width } else { (margins.left + margins.right) as _ }) as _,
475                    height: (scrollbars_height) as _,
476                },
477                this.horizontal_value().get() as i32,
478                this.horizontal_page_size().get() as i32,
479                this.horizontal_max().get() as i32,
480                data.active_controls,
481                data.pressed == 1,
482                initial_state
483            );
484        }
485    }
486
487    fn bounding_rect(
488        self: core::pin::Pin<&Self>,
489        _window_adapter: &Rc<dyn WindowAdapter>,
490        _self_rc: &ItemRc,
491        geometry: LogicalRect,
492    ) -> LogicalRect {
493        geometry
494    }
495
496    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
497        false
498    }
499}
500
501impl ItemConsts for NativeScrollView {
502    const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
503        Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection();
504}
505
506declare_item_vtable! {
507fn slint_get_NativeScrollViewVTable() -> NativeScrollViewVTable for NativeScrollView
508}