/* LICENSE BEGIN This file is part of the SixtyFPS Project -- https://sixtyfps.io Copyright (c) 2021 Olivier Goffart Copyright (c) 2021 Simon Hausmann SPDX-License-Identifier: GPL-3.0-only This file is also available under commercial licensing terms. Please contact info@sixtyfps.io for more information. LICENSE END */ /*! This module contains all the native Qt widget implementation that forwards to QStyle. Same as in sixtyfps_corelib::items, when When adding an item or a property, it needs to be kept in sync with different place. - It needs to be changed in this module - the Widget list in lib.rs - In the compiler: builtins.60 - For the C++ code (new item only): the build.rs to export the new item, and the `using` declaration in sixtyfps.h - Don't forget to update the documentation */ #![allow(non_upper_case_globals)] use const_field_offset::FieldOffsets; use core::pin::Pin; use cpp::cpp; use sixtyfps_corelib::graphics::{Color, Rect}; use sixtyfps_corelib::input::{ FocusEvent, InputEventFilterResult, InputEventResult, KeyEvent, KeyEventResult, MouseEvent, }; use sixtyfps_corelib::item_rendering::{CachedRenderingData, ItemRenderer}; use sixtyfps_corelib::items::{Item, ItemConsts, ItemRc, ItemVTable, VoidArg}; use sixtyfps_corelib::layout::{LayoutInfo, Orientation}; use sixtyfps_corelib::rtti::*; use sixtyfps_corelib::window::WindowRc; use sixtyfps_corelib::{ declare_item_vtable, Callback, ItemVTable_static, Property, SharedString, SharedVector, }; use sixtyfps_corelib_macros::*; use std::rc::Rc; type ItemRendererRef<'a> = &'a mut dyn ItemRenderer; use crate::qt_window::QPainter; /// Helper macro to get the size from the width and height property, /// and return Default::default in case the size is too small macro_rules! get_size { ($self:ident) => {{ let width = $self.width(); let height = $self.height(); if width < 1. || height < 1. { return Default::default(); }; qttypes::QSize { width: width as _, height: height as _ } }}; } macro_rules! fn_render { ($this:ident $dpr:ident $size:ident $painter:ident => $($tt:tt)*) => { fn render(self: Pin<&Self>, backend: &mut &mut dyn ItemRenderer) { let $dpr: f32 = backend.scale_factor(); if let Some(painter) = ::downcast_mut::(backend.as_any()) { let $size: qttypes::QSize = get_size!(self); let $this = self; painter.save_state(); let $painter = painter; $($tt)* $painter.restore_state(); } else { // Fallback: this happen when the Qt backend is not used and the gl backend is used instead backend.draw_cached_pixmap( &self.cached_rendering_data, &mut |callback| { let width = self.width() * $dpr; let height = self.height() * $dpr; if width < 1. || height < 1. { return Default::default(); }; let $size = qttypes::QSize { width: width as _, height: height as _ }; let mut imgarray = QImageWrapArray::new($size, $dpr); let img = &mut imgarray.img; let mut painter_ = cpp!(unsafe [img as "QImage*"] -> QPainter as "QPainter" { return QPainter(img); }); let $painter = &mut painter_; let $this = self; $($tt)* drop(painter_); imgarray.draw(callback); }, ); } } }; } struct QImageWrapArray { /// The image reference the array, so the array must outlive the image without being detached or accessed img: qttypes::QImage, array: SharedVector, } impl QImageWrapArray { pub fn new(size: qttypes::QSize, dpr: f32) -> Self { let mut array = SharedVector::default(); array.resize((size.width * size.height * 4) as usize, 0); let array_ptr = array.as_slice_mut().as_mut_ptr(); let img = cpp!(unsafe [size as "QSize", array_ptr as "uchar*", dpr as "float"] -> qttypes::QImage as "QImage" { QImage img(array_ptr, size.width(), size.height(), size.width() * 4, QImage::Format_ARGB32_Premultiplied); img.setDevicePixelRatio(dpr); return img; }); QImageWrapArray { img, array } } pub fn draw(&self, callback: &mut dyn FnMut(u32, u32, &[u8])) { let size = self.img.size(); callback(size.width, size.height, self.array.as_slice()); } } cpp! {{ #include #include #include #include #include #include #include #include #include void ensure_initialized() { static auto app [[maybe_unused]] = []{ if (qApp) { return qApp; } QCoreApplication::setAttribute(Qt::AA_PluginApplication, true); static int argc = 1; static char argv[] = "sixtyfps"; static char *argv2[] = { argv }; // Leak the QApplication, otherwise it crashes on exit // (because the QGuiApplication destructor access some Q_GLOBAL_STATIC which are already gone) return new QApplication(argc, argv2); }(); } }} #[repr(C)] #[derive(FieldOffsets, Default, SixtyFPSElement)] #[pin] pub struct NativeButton { pub x: Property, pub y: Property, pub width: Property, pub height: Property, pub text: Property, pub icon: Property, pub enabled: Property, pub pressed: Property, pub clicked: Callback, pub cached_rendering_data: CachedRenderingData, } impl Item for NativeButton { fn init(self: Pin<&Self>, _window: &WindowRc) {} fn geometry(self: Pin<&Self>) -> Rect { euclid::rect(self.x(), self.y(), self.width(), self.height()) } fn layouting_info( self: Pin<&Self>, orientation: Orientation, _window: &WindowRc, ) -> LayoutInfo { let mut text: qttypes::QString = self.text().as_str().into(); let icon: qttypes::QPixmap = crate::qt_window::load_image_from_resource( (&self.icon()).into(), None, Default::default(), ) .unwrap_or_default(); let size = cpp!(unsafe [ mut text as "QString", icon as "QPixmap" ] -> qttypes::QSize as "QSize" { ensure_initialized(); QStyleOptionButton option; if (text.isEmpty()) text = "**"; option.rect = option.fontMetrics.boundingRect(text); option.text = std::move(text); option.icon = icon; return qApp->style()->sizeFromContents(QStyle::CT_PushButton, &option, option.rect.size(), nullptr); }); LayoutInfo { min: match orientation { Orientation::Horizontal => size.width as f32, Orientation::Vertical => size.height as f32, }, ..LayoutInfo::default() } } fn input_event_filter_before_children( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &ItemRc, ) -> InputEventFilterResult { InputEventFilterResult::ForwardEvent } fn input_event( self: Pin<&Self>, event: MouseEvent, _window: &WindowRc, _self_rc: &sixtyfps_corelib::items::ItemRc, ) -> InputEventResult { let enabled = self.enabled(); if !enabled { return InputEventResult::EventIgnored; } Self::FIELD_OFFSETS.pressed.apply_pin(self).set(match event { MouseEvent::MousePressed { .. } => true, MouseEvent::MouseExit | MouseEvent::MouseReleased { .. } => false, MouseEvent::MouseMoved { .. } => { return if self.pressed() { InputEventResult::GrabMouse } else { InputEventResult::EventIgnored } } MouseEvent::MouseWheel { .. } => return InputEventResult::EventIgnored, }); if matches!(event, MouseEvent::MouseReleased { .. }) { Self::FIELD_OFFSETS.clicked.apply_pin(self).call(&()); InputEventResult::EventAccepted } else { InputEventResult::GrabMouse } } fn key_event(self: Pin<&Self>, _: &KeyEvent, _window: &WindowRc) -> KeyEventResult { KeyEventResult::EventIgnored } fn focus_event(self: Pin<&Self>, _: &FocusEvent, _window: &WindowRc) {} fn_render! { this dpr size painter => let down: bool = this.pressed(); let text: qttypes::QString = this.text().as_str().into(); let icon : qttypes::QPixmap = crate::qt_window::load_image_from_resource( (&this.icon()).into(), None, Default::default(), ) .unwrap_or_default(); let enabled = this.enabled(); cpp!(unsafe [ painter as "QPainter*", text as "QString", icon as "QPixmap", enabled as "bool", size as "QSize", down as "bool", dpr as "float" ] { QStyleOptionButton option; option.text = std::move(text); option.icon = icon; option.rect = QRect(QPoint(), size / dpr); if (down) option.state |= QStyle::State_Sunken; else option.state |= QStyle::State_Raised; if (enabled) { option.state |= QStyle::State_Enabled; } else { option.palette.setCurrentColorGroup(QPalette::Disabled); } qApp->style()->drawControl(QStyle::CE_PushButton, &option, painter, nullptr); }); } } impl ItemConsts for NativeButton { const cached_rendering_data_offset: const_field_offset::FieldOffset = Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); } declare_item_vtable! { fn sixtyfps_get_NativeButtonVTable() -> NativeButtonVTable for NativeButton } #[repr(C)] #[derive(FieldOffsets, Default, SixtyFPSElement)] #[pin] pub struct NativeCheckBox { pub x: Property, pub y: Property, pub width: Property, pub height: Property, pub enabled: Property, pub toggled: Callback, pub text: Property, pub checked: Property, pub cached_rendering_data: CachedRenderingData, } impl Item for NativeCheckBox { fn init(self: Pin<&Self>, _window: &WindowRc) {} fn geometry(self: Pin<&Self>) -> Rect { euclid::rect(self.x(), self.y(), self.width(), self.height()) } fn layouting_info( self: Pin<&Self>, orientation: Orientation, _window: &WindowRc, ) -> LayoutInfo { let text: qttypes::QString = self.text().as_str().into(); let size = cpp!(unsafe [ text as "QString" ] -> qttypes::QSize as "QSize" { ensure_initialized(); QStyleOptionButton option; option.rect = option.fontMetrics.boundingRect(text); option.text = std::move(text); return qApp->style()->sizeFromContents(QStyle::CT_CheckBox, &option, option.rect.size(), nullptr); }); match orientation { Orientation::Horizontal => { LayoutInfo { min: size.width as f32, stretch: 1., ..LayoutInfo::default() } } Orientation::Vertical => LayoutInfo { min: size.height as f32, max: size.height as f32, ..LayoutInfo::default() }, } } fn input_event_filter_before_children( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &ItemRc, ) -> InputEventFilterResult { InputEventFilterResult::ForwardEvent } fn input_event( self: Pin<&Self>, event: MouseEvent, _window: &WindowRc, _self_rc: &sixtyfps_corelib::items::ItemRc, ) -> InputEventResult { if !self.enabled() { return InputEventResult::EventIgnored; } if matches!(event, MouseEvent::MouseReleased { .. }) { Self::FIELD_OFFSETS.checked.apply_pin(self).set(!self.checked()); Self::FIELD_OFFSETS.toggled.apply_pin(self).call(&()) } InputEventResult::EventAccepted } fn key_event(self: Pin<&Self>, _: &KeyEvent, _window: &WindowRc) -> KeyEventResult { KeyEventResult::EventIgnored } fn focus_event(self: Pin<&Self>, _: &FocusEvent, _window: &WindowRc) {} fn_render! { this dpr size painter => let checked: bool = this.checked(); let enabled = this.enabled(); let text: qttypes::QString = this.text().as_str().into(); cpp!(unsafe [ painter as "QPainter*", enabled as "bool", text as "QString", size as "QSize", checked as "bool", dpr as "float" ] { QStyleOptionButton option; option.text = std::move(text); option.rect = QRect(QPoint(), size / dpr); option.state |= checked ? QStyle::State_On : QStyle::State_Off; if (enabled) { option.state |= QStyle::State_Enabled; } else { option.palette.setCurrentColorGroup(QPalette::Disabled); } qApp->style()->drawControl(QStyle::CE_CheckBox, &option, painter, nullptr); }); } } impl ItemConsts for NativeCheckBox { const cached_rendering_data_offset: const_field_offset::FieldOffset = Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); } declare_item_vtable! { fn sixtyfps_get_NativeCheckBoxVTable() -> NativeCheckBoxVTable for NativeCheckBox } #[derive(Default, Copy, Clone, Debug, PartialEq)] #[repr(C)] struct NativeSpinBoxData { active_controls: u32, pressed: bool, } #[repr(C)] #[derive(FieldOffsets, Default, SixtyFPSElement)] #[pin] pub struct NativeSpinBox { pub x: Property, pub y: Property, pub width: Property, pub height: Property, pub enabled: Property, pub value: Property, pub minimum: Property, pub maximum: Property, pub cached_rendering_data: CachedRenderingData, data: Property, } cpp! {{ void initQSpinBoxOptions(QStyleOptionSpinBox &option, bool pressed, bool enabled, int active_controls) { auto style = qApp->style(); option.activeSubControls = QStyle::SC_None; option.subControls = QStyle::SC_SpinBoxEditField | QStyle::SC_SpinBoxUp | QStyle::SC_SpinBoxDown; if (style->styleHint(QStyle::SH_SpinBox_ButtonsInsideFrame, nullptr, nullptr)) option.subControls |= QStyle::SC_SpinBoxFrame; option.activeSubControls = {active_controls}; if (enabled) { option.state |= QStyle::State_Enabled; } else { option.palette.setCurrentColorGroup(QPalette::Disabled); } option.state |= QStyle::State_Active; if (pressed) { option.state |= QStyle::State_Sunken | QStyle::State_MouseOver; } /*if (active_controls) { option.state |= QStyle::State_MouseOver; }*/ option.stepEnabled = QAbstractSpinBox::StepDownEnabled | QAbstractSpinBox::StepUpEnabled; option.frame = true; } }} impl Item for NativeSpinBox { fn init(self: Pin<&Self>, _window: &WindowRc) {} fn geometry(self: Pin<&Self>) -> Rect { euclid::rect(self.x(), self.y(), self.width(), self.height()) } fn layouting_info( self: Pin<&Self>, orientation: Orientation, _window: &WindowRc, ) -> LayoutInfo { //let value: i32 = self.value(); let data = self.data(); let active_controls = data.active_controls; let pressed = data.pressed; let enabled = self.enabled(); let size = cpp!(unsafe [ //value as "int", active_controls as "int", pressed as "bool", enabled as "bool" ] -> qttypes::QSize as "QSize" { ensure_initialized(); auto style = qApp->style(); QStyleOptionSpinBox option; initQSpinBoxOptions(option, pressed, enabled, active_controls); auto content = option.fontMetrics.boundingRect("0000"); return style->sizeFromContents(QStyle::CT_SpinBox, &option, content.size(), nullptr); }); match orientation { Orientation::Horizontal => { LayoutInfo { min: size.width as f32, stretch: 1., ..LayoutInfo::default() } } Orientation::Vertical => LayoutInfo { min: size.height as f32, max: size.height as f32, ..LayoutInfo::default() }, } } fn input_event_filter_before_children( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &ItemRc, ) -> InputEventFilterResult { InputEventFilterResult::ForwardEvent } fn input_event( self: Pin<&Self>, event: MouseEvent, _window: &WindowRc, _self_rc: &sixtyfps_corelib::items::ItemRc, ) -> InputEventResult { let size: qttypes::QSize = get_size!(self); let enabled = self.enabled(); let mut data = self.data(); let active_controls = data.active_controls; let pressed = data.pressed; let pos = event.pos().map(|p| qttypes::QPoint { x: p.x as _, y: p.y as _ }).unwrap_or_default(); let new_control = cpp!(unsafe [ pos as "QPoint", size as "QSize", enabled as "bool", active_controls as "int", pressed as "bool" ] -> u32 as "int" { ensure_initialized(); auto style = qApp->style(); QStyleOptionSpinBox option; option.rect = { QPoint{}, size }; initQSpinBoxOptions(option, pressed, enabled, active_controls); return style->hitTestComplexControl(QStyle::CC_SpinBox, &option, pos, nullptr); }); let changed = new_control != active_controls || match event { MouseEvent::MousePressed { .. } => { data.pressed = true; true } MouseEvent::MouseExit => { data.pressed = false; true } MouseEvent::MouseReleased { .. } => { data.pressed = false; if new_control == cpp!(unsafe []->u32 as "int" { return QStyle::SC_SpinBoxUp;}) && enabled { let v = self.value(); if v < self.maximum() { self.value.set(v + 1); } } if new_control == cpp!(unsafe []->u32 as "int" { return QStyle::SC_SpinBoxDown;}) && enabled { let v = self.value(); if v > self.minimum() { self.value.set(v - 1); } } true } MouseEvent::MouseMoved { .. } => false, MouseEvent::MouseWheel { .. } => false, // TODO }; data.active_controls = new_control; if changed { self.data.set(data); } InputEventResult::EventAccepted } fn key_event(self: Pin<&Self>, _: &KeyEvent, _window: &WindowRc) -> KeyEventResult { KeyEventResult::EventIgnored } fn focus_event(self: Pin<&Self>, _: &FocusEvent, _window: &WindowRc) {} fn_render! { this dpr size painter => let value: i32 = this.value(); let enabled = this.enabled(); let data = this.data(); let active_controls = data.active_controls; let pressed = data.pressed; cpp!(unsafe [ painter as "QPainter*", value as "int", enabled as "bool", size as "QSize", active_controls as "int", pressed as "bool", dpr as "float" ] { auto style = qApp->style(); QStyleOptionSpinBox option; option.rect = QRect(QPoint(), size / dpr); initQSpinBoxOptions(option, pressed, enabled, active_controls); style->drawComplexControl(QStyle::CC_SpinBox, &option, painter, nullptr); auto text_rect = style->subControlRect(QStyle::CC_SpinBox, &option, QStyle::SC_SpinBoxEditField, nullptr); painter->setPen(option.palette.color(QPalette::Text)); painter->drawText(text_rect, QString::number(value)); }); } } impl ItemConsts for NativeSpinBox { const cached_rendering_data_offset: const_field_offset::FieldOffset = Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); } declare_item_vtable! { fn sixtyfps_get_NativeSpinBoxVTable() -> NativeSpinBoxVTable for NativeSpinBox } #[derive(Default, Copy, Clone, Debug, PartialEq)] #[repr(C)] struct NativeSliderData { active_controls: u32, /// For sliders, this is a bool, For scroll area: 1 == horizontal, 2 == vertical pressed: u8, pressed_x: f32, pressed_val: f32, } type FloatArg = (f32,); #[repr(C)] #[derive(FieldOffsets, Default, SixtyFPSElement)] #[pin] pub struct NativeSlider { pub x: Property, pub y: Property, pub width: Property, pub height: Property, pub enabled: Property, pub value: Property, pub minimum: Property, pub maximum: Property, pub cached_rendering_data: CachedRenderingData, data: Property, pub changed: Callback, } cpp! {{ void initQSliderOptions(QStyleOptionSlider &option, bool pressed, bool enabled, int active_controls, int minimum, int maximum, int value) { option.subControls = QStyle::SC_SliderGroove | QStyle::SC_SliderHandle; option.activeSubControls = { active_controls }; option.orientation = Qt::Horizontal; option.maximum = maximum; option.minimum = minimum; option.sliderPosition = value; option.sliderValue = value; if (enabled) { option.state |= QStyle::State_Enabled; } else { option.palette.setCurrentColorGroup(QPalette::Disabled); } option.state |= QStyle::State_Active | QStyle::State_Horizontal; if (pressed) { option.state |= QStyle::State_Sunken | QStyle::State_MouseOver; } } }} impl Item for NativeSlider { fn init(self: Pin<&Self>, _window: &WindowRc) {} fn geometry(self: Pin<&Self>) -> Rect { euclid::rect(self.x(), self.y(), self.width(), self.height()) } fn layouting_info( self: Pin<&Self>, orientation: Orientation, _window: &WindowRc, ) -> LayoutInfo { let enabled = self.enabled(); let value = self.value() as i32; let min = self.minimum() as i32; let max = self.maximum() as i32; let data = self.data(); let active_controls = data.active_controls; let pressed = data.pressed; let size = cpp!(unsafe [ enabled as "bool", value as "int", min as "int", max as "int", active_controls as "int", pressed as "bool" ] -> qttypes::QSize as "QSize" { ensure_initialized(); QStyleOptionSlider option; initQSliderOptions(option, pressed, enabled, active_controls, min, max, value); auto style = qApp->style(); auto thick = style->pixelMetric(QStyle::PM_SliderThickness, &option, nullptr); return style->sizeFromContents(QStyle::CT_Slider, &option, QSize(0, thick), nullptr); }); match orientation { Orientation::Horizontal => { LayoutInfo { min: size.width as f32, stretch: 1., ..LayoutInfo::default() } } Orientation::Vertical => LayoutInfo { min: size.height as f32, max: size.height as f32, ..LayoutInfo::default() }, } } fn input_event_filter_before_children( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &ItemRc, ) -> InputEventFilterResult { InputEventFilterResult::ForwardEvent } fn input_event( self: Pin<&Self>, event: MouseEvent, _window: &WindowRc, _self_rc: &sixtyfps_corelib::items::ItemRc, ) -> InputEventResult { let size: qttypes::QSize = get_size!(self); let enabled = self.enabled(); let value = self.value() as f32; let min = self.minimum() as f32; let max = self.maximum() as f32; let mut data = self.data(); let active_controls = data.active_controls; let pressed: bool = data.pressed != 0; let pos = event.pos().map(|p| qttypes::QPoint { x: p.x as _, y: p.y as _ }).unwrap_or_default(); let new_control = cpp!(unsafe [ pos as "QPoint", size as "QSize", enabled as "bool", value as "float", min as "float", max as "float", active_controls as "int", pressed as "bool" ] -> u32 as "int" { ensure_initialized(); QStyleOptionSlider option; initQSliderOptions(option, pressed, enabled, active_controls, min, max, value); auto style = qApp->style(); option.rect = { QPoint{}, size }; return style->hitTestComplexControl(QStyle::CC_Slider, &option, pos, nullptr); }); let result = match event { MouseEvent::MousePressed { pos } if enabled => { data.pressed_x = pos.x as f32; data.pressed = 1; data.pressed_val = value; InputEventResult::GrabMouse } MouseEvent::MouseExit | MouseEvent::MouseReleased { .. } => { data.pressed = 0; InputEventResult::EventAccepted } MouseEvent::MouseMoved { pos } if enabled => { if data.pressed != 0 { // FIXME: use QStyle::subControlRect to find out the actual size of the groove let new_val = data.pressed_val + ((pos.x as f32) - data.pressed_x) * (max - min) / size.width as f32; let new_val = new_val.max(min).min(max); self.value.set(new_val); Self::FIELD_OFFSETS.changed.apply_pin(self).call(&(new_val,)); InputEventResult::GrabMouse } else { InputEventResult::EventIgnored } } _ => { assert!(!enabled); data.pressed = 0; InputEventResult::EventIgnored } }; data.active_controls = new_control; self.data.set(data); result } fn key_event(self: Pin<&Self>, _: &KeyEvent, _window: &WindowRc) -> KeyEventResult { KeyEventResult::EventIgnored } fn focus_event(self: Pin<&Self>, _: &FocusEvent, _window: &WindowRc) {} fn_render! { this dpr size painter => let enabled = this.enabled(); let value = this.value() as i32; let min = this.minimum() as i32; let max = this.maximum() as i32; let data = this.data(); let active_controls = data.active_controls; let pressed = data.pressed; cpp!(unsafe [ painter as "QPainter*", enabled as "bool", value as "int", min as "int", max as "int", size as "QSize", active_controls as "int", pressed as "bool", dpr as "float" ] { QStyleOptionSlider option; option.rect = QRect(QPoint(), size / dpr); initQSliderOptions(option, pressed, enabled, active_controls, min, max, value); auto style = qApp->style(); style->drawComplexControl(QStyle::CC_Slider, &option, painter, nullptr); }); } } impl ItemConsts for NativeSlider { const cached_rendering_data_offset: const_field_offset::FieldOffset = Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); } declare_item_vtable! { fn sixtyfps_get_NativeSliderVTable() -> NativeSliderVTable for NativeSlider } #[repr(C)] #[derive(FieldOffsets, Default, SixtyFPSElement)] #[pin] pub struct NativeGroupBox { pub x: Property, pub y: Property, pub width: Property, pub height: Property, pub enabled: Property, pub title: 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, } #[repr(C)] #[derive(FieldOffsets, Default)] #[pin] struct GroupBoxData { title: Property, paddings: Property, } impl Item for NativeGroupBox { fn init(self: Pin<&Self>, _window: &WindowRc) { let shared_data = Rc::pin(GroupBoxData::default()); Property::link_two_way( Self::FIELD_OFFSETS.title.apply_pin(self), GroupBoxData::FIELD_OFFSETS.title.apply_pin(shared_data.as_ref()), ); shared_data.paddings.set_binding({ let shared_data_weak = pin_weak::rc::PinWeak::downgrade(shared_data.clone()); move || { let shared_data = shared_data_weak.upgrade().unwrap(); let text: qttypes::QString = GroupBoxData::FIELD_OFFSETS.title.apply_pin(shared_data.as_ref()).get().as_str().into(); cpp!(unsafe [ text as "QString" ] -> qttypes::QMargins as "QMargins" { ensure_initialized(); QStyleOptionGroupBox option; option.text = text; option.lineWidth = 1; option.midLineWidth = 0; option.subControls = QStyle::SC_GroupBoxFrame; if (!text.isEmpty()) { option.subControls |= QStyle::SC_GroupBoxLabel; } // Just some size big enough to be sure that the frame fits in it option.rect = QRect(0, 0, 10000, 10000); option.textColor = QColor(qApp->style()->styleHint( QStyle::SH_GroupBox_TextLabelColor, &option)); QRect contentsRect = qApp->style()->subControlRect( QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxContents); //QRect elementRect = qApp->style()->subElementRect( // QStyle::SE_GroupBoxLayoutItem, &option); auto hs = qApp->style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing, &option); auto vs = qApp->style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing, &option); return { (contentsRect.left() + hs), (contentsRect.top() + vs), (option.rect.right() - contentsRect.right() + hs), (option.rect.bottom() - contentsRect.bottom() + vs) }; }) } }); self.native_padding_left.set_binding({ let shared_data = shared_data.clone(); move || { let margins = GroupBoxData::FIELD_OFFSETS.paddings.apply_pin(shared_data.as_ref()).get(); margins.left as _ } }); self.native_padding_right.set_binding({ let shared_data = shared_data.clone(); move || { let margins = GroupBoxData::FIELD_OFFSETS.paddings.apply_pin(shared_data.as_ref()).get(); margins.right as _ } }); self.native_padding_top.set_binding({ let shared_data = shared_data.clone(); move || { let margins = GroupBoxData::FIELD_OFFSETS.paddings.apply_pin(shared_data.as_ref()).get(); margins.top as _ } }); self.native_padding_bottom.set_binding({ move || { let margins = GroupBoxData::FIELD_OFFSETS.paddings.apply_pin(shared_data.as_ref()).get(); margins.bottom as _ } }); } fn geometry(self: Pin<&Self>) -> Rect { euclid::rect(self.x(), self.y(), self.width(), self.height()) } fn layouting_info( self: Pin<&Self>, orientation: Orientation, _window: &WindowRc, ) -> LayoutInfo { LayoutInfo { min: match orientation { Orientation::Horizontal => self.native_padding_left() + self.native_padding_right(), Orientation::Vertical => self.native_padding_top() + self.native_padding_bottom(), }, stretch: 1., ..LayoutInfo::default() } } fn input_event_filter_before_children( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &ItemRc, ) -> InputEventFilterResult { InputEventFilterResult::ForwardEvent } fn input_event( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &sixtyfps_corelib::items::ItemRc, ) -> InputEventResult { InputEventResult::EventIgnored } fn key_event(self: Pin<&Self>, _: &KeyEvent, _window: &WindowRc) -> KeyEventResult { KeyEventResult::EventIgnored } fn focus_event(self: Pin<&Self>, _: &FocusEvent, _window: &WindowRc) {} fn_render! { this dpr size painter => let text: qttypes::QString = this.title().as_str().into(); let enabled = this.enabled(); cpp!(unsafe [ painter as "QPainter*", text as "QString", enabled as "bool", size as "QSize", dpr as "float" ] { QStyleOptionGroupBox option; if (enabled) { option.state |= QStyle::State_Enabled; } else { option.palette.setCurrentColorGroup(QPalette::Disabled); } option.rect = QRect(QPoint(), size / dpr); option.text = text; option.lineWidth = 1; option.midLineWidth = 0; option.subControls = QStyle::SC_GroupBoxFrame; if (!text.isEmpty()) { option.subControls |= QStyle::SC_GroupBoxLabel; } option.textColor = QColor(qApp->style()->styleHint( QStyle::SH_GroupBox_TextLabelColor, &option)); qApp->style()->drawComplexControl(QStyle::CC_GroupBox, &option, painter, nullptr); }); } } impl ItemConsts for NativeGroupBox { const cached_rendering_data_offset: const_field_offset::FieldOffset = Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); } declare_item_vtable! { fn sixtyfps_get_NativeGroupBoxVTable() -> NativeGroupBoxVTable for NativeGroupBox } #[repr(C)] #[derive(FieldOffsets, Default, SixtyFPSElement)] #[pin] pub struct NativeLineEdit { pub x: Property, pub y: Property, pub width: Property, pub height: 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 focused: Property, pub enabled: Property, } impl Item for NativeLineEdit { fn init(self: Pin<&Self>, _window: &WindowRc) { let paddings = Rc::pin(Property::default()); paddings.as_ref().set_binding(move || { cpp!(unsafe [] -> qttypes::QMargins as "QMargins" { ensure_initialized(); QStyleOptionFrame option; option.state |= QStyle::State_Enabled; option.lineWidth = 1; option.midLineWidth = 0; // Just some size big enough to be sure that the frame fits in it option.rect = QRect(0, 0, 10000, 10000); QRect contentsRect = qApp->style()->subElementRect( QStyle::SE_LineEditContents, &option); // ### remove extra margins return { (2 + contentsRect.left()), (4 + contentsRect.top()), (2 + option.rect.right() - contentsRect.right()), (4 + option.rect.bottom() - contentsRect.bottom()) }; }) }); self.native_padding_left.set_binding({ let paddings = paddings.clone(); move || paddings.as_ref().get().left as _ }); self.native_padding_right.set_binding({ let paddings = paddings.clone(); move || paddings.as_ref().get().right as _ }); self.native_padding_top.set_binding({ let paddings = paddings.clone(); move || paddings.as_ref().get().top as _ }); self.native_padding_bottom.set_binding({ let paddings = paddings; move || paddings.as_ref().get().bottom as _ }); } fn geometry(self: Pin<&Self>) -> Rect { euclid::rect(self.x(), self.y(), self.width(), self.height()) } fn layouting_info( self: Pin<&Self>, orientation: Orientation, _window: &WindowRc, ) -> LayoutInfo { LayoutInfo { min: match orientation { Orientation::Horizontal => self.native_padding_left() + self.native_padding_right(), Orientation::Vertical => self.native_padding_top() + self.native_padding_bottom(), }, stretch: if orientation == Orientation::Horizontal { 1. } else { 0. }, ..LayoutInfo::default() } } fn input_event_filter_before_children( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &ItemRc, ) -> InputEventFilterResult { InputEventFilterResult::ForwardAndIgnore } fn input_event( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &sixtyfps_corelib::items::ItemRc, ) -> InputEventResult { InputEventResult::EventIgnored } fn key_event(self: Pin<&Self>, _: &KeyEvent, _window: &WindowRc) -> KeyEventResult { KeyEventResult::EventIgnored } fn focus_event(self: Pin<&Self>, _: &FocusEvent, _window: &WindowRc) {} fn_render! { this dpr size painter => let focused: bool = this.focused(); let enabled: bool = this.enabled(); cpp!(unsafe [ painter as "QPainter*", size as "QSize", dpr as "float", enabled as "bool", focused as "bool" ] { QStyleOptionFrame option; option.rect = QRect(QPoint(), size / dpr); option.lineWidth = 1; option.midLineWidth = 0; if (focused) option.state |= QStyle::State_HasFocus; if (enabled) { option.state |= QStyle::State_Enabled; } else { option.palette.setCurrentColorGroup(QPalette::Disabled); } qApp->style()->drawPrimitive(QStyle::PE_PanelLineEdit, &option, painter, nullptr); }); } } impl ItemConsts for NativeLineEdit { const cached_rendering_data_offset: const_field_offset::FieldOffset = Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); } declare_item_vtable! { fn sixtyfps_get_NativeLineEditVTable() -> NativeLineEditVTable for NativeLineEdit } #[repr(C)] #[derive(FieldOffsets, Default, SixtyFPSElement)] #[pin] pub struct NativeScrollView { pub x: Property, pub y: Property, pub width: Property, pub height: Property, 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, data: Property, } impl Item for NativeScrollView { fn init(self: Pin<&Self>, _window: &WindowRc) { 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); 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); /*int hscrollOverlap = hbar->style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarOverlap, &opt, hbar); int vscrollOverlap = vbar->style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarOverlap, &opt, vbar);*/ 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 || paddings.as_ref().get().left as _ }); self.native_padding_right.set_binding({ let paddings = paddings.clone(); move || paddings.as_ref().get().right as _ }); self.native_padding_top.set_binding({ let paddings = paddings.clone(); move || paddings.as_ref().get().top as _ }); self.native_padding_bottom.set_binding({ let paddings = paddings; move || paddings.as_ref().get().bottom as _ }); } fn geometry(self: Pin<&Self>) -> Rect { euclid::rect(self.x(), self.y(), self.width(), self.height()) } fn layouting_info( self: Pin<&Self>, orientation: Orientation, _window: &WindowRc, ) -> LayoutInfo { LayoutInfo { min: match orientation { Orientation::Horizontal => self.native_padding_left() + self.native_padding_right(), Orientation::Vertical => self.native_padding_top() + self.native_padding_bottom(), }, stretch: 1., ..LayoutInfo::default() } } fn input_event_filter_before_children( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &ItemRc, ) -> InputEventFilterResult { InputEventFilterResult::ForwardEvent } fn input_event( self: Pin<&Self>, event: MouseEvent, _window: &WindowRc, _self_rc: &sixtyfps_corelib::items::ItemRc, ) -> InputEventResult { let size: qttypes::QSize = get_size!(self); let mut data = self.data(); let active_controls = data.active_controls; let pressed = data.pressed; let left = self.native_padding_left(); let right = self.native_padding_right(); let top = self.native_padding_top(); let bottom = self.native_padding_bottom(); 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() 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); 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::MousePressed { .. } => { 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::MouseExit => { data.pressed = 0; InputEventResult::EventIgnored } MouseEvent::MouseReleased { .. } => { 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(-(new_val.min(max).max(0) as f32)); InputEventResult::EventIgnored } MouseEvent::MouseMoved { .. } => { 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(-new_val.min(max).max(0.)); InputEventResult::GrabMouse } else { InputEventResult::EventAccepted } } MouseEvent::MouseWheel { .. } => { // TODO InputEventResult::EventAccepted } }; self.data.set(data); result }; let pos = event.pos().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() as i32, self.vertical_max() 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() as i32, self.horizontal_max() as i32, ) } else { Default::default() } } fn key_event(self: Pin<&Self>, _: &KeyEvent, _window: &WindowRc) -> KeyEventResult { KeyEventResult::EventIgnored } fn focus_event(self: Pin<&Self>, _: &FocusEvent, _window: &WindowRc) {} fn_render! { this dpr size painter => let data = this.data(); let left = this.native_padding_left(); let right = this.native_padding_right(); let top = this.native_padding_top(); let bottom = this.native_padding_bottom(); let corner_rect = qttypes::QRectF { x: (size.width as f32 / dpr - (right - left)) as _, y: (size.height as f32 / dpr - (bottom - top)) as _, width: (right - left) as _, height: (bottom - top) as _, }; cpp!(unsafe [painter as "QPainter*", corner_rect as "QRectF"] { ensure_initialized(); QStyleOptionFrame frameOption; frameOption.frameShape = QFrame::StyledPanel; frameOption.lineWidth = 1; frameOption.midLineWidth = 0; frameOption.rect = corner_rect.toAlignedRect(); qApp->style()->drawPrimitive(QStyle::PE_PanelScrollAreaCorner, &frameOption, painter, nullptr); frameOption.rect = QRect(QPoint(), corner_rect.toAlignedRect().topLeft()); qApp->style()->drawControl(QStyle::CE_ShapedFrame, &frameOption, painter, nullptr); }); let draw_scrollbar = |horizontal: bool, rect: qttypes::QRectF, value: i32, page_size: i32, max: i32, active_controls: u32, pressed: bool| { cpp!(unsafe [ painter as "QPainter*", 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" ] { auto r = rect.toAlignedRect(); // 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.rect = QRect(QPoint(), r.size()); initQSliderOptions(option, pressed, true, active_controls, 0, max / dpr, -value / dpr); option.subControls = QStyle::SC_All; option.pageStep = page_size / dpr; if (!horizontal) { option.state ^= QStyle::State_Horizontal; option.orientation = Qt::Vertical; } auto style = qApp->style(); style->drawComplexControl(QStyle::CC_ScrollBar, &option, painter, nullptr); #if defined(Q_OS_MAC) } painter->drawImage(r.topLeft(), scrollbar_image); #endif }); }; draw_scrollbar( false, qttypes::QRectF { x: ((size.width as f32 - right + left) / dpr) as _, y: 0., width: ((right - left) / dpr) as _, height: ((size.height as f32 - bottom + top) / dpr) as _, }, this.vertical_value() as i32, this.vertical_page_size() as i32, this.vertical_max() as i32, data.active_controls, data.pressed == 2, ); draw_scrollbar( true, qttypes::QRectF { x: 0., y: ((size.height as f32 - bottom + top) / dpr) as _, width: ((size.width as f32 - right + left) / dpr) as _, height: ((bottom - top) / dpr) as _, }, this.horizontal_value() as i32, this.horizontal_page_size() as i32, this.horizontal_max() as i32, data.active_controls, data.pressed == 1, ); } } 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 sixtyfps_get_NativeScrollViewVTable() -> NativeScrollViewVTable for NativeScrollView } #[repr(C)] #[derive(FieldOffsets, Default, SixtyFPSElement)] #[pin] pub struct NativeStandardListViewItem { pub x: Property, pub y: Property, pub width: Property, pub height: Property, pub item: Property, pub index: Property, pub is_selected: Property, pub cached_rendering_data: CachedRenderingData, } impl Item for NativeStandardListViewItem { fn init(self: Pin<&Self>, _window: &WindowRc) {} fn geometry(self: Pin<&Self>) -> Rect { euclid::rect(self.x(), self.y(), self.width(), self.height()) } fn layouting_info( self: Pin<&Self>, orientation: Orientation, _window: &WindowRc, ) -> LayoutInfo { let index: i32 = self.index(); let item = self.item(); let text: qttypes::QString = item.text.as_str().into(); let s = cpp!(unsafe [ index as "int", text as "QString" ] -> qttypes::QSize as "QSize" { ensure_initialized(); QStyleOptionViewItem option; option.decorationPosition = QStyleOptionViewItem::Left; option.decorationAlignment = Qt::AlignCenter; option.displayAlignment = Qt::AlignLeft|Qt::AlignVCenter; option.showDecorationSelected = qApp->style()->styleHint(QStyle::SH_ItemView_ShowDecorationSelected, nullptr, nullptr); if (index % 2) { option.features |= QStyleOptionViewItem::Alternate; } option.features |= QStyleOptionViewItem::HasDisplay; option.text = text; return qApp->style()->sizeFromContents(QStyle::CT_ItemViewItem, &option, QSize{}, nullptr); }); let min = match orientation { Orientation::Horizontal => s.width, Orientation::Vertical => s.height, } as f32; LayoutInfo { min, preferred: min, ..LayoutInfo::default() } } fn input_event_filter_before_children( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &ItemRc, ) -> InputEventFilterResult { InputEventFilterResult::ForwardAndIgnore } fn input_event( self: Pin<&Self>, _event: MouseEvent, _window: &WindowRc, _self_rc: &sixtyfps_corelib::items::ItemRc, ) -> InputEventResult { InputEventResult::EventIgnored } fn key_event(self: Pin<&Self>, _: &KeyEvent, _window: &WindowRc) -> KeyEventResult { KeyEventResult::EventIgnored } fn focus_event(self: Pin<&Self>, _: &FocusEvent, _window: &WindowRc) {} fn_render! { this dpr size painter => let index: i32 = this.index(); let is_selected: bool = this.is_selected(); let item = this.item(); let text: qttypes::QString = item.text.as_str().into(); cpp!(unsafe [ painter as "QPainter*", size as "QSize", dpr as "float", index as "int", is_selected as "bool", text as "QString" ] { QStyleOptionViewItem option; option.rect = QRect(QPoint(), size / dpr); option.state = QStyle::State_Enabled | QStyle::State_Active; if (is_selected) { option.state |= QStyle::State_Selected; } option.decorationPosition = QStyleOptionViewItem::Left; option.decorationAlignment = Qt::AlignCenter; option.displayAlignment = Qt::AlignLeft|Qt::AlignVCenter; option.showDecorationSelected = qApp->style()->styleHint(QStyle::SH_ItemView_ShowDecorationSelected, nullptr, nullptr); if (index % 2) { option.features |= QStyleOptionViewItem::Alternate; } option.features |= QStyleOptionViewItem::HasDisplay; option.text = text; // CE_ItemViewItem in QCommonStyle calls setClipRect on the painter and replace the clips. So we need to cheat. auto engine = painter->paintEngine(); auto old_clip = engine->systemClip(); auto new_clip = old_clip & (painter->clipRegion() * painter->transform()); if (new_clip.isEmpty()) return; engine->setSystemClip(new_clip); qApp->style()->drawPrimitive(QStyle::PE_PanelItemViewRow, &option, painter, nullptr); qApp->style()->drawControl(QStyle::CE_ItemViewItem, &option, painter, nullptr); engine->setSystemClip(old_clip); }); } } impl ItemConsts for NativeStandardListViewItem { const cached_rendering_data_offset: const_field_offset::FieldOffset = Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); } declare_item_vtable! { fn sixtyfps_get_NativeStandardListViewItemVTable() -> NativeStandardListViewItemVTable for NativeStandardListViewItem } #[repr(C)] #[derive(FieldOffsets, Default, SixtyFPSElement)] #[pin] pub struct NativeComboBox { pub x: Property, pub y: Property, pub width: Property, pub height: Property, pub enabled: Property, pub pressed: Property, pub is_open: Property, pub current_value: Property, pub cached_rendering_data: CachedRenderingData, pub open_popup: Callback, } impl Item for NativeComboBox { fn init(self: Pin<&Self>, _window: &WindowRc) {} fn geometry(self: Pin<&Self>) -> Rect { euclid::rect(self.x(), self.y(), self.width(), self.height()) } fn layouting_info( self: Pin<&Self>, orientation: Orientation, _window: &WindowRc, ) -> LayoutInfo { let size = cpp!(unsafe [] -> qttypes::QSize as "QSize" { ensure_initialized(); QStyleOptionComboBox option; // FIXME option.rect = option.fontMetrics.boundingRect("*************"); option.subControls = QStyle::SC_All; return qApp->style()->sizeFromContents(QStyle::CT_ComboBox, &option, option.rect.size(), nullptr); }); LayoutInfo { min: match orientation { Orientation::Horizontal => size.width, Orientation::Vertical => size.height, } as f32, ..LayoutInfo::default() } } fn input_event_filter_before_children( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &ItemRc, ) -> InputEventFilterResult { InputEventFilterResult::ForwardEvent } fn input_event( self: Pin<&Self>, event: MouseEvent, _window: &WindowRc, _self_rc: &sixtyfps_corelib::items::ItemRc, ) -> InputEventResult { let enabled = self.enabled(); if !enabled { return InputEventResult::EventIgnored; } // FIXME: this is the input event of a button, but we need to do the proper hit test Self::FIELD_OFFSETS.pressed.apply_pin(self).set(match event { MouseEvent::MousePressed { .. } => true, MouseEvent::MouseExit | MouseEvent::MouseReleased { .. } => false, MouseEvent::MouseMoved { .. } => { return if self.pressed() { InputEventResult::GrabMouse } else { InputEventResult::EventIgnored } } MouseEvent::MouseWheel { .. } => return InputEventResult::EventIgnored, }); if matches!(event, MouseEvent::MouseReleased { .. }) { Self::FIELD_OFFSETS.is_open.apply_pin(self).set(true); Self::FIELD_OFFSETS.open_popup.apply_pin(self).call(&()); InputEventResult::EventAccepted } else { InputEventResult::GrabMouse } } fn key_event(self: Pin<&Self>, _: &KeyEvent, _window: &WindowRc) -> KeyEventResult { KeyEventResult::EventIgnored } fn focus_event(self: Pin<&Self>, _: &FocusEvent, _window: &WindowRc) {} fn_render! { this dpr size painter => let down: bool = this.pressed(); let is_open: bool = this.is_open(); let text: qttypes::QString = this.current_value().as_str().into(); let enabled = this.enabled(); cpp!(unsafe [ painter as "QPainter*", text as "QString", enabled as "bool", size as "QSize", down as "bool", is_open as "bool", dpr as "float" ] { QStyleOptionComboBox option; option.currentText = std::move(text); option.rect = QRect(QPoint(), size / dpr); if (down) option.state |= QStyle::State_Sunken; else option.state |= QStyle::State_Raised; if (enabled) { option.state |= QStyle::State_Enabled; } else { option.palette.setCurrentColorGroup(QPalette::Disabled); } if (is_open) option.state |= QStyle::State_On; option.subControls = QStyle::SC_All; qApp->style()->drawComplexControl(QStyle::CC_ComboBox, &option, painter, nullptr); qApp->style()->drawControl(QStyle::CE_ComboBoxLabel, &option, painter, nullptr); }); } } impl ItemConsts for NativeComboBox { const cached_rendering_data_offset: const_field_offset::FieldOffset = Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); } declare_item_vtable! { fn sixtyfps_get_NativeComboBoxVTable() -> NativeComboBoxVTable for NativeComboBox } #[repr(C)] #[derive(FieldOffsets, Default, SixtyFPSElement)] #[pin] pub struct NativeTabWidget { pub x: Property, pub y: Property, pub width: Property, pub height: Property, pub cached_rendering_data: CachedRenderingData, pub content_min_height: Property, pub content_min_width: Property, pub tabbar_preferred_height: Property, pub tabbar_preferred_width: Property, // outputs pub content_x: Property, pub content_y: Property, pub content_height: Property, pub content_width: Property, pub tabbar_x: Property, pub tabbar_y: Property, pub tabbar_height: Property, pub tabbar_width: Property, } impl Item for NativeTabWidget { fn init(self: Pin<&Self>, _window: &WindowRc) { #[derive(Default, Clone)] #[repr(C)] struct TabWidgetRects { content: qttypes::QRectF, tabbar: qttypes::QRectF, } cpp! {{ struct TabWidgetRects { QRectF content, tabbar; }; }} #[repr(C)] #[derive(FieldOffsets, Default)] #[pin] struct TabBarSharedData { width: Property, height: Property, tabbar_preferred_height: Property, tabbar_preferred_width: Property, rects: Property, } let shared_data = Rc::pin(TabBarSharedData::default()); macro_rules! link { ($prop:ident) => { Property::link_two_way( Self::FIELD_OFFSETS.$prop.apply_pin(self), TabBarSharedData::FIELD_OFFSETS.$prop.apply_pin(shared_data.as_ref()), ); }; } link!(width); link!(height); link!(tabbar_preferred_width); link!(tabbar_preferred_height); let shared_data_weak = pin_weak::rc::PinWeak::downgrade(shared_data.clone()); shared_data.rects.set_binding(move || { let shared_data = shared_data_weak.upgrade().unwrap(); let size = qttypes::QSizeF { width: TabBarSharedData::FIELD_OFFSETS.width.apply_pin(shared_data.as_ref()).get() as _, height: TabBarSharedData::FIELD_OFFSETS.height.apply_pin(shared_data.as_ref()).get() as _, }; let tabbar_size = qttypes::QSizeF { width: TabBarSharedData::FIELD_OFFSETS.tabbar_preferred_width.apply_pin(shared_data.as_ref()).get() as _, height: TabBarSharedData::FIELD_OFFSETS.tabbar_preferred_height.apply_pin(shared_data.as_ref()).get() as _, }; cpp!(unsafe [size as "QSizeF", tabbar_size as "QSizeF"] -> TabWidgetRects as "TabWidgetRects" { ensure_initialized(); QStyleOptionTabWidgetFrame option; auto style = qApp->style(); option.lineWidth = style->pixelMetric(QStyle::PM_DefaultFrameWidth, 0, nullptr); option.shape = QTabBar::RoundedNorth; option.rect = QRect(QPoint(), size.toSize()); option.tabBarSize = tabbar_size.toSize(); option.tabBarRect = QRect(QPoint(), option.tabBarSize); option.rightCornerWidgetSize = QSize(0, 0); option.leftCornerWidgetSize = QSize(0, 0); QRect contentsRect = style->subElementRect(QStyle::SE_TabWidgetTabContents, &option, nullptr); QRect tabbarRect = style->subElementRect(QStyle::SE_TabWidgetTabBar, &option, nullptr); return {contentsRect, tabbarRect}; }) }); macro_rules! bind { ($prop:ident = $field1:ident.$field2:ident) => { let shared_data = shared_data.clone(); self.$prop.set_binding(move || { let rects = TabBarSharedData::FIELD_OFFSETS.rects.apply_pin(shared_data.as_ref()).get(); rects.$field1.$field2 as f32 }); }; } bind!(content_x = content.x); bind!(content_y = content.y); bind!(content_width = content.width); bind!(content_height = content.height); bind!(tabbar_x = tabbar.x); bind!(tabbar_y = tabbar.y); bind!(tabbar_width = tabbar.width); bind!(tabbar_height = tabbar.height); } fn geometry(self: Pin<&Self>) -> Rect { euclid::rect(self.x(), self.y(), self.width(), self.height()) } fn layouting_info( self: Pin<&Self>, orientation: Orientation, _window: &WindowRc, ) -> LayoutInfo { let content_size = qttypes::QSizeF { width: self.content_min_width() as _, height: self.content_min_height() as _, }; let tabbar_size = qttypes::QSizeF { width: self.tabbar_preferred_width() as _, height: self.tabbar_preferred_height() as _, }; let size = cpp!(unsafe [content_size as "QSizeF", tabbar_size as "QSizeF"] -> qttypes::QSize as "QSize" { ensure_initialized(); QStyleOptionTabWidgetFrame option; auto style = qApp->style(); option.lineWidth = style->pixelMetric(QStyle::PM_DefaultFrameWidth, 0, nullptr); option.shape = QTabBar::RoundedNorth; option.tabBarSize = tabbar_size.toSize(); option.rightCornerWidgetSize = QSize(0, 0); option.leftCornerWidgetSize = QSize(0, 0); auto sz = QSize(qMax(content_size.width(), tabbar_size.width()), content_size.height() + tabbar_size.height()); return style->sizeFromContents(QStyle::CT_TabWidget, &option, sz, nullptr); }); LayoutInfo { min: match orientation { Orientation::Horizontal => size.width as f32, Orientation::Vertical => size.height as f32, }, preferred: match orientation { Orientation::Horizontal => size.width as f32, Orientation::Vertical => size.height as f32, }, stretch: 1., ..LayoutInfo::default() } } fn input_event_filter_before_children( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &ItemRc, ) -> InputEventFilterResult { InputEventFilterResult::ForwardEvent } fn input_event( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &sixtyfps_corelib::items::ItemRc, ) -> InputEventResult { InputEventResult::EventIgnored } fn key_event(self: Pin<&Self>, _: &KeyEvent, _window: &WindowRc) -> KeyEventResult { KeyEventResult::EventIgnored } fn focus_event(self: Pin<&Self>, _: &FocusEvent, _window: &WindowRc) {} fn_render! { this dpr size painter => let tabbar_size = qttypes::QSizeF { width: this.tabbar_preferred_width() as _, height: this.tabbar_preferred_height() as _, }; cpp!(unsafe [ painter as "QPainter*", size as "QSize", dpr as "float", tabbar_size as "QSizeF" ] { QStyleOptionTabWidgetFrame option; auto style = qApp->style(); option.lineWidth = style->pixelMetric(QStyle::PM_DefaultFrameWidth, 0, nullptr); option.shape = QTabBar::RoundedNorth; if (true /*enabled*/) { option.state |= QStyle::State_Enabled; } else { option.palette.setCurrentColorGroup(QPalette::Disabled); } option.rect = QRect(QPoint(), size / dpr); option.tabBarSize = tabbar_size.toSize(); option.rightCornerWidgetSize = QSize(0, 0); option.leftCornerWidgetSize = QSize(0, 0); option.tabBarRect = style->subElementRect(QStyle::SE_TabWidgetTabBar, &option, nullptr); option.rect = style->subElementRect(QStyle::SE_TabWidgetTabPane, &option, nullptr); style->drawPrimitive(QStyle::PE_FrameTabWidget, &option, painter, nullptr); /* -- we don't need to draw the base since we already draw the frame QStyleOptionTab tabOverlap; tabOverlap.shape = option.shape; int overlap = style->pixelMetric(QStyle::PM_TabBarBaseOverlap, &tabOverlap, nullptr); QStyleOptionTabBarBase optTabBase; static_cast(optTabBase) = (option); optTabBase.shape = option.shape; optTabBase.rect = option.tabBarRect; if (overlap > 0) { optTabBase.rect.setHeight(optTabBase.rect.height() - overlap); } optTabBase.tabBarRect = option.tabBarRect; optTabBase.selectedTabRect = option.selectedTabRect; style->drawPrimitive(QStyle::PE_FrameTabBarBase, &optTabBase, painter, nullptr);*/ }); } } impl ItemConsts for NativeTabWidget { const cached_rendering_data_offset: const_field_offset::FieldOffset = Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); } declare_item_vtable! { fn sixtyfps_get_NativeTabWidgetVTable() -> NativeTabWidgetVTable for NativeTabWidget } #[repr(C)] #[derive(FieldOffsets, Default, SixtyFPSElement)] #[pin] pub struct NativeTab { pub x: Property, pub y: Property, pub width: Property, pub height: Property, pub title: Property, pub icon: Property, pub enabled: Property, pub pressed: Property, pub current: Property, pub num_tabs: Property, pub tab_index: Property, pub cached_rendering_data: CachedRenderingData, } impl Item for NativeTab { fn init(self: Pin<&Self>, _window: &WindowRc) {} fn geometry(self: Pin<&Self>) -> Rect { euclid::rect(self.x(), self.y(), self.width(), self.height()) } fn layouting_info( self: Pin<&Self>, orientation: Orientation, _window: &WindowRc, ) -> LayoutInfo { let text: qttypes::QString = self.title().as_str().into(); let icon: qttypes::QPixmap = crate::qt_window::load_image_from_resource( (&self.icon()).into(), None, Default::default(), ) .unwrap_or_default(); let tab_index: i32 = self.tab_index(); let num_tabs: i32 = self.num_tabs(); let size = cpp!(unsafe [ text as "QString", icon as "QPixmap", tab_index as "int", num_tabs as "int" ] -> qttypes::QSize as "QSize" { ensure_initialized(); QStyleOptionTab option; option.rect = option.fontMetrics.boundingRect(text); option.text = text; option.icon = icon; option.shape = QTabBar::RoundedNorth; option.position = num_tabs == 1 ? QStyleOptionTab::OnlyOneTab : tab_index == 0 ? QStyleOptionTab::Beginning : tab_index == num_tabs - 1 ? QStyleOptionTab::End : QStyleOptionTab::Middle; auto style = qApp->style(); int hframe = style->pixelMetric(QStyle::PM_TabBarTabHSpace, &option, nullptr); int vframe = style->pixelMetric(QStyle::PM_TabBarTabVSpace, &option, nullptr); int padding = icon.isNull() ? 0 : 4; int textWidth = option.fontMetrics.size(Qt::TextShowMnemonic, text).width(); auto iconSize = icon.isNull() ? 0 : style->pixelMetric(QStyle::PM_TabBarIconSize, nullptr, nullptr); QSize csz = QSize(textWidth + iconSize + hframe + padding, qMax(option.fontMetrics.height(), iconSize) + vframe); return style->sizeFromContents(QStyle::CT_TabBarTab, &option, csz, nullptr); }); LayoutInfo { min: match orientation { // FIXME: the minimum width is arbitrary, Qt uses the size of two letters + ellipses Orientation::Horizontal => size.width.min(size.height * 2) as f32, Orientation::Vertical => size.height as f32, }, preferred: match orientation { Orientation::Horizontal => size.width as f32, Orientation::Vertical => size.height as f32, }, ..LayoutInfo::default() } } fn input_event_filter_before_children( self: Pin<&Self>, _: MouseEvent, _window: &WindowRc, _self_rc: &ItemRc, ) -> InputEventFilterResult { InputEventFilterResult::ForwardEvent } fn input_event( self: Pin<&Self>, event: MouseEvent, _window: &WindowRc, _self_rc: &sixtyfps_corelib::items::ItemRc, ) -> InputEventResult { let enabled = self.enabled(); if !enabled { return InputEventResult::EventIgnored; } Self::FIELD_OFFSETS.pressed.apply_pin(self).set(match event { MouseEvent::MousePressed { .. } => true, MouseEvent::MouseExit | MouseEvent::MouseReleased { .. } => false, MouseEvent::MouseMoved { .. } => { return if self.pressed() { InputEventResult::GrabMouse } else { InputEventResult::EventIgnored } } MouseEvent::MouseWheel { .. } => return InputEventResult::EventIgnored, }); let click_on_press = cpp!(unsafe [] -> bool as "bool" { return qApp->style()->styleHint(QStyle::SH_TabBar_SelectMouseType, nullptr, nullptr) == QEvent::MouseButtonPress; }); if matches!(event, MouseEvent::MouseReleased { .. } if !click_on_press) || matches!(event, MouseEvent::MousePressed { .. } if click_on_press) { self.current.set(self.tab_index()); InputEventResult::EventAccepted } else { InputEventResult::GrabMouse } } fn key_event(self: Pin<&Self>, _: &KeyEvent, _window: &WindowRc) -> KeyEventResult { KeyEventResult::EventIgnored } fn focus_event(self: Pin<&Self>, _: &FocusEvent, _window: &WindowRc) {} fn_render! { this dpr size painter => let down: bool = this.pressed(); let text: qttypes::QString = this.title().as_str().into(); let icon: qttypes::QPixmap = crate::qt_window::load_image_from_resource( (&this.icon()).into(), None, Default::default(), ) .unwrap_or_default(); let enabled: bool = this.enabled(); let current: i32 = this.current(); let tab_index: i32 = this.tab_index(); let num_tabs: i32 = this.num_tabs(); cpp!(unsafe [ painter as "QPainter*", text as "QString", icon as "QPixmap", enabled as "bool", size as "QSize", down as "bool", dpr as "float", tab_index as "int", current as "int", num_tabs as "int" ] { ensure_initialized(); QStyleOptionTab option; option.rect = QRect(QPoint(), size / dpr);; option.text = text; option.icon = icon; option.shape = QTabBar::RoundedNorth; option.position = num_tabs == 1 ? QStyleOptionTab::OnlyOneTab : tab_index == 0 ? QStyleOptionTab::Beginning : tab_index == num_tabs - 1 ? QStyleOptionTab::End : QStyleOptionTab::Middle; /* -- does not render correctly with the fusion style because we don't draw the selected on top option.selectedPosition = current == tab_index - 1 ? QStyleOptionTab::NextIsSelected : current == tab_index + 1 ? QStyleOptionTab::PreviousIsSelected : QStyleOptionTab::NotAdjacent;*/ if (down) option.state |= QStyle::State_Sunken; else option.state |= QStyle::State_Raised; if (enabled) { option.state |= QStyle::State_Enabled; } else { option.palette.setCurrentColorGroup(QPalette::Disabled); } if (current == tab_index) option.state |= QStyle::State_Selected; qApp->style()->drawControl(QStyle::CE_TabBarTab, &option, painter, nullptr); }); } } impl ItemConsts for NativeTab { const cached_rendering_data_offset: const_field_offset::FieldOffset = Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); } declare_item_vtable! { fn sixtyfps_get_NativeTabVTable() -> NativeTabVTable for NativeTab } #[repr(C)] #[derive(FieldOffsets, SixtyFPSElement)] #[pin] pub struct NativeStyleMetrics { pub layout_spacing: Property, pub layout_padding: Property, pub text_cursor_width: Property, pub window_background: Property, } impl Default for NativeStyleMetrics { fn default() -> Self { let s = NativeStyleMetrics { layout_spacing: Default::default(), layout_padding: Default::default(), text_cursor_width: Default::default(), window_background: Default::default(), }; sixtyfps_init_native_style_metrics(&s); s } } impl NativeStyleMetrics { pub fn new() -> Pin> { Rc::pin(Self::default()) } } /// Initialize the native style metrics #[no_mangle] pub extern "C" fn sixtyfps_init_native_style_metrics(self_: &NativeStyleMetrics) { let layout_spacing = cpp!(unsafe [] -> f32 as "float" { ensure_initialized(); return qApp->style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); }); self_.layout_spacing.set(layout_spacing.max(0.0)); let text_cursor_width = cpp!(unsafe [] -> f32 as "float" { return qApp->style()->pixelMetric(QStyle::PM_TextCursorWidth); }); self_.text_cursor_width.set(text_cursor_width.max(0.0)); let window_background = cpp!(unsafe[] -> u32 as "QRgb" { return qApp->palette().color(QPalette::Window).rgba(); }); self_.window_background.set(Color::from_argb_encoded(window_background)) }