• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }