Strawman implementation of a combo box

This commit is contained in:
Olivier Goffart 2020-11-03 17:55:49 +01:00
parent a6dbd0d7fa
commit 8a95b806c7
9 changed files with 291 additions and 5 deletions

View file

@ -50,6 +50,7 @@ extern const cbindgen_private::ItemVTable NativeGroupBoxVTable;
extern const cbindgen_private::ItemVTable NativeLineEditVTable; extern const cbindgen_private::ItemVTable NativeLineEditVTable;
extern const cbindgen_private::ItemVTable NativeScrollViewVTable; extern const cbindgen_private::ItemVTable NativeScrollViewVTable;
extern const cbindgen_private::ItemVTable NativeStandardListViewItemVTable; extern const cbindgen_private::ItemVTable NativeStandardListViewItemVTable;
extern const cbindgen_private::ItemVTable NativeComboBoxVTable;
} }
} }
@ -134,6 +135,7 @@ using cbindgen_private::NativeSlider;
using cbindgen_private::NativeSpinBox; using cbindgen_private::NativeSpinBox;
using cbindgen_private::NativeStandardListViewItem; using cbindgen_private::NativeStandardListViewItem;
using cbindgen_private::NativeStyleMetrics; using cbindgen_private::NativeStyleMetrics;
using cbindgen_private::NativeComboBox;
namespace private_api { namespace private_api {
constexpr inline ItemTreeNode make_item_node(std::uintptr_t offset, constexpr inline ItemTreeNode make_item_node(std::uintptr_t offset,

View file

@ -7,7 +7,7 @@
This file is also available under commercial licensing terms. This file is also available under commercial licensing terms.
Please contact info@sixtyfps.io for more information. Please contact info@sixtyfps.io for more information.
LICENSE END */ LICENSE END */
import { Button, CheckBox, SpinBox, Slider, GroupBox, LineEdit, StandardListView } from "sixtyfps_widgets.60"; import { Button, CheckBox, SpinBox, Slider, GroupBox, LineEdit, StandardListView, ComboBox } from "sixtyfps_widgets.60";
App := Window { App := Window {
width: 500px; width: 500px;
@ -67,6 +67,13 @@ App := Window {
value: 42; value: 42;
} }
} }
GroupBox {
title: "ComboBox";
ComboBox {
model: ["Select Something", "From this", "Combobox"];
}
}
} }
GroupBox { GroupBox {

View file

@ -532,6 +532,20 @@ impl TypeRegister {
], ],
&[], &[],
); );
native_class(
&mut register,
"NativeComboBox",
&[
("x", Type::Length),
("y", Type::Length),
("width", Type::Length),
("height", Type::Length),
("current_value", Type::String),
("is_open", Type::Bool),
("enabled", Type::Bool),
],
&[],
);
let mut native_style_metrics = let mut native_style_metrics =
BuiltinElement::new(Rc::new(NativeClass::new_with_properties( BuiltinElement::new(Rc::new(NativeClass::new_with_properties(

View file

@ -102,4 +102,38 @@ export StandardListView := ListView {
clicked => { current_item = i; } clicked => { current_item = i; }
} }
} }
} }
export ComboBox := NativeComboBox {
property <[string]> model;
property <int> current_index : -1;
enabled: true;
if (is_open && enabled) : Rectangle {
y: root.height;
width: root.width;
for value[idx] in root.model: Rectangle {
y: idx * height;
width: 100%;
height: root.height;
color: idx == root.current_index ? lightblue : white;
Text {
width: 100%;
height: 100%;
text: value;
}
TouchArea {
width: 100%;
height: 100%;
clicked => {
if (root.enabled) {
root.current_index = idx;
root.current_value = value;
}
is_open = false;
}
}
}
}
}

View file

@ -418,3 +418,72 @@ export StandardListView := ListView {
} }
} }
export ComboBox := Rectangle {
property <[string]> model;
property <int> current_index : -1;
property <string> current_value;
property <bool> is_open: false;
property <bool> enabled: true;
border_width: 2px;
border_radius: 10px;
border_color: Palette.text_color;
color: touch_area.pressed ? Palette.heighlight_background : Palette.button_background;
animate color { duration: 100ms; }
horizontal-stretch: 0;
vertical-stretch: 0;
minimum_width: 170px;
HorizontalLayout {
padding: root.border_radius;
t := Text {
text <=> root.current_value;
horizontal_alignment: align_center;
vertical_alignment: align_center;
color: root.enabled ? Palette.text_color : Palette.text_color_disabled;
horizontal-stretch: 1;
}
Text {
text:"▼";
color: root.enabled ? Palette.text_color : Palette.text_color_disabled;
horizontal-stretch: 0;
}
}
touch_area := TouchArea {
width: 100%;
height: 100%;
clicked => {
if (root.enabled) {
is_open = !is_open;
}
}
}
if (is_open && enabled) : Rectangle {
y: root.height;
width: root.width;
for value[idx] in root.model: Rectangle {
y: idx * height;
width: 100%;
height: t.height;
color: idx == root.current_index ? Palette.heighlight_background : Palette.base_background_color;
Text {
width: 100%;
height: 100%;
text: value;
}
TouchArea {
width: 100%;
height: 100%;
clicked => {
if (root.enabled) {
root.current_index = idx;
root.current_value = value;
}
is_open = false;
}
}
}
}
}

