• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023-2024 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 */
15import curves from '@ohos.curves';
16import { KeyCode } from '@ohos.multimodalInput.keyCode';
17import util from '@ohos.util';
18import { LengthMetrics, LengthUnit } from '@ohos.arkui.node';
19import I18n from '@ohos.i18n';
20
21const MIN_ITEM_COUNT = 2
22const MAX_ITEM_COUNT = 5
23const DEFAULT_MAX_FONT_SCALE: number = 1
24const MAX_MAX_FONT_SCALE: number = 2
25const MIN_MAX_FONT_SCALE: number = 1
26const RESOURCE_TYPE_FLOAT = 10002;
27const RESOURCE_TYPE_INTEGER = 10007;
28const CAPSULE_FOCUS_SELECTED_OFFSET: number = 4;
29// Space character for selected accessibility description - prevents screen readers from announcing
30const ACCESSIBILITY_SELECTED_DESCRIPTION = ' ';
31const ACCESSIBILITY_DEFAULT_DESCRIPTION = '';
32
33interface SegmentButtonThemeInterface {
34  SEGMENT_TEXT_VERTICAL_PADDING: Resource;
35  SEGMENT_TEXT_HORIZONTAL_PADDING: Resource;
36  SEGMENT_TEXT_CAPSULE_VERTICAL_PADDING: Resource;
37  SEGMENT_BUTTON_FOCUS_CUSTOMIZED_BG_COLOR: ResourceColor;
38  FONT_COLOR: ResourceColor,
39  TAB_SELECTED_FONT_COLOR: ResourceColor,
40  CAPSULE_SELECTED_FONT_COLOR: ResourceColor,
41  FONT_SIZE: DimensionNoPercentage,
42  SELECTED_FONT_SIZE: DimensionNoPercentage,
43  BACKGROUND_COLOR: ResourceColor,
44  TAB_SELECTED_BACKGROUND_COLOR: ResourceColor,
45  CAPSULE_SELECTED_BACKGROUND_COLOR: ResourceColor,
46  FOCUS_BORDER_COLOR: ResourceColor,
47  HOVER_COLOR: ResourceColor,
48  PRESS_COLOR: ResourceColor,
49  BACKGROUND_BLUR_STYLE: Resource,
50  CONSTRAINT_SIZE_MIN_HEIGHT: DimensionNoPercentage,
51  SEGMENT_BUTTON_MIN_FONT_SIZE: DimensionNoPercentage,
52  SEGMENT_BUTTON_NORMAL_BORDER_RADIUS: Length | BorderRadiuses | LocalizedBorderRadiuses,
53  SEGMENT_ITEM_TEXT_OVERFLOW: Resource,
54  SEGMENT_BUTTON_FOCUS_TEXT_COLOR: ResourceColor,
55  SEGMENT_BUTTON_SHADOW: Resource,
56  SEGMENT_FOCUS_STYLE_CUSTOMIZED: Resource,
57  SEGMENT_BUTTON_CONTAINER_SHAPE: Resource,
58  SEGMENT_BUTTON_SELECTED_BACKGROUND_SHAPE: Resource
59}
60
61const segmentButtonTheme: SegmentButtonThemeInterface = {
62  FONT_COLOR: $r('sys.color.segment_button_unselected_text_color'),
63  TAB_SELECTED_FONT_COLOR: $r('sys.color.segment_button_checked_text_color'),
64  CAPSULE_SELECTED_FONT_COLOR: $r('sys.color.ohos_id_color_foreground_contrary'),
65  FONT_SIZE: $r('sys.float.segment_button_unselected_text_size'),
66  SELECTED_FONT_SIZE: $r('sys.float.segment_button_checked_text_size'),
67  BACKGROUND_COLOR: $r('sys.color.segment_button_backboard_color'),
68  TAB_SELECTED_BACKGROUND_COLOR: $r('sys.color.segment_button_checked_foreground_color'),
69  CAPSULE_SELECTED_BACKGROUND_COLOR: $r('sys.color.ohos_id_color_emphasize'),
70  FOCUS_BORDER_COLOR: $r('sys.color.ohos_id_color_focused_outline'),
71  HOVER_COLOR: $r('sys.color.segment_button_hover_color'),
72  PRESS_COLOR: $r('sys.color.segment_button_press_color'),
73  BACKGROUND_BLUR_STYLE: $r('sys.float.segment_button_background_blur_style'),
74  CONSTRAINT_SIZE_MIN_HEIGHT: $r('sys.float.segment_button_height'),
75  SEGMENT_BUTTON_MIN_FONT_SIZE: $r('sys.float.segment_button_min_font_size'),
76  SEGMENT_BUTTON_NORMAL_BORDER_RADIUS: $r('sys.float.segment_button_normal_border_radius'),
77  SEGMENT_ITEM_TEXT_OVERFLOW: $r('sys.float.segment_marquee'),
78  SEGMENT_BUTTON_FOCUS_TEXT_COLOR: $r('sys.color.segment_button_focus_text_primary'),
79  SEGMENT_BUTTON_SHADOW: $r('sys.float.segment_button_shadow'),
80  SEGMENT_TEXT_HORIZONTAL_PADDING: $r('sys.float.segment_button_text_l_r_padding'),
81  SEGMENT_TEXT_VERTICAL_PADDING: $r('sys.float.segment_button_text_u_d_padding'),
82  SEGMENT_TEXT_CAPSULE_VERTICAL_PADDING: $r('sys.float.segment_button_text_capsule_u_d_padding'),
83  SEGMENT_BUTTON_FOCUS_CUSTOMIZED_BG_COLOR: $r('sys.color.segment_button_focus_backboard_primary'),
84  SEGMENT_FOCUS_STYLE_CUSTOMIZED: $r('sys.float.segment_focus_control'),
85  SEGMENT_BUTTON_CONTAINER_SHAPE: $r('sys.float.segmentbutton_container_shape'),
86  SEGMENT_BUTTON_SELECTED_BACKGROUND_SHAPE: $r('sys.float.segmentbutton_selected_background_shape')
87}
88
89interface Point {
90  x: number
91  y: number
92}
93
94function nearEqual(first: number, second: number): boolean {
95  return Math.abs(first - second) < 0.001
96}
97
98function validateLengthMetrics(value: LengthMetrics | undefined, defaultValue: LengthMetrics): LengthMetrics {
99  const actualValue = value ?? defaultValue;
100  return (actualValue.value < 0 || actualValue.unit === LengthUnit.PERCENT) ? defaultValue : actualValue;
101}
102
103interface SegmentButtonTextItem {
104  text: ResourceStr
105  accessibilityLevel?: string
106  accessibilityDescription?: ResourceStr
107}
108
109interface SegmentButtonIconItem {
110  icon: ResourceStr,
111  iconAccessibilityText?: ResourceStr
112  selectedIcon: ResourceStr
113  selectedIconAccessibilityText?: ResourceStr
114  accessibilityLevel?: string
115  accessibilityDescription?: ResourceStr
116}
117
118interface SegmentButtonIconTextItem {
119  icon: ResourceStr,
120  iconAccessibilityText?: ResourceStr
121  selectedIcon: ResourceStr,
122  selectedIconAccessibilityText?: ResourceStr
123  text: ResourceStr
124  accessibilityLevel?: string
125  accessibilityDescription?: ResourceStr
126}
127
128type DimensionNoPercentage = PX | VP | FP | LPX | Resource
129
130interface CommonSegmentButtonOptions {
131  fontColor?: ResourceColor
132  selectedFontColor?: ResourceColor
133  fontSize?: DimensionNoPercentage
134  selectedFontSize?: DimensionNoPercentage
135  fontWeight?: FontWeight
136  selectedFontWeight?: FontWeight
137  backgroundColor?: ResourceColor
138  selectedBackgroundColor?: ResourceColor
139  imageSize?: SizeOptions
140  buttonPadding?: Padding | Dimension
141  textPadding?: Padding | Dimension
142  localizedTextPadding?: LocalizedPadding
143  localizedButtonPadding?: LocalizedPadding
144  backgroundBlurStyle?: BlurStyle
145  direction?: Direction
146  borderRadiusMode?: BorderRadiusMode
147  backgroundBorderRadius?: LengthMetrics
148  itemBorderRadius?: LengthMetrics
149}
150
151type ItemRestriction<T> = [T, T, T?, T?, T?]
152type SegmentButtonItemTuple = ItemRestriction<SegmentButtonTextItem> |
153ItemRestriction<SegmentButtonIconItem> | ItemRestriction<SegmentButtonIconTextItem>
154type SegmentButtonItemArray = Array<SegmentButtonTextItem> |
155Array<SegmentButtonIconItem> | Array<SegmentButtonIconTextItem>
156
157export interface TabSegmentButtonConstructionOptions extends CommonSegmentButtonOptions {
158  buttons: ItemRestriction<SegmentButtonTextItem>
159}
160
161export interface CapsuleSegmentButtonConstructionOptions extends CommonSegmentButtonOptions {
162  buttons: SegmentButtonItemTuple
163  multiply?: boolean
164}
165
166export interface TabSegmentButtonOptions extends TabSegmentButtonConstructionOptions {
167  type: 'tab',
168}
169
170export interface CapsuleSegmentButtonOptions extends CapsuleSegmentButtonConstructionOptions {
171  type: 'capsule'
172}
173
174export enum BorderRadiusMode {
175  /**
176   * DEFAULT Mode, the framework automatically calculates the border radius
177   */
178  DEFAULT = 0,
179
180  /**
181   * CUSTOM Mode, the developer sets the border radius
182   */
183  CUSTOM = 1
184}
185
186interface SegmentButtonItemOptionsConstructorOptions {
187  icon?: ResourceStr
188  iconAccessibilityText?: ResourceStr
189  selectedIcon?: ResourceStr
190  selectedIconAccessibilityText?: ResourceStr
191  text?: ResourceStr
192  accessibilityLevel?: string
193  accessibilityDescription?: ResourceStr
194}
195
196@Observed
197class SegmentButtonItemOptions {
198  public icon?: ResourceStr
199  public iconAccessibilityText?: ResourceStr
200  public selectedIcon?: ResourceStr
201  public selectedIconAccessibilityText?: ResourceStr
202  public text?: ResourceStr
203  public accessibilityLevel?: string
204  public accessibilityDescription?: ResourceStr
205
206  constructor(options: SegmentButtonItemOptionsConstructorOptions) {
207    this.icon = options.icon
208    this.selectedIcon = options.selectedIcon
209    this.text = options.text
210    this.iconAccessibilityText = options.iconAccessibilityText
211    this.selectedIconAccessibilityText = options.selectedIconAccessibilityText
212    this.accessibilityLevel = options.accessibilityLevel
213    this.accessibilityDescription = options.accessibilityDescription
214  }
215}
216
217@Observed
218export class SegmentButtonItemOptionsArray extends Array<SegmentButtonItemOptions> {
219  public changeStartIndex: number | undefined = void 0
220  public deleteCount: number | undefined = void 0
221  public addLength: number | undefined = void 0
222
223  constructor(length: number)
224
225  constructor(elements: SegmentButtonItemTuple)
226
227  constructor(elementsOrLength: SegmentButtonItemTuple | number) {
228
229    super(typeof elementsOrLength === 'number' ? elementsOrLength : 0);
230
231    if (typeof elementsOrLength !== 'number' && elementsOrLength !== void 0) {
232      super.push(...elementsOrLength.map((element?: SegmentButtonTextItem | SegmentButtonIconItem |
233      SegmentButtonIconTextItem) => new SegmentButtonItemOptions(element as
234      SegmentButtonItemOptionsConstructorOptions)))
235    }
236  }
237
238  push(...items: SegmentButtonItemArray): number {
239    if (this.length + items.length > MAX_ITEM_COUNT) {
240      console.warn('Exceeded the maximum number of elements (5).')
241      return this.length
242    }
243    this.changeStartIndex = this.length
244    this.deleteCount = 0
245    this.addLength = items.length
246    return super.push(...items.map((element: SegmentButtonItemOptionsConstructorOptions) =>
247    new SegmentButtonItemOptions(element)))
248  }
249
250  pop() {
251    if (this.length <= MIN_ITEM_COUNT) {
252      console.warn('Below the minimum number of elements (2).')
253      return void 0
254    }
255    this.changeStartIndex = this.length - 1
256    this.deleteCount = 1
257    this.addLength = 0
258    return super.pop()
259  }
260
261  shift() {
262    if (this.length <= MIN_ITEM_COUNT) {
263      console.warn('Below the minimum number of elements (2).')
264      return void 0
265    }
266    this.changeStartIndex = 0
267    this.deleteCount = 1
268    this.addLength = 0
269    return super.shift()
270  }
271
272  unshift(...items: SegmentButtonItemArray): number {
273    if (this.length + items.length > MAX_ITEM_COUNT) {
274      console.warn('Exceeded the maximum number of elements (5).')
275      return this.length
276    }
277    if (items.length > 0) {
278      this.changeStartIndex = 0
279      this.deleteCount = 0
280      this.addLength = items.length
281    }
282    return super.unshift(...items.map((element: SegmentButtonItemOptionsConstructorOptions) =>
283    new SegmentButtonItemOptions(element)))
284  }
285
286  splice(start: number, deleteCount: number, ...items: SegmentButtonItemOptions[]): SegmentButtonItemOptions[] {
287    let length = (this.length - deleteCount) < 0 ? 0 : (this.length - deleteCount)
288    length += items.length
289    if (length < MIN_ITEM_COUNT) {
290      console.warn('Below the minimum number of elements (2).')
291      return []
292    }
293    if (length > MAX_ITEM_COUNT) {
294      console.warn('Exceeded the maximum number of elements (5).')
295      return []
296    }
297    this.changeStartIndex = start
298    this.deleteCount = deleteCount
299    this.addLength = items.length
300    return super.splice(start, deleteCount, ...items)
301  }
302
303  static create(elements: SegmentButtonItemTuple): SegmentButtonItemOptionsArray {
304    return new SegmentButtonItemOptionsArray(elements)
305  }
306}
307
308@Observed
309export class SegmentButtonOptions {
310  public type: 'tab' | 'capsule'
311  public multiply: boolean = false
312  public fontColor: ResourceColor
313  public selectedFontColor: ResourceColor
314  public fontSize: DimensionNoPercentage
315  public selectedFontSize: DimensionNoPercentage
316  public fontWeight: FontWeight
317  public selectedFontWeight: FontWeight
318  public backgroundColor: ResourceColor
319  public selectedBackgroundColor: ResourceColor
320  public imageSize: SizeOptions
321  public buttonPadding: Padding | Dimension | undefined
322  public textPadding: Padding | Dimension | undefined
323  public componentPadding: Padding | Dimension
324  public localizedTextPadding?: LocalizedPadding
325  public localizedButtonPadding?: LocalizedPadding
326  public showText: boolean = false
327  public showIcon: boolean = false
328  public iconTextRadius?: number
329  public iconTextBackgroundRadius?: number
330  public backgroundBlurStyle: BlurStyle
331  public direction?: Direction
332  public borderRadiusMode?: BorderRadiusMode
333  public backgroundBorderRadius?: LengthMetrics
334  public itemBorderRadius?: LengthMetrics
335  private _buttons: SegmentButtonItemOptionsArray | undefined = void 0
336
337  get buttons() {
338    return this._buttons
339  }
340
341  set buttons(val) {
342    if (this._buttons !== void 0 && this._buttons !== val) {
343      this.onButtonsChange?.()
344    }
345    this._buttons = val
346  }
347
348  public onButtonsChange?: () => void
349
350  constructor(options: TabSegmentButtonOptions | CapsuleSegmentButtonOptions) {
351    this.fontColor = options.fontColor ?? segmentButtonTheme.FONT_COLOR
352    this.selectedFontColor = options.selectedFontColor ?? segmentButtonTheme.TAB_SELECTED_FONT_COLOR
353    this.fontSize = options.fontSize ?? segmentButtonTheme.FONT_SIZE
354    this.selectedFontSize = options.selectedFontSize ?? segmentButtonTheme.SELECTED_FONT_SIZE
355    this.fontWeight = options.fontWeight ?? FontWeight.Regular
356    this.selectedFontWeight = options.selectedFontWeight ?? FontWeight.Medium
357    this.backgroundColor = options.backgroundColor ?? segmentButtonTheme.BACKGROUND_COLOR
358    this.selectedBackgroundColor = options.selectedBackgroundColor ?? segmentButtonTheme.TAB_SELECTED_BACKGROUND_COLOR
359    this.imageSize = options.imageSize ?? { width: 24, height: 24 }
360    this.buttonPadding = options.buttonPadding
361    this.textPadding = options.textPadding
362    this.type = options.type
363    this.backgroundBlurStyle =
364      options.backgroundBlurStyle ??
365        LengthMetrics.resource(segmentButtonTheme.BACKGROUND_BLUR_STYLE).value as BlurStyle;
366    this.localizedTextPadding = options.localizedTextPadding
367    this.localizedButtonPadding = options.localizedButtonPadding
368    this.direction = options.direction ?? Direction.Auto
369    this.borderRadiusMode = options.borderRadiusMode ?? BorderRadiusMode.DEFAULT
370    if (this.borderRadiusMode !== BorderRadiusMode.DEFAULT &&
371      this.borderRadiusMode !== BorderRadiusMode.CUSTOM) {
372      this.borderRadiusMode = BorderRadiusMode.DEFAULT;
373    }
374    this.backgroundBorderRadius = validateLengthMetrics(
375      options.backgroundBorderRadius,
376      LengthMetrics.resource(segmentButtonTheme.SEGMENT_BUTTON_CONTAINER_SHAPE)
377    );
378    this.itemBorderRadius = validateLengthMetrics(
379      options.itemBorderRadius,
380      LengthMetrics.resource(segmentButtonTheme.SEGMENT_BUTTON_SELECTED_BACKGROUND_SHAPE)
381    );
382    this.buttons = new SegmentButtonItemOptionsArray(options.buttons)
383    if (this.type === 'capsule') {
384      this.multiply = (options as CapsuleSegmentButtonOptions).multiply ?? false
385      this.onButtonsUpdated();
386      this.selectedFontColor = options.selectedFontColor ?? segmentButtonTheme.CAPSULE_SELECTED_FONT_COLOR
387      this.selectedBackgroundColor = options.selectedBackgroundColor ??
388      segmentButtonTheme.CAPSULE_SELECTED_BACKGROUND_COLOR
389    } else {
390      this.showText = true
391    }
392    let themePadding = LengthMetrics.resource($r('sys.float.segment_button_baseplate_padding')).value;
393    this.componentPadding = this.multiply ? 0 : themePadding;
394  }
395
396  public onButtonsUpdated() {
397    this.buttons?.forEach(button => {
398      this.showText ||= button.text !== void 0;
399      this.showIcon ||= button.icon !== void 0 || button.selectedIcon !== void 0;
400    })
401    if (this.showText && this.showIcon) {
402      this.iconTextRadius = 12;
403      this.iconTextBackgroundRadius = 14;
404    }
405  }
406
407  static tab(options: TabSegmentButtonConstructionOptions): SegmentButtonOptions {
408    return new SegmentButtonOptions({
409      type: 'tab',
410      buttons: options.buttons,
411      fontColor: options.fontColor,
412      selectedFontColor: options.selectedFontColor,
413      fontSize: options.fontSize,
414      selectedFontSize: options.selectedFontSize,
415      fontWeight: options.fontWeight,
416      selectedFontWeight: options.selectedFontWeight,
417      backgroundColor: options.backgroundColor,
418      selectedBackgroundColor: options.selectedBackgroundColor,
419      imageSize: options.imageSize,
420      buttonPadding: options.buttonPadding,
421      textPadding: options.textPadding,
422      localizedTextPadding: options.localizedTextPadding,
423      localizedButtonPadding: options.localizedButtonPadding,
424      backgroundBlurStyle: options.backgroundBlurStyle,
425      direction: options.direction,
426      borderRadiusMode: options.borderRadiusMode,
427      backgroundBorderRadius: options.backgroundBorderRadius,
428      itemBorderRadius: options.itemBorderRadius
429    })
430  }
431
432  static capsule(options: CapsuleSegmentButtonConstructionOptions): SegmentButtonOptions {
433    return new SegmentButtonOptions({
434      type: 'capsule',
435      buttons: options.buttons,
436      multiply: options.multiply,
437      fontColor: options.fontColor,
438      selectedFontColor: options.selectedFontColor,
439      fontSize: options.fontSize,
440      selectedFontSize: options.selectedFontSize,
441      fontWeight: options.fontWeight,
442      selectedFontWeight: options.selectedFontWeight,
443      backgroundColor: options.backgroundColor,
444      selectedBackgroundColor: options.selectedBackgroundColor,
445      imageSize: options.imageSize,
446      buttonPadding: options.buttonPadding,
447      textPadding: options.textPadding,
448      localizedTextPadding: options.localizedTextPadding,
449      localizedButtonPadding: options.localizedButtonPadding,
450      backgroundBlurStyle: options.backgroundBlurStyle,
451      direction: options.direction,
452      borderRadiusMode: options.borderRadiusMode,
453      backgroundBorderRadius: options.backgroundBorderRadius,
454      itemBorderRadius: options.itemBorderRadius
455    })
456  }
457}
458
459@Component
460struct MultiSelectBackground {
461  @ObjectLink optionsArray: SegmentButtonItemOptionsArray
462  @ObjectLink options: SegmentButtonOptions
463  @Consume buttonBorderRadius: LocalizedBorderRadiuses[]
464  @Consume buttonItemsSize: SizeOptions[]
465
466  build() {
467    Row({ space: 1 }) {
468      ForEach(this.optionsArray, (_: SegmentButtonItemOptions, index) => {
469        if (index < MAX_ITEM_COUNT) {
470          Stack()
471            .direction(this.options.direction)
472            .layoutWeight(1)
473            .height(this.buttonItemsSize[index].height)
474            .backgroundColor(this.options.backgroundColor ?? segmentButtonTheme.BACKGROUND_COLOR)
475            .borderRadius(this.buttonBorderRadius[index])
476            .backgroundBlurStyle(this.options.backgroundBlurStyle, undefined, { disableSystemAdaptation: true })
477        }
478      })
479    }
480    .direction(this.options.direction)
481    .padding(this.options.componentPadding)
482  }
483}
484
485@Component
486struct SelectItem {
487  @ObjectLink optionsArray: SegmentButtonItemOptionsArray
488  @ObjectLink options: SegmentButtonOptions
489  @Link selectedIndexes: number[]
490  @Consume buttonItemsSize: SizeOptions[]
491  @Consume selectedItemPosition: LocalizedEdges
492  @Consume zoomScaleArray: number[]
493  @Consume buttonBorderRadius: LocalizedBorderRadiuses[]
494
495  build() {
496    if (this.selectedIndexes !== void 0 && this.selectedIndexes.length !== 0) {
497      Stack()
498        .direction(this.options.direction)
499        .borderRadius(this.buttonBorderRadius[this.selectedIndexes[0]])
500        .size(this.buttonItemsSize[this.selectedIndexes[0]])
501        .backgroundColor(this.options.selectedBackgroundColor ??
502          (this.options.type === 'tab' ? segmentButtonTheme.TAB_SELECTED_BACKGROUND_COLOR :
503          segmentButtonTheme.CAPSULE_SELECTED_BACKGROUND_COLOR))
504        .position(this.selectedItemPosition)
505        .scale({ x: this.zoomScaleArray[this.selectedIndexes[0]], y: this.zoomScaleArray[this.selectedIndexes[0]] })
506        .shadow(resourceToNumber(this.getUIContext()?.getHostContext(), segmentButtonTheme.SEGMENT_BUTTON_SHADOW,
507          0) as ShadowStyle)
508    }
509  }
510}
511
512@Component
513struct MultiSelectItemArray {
514  @ObjectLink optionsArray: SegmentButtonItemOptionsArray
515  @ObjectLink @Watch('onOptionsChange') options: SegmentButtonOptions
516  @Link @Watch('onSelectedChange') selectedIndexes: number[]
517  @Consume buttonItemsSize: SizeOptions[]
518  @Consume zoomScaleArray: number[]
519  @Consume buttonBorderRadius: LocalizedBorderRadiuses[]
520  @State multiColor: ResourceColor[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => Color.Transparent)
521
522  onOptionsChange() {
523    for (let i = 0; i < this.selectedIndexes.length; i++) {
524      this.multiColor[this.selectedIndexes[i]] = this.options.selectedBackgroundColor ??
525      segmentButtonTheme.CAPSULE_SELECTED_BACKGROUND_COLOR
526    }
527  }
528
529  onSelectedChange() {
530    for (let i = 0; i < MAX_ITEM_COUNT; i++) {
531      this.multiColor[i] = Color.Transparent
532    }
533    for (let i = 0; i < this.selectedIndexes.length; i++) {
534      this.multiColor[this.selectedIndexes[i]] = this.options.selectedBackgroundColor ??
535      segmentButtonTheme.CAPSULE_SELECTED_BACKGROUND_COLOR
536    }
537  }
538
539  aboutToAppear() {
540    for (let i = 0; i < this.selectedIndexes.length; i++) {
541      this.multiColor[this.selectedIndexes[i]] = this.options.selectedBackgroundColor ??
542      segmentButtonTheme.CAPSULE_SELECTED_BACKGROUND_COLOR
543    }
544  }
545
546  build() {
547    Row({ space: 1 }) {
548      ForEach(this.optionsArray, (_: SegmentButtonItemOptions, index) => {
549        if (index < MAX_ITEM_COUNT) {
550          Stack()
551            .direction(this.options.direction)
552            .width(this.buttonItemsSize[index].width)
553            .height(this.buttonItemsSize[index].height)
554            .backgroundColor(this.multiColor[index])
555            .borderRadius(this.buttonBorderRadius[index])
556        }
557      })
558    }
559    .direction(this.options.direction)
560    .padding(this.options.componentPadding)
561  }
562}
563
564@Component
565struct SegmentButtonItem {
566  @Link selectedIndexes: number[]
567  @Link @Watch('onFocusIndex') focusIndex: number;
568  @Prop @Require maxFontScale: number | Resource
569  @ObjectLink itemOptions: SegmentButtonItemOptions
570  @ObjectLink options: SegmentButtonOptions;
571  @ObjectLink property: ItemProperty
572  @Prop index: number
573  @State isTextSupportMarquee: boolean =
574    resourceToNumber(this.getUIContext()?.getHostContext(), segmentButtonTheme.SEGMENT_ITEM_TEXT_OVERFLOW, 1.0) === 0.0;
575  @Prop isMarqueeAndFadeout: boolean;
576  @Prop isSegmentFocusStyleCustomized: boolean;
577  @State isTextInMarqueeCondition: boolean = false;
578  @State isButtonTextFadeout?: boolean = false;
579  private groupId: string = ''
580  @Prop @Watch('onFocusIndex') hover: boolean;
581
582  private getTextPadding(): Padding | Dimension | LocalizedPadding {
583    if (this.options.localizedTextPadding) {
584      return this.options.localizedTextPadding
585    }
586    if (this.options.textPadding !== void (0)) {
587      return this.options.textPadding
588    }
589    return 0
590  }
591
592  private getButtonPadding(): Padding | Dimension | LocalizedPadding {
593    if (this.options.localizedButtonPadding) {
594      return this.options.localizedButtonPadding
595    }
596    if (this.options.buttonPadding !== void (0)) {
597      return this.options.buttonPadding
598    }
599    if (this.options.type === 'capsule' && this.options.showText && this.options.showIcon) {
600      return {
601        top: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_CAPSULE_VERTICAL_PADDING),
602        bottom: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_CAPSULE_VERTICAL_PADDING),
603        start: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_HORIZONTAL_PADDING),
604        end: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_HORIZONTAL_PADDING)
605      }
606    }
607    return {
608      top: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_VERTICAL_PADDING),
609      bottom: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_VERTICAL_PADDING),
610      start: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_HORIZONTAL_PADDING),
611      end: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_HORIZONTAL_PADDING)
612    }
613  }
614
615  onFocusIndex(): void {
616    this.isTextInMarqueeCondition =
617      this.isSegmentFocusStyleCustomized && (this.focusIndex === this.index || this.hover);
618  }
619
620  aboutToAppear(): void {
621    this.isButtonTextFadeout = this.isSegmentFocusStyleCustomized;
622  }
623
624  isDefaultSelectedFontColor(): boolean {
625    if (this.options.type === 'tab') {
626      return this.options.selectedFontColor === segmentButtonTheme.TAB_SELECTED_FONT_COLOR;
627    } else if (this.options.type === 'capsule') {
628      return this.options.selectedFontColor === segmentButtonTheme.CAPSULE_SELECTED_FONT_COLOR;
629    }
630    return false;
631  }
632
633  private getFontColor(): ResourceColor {
634    if (this.property.isSelected) {
635      if (this.isDefaultSelectedFontColor() && this.isSegmentFocusStyleCustomized && this.focusIndex === this.index) {
636        return segmentButtonTheme.SEGMENT_BUTTON_FOCUS_TEXT_COLOR;
637      }
638      return this.options.selectedFontColor ?? segmentButtonTheme.CAPSULE_SELECTED_FONT_COLOR;
639    }
640    return this.options.fontColor ?? segmentButtonTheme.FONT_COLOR;
641  }
642
643  private getAccessibilityText(): Resource | undefined {
644    if (this.selectedIndexes.includes(this.index) &&
645      typeof this.itemOptions.selectedIconAccessibilityText !== undefined) {
646      return this.itemOptions.selectedIconAccessibilityText as Resource
647    } else if (!this.selectedIndexes.includes(this.index) &&
648      typeof this.itemOptions.iconAccessibilityText !== undefined) {
649      return this.itemOptions.iconAccessibilityText as Resource
650    }
651    return undefined;
652  }
653
654  build() {
655    Column({ space: 2 }) {
656      if (this.options.showIcon) {
657        Image(this.property.isSelected ? this.itemOptions.selectedIcon : this.itemOptions.icon)
658          .direction(this.options.direction)
659          .size(this.options.imageSize ?? { width: 24, height: 24 })
660          .draggable(false)
661          .fillColor(this.getFontColor())
662          .accessibilityText(this.getAccessibilityText())
663      }
664      if (this.options.showText) {
665        Text(this.itemOptions.text)
666          .direction(this.options.direction)
667          .fontColor(this.getFontColor())
668          .fontWeight(this.property.fontWeight)
669          .fontSize(this.property.fontSize)
670          .minFontSize(this.isSegmentFocusStyleCustomized ? this.property.fontSize : 9)
671          .maxFontSize(this.property.fontSize)
672          .maxFontScale(this.maxFontScale)
673          .textOverflow({
674            overflow: this.isTextSupportMarquee ? TextOverflow.MARQUEE : TextOverflow.Ellipsis
675          })
676          .marqueeOptions({
677            start: this.isTextInMarqueeCondition,
678            fadeout: this.isButtonTextFadeout,
679            marqueeStartPolicy: MarqueeStartPolicy.DEFAULT
680          })
681          .maxLines(1)
682          .textAlign(TextAlign.Center)
683          .padding(this.getTextPadding())
684      }
685    }
686    .direction(this.options.direction)
687    .justifyContent(FlexAlign.Center)
688    .padding(this.getButtonPadding())
689    .constraintSize({ minHeight: segmentButtonTheme.CONSTRAINT_SIZE_MIN_HEIGHT })
690  }
691}
692
693@Observed
694class HoverColorProperty {
695  public hoverColor: ResourceColor = Color.Transparent
696}
697
698@Component
699struct PressAndHoverEffect {
700  @Consume buttonItemsSize: SizeOptions[]
701  @Prop press: boolean
702  @Prop hover: boolean
703  @ObjectLink colorProperty: HoverColorProperty
704  @Consume buttonBorderRadius: LocalizedBorderRadiuses[]
705  @ObjectLink options: SegmentButtonOptions;
706  pressIndex: number = 0
707  pressColor: ResourceColor = segmentButtonTheme.PRESS_COLOR
708
709  build() {
710    Stack()
711      .direction(this.options.direction)
712      .size(this.buttonItemsSize[this.pressIndex])
713      .backgroundColor(this.press && this.hover ? this.pressColor : this.colorProperty.hoverColor)
714      .borderRadius(this.buttonBorderRadius[this.pressIndex])
715  }
716}
717
718@Component
719struct PressAndHoverEffectArray {
720  @ObjectLink buttons: SegmentButtonItemOptionsArray
721  @ObjectLink options: SegmentButtonOptions
722  @Link pressArray: boolean[]
723  @Link hoverArray: boolean[]
724  @Link hoverColorArray: HoverColorProperty[]
725  @Consume zoomScaleArray: number[]
726
727  build() {
728    Row({ space: 1 }) {
729      ForEach(this.buttons, (item: SegmentButtonItemOptions, index) => {
730        if (index < MAX_ITEM_COUNT) {
731          Stack() {
732            PressAndHoverEffect({
733              pressIndex: index,
734              colorProperty: this.hoverColorArray[index],
735              press: this.pressArray[index],
736              hover: this.hoverArray[index],
737              options: this.options,
738            })
739          }
740          .direction(this.options.direction)
741          .scale({
742            x: this.options.type === 'capsule' && (this.options.multiply ?? false) ? 1 : this.zoomScaleArray[index],
743            y: this.options.type === 'capsule' && (this.options.multiply ?? false) ? 1 : this.zoomScaleArray[index]
744          })
745        }
746      })
747    }.direction(this.options.direction)
748  }
749}
750
751@Component
752struct SegmentButtonItemArrayComponent {
753  @ObjectLink @Watch('onOptionsArrayChange') optionsArray: SegmentButtonItemOptionsArray
754  @ObjectLink @Watch('onOptionsChange') options: SegmentButtonOptions
755  @Link selectedIndexes: number[]
756  @Consume componentSize: SizeOptions
757  @Consume buttonBorderRadius: LocalizedBorderRadiuses[]
758  @Consume @Watch('onButtonItemsSizeChange') buttonItemsSize: SizeOptions[]
759  @Consume buttonItemsPosition: LocalizedEdges[]
760  @Consume @Watch('onFocusIndex') focusIndex: number;
761  @Consume zoomScaleArray: number[]
762  @Consume buttonItemProperty: ItemProperty[]
763  @Consume buttonItemsSelected: boolean[]
764  @Link pressArray: boolean[]
765  @Link hoverArray: boolean[]
766  @Link hoverColorArray: HoverColorProperty[]
767  @Prop @Require maxFontScale: number | Resource
768  @State buttonWidth: number[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => 0)
769  @State buttonHeight: number[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => 0)
770  @State isMarqueeAndFadeout: boolean = false;
771  private buttonItemsRealHeight: number[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => 0)
772  private groupId: string = util.generateRandomUUID(true)
773  public onItemClicked?: Callback<number>
774  @Prop isSegmentFocusStyleCustomized: boolean;
775
776  onButtonItemsSizeChange() {
777    this.buttonItemsSize.forEach((value, index) => {
778      this.buttonWidth[index] = value.width as number
779      this.buttonHeight[index] = value.height as number
780    })
781  }
782
783  changeSelectedIndexes(buttonsLength: number) {
784    if (this.optionsArray.changeStartIndex === void 0 || this.optionsArray.deleteCount === void 0 ||
785      this.optionsArray.addLength === void 0) {
786      return
787    }
788    if (!(this.options.multiply ?? false)) {
789      // Single-select
790      if (this.selectedIndexes[0] === void 0) {
791        return
792      }
793      if (this.selectedIndexes[0] < this.optionsArray.changeStartIndex) {
794        return
795      }
796      if (this.optionsArray.changeStartIndex + this.optionsArray.deleteCount > this.selectedIndexes[0]) {
797        if (this.options.type === 'tab') {
798          this.selectedIndexes[0] = 0
799        } else if (this.options.type === 'capsule') {
800          this.selectedIndexes = []
801        }
802      } else {
803        this.selectedIndexes[0] = this.selectedIndexes[0] - this.optionsArray.deleteCount + this.optionsArray.addLength
804      }
805    } else {
806      // Multi-select
807      let saveIndexes = this.selectedIndexes
808      for (let i = 0; i < this.optionsArray.deleteCount; i++) {
809        let deleteIndex = saveIndexes.indexOf(this.optionsArray.changeStartIndex)
810        let indexes = saveIndexes.map(value => this.optionsArray.changeStartIndex &&
811          (value > this.optionsArray.changeStartIndex) ? value - 1 : value)
812        if (deleteIndex !== -1) {
813          indexes.splice(deleteIndex, 1)
814        }
815        saveIndexes = indexes
816      }
817      for (let i = 0; i < this.optionsArray.addLength; i++) {
818        let indexes = saveIndexes.map(value => this.optionsArray.changeStartIndex &&
819          (value >= this.optionsArray.changeStartIndex) ? value + 1 : value)
820        saveIndexes = indexes
821      }
822      this.selectedIndexes = saveIndexes
823    }
824
825  }
826
827  changeFocusIndex(buttonsLength: number) {
828    if (this.optionsArray.changeStartIndex === void 0 || this.optionsArray.deleteCount === void 0 ||
829      this.optionsArray.addLength === void 0) {
830      return
831    }
832    if (this.focusIndex === -1) {
833      return
834    }
835    if (this.focusIndex < this.optionsArray.changeStartIndex) {
836      return
837    }
838    if (this.optionsArray.changeStartIndex + this.optionsArray.deleteCount > this.focusIndex) {
839      this.focusIndex = 0
840    } else {
841      this.focusIndex = this.focusIndex - this.optionsArray.deleteCount + this.optionsArray.addLength
842    }
843
844  }
845
846  onOptionsArrayChange() {
847    if (this.options === void 0 || this.options.buttons === void 0) {
848      return
849    }
850    let buttonsLength = Math.min(this.options.buttons.length, this.buttonItemsSize.length)
851    if (this.optionsArray.changeStartIndex !== void 0 && this.optionsArray.deleteCount !== void 0 &&
852      this.optionsArray.addLength !== void 0) {
853      this.changeSelectedIndexes(buttonsLength)
854      this.changeFocusIndex(buttonsLength)
855      this.optionsArray.changeStartIndex = void 0
856      this.optionsArray.deleteCount = void 0
857      this.optionsArray.addLength = void 0
858    }
859  }
860
861  onOptionsChange() {
862    if (this.options === void 0 || this.options.buttons === void 0) {
863      return
864    }
865    this.calculateBorderRadius()
866  }
867
868  onFocusIndex(): void {
869    this.isMarqueeAndFadeout = this.isSegmentFocusStyleCustomized && !this.isMarqueeAndFadeout;
870  }
871
872  aboutToAppear() {
873    for (let index = 0; index < this.buttonItemsRealHeight.length; index++) {
874      this.buttonItemsRealHeight[index] = 0
875    }
876  }
877
878  private getFocusItemBorderRadius(index: number): LocalizedBorderRadiuses {
879    if (index < 0 || index >= this.buttonBorderRadius.length) {
880      return {
881        topStart: LengthMetrics.vp(0),
882        topEnd: LengthMetrics.vp(0),
883        bottomStart: LengthMetrics.vp(0),
884        bottomEnd: LengthMetrics.vp(0)
885      };
886    }
887
888    let focusOffset = 0;
889    if (this.options.type === 'capsule' &&
890      this.focusIndex >= 0 &&
891      this.focusIndex < this.buttonItemsSelected.length &&
892    this.buttonItemsSelected[this.focusIndex]) {
893      focusOffset = CAPSULE_FOCUS_SELECTED_OFFSET;
894    }
895
896    let borderRadius: LocalizedBorderRadiuses = this.buttonBorderRadius[index];
897
898    return {
899      topStart: LengthMetrics.vp((borderRadius.topStart?.value ?? 0) + focusOffset),
900      topEnd: LengthMetrics.vp((borderRadius.topEnd?.value ?? 0) + focusOffset),
901      bottomStart: LengthMetrics.vp((borderRadius.bottomStart?.value ?? 0) + focusOffset),
902      bottomEnd: LengthMetrics.vp((borderRadius.bottomEnd?.value ?? 0) + focusOffset)
903    };
904  }
905
906  private getFocusStackSize(index: number): SizeOptions {
907    const isCapsuleAndSelected = this.options.type === 'capsule' &&
908      this.focusIndex >= 0 &&
909      this.focusIndex < this.buttonItemsSelected.length &&
910    this.buttonItemsSelected[this.focusIndex];
911
912    return {
913      width: isCapsuleAndSelected
914        ? this.buttonWidth[index] + CAPSULE_FOCUS_SELECTED_OFFSET * 2
915        : this.buttonWidth[index],
916      height: isCapsuleAndSelected
917        ? this.buttonHeight[index] + CAPSULE_FOCUS_SELECTED_OFFSET * 2
918        : this.buttonHeight[index]
919    };
920  }
921
922  @Builder
923  focusStack(index: number) {
924    Stack() {
925      Stack()
926        .direction(this.options.direction)
927        .borderRadius(this.getFocusItemBorderRadius(index))
928        .size(this.getFocusStackSize(index))
929        .borderColor(segmentButtonTheme.FOCUS_BORDER_COLOR)
930        .borderWidth(2)
931    }
932    .direction(this.options.direction)
933    .size({ width: 1, height: 1 })
934    .align(Alignment.Center)
935    // 当前仅TV场景isSegmentFocusStyleCustomized为true,TV场景需要使用按键内置的focus样式,故隐藏此高级组件自定义的focus样式
936    .visibility(!this.isSegmentFocusStyleCustomized && this.focusIndex === index ? Visibility.Visible : Visibility.None)
937  }
938
939  calculateBorderRadius() {
940    // Calculate the border radius for each button
941    let borderRadiusArray: LocalizedBorderRadiuses[] = Array.from({
942      length: MAX_ITEM_COUNT
943    }, (_: Object): LocalizedBorderRadiuses => {
944      return {
945        topStart: LengthMetrics.vp(0),
946        topEnd: LengthMetrics.vp(0),
947        bottomStart: LengthMetrics.vp(0),
948        bottomEnd: LengthMetrics.vp(0)
949      }
950    });
951
952    const isSingleSelect = this.options.type === 'tab' || !(this.options.multiply ?? false);
953    const buttonsLength =
954      this.options.buttons ? Math.min(this.options.buttons.length, this.buttonItemsSize.length) : MIN_ITEM_COUNT;
955
956    const setAllCorners = (array: LocalizedBorderRadiuses[], index: number, lengthMetrics: LengthMetrics) => {
957      if (!array || index < 0 || index >= array.length) {
958        return;
959      }
960
961      const safeLengthMetrics = lengthMetrics.value < 0 ? LengthMetrics.vp(0) : lengthMetrics;
962      array[index].topStart = safeLengthMetrics;
963      array[index].topEnd = safeLengthMetrics;
964      array[index].bottomStart = safeLengthMetrics;
965      array[index].bottomEnd = safeLengthMetrics;
966    };
967
968    const setLeftCorners = (array: LocalizedBorderRadiuses[], index: number, lengthMetrics: LengthMetrics) => {
969      if (!array || index < 0 || index >= array.length) {
970        return;
971      }
972      const safeLengthMetrics = lengthMetrics.value < 0 ? LengthMetrics.vp(0) : lengthMetrics;
973      const zeroLengthMetrics = LengthMetrics.vp(0);
974      array[index].topStart = safeLengthMetrics;
975      array[index].topEnd = zeroLengthMetrics;
976      array[index].bottomStart = safeLengthMetrics;
977      array[index].bottomEnd = zeroLengthMetrics;
978    };
979
980    const setRightCorners = (array: LocalizedBorderRadiuses[], index: number, lengthMetrics: LengthMetrics) => {
981      if (!array || index < 0 || index >= array.length) {
982        return;
983      }
984      const safeLengthMetrics = lengthMetrics.value < 0 ? LengthMetrics.vp(0) : lengthMetrics;
985      const zeroLengthMetrics = LengthMetrics.vp(0);
986      array[index].topStart = zeroLengthMetrics;
987      array[index].topEnd = safeLengthMetrics;
988      array[index].bottomStart = zeroLengthMetrics;
989      array[index].bottomEnd = safeLengthMetrics;
990    };
991
992    const setMiddleCorners = (array: LocalizedBorderRadiuses[], index: number) => {
993      if (!array || index < 0 || index >= array.length) {
994        return;
995      }
996      array[index].topStart = LengthMetrics.vp(0);
997      array[index].topEnd = LengthMetrics.vp(0);
998      array[index].bottomStart = LengthMetrics.vp(0);
999      array[index].bottomEnd = LengthMetrics.vp(0);
1000    };
1001
1002    for (let index = 0; index < this.buttonBorderRadius.length; index++) {
1003      let halfButtonItemsSizeHeight = this.buttonItemsSize[index].height as number / 2;
1004      let radius = this.options.iconTextRadius ?? halfButtonItemsSizeHeight; // default radius
1005      // Determine which border radius to use based on mode setting
1006      const isCustomMode = this.options.borderRadiusMode === BorderRadiusMode.CUSTOM &&
1007        this.options.itemBorderRadius !== undefined;
1008
1009      let radiusLengthMetrics: LengthMetrics;
1010      if (isCustomMode && this.options.itemBorderRadius) {
1011        // Use custom border radius from options
1012        radiusLengthMetrics = this.options.itemBorderRadius;
1013      } else {
1014        // Use default calculated radius value
1015        radiusLengthMetrics = LengthMetrics.vp(radius);
1016      }
1017      if (isSingleSelect) {
1018        // single-select
1019        setAllCorners(borderRadiusArray, index, radiusLengthMetrics);
1020      } else {
1021        // multi-select
1022        if (index === 0) {
1023          setLeftCorners(borderRadiusArray, index, radiusLengthMetrics);
1024        } else if (index === buttonsLength - 1) {
1025          setRightCorners(borderRadiusArray, index, radiusLengthMetrics);
1026        } else {
1027          setMiddleCorners(borderRadiusArray, index);
1028        }
1029      }
1030    }
1031
1032    this.buttonBorderRadius = borderRadiusArray;
1033  }
1034
1035  getAccessibilityDescription(value?: ResourceStr, index?: number): string | undefined {
1036    if (value !== undefined) {
1037      return value as string;
1038    }
1039    const isSingleSelect = this.options.type === 'tab' || !this.options.multiply;
1040
1041    if (isSingleSelect && index !== undefined && this.selectedIndexes.includes(index)) {
1042      return ACCESSIBILITY_SELECTED_DESCRIPTION;
1043    }
1044
1045    return ACCESSIBILITY_DEFAULT_DESCRIPTION;
1046  }
1047
1048  isDefaultSelectedBgColor(): boolean {
1049    if (this.options.type === 'tab') {
1050      return this.options.selectedBackgroundColor === segmentButtonTheme.TAB_SELECTED_BACKGROUND_COLOR;
1051    } else if (this.options.type === 'capsule') {
1052      return this.options.selectedBackgroundColor === segmentButtonTheme.CAPSULE_SELECTED_BACKGROUND_COLOR;
1053    }
1054    return true;
1055  }
1056
1057  build() {
1058    if (this.optionsArray !== void 0 && this.optionsArray.length > 1) {
1059      Row({ space: 1 }) {
1060        ForEach(this.optionsArray, (item: SegmentButtonItemOptions, index) => {
1061          if (index < MAX_ITEM_COUNT) {
1062            Button() {
1063              SegmentButtonItem({
1064                isMarqueeAndFadeout: this.isMarqueeAndFadeout,
1065                isSegmentFocusStyleCustomized: this.isSegmentFocusStyleCustomized,
1066                selectedIndexes: $selectedIndexes,
1067                focusIndex: this.focusIndex,
1068                index: index,
1069                itemOptions: item,
1070                options: this.options,
1071                property: this.buttonItemProperty[index],
1072                groupId: this.groupId,
1073                maxFontScale: this.maxFontScale,
1074                hover: this.hoverArray[index],
1075              })
1076                .onSizeChange((_, newValue) => {
1077                  // Calculate height of items
1078                  this.buttonItemsRealHeight[index] = newValue.height as number
1079                  let maxHeight = Math.max(...this.buttonItemsRealHeight.slice(0, this.options.buttons ?
1080                  this.options.buttons.length : 0))
1081                  for (let index = 0; index < this.buttonItemsSize.length; index++) {
1082                    this.buttonItemsSize[index] = { width: this.buttonItemsSize[index].width, height: maxHeight }
1083                  }
1084                  this.calculateBorderRadius()
1085                })
1086            }
1087            .focusScopePriority(this.groupId,
1088              Math.min(...this.selectedIndexes) === index ? FocusPriority.PREVIOUS : FocusPriority.AUTO)
1089            .type(ButtonType.Normal)
1090            .stateEffect(false)
1091            .hoverEffect(HoverEffect.None)
1092            .backgroundColor(Color.Transparent)
1093            .accessibilityLevel(item.accessibilityLevel)
1094            .accessibilitySelected(this.options.multiply ? undefined : this.selectedIndexes.includes(index))
1095            .accessibilityChecked(this.options.multiply ? this.selectedIndexes.includes(index) : undefined)
1096            .accessibilityDescription(this.getAccessibilityDescription(item.accessibilityDescription, index))
1097            .direction(this.options.direction)
1098            .borderRadius(this.buttonBorderRadius[index])
1099            .scale({
1100              x: this.options.type === 'capsule' && (this.options.multiply ?? false) ? 1 : this.zoomScaleArray[index],
1101              y: this.options.type === 'capsule' && (this.options.multiply ?? false) ? 1 : this.zoomScaleArray[index]
1102            })
1103            .layoutWeight(1)
1104            .padding(0)
1105            .onSizeChange((_, newValue) => {
1106              this.buttonItemsSize[index] = { width: newValue.width, height: this.buttonItemsSize[index].height }
1107              //measure position
1108              if (newValue.width) {
1109                this.buttonItemsPosition[index] = {
1110                  start: LengthMetrics.vp(Number.parseFloat(this.options.componentPadding.toString()) +
1111                    (Number.parseFloat(newValue.width.toString()) + 1) * index),
1112                  top: LengthMetrics.px(Math.floor(this.getUIContext()
1113                    .vp2px(Number.parseFloat(this.options.componentPadding.toString()))))
1114                }
1115              }
1116            })
1117            .overlay(this.focusStack(index), { align: Alignment.Center })
1118            .attributeModifier(this.isSegmentFocusStyleCustomized ? undefined :
1119              new FocusStyleButtonModifier((isFocused: boolean): void => {
1120                if (!isFocused && this.focusIndex === index) {
1121                  this.focusIndex = -1;
1122                  return;
1123                }
1124                if (isFocused) {
1125                  this.focusIndex = index;
1126                }
1127              }))
1128            .onFocus(() => {
1129              this.focusIndex = index;
1130              if (this.isSegmentFocusStyleCustomized) {
1131                this.customizeSegmentFocusStyle(index);
1132              }
1133            })
1134            .onBlur(() => {
1135              if (this.focusIndex === index) {
1136                this.focusIndex = -1;
1137              }
1138              this.hoverColorArray[index].hoverColor = Color.Transparent;
1139            })
1140            .gesture(TapGesture().onAction(() => {
1141              if (this.onItemClicked) {
1142                this.onItemClicked(index)
1143              }
1144              if (this.options.type === 'capsule' && (this.options.multiply ?? false)) {
1145                if (this.selectedIndexes.indexOf(index) === -1) {
1146                  this.selectedIndexes.push(index)
1147                } else {
1148                  this.selectedIndexes.splice(this.selectedIndexes.indexOf(index), 1)
1149                }
1150              } else {
1151                this.selectedIndexes[0] = index
1152              }
1153            }))
1154            .onTouch((event: TouchEvent) => {
1155              if (this.isSegmentFocusStyleCustomized) {
1156                this.getUIContext().getFocusController().clearFocus();
1157              }
1158              if (event.source !== SourceType.TouchScreen) {
1159                return
1160              }
1161              if (event.type === TouchType.Down) {
1162                animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => {
1163                  this.zoomScaleArray[index] = 0.95
1164                })
1165              } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
1166                animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => {
1167                  this.zoomScaleArray[index] = 1
1168                })
1169              }
1170            })
1171            .onHover((isHover: boolean) => {
1172              this.hoverArray[index] = isHover
1173              if (isHover) {
1174                animateTo({ duration: 250, curve: Curve.Friction }, () => {
1175                  this.hoverColorArray[index].hoverColor =
1176                    this.isSegmentFocusStyleCustomized && this.focusIndex === index ?
1177                    segmentButtonTheme.SEGMENT_BUTTON_FOCUS_CUSTOMIZED_BG_COLOR : segmentButtonTheme.HOVER_COLOR;
1178                })
1179              } else {
1180                animateTo({ duration: 250, curve: Curve.Friction }, () => {
1181                  this.hoverColorArray[index].hoverColor =
1182                    this.isSegmentFocusStyleCustomized && this.focusIndex === index ?
1183                    segmentButtonTheme.SEGMENT_BUTTON_FOCUS_CUSTOMIZED_BG_COLOR : Color.Transparent;
1184                })
1185              }
1186            })
1187            .onMouse((event: MouseEvent) => {
1188              switch (event.action) {
1189                case MouseAction.Press:
1190                  animateTo({ curve: curves.springMotion(0.347, 0.99) }, () => {
1191                    this.zoomScaleArray[index] = 0.95
1192                  })
1193                  animateTo({ duration: 100, curve: Curve.Sharp }, () => {
1194                    this.pressArray[index] = true
1195                  })
1196                  break;
1197                case MouseAction.Release:
1198                  animateTo({ curve: curves.springMotion(0.347, 0.99) }, () => {
1199                    this.zoomScaleArray[index] = 1
1200                  })
1201                  animateTo({ duration: 100, curve: Curve.Sharp }, () => {
1202                    this.pressArray[index] = false
1203                  })
1204                  break;
1205              }
1206            })
1207          }
1208        })
1209      }
1210      .direction(this.options.direction)
1211      .focusScopeId(this.groupId, true)
1212      .padding(this.options.componentPadding)
1213      .onSizeChange((_, newValue) => {
1214        this.componentSize = { width: newValue.width, height: newValue.height }
1215      })
1216    }
1217  }
1218
1219  /**
1220   * 设置segmentbutton获焦时的样式
1221   * @param index
1222   */
1223  private customizeSegmentFocusStyle(index: number) {
1224    if (this.selectedIndexes !== void 0 && this.selectedIndexes.length !== 0 &&
1225      this.selectedIndexes[0] === index) { // 选中态
1226      this.hoverColorArray[index].hoverColor = this.isDefaultSelectedBgColor() ?
1227      segmentButtonTheme.SEGMENT_BUTTON_FOCUS_CUSTOMIZED_BG_COLOR : this.options.selectedBackgroundColor;
1228    } else { // 未选中态
1229      this.hoverColorArray[index].hoverColor = this.options.backgroundColor === segmentButtonTheme.BACKGROUND_COLOR ?
1230      segmentButtonTheme.SEGMENT_BUTTON_FOCUS_CUSTOMIZED_BG_COLOR : this.options.backgroundColor;
1231    }
1232  }
1233}
1234
1235@Observed
1236class ItemProperty {
1237  public fontColor: ResourceColor = segmentButtonTheme.FONT_COLOR
1238  public fontSize: DimensionNoPercentage = segmentButtonTheme.FONT_SIZE
1239  public fontWeight: FontWeight = FontWeight.Regular
1240  public isSelected: boolean = false
1241}
1242
1243@Component
1244export struct SegmentButton {
1245  @ObjectLink @Watch('onOptionsChange') options: SegmentButtonOptions
1246  @Link @Watch('onSelectedChange') selectedIndexes: number[]
1247  public onItemClicked?: Callback<number>
1248  @Prop maxFontScale: number | Resource = DEFAULT_MAX_FONT_SCALE
1249  @Provide componentSize: SizeOptions = { width: 0, height: 0 }
1250  @Provide buttonBorderRadius: LocalizedBorderRadiuses[] = Array.from({
1251    length: MAX_ITEM_COUNT
1252  }, (_: Object, index): LocalizedBorderRadiuses => {
1253    return {
1254      topStart: LengthMetrics.vp(0),
1255      topEnd: LengthMetrics.vp(0),
1256      bottomStart: LengthMetrics.vp(0),
1257      bottomEnd: LengthMetrics.vp(0)
1258    }
1259  })
1260  @Provide buttonItemsSize: SizeOptions[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index): SizeOptions => {
1261    return {}
1262  })
1263  @Provide @Watch('onItemsPositionChange') buttonItemsPosition: LocalizedEdges[] = Array.from({
1264    length: MAX_ITEM_COUNT
1265  }, (_: Object, index): LocalizedEdges => {
1266    return {}
1267  })
1268  @Provide buttonItemsSelected: boolean[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => false)
1269  @Provide buttonItemProperty: ItemProperty[] = Array.from({
1270    length: MAX_ITEM_COUNT
1271  }, (_: Object, index) => new ItemProperty())
1272  @Provide focusIndex: number = -1
1273  @Provide selectedItemPosition: LocalizedEdges = {}
1274  @Provide zoomScaleArray: number[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => 1.0)
1275  @State pressArray: boolean[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => false)
1276  @State hoverArray: boolean[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => false)
1277  @State hoverColorArray: HoverColorProperty[] = Array.from({
1278    length: MAX_ITEM_COUNT
1279  }, (_: Object, index) => new HoverColorProperty())
1280  private doSelectedChangeAnimate: boolean = false
1281  private isCurrentPositionSelected: boolean = false
1282  private panGestureStartPoint: Point = { x: 0, y: 0 }
1283  private isPanGestureMoved: boolean = false
1284  @State shouldMirror: boolean = false
1285  private isGestureInProgress: boolean = false;
1286  private isCustomizedCache?: boolean;
1287
1288  onItemsPositionChange() {
1289    if (this.options === void 0 || this.options.buttons === void 0) {
1290      return
1291    }
1292    if (this.options.type === 'capsule') {
1293      this.options.onButtonsUpdated();
1294    }
1295    if (this.doSelectedChangeAnimate) {
1296      this.updateAnimatedProperty(this.getSelectedChangeCurve())
1297    } else {
1298      this.updateAnimatedProperty(null)
1299    }
1300  }
1301
1302  setItemsSelected() {
1303    this.buttonItemsSelected.forEach((_, index) => {
1304      this.buttonItemsSelected[index] = false
1305    })
1306    if (this.options.type === 'capsule' && (this.options.multiply ?? false)) {
1307      this.selectedIndexes.forEach(index => this.buttonItemsSelected[index] = true)
1308    } else {
1309      this.buttonItemsSelected[this.selectedIndexes[0]] = true
1310    }
1311  }
1312
1313  updateSelectedIndexes() {
1314    if (this.selectedIndexes === void 0) {
1315      this.selectedIndexes = []
1316    }
1317    if (this.options.type === 'tab' && this.selectedIndexes.length === 0) {
1318      this.selectedIndexes[0] = 0
1319    }
1320    if (this.selectedIndexes.length > 1) {
1321      if (this.options.type === 'tab') {
1322        this.selectedIndexes = [0]
1323      }
1324      if (this.options.type === 'capsule' && !(this.options.multiply ?? false)) {
1325        this.selectedIndexes = []
1326      }
1327    }
1328    let invalid = this.selectedIndexes.some(index => {
1329      return (index === void 0 || index < 0 || (this.options.buttons && index >= this.options.buttons.length))
1330    })
1331    if (invalid) {
1332      if (this.options.type === 'tab') {
1333        this.selectedIndexes = [0]
1334      } else {
1335        this.selectedIndexes = []
1336      }
1337    }
1338  }
1339
1340  onOptionsChange() {
1341    if (this.options === void 0 || this.options.buttons === void 0) {
1342      return
1343    }
1344    this.shouldMirror = this.isShouldMirror()
1345    this.updateSelectedIndexes()
1346    this.setItemsSelected()
1347    this.updateAnimatedProperty(null)
1348  }
1349
1350  onSelectedChange() {
1351    if (this.options === void 0 || this.options.buttons === void 0) {
1352      return
1353    }
1354    this.updateSelectedIndexes()
1355    this.setItemsSelected()
1356    if (this.doSelectedChangeAnimate) {
1357      this.updateAnimatedProperty(this.getSelectedChangeCurve())
1358    } else {
1359      this.updateAnimatedProperty(null)
1360    }
1361  }
1362
1363  aboutToAppear() {
1364    if (this.options === void 0 || this.options.buttons === void 0) {
1365      return
1366    }
1367    this.options.onButtonsChange = () => {
1368      if (this.options.type === 'tab') {
1369        this.selectedIndexes = [0]
1370      } else {
1371        this.selectedIndexes = []
1372      }
1373    }
1374    this.shouldMirror = this.isShouldMirror()
1375    this.updateSelectedIndexes()
1376    this.setItemsSelected()
1377    this.updateAnimatedProperty(null)
1378  }
1379
1380  private isMouseWheelScroll(event: GestureEvent) {
1381    return event.source === SourceType.Mouse && !this.isPanGestureMoved
1382  }
1383
1384  private isMovedFromPanGestureStartPoint(x: number, y: number) {
1385    return !nearEqual(x, this.panGestureStartPoint.x) || !nearEqual(y, this.panGestureStartPoint.y)
1386  }
1387
1388  private isShouldMirror(): boolean {
1389    if (this.options.direction == Direction.Rtl) {
1390      return true
1391    }
1392    // 获取系统语言
1393    try {
1394      let systemLanguage: string = I18n.System.getSystemLanguage();
1395      if (I18n.isRTL(systemLanguage) && this.options.direction != Direction.Ltr) {
1396        return true
1397      }
1398    } catch (error) {
1399      console.error(`Ace SegmentButton getSystemLanguage, error: ${error.toString()}`);
1400    }
1401    return false
1402  }
1403
1404  private isSegmentFocusStyleCustomized(): boolean {
1405    if (this.isCustomizedCache === undefined) {
1406      this.isCustomizedCache = resourceToNumber(
1407        this.getUIContext()?.getHostContext(),
1408        segmentButtonTheme.SEGMENT_FOCUS_STYLE_CUSTOMIZED,
1409        1.0
1410      ) < 0.1; //PC platform returns 0.0, default returns 1.0, using <0.1 to differentiate platform styles.
1411    }
1412    return this.isCustomizedCache;
1413  }
1414
1415  build() {
1416    Stack() {
1417      if (this.options !== void 0 && this.options.buttons != void 0) {
1418        if (this.options.type === 'capsule' && (this.options.multiply ?? false)) {
1419          MultiSelectBackground({
1420            optionsArray: this.options.buttons,
1421            options: this.options,
1422          })
1423        } else {
1424          Stack() {
1425            if (this.options.buttons !== void 0 && this.options.buttons.length > 1) {
1426              PressAndHoverEffectArray({
1427                options: this.options,
1428                buttons: this.options.buttons,
1429                pressArray: this.pressArray,
1430                hoverArray: this.hoverArray,
1431                hoverColorArray: this.hoverColorArray
1432              })
1433            }
1434          }
1435          .direction(this.options.direction)
1436          .size(this.componentSize)
1437          .backgroundColor(this.options.backgroundColor ?? segmentButtonTheme.BACKGROUND_COLOR)
1438          .borderRadius(getBackgroundBorderRadius(
1439            this.options,
1440            this.componentSize.height as number / 2
1441          ))
1442          .backgroundBlurStyle(this.options.backgroundBlurStyle, undefined, { disableSystemAdaptation: true })
1443        }
1444        Stack() {
1445          if (this.options.type === 'capsule' && (this.options.multiply ?? false)) {
1446            MultiSelectItemArray({
1447              optionsArray: this.options.buttons,
1448              options: this.options,
1449              selectedIndexes: $selectedIndexes
1450            })
1451          } else {
1452            SelectItem({
1453              optionsArray: this.options.buttons,
1454              options: this.options,
1455              selectedIndexes: $selectedIndexes
1456            })
1457          }
1458        }
1459        .direction(this.options.direction)
1460        .size(this.componentSize)
1461        .animation({ duration: 0 })
1462        .borderRadius(getBackgroundBorderRadius(
1463          this.options,
1464          this.componentSize.height as number / 2
1465        ))
1466        .clip(true)
1467
1468        SegmentButtonItemArrayComponent({
1469          pressArray: this.pressArray,
1470          hoverArray: this.hoverArray,
1471          hoverColorArray: this.hoverColorArray,
1472          optionsArray: this.options.buttons,
1473          options: this.options,
1474          selectedIndexes: $selectedIndexes,
1475          maxFontScale: this.getMaxFontSize(),
1476          onItemClicked: this.onItemClicked,
1477          isSegmentFocusStyleCustomized: this.isSegmentFocusStyleCustomized()
1478        })
1479      }
1480    }
1481    .direction(this.options ? this.options.direction : undefined)
1482    .onBlur(() => {
1483      this.focusIndex = -1
1484    })
1485    .onKeyEvent((event: KeyEvent) => {
1486      if (this.options === void 0 || this.options.buttons === void 0) {
1487        return
1488      }
1489      if (event.type === KeyType.Down) {
1490        if (event.keyCode === KeyCode.KEYCODE_SPACE || event.keyCode === KeyCode.KEYCODE_ENTER ||
1491          event.keyCode === KeyCode.KEYCODE_NUMPAD_ENTER) {
1492          if (this.options.type === 'capsule' && (this.options.multiply ?? false)) {
1493            if (this.selectedIndexes.indexOf(this.focusIndex) === -1) {
1494              // Select
1495              this.selectedIndexes.push(this.focusIndex)
1496            } else {
1497              // Unselect
1498              this.selectedIndexes.splice(this.selectedIndexes.indexOf(this.focusIndex), 1)
1499            }
1500          } else {
1501            // Pressed
1502            this.selectedIndexes[0] = this.focusIndex
1503          }
1504        }
1505      }
1506    })
1507    .accessibilityLevel('no')
1508    .priorityGesture(
1509      GestureGroup(GestureMode.Parallel,
1510        TapGesture()
1511          .onAction((event: GestureEvent) => {
1512            if (this.isGestureInProgress) {
1513              return;
1514            }
1515            let fingerInfo = event.fingerList.find(Boolean)
1516            if (fingerInfo === void 0) {
1517              return
1518            }
1519            if (this.options === void 0 || this.options.buttons === void 0) {
1520              return
1521            }
1522            let selectedInfo = fingerInfo.localX
1523
1524            let buttonLength: number = Math.min(this.options.buttons.length, this.buttonItemsSize.length)
1525            for (let i = 0; i < buttonLength; i++) {
1526              selectedInfo = selectedInfo - (this.buttonItemsSize[i].width as number)
1527              if (selectedInfo >= 0) {
1528                continue
1529              }
1530              this.doSelectedChangeAnimate =
1531                this.selectedIndexes[0] > Math.min(this.options.buttons.length,
1532                  this.buttonItemsSize.length) ? false : true
1533
1534              let realClickIndex: number = this.isShouldMirror() ? buttonLength - 1 - i : i
1535              if (this.onItemClicked) {
1536                this.onItemClicked(realClickIndex)
1537              }
1538              if (this.options.type === 'capsule' && (this.options.multiply ?? false)) {
1539                let selectedIndex: number = this.selectedIndexes.indexOf(realClickIndex)
1540                if (selectedIndex === -1) {
1541                  this.selectedIndexes.push(realClickIndex)
1542                } else {
1543                  this.selectedIndexes.splice(selectedIndex, 1)
1544                }
1545              } else {
1546                this.selectedIndexes[0] = realClickIndex
1547              }
1548              this.doSelectedChangeAnimate = false
1549              break
1550            }
1551          }),
1552        SwipeGesture()
1553          .onAction((event: GestureEvent) => {
1554            if (this.options === void 0 || this.options.buttons === void 0 ||
1555              event.sourceTool === SourceTool.TOUCHPAD) {
1556              return
1557            }
1558            if (this.options.type === 'capsule' && (this.options.multiply ?? false)) {
1559              // Non swipe gesture in multi-select mode
1560              return
1561            }
1562            if (this.isCurrentPositionSelected) {
1563              return
1564            }
1565
1566            // Only handle horizontal swipes (angle between -45 to 45 degrees or 135 to 225 degrees)
1567            let isHorizontalSwipe = (Math.abs(event.angle) <= 45) || (Math.abs(event.angle) >= 135);
1568            if (!isHorizontalSwipe) {
1569              return;
1570            }
1571
1572            let isSwipeRight = Math.abs(event.angle) <= 45; // swipe right
1573            let isSwipeLeft = Math.abs(event.angle) >= 135; // swipe left
1574
1575            let isSwipeToNext = this.isShouldMirror() ? isSwipeLeft : isSwipeRight;
1576            let isSwipeToPrevious = this.isShouldMirror() ? isSwipeRight : isSwipeLeft;
1577
1578            if (isSwipeToNext && this.selectedIndexes[0] !== Math.min(this.options.buttons.length,
1579              this.buttonItemsSize.length) - 1) {
1580              // Move to next
1581              this.doSelectedChangeAnimate = true
1582              this.selectedIndexes[0] = this.selectedIndexes[0] + 1
1583              this.doSelectedChangeAnimate = false
1584            } else if (isSwipeToPrevious && this.selectedIndexes[0] !== 0) {
1585              // Move to previous
1586              this.doSelectedChangeAnimate = true
1587              this.selectedIndexes[0] = this.selectedIndexes[0] - 1
1588              this.doSelectedChangeAnimate = false
1589            }
1590          }),
1591        PanGesture({direction: PanDirection.Horizontal})
1592          .onActionStart((event: GestureEvent) => {
1593            this.isGestureInProgress = true;
1594            if (this.options === void 0 || this.options.buttons === void 0) {
1595              return
1596            }
1597            if (this.options.type === 'capsule' && (this.options.multiply ?? false)) {
1598              // Non drag gesture in multi-select mode
1599              return
1600            }
1601            let fingerInfo = event.fingerList.find(Boolean)
1602            if (fingerInfo === void 0) {
1603              return
1604            }
1605            let selectedInfo = fingerInfo.localX
1606            this.panGestureStartPoint = { x: fingerInfo.globalX, y: fingerInfo.globalY }
1607            this.isPanGestureMoved = false
1608
1609            let buttonLength: number = Math.min(this.options.buttons.length, this.buttonItemsSize.length);
1610            for (let i = 0; i < buttonLength; i++) {
1611              selectedInfo = selectedInfo - (this.buttonItemsSize[i].width as number)
1612              if (selectedInfo < 0) {
1613                let realIndex = this.isShouldMirror() ? buttonLength - 1 - i : i;
1614                this.isCurrentPositionSelected = realIndex === this.selectedIndexes[0] ? true : false;
1615                break
1616              }
1617            }
1618          })
1619          .onActionUpdate((event: GestureEvent) => {
1620            if (this.options === void 0 || this.options.buttons === void 0) {
1621              return
1622            }
1623            if (this.options.type === 'capsule' && (this.options.multiply ?? false)) {
1624              // Non drag gesture in multi-select mode
1625              return
1626            }
1627            if (!this.isCurrentPositionSelected) {
1628              return
1629            }
1630            let fingerInfo = event.fingerList.find(Boolean)
1631            if (fingerInfo === void 0) {
1632              return
1633            }
1634            let selectedInfo = fingerInfo.localX
1635            if (!this.isPanGestureMoved && this.isMovedFromPanGestureStartPoint(fingerInfo.globalX,
1636              fingerInfo.globalY)) {
1637              this.isPanGestureMoved = true
1638            }
1639
1640            let buttonLength: number = Math.min(this.options.buttons.length, this.buttonItemsSize.length);
1641            for (let i = 0; i < buttonLength; i++) {
1642              selectedInfo = selectedInfo - (this.buttonItemsSize[i].width as number);
1643              if (selectedInfo < 0) {
1644                let realIndex = this.isShouldMirror() ? buttonLength - 1 - i : i;
1645                this.doSelectedChangeAnimate = true;
1646                this.selectedIndexes[0] = realIndex;
1647                this.doSelectedChangeAnimate = false;
1648                break;
1649              }
1650            }
1651            this.zoomScaleArray.forEach((_, index) => {
1652              if (index === this.selectedIndexes[0]) {
1653                animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => {
1654                  this.zoomScaleArray[index] = 0.95
1655                })
1656              } else {
1657                animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => {
1658                  this.zoomScaleArray[index] = 1
1659                })
1660              }
1661            })
1662          })
1663          .onActionEnd((event: GestureEvent) => {
1664            this.isGestureInProgress = false;
1665            if (this.options === void 0 || this.options.buttons === void 0) {
1666              return
1667            }
1668            if (this.options.type === 'capsule' && (this.options.multiply ?? false)) {
1669              // Non drag gesture in multi-select mode
1670              return
1671            }
1672            let fingerInfo = event.fingerList.find(Boolean)
1673            if (fingerInfo === void 0) {
1674              return
1675            }
1676            if (!this.isPanGestureMoved && this.isMovedFromPanGestureStartPoint(fingerInfo.globalX,
1677              fingerInfo.globalY)) {
1678              this.isPanGestureMoved = true
1679            }
1680            if (this.isMouseWheelScroll(event)) {
1681              let offset = event.offsetX !== 0 ? event.offsetX : event.offsetY
1682              this.doSelectedChangeAnimate = true
1683
1684              // Reverse mouse wheel direction in mirrored layout
1685              let shouldMoveNext = this.isShouldMirror() ? offset > 0 : offset < 0;
1686              let shouldMovePrevious = this.isShouldMirror() ? offset < 0 : offset > 0;
1687
1688              if (shouldMovePrevious && this.selectedIndexes[0] > 0) {
1689                this.selectedIndexes[0] -= 1;
1690              } else if (shouldMoveNext && this.selectedIndexes[0] < Math.min(this.options.buttons.length,
1691                this.buttonItemsSize.length) - 1) {
1692                this.selectedIndexes[0] += 1
1693              }
1694              this.doSelectedChangeAnimate = false
1695            }
1696            animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => {
1697              this.zoomScaleArray[this.selectedIndexes[0]] = 1
1698            })
1699            this.isCurrentPositionSelected = false
1700          })
1701          .onActionCancel(() => {
1702            this.isGestureInProgress = false;
1703            if (this.options === void 0 || this.options.buttons === void 0) {
1704              return
1705            }
1706            if (this.options.type === 'capsule' && (this.options.multiply ?? false)) {
1707              return
1708            }
1709            animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => {
1710              this.zoomScaleArray[this.selectedIndexes[0]] = 1
1711            })
1712            this.isCurrentPositionSelected = false
1713          })
1714      )
1715    )
1716  }
1717
1718  getMaxFontSize(): number {
1719    if (typeof this.maxFontScale === void 0) {
1720      return DEFAULT_MAX_FONT_SCALE;
1721    }
1722    if (typeof this.maxFontScale === 'number') {
1723      return Math.max(Math.min(this.maxFontScale, MAX_MAX_FONT_SCALE), MIN_MAX_FONT_SCALE);
1724    }
1725    const resourceManager = this.getUIContext().getHostContext()?.resourceManager;
1726    if (!resourceManager) {
1727      return DEFAULT_MAX_FONT_SCALE;
1728    }
1729    try {
1730      return resourceManager.getNumber(this.maxFontScale.id);
1731    } catch (error) {
1732      console.error(`Ace SegmentButton getMaxFontSize, error: ${error.toString()}`);
1733      return DEFAULT_MAX_FONT_SCALE;
1734    }
1735  }
1736
1737  getSelectedChangeCurve(): ICurve | null {
1738    if (this.options.type === 'capsule' && (this.options.multiply ?? false)) {
1739      return null
1740    }
1741    return curves.springMotion(0.347, 0.99)
1742  }
1743
1744  updateAnimatedProperty(curve: ICurve | null) {
1745    let setAnimatedPropertyFunc = () => {
1746      this.selectedItemPosition =
1747        this.selectedIndexes.length === 0 ? {} : this.buttonItemsPosition[this.selectedIndexes[0]]
1748      this.buttonItemsSelected.forEach((selected, index) => {
1749        this.buttonItemProperty[index].fontColor = selected ?
1750          this.options.selectedFontColor ?? (this.options.type === 'tab' ?
1751          segmentButtonTheme.TAB_SELECTED_FONT_COLOR : segmentButtonTheme.CAPSULE_SELECTED_FONT_COLOR) :
1752          this.options.fontColor ?? segmentButtonTheme.FONT_COLOR
1753      })
1754    }
1755    if (curve) {
1756      animateTo({ curve: curve }, setAnimatedPropertyFunc)
1757    } else {
1758      setAnimatedPropertyFunc()
1759    }
1760    this.buttonItemsSelected.forEach((selected, index) => {
1761      this.buttonItemProperty[index].fontSize = selected ? this.options.selectedFontSize ??
1762      segmentButtonTheme.SELECTED_FONT_SIZE : this.options.fontSize ?? segmentButtonTheme.FONT_SIZE
1763      this.buttonItemProperty[index].fontWeight = selected ? this.options.selectedFontWeight ?? FontWeight.Medium :
1764        this.options.fontWeight ?? FontWeight.Regular
1765      this.buttonItemProperty[index].isSelected = selected
1766    })
1767  }
1768}
1769
1770function resourceToNumber(context: Context | undefined, resource: Resource, defaultValue: number): number {
1771  if (!resource || !resource.type || !context) {
1772    console.error('[SegmentButton] failed: resource get fail.');
1773    return defaultValue;
1774  }
1775  let resourceManager = context?.resourceManager;
1776  if (!resourceManager) {
1777    console.error('[SegmentButton] failed to get resourceManager.');
1778    return defaultValue;
1779  }
1780  switch (resource.type) {
1781    case RESOURCE_TYPE_FLOAT:
1782    case RESOURCE_TYPE_INTEGER:
1783      try {
1784        if (resource.id !== -1) {
1785          return resourceManager.getNumber(resource);
1786        }
1787        return resourceManager.getNumberByName((resource.params as string[])[0].split('.')[2]);
1788      } catch (error) {
1789        console.error(`[SegmentButton] get resource error, return defaultValue`);
1790        return defaultValue;
1791      }
1792    default:
1793      return defaultValue;
1794  }
1795}
1796
1797class LengthMetricsUtils {
1798  private static instance?: LengthMetricsUtils;
1799
1800  private constructor() {
1801  }
1802
1803  public static getInstance(): LengthMetricsUtils {
1804    if (!LengthMetricsUtils.instance) {
1805      LengthMetricsUtils.instance = new LengthMetricsUtils();
1806    }
1807    return LengthMetricsUtils.instance;
1808  }
1809
1810  stringify(metrics: LengthMetrics): Dimension {
1811    switch (metrics.unit) {
1812      case LengthUnit.PX:
1813        return `${metrics.value}px`;
1814      case LengthUnit.VP:
1815        return `${metrics.value}vp`;
1816      case LengthUnit.FP:
1817        return `${metrics.value}fp`;
1818      case LengthUnit.PERCENT:
1819        return `${metrics.value}%`;
1820      case LengthUnit.LPX:
1821        return `${metrics.value}lpx`;
1822    }
1823  }
1824
1825  isNaturalNumber(metrics: LengthMetrics): boolean {
1826    return metrics.value >= 0;
1827  }
1828}
1829
1830function getBackgroundBorderRadius(options: SegmentButtonOptions, defaultRadius: number): Length {
1831  if (options.borderRadiusMode === BorderRadiusMode.CUSTOM) {
1832    // For capsule multi-select buttons, use itemBorderRadius
1833    if (options.type === 'capsule' && (options.multiply ?? false) && options.itemBorderRadius !== undefined) {
1834      return LengthMetricsUtils.getInstance().stringify(options.itemBorderRadius);
1835    } else if (options.backgroundBorderRadius !== undefined) {
1836      return LengthMetricsUtils.getInstance().stringify(options.backgroundBorderRadius);
1837    }
1838  }
1839
1840  if (options.type === 'capsule' && (options.multiply ?? false)) {
1841    return options.iconTextRadius ?? options.iconTextBackgroundRadius ?? defaultRadius;
1842  }
1843  return options.iconTextBackgroundRadius ?? defaultRadius;
1844}
1845
1846class FocusStyleButtonModifier implements AttributeModifier<ButtonAttribute> {
1847  private stateStyleAction?: (isFocused: boolean) => void;
1848
1849  constructor(stateStyleAction: (isFocused: boolean) => void) {
1850    this.stateStyleAction = stateStyleAction;
1851  }
1852
1853  applyNormalAttribute(instance: ButtonAttribute): void {
1854    this.stateStyleAction && this.stateStyleAction(false);
1855  }
1856
1857  applyFocusedAttribute(instance: ButtonAttribute): void {
1858    this.stateStyleAction && this.stateStyleAction(true);
1859  }
1860}