| 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 | |
| 4 | import { CupertinoFontSettings, CupertinoPalette } from "styling.slint" ; |
| 5 | import { ScrollBar } from "scrollview.slint" ; |
| 6 | import { FocusBorder } from "components.slint" ; |
| 7 | |
| 8 | // FIXME: After auto-hiding of scrollbars is implemented, use TextEditBase |
| 9 | component 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 | |
| 64 | export 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 | |