slint/internal/compiler/widgets/fluent-base/std-widgets-base.slint
Simon Hausmann c428601370
Add support for select-all(), cut(), copy() and paste() functions on text input elements (#2804)
In the compiler this is still very primitive, but an attempt to start a
generic interface. The basic assumption is that all item functions will
eventually need access to the window adapter and itemrc. Support for
additional arguments is still missing.

Also missing is support for the function access via rtti in the
interpreter, hence the hardcoding at the moment.
2023-06-01 16:04:53 +02:00

744 lines
24 KiB
Text

// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
// cSpell: ignore combobox spinbox standardbutton
import { LineEditInner, TextEdit, AboutSlint } from "../common/common.slint";
import { StandardButton } from "../common/standardbutton.slint";
import { StyleMetrics, ScrollView, Button, Palette, Switch } from "std-widgets-impl.slint";
export { StyleMetrics, ScrollView, Button, StandardButton, TextEdit, AboutSlint, Switch }
export * from "widget-table-view.slint";
export component CheckBox {
callback toggled;
in property <string> text <=> text.text;
in-out property <bool> checked;
out property <bool> has-focus: fs.has-focus;
in property<bool> enabled: true;
min-height: 20px;
horizontal-stretch: 0;
vertical-stretch: 0;
accessible-label <=> text.text;
accessible-checkable: true;
accessible-checked <=> root.checked;
accessible-role: checkbox;
HorizontalLayout {
spacing: 8px;
VerticalLayout {
alignment: center;
Rectangle {
border-width: 1px;
border-radius: 2px;
/* border-color: !enabled ? Palette.neutralLighter : Palette.neutralSecondaryAlt;
background: !enabled ? Palette.white
: touch.pressed ? Palette.neutralLight
: touch.has-hover ? Palette.neutralLighter
: Palette.themePrimary;*/
border-color: root.checked ? self.background : !root.enabled ? Palette.neutralTertiaryAlt : Palette.neutralSecondaryAlt;
background: !root.checked ? Palette.white
: !root.enabled ? Palette.neutralTertiaryAlt
: touch.has-hover || touch.pressed ? Palette.themeDark
: Palette.themePrimary;
animate background { duration: 250ms; easing: ease; }
//width: height;
vertical-stretch: 0;
width: 20px;
height: 20px;
if (root.checked || touch.has-hover || touch.pressed) : Path {
width: 66%;
height: 66%;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
commands: "M.22.5.42.7.78.34.74.3.42.62.26.54z";
fill: root.checked ? Palette.white : Palette.neutralSecondaryAlt;
}
}
}
text := Text {
color: !root.enabled ? Palette.neutralTertiary : Palette.neutralDark;
horizontal-alignment: left;
vertical-alignment: center;
vertical-stretch: 1;
}
}
touch := TouchArea {
enabled <=> root.enabled;
clicked => {
if (root.enabled) {
root.checked = !root.checked;
root.toggled();
}
}
}
fs := FocusScope {
x:0;
width: 0px; // Do not react on clicks
enabled <=> root.enabled;
key-pressed(event) => {
if (event.text == " " || event.text == "\n") {
touch.clicked();
return accept;
}
return reject;
}
}
Rectangle { // Focus rectangle
x: -3px;
y: self.x;
width: parent.width - 2*self.x;
height: parent.height - 2*self.y;
border-width: root.enabled && root.has-focus ? 1px : 0px;
border-color: Palette.black;
}
}
component SpinBoxButton inherits Rectangle {
callback clicked <=> touch.clicked;
in-out property<bool> enabled <=> touch.enabled;
background: !root.enabled ? transparent
: touch.pressed ? Palette.neutralLight
: touch.has-hover ? Palette.neutralLighter
: Palette.white;
in-out property <color> symbol-color: !root.enabled ? Palette.neutralTertiary
: touch.pressed || touch.has-hover ? Palette.neutralPrimary
: Palette.neutralSecondary;
touch := TouchArea { }
}
export component SpinBox {
in-out property <int> value;
in property <int> minimum;
in property <int> maximum: 100;
in property <bool> enabled <=> fs.enabled;
out property <bool> has-focus <=> fs.has-focus;
forward-focus: fs;
min-height: max(32px, l.min-height);
horizontal-stretch: 1;
vertical-stretch: 0;
accessible-role: spinbox;
accessible-value: root.value;
accessible-value-minimum: root.minimum;
accessible-value-maximum: root.maximum;
accessible-value-step: (root.maximum - root.minimum) / 100;
fs := FocusScope {
Rectangle {
background: !root.enabled ? Palette.neutralLighter : Palette.white;
}
l := GridLayout {
padding-left: 8px;
padding-top: 3px;
padding-bottom: 3px;
text := Text {
rowspan: 2;
text: root.value;
color: !root.enabled ? Palette.neutralTertiary : Palette.neutralDark;
horizontal-alignment: left;
vertical-alignment: center;
}
Rectangle { width: 8px; }
button := SpinBoxButton {
width: 25px;
enabled: root.enabled;
Path {
commands: "M978.2,688.9l-84.2,82.1c-15.7,15.3-41.1,15.3-56.7,0l-341-304.2L162.6,764.5c-15.5,15.1-41,15.1-56.6,0l-84.3-82.1c-15.6-15.2-15.6-39.9,0-55.2l446.6-398.2c15.7-15.3,41-15.3,56.7,0l6.9,6.7l446.3,398.1C993.9,649,993.9,673.7,978.2,688.9z";
fill: parent.symbol-color;
height: 33%;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
}
clicked => {
if (root.value < root.maximum) {
root.value += 1;
}
root.focus();
}
}
SpinBoxButton {
row: 1; col: 2;
enabled: root.enabled;
Path {
commands: "M21.8,311.1l84.2-82.1c15.7-15.2,41-15.2,56.7,0l341.1,304.1l333.7-297.5c15.5-15.2,41-15.2,56.6,0l84.3,82.1c15.6,15.2,15.6,40,0,55.2L531.7,771c-15.7,15.3-41,15.3-56.7,0l-6.9-6.7L21.8,366.3C6.1,351,6.1,326.3,21.8,311.1z";
fill: parent.symbol-color;
height: 33%;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
}
clicked => {
if (root.value > root.minimum) {
root.value -= 1;
}
root.focus();
}
}
}
Rectangle {
x: root.enabled && root.has-focus ? -2px : 0px;
y: self.x;
width: parent.width - 2*self.x;
height: parent.height - 2*self.y;
border-radius: 2px;
border-width: !root.enabled ? 0px : root.has-focus ? 3px : 1px;
border-color: !root.enabled ? Palette.neutralLighter
: root.has-focus ? Palette.themeSecondary
: Palette.neutralDark;
}
key-pressed(event) => {
if (root.enabled && event.text == Key.UpArrow && root.value < root.maximum) {
root.value += 1;
accept
} else if (root.enabled && event.text == Key.DownArrow && root.value > root.minimum) {
root.value -= 1;
accept
} else {
reject
}
}
}
}
export component ProgressIndicator {
private property <float> prog: (progress - minimum) / (maximum - minimum);
in property<float> maximum: 100;
in property<float> minimum: 0;
in property <float> progress;
in property <bool> indeterminate;
min-height: 2px;
horizontal-stretch: 1;
vertical-stretch: 0;
accessible-role: none;
accessible-value: root.progress;
accessible-value-minimum: root.minimum;
accessible-value-maximum: root.maximum;
container := Rectangle {
background: Palette.neutralLight;
clip: true;
track := Rectangle {
background: !root.indeterminate ? Palette.themePrimary : Palette.primaryGradient;
x: !root.indeterminate ? 0px : (parent.width * mod(animation-tick(), 2s) / 1.5s);
y: (parent.height - self.height) / 2;
width: !root.indeterminate ? parent.width * root.prog : parent.width * 0.3;
border-radius: container.border-radius;
}
}
}
export component Slider {
in property<float> maximum: 100;
in property<float> minimum: 0;
in-out property<float> value;
out property<bool> has-focus: fs.has-focus;
in property<bool> enabled <=> touch.enabled;
callback changed(float);
min-height: 24px;
min-width: 100px;
horizontal-stretch: 1;
vertical-stretch: 0;
accessible-role: slider;
accessible-value: root.value;
accessible-value-minimum: root.minimum;
accessible-value-maximum: root.maximum;
accessible-value-step: (root.maximum - root.minimum) / 100;
Rectangle {
width: parent.width - parent.min-height;
x: parent.height / 2;
height: parent.min-height / 4;
y: (parent.height - self.height) / 2;
border-radius: self.height/2;
background: !root.enabled ? Palette.neutralLighter
: touch.has-hover ? Palette.themeLight
: Palette.neutralTertiaryAlt;
}
Rectangle {
width: (parent.width - parent.min-height) * ((root.value - root.minimum) / (root.maximum - root.minimum));
x: parent.height / 2;
height: parent.min-height / 4;
y: (parent.height - self.height) / 2;
border-radius: self.height/2;
background: !root.enabled ? Palette.neutralTertiary
: touch.has-hover ? Palette.themeSecondary
: Palette.neutralSecondary;
}
handle := Rectangle {
property<length> border: 3px;
width: self.height;
height: parent.height - 2 * self.border;
border-width: 3px;
border-radius: self.height / 2;
border-color: !root.enabled ? Palette.neutralTertiaryAlt
: touch.has-hover ? Palette.themePrimary
: Palette.neutralSecondary;
background: Palette.white;
x: (root.width - handle.width) * (root.value - root.minimum)/(root.maximum - root.minimum);
y: self.border;
}
touch := TouchArea {
width: parent.width;
height: parent.height;
property <float> pressed-value;
pointer-event(event) => {
if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) {
self.pressed-value = root.value;
}
}
moved => {
if (self.enabled && self.pressed) {
root.value = max(root.minimum, min(root.maximum,
self.pressed-value + (touch.mouse-x - touch.pressed-x) * (root.maximum - root.minimum) / (root.width - handle.width)));
root.changed(root.value);
}
}
}
fs := FocusScope {
x:0;
width: 0px;
key-pressed(event) => {
if (self.enabled && event.text == Key.RightArrow) {
root.value = Math.min(root.value + 1, root.maximum);
accept
} else if (self.enabled && event.text == Key.LeftArrow) {
root.value = Math.max(root.value - 1, root.minimum);
accept
} else {
reject
}
}
}
Rectangle { // Focus rectangle
border-width: root.enabled && root.has-focus ? 1px : 0px;
border-color: Palette.black;
}
}
export component GroupBox {
in property <string> title <=> label.text;
in property<bool> enabled: true;
VerticalLayout {
spacing: 8px;
padding-top: 16px;
padding-bottom: 8px;
label := Text {
vertical-stretch: 0;
color: !root.enabled ? Palette.neutralTertiary : Palette.neutralDark;
font-weight: 600;
}
Rectangle {
vertical-stretch: 1;
GridLayout {
@children
}
}
}
}
export component TabWidgetImpl inherits Rectangle {
out property <length> content-x: 0;
out property <length> content-y: root.tabbar-preferred-height;
out property <length> content-height: root.height - root.tabbar-preferred-height;
out property <length> content-width: root.width;
out property <length> tabbar-x: 0;
out property <length> tabbar-y: 0;
out property <length> tabbar-height: root.tabbar-preferred-height;
out property <length> tabbar-width: root.width;
in property <length> tabbar-preferred-height;
in property <length> tabbar-preferred-width;
in property <length> content-min-height;
in property <length> content-min-width;
in property <int> current-index;
in property <int> current-focused;
preferred-width: root.content-min-width;
min-width: max(root.content-min-width, root.tabbar-preferred-width);
preferred-height: root.content-min-height + root.tabbar-preferred-height;
min-height: root.content-min-height + root.tabbar-preferred-height;
}
export component TabImpl inherits Rectangle {
in property<string> title <=> t.text;
in property<bool> enabled: true;
property<bool> has-focus: root.current-focused == root.tab-index;
in-out property<int> current; // The currently selected tab
in property<int> current-focused; // The currently focused tab
in property<int> tab-index; // The index of this tab
in property<int> num-tabs; // The total number of tabs
min-height: t.preferred-height + 16px;
preferred-width: t.preferred-width + 16px;
background: !root.enabled ? Palette.neutralLighter
: touch.pressed ? Palette.neutralLight
: touch.has-hover ? Palette.neutralLighter
: Palette.white;
horizontal-stretch: 0;
vertical-stretch: 0;
accessible-role: tab;
accessible-label: root.title;
touch := TouchArea {
enabled <=> root.enabled;
clicked => {
root.current = root.tab-index;
}
}
t := Text {
width: parent.width;
height: parent.height;
vertical-alignment: center;
horizontal-alignment: center;
color: !root.enabled ? Palette.neutralTertiary : Palette.neutralPrimary;
font-weight: root.current == root.tab-index ? 600 : 500;
}
Rectangle {
height: 3px;
width: touch.has-hover && root.current == root.tab-index ? parent.width : parent.width - 16px;
animate width { duration: 250ms; easing: ease-out; }
background: root.current == root.tab-index ? Palette.themeSecondary : transparent;
y: parent.height - self.height;
x: (parent.width - self.width) / 2;
}
Rectangle { // Focus Rectangle
border-width: root.enabled && root.has-focus ? 1px : 0px;
border-color: Palette.black;
}
}
export component TabBarImpl {
// injected properties:
in-out property<int> current; // The currently selected tab
in-out property<int> current-focused: fs.has-focus ? fs.focused-tab : -1; // The currently focused tab
in-out property<int> num-tabs; // The total number of tabs
HorizontalLayout {
spacing: 8px;
alignment: start;
@children
}
accessible-role: tab;
accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current;
fs := FocusScope {
x:0;
width: 0px; // Do not react on clicks
property<int> focused-tab: 0;
key-pressed(event) => {
if (event.text == "\n") {
root.current = root.current-focused;
return accept;
}
if (event.text == Key.LeftArrow) {
self.focused-tab = Math.max(self.focused-tab - 1, 0);
return accept;
}
if (event.text == Key.RightArrow) {
self.focused-tab = Math.min(self.focused-tab + 1, root.num-tabs - 1);
return accept;
}
return reject;
}
key-released(event) => {
if (event.text == " ") {
root.current = root.current-focused;
return accept;
}
return reject;
}
}
}
export component TabWidget inherits TabWidget {}
export component LineEdit {
in property <length> font-size <=> inner.font-size;
in-out property <string> text <=> inner.text;
in property <string> placeholder-text <=> inner.placeholder-text;
out property <bool> has-focus: inner.has-focus;
in property <bool> enabled <=> inner.enabled;
in property input-type <=> inner.input-type;
in property horizontal-alignment <=> inner.horizontal-alignment;
in property read-only <=> inner.read-only;
callback accepted <=> inner.accepted;
callback edited <=> inner.edited;
forward-focus: inner;
// border-color: root.has-focus ? Palette.highlight-background : #ffffff;
public function select-all() {
inner.select-all();
}
public function cut() {
inner.cut();
}
public function copy() {
inner.copy();
}
public function paste() {
inner.paste();
}
horizontal-stretch: 1;
vertical-stretch: 0;
min-height: max(32px, l.min-height);
Rectangle {
background: !root.enabled ? Palette.neutralLighter : Palette.white;
border-radius: 2px;
border-width: !root.enabled ? 0px : root.has-focus ? 2px : 1px;
border-color: !root.enabled ? Palette.neutralLighter
: root.has-focus ? Palette.themeSecondary
: Palette.neutralPrimary;
}
l := HorizontalLayout {
padding-left: 8px;
padding-right: 8px;
padding-top: 3px;
padding-bottom: 3px;
inner := LineEditInner {
placeholder-color: !self.enabled ? Palette.neutralTertiary : Palette.neutralSecondary;
}
}
}
export component ListView inherits ScrollView {
@children
}
component StandardListViewBase inherits ListView {
private property <length> item-height: self.viewport-height / self.model.length;
private property <length> current-item-y: self.viewport-y + current-item * item-height;
in property<[StandardListViewItem]> model;
in-out property<int> current-item: -1;
for item[idx] in root.model : Rectangle {
l := HorizontalLayout {
padding: 8px;
spacing: 0px;
t := Text {
text: item.text;
color: Palette.neutralPrimary;
}
}
background: idx == root.current-item ? Palette.neutralLighter
: touch.has-hover ? Palette.neutralLighterAlt : transparent;
touch := TouchArea {
width: parent.width;
height: parent.height;
clicked => {
set-current-item(idx);
}
}
}
public function set-current-item(index: int) {
if(index < 0 || index >= model.length) {
return;
}
current-item = index;
if(current-item-y < 0) {
self.viewport-y += 0 - current-item-y;
}
if(current-item-y + item-height > self.visible-height) {
self.viewport-y -= current-item-y + item-height - self.visible-height;
}
}
}
export component StandardListView inherits StandardListViewBase {
FocusScope {
key-pressed(event) => {
if (event.text == Key.UpArrow) {
root.set-current-item(root.current-item - 1);
return accept;
} else if (event.text == Key.DownArrow) {
root.set-current-item(root.current-item + 1);
return accept;
}
reject
}
}
}
export component ComboBox {
in property <[string]> model;
in-out property <int> current-index : 0;
in-out property <string> current-value: root.model[root.current-index];
//property <bool> is-open: false;
callback selected(string);
accessible-role: combobox;
accessible-value <=> root.current-value;
out property has-focus <=> fs.has-focus;
in property enabled <=> fs.enabled;
forward-focus: fs;
fs := FocusScope {
key-pressed(event) => {
if (event.text == Key.UpArrow) {
root.current-index = Math.max(root.current-index - 1, 0);
root.current-value = root.model[root.current-index];
return accept;
} else if (event.text == Key.DownArrow) {
root.current-index = Math.min(root.current-index + 1, root.model.length - 1);
root.current-value = root.model[root.current-index];
return accept;
// PopupWindow can not get hidden again at this time, so do not allow to pop that up.
// } else if (event.text == Key.Return) {
// touch.clicked()
// return accept;
}
return reject;
}
touch := TouchArea {
enabled <=> root.enabled;
clicked => {
root.focus();
popup.show();
}
}
}
Rectangle {
background: !root.enabled ? Palette.neutralLighter : Palette.white;
border-radius: 2px;
border-width: !root.enabled ? 0px : root.has-focus ? 3px : 1px;
border-color: !root.enabled ? Palette.neutralLighter
: root.has-focus ? Palette.themeSecondary
: Palette.neutralPrimary;
}
horizontal-stretch: 1;
vertical-stretch: 0;
min-width: 170px;
min-height: max(32px, l.min-height);
l := HorizontalLayout {
padding-left: 8px;
padding-right: 8px;
padding-bottom: 3px;
padding-top: 3px;
spacing: 8px;
t := Text {
text <=> root.current-value;
horizontal-alignment: left;
vertical-alignment: center;
horizontal-stretch: 1;
color: !root.enabled ? Palette.neutralTertiary
: root.has-focus || touch.has-hover ? Palette.neutralPrimary
: Palette.neutralSecondary;
min-width: 0;
}
Rectangle {
width: 25px;
Path {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
height: 8px;
width: 25px;
commands: "M.22.4.5.64.78.4.74.36.5.6.26.36z";
fill: t.color;
}
}
}
popup := PopupWindow {
x:0;
y: root.height;
width: root.width;
Rectangle {
border-color: Palette.neutralLighter;
border-width: 1px;
/*drop-shadow-color: Palette.neutralTertiary;
drop-shadow-blur: 5px;*/
background: Palette.white;
}
VerticalLayout {
for value[idx] in root.model: Rectangle {
background: idx == root.current-index ? Palette.neutralLighter
: item-area.has-hover ? Palette.neutralLighterAlt : transparent;
VerticalLayout {
padding: 10px;
Text {
text: value;
}
}
item-area := TouchArea {
width: 100%;
height: 100%;
clicked => {
if (root.enabled) {
root.current-index = idx;
root.current-value = value;
root.selected(root.current-value);
}
}
}
}
}
}
}
export component VerticalBox inherits VerticalLayout {
spacing: StyleMetrics.layout-spacing;
padding: StyleMetrics.layout-padding;
}
export component HorizontalBox inherits HorizontalLayout {
spacing: StyleMetrics.layout-spacing;
padding: StyleMetrics.layout-padding;
}
export component GridBox inherits GridLayout {
spacing: StyleMetrics.layout-spacing;
padding: StyleMetrics.layout-padding;
}