• 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'
17import MeasureText from '@ohos.measure'
18import window from '@ohos.window'
19import common from '@ohos.app.ability.common'
20import { BusinessError } from '@kit.BasicServicesKit'
21import { hilog } from '@kit.PerformanceAnalysisKit'
22import { SymbolGlyphModifier } from '@kit.ArkUI'
23
24export interface TabTitleBarMenuItem {
25  value: ResourceStr;
26  symbolStyle?: SymbolGlyphModifier;
27  isEnabled?: boolean;
28  action?: () => void;
29  label?: ResourceStr;
30  accessibilityText?: ResourceStr;
31  accessibilityLevel?: string;
32  accessibilityDescription?: ResourceStr;
33}
34
35export interface TabTitleBarTabItem {
36  title: ResourceStr;
37  icon?: ResourceStr;
38  symbolStyle?: SymbolGlyphModifier;
39}
40
41const PUBLIC_MORE = $r('sys.symbol.dot_grid_2x2')
42const TEXT_EDITABLE_DIALOG = '18.3fp'
43const IMAGE_SIZE = '64vp'
44const MAX_DIALOG = '256vp'
45const MIN_DIALOG = '216vp'
46const RESOURCE_TYPE_SYMBOL: number = 40000;
47
48class ButtonGestureModifier implements GestureModifier {
49  public static readonly longPressTime: number = 500;
50  public static readonly minFontSize: number = 1.75;
51  public fontSize: number = 1;
52  public controller: CustomDialogController | null = null;
53
54  constructor(controller: CustomDialogController | null) {
55    this.controller = controller;
56  }
57
58  applyGesture(event: UIGestureEvent): void {
59    if (this.fontSize >= ButtonGestureModifier.minFontSize) {
60      event.addGesture(
61        new LongPressGestureHandler({ repeat: false, duration: ButtonGestureModifier.longPressTime })
62          .onAction(() => {
63            if (event) {
64              this.controller?.open();
65            }
66          })
67          .onActionEnd(() => {
68            this.controller?.close();
69          })
70      )
71    } else {
72      event.clearGestures();
73    }
74  }
75}
76
77@Component
78export struct TabTitleBar {
79  tabItems: Array<TabTitleBarTabItem> = [];
80  menuItems: Array<TabTitleBarMenuItem> = [];
81  @BuilderParam swiperContent: () => void
82
83  @State tabWidth: number = 0
84  @State currentIndex: number = 0
85  @State fontSize: number = 1
86
87  static readonly totalHeight = 56
88  static readonly correctionOffset = -40.0
89  static readonly gradientMaskWidth = 24
90  private static instanceCount = 0
91
92  private menuSectionWidth = 0
93  private tabOffsets: Array<number> = [];
94  private imageWidths: Array<number> = [];
95
96  private scroller: Scroller = new Scroller()
97  private swiperController: SwiperController = new SwiperController()
98  private settings: RenderingContextSettings = new RenderingContextSettings(true)
99  private leftContext2D: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
100  private rightContext2D: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
101
102  @Builder
103  GradientMask(context2D: CanvasRenderingContext2D, x0: number, y0: number, x1: number, y1: number) {
104    Column() {
105      Canvas(context2D)
106        .width(TabTitleBar.gradientMaskWidth)
107        .height(TabTitleBar.totalHeight)
108        .onReady(() => {
109          let grad = context2D.createLinearGradient(x0, y0, x1, y1);
110          grad.addColorStop(0.0, '#ffffffff')
111          grad.addColorStop(1, '#00ffffff')
112          context2D.fillStyle = grad
113          context2D.fillRect(0, 0, TabTitleBar.gradientMaskWidth, TabTitleBar.totalHeight)
114        })
115    }
116    .blendMode(BlendMode.DST_OUT)
117    .width(TabTitleBar.gradientMaskWidth)
118    .height(TabTitleBar.totalHeight)
119  }
120
121  @Builder
122  emptyBuilder() {
123  }
124
125  aboutToAppear() {
126    if (!this.swiperContent) {
127      this.swiperContent = this.emptyBuilder;
128    }
129    this.tabItems.forEach((_elem) => {
130      this.imageWidths.push(0)
131    })
132    this.loadOffsets()
133  }
134
135  loadOffsets() {
136    this.tabOffsets.length = 0
137
138    let tabOffset = 0
139    this.tabOffsets.push(tabOffset)
140    tabOffset += TabContentItem.marginFirst
141
142    this.tabItems.forEach((tabItem, index) => {
143      if (tabItem.icon !== undefined || tabItem.symbolStyle !== undefined) {
144        if (Math.abs(this.imageWidths[index]) > TabContentItem.imageHotZoneWidth) {
145          tabOffset += this.imageWidths[index]
146        } else {
147          tabOffset += TabContentItem.imageHotZoneWidth
148        }
149      } else {
150        tabOffset += TabContentItem.paddingLeft
151        tabOffset += px2vp(MeasureText.measureText({
152          textContent: tabItem.title.toString(),
153          fontSize: 18,
154          fontWeight: FontWeight.Medium,
155        }))
156        tabOffset += TabContentItem.paddingRight
157      }
158      this.tabOffsets.push(tabOffset)
159    })
160  }
161
162  build() {
163    Column() {
164      Flex({
165        justifyContent: FlexAlign.SpaceBetween,
166        alignItems: ItemAlign.Stretch
167      }) {
168        Stack({ alignContent: Alignment.End }) {
169          Stack({ alignContent: Alignment.Start }) {
170            Column() {
171              List({ initialIndex: 0, scroller: this.scroller, space: 0 }) {
172                ForEach(this.tabItems, (tabItem: TabTitleBarTabItem, index: number) => {
173                  ListItem() {
174                    TabContentItem({
175                      item: tabItem,
176                      index: index,
177                      maxIndex: this.tabItems.length - 1,
178                      currentIndex: this.currentIndex,
179                      onCustomClick: (itemIndex) => this.currentIndex = itemIndex,
180                      onImageComplete: (width) => {
181                        this.imageWidths[index] = width
182                        this.loadOffsets()
183                      }
184                    })
185                  }
186                })
187              }
188              .width('100%')
189              .height(TabTitleBar.totalHeight)
190              .constraintSize({ maxWidth: this.tabWidth })
191              .edgeEffect(EdgeEffect.Spring)
192              .listDirection(Axis.Horizontal)
193              .scrollBar(BarState.Off)
194            }
195            this.GradientMask(this.leftContext2D, 0, TabTitleBar.totalHeight / 2,
196              TabTitleBar.gradientMaskWidth, TabTitleBar.totalHeight / 2)
197          }
198          this.GradientMask(this.rightContext2D, TabTitleBar.gradientMaskWidth,
199            TabTitleBar.totalHeight / 2, 0, TabTitleBar.totalHeight / 2)
200        }
201        .blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN)
202
203        if (this.menuItems !== undefined && this.menuItems.length > 0) {
204          CollapsibleMenuSection({ menuItems: this.menuItems, index: 1 + TabTitleBar.instanceCount++ })
205            .height(TabTitleBar.totalHeight)
206            .onAreaChange((_oldValue, newValue) => {
207              this.menuSectionWidth = Number(newValue.width)
208            })
209        }
210      }
211      .backgroundColor($r('sys.color.ohos_id_color_background'))
212      .margin({ right: $r('sys.float.ohos_id_max_padding_end') })
213      .onAreaChange((_oldValue, newValue) => {
214        this.tabWidth = Number(newValue.width) - this.menuSectionWidth
215      })
216
217      Column() {
218        Swiper(this.swiperController) { this.swiperContent() }
219        .index(this.currentIndex)
220        .itemSpace(0)
221        .indicator(false)
222        .width('100%')
223        .height('100%')
224        .curve(Curve.Friction)
225        .onChange((index) => {
226          const offset = this.tabOffsets[index] + TabTitleBar.correctionOffset
227          this.currentIndex = index
228          this.scroller.scrollTo({
229            xOffset: offset > 0 ? offset : 0,
230            yOffset: 0,
231            animation: {
232              duration: 300,
233              curve: Curve.EaseInOut
234            }
235          })
236        })
237        .onAppear(() => {
238          this.scroller.scrollToIndex(this.currentIndex)
239          this.scroller.scrollBy(TabTitleBar.correctionOffset, 0)
240        })
241      }
242    }
243  }
244}
245
246@Component
247struct CollapsibleMenuSection {
248  menuItems: Array<TabTitleBarMenuItem> = [];
249  index: number = 0;
250  item: TabTitleBarMenuItem = {
251    value: PUBLIC_MORE,
252    symbolStyle: new SymbolGlyphModifier(PUBLIC_MORE),
253    label: $r('sys.string.ohos_toolbar_more'),
254  } as TabTitleBarMenuItem;
255  minFontSize: number = 1.75;
256  isFollowingSystemFontScale: boolean = false;
257  maxFontScale: number = 1;
258  systemFontScale?: number = 1;
259
260  static readonly maxCountOfVisibleItems = 1
261  private static readonly focusPadding = 4
262  private static readonly marginsNum = 2
263  private firstFocusableIndex = -1
264
265  @State isPopupShown: boolean = false
266
267  @State isMoreIconOnFocus: boolean = false
268  @State isMoreIconOnHover: boolean = false
269  @State isMoreIconOnClick: boolean = false
270  @Prop @Watch('onFontSizeUpdated') fontSize: number = 1;
271
272  dialogController: CustomDialogController | null = new CustomDialogController({
273    builder: TabTitleBarDialog({
274      cancel: () => {
275      },
276      confirm: () => {
277      },
278      tabTitleDialog: this.item,
279      tabTitleBarDialog: this.item.label ? this.item.label : '',
280      fontSize: this.fontSize,
281    }),
282    maskColor: Color.Transparent,
283    isModal: true,
284    customStyle: true,
285  })
286
287  @State buttonGestureModifier: ButtonGestureModifier = new ButtonGestureModifier(this.dialogController);
288
289  getMoreIconFgColor() {
290    return this.isMoreIconOnClick
291      ? $r('sys.color.ohos_id_color_titlebar_icon_pressed')
292      : $r('sys.color.ohos_id_color_titlebar_icon')
293  }
294
295  getMoreIconBgColor() {
296    if (this.isMoreIconOnClick) {
297      return $r('sys.color.ohos_id_color_click_effect')
298    } else if (this.isMoreIconOnHover) {
299      return $r('sys.color.ohos_id_color_hover')
300    } else {
301      return Color.Transparent
302    }
303  }
304
305  aboutToAppear() {
306    try {
307      let uiContent: UIContext = this.getUIContext();
308      this.isFollowingSystemFontScale = uiContent.isFollowingSystemFontScale();
309      this.maxFontScale = uiContent.getMaxFontScale();
310    } catch (exception) {
311      let code: number = (exception as BusinessError).code;
312      let message: string = (exception as BusinessError).message;
313      hilog.error(0x3900, 'Ace', `Faild to decideFontScale,cause, code: ${code}, message: ${message}`);
314    }
315    this.menuItems.forEach((item, index) => {
316      if (item.isEnabled && this.firstFocusableIndex === -1 &&
317        index > CollapsibleMenuSection.maxCountOfVisibleItems - 2) {
318        this.firstFocusableIndex = this.index * 1000 + index + 1
319      }
320    })
321    this.fontSize = this.decideFontScale()
322  }
323
324  decideFontScale(): number {
325    let uiContent: UIContext = this.getUIContext();
326    this.systemFontScale = (uiContent.getHostContext() as common.UIAbilityContext)?.config?.fontSizeScale ?? 1;
327    if (!this.isFollowingSystemFontScale) {
328      return 1;
329    }
330    return Math.min(this.systemFontScale, this.maxFontScale);
331  }
332
333  onFontSizeUpdated(): void {
334    this.buttonGestureModifier.fontSize = this.fontSize;
335  }
336
337  build() {
338    Column() {
339      Row() {
340        if (this.menuItems.length <= CollapsibleMenuSection.maxCountOfVisibleItems) {
341          ForEach(this.menuItems, (item: TabTitleBarMenuItem, index: number) => {
342            ImageMenuItem({ item: item, index: this.index * 1000 + index + 1 })
343          })
344        } else {
345          ForEach(this.menuItems.slice(0, CollapsibleMenuSection.maxCountOfVisibleItems - 1),
346            (item: TabTitleBarMenuItem, index: number) => {
347              ImageMenuItem({ item: item, index: this.index * 1000 + index + 1 })
348            })
349
350          Button({ type: ButtonType.Normal, stateEffect: true }) {
351            SymbolGlyph(PUBLIC_MORE)
352              .fontSize(TabContentItem.symbolSize)
353              .draggable(false)
354              .fontColor([$r('sys.color.icon_primary')])
355              .focusable(true)
356          }
357          .accessibilityText($r('sys.string.ohos_toolbar_more'))
358          .width(ImageMenuItem.imageHotZoneWidth)
359          .height(ImageMenuItem.imageHotZoneWidth)
360          .borderRadius(ImageMenuItem.buttonBorderRadius)
361          .foregroundColor(this.getMoreIconFgColor())
362          .backgroundColor(this.getMoreIconBgColor())
363          .stateStyles({
364            focused: {
365              .border({
366                radius: $r('sys.float.ohos_id_corner_radius_clicked'),
367                width: ImageMenuItem.focusBorderWidth,
368                color: $r('sys.color.ohos_id_color_focused_outline'),
369                style: BorderStyle.Solid
370              })
371            },
372            normal: {
373              .border({
374                radius: $r('sys.float.ohos_id_corner_radius_clicked'),
375                width: 0
376              })
377            }
378          })
379          .onFocus(() => this.isMoreIconOnFocus = true)
380          .onBlur(() => this.isMoreIconOnFocus = false)
381          .onHover((isOn) => this.isMoreIconOnHover = isOn)
382          .onKeyEvent((event) => {
383            if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) {
384              return
385            }
386            if (event.type === KeyType.Down) {
387              this.isMoreIconOnClick = true
388            }
389            if (event.type === KeyType.Up) {
390              this.isMoreIconOnClick = false
391            }
392          })
393          .onTouch((event) => {
394            if (event.type === TouchType.Down) {
395              this.isMoreIconOnClick = true
396            }
397            if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
398              this.isMoreIconOnClick = false
399              if (this.fontSize >= this.minFontSize) {
400                this.dialogController?.close()
401              }
402            }
403          })
404          .onClick(() => this.isPopupShown = true)
405          .gestureModifier(this.buttonGestureModifier)
406          .bindPopup(this.isPopupShown, {
407            builder: this.popupBuilder,
408            placement: Placement.Bottom,
409            popupColor: Color.White,
410            enableArrow: false,
411            onStateChange: (e) => {
412              this.isPopupShown = e.isVisible
413              if (!e.isVisible) {
414                this.isMoreIconOnClick = false
415              }
416            }
417          })
418        }
419      }
420    }
421    .height('100%')
422    .justifyContent(FlexAlign.Center)
423  }
424
425  onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Layoutable[], constraint: ConstraintSizeOptions): void {
426    children.forEach((child) => {
427      child.layout({ x: 0, y: 0 });
428    })
429    this.fontSize = this.decideFontScale();
430  }
431
432  @Builder
433  popupBuilder() {
434    Column() {
435      ForEach(this.menuItems.slice(CollapsibleMenuSection.maxCountOfVisibleItems - 1, this.menuItems.length),
436        (item: TabTitleBarMenuItem, index: number) => {
437          ImageMenuItem({ item: item, index: this.index * 1000 +
438          CollapsibleMenuSection.maxCountOfVisibleItems + index })
439        })
440    }
441    .width(ImageMenuItem.imageHotZoneWidth + CollapsibleMenuSection.focusPadding * CollapsibleMenuSection.marginsNum)
442    .margin({ top: CollapsibleMenuSection.focusPadding, bottom: CollapsibleMenuSection.focusPadding })
443    .onAppear(() => {
444      focusControl.requestFocus(ImageMenuItem.focusablePrefix + this.firstFocusableIndex)
445    })
446  }
447}
448
449@Component
450struct TabContentItem {
451  item: TabTitleBarTabItem = { title: '' };
452  index: number = 0;
453  maxIndex: number = 0;
454  onCustomClick?: (index: number) => void
455  onImageComplete?: (width: number) => void
456
457  @Prop currentIndex: number
458
459  @State isOnFocus: boolean = false
460  @State isOnHover: boolean = false
461  @State isOnClick: boolean = false
462  @State tabWidth: number = 0
463
464  @State imageWidth: number = 24
465  @State imageHeight: number = 24
466
467  static readonly imageSize = 24
468  static readonly symbolSize = '24vp'
469  static readonly imageHotZoneWidth = 48
470  static readonly imageMagnificationFactor = 1.4
471  static readonly buttonBorderRadius = 8
472  static readonly focusBorderWidth = 2
473  static readonly paddingLeft = 8
474  static readonly paddingRight = 8
475  static readonly marginFirst = 16
476
477  getBgColor() {
478    if (this.isOnClick) {
479      return $r('sys.color.ohos_id_color_click_effect')
480    } else if (this.isOnHover) {
481      return $r('sys.color.ohos_id_color_hover')
482    } else {
483      return Color.Transparent
484    }
485  }
486
487  getBorderAttr(): BorderOptions {
488    if (this.isOnFocus) {
489      return {
490        radius: $r('sys.float.ohos_id_corner_radius_clicked'),
491        width: TabContentItem.focusBorderWidth,
492        color: $r('sys.color.ohos_id_color_focused_outline'),
493        style: BorderStyle.Solid
494      }
495    }
496    return { width: 0 }
497  }
498
499  getImageScaleFactor(): number {
500    return this.index === this.currentIndex ? TabContentItem.imageMagnificationFactor : 1
501  }
502
503  getImageLayoutWidth(): number {
504    return TabContentItem.imageSize / Math.max(this.imageHeight, 1.0) * this.imageWidth
505  }
506
507  private toStringFormat(resource: ResourceStr | undefined): string | undefined {
508    if (typeof resource === 'string') {
509      return resource;
510    } else if (typeof resource === 'undefined') {
511      return '';
512    } else {
513      let resourceString: string = '';
514      try {
515        resourceString = getContext()?.resourceManager?.getStringSync(resource);
516      } catch (err) {
517        let code: number = (err as BusinessError)?.code;
518        let message: string = (err as BusinessError)?.message;
519        hilog.error(0x3900, 'Ace', `Faild to TabTitleBar toStringFormat,code: ${code},message:${message}`);
520      }
521      return resourceString;
522    }
523  }
524
525  build() {
526    Stack() {
527      Row() {
528        Column() {
529          if (this.item.icon === undefined && this.item.symbolStyle === undefined) {
530            Text(this.item.title)
531              .fontSize(this.index === this.currentIndex
532                ? $r('sys.float.ohos_id_text_size_headline7')
533                : $r('sys.float.ohos_id_text_size_headline9'))
534              .fontColor(this.index === this.currentIndex
535                ? $r('sys.color.ohos_id_color_titlebar_text')
536                : $r('sys.color.ohos_id_color_titlebar_text_off'))
537              .fontWeight(FontWeight.Medium)
538              .focusable(true)
539              .animation({ duration: 300 })
540              .padding({
541                top: this.index === this.currentIndex ? 6 : 10,
542                left: TabContentItem.paddingLeft,
543                bottom: 2,
544                right: TabContentItem.paddingRight
545              })
546              .onFocus(() => this.isOnFocus = true)
547              .onBlur(() => this.isOnFocus = false)
548              .onHover((isOn) => this.isOnHover = isOn)
549              .onKeyEvent((event) => {
550                if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) {
551                  return
552                }
553                if (event.type === KeyType.Down) {
554                  this.isOnClick = true
555                }
556                if (event.type === KeyType.Up) {
557                  this.isOnClick = false
558                }
559              })
560              .onTouch((event) => {
561                if (event.type === TouchType.Down) {
562                  this.isOnClick = true
563                }
564                if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
565                  this.isOnClick = false
566                }
567              })
568              .onClick(() => this.onCustomClick && this.onCustomClick(this.index))
569              .accessibilitySelected(this.index === this.currentIndex)
570          } else {
571            Row() {
572              if (this.item.symbolStyle) {
573                SymbolGlyph()
574                  .fontColor([$r('sys.color.icon_primary')])
575                  .attributeModifier(this.item.symbolStyle)
576                  .fontSize(TabContentItem.symbolSize)
577                  .width(this.getImageLayoutWidth())
578                  .height(TabContentItem.imageSize)
579                  .accessibilityText(this.toStringFormat(this.item.title))
580                  .scale({
581                    x: this.getImageScaleFactor(),
582                    y: this.getImageScaleFactor()
583                  })
584                  .animation({ duration: 300 })
585                  .hitTestBehavior(HitTestMode.None)
586                  .focusable(true)
587                  .symbolEffect(new SymbolEffect(), false)
588              } else {
589                if (Util.isSymbolResource(this.item.icon)) {
590                  SymbolGlyph(this.item.icon as Resource)
591                    .fontColor([$r('sys.color.icon_primary')])
592                    .fontSize(TabContentItem.symbolSize)
593                    .width(this.getImageLayoutWidth())
594                    .height(TabContentItem.imageSize)
595                    .accessibilityText(this.toStringFormat(this.item.title))
596                    .scale({
597                      x: this.getImageScaleFactor(),
598                      y: this.getImageScaleFactor()
599                    })
600                    .animation({ duration: 300 })
601                    .hitTestBehavior(HitTestMode.None)
602                    .focusable(true)
603                } else {
604                  Image(this.item.icon)
605                    .alt(this.item.title)
606                    .width(this.getImageLayoutWidth())
607                    .height(TabContentItem.imageSize)
608                    .objectFit(ImageFit.Fill)
609                    .accessibilityText(this.toStringFormat(this.item.title))
610                    .scale({
611                      x: this.getImageScaleFactor(),
612                      y: this.getImageScaleFactor()
613                    })
614                    .animation({ duration: 300 })
615                    .hitTestBehavior(HitTestMode.None)
616                    .focusable(true)
617                    .onComplete((event) => {
618                      if (!this.onImageComplete) {
619                        return
620                      }
621                      this.imageWidth = px2vp(event?.width);
622                      this.imageHeight = px2vp(event?.height);
623                      this.onImageComplete(px2vp(event?.componentWidth) +
624                      TabContentItem.paddingLeft + TabContentItem.paddingRight);
625                    })
626                    .onError((event) => {
627                      if (!this.onImageComplete) {
628                        return
629                      }
630                      this.onImageComplete(px2vp(event.componentWidth) +
631                      TabContentItem.paddingLeft + TabContentItem.paddingRight)
632                    })
633                }
634              }
635            }
636            .width(this.getImageLayoutWidth() * this.getImageScaleFactor() +
637            TabContentItem.paddingLeft + TabContentItem.paddingRight)
638            .constraintSize({
639              minWidth: TabContentItem.imageHotZoneWidth,
640              minHeight: TabContentItem.imageHotZoneWidth
641            })
642            .animation({ duration: 300 })
643            .justifyContent(FlexAlign.Center)
644            .onFocus(() => this.isOnFocus = true)
645            .onBlur(() => this.isOnFocus = false)
646            .onHover((isOn) => this.isOnHover = isOn)
647            .onKeyEvent((event) => {
648              if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) {
649                return
650              }
651              if (event.type === KeyType.Down) {
652                this.isOnClick = true
653              }
654              if (event.type === KeyType.Up) {
655                this.isOnClick = false
656              }
657            })
658            .onTouch((event) => {
659              if (event.type === TouchType.Down) {
660                this.isOnClick = true
661              }
662              if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
663                this.isOnClick = false
664              }
665            })
666            .onClick(() => this.onCustomClick && this.onCustomClick(this.index))
667            .accessibilitySelected(this.index === this.currentIndex)
668          }
669        }
670        .justifyContent(FlexAlign.Center)
671      }
672      .height(TabTitleBar.totalHeight)
673      .alignItems(VerticalAlign.Center)
674      .justifyContent(FlexAlign.Center)
675      .borderRadius(TabContentItem.buttonBorderRadius)
676      .backgroundColor(this.getBgColor())
677      .onAreaChange((_oldValue, newValue) => {
678        this.tabWidth = Number(newValue.width)
679      })
680
681      if (this.isOnFocus && this.tabWidth > 0) {
682        Row()
683          .width(this.tabWidth)
684          .height(TabTitleBar.totalHeight)
685          .hitTestBehavior(HitTestMode.None)
686          .borderRadius(TabContentItem.buttonBorderRadius)
687          .stateStyles({
688            focused: {
689              .border(this.getBorderAttr())
690            },
691            normal: {
692              .border({
693                radius: $r('sys.float.ohos_id_corner_radius_clicked'),
694                width: 0
695              })
696            }
697          })
698      }
699    }
700    .margin({
701      left: this.index === 0 ? TabContentItem.marginFirst : 0,
702      right: this.index === this.maxIndex ? 12 : 0
703    })
704  }
705}
706
707@Component
708struct ImageMenuItem {
709  item: TabTitleBarMenuItem = { value: '' };
710  index: number = 0;
711
712  static readonly imageSize = 24
713  static readonly imageHotZoneWidth = 48
714  static readonly buttonBorderRadius = 8
715  static readonly focusBorderWidth = 2
716  static readonly disabledImageOpacity = 0.4
717  static readonly focusablePrefix = "Id-TabTitleBar-ImageMenuItem-"
718
719  @State isOnFocus: boolean = false
720  @State isOnHover: boolean = false
721  @State isOnClick: boolean = false
722
723  getFgColor() {
724    return this.isOnClick
725      ? $r('sys.color.ohos_id_color_titlebar_icon_pressed')
726      : $r('sys.color.ohos_id_color_titlebar_icon')
727  }
728
729  getBgColor() {
730    if (this.isOnClick) {
731      return $r('sys.color.ohos_id_color_click_effect')
732    } else if (this.isOnHover) {
733      return $r('sys.color.ohos_id_color_hover')
734    } else {
735      return Color.Transparent
736    }
737  }
738
739  private toStringFormat(resource: ResourceStr | undefined): string | undefined {
740    if (typeof resource === 'string') {
741      return resource;
742    } else if (typeof resource === 'undefined') {
743      return '';
744    } else {
745      let resourceString: string = '';
746      try {
747        resourceString = getContext()?.resourceManager?.getStringSync(resource);
748      } catch (err) {
749        let code: number = (err as BusinessError)?.code;
750        let message: string = (err as BusinessError)?.message;
751        hilog.error(0x3900, 'Ace', `Faild to TabTitleBar toStringFormat,code: ${code},message:${message}`);
752      }
753      return resourceString;
754    }
755  }
756
757  private getAccessibilityReadText(): string | undefined {
758    if (this.item.value === PUBLIC_MORE) {
759      return getContext()?.resourceManager?.getStringByNameSync('ohos_toolbar_more');
760    } else if (this.item.accessibilityText) {
761      return this.toStringFormat(this.item.accessibilityText);
762    } else if (this.item.label) {
763      return this.toStringFormat(this.item.label);
764    }
765    return ' ';
766  }
767
768  build() {
769    Button({ type: ButtonType.Normal, stateEffect: this.item.isEnabled }) {
770      if (this.item.symbolStyle) {
771        SymbolGlyph()
772          .fontColor([$r('sys.color.font_primary')])
773          .attributeModifier(this.item.symbolStyle)
774          .fontSize(TabContentItem.symbolSize)
775          .draggable(false)
776          .focusable(this.item?.isEnabled)
777          .key(ImageMenuItem.focusablePrefix + this.index)
778          .symbolEffect(new SymbolEffect(), false)
779      } else {
780        if (Util.isSymbolResource(this.item.value)) {
781          SymbolGlyph(this.item.value as Resource)
782            .fontColor([$r('sys.color.font_primary')])
783            .fontSize(TabContentItem.symbolSize)
784            .draggable(false)
785            .focusable(this.item?.isEnabled)
786            .key(ImageMenuItem.focusablePrefix + this.index)
787        } else {
788          Image(this.item.value)
789            .width(ImageMenuItem.imageSize)
790            .height(ImageMenuItem.imageSize)
791            .focusable(this.item.isEnabled)
792            .key(ImageMenuItem.focusablePrefix + this.index)
793            .draggable(false)
794        }
795      }
796    }
797    .accessibilityText(this.getAccessibilityReadText())
798    .accessibilityLevel(this.item?.accessibilityLevel ?? 'auto')
799    .accessibilityDescription(this.toStringFormat(this.item?.accessibilityDescription))
800    .width(ImageMenuItem.imageHotZoneWidth)
801    .height(ImageMenuItem.imageHotZoneWidth)
802    .borderRadius(ImageMenuItem.buttonBorderRadius)
803    .foregroundColor(this.getFgColor())
804    .backgroundColor(this.getBgColor())
805    .enabled(this.item.isEnabled ? this.item.isEnabled : false)
806    .stateStyles({
807      focused: {
808        .border({
809          radius: $r('sys.float.ohos_id_corner_radius_clicked'),
810          width: ImageMenuItem.focusBorderWidth,
811          color: $r('sys.color.ohos_id_color_focused_outline'),
812          style: BorderStyle.Solid
813        })
814      },
815      normal: {
816        .border({
817          radius: $r('sys.float.ohos_id_corner_radius_clicked'),
818          width: 0
819        })
820      }
821    })
822    .onFocus(() => {
823      if (!this.item.isEnabled) {
824        return
825      }
826      this.isOnFocus = true
827    })
828    .onBlur(() => this.isOnFocus = false)
829    .onHover((isOn) => {
830      if (!this.item.isEnabled) {
831        return
832      }
833      this.isOnHover = isOn
834    })
835    .onKeyEvent((event) => {
836      if (!this.item.isEnabled) {
837        return
838      }
839      if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) {
840        return
841      }
842      if (event.type === KeyType.Down) {
843        this.isOnClick = true
844      }
845      if (event.type === KeyType.Up) {
846        this.isOnClick = false
847      }
848    })
849    .onTouch((event) => {
850      if (!this.item.isEnabled) {
851        return
852      }
853      if (event.type === TouchType.Down) {
854        this.isOnClick = true
855      }
856      if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
857        this.isOnClick = false
858      }
859    })
860    .onClick(() => this.item.isEnabled && this.item.action && this.item.action())
861  }
862}
863
864/**
865 *  TabTitleBarDialog
866 */
867@CustomDialog
868struct TabTitleBarDialog {
869  tabTitleDialog: TabTitleBarMenuItem = { value: '' };
870  callbackId: number | undefined = undefined;
871  tabTitleBarDialog?: ResourceStr = '';
872  mainWindowStage: window.Window | undefined = undefined;
873  controller?: CustomDialogController
874  minFontSize: number = 1.75;
875  maxFontSize: number = 3.2;
876  screenWidth: number = 640;
877  verticalScreenLines: number = 6;
878  horizontalsScreenLines: number = 1;
879  @StorageLink('mainWindow') mainWindow: Promise<window.Window> | undefined = undefined;
880  @State fontSize: number = 1;
881  @State maxLines: number = 1;
882  @StorageProp('windowStandardHeight') windowStandardHeight: number = 0;
883  cancel: () => void = () => {
884  }
885  confirm: () => void = () => {
886  }
887
888  build() {
889    if (this.tabTitleBarDialog) {
890      Column() {
891        if (this.tabTitleDialog.symbolStyle) {
892          SymbolGlyph()
893            .fontColor([$r('sys.color.font_primary')])
894            .attributeModifier(this.tabTitleDialog.symbolStyle)
895            .fontSize(IMAGE_SIZE)
896            .draggable(false)
897            .focusable(this.tabTitleDialog?.isEnabled)
898            .margin({
899              top: $r('sys.float.padding_level24'),
900              bottom: $r('sys.float.padding_level8'),
901            })
902            .symbolEffect(new SymbolEffect(), false)
903        } else if (this.tabTitleDialog.value) {
904          if (Util.isSymbolResource(this.tabTitleDialog.value)) {
905            SymbolGlyph(this.tabTitleDialog.value as Resource)
906              .fontColor([$r('sys.color.font_primary')])
907              .fontSize(IMAGE_SIZE)
908              .draggable(false)
909              .focusable(this.tabTitleDialog?.isEnabled)
910              .margin({
911                top: $r('sys.float.padding_level24'),
912                bottom: $r('sys.float.padding_level8'),
913              })
914          } else {
915            Image(this.tabTitleDialog.value)
916              .width(IMAGE_SIZE)
917              .height(IMAGE_SIZE)
918              .margin({
919                top: $r('sys.float.padding_level24'),
920                bottom: $r('sys.float.padding_level8'),
921              })
922              .fillColor($r('sys.color.icon_primary'))
923          }
924        }
925        Column() {
926          Text(this.tabTitleBarDialog)
927            .fontSize(TEXT_EDITABLE_DIALOG)
928            .textOverflow({ overflow: TextOverflow.Ellipsis })
929            .maxLines(this.maxLines)
930            .width('100%')
931            .textAlign(TextAlign.Center)
932            .fontColor($r('sys.color.font_primary'))
933        }
934        .width('100%')
935        .padding({
936          left: $r('sys.float.padding_level4'),
937          right: $r('sys.float.padding_level4'),
938          bottom: $r('sys.float.padding_level12'),
939        })
940      }
941      .width(this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG)
942      .constraintSize({ minHeight: this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG })
943      .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK)
944      .shadow(ShadowStyle.OUTER_DEFAULT_LG)
945      .borderRadius($r('sys.float.corner_radius_level10'))
946    } else {
947      Column() {
948        if (this.tabTitleDialog.symbolStyle) {
949          SymbolGlyph()
950            .fontColor([$r('sys.color.font_primary')])
951            .attributeModifier(this.tabTitleDialog.symbolStyle)
952            .fontSize(IMAGE_SIZE)
953            .draggable(false)
954            .focusable(this.tabTitleDialog?.isEnabled)
955            .symbolEffect(new SymbolEffect(), false)
956        } else if (this.tabTitleDialog.value){
957          if (Util.isSymbolResource(this.tabTitleDialog.value)) {
958            SymbolGlyph(this.tabTitleDialog.value as Resource)
959              .fontColor([$r('sys.color.font_primary')])
960              .fontSize(IMAGE_SIZE)
961              .draggable(false)
962              .focusable(this.tabTitleDialog?.isEnabled)
963              .margin({
964                top: $r('sys.float.padding_level24'),
965                bottom: $r('sys.float.padding_level8'),
966              })
967          } else {
968            Image(this.tabTitleDialog.value)
969              .width(IMAGE_SIZE)
970              .height(IMAGE_SIZE)
971              .fillColor($r('sys.color.icon_primary'))
972          }
973        }
974      }
975      .width(this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG)
976      .constraintSize({ minHeight: this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG })
977      .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK)
978      .shadow(ShadowStyle.OUTER_DEFAULT_LG)
979      .borderRadius($r('sys.float.corner_radius_level10'))
980      .justifyContent(FlexAlign.Center)
981    }
982  }
983
984  async aboutToAppear(): Promise<void> {
985    let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
986    this.mainWindowStage = context.windowStage.getMainWindowSync();
987    let properties: window.WindowProperties = this.mainWindowStage.getWindowProperties();
988    let rect = properties.windowRect;
989    if (px2vp(rect.height) > this.screenWidth) {
990      this.maxLines = this.verticalScreenLines;
991    } else {
992      this.maxLines = this.horizontalsScreenLines;
993    }
994  }
995}
996
997class Util {
998  public static isSymbolResource(resourceStr: ResourceStr | undefined): boolean {
999    if (!Util.isResourceType(resourceStr)) {
1000      return false;
1001    }
1002    let resource = resourceStr as Resource;
1003    return resource.type === RESOURCE_TYPE_SYMBOL;
1004  }
1005
1006  public static isResourceType(resource: ResourceStr | Resource | undefined): boolean {
1007    if (!resource) {
1008      return false;
1009    }
1010    if (typeof resource === 'string' || typeof resource === 'undefined') {
1011      return false;
1012    }
1013    return true;
1014  }
1015}