1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4import { Button, CheckBox, ListView, Palette, ScrollView, LineEdit } from "std-widgets.slint";
5import { EditorFontSettings, EditorSizeSettings, EditorSpaceSettings, EditorPalette } from "./styling.slint";
6import { Api, SelectionStackFrame, SelectionStackFilter } from "../api.slint";
7import { Icons } from "styling.slint";
8
9component FilterList {
10 public function show() { pop.show(); }
11 public function close() { pop.close(); }
12
13 out property <SelectionStackFilter> filter: {
14 if (!self.filter-layout && !self.filter-interactive && self.filter-other) {
15 return SelectionStackFilter.Others;
16 } else if (!self.filter-layout && self.filter-interactive && !self.filter-other) {
17 return SelectionStackFilter.Interactive;
18 } else if (!self.filter-layout && self.filter-interactive && self.filter-other) {
19 return SelectionStackFilter.InteractiveAndOthers;
20 } else if (self.filter-layout && !self.filter-interactive && !self.filter-other) {
21 return SelectionStackFilter.Layouts;
22 } else if (self.filter-layout && !self.filter-interactive && self.filter-other) {
23 return SelectionStackFilter.LayoutsAndOthers;
24 } else if (self.filter-layout && self.filter-interactive && !self.filter-other) {
25 return SelectionStackFilter.LayoutsAndInteractive;
26 } else if (self.filter-layout && self.filter-interactive && self.filter-other) {
27 return SelectionStackFilter.Everything;
28 }
29 return SelectionStackFilter.Nothing;
30 }
31 out property <bool> has-filter: !self.filter-layout || !self.filter-interactive || !self.filter-other;
32
33 in-out property <bool> filter-interactive: true;
34 in-out property <bool> filter-layout: true;
35 in-out property <bool> filter-other: true;
36
37 width: 0px;
38 height: 0px;
39
40 pop := PopupWindow {
41 width: self.preferred-width;
42 height: self.preferred-height;
43
44 close-policy: PopupClosePolicy.close-on-click-outside;
45
46 Rectangle {
47 border-color: Palette.border;
48 border-width: 1px;
49 border-radius: EditorSizeSettings.radius;
50
51 drop-shadow-blur: EditorSpaceSettings.default-padding;
52 drop-shadow-color: Palette.foreground.transparentize(0.9);
53
54 background: Palette.alternate-background;
55
56 TouchArea {
57 // Just block events from reaching other TouchAreas!
58 }
59
60 VerticalLayout {
61 padding: EditorSpaceSettings.default-padding;
62 spacing: EditorSpaceSettings.default-spacing;
63
64 lcb := CheckBox {
65 text: @tr("Layouts");
66 checked <=> root.filter-layout;
67 }
68
69 icb := CheckBox {
70 text: @tr("Interactive");
71 checked <=> root.filter-interactive;
72 }
73
74 ocb := CheckBox {
75 text: @tr("Other");
76 checked <=> root.filter-other;
77 }
78 }
79 }
80 }
81}
82
83component PopupInner inherits Rectangle {
84 in property <length> preview-width: 500px;
85 in property <length> preview-height: 900px;
86
87 callback close();
88
89 in-out property <bool> filter-interactive: true;
90 in-out property <bool> filter-layout: true;
91 in-out property <bool> filter-other: true;
92 in-out property <string> filter-text: "";
93
94 in property <length> max-popup-height: 900px;
95 in-out property <length> selection-x: 0px;
96 in-out property <length> selection-y: 0px;
97 in-out property <[SelectionStackFrame]> selection-stack: [
98 {
99 width: 100%,
100 height: 40%,
101 x: 10%,
102 y: 0%,
103 is-in-root-component: true,
104 is-layout: false,
105 is-interactive: true,
106 is-selected: false,
107 type-name: "TypeA",
108 file-name: "thumb.slint",
109 id: "some_a",
110 },
111 {
112 width: 50%,
113 height: 50%,
114 x: 0%,
115 y: 0%,
116 is-in-root-component: false,
117 is-layout: true,
118 is-interactive: false,
119 is-selected: false,
120 file-name: "thumb.slint",
121 type-name: "HorizontalLayout",
122 },
123 {
124 width: 50%,
125 height: 10%,
126 x: 20%,
127 y: 40%,
128 is-in-root-component: false,
129 is-layout: false,
130 is-interactive: false,
131 is-selected: false,
132 type-name: "Rectangle",
133 file-name: "tiling.slint",
134 id: "",
135 },
136 {
137 width: 50%,
138 height: 50%,
139 x: 50%,
140 y: 50%,
141 is-in-root-component: false,
142 is-interactive: false,
143 is-selected: true,
144 type-name: "TypeA",
145 file-name: "finger.slint",
146 id: "some_a",
147 },
148 {
149 width: 250%,
150 height: 50%,
151 x: 50%,
152 y: 50%,
153 is-in-root-component: false,
154 is-layout: false,
155 is-interactive: true,
156 is-selected: false,
157 type-name: "Button",
158 file-name: "finger.slint",
159 id: "alsoInteractive",
160 },
161 {
162 width: 50%,
163 height: 50%,
164 x: 0%,
165 y: 50%,
166 is-in-root-component: false,
167 is-layout: true,
168 is-interactive: false,
169 is-selected: false,
170 file-name: "finger.slint",
171 type-name: "VerticalLayout",
172 },
173 {
174 width: 5%,
175 height: 5%,
176 x: 25%,
177 y: 25%,
178 is-in-root-component: true,
179 is-layout: false,
180 is-interactive: false,
181 is-selected: false,
182 id: "words",
183 type-name: "Text",
184 },
185 {
186 width: 50%,
187 height: 100%,
188 x: 0%,
189 y: 0%,
190 is-in-root-component: true,
191 is-layout: false,
192 is-interactive: false,
193 is-selected: false,
194 type-name: "Window",
195 id: "",
196 }
197 ];
198
199 private property <SelectionStackFrame> select-on-close: Api.find-selected-selection-stack-frame(self.selection-stack);
200
201 private property <float> aspect-ratio: preview-width / preview-height;
202
203 private property <length> max-rect-size: 40px;
204 private property <length> frame-height: self.max-rect-size + EditorSpaceSettings.default-padding * 2 + 1rem;
205 private property <int> max-visible-frames: Math.floor(((self.max-popup-height / 1px) * 0.8) / (self.frame-height / 1px));
206 private property <int> visible-frames: Math.min(self.max-visible-frames, root.selection-stack.length);
207
208 function unselect-previewed-selection() {
209 if self.select-on-close.element-path != "" {
210 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);
211 }
212 }
213
214 private property <[SelectionStackFrame]> filtered-model: Api.filter-sort-selection-stack(root.selection-stack, filter-edit.text, filter-list.filter);
215 border-radius: EditorSizeSettings.radius;
216
217 border-width: 0.5px;
218 background: Palette.background;
219 drop-shadow-blur: 10px;
220 drop-shadow-color: black;
221 border-color: lightgray;
222
223 width: 250px;
224
225 TouchArea {
226 changed has-hover => {
227 if self.has-hover {
228 root.unselect-previewed-selection();
229 }
230 }
231 }
232
233 VerticalLayout {
234 spacing: EditorSpaceSettings.default-spacing;
235
236 header := HorizontalLayout {
237 padding: EditorSpaceSettings.default-padding;
238 padding-bottom: 0px;
239 spacing: EditorSpaceSettings.default-spacing;
240
241 filter-button := Button {
242 private property <bool> filter-state: filter-list.filter != SelectionStackFilter.Everything || filter-edit.text != "";
243
244 changed filter-state => {
245 self.checked = filter-state;
246 }
247
248 icon: Icons.filter;
249 checkable: true;
250 colorize-icon: true;
251
252 clicked => {
253 filter-list.show();
254 filter-edit.clear-focus();
255 self.checked = filter-state;
256 }
257
258 init => {
259 self.checked = filter-state;
260 }
261 }
262
263 filter-edit := LineEdit {
264 placeholder-text: "Filter";
265 min-width: 50px;
266 text <=> root.filter-text;
267 init => {
268 self.focus();
269 }
270
271 changed has-focus => {
272 if !self.has-focus {
273 self.focus();
274 }
275 }
276 }
277 }
278
279 Rectangle {
280 VerticalLayout {
281 if filtered-model.length == 0: Rectangle {
282 width: 100%;
283 height: (root.visible-frames * root.frame-height);
284
285 Text {
286 text: @tr("No match");
287 }
288 }
289
290 if filtered-model.length >= 1: ScrollView {
291 height: (root.visible-frames * root.frame-height);
292
293 list-view := VerticalLayout {
294 for frame[index] in root.filtered-model: frame-rect := Rectangle {
295 height: root.frame-height;
296
297 function frame-color(frame: SelectionStackFrame) -> brush {
298 return EditorPalette.interactive-element-selection-secondary;
299 }
300 function frame-background(frame: SelectionStackFrame) -> brush {
301 if frame.is-interactive {
302 return EditorPalette.interactive-element-selection-primary;
303 } else if frame.is-layout {
304 return transparent;
305 } else if frame.is-selected {
306 EditorPalette.general-element-selection-selected;
307 } else {
308 return EditorPalette.general-element-selection-primary;
309 }
310 }
311 function calculate_pos(p: length, percent: float) -> length {
312 return Math.round((p / 1px) * percent) * 1px;
313 }
314 function calculate_size(p: length, percent: float) -> length {
315 return Math.max(Math.round((p / 1px) * percent), 2) * 1px;
316 }
317 VerticalLayout {
318 padding-top: EditorSpaceSettings.default-padding / 2;
319 if !frame.is-in-root-component: Text {
320 x: EditorSpaceSettings.default-padding;
321 text: frame.file-name;
322 overflow: elide;
323 color: frame.is-selected ? Palette.accent-foreground : Palette.foreground;
324 font-size: EditorFontSettings.label-sub.font-size - 3px;
325 font-italic: true;
326 }
327
328 HorizontalLayout {
329 visible: (frame.type-name == "Window") ? false : true;
330 padding-left: EditorSpaceSettings.default-padding;
331 spacing: EditorSpaceSettings.default-spacing / 2.0;
332 alignment: start;
333
334 VerticalLayout {
335 alignment: center;
336 padding-bottom: EditorSpaceSettings.default-padding / 2;
337
338 Rectangle {
339 function calculate_aspect_ratio_box(aspect-ratio: float, max-rect-length: length) -> length {
340 return Math.min(aspect-ratio, 1.0) * max-rect-length;
341 }
342 clip: true;
343 width: calculate_aspect_ratio_box(root.aspect-ratio, root.max-rect-size) + EditorSpaceSettings.default-padding;
344 height: calculate_aspect_ratio_box(1.0 / root.aspect-ratio, root.max-rect-size) + EditorSpaceSettings.default-padding / 2;
345 measure-rect := Rectangle {
346 width: calculate_aspect_ratio_box(root.aspect-ratio, root.max-rect-size);
347 height: calculate_aspect_ratio_box(1.0 / root.aspect-ratio, root.max-rect-size);
348
349 border-color: frame.is-selected ? EditorPalette.general-element-selection-selected.transparentize(0.5) : Palette.foreground.transparentize(0.65);
350 border-width: 1px;
351 placeholder-rect := Rectangle {
352 x: calculate_pos(measure-rect.width, frame.x);
353 y: calculate_pos(measure-rect.height, frame.y);
354 width: calculate_size(measure-rect.width, frame.width);
355 height: calculate_size(measure-rect.height, frame.height);
356
357 border-color: frame-rect.frame-color(frame);
358 border-width: 0.5px;
359 background: frame-rect.frame-background(frame);
360 if frame.is-layout: Rectangle {
361 border-color: EditorPalette.layout-element-selection-secondary;
362 border-width: 0.5px;
363 if (frame.type-name == "HorizontalLayout" || frame.type-name == "HorizontalBox"): HorizontalLayout {
364 spacing: 1px;
365 padding: 1px;
366
367 Rectangle {
368 background: EditorPalette.layout-element-selection-primary;
369 }
370
371 Rectangle {
372 background: EditorPalette.layout-element-selection-primary;
373 }
374 }
375
376 if (frame.type-name == "VerticalLayout" || frame.type-name == "VerticalBox"): VerticalLayout {
377 spacing: 1.5px;
378 padding: 1.5px;
379
380 Rectangle {
381 background: EditorPalette.layout-element-selection-primary;
382 }
383
384 Rectangle {
385 background: EditorPalette.layout-element-selection-primary;
386 }
387 }
388 if (frame.type-name == "GridLayout" || frame.type-name == "GridBox"): VerticalLayout {
389 spacing: 1px;
390 HorizontalLayout {
391 spacing: 1.5px;
392 padding: 1.5px;
393 Rectangle {
394 background: EditorPalette.layout-element-selection-primary;
395 }
396
397 Rectangle {
398 background: EditorPalette.layout-element-selection-primary;
399 }
400 }
401
402 HorizontalLayout {
403 spacing: 1.5px;
404 padding: 1.5px;
405 Rectangle {
406 background: EditorPalette.layout-element-selection-primary;
407 }
408
409 Rectangle {
410 background: EditorPalette.layout-element-selection-primary;
411 }
412 }
413 }
414 }
415 }
416 }
417 }
418 }
419
420 VerticalLayout {
421 spacing: EditorSpaceSettings.default-spacing / 2;
422 alignment: center;
423
424 if frame.id != "": Text {
425 text: frame.id;
426 overflow: elide;
427 color: frame.is-selected ? Palette.accent-foreground : Palette.foreground;
428 font-weight: EditorFontSettings.bold-font-weight;
429 font-size: EditorFontSettings.label.font-size;
430 }
431
432 Text {
433 text: frame.type-name;
434 overflow: elide;
435 color: frame.is-selected ? Palette.accent-foreground : Palette.foreground;
436 font-size: EditorFontSettings.label-sub.font-size;
437 font-italic: true;
438 }
439 }
440 }
441
442 Rectangle {
443 height: 1px;
444 background: Palette.border;
445 }
446 }
447
448 x: -EditorSpaceSettings.default-padding / 2;
449 border-width: 0px;
450 states [
451 hover when ta.has-hover && !frame.is-selected: {
452 background: Palette.accent-background.transparentize(0.9);
453 }
454 unhover when !ta.has-hover && !frame.is-selected: {
455 background: Palette.background;
456 }
457 selected when frame.is-selected: {
458 background: Palette.accent-background;
459 }
460 ]
461
462 ta := TouchArea {
463 clicked() => {
464 Api.select-element(frame.element-path, frame.element-offset, frame.x * root.preview-width, frame.y * root.preview-height);
465 root.select-on-close = { };
466 root.close();
467 }
468 changed has-hover => {
469 if self.has-hover {
470 Api.select-element(frame.element-path, frame.element-offset, frame.x * root.preview-width, frame.y * root.preview-height);
471 }
472 }
473 }
474 }
475 }
476 }
477 }
478
479 Rectangle {
480 height: 3px;
481 y: 0px;
482 width: 100%;
483 background: EditorPalette.shadow-gradient;
484 }
485 }
486 }
487
488 filter-list := FilterList {
489 x: filter-button.x + EditorSpaceSettings.default-spacing;
490 y: filter-button.y + filter-button.height + EditorSpaceSettings.default-spacing;
491
492 filter-layout <=> root.filter-layout;
493 filter-interactive <=> root.filter-interactive;
494 filter-other <=> root.filter-other;
495 }
496}
497
498export component SelectionPopup {
499 public function show-selection-stack(x: length, y: length) {
500 self.selection-x = x;
501 self.selection-y = y;
502 self.selection-stack = Api.selection-stack-at(self.selection-x, self.selection-y);
503
504 popup.show();
505 }
506
507 in property <length> preview-width;
508 in property <length> preview-height;
509 in property <length> max-popup-height;
510 private property <[SelectionStackFrame]> selection-stack;
511 private property <length> selection-x;
512 private property <length> selection-y;
513
514 private property <bool> filter-interactive: true;
515 private property <bool> filter-layout: true;
516 private property <bool> filter-other: true;
517 private property <string> filter-text: "";
518
519 popup := PopupWindow {
520 in property <length> preview-width: root.preview-width;
521 in property <length> preview-height: root.preview-height;
522 in-out property <[SelectionStackFrame]> selection-stack: root.selection-stack;
523 in-out property <length> selection-x: root.selection-x;
524 in-out property <length> selection-y: root.selection-y;
525 in property <length> max-popup-height: root.max-popup-height;
526
527 in-out property <bool> filter-interactive <=> root.filter-interactive;
528 in-out property <bool> filter-layout <=> root.filter-layout;
529 in-out property <bool> filter-other <=> root.filter-other;
530 in-out property <string> filter-text <=> root.filter-text;
531
532 close-policy: PopupClosePolicy.close-on-click-outside;
533
534 max-height: root.max-popup-height;
535
536 x: root.selection-x + 10px;
537 y: root.selection-y + 10px;
538
539 VerticalLayout {
540 inner := PopupInner {
541 close() => {
542 popup.close();
543 }
544
545 filter-text <=> popup.filter-text;
546 filter-layout <=> popup.filter-layout;
547 filter-interactive <=> popup.filter-interactive;
548 filter-other <=> popup.filter-other;
549
550 preview-width <=> popup.preview-width;
551 preview-height <=> popup.preview-height;
552
553 max-popup-height <=> popup.max-popup-height;
554 selection-stack <=> popup.selection-stack;
555 selection-x <=> popup.selection-x;
556 selection-y <=> popup.selection-y;
557 }
558 }
559 }
560}
561