1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: MIT |
3 | |
4 | global Palette { |
5 | out property <color> window-background: #eee; |
6 | out property <color> widget-background: #ddd; |
7 | out property <color> widget-stroke: #888; |
8 | out property <color> window-border: #ccc; |
9 | out property <color> text-color: #666; |
10 | out property <color> hyper-blue: #90d1ff; |
11 | |
12 | } |
13 | |
14 | //------ MdiWindow ---- |
15 | |
16 | component MdiWindow inherits Rectangle { |
17 | in property <string> title; |
18 | in-out property <length> window-x <=> window.x; |
19 | in-out property <length> window-y <=> window.y; |
20 | in-out property <bool> is-open: true; |
21 | |
22 | width: 100%; |
23 | height: 100%; |
24 | |
25 | |
26 | window := Rectangle { |
27 | property <length> open-width: l.preferred-width; |
28 | property <length> open-height: l.preferred-height; |
29 | |
30 | x:0; |
31 | y:0; |
32 | background: Palette.window-background; |
33 | border-width: 2px; |
34 | border-color: Palette.window-border; |
35 | border-radius: 6px; |
36 | drop-shadow-blur: 25px; |
37 | drop-shadow-color: Palette.window-border; |
38 | width: l.preferred-width; |
39 | height: l.preferred-height - hidden.preferred-height; |
40 | clip: true; |
41 | |
42 | states [ |
43 | open when root.is-open : { |
44 | width: self.open-width; |
45 | height: self.open-height; |
46 | expand.angle: 0deg; |
47 | |
48 | in { animate width, height, expand.angle { duration: 150ms; easing: ease; } } |
49 | out { animate width, height, expand.angle { duration: 150ms; easing: ease; } } |
50 | } |
51 | ] |
52 | |
53 | TouchArea {} |
54 | |
55 | l := VerticalLayout { |
56 | padding: window.border-width; |
57 | alignment: root.is-open ? stretch : start; |
58 | title_bar := TouchArea { |
59 | moved => { |
60 | if (self.pressed) { |
61 | root.window-x += self.mouse-x - self.pressed-x; |
62 | root.window-y += self.mouse-y - self.pressed-y; |
63 | } |
64 | } |
65 | |
66 | HorizontalLayout { |
67 | padding: window.border-width; |
68 | spacing: window.border-width * 2; |
69 | |
70 | expand := TouchArea { |
71 | clicked => { root.is-open = !root.is-open; } |
72 | |
73 | property <angle> angle: -90deg; |
74 | width: 30px; |
75 | |
76 | Path { |
77 | stroke-width: window.border-width * (expand.has-hover ? 1.5 : 1); |
78 | stroke: parent.has-hover ? Palette.widget-stroke.darker(100%) : Palette.widget-stroke; |
79 | viewbox-x: -1.5; |
80 | viewbox-y: -1.5; |
81 | viewbox-height: 3; |
82 | viewbox-width: 3; |
83 | |
84 | MoveTo { x: cos(expand.angle) * -1 - sin(expand.angle) * -1; y: sin(expand.angle) * -1 + cos(expand.angle) * -1; } |
85 | LineTo { x: cos(expand.angle) * 1 - sin(expand.angle) * -1; y: sin(expand.angle) * 1 + cos(expand.angle) * -1; } |
86 | LineTo { x: cos(expand.angle) * 0 - sin(expand.angle) * 1; y: sin(expand.angle) * 0 + cos(expand.angle) * 1; } |
87 | LineTo { x: cos(expand.angle) * -1 - sin(expand.angle) * -1; y: sin(expand.angle) * -1 + cos(expand.angle) * -1; } |
88 | } |
89 | } |
90 | Text { |
91 | text: root.title; |
92 | horizontal-alignment: center; |
93 | color: Palette.text-color; |
94 | } |
95 | close_button := TouchArea { |
96 | clicked => { root.visible = false; } |
97 | |
98 | width: 30px; |
99 | |
100 | Path { |
101 | stroke-width: window.border-width * (close-button.has-hover ? 1.5 : 1); |
102 | stroke: parent.has-hover ? Palette.widget-stroke.darker(100%) : Palette.widget-stroke; |
103 | viewbox-x: -1.5; |
104 | viewbox-y: -1.5; |
105 | viewbox-height: 3; |
106 | viewbox-width: 3; |
107 | |
108 | MoveTo { x: -1; y: -1; } |
109 | LineTo { x: 1; y: 1; } |
110 | MoveTo { x: -1; y: 1; } |
111 | LineTo { x: 1; y: -1; } |
112 | } |
113 | } |
114 | } |
115 | } |
116 | hidden := VerticalLayout { |
117 | visible: root.is-open; |
118 | |
119 | Rectangle { |
120 | height: window.border-width; |
121 | background: window.border-color; |
122 | } |
123 | |
124 | @children |
125 | } |
126 | } |
127 | |
128 | if root.is-open : resize-handle := TouchArea { |
129 | moved => { |
130 | if (self.pressed) { |
131 | window.open-width = max(l.min-width, min(l.max-width, window.open-width + self.mouse-x - self.pressed-x)); |
132 | window.open-height = max(l.min-height, min(l.max-height, window.open-height + self.mouse-y - self.pressed-y)); |
133 | } |
134 | } |
135 | |
136 | width: 20px; |
137 | height: self.width; |
138 | x: parent.width - self.width; |
139 | y: parent.height - self.height; |
140 | mouse-cursor: MouseCursor.nwse-resize; |
141 | |
142 | Path { |
143 | stroke-width: window.border-width; |
144 | stroke: Palette.window-border; |
145 | viewbox-height: 1.2; viewbox-width: 1.2; |
146 | |
147 | MoveTo { x: 0; y: 1; } |
148 | LineTo { x: 1; y: 0; } |
149 | MoveTo { x: 0.4; y: 1; } |
150 | LineTo { x: 1; y: 0.4; } |
151 | MoveTo { x: 0.8; y: 1; } |
152 | LineTo { x: 1; y: 0.8; } |
153 | } |
154 | } |
155 | } |
156 | } |
157 | |
158 | //------ Widgets ------ |
159 | |
160 | import {LineEdit, TextEdit, ComboBox, GridBox, VerticalBox, HorizontalBox, StyleMetrics} from "std-widgets.slint" ; |
161 | |
162 | component Label inherits Text { |
163 | color: Palette.text-color; |
164 | } |
165 | |
166 | component Button inherits TouchArea { |
167 | in property text <=> t.text; |
168 | |
169 | min-height: t.min-height; |
170 | min-width: t.min-width + 10px; |
171 | |
172 | Rectangle { |
173 | border-width: 1.5px; |
174 | border-color: root.has-hover ? Palette.widget-stroke : transparent; |
175 | border-radius: 3px; |
176 | background: root.pressed ? Palette.widget-background.darker(30%) : Palette.widget-background; |
177 | |
178 | t := Label { |
179 | y:0; |
180 | width: 100%; |
181 | horizontal-alignment: center; |
182 | } |
183 | } |
184 | } |
185 | |
186 | component CheckBox inherits TouchArea { |
187 | in property text <=> t.text; |
188 | in-out property <bool> checked; |
189 | |
190 | clicked => { root.checked = !root.checked; } |
191 | |
192 | HorizontalLayout { |
193 | spacing: 5px; |
194 | |
195 | VerticalLayout { |
196 | alignment: center; |
197 | |
198 | Rectangle { |
199 | width: 20px; |
200 | height: 20px; |
201 | border-width: 1.5px; |
202 | border-color: root.has-hover ? Palette.widget-stroke : transparent; |
203 | border-radius: 3px; |
204 | background: root.pressed ? Palette.widget-background.darker(30%) : Palette.widget-background; |
205 | |
206 | if root.checked : Path { |
207 | stroke: root.has-hover ? Palette.widget-stroke.darker(100%) : Palette.widget-stroke; |
208 | stroke-width: root.pressed ? 2.5px : 2px; |
209 | viewbox-height: 1; viewbox-width: 1; |
210 | |
211 | MoveTo { x: 0.2; y: 0.5; } |
212 | LineTo { x: 0.5; y: 0.8; } |
213 | LineTo { x: 0.8; y: 0.2; } |
214 | } |
215 | } |
216 | } |
217 | t := Label { |
218 | |
219 | } |
220 | } |
221 | } |
222 | |
223 | component RadioButton inherits TouchArea { |
224 | in property text <=> t.text; |
225 | in-out property <bool> checked; |
226 | |
227 | HorizontalLayout { |
228 | spacing: 5px; |
229 | |
230 | VerticalLayout { |
231 | alignment: center; |
232 | |
233 | Rectangle { |
234 | width: 20px; |
235 | height: 20px; |
236 | border-width: 1.5px; |
237 | border-color: root.has-hover ? Palette.widget-stroke : transparent; |
238 | border-radius: self.width / 2; |
239 | background: root.pressed ? Palette.widget-background.darker(30%) : Palette.widget-background; |
240 | |
241 | if root.checked : Rectangle { |
242 | background: root.has-hover ? Palette.widget-stroke.darker(100%) : Palette.widget-stroke; |
243 | border-radius: self.width / 2; |
244 | width: parent.width / 2; |
245 | height: parent.width / 2; |
246 | x: parent.width / 4; |
247 | y: parent.width / 4; |
248 | } |
249 | } |
250 | } |
251 | |
252 | t := Label { |
253 | |
254 | } |
255 | } |
256 | } |
257 | |
258 | component SelectableLabel inherits TouchArea { |
259 | in-out property <bool> checked; |
260 | in-out property text <=> t.text; |
261 | |
262 | min-height: t.min-height; |
263 | min-width: t.min-width + 10px; |
264 | |
265 | Rectangle { |
266 | border-width: 1.5px; |
267 | border-color: root.has-hover ? Palette.widget-stroke : transparent; |
268 | border-radius: 3px; |
269 | background: |
270 | root.checked ? Palette.hyper-blue : |
271 | root.pressed ? Palette.widget-background.darker(30%) : |
272 | root.has-hover ? Palette.widget-background : transparent; |
273 | |
274 | t := Label { |
275 | y:0; |
276 | width: 100%; |
277 | horizontal-alignment: center; |
278 | } |
279 | } |
280 | } |
281 | |
282 | component Slider inherits Rectangle { |
283 | in property <bool> enabled <=> touch.enabled; |
284 | in property <float> maximum: 100; |
285 | in property <float> minimum: 0; |
286 | in-out property <float> value; |
287 | |
288 | callback changed(float); |
289 | |
290 | min-height: 24px; |
291 | min-width: 100px; |
292 | horizontal-stretch: 1; |
293 | vertical-stretch: 0; |
294 | |
295 | Rectangle { |
296 | width: parent.width; |
297 | height: parent.height / 2; |
298 | y: (parent.height - self.height) / 2; |
299 | border-radius: 2px; |
300 | background: Palette.widget-background; |
301 | } |
302 | |
303 | handle := Rectangle { |
304 | width: self.height; |
305 | height: parent.height; |
306 | border-radius: self.height / 2; |
307 | border-color: touch.has-hover ? Palette.widget-stroke.darker(100%) : Palette.widget-stroke; |
308 | border-width: touch.pressed ? 4px : touch.has-hover ? 3px : 2px; |
309 | background: touch.pressed ? Palette.widget-background.darker(30%) : Palette.widget-background; |
310 | x: (root.width - handle.width) * max(0, min(1, (root.value - root.minimum)/(root.maximum - root.minimum))); |
311 | } |
312 | |
313 | touch := TouchArea { |
314 | pointer-event(event) => { |
315 | if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) { |
316 | self.pressed-value = root.value; |
317 | } |
318 | } |
319 | moved => { |
320 | if (self.enabled && self.pressed) { |
321 | root.value = max(root.minimum, min(root.maximum, |
322 | self.pressed-value + (touch.mouse-x - touch.pressed-x) * (root.maximum - root.minimum) / (root.width - handle.width))); |
323 | root.changed(root.value); |
324 | } |
325 | } |
326 | |
327 | property <float> pressed-value; |
328 | |
329 | width: parent.width; |
330 | height: parent.height; |
331 | } |
332 | } |
333 | |
334 | component Hyperlink inherits Text { |
335 | in-out property <string> link; |
336 | |
337 | color: Palette.hyper-blue; |
338 | |
339 | TouchArea { mouse-cursor: pointer; } |
340 | } |
341 | |
342 | component DragValue inherits TouchArea { |
343 | in-out property <float> value; |
344 | in-out property <float> _pressed-value; |
345 | |
346 | moved => { |
347 | if (root.pressed) { |
348 | root.value = root._pressed-value +(root.mouse-x - root.pressed-x) / 2px; |
349 | } |
350 | } |
351 | pointer-event(e) => { |
352 | if (e.kind == PointerEventKind.down) { |
353 | root._pressed-value = root.value; |
354 | } |
355 | } |
356 | |
357 | min-height: t.min-height; |
358 | min-width: Math.max(t.min-width + 10px, 50px); |
359 | mouse-cursor: MouseCursor.ew-resize; |
360 | |
361 | Rectangle { |
362 | border-width: 1.5px; |
363 | border-color: root.has-hover ? Palette.widget-stroke : transparent; |
364 | border-radius: 3px; |
365 | background: root.pressed ? Palette.widget-background.darker(30%) : Palette.widget-background; |
366 | |
367 | t := Label { |
368 | y:0; |
369 | width: 100%; |
370 | horizontal-alignment: center; |
371 | text: round(root.value); |
372 | } |
373 | } |
374 | } |
375 | |
376 | component ProgressBar inherits Rectangle { |
377 | in-out property <float> value; |
378 | |
379 | min-height: 24px; |
380 | min-width: 100px; |
381 | background: StyleMetrics.textedit-background; |
382 | border-radius: root.height / 2; |
383 | |
384 | Rectangle { |
385 | x:0; |
386 | height: 100%; |
387 | width: self.height + (parent.width - self.height) * max(0, min(1, root.value / 100)); |
388 | border-radius: self.height / 2; |
389 | background: Palette.hyper-blue; |
390 | } |
391 | |
392 | Label { |
393 | height: 100%; |
394 | vertical-alignment: center; |
395 | x: self.height/2; |
396 | text: round(root.value) + "%" ; |
397 | } |
398 | } |
399 | |
400 | |
401 | //------ Demo apps ------- |
402 | |
403 | component Gallery inherits GridBox { |
404 | |
405 | function unsel() { r1.checked = false; r2.checked = false; r3.checked = false; } |
406 | |
407 | Row { |
408 | Text { text: "Label:" ; } |
409 | Label { text: "Welcome to the widget gallery!" ; } |
410 | } |
411 | |
412 | Row { |
413 | Text { text: "Hyperlink:" ; } |
414 | Hyperlink { text: "Slint homepage" ; link: "https://slint.dev" ; } |
415 | } |
416 | |
417 | Row { |
418 | Text { text: "TextEdit:" ; } |
419 | LineEdit { placeholder-text: "WriteSomething here" ;} |
420 | } |
421 | |
422 | Row { |
423 | Text { text: "Button:" ; } |
424 | HorizontalLayout { |
425 | alignment: start; |
426 | Button { text: "Click me!" ; clicked => { cb.checked = !cb.checked; } } |
427 | } |
428 | } |
429 | |
430 | Row { |
431 | Text { text: "Checkbox:" ; } |
432 | cb := CheckBox { text: "Checkbox" ; } |
433 | } |
434 | |
435 | Row { |
436 | Text { text: "RadioButton:" ; } |
437 | HorizontalBox { |
438 | alignment: start; |
439 | r1 := RadioButton { text: "First" ; clicked => { root.unsel(); r1.checked = true; } } |
440 | r2 := RadioButton { text: "Second" ; clicked => { root.unsel(); r2.checked = true; } } |
441 | r3 := RadioButton { text: "Third" ; clicked => { root.unsel(); r3.checked = true; } } |
442 | } |
443 | } |
444 | |
445 | Row { |
446 | Text { text: "SelectableLabel:" ; } |
447 | HorizontalBox { |
448 | alignment: start; |
449 | SelectableLabel { text: "First" ; checked <=> r1.checked; clicked => { root.unsel(); r1.checked = true; } } |
450 | SelectableLabel { text: "Second" ; checked <=> r2.checked; clicked => { root.unsel(); r2.checked = true; } } |
451 | SelectableLabel { text: "Third" ; checked <=> r3.checked; clicked => { root.unsel(); r3.checked = true; } } |
452 | } |
453 | } |
454 | |
455 | Row { |
456 | Text { text: "ComboBox:" ; } |
457 | |
458 | HorizontalBox { |
459 | alignment: start; |
460 | ComboBox { |
461 | selected => { |
462 | r1.checked = self.current-index == 0; |
463 | r2.checked = self.current-index == 1; |
464 | r3.checked = self.current-index == 2; |
465 | } |
466 | |
467 | model: ["First" , "Second" , "Third" ]; |
468 | } |
469 | |
470 | Label { text: "Take your pick" ; } |
471 | } |
472 | } |
473 | |
474 | Row { |
475 | Text { text: "Slider:" ; } |
476 | sl := Slider { } |
477 | } |
478 | |
479 | Row { |
480 | Text { text: "DragValue:" ; } |
481 | |
482 | HorizontalLayout { |
483 | alignment: start; |
484 | DragValue { value <=> sl.value; } |
485 | } |
486 | } |
487 | |
488 | Row { |
489 | Text { text: "ProgressBar:" ; } |
490 | ProgressBar { value <=> sl.value; } |
491 | } |
492 | |
493 | Rectangle {} |
494 | } |
495 | |
496 | component TextEditDemo inherits VerticalLayout { |
497 | preferred-height: 150px; |
498 | preferred-width: 300px; |
499 | |
500 | TextEdit { |
501 | text: "Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum" ; |
502 | } |
503 | } |
504 | |
505 | export component Demo inherits Window { |
506 | preferred-width: 1024px; |
507 | preferred-height: 800px; |
508 | background: white; |
509 | |
510 | w1 := MdiWindow { |
511 | x:0;y:0; |
512 | title: "🗄 Widget Gallery" ; |
513 | window-x: 30px; |
514 | window-y: 20px; |
515 | |
516 | Gallery { } |
517 | } |
518 | w2 := MdiWindow { |
519 | x:0;y:0; |
520 | visible: false; |
521 | title: "🗉 TextEdit" ; |
522 | window-x: 230px; |
523 | window-y: 520px; |
524 | |
525 | TextEditDemo { } |
526 | } |
527 | |
528 | side-panel := Rectangle { |
529 | border-color: resize-side-panel.has-hover ? Palette.window-border.darker(100%) : Palette.window-border; |
530 | border-width: 2px; |
531 | background: Palette.window-background; |
532 | x: parent.width - self.width; |
533 | width: side-panel-l.preferred-width; |
534 | height: 100%; |
535 | |
536 | side-panel-l := VerticalBox { |
537 | alignment: start; |
538 | |
539 | Label { |
540 | font-weight: 500; |
541 | text: "Slint Demos" ; |
542 | horizontal-alignment: center; |
543 | } |
544 | Rectangle { height: 2px; background: Palette.window-border; } |
545 | Label { |
546 | preferred-width: 0px; |
547 | text: "This is a demo which is based on the demo from the egui framework" ; |
548 | wrap: word-wrap; |
549 | horizontal-alignment: center; |
550 | } |
551 | Rectangle { height: 2px; background: Palette.window-border; } |
552 | CheckBox { text: w1.title; checked <=> w1.visible; } |
553 | CheckBox { text: w2.title; checked <=> w2.visible; } |
554 | |
555 | } |
556 | resize-side-panel := TouchArea { |
557 | moved => { |
558 | if (self.pressed) { |
559 | side-panel.width = max(side-panel-l.min-width, min(root.width, side-panel.width - (self.mouse-x - self.pressed-x))); |
560 | } |
561 | } |
562 | |
563 | x:0; |
564 | height: 100%; |
565 | width: 4px; |
566 | mouse-cursor: ew-resize; |
567 | } |
568 | } |
569 | } |
570 | |