// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.0 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 text <=> text.text; in-out property checked; out property has-focus: fs.has-focus; in property 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 enabled <=> touch.enabled; background: !root.enabled ? transparent : touch.pressed ? Palette.neutralLight : touch.has-hover ? Palette.neutralLighter : Palette.white; in-out property symbol-color: !root.enabled ? Palette.neutralTertiary : touch.pressed || touch.has-hover ? Palette.neutralPrimary : Palette.neutralSecondary; touch := TouchArea { } } export component SpinBox { in-out property value; in property minimum; in property maximum: 100; in property enabled <=> fs.enabled; out property 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 prog: (progress - minimum) / (maximum - minimum); in property maximum: 100; in property minimum: 0; in property progress; in property 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 maximum: 100; in property minimum: 0; in-out property value; out property has-focus: fs.has-focus; in property 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 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 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 title <=> label.text; in property 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 content-x: 0; out property content-y: root.tabbar-preferred-height; out property content-height: root.height - root.tabbar-preferred-height; out property content-width: root.width; out property tabbar-x: 0; out property tabbar-y: 0; out property tabbar-height: root.tabbar-preferred-height; out property tabbar-width: root.width; in property tabbar-preferred-height; in property tabbar-preferred-width; in property content-min-height; in property content-min-width; in property current-index; in property 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 title <=> t.text; in property enabled: true; property has-focus: root.current-focused == root.tab-index; in-out property current; // The currently selected tab in property current-focused; // The currently focused tab in property tab-index; // The index of this tab in property 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 current; // The currently selected tab in-out property current-focused: fs.has-focus ? fs.focused-tab : -1; // The currently focused tab in-out property 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 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 font-size <=> inner.font-size; in-out property text <=> inner.text; in property placeholder-text <=> inner.placeholder-text; out property has-focus: inner.has-focus; in property 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 item-height: self.viewport-height / self.model.length; private property current-item-y: self.viewport-y + current-item * item-height; in property<[StandardListViewItem]> model; in-out property 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 current-index : 0; in-out property current-value: root.model[root.current-index]; //property 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; }