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 { CupertinoFontSettings, CupertinoPalette } from "styling.slint";
5import { ScrollBar } from "scrollview.slint";
6import { FocusBorder } from "components.slint";
7
8// FIXME: After auto-hiding of scrollbars is implemented, use TextEditBase
9component ScrollView {
10 in property <bool> enabled: true;
11 out property <length> visible-width <=> flickable.width;
12 out property <length> visible-height <=> flickable.height;
13 in-out property <length> viewport-width <=> flickable.viewport-width;
14 in-out property <length> viewport-height <=> flickable.viewport-height;
15 in-out property <length> viewport-x <=> flickable.viewport-x;
16 in-out property <length> viewport-y <=> flickable.viewport-y;
17 // FIXME: remove. This property is currently set by the ListView and is used by the native style to draw the scrollbar differently when it has focus
18 in-out property <bool> has-focus;
19
20 min-height: 50px;
21 min-width: 50px;
22 horizontal-stretch: 1;
23 vertical-stretch: 1;
24 preferred-height: 100%;
25 preferred-width: 100%;
26
27 flickable := Flickable {
28 x: 2px;
29 y: 2px;
30 interactive: false;
31 viewport-y <=> vertical-bar.value;
32 viewport-x <=> horizontal-bar.value;
33 width: parent.width - 16px;
34 height: 100%;
35
36 @children
37 }
38
39 vertical-bar := ScrollBar {
40 enabled: root.enabled;
41 x: parent.width - self.width;
42 y: 0;
43 width: self.has-hover ? 20px : 12px;
44 height: horizontal-bar.visible ? parent.height - horizontal-bar.height : parent.height;
45 horizontal: false;
46 maximum: flickable.viewport-height - flickable.height;
47 page-size: flickable.height;
48 visible: flickable.viewport-height > flickable.height;
49 }
50
51 horizontal-bar := ScrollBar {
52 enabled: root.enabled;
53 width: vertical-bar.visible ? parent.width - vertical-bar.width : parent.width;
54 height: self.has-hover ? 20px : 12px;
55 y: parent.height - self.height;
56 x: 0;
57 horizontal: true;
58 maximum: flickable.viewport-width - flickable.width;
59 page-size: flickable.width;
60 visible: flickable.viewport-width > flickable.width;
61 }
62}
63
64export component TextEdit {
65 in property <TextWrap> wrap <=> text-input.wrap;
66 in property <TextHorizontalAlignment> horizontal-alignment <=> text-input.horizontal-alignment;
67 in property <bool> read-only <=> text-input.read-only;
68 in property <length> font-size <=> text-input.font-size;
69 in property <bool> enabled <=> text-input.enabled;
70 out property <length> visible-width <=> scroll-view.visible-width;
71 out property <length> visible-height <=> scroll-view.visible-height;
72 in-out property <bool> has-focus: text-input.has-focus;
73 in-out property <string> text <=> text-input.text;
74 in-out property <length> viewport-x <=> scroll-view.viewport-x;
75 in-out property <length> viewport-y <=> scroll-view.viewport-y;
76 in-out property <length> viewport-width <=> scroll-view.viewport-width;
77 in-out property <length> viewport-height <=> scroll-view.viewport-height;
78 in property <string> placeholder-text;
79
80 callback edited(text: string);
81 callback key-pressed(event: KeyEvent) -> EventResult;
82 callback key-released(event: KeyEvent) -> EventResult;
83
84 accessible-role: AccessibleRole.text-input;
85 accessible-enabled: root.enabled;
86 accessible-value <=> text;
87 accessible-placeholder-text: text == "" ? placeholder-text : "";
88 accessible-read-only: root.read-only;
89
90 public function set-selection-offsets(start: int, end: int) {
91 text-input.set-selection-offsets(start, end);
92 }
93
94 public function select-all() {
95 text-input.select-all();
96 }
97
98 public function clear-selection() {
99 text-input.clear-selection();
100 }
101
102 public function cut() {
103 text-input.cut();
104 }
105
106 public function copy() {
107 text-input.copy();
108 }
109
110 public function paste() {
111 text-input.paste();
112 }
113
114 forward-focus: text-input;
115 horizontal-stretch: 1;
116 vertical-stretch: 1;
117
118 states [
119 disabled when !root.enabled: {
120 text-input.color: CupertinoPalette.foreground-secondary;
121 background.background: CupertinoPalette.tertiary-control-background;
122 }
123 focused when root.has-focus: {
124 background.background: CupertinoPalette.control-background;
125 }
126 ]
127
128 FocusBorder {
129 x: (parent.width - self.width) / 2;
130 y: (parent.height - self.height) / 2;
131 width: parent.width + 6px;
132 height: parent.height + 6px;
133 has-focus: root.has-focus;
134 }
135
136 background := Rectangle {
137 background: CupertinoPalette.alternate-background;
138 border-color: CupertinoPalette.border;
139 border-width: 1px;
140 }
141
142 ContextMenuArea {
143 Menu {
144 MenuItem {
145 title: @tr("Cut");
146 activated => { text-input.cut(); }
147 }
148 MenuItem {
149 title: @tr("Copy");
150 activated => { text-input.copy(); }
151 }
152 MenuItem {
153 title: @tr("Paste");
154 activated => { text-input.paste(); }
155 }
156 MenuItem {
157 title: @tr("Select All");
158 activated => { text-input.select-all(); }
159 }
160 }
161
162 scroll-view := ScrollView {
163 x: 8px;
164 y: 8px;
165 width: parent.width - 16px;
166 height: parent.height - 16px;
167 viewport-width: root.wrap == TextWrap.no-wrap ? max(self.visible-width, text-input.preferred-width) : self.visible-width;
168 viewport-height: max(self.visible-height, text-input.preferred-height);
169
170 text-input := TextInput {
171 enabled: true;
172 color: CupertinoPalette.foreground;
173 font-size: CupertinoFontSettings.body.font-size;
174 font-weight: CupertinoFontSettings.body.font-weight;
175 selection-background-color: CupertinoPalette.selection-background;
176 selection-foreground-color: self.color;
177 single-line: false;
178 wrap: word-wrap;
179 page-height: scroll-view.visible-height;
180 // Disable TextInput's built-in accessibility support as the widget takes care of that.
181 accessible-role: none;
182
183 edited => {
184 root.edited(self.text);
185 }
186
187 key-pressed(event) => {
188 root.key-pressed(event)
189 }
190
191 key-released(event) => {
192 root.key-released(event)
193 }
194
195 cursor-position-changed(cpos) => {
196 if (cpos.x + root.viewport-x < 12px) {
197 root.viewport-x = min(0px, max(parent.visible-width - self.width, - cpos.x + 12px));
198 } else if (cpos.x + root.viewport-x > parent.visible-width - 12px) {
199 root.viewport-x = min(0px, max(parent.visible-width - self.width, parent.visible-width - cpos.x - 12px));
200 }
201 if (cpos.y + root.viewport-y < 12px) {
202 root.viewport-y = min(0px, max(parent.visible-height - self.height, - cpos.y + 12px));
203 } else if (cpos.y + root.viewport-y > parent.visible-height - 12px - 20px) {
204 // FIXME: font-height hardcoded to 20px
205 root.viewport-y = min(0px, max(parent.visible-height - self.height, parent.visible-height - cpos.y - 12px - 20px));
206 }
207 }
208 }
209 }
210 }
211
212 placeholder := Text {
213 x: scroll-view.x;
214 y: scroll-view.y;
215 width: scroll-view.width;
216 vertical-alignment: top;
217 text: (root.text == "" && text-input.preedit-text == "") ? root.placeholder-text : "";
218 font-size: text-input.font-size;
219 font-italic: text-input.font-italic;
220 font-weight: text-input.font-weight;
221 font-family: text-input.font-family;
222 color: CupertinoPalette.foreground-secondary;
223 overflow: elide;
224 // `accessible-placeholder-text` is set on TextEdit already
225 accessible-role: none;
226 }
227}
228