@effindomv2/fui-as 0.1.11 → 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.
- package/package.json +2 -2
- package/src/Fui.ts +4 -0
- package/src/controls/Checkbox.ts +29 -4
- package/src/controls/ControlSizing.ts +158 -0
- package/src/controls/Dropdown.ts +87 -20
- package/src/controls/RadioButton.ts +29 -4
- package/src/controls/Slider.ts +31 -5
- package/src/controls/index.ts +1 -0
- package/src/controls/internal/CheckboxIndicatorPresenter.ts +45 -8
- package/src/controls/internal/DropdownChevronPresenter.ts +32 -5
- package/src/controls/internal/DropdownFieldPresenter.ts +69 -8
- package/src/controls/internal/DropdownOptionRowPresenter.ts +38 -6
- package/src/controls/internal/PressableLabeledControl.ts +11 -4
- package/src/controls/internal/RadioIndicatorPresenter.ts +59 -13
- package/src/controls/internal/SliderPresenter.ts +62 -22
- package/src/controls/templating.ts +1 -0
|
@@ -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(
|
|
42
|
-
.height(
|
|
67
|
+
.width(metrics.indicatorSize, Unit.Pixel)
|
|
68
|
+
.height(metrics.indicatorSize, Unit.Pixel)
|
|
43
69
|
.alignItems(1)
|
|
44
70
|
.justifyContent(1);
|
|
45
|
-
super(root,
|
|
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(
|
|
53
|
-
.height(
|
|
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(
|
|
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
|
|
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(
|
|
42
|
-
.height(
|
|
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
|
-
.
|
|
55
|
-
.
|
|
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(
|
|
70
|
-
.height(
|
|
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(
|
|
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,
|
|
149
|
+
.font(theme.fonts.body, metrics.fontSize)
|
|
93
150
|
.textColor(state.enabled ? theme.colors.textPrimary : theme.colors.textMuted);
|
|
94
151
|
this.chevronHost
|
|
95
|
-
.width(
|
|
96
|
-
.height(
|
|
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
|
|
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,
|
|
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(
|
|
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,
|
|
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;
|
|
@@ -43,9 +44,7 @@ export class PressableLabeledControl extends FlexBox {
|
|
|
43
44
|
this.gapNode = new FlexBox()
|
|
44
45
|
.width(activeTheme.value.spacing.sm, Unit.Pixel)
|
|
45
46
|
.height(1.0, Unit.Pixel);
|
|
46
|
-
this.labelHost = new FlexBox()
|
|
47
|
-
.fillWidth();
|
|
48
|
-
this.labelNode.width(100.0, Unit.Percent);
|
|
47
|
+
this.labelHost = new FlexBox();
|
|
49
48
|
this.labelHost.child(this.labelNode);
|
|
50
49
|
|
|
51
50
|
this.semanticRole(role);
|
|
@@ -173,6 +172,11 @@ export class PressableLabeledControl extends FlexBox {
|
|
|
173
172
|
previousRoot.dispose();
|
|
174
173
|
}
|
|
175
174
|
|
|
175
|
+
protected setLabelFontSizeOverride(fontSize: f32): void {
|
|
176
|
+
this.labelFontSizeOverride = fontSize > 0.0 ? fontSize : 0.0;
|
|
177
|
+
this.syncBaseTheme(activeTheme.value);
|
|
178
|
+
}
|
|
179
|
+
|
|
176
180
|
protected syncBaseTheme(theme: Theme): void {
|
|
177
181
|
this.cursor(this.isEnabled ? CursorStyle.Pointer : CursorStyle.Default);
|
|
178
182
|
this.cornerRadius(theme.spacing.sm);
|
|
@@ -184,7 +188,10 @@ export class PressableLabeledControl extends FlexBox {
|
|
|
184
188
|
this.padding(theme.spacing.xs, theme.spacing.xs, theme.spacing.xs, theme.spacing.xs);
|
|
185
189
|
this.opacity(this.isEnabled ? 1.0 : 0.6);
|
|
186
190
|
this.gapNode.width(theme.spacing.sm, Unit.Pixel);
|
|
187
|
-
this.labelNode.font(
|
|
191
|
+
this.labelNode.font(
|
|
192
|
+
theme.fonts.body,
|
|
193
|
+
this.labelFontSizeOverride > 0.0 ? this.labelFontSizeOverride : theme.fonts.sizeBody,
|
|
194
|
+
);
|
|
188
195
|
this.labelNode.textColor(this.isEnabled ? theme.colors.textPrimary : theme.colors.textMuted);
|
|
189
196
|
this.syncFocusChrome(theme);
|
|
190
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(
|
|
40
|
-
.height(
|
|
76
|
+
.width(metrics.indicatorSize, Unit.Pixel)
|
|
77
|
+
.height(metrics.indicatorSize, Unit.Pixel)
|
|
41
78
|
.alignItems(1)
|
|
42
79
|
.justifyContent(1);
|
|
43
|
-
super(root,
|
|
80
|
+
super(root, metrics);
|
|
81
|
+
this.geometry = metrics;
|
|
44
82
|
const dotNode = new FlexBox()
|
|
45
83
|
.positionAbsolute()
|
|
46
|
-
.position(
|
|
47
|
-
.width(
|
|
48
|
-
.height(
|
|
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(
|
|
58
|
-
this.root.border(
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
}
|