1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
3
4// cSpell: ignore resizer
5
6import { Button, ComboBox, HorizontalBox, ListView, ScrollView, Palette, VerticalBox } from "std-widgets.slint";
7import { Diagnostics, DiagnosticsOverlay } from "diagnostics-overlay.slint";
8import { Resizer } from "resizer.slint";
9
10enum SelectionKind {
11 none,
12 select_at,
13 select_up_or_down,
14}
15
16struct SelectionRectangle {
17 x: length,
18 y: length,
19 width: length,
20 height: length,
21}
22
23export struct Selection {
24 geometry: SelectionRectangle,
25 border-color: color,
26 is-primary: bool,
27 is-moveable: bool,
28 is-resizable: bool,
29}
30
31component SelectionFrame {
32 in property <Selection> selection;
33 in property <bool> interactive: true;
34
35 x: root.selection.geometry.x;
36 y: root.selection.geometry.y;
37 width: root.selection.geometry.width;
38 height: root.selection.geometry.height;
39
40 callback update-geometry(/* x */ length, /* y */ length, /* width */ length, /* height */ length);
41 callback select-behind(/* x */ length, /* y */ length, /* enter component? */ bool, /* same file? */ bool);
42 callback selected-element-delete();
43
44 if !root.interactive || !selection.is-primary: Rectangle {
45 x: 0;
46 y: 0;
47 border-color: root.selection.border-color;
48 border-width: 1px;
49 }
50
51 if root.interactive && selection.is-primary: Resizer {
52 double-clicked(x, y, modifiers) => {
53 root.select-behind(self.absolute-position.x + x, self.absolute-position.y + y, modifiers.control, modifiers.shift);
54 }
55
56 is-moveable: root.selection.is-moveable;
57 is-resizable: root.selection.is-resizable;
58
59 x-position: root.x;
60 y-position: root.y;
61
62 color: root.selection.border-color;
63 x: 0;
64 y: 0;
65 width: root.width;
66 height: root.height;
67
68 update-geometry(x, y, w, h, done) => {
69 root.x = x;
70 root.y = y;
71 root.width = w;
72 root.height = h;
73 if done {
74 root.update-geometry(x, y, w, h);
75 }
76 }
77
78 inner := Rectangle {
79 border-color: root.selection.border-color;
80 border-width: 1px;
81 background: parent.resizing || parent.moving ? root.selection.border-color.with-alpha(0.5) : root.selection.border-color.with-alpha(0.0);
82 }
83 }
84
85 // Size label:
86 if selection.is-resizable && root.selection.is-primary && interactive: Rectangle {
87 x: (root.width - label.width) * 0.5;
88 y: root.height + 3px;
89 width: label.width;
90 height: label.height;
91
92 label := Rectangle {
93 background: root.selection.border-color;
94 width: label-text.width * 1.2;
95 height: label-text.height * 1.2;
96 label-text := Text {
97 color: Colors.white;
98 text: Math.round(root.width/1px) + "x" + Math.round(root.height/1px);
99 }
100 }
101 }
102}
103
104export enum DrawAreaMode {
105 uninitialized,
106 viewing,
107 designing,
108 error,
109}
110
111export component DrawArea {
112 in property <[Diagnostics]> diagnostics;
113 in property <[Selection]> selections;
114 in property <component-factory> preview-area;
115 in property <bool> design-mode;
116
117 out property <bool> preview-visible: preview-area-container.has-component && !diagnostics.diagnostics-open;
118 out property <DrawAreaMode> mode: uninitialized;
119
120 out property <length> preview-area-position-x: preview-area-container.absolute-position.x;
121 out property <length> preview-area-position-y: preview-area-container.absolute-position.y;
122 out property <length> preview-area-width: preview-visible ? preview-area-container.width : 0px;
123 out property <length> preview-area-height: preview-visible ? preview-area-container.height : 0px;
124
125 callback select-at(/* x */ length, /* y */ length, /* enter_component? */ bool);
126 callback select-behind(/* x */ length, /* y */ length, /* enter_component? */ bool, /* reverse */ bool);
127 callback show-document(/* url */ string, /* line */ int, /* column */ int);
128 callback reselect();
129 // Reselect element e.g. after changing the window size (which may move the element)
130 callback unselect();
131 callback selected-element-update-geometry(/* x */ length, /* y */ length, /* width */ length, /* height */ length);
132 callback selected-element-delete();
133
134 preferred-height: max(preview-area-container.preferred-height, preview-area-container.min-height) + 2 * scroll-view.border;
135 preferred-width: max(preview-area-container.preferred-width, preview-area-container.min-width) + 2 * scroll-view.border;
136
137 scroll-view := ScrollView {
138 out property <length> border: 60px;
139
140 viewport-width: drawing-rect.width;
141 viewport-height: drawing-rect.height;
142
143 drawing-rect := Rectangle {
144 background: Palette.alternate-background;
145
146 width: max(scroll-view.visible-width, main-resizer.width + scroll-view.border);
147 height: max(scroll-view.visible-height, main-resizer.height + scroll-view.border);
148
149 unselect-area := TouchArea {
150 clicked => {
151 root.unselect();
152 }
153 mouse-cursor: crosshair;
154 enabled: root.mode == DrawAreaMode.designing;
155 }
156
157 content-border := Rectangle {
158 x: main-resizer.x + (main-resizer.width - self.width) / 2;
159 y: main-resizer.y + (main-resizer.height - self.height) / 2;
160 width: main-resizer.width + 2 * self.border-width;
161 height: main-resizer.height + 2 * self.border-width;
162 border-width: 1px;
163 border-color: Palette.border;
164 }
165
166 main-resizer := Resizer {
167 is-moveable: false;
168 is-resizable <=> preview-area-container.is-resizable;
169
170 x-position: parent.x;
171 y-position: parent.y;
172
173 update-geometry(_, _, w, h) => {
174 preview-area-container.width = clamp(w, preview-area-container.min-width, preview-area-container.max-width);
175 preview-area-container.height = clamp(h, preview-area-container.min-height, preview-area-container.max-height);
176 reselect();
177 }
178
179 width: preview-area-container.width;
180 height: preview-area-container.height;
181
182 // Also make a condition that abuses the fact that the init callback
183 // is called every time the condition is dirty, to make sure that the size
184 // is within the bounds.
185 // Query the preview-area to make sure this is evaluated when it changes
186 if preview-area-container.has-component && root.preview-area == preview-area-container.component-factory: Rectangle {
187 init => {
188 preview-area-container.width = clamp(preview-area-container.width, max(preview-area-container.min-width, 16px), max(preview-area-container.max-width, 16px));
189 preview-area-container.height = clamp(preview-area-container.height, max(preview-area-container.min-height, 16px), max(preview-area-container.max-height, 16px));
190 }
191 }
192
193 preview-area-container := ComponentContainer {
194 property <bool> is-resizable: (self.min-width != self.max-width && self.min-height != self.max-height) && self.has-component;
195
196 component-factory <=> root.preview-area;
197
198 // The width and the height can't depend on the layout info of the inner item otherwise this would
199 // cause a recursion if this happens (#3989)
200 // Instead, we use a init function to initialize
201 width: 0px;
202 height: 0px;
203
204 init => {
205 self.width = max(self.preferred-width, self.min-width);
206 self.height = max(self.preferred-height, self.min-height);
207 }
208 }
209
210 selection-area := TouchArea {
211 private property <length> selection-x: 0px;
212 private property <length> selection-y: 0px;
213 private property <SelectionKind> selection-kind: SelectionKind.none;
214
215 clicked => {
216 self.selection-x = self.pressed-x;
217 self.selection-y = self.pressed-y;
218 self.selection-kind = SelectionKind.select-at;
219 }
220 double-clicked => {
221 self.selection-x = self.pressed-x;
222 self.selection-y = self.pressed-y;
223 self.selection-kind = SelectionKind.select-up-or-down;
224 }
225
226 pointer-event(event) => {
227 // This needs to fire AFTER clicked and double-clicked to work :-/
228 if (event.kind == PointerEventKind.up) {
229 if (self.selection-kind == SelectionKind.select_up_or_down) {
230 root.select-behind(self.selection-x, self.selection-y, event.modifiers.control, event.modifiers.shift);
231 } else if (self.selection-kind == SelectionKind.select-at) {
232 root.select-at(self.selection-x, self.selection-y, event.modifiers.control);
233 }
234 }
235 self.selection-kind = SelectionKind.none;
236 }
237 mouse-cursor: crosshair;
238 enabled: root.mode == DrawAreaMode.designing;
239 }
240
241 selection-display-area := Rectangle {
242 for s in root.selections: SelectionFrame {
243 interactive: root.mode == DrawAreaMode.designing;
244 selection: s;
245 update-geometry(x, y, w, h) => {
246 root.selected-element-update-geometry(x, y, w, h);
247 }
248
249 selected-element-delete() => {
250 root.selected-element-delete();
251 }
252
253 select-behind(x, y, c, f) => {
254 root.select-behind(x, y, c, f);
255 }
256 }
257 }
258 }
259 }
260 }
261
262 // Diagnostics overlay:
263 diagnostics := DiagnosticsOverlay {
264 width: 100%;
265 height: 100%;
266 diagnostics <=> root.diagnostics;
267 show-document(url, line, column) => {
268 root.show-document(url, line, column);
269 }
270 }
271
272 states [
273 uninitialized when !preview-area-container.has-component: {
274 root.mode: DrawAreaMode.uninitialized;
275 }
276 error when diagnostics.diagnostics-open && preview-area-container.has-component: {
277 root.mode: DrawAreaMode.error;
278 }
279 designing when root.design-mode && !diagnostics.diagnostics-open && preview-area-container.has-component: {
280 root.mode: DrawAreaMode.designing;
281 }
282 viewing when !root.design-mode && !diagnostics.diagnostics-open && preview-area-container.has-component: {
283 root.mode: DrawAreaMode.viewing;
284 }
285 ]
286}
287