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                    }
175                    data.active_controls = new_control;
176                    InputEventResult::GrabMouse
177                }
178                MouseEvent::Exit => {
179                    data.pressed = 0;
180                    InputEventResult::EventIgnored
181                }
182                MouseEvent::Released { .. } => {
183                    data.pressed = 0;
184                    let new_val = cpp!(unsafe [active_controls as "int", value as "int", max as "int", page_size as "int"] -> i32 as "int" {
185                        switch (active_controls) {
186                            case QStyle::SC_ScrollBarAddPage:
187                                return -value + page_size;
188                            case QStyle::SC_ScrollBarSubPage:
189                                return -value - page_size;
190                            case QStyle::SC_ScrollBarAddLine:
191                                return -value + 3.;
192                            case QStyle::SC_ScrollBarSubLine:
193                                return -value - 3.;
194                            case QStyle::SC_ScrollBarFirst:
195                                return 0;
196                            case QStyle::SC_ScrollBarLast:
197                                return max;
198                            default:
199                                return -value;
200                        }
201                    });
202                    let old_val = value_prop.get();
203                    let new_val = LogicalLength::new(-(new_val.min(max).max(0) as f32));
204                    value_prop.set(new_val);
205                    if new_val != old_val {
206                        Self::FIELD_OFFSETS.scrolled.apply_pin(self).call(&());
207                    }
208                    InputEventResult::EventIgnored
209                }
210                MouseEvent::Moved { .. } => {
211                    if data.pressed != 0 && data.active_controls == SC_ScrollBarSlider {
212                        let max = max as f32;
213                        let new_val = data.pressed_val
214                            + ((pos as f32) - data.pressed_x) * (max + (page_size as f32))
215                                / size as f32;
216                        let old_val = value_prop.get();
217                        let new_val = LogicalLength::new(-new_val.min(max).max(0.));
218                        value_prop.set(new_val);
219                        if new_val != old_val {
220                            Self::FIELD_OFFSETS.scrolled.apply_pin(self).call(&());
221                        }
222                        InputEventResult::GrabMouse
223                    } else {
224                        InputEventResult::EventAccepted
225                    }
226                }
227                MouseEvent::Wheel { delta_x, delta_y, .. } => {
228                    let max = max as f32;
229                    let new_val;
230                    if horizontal {
231                        new_val = value as f32 + delta_x;
232                    } else {
233                        new_val = value as f32 + delta_y;
234                    }
235                    let old_val = value_prop.get();
236                    let new_val = LogicalLength::new(new_val.min(0.).max(-max));
237                    value_prop.set(new_val);
238                    if new_val != old_val {
239                        Self::FIELD_OFFSETS.scrolled.apply_pin(self).call(&());
240                    }
241                    InputEventResult::EventAccepted
242                }
243                MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
244            };
245            self.data.set(data);
246            result
247        };
248
249        let pos = event.position().unwrap_or_default();
250
251        if pressed == 2 || (pressed == 0 && pos.x > (size.width as f32 - right)) {
252            handle_scrollbar(
253                false,
254                qttypes::QPoint {
255                    x: (pos.x - (size.width as f32 - right)) as _,
256                    y: (pos.y - top) as _,
257                },
258                qttypes::QSize {
259                    width: (right - left) as _,
260                    height: (size.height as f32 - (bottom + top)) as _,
261                },
262                Self::FIELD_OFFSETS.vertical_value.apply_pin(self),
263                self.vertical_page_size().get() as i32,
264                self.vertical_max().get() as i32,
265            )
266        } else if pressed == 1 || pos.y > (size.height as f32 - bottom) {
267            handle_scrollbar(
268                true,
269                qttypes::QPoint {
270                    x: (pos.x - left) as _,
271                    y: (pos.y - (size.height as f32 - bottom)) as _,
272                },
273                qttypes::QSize {
274                    width: (size.width as f32 - (right + left)) as _,
275                    height: (bottom - top) as _,
276                },
277                Self::FIELD_OFFSETS.horizontal_value.apply_pin(self),
278                self.horizontal_page_size().get() as i32,
279                self.horizontal_max().get() as i32,
280            )
281        } else {
282            Default::default()
283        }
284    }
285
286    fn capture_key_event(
287        self: Pin<&Self>,
288        _event: &KeyEvent,
289        _window_adapter: &Rc<dyn WindowAdapter>,
290        _self_rc: &ItemRc,
291    ) -> KeyEventResult {
292        KeyEventResult::EventIgnored
293    }
294
295    fn key_event(
296        self: Pin<&Self>,
297        _: &KeyEvent,
298        _window_adapter: &Rc<dyn WindowAdapter>,
299        _self_rc: &ItemRc,
300    ) -> KeyEventResult {
301        KeyEventResult::EventIgnored
302    }
303
304    fn focus_event(
305        self: Pin<&Self>,
306        _: &FocusEvent,
307        _window_adapter: &Rc<dyn WindowAdapter>,
308        _self_rc: &ItemRc,
309    ) -> FocusEventResult {
310        FocusEventResult::FocusIgnored
311    }
312
313    fn_render! { this dpr size painter widget initial_state =>
314
315        let data = this.data();
316        let margins = qttypes::QMargins {
317            left: this.native_padding_left().get() as _,
318            top: this.native_padding_top().get() as _,
319            right: this.native_padding_right().get() as _,
320            bottom: this.native_padding_bottom().get() as _,
321        };
322        let enabled: bool = this.enabled();
323        let has_focus: bool = this.has_focus();
324        let vertical_bar_visible = (this.vertical_scrollbar_policy() == ScrollBarPolicy::AlwaysOn) || ((this.vertical_scrollbar_policy() == ScrollBarPolicy::AsNeeded) && (this.vertical_max().get() > 0.0));
325        let horizontal_bar_visible = (this.horizontal_scrollbar_policy() == ScrollBarPolicy::AlwaysOn) || ((this.horizontal_scrollbar_policy() == ScrollBarPolicy::AsNeeded) && (this.horizontal_max().get() > 0.0));
326        let scrollbar_bar_visible = vertical_bar_visible || horizontal_bar_visible;
327        let frame_around_contents = cpp!(unsafe [
328            painter as "QPainterPtr*",
329            widget as "QWidget*",
330            size as "QSize",
331            dpr as "float",
332            margins as "QMargins",
333            enabled as "bool",
334            has_focus as "bool",
335            initial_state as "int",
336            scrollbar_bar_visible as "bool"
337        ] -> bool as "bool" {
338            ensure_initialized();
339            QStyleOptionFrame frameOption;
340            frameOption.styleObject = widget;
341            frameOption.state |= QStyle::State(initial_state);
342            frameOption.frameShape = QFrame::StyledPanel;
343
344            frameOption.lineWidth = 1;
345            frameOption.midLineWidth = 0;
346            frameOption.rect = QRect(QPoint(), size / dpr);
347            frameOption.state |= QStyle::State_Sunken;
348            if (enabled) {
349                frameOption.state |= QStyle::State_Enabled;
350            } else {
351                frameOption.palette.setCurrentColorGroup(QPalette::Disabled);
352            }
353            if (has_focus)
354                frameOption.state |= QStyle::State_HasFocus;
355            //int scrollOverlap = qApp->style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarOverlap, &frameOption, nullptr);
356            bool foac = qApp->style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents, &frameOption, widget);
357            // this assume that the frame size is the same on both side, so that the scrollbar width is (right-left)
358            QSize corner_size = QSize(margins.right() - margins.left(), margins.bottom() - margins.top());
359            if (foac) {
360                frameOption.rect = QRect(QPoint(), (size / dpr) - corner_size);
361                qApp->style()->drawControl(QStyle::CE_ShapedFrame, &frameOption, painter->get(), widget);
362                frameOption.rect = QRect(frameOption.rect.bottomRight() + QPoint(1, 1), corner_size);
363                if (scrollbar_bar_visible) {
364                    qApp->style()->drawPrimitive(QStyle::PE_PanelScrollAreaCorner, &frameOption, painter->get(), widget);
365                }
366            } else {
367                qApp->style()->drawControl(QStyle::CE_ShapedFrame, &frameOption, painter->get(), widget);
368                frameOption.rect = QRect(frameOption.rect.bottomRight() + QPoint(1, 1) - QPoint(margins.right(), margins.bottom()), corner_size);
369                if (scrollbar_bar_visible) {
370                    qApp->style()->drawPrimitive(QStyle::PE_PanelScrollAreaCorner, &frameOption, painter->get(), widget);
371                }
372            }
373            return foac;
374        });
375
376        let draw_scrollbar = |horizontal: bool,
377                              rect: qttypes::QRectF,
378                              value: i32,
379                              page_size: i32,
380                              max: i32,
381                              active_controls: u32,
382                              pressed: bool,
383                              initial_state: i32| {
384            cpp!(unsafe [
385                painter as "QPainterPtr*",
386                widget as "QWidget*",
387                value as "int",
388                page_size as "int",
389                max as "int",
390                rect as "QRectF",
391                active_controls as "int",
392                pressed as "bool",
393                dpr as "float",
394                horizontal as "bool",
395                has_focus as "bool",
396                initial_state as "int"
397            ] {
398                QPainter *painter_ = painter->get();
399                auto r = rect.toAlignedRect();
400                // The mac style may crash on invalid rectangles (#595)
401                if (!r.isValid())
402                    return;
403                // The mac style ignores painter translations (due to CGContextRef redirection) as well as
404                // option.rect's top-left - hence this hack with an intermediate buffer.
405            #if defined(Q_OS_MAC)
406                QImage scrollbar_image(r.size(), QImage::Format_ARGB32_Premultiplied);
407                scrollbar_image.fill(Qt::transparent);
408                {QPainter p(&scrollbar_image); QPainter *painter_ = &p;
409            #else
410                painter_->save();
411                auto cleanup = qScopeGuard([&] { painter_->restore(); });
412                painter_->translate(r.topLeft()); // There is bugs in the styles if the scrollbar is not in (0,0)
413            #endif
414                QStyleOptionSlider option;
415                option.state |= QStyle::State(initial_state);
416                option.rect = QRect(QPoint(), r.size());
417                initQSliderOptions(option, pressed, true, active_controls, 0, max / dpr, -value / dpr, false);
418                option.subControls = QStyle::SC_All;
419                option.pageStep = page_size / dpr;
420                if (has_focus)
421                    option.state |= QStyle::State_HasFocus;
422
423                if (!horizontal) {
424                    option.state ^= QStyle::State_Horizontal;
425                    option.orientation = Qt::Vertical;
426                }
427
428                auto style = qApp->style();
429                style->drawComplexControl(QStyle::CC_ScrollBar, &option, painter_, widget);
430            #if defined(Q_OS_MAC)
431                }
432                (painter_)->drawImage(r.topLeft(), scrollbar_image);
433            #endif
434            });
435        };
436
437        let scrollbars_width = (margins.right - margins.left) as f32;
438        let scrollbars_height = (margins.bottom - margins.top) as f32;
439        if vertical_bar_visible {
440            draw_scrollbar(
441                false,
442                qttypes::QRectF {
443                    x: ((size.width as f32 / dpr) - if frame_around_contents { scrollbars_width } else { margins.right as _ }) as _,
444                    y: (if frame_around_contents { 0 } else { margins.top }) as _,
445                    width: scrollbars_width as _,
446                    height: ((size.height as f32 / dpr) - if frame_around_contents { scrollbars_height } else { (margins.bottom + margins.top) as f32 }) as _,
447                },
448                this.vertical_value().get() as i32,
449                this.vertical_page_size().get() as i32,
450                this.vertical_max().get() as i32,
451                data.active_controls,
452                data.pressed == 2,
453                initial_state
454            );
455        }
456        if horizontal_bar_visible {
457            draw_scrollbar(
458                true,
459                qttypes::QRectF {
460                    x: (if frame_around_contents { 0 } else { margins.left }) as _,
461                    y: ((size.height as f32 / dpr) - if frame_around_contents { scrollbars_height } else { margins.bottom as _ }) as _,
462                    width: ((size.width as f32 / dpr) - if frame_around_contents { scrollbars_width } else { (margins.left + margins.right) as _ }) as _,
463                    height: (scrollbars_height) as _,
464                },
465                this.horizontal_value().get() as i32,
466                this.horizontal_page_size().get() as i32,
467                this.horizontal_max().get() as i32,
468                data.active_controls,
469                data.pressed == 1,
470                initial_state
471            );
472        }
473    }
474
475    fn bounding_rect(
476        self: core::pin::Pin<&Self>,
477        _window_adapter: &Rc<dyn WindowAdapter>,
478        _self_rc: &ItemRc,
479        geometry: LogicalRect,
480    ) -> LogicalRect {
481        geometry
482    }
483
484    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
485        false
486    }
487}
488
489impl ItemConsts for NativeScrollView {
490    const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
491        Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection();
492}
493
494declare_item_vtable! {
495fn slint_get_NativeScrollViewVTable() -> NativeScrollViewVTable for NativeScrollView
496}