diff --git a/REUSE.toml b/REUSE.toml index c6a05e120a7..97493328c2e 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -159,6 +159,7 @@ path = [ "tools/lsp/ui/assets/chevron-down.svg", "tools/lsp/ui/assets/inspect.svg", "tools/lsp/ui/assets/layout-sidebar**.svg", + "tools/lsp/ui/assets/search.svg", "tools/lsp/ui/assets/sync.svg", ] precedence = "aggregate" diff --git a/tools/lsp/preview/element_selection.rs b/tools/lsp/preview/element_selection.rs index 91a82124fec..cfc4a5c5656 100644 --- a/tools/lsp/preview/element_selection.rs +++ b/tools/lsp/preview/element_selection.rs @@ -444,6 +444,35 @@ pub fn selection_stack_at( Rc::new(slint::VecModel::from(result)).into() } +pub fn filter_sort_selection_stack( + model: slint::ModelRc, + filter: slint::SharedString, +) -> slint::ModelRc { + use slint::ModelExt; + + eprintln!("filter: {filter}"); + + let filter = filter.to_string(); + + if filter.is_empty() { + model + } else if filter.as_str().chars().any(|c| !c.is_lowercase()) { + Rc::new(model.filter(move |frame| { + frame.id.contains(&filter) + || frame.type_name.contains(&filter) + || frame.file_name.contains(&filter) + })) + .into() + } else { + Rc::new(model.filter(move |frame| { + frame.id.to_lowercase().contains(&filter) + || frame.type_name.to_lowercase().contains(&filter) + || frame.file_name.to_lowercase().contains(&filter) + })) + .into() + } +} + pub fn parent_layout_kind(element: &common::ElementRcNode) -> ui::LayoutKind { element.parent().map(|p| p.layout_kind()).unwrap_or(ui::LayoutKind::None) } diff --git a/tools/lsp/preview/ui.rs b/tools/lsp/preview/ui.rs index 776578cfba3..5a6cd728009 100644 --- a/tools/lsp/preview/ui.rs +++ b/tools/lsp/preview/ui.rs @@ -72,6 +72,7 @@ pub fn create_ui(style: String, experimental: bool) -> Result [SelectionStackFrame]; + pure callback filter-sort-selection-stack(model: [SelectionStackFrame], filter: string) -> [SelectionStackFrame]; pure callback find-selected-selection-stack-frame([SelectionStackFrame]) -> SelectionStackFrame; callback select-element(file: string, offset: int, x: length, y: length); diff --git a/tools/lsp/ui/assets/search.svg b/tools/lsp/ui/assets/search.svg new file mode 100644 index 00000000000..13c4df07b7f --- /dev/null +++ b/tools/lsp/ui/assets/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tools/lsp/ui/components/selection-popup.slint b/tools/lsp/ui/components/selection-popup.slint index aa199a78c20..9dc5649b2c3 100644 --- a/tools/lsp/ui/components/selection-popup.slint +++ b/tools/lsp/ui/components/selection-popup.slint @@ -1,14 +1,17 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -import { ListView, Palette, ScrollView } from "std-widgets.slint"; +import { ListView, Palette, ScrollView, LineEdit } from "std-widgets.slint"; import { EditorFontSettings, EditorSizeSettings, EditorSpaceSettings, EditorPalette } from "./styling.slint"; import { Api, SelectionStackFrame } from "../api.slint"; +import { Icons } from "styling.slint"; component PopupInner inherits Rectangle { in property preview-width: 500px; in property preview-height: 900px; + callback close(); + in property max-popup-height: 900px; in-out property selection-x: 0px; in-out property selection-y: 0px; @@ -138,113 +141,111 @@ component PopupInner inherits Rectangle { border-color: lightgray; width: 250px; - height: self.visible-frames * self.frame-height; - - ScrollView { - list-view := VerticalLayout { + VerticalLayout { + padding: EditorSpaceSettings.default-padding; + spacing: EditorSpaceSettings.default-spacing; - for frame[index] in root.selection-stack: frame-rect := Rectangle { - width: 100%; - height: root.frame-height; + header := HorizontalLayout { + spacing: EditorSpaceSettings.default-spacing; - 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; - } + Rectangle { + Image { + source: Icons.search; + colorize: Palette.foreground; + width: EditorSizeSettings.default-icon-width; + height: EditorSizeSettings.default-icon-width; } - 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; + } + + filter-edit := LineEdit { + placeholder-text: "Filter"; + + init => { + self.focus(); } + } + } - 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; - } + ScrollView { + height: (root.visible-frames * root.frame-height); - HorizontalLayout { - visible: (frame.type-name == "Window") ? false : true; - padding-left: EditorSpaceSettings.default-padding / 2.0; - spacing: EditorSpaceSettings.default-spacing / 2.0; - alignment: start; + list-view := VerticalLayout { - VerticalLayout { - alignment: center; - padding-bottom: EditorSpaceSettings.default-padding / 2; + for frame[index] in Api.filter-sort-selection-stack(root.selection-stack, filter-edit.text): frame-rect := Rectangle { + height: root.frame-height; - 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; + 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; + } - Rectangle { - background: EditorPalette.layout-element-selection-primary; - } + 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; + } - Rectangle { - background: EditorPalette.layout-element-selection-primary; - } - } + HorizontalLayout { + visible: (frame.type-name == "Window") ? false : true; + padding-left: EditorSpaceSettings.default-padding / 2.0; + spacing: EditorSpaceSettings.default-spacing / 2.0; + alignment: start; - if (frame.type-name == "VerticalLayout" || frame.type-name == "VerticalBox"): VerticalLayout { - spacing: 1.5px; - padding: 1.5px; + VerticalLayout { + alignment: center; + padding-bottom: EditorSpaceSettings.default-padding / 2; - Rectangle { - background: EditorPalette.layout-element-selection-primary; - } + 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; - } - } - 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; } @@ -254,9 +255,10 @@ component PopupInner inherits Rectangle { } } - HorizontalLayout { + if (frame.type-name == "VerticalLayout" || frame.type-name == "VerticalBox"): VerticalLayout { spacing: 1.5px; padding: 1.5px; + Rectangle { background: EditorPalette.layout-element-selection-primary; } @@ -265,64 +267,91 @@ component PopupInner inherits 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 { - padding-left: EditorSpaceSettings.default-padding; - 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; - } + VerticalLayout { + padding-left: EditorSpaceSettings.default-padding; + 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 + (frame.is-interactive ? " (TouchArea)" : ""); - overflow: elide; - color: frame.is-selected ? Palette.accent-foreground : Palette.foreground; - font-size: EditorFontSettings.label-sub.font-size; - font-italic: true; + Text { + text: frame.type-name + (frame.is-interactive ? " (TouchArea)" : ""); + 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; + Rectangle { + height: 1px; + background: Palette.border; + } } - } - 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; - } - ] + 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 = {}; - } - changed has-hover => { - if self.has-hover { + ta := TouchArea { + clicked() => { Api.select-element(frame.element-path, frame.element-offset, frame.x * root.preview-width, frame.y * root.preview-height); - } else { - root.unselect-previewed-selection(); + 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); + } else { + root.unselect-previewed-selection(); + } } } } @@ -355,6 +384,8 @@ export component SelectionPopup { in-out property selection-y: root.selection-y; in property max-popup-height: root.max-popup-height; + close-policy: PopupClosePolicy.close-on-click-outside; + max-height: root.max-popup-height; x: root.selection-x + 10px; @@ -362,6 +393,10 @@ export component SelectionPopup { VerticalLayout { inner := PopupInner { + close() => { + popup.close(); + } + preview-width <=> popup.preview-width; preview-height <=> popup.preview-height; diff --git a/tools/lsp/ui/components/styling.slint b/tools/lsp/ui/components/styling.slint index 5831036304b..f2985923536 100644 --- a/tools/lsp/ui/components/styling.slint +++ b/tools/lsp/ui/components/styling.slint @@ -7,12 +7,12 @@ export global Icons { out property add: @image-url("../assets/add.svg"); out property chevron-down: @image-url("../assets/chevron-down.svg"); out property inspect: @image-url("../assets/inspect.svg"); + out property search: @image-url("../assets/search.svg"); out property sidebar-left-off: @image-url("../assets/layout-sidebar-left-off.svg"); out property sidebar-right-off: @image-url("../assets/layout-sidebar-right-off.svg"); out property sidebar-left: @image-url("../assets/layout-sidebar-left.svg"); out property sidebar-right: @image-url("../assets/layout-sidebar-right.svg"); out property sync: @image-url("../assets/sync.svg"); - } export struct TextStyle { @@ -56,7 +56,6 @@ export global EditorSpaceSettings { in property default-spacing: 8px; in property group-indent: 10px; in property property-spacing: 5px; - } export global EditorSizeSettings {