View file

@ -14,11 +14,10 @@ When adding an item or a property, it needs to be kept in sync with different pl
(This is less than ideal and maybe we can have some automation later) (This is less than ideal and maybe we can have some automation later)
- It needs to be changed in this module - It needs to be changed in this module
- The ItemVTable_static at the end of datastructures.rs (new items only)
- In the compiler: typeregister.rs - In the compiler: typeregister.rs
- In the vewer: main.rs - In the vewer: main.rs
- For the C++ code (new item only): the build.rs to export the new item, and the `using` declaration in sixtyfps.h - 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(unsafe_code)] #![allow(unsafe_code)]

View file

@ -64,7 +64,8 @@ pub type NativeWidgets =
(widgets::NativeLineEdit, (widgets::NativeLineEdit,
(widgets::NativeScrollView, (widgets::NativeScrollView,
(widgets::NativeStandardListViewItem, (widgets::NativeStandardListViewItem,
())))))))); (widgets::NativeComboBox,
())))))))));
#[cfg(not(no_qt))] #[cfg(not(no_qt))]
#[rustfmt::skip] #[rustfmt::skip]

View file

@ -7,7 +7,23 @@
This file is also available under commercial licensing terms. This file is also available under commercial licensing terms.
Please contact info@sixtyfps.io for more information. Please contact info@sixtyfps.io for more information.
LICENSE END */ LICENSE END */
/*!
This module contains all the native Qt widgetimplementation 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: typeregister.rs
- 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)] #![allow(non_upper_case_globals)]
use const_field_offset::FieldOffsets; use const_field_offset::FieldOffsets;
use core::pin::Pin; use core::pin::Pin;
use cpp::cpp; use cpp::cpp;
@ -1600,6 +1616,149 @@ impl ItemConsts for NativeStandardListViewItem {
ItemVTable_static! { #[no_mangle] pub static NativeStandardListViewItemVTable for NativeStandardListViewItem } ItemVTable_static! { #[no_mangle] pub static NativeStandardListViewItemVTable for NativeStandardListViewItem }
#[repr(C)]
#[derive(FieldOffsets, Default, BuiltinItem)]
#[pin]
pub struct NativeComboBox {
pub x: Property<f32>,
pub y: Property<f32>,
pub width: Property<f32>,
pub height: Property<f32>,
pub enabled: Property<bool>,
pub pressed: Property<bool>,
pub is_open: Property<bool>,
pub current_value: Property<SharedString>,
pub cached_rendering_data: CachedRenderingData,
}
impl Item for NativeComboBox {
fn init(self: Pin<&Self>, _window: &ComponentWindow) {}
fn geometry(self: Pin<&Self>) -> Rect {
euclid::rect(
Self::FIELD_OFFSETS.x.apply_pin(self).get(),
Self::FIELD_OFFSETS.y.apply_pin(self).get(),
Self::FIELD_OFFSETS.width.apply_pin(self).get(),
Self::FIELD_OFFSETS.height.apply_pin(self).get(),
)
}
fn rendering_primitive(
self: Pin<&Self>,
window: &ComponentWindow,
) -> HighLevelRenderingPrimitive {
let down: bool = Self::FIELD_OFFSETS.pressed.apply_pin(self).get();
let is_open: bool = Self::FIELD_OFFSETS.is_open.apply_pin(self).get();
let text: qttypes::QString =
Self::FIELD_OFFSETS.current_value.apply_pin(self).get().as_str().into();
let enabled = Self::FIELD_OFFSETS.enabled.apply_pin(self).get();
let size: qttypes::QSize = get_size!(self);
let dpr = window.scale_factor();
let mut imgarray = QImageWrapArray::new(size, dpr);
let img = &mut imgarray.img;
cpp!(unsafe [
img as "QImage*",
text as "QString",
enabled as "bool",
size as "QSize",
down as "bool",
is_open as "bool",
dpr as "float"
] {
QPainter p(img);
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;
if (is_open)
option.state |= QStyle::State_On;
option.subControls = QStyle::SC_All;
qApp->style()->drawComplexControl(QStyle::CC_ComboBox, &option, &p, nullptr);
qApp->style()->drawControl(QStyle::CE_ComboBoxLabel, &option, &p, nullptr);
});
return HighLevelRenderingPrimitive::Image { source: imgarray.to_resource() };
}
fn rendering_variables(
self: Pin<&Self>,
_window: &ComponentWindow,
) -> SharedArray<RenderingVariable> {
SharedArray::default()
}
fn layouting_info(self: Pin<&Self>, window: &ComponentWindow) -> LayoutInfo {
let text: qttypes::QString =
Self::FIELD_OFFSETS.current_value.apply_pin(self).get().as_str().into();
let dpr = window.scale_factor();
let size = cpp!(unsafe [
text as "QString",
dpr as "float"
] -> qttypes::QSize as "QSize" {
ensure_initialized();
QStyleOptionButton option;
// FIXME
option.rect = option.fontMetrics.boundingRect("*****************");
option.text = std::move(text);
return qApp->style()->sizeFromContents(QStyle::CT_ComboBox, &option, option.rect.size(), nullptr) * dpr;
});
LayoutInfo {
min_width: size.width as f32,
min_height: size.height as f32,
..LayoutInfo::default()
}
}
fn input_event(
self: Pin<&Self>,
event: MouseEvent,
_window: &ComponentWindow,
_app_component: ComponentRefPin,
) -> InputEventResult {
let enabled = Self::FIELD_OFFSETS.enabled.apply_pin(self).get();
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.what {
MouseEventType::MousePressed => true,
MouseEventType::MouseExit | MouseEventType::MouseReleased => false,
MouseEventType::MouseMoved => {
return if Self::FIELD_OFFSETS.pressed.apply_pin(self).get() {
InputEventResult::GrabMouse
} else {
InputEventResult::EventIgnored
}
}
});
if matches!(event.what, MouseEventType::MouseReleased) {
Self::FIELD_OFFSETS.is_open.apply_pin(self).set(true);
InputEventResult::EventAccepted
} else {
InputEventResult::GrabMouse
}
}
fn key_event(self: Pin<&Self>, _: &KeyEvent, _window: &ComponentWindow) -> KeyEventResult {
KeyEventResult::EventIgnored
}
fn focus_event(self: Pin<&Self>, _: &FocusEvent, _window: &ComponentWindow) {}
}
impl ItemConsts for NativeComboBox {
const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection();
}
ItemVTable_static! { #[no_mangle] pub static NativeComboBoxVTable for NativeComboBox }
#[repr(C)] #[repr(C)]
#[derive(FieldOffsets, BuiltinItem)] #[derive(FieldOffsets, BuiltinItem)]
#[pin] #[pin]

View file

@ -236,6 +236,7 @@ fn gen_backend_qt(include_dir: &Path) -> anyhow::Result<()> {
"NativeLineEdit", "NativeLineEdit",
"NativeScrollView", "NativeScrollView",
"NativeStandardListViewItem", "NativeStandardListViewItem",
"NativeComboBox",
] ]
.iter() .iter()
.map(|x| x.to_string()) .map(|x| x.to_string())