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 | |
6 | import { Button, ComboBox, HorizontalBox, ListView, ScrollView, Palette, VerticalBox } from "std-widgets.slint" ; |
7 | import { Diagnostics, DiagnosticsOverlay } from "diagnostics-overlay.slint" ; |
8 | import { Resizer } from "resizer.slint" ; |
9 | |
10 | enum SelectionKind { |
11 | none, |
12 | select_at, |
13 | select_up_or_down, |
14 | } |
15 | |
16 | struct SelectionRectangle { |
17 | x: length, |
18 | y: length, |
19 | width: length, |
20 | height: length, |
21 | } |
22 | |
23 | export struct Selection { |
24 | geometry: SelectionRectangle, |
25 | border-color: color, |
26 | is-primary: bool, |
27 | is-moveable: bool, |
28 | is-resizable: bool, |
29 | } |
30 | |
31 | component 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 | |
104 | export enum DrawAreaMode { |
105 | uninitialized, |
106 | viewing, |
107 | designing, |
108 | error, |
109 | } |
110 | |
111 | export 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 | |