@effindomv2/fui-as 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { BorderStyle, SemanticCheckedState, Unit } from "../../core/ffi";
2
2
  import { Theme } from "../../core/Theme";
3
3
  import { FlexBox, Svg } from "../../nodes";
4
+ import { LabeledControlSizing } from "../ControlSizing";
4
5
  import {
5
6
  PressableIndicatorMetrics,
6
7
  PressableIndicatorPresenter,
@@ -9,6 +10,30 @@ import {
9
10
 
10
11
  const CHECKBOX_CHECK_SVG_URL = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 14 14'><path d='M3.25 8.25 6.35 11.35 12.75 4.95' fill='none' stroke='%23000000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/></svg>";
11
12
 
13
+ class CheckboxIndicatorMetrics extends PressableIndicatorMetrics {
14
+ constructor(
15
+ readonly indicatorSize: f32,
16
+ readonly cornerRadius: f32,
17
+ readonly checkMarkSize: f32,
18
+ ) {
19
+ super(indicatorSize, indicatorSize);
20
+ }
21
+ }
22
+
23
+ const DEFAULT_CHECKBOX_METRICS = new CheckboxIndicatorMetrics(20.0, 4.0, 16.0);
24
+
25
+ function resolveCheckboxMetrics(sizing: LabeledControlSizing | null): CheckboxIndicatorMetrics {
26
+ if (sizing === null || !sizing.hasIndicatorSize) {
27
+ return DEFAULT_CHECKBOX_METRICS;
28
+ }
29
+ const indicatorSize = sizing.indicatorSizePx;
30
+ return new CheckboxIndicatorMetrics(
31
+ indicatorSize,
32
+ indicatorSize * 0.2,
33
+ indicatorSize * 0.8,
34
+ );
35
+ }
36
+
12
37
  export class CheckboxIndicatorVisualState extends PressableIndicatorVisualState {
13
38
  constructor(
14
39
  readonly checkedState: SemanticCheckedState,
@@ -34,23 +59,25 @@ export abstract class CheckboxIndicatorTemplate {
34
59
  }
35
60
 
36
61
  class DefaultCheckboxIndicatorPresenter extends CheckboxIndicatorPresenter {
62
+ private readonly geometry: CheckboxIndicatorMetrics;
37
63
  private readonly markNode: Svg;
38
64
 
39
- constructor() {
65
+ constructor(metrics: CheckboxIndicatorMetrics = DEFAULT_CHECKBOX_METRICS) {
40
66
  const root = new FlexBox()
41
- .width(20.0, Unit.Pixel)
42
- .height(20.0, Unit.Pixel)
67
+ .width(metrics.indicatorSize, Unit.Pixel)
68
+ .height(metrics.indicatorSize, Unit.Pixel)
43
69
  .alignItems(1)
44
70
  .justifyContent(1);
45
- super(root, new PressableIndicatorMetrics(20.0, 20.0));
71
+ super(root, metrics);
72
+ this.geometry = metrics;
46
73
  const markHost = new FlexBox()
47
74
  .fillSize()
48
75
  .alignItems(1)
49
76
  .justifyContent(1);
50
77
  const markNode = new Svg();
51
78
  markNode
52
- .width(16.0, Unit.Pixel)
53
- .height(16.0, Unit.Pixel);
79
+ .width(metrics.checkMarkSize, Unit.Pixel)
80
+ .height(metrics.checkMarkSize, Unit.Pixel);
54
81
  this.markNode = markNode;
55
82
  markHost.child(markNode);
56
83
  root.child(markHost);
@@ -59,6 +86,7 @@ class DefaultCheckboxIndicatorPresenter extends CheckboxIndicatorPresenter {
59
86
  apply(theme: Theme, state: CheckboxIndicatorVisualState): void {
60
87
  let background = theme.colors.surface;
61
88
  let borderColor = theme.colors.border;
89
+ const geometry = this.geometry;
62
90
  let markVisible = false;
63
91
  let markColor = theme.colors.textPrimary;
64
92
  if (state.checkedState == SemanticCheckedState.True || state.checkedState == SemanticCheckedState.Mixed) {
@@ -71,7 +99,7 @@ class DefaultCheckboxIndicatorPresenter extends CheckboxIndicatorPresenter {
71
99
  } else if (state.hovered) {
72
100
  background = theme.colors.background;
73
101
  }
74
- this.root.cornerRadius(theme.spacing.xs);
102
+ this.root.cornerRadius(geometry.cornerRadius);
75
103
  this.root.border(1.0, borderColor, BorderStyle.Solid);
76
104
  this.root.bgColor(background);
77
105
  if (markVisible) {
@@ -79,7 +107,12 @@ class DefaultCheckboxIndicatorPresenter extends CheckboxIndicatorPresenter {
79
107
  } else {
80
108
  this.markNode.clearSource();
81
109
  }
82
- this.markNode.tint(markColor);
110
+ this.markNode
111
+ .width(geometry.checkMarkSize, Unit.Pixel)
112
+ .height(geometry.checkMarkSize, Unit.Pixel)
113
+ .opacity(markVisible ? 1.0 : 0.0);
114
+ const markNode = this.markNode as Svg;
115
+ markNode.tint(markColor);
83
116
  }
84
117
  }
85
118
 
@@ -90,3 +123,7 @@ class DefaultCheckboxIndicatorTemplate extends CheckboxIndicatorTemplate {
90
123
  }
91
124
 
92
125
  export const defaultCheckboxIndicatorTemplate = new DefaultCheckboxIndicatorTemplate();
126
+
127
+ export function createDefaultCheckboxIndicatorPresenter(sizing: LabeledControlSizing | null = null): CheckboxIndicatorPresenter {
128
+ return new DefaultCheckboxIndicatorPresenter(resolveCheckboxMetrics(sizing));
129
+ }
@@ -1,9 +1,26 @@
1
1
  import { AlignItems, JustifyContent, Unit } from "../../core/ffi";
2
2
  import { Theme } from "../../core/Theme";
3
3
  import { FlexBox, Svg } from "../../nodes";
4
+ import { DropdownSizing } from "../ControlSizing";
4
5
 
5
6
  const DROPDOWN_CHEVRON_COLLAPSED_SVG = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M3 4.5 6 7.5 9 4.5' fill='none' stroke='%23000000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>";
6
7
  const DROPDOWN_CHEVRON_EXPANDED_SVG = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M3 7.5 6 4.5 9 7.5' fill='none' stroke='%23000000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>";
8
+ const DEFAULT_DROPDOWN_CHEVRON_ICON_SIZE: f32 = 12.0;
9
+
10
+ class DropdownChevronMetrics {
11
+ constructor(
12
+ readonly iconSize: f32,
13
+ ) {}
14
+ }
15
+
16
+ const DEFAULT_DROPDOWN_CHEVRON_METRICS = new DropdownChevronMetrics(DEFAULT_DROPDOWN_CHEVRON_ICON_SIZE);
17
+
18
+ function resolveChevronMetrics(sizing: DropdownSizing | null): DropdownChevronMetrics {
19
+ if (sizing === null || !sizing.hasChevronIconSize) {
20
+ return DEFAULT_DROPDOWN_CHEVRON_METRICS;
21
+ }
22
+ return new DropdownChevronMetrics(sizing.chevronIconSizePx);
23
+ }
7
24
 
8
25
  export class DropdownChevronVisualState {
9
26
  constructor(
@@ -30,29 +47,35 @@ export abstract class DropdownChevronTemplate {
30
47
  }
31
48
 
32
49
  class DefaultDropdownChevronPresenter extends DropdownChevronPresenter {
50
+ private readonly metrics: DropdownChevronMetrics;
33
51
  private readonly iconNode: Svg;
34
52
 
35
- constructor() {
53
+ constructor(metrics: DropdownChevronMetrics = DEFAULT_DROPDOWN_CHEVRON_METRICS) {
36
54
  const root = new FlexBox()
37
55
  .fillSize()
38
56
  .alignItems(AlignItems.Center)
39
57
  .justifyContent(JustifyContent.Center);
40
58
  const iconNode = new Svg()
41
- .width(12.0, Unit.Pixel)
42
- .height(12.0, Unit.Pixel) as Svg;
59
+ .width(metrics.iconSize, Unit.Pixel)
60
+ .height(metrics.iconSize, Unit.Pixel) as Svg;
43
61
  root.child(iconNode);
44
62
  super(root);
63
+ this.metrics = metrics;
45
64
  this.iconNode = iconNode;
46
65
  }
47
66
 
48
67
  apply(theme: Theme, state: DropdownChevronVisualState): void {
68
+ const metrics = this.metrics;
49
69
  this.root
50
70
  .fillSize()
51
71
  .alignItems(AlignItems.Center)
52
72
  .justifyContent(JustifyContent.Center);
53
73
  this.iconNode
54
- .source(state.open ? DROPDOWN_CHEVRON_EXPANDED_SVG : DROPDOWN_CHEVRON_COLLAPSED_SVG)
55
- .tint(!state.enabled ? theme.colors.textMuted : (state.hovered ? theme.colors.textPrimary : theme.colors.textMuted));
74
+ .width(metrics.iconSize, Unit.Pixel)
75
+ .height(metrics.iconSize, Unit.Pixel);
76
+ const iconNode = this.iconNode as Svg;
77
+ iconNode.source(state.open ? DROPDOWN_CHEVRON_EXPANDED_SVG : DROPDOWN_CHEVRON_COLLAPSED_SVG);
78
+ iconNode.tint(!state.enabled ? theme.colors.textMuted : (state.hovered ? theme.colors.textPrimary : theme.colors.textMuted));
56
79
  }
57
80
  }
58
81
 
@@ -63,3 +86,7 @@ class DefaultDropdownChevronTemplate extends DropdownChevronTemplate {
63
86
  }
64
87
 
65
88
  export const defaultDropdownChevronTemplate = new DefaultDropdownChevronTemplate();
89
+
90
+ export function createDefaultDropdownChevronPresenter(sizing: DropdownSizing | null = null): DropdownChevronPresenter {
91
+ return new DefaultDropdownChevronPresenter(resolveChevronMetrics(sizing));
92
+ }
@@ -8,8 +8,58 @@ import {
8
8
  } from "../../core/ffi";
9
9
  import { Theme } from "../../core/Theme";
10
10
  import { FlexBox, Text } from "../../nodes";
11
+ import { DropdownSizing } from "../ControlSizing";
11
12
 
12
13
  const DEFAULT_CHEVRON_BOX_SIZE: f32 = 16.0;
14
+ const DEFAULT_FIELD_PADDING_X: f32 = 16.0;
15
+ const DEFAULT_FIELD_PADDING_Y: f32 = 8.0;
16
+ const DEFAULT_FIELD_FONT_SIZE: f32 = 16.0;
17
+ const DEFAULT_FIELD_HEIGHT: f32 = 32.0;
18
+
19
+ export class DropdownFieldMetrics {
20
+ constructor(
21
+ readonly height: f32,
22
+ readonly fontSize: f32,
23
+ readonly chevronBoxSize: f32,
24
+ readonly paddingLeft: f32,
25
+ readonly paddingTop: f32,
26
+ readonly paddingRight: f32,
27
+ readonly paddingBottom: f32,
28
+ ) {}
29
+ }
30
+
31
+ const DEFAULT_DROPDOWN_FIELD_METRICS = new DropdownFieldMetrics(
32
+ DEFAULT_FIELD_HEIGHT,
33
+ DEFAULT_FIELD_FONT_SIZE,
34
+ DEFAULT_CHEVRON_BOX_SIZE,
35
+ DEFAULT_FIELD_PADDING_X,
36
+ DEFAULT_FIELD_PADDING_Y,
37
+ DEFAULT_FIELD_PADDING_X,
38
+ DEFAULT_FIELD_PADDING_Y,
39
+ );
40
+
41
+ function resolveFieldMetrics(sizing: DropdownSizing | null): DropdownFieldMetrics {
42
+ if (
43
+ sizing === null ||
44
+ (!sizing.hasFieldHeight && !sizing.hasFieldFontSize && !sizing.hasChevronBoxSize)
45
+ ) {
46
+ return DEFAULT_DROPDOWN_FIELD_METRICS;
47
+ }
48
+ const fontSize = sizing.hasFieldFontSize ? sizing.fieldFontSizePx : DEFAULT_DROPDOWN_FIELD_METRICS.fontSize;
49
+ const chevronBoxSize = sizing.hasChevronBoxSize ? sizing.chevronBoxSizePx : DEFAULT_DROPDOWN_FIELD_METRICS.chevronBoxSize;
50
+ const contentHeight = fontSize > chevronBoxSize ? fontSize : chevronBoxSize;
51
+ const height = sizing.hasFieldHeight ? sizing.fieldHeightPx : contentHeight + (DEFAULT_FIELD_PADDING_Y * 2.0);
52
+ const verticalPadding = height > contentHeight ? (height - contentHeight) * 0.5 : 0.0;
53
+ return new DropdownFieldMetrics(
54
+ height,
55
+ fontSize,
56
+ chevronBoxSize,
57
+ DEFAULT_FIELD_PADDING_X,
58
+ verticalPadding,
59
+ DEFAULT_FIELD_PADDING_X,
60
+ verticalPadding,
61
+ );
62
+ }
13
63
 
14
64
  export class DropdownFieldVisualState {
15
65
  constructor(
@@ -27,6 +77,7 @@ export abstract class DropdownFieldPresenter {
27
77
  private readonly valueHostValue: FlexBox,
28
78
  private readonly valueNodeValue: Text,
29
79
  private readonly chevronHostValue: FlexBox,
80
+ private readonly metricsValue: DropdownFieldMetrics = DEFAULT_DROPDOWN_FIELD_METRICS,
30
81
  ) {}
31
82
 
32
83
  get root(): FlexBox {
@@ -45,6 +96,10 @@ export abstract class DropdownFieldPresenter {
45
96
  return this.chevronHostValue;
46
97
  }
47
98
 
99
+ get metrics(): DropdownFieldMetrics {
100
+ return this.metricsValue;
101
+ }
102
+
48
103
  abstract apply(theme: Theme, state: DropdownFieldVisualState): void;
49
104
  }
50
105
 
@@ -53,7 +108,7 @@ export abstract class DropdownFieldTemplate {
53
108
  }
54
109
 
55
110
  class DefaultDropdownFieldPresenter extends DropdownFieldPresenter {
56
- constructor() {
111
+ constructor(metrics: DropdownFieldMetrics = DEFAULT_DROPDOWN_FIELD_METRICS) {
57
112
  const valueNode = new Text("")
58
113
  .selectable(false)
59
114
  .width(100.0, Unit.Percent)
@@ -66,8 +121,8 @@ class DefaultDropdownFieldPresenter extends DropdownFieldPresenter {
66
121
  .fillWidth()
67
122
  .child(valueNode) as FlexBox;
68
123
  const chevronHost = new FlexBox()
69
- .width(DEFAULT_CHEVRON_BOX_SIZE, Unit.Pixel)
70
- .height(DEFAULT_CHEVRON_BOX_SIZE, Unit.Pixel)
124
+ .width(metrics.chevronBoxSize, Unit.Pixel)
125
+ .height(metrics.chevronBoxSize, Unit.Pixel)
71
126
  .alignItems(AlignItems.Center)
72
127
  .justifyContent(JustifyContent.Center);
73
128
  const root = new FlexBox()
@@ -75,25 +130,27 @@ class DefaultDropdownFieldPresenter extends DropdownFieldPresenter {
75
130
  .alignItems(AlignItems.Center)
76
131
  .child(valueHost)
77
132
  .child(chevronHost);
78
- super(root, valueHost, valueNode, chevronHost);
133
+ super(root, valueHost, valueNode, chevronHost, metrics);
79
134
  }
80
135
 
81
136
  apply(theme: Theme, state: DropdownFieldVisualState): void {
137
+ const metrics = this.metrics;
82
138
  this.root
83
139
  .flexDirection(FlexDirection.Row)
84
140
  .alignItems(AlignItems.Center)
141
+ .height(metrics.height, Unit.Pixel)
85
142
  .cornerRadius(theme.spacing.sm)
86
143
  .border(2.0, theme.colors.border, BorderStyle.Solid)
87
- .padding(theme.spacing.md, theme.spacing.sm, theme.spacing.md, theme.spacing.sm)
144
+ .padding(metrics.paddingLeft, metrics.paddingTop, metrics.paddingRight, metrics.paddingBottom)
88
145
  .bgColor(state.pressed && state.enabled ? theme.colors.background : theme.colors.surface);
89
146
  this.valueHost
90
147
  .fillWidth();
91
148
  this.valueNode
92
- .font(theme.fonts.body, theme.fonts.sizeBody)
149
+ .font(theme.fonts.body, metrics.fontSize)
93
150
  .textColor(state.enabled ? theme.colors.textPrimary : theme.colors.textMuted);
94
151
  this.chevronHost
95
- .width(DEFAULT_CHEVRON_BOX_SIZE, Unit.Pixel)
96
- .height(DEFAULT_CHEVRON_BOX_SIZE, Unit.Pixel)
152
+ .width(metrics.chevronBoxSize, Unit.Pixel)
153
+ .height(metrics.chevronBoxSize, Unit.Pixel)
97
154
  .alignItems(AlignItems.Center)
98
155
  .justifyContent(JustifyContent.Center);
99
156
  }
@@ -106,3 +163,7 @@ class DefaultDropdownFieldTemplate extends DropdownFieldTemplate {
106
163
  }
107
164
 
108
165
  export const defaultDropdownFieldTemplate = new DefaultDropdownFieldTemplate();
166
+
167
+ export function createDefaultDropdownFieldPresenter(sizing: DropdownSizing | null = null): DropdownFieldPresenter {
168
+ return new DefaultDropdownFieldPresenter(resolveFieldMetrics(sizing));
169
+ }
@@ -1,13 +1,38 @@
1
- import { AlignItems, Unit } from "../../core/ffi";
1
+ import { AlignItems, TextVerticalAlign, Unit } from "../../core/ffi";
2
2
  import { Theme } from "../../core/Theme";
3
3
  import { FlexBox, Text } from "../../nodes";
4
+ import { DropdownSizing } from "../ControlSizing";
4
5
 
5
6
  export class DropdownOptionRowMetrics {
6
7
  constructor(
7
8
  readonly height: f32,
9
+ readonly paddingLeft: f32 = 10.0,
10
+ readonly paddingTop: f32 = 6.0,
11
+ readonly paddingRight: f32 = 10.0,
12
+ readonly paddingBottom: f32 = 6.0,
13
+ readonly fontSize: f32 = 16.0,
8
14
  ) {}
9
15
  }
10
16
 
17
+ const DEFAULT_DROPDOWN_OPTION_ROW_METRICS = new DropdownOptionRowMetrics(34.0);
18
+
19
+ function resolveOptionRowMetrics(sizing: DropdownSizing | null): DropdownOptionRowMetrics {
20
+ if (sizing === null || (!sizing.hasOptionHeight && !sizing.hasOptionFontSize)) {
21
+ return DEFAULT_DROPDOWN_OPTION_ROW_METRICS;
22
+ }
23
+ const fontSize = sizing.hasOptionFontSize ? sizing.optionFontSizePx : DEFAULT_DROPDOWN_OPTION_ROW_METRICS.fontSize;
24
+ const height = sizing.hasOptionHeight ? sizing.optionHeightPx : DEFAULT_DROPDOWN_OPTION_ROW_METRICS.height;
25
+ const verticalPadding = height > fontSize ? (height - fontSize) * 0.5 : 0.0;
26
+ return new DropdownOptionRowMetrics(
27
+ height,
28
+ DEFAULT_DROPDOWN_OPTION_ROW_METRICS.paddingLeft,
29
+ verticalPadding,
30
+ DEFAULT_DROPDOWN_OPTION_ROW_METRICS.paddingRight,
31
+ verticalPadding,
32
+ fontSize,
33
+ );
34
+ }
35
+
11
36
  export class DropdownOptionRowVisualState {
12
37
  constructor(
13
38
  readonly highlighted: bool,
@@ -43,27 +68,30 @@ export abstract class DropdownOptionRowTemplate {
43
68
  }
44
69
 
45
70
  class DefaultDropdownOptionRowPresenter extends DropdownOptionRowPresenter {
46
- constructor() {
71
+ constructor(metrics: DropdownOptionRowMetrics = DEFAULT_DROPDOWN_OPTION_ROW_METRICS) {
47
72
  const labelNode = new Text("")
48
73
  .selectable(false)
49
74
  .width(100.0, Unit.Percent)
50
75
  .maxLines(1)
51
76
  .wrapping(false) as Text;
52
- labelNode.overflowFade(true, false);
77
+ labelNode
78
+ .overflowFade(true, false)
79
+ .verticalAlign(TextVerticalAlign.Center);
53
80
  const root = new FlexBox()
54
81
  .fillSize()
55
82
  .alignItems(AlignItems.Center)
56
83
  .child(labelNode);
57
- super(root, labelNode, new DropdownOptionRowMetrics(34.0));
84
+ super(root, labelNode, metrics);
58
85
  }
59
86
 
60
87
  apply(theme: Theme, state: DropdownOptionRowVisualState): void {
88
+ const metrics = this.metrics;
61
89
  this.root
62
- .padding(10.0, 6.0, 10.0, 6.0)
90
+ .padding(metrics.paddingLeft, metrics.paddingTop, metrics.paddingRight, metrics.paddingBottom)
63
91
  .cornerRadius(theme.spacing.xs)
64
92
  .bgColor(state.highlighted ? theme.contextMenu.item.hoverBackground : 0x00000000);
65
93
  this.labelNode
66
- .font(theme.fonts.body, theme.fonts.sizeBody)
94
+ .font(theme.fonts.body, metrics.fontSize)
67
95
  .textColor(
68
96
  !state.enabled
69
97
  ? theme.colors.textMuted
@@ -79,3 +107,7 @@ class DefaultDropdownOptionRowTemplate extends DropdownOptionRowTemplate {
79
107
  }
80
108
 
81
109
  export const defaultDropdownOptionRowTemplate = new DefaultDropdownOptionRowTemplate();
110
+
111
+ export function createDefaultDropdownOptionRowPresenter(sizing: DropdownSizing | null = null): DropdownOptionRowPresenter {
112
+ return new DefaultDropdownOptionRowPresenter(resolveOptionRowMetrics(sizing));
113
+ }
@@ -30,6 +30,7 @@ export class PressableLabeledControl extends FlexBox {
30
30
  private readonly labelHost: FlexBox;
31
31
  private readonly disposables: Array<Disposable> = new Array<Disposable>();
32
32
  private disposed: bool = false;
33
+ private labelFontSizeOverride: f32 = 0.0;
33
34
  protected hoveredState: bool = false;
34
35
  protected pressedState: bool = false;
35
36
  protected focusedState: bool = false;
@@ -171,6 +172,11 @@ export class PressableLabeledControl extends FlexBox {
171
172
  previousRoot.dispose();
172
173
  }
173
174
 
175
+ protected setLabelFontSizeOverride(fontSize: f32): void {
176
+ this.labelFontSizeOverride = fontSize > 0.0 ? fontSize : 0.0;
177
+ this.syncBaseTheme(activeTheme.value);
178
+ }
179
+
174
180
  protected syncBaseTheme(theme: Theme): void {
175
181
  this.cursor(this.isEnabled ? CursorStyle.Pointer : CursorStyle.Default);
176
182
  this.cornerRadius(theme.spacing.sm);
@@ -182,7 +188,10 @@ export class PressableLabeledControl extends FlexBox {
182
188
  this.padding(theme.spacing.xs, theme.spacing.xs, theme.spacing.xs, theme.spacing.xs);
183
189
  this.opacity(this.isEnabled ? 1.0 : 0.6);
184
190
  this.gapNode.width(theme.spacing.sm, Unit.Pixel);
185
- this.labelNode.font(theme.fonts.body, theme.fonts.sizeBody);
191
+ this.labelNode.font(
192
+ theme.fonts.body,
193
+ this.labelFontSizeOverride > 0.0 ? this.labelFontSizeOverride : theme.fonts.sizeBody,
194
+ );
186
195
  this.labelNode.textColor(this.isEnabled ? theme.colors.textPrimary : theme.colors.textMuted);
187
196
  this.syncFocusChrome(theme);
188
197
  }
@@ -1,6 +1,7 @@
1
1
  import { BorderStyle, Unit } from "../../core/ffi";
2
2
  import { Theme } from "../../core/Theme";
3
3
  import { FlexBox } from "../../nodes";
4
+ import { LabeledControlSizing } from "../ControlSizing";
4
5
  import {
5
6
  PressableIndicatorMetrics,
6
7
  PressableIndicatorPresenter,
@@ -31,36 +32,77 @@ export abstract class RadioIndicatorTemplate {
31
32
  abstract create(): RadioIndicatorPresenter;
32
33
  }
33
34
 
35
+ class RadioIndicatorMetrics extends PressableIndicatorMetrics {
36
+ constructor(
37
+ readonly indicatorSize: f32,
38
+ readonly dotSize: f32,
39
+ readonly borderWidth: f32,
40
+ ) {
41
+ super(indicatorSize, indicatorSize);
42
+ }
43
+ }
44
+
45
+ function centeredInset(outerSize: f32, innerSize: f32): f32 {
46
+ return outerSize > innerSize ? (outerSize - innerSize) * 0.5 : 0.0;
47
+ }
48
+
49
+ function dotInset(metrics: RadioIndicatorMetrics): f32 {
50
+ const inset = centeredInset(metrics.indicatorSize, metrics.dotSize);
51
+ return inset > metrics.borderWidth
52
+ ? inset - metrics.borderWidth
53
+ : 0.0;
54
+ }
55
+
56
+ const DEFAULT_RADIO_METRICS = new RadioIndicatorMetrics(20.0, 8.0, 1.0);
57
+
58
+ function resolveRadioMetrics(sizing: LabeledControlSizing | null): RadioIndicatorMetrics {
59
+ if (sizing === null || !sizing.hasIndicatorSize) {
60
+ return DEFAULT_RADIO_METRICS;
61
+ }
62
+ const indicatorSize = sizing.indicatorSizePx;
63
+ return new RadioIndicatorMetrics(
64
+ indicatorSize,
65
+ indicatorSize * 0.4,
66
+ indicatorSize >= 24.0 ? 2.0 : 1.0,
67
+ );
68
+ }
69
+
34
70
  class DefaultRadioIndicatorPresenter extends RadioIndicatorPresenter {
71
+ private readonly geometry: RadioIndicatorMetrics;
35
72
  private readonly dotNode: FlexBox;
36
73
 
37
- constructor() {
74
+ constructor(metrics: RadioIndicatorMetrics = DEFAULT_RADIO_METRICS) {
38
75
  const root = new FlexBox()
39
- .width(20.0, Unit.Pixel)
40
- .height(20.0, Unit.Pixel)
76
+ .width(metrics.indicatorSize, Unit.Pixel)
77
+ .height(metrics.indicatorSize, Unit.Pixel)
41
78
  .alignItems(1)
42
79
  .justifyContent(1);
43
- super(root, new PressableIndicatorMetrics(20.0, 20.0));
80
+ super(root, metrics);
81
+ this.geometry = metrics;
44
82
  const dotNode = new FlexBox()
45
83
  .positionAbsolute()
46
- .position(5.0, 5.0)
47
- .width(8.0, Unit.Pixel)
48
- .height(8.0, Unit.Pixel);
84
+ .position(dotInset(metrics), dotInset(metrics))
85
+ .width(metrics.dotSize, Unit.Pixel)
86
+ .height(metrics.dotSize, Unit.Pixel);
49
87
  this.dotNode = dotNode;
50
88
  root.child(dotNode);
51
89
  }
52
90
 
53
91
  apply(theme: Theme, state: RadioIndicatorVisualState): void {
92
+ const geometry = this.geometry;
54
93
  const outerColor = state.checked
55
94
  ? (state.pressed ? theme.colors.accentPressed : (state.hovered ? theme.colors.accentHovered : theme.colors.accent))
56
95
  : theme.colors.border;
57
- this.root.cornerRadius(10.0);
58
- this.root.border(1.0, outerColor, BorderStyle.Solid);
96
+ this.root.cornerRadius(geometry.indicatorSize * 0.5);
97
+ this.root.border(geometry.borderWidth, outerColor, BorderStyle.Solid);
59
98
  this.root.bgColor(theme.colors.surface);
60
- this.dotNode.cornerRadius(4.0);
61
- this.dotNode.position(5.0, 5.0);
62
- this.dotNode.bgColor(outerColor);
63
- this.dotNode.opacity(state.checked ? 1.0 : 0.0);
99
+ this.dotNode
100
+ .cornerRadius(geometry.dotSize * 0.5)
101
+ .position(dotInset(geometry), dotInset(geometry))
102
+ .width(geometry.dotSize, Unit.Pixel)
103
+ .height(geometry.dotSize, Unit.Pixel)
104
+ .bgColor(outerColor)
105
+ .opacity(state.checked ? 1.0 : 0.0);
64
106
  }
65
107
  }
66
108
 
@@ -71,3 +113,7 @@ class DefaultRadioIndicatorTemplate extends RadioIndicatorTemplate {
71
113
  }
72
114
 
73
115
  export const defaultRadioIndicatorTemplate = new DefaultRadioIndicatorTemplate();
116
+
117
+ export function createDefaultRadioIndicatorPresenter(sizing: LabeledControlSizing | null = null): RadioIndicatorPresenter {
118
+ return new DefaultRadioIndicatorPresenter(resolveRadioMetrics(sizing));
119
+ }