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