• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 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 */
15
16import { KeyCode } from '@ohos.multimodalInput.keyCode';
17import measure from '@ohos.measure';
18import mediaquery from '@ohos.mediaquery';
19import resourceManager from '@ohos.resourceManager';
20import { ColorMetrics, LengthMetrics, LengthUnit } from '@ohos.arkui.node';
21import EnvironmentCallback from '@ohos.app.ability.EnvironmentCallback';
22import { SymbolGlyphModifier } from '@ohos.arkui.modifier';
23import componentUtils from '@ohos.arkui.componentUtils';
24import hilog from '@ohos.hilog';
25
26export enum ChipSize {
27  NORMAL = "NORMAL",
28  SMALL = "SMALL"
29}
30
31enum BreakPointsType {
32  SM = "SM",
33  MD = "MD",
34  LG = "LG"
35}
36
37export interface IconCommonOptions {
38  src: ResourceStr;
39  size?: SizeOptions;
40  fillColor?: ResourceColor;
41  activatedFillColor?: ResourceColor;
42}
43
44export interface SuffixIconOptions extends IconCommonOptions {
45  action?: () => void;
46}
47
48export interface PrefixIconOptions extends IconCommonOptions {}
49
50export interface ChipSymbolGlyphOptions {
51  normal?: SymbolGlyphModifier;
52  activated?: SymbolGlyphModifier;
53}
54
55export interface LabelMarginOptions {
56  left?: Dimension;
57  right?: Dimension;
58}
59
60export interface LocalizedLabelMarginOptions {
61  start?: LengthMetrics;
62  end?: LengthMetrics;
63}
64
65export interface LabelOptions {
66  text: string;
67  fontSize?: Dimension;
68  fontColor?: ResourceColor;
69  activatedFontColor?: ResourceColor;
70  fontFamily?: string;
71  labelMargin?: LabelMarginOptions;
72  localizedLabelMargin?: LocalizedLabelMarginOptions;
73}
74
75interface IconTheme {
76  size: SizeOptions;
77  fillColor: ResourceColor;
78  activatedFillColor: ResourceColor;
79}
80
81interface PrefixIconTheme extends IconTheme {}
82
83interface SuffixIconTheme extends IconTheme {
84  defaultDeleteIcon: ResourceStr;
85  focusable: boolean;
86}
87
88interface DefaultSymbolTheme {
89  normalFontColor: Array<ResourceColor>;
90  activatedFontColor: Array<ResourceColor>;
91  fontSize: Length;
92  defaultEffect: number;
93}
94
95interface LabelTheme {
96  normalFontSize: Dimension;
97  smallFontSize: Dimension;
98  fontColor: ResourceColor;
99  activatedFontColor: ResourceColor;
100  fontFamily: string;
101  normalMargin: Margin;
102  localizedNormalMargin: LocalizedMargin;
103  smallMargin: Margin;
104  localizedSmallMargin: LocalizedMargin;
105  defaultFontSize: Dimension;
106}
107
108interface ChipNodeOpacity {
109  normal: number;
110  hover: number;
111  pressed: number;
112  disabled: number;
113}
114
115interface ChipNodeConstraintWidth {
116  breakPointMinWidth: number,
117  breakPointSmMaxWidth: number,
118  breakPointMdMaxWidth: number,
119  breakPointLgMaxWidth: number,
120}
121
122interface ChipNodeTheme {
123  suitAgeScale: number;
124  minLabelWidth: Dimension;
125  normalHeight: Dimension;
126  smallHeight: Dimension;
127  enabled: boolean;
128  activated: boolean;
129  backgroundColor: ResourceColor;
130  activatedBackgroundColor: ResourceColor;
131  focusOutlineColor: ResourceColor;
132  normalBorderRadius: Dimension;
133  smallBorderRadius: Dimension;
134  borderWidth: number;
135  localizedNormalPadding: LocalizedPadding;
136  localizedSmallPadding: LocalizedPadding;
137  hoverBlendColor: ResourceColor;
138  pressedBlendColor: ResourceColor;
139  opacity: ChipNodeOpacity;
140  breakPointConstraintWidth: ChipNodeConstraintWidth;
141}
142
143interface ChipTheme {
144  prefixIcon: PrefixIconTheme;
145  label: LabelTheme;
146  suffixIcon: SuffixIconTheme;
147  defaultSymbol: DefaultSymbolTheme;
148  chipNode: ChipNodeTheme;
149}
150
151export const defaultTheme: ChipTheme = {
152  prefixIcon: {
153    size: { width: 16, height: 16 },
154    fillColor: $r('sys.color.ohos_id_color_secondary'),
155    activatedFillColor: $r('sys.color.ohos_id_color_text_primary_contrary'),
156  },
157  label: {
158    normalFontSize: $r('sys.float.ohos_id_text_size_button2'),
159    smallFontSize: $r('sys.float.ohos_id_text_size_button2'),
160    fontColor: $r('sys.color.ohos_id_color_text_primary'),
161    activatedFontColor: $r('sys.color.ohos_id_color_text_primary_contrary'),
162    fontFamily: "HarmonyOS Sans",
163    normalMargin: { left: 6, right: 6, top: 0, bottom: 0 },
164    smallMargin: { left: 4, right: 4, top: 0, bottom: 0 },
165    defaultFontSize: 14,
166    localizedNormalMargin: {
167      start: LengthMetrics.vp(6),
168      end: LengthMetrics.vp(6),
169      top: LengthMetrics.vp(0),
170      bottom: LengthMetrics.vp(0)
171    },
172    localizedSmallMargin: {
173      start: LengthMetrics.vp(4),
174      end: LengthMetrics.vp(4),
175      top: LengthMetrics.vp(0),
176      bottom: LengthMetrics.vp(0),
177    }
178  },
179  suffixIcon: {
180    size: { width: 16, height: 16 },
181    fillColor: $r('sys.color.ohos_id_color_secondary'),
182    activatedFillColor: $r('sys.color.ohos_id_color_text_primary_contrary'),
183    defaultDeleteIcon: $r('sys.media.ohos_ic_public_cancel', 16, 16),
184    focusable: false,
185  },
186  defaultSymbol: {
187    normalFontColor: [$r('sys.color.ohos_id_color_secondary')],
188    activatedFontColor: [$r('sys.color.ohos_id_color_text_primary_contrary')],
189    fontSize: 16,
190    defaultEffect: -1,
191  },
192  chipNode: {
193    suitAgeScale: 1.75,
194    minLabelWidth: 12,
195    normalHeight: 36,
196    smallHeight: 28,
197    enabled: true,
198    activated: false,
199    backgroundColor: $r('sys.color.ohos_id_color_button_normal'),
200    activatedBackgroundColor: $r('sys.color.ohos_id_color_emphasize'),
201    focusOutlineColor: $r('sys.color.ohos_id_color_focused_outline'),
202    normalBorderRadius: $r('sys.float.ohos_id_corner_radius_tips_instant_tip'),
203    smallBorderRadius: $r('sys.float.ohos_id_corner_radius_piece'),
204    borderWidth: 2,
205    localizedNormalPadding: {
206      start: LengthMetrics.vp(16),
207      end: LengthMetrics.vp(16),
208      top: LengthMetrics.vp(4),
209      bottom: LengthMetrics.vp(4)
210    },
211    localizedSmallPadding: {
212      start: LengthMetrics.vp(12),
213      end: LengthMetrics.vp(12),
214      top: LengthMetrics.vp(4),
215      bottom: LengthMetrics.vp(4)
216    },
217    hoverBlendColor: $r('sys.color.ohos_id_color_hover'),
218    pressedBlendColor: $r('sys.color.ohos_id_color_click_effect'),
219    opacity: { normal: 1, hover: 0.95, pressed: 0.9, disabled: 0.4 },
220    breakPointConstraintWidth: {
221      breakPointMinWidth: 128,
222      breakPointSmMaxWidth: 156,
223      breakPointMdMaxWidth: 280,
224      breakPointLgMaxWidth: 400
225    }
226  }
227};
228
229const noop = () => {
230};
231
232interface ChipOptions {
233  prefixIcon?: PrefixIconOptions;
234  prefixSymbol?: ChipSymbolGlyphOptions;
235  label: LabelOptions;
236  suffixIcon?: SuffixIconOptions;
237  suffixSymbol?: ChipSymbolGlyphOptions;
238  allowClose?: boolean;
239  enabled?: boolean;
240  activated?: boolean;
241  backgroundColor?: ResourceColor;
242  activatedBackgroundColor?: ResourceColor;
243  borderRadius?: Dimension;
244  size?: ChipSize | SizeOptions;
245  direction?: Direction;
246  onClose?: () => void
247  onClicked?: () => void
248}
249
250@Builder
251export function Chip(options: ChipOptions) {
252  ChipComponent({
253    chipSize: options.size,
254    prefixIcon: options.prefixIcon,
255    prefixSymbol: options.prefixSymbol,
256    label: options.label,
257    suffixIcon: options.suffixIcon,
258    suffixSymbol: options.suffixSymbol,
259    allowClose: options.allowClose,
260    chipEnabled: options.enabled,
261    chipActivated: options.activated,
262    chipNodeBackgroundColor: options.backgroundColor,
263    chipNodeActivatedBackgroundColor: options.activatedBackgroundColor,
264    chipNodeRadius: options.borderRadius,
265    chipDirection: options.direction,
266    onClose: options.onClose,
267    onClicked: options.onClicked,
268  })
269}
270
271@Component
272export struct ChipComponent {
273  private theme: ChipTheme = defaultTheme;
274  @Prop chipSize: ChipSize | SizeOptions = ChipSize.NORMAL
275  @Prop allowClose: boolean = true
276  @Prop chipDirection: Direction = Direction.Auto
277  @Prop prefixIcon: PrefixIconOptions = { src: "" }
278  @Prop prefixSymbol: ChipSymbolGlyphOptions
279  @Prop label: LabelOptions = { text: "" }
280  @Prop suffixIcon: SuffixIconOptions = { src: "" }
281  @Prop suffixSymbol: ChipSymbolGlyphOptions
282  @Prop chipNodeBackgroundColor: ResourceColor = this.theme.chipNode.backgroundColor
283  @Prop chipNodeActivatedBackgroundColor: ResourceColor = this.theme.chipNode.activatedBackgroundColor
284  @Prop chipNodeRadius: Dimension | undefined = void (0)
285  @Prop chipEnabled: boolean = true
286  @Prop chipActivated: boolean = false
287  @State isHover: boolean = false
288  @State chipScale: ScaleOptions = { x: 1, y: 1 }
289  @State chipOpacity: number = 1
290  @State chipBlendColor: ResourceColor = Color.Transparent
291  @State deleteChip: boolean = false
292  @State chipNodeOnFocus: boolean = false
293  @State useDefaultSuffixIcon: boolean = false
294  private chipNodeSize: SizeOptions = {}
295  private onClose: () => void = noop
296  private onClicked: () => void = noop
297  @State suffixIconOnFocus: boolean = false
298  @State chipBreakPoints: BreakPointsType = BreakPointsType.SM
299  private smListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync("0vp<width<600vp")
300  private mdListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync("600vp<=width<840vp")
301  private lgListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync("840vp<=width")
302  @State private isShowPressedBackGroundColor: boolean = false
303  @State fontSizeScale: number | undefined = 0
304  @State fontWeightScale: number | undefined = 0
305  private callbacks: EnvironmentCallback = {
306    onConfigurationUpdated: (configuration) => {
307      this.fontSizeScale = configuration.fontSizeScale;
308      this.fontWeightScale = configuration.fontWeightScale;
309    }, onMemoryLevel() {
310    }
311  }
312  private callbackId: number | undefined = undefined
313  @State prefixSymbolWidth: Length | undefined = this.toVp(componentUtils.getRectangleById("PrefixSymbolGlyph")?.size?.width);
314  @State suffixSymbolWidth: Length | undefined = this.toVp(componentUtils.getRectangleById("SuffixSymbolGlyph")?.size?.width);
315  @State symbolEffect: SymbolEffect = new SymbolEffect();
316
317  private isChipSizeEnum(): boolean {
318    return typeof (this.chipSize) === 'string'
319  }
320
321  private getLabelFontSize(): Dimension {
322    if (this.label?.fontSize !== void (0) && this.toVp(this.label.fontSize) >= 0) {
323      return this.label.fontSize
324    } else {
325      if (this.isChipSizeEnum() && this.chipSize === ChipSize.SMALL) {
326        try {
327          resourceManager.getSystemResourceManager()
328            .getNumberByName((((this.theme.label.smallFontSize as Resource).params as string[])[0]).split('.')[2])
329          return this.theme.label.smallFontSize
330        } catch (error) {
331          return this.theme.label.defaultFontSize
332        }
333      } else {
334        try {
335          resourceManager.getSystemResourceManager()
336            .getNumberByName((((this.theme.label.normalFontSize as Resource).params as string[])[0]).split('.')[2])
337          return this.theme.label.normalFontSize
338        } catch (error) {
339          return this.theme.label.defaultFontSize
340        }
341      }
342    }
343  }
344
345  private getLabelFontColor(): ResourceColor {
346    if (this.getChipActive()) {
347      return this.label?.activatedFontColor ?? this.theme.label.activatedFontColor
348    }
349    return this.label?.fontColor ?? this.theme.label.fontColor
350  }
351
352  private getLabelFontFamily(): string {
353    return this.label?.fontFamily ?? this.theme.label.fontFamily
354  }
355
356  private getLabelFontWeight(): FontWeight {
357    if (this.getChipActive()) {
358      return FontWeight.Medium
359    }
360    return FontWeight.Regular
361  }
362
363  private lengthMetricsToVp(lengthMetrics?: LengthMetrics): number {
364    let defaultValue: number = 0;
365    if (lengthMetrics) {
366      switch (lengthMetrics.unit) {
367        case LengthUnit.PX:
368          return px2vp(lengthMetrics.value)
369        case LengthUnit.VP:
370          return lengthMetrics.value
371        case LengthUnit.FP:
372          px2vp(fp2px(lengthMetrics.value))
373          break
374        case LengthUnit.PERCENT:
375          return Number.NEGATIVE_INFINITY
376        case LengthUnit.LPX:
377          return px2vp(lpx2px(lengthMetrics.value))
378      }
379    }
380    return defaultValue;
381  }
382
383  private toVp(value: Dimension | Length | undefined): number {
384    if (value === void (0)) {
385      return Number.NEGATIVE_INFINITY
386    }
387    switch (typeof (value)) {
388      case 'number':
389        return value as number
390      case 'object':
391        try {
392          if ((value as Resource).id !== -1) {
393            return px2vp(getContext(this).resourceManager.getNumber((value as Resource).id))
394          } else {
395            return px2vp(getContext(this)
396              .resourceManager
397              .getNumberByName(((value.params as string[])[0]).split('.')[2]))
398          }
399        } catch (error) {
400          return Number.NEGATIVE_INFINITY
401        }
402      case 'string':
403        let regex: RegExp = new RegExp("(-?\\d+(?:\\.\\d+)?)_?(fp|vp|px|lpx|%)?$", "i");
404        let matches: RegExpMatchArray | null = value.match(regex);
405        if (!matches) {
406          return Number.NEGATIVE_INFINITY
407        }
408        let length: number = Number(matches?.[1] ?? 0);
409        let unit: string = matches?.[2] ?? 'vp'
410        switch (unit.toLowerCase()) {
411          case 'px':
412            length = px2vp(length)
413            break
414          case 'fp':
415            length = px2vp(fp2px(length))
416            break
417          case 'lpx':
418            length = px2vp(lpx2px(length))
419            break
420          case '%':
421            length = Number.NEGATIVE_INFINITY
422            break
423          case 'vp':
424            break
425          default:
426            break
427        }
428        return length
429      default:
430        return Number.NEGATIVE_INFINITY
431    }
432  }
433
434  private getLabelMargin(): Margin {
435    let labelMargin: Margin = { left: 0, right: 0 }
436    if (this.label?.labelMargin?.left !== void (0) && this.toVp(this.label.labelMargin.left) >= 0) {
437      labelMargin.left = this.label?.labelMargin?.left
438    } else if ((this.prefixSymbol?.normal || this.prefixSymbol?.activated) || this.prefixIcon?.src) {
439      if (this.isChipSizeEnum() && this.chipSize == ChipSize.SMALL) {
440        labelMargin.left = this.theme.label.smallMargin.left
441      } else {
442        labelMargin.left = this.theme.label.normalMargin.left
443      }
444    }
445    if (this.label?.labelMargin?.right !== void (0) && this.toVp(this.label.labelMargin.right) >= 0) {
446      labelMargin.right = this.label?.labelMargin?.right
447    } else if ((this.suffixSymbol?.normal || this.suffixSymbol?.activated) ||
448      this.suffixIcon?.src || this.useDefaultSuffixIcon) {
449      if (this.isChipSizeEnum() && this.chipSize == ChipSize.SMALL) {
450        labelMargin.right = this.theme.label.smallMargin.right
451      } else {
452        labelMargin.right = this.theme.label.normalMargin.right
453      }
454    }
455    return labelMargin
456  }
457
458  private getLocalizedLabelMargin(): LocalizedMargin {
459    let localizedLabelMargin: LocalizedMargin = { start: LengthMetrics.vp(0), end: LengthMetrics.vp(0) }
460    if (this.label?.localizedLabelMargin?.start?.value !== void (0) &&
461      this.lengthMetricsToVp(this.label.localizedLabelMargin.start) >= 0) {
462      localizedLabelMargin.start = this.label?.localizedLabelMargin?.start
463    } else if ((this.prefixSymbol?.normal || this.prefixSymbol?.activated) || this.prefixIcon?.src) {
464      if (this.isChipSizeEnum() && this.chipSize == ChipSize.SMALL) {
465        localizedLabelMargin.start = this.theme.label.localizedSmallMargin.start
466      } else {
467        localizedLabelMargin.start = this.theme.label.localizedNormalMargin.start
468      }
469    }
470    if (this.label?.localizedLabelMargin?.end?.value !== void (0) &&
471      this.lengthMetricsToVp(this.label.localizedLabelMargin.end) >= 0) {
472      localizedLabelMargin.end = this.label?.localizedLabelMargin?.end
473    } else if ((this.suffixSymbol?.normal || this.suffixSymbol?.activated) ||
474      this.suffixIcon?.src || this.useDefaultSuffixIcon) {
475      if (this.isChipSizeEnum() && this.chipSize == ChipSize.SMALL) {
476        localizedLabelMargin.end = this.theme.label.localizedSmallMargin.end
477      } else {
478        localizedLabelMargin.end = this.theme.label.localizedNormalMargin.end
479      }
480    }
481    return localizedLabelMargin
482  }
483
484  private getLabelStartEndVp(): LocalizedMargin {
485    let labelMargin: LocalizedMargin = this.getLocalizedLabelMargin()
486    if (this.label && (this.label.labelMargin !== void (0)) && (this.label.localizedLabelMargin === void (0))) {
487      let margin: Margin = this.getLabelMargin()
488      return {
489        start: LengthMetrics.vp(this.toVp(margin.left)),
490        end: LengthMetrics.vp(this.toVp(margin.right))
491      }
492    }
493    return {
494      start: LengthMetrics.vp(this.lengthMetricsToVp(labelMargin.start)),
495      end: LengthMetrics.vp(this.lengthMetricsToVp(labelMargin.end))
496    }
497  }
498
499  private getActualLabelMargin(): Margin | LocalizedMargin {
500    let localizedLabelMargin: LocalizedMargin = this.getLocalizedLabelMargin()
501    if (this.label && this.label.localizedLabelMargin !== void (0)) {
502      return localizedLabelMargin
503    }
504    if (this.label && this.label.labelMargin !== void (0)) {
505      return this.getLabelMargin()
506    }
507    return localizedLabelMargin
508  }
509
510  private getSuffixIconSize(): SizeOptions {
511    let suffixIconSize: SizeOptions = { width: 0, height: 0 }
512    if (this.suffixIcon?.size?.width !== void (0) && this.toVp(this.suffixIcon?.size?.width) >= 0) {
513      suffixIconSize.width = this.suffixIcon?.size?.width
514    } else {
515      if (this.getSuffixIconSrc()) {
516        suffixIconSize.width = this.theme.suffixIcon.size.width
517      } else {
518        suffixIconSize.width = 0
519      }
520    }
521    if (this.suffixIcon?.size?.height !== void (0) && this.toVp(this.suffixIcon?.size?.height) >= 0) {
522      suffixIconSize.height = this.suffixIcon?.size?.height
523    } else {
524      if (this.getSuffixIconSrc()) {
525        suffixIconSize.height = this.theme.suffixIcon.size.height
526      } else {
527        suffixIconSize.height = 0
528      }
529    }
530    return suffixIconSize
531  }
532
533  private getPrefixIconSize(): SizeOptions {
534    let prefixIconSize: SizeOptions = { width: 0, height: 0 }
535    if (this.prefixIcon?.size?.width !== void (0) && this.toVp(this.prefixIcon?.size?.width) >= 0) {
536      prefixIconSize.width = this.prefixIcon?.size?.width
537    } else {
538      if (this.prefixIcon?.src) {
539        prefixIconSize.width = this.theme.prefixIcon.size.width
540      } else {
541        prefixIconSize.width = 0
542      }
543    }
544    if (this.prefixIcon?.size?.height !== void (0) && this.toVp(this.prefixIcon?.size?.height) >= 0) {
545      prefixIconSize.height = this.prefixIcon?.size?.height
546    } else {
547      if (this.prefixIcon?.src) {
548        prefixIconSize.height = this.theme.prefixIcon.size.height
549      } else {
550        prefixIconSize.height = 0
551      }
552    }
553    return prefixIconSize
554  }
555
556  private getPrefixIconFilledColor(): ResourceColor {
557    if (this.getChipActive()) {
558      return this.prefixIcon?.activatedFillColor ?? this.theme.prefixIcon.activatedFillColor
559    }
560    return this.prefixIcon?.fillColor ?? this.theme.prefixIcon.fillColor
561  }
562
563  private getSuffixIconFilledColor(): ResourceColor {
564    if (this.getChipActive()) {
565      return this.suffixIcon?.activatedFillColor ?? this.theme.suffixIcon.activatedFillColor
566    }
567    return this.suffixIcon?.fillColor ?? this.theme.suffixIcon.fillColor
568  }
569
570  private getDefaultSymbolColor(): Array<ResourceColor> {
571    if (this.getChipActive()) {
572      return this.theme.defaultSymbol.activatedFontColor
573    }
574    return this.theme.defaultSymbol.normalFontColor
575  }
576
577  private getPrefixSymbolModifier(): SymbolGlyphModifier | undefined {
578    if (this.getChipActive()) {
579      return this.prefixSymbol?.activated
580    }
581    return this.prefixSymbol?.normal
582  }
583
584  private getSuffixSymbolModifier(): SymbolGlyphModifier | undefined {
585    if (this.getChipActive()) {
586      return this.suffixSymbol?.activated
587    }
588    return this.suffixSymbol?.normal
589  }
590
591  private getSuffixIconFocusable(): boolean {
592    return (this.useDefaultSuffixIcon && (this.allowClose ?? true)) || this.suffixIcon?.action !== void (0)
593  }
594
595  private getChipNodePadding(): LocalizedPadding {
596    return (this.isChipSizeEnum() && this.chipSize === ChipSize.SMALL) ? this.theme.chipNode.localizedSmallPadding : this.theme.chipNode.localizedNormalPadding
597  }
598
599  private getChipNodeRadius(): Dimension {
600    if (this.chipNodeRadius !== void (0) && this.toVp(this.chipNodeRadius) >= 0) {
601      return this.chipNodeRadius as Dimension
602    } else {
603      return ((this.isChipSizeEnum() && this.chipSize === ChipSize.SMALL) ?
604      this.theme.chipNode.smallBorderRadius : this.theme.chipNode.normalBorderRadius)
605    }
606  }
607
608  private getChipNodeBackGroundColor(): ResourceColor {
609    let currentColor: ResourceColor;
610
611    if (this.getChipActive()) {
612      currentColor = this.chipNodeActivatedBackgroundColor ?? this.theme.chipNode.activatedBackgroundColor
613    } else {
614      currentColor = this.chipNodeBackgroundColor ?? this.theme.chipNode.backgroundColor
615    }
616
617    let sourceColor: ColorMetrics;
618
619    try {
620      sourceColor = ColorMetrics.resourceColor(currentColor);
621    } catch (err) {
622      hilog.error(0x3900, 'Ace', `Chip resourceColor, error: ${err.toString()}`);
623      sourceColor = ColorMetrics.resourceColor(Color.Transparent);
624    }
625    if (!this.isShowPressedBackGroundColor) {
626      return sourceColor.color
627    }
628
629    return sourceColor
630      .blendColor(ColorMetrics.resourceColor("#19000000"))
631      .color
632  }
633
634  private getChipNodeHeight(): Length {
635    if (this.isChipSizeEnum()) {
636      return this.chipSize === ChipSize.SMALL ? this.theme.chipNode.smallHeight : this.theme.chipNode.normalHeight
637    } else {
638      this.chipNodeSize = this.chipSize as SizeOptions
639      return (this.chipNodeSize?.height !== void (0) && this.toVp(this.chipNodeSize?.height) >= 0) ?
640      this.toVp(this.chipNodeSize?.height) : this.theme.chipNode.normalHeight
641    }
642  }
643
644  private getLabelWidth(): number {
645    return px2vp(measure.measureText({
646      textContent: this.label?.text ?? "",
647      fontSize: this.getLabelFontSize(),
648      fontFamily: this.label?.fontFamily ?? this.theme.label.fontFamily,
649      fontWeight: this.getLabelFontWeight(),
650      maxLines: 1,
651      overflow: TextOverflow.Ellipsis,
652      textAlign: TextAlign.Center
653    }))
654  }
655
656  private getCalculateChipNodeWidth(): number {
657    let calWidth: number = 0
658    let startEndVp: LocalizedMargin = this.getLabelStartEndVp()
659    calWidth += this.getChipNodePadding().start?.value ?? 0
660    calWidth += this.toVp(this.getPrefixChipWidth())
661    calWidth += this.toVp(startEndVp.start?.value ?? 0)
662    calWidth += this.getLabelWidth()
663    calWidth += this.toVp(startEndVp.end?.value ?? 0)
664    calWidth += this.toVp(this.getSuffixChipWidth())
665    calWidth += this.getChipNodePadding().end?.value ?? 0
666    return calWidth
667  }
668
669  private getPrefixChipWidth(): Length | undefined {
670    if (this.prefixSymbol?.normal || this.prefixSymbol?.activated) {
671      return this.prefixSymbolWidth
672    } else if (this.prefixIcon?.src) {
673      return this.getPrefixIconSize().width
674    } else {
675      return 0
676    }
677  }
678
679  private getSuffixChipWidth(): Length | undefined {
680    if (this.suffixSymbol?.normal || this.suffixSymbol?.activated) {
681      return this.suffixSymbolWidth
682    } else if (this.suffixIcon?.src) {
683      return this.getSuffixIconSize().width
684    } else if (!this.suffixIcon?.src && (this.allowClose ?? true)) {
685      return this.theme.defaultSymbol.fontSize
686    } else {
687      return 0
688    }
689  }
690
691  private getReserveChipNodeWidth(): number {
692    return this.getCalculateChipNodeWidth() - this.getLabelWidth() + (this.theme.chipNode.minLabelWidth as number)
693  }
694
695  private getChipEnable(): boolean {
696    return this.chipEnabled || this.chipEnabled === void (0)
697  }
698
699  private getChipActive(): boolean {
700    return this.chipActivated
701  }
702
703  private getChipNodeOpacity(): number {
704    return this.getChipEnable() ? this.chipOpacity : this.theme.chipNode.opacity.disabled
705  }
706
707  private handleTouch(event: TouchEvent) {
708    if (!this.getChipEnable()) {
709      return
710    }
711    if (this.isHover) {
712      if (event.type === TouchType.Down || event.type === TouchType.Move) {
713        this.isShowPressedBackGroundColor = true
714      } else if (event.type === TouchType.Up) {
715        this.isShowPressedBackGroundColor = false
716      } else {
717        this.isShowPressedBackGroundColor = false
718      }
719    } else {
720      if (event.type === TouchType.Down || event.type === TouchType.Move) {
721        this.isShowPressedBackGroundColor = true
722      } else if (event.type === TouchType.Up) {
723        this.isShowPressedBackGroundColor = false
724      } else {
725        this.isShowPressedBackGroundColor = false
726      }
727    }
728  }
729
730  private hoverAnimate(isHover: boolean) {
731    if (!this.getChipEnable()) {
732      return
733    }
734    this.isHover = isHover
735    if (this.isHover) {
736      this.isShowPressedBackGroundColor = true
737    } else {
738      this.isShowPressedBackGroundColor = false
739    }
740  }
741
742  private deleteChipNodeAnimate() {
743    animateTo({ duration: 150, curve: Curve.Sharp }, () => {
744      this.chipOpacity = 0
745      this.chipBlendColor = Color.Transparent
746    })
747    animateTo({
748      duration: 150, curve: Curve.FastOutLinearIn, onFinish: () => {
749        this.deleteChip = true
750      }
751    },
752      () => {
753        this.chipScale = { x: 0.85, y: 0.85 }
754      })
755  }
756
757  private getSuffixIconSrc(): ResourceStr | undefined {
758    this.useDefaultSuffixIcon = !this.suffixIcon?.src && (this.allowClose ?? true)
759    return this.useDefaultSuffixIcon ? this.theme.suffixIcon.defaultDeleteIcon : (this.suffixIcon?.src ?? void (0))
760  }
761
762  private getChipNodeWidth(): Length {
763    if (!this.isChipSizeEnum()) {
764      this.chipNodeSize = this.chipSize as SizeOptions
765      if (this.chipNodeSize?.width !== void (0) && this.toVp(this.chipNodeSize.width) >= 0) {
766        return this.toVp(this.chipNodeSize.width)
767      }
768    }
769    let constraintWidth: ConstraintSizeOptions = this.getChipConstraintWidth()
770    return Math.min(Math.max(this.getCalculateChipNodeWidth(),
771      constraintWidth.minWidth as number), constraintWidth.maxWidth as number);
772  }
773
774  private getFocusOverlaySize(): SizeOptions {
775    return {
776      width: Math.max(this.getChipNodeWidth() as number, this.getChipConstraintWidth().minWidth as number) + 8,
777      height: this.getChipNodeHeight() as number + 8
778    }
779  }
780
781  private getChipConstraintWidth(): ConstraintSizeOptions {
782    let calcMinWidth: number = this.getReserveChipNodeWidth()
783
784    let constraintWidth: number = this.getCalculateChipNodeWidth()
785    let constraintSize: ConstraintSizeOptions
786    switch (this.chipBreakPoints) {
787      case BreakPointsType.SM:
788        constraintSize = {
789          minWidth: calcMinWidth,
790          maxWidth: Math.min(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointSmMaxWidth)
791        }
792        break
793      case BreakPointsType.MD:
794        constraintSize = {
795          minWidth: Math.max(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointMinWidth),
796          maxWidth: Math.min(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointMdMaxWidth)
797        }
798        break
799      case BreakPointsType.LG:
800        constraintSize = {
801          minWidth: Math.max(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointMinWidth),
802          maxWidth: Math.min(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointLgMaxWidth)
803        }
804        break
805      default:
806        constraintSize = { minWidth: calcMinWidth, maxWidth: constraintWidth }
807        break
808    }
809    constraintSize.minWidth = Math.min(Math.max(this.getCalculateChipNodeWidth(),
810      constraintSize.minWidth as number), constraintSize.maxWidth as number)
811    constraintSize.minHeight = this.getChipNodeHeight()
812    if (!this.isChipSizeEnum() && this.chipNodeSize?.height !== void (0) && this.toVp(this.chipNodeSize?.height) >= 0) {
813      constraintSize.maxHeight = this.toVp(this.chipNodeSize.height)
814      constraintSize.minHeight = this.toVp(this.chipNodeSize.height)
815    }
816    if (!this.isChipSizeEnum() && this.chipNodeSize?.width !== void (0) && this.toVp(this.chipNodeSize?.width) >= 0) {
817      constraintSize.minWidth = this.toVp(this.chipNodeSize.width)
818      constraintSize.maxWidth = this.toVp(this.chipNodeSize.width)
819    } else if (this.toVp(this.fontSizeScale) >= this.theme.chipNode.suitAgeScale) {
820      constraintSize.minWidth = void (0)
821      constraintSize.maxWidth = void (0)
822    }
823    return constraintSize
824  }
825
826  @Builder
827  focusOverlay() {
828    Stack() {
829      if (this.chipNodeOnFocus && !this.suffixIconOnFocus) {
830        Stack()
831          .direction(this.chipDirection)
832          .borderRadius(this.toVp(this.getChipNodeRadius()) + 4)
833          .size(this.getFocusOverlaySize())
834          .borderColor(this.theme.chipNode.focusOutlineColor)
835          .borderWidth(this.theme.chipNode.borderWidth)
836      }
837    }
838    .direction(this.chipDirection)
839    .size({ width: 1, height: 1 })
840    .align(Alignment.Center)
841  }
842
843  @Styles
844  suffixIconFocusStyles() {
845    .borderColor(this.theme.chipNode.focusOutlineColor)
846    .borderWidth(this.getSuffixIconFocusable() ? this.theme.chipNode.borderWidth : 0)
847  }
848
849  @Styles
850  suffixIconNormalStyles() {
851    .borderColor(Color.Transparent)
852    .borderWidth(0)
853  }
854
855  aboutToAppear() {
856    this.smListener.on("change", (mediaQueryResult: mediaquery.MediaQueryResult) => {
857      if (mediaQueryResult.matches) {
858        this.chipBreakPoints = BreakPointsType.SM
859      }
860    })
861    this.mdListener.on("change", (mediaQueryResult: mediaquery.MediaQueryResult) => {
862      if (mediaQueryResult.matches) {
863        this.chipBreakPoints = BreakPointsType.MD
864      }
865    })
866    this.lgListener.on("change", (mediaQueryResult: mediaquery.MediaQueryResult) => {
867      if (mediaQueryResult.matches) {
868        this.chipBreakPoints = BreakPointsType.LG
869      }
870    })
871    this.callbackId = this.getUIContext()
872      .getHostContext()
873    ?.getApplicationContext()
874    ?.on('environment', this.callbacks);
875  }
876
877  private getVisibility(): Visibility {
878    if (this.toVp(this.getChipNodeHeight()) > 0) {
879      return Visibility.Visible
880    } else {
881      return Visibility.None
882    }
883  }
884
885  aboutToDisappear() {
886    this.smListener.off("change")
887    this.mdListener.off("change")
888    this.lgListener.off("change")
889    if (this.callbackId) {
890      this.getUIContext()
891        .getHostContext()
892      ?.getApplicationContext()
893      ?.off('environment', this.callbackId);
894      this.callbackId = void (0)
895    }
896  }
897
898  @Builder
899  chipBuilder() {
900    Button() {
901      Row() {
902        if (this.prefixSymbol?.normal || this.prefixSymbol?.activated) {
903          SymbolGlyph()
904            .fontSize(this.theme.defaultSymbol.fontSize)
905            .fontColor(this.getDefaultSymbolColor())
906            .attributeModifier(this.getPrefixSymbolModifier())
907            .effectStrategy(SymbolEffectStrategy.NONE)
908            .symbolEffect(this.symbolEffect, false)
909            .symbolEffect(this.symbolEffect, this.theme.defaultSymbol.defaultEffect)
910            .onSizeChange((oldValue, newValue) => {
911              this.prefixSymbolWidth = newValue?.width
912            })
913            .key("PrefixSymbolGlyph")
914        } else if (this.prefixIcon?.src !== "") {
915          Image(this.prefixIcon?.src)
916            .direction(this.chipDirection)
917            .opacity(this.getChipNodeOpacity())
918            .size(this.getPrefixIconSize())
919            .fillColor(this.getPrefixIconFilledColor())
920            .enabled(this.getChipEnable())
921            .objectFit(ImageFit.Cover)
922            .focusable(false)
923            .flexShrink(0)
924            .visibility(this.getVisibility())
925            .draggable(false)
926        }
927
928        Text(this.label?.text ?? "")
929          .direction(this.chipDirection)
930          .opacity(this.getChipNodeOpacity())
931          .fontSize(this.getLabelFontSize())
932          .fontColor(this.getLabelFontColor())
933          .fontFamily(this.getLabelFontFamily())
934          .fontWeight(this.getLabelFontWeight())
935          .margin(this.getActualLabelMargin())
936          .enabled(this.getChipEnable())
937          .maxLines(1)
938          .textOverflow({ overflow: TextOverflow.Ellipsis })
939          .flexShrink(1)
940          .focusable(true)
941          .textAlign(TextAlign.Center)
942          .visibility(this.getVisibility())
943          .draggable(false)
944
945        if (this.suffixSymbol?.normal || this.suffixSymbol?.activated) {
946          SymbolGlyph()
947            .fontSize(this.theme.defaultSymbol.fontSize)
948            .fontColor(this.getDefaultSymbolColor())
949            .attributeModifier(this.getSuffixSymbolModifier())
950            .effectStrategy(SymbolEffectStrategy.NONE)
951            .symbolEffect(this.symbolEffect, false)
952            .symbolEffect(this.symbolEffect, this.theme.defaultSymbol.defaultEffect)
953            .onSizeChange((oldValue, newValue) => {
954              this.suffixSymbolWidth = newValue?.width
955            })
956            .key("SuffixSymbolGlyph")
957        } else if (this.suffixIcon?.src !== "") {
958          Image(this.getSuffixIconSrc())
959            .direction(this.chipDirection)
960            .opacity(this.getChipNodeOpacity())
961            .size(this.getSuffixIconSize())
962            .fillColor(this.getSuffixIconFilledColor())
963            .enabled(this.getChipEnable())
964            .focusable(this.getSuffixIconFocusable())
965            .objectFit(ImageFit.Cover)
966            .flexShrink(0)
967            .visibility(this.getVisibility())
968            .draggable(false)
969            .onFocus(() => {
970              this.suffixIconOnFocus = true
971            })
972            .onBlur(() => {
973              this.suffixIconOnFocus = false
974            })
975            .onClick(() => {
976              if (!this.getChipEnable()) {
977                return
978              }
979              if (this.suffixIcon?.action) {
980                this.suffixIcon.action()
981                return
982              }
983              if ((this.allowClose ?? true) && this.useDefaultSuffixIcon) {
984                this.onClose()
985                this.deleteChipNodeAnimate()
986                return
987              }
988              this.onClicked()
989            })
990        } else if (this.allowClose ?? true) {
991          SymbolGlyph($r('sys.symbol.xmark'))
992            .fontSize(this.theme.defaultSymbol.fontSize)
993            .fontColor(this.getDefaultSymbolColor())
994            .onClick(() => {
995              if (!this.getChipEnable()) {
996                return
997              }
998              this.onClose()
999              this.deleteChipNodeAnimate()
1000            })
1001        }
1002
1003      }
1004      .direction(this.chipDirection)
1005      .alignItems(VerticalAlign.Center)
1006      .justifyContent(FlexAlign.Center)
1007      .padding(this.getChipNodePadding())
1008      .constraintSize(this.getChipConstraintWidth())
1009    }
1010    .constraintSize(this.getChipConstraintWidth())
1011    .direction(this.chipDirection)
1012    .type(ButtonType.Normal)
1013    .clip(false)
1014    .backgroundColor(this.getChipNodeBackGroundColor())
1015    .borderRadius(this.getChipNodeRadius())
1016    .enabled(this.getChipEnable())
1017    .scale(this.chipScale)
1018    .focusable(true)
1019    .opacity(this.getChipNodeOpacity())
1020    .onFocus(() => {
1021      this.chipNodeOnFocus = true
1022    })
1023    .onBlur(() => {
1024      this.chipNodeOnFocus = false
1025    })
1026    .onTouch((event) => {
1027      this.handleTouch(event)
1028    })
1029    .onHover((isHover: boolean) => {
1030      if (isHover) {
1031        this.isShowPressedBackGroundColor = true
1032      } else {
1033        if (!this.isShowPressedBackGroundColor && isHover) {
1034          this.isShowPressedBackGroundColor = true
1035        } else {
1036          this.isShowPressedBackGroundColor = false
1037        }
1038      }
1039    })
1040    .onKeyEvent((event) => {
1041      if (event.type === KeyType.Down && event.keyCode === KeyCode.KEYCODE_FORWARD_DEL && !this.suffixIconOnFocus) {
1042        this.deleteChipNodeAnimate()
1043      }
1044    })
1045    .onClick(this.onClicked === noop ? undefined : this.onClicked.bind(this))
1046  }
1047
1048  build() {
1049    if (!this.deleteChip) {
1050      this.chipBuilder()
1051    }
1052  }
1053}
1054