• 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 = util.generateRandomUUID();
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      .clip(false)
812      .direction(this.languageDirection)
813    }
814    .backgroundColor(this.getButtonBackgroundColor())
815    .backgroundEffect(this.buttonBackgroundEffect)
816    .borderRadius(this.getButtonBorderRadius())
817    .clip(false)
818    .constraintSize({
819      minWidth: '100%',
820      minHeight: this.getButtonMinHeight()
821    })
822    .direction(this.languageDirection)
823  }
824
825  private getItems(): SegmentButtonV2Items {
826    return this.items ?? EMPTY_ITEMS;
827  }
828
829  private getItemBackplateShadow(): ShadowOptions | ShadowStyle | undefined {
830    return this.itemShadow ?? this.theme.itemShadow
831  }
832
833  private getButtonBackgroundBlurStyle(): BlurStyle | undefined {
834    if (this.buttonBackgroundEffect) {
835      return undefined;
836    }
837    return this.buttonBackgroundBlurStyle;
838  }
839
840  private getButtonBackgroundBlurStyleOptions(): BackgroundBlurStyleOptions | undefined {
841    if (this.buttonBackgroundEffect) {
842      return undefined;
843    }
844    return this.buttonBackgroundBlurStyleOptions;
845  }
846
847  private getItemScale(index: number): ScaleOptions {
848    const pressed: boolean = this.isPressed(index);
849    const scale: number = pressed ? 0.95 : 1;
850    return { x: scale, y: scale, };
851  }
852
853  private isPressed(index: number): boolean {
854    return this.mousePressedItemIndex === index;
855  }
856
857  private updateHoveredItemIndex(index: number) {
858    if (index === this.hoveredItemIndex) {
859      return;
860    }
861    animateTo({ duration: 250, curve: Curve.Friction }, () => {
862      this.hoveredItemIndex = index;
863    });
864  }
865
866  private updateMousePressedItemIndex(index: number) {
867    if (index === this.mousePressedItemIndex) {
868      return;
869    }
870    animateTo({ duration: 250, curve: Curve.Friction }, () => {
871      this.mousePressedItemIndex = index;
872    });
873  }
874
875  private updateTouchPressedItemIndex(index: number) {
876    if (index === this.touchPressedItemIndex) {
877      return;
878    }
879    animateTo({ duration: 250, curve: Curve.Friction }, () => {
880      this.touchPressedItemIndex = index;
881    });
882  }
883
884  private isRTL(): boolean {
885    if (this.languageDirection || this.languageDirection === Direction.Auto) {
886      return i18n.isRTL(i18n.System.getSystemLanguage());
887    }
888    return this.languageDirection === Direction.Rtl;
889  }
890
891  private getEffectBackgroundColor(repeatItem: RepeatItem<SegmentButtonV2ItemRect>): ResourceColor {
892    if (repeatItem.index === this.mousePressedItemIndex) {
893      return $r('sys.color.interactive_click');
894    }
895    if (repeatItem.index === this.hoveredItemIndex) {
896      return $r('sys.color.interactive_hover');
897    }
898    return Color.Transparent;
899  }
900
901  private getItemBorderRadius(): Length | BorderRadiuses | LocalizedBorderRadiuses {
902    if (this.itemBorderRadius && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemBorderRadius)) {
903      return LengthMetricsUtils.getInstance().stringify(this.itemBorderRadius);
904    }
905    return this.theme.itemBorderRadius;
906
907  }
908
909  private getItemSelectedBackgroundColor(): ResourceColor {
910    if (this.itemSelectedBackgroundColor) {
911      return this.itemSelectedBackgroundColor.color;
912    }
913    return this.theme.itemSelectedBackgroundColor;
914  }
915
916  getItemSpace(): LengthMetrics {
917    if (this.itemSpace && this.itemSpace.unit !== LengthUnit.PERCENT
918      && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSpace)) {
919      return this.itemSpace;
920    }
921    return this.theme.itemSpace;
922  }
923
924  getIndexByPosition(globalX: number, globalY: number): number {
925    let index = 0;
926    while (index < this.itemRects.length) {
927      const rect = this.itemRects[index];
928      if (this.isPointOnRect(globalX, globalY, rect)) {
929        return index;
930      }
931      ++index;
932    }
933    return -1;
934  }
935
936  private isPointOnRect(globalX: number, globalY: number, rect: SegmentButtonV2ItemRect): boolean {
937    return globalX >= rect.globalPosition.x && globalX <= rect.globalPosition.x + rect.size.width &&
938      globalY >= rect.globalPosition.y && globalY <= rect.globalPosition.y + rect.size.height;
939  }
940
941  private updateSelectedIndex(selectedIndex: number) {
942    if (!this.isItemEnabled(selectedIndex) || selectedIndex === this.selectedIndex
943    ) {
944      return;
945    }
946    this.getUIContext().animateTo({ curve: curves.springMotion(0.347, 0.99) }, () => {
947      this.$selectedIndex?.(selectedIndex);
948    });
949  }
950
951  private updateItemScale(scale: number) {
952    if (this.itemScale === scale) {
953      return;
954    }
955    this.getUIContext().animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => {
956      this.itemScale = scale;
957    });
958  }
959
960  private getItemAccessibilityDescription(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined {
961    return repeatItem.item.accessibilityDescription as ESObject as string;
962  }
963
964  private getItemAccessibilityText(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined {
965    return repeatItem.item.accessibilityText as ESObject as string;
966  }
967
968  private isSelected(repeatItem: RepeatItem<SegmentButtonV2Item>): boolean | undefined {
969    return repeatItem.index === this.normalizedSelectedIndex;
970  }
971
972  private getButtonPadding(): Length | Padding | LocalizedPadding {
973    if (this.buttonPadding && LengthMetricsUtils.getInstance().isNaturalNumber(this.buttonPadding)) {
974      return LengthMetricsUtils.getInstance().stringify(this.buttonPadding);
975    }
976    return this.theme.buttonPadding;
977  }
978
979  private getButtonBorderRadius(): Length | BorderRadiuses | LocalizedBorderRadiuses {
980    if (this.buttonBorderRadius && LengthMetricsUtils.getInstance().isNaturalNumber(this.buttonBorderRadius)) {
981      return LengthMetricsUtils.getInstance().stringify(this.buttonBorderRadius);
982    }
983    return this.theme.buttonBorderRadius;
984  }
985
986  private getButtonBackgroundColor(): ResourceColor {
987    if (this.buttonBackgroundColor) {
988      return this.buttonBackgroundColor.color;
989    }
990    return this.theme.buttonBackgroundColor;
991  }
992
993  private getButtonMinHeight(): Dimension {
994    if (this.buttonMinHeight && LengthMetricsUtils.getInstance().isNaturalNumber(this.buttonMinHeight)) {
995      return LengthMetricsUtils.getInstance().stringify(this.buttonMinHeight);
996    }
997    const items = this.getItems();
998    return items.hasHybrid ? this.theme.hybridButtonMinHeight : this.theme.buttonMinHeight;
999  }
1000}
1001
1002interface MultiplySegmentButtonV2Theme extends SegmentButtonV2ContentTheme {
1003  itemBackgroundColor: ResourceColor;
1004  itemSelectedBackgroundColor: ResourceColor;
1005  itemBorderRadius: Resource;
1006}
1007
1008const multiplyCapsuleTheme: MultiplySegmentButtonV2Theme = {
1009  itemBorderRadius: $r('sys.float.segment_button_v2_multi_corner_radius'),
1010  itemBackgroundColor: $r('sys.color.segment_button_v2_multi_capsule_button_background'),
1011  itemSelectedBackgroundColor: $r('sys.color.comp_background_emphasize'),
1012  itemSpace: LengthMetrics.vp(1),
1013  itemFontColor: $r('sys.color.font_secondary'),
1014  itemSelectedFontColor: $r('sys.color.font_on_primary'),
1015  itemFontWeight: FontWeight.Medium,
1016  itemSelectedFontWeight: FontWeight.Medium,
1017  itemIconFillColor: $r('sys.color.icon_secondary'),
1018  itemSelectedIconFillColor: $r('sys.color.font_on_primary'),
1019  itemSymbolFontColor: $r('sys.color.font_secondary'),
1020  itemSelectedSymbolFontColor: $r('sys.color.font_on_primary'),
1021  itemFontSize: $r('sys.float.ohos_id_text_size_button2'),
1022  itemIconSize: 24,
1023  itemSymbolFontSize: 20,
1024  itemPadding: {
1025    top: LengthMetrics.resource($r('sys.float.padding_level2')),
1026    bottom: LengthMetrics.resource($r('sys.float.padding_level2')),
1027    start: LengthMetrics.resource($r('sys.float.padding_level4')),
1028    end: LengthMetrics.resource($r('sys.float.padding_level4')),
1029  },
1030  itemMinHeight: $r('sys.float.segment_button_v2_multi_singleline_height'),
1031  hybridItemMinHeight: $r('sys.float.segment_button_v2_multi_doubleline_height'),
1032  itemMaxFontScale: SMALLEST_MAX_FONT_SCALE,
1033  itemMaxFontScaleSmallest: SMALLEST_MAX_FONT_SCALE,
1034  itemMaxFontScaleLargest: LARGEST_MAX_FONT_SCALE,
1035  itemMinFontScale: SMALLEST_MIN_FONT_SCALE,
1036  itemMinFontScaleSmallest: SMALLEST_MIN_FONT_SCALE,
1037  itemMinFontScaleLargest: LARGEST_MIN_FONT_SCALE,
1038};
1039
1040@ComponentV2
1041export struct MultiCapsuleSegmentButtonV2 {
1042  @Require
1043  @Param
1044  items: SegmentButtonV2Items;
1045  @Require
1046  @Param
1047  selectedIndexes: number[];
1048  @Event
1049  $selectedIndexes: OnSelectedIndexesChange;
1050  @Event
1051  onItemClicked?: Callback<number>;
1052  @Param
1053  itemMinFontScale?: number | Resource = undefined;
1054  @Param
1055  itemMaxFontScale?: number | Resource = undefined;
1056  @Param
1057  itemSpace?: LengthMetrics = undefined;
1058  @Param
1059  itemFontColor?: ColorMetrics = undefined;
1060  @Param
1061  itemSelectedFontColor?: ColorMetrics = undefined;
1062  @Param
1063  itemFontSize?: LengthMetrics = undefined;
1064  @Param
1065  itemSelectedFontSize?: LengthMetrics = undefined;
1066  @Param
1067  itemFontWeight?: FontWeight = undefined;
1068  @Param
1069  itemSelectedFontWeight?: FontWeight = undefined;
1070  @Param
1071  itemBorderRadius?: LengthMetrics = undefined;
1072  @Param
1073  itemBackgroundColor?: ColorMetrics = undefined;
1074  @Param
1075  itemBackgroundEffect?: BackgroundEffectOptions = undefined;
1076  @Param
1077  itemBackgroundBlurStyle?: BlurStyle = undefined;
1078  @Param
1079  itemBackgroundBlurStyleOptions?: BackgroundBlurStyleOptions = undefined;
1080  @Param
1081  itemSelectedBackgroundColor?: ColorMetrics = undefined;
1082  @Param
1083  itemIconSize?: SizeT<LengthMetrics> = undefined;
1084  @Param
1085  itemIconFillColor?: ColorMetrics = undefined;
1086  @Param
1087  itemSelectedIconFillColor?: ColorMetrics = undefined;
1088  @Param
1089  itemSymbolFontSize?: LengthMetrics = undefined;
1090  @Param
1091  itemSymbolFontColor?: ColorMetrics = undefined;
1092  @Param
1093  itemSelectedSymbolFontColor?: ColorMetrics = undefined;
1094  @Param
1095  itemMinHeight?: LengthMetrics = undefined;
1096  @Param
1097  itemPadding?: LocalizedPadding = undefined;
1098  @Param
1099  languageDirection?: Direction = undefined;
1100  private theme: MultiplySegmentButtonV2Theme = multiplyCapsuleTheme;
1101  private focusGroupId: string = util.generateRandomUUID();
1102
1103  build() {
1104    Flex({ alignItems: ItemAlign.Stretch, space: { main: this.getItemSpace() } }) {
1105      Repeat(this.getItems())
1106        .each((repeatItem: RepeatItem<SegmentButtonV2Item>) => {
1107          Button({ type: ButtonType.Normal }) {
1108            SegmentButtonV2ItemContent({
1109              theme: this.theme,
1110              item: repeatItem.item,
1111              selected: this.isSelected(repeatItem),
1112              hasHybrid: this.getItems().hasHybrid,
1113              itemMinFontScale: this.itemMinFontScale,
1114              itemMaxFontScale: this.itemMaxFontScale,
1115              itemFontColor: this.itemFontColor,
1116              itemSelectedFontColor: this.itemSelectedFontColor,
1117              itemFontSize: this.itemFontSize,
1118              itemSelectedFontSize: this.itemSelectedFontSize,
1119              itemFontWeight: this.itemFontWeight,
1120              itemSelectedFontWeight: this.itemSelectedFontWeight,
1121              itemIconSize: this.itemIconSize,
1122              itemIconFillColor: this.itemIconFillColor,
1123              itemSelectedIconFillColor: this.itemSelectedIconFillColor,
1124              itemSymbolFontSize: this.itemSymbolFontSize,
1125              itemSymbolFontColor: this.itemSymbolFontColor,
1126              itemSelectedSymbolFontColor: this.itemSelectedSymbolFontColor,
1127              itemMinHeight: this.itemMinHeight,
1128              itemPadding: this.itemPadding,
1129              languageDirection: this.languageDirection,
1130            })
1131              .borderRadius(this.getItemButtonBorderRadius(repeatItem))
1132              .backgroundBlurStyle(this.getItemBackgroundBlurStyle(), this.getItemBackgroundBlurStyleOptions())
1133              .direction(this.languageDirection)
1134          }
1135          .accessibilityGroup(true)
1136          .accessibilityChecked(this.isSelected(repeatItem))
1137          .accessibilityText(this.getItemAccessibilityText(repeatItem))
1138          .accessibilityDescription(this.getItemAccessibilityDescription(repeatItem))
1139          .accessibilityLevel(repeatItem.item.accessibilityLevel)
1140          .backgroundColor(this.getItemBackgroundColor(repeatItem))
1141          .backgroundEffect(this.itemBackgroundEffect)
1142          .borderRadius(this.getItemButtonBorderRadius(repeatItem))
1143          .constraintSize({ minHeight: this.getItemMinHeight() })
1144          .direction(this.languageDirection)
1145          .enabled(repeatItem.item.enabled)
1146          .focusScopePriority(this.focusGroupId, this.getFocusPriority(repeatItem))
1147          .layoutWeight(1)
1148          .padding(0)
1149          .onClick(() => {
1150            this.onItemClicked?.(repeatItem.index);
1151            let selection: number[];
1152            const items = this.getItems();
1153            const selectedIndexes = this.selectedIndexes ?? [];
1154            if (this.isSelected(repeatItem)) {
1155              selection = selectedIndexes.filter((index) => {
1156                if (index < 0 || index > items.length - 1) {
1157                  return false;
1158                }
1159                return index !== repeatItem.index;
1160              });
1161            } else {
1162              selection = selectedIndexes
1163                .filter((index) => index >= 0 && index <= items.length - 1)
1164                .concat(repeatItem.index);
1165            }
1166            this.$selectedIndexes(selection);
1167          })
1168        })
1169        .key(generateUniqueKye(this.focusGroupId))
1170    }
1171    .clip(false)
1172    .direction(this.languageDirection)
1173    .focusScopeId(this.focusGroupId, true)
1174  }
1175
1176  private getFocusPriority(repeatItem: RepeatItem<SegmentButtonV2Item>): FocusPriority | undefined {
1177    return Math.min(...this.selectedIndexes) === repeatItem.index ? FocusPriority.PREVIOUS : FocusPriority.AUTO;
1178  }
1179
1180  getItems(): SegmentButtonV2Items {
1181    return this.items ?? EMPTY_ITEMS;
1182  }
1183
1184  getItemBackgroundBlurStyleOptions(): BackgroundBlurStyleOptions | undefined {
1185    if (this.itemBackgroundEffect) {
1186      return undefined;
1187    }
1188    return this.itemBackgroundBlurStyleOptions;
1189  }
1190
1191  getItemBackgroundBlurStyle(): BlurStyle | undefined {
1192    if (this.itemBackgroundEffect) {
1193      return undefined;
1194    }
1195    return this.itemBackgroundBlurStyle;
1196  }
1197
1198  private getItemAccessibilityDescription(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined {
1199    return repeatItem.item.accessibilityDescription as ESObject as string;
1200  }
1201
1202  private getItemAccessibilityText(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined {
1203    return repeatItem.item.accessibilityText as ESObject as string;
1204  }
1205
1206  private getItemSpace(): LengthMetrics {
1207    if (this.itemSpace && this.itemSpace.unit !== LengthUnit.PERCENT
1208      && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSpace)) {
1209      return this.itemSpace;
1210    }
1211    return this.theme.itemSpace;
1212  }
1213
1214  private getItemMinHeight(): Length | undefined {
1215    if (this.itemMinHeight && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemMinHeight)) {
1216      return LengthMetricsUtils.getInstance().stringify(this.itemMinHeight);
1217    }
1218    return this.theme.itemMinHeight;
1219  }
1220
1221  private getItemBackgroundColor(repeatItem: RepeatItem<SegmentButtonV2Item>): ResourceColor {
1222    if (this.isSelected(repeatItem)) {
1223      return this.itemSelectedBackgroundColor?.color ?? this.theme.itemSelectedBackgroundColor;
1224    }
1225    return this.itemBackgroundColor?.color ?? this.theme.itemBackgroundColor;
1226  }
1227
1228  private isSelected(repeatItem: RepeatItem<SegmentButtonV2Item>): boolean {
1229    const selectedIndexes = this.selectedIndexes ?? [];
1230    return selectedIndexes.includes(repeatItem.index);
1231  }
1232
1233  private getItemButtonBorderRadius(repeatItem: RepeatItem<SegmentButtonV2Item>): LocalizedBorderRadiuses {
1234    const items = this.getItems();
1235    const noneBorderRadius: LengthMetrics = LengthMetrics.vp(0);
1236    const borderRadiuses: LocalizedBorderRadiuses = {
1237      topStart: noneBorderRadius,
1238      bottomStart: noneBorderRadius,
1239      topEnd: noneBorderRadius,
1240      bottomEnd: noneBorderRadius,
1241    };
1242    if (repeatItem.index === 0) {
1243      const borderRadius: LengthMetrics = this.itemBorderRadius ?? LengthMetrics.resource(this.theme.itemBorderRadius);
1244      borderRadiuses.topStart = borderRadius;
1245      borderRadiuses.bottomStart = borderRadius;
1246
1247    }
1248    if (repeatItem.index === items.length - 1) {
1249      const borderRadius: LengthMetrics = this.itemBorderRadius ?? LengthMetrics.resource(this.theme.itemBorderRadius);
1250      borderRadiuses.topEnd = borderRadius;
1251      borderRadiuses.bottomEnd = borderRadius;
1252    }
1253    return borderRadiuses;
1254  }
1255}
1256
1257@ComponentV2
1258struct SegmentButtonV2ItemContent {
1259  @Require
1260  @Param
1261  hasHybrid: boolean;
1262  @Require
1263  @Param
1264  item: SegmentButtonV2Item;
1265  @Require
1266  @Param
1267  selected: boolean;
1268  @Require
1269  @Param
1270  theme: SegmentButtonV2ContentTheme;
1271  @Require
1272  @Param
1273  itemMinFontScale?: number | Resource = undefined;
1274  @Require
1275  @Param
1276  itemMaxFontScale?: number | Resource = undefined;
1277  @Require
1278  @Param
1279  itemFontColor?: ColorMetrics = undefined;
1280  @Require
1281  @Param
1282  itemSelectedFontColor?: ColorMetrics = undefined;
1283  @Require
1284  @Param
1285  itemFontSize?: LengthMetrics = undefined;
1286  @Require
1287  @Param
1288  itemSelectedFontSize?: LengthMetrics = undefined;
1289  @Require
1290  @Param
1291  itemFontWeight?: FontWeight = undefined;
1292  @Require
1293  @Param
1294  itemSelectedFontWeight?: FontWeight = undefined;
1295  @Require
1296  @Param
1297  itemIconSize?: SizeT<LengthMetrics> = undefined;
1298  @Require
1299  @Param
1300  itemIconFillColor?: ColorMetrics = undefined;
1301  @Require
1302  @Param
1303  itemSelectedIconFillColor?: ColorMetrics = undefined;
1304  @Require
1305  @Param
1306  itemSymbolFontSize?: LengthMetrics = undefined;
1307  @Require
1308  @Param
1309  itemSymbolFontColor?: ColorMetrics = undefined;
1310  @Require
1311  @Param
1312  itemSelectedSymbolFontColor?: ColorMetrics = undefined;
1313  @Require
1314  @Param
1315  itemMinHeight?: LengthMetrics = undefined;
1316  @Require
1317  @Param
1318  itemPadding?: LocalizedPadding = undefined;
1319  @Require
1320  @Param
1321  languageDirection?: Direction = undefined;
1322
1323  build() {
1324    Column({ space: 2 }) {
1325      if (this.item.symbol || this.item.symbolModifier) {
1326        SymbolGlyph(this.item.symbol)
1327          .fontSize(this.getSymbolFontSize())
1328          .fontColor([this.getItemSymbolFillColor()])
1329          .direction(this.languageDirection)
1330          .attributeModifier(this.item.symbolModifier)
1331      } else if (this.item.icon) {
1332        Image(this.item.icon)
1333          .fillColor(this.getItemIconFillColor())
1334          .width(this.getItemIconWidth())
1335          .height(this.getItemIconHeight())
1336          .direction(this.languageDirection)
1337          .draggable(false)
1338          .attributeModifier(this.item.iconModifier)
1339      }
1340
1341      if (this.item.text) {
1342        Text(this.item.text)
1343          .direction(this.languageDirection)
1344          .fontSize(this.getItemFontSize())
1345          .fontColor(this.getItemFontColor())
1346          .fontWeight(this.getItemFontWeight())
1347          .textOverflow({ overflow: TextOverflow.Ellipsis })
1348          .maxLines(1)
1349          .maxFontScale(this.getItemMaxFontScale())
1350          .minFontScale(this.getItemMinFontScale())
1351          .attributeModifier(this.item.textModifier)
1352      }
1353    }
1354    .constraintSize({ minHeight: this.getItemMinHeight(), minWidth: '100%' })
1355    .direction(this.languageDirection)
1356    .justifyContent(FlexAlign.Center)
1357    .padding(this.getItemPadding())
1358  }
1359
1360  private getItemFontWeight(): string | number | FontWeight {
1361    if (this.selected) {
1362      return this.itemSelectedFontWeight ?? this.theme.itemSelectedFontWeight;
1363    }
1364    return this.itemFontWeight ?? this.theme.itemFontWeight;
1365  }
1366
1367  getItemSymbolFillColor(): ResourceColor {
1368    if (this.selected) {
1369      return this.itemSelectedSymbolFontColor?.color ?? this.theme.itemSelectedSymbolFontColor;
1370    }
1371    return this.itemSymbolFontColor?.color ?? this.theme.itemSymbolFontColor;
1372  }
1373
1374  private getSymbolFontSize(): Dimension {
1375    if (this.itemSymbolFontSize && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSymbolFontSize) && this.itemSymbolFontSize.unit !== LengthUnit.PERCENT) {
1376      return LengthMetricsUtils.getInstance().stringify(this.itemSymbolFontSize);
1377    }
1378    return this.theme.itemSymbolFontSize;
1379  }
1380
1381  private getItemMaxFontScale() {
1382    if (typeof this.itemMaxFontScale === 'number') {
1383      return normalize(this.itemMaxFontScale, this.theme.itemMaxFontScaleSmallest, this.theme.itemMaxFontScaleLargest);
1384    }
1385    if (typeof this.itemMaxFontScale === 'object') {
1386      const itemMaxFontScale: number =
1387        parseNumericResource(this.getUIContext(), this.itemMaxFontScale) ?? SMALLEST_MAX_FONT_SCALE;
1388      return normalize(itemMaxFontScale, this.theme.itemMaxFontScaleSmallest, this.theme.itemMaxFontScaleLargest);
1389    }
1390    return SMALLEST_MAX_FONT_SCALE;
1391  }
1392
1393  private getItemMinFontScale() {
1394    if (typeof this.itemMinFontScale === 'number') {
1395      return normalize(this.itemMinFontScale, this.theme.itemMinFontScaleSmallest, this.theme.itemMinFontScaleLargest);
1396    }
1397    if (typeof this.itemMinFontScale === 'object') {
1398      const itemMinFontScale =
1399        parseNumericResource(this.getUIContext(), this.itemMinFontScale) ?? SMALLEST_MIN_FONT_SCALE;
1400      return normalize(itemMinFontScale, this.theme.itemMinFontScaleSmallest, this.theme.itemMinFontScaleLargest);
1401    }
1402    return SMALLEST_MIN_FONT_SCALE;
1403  }
1404
1405  private getItemPadding(): LocalizedPadding | Length | Padding {
1406    const itemPadding: LocalizedPadding = {
1407      top: this.theme.itemPadding.top,
1408      bottom: this.theme.itemPadding.bottom,
1409      start: this.theme.itemPadding.start,
1410      end: this.theme.itemPadding.end,
1411    };
1412
1413    if (this.itemPadding?.top && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.top)) {
1414      itemPadding.top = this.itemPadding.top;
1415    }
1416
1417    if (this.itemPadding?.bottom && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.bottom)) {
1418      itemPadding.bottom = this.itemPadding.bottom;
1419    }
1420
1421    if (this.itemPadding?.start && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.start)) {
1422      itemPadding.start = this.itemPadding.start;
1423    }
1424
1425    if (this.itemPadding?.end && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.end)) {
1426      itemPadding.end = this.itemPadding.end;
1427    }
1428
1429    return itemPadding;
1430  }
1431
1432  private getItemMinHeight(): Length | undefined {
1433    if (this.itemMinHeight && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemMinHeight)) {
1434      return LengthMetricsUtils.getInstance().stringify(this.itemMinHeight);
1435    }
1436    return this.hasHybrid ? this.theme.hybridItemMinHeight : this.theme.itemMinHeight;
1437  }
1438
1439  private getItemFontColor(): ResourceColor {
1440    if (this.selected) {
1441      if (this.itemSelectedFontColor) {
1442        return this.itemSelectedFontColor.color;
1443      }
1444      return this.theme.itemSelectedFontColor;
1445    }
1446    if (this.itemFontColor) {
1447      return this.itemFontColor.color;
1448    }
1449    return this.theme.itemFontColor;
1450  }
1451
1452  private getItemFontSize(): Length {
1453    if (this.selected) {
1454      if (this.itemSelectedFontSize && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSelectedFontSize)  && this.itemSelectedFontSize.unit !== LengthUnit.PERCENT) {
1455        return LengthMetricsUtils.getInstance().stringify(this.itemSelectedFontSize);
1456      }
1457      return this.theme.itemFontSize;
1458    }
1459    if (this.itemFontSize && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemFontSize) && this.itemFontSize.unit !== LengthUnit.PERCENT) {
1460      return LengthMetricsUtils.getInstance().stringify(this.itemFontSize);
1461    }
1462    return this.theme.itemFontSize;
1463  }
1464
1465  private getItemIconHeight(): Length {
1466    if (this.itemIconSize?.height && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemIconSize.height)) {
1467      return LengthMetricsUtils.getInstance().stringify(this.itemIconSize.height);
1468    }
1469    return this.theme.itemIconSize;
1470  }
1471
1472  private getItemIconWidth(): Length {
1473    if (this.itemIconSize?.width && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemIconSize.width)) {
1474      return LengthMetricsUtils.getInstance().stringify(this.itemIconSize.width);
1475    }
1476    return this.theme.itemIconSize;
1477  }
1478
1479  private getItemIconFillColor(): ResourceColor {
1480    if (this.selected) {
1481      if (this.itemSelectedIconFillColor) {
1482        return this.itemSelectedIconFillColor.color;
1483      }
1484      return this.theme.itemSelectedIconFillColor;
1485    }
1486    if (this.itemIconFillColor) {
1487      return this.itemIconFillColor.color;
1488    }
1489    return this.theme.itemIconFillColor;
1490  }
1491}
1492
1493class LengthMetricsUtils {
1494  private static instance?: LengthMetricsUtils;
1495
1496  private constructor() {
1497  }
1498
1499  public static getInstance(): LengthMetricsUtils {
1500    if (!LengthMetricsUtils.instance) {
1501      LengthMetricsUtils.instance = new LengthMetricsUtils();
1502    }
1503    return LengthMetricsUtils.instance;
1504  }
1505
1506  stringify(metrics: LengthMetrics): Dimension {
1507    switch (metrics.unit) {
1508      case LengthUnit.PX:
1509        return `${metrics.value}px`;
1510      case LengthUnit.VP:
1511        return `${metrics.value}vp`;
1512      case LengthUnit.FP:
1513        return `${metrics.value}fp`;
1514      case LengthUnit.PERCENT:
1515        return `${metrics.value}%`;
1516      case LengthUnit.LPX:
1517        return `${metrics.value}lpx`;
1518    }
1519  }
1520
1521  isNaturalNumber(metrics: LengthMetrics): boolean {
1522    return metrics.value >= 0;
1523  }
1524}
1525
1526function parseNumericResource(context: UIContext, resource: Resource): number | undefined {
1527  const resourceManager = context.getHostContext()?.resourceManager;
1528  if (!resourceManager) {
1529    return undefined;
1530  }
1531  try {
1532    return resourceManager.getNumber(resource);
1533  } catch (err) {
1534    // todo log err
1535    return undefined;
1536  }
1537}
1538
1539function normalize(value: number, min: number, max: number): number {
1540  return Math.min(Math.max(value, min), max);
1541}
1542
1543function generateUniqueKye(groupId: string) {
1544  return (item: SegmentButtonV2Item, index: number): string => {
1545    let key = groupId;
1546    if (item.text) {
1547      if (typeof item.text === 'string') {
1548        key += item.text;
1549      } else {
1550        key += getResourceUniqueId(item.text);
1551      }
1552    }
1553    if (item.icon) {
1554      if (typeof item.icon === 'string') {
1555        key += item.icon;
1556      } else {
1557        key += getResourceUniqueId(item.icon);
1558      }
1559    }
1560    if (item.symbol) {
1561      key += getResourceUniqueId(item.symbol);
1562    }
1563    return key;
1564  }
1565}
1566
1567function getResourceUniqueId(resource: Resource): string {
1568  if (resource.id !== -1) {
1569    return `${resource.id}`;
1570  } else {
1571    return JSON.stringify(resource);
1572  }
1573}
1574