1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: MIT
3
4global 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
16component 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
160import {LineEdit, TextEdit, ComboBox, GridBox, VerticalBox, HorizontalBox, StyleMetrics} from "std-widgets.slint";
161
162component Label inherits Text {
163 color: Palette.text-color;
164}
165
166component 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
186component 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
223component 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
258component 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
282component 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
334component Hyperlink inherits Text {
335 in-out property <string> link;
336
337 color: Palette.hyper-blue;
338
339 TouchArea { mouse-cursor: pointer; }
340}
341
342component 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
376component 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
403component 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
496component 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
505export 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