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}