1/* 2 * Copyright (c) 2023-2023 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import { KeyCode } from '@ohos.multimodalInput.keyCode' 17 18export declare type ComposeTitleBarMenuItem = { 19 value: ResourceStr 20 isEnabled: boolean 21 action?: () => void 22} 23 24const PUBLIC_MORE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAY' + 25 'AAABS3GwHAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAEZ0FNQQAAsY58+1GTAAA' + 26 'AAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAOxAAADsQBlSsOGwAABEZJREFUeNrt3D1rFFEUBuA' + 27 'xhmAhFlYpUohYiYWFRcAmKAhWK2pjo1iKf8BCMIKFf8BarCyMhVj4VZhGSKEg2FqJyCKWIhYWnstMINgYsh+cmfs88BI' + 28 'Cydxw7jmzu2HvNg0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBN+3r6dx+LXIqsRpa7FF8j48hm5Fn3Peo9mAEYRdY' + 29 'jJ3f582Vj7nZfUe/eDsCRyMPI2h5/fyNyI/JDT6v3Tvt7sBllE15ETkxwjeORi5G3ke/6W737MgBnI68jh6ZwrcORq5H' + 30 'nhkC9+zAA5YXXy8jBKV5zKXIu8jjyS7+rd+YBeNVtyrSVO9PRyBM9r94LSTfjWuTUDK9/eYIXeENUbb0zDsBi5PYc1rm' + 31 'j79U74wCszuih+F/ljrSi/+uud8YBGA10rayqrnfGAVgb6FpZVV3vjAOwPNC1sqq63hkHYGWga2VVdb0XKt/8Rf1fd70' + 32 'zDsB4jmt5u3Tl9a59AMb6v+56ZxyArYGulVXV9c44ABtzXOup/q+73hkH4N2cHio/Rj7r/7rrnXEAfkfuz2Gddb2v3ln' + 33 '/DfpgxneLzaY9xE3l9c46AH8iVyI/Z3Dt8nB/Xc+rd5H5QMy3yJemPVs6zY0edc9HUe/0Z4I/dQ/N5Vjd0oTXKp9QcKF' + 34 'pD2qj3r0YgO1NeRM507TH6/bifeR85IMeV++d+vTBWOV9JDcjt5rdv6uw3M3uRR7pa/Xu+wBsOxA53bTnTP/3UX1b3fN' + 35 'Q1BsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqyr6d/97HIpchqZLlL8TUyjmxGnnX' + 36 'fo96DGYBRZD1ycpc/XzbmbvcV9e7tAByJPIys7fH3NyI3Ij/0tHrvtL8Hm1E24UXkxATXOB65GHkb+a6/1bsvA3A28jp' + 37 'yaArXOhy5GnluCNS7DwNQXni9jByc4jWXIucijyO/9Lt6Zx6AV92mTFu5Mx2NPNHz6r2QdDOuRU7N8PqXJ3iBN0TV1jv' + 38 'jACxGbs9hnTv6Xr0zDsDqjB6K/1XuSCv6v+56ZxyA0UDXyqrqemccgLWBrpVV1fXOOADLA10rq6rrnXEAVga6VlZV13u' + 39 'h8s1f1P911zvjAIznuJa3S1de79oHYKz/6653xgHYGuhaWVVd74wDsDHHtZ7q/7rrnXEA3s3pofJj5LP+r7veGQfgd+T' + 40 '+HNZZ1/vqnfXfoA9mfLfYbNpD3FRe76wD8CdyJfJzBtcuD/fX9bx6F5kPxHyLfGnas6XT3OhR93wU9U5/JvhT99BcjtU' + 41 'tTXit8gkFF5r2oDbq3YsB2N6UN5EzTXu8bi/eR85HPuhx9d6pTx+MVd5HcjNyq9n9uwrL3exe5JG+Vu++D8C2A5HTTXv' + 42 'O9H8f1bfVPQ9FvQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgCn7C9HjBtwWfXpKAAAAAElFTkSuQmCC' 43 44const PUBLIC_BACK = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAY' + 45 'AAABS3GwHAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAEZ0FNQQAAsY58+1GTAAAAA' + 46 'XNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAOxAAADsQBlSsOGwAAA8VJREFUeNrt3LFLlHEYwPFXz0G' + 47 'iIZpEoikkwsFRIiK3gqCigxIC/4Kmhv6OoChouaGoqKCgCKducGh0cDAIamhwiCaHCIeelztpUszee/vl8/nAM3Vd8nufr' + 48 '+fddVYVAAAAAAAAAAAAAAAAAAAAAABQijFH0KhrMd2Y2ZitmNWYRzHLjkYAB9lUzMOYizv8eS/mZsymoypLxxE0svzvY07' + 49 'vcpu5mOmY145LAAdx+U/u4bZzwx+JPjq2cow7glaWf1vXsQkg6/JvPwoggJTLjwDSL/8nRyiAzN/5nzpGAWRd/n7MM0cpg' + 50 'IzLvx6z6CjL453gdpZ/IWbDcQrA8iMAy48ALD8CsPwIwPIjAMuPACw/ArD8CMDyIwDLjwAsPwKw/AjA8iMAy48ALD8CsPw' + 51 'IwPIjAMuPACw/ArD85A3A8pM2AMtP2gAsP2kDsPykDcDykzYAy0/aACw/aQOw/KQNwPKTNgDLT9oALD9pA7D8pA3A8pM2A' + 52 'MtP2gAsP2kDsPykDcDykzYAy0/aACw/aQOw/KQNwPKTNgDLT9oALD9pA7D8pA3A8pM2AMtP2gAsP2kDsPykDcDykzYAy0/' + 53 'aACw/aQOw/KQNwPLz3xlv6H4mYp5YfrI+AizF9BwnI/AlZi3mbsxy03feaeh+HsQcc60YgSMxMzE3YmZj3sX8LOlHoPoLn' + 54 'HedaEE35n5pzwF856dN9SPBpZICmHRNaNnlkgL46nrQsvmSAqhftlx1TWjR4ZICqPVcE1q0XloA96rBa7XQhl5pAWzFXKm' + 55 '8i8vo9WMeN3VnnQa/sO8xL2POxEy7Toxo+RdjNpu6w1F9HuBqNXi99lw1eKMM9utHzIeYV8MftbccCQAAAAAAsBdt/XLc+s' + 56 'Py9W+MmPqL+1iJuVA1+C4gdFr6d77FvK0GH2nb739lPR5zNuZ51eBnQhFAJQIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIE' + 57 'IAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAI' + 58 'EIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIE8M8jmBlGgABSRnAqZiXms+MUQNYIDnkUKMu4I/gj6z' + 59 'ELMRv7/PsnHKEAMkcw6fgEkDmCNUcngMwRvHFsngRnfWJcL/9tRyaAgxrB+ZijO9ymH7MUs+m4yjLmCBozEXMr5nr1+9We1' + 60 'ZgXMXccDwAAAAAAAAAAAAAAAAAAAAAAwO5+AfVgtqHKRnawAAAAAElFTkSuQmCC' 61 62@Component 63export struct ComposeTitleBar { 64 item: ComposeTitleBarMenuItem 65 title: ResourceStr 66 subtitle: ResourceStr 67 menuItems: Array<ComposeTitleBarMenuItem> 68 69 @State titleMaxWidth: number = 0 70 @State backActive: boolean = false 71 72 private static readonly totalHeight = 56 73 private static readonly leftPadding = 12 74 private static readonly rightPadding = 12 75 private static readonly portraitImageSize = 40 76 private static readonly portraitImageLeftPadding = 4 77 private static readonly portraitImageRightPadding = 16 78 private static instanceCount = 0 79 80 build() { 81 Flex({ 82 justifyContent: FlexAlign.SpaceBetween, 83 alignItems: ItemAlign.Stretch 84 }) { 85 Row() { 86 Navigator() 87 .active(this.backActive) 88 89 ImageMenuItem({ item: { 90 value: $r('sys.media.ohos_ic_back'), 91 isEnabled: true, 92 action: () => this.backActive = true 93 }, index: -1 }) 94 95 if (this.item !== undefined) { 96 Image(this.item.value) 97 .width(ComposeTitleBar.portraitImageSize) 98 .height(ComposeTitleBar.portraitImageSize) 99 .margin({ 100 left: $r('sys.float.ohos_id_text_paragraph_margin_xs'), 101 right: $r('sys.float.ohos_id_text_paragraph_margin_m') 102 }) 103 .focusable(false) 104 .borderRadius(ImageMenuItem.buttonBorderRadius) 105 } 106 107 Column() { 108 if (this.title !== undefined) { 109 Row() { 110 Text(this.title) 111 .fontWeight(FontWeight.Medium) 112 .fontSize($r('sys.float.ohos_id_text_size_headline8')) 113 .fontColor($r('sys.color.ohos_id_color_titlebar_text')) 114 .maxLines(this.subtitle !== undefined ? 1 : 2) 115 .textOverflow({ overflow: TextOverflow.Ellipsis }) 116 .constraintSize({ maxWidth: this.titleMaxWidth }) 117 } 118 .justifyContent(FlexAlign.Start) 119 } 120 if (this.subtitle !== undefined) { 121 Row() { 122 Text(this.subtitle) 123 .fontSize($r('sys.float.ohos_id_text_size_over_line')) 124 .fontColor($r('sys.color.ohos_id_color_titlebar_subtitle_text')) 125 .maxLines(1) 126 .textOverflow({ overflow: TextOverflow.Ellipsis }) 127 .constraintSize({ maxWidth: this.titleMaxWidth }) 128 } 129 .justifyContent(FlexAlign.Start) 130 } 131 } 132 .justifyContent(FlexAlign.Start) 133 .alignItems(HorizontalAlign.Start) 134 .constraintSize({ maxWidth: this.titleMaxWidth }) 135 } 136 .margin({ left: $r('sys.float.ohos_id_default_padding_start') }) 137 138 if (this.menuItems !== undefined && this.menuItems.length > 0) { 139 CollapsibleMenuSection({ menuItems: this.menuItems, index: 1 + ComposeTitleBar.instanceCount++ }) 140 } 141 } 142 .width('100%') 143 .height(ComposeTitleBar.totalHeight) 144 .backgroundColor($r('sys.color.ohos_id_color_background')) 145 .onAreaChange((_oldValue: Area, newValue: Area) => { 146 let newWidth = Number(newValue.width) 147 if (this.menuItems !== undefined) { 148 let menusLength = this.menuItems.length 149 if (menusLength >= CollapsibleMenuSection.maxCountOfVisibleItems) { 150 newWidth = newWidth - ImageMenuItem.imageHotZoneWidth * CollapsibleMenuSection.maxCountOfVisibleItems 151 } else if (menusLength > 0) { 152 newWidth = newWidth - ImageMenuItem.imageHotZoneWidth * menusLength 153 } 154 } 155 this.titleMaxWidth = newWidth 156 this.titleMaxWidth -= ComposeTitleBar.leftPadding 157 this.titleMaxWidth -= ImageMenuItem.imageHotZoneWidth 158 if (this.item !== undefined) { 159 this.titleMaxWidth -= ComposeTitleBar.portraitImageLeftPadding 160 + ComposeTitleBar.portraitImageSize 161 + ComposeTitleBar.portraitImageRightPadding 162 } 163 this.titleMaxWidth -= ComposeTitleBar.rightPadding 164 }) 165 } 166} 167 168@Component 169struct CollapsibleMenuSection { 170 menuItems: Array<ComposeTitleBarMenuItem> 171 index: number 172 173 static readonly maxCountOfVisibleItems = 3 174 private static readonly focusPadding = 4 175 private static readonly marginsNum = 2 176 private firstFocusableIndex = -1 177 178 @State isPopupShown: boolean = false 179 180 @State isMoreIconOnFocus: boolean = false 181 @State isMoreIconOnHover: boolean = false 182 @State isMoreIconOnClick: boolean = false 183 184 getMoreIconFgColor() { 185 return this.isMoreIconOnClick 186 ? $r('sys.color.ohos_id_color_titlebar_icon_pressed') 187 : $r('sys.color.ohos_id_color_titlebar_icon') 188 } 189 190 getMoreIconBgColor() { 191 if (this.isMoreIconOnClick) { 192 return $r('sys.color.ohos_id_color_click_effect') 193 } else if (this.isMoreIconOnHover) { 194 return $r('sys.color.ohos_id_color_hover') 195 } else { 196 return Color.Transparent 197 } 198 } 199 200 aboutToAppear() { 201 this.menuItems.forEach((item, index) => { 202 if (item.isEnabled && this.firstFocusableIndex == -1 && index > CollapsibleMenuSection.maxCountOfVisibleItems - 2) { 203 this.firstFocusableIndex = this.index * 1000 + index + 1 204 } 205 }) 206 } 207 208 build() { 209 Column() { 210 Row() { 211 if (this.menuItems.length <= CollapsibleMenuSection.maxCountOfVisibleItems) { 212 ForEach(this.menuItems, (item, index) => { 213 ImageMenuItem({ item: item, index: this.index * 1000 + index + 1 }) 214 }) 215 } else { 216 ForEach(this.menuItems.slice(0, CollapsibleMenuSection.maxCountOfVisibleItems - 1), (item, index) => { 217 ImageMenuItem({ item: item, index: this.index * 1000 + index + 1 }) 218 }) 219 220 Row() { 221 Image(PUBLIC_MORE) 222 .width(ImageMenuItem.imageSize) 223 .height(ImageMenuItem.imageSize) 224 .focusable(true) 225 } 226 .width(ImageMenuItem.imageHotZoneWidth) 227 .height(ImageMenuItem.imageHotZoneWidth) 228 .borderRadius(ImageMenuItem.buttonBorderRadius) 229 .foregroundColor(this.getMoreIconFgColor()) 230 .backgroundColor(this.getMoreIconBgColor()) 231 .justifyContent(FlexAlign.Center) 232 .stateStyles({ 233 focused: { 234 .border({ 235 radius: $r('sys.float.ohos_id_corner_radius_clicked'), 236 width: ImageMenuItem.focusBorderWidth, 237 color: $r('sys.color.ohos_id_color_focused_outline'), 238 style: BorderStyle.Solid 239 }) 240 }, 241 normal: { 242 .border({ 243 radius: $r('sys.float.ohos_id_corner_radius_clicked'), 244 width: 0 245 }) 246 } 247 }) 248 .onFocus(() => this.isMoreIconOnFocus = true) 249 .onBlur(() => this.isMoreIconOnFocus = false) 250 .onHover((isOn) => this.isMoreIconOnHover = isOn) 251 .onKeyEvent((event) => { 252 if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { 253 return 254 } 255 if (event.type === KeyType.Down) { 256 this.isMoreIconOnClick = true 257 } 258 if (event.type === KeyType.Up) { 259 this.isMoreIconOnClick = false 260 } 261 }) 262 .onTouch((event) => { 263 if (event.type === TouchType.Down) { 264 this.isMoreIconOnClick = true 265 } 266 if (event.type === TouchType.Up) { 267 this.isMoreIconOnClick = false 268 } 269 }) 270 .onClick(() => this.isPopupShown = true) 271 .bindPopup(this.isPopupShown, { 272 builder: this.popupBuilder, 273 placement: Placement.Bottom, 274 popupColor: Color.White, 275 enableArrow: false, 276 onStateChange: (e) => { 277 this.isPopupShown = e.isVisible 278 if (!e.isVisible) { 279 this.isMoreIconOnClick = false 280 } 281 } 282 }) 283 } 284 } 285 } 286 .height('100%') 287 .margin({ right: $r('sys.float.ohos_id_default_padding_end') }) 288 .justifyContent(FlexAlign.Center) 289 } 290 291 @Builder 292 popupBuilder() { 293 Column() { 294 ForEach(this.menuItems.slice(CollapsibleMenuSection.maxCountOfVisibleItems - 1, this.menuItems.length), (item, index) => { 295 ImageMenuItem({ item: item, index: this.index * 1000 + CollapsibleMenuSection.maxCountOfVisibleItems + index }) 296 }) 297 } 298 .width(ImageMenuItem.imageHotZoneWidth + CollapsibleMenuSection.focusPadding * CollapsibleMenuSection.marginsNum) 299 .margin({ top: CollapsibleMenuSection.focusPadding, bottom: CollapsibleMenuSection.focusPadding }) 300 .onAppear(() => { 301 focusControl.requestFocus(ImageMenuItem.focusablePrefix + this.firstFocusableIndex) 302 }) 303 } 304} 305 306@Component 307struct ImageMenuItem { 308 item: ComposeTitleBarMenuItem 309 index: number 310 311 static readonly imageSize = 24 312 static readonly imageHotZoneWidth = 48 313 static readonly buttonBorderRadius = 8 314 static readonly focusBorderWidth = 2 315 static readonly disabledImageOpacity = 0.4 316 static readonly focusablePrefix = "Id-ComposeTitleBar-ImageMenuItem-" 317 318 @State isOnFocus: boolean = false 319 @State isOnHover: boolean = false 320 @State isOnClick: boolean = false 321 322 getFgColor() { 323 return this.isOnClick 324 ? $r('sys.color.ohos_id_color_titlebar_icon_pressed') 325 : $r('sys.color.ohos_id_color_titlebar_icon') 326 } 327 328 getBgColor() { 329 if (this.isOnClick) { 330 return $r('sys.color.ohos_id_color_click_effect') 331 } else if (this.isOnHover) { 332 return $r('sys.color.ohos_id_color_hover') 333 } else { 334 return Color.Transparent 335 } 336 } 337 338 build() { 339 Row() { 340 Image(this.item.value) 341 .width(ImageMenuItem.imageSize) 342 .height(ImageMenuItem.imageSize) 343 .focusable(this.item.isEnabled) 344 .key(ImageMenuItem.focusablePrefix + this.index) 345 .fillColor($r('sys.color.ohos_id_color_text_primary')) 346 } 347 .width(ImageMenuItem.imageHotZoneWidth) 348 .height(ImageMenuItem.imageHotZoneWidth) 349 .borderRadius(ImageMenuItem.buttonBorderRadius) 350 .foregroundColor(this.getFgColor()) 351 .backgroundColor(this.getBgColor()) 352 .justifyContent(FlexAlign.Center) 353 .opacity(this.item.isEnabled ? 1 : ImageMenuItem.disabledImageOpacity) 354 .stateStyles({ 355 focused: { 356 .border({ 357 radius: $r('sys.float.ohos_id_corner_radius_clicked'), 358 width: ImageMenuItem.focusBorderWidth, 359 color: $r('sys.color.ohos_id_color_focused_outline'), 360 style: BorderStyle.Solid 361 }) 362 }, 363 normal: { 364 .border({ 365 radius: $r('sys.float.ohos_id_corner_radius_clicked'), 366 width: 0 367 }) 368 } 369 }) 370 .onFocus(() => { 371 if (!this.item.isEnabled) { 372 return 373 } 374 this.isOnFocus = true 375 }) 376 .onBlur(() => this.isOnFocus = false) 377 .onHover((isOn) => { 378 if (!this.item.isEnabled) { 379 return 380 } 381 this.isOnHover = isOn 382 }) 383 .onKeyEvent((event) => { 384 if (!this.item.isEnabled) { 385 return 386 } 387 if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { 388 return 389 } 390 if (event.type === KeyType.Down) { 391 this.isOnClick = true 392 } 393 if (event.type === KeyType.Up) { 394 this.isOnClick = false 395 } 396 }) 397 .onTouch((event) => { 398 if (!this.item.isEnabled) { 399 return 400 } 401 if (event.type === TouchType.Down) { 402 this.isOnClick = true 403 } 404 if (event.type === TouchType.Up) { 405 this.isOnClick = false 406 } 407 }) 408 .onClick(() => this.item.isEnabled && this.item.action && this.item.action()) 409 } 410} 411 412export default { ComposeTitleBar }