// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 import { Button, CheckBox, ListView, Palette, ScrollView, LineEdit } from "std-widgets.slint"; import { EditorFontSettings, EditorSizeSettings, EditorSpaceSettings, EditorPalette } from "./styling.slint"; import { Api, SelectionStackFrame, SelectionStackFilter } from "../api.slint"; import { Icons } from "styling.slint"; component FilterList { public function show() { pop.show(); } public function close() { pop.close(); } out property filter: { if (!self.filter-layout && !self.filter-interactive && self.filter-other) { return SelectionStackFilter.Others; } else if (!self.filter-layout && self.filter-interactive && !self.filter-other) { return SelectionStackFilter.Interactive; } else if (!self.filter-layout && self.filter-interactive && self.filter-other) { return SelectionStackFilter.InteractiveAndOthers; } else if (self.filter-layout && !self.filter-interactive && !self.filter-other) { return SelectionStackFilter.Layouts; } else if (self.filter-layout && !self.filter-interactive && self.filter-other) { return SelectionStackFilter.LayoutsAndOthers; } else if (self.filter-layout && self.filter-interactive && !self.filter-other) { return SelectionStackFilter.LayoutsAndInteractive; } else if (self.filter-layout && self.filter-interactive && self.filter-other) { return SelectionStackFilter.Everything; } return SelectionStackFilter.Nothing; } out property has-filter: !self.filter-layout || !self.filter-interactive || !self.filter-other; in-out property filter-interactive: true; in-out property filter-layout: true; in-out property filter-other: true; width: 0px; height: 0px; pop := PopupWindow { width: self.preferred-width; height: self.preferred-height; close-policy: PopupClosePolicy.close-on-click-outside; Rectangle { border-color: Palette.border; border-width: 1px; border-radius: EditorSizeSettings.radius; drop-shadow-blur: EditorSpaceSettings.default-padding; drop-shadow-color: Palette.foreground.transparentize(0.9); background: Palette.alternate-background; TouchArea { // Just block events from reaching other TouchAreas! } VerticalLayout { padding: EditorSpaceSettings.default-padding; spacing: EditorSpaceSettings.default-spacing; lcb := CheckBox { text: @tr("Layouts"); checked <=> root.filter-layout; } icb := CheckBox { text: @tr("Interactive"); checked <=> root.filter-interactive; } ocb := CheckBox { text: @tr("Other"); checked <=> root.filter-other; } } } } } component PopupInner inherits Rectangle { in property preview-width: 500px; in property preview-height: 900px; callback close(); in-out property filter-interactive: true; in-out property filter-layout: true; in-out property filter-other: true; in-out property filter-text: ""; in property max-popup-height: 900px; in-out property selection-x: 0px; in-out property selection-y: 0px; in-out property <[SelectionStackFrame]> selection-stack: [ { width: 100%, height: 40%, x: 10%, y: 0%, is-in-root-component: true, is-layout: false, is-interactive: true, is-selected: false, type-name: "TypeA", file-name: "thumb.slint", id: "some_a", }, { width: 50%, height: 50%, x: 0%, y: 0%, is-in-root-component: false, is-layout: true, is-interactive: false, is-selected: false, file-name: "thumb.slint", type-name: "HorizontalLayout", }, { width: 50%, height: 10%, x: 20%, y: 40%, is-in-root-component: false, is-layout: false, is-interactive: false, is-selected: false, type-name: "Rectangle", file-name: "tiling.slint", id: "", }, { width: 50%, height: 50%, x: 50%, y: 50%, is-in-root-component: false, is-interactive: false, is-selected: true, type-name: "TypeA", file-name: "finger.slint", id: "some_a", }, { width: 250%, height: 50%, x: 50%, y: 50%, is-in-root-component: false, is-layout: false, is-interactive: true, is-selected: false, type-name: "Button", file-name: "finger.slint", id: "alsoInteractive", }, { width: 50%, height: 50%, x: 0%, y: 50%, is-in-root-component: false, is-layout: true, is-interactive: false, is-selected: false, file-name: "finger.slint", type-name: "VerticalLayout", }, { width: 5%, height: 5%, x: 25%, y: 25%, is-in-root-component: true, is-layout: false, is-interactive: false, is-selected: false, id: "words", type-name: "Text", }, { width: 50%, height: 100%, x: 0%, y: 0%, is-in-root-component: true, is-layout: false, is-interactive: false, is-selected: false, type-name: "Window", id: "", } ]; private property select-on-close: Api.find-selected-selection-stack-frame(self.selection-stack); private property aspect-ratio: preview-width / preview-height; private property max-rect-size: 40px; private property frame-height: self.max-rect-size + EditorSpaceSettings.default-padding * 2 + 1rem; private property max-visible-frames: Math.floor(((self.max-popup-height / 1px) * 0.8) / (self.frame-height / 1px)); private property visible-frames: Math.min(self.max-visible-frames, root.selection-stack.length); function unselect-previewed-selection() { if self.select-on-close.element-path != "" { Api.select-element(self.select-on-close.element-path, self.select-on-close.element-offset, self.select-on-close.x * root.preview-width, self.select-on-close.y * root.preview-height); } } private property <[SelectionStackFrame]> filtered-model: Api.filter-sort-selection-stack(root.selection-stack, filter-edit.text, filter-list.filter); border-radius: EditorSizeSettings.radius; border-width: 0.5px; background: Palette.background; drop-shadow-blur: 10px; drop-shadow-color: black; border-color: lightgray; width: 250px; TouchArea { changed has-hover => { if self.has-hover { root.unselect-previewed-selection(); } } } VerticalLayout { spacing: EditorSpaceSettings.default-spacing; header := HorizontalLayout { padding: EditorSpaceSettings.default-padding; padding-bottom: 0px; spacing: EditorSpaceSettings.default-spacing; filter-button := Button { private property filter-state: filter-list.filter != SelectionStackFilter.Everything || filter-edit.text != ""; changed filter-state => { self.checked = filter-state; } icon: Icons.filter; checkable: true; colorize-icon: true; clicked => { filter-list.show(); filter-edit.clear-focus(); self.checked = filter-state; } init => { self.checked = filter-state; } } filter-edit := LineEdit { placeholder-text: "Filter"; min-width: 50px; text <=> root.filter-text; init => { self.focus(); } changed has-focus => { if !self.has-focus { self.focus(); } } } } Rectangle { VerticalLayout { if filtered-model.length == 0: Rectangle { width: 100%; height: (root.visible-frames * root.frame-height); Text { text: @tr("No match"); } } if filtered-model.length >= 1: ScrollView { height: (root.visible-frames * root.frame-height); list-view := VerticalLayout { for frame[index] in root.filtered-model: frame-rect := Rectangle { height: root.frame-height; function frame-color(frame: SelectionStackFrame) -> brush { return EditorPalette.interactive-element-selection-secondary; } function frame-background(frame: SelectionStackFrame) -> brush { if frame.is-interactive { return EditorPalette.interactive-element-selection-primary; } else if frame.is-layout { return transparent; } else if frame.is-selected { EditorPalette.general-element-selection-selected; } else { return EditorPalette.general-element-selection-primary; } } function calculate_pos(p: length, percent: float) -> length { return Math.round((p / 1px) * percent) * 1px; } function calculate_size(p: length, percent: float) -> length { return Math.max(Math.round((p / 1px) * percent), 2) * 1px; } VerticalLayout { padding-top: EditorSpaceSettings.default-padding / 2; if !frame.is-in-root-component: Text { x: EditorSpaceSettings.default-padding; text: frame.file-name; overflow: elide; color: frame.is-selected ? Palette.accent-foreground : Palette.foreground; font-size: EditorFontSettings.label-sub.font-size - 3px; font-italic: true; } HorizontalLayout { visible: (frame.type-name == "Window") ? false : true; padding-left: EditorSpaceSettings.default-padding; spacing: EditorSpaceSettings.default-spacing / 2.0; alignment: start; VerticalLayout { alignment: center; padding-bottom: EditorSpaceSettings.default-padding / 2; Rectangle { function calculate_aspect_ratio_box(aspect-ratio: float, max-rect-length: length) -> length { return Math.min(aspect-ratio, 1.0) * max-rect-length; } clip: true; width: calculate_aspect_ratio_box(root.aspect-ratio, root.max-rect-size) + EditorSpaceSettings.default-padding; height: calculate_aspect_ratio_box(1.0 / root.aspect-ratio, root.max-rect-size) + EditorSpaceSettings.default-padding / 2; measure-rect := Rectangle { width: calculate_aspect_ratio_box(root.aspect-ratio, root.max-rect-size); height: calculate_aspect_ratio_box(1.0 / root.aspect-ratio, root.max-rect-size); border-color: frame.is-selected ? EditorPalette.general-element-selection-selected.transparentize(0.5) : Palette.foreground.transparentize(0.65); border-width: 1px; placeholder-rect := Rectangle { x: calculate_pos(measure-rect.width, frame.x); y: calculate_pos(measure-rect.height, frame.y); width: calculate_size(measure-rect.width, frame.width); height: calculate_size(measure-rect.height, frame.height); border-color: frame-rect.frame-color(frame); border-width: 0.5px; background: frame-rect.frame-background(frame); if frame.is-layout: Rectangle { border-color: EditorPalette.layout-element-selection-secondary; border-width: 0.5px; if (frame.type-name == "HorizontalLayout" || frame.type-name == "HorizontalBox"): HorizontalLayout { spacing: 1px; padding: 1px; Rectangle { background: EditorPalette.layout-element-selection-primary; } Rectangle { background: EditorPalette.layout-element-selection-primary; } } if (frame.type-name == "VerticalLayout" || frame.type-name == "VerticalBox"): VerticalLayout { spacing: 1.5px; padding: 1.5px; Rectangle { background: EditorPalette.layout-element-selection-primary; } Rectangle { background: EditorPalette.layout-element-selection-primary; } } if (frame.type-name == "GridLayout" || frame.type-name == "GridBox"): VerticalLayout { spacing: 1px; HorizontalLayout { spacing: 1.5px; padding: 1.5px; Rectangle { background: EditorPalette.layout-element-selection-primary; } Rectangle { background: EditorPalette.layout-element-selection-primary; } } HorizontalLayout { spacing: 1.5px; padding: 1.5px; Rectangle { background: EditorPalette.layout-element-selection-primary; } Rectangle { background: EditorPalette.layout-element-selection-primary; } } } } } } } } VerticalLayout { spacing: EditorSpaceSettings.default-spacing / 2; alignment: center; if frame.id != "": Text { text: frame.id; overflow: elide; color: frame.is-selected ? Palette.accent-foreground : Palette.foreground; font-weight: EditorFontSettings.bold-font-weight; font-size: EditorFontSettings.label.font-size; } Text { text: frame.type-name; overflow: elide; color: frame.is-selected ? Palette.accent-foreground : Palette.foreground; font-size: EditorFontSettings.label-sub.font-size; font-italic: true; } } } Rectangle { height: 1px; background: Palette.border; } } x: -EditorSpaceSettings.default-padding / 2; border-width: 0px; states [ hover when ta.has-hover && !frame.is-selected: { background: Palette.accent-background.transparentize(0.9); } unhover when !ta.has-hover && !frame.is-selected: { background: Palette.background; } selected when frame.is-selected: { background: Palette.accent-background; } ] ta := TouchArea { clicked() => { Api.select-element(frame.element-path, frame.element-offset, frame.x * root.preview-width, frame.y * root.preview-height); root.select-on-close = { }; root.close(); } changed has-hover => { if self.has-hover { Api.select-element(frame.element-path, frame.element-offset, frame.x * root.preview-width, frame.y * root.preview-height); } } } } } } } Rectangle { height: 3px; y: 0px; width: 100%; background: EditorPalette.shadow-gradient; } } } filter-list := FilterList { x: filter-button.x + EditorSpaceSettings.default-spacing; y: filter-button.y + filter-button.height + EditorSpaceSettings.default-spacing; filter-layout <=> root.filter-layout; filter-interactive <=> root.filter-interactive; filter-other <=> root.filter-other; } } export component SelectionPopup { public function show-selection-stack(x: length, y: length) { self.selection-x = x; self.selection-y = y; self.selection-stack = Api.selection-stack-at(self.selection-x, self.selection-y); popup.show(); } in property preview-width; in property preview-height; in property max-popup-height; private property <[SelectionStackFrame]> selection-stack; private property selection-x; private property selection-y; private property filter-interactive: true; private property filter-layout: true; private property filter-other: true; private property filter-text: ""; popup := PopupWindow { in property preview-width: root.preview-width; in property preview-height: root.preview-height; in-out property <[SelectionStackFrame]> selection-stack: root.selection-stack; in-out property selection-x: root.selection-x; in-out property selection-y: root.selection-y; in property max-popup-height: root.max-popup-height; in-out property filter-interactive <=> root.filter-interactive; in-out property filter-layout <=> root.filter-layout; in-out property filter-other <=> root.filter-other; in-out property filter-text <=> root.filter-text; close-policy: PopupClosePolicy.close-on-click-outside; max-height: root.max-popup-height; x: root.selection-x + 10px; y: root.selection-y + 10px; VerticalLayout { inner := PopupInner { close() => { popup.close(); } filter-text <=> popup.filter-text; filter-layout <=> popup.filter-layout; filter-interactive <=> popup.filter-interactive; filter-other <=> popup.filter-other; preview-width <=> popup.preview-width; preview-height <=> popup.preview-height; max-popup-height <=> popup.max-popup-height; selection-stack <=> popup.selection-stack; selection-x <=> popup.selection-x; selection-y <=> popup.selection-y; } } } }