// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 use i_slint_core::input::FocusEventResult; use super::*; #[repr(C)] #[derive(FieldOffsets, Default, SlintElement)] #[pin] pub struct NativeScrollView { pub horizontal_max: Property, pub horizontal_page_size: Property, pub horizontal_value: Property, pub vertical_max: Property, pub vertical_page_size: Property, pub vertical_value: Property, pub cached_rendering_data: CachedRenderingData, pub native_padding_left: Property, pub native_padding_right: Property, pub native_padding_top: Property, pub native_padding_bottom: Property, pub enabled: Property, pub has_focus: Property, data: Property, widget_ptr: std::cell::Cell, animation_tracker: Property, // TODO: allocate two widgets for each scrollbar and a tracker for each as well, // for animated scrollbars... } impl Item for NativeScrollView { fn init(self: Pin<&Self>, _self_rc: &ItemRc) { let animation_tracker_property_ptr = Self::FIELD_OFFSETS.animation_tracker.apply_pin(self); self.widget_ptr.set(cpp! { unsafe [animation_tracker_property_ptr as "void*"] -> SlintTypeErasedWidgetPtr as "std::unique_ptr" { return make_unique_animated_widget(animation_tracker_property_ptr); }}); let paddings = Rc::pin(Property::default()); paddings.as_ref().set_binding(move || { cpp!(unsafe [] -> qttypes::QMargins as "QMargins" { ensure_initialized(); QStyleOptionSlider option; initQSliderOptions(option, false, true, 0, 0, 1000, 1000, false); int extent = qApp->style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, nullptr); int sliderMin = qApp->style()->pixelMetric(QStyle::PM_ScrollBarSliderMin, &option, nullptr); auto horizontal_size = qApp->style()->sizeFromContents(QStyle::CT_ScrollBar, &option, QSize(extent * 2 + sliderMin, extent), nullptr); option.state ^= QStyle::State_Horizontal; option.orientation = Qt::Vertical; extent = qApp->style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, nullptr); sliderMin = qApp->style()->pixelMetric(QStyle::PM_ScrollBarSliderMin, &option, nullptr); auto vertical_size = qApp->style()->sizeFromContents(QStyle::CT_ScrollBar, &option, QSize(extent, extent * 2 + sliderMin), nullptr); QStyleOptionFrame frameOption; frameOption.rect = QRect(QPoint(), QSize(1000, 1000)); frameOption.frameShape = QFrame::StyledPanel; frameOption.lineWidth = 1; frameOption.midLineWidth = 0; QRect cr = qApp->style()->subElementRect(QStyle::SE_ShapedFrameContents, &frameOption, nullptr); return { cr.left(), cr.top(), (vertical_size.width() + frameOption.rect.right() - cr.right()), (horizontal_size.height() + frameOption.rect.bottom() - cr.bottom()) }; }) }); self.native_padding_left.set_binding({ let paddings = paddings.clone(); move || LogicalLength::new(paddings.as_ref().get().left as _) }); self.native_padding_right.set_binding({ let paddings = paddings.clone(); move || LogicalLength::new(paddings.as_ref().get().right as _) }); self.native_padding_top.set_binding({ let paddings = paddings.clone(); move || LogicalLength::new(paddings.as_ref().get().top as _) }); self.native_padding_bottom.set_binding({ let paddings = paddings; move || LogicalLength::new(paddings.as_ref().get().bottom as _) }); } fn layout_info( self: Pin<&Self>, orientation: Orientation, _window_adapter: &Rc, ) -> LayoutInfo { let min = match orientation { Orientation::Horizontal => self.native_padding_left() + self.native_padding_right(), Orientation::Vertical => self.native_padding_top() + self.native_padding_bottom(), } .get(); LayoutInfo { min, preferred: min, stretch: 1., ..LayoutInfo::default() } } fn input_event_filter_before_children( self: Pin<&Self>, _: MouseEvent, _window_adapter: &Rc, _self_rc: &ItemRc, ) -> InputEventFilterResult { InputEventFilterResult::ForwardEvent } fn input_event( self: Pin<&Self>, event: MouseEvent, _window_adapter: &Rc, self_rc: &i_slint_core::items::ItemRc, ) -> InputEventResult { let size: qttypes::QSize = get_size!(self_rc); let mut data = self.data(); let active_controls = data.active_controls; let pressed = data.pressed; let left = self.native_padding_left().get(); let right = self.native_padding_right().get(); let top = self.native_padding_top().get(); let bottom = self.native_padding_bottom().get(); let mut handle_scrollbar = |horizontal: bool, pos: qttypes::QPoint, size: qttypes::QSize, value_prop: Pin<&Property>, page_size: i32, max: i32| { let pressed: bool = data.pressed != 0; let value: i32 = value_prop.get().get() as i32; let new_control = cpp!(unsafe [ pos as "QPoint", value as "int", page_size as "int", max as "int", size as "QSize", active_controls as "int", pressed as "bool", horizontal as "bool" ] -> u32 as "int" { ensure_initialized(); QStyleOptionSlider option; initQSliderOptions(option, pressed, true, active_controls, 0, max, -value, false); option.pageStep = page_size; if (!horizontal) { option.state ^= QStyle::State_Horizontal; option.orientation = Qt::Vertical; } auto style = qApp->style(); option.rect = { QPoint{}, size }; return style->hitTestComplexControl(QStyle::CC_ScrollBar, &option, pos, nullptr); }); #[allow(non_snake_case)] let SC_ScrollBarSlider = cpp!(unsafe []->u32 as "int" { return QStyle::SC_ScrollBarSlider;}); let (pos, size) = if horizontal { (pos.x, size.width) } else { (pos.y, size.height) }; let result = match event { MouseEvent::Pressed { .. } => { data.pressed = if horizontal { 1 } else { 2 }; if new_control == SC_ScrollBarSlider { data.pressed_x = pos as f32; data.pressed_val = -value as f32; } data.active_controls = new_control; InputEventResult::GrabMouse } MouseEvent::Exit => { data.pressed = 0; InputEventResult::EventIgnored } MouseEvent::Released { .. } => { data.pressed = 0; let new_val = cpp!(unsafe [active_controls as "int", value as "int", max as "int", page_size as "int"] -> i32 as "int" { switch (active_controls) { case QStyle::SC_ScrollBarAddPage: return -value + page_size; case QStyle::SC_ScrollBarSubPage: return -value - page_size; case QStyle::SC_ScrollBarAddLine: return -value + 3.; case QStyle::SC_ScrollBarSubLine: return -value - 3.; case QStyle::SC_ScrollBarFirst: return 0; case QStyle::SC_ScrollBarLast: return max; default: return -value; } }); value_prop.set(LogicalLength::new(-(new_val.min(max).max(0) as f32))); InputEventResult::EventIgnored } MouseEvent::Moved { .. } => { if data.pressed != 0 && data.active_controls == SC_ScrollBarSlider { let max = max as f32; let new_val = data.pressed_val + ((pos as f32) - data.pressed_x) * (max + (page_size as f32)) / size as f32; value_prop.set(LogicalLength::new(-new_val.min(max).max(0.))); InputEventResult::GrabMouse } else { InputEventResult::EventAccepted } } MouseEvent::Wheel { delta_x, delta_y, .. } => { if horizontal { let max = max as f32; let new_val = value as f32 + delta_x; value_prop.set(LogicalLength::new(new_val.min(0.).max(-max))); } else { let max = max as f32; let new_val = value as f32 + delta_y; value_prop.set(LogicalLength::new(new_val.min(0.).max(-max))); } InputEventResult::EventAccepted } }; self.data.set(data); result }; let pos = event.position().unwrap_or_default(); if pressed == 2 || (pressed == 0 && pos.x > (size.width as f32 - right)) { handle_scrollbar( false, qttypes::QPoint { x: (pos.x - (size.width as f32 - right)) as _, y: (pos.y - top) as _, }, qttypes::QSize { width: (right - left) as _, height: (size.height as f32 - (bottom + top)) as _, }, Self::FIELD_OFFSETS.vertical_value.apply_pin(self), self.vertical_page_size().get() as i32, self.vertical_max().get() as i32, ) } else if pressed == 1 || pos.y > (size.height as f32 - bottom) { handle_scrollbar( true, qttypes::QPoint { x: (pos.x - left) as _, y: (pos.y - (size.height as f32 - bottom)) as _, }, qttypes::QSize { width: (size.width as f32 - (right + left)) as _, height: (bottom - top) as _, }, Self::FIELD_OFFSETS.horizontal_value.apply_pin(self), self.horizontal_page_size().get() as i32, self.horizontal_max().get() as i32, ) } else { Default::default() } } fn key_event( self: Pin<&Self>, _: &KeyEvent, _window_adapter: &Rc, _self_rc: &ItemRc, ) -> KeyEventResult { KeyEventResult::EventIgnored } fn focus_event( self: Pin<&Self>, _: &FocusEvent, _window_adapter: &Rc, _self_rc: &ItemRc, ) -> FocusEventResult { FocusEventResult::FocusIgnored } fn_render! { this dpr size painter widget initial_state => let data = this.data(); let margins = qttypes::QMargins { left: this.native_padding_left().get() as _, top: this.native_padding_top().get() as _, right: this.native_padding_right().get() as _, bottom: this.native_padding_bottom().get() as _, }; let enabled: bool = this.enabled(); let has_focus: bool = this.has_focus(); let frame_around_contents = cpp!(unsafe [ painter as "QPainterPtr*", widget as "QWidget*", size as "QSize", dpr as "float", margins as "QMargins", enabled as "bool", has_focus as "bool", initial_state as "int" ] -> bool as "bool" { ensure_initialized(); QStyleOptionFrame frameOption; frameOption.styleObject = widget; frameOption.state |= QStyle::State(initial_state); frameOption.frameShape = QFrame::StyledPanel; frameOption.lineWidth = 1; frameOption.midLineWidth = 0; frameOption.rect = QRect(QPoint(), size / dpr); frameOption.state |= QStyle::State_Sunken; if (enabled) { frameOption.state |= QStyle::State_Enabled; } else { frameOption.palette.setCurrentColorGroup(QPalette::Disabled); } if (has_focus) frameOption.state |= QStyle::State_HasFocus; //int scrollOverlap = qApp->style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarOverlap, &frameOption, nullptr); bool foac = qApp->style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents, &frameOption, widget); // this assume that the frame size is the same on both side, so that the scrollbar width is (right-left) QSize corner_size = QSize(margins.right() - margins.left(), margins.bottom() - margins.top()); if (foac) { frameOption.rect = QRect(QPoint(), (size / dpr) - corner_size); qApp->style()->drawControl(QStyle::CE_ShapedFrame, &frameOption, painter->get(), widget); frameOption.rect = QRect(frameOption.rect.bottomRight() + QPoint(1, 1), corner_size); qApp->style()->drawPrimitive(QStyle::PE_PanelScrollAreaCorner, &frameOption, painter->get(), widget); } else { qApp->style()->drawControl(QStyle::CE_ShapedFrame, &frameOption, painter->get(), widget); frameOption.rect = QRect(frameOption.rect.bottomRight() + QPoint(1, 1) - QPoint(margins.right(), margins.bottom()), corner_size); qApp->style()->drawPrimitive(QStyle::PE_PanelScrollAreaCorner, &frameOption, painter->get(), widget); } return foac; }); let draw_scrollbar = |horizontal: bool, rect: qttypes::QRectF, value: i32, page_size: i32, max: i32, active_controls: u32, pressed: bool, initial_state: i32| { cpp!(unsafe [ painter as "QPainterPtr*", widget as "QWidget*", value as "int", page_size as "int", max as "int", rect as "QRectF", active_controls as "int", pressed as "bool", dpr as "float", horizontal as "bool", has_focus as "bool", initial_state as "int" ] { QPainter *painter_ = painter->get(); auto r = rect.toAlignedRect(); // The mac style may crash on invalid rectangles (#595) if (!r.isValid()) return; // The mac style ignores painter translations (due to CGContextRef redirection) as well as // option.rect's top-left - hence this hack with an intermediate buffer. #if defined(Q_OS_MAC) QImage scrollbar_image(r.size(), QImage::Format_ARGB32_Premultiplied); scrollbar_image.fill(Qt::transparent); {QPainter p(&scrollbar_image); QPainter *painter_ = &p; #else painter_->save(); auto cleanup = qScopeGuard([&] { painter_->restore(); }); painter_->translate(r.topLeft()); // There is bugs in the styles if the scrollbar is not in (0,0) #endif QStyleOptionSlider option; option.state |= QStyle::State(initial_state); option.rect = QRect(QPoint(), r.size()); initQSliderOptions(option, pressed, true, active_controls, 0, max / dpr, -value / dpr, false); option.subControls = QStyle::SC_All; option.pageStep = page_size / dpr; if (has_focus) option.state |= QStyle::State_HasFocus; if (!horizontal) { option.state ^= QStyle::State_Horizontal; option.orientation = Qt::Vertical; } auto style = qApp->style(); style->drawComplexControl(QStyle::CC_ScrollBar, &option, painter_, widget); #if defined(Q_OS_MAC) } (painter_)->drawImage(r.topLeft(), scrollbar_image); #endif }); }; let scrollbars_width = (margins.right - margins.left) as f32; let scrollbars_height = (margins.bottom - margins.top) as f32; draw_scrollbar( false, qttypes::QRectF { x: ((size.width as f32 / dpr) - if frame_around_contents { scrollbars_width } else { margins.right as _ }) as _, y: (if frame_around_contents { 0 } else { margins.top }) as _, width: scrollbars_width as _, height: ((size.height as f32 / dpr) - if frame_around_contents { scrollbars_height } else { (margins.bottom + margins.top) as f32 }) as _, }, this.vertical_value().get() as i32, this.vertical_page_size().get() as i32, this.vertical_max().get() as i32, data.active_controls, data.pressed == 2, initial_state ); draw_scrollbar( true, qttypes::QRectF { x: (if frame_around_contents { 0 } else { margins.left }) as _, y: ((size.height as f32 / dpr) - if frame_around_contents { scrollbars_height } else { margins.bottom as _ }) as _, width: ((size.width as f32 / dpr) - if frame_around_contents { scrollbars_width } else { (margins.left + margins.right) as _ }) as _, height: (scrollbars_height) as _, }, this.horizontal_value().get() as i32, this.horizontal_page_size().get() as i32, this.horizontal_max().get() as i32, data.active_controls, data.pressed == 1, initial_state ); } } impl ItemConsts for NativeScrollView { const cached_rendering_data_offset: const_field_offset::FieldOffset = Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); } declare_item_vtable! { fn slint_get_NativeScrollViewVTable() -> NativeScrollViewVTable for NativeScrollView }