Skip to content

Commit d78bbc2

Browse files
committed
Add complex node fields
1 parent 4b2402e commit d78bbc2

10 files changed

Lines changed: 2014 additions & 0 deletions

File tree

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
* * Copyright 2026 Google LLC. All rights reserved.
3+
* *
4+
* * Licensed under the Apache License, Version 2.0 (the "License");
5+
* * you may not use this file except in compliance with the License.
6+
* * You may obtain a copy of the License at
7+
* *
8+
* * http://www.apache.org/licenses/LICENSE-2.0
9+
* *
10+
* * Unless required by applicable law or agreed to in writing, software
11+
* * distributed under the License is distributed on an "AS IS" BASIS,
12+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* * See the License for the specific language governing permissions and
14+
* * limitations under the License.
15+
*/
16+
@file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
17+
18+
package com.example.cahier.developer.brushgraph.ui.fields
19+
20+
import androidx.compose.foundation.layout.Column
21+
import androidx.compose.foundation.layout.Row
22+
import androidx.compose.foundation.layout.fillMaxWidth
23+
import androidx.compose.foundation.layout.padding
24+
import androidx.compose.material3.DropdownMenuItem
25+
import androidx.compose.material3.ExposedDropdownMenuBox
26+
import androidx.compose.material3.ExposedDropdownMenuDefaults
27+
import androidx.compose.material3.HorizontalDivider
28+
import androidx.compose.material3.MaterialTheme
29+
import androidx.compose.material3.OutlinedTextField
30+
import androidx.compose.material3.Text
31+
import androidx.compose.runtime.Composable
32+
import androidx.compose.runtime.getValue
33+
import androidx.compose.runtime.mutableStateOf
34+
import androidx.compose.runtime.remember
35+
import androidx.compose.runtime.setValue
36+
import androidx.compose.ui.Alignment
37+
import androidx.compose.ui.Modifier
38+
import androidx.compose.ui.res.stringResource
39+
import androidx.compose.ui.unit.dp
40+
import com.example.cahier.R
41+
import com.example.cahier.developer.brushgraph.data.NodeData
42+
import com.example.cahier.developer.brushgraph.data.displayStringRId
43+
import com.example.cahier.developer.brushgraph.ui.fields.NODE_TYPES_START
44+
import com.example.cahier.developer.brushgraph.ui.fields.NODE_TYPES_OPERATOR
45+
import com.example.cahier.developer.brushgraph.ui.fields.NODE_TYPES_TERMINAL
46+
import ink.proto.BrushBehavior as ProtoBrushBehavior
47+
48+
@Composable
49+
fun BehaviorNodeFields(
50+
data: NodeData.Behavior,
51+
onUpdate: (NodeData) -> Unit,
52+
onDropdownEditComplete: () -> Unit,
53+
onFieldEditComplete: () -> Unit,
54+
textFieldsLocked: Boolean,
55+
modifier: Modifier = Modifier
56+
) {
57+
val behaviorNode = data.node
58+
val nodeCase = behaviorNode.nodeCase
59+
var expandedNodeTypes by remember { mutableStateOf(false) }
60+
61+
Column(modifier = modifier) {
62+
// Node Type Selector
63+
Row(
64+
verticalAlignment = Alignment.CenterVertically,
65+
modifier = Modifier.fillMaxWidth()
66+
) {
67+
ExposedDropdownMenuBox(
68+
expanded = expandedNodeTypes,
69+
onExpandedChange = { expandedNodeTypes = it },
70+
modifier = Modifier.weight(1f)
71+
) {
72+
OutlinedTextField(
73+
value = stringResource(nodeCase.displayStringRId()),
74+
onValueChange = {},
75+
readOnly = true,
76+
label = { Text(stringResource(R.string.bg_node_type)) },
77+
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedNodeTypes) },
78+
modifier = Modifier.menuAnchor().fillMaxWidth()
79+
)
80+
ExposedDropdownMenu(
81+
expanded = expandedNodeTypes,
82+
onDismissRequest = { expandedNodeTypes = false }
83+
) {
84+
@Composable
85+
fun DropdownSection(label: String, types: List<ProtoBrushBehavior.Node.NodeCase>) {
86+
Row(
87+
verticalAlignment = Alignment.CenterVertically,
88+
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
89+
) {
90+
HorizontalDivider(modifier = Modifier.weight(1f))
91+
Text(
92+
text = label,
93+
style = MaterialTheme.typography.labelSmall,
94+
color = MaterialTheme.colorScheme.outline,
95+
modifier = Modifier.padding(horizontal = 8.dp)
96+
)
97+
HorizontalDivider(modifier = Modifier.weight(1f))
98+
}
99+
types.forEach { type ->
100+
DropdownMenuItem(
101+
text = { Text(stringResource(type.displayStringRId())) },
102+
onClick = {
103+
if (type != nodeCase) {
104+
onUpdate(createDefaultNode(type))
105+
onDropdownEditComplete()
106+
}
107+
expandedNodeTypes = false
108+
}
109+
)
110+
}
111+
}
112+
113+
DropdownSection(stringResource(R.string.bg_section_start_nodes), NODE_TYPES_START)
114+
DropdownSection(stringResource(R.string.bg_section_operator_nodes), NODE_TYPES_OPERATOR)
115+
DropdownSection(stringResource(R.string.bg_section_terminal_nodes), NODE_TYPES_TERMINAL)
116+
}
117+
}
118+
}
119+
120+
// Developer Comment
121+
if (nodeCase == ProtoBrushBehavior.Node.NodeCase.TARGET_NODE ||
122+
nodeCase == ProtoBrushBehavior.Node.NodeCase.POLAR_TARGET_NODE) {
123+
OutlinedTextField(
124+
value = data.developerComment,
125+
onValueChange = {
126+
onUpdate(data.copy(developerComment = it))
127+
},
128+
label = { Text(stringResource(R.string.bg_developer_comment)) },
129+
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
130+
minLines = 2,
131+
enabled = !textFieldsLocked,
132+
)
133+
}
134+
135+
// Dispatch to specific node fields
136+
when (nodeCase) {
137+
ProtoBrushBehavior.Node.NodeCase.SOURCE_NODE -> {
138+
SourceNodeFields(
139+
sourceNode = behaviorNode.sourceNode,
140+
behaviorNode = behaviorNode,
141+
onUpdate = onUpdate,
142+
onDropdownEditComplete = onDropdownEditComplete,
143+
onFieldEditComplete = onFieldEditComplete,
144+
textFieldsLocked = textFieldsLocked
145+
)
146+
}
147+
ProtoBrushBehavior.Node.NodeCase.CONSTANT_NODE -> {
148+
ConstantNodeFields(
149+
constantNode = behaviorNode.constantNode,
150+
behaviorNode = behaviorNode,
151+
onUpdate = onUpdate,
152+
onFieldEditComplete = onFieldEditComplete
153+
)
154+
}
155+
ProtoBrushBehavior.Node.NodeCase.NOISE_NODE -> {
156+
NoiseNodeFields(
157+
noiseNode = behaviorNode.noiseNode,
158+
behaviorNode = behaviorNode,
159+
onUpdate = onUpdate,
160+
onFieldEditComplete = onFieldEditComplete,
161+
onDropdownEditComplete = onDropdownEditComplete,
162+
textFieldsLocked = textFieldsLocked
163+
)
164+
}
165+
ProtoBrushBehavior.Node.NodeCase.TOOL_TYPE_FILTER_NODE -> {
166+
ToolTypeFilterNodeFields(
167+
filterNode = behaviorNode.toolTypeFilterNode,
168+
behaviorNode = behaviorNode,
169+
onUpdate = onUpdate
170+
)
171+
}
172+
ProtoBrushBehavior.Node.NodeCase.DAMPING_NODE -> {
173+
DampingNodeFields(
174+
dampingNode = behaviorNode.dampingNode,
175+
behaviorNode = behaviorNode,
176+
onUpdate = onUpdate,
177+
onFieldEditComplete = onFieldEditComplete,
178+
onDropdownEditComplete = onDropdownEditComplete
179+
)
180+
}
181+
ProtoBrushBehavior.Node.NodeCase.RESPONSE_NODE -> {
182+
ResponseNodeFields(
183+
responseNode = behaviorNode.responseNode,
184+
behaviorNode = behaviorNode,
185+
onUpdate = onUpdate
186+
)
187+
}
188+
ProtoBrushBehavior.Node.NodeCase.INTEGRAL_NODE -> {
189+
IntegralNodeFields(
190+
integralNode = behaviorNode.integralNode,
191+
behaviorNode = behaviorNode,
192+
onUpdate = onUpdate,
193+
onFieldEditComplete = onFieldEditComplete,
194+
onDropdownEditComplete = onDropdownEditComplete
195+
)
196+
}
197+
ProtoBrushBehavior.Node.NodeCase.BINARY_OP_NODE -> {
198+
BinaryOpNodeFields(
199+
binaryNode = behaviorNode.binaryOpNode,
200+
behaviorNode = behaviorNode,
201+
onUpdate = onUpdate,
202+
onDropdownEditComplete = onDropdownEditComplete
203+
)
204+
}
205+
ProtoBrushBehavior.Node.NodeCase.INTERPOLATION_NODE -> {
206+
InterpolationNodeFields(
207+
interpNode = behaviorNode.interpolationNode,
208+
behaviorNode = behaviorNode,
209+
onUpdate = onUpdate
210+
)
211+
}
212+
ProtoBrushBehavior.Node.NodeCase.TARGET_NODE -> {
213+
TargetNodeFields(
214+
targetNode = behaviorNode.targetNode,
215+
behaviorNode = behaviorNode,
216+
onUpdate = onUpdate,
217+
onFieldEditComplete = onFieldEditComplete,
218+
onDropdownEditComplete = onDropdownEditComplete
219+
)
220+
}
221+
ProtoBrushBehavior.Node.NodeCase.POLAR_TARGET_NODE -> {
222+
PolarTargetNodeFields(
223+
polarNode = behaviorNode.polarTargetNode,
224+
behaviorNode = behaviorNode,
225+
onUpdate = onUpdate,
226+
onFieldEditComplete = onFieldEditComplete,
227+
onDropdownEditComplete = onDropdownEditComplete
228+
)
229+
}
230+
else -> {
231+
// Fallback or empty view for unsupported nodes
232+
Text(stringResource(R.string.bg_err_unsupported_behavior_node_type, nodeCase.toString()))
233+
}
234+
}
235+
}
236+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* * Copyright 2026 Google LLC. All rights reserved.
3+
* *
4+
* * Licensed under the Apache License, Version 2.0 (the "License");
5+
* * you may not use this file except in compliance with the License.
6+
* * You may obtain a copy of the License at
7+
* *
8+
* * http://www.apache.org/licenses/LICENSE-2.0
9+
* *
10+
* * Unless required by applicable law or agreed to in writing, software
11+
* * distributed under the License is distributed on an "AS IS" BASIS,
12+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* * See the License for the specific language governing permissions and
14+
* * limitations under the License.
15+
*/
16+
@file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
17+
18+
package com.example.cahier.developer.brushgraph.ui.fields
19+
20+
import androidx.compose.foundation.BorderStroke
21+
import androidx.compose.foundation.layout.Row
22+
import androidx.compose.foundation.layout.Spacer
23+
import androidx.compose.foundation.layout.padding
24+
import androidx.compose.foundation.layout.size
25+
import androidx.compose.foundation.layout.width
26+
import androidx.compose.foundation.shape.RoundedCornerShape
27+
import androidx.compose.material3.MaterialTheme
28+
import androidx.compose.material3.Surface
29+
import androidx.compose.material3.Text
30+
import androidx.compose.runtime.Composable
31+
import androidx.compose.ui.Alignment
32+
import androidx.compose.ui.Modifier
33+
import androidx.compose.ui.graphics.Color
34+
import androidx.compose.ui.graphics.toArgb
35+
import ink.proto.Color as ProtoColor
36+
import androidx.compose.ui.res.stringResource
37+
import androidx.compose.ui.unit.dp
38+
import com.example.cahier.R
39+
import com.example.cahier.developer.brushdesigner.ui.NumericField
40+
import com.example.cahier.developer.brushdesigner.ui.NumericLimits
41+
import com.example.cahier.developer.brushdesigner.ui.EnumDropdown
42+
import com.example.cahier.developer.brushgraph.data.NodeData
43+
import com.example.cahier.developer.brushgraph.ui.FieldWithTooltip
44+
import com.example.cahier.developer.brushgraph.ui.getColorFunctionTooltip
45+
import ink.proto.ColorFunction as ProtoColorFunction
46+
47+
@Composable
48+
fun ColorFunctionNodeFields(
49+
function: ProtoColorFunction,
50+
onUpdate: (NodeData) -> Unit,
51+
onChooseColor: (Color, (Color) -> Unit) -> Unit,
52+
onDropdownEditComplete: () -> Unit,
53+
onFieldEditComplete: () -> Unit,
54+
modifier: Modifier = Modifier
55+
) {
56+
val currentTypeResId = if (function.hasOpacityMultiplier()) {
57+
R.string.bg_opacity_multiplier
58+
} else {
59+
R.string.bg_replace_color
60+
}
61+
62+
FieldWithTooltip(
63+
tooltipTitle = stringResource(R.string.bg_title_function_type_format, stringResource(currentTypeResId)),
64+
tooltipText = stringResource(getColorFunctionTooltip(currentTypeResId)),
65+
modifier = modifier
66+
) {
67+
EnumDropdown(
68+
label = stringResource(R.string.bg_function_type),
69+
currentValue = currentTypeResId,
70+
values = listOf(R.string.bg_opacity_multiplier, R.string.bg_replace_color),
71+
displayName = { stringResource(it) },
72+
onSelected = { resId ->
73+
if (resId != currentTypeResId) {
74+
onUpdate(
75+
if (resId == R.string.bg_opacity_multiplier) {
76+
NodeData.ColorFunction(
77+
ProtoColorFunction.newBuilder().setOpacityMultiplier(1f).build()
78+
)
79+
} else {
80+
NodeData.ColorFunction(
81+
ProtoColorFunction.newBuilder()
82+
.setReplaceColor(
83+
ProtoColor.newBuilder()
84+
.setRed(0f)
85+
.setGreen(0f)
86+
.setBlue(0f)
87+
.setAlpha(1f)
88+
.build()
89+
)
90+
.build()
91+
)
92+
}
93+
)
94+
}
95+
onDropdownEditComplete()
96+
}
97+
)
98+
}
99+
100+
if (function.hasOpacityMultiplier()) {
101+
NumericField(
102+
title = stringResource(R.string.bg_label_opacity_multiplier),
103+
value = function.opacityMultiplier,
104+
limits = NumericLimits.standard(0f, 2f, 0.01f),
105+
onValueChanged = { onUpdate(NodeData.ColorFunction(function.toBuilder().setOpacityMultiplier(it).build())) },
106+
onValueChangeFinished = onFieldEditComplete
107+
)
108+
} else if (function.hasReplaceColor()) {
109+
val color = function.replaceColor
110+
val composeColor =
111+
Color(red = color.red, green = color.green, blue = color.blue, alpha = color.alpha)
112+
Row(
113+
verticalAlignment = Alignment.CenterVertically,
114+
modifier = Modifier.padding(vertical = 8.dp)
115+
) {
116+
Text(stringResource(R.string.bg_color_label), style = MaterialTheme.typography.bodyMedium)
117+
Surface(
118+
onClick = {
119+
onChooseColor(composeColor) { newColor ->
120+
onUpdate(
121+
NodeData.ColorFunction(
122+
function.toBuilder()
123+
.setReplaceColor(
124+
ProtoColor.newBuilder()
125+
.setRed(newColor.red)
126+
.setGreen(newColor.green)
127+
.setBlue(newColor.blue)
128+
.setAlpha(newColor.alpha)
129+
.build()
130+
)
131+
.build()
132+
)
133+
)
134+
}
135+
},
136+
shape = RoundedCornerShape(4.dp),
137+
color = composeColor,
138+
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
139+
modifier = Modifier.size(40.dp)
140+
) {}
141+
Spacer(Modifier.width(8.dp))
142+
Text(
143+
text = String.format("ARGB #%08X", (composeColor.toArgb())),
144+
style = MaterialTheme.typography.bodySmall,
145+
)
146+
}
147+
}
148+
}

0 commit comments

Comments
 (0)