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 isItemOnFocus: 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 79 build() { 80 Flex({ 81 justifyContent: FlexAlign.SpaceBetween, 82 alignItems: ItemAlign.Stretch 83 }) { 84 Row() { 85 Navigator() { 86 ImageMenuItem({ item: { 87 value: PUBLIC_BACK, 88 isEnabled: true 89 } }) 90 } 91 if (this.item !== undefined) { 92 Image(this.item.value) 93 .width(ComposeTitleBar.portraitImageSize) 94 .height(ComposeTitleBar.portraitImageSize) 95 .margin({ 96 left: $r('sys.float.ohos_id_text_paragraph_margin_xs'), 97 right: $r('sys.float.ohos_id_text_paragraph_margin_m') 98 }) 99 .focusable(this.item.isEnabled) 100 .borderRadius(ImageMenuItem.buttonBorderRadius) 101 .onFocus(() => this.isItemOnFocus = true) 102 .onBlur(() => this.isItemOnFocus = false) 103 .border(this.isItemOnFocus ? 104 { width: ImageMenuItem.focusBorderWidth, 105 color: $r('sys.color.ohos_id_color_emphasize'), 106 style: BorderStyle.Solid 107 } : { width: 0 }) 108 .onClick(() => this.item.isEnabled && this.item.action && this.item.action()) 109 } 110 Column() { 111 if (this.title !== undefined) { 112 Row() { 113 Text(this.title) 114 .fontWeight(FontWeight.Medium) 115 .fontSize($r('sys.float.ohos_id_text_size_headline8')) 116 .fontColor($r('sys.color.ohos_id_color_titlebar_text')) 117 .maxLines(this.subtitle !== undefined ? 1 : 2) 118 .textOverflow({ overflow: TextOverflow.Ellipsis }) 119 .constraintSize({ maxWidth: this.titleMaxWidth }) 120 } 121 .justifyContent(FlexAlign.Start) 122 } 123 if (this.subtitle !== undefined) { 124 Row() { 125 Text(this.subtitle) 126 .fontSize($r('sys.float.ohos_id_text_size_over_line')) 127 .fontColor($r('sys.color.ohos_id_color_titlebar_subtitle_text')) 128 .maxLines(1) 129 .textOverflow({ overflow: TextOverflow.Ellipsis }) 130 .constraintSize({ maxWidth: this.titleMaxWidth }) 131 } 132 .justifyContent(FlexAlign.Start) 133 } 134 } 135 .justifyContent(FlexAlign.Start) 136 .alignItems(HorizontalAlign.Start) 137 .constraintSize({ maxWidth: this.titleMaxWidth }) 138 } 139 .margin({ left: $r('sys.float.ohos_id_default_padding_start') }) 140 if (this.menuItems !== undefined && this.menuItems.length > 0) { 141 CollapsibleMenuSection({ menuItems: this.menuItems }) 142 } 143 } 144 .width('100%') 145 .height(ComposeTitleBar.totalHeight) 146 .backgroundColor($r('sys.color.ohos_id_color_background')) 147 .onAreaChange((_oldValue: Area, newValue: Area) => { 148 let newWidth = Number(newValue.width) 149 if (this.menuItems !== undefined) { 150 let menusLength = this.menuItems.length 151 if (menusLength >= CollapsibleMenuSection.maxCountOfVisibleItems) { 152 newWidth = newWidth - ImageMenuItem.imageHotZoneWidth * CollapsibleMenuSection.maxCountOfVisibleItems 153 } else if (menusLength > 0) { 154 newWidth = newWidth - ImageMenuItem.imageHotZoneWidth * menusLength 155 } 156 } 157 this.titleMaxWidth = newWidth 158 this.titleMaxWidth -= ComposeTitleBar.leftPadding 159 this.titleMaxWidth -= ImageMenuItem.imageHotZoneWidth 160 if (this.item !== undefined) { 161 this.titleMaxWidth -= ComposeTitleBar.portraitImageLeftPadding 162 + ComposeTitleBar.portraitImageSize 163 + ComposeTitleBar.portraitImageRightPadding 164 } 165 this.titleMaxWidth -= ComposeTitleBar.rightPadding 166 }) 167 } 168} 169 170@Component 171struct CollapsibleMenuSection { 172 menuItems: Array<ComposeTitleBarMenuItem> 173 174 static readonly maxCountOfVisibleItems = 3 175 private static readonly focusPadding = 4 176 private static readonly marginsNum = 2 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 build() { 201 Column() { 202 Row() { 203 if (this.menuItems.length <= CollapsibleMenuSection.maxCountOfVisibleItems) { 204 ForEach(this.menuItems, (item) => { 205 ImageMenuItem({ item: item }) 206 }) 207 } else { 208 ForEach(this.menuItems.slice(0, CollapsibleMenuSection.maxCountOfVisibleItems - 1), (item) => { 209 ImageMenuItem({ item: item }) 210 }) 211 212 Row() { 213 Image(PUBLIC_MORE) 214 .width(ImageMenuItem.imageSize) 215 .height(ImageMenuItem.imageSize) 216 .focusable(true) 217 } 218 .width(ImageMenuItem.imageHotZoneWidth) 219 .height(ImageMenuItem.imageHotZoneWidth) 220 .borderRadius(ImageMenuItem.buttonBorderRadius) 221 .foregroundColor(this.getMoreIconFgColor()) 222 .backgroundColor(this.getMoreIconBgColor()) 223 .justifyContent(FlexAlign.Center) 224 .border(this.isMoreIconOnFocus ? 225 { width: ImageMenuItem.focusBorderWidth, 226 color: $r('sys.color.ohos_id_color_emphasize'), 227 style: BorderStyle.Solid 228 } : { width: 0 }) 229 .onFocus(() => this.isMoreIconOnFocus = true) 230 .onBlur(() => this.isMoreIconOnFocus = false) 231 .onHover((isOn) => this.isMoreIconOnHover = isOn) 232 .onKeyEvent((event) => { 233 if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { 234 return 235 } 236 if (event.type === KeyType.Down) { 237 this.isMoreIconOnClick = true 238 } 239 if (event.type === KeyType.Up) { 240 this.isMoreIconOnClick = false 241 } 242 }) 243 .onTouch((event) => { 244 if (event.type === TouchType.Down) { 245 this.isMoreIconOnClick = true 246 } 247 if (event.type === TouchType.Up) { 248 this.isMoreIconOnClick = false 249 } 250 }) 251 .onClick(() => this.isPopupShown = true) 252 .bindPopup(this.isPopupShown, { 253 builder: this.popupBuilder, 254 placement: Placement.Bottom, 255 popupColor: Color.White, 256 enableArrow: false, 257 onStateChange: (e) => this.isPopupShown = e.isVisible 258 }) 259 } 260 } 261 } 262 .height('100%') 263 .margin({ right: $r('sys.float.ohos_id_default_padding_end') }) 264 .justifyContent(FlexAlign.Center) 265 } 266 267 @Builder popupBuilder() { 268 Column() { 269 ForEach(this.menuItems.slice(CollapsibleMenuSection.maxCountOfVisibleItems - 1, this.menuItems.length), (item, _index?) => { 270 ImageMenuItem({ item: item }) 271 }) 272 } 273 .width(ImageMenuItem.imageHotZoneWidth + CollapsibleMenuSection.focusPadding * CollapsibleMenuSection.marginsNum) 274 .margin({ top: CollapsibleMenuSection.focusPadding, bottom: CollapsibleMenuSection.focusPadding }) 275 } 276} 277 278@Component 279struct ImageMenuItem { 280 item: ComposeTitleBarMenuItem 281 282 static readonly imageSize = 24 283 static readonly imageHotZoneWidth = 48 284 static readonly buttonBorderRadius = 8 285 static readonly focusBorderWidth = 2 286 static readonly disabledImageOpacity = 0.4 287 288 @State isOnFocus: boolean = false 289 @State isOnHover: boolean = false 290 @State isOnClick: boolean = false 291 292 getFgColor() { 293 return this.isOnClick 294 ? $r('sys.color.ohos_id_color_titlebar_icon_pressed') 295 : $r('sys.color.ohos_id_color_titlebar_icon') 296 } 297 298 getBgColor() { 299 if (this.isOnClick) { 300 return $r('sys.color.ohos_id_color_click_effect') 301 } else if (this.isOnHover) { 302 return $r('sys.color.ohos_id_color_hover') 303 } else { 304 return Color.Transparent 305 } 306 } 307 308 build() { 309 Row() { 310 Image(this.item.value) 311 .width(ImageMenuItem.imageSize) 312 .height(ImageMenuItem.imageSize) 313 .focusable(this.item.isEnabled) 314 } 315 .width(ImageMenuItem.imageHotZoneWidth) 316 .height(ImageMenuItem.imageHotZoneWidth) 317 .borderRadius(ImageMenuItem.buttonBorderRadius) 318 .foregroundColor(this.getFgColor()) 319 .backgroundColor(this.getBgColor()) 320 .justifyContent(FlexAlign.Center) 321 .opacity(this.item.isEnabled ? 1 : ImageMenuItem.disabledImageOpacity) 322 .border(this.isOnFocus ? 323 { width: ImageMenuItem.focusBorderWidth, 324 color: $r('sys.color.ohos_id_color_emphasize'), 325 style: BorderStyle.Solid 326 } : { width: 0 }) 327 .onFocus(() => { 328 if (!this.item.isEnabled) { 329 return 330 } 331 this.isOnFocus = true 332 }) 333 .onBlur(() => this.isOnFocus = false) 334 .onHover((isOn) => { 335 if (!this.item.isEnabled) { 336 return 337 } 338 this.isOnHover = isOn 339 }) 340 .onKeyEvent((event) => { 341 if (!this.item.isEnabled) { 342 return 343 } 344 if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { 345 return 346 } 347 if (event.type === KeyType.Down) { 348 this.isOnClick = true 349 } 350 if (event.type === KeyType.Up) { 351 this.isOnClick = false 352 } 353 }) 354 .onTouch((event) => { 355 if (!this.item.isEnabled) { 356 return 357 } 358 if (event.type === TouchType.Down) { 359 this.isOnClick = true 360 } 361 if (event.type === TouchType.Up) { 362 this.isOnClick = false 363 } 364 }) 365 .onClick(() => this.item.isEnabled && this.item.action && this.item.action()) 366 } 367} 368 369export default {ComposeTitleBar}