• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2025 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 {
17  ColorMetrics,
18  curves,
19  ImageModifier,
20  LengthMetrics,
21  LengthUnit,
22  SymbolGlyphModifier,
23  TextModifier,
24  UIContext,
25} from '@kit.ArkUI';
26import { SizeT } from '@ohos.arkui.node';
27import { i18n } from '@kit.LocalizationKit';
28import util from '@ohos.util';
29
30export interface SegmentButtonV2ItemOptions {
31  text?: ResourceStr;
32  icon?: ResourceStr;
33  symbol?: Resource;
34  enabled?: boolean;
35  textModifier?: TextModifier;
36  iconModifier?: ImageModifier;
37  symbolModifier?: SymbolGlyphModifier;
38  accessibilityText?: ResourceStr;
39  accessibilityDescription?: ResourceStr;
40  accessibilityLevel?: string;
41}
42
43export type OnSelectedIndexChange = (selectedIndex: number) => void;
44
45export type OnSelectedIndexesChange = (selectedIndexes: number[]) => void;
46
47interface SegmentButtonV2ContentTheme {
48  itemSpace: LengthMetrics;
49  itemFontSize: Dimension;
50  itemFontColor: ResourceColor;
51  itemFontWeight: FontWeight;
52  itemSelectedFontWeight: FontWeight;
53  itemSelectedFontColor: ResourceColor;
54  itemIconSize: Dimension;
55  itemIconFillColor: ResourceColor;
56  itemSelectedIconFillColor: ResourceColor;
57  itemSymbolFontSize: Dimension;
58  itemSymbolFontColor: ResourceColor;
59  itemSelectedSymbolFontColor: ResourceColor;
60  itemMinHeight: Dimension;
61  hybridItemMinHeight: Dimension;
62  itemPadding: LocalizedPadding;
63  itemMaxFontScale: number | Resource;
64  itemMaxFontScaleSmallest: number;
65  itemMaxFontScaleLargest: number;
66  itemMinFontScale: number | Resource;
67  itemMinFontScaleSmallest: number;
68  itemMinFontScaleLargest: number;
69}
70
71interface SimpleSegmentButtonV2Theme extends SegmentButtonV2ContentTheme {
72  buttonBackgroundColor: Resource;
73  buttonBorderRadius: Resource;
74  buttonMinHeight: Dimension;
75  hybridButtonMinHeight: Dimension;
76  buttonPadding: Resource;
77  itemSelectedBackgroundColor: ResourceColor;
78  itemBorderRadius: Resource;
79  itemShadow: ShadowStyle;
80}
81
82interface SegmentButtonV2ItemRect {
83  size: SizeT<number>;
84  position: PositionT<number>;
85  globalPosition: PositionT<number>;
86}
87
88const SMALLEST_MAX_FONT_SCALE: number = 1;
89const LARGEST_MAX_FONT_SCALE: number = 2;
90const SMALLEST_MIN_FONT_SCALE: number = 0;
91const LARGEST_MIN_FONT_SCALE: number = 1;
92
93const tabSimpleTheme: SimpleSegmentButtonV2Theme = {
94  buttonBackgroundColor: $r('sys.color.segment_button_v2_tab_button_background'),
95  buttonBorderRadius: $r('sys.float.segment_button_v2_background_corner_radius'),
96  buttonMinHeight: $r('sys.float.segment_button_v2_singleline_background_height'),
97  hybridButtonMinHeight: $r('sys.float.segment_button_v2_doubleline_background_height'),
98  buttonPadding: $r('sys.float.padding_level1'),
99  itemSelectedBackgroundColor: $r('sys.color.segment_button_v2_tab_selected_item_background'),
100  itemBorderRadius: $r('sys.float.segment_button_v2_selected_corner_radius'),
101  itemSpace: LengthMetrics.vp(0),
102  itemFontSize: $r('sys.float.ohos_id_text_size_button2'),
103  itemFontColor: $r('sys.color.font_secondary'),
104  itemSelectedFontColor: $r('sys.color.font_primary'),
105  itemFontWeight: FontWeight.Medium,
106  itemSelectedFontWeight: FontWeight.Medium,
107  itemIconSize: 24,
108  itemIconFillColor: $r('sys.color.font_secondary'),
109  itemSelectedIconFillColor: $r('sys.color.font_primary'),
110  itemSymbolFontSize: 20,
111  itemSymbolFontColor: $r('sys.color.font_secondary'),
112  itemSelectedSymbolFontColor: $r('sys.color.font_primary'),
113  itemMinHeight: $r('sys.float.segment_button_v2_singleline_selected_height'),
114  hybridItemMinHeight: $r('sys.float.segment_button_v2_doubleline_selected_height'),
115  itemPadding: {
116    top: LengthMetrics.resource($r('sys.float.padding_level2')),
117    bottom: LengthMetrics.resource($r('sys.float.padding_level2')),
118    start: LengthMetrics.resource($r('sys.float.padding_level4')),
119    end: LengthMetrics.resource($r('sys.float.padding_level4')),
120  },
121  itemShadow: ShadowStyle.OUTER_DEFAULT_XS,
122  itemMaxFontScale: SMALLEST_MAX_FONT_SCALE,
123  itemMaxFontScaleSmallest: SMALLEST_MAX_FONT_SCALE,
124  itemMaxFontScaleLargest: LARGEST_MAX_FONT_SCALE,
125  itemMinFontScale: SMALLEST_MIN_FONT_SCALE,
126  itemMinFontScaleSmallest: SMALLEST_MIN_FONT_SCALE,
127  itemMinFontScaleLargest: LARGEST_MIN_FONT_SCALE,
128};
129
130const capsuleSimpleTheme: SimpleSegmentButtonV2Theme = {
131  buttonBackgroundColor: $r('sys.color.segment_button_v2_tab_button_background'),
132  buttonBorderRadius: $r('sys.float.segment_button_v2_background_corner_radius'),
133  buttonMinHeight: $r('sys.float.segment_button_v2_singleline_background_height'),
134  hybridButtonMinHeight: $r('sys.float.segment_button_v2_doubleline_background_height'),
135  buttonPadding: $r('sys.float.padding_level1'),
136  itemSelectedBackgroundColor: $r('sys.color.comp_background_emphasize'),
137  itemBorderRadius: $r('sys.float.segment_button_v2_selected_corner_radius'),
138  itemSpace: LengthMetrics.vp(0),
139  itemFontSize: $r('sys.float.ohos_id_text_size_button2'),
140  itemFontColor: $r('sys.color.font_secondary'),
141  itemSelectedFontColor: $r('sys.color.font_on_primary'),
142  itemFontWeight: FontWeight.Medium,
143  itemSelectedFontWeight: FontWeight.Medium,
144  itemIconSize: 24,
145  itemIconFillColor: $r('sys.color.icon_secondary'),
146  itemSelectedIconFillColor: $r('sys.color.font_on_primary'),
147  itemSymbolFontSize: 20,
148  itemSymbolFontColor: $r('sys.color.font_secondary'),
149  itemSelectedSymbolFontColor: $r('sys.color.font_on_primary'),
150  itemMinHeight: $r('sys.float.segment_button_v2_singleline_selected_height'),
151  hybridItemMinHeight: $r('sys.float.segment_button_v2_doubleline_selected_height'),
152  itemPadding: {
153    top: LengthMetrics.resource($r('sys.float.padding_level2')),
154    bottom: LengthMetrics.resource($r('sys.float.padding_level2')),
155    start: LengthMetrics.resource($r('sys.float.padding_level4')),
156    end: LengthMetrics.resource($r('sys.float.padding_level4')),
157  },
158  itemShadow: ShadowStyle.OUTER_DEFAULT_XS,
159  itemMaxFontScale: SMALLEST_MAX_FONT_SCALE,
160  itemMaxFontScaleSmallest: SMALLEST_MAX_FONT_SCALE,
161  itemMaxFontScaleLargest: LARGEST_MAX_FONT_SCALE,
162  itemMinFontScale: SMALLEST_MIN_FONT_SCALE,
163  itemMinFontScaleSmallest: SMALLEST_MIN_FONT_SCALE,
164  itemMinFontScaleLargest: LARGEST_MIN_FONT_SCALE,
165}
166
167@ObservedV2
168export class SegmentButtonV2Item {
169  @Trace text?: ResourceStr;
170  @Trace icon?: ResourceStr;
171  @Trace symbol?: Resource;
172  @Trace enabled: boolean;
173  @Trace textModifier?: TextModifier;
174  @Trace iconModifier?: ImageModifier;
175  @Trace symbolModifier?: SymbolGlyphModifier;
176  @Trace accessibilityText?: ResourceStr;
177  @Trace accessibilityDescription?: ResourceStr;
178  @Trace accessibilityLevel?: string;
179
180  constructor(options: SegmentButtonV2ItemOptions) {
181    this.text = options.text;
182    this.icon = options.icon;
183    this.symbol = options.symbol;
184    this.enabled = options.enabled ?? true;
185    this.textModifier = options.textModifier;
186    this.iconModifier = options.iconModifier;
187    this.symbolModifier = options.symbolModifier;
188    this.accessibilityText = options.accessibilityText;
189    this.accessibilityDescription = options.accessibilityDescription;
190    this.accessibilityLevel = options.accessibilityLevel;
191  }
192
193  @Computed
194  get isHybrid(): boolean {
195    return !!this.text && (!!this.icon || !!this.symbol);
196  }
197}
198
199@ObservedV2
200export class SegmentButtonV2Items extends Array<SegmentButtonV2Item> {
201  constructor(length: number);
202
203  constructor(items: SegmentButtonV2ItemOptions[]);
204
205  constructor(lengthOrItemOptionsArray: number | SegmentButtonV2ItemOptions[]) {
206    super(typeof lengthOrItemOptionsArray === 'number' ? lengthOrItemOptionsArray : 0);
207
208    if (typeof lengthOrItemOptionsArray !== 'number' && lengthOrItemOptionsArray && lengthOrItemOptionsArray.length) {
209      for (let options of lengthOrItemOptionsArray) {
210        if (options) {
211          this.push(new SegmentButtonV2Item(options))
212        }
213      }
214    }
215  }
216
217  @Computed
218  get hasHybrid(): boolean {
219    return this.some((item) => item.isHybrid);
220  }
221}
222
223const EMPTY_ITEMS = new SegmentButtonV2Items([]);
224
225@ComponentV2
226export struct TabSegmentButtonV2 {
227  @Require
228  @Param
229  items: SegmentButtonV2Items;
230  @Require
231  @Param
232  selectedIndex: number;
233  @Event
234  $selectedIndex?: OnSelectedIndexChange;
235  @Event
236  onItemClicked?: Callback<number>;
237  @Param
238  itemMinFontScale?: number | Resource = undefined;
239  @Param
240  itemMaxFontScale?: number | Resource = undefined;
241  @Param
242  itemSpace?: LengthMetrics = undefined;
243  @Param
244  itemFontSize?: LengthMetrics = undefined;
245  @Param
246  itemSelectedFontSize?: LengthMetrics = undefined;
247  @Param
248  itemFontColor?: ColorMetrics = undefined;
249  @Param
250  itemSelectedFontColor?: ColorMetrics = undefined;
251  @Param
252  itemFontWeight?: FontWeight = undefined;
253  @Param
254  itemSelectedFontWeight?: FontWeight = undefined;
255  @Param
256  itemBorderRadius?: LengthMetrics = undefined;
257  @Param
258  itemSelectedBackgroundColor?: ColorMetrics = undefined;
259  @Param
260  itemIconSize?: SizeT<LengthMetrics> = undefined;
261  @Param
262  itemIconFillColor?: ColorMetrics = undefined;
263  @Param
264  itemSelectedIconFillColor?: ColorMetrics = undefined;
265  @Param
266  itemSymbolFontSize?: LengthMetrics = undefined;
267  @Param
268  itemSymbolFontColor?: ColorMetrics = undefined;
269  @Param
270  itemSelectedSymbolFontColor?: ColorMetrics = undefined;
271  @Param
272  itemMinHeight?: LengthMetrics = undefined;
273  @Param
274  itemPadding?: LocalizedPadding = undefined;
275  @Param
276  itemShadow?: ShadowOptions | ShadowStyle = undefined;
277  @Param
278  buttonBackgroundColor?: ColorMetrics = undefined;
279  @Param
280  buttonBackgroundBlurStyle?: BlurStyle = undefined;
281  @Param
282  buttonBackgroundBlurStyleOptions?: BackgroundBlurStyleOptions = undefined;
283  @Param
284  buttonBackgroundEffect?: BackgroundEffectOptions = undefined;
285  @Param
286  buttonBorderRadius?: LengthMetrics = undefined;
287  @Param
288  buttonMinHeight?: LengthMetrics = undefined;
289  @Param
290  buttonPadding?: LengthMetrics = undefined;
291  @Param
292  languageDirection?: Direction = undefined;
293
294  build() {
295    SimpleSegmentButtonV2({
296      theme: tabSimpleTheme,
297      items: this.items,
298      selectedIndex: this.selectedIndex,
299      $selectedIndex: (selectedIndex) => {
300        this.$selectedIndex?.(selectedIndex);
301      },
302      onItemClicked: this.onItemClicked,
303      itemMinFontScale: this.itemMinFontScale,
304      itemMaxFontScale: this.itemMaxFontScale,
305      itemSpace: this.itemSpace,
306      itemFontColor: this.itemFontColor,
307      itemSelectedFontColor: this.itemSelectedFontColor,
308      itemFontSize: this.itemFontSize,
309      itemSelectedFontSize: this.itemSelectedFontSize,
310      itemFontWeight: this.itemFontWeight,
311      itemSelectedFontWeight: this.itemSelectedFontWeight,
312      itemSelectedBackgroundColor: this.itemSelectedBackgroundColor,
313      itemIconSize: this.itemIconSize,
314      itemIconFillColor: this.itemIconFillColor,
315      itemSelectedIconFillColor: this.itemSelectedIconFillColor,
316      itemSymbolFontSize: this.itemSymbolFontSize,
317      itemSymbolFontColor: this.itemSymbolFontColor,
318      itemSelectedSymbolFontColor: this.itemSelectedSymbolFontColor,
319      itemBorderRadius: this.itemBorderRadius,
320      itemMinHeight: this.itemMinHeight,
321      itemPadding: this.itemPadding,
322      itemShadow: this.itemShadow,
323      buttonBackgroundColor: this.buttonBackgroundColor,
324      buttonBackgroundBlurStyle: this.buttonBackgroundBlurStyle,
325      buttonBackgroundBlurStyleOptions: this.buttonBackgroundBlurStyleOptions,
326      buttonBackgroundEffect: this.buttonBackgroundEffect,
327      buttonBorderRadius: this.buttonBorderRadius,
328      buttonMinHeight: this.buttonMinHeight,
329      buttonPadding: this.buttonPadding,
330      languageDirection: this.languageDirection,
331    })
332  }
333}
334
335@ComponentV2
336export struct CapsuleSegmentButtonV2 {
337  @Require
338  @Param
339  items: SegmentButtonV2Items;
340  @Require
341  @Param
342  selectedIndex: number;
343  @Event
344  $selectedIndex?: OnSelectedIndexChange;
345  @Event
346  onItemClicked?: Callback<number>;
347  @Param
348  itemMinFontScale?: number | Resource = undefined;
349  @Param
350  itemMaxFontScale?: number | Resource = undefined;
351  @Param
352  itemSpace?: LengthMetrics = undefined;
353  @Param
354  itemFontColor?: ColorMetrics = undefined;
355  @Param
356  itemSelectedFontColor?: ColorMetrics = undefined;
357  @Param
358  itemFontSize?: LengthMetrics = undefined;
359  @Param
360  itemSelectedFontSize?: LengthMetrics = undefined;
361  @Param
362  itemFontWeight?: FontWeight = undefined;
363  @Param
364  itemSelectedFontWeight?: FontWeight = undefined;
365  @Param
366  itemBorderRadius?: LengthMetrics = undefined;
367  @Param
368  itemSelectedBackgroundColor?: ColorMetrics = undefined;
369  @Param
370  itemIconSize?: SizeT<LengthMetrics> = undefined;
371  @Param
372  itemIconFillColor?: ColorMetrics = undefined;
373  @Param
374  itemSelectedIconFillColor?: ColorMetrics = undefined;
375  @Param
376  itemSymbolFontSize?: LengthMetrics = undefined;
377  @Param
378  itemSymbolFontColor?: ColorMetrics = undefined;
379  @Param
380  itemSelectedSymbolFontColor?: ColorMetrics = undefined;
381  @Param
382  itemMinHeight?: LengthMetrics = undefined;
383  @Param
384  itemPadding?: LocalizedPadding = undefined;
385  @Param
386  itemShadow?: ShadowOptions | ShadowStyle = undefined;
387  @Param
388  buttonBackgroundColor?: ColorMetrics = undefined;
389  @Param
390  buttonBackgroundBlurStyle?: BlurStyle = undefined;
391  @Param
392  buttonBackgroundBlurStyleOptions?: BackgroundBlurStyleOptions = undefined;
393  @Param
394  buttonBackgroundEffect?: BackgroundEffectOptions = undefined;
395  @Param
396  buttonBorderRadius?: LengthMetrics = undefined;
397  @Param
398  buttonMinHeight?: LengthMetrics = undefined;
399  @Param
400  buttonPadding?: LengthMetrics = undefined;
401  @Param
402  languageDirection?: Direction = undefined;
403
404  build() {
405    SimpleSegmentButtonV2({
406      theme: capsuleSimpleTheme,
407      items: this.items,
408      selectedIndex: this.selectedIndex,
409      $selectedIndex: (selectedIndex) => {
410        this.$selectedIndex?.(selectedIndex);
411      },
412      onItemClicked: this.onItemClicked,
413      itemMinFontScale: this.itemMinFontScale,
414      itemMaxFontScale: this.itemMaxFontScale,
415      itemSpace: this.itemSpace,
416      itemFontColor: this.itemFontColor,
417      itemSelectedFontColor: this.itemSelectedFontColor,
418      itemFontSize: this.itemFontSize,
419      itemSelectedFontSize: this.itemSelectedFontSize,
420      itemFontWeight: this.itemFontWeight,
421      itemSelectedFontWeight: this.itemSelectedFontWeight,
422      itemSelectedBackgroundColor: this.itemSelectedBackgroundColor,
423      itemIconSize: this.itemIconSize,
424      itemIconFillColor: this.itemIconFillColor,
425      itemSelectedIconFillColor: this.itemSelectedIconFillColor,
426      itemSymbolFontSize: this.itemSymbolFontSize,
427      itemSymbolFontColor: this.itemSymbolFontColor,
428      itemSelectedSymbolFontColor: this.itemSelectedSymbolFontColor,
429      itemBorderRadius: this.itemBorderRadius,
430      itemMinHeight: this.itemMinHeight,
431      itemPadding: this.itemPadding,
432      itemShadow: this.itemShadow,
433      buttonBackgroundColor: this.buttonBackgroundColor,
434      buttonBackgroundBlurStyle: this.buttonBackgroundBlurStyle,
435      buttonBackgroundBlurStyleOptions: this.buttonBackgroundBlurStyleOptions,
436      buttonBackgroundEffect: this.buttonBackgroundEffect,
437      buttonBorderRadius: this.buttonBorderRadius,
438      buttonMinHeight: this.buttonMinHeight,
439      buttonPadding: this.buttonPadding,
440      languageDirection: this.languageDirection,
441    })
442  }
443}
444
445@ComponentV2
446struct SimpleSegmentButtonV2 {
447  @Require
448  @Param
449  items: SegmentButtonV2Items;
450  @Require
451  @Param
452  selectedIndex: number;
453  @Event
454  $selectedIndex?: OnSelectedIndexChange;
455  @Require
456  @Param
457  theme: SimpleSegmentButtonV2Theme;
458  @Event
459  onItemClicked?: Callback<number>;
460  @Require
461  @Param
462  itemMinFontScale?: number | Resource = undefined;
463  @Require
464  @Param
465  itemMaxFontScale?: number | Resource = undefined;
466  @Require
467  @Param
468  itemSpace?: LengthMetrics = undefined;
469  @Require
470  @Param
471  itemFontColor?: ColorMetrics = undefined;
472  @Require
473  @Param
474  itemSelectedFontColor?: ColorMetrics = undefined;
475  @Require
476  @Param
477  itemFontSize?: LengthMetrics = undefined;
478  @Require
479  @Param
480  itemSelectedFontSize?: LengthMetrics = undefined;
481  @Require
482  @Param
483  itemFontWeight?: FontWeight = undefined;
484  @Require
485  @Param
486  itemSelectedFontWeight?: FontWeight = undefined;
487  @Require
488  @Param
489  itemBorderRadius?: LengthMetrics = undefined;
490  @Require
491  @Param
492  itemSelectedBackgroundColor?: ColorMetrics = undefined;
493  @Require
494  @Param
495  itemIconSize?: SizeT<LengthMetrics> = undefined;
496  @Require
497  @Param
498  itemIconFillColor?: ColorMetrics = undefined;
499  @Require
500  @Param
501  itemSelectedIconFillColor?: ColorMetrics = undefined;
502  @Require
503  @Param
504  itemSymbolFontSize?: LengthMetrics = undefined;
505  @Require
506  @Param
507  itemSymbolFontColor?: ColorMetrics = undefined;
508  @Require
509  @Param
510  itemSelectedSymbolFontColor?: ColorMetrics = undefined;
511  @Require
512  @Param
513  itemMinHeight?: LengthMetrics = undefined;
514  @Require
515  @Param
516  itemPadding?: LocalizedPadding = undefined;
517  @Require
518  @Param
519  itemShadow?: ShadowOptions | ShadowStyle = undefined;
520  @Require
521  @Param
522  buttonBackgroundColor?: ColorMetrics = undefined;
523  @Require
524  @Param
525  buttonBackgroundBlurStyle?: BlurStyle = undefined;
526  @Require
527  @Param
528  buttonBackgroundBlurStyleOptions?: BackgroundBlurStyleOptions = undefined;
529  @Require
530  @Param
531  buttonBackgroundEffect?: BackgroundEffectOptions = undefined;
532  @Require
533  @Param
534  buttonBorderRadius?: LengthMetrics = undefined;
535  @Require
536  @Param
537  buttonMinHeight?: LengthMetrics = undefined;
538  @Require
539  @Param
540  buttonPadding?: LengthMetrics = undefined;
541  @Require
542  @Param
543  languageDirection?: Direction = undefined;
544  @Local
545  itemRects: SegmentButtonV2ItemRect[] = [];
546  @Local
547  itemScale: number = 1;
548  @Local
549  hoveredItemIndex: number = -1;
550  @Local
551  mousePressedItemIndex: number = -1;
552  @Local
553  touchPressedItemIndex: number = -1;
554  private isMouseWheelScroll: boolean = false;
555  private isDragging: boolean = false;
556  private panStartGlobalX: number = 0;
557  private panStartIndex: number = -1;
558  private focusGroupId: string = GroupIdGenerator.getInstance().generate();
559
560  @Computed
561  get normalizedSelectedIndex(): number {
562    const items = this.getItems();
563    return normalize(this.selectedIndex, 0, items.length - 1);
564  }
565
566  @Computed
567  get selectedItemRect(): SegmentButtonV2ItemRect | undefined {
568    return this.itemRects[this.normalizedSelectedIndex];
569  }
570
571  @LocalBuilder
572  private ContentLayer() {
573    Flex({ alignItems: ItemAlign.Stretch, space: { main: this.getItemSpace() } }) {
574      Repeat(this.getItems())
575        .each((repeatItem: RepeatItem<SegmentButtonV2Item>) => {
576          Button({ type: ButtonType.Normal }) {
577            SegmentButtonV2ItemContent({
578              theme: this.theme,
579              item: repeatItem.item,
580              selected: this.isSelected(repeatItem),
581              itemMinFontScale: this.itemMinFontScale,
582              itemMaxFontScale: this.itemMaxFontScale,
583              itemFontColor: this.itemFontColor,
584              itemSelectedFontColor: this.itemSelectedFontColor,
585              itemFontSize: this.itemFontSize,
586              itemSelectedFontSize: this.itemSelectedFontSize,
587              itemFontWeight: this.itemFontWeight,
588              itemSelectedFontWeight: this.itemSelectedFontWeight,
589              itemIconSize: this.itemIconSize,
590              itemIconFillColor: this.itemIconFillColor,
591              itemSelectedIconFillColor: this.itemSelectedIconFillColor,
592              itemSymbolFontSize: this.itemSymbolFontSize,
593              itemSymbolFontColor: this.itemSymbolFontColor,
594              itemSelectedSymbolFontColor: this.itemSelectedSymbolFontColor,
595              itemMinHeight: this.itemMinHeight,
596              itemPadding: this.itemPadding,
597              languageDirection: this.languageDirection,
598              hasHybrid: this.getItems().hasHybrid,
599            })
600          }
601          .accessibilityGroup(true)
602          .accessibilitySelected(this.isSelected(repeatItem))
603          .accessibilityText(this.getItemAccessibilityText(repeatItem))
604          .accessibilityDescription(this.getItemAccessibilityDescription(repeatItem))
605          .accessibilityLevel(repeatItem.item.accessibilityLevel)
606          .backgroundColor(Color.Transparent)
607          .borderRadius(this.getItemBorderRadius())
608          .direction(this.languageDirection)
609          .enabled(repeatItem.item.enabled)
610          .focusScopePriority(this.focusGroupId, this.getFocusPriority(repeatItem))
611          .hoverEffect(HoverEffect.None)
612          .layoutWeight(1)
613          .padding(0)
614          .scale(this.getItemScale(repeatItem.index))
615          .stateEffect(false)
616          .onAreaChange((_, area) => {
617            this.itemRects[repeatItem.index] = {
618              size: {
619                width: area.width as number,
620                height: area.height as number,
621              },
622              position: {
623                x: area.position.x as number,
624                y: area.position.y as number,
625              },
626              globalPosition: {
627                x: area.globalPosition.x as number,
628                y: area.globalPosition.y as number,
629              }
630            };
631          })
632          .gesture(
633            TapGesture().onAction(() => {
634              this.onItemClicked?.(repeatItem.index);
635              this.updateSelectedIndex(repeatItem.index);
636            })
637          )
638          .onTouch((event) => {
639            if (event.type === TouchType.Down) {
640              if (this.isSelected(repeatItem)) {
641                this.updateItemScale(0.95);
642              }
643              this.updateTouchPressedItemIndex(repeatItem.index);
644            } else if ([TouchType.Up, TouchType.Cancel].includes(event.type)) {
645              this.updateItemScale(1)
646              this.updateTouchPressedItemIndex(-1);
647            }
648          })
649          .onHover((isHover) => {
650            if (isHover) {
651              this.updateHoveredItemIndex(repeatItem.index);
652            } else {
653              this.updateHoveredItemIndex(-1);
654            }
655          })
656          .onMouse((event) => {
657            if (event.action === MouseAction.Press) {
658              this.updateMousePressedItemIndex(repeatItem.index);
659            } else if ([MouseAction.Release, MouseAction.CANCEL].includes(event.action)) {
660              this.updateMousePressedItemIndex(-1);
661            }
662          })
663        })
664        .key(generateUniqueKye(this.focusGroupId))
665    }
666    .constraintSize({
667      minWidth: '100%',
668      minHeight: this.getButtonMinHeight()
669    })
670    .clip(false)
671    .direction(this.languageDirection)
672    .focusScopeId(this.focusGroupId, true)
673    .padding(this.getButtonPadding())
674    .priorityGesture(
675      PanGesture()
676        .onActionStart((event) => {
677          const finger = event.fingerList.find(Boolean);
678          if (!finger) {
679            return;
680          }
681          const index = this.getIndexByPosition(finger.globalX, finger.globalY);
682          if (!this.isItemEnabled(index)) {
683            return;
684          }
685          if (event.axisHorizontal !== 0 || event.axisVertical !== 0) {
686            this.isMouseWheelScroll = true;
687            return;
688          }
689          if (index === this.normalizedSelectedIndex) {
690            this.isDragging = true;
691          }
692          this.panStartGlobalX = finger.globalX;
693          this.panStartIndex = index;
694        })
695        .onActionUpdate((event) => {
696          if (!this.isDragging) {
697            return;
698          }
699          const finger = event.fingerList.find(Boolean);
700          if (!finger) {
701            return;
702          }
703          const index = this.getIndexByPosition(finger.globalX, finger.globalY);
704          this.updateSelectedIndex(index);
705        })
706        .onActionEnd((event) => {
707          if (!this.isItemEnabled(this.panStartIndex)) {
708            return;
709          }
710          // handle mouse wheel scroll event
711          if (this.isMouseWheelScroll) {
712            const offset = event.offsetX !== 0 ? event.offsetX : event.offsetY;
713            const deltaIndex = offset < 0 ? 1 : -1;
714            this.updateSelectedIndex(this.normalizedSelectedIndex + deltaIndex);
715            this.isMouseWheelScroll = false;
716            return;
717          }
718          // handle drag event
719          if (this.isDragging) {
720            this.isDragging = false;
721            return;
722          }
723          // handle swipe event
724          if (!this.isItemEnabled(this.normalizedSelectedIndex)) {
725            return;
726          }
727          const finger = event.fingerList.find(Boolean);
728          if (!finger) {
729            return;
730          }
731          let deltaIndex = finger.globalX - this.panStartGlobalX < 0 ? -1 : 1;
732          if (this.isRTL()) {
733            deltaIndex = -deltaIndex;
734          }
735          this.updateSelectedIndex(this.normalizedSelectedIndex + deltaIndex);
736        })
737        .onActionCancel(() => {
738          this.isDragging = false;
739          this.isMouseWheelScroll = false;
740          this.panStartIndex = -1;
741        })
742    )
743  }
744
745  private getFocusPriority(repeatItem: RepeatItem<SegmentButtonV2Item>): FocusPriority | undefined {
746    return this.normalizedSelectedIndex === repeatItem.index ? FocusPriority.PREVIOUS : FocusPriority.AUTO;
747  }
748
749  private isItemEnabled(index: number): boolean {
750    const items = this.getItems();
751    if (index < 0 || index >= items.length) {
752      return false;
753    }
754    return items[index].enabled;
755  }
756
757  @LocalBuilder
758  private BackplateLayer() {
759    if (this.selectedItemRect) {
760      Stack()
761        .position({
762          x: this.selectedItemRect.position.x,
763          y: this.selectedItemRect.position.y,
764        })
765        .backgroundColor(this.getItemSelectedBackgroundColor())
766        .borderRadius(this.getItemBorderRadius())
767        .scale({ x: this.itemScale, y: this.itemScale })
768        .shadow(this.getItemBackplateShadow())
769        .height(this.selectedItemRect.size.height)
770        .width(this.selectedItemRect.size.width)
771    }
772  }
773
774  @LocalBuilder
775  EffectLayer() {
776    Repeat(this.getItemRects())
777      .each((repeatItem) => {
778        Stack()
779          .backgroundColor(this.getEffectBackgroundColor(repeatItem))
780          .borderRadius(this.getItemBorderRadius())
781          .height(repeatItem.item.size.height)
782          .position({
783            x: repeatItem.item.position.x,
784            y: repeatItem.item.position.y
785          })
786          .scale(this.getItemScale(repeatItem.index))
787          .width(repeatItem.item.size.width)
788      })
789  }
790
791  private getItemRects(): SegmentButtonV2ItemRect[] {
792    if (!this.items) {
793      return [];
794    }
795    if (this.items.length === this.itemRects.length) {
796      return this.itemRects;
797    }
798
799    return this.itemRects.slice(0, this.items.length);
800  }
801
802  build() {
803    Stack() {
804      Stack() {
805        this.EffectLayer()
806        this.BackplateLayer()
807        this.ContentLayer()
808      }
809      .borderRadius(this.getButtonBorderRadius())
810      .backgroundBlurStyle(this.getButtonBackgroundBlurStyle(), this.getButtonBackgroundBlurStyleOptions(),
811        { disableSystemAdaptation: true })
812      .clip(false)
813      .direction(this.languageDirection)
814    }
815    .backgroundColor(this.getButtonBackgroundColor())
816    .backgroundEffect(this.buttonBackgroundEffect, { disableSystemAdaptation: true })
817    .borderRadius(this.getButtonBorderRadius())
818    .clip(false)
819    .constraintSize({
820      minWidth: '100%',
821      minHeight: this.getButtonMinHeight()
822    })
823    .direction(this.languageDirection)
824  }
825
826  private getItems(): SegmentButtonV2Items {
827    return this.items ?? EMPTY_ITEMS;
828  }
829
830  private getItemBackplateShadow(): ShadowOptions | ShadowStyle | undefined {
831    return this.itemShadow ?? this.theme.itemShadow
832  }
833
834  private getButtonBackgroundBlurStyle(): BlurStyle | undefined {
835    if (this.buttonBackgroundEffect) {
836      return undefined;
837    }
838    return this.buttonBackgroundBlurStyle;
839  }
840
841  private getButtonBackgroundBlurStyleOptions(): BackgroundBlurStyleOptions | undefined {
842    if (this.buttonBackgroundEffect) {
843      return undefined;
844    }
845    return this.buttonBackgroundBlurStyleOptions;
846  }
847
848  private getItemScale(index: number): ScaleOptions {
849    const pressed: boolean = this.isPressed(index);
850    const scale: number = pressed ? 0.95 : 1;
851    return { x: scale, y: scale, };
852  }
853
854  private isPressed(index: number): boolean {
855    return this.mousePressedItemIndex === index;
856  }
857
858  private updateHoveredItemIndex(index: number) {
859    if (index === this.hoveredItemIndex) {
860      return;
861    }
862    animateTo({ duration: 250, curve: Curve.Friction }, () => {
863      this.hoveredItemIndex = index;
864    });
865  }
866
867  private updateMousePressedItemIndex(index: number) {
868    if (index === this.mousePressedItemIndex) {
869      return;
870    }
871    animateTo({ duration: 250, curve: Curve.Friction }, () => {
872      this.mousePressedItemIndex = index;
873    });
874  }
875
876  private updateTouchPressedItemIndex(index: number) {
877    if (index === this.touchPressedItemIndex) {
878      return;
879    }
880    animateTo({ duration: 250, curve: Curve.Friction }, () => {
881      this.touchPressedItemIndex = index;
882    });
883  }
884
885  private isRTL(): boolean {
886    if (this.languageDirection || this.languageDirection === Direction.Auto) {
887      return i18n.isRTL(i18n.System.getSystemLanguage());
888    }
889    return this.languageDirection === Direction.Rtl;
890  }
891
892  private getEffectBackgroundColor(repeatItem: RepeatItem<SegmentButtonV2ItemRect>): ResourceColor {
893    if (repeatItem.index === this.mousePressedItemIndex) {
894      return $r('sys.color.interactive_click');
895    }
896    if (repeatItem.index === this.hoveredItemIndex) {
897      return $r('sys.color.interactive_hover');
898    }
899    return Color.Transparent;
900  }
901
902  private getItemBorderRadius(): Length | BorderRadiuses | LocalizedBorderRadiuses {
903    if (this.itemBorderRadius && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemBorderRadius)) {
904      return LengthMetricsUtils.getInstance().stringify(this.itemBorderRadius);
905    }
906    return this.theme.itemBorderRadius;
907
908  }
909
910  private getItemSelectedBackgroundColor(): ResourceColor {
911    if (this.itemSelectedBackgroundColor) {
912      return this.itemSelectedBackgroundColor.color;
913    }
914    return this.theme.itemSelectedBackgroundColor;
915  }
916
917  getItemSpace(): LengthMetrics {
918    if (this.itemSpace && this.itemSpace.unit !== LengthUnit.PERCENT
919      && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSpace)) {
920      return this.itemSpace;
921    }
922    return this.theme.itemSpace;
923  }
924
925  getIndexByPosition(globalX: number, globalY: number): number {
926    let index = 0;
927    while (index < this.itemRects.length) {
928      const rect = this.itemRects[index];
929      if (this.isPointOnRect(globalX, globalY, rect)) {
930        return index;
931      }
932      ++index;
933    }
934    return -1;
935  }
936
937  private isPointOnRect(globalX: number, globalY: number, rect: SegmentButtonV2ItemRect): boolean {
938    return globalX >= rect.globalPosition.x && globalX <= rect.globalPosition.x + rect.size.width &&
939      globalY >= rect.globalPosition.y && globalY <= rect.globalPosition.y + rect.size.height;
940  }
941
942  private updateSelectedIndex(selectedIndex: number) {
943    if (!this.isItemEnabled(selectedIndex) || selectedIndex === this.selectedIndex
944    ) {
945      return;
946    }
947    this.getUIContext().animateTo({ curve: curves.springMotion(0.347, 0.99) }, () => {
948      this.$selectedIndex?.(selectedIndex);
949    });
950  }
951
952  private updateItemScale(scale: number) {
953    if (this.itemScale === scale) {
954      return;
955    }
956    this.getUIContext().animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => {
957      this.itemScale = scale;
958    });
959  }
960
961  private getItemAccessibilityDescription(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined {
962    return repeatItem.item.accessibilityDescription as ESObject as string;
963  }
964
965  private getItemAccessibilityText(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined {
966    return repeatItem.item.accessibilityText as ESObject as string;
967  }
968
969  private isSelected(repeatItem: RepeatItem<SegmentButtonV2Item>): boolean | undefined {
970    return repeatItem.index === this.normalizedSelectedIndex;
971  }
972
973  private getButtonPadding(): Length | Padding | LocalizedPadding {
974    if (this.buttonPadding && LengthMetricsUtils.getInstance().isNaturalNumber(this.buttonPadding)) {
975      return LengthMetricsUtils.getInstance().stringify(this.buttonPadding);
976    }
977    return this.theme.buttonPadding;
978  }
979
980  private getButtonBorderRadius(): Length | BorderRadiuses | LocalizedBorderRadiuses {
981    if (this.buttonBorderRadius && LengthMetricsUtils.getInstance().isNaturalNumber(this.buttonBorderRadius)) {
982      return LengthMetricsUtils.getInstance().stringify(this.buttonBorderRadius);
983    }
984    return this.theme.buttonBorderRadius;
985  }
986
987  private getButtonBackgroundColor(): ResourceColor {
988    if (this.buttonBackgroundColor) {
989      return this.buttonBackgroundColor.color;
990    }
991    return this.theme.buttonBackgroundColor;
992  }
993
994  private getButtonMinHeight(): Dimension {
995    if (this.buttonMinHeight && LengthMetricsUtils.getInstance().isNaturalNumber(this.buttonMinHeight)) {
996      return LengthMetricsUtils.getInstance().stringify(this.buttonMinHeight);
997    }
998    const items = this.getItems();
999    return items.hasHybrid ? this.theme.hybridButtonMinHeight : this.theme.buttonMinHeight;
1000  }
1001}
1002
1003interface MultiplySegmentButtonV2Theme extends SegmentButtonV2ContentTheme {
1004  itemBackgroundColor: ResourceColor;
1005  itemSelectedBackgroundColor: ResourceColor;
1006  itemBorderRadius: Resource;
1007}
1008
1009const multiplyCapsuleTheme: MultiplySegmentButtonV2Theme = {
1010  itemBorderRadius: $r('sys.float.segment_button_v2_multi_corner_radius'),
1011  itemBackgroundColor: $r('sys.color.segment_button_v2_multi_capsule_button_background'),
1012  itemSelectedBackgroundColor: $r('sys.color.comp_background_emphasize'),
1013  itemSpace: LengthMetrics.vp(1),
1014  itemFontColor: $r('sys.color.font_secondary'),
1015  itemSelectedFontColor: $r('sys.color.font_on_primary'),
1016  itemFontWeight: FontWeight.Medium,
1017  itemSelectedFontWeight: FontWeight.Medium,
1018  itemIconFillColor: $r('sys.color.icon_secondary'),
1019  itemSelectedIconFillColor: $r('sys.color.font_on_primary'),
1020  itemSymbolFontColor: $r('sys.color.font_secondary'),
1021  itemSelectedSymbolFontColor: $r('sys.color.font_on_primary'),
1022  itemFontSize: $r('sys.float.ohos_id_text_size_button2'),
1023  itemIconSize: 24,
1024  itemSymbolFontSize: 20,
1025  itemPadding: {
1026    top: LengthMetrics.resource($r('sys.float.padding_level2')),
1027    bottom: LengthMetrics.resource($r('sys.float.padding_level2')),
1028    start: LengthMetrics.resource($r('sys.float.padding_level4')),
1029    end: LengthMetrics.resource($r('sys.float.padding_level4')),
1030  },
1031  itemMinHeight: $r('sys.float.segment_button_v2_multi_singleline_height'),
1032  hybridItemMinHeight: $r('sys.float.segment_button_v2_multi_doubleline_height'),
1033  itemMaxFontScale: SMALLEST_MAX_FONT_SCALE,
1034  itemMaxFontScaleSmallest: SMALLEST_MAX_FONT_SCALE,
1035  itemMaxFontScaleLargest: LARGEST_MAX_FONT_SCALE,
1036  itemMinFontScale: SMALLEST_MIN_FONT_SCALE,
1037  itemMinFontScaleSmallest: SMALLEST_MIN_FONT_SCALE,
1038  itemMinFontScaleLargest: LARGEST_MIN_FONT_SCALE,
1039};
1040
1041@ComponentV2
1042export struct MultiCapsuleSegmentButtonV2 {
1043  @Require
1044  @Param
1045  items: SegmentButtonV2Items;
1046  @Require
1047  @Param
1048  selectedIndexes: number[];
1049  @Event
1050  $selectedIndexes: OnSelectedIndexesChange;
1051  @Event
1052  onItemClicked?: Callback<number>;
1053  @Param
1054  itemMinFontScale?: number | Resource = undefined;
1055  @Param
1056  itemMaxFontScale?: number | Resource = undefined;
1057  @Param
1058  itemSpace?: LengthMetrics = undefined;
1059  @Param
1060  itemFontColor?: ColorMetrics = undefined;
1061  @Param
1062  itemSelectedFontColor?: ColorMetrics = undefined;
1063  @Param
1064  itemFontSize?: LengthMetrics = undefined;
1065  @Param
1066  itemSelectedFontSize?: LengthMetrics = undefined;
1067  @Param
1068  itemFontWeight?: FontWeight = undefined;
1069  @Param
1070  itemSelectedFontWeight?: FontWeight = undefined;
1071  @Param
1072  itemBorderRadius?: LengthMetrics = undefined;
1073  @Param
1074  itemBackgroundColor?: ColorMetrics = undefined;
1075  @Param
1076  itemBackgroundEffect?: BackgroundEffectOptions = undefined;
1077  @Param
1078  itemBackgroundBlurStyle?: BlurStyle = undefined;
1079  @Param
1080  itemBackgroundBlurStyleOptions?: BackgroundBlurStyleOptions = undefined;
1081  @Param
1082  itemSelectedBackgroundColor?: ColorMetrics = undefined;
1083  @Param
1084  itemIconSize?: SizeT<LengthMetrics> = undefined;
1085  @Param
1086  itemIconFillColor?: ColorMetrics = undefined;
1087  @Param
1088  itemSelectedIconFillColor?: ColorMetrics = undefined;
1089  @Param
1090  itemSymbolFontSize?: LengthMetrics = undefined;
1091  @Param
1092  itemSymbolFontColor?: ColorMetrics = undefined;
1093  @Param
1094  itemSelectedSymbolFontColor?: ColorMetrics = undefined;
1095  @Param
1096  itemMinHeight?: LengthMetrics = undefined;
1097  @Param
1098  itemPadding?: LocalizedPadding = undefined;
1099  @Param
1100  languageDirection?: Direction = undefined;
1101  private theme: MultiplySegmentButtonV2Theme = multiplyCapsuleTheme;
1102  private focusGroupId: string = GroupIdGenerator.getInstance().generate();
1103
1104  build() {
1105    Flex({ alignItems: ItemAlign.Stretch, space: { main: this.getItemSpace() } }) {
1106      Repeat(this.getItems())
1107        .each((repeatItem: RepeatItem<SegmentButtonV2Item>) => {
1108          Button({ type: ButtonType.Normal }) {
1109            SegmentButtonV2ItemContent({
1110              theme: this.theme,
1111              item: repeatItem.item,
1112              selected: this.isSelected(repeatItem),
1113              hasHybrid: this.getItems().hasHybrid,
1114              itemMinFontScale: this.itemMinFontScale,
1115              itemMaxFontScale: this.itemMaxFontScale,
1116              itemFontColor: this.itemFontColor,
1117              itemSelectedFontColor: this.itemSelectedFontColor,
1118              itemFontSize: this.itemFontSize,
1119              itemSelectedFontSize: this.itemSelectedFontSize,
1120              itemFontWeight: this.itemFontWeight,
1121              itemSelectedFontWeight: this.itemSelectedFontWeight,
1122              itemIconSize: this.itemIconSize,
1123              itemIconFillColor: this.itemIconFillColor,
1124              itemSelectedIconFillColor: this.itemSelectedIconFillColor,
1125              itemSymbolFontSize: this.itemSymbolFontSize,
1126              itemSymbolFontColor: this.itemSymbolFontColor,
1127              itemSelectedSymbolFontColor: this.itemSelectedSymbolFontColor,
1128              itemMinHeight: this.itemMinHeight,
1129              itemPadding: this.itemPadding,
1130              languageDirection: this.languageDirection,
1131            })
1132              .borderRadius(this.getItemButtonBorderRadius(repeatItem))
1133              .backgroundBlurStyle(this.getItemBackgroundBlurStyle(), this.getItemBackgroundBlurStyleOptions(),
1134                { disableSystemAdaptation: true })
1135              .direction(this.languageDirection)
1136          }
1137          .accessibilityGroup(true)
1138          .accessibilityChecked(this.isSelected(repeatItem))
1139          .accessibilityText(this.getItemAccessibilityText(repeatItem))
1140          .accessibilityDescription(this.getItemAccessibilityDescription(repeatItem))
1141          .accessibilityLevel(repeatItem.item.accessibilityLevel)
1142          .backgroundColor(this.getItemBackgroundColor(repeatItem))
1143          .backgroundEffect(this.itemBackgroundEffect, { disableSystemAdaptation: true })
1144          .borderRadius(this.getItemButtonBorderRadius(repeatItem))
1145          .constraintSize({ minHeight: this.getItemMinHeight() })
1146          .direction(this.languageDirection)
1147          .enabled(repeatItem.item.enabled)
1148          .focusScopePriority(this.focusGroupId, this.getFocusPriority(repeatItem))
1149          .layoutWeight(1)
1150          .padding(0)
1151          .onClick(() => {
1152            this.onItemClicked?.(repeatItem.index);
1153            let selection: number[];
1154            const items = this.getItems();
1155            const selectedIndexes = this.selectedIndexes ?? [];
1156            if (this.isSelected(repeatItem)) {
1157              selection = selectedIndexes.filter((index) => {
1158                if (index < 0 || index > items.length - 1) {
1159                  return false;
1160                }
1161                return index !== repeatItem.index;
1162              });
1163            } else {
1164              selection = selectedIndexes
1165                .filter((index) => index >= 0 && index <= items.length - 1)
1166                .concat(repeatItem.index);
1167            }
1168            this.$selectedIndexes(selection);
1169          })
1170        })
1171        .key(generateUniqueKye(this.focusGroupId))
1172    }
1173    .clip(false)
1174    .direction(this.languageDirection)
1175    .focusScopeId(this.focusGroupId, true)
1176  }
1177
1178  private getFocusPriority(repeatItem: RepeatItem<SegmentButtonV2Item>): FocusPriority | undefined {
1179    return Math.min(...this.selectedIndexes) === repeatItem.index ? FocusPriority.PREVIOUS : FocusPriority.AUTO;
1180  }
1181
1182  getItems(): SegmentButtonV2Items {
1183    return this.items ?? EMPTY_ITEMS;
1184  }
1185
1186  getItemBackgroundBlurStyleOptions(): BackgroundBlurStyleOptions | undefined {
1187    if (this.itemBackgroundEffect) {
1188      return undefined;
1189    }
1190    return this.itemBackgroundBlurStyleOptions;
1191  }
1192
1193  getItemBackgroundBlurStyle(): BlurStyle | undefined {
1194    if (this.itemBackgroundEffect) {
1195      return undefined;
1196    }
1197    return this.itemBackgroundBlurStyle;
1198  }
1199
1200  private getItemAccessibilityDescription(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined {
1201    return repeatItem.item.accessibilityDescription as ESObject as string;
1202  }
1203
1204  private getItemAccessibilityText(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined {
1205    return repeatItem.item.accessibilityText as ESObject as string;
1206  }
1207
1208  private getItemSpace(): LengthMetrics {
1209    if (this.itemSpace && this.itemSpace.unit !== LengthUnit.PERCENT
1210      && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSpace)) {
1211      return this.itemSpace;
1212    }
1213    return this.theme.itemSpace;
1214  }
1215
1216  private getItemMinHeight(): Length | undefined {
1217    if (this.itemMinHeight && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemMinHeight)) {
1218      return LengthMetricsUtils.getInstance().stringify(this.itemMinHeight);
1219    }
1220    return this.theme.itemMinHeight;
1221  }
1222
1223  private getItemBackgroundColor(repeatItem: RepeatItem<SegmentButtonV2Item>): ResourceColor {
1224    if (this.isSelected(repeatItem)) {
1225      return this.itemSelectedBackgroundColor?.color ?? this.theme.itemSelectedBackgroundColor;
1226    }
1227    return this.itemBackgroundColor?.color ?? this.theme.itemBackgroundColor;
1228  }
1229
1230  private isSelected(repeatItem: RepeatItem<SegmentButtonV2Item>): boolean {
1231    const selectedIndexes = this.selectedIndexes ?? [];
1232    return selectedIndexes.includes(repeatItem.index);
1233  }
1234
1235  private getItemButtonBorderRadius(repeatItem: RepeatItem<SegmentButtonV2Item>): LocalizedBorderRadiuses {
1236    const items = this.getItems();
1237    const noneBorderRadius: LengthMetrics = LengthMetrics.vp(0);
1238    const borderRadiuses: LocalizedBorderRadiuses = {
1239      topStart: noneBorderRadius,
1240      bottomStart: noneBorderRadius,
1241      topEnd: noneBorderRadius,
1242      bottomEnd: noneBorderRadius,
1243    };
1244    if (repeatItem.index === 0) {
1245      const borderRadius: LengthMetrics = this.itemBorderRadius ?? LengthMetrics.resource(this.theme.itemBorderRadius);
1246      borderRadiuses.topStart = borderRadius;
1247      borderRadiuses.bottomStart = borderRadius;
1248
1249    }
1250    if (repeatItem.index === items.length - 1) {
1251      const borderRadius: LengthMetrics = this.itemBorderRadius ?? LengthMetrics.resource(this.theme.itemBorderRadius);
1252      borderRadiuses.topEnd = borderRadius;
1253      borderRadiuses.bottomEnd = borderRadius;
1254    }
1255    return borderRadiuses;
1256  }
1257}
1258
1259@ComponentV2
1260struct SegmentButtonV2ItemContent {
1261  @Require
1262  @Param
1263  hasHybrid: boolean;
1264  @Require
1265  @Param
1266  item: SegmentButtonV2Item;
1267  @Require
1268  @Param
1269  selected: boolean;
1270  @Require
1271  @Param
1272  theme: SegmentButtonV2ContentTheme;
1273  @Require
1274  @Param
1275  itemMinFontScale?: number | Resource = undefined;
1276  @Require
1277  @Param
1278  itemMaxFontScale?: number | Resource = undefined;
1279  @Require
1280  @Param
1281  itemFontColor?: ColorMetrics = undefined;
1282  @Require
1283  @Param
1284  itemSelectedFontColor?: ColorMetrics = undefined;
1285  @Require
1286  @Param
1287  itemFontSize?: LengthMetrics = undefined;
1288  @Require
1289  @Param
1290  itemSelectedFontSize?: LengthMetrics = undefined;
1291  @Require
1292  @Param
1293  itemFontWeight?: FontWeight = undefined;
1294  @Require
1295  @Param
1296  itemSelectedFontWeight?: FontWeight = undefined;
1297  @Require
1298  @Param
1299  itemIconSize?: SizeT<LengthMetrics> = undefined;
1300  @Require
1301  @Param
1302  itemIconFillColor?: ColorMetrics = undefined;
1303  @Require
1304  @Param
1305  itemSelectedIconFillColor?: ColorMetrics = undefined;
1306  @Require
1307  @Param
1308  itemSymbolFontSize?: LengthMetrics = undefined;
1309  @Require
1310  @Param
1311  itemSymbolFontColor?: ColorMetrics = undefined;
1312  @Require
1313  @Param
1314  itemSelectedSymbolFontColor?: ColorMetrics = undefined;
1315  @Require
1316  @Param
1317  itemMinHeight?: LengthMetrics = undefined;
1318  @Require
1319  @Param
1320  itemPadding?: LocalizedPadding = undefined;
1321  @Require
1322  @Param
1323  languageDirection?: Direction = undefined;
1324
1325  build() {
1326    Column({ space: 2 }) {
1327      if (this.item.symbol || this.item.symbolModifier) {
1328        SymbolGlyph(this.item.symbol)
1329          .fontSize(this.getSymbolFontSize())
1330          .fontColor([this.getItemSymbolFillColor()])
1331          .direction(this.languageDirection)
1332          .attributeModifier(this.item.symbolModifier)
1333      } else if (this.item.icon) {
1334        Image(this.item.icon)
1335          .fillColor(this.getItemIconFillColor())
1336          .width(this.getItemIconWidth())
1337          .height(this.getItemIconHeight())
1338          .direction(this.languageDirection)
1339          .draggable(false)
1340          .attributeModifier(this.item.iconModifier)
1341      }
1342
1343      if (this.item.text) {
1344        Text(this.item.text)
1345          .direction(this.languageDirection)
1346          .fontSize(this.getItemFontSize())
1347          .fontColor(this.getItemFontColor())
1348          .fontWeight(this.getItemFontWeight())
1349          .textOverflow({ overflow: TextOverflow.Ellipsis })
1350          .maxLines(1)
1351          .maxFontScale(this.getItemMaxFontScale())
1352          .minFontScale(this.getItemMinFontScale())
1353          .attributeModifier(this.item.textModifier)
1354      }
1355    }
1356    .constraintSize({ minHeight: this.getItemMinHeight(), minWidth: '100%' })
1357    .direction(this.languageDirection)
1358    .justifyContent(FlexAlign.Center)
1359    .padding(this.getItemPadding())
1360  }
1361
1362  private getItemFontWeight(): string | number | FontWeight {
1363    if (this.selected) {
1364      return this.itemSelectedFontWeight ?? this.theme.itemSelectedFontWeight;
1365    }
1366    return this.itemFontWeight ?? this.theme.itemFontWeight;
1367  }
1368
1369  getItemSymbolFillColor(): ResourceColor {
1370    if (this.selected) {
1371      return this.itemSelectedSymbolFontColor?.color ?? this.theme.itemSelectedSymbolFontColor;
1372    }
1373    return this.itemSymbolFontColor?.color ?? this.theme.itemSymbolFontColor;
1374  }
1375
1376  private getSymbolFontSize(): Dimension {
1377    if (this.itemSymbolFontSize && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSymbolFontSize) &&
1378      this.itemSymbolFontSize.unit !== LengthUnit.PERCENT) {
1379      return LengthMetricsUtils.getInstance().stringify(this.itemSymbolFontSize);
1380    }
1381    return this.theme.itemSymbolFontSize;
1382  }
1383
1384  private getItemMaxFontScale() {
1385    if (typeof this.itemMaxFontScale === 'number') {
1386      return normalize(this.itemMaxFontScale, this.theme.itemMaxFontScaleSmallest, this.theme.itemMaxFontScaleLargest);
1387    }
1388    if (typeof this.itemMaxFontScale === 'object') {
1389      const itemMaxFontScale: number =
1390        parseNumericResource(this.getUIContext(), this.itemMaxFontScale) ?? SMALLEST_MAX_FONT_SCALE;
1391      return normalize(itemMaxFontScale, this.theme.itemMaxFontScaleSmallest, this.theme.itemMaxFontScaleLargest);
1392    }
1393    return SMALLEST_MAX_FONT_SCALE;
1394  }
1395
1396  private getItemMinFontScale() {
1397    if (typeof this.itemMinFontScale === 'number') {
1398      return normalize(this.itemMinFontScale, this.theme.itemMinFontScaleSmallest, this.theme.itemMinFontScaleLargest);
1399    }
1400    if (typeof this.itemMinFontScale === 'object') {
1401      const itemMinFontScale =
1402        parseNumericResource(this.getUIContext(), this.itemMinFontScale) ?? SMALLEST_MIN_FONT_SCALE;
1403      return normalize(itemMinFontScale, this.theme.itemMinFontScaleSmallest, this.theme.itemMinFontScaleLargest);
1404    }
1405    return SMALLEST_MIN_FONT_SCALE;
1406  }
1407
1408  private getItemPadding(): LocalizedPadding | Length | Padding {
1409    const itemPadding: LocalizedPadding = {
1410      top: this.theme.itemPadding.top,
1411      bottom: this.theme.itemPadding.bottom,
1412      start: this.theme.itemPadding.start,
1413      end: this.theme.itemPadding.end,
1414    };
1415
1416    if (this.itemPadding?.top && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.top)) {
1417      itemPadding.top = this.itemPadding.top;
1418    }
1419
1420    if (this.itemPadding?.bottom && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.bottom)) {
1421      itemPadding.bottom = this.itemPadding.bottom;
1422    }
1423
1424    if (this.itemPadding?.start && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.start)) {
1425      itemPadding.start = this.itemPadding.start;
1426    }
1427
1428    if (this.itemPadding?.end && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.end)) {
1429      itemPadding.end = this.itemPadding.end;
1430    }
1431
1432    return itemPadding;
1433  }
1434
1435  private getItemMinHeight(): Length | undefined {
1436    if (this.itemMinHeight && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemMinHeight)) {
1437      return LengthMetricsUtils.getInstance().stringify(this.itemMinHeight);
1438    }
1439    return this.hasHybrid ? this.theme.hybridItemMinHeight : this.theme.itemMinHeight;
1440  }
1441
1442  private getItemFontColor(): ResourceColor {
1443    if (this.selected) {
1444      if (this.itemSelectedFontColor) {
1445        return this.itemSelectedFontColor.color;
1446      }
1447      return this.theme.itemSelectedFontColor;
1448    }
1449    if (this.itemFontColor) {
1450      return this.itemFontColor.color;
1451    }
1452    return this.theme.itemFontColor;
1453  }
1454
1455  private getItemFontSize(): Length {
1456    if (this.selected) {
1457      if (this.itemSelectedFontSize && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSelectedFontSize) &&
1458        this.itemSelectedFontSize.unit !== LengthUnit.PERCENT) {
1459        return LengthMetricsUtils.getInstance().stringify(this.itemSelectedFontSize);
1460      }
1461      return this.theme.itemFontSize;
1462    }
1463    if (this.itemFontSize && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemFontSize) &&
1464      this.itemFontSize.unit !== LengthUnit.PERCENT) {
1465      return LengthMetricsUtils.getInstance().stringify(this.itemFontSize);
1466    }
1467    return this.theme.itemFontSize;
1468  }
1469
1470  private getItemIconHeight(): Length {
1471    if (this.itemIconSize?.height && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemIconSize.height)) {
1472      return LengthMetricsUtils.getInstance().stringify(this.itemIconSize.height);
1473    }
1474    return this.theme.itemIconSize;
1475  }
1476
1477  private getItemIconWidth(): Length {
1478    if (this.itemIconSize?.width && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemIconSize.width)) {
1479      return LengthMetricsUtils.getInstance().stringify(this.itemIconSize.width);
1480    }
1481    return this.theme.itemIconSize;
1482  }
1483
1484  private getItemIconFillColor(): ResourceColor {
1485    if (this.selected) {
1486      if (this.itemSelectedIconFillColor) {
1487        return this.itemSelectedIconFillColor.color;
1488      }
1489      return this.theme.itemSelectedIconFillColor;
1490    }
1491    if (this.itemIconFillColor) {
1492      return this.itemIconFillColor.color;
1493    }
1494    return this.theme.itemIconFillColor;
1495  }
1496}
1497
1498class LengthMetricsUtils {
1499  private static instance?: LengthMetricsUtils;
1500
1501  private constructor() {
1502  }
1503
1504  public static getInstance(): LengthMetricsUtils {
1505    if (!LengthMetricsUtils.instance) {
1506      LengthMetricsUtils.instance = new LengthMetricsUtils();
1507    }
1508    return LengthMetricsUtils.instance;
1509  }
1510
1511  stringify(metrics: LengthMetrics): Dimension {
1512    switch (metrics.unit) {
1513      case LengthUnit.PX:
1514        return `${metrics.value}px`;
1515      case LengthUnit.VP:
1516        return `${metrics.value}vp`;
1517      case LengthUnit.FP:
1518        return `${metrics.value}fp`;
1519      case LengthUnit.PERCENT:
1520        return `${metrics.value}%`;
1521      case LengthUnit.LPX:
1522        return `${metrics.value}lpx`;
1523    }
1524  }
1525
1526  isNaturalNumber(metrics: LengthMetrics): boolean {
1527    return metrics.value >= 0;
1528  }
1529}
1530
1531function parseNumericResource(context: UIContext, resource: Resource): number | undefined {
1532  const resourceManager = context.getHostContext()?.resourceManager;
1533  if (!resourceManager) {
1534    return undefined;
1535  }
1536  try {
1537    return resourceManager.getNumber(resource);
1538  } catch (err) {
1539    // todo log err
1540    return undefined;
1541  }
1542}
1543
1544function normalize(value: number, min: number, max: number): number {
1545  return Math.min(Math.max(value, min), max);
1546}
1547
1548function generateUniqueKye(groupId: string) {
1549  return (item: SegmentButtonV2Item, index: number): string => {
1550    let key = groupId;
1551    if (item.text) {
1552      if (typeof item.text === 'string') {
1553        key += item.text;
1554      } else {
1555        key += getResourceUniqueId(item.text);
1556      }
1557    }
1558    if (item.icon) {
1559      if (typeof item.icon === 'string') {
1560        key += item.icon;
1561      } else {
1562        key += getResourceUniqueId(item.icon);
1563      }
1564    }
1565    if (item.symbol) {
1566      key += getResourceUniqueId(item.symbol);
1567    }
1568    return key;
1569  }
1570}
1571
1572function getResourceUniqueId(resource: Resource): string {
1573  if (resource.id !== -1) {
1574    return `${resource.id}`;
1575  } else {
1576    return JSON.stringify(resource);
1577  }
1578}
1579
1580class GroupIdGenerator {
1581  private static instance: GroupIdGenerator | null = null;
1582  private id: number = 0;
1583
1584  private constructor() {
1585  }
1586
1587  public static getInstance(): GroupIdGenerator {
1588    if (!GroupIdGenerator.instance) {
1589      GroupIdGenerator.instance = new GroupIdGenerator();
1590    }
1591    return GroupIdGenerator.instance;
1592  }
1593
1594  public generate(): string {
1595    return util.generateRandomUUID() || `SegmentButton-${this.id++}`;
1596  }
1597}
1598