Made ComboBox scrollable (#5581)

This commit is contained in:
Florian Blasius 2024-07-12 13:14:00 +00:00 committed by GitHub
parent 16996dfa16
commit 806d12bcb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 214 additions and 158 deletions

View file

@ -40,6 +40,7 @@ All notable changes to this project are documented in this file.
- Added `step-size` to `SpinBox`
- Added `TimePickerPopup` and `DatePickerPopup`.
- Fixed accessible value and actions on ProgressIndicator, Spinner, Spinbox, CheckBox, Switch
- Made `ComboBox` scrollable
### C++ API

View file

@ -33,7 +33,7 @@ export component ComboBoxBase {
public function move-selection-down() {
root.select(Math.min(root.current-index + 1, root.model.length - 1));
}
function reset-current() {
root.current-index = 0;
}

View file

@ -1,9 +1,10 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import { CosmicFontSettings, CosmicPalette, Icons } from "styling.slint";
import { CosmicFontSettings, CosmicPalette, Icons, CosmicSizeSettings } from "styling.slint";
import { MenuBorder, ListItem, StateLayerBase } from "components.slint";
import { ComboBoxBase } from "../common/combobox-base.slint";
import { ScrollView } from "./scrollview.slint";
export component ComboBox {
in property <[string]> model <=> base.model;
@ -13,6 +14,9 @@ export component ComboBox {
in-out property <string> current-value <=> base.current-value;
callback selected <=> base.selected;
property <length> popup-padding: 4px;
property <int> visible-items: 6;
min-width: max(160px, layout.min-height);
min-height: max(32px, layout.min-height);
@ -75,29 +79,35 @@ export component ComboBox {
enabled: root.enabled;
}
}
popup := PopupWindow {
x: 0;
// Position the popup so that the first element is over the popup.
// Ideally it should be so that the current element is over the popup.
y: root.height + 4px;
width: root.width;
height: root.visible-items * CosmicSizeSettings.item-height + 2 * root.popup-padding;
MenuBorder {
VerticalLayout {
padding: 8px;
ScrollView {
VerticalLayout {
alignment: start;
padding: root.popup-padding;
for value[index] in root.model : ListItem {
item: { text: value };
is-selected: index == root.current-index;
has-hover: touch-area.has-hover;
pressed: touch-area.pressed;
for value[index] in root.model : ListItem {
item: { text: value };
is-selected: index == root.current-index;
has-hover: touch-area.has-hover;
pressed: touch-area.pressed;
touch-area := TouchArea {
clicked => {
base.select(index);
touch-area := TouchArea {
clicked => {
base.select(index);
}
}
}
}
}
}
}
}
}
}

View file

@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import { Icons, CosmicPalette, CosmicFontSettings } from "styling.slint";
import { Icons, CosmicPalette, CosmicFontSettings, CosmicSizeSettings } from "styling.slint";
export component StateLayerBase {
in property <length> border-radius <=> overlay.border-radius;
@ -99,7 +99,7 @@ export component ListItem {
in property <length> pressed-y;
min-width: layout.min-width;
min-height: max(40px, layout.min-height);
min-height: max(CosmicSizeSettings.item-height, layout.min-height);
vertical-stretch: 0;
horizontal-stretch: 1;

View file

@ -81,3 +81,7 @@ export global Icons {
out property <image> edit: @image-url("_edit.svg");
out property <image> calendar: @image-url("_calendar.svg");
}
export global CosmicSizeSettings {
out property <length> item-height: 40px;
}

View file

@ -1,46 +1,49 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import { CupertinoFontSettings, CupertinoPalette, Icons } from "styling.slint";
import { CupertinoFontSettings, CupertinoPalette, Icons, CupertinoSizeSettings } from "styling.slint";
import { MenuBorder, ListItem, FocusBorder } from "components.slint";
import { ComboBoxBase } from "../common/combobox-base.slint";
import { ScrollView } from "./scrollview.slint";
export component ComboBox {
in property <[string]> model <=> i-base.model;
in property <bool> enabled <=> i-base.enabled;
out property <bool> has-focus <=> i-base.has-focus;
in-out property <int> current-index <=> i-base.current-index;
in-out property <string> current-value <=> i-base.current-value;
in property <[string]> model <=> base.model;
in property <bool> enabled <=> base.enabled;
out property <bool> has-focus <=> base.has-focus;
in-out property <int> current-index <=> base.current-index;
in-out property <string> current-value <=> base.current-value;
callback selected <=> i-base.selected;
callback selected <=> base.selected;
private property <brush> background: CupertinoPalette.control-background;
property <brush> background: CupertinoPalette.control-background;
property <length> popup-padding: 4px;
property <int> visible-items: 6;
min-width: max(160px, i-layout.min-width);
min-height: max(22px, i-layout.min-height);
min-width: max(160px, layout.min-width);
min-height: max(22px, layout.min-height);
horizontal-stretch: 1;
vertical-stretch: 0;
forward-focus: i-base;
forward-focus: base;
accessible-role: combobox;
states [
disabled when !root.enabled : {
i-text.color: CupertinoPalette.foreground-secondary;
i-top-icon.colorize: CupertinoPalette.foreground-secondary;
i-bottom-icon.colorize: CupertinoPalette.foreground-secondary;
text.color: CupertinoPalette.foreground-secondary;
top-icon.colorize: CupertinoPalette.foreground-secondary;
bottom-icon.colorize: CupertinoPalette.foreground-secondary;
root.background: CupertinoPalette.tertiary-control-background;
}
pressed when i-base.pressed : {
pressed when base.pressed : {
root.background: CupertinoPalette.secondary-control-background;
}
]
i-base := ComboBoxBase {
base := ComboBoxBase {
width: 100%;
height: 100%;
show-popup => {
i-popup.show();
popup.show();
}
}
@ -72,14 +75,14 @@ export component ComboBox {
}
}
i-layout := HorizontalLayout {
layout := HorizontalLayout {
padding-left: 8px;
padding-right: 8px;
padding-top: 4px;
padding-bottom: 4px;
spacing: 4px;
i-text := Text {
text := Text {
horizontal-alignment: left;
vertical-alignment: center;
font-size: CupertinoFontSettings.body.font-size;
@ -129,13 +132,13 @@ export component ComboBox {
padding: 4px;
spacing: 4px;
i-top-icon := Image {
top-icon := Image {
x: (parent.width - self.width) / 2;
colorize: CupertinoPalette.accent-foreground;
source: Icons.chevron-up;
}
i-bottom-icon := Image {
bottom-icon := Image {
x: (parent.width - self.width) / 2;
colorize: CupertinoPalette.accent-foreground;
source: Icons.chevron-down;
@ -144,27 +147,31 @@ export component ComboBox {
}
}
i-popup := PopupWindow {
popup := PopupWindow {
x: 0;
y: parent.height + 6px;
min-width: root.width;
width: root.width;
height: root.visible-items * CupertinoSizeSettings.item-height + 2 * root.popup-padding;
MenuBorder {
VerticalLayout {
padding: 4px;
ScrollView {
VerticalLayout {
alignment: start;
padding: root.popup-padding;
for value[index] in root.model : ListItem {
padding-horizontal: 0;
item: { text: value };
is-selected: index == root.current-index;
has-hover: i-touch-area.has-hover;
pressed: i-touch-area.pressed;
pressed-x: i-touch-area.pressed-x;
pressed-y: i-touch-area.pressed-y;
for value[index] in root.model : ListItem {
padding-horizontal: 0;
item: { text: value };
is-selected: index == root.current-index;
has-hover: touch-area.has-hover;
pressed: touch-area.pressed;
pressed-x: touch-area.pressed-x;
pressed-y: touch-area.pressed-y;
i-touch-area := TouchArea {
clicked => {
i-base.select(index);
touch-area := TouchArea {
clicked => {
base.select(index);
}
}
}
}

View file

@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import { CupertinoPalette, CupertinoFontSettings, Icons } from "styling.slint";
import { CupertinoPalette, CupertinoFontSettings, Icons, CupertinoSizeSettings } from "styling.slint";
export component FocusBorder inherits Rectangle {
in property <bool> has-focus;
@ -55,7 +55,7 @@ export component ListItem {
in property <length> pressed-y;
min-width: i-layout.min-width;
min-height: max(22px, i-layout.min-height);
min-height: max(CupertinoSizeSettings.item-height, i-layout.min-height);
vertical-stretch: 0;
horizontal-stretch: 1;

View file

@ -100,3 +100,7 @@ export global Icons {
out property <image> edit: @image-url("_edit.svg");
out property <image> calendar: @image-url("_calendar.svg");
}
export global CupertinoSizeSettings {
out property <length> item-height: 22px;
}

View file

@ -1,55 +1,59 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import { FluentFontSettings, FluentPalette, Icons } from "styling.slint";
import { FluentFontSettings, FluentPalette, Icons, FluentSizeSettings } from "styling.slint";
import { MenuBorder, ListItem, FocusBorder } from "components.slint";
import { ComboBoxBase } from "../common/combobox-base.slint";
import { ScrollView } from "./scrollview.slint";
export component ComboBox {
in property <[string]> model <=> i-base.model;
in property <bool> enabled <=> i-base.enabled;
out property <bool> has-focus <=> i-base.has-focus;
in-out property <int> current-index <=> i-base.current-index;
in-out property <string> current-value <=> i-base.current-value;
in property <[string]> model <=> base.model;
in property <bool> enabled <=> base.enabled;
out property <bool> has-focus <=> base.has-focus;
in-out property <int> current-index <=> base.current-index;
in-out property <string> current-value <=> base.current-value;
callback selected <=> i-base.selected;
callback selected <=> base.selected;
min-width: max(160px, i-layout.min-height);
min-height: max(32px, i-layout.min-height);
property <length> popup-padding: 4px;
property <int> visible-items: 6;
min-width: max(160px, layout.min-height);
min-height: max(32px, layout.min-height);
horizontal-stretch: 1;
vertical-stretch: 0;
forward-focus: i-base;
forward-focus: base;
accessible-role: combobox;
states [
disabled when !root.enabled : {
i-background.background: FluentPalette.control-disabled;
i-background.border-color: FluentPalette.border;
i-text.color: FluentPalette.text-disabled;
i-icon.colorize: FluentPalette.text-disabled;
background.background: FluentPalette.control-disabled;
background.border-color: FluentPalette.border;
text.color: FluentPalette.text-disabled;
icon.colorize: FluentPalette.text-disabled;
}
pressed when i-base.pressed : {
i-background.background: FluentPalette.control-alt-tertiary;
i-background.border-color: FluentPalette.border;
i-text.color: FluentPalette.text-secondary;
i-icon.colorize: FluentPalette.text-tertiary;
pressed when base.pressed : {
background.background: FluentPalette.control-alt-tertiary;
background.border-color: FluentPalette.border;
text.color: FluentPalette.text-secondary;
icon.colorize: FluentPalette.text-tertiary;
}
hover when i-base.has-hover : {
i-background.background: FluentPalette.control-secondary;
hover when base.has-hover : {
background.background: FluentPalette.control-secondary;
}
]
i-base := ComboBoxBase {
base := ComboBoxBase {
width: 100%;
height: 100%;
show-popup => {
i-popup.show();
popup.show();
}
}
i-background := Rectangle {
background := Rectangle {
border-radius: 3px;
background: FluentPalette.control-background;
border-width: 1px;
@ -57,12 +61,12 @@ export component ComboBox {
animate border-color { duration: 200ms; }
i-layout := HorizontalLayout {
layout := HorizontalLayout {
padding-left: 11px;
padding-right: 11px;
spacing: 8px;
i-text := Text {
text := Text {
horizontal-alignment: left;
vertical-alignment: center;
font-size: FluentFontSettings.body.font-size;
@ -71,7 +75,7 @@ export component ComboBox {
text: root.current-value;
}
i-icon := Image {
icon := Image {
colorize: FluentPalette.text-secondary;
width: 12px;
source: Icons.dropdown;
@ -83,30 +87,34 @@ export component ComboBox {
}
// focus border
if (root.has-focus && root.enabled) : FocusBorder {
border-radius: i-background.border-radius;
if root.has-focus && root.enabled : FocusBorder {
border-radius: background.border-radius;
}
i-popup := PopupWindow {
popup := PopupWindow {
x: 0;
// Position the popup so that the first element is over the popup.
// Ideally it should be so that the current element is over the popup.
y: -4px;
width: root.width;
height: root.visible-items * FluentSizeSettings.item-height + 2 * root.popup-padding;
MenuBorder {
VerticalLayout {
padding: 4px;
ScrollView {
VerticalLayout {
alignment: start;
padding: root.popup-padding;
for value[index] in root.model : ListItem {
item: { text: value };
is-selected: index == root.current-index;
has-hover: touch-area.has-hover;
pressed: touch-area.pressed;
for value[index] in root.model : ListItem {
item: { text: value };
is-selected: index == root.current-index;
has-hover: i-touch-area.has-hover;
pressed: i-touch-area.pressed;
i-touch-area := TouchArea {
clicked => {
i-base.select(index);
touch-area := TouchArea {
clicked => {
base.select(index);
}
}
}
}

View file

@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import { FluentPalette, FluentFontSettings } from "styling.slint";
import { FluentPalette, FluentFontSettings, FluentSizeSettings } from "styling.slint";
export component FocusBorder inherits Rectangle {
border-width: 2px;
@ -43,7 +43,7 @@ export component ListItem {
in property <length> pressed-y;
min-width: i-layout.min-width;
min-height: max(40px, i-layout.min-height);
min-height: max(FluentSizeSettings.item-height, i-layout.min-height);
vertical-stretch: 0;
horizontal-stretch: 1;

View file

@ -111,3 +111,7 @@ export global Icons {
out property <image> edit: @image-url("_edit.svg");
out property <image> calendar: @image-url("_calendar.svg");
}
export global FluentSizeSettings {
out property <length> item-height: 40px;
}

View file

@ -2,51 +2,54 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import { MaterialPalette, MaterialFontSettings, Elevation, Icons } from "styling.slint";
import { MaterialPalette, MaterialFontSettings, Elevation, Icons, MaterialSizeSettings } from "styling.slint";
import { ListItem, StateLayer } from "components.slint";
import { ComboBoxBase } from "../common/combobox-base.slint";
import { ScrollView } from "./scrollview.slint";
export component ComboBox {
in property <[string]> model <=> i-base.model;
in property <bool> enabled <=> i-base.enabled;
out property <bool> has-focus <=> i-base.has-focus;
in-out property <int> current-index <=> i-base.current-index;
in-out property <string> current-value <=> i-base.current-value;
in property <[string]> model <=> base.model;
in property <bool> enabled <=> base.enabled;
out property <bool> has-focus <=> base.has-focus;
in-out property <int> current-index <=> base.current-index;
in-out property <string> current-value <=> base.current-value;
callback selected <=> i-base.selected;
callback selected <=> base.selected;
property <int> visible-items: 6;
min-width: max(160px, i-layout.min-width);
min-height: max(22px, i-layout.min-height);
min-width: max(160px, layout.min-width);
min-height: max(22px, layout.min-height);
horizontal-stretch: 1;
vertical-stretch: 0;
forward-focus: i-base;
forward-focus: base;
accessible-role: combobox;
states [
disabled when !root.enabled : {
i-background.border-color: MaterialPalette.control-foreground;
i-background.opacity: 0.38;
i-label.opacity: 0.38;
i-icon.opacity: 0.38;
background.border-color: MaterialPalette.control-foreground;
background.opacity: 0.38;
label.opacity: 0.38;
icon.opacity: 0.38;
}
focused when root.has-focus : {
i-background.border-width: 2px;
i-background.border-color: MaterialPalette.accent-background;
i-label.color: MaterialPalette.accent-background;
i-icon.colorize: MaterialPalette.accent-background;
background.border-width: 2px;
background.border-color: MaterialPalette.accent-background;
label.color: MaterialPalette.accent-background;
icon.colorize: MaterialPalette.accent-background;
}
]
i-base := ComboBoxBase {
base := ComboBoxBase {
width: 100%;
height: 100%;
show-popup => {
i-popup.show();
popup.show();
}
}
i-background := Rectangle {
background := Rectangle {
width: 100%;
height: 100%;
border-radius: 4px;
@ -54,12 +57,12 @@ export component ComboBox {
border-color: MaterialPalette.border;
}
i-layout := HorizontalLayout {
layout := HorizontalLayout {
padding-left: 16px;
padding-right: 12px;
spacing: 16px;
i-label := Text {
label := Text {
text <=> root.current-value;
color: MaterialPalette.control-foreground;
vertical-alignment: center;
@ -69,7 +72,7 @@ export component ComboBox {
font-weight: MaterialFontSettings.body-large.font-weight;
}
i-icon := Image {
icon := Image {
width: 24px;
height: 24px;
y: (parent.height - self.height) / 2;
@ -78,12 +81,13 @@ export component ComboBox {
}
}
i-popup := PopupWindow {
popup := PopupWindow {
x: 0;
y: root.height;
width: root.width;
height: root.visible-items * MaterialSizeSettings.item-height;
i-popup-container := Rectangle {
popup-container := Rectangle {
background: MaterialPalette.alternate-background;
drop-shadow-color: MaterialPalette.shadow;
drop-shadow-blur: Elevation.level2;
@ -91,16 +95,20 @@ export component ComboBox {
border-radius: 4px;
}
VerticalLayout {
for value[index] in root.model: ListItem {
item: { text: value };
is-selected: index == root.current-index;
has-hover: i-touch-area.has-hover;
pressed: i-touch-area.pressed;
ScrollView {
VerticalLayout {
alignment: start;
i-touch-area := StateLayer {
clicked => {
i-base.select(index);
for value[index] in root.model: ListItem {
item: { text: value };
is-selected: index == root.current-index;
has-hover: touch-area.has-hover;
pressed: touch-area.pressed;
touch-area := StateLayer {
clicked => {
base.select(index);
}
}
}
}

View file

@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import { MaterialPalette, MaterialFontSettings } from "styling.slint";
import { MaterialPalette, MaterialFontSettings, MaterialSizeSettings } from "styling.slint";
export component Ripple inherits Rectangle {
in property <length> ripple-x;
@ -101,7 +101,7 @@ export component ListItem {
in property <length> pressed-y;
min-width: i-layout.min-width;
min-height: max(48px, i-layout.min-height);
min-height: max(MaterialSizeSettings.item-height, i-layout.min-height);
vertical-stretch: 0;
horizontal-stretch: 1;

View file

@ -83,3 +83,7 @@ export global Icons {
out property <image> edit: @image-url("_edit.svg");
out property <image> calendar: @image-url("_calendar.svg");
}
export global MaterialSizeSettings {
out property <length> item-height: 48px;
}

View file

@ -2,58 +2,64 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import { ComboBoxBase } from "../common/combobox-base.slint";
import { ScrollView } from "./scrollview.slint";
export component ComboBox {
in property <[string]> model <=> i-base.model;
in property <bool> enabled <=> i-base.enabled;
out property <bool> has-focus <=> i-base.has-focus;
in-out property <int> current-index <=> i-base.current-index;
in-out property <string> current-value <=> i-base.current-value;
in property <[string]> model <=> base.model;
in property <bool> enabled <=> base.enabled;
out property <bool> has-focus <=> base.has-focus;
in-out property <int> current-index <=> base.current-index;
in-out property <string> current-value <=> base.current-value;
callback selected <=> i-base.selected;
callback selected <=> base.selected;
property <length> popup-height: 224px;
accessible-role: combobox;
accessible-value <=> root.current-value;
forward-focus: i-base;
forward-focus: base;
HorizontalLayout {
i-native := NativeComboBox {
native := NativeComboBox {
current-value <=> root.current-value;
has-focus <=> root.has-focus;
enabled <=> root.enabled;
}
}
i-base := ComboBoxBase {
base := ComboBoxBase {
width: 100%;
height: 100%;
show-popup => {
i-popup.show();
popup.show();
}
}
i-popup := PopupWindow {
popup := PopupWindow {
x: 0;
y: root.height;
width: root.width;
height: root.popup-height;
NativeComboBoxPopup {
width: 100%;
height: 100%;
}
VerticalLayout {
spacing: 0px;
ScrollView {
VerticalLayout {
alignment: start;
for value[index] in root.model: NativeStandardListViewItem {
item: { text: value };
is-selected: root.current-index == index;
has-hover: ta.has-hover;
combobox: true;
for value[index] in root.model: NativeStandardListViewItem {
item: { text: value };
is-selected: root.current-index == index;
has-hover: ta.has-hover;
combobox: true;
ta := TouchArea {
clicked => {
i-base.select(index);
ta := TouchArea {
clicked => {
base.select(index);
}
}
}
